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 关键字

  1. static 的第一个作用也是最重要的一条:隐藏。(static函数,static变量均可)
    当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。

  2. static 的第二个作用是保持变量内容的持久。(static变量中的记忆功能和全局生 存期)存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。

  3. static的第三个作用是默认初始化为0(static 变量)
    其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区, 内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。

static 变量

  1. 函数内的 static 变量: 函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只 被分配一次,因此其值在下次调用时仍维持上次的值;
  2. 模块内的 static 全局变量: 在模块内的static全局变量可以被模块内所用函数访问,但不能被模块外其它函数访问;在模块内的static函数只可被这一模块内的其它函数调用,这个函数的使用范围被限制在声明它的模块内;
  3. 在类中的 static 成员变量: 在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝;在类中的static成员函数属于整个类所拥有,这个函数不接收this指针,因而只能访问类的static成员变量。static类对象必须要在类外进行初始化,static修饰的变量先于对象存在,所以 static修饰的变量要在类外初始化;
  4. static 修饰的类成员函数: 由于 static 修饰的类成员属于类,不属于对象,因此 static 类成员函数是没有 this 指针的,this 指针是指向本对象的指针。正因为没有 this 指针,所以 static类成员函数不能访问非static的类成员,只能访问 static 修饰的类成员;static 成员函数不能被 virtual 修饰,static 成员不属于任何对象或实例,所以 加上 virtual 没有任何实际意义;
  5. 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

为什么要内存对齐?

  1. 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  2. 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

内存对齐数

每个特定平台上的编译器都有自己的默认“对齐系数”(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;
};
  1. 如果只有一个类型为 A 的成员,对齐之后的长度为 sizeof(A)
  2. 如果有 n 个类型为 A 的成员,对齐之后的长度为 n*sizeof(A)
  3. 需要根据对齐规则,具体对齐规则比较复杂,参考文档

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 ?

标准库容器本质上就是一个类,这和它其中的类成员相关,而和容器中存储多少数据,模板类型均无关。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,905评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,140评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,791评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,483评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,476评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,516评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,905评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,560评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,778评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,557评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,635评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,338评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,925评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,898评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,142评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,818评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,347评论 2 342

推荐阅读更多精彩内容

  • 几种语言的特性 汇编程序:将汇编语言源程序翻译成目标程序编译程序:将高级语言源程序翻译成目标程序解释程序:将高级语...
    囊萤映雪的萤阅读 2,861评论 1 5
  • 1.在C++ 程序中调用被C 编译器编译后的函数,为什么要加extern “C”? 答:首先,extern是C/C...
    曾令伟阅读 916评论 0 4
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,082评论 1 32
  • ———————————————回答好下面的足够了---------------------------------...
    恒爱DE问候阅读 1,709评论 0 4
  • 注:这是第三遍读《C语言深度解剖》,想想好像自从大学开始就没读完过几本书,其中谭浩强的那本《C语言程序设计(第四版...
    HavenXie阅读 1,711评论 1 6