2. C++ 内存模型
[toc]
C++ 的内存模型包含五个区
栈区: 由编译器自动分配释放,存放函数的参数值,局部变量的值等,其操作方式类似于数据结构中的栈。
堆区: 一般由程序员分配释放,若程序员不释放,程序结束时可能由 OS 回收。它与数据结构中的堆是两回事,分配方式倒是类似于链表。
全局区(静态区): 全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统释放。
文字常量区: 常量字符串就是放在这里的。程序结束后由系统释放。
程序代码区: 存放函数体的二进制代码。
栈区和堆区的区别
管理方式 : 对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说, 释放工作由程序员控制,容易产生memory leak。
空间大小 : 一般来讲在 32 位系统下,堆内存可以达到 4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的,例如,在 VC6 下面,默认的栈空间大小是 1M(好像是,记不清楚了)。当然,我们可 以修改: 打开工程,依次操作菜单如下:Project->Setting->Link,在 Category 中选中 Output,然后在 Reserve 中设定堆栈的最大值和 commit。 注意:reserve 最 小值为 4Byte;commit 是保留在虚拟内存的页文件里面,它设置的较大会使栈开辟较大的值,可能增加内存的开销和启动时间。
碎片问题 : 对于堆来讲,频繁的 new/delete 势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出。
生长方向 : 对于堆来讲,生长方向是向上的,也就是向着内存地址增加的方向;对 于栈来讲,它的生长方向是向下的,是向着内存地址减小的方向增长。
分配方式 : 堆都是动态分配的,没有静态分配的堆。栈有 2 种分配方式:静态分配 和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由 alloca 函数进行分配,但是栈的动态分配和堆是不同的,它的动态分配是由编译器进行释 放,无需我们手工实现。
分配效率 : 栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比 较高。堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存, 库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜 索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多) 就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小 的内存,然后进行返回。显然,堆的效率比栈要低得多。
static 关键字
-
static 的第一个作用也是最重要的一条:隐藏。(static函数,static变量均可)
当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。 -
static 的第二个作用是保持变量内容的持久。(static变量中的记忆功能和全局生 存期)存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。
-
static的第三个作用是默认初始化为0(static 变量)
其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区, 内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。
static 变量
- 函数内的 static 变量: 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只 被分配一次,因此其值在下次调用时仍维持上次的值;
- 模块内的 static 全局变量: 在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
- 在类中的 static 成员变量: 在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。static类对象必须要在类外进行初始化,static修饰的变量先于对象存在,所以 static修饰的变量要在类外初始化;
- static 修饰的类成员函数: 由于 static 修饰的类成员属于类,不属于对象,因此 static 类成员函数是没有 this 指针的,this 指针是指向本对象的指针。正因为没有 this 指针,所以 static类成员函数不能访问非static的类成员,只能访问 static 修饰的类成员;static 成员函数不能被 virtual 修饰,static 成员不属于任何对象或实例,所以 加上 virtual 没有任何实际意义;
- static 修饰的函数: 用 static 修饰的普通函数会失去全局性,只能在该模块内可见。
如何理解只能在当前文件中起作用?
例如在 a.h 中定义了 static
变量 m,那么在 cpp 文件中包含了头文件 a.h 则可以随意使用 static
变量 m;如果 static
变量 m 定义在了 cpp 中,那么此时 static
变量只能在本文件中使用,其他文件使用不到。
假如在 b.cpp 中定义了 static
变量 m,在 a.cpp 中使用,如果是普通变量在加 extern
关键字在 a.cpp 把变量 m 声明一下就可以使用,但是 static
变量并不会起作用。
b.cpp 中声明一个全局变量和全局函数
int a = 10;
void func(){
a = 12;
}
a.cpp 希望使用这个变量,用 extern 声明一下
extern void func();
int main() {
extern int a;
func();
std::cout << a << std::endl;
}
输出:12
b.cpp 中声明一个静态全局变量
static int a = 10;
void func(){
a = 12;
}
a.cpp 中如下写会报错:a 未定义
extern void func();
int main() {
extern int a;
func();
std::cout << a << std::endl;
}
如果 b.cpp 和 a.cpp 中都定义了 a 的全局变量或 func 的全局函数,则编译会报错。其中一个加上 static,则会编译通过,它只会在该文件中可见,且回覆盖全局变量
b.cpp
int a = 10;
void func(){
a = 12;
}
a.cpp
static int a = 5; // 不加 static 会报错
static void func(){
a += 13;
}
int main() {
func();
std::cout << a << std::endl;
}
输出:18
内存对齐和 sizeof
为什么要内存对齐?
- 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
- 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
内存对齐数
每个特定平台上的编译器都有自己的默认“对齐系数”(32位机一般为4,64位机一般为8),通常为4或8的倍数。
设置对齐参数:#pragma pack(n)
,n为对齐系数,不想对齐可以设置为1;设置为0,也表示为使用默认对齐取消自定义字节对齐方式,使用默认对齐方式:#pragma pack()
sizeof
它的基本作用是判断数据类型或者表达式长度,要注意的是这不是一个函数,而是一个C++中的关键字!字节数的计算在程序编译时进行,而不是在程序执行的过程中才计算出来!
空类或空联合体的 sizeof
在 C++ 中规定了空结构体,空类和空联合体的内存所占大小为 1 字节,因为 C++ 中规定,任何不同的对象不能拥有相同的内存地址。
struct A{};
class B{};
union C{};
int main() {
std::cout << sizeof(A) << std::endl;
std::cout << sizeof(B) << std::endl;
std::cout << sizeof(C) << std::endl;
}
输出:
1
1
1
struct 的对齐方式
以下面若干个结构体为例:
#pragma pack(4)
struct A{ // sizeof(A) = 8
char a;
int b;
};
#pragma pack(2)
struct A{ // sizeof(A) = 6
char a;
int b;
};
#pragma pack(2)
struct A{ // sizeof(A) = 14
int a;
double b;
char c;
};
#pragma pack(4)
struct A{ // sizeof(A) = 8
char a;
short b;
int c;
};
#pragma pack(4)
struct A{ // sizeof(A) = 3
char a;
char b;
char c;
};
#pragma pack(8)
struct B{ // sizeof(B) = 8;
char c;
int b;
};
- 如果只有一个类型为 A 的成员,对齐之后的长度为 sizeof(A)
- 如果有 n 个类型为 A 的成员,对齐之后的长度为 n*sizeof(A)
- 需要根据对齐规则,具体对齐规则比较复杂,参考文档
class 和 struct 的内存对齐方式一样,同时需要注意存在虚表指针的情况,它放在所有成员变量的第一个,它按指针的长度进行对其。
union 的对齐方式
union 的对齐方式很简单,union 的 sizeof 的大小只和其中最长成员的大小有关
union C1{
int a;
double c;
};
union C2{
char c;
int b;
short a;
};
union C3{
int a;
int b;
int c;
};
int main() {
std::cout << sizeof(C1) << std::endl;
std::cout << sizeof(C2) << std::endl;
std::cout << sizeof(C3) << std::endl;
}
输出:
8
4
4
union 的不同之处就在于,它所有的元素共享同一内存单元,当我们给联合一个成员赋值的时候,另一个成员的值就被覆盖掉。
常见的 sizeof 的问题
指针的 sizeof ?
指针本质上是一个地址,所以和它运行的平台的寻址能力有关,32 位机结果为 4,64 位机结果为 8
一个函数的 sizeof ?
一个函数的 sizeof 之和它的返回值有关,其实此时相当于发生了一次函数调用。同理 void 类型的函数不可以求 sizeof;但 void* 可以,因为其相当于求一个指针的长度。
数组的 sizeof ?
数组的 sizeof 跟数组元素的个数 n 和 数组类型 T 有关,sizeof 的结果为 n*sizeof(T)
容器的 sizeof ?
标准库容器本质上就是一个类,这和它其中的类成员相关,而和容器中存储多少数据,模板类型均无关。