关于内存
内存(Memory)是计算机极其重要的一个部分,也是手机电脑玩家攀比的一个重要指标。但内存一直是很多学习C和C++的学生一大难点。而很多同学至今仍然分不清磁盘和RAM内存有什么不一样。。
A:你新买的电脑内存多大呀?
B:我的是16G内存。
A:我的手机都有64G内存欸。。
不论你是否充当过上述对话里面的角色A,首先明确一点:以下讨论的内存,均不是指磁盘内存。有关更多磁盘内存和ram的区别,可以去翻阅其他更加专业的资料。这里说的内存,都是你可以在任务管理器里面看到的那个内存。
这才是我们讨论的内存
内存四区
以笔者目前看过的教程来讲,关于内存模型,各个教程差别很大,都采用不同的方式,甚至还和生存期混起来讲解。给我也是造成了很大的疑惑。我这里主要介绍模型是我认为的比较全面的内存模型,该模型把程序内存分为四个区,如下:
内存四区 | 存放的东西 |
---|---|
代码区(code) | 你写的代码,各种函数(包括类成员函数) |
全局区(global) | 全局&静态&常量:全局(global)变量,静态(static)变量,各种常量(字符串字面量,const修饰的量) |
栈(stack) | 局部变量&函数参数:在函数内声明的变量(包括main函数) |
堆(heap) | 使用new,malloc,colloc等关键字手动开辟的内存 |
代码区
比起其他几个区,代码区不为人熟知。
代码区存放着函数,我们平时并不会去关注一个函数的地址,所以很多人并不知道代码区的存在。
代码区存放一般函数
有人可能好奇,运行下面这样的代码:
int func(){return 0;}
...
cout<< func <<endl;
或者是:
cout<< &func <<endl;
如果是gcc,你可能会得到如下警告:
warning: the address of 'int func()' will never be NULL [-Waddress]
忽视警告,打印出结果,发现都是1。这就是因为函数存放于代码区的缘故。
代码区存放类函数
类函数是对象的方法,会给人一种对象内含了函数的错觉。假设有下面一个类:
class Cat
{
private:
int age;
double length;
bool gender;
public:
void eat();
void sleep();
};
Cat Tom;
我们创建一个Cat
对象Tom
,获得Tom的占用内存大小:
cout<< sizeof(Tom)<<endl;
结果是24。这里是因为内存对齐的原因,把bool
和int
的内存也对齐为double
。这干扰了我们的判断。有关内存对齐的更多知识,可以去查找更专业的文章。
所以我们采用三个一样大小的数据,再打印Tom
的大小:
class Cat
{
private:
int age;
int length;
int gender;
public:
void eat();
void sleep();
};
Cat Tom;
cout<< sizeof(Tom)<<endl;
结果是12,刚好只是3个int的大小,没有用来存放类函数的空间。
由此可见,对象内不包含类函数。所有的类函数都如类里面的static
数据一样,只不过前者存放于代码区,后者存放于全局区。而类内的static函数
和non-static函数
的区别只在于,non-static函数
隐含了一个指针参数,这个指针参数就是指向各个对象的this
指针。
PS:关于这个机制,《Essential C++》内有一定论述,起初笔者以为是预处理为其加上了this指针的参数,但用g++ -E命令并没有得到想要的结果。本内容很多是基于笔者实测作出的推断,有错误的地方请联系笔者及时修改。
全局区
全局区内容有三:
- 全局变量
- 静态变量
- 常量(const修饰的和字符串字面量)
#include<iostream>
#include<string>
using namespace std;
//Global
string s{"Hello World"};
int i{9};
int main()
{
//static
static double pi{3.1415926};
//Local
unsigned j = 10;
//constant, includes string literals and const variables.
cout << "Literal:" << &"String" << endl;
cout << "Global:" << &i << endl;
cout <<"Global Object"<< &s << endl;
cout << "Static:" << &pi << endl;
cout << "Local:" << &j << endl;
return 0;
}
在上述程序内,我演示了打印全局变量、对象还有静态变量和字符串字面量的内存位置。打印的具体数值可能因为设备、编译器不同而不同。在我vscode(gcc)中的结果是:
0x代表16进制,可见全局量、静态量都在0x40xxx区域。而与之相比的局部变量则在0x61xxx段。
关于其他字面量存放位置,有待更专业的解答。
因此我们会发现,循环、函数内声明的static
变量不会被反复初始化,也不会因为循环或者函数结束而被销毁。但其作用域却被限制在{}内,我们也不能在外面使用它。
而类内的static成员作用域也是类作用域,所以我们可以在类外初始化它,但不能在构造函数内初始化它。
TIP:因为static变量不像堆内变量一样可以手动释放,它在程序结束才会自动释放,所以少使用它。
此外,static变量默认值都是0。
栈
栈(stack)是一块由编译器自动管理的内存。内含函数参数和局部变量,当参数或者变量不再使用,就会被自动销毁。
局部变量当然也包括main函数内声明的变量和循环内声明的变量。
因为这样的特性,所以不要返回局部变量的地址,编译器可能会考虑到你不懂这个特性,不马上销毁这个数据。但请不要这样做。
//不要像下面这样写代码
int* arr()
{
int a[10]={1,2,3,4,5,6,7,8,9,10};
return a;
}
此外,栈的大小有限,不要把太多数据放在栈中(比如int a[100][100][100]
),栈溢出会给出警告。
点名某网站stackoverflow。
堆
堆(heap)的特点就是大,但因为需要手动去管理内存,所以内存泄露常常发生在这里。
我们使用一些函数和关键字来管理堆内存。
//C-version
Type *p=(Type*)malloc(Type);
Type *pa=(Type*)malloc(num*sizeof(Type));
free(p);
free(pa);
//C++-version
Type *p{new Type()};
Type *pa{new Type[num]};
delete p;
delete[] p;
C中malloc函数返回void*类型,需要强制转换为需要的类型;而C++new关键字允许开辟内存同时初始化。虽然你的操作系统会在程序结束之后回收堆内存,但建议一开始就做好内存管理。
我们经常利用构造函数和析构函数完成内存管理。
class Game
{
public:
Game();
~Game();
void gameStart();
void gameRun();
void gameEnd();
private:
//我们把数据成员中体积大的对象都放入堆中,减小栈的压力。
UI* start;
Map* map;
Snake* snake;
Food* food;
static Tools tool;
unsigned speed;
};
Game::Game()//列表初始化
:start(new UI{}),
map(new Map{}),
snake(new Snake{}),
food(new Food{ *map,'$' })
{}
Game::~Game()
{
delete start;
delete map;
delete snake;
}
其他
除了以上内存四区,计算机还有一些区域可以存储数据。
寄存器
在C和C++中有一个前缀register
,使用这个前缀可以声明一个寄存器变量。
register int var;
这样的变量会放在CPU的寄存器中,便于CPU使用。因此不要对这样的变量取地址。
但和inline关键字一样,register只能向编译器发出请求,最终由编译器决定是否把变量放在寄存器中。
磁盘
磁盘是存放文件的场所,你写的源代码,库,.exe都会存在磁盘中。可以通过文件读写操作把数据保存到磁盘。这里不介绍如何进行文件读写,有兴趣的读者可以去找其他文章。