C++变量内存分配及类型修饰符

前言

了解C++程序内存分配,有助于深刻理解变量的初始化值以及其生存周期。另外,变量类型修饰符也会影响到变量的初始化值及其生存周期。掌握了不同类型变量的初始化值及其生存周期,能够让我们设计程序时定义变量时更准确。

内存分配

1.     C++程序的内存布局

现代电脑都是遵循冯诺依曼体系结构,所以C++程序的内存布局也是遵循该体系的。主要包括5个部分,即代码段、数据段、BSS段、堆和栈、。

1.    
代码段

代码段(code segment/text segment),通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。另外关于字符串常量存储的位置,也与编译器有一定的关系,也有可能放在数据段。

2.    
数据段

数据段(data
segment),通常是指用来存放程序中已初始化的全局变量/静态变量的一块内存区域。数据段属于静态内存分配。

3.    
BSS段

BSS段(BSS segment),通常是指用来存放程序中未初始化的全局变量/静态变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。

4.    

堆(Heap),是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc/new等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用free/delete等函数释放内存时,被释放的内存从堆中被剔除(堆被缩减)。

5.    

栈(stack),栈也称堆栈,是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。除此以外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进后出特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。压栈出栈,后压入栈的处在栈顶,所以最先出栈。也即后进先出(last-in,first-out,LIFO)。

2.     变量内存存储

按申请内存的方式不同,主要分为静态存储区和动态存储区。

1.    
静态存储区

静态存储区内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。静态存储区主要包括只读存储区、已经初始化的全局变量/静态变量存储区和未初始化的全局变量/静态变量存储区。

2.    
动态存储区

动态存储区内存是在程序运行的时候,根据需要动态分配的。动态存储区主要包括堆和栈。


低地址

高地址


只读存储区


静态存储区


已初始化的全局变量/静态变量存储区


未初始化的全局变量/静态变量存储区


堆区


动态存储区


栈区

int g_idx
= 10;   // 已初始化的全局变量

int* g_pArr;       // 未初始化的全局变量,其默认值为0

int _tmain(int argc, _TCHAR* argv[])

{

// nCnt存储在栈区

int nCnt
= 100;

// pPath存储在栈上,"c:\\work"是字符串常量存放在常量区

// pPath指向这个字符串常量,所以不能修改pPath指向的内容

char* pPath
= "c:\\work";

// 00411475  mov  
dword ptr [pPath],offset string "c:\\work" (415754h)

// 已经初始化的静态变量

static int
nCurCnt = 1;

// pCnt存储在栈上,但pCnt指向的内存是在堆上分配的个int

char* pNewPath
= new char[10];

memset(pNewPath, 0, 10);

// 通过上下两段汇编代码可以看出,"c:\\work"被编译器优化成了同一个常量

strcpy_s(pNewPath, 10, "c:\\work");

// 
004114A5  mov         esi,esp

//  004114A7  push       
offset string "c:\\work" (415754h)

delete[] pNewPath;

pNewPath = NULL;

return 0;

}

变量类型修饰符

1.     const

概念:指使用类型修饰符const说明的类型,常类型的变量或对象的值是不能更新的。

语法形式:

限定非指针类型:下面两种形式作用一样,即变量为常量,不能修改。

int const MAX_DEVICE_CNT =
32;

const int MIN_DEVICE_CNT =
16;

限定指针类型靠近谁就限定谁,可以理解为就近原则。

int nValue = 10;

const int* pPtr =
&nValue;  // 限定pPtr指向的值,但不限定pPtr本身

int* const pValue =
&nValue;    // 限定pValue本身,但不限定pValue指针的值

const int* const pV= &nValue;   // 指针和值都限定

初始化

i.             
类的成员变量,初始化必须在构造函数初始化列表中完成。

ii.             
其他情况,声明必须和定义在一起,即声明的同时进行定义。

作用域:

iii.             
在函数体{}内,作用域为局部作用域。

iv.             
作为类成员变量,作用域即为当前类。

v.             
和static一起声明在类声明中,作用域为全局,通过类名访问。

vi.             
在类/函数体外,作用域为文件作用域,即只能被当前文件所使用。

vii.             
和static组合时,作用域不变,依然为文件作用域。

viii.             
和extern组合时,那么此变量的作用域改变为全局的。

const extern int MIN_DEVICE_CNT =
16; // 声明定义

extern const int MIN_DEVICE_CNT =
16; // 声明定义

const extern int MIN_DEVICE_CNT;   // 外部文件使用时声明

extern const int MIN_DEVICE_CNT; // 外部文件使用时声明

注:当前两个变量的访问形式相同,且作用域重叠,链接时,会报重复定义的错误。如果当前常量只想在当前文件内使用,请定义申明在当前CPP文件最前面。如果只想在当前类中使用,可以定义在类声明中。

2.     static

概念:改变声明变量的生存周期为全局。

语法形式:Static不能和extern共用,且与const,类型(int等)可以随意位置。

const static int MIN_DEVICE_CNT;

static const int MIN_DEVICE_CNT;

int const static MIN_DEVICE_CNT;

初始化:全局变量/静态变量都存在静态存储区,指明初始化值和未指明初始化值都是可以的。如果未指明初始化值其默认值为0.

static int MIN_DEVICE_CNT =
16; // 初始化值为16

static int MIN_DEVICE_CNT;      // 未初始化,默认内存值0

作用域:

ix.             
在函数体{}内,作用域为局部作用域,只能为当前{}访问,但是生存期却为全局的,第2次访问时,不会再初始化,而是记住上次的值。

x.             
作为类成员变量,作用域为全局,但是必须通过类名访问。

xi.             
在类/函数体外,作用域为文件作用域,即只能被当前文件所使用。

注:如果1个变量只想在当前文件内使用,最好定义在CPP文件中。如果定义在头文件里,那么包含此头文件的CPP文件都会生成一个当前文件作用域的同名变量,不仅容易引起歧义,也浪费空间。

3.    
extern

概念:声明当前变量已经在外部文件有定义。

语法形式: extern不能和static共用,且与const,类型(int等)可以随意位置。定义时加与不加extern均可,但声明的时候必须添加,否则会造成作用域重叠链接错误。另外extern与const的组合使用,见const条款。

int MAX_DEVICE_CNT = 32;        // 文件A.cpp中的声明定义

extern int MAX_DEVICE_CNT = 32; // 文件A.cpp中的声明定义

extern int MAX_DEVICE_CNT;     // 文件B.cpp中的声明定义

初始化:必须先有定义,也即需要初始化值,然后才能外部声明。

作用域:改变const的作用域,将const的文件作用域改为全局作用域。

注:全局数组变量的外部声明不能声明成指针,必须声明为数组类型。如:

int nArr[4] ={1, 2,
3, 4}; // 文件A.cpp中的声明定义

extern int nArr[4]   ;       // ok: 文件B.cpp中的声明定义

extern int nArr[];           // ok: 文件B.cpp中的声明定义

4.     volatile

概念:告诉编译器限定的变量是易变的,不能被优化。

语法形式:volatile在语法形式上基本和const一样,可以和extern、const、static一起共用。在限定指针时,也分限定指针本身和指针指向的值。

int* volatile vip; // vip is a
volatile pointer to int

volatile int* ivp; // ivp is a pointer
to volatile int

volatile int* volatile ivp; // ivp is a
volatile pointer to volatile int

初始化:volatile限定不能被去掉,也即用取地址时也必须限定指针指向的值为volatile,否则编译错误。指针的取地址同样.

volatile int nValue = 4;

int* pInt =
&nValue;             // error:不能更改nValue的volatile属性

volatile int* pValue = &nValue;
// ok:限定pValue指向的值,即没有改变nValue

作用域:不改变作用域。

注:什么时候使用volatile呢?这需要弄明白,什么时候不用volatile会出错。如:

void Square(int* pValue)

{

int nA = (*pValue);

// 004114FE 
mov         eax,dword ptr [pValue]

// 00411501 
mov         ecx,dword ptr [eax]

// 00411503 
mov         dword ptr [nA],ecx

int nB = (*pValue);

// 00411506 
mov         eax,dword ptr [pValue]

// 00411509 
mov         ecx,dword ptr [eax]

// 0041150B 
mov         dword ptr [nB],ecx

}

上面这是未优化的代码,分别从指向的内存取值。如果是优化过的代码,那么第二次取值就可能直接从寄存器中取值了,而不会再从内存里去取值了。如果在两次赋值之间,另外一个线程更改了pValue指向的值的时候,nB如果依然从寄存器中取值,实际上取得的是错误的值。这个时候,就需要限定pValue指向的值了。需要volatile int* pValue作为参数。这样,无论是nA,还是nB取得的值都真实的。所以,当有多个线程改变同一个变量时,这个变量就需要考虑使用volatile限定了。当然不加volatile也并不能保证当前线程里多次访问全局变量的值不被其他线程改变,要达到这样的效果,需要使用Critical Section、Mutex等线程锁手段来解决。

时间: 2024-10-09 21:47:12

C++变量内存分配及类型修饰符的相关文章

c语言类型修饰符及内存

太难了,嵌入式这条路是个大坑啊,学的东西太多,但没办法,既然选择了这条路,坚持走下去吧,打好基础,铺好每一块砖,才能走的更加稳健 今天来学习一下c语言类型修饰符及内存分布 1.auto int a; 默认在内存 2.register int a; 限制变量定义在寄存器上的修饰符 编译器会尽量安排CPU的寄存器去存放这个a,如果寄存器不足,a还是放在内存中 取地址符号&对a不起作用 3.static 应用场景: 1.函数内的变量 2.函数外的变量 3.函数的修饰符(函数也是一个变量) int fu

C语言学习笔记(2):volatile与register类型修饰符

1.volatile volatile是易变的,不稳定的意思,volatile是关键字,是一种类型修饰符,用它修饰的变量表示可以被某些编译器未知的因素更改,比如操作系统.硬件或者其他线程等,遇到这个关键字声明的变量,编译器对访问该变量的代码不在进行优化,从而可以提供对特殊地址的稳定访问.那么什么是编译器优化呢? 为了提高运行效率,攻城湿们可是费尽心机地把代码优化,把程序运行时存取速度优化.一般,分为硬件优化和软件优化.硬件优化,流水线工作,详细可以参考<计算机组成原理>.软件优化,一部分是程序

关于C语言的类型修饰符

分享网址 http://wenku.baidu.com/link?url=e-xWNn7f84rrEf_vhHz5CQh2LCVmaGBTA6iB0BC8zPv_8eXz5SKmRofsSuenh8wn_JjeQZBD103xCA6wqDkYo9SzlIvKgCpYwwrbyNvbxVS 在一般的 C 教科书中,可以见到 6 种类型修饰符,分别是 : auto, const, register, static, volatile, extern. 局部变量除非显式指明为 static, 否则默认

类型修饰符

1,auto 默认情况下,char a --> 实际就是auto char a:只是将auto省略了. auto是一个可读可写的内存空间--栈空间 2,register 1,限制变量定义在寄存器上的修饰符,cpu上的寄存器(cpu内部内存),可以使变量不用指向内存,直接指向cpu的寄存器 2,定义一些快速访问的变量,编译器会尽量安排CPU的寄存器去存放这个这个A,如果寄存器不足,A还是会放在存储器中 3,无法访问变量的地址     register int a;     a = 0x10;   

功能:类型修饰符long和unsigned的使用

#include<stdio.h> main() { char a1,b1; unsigned char a2,b2; int x1,y1; long x2,y2; a1=127;    b1=129; a2=127;    b2=129; x1=32767;   y1=32769; x2=32767;   y2=32769; printf("a1=%d,a2=%u,b1=%d,b2=%u\n",a1,a2,b1,b2); printf("x1=%d,x2=%u,

c++中函数中变量内存分配以及返回指针、引用类型的思考

众所周知,我们在编程的时候经常会在函数中声明局部变量(包括普通类型的变量.指针.引用等等). 同时,为了满足程序功能的需要,函数的返回值也经常是指针类型或是引用类型,而这返回的指针或是引用也经常指向函数中我们自己声明的局部变量. 这样,程序在某些情况下就可能存在一定的问题.看似很简单的问题,通过仔细的分析,我们就能够更好的理解c++中内存分配和释放的问题. 好,废话不多说,我们进入正题.首先,简单介绍一下程序的内存区域的分配: 程序的内存分配 ①堆区(heap).这一部分主要是由程序开发人员自己

java接口中成员变量和方法的默认修饰符(转)

Java的interface中,成员变量的默认修饰符为:public static final 所以我们在interface中定义成员变量的时候,可以 1:public static final String name = "张三"; 2:String name = "张三"; 以上两种都可以,老司机一般都是第二种.既然是静态最终的变量,也就意味着在外面访问的时候不能修改这个成员变量的值.所以在接口中定义成员变量的,一般都是常量.不会修改的.如果要进行修改的话,定义

C语言结构体变量内存分配与地址对齐

地址对齐简单来说就是为了提高访问内存的速度. 数组的地址分配比较简单,由于数据类型相同,地址对齐是一件自然而然的事情. 结构体由于存在不同基本数据类型的组合,所以地址对齐存在不同情况,但总体来说有以下规则: 原则1:数据成员对齐规则:结构的数据成员,第一个数据成员放在偏移量(offset)为0的地方,以后每个数据成员存储的起始位置要从该成员大小的整数倍开始(比如int在32位机为4字节,则要从4的整数倍地址开始存储). 原则2:收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大

变量内存分配

方法1: char *a = new char[10]; delete a; 方法2: char a[10] = "";