本章思维导图:
注:本章思维导图对应的xmind
文件和.png
文件已同步导入至资源
在C/C++中,内存区域主要划分为:内核区域、栈区、内存映射段、堆区、数据段、代码段等区域,如图:
本篇我们主要讨论栈区、堆区、数据段和代码段这四个区域。
栈区:主要存放运行函数而分配的局部变量、函数参数、返回数据、返回地址等。存放在栈区的变量的生命周期就是所在函数的作用域。
堆区:主要存放的是
malloc、realloc、calloc、new
等动态开辟函数开辟的空间。存放在堆区的变量的生命周期是整个进程。数据段:主要存放的是我们在函数外定义的全局数据,或者
static
静态数据。存放在数据段的变量的生周期是整个进程。代码段:主要存放的是
const
只读常量即字面量
下面,我们通过一个具体的例子来弄清楚各类数据到底存放在计算机的那一块空间:
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVar = 1;
const int localVar = 1;
int num1[10] = {1, 2, 3, 4};
char char2[] = "abcd";
char* pChar3 = "abcd";
int* ptr1 = (int*)malloc(sizeof (int)*4);
free (ptr1);
free (ptr3);
}
Question:
/*
选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
globalVar在哪里?____ staticGlobalVar在哪里?____ staticVar在哪里?____ const localVar在哪里?____
num1 在哪里?____
char2在哪里?____ *char2在哪里?___
pChar3在哪里?____ *pChar3在哪里?____
ptr1在哪里?____ *ptr1在哪里?____
*/
globalVar
定义在全局(函数外),是一个全局变量,因此存放在数据段staticGlobalVar
也是一个定义在全局的静态变量,因此也存放在数据段(静态区)
- 需要注意:尽管
globalVar
和staticGlobalVar
都存放在数据段中,但是它们之间也有链接属性的区别。globaVar
可以多文件共同使用,而staticGlobalVar
只能在本文件使用。staticVar
尽管定义在函数内,但是由static
修饰,是一个静态变量,因此也存放在数据段const localVar
定义在函数内,是一个局部变量,因此存放在栈区
- 注:千万不要认为
localVar
被const
修饰他就存放在代码段(常量区),const
只是修饰局部变量localVar
,表示它的值不能被修改。num1
是静态开辟的数组,所以存放在栈区。
char2
也是个静态开辟的字符串数组,因此也存放在栈区
*char2
:这里的char2
表示首元素的地址,所以*char2
就表示字符a
。看到这里,可能有小伙伴就会说*char2
存放在常量区,但事实上*char2
还是存放在栈区。我们可以从两个方面解释:
pchar3
就是定义在函数内的局部变量,因此存放在栈区
pchar3
指针指向的就是字符串字面量abcd
,而字符串字面量const
常量,因此存储在代码段
ptr1
就是定义在函数内的局部变量,因此存放在栈区
ptr1
指向的是malloc
开辟的空间,因此*ptr1
存放在堆区
- C++使用
new
和delete
来动态管理内存new
和delete
是C++内置的两个运算符,不需要显示的包含头文件来使用。
有小伙伴可能会有疑惑:
既然C语言已经可以用
malloc、realloc、free
等函数来动态管理内存了,为什么C++还要新创建两个运算符new
和delete
来替代C语言的方法呢?
一个原因是,C++是面向对象的,C++有C语言没有的class
类,类的初始化必须调用构造函数,但是构造函数不能显示调用,例如:
class A
{
public:
A(int a = 1)
:_a(a)
{
}
private:
int _a;
};
int main()
{
A* p = (A*)malloc(sizeof(A));
p->A();
return 0;
}
//会报错:类型名称“A”不能出现在类成员访问表达式的右侧
可见,C语言并不能解决C++自定义类型初始化的问题,所以C++才要新创建两个运算符new
和delete
来解决这一问题。
这次,我们换用C++的new
运算符来动态开辟内存,并用delete
进行空间的释放:
class A
{
public:
A(int a = 1)
:_a(a)
{
}
~A()
{
}
private:
int _a;
};
int main()
{
A* p = new A;
delete p;
return 0;
}
让我们进行调试:
可以看到:
new
在给自定义类型开辟空间时,会在给对象开辟完空间后继续调用该对象的默认构造完成初始化。
delete
在释放自定义类型对象之前,会先调用对象的析构函数,再释放对象的空间
除了上面最重要的不同之外,malloc/free
和new/delete
还有许多不同之处:
malloc/free
是函数,而new/delete
是运算符malloc
开辟空间时需要用sizeof
计算开辟空间的大小,而new
只需要在后面加上开辟对象的类型就行malloc
的返回值时void*
,实际使用时需要强制类型转换;而new
不需要,因为开辟时就指定了对象类型malloc
失败时,会返回0,因此需要对结果进行判空;而new
不需要,但是需要进行异常捕获
如果一次只开辟、释放单个对象,基本格式为:
new type;
- 例如:
int*p = new int; delete p;
如果一次开辟、释放多个对象,基本格式为:
new type[nums]
- 例如:
int*p = new int[10]; delete[] p
对于自定义类型,new
和delete
的使用和内置类型基本一致,但是我们可以在new
的同时给默认构造传参(如果可以传的话),这样可以一次创建初始值不同的多个同一类型的多个对象:
例如:
class AB
{
public:
AB(int a = 1, int b = 1)
: _a(a)
, _b(b)
{
}
private:
int _a;
int _b;
};
class C
{
public:
C(int c = 1)
:_c(c)
{
}
private:
int _c;
};
int main()
{
AB* p1 = new AB; //生成一个对象:_a = 1, _b = 1
AB* p2 = new AB{ 2,2 }; //生成一个对象: _a = 2, _b = 2
C* p3 = new C(2); //生成一个对象:_c = 2
C* p4 = new C[3]; //生成三个对象:_c都为1
C* p5 = new C[3]{ 2, 3, 4 }; //生成三个对象:_c分别为2,3,4
AB* p6 = new AB[3]{ {2,2}, {3,3}, {4,4} }; //生成三个对象: _a, _b分别为(2,2)、(3,3)、(4,4)
delete p1;
delete p2;
delete p3;
delete[] p4;
delete[] p5;
delete[] p6;
return 0;
}
特别注意:
new/delete
和new[]/delete[]
必须配套使用,不能随意搭配!!!
实际上,执行运算符new
开辟空间和delete
释放空间时,编译器都会调用operator new
和operaotr delete
这两个函数
注意:
不要把
operator new
和operaotr delete
这两个函数理解为运算符重载
我们可以先来看看operator new
的具体实现:
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
可以看到,operator new
开空间时实际上用的也是C语言的malloc
函数,operator new
只不过是对malloc
的一层封装。那么为什么要对malloc
进行封装呢?
malloc
失败时返回的是整数0,这不符合C++面向对象的要求,因此C++要对malloc
进行封装,使内存开辟失败时可以抛出异常
同样**,实际上operator delete
也是用C语言的free
函数,来实现对空间的释放**
//等效
int* p1 = (int*)malloc(sizeof(int) * 10);
int* p2 = (int*)operator new(sizeof(int) * 10);
//等效
free(p1);
operator delete(p2);
现在,我们对于C++new
和delete
对于自定义类型空间的开辟和释放就更加明了了:
new
首先调用operator new
来开辟对象的空间,再调用对象的默认构造进行初始化delete
首先调用析构函数释放对象成员变量的资源,再调用operator delete
释放该对象的空间
在之后的学习过程中,我们可能会遇到需要显式调用自定义类型构造函数的情况。但是一般情况下,构造函数不支持显式调用,此时定位new就可以帮我们解决这个问题。
- 定位
nwe
可以显示调用构造函数- 其基本格式为
new (place_address) type或者new (place_address) type(initializer-list)
place_address
就是是一个指向和一个类相同大小空间的指针type
就是类类型initializer-list
就是构造函数的参数列表
例如:
class A
{
public:
A(int a = 1)
: _a(a)
{
}
~A()
{
}
private:
int _a;
};
//构造函数不能显式调用,但是析构函数可以
int main()
{
A* p = (A*)malloc(sizeof(A));
new(p)A(10);
p->~A();
return 0;
}![请添加图片描述](https://img-blog.csdnimg.cn/49bacd2462d14cf3a6b58056d5206c4a.gif)
下一篇,我们将对C++的模板和泛型编程展开讲解,感兴趣的小伙伴可以关注此专栏。