8.内存模型与命名空间
一个CPP程序从编码到执行
-
预处理,产生.ii文件
预处理用于将所有的#include头文件及宏定义替换成其真正的内容。预处理后得到的依然是文本文件,但是文件体积会大很多。
gcc -E -I./inc test.c -o test.i # 上述命令中-E是让编译器在预处理之后就退出,不进行后续编译过程;-I指定头文件目录,这里指定的是我们自定义的头文件目录;-o指定输出文件名。 -
编译, 产生汇编文件,.s文件
将预处理之后的程序转换成特定汇编代码的过程。
gcc -S -I./inc test.c -o test.s -
汇编,产生目标文件,.o或.obj文件
将上一步的汇编代码转换成机器码,产生的文件叫目标文件,为二进制文件。
gcc -c test.s -o test.o这一步会为每一个源文件产生一个目标文件。因此
mymath.c也需要产生一个mymath.o文件 -
链接,产生可执行文件,.out或.exe文件
将多个目标文件以及所需要的库文件(.so等)链接成最终的可执行文件。
ld -o test.out test.o inc/mymath.o ...libraries...
为什么C++ 不提倡使用宏定义而是使用 const 或 constexpr?
- 类型安全性:宏定义是简单的文本替换,在编译器处理之前,它们不会进行任何类型检查。这意味着宏定义可能会导致类型错误,而
const变量是具有类型的,可以进行类型检查。 - 作用域:宏定义是全局的,它们在整个程序中都是可见的。而
const变量可以在特定的作用域内定义,使得程序更加模块化和可读性更好。 - 调试和错误信息:使用宏定义时,编译器生成的错误信息可能会很难理解,因为宏定义会直接展开到代码中。而
const变量会在编译器进行类型检查时产生更好的错误消息。 - 更好的代码维护:宏定义往往不够直观,且很难调试。而使用
const变量可以更清晰地表达程序员的意图,使得代码更易于维护。 - 支持调试器:宏定义生成的代码可能会使得调试器无法准确地跟踪变量的值,而
const变量则可以正常地被调试器跟踪。
C++内存管理
内存分布
-
Text Segment 代码区 文本区
- 存放可执行程序的机器码
- 存放只读数据
-
Data Segment 数据区 静态存储
-
存放已初始化的全局、静态变量
-
BSS (block started by symbol)静态存储
- 存放未初始化的全局和静态变量。默认设置为0
-
存放常量数据
-
-
Heap 堆区 动态存储
- 从低地址到高地址增长。用于动态内存分配
- 由new、malloc实现内存的管理
-
Stack 栈区 自动存储
- 从高地址向低地址增长。
- 由编译器自动分配管理。用于局部变量(自动变量),函数参数值,返回变量等
函数栈
函数栈是用来管理函数调用和局部变量的一种数据结构,用于存储函数调用的上下文信息。
当调用函数时,一块内连续内存(栈区的一段内存)压入栈帧,函数返回时,弹出栈帧。
栈帧:
-
每个函数调用都会在函数栈上创建一个称为栈帧的数据结构,用来存储函数的一些信息。
-
通常包括
- 局部变量:函数中声明的变量,在函数被执行时,局部变量(就是在函数内创建的自动变量)就被添加到栈中了。
- 参数:传递给函数的参数
- 返回地址:函数执行完需要返回的地址
- 其他调用相关信息:如函数调用者的栈帧指针等。
栈指针:
- 栈指针式一个指向函数栈顶部的指针,它指向当前函数栈帧的顶部
全局变量的管理
- 当全局/静态变量未初始化时,他们被存放在BSS段,在程序执行时,被初始化为0。
- 当全局/静态变量被初始化,存在数据区。
int x;
int z = 5;
const int q = 6;
void func()
{
static int y;
}
int main()
{
return 0;
}
这里的z一直存储在数据区。x、y被存储在BSS,q被存储在常量区。但总的来说全部存在数据区。
单独编译
-
将不同功能的程序分开放置,可以集中管理,单独编译。
-
一个程序分为三部分:
- 头文件
- 头文件的实现方法
- 主文件
-
头文件应该包含哪些文件?
不是具体实现,仅是告诉编译器如何做的各种量。
-
各种声明
- 函数原型;
- 类声明;
- 结构声明;
- 模板声明
-
在项目中多个文件用到的符号常量
- #define 定义的符号常量
- const 定义的符号常量
-
内敛函数
内敛函数不是被调用,而是会添加到被调用处。而且经常忽略声明,而直接定义。
-
-
#include <> 和" "的区别
- <>尖括号经常被用来包裹标准库文件。编译器将在计算机的标准头文件库中查找。
- ""引号常用来包裹自己编写的头文件。编译器首先在当前的工作目录或项目目录中查找,如果找不到再去标准位置查找。
-
如何使用预编译命令防止编译器将文件预编译两遍?
#ifndef 全大写项目名_H_ #define 全大写项目名_H_ // 头文件内容 #endif //ex: #ifndef CPP_LEARNING_H_ #define CPP_LEARNING_H_ #endif
存储持续性、作用域和链接性
概念
持续性:存在范围,指他们从被分配内存到被释放的持续过程。
作用域:名称的作用范围。描述名称在文件的多大范围内可见。
链接性:描述了名称如何在不同单元间共享。链接性的内外之分是指文件内外。为外部的名称可以在文件间共享,为内部的只能由一个文件中的函数共享。
不能在函数内定义函数,函数的作用域应是整个类或者整个命名空间,但不能是局部的。局部的毫无意义。
自动存储持续性
-
拥有自动存储持续性的变量称为自动变量。
-
在函数中声明的函数参数和变量的持续性为自动持续性,作用域为局部。无链接性。
-
它们在开始执行其所属代码块时被创建(所以其在代码块开始执行就被分配内存),在代码块执行结束后被销毁(所占内存被释放)。
-
编译器使用栈来管理自动变量。
程序使用两个指针来跟踪栈。一个指向栈底--栈开始的位置;一个指向栈顶--下一个可用单元(注意,这里是超顶)。当函数被调用时,其自动变量被放入栈中,栈顶指针指向下一个可用内存单元。当程序执行结束,栈顶指针被重置为函数被调用前的值(释放内存)。局部变量的值没有被删除,只不过不再被标记,他们所占据的空间将被下一个添加到栈中的函数所调用。
静态存储持续性
静态存储持续性变量:在整个程序的执行期间存在,注意这包括函数内部的静态变量。 在程序被执行时分配内存,程序结束后释放。被放在数据区(已初始化的正常放,未初始化的放BSS且被自动设置为0)。
注意: 静态存储持续性变量不是静态变量,或者不只是。其包括全局变量和静态持续性变量。
-
静态存储,外部链接性 (全局变量,或称外部变量)
-
在代码块的外部声明。
-
C++提供了两种变量的声明:
- 定义声明,他给变量分配存储空间。应该在定义中初始化变量。
- 引用声明,他不给变量分配空间,而是引用已有的变量。使用关键字extern 修饰。
-
单定义原则: (同持续性的)变量只能有一次定义。但应该在每一个使用外部变量的文件中都声明(引用声明)它。如果对变量进行多次定义,即对变量声明且没有使用extern,或者多个地方进行了初始化(进行初始化,编译器将自动忽略extern关键字,为其分配空间),那么程序将会产生链接(ld)错误。所以单独用一个文件来定义声明外部变量是一个好的选择。
-
在函数内部可以再次定义,但此时定义的是同名的自动变量。如果不进行指明,那么此函数中使用的变量将会是这个自动变量。可以使用域解析运算符(::)来访问隐藏的外部变量(放在变量名前面,表示使用全局版本)。
-
-
静态存储,内部链接性 (静态变量)
在代码块外部声明,并使用static限定符修饰。仅在其所在的文件中使用。
-
静态存储,无链接性 (静态变量)
在代码块内部证明,并使用static限定符修饰。
总结变量的存储方式
| 存储描述 | 持续性 | 作用域 | 链接性 | 如何声明 |
|---|---|---|---|---|
| 自动 | 自动 | 代码块 | 无 | 在代码块中 |
| 寄存器 | 自动 | 代码块 | 无 | 在代码块中,以关键字register |
| 静态,外部链接性 | 静态 | 文件 | 外部 | 函数外 |
| 静态, 内部链接性 | 静态 | 文件 | 内部 | 函数外,使用static |
| 静态,无链接性 | 静态 | 代码块 | 无 | 代码块内,使用static |
...;
int global = 1000; // 全局变量,静态持续性,外部链接性
static int one_file = 50; // 静态变量,静态持续性,内部链接性
int main()
{
...;
}
// 与llama不同的是,count在fc1没有被执行时,也留在内存中
void fc1(int n)
{
static int count = 0; // 静态变量,静态持续性,无链接性
int llama = 0; // 自动变量
...;
}
void fc2(int q)
{
...;
}
-
静态持续性变量的初始化过程:(以下变量均指静态持续性变量)
- 首先所有的变量都被零初始化,无论他们有无显式地被初始化。
- 如果使用了常量表达式,那么编译器执行常量表达式初始化。
- 如果没有足够的信息,如引用了某个函数,那么变量将被动态的初始化。这需要等到用到的信息被链接且程序执行。
注意:如果没有显式初始化,编译器将会把他们设置为0(0,null)。
#include <cmath>
int x; // 0 init
int y = 5; // 常量表达式
int z = 13 *13; // 常量表达式
const double pi = 4.0 * atan(1.0); // 动态初始化
-
说明符和限定字
-
说明符:
-
register:用于在声明中指示寄存器存储,在C++11中,只是用于显式指出变量是自动的
-
thread_local:用于线程
-
mutable:用于指出结构或类即使被声明为const,其某个成员也可以被修改。
struct data { char name[30]; mutable int accesses; } const data veep = {"Claybourne Clodde", 0}; strcpy(veep.name, "Joye"); // 不被允许 veep.accesses++; // 被允许
-
-
限定符:
- const:常量,在C++中,常量的链接性是内部的。这意味着可以将其放到头文件中,然后包含到每个使用它的地方。
- volatile:表明即使程序代码没有对内存单元进行修改,其值也可能发生变化。(硬件、共享数据。)将变量声明为volatile,则编译器不会对这个变量进行缓存优化。(如程序两次使用了某个值,编译器可能对程序进行优化,将此值 存入寄存器中,这种优化假设这个变量在这两次使用之间不会发生变化。)
-
-
函数和链接性
- 所有函数的存储持续性都自动为静态的。可以使用static将函数的链接性设置为内部的。
static vint private(int x);。 - 单定义规则也适用于函数,如果想要使用函数,就要使用这个函数的每一个文件包含其函数原型。内联函数不受此限制,但c++要求同一个函数的所有内联定义都相同。
- C++首先在程序文件中查找函数,如果找不到则找库函数。这意味同名函数覆盖。
- 所有函数的存储持续性都自动为静态的。可以使用static将函数的链接性设置为内部的。
动态存储持续性
通过new与delete管理动态内存。
-
为new运算符初始化
答案使在类型名后面加上初始值,并用括号(小括号,大括号,但不能是中括号,因为它是声明数组)括起来。其实就是列表初始化。
int *pi = new int(6); // *p1被设置为6; int *pi = new int{6}; double *ar = new int [4] {2, 3, 4, 5}; struct where{double x, double y, double z}; where *one = new where{2.5, 3.6, 9.8}; -
new失败该如何做?
之前会让new返回空指针,现在将会引发
std::bad_alloc异常。 -
定位new运算符
指定要使用的内存。要包含头文件new。
#include <new> char buffer1[50]; char buffer2[500]; // 从 buffer中分配空间 p2 = new (buffer1) chaff; // chaff is a struct p3 = new (buffer2) int [20];是否要delete来释放空间,需要看指定的空间在哪。自动、静态、全局不需要。使用new指定的空间,可以使用常规delete来释放整个内存块。
线程存储持续性
名称空间(命名空间)
-
潜在作用域、作用域。
-
如何声明一个命名空间
使用namespace创建命名空间。命名空间可以是全局的,也可以位于另一个命名空间中,但不能位于代码块中。因为默认情况下其名称链接性是外部的。
// 创建命名空间 namespace Jack { double pail; void fetch(); struct Well {}; namespace Jill { double pal; } } // 将名称添加到已有的命名空间中 namespace Jack { char * goose{const char *}; } // 为在命名空间中声明的函数原型提供实现。 namespace Jack { void fetch() { ...; } } // 使用御解析符使用命名空间中的名称 Jack::pail = 12.254; Jack::Well {...}; -
using声明与using编译指令
using Jack::pail; // 此后pail被添加到他所属的声明区域 using namespace Jack; // 使Jack的所有名称都可用。