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等线程锁手段来解决。
C++变量内存分配及类型修饰符的更多相关文章
- [面试]volatile类型修饰符/内存屏障/处理器缓存
volatile类型修饰符 本篇文章的目的是为了自己梳理面试知识点, 在这里做一下笔记. 绝大部分内容是基于这些文章的内容进行了copy+整理: 1. http://www.infoq.com/cn/ ...
- c语言类型修饰符及内存
今天来学习一下c语言类型修饰符及内存分布 1.auto int a; 默认在内存 2.register int a; 限制变量定义在寄存器上的修饰符 编译器会尽量安排CPU的寄存器去存放这个a,如果寄 ...
- JAVA类型修饰符(public,protected,private,friendly)
JAVA类型修饰符(public,protected,private,friendly) public的类.类属变量及方法.包内及包外的不论什么类均能够訪问:protected的类.类属变量及方法,包 ...
- Linux学习---类型修饰符
auto eg:aoto int a; 默认情况--------->分配的内存可读可写的区域. register eg:register int a; 限制变量定义在寄存器上的修饰符 定义一 ...
- C++11常用特性介绍——auto类型修饰符
1.C++11常用特性介绍 从本篇开始介绍C++11常用特性,大致分:关键字及新语法.STL容器.多线程.智能指针内存管理,最后讲一下std::bind和std::function 二.关键字和新语法 ...
- volatile 类型修饰符
volatile 类型修饰符 1.解释 就像大家更熟悉的const一样,volatile是一个类型修饰符(type specifier).它是被设计用来修饰被不同线程访问和修改的变量.如果不加入vol ...
- c++中函数中变量内存分配以及返回指针、引用类型的思考
众所周知,我们在编程的时候经常会在函数中声明局部变量(包括普通类型的变量.指针.引用等等). 同时,为了满足程序功能的需要,函数的返回值也经常是指针类型或是引用类型,而这返回的指针或是引用也经常指向函 ...
- 【译文】 C#面向对象的基本概念 (Basic C# OOP Concept) 第一部分(类,对象,变量,方法,访问修饰符)
译文出处:http://www.codeproject.com/Articles/838365/Basic-Csharp-OOP-Concept 相关文档:http://files.cnblogs.c ...
- java接口中成员变量和方法的默认修饰符(转)
Java的interface中,成员变量的默认修饰符为:public static final 所以我们在interface中定义成员变量的时候,可以 1:public static final St ...
随机推荐
- Java动态代理代码快速上手
动态代理的两个核心的点是:代理的行为 和 代理机构. 举个例子,上大学的时候,很多同学吃午饭的时候都是叫别人带饭,有一个人H特别热心肠,想了一个办法,他在门口挂了个公示牌,每天有谁想要找人带饭就写公告 ...
- (数据科学学习手札49)Scala中的模式匹配
一.简介 Scala中的模式匹配类似Java中的switch语句,且更加稳健,本文就将针对Scala中模式匹配的一些基本实例进行介绍: 二.Scala中的模式匹配 2.1 基本格式 Scala中模式匹 ...
- 20155229 2016-2017-2《Java程序设计》课程总结
20155229 2016-2017-2<Java程序设计>课程总结 每周作业链接汇总 预备作业1:对专业的期待和对师生关系的理解 预备作业2:分析自我技能延展到c语言学习状况 预备作业3 ...
- 20155338 《Java程序设计》实验三(敏捷开发与XP实践)实验报告
20155338 <Java程序设计>实验三(敏捷开发与XP实践)实验报告 一.实验内容及步骤 (一)使用Code菜单 • 在IDEA中使用工具(Code->Reformate Co ...
- docker容器的启动、停止、运行、导入、导出、删除
原文:docker容器的启动.停止.运行.导入.导出.删除 版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/jiang425776024/articl ...
- NPOI读取Excel到集合对象
之前做过的项目中有个需要读取Excel文件内容的需求,因此使用NPOI实现,写下以下代码,这个只是一个代码段,还有很多地方需要优化,希望能对大家有所帮助 public static IList< ...
- Oracle数据库备份与还原命令
http://www.2cto.com/database/201305/210262.html
- 关于Eclipse在servlet中连接数据库时出现驱动加载失败的解决
问题:在队友发来的项目中想将他获取到的数据通过数据库储存,出现驱动加载失败问题 解决:首先百度了下相关情况,大多数都是说下载mysql-connector-java-5.1.39-bin.jar包,然 ...
- 文件批量加密重命名--python脚本AND mysql命令行导入数据库
在考试中学生交上来的报告,需要进行一下文件名加密,这样阅卷老师就不知道是谁的报告了 在百度帮助下,完成了加密和解密脚本, 加密 #!/usr/bin/python # -*- coding: utf- ...
- 获取安卓app的appPackage和appActivity
1.需要配置好android的开发环境后,打开cmd命令窗口 2.在命令窗口中输入,adb logcat>D:/log.log,抓取日志 3.运行启动app 4.查看日志log 5.搜索日志的关 ...