目录
注
本笔记参考:《C++ PRIMER PLUS(第6版)》
指针与自由存储空间
指针是一种变量,其存储的是地址。一般,我们可以使用地址运算符(&)来找到变量对应的地址,例如:
#include<iostream>
int main()
{
using namespace std;
int a_1 = 6;
double b_1 = 4.5;
cout << "变量a_1的值 = " << a_1;
cout << "\t\t对应的地址 = " << &a_1 << endl;
cout << "变量b_1的值 = " << b_1;
cout << "\t对应的地址 = " << &b_1 << endl;
return 0;
}
程序执行的结果是:
Ubantu系统下的g++:
VS2022下:
可以看到,在cout通过十六进制的形式实现了对地址的输出。
在上述的两个程序执行的结果中,我们可以看到不同系统的存储方式的不同:
- 在Ubantu系统的实现下,a_1 在内存里的存储位置是比 b_1 低4个字节的,这正好对应了int类型的大小(4个字节);
- 而在VS2022的实现下,两个变量没有被存储在相邻的内存单元内。
总结:使用常规变量时,值是指定的量,而地址则是派生量。
指针策略
先粗略看看源文件到运行结果的过程:
面向对象编程与传统的过程性编程的区别在于,OOP强调在运行阶段(而不是编译阶段)进行决策。通过将决策推迟到运行阶段进行,使程序更加灵活,譬如:
同样的一个数组,这次可以告诉它,我们需要20个元素,下次可以要求200个元素。这就是在一些阶段确定数组的长度(C++中实现的方法是使用关键字new)。
上述的策略与处理常规变量的方式不同,将地址视为指定的值,值则作为派生量存在。这就是指针 —— 将地址作为存储对象。(也因此,指针名表示的是地址)
类似于从常规变量中调用地址,从地址变量中调用值也需要对应的运算符:*运算符(间接值,又称解除引用)。例如:
#include<iostream>
int main()
{
using namespace std;
int into = 12;
int* mainly; //声明一个指针变量
mainly = &into; //将地址赋给指针变量
cout << "①表示into的地址:"; // &into 和 mainly 完全等价
cout << "\n&into = " << mainly;
cout << "\nmainly = " << mainly;
cout << "\n\n②表示into内部存储的值:"; // into 和 *mainly 完全等价
cout << "\ninto =" << into;
cout << "\n*mainly =" << *mainly; //使用 *运算符 找到指针指向地址内存储的值
cout << "\n\n③若 *mainly = *mainly + 1:";
*mainly = *mainly + 1;
cout << "\n此时into = " << into << endl;
return 0;
}
程序执行的结果是:
由此可知,int变量into 和 指针变量mainly 就像是硬币的两面,表示的都是同一个数据的不同方面。
声明和初始化指针
实际上,无论指针指向的值的类型是什么,指针的大小是不变的(大小可能随系统不同而变)。但是,不同的类型拥有不同的对于数据的访问权限,因此在进行指针的声明时,必须指定指针指向的数据的类型。例如:
int* mainly;
上述的这行语句告诉我们:
- *mainly的类型是int,而不是指针;
- mainly是指针。
其实,*运算符两边的空格是可选的:
int *ptr; //强调*ptr是一个int类型的值
int* ptr; //强调int*是一种(复合)类型(指向int的指针)
这并不要紧,但要注意下面的代码:
int* p1, p2;
在这条语句中,p1是一个指针,但是 p2 是一个 int变量 。对于每一个指针变量名,都需要使用一个[ * ]。
除此之外,也可以在声明中初始化指针,例如:
int height = 5;
int* ph = &height;
使用例
#include<iostream>
int main()
{
using namespace std;
int a_1 = 5;
int* a_p = &a_1;
cout << "a_1的值 = " << a_1 << endl;
cout << "a_1的地址 = " << &a_1 << endl << endl;
cout << "*a_p的值 = " << *a_p << endl;
cout << "a_p的值 = " << a_p << endl;
return 0;
}
程序执行的结果是:
指针的危险
在创建指针时,系统会分配用来存储地址的内存,但不会分配用来存储指针指向的数据的内存。为数据分配空间是一个独立的步骤。
long* fellow;
*fellow = 232323; //fellow指向了未知的、还未开辟的地址
在没有指定之前,指针fellow指向的地址是未知的,甚至。指向的位置可能恰巧是其他程序代码的地址。这种错误会导致一些隐匿、并且难以跟踪的错误。
所以,在对指针使用 解除引用 之前,一定要将指针初始化为确定、合适的地址。
指针和数字
首先,指针不是整型。在概念上,指针与整型是截然不同的类型:整数描述的是可执行加减乘除的数字,而指针描述的是位置。因此,直接将整数赋给指针是不行的:
int* pt;
pt = 0xB8000000; //类型不匹配
实际上,上述的赋值在C99标准发布之前是成立的。
如果要将数字值作为地址使用,就要用到强制类型转换:
int* pt;
pt = (int*)0xB80000000; //类型匹配
略微总结以下目前的知识:
- 变量:在编译时分配的有名字的内存;
- 指针:为 可以通过名称直接访问的内存 提供了一个别名。
显然,指针不止上述的用法。这就是接下来的内容了:通过指针,可以在运行阶段对未命名的内存进行分配,以此存储值(动态内存分配)。
使用new来分配内存
先看看内存区域的划分:
一般,我们创建的局部变量都会被开辟在栈区,但通过动态内存分配的空间会被开辟在堆区。
在C语言中,可以通过函数malloc( )分配内存,但在C++中,更加推荐使用 new运算符 。例如:
int* pn = new int;
在使用new运算符时,程序员需要做的,是告诉new将要为何种类型的数据分配内存;之后,new将会在堆区上找到一块合适的内存块,并返回该内存块的首地址。
如果将上述语句与之前的指针赋值方式进行对比:
int into;
int* mainly = into;
会发现,通过 new 开辟的空间只能通过 指针pn 进行访问,但之前的赋值方式会使被开辟的空间还能通过名称into进行访问。这表明:
- 指针pn 指向的并不是一个变量,但却是一个数据对象(为数据项分配的内存块,可以是结构、基本类型等);
- 使用new的内存分配方式 使得 程序在管理内存方面获得了更大的控制权。
动态分配内存的通用格式如下:
使用例
#include<iostream>
int main()
{
using namespace std;
int nights = 1001;
int* pt = new int; //在堆区开辟一个int类型大小的空间
*pt = 1001;
cout << "nights的值 = " << nights
<< "\t地址 = " << &nights << endl;
cout << "*pt的值 = " << *pt
<< "\t地址 = " << pt << endl;
double* pd = new double; //在堆区开辟一个double类型大小的空间
*pd = 10000001.0; //存储一个double类型的值
cout << "\n*pd的值 = " << *pd << endl;
cout << "指针pd指向的地址 = " << pd << endl;
cout << "存储指针pd的地址 = " << &pd << endl;
cout << "\npt的大小 = " << sizeof(pt)
<< "\t*pt的大小 = " << sizeof(*pt) << endl;
cout << "pd的大小 = " << sizeof(pd)
<< "\t*pd的大小 = " << sizeof(*pd) << endl;
delete pd; //记得释放通过new开辟的空间
return 0;
}
程序执行的结果:
【分析】
使用new为int类型和double类型的变量进行内存分配,这是发生在程序运行阶段的。
上述程序表明:
- 通过new返回的是内存块的首地址,无法指出类型(因此,在使用new进行内存开辟时,必须指名类型);
- 指向int类型的指针与指向double类型的指针大小相同。
当计算机没有足够的内存来满足new的请求时,new会引发异常(在较早的实现中,会返回空指针。)。
使用delete释放内存
delete运算符的作用:当(通过new开辟的)内存被使用完毕时,将内存归还到内存池。这种做法可以提升内存使用的效率:被归还或者释放(free)的内存可被程序的其他部分使用。例如:
int* ps = new int; //使用new开辟空间
//省略中间的代码
delete ps; //释放内存
这种释放不会删除指针ps本身,也就是说,指针ps可以被使用,用以指向其他空间。
注意:
- new 与 delete 一定要配对使用,否则将会引发内存泄漏 —— 已被分配而没有得到释放的内存将无法被在此使用。
- 不要尝试释放已经被释放的内存块,其会造成什么结果是不确定的。
delete不能被用以释放通过声明变量获得的内存:
int main() { int a = 0; int* pa = &a; delete pa; //不成立,pa指向的内存空间不是通过new在堆上开辟的 return 0; }
(不过可以对空指针使用delete……)
注意,delete的操作数并非是接收new的指针,而是 new返回的地址 。所以下方的代码是可行的:
int* ps = new int;
int* pq = ps; //pq 和 ps 指向的是同一块空间
delete pq; //通过第二个指针pq进行删除
一般情况下,上述写法可能增加将同一个内存块删除两次的错误。但对于返回指针的函数,上述写法有其道理。
使用new创建动态数组
- 静态联编(static binding):在编译时为数组分配内存,数组的长度在编写程序时确定;
- 动态联编(dynamic binding):在程序运行阶段创建数组,数组的长度在运行程序时确定。这样创建的数组被称为动态数组。
接下来看看如何使用new创建及使用动态数组:
1. 使用new创建动态数组
例如:
int* psome = new int[10]; //创建一个包含10个int元素的数组
在创建动态数组时,要描述数组的元素类型和元素数目。开辟空间成功时,new将返回第一个元素的地址。
尽管创建动态数组并不特殊,但在使用delete释放数组空间时却要注意,使用:
delete[] psome; //删除动态数组psome
加入 方括号[],告诉程序将要释放的是整个数组,而不是单个元素。也就是说,如果new带有[],则delete也要带有[]。
上述例子表明:new 和 delete 的格式必须匹配(实际上,不匹配的后果将会是不确定的)。
小总结:
- 不要使用delete来释放不是new分配的内存;
- 不要使用delete释放同一个内存块两次;
- new 和 delete 的格式要匹配;
- 对空指针使用delete是安全的。
注:使用 sizeof运算符 是无法确定动态数组包含的字节数的,这是因为程序不会公开动态分配的内存量。因此,在使用动态分配内存时,程序员需要主动跟踪元素的个数。
为动态数组分配内存的通用格式:
2. 使用动态数组
以上述的指针psome为例:
可以使用数组的访问形式来访问该指针的每一个元素,类似于 psome[0] ,对应数字第1个元素;psome[1] ,对应数字第2个元素;……
上述方法之所以能够成立,是因为C和C++内部都是使用指针来处理数组,故可以通过 使用数组 的形式来 使用指针 (数组与指针基本相等)。
例如:
#include<iostream>
int main()
{
using namespace std;
double* p3 = new double[3]; //创建一个可以存储3个double类型的动态数组
p3[0] = 0.2;
p3[1] = 0.5;
p3[2] = 0.8;
cout << "p3[1] 的值 = " << p3[1] << endl;
cout << endl << "当 p3 = p3 + 1 :" << endl;
p3 = p3 + 1; //指针指向数组的下一个元素
cout << " p3[0] 的值 = " << p3[0] << endl
<< " p3[1] 的值 = " << p3[1] << endl;
p3 = p3 - 1; //指针重新指向数组首元素
delete[] p3; //释放内存
return 0;
}
程序执行的结果是:
上述程序将指针p3作为数组进行使用,p3[0]是第一个元素。由此可知,使用[ ]进行表示等同于对指针进行解除引用。但指针与数组之间仍然存在区别:
p3 = p3 + 1; //这种写法是不被数组允许的,但是指针可以这么实现
注意:数组不允许修改数组名的值。但指针是变量,所以可以修改指针的值。
相邻的int地址会相差4\8个字节(也有2个字节),但p3+1后直接指向下一个元素的地址,这就是指针算术的不同了。
指针、数组和指针算术
指针与数组之所以基本相等,是因为 指针算术 和 C++内部处理数组的方式 。
1. 指针算术:将指针变量加1后,增加的量等于该指针指向的类型的字节数。这说明:C++将数组名解释为地址。例如:
#include<iostream>
int main()
{
using namespace std;
double wages[3] = { 10000.0, 20000.0, 30000.0 };
short stacks[3] = { 3, 2, 1 };
//两种获得数组地址的方式
double* pw = wages; //数组名就是数组首元素的地址
short* ps = &stacks[0]; //使用地址运算符(&)
//使用数组元素
cout << "pw = " << pw << ",*pw = " << *pw << endl;
pw = pw + 1;
cout << "将 pw + 1:\n";
cout << "pw = " << pw << ",*pw = " << *pw << "\n\n";
cout << "ps = " << ps << ",*ps = " << *ps << endl;
ps = ps + 1;
cout << "将 ps + 1:\n";
cout << "ps = " << ps << ",*ps = " << *ps << "\n\n";
cout << "通过下标访问元素\n";
cout << "stacks[0] = " << stacks[0]
<< ",stacks[1] = " << stacks[1] << endl;
cout << "通过指针访问元素\n";
cout << "*stacks = " << *stacks
<< " ,*(stacks + 1) = " << *(stacks + 1) << endl;
cout << "\n数组wages的大小 = " << sizeof(wages);
cout << "\n指针pw的大小 = " << sizeof(pw) << endl;
return 0;
}
程序执行的结果是:
【分析】
① 在上述代码中,pw是一个double类型的指针,而ps是一个short类型的指针,两者的访问权限不同。因此,pw + 1 → pw的值加8,ps + 1 → ps的值加2。
② 另外,从程序执行的结果可知,数组表达式stack[1] 在C++编译器中等价于 *(stack + 1) 。所以在很多情况下,可以使用相同的方式使用指针名和数组名。数组和指针的区别在一在于:指针是变量,数组名是常量。
③ 并且,在使用关键字sizeof时,C++将不会把数组名解释为地址,因此一个指针和一个数组通过sizeof得到的结果往往是不同的:
ps:尽管数组名经常被解释为其第一个元素的地址,但是,在对数组名使用地址运算符时,得到的地址将被解释为整个数组的地址。
short tell[10]; cout << tell << endl; //将打印数组首元素的地址 cout << &tell << endl; //将打印整个数组的地址
程序执行的结果是:
可知:
- tell是一个short指针(*short),是一个2个字节的内存块的地址;
- &tell是一个short数组的指针(short(*)[20]),是一个20个字节的内存块的地址。
除此之外,形如:
short(*pas)[10] = &tell;
其中,指针pas的类型应该是 short(*)[10] 。之所以会存在这种类型,是因为优先级与结合性的存在:
- 括号内的优先级较高,使pas先于*结合,认为pas内存储的类型应该是一个指针;
- 再与short [10]结合,pas成为一个short类型的指针数组;
指针与字符串
指针与数组的使用方式如此相似,再C-风格的字符串中,同样如此:
#include<iostream>
int main()
{
using namespace std;
char flower[10] = "玫瑰";
cout << flower << " 是红色的。\n";
return 0;
}
程序执行的结果是:
数组名是数组首元素的地址,因此上述cout语句中的 flower 是包含字符'r'的char类型元素的地址。如果为cout对象提供一个字符的地址,cout将从该字符开始打印,直到遇到空字符(\0)。
由上述语句可知,可以将指向char的指针变量作为cout的参数,因为这种指针也是char类型的地址。
除此之外,上述语句还提到了 " 是红色的。\n" 这种字符串常量,其实,这种表达式也代表了其内部字符串的首元素的地址。
在大部分C++表达式中,会被解释为字符串首元素地址的有:
- char数组名;
- char指针;
- 用引号引起来的字符串。
在之前提到的,C语言库提供了许多进行字符串操作的函数,比如:
- strlen( ):它将返回字符串的长度;
- strcpy( ):将字符串从一个位置复制到另一个位置;
- ……
使用例
#include<iostream>
#include<cstring> //包含了函数strlen( ),strcpy( )
int main()
{
using namespace std;
char animal[20] = "bear";
const char* bird = "wren";
char* ps; //此时,ps内部存储信息尚不明了,如果打印,可能出现乱码
cout << animal << " 和 " << bird << "\n"; //输出 指针与 数组 内存储的字符串
cout << "\n请输入一种动物(用英文):";
cin >> animal; //可以输入的字符串长度不超过20个字节
//此时语句 cin >> ps; 是不成立的,因为ps指向的空间仍不确定
ps = animal; //两者将指向相同的内存单元和字符串
cout <<"你输入了:" << ps << "。\n"; //和使用数组animal的效果是一样的
cout << "\n在使用函数strcpy( )之前:\n";
cout << animal << " (animal)的内存地址是 " << (int*)animal << endl;
cout << ps << " (ps)的内存地址是 " << (int*)ps << endl;
//上述语句中的(int*)是强制类型转换
ps = new char[strlen(animal) + 1]; //在堆上为指针ps申请了一块空间
strcpy(ps, animal); //将ps指向的字符串复制到数组animal后面
cout << "\n在使用函数strcpy( )之后:\n";
cout << animal << " (animal)的内存地址是 " << (int*)animal << endl;
cout << ps << " (ps)的内存地址是 " << (int*)ps << endl;
delete[]ps; //归还从堆上开辟的地址
return 0;
}
程序执行的结果是:
【分析】
① 首先是:
const char* bird = "wren"; //认为bird指向一个字符串常量
该语句将char指针初始化并指向了一个字符串字面值(常量),即把"wren"的地址赋给了指针bird,这也是上述语句中出现const的原因。如果没有const,将会触发报错:
(ps:因为被const修饰,所以上述的指针bird实际上是无法修改的)
一般地,编译器会为程序中出现的,由引号引起来的字符串留出一些空间,并且将这些字符串与地址关联起来。
------
② 在上述创建指针ps时,并未对该指针进行初始化,此时在这种情况下输出ps指向的内容,会发生的结果是未知的。(可能是显示空行、也可能是显示乱码、报错或者程序崩溃)
为了避免上述麻烦,可以使用 std::string对象、已分配的内存地址 或者 使用new初始化后的指针。
------
③ 在上述代码中,通过 const char* bird = "wren"; 可知,bird指向一个字符串常量。对于指针bird,一般不建议令其出现在输入语句(如:cin >> bird)中,因为尽管在C++中,字符串字面值被认为是常量:
- 但是,在一些编译器中,字符串字面值仍被认为是 只读常量 ,对于这种常量,程序是无法进行修改的;
- 另外,有些编译器只使用字符串字面值的一个副本来表示程序中出现的该字面值,C++无法保证字符串字面值被唯一地存储。
对于第2点:
如果在出现中多次使用同一个字符串字面值(如:"wren"),编译器可能出现两种处理方式:
一. 存储该字符串的多个副本;
二. 只存储一个副本。
对于第二种情况,指针(如:bird)将会指向该字符串(如:wren)的唯一一个读本。此时使用指针读入字符串,将不止影响一处调用了该字符串的语句。
------
④先看下面的代码:
cout << animal << " (animal)的内存地址是 " << (int*)animal << endl;
cout << ps << " (ps)的内存地址是 " << (int*)ps << endl;
为cout提供指针将会出现两种结果:
- 不是char*,将打印地址;
- 是char*,将打印该指针指向的字符串。
而上述的 animal 与 ps 就是char*类型的指针,为了打印地址,需要通过强制类型转换将它们转换成另一种类型的指针,上述语句选择了 (int*) 。
ps = animal;
上述语句实际上是将 数组animal 指向的地址复制给了 指针ps ,也就是说,它们指向的是同一块内存单元。
------
⑤
ps = new char[strlen(animal) + 1];
strcpy(ps, animal);
上方语句中:
第一句:使用new分配空间,通过函数strlen( )计算数组animal的大小,+1 是为了存储空字符(\0)。
第二句:在④中我们可以得知,如果直接进行 ps = animal; 也只会发生地址的复制,为了进行字符串的复制,需要使用库函数strcpy( ):strcpy(ps, animal); 。
在之前提到strcpy( )时,可以发现该函数有两个参数:第一个是目标地址,第二个是要复制的字符串的地址。要使用该函数,需要注意:目标空间存在,并且空间足够大。
有上述可知,初始化数组可以通过:1. 赋值运算符(=);2. strcpy( )或者strncpy( )。
但是在使用strcpy时可能会出现问题:
#include<iostream> #include<cstring> int main() { using namespace std; char food[10] = { 0 }; //初始化为空 strcpy(food, "Hello world!!"); //"Hello world!!"共13个字符,13 > 10 return 0; }
如果进行调试,在完成strcpy( )所在的语句后,有:
会发现,数组food并没有以空字符(\0)结尾,而通过strcpy( )复制到food内的字符串也是不完整的。
实际上,被复制的字符串"Hello world!!"的剩余字符将会被复制到数组food后面的内存中,这可能会覆盖程序正在被使用的其他内存。
为了避免这种情况,可以使用strncpy( ):
该函数接受的第三个参数num是:要复制的最大字符数。但即使如此,也可能会出现数组内存过早用尽的情况,为了防止报错,可以将数组的最后一个元素主动设置为空字符。
使用new创建动态结构
如果需要在程序运行时为结构分配空间,也可以使用new运算符来完成,即创建动态结构。(由于类与结构非常相似,所以创建动态结构的技术也适用于类)
通过new进行结构创建分为两步:① 创建结构;② 访问其成员。
① 创建结构
创建结构的方式与创建C++内置类型的方式完全相同:
如果new成功完成任务,这就把足以存储structName结构的一块可用内存的地址分配给了指针pointer_name。
②访问成员
通过成员运算符( . )访问动态结构的成员是不可行的,因为动态结构没有名称,只有一个指向该结构的指针(换言之,只知道结构的地址)。
为此,C++提供了 箭头成员运算符(->),这种运算符可用于指向结构的指针,例如:
#include<iostream>
struct things
{
int good;
int bad;
};
int main()
{
using namespace std;
things th_1 = { 123, 321 };
things* pth = &th_1; //指向结构th_1的指针
cout << "通过成员运算符 (.) 访问结构的成员";
cout << "\nth_1.good = " << th_1.good;
cout << "\nth_1.bad = " << th_1.bad;
cout << "\n\n通过箭头成员运算符 (->) 访问结构的成员";
cout << "\npth->good = " << pth->good;
cout << "\npth->bad = " << pth->bad << "\n";
return 0;
}
程序执行的结果是:
除此之外,也可以将结构指针解引用,再进行成员访问,如:(*pth).good 就是结构th_1的good成员。(ps:在对pth进行解引用操作时 有一个括号,这是因为成员运算符的优先级是高于解除引用的,为此需要使用括号来改变优先级)
new与delete的使用例
#include<iostream> #include<cstring> //其实,string.h 也可以 using namespace std; char* getname(); int main() { char* name; name = getname(); //调用函数getname获得字符串的地址 cout << name << " 的地址是 " << (int*)name << "\n\n"; delete[] name; //释放内存 name = getname(); //调用函数getname获得字符串的地址 cout << name << " 的地址是 " << (int*)name << "\n\n"; delete[] name; //再次释放内存 return 0; } char* getname() //返回一个char类型的指针 { char temp[80]; //使用较大的空间进行暂时性存储 cout << "请输入最新的名字:"; cin >> temp; char* pn = new char[strlen(temp) + 1]; //计算字符串的长度,节省空间 strcpy(pn, temp); //将字符串拷贝到一块更小的内存空间内 return pn; //当函数结束时,数组temp的生命周期结束,其占用的空间返回系统 }
程序执行的结果:
【分析】
上述函数getname返回的并不是字符串所在的地址,而是字符串副本的地址。
另一方面,实际上并不建议将new与delete分开到不同的函数内,这样可能导致忘记释放内存。
自动存储、静态存储和动态存储
根据内存分配的方式,C++有3种管理数据内存的方式:自动存储、静态存储和动态存储(也称为自由存储空间或堆)。(C++11还新增了第四种类型——线程存储)
1. 自动存储
在函数内部定义的常规变量使用 自动存储空间 ,被称为自动变量(一种局部变量)。这种变量在其所属函数被调用时自动产生,其生命周期仅存在于所属函数内部,当函数结束时,变量消亡。
例如:
char* getname()
{
char temp[80]; //数组temp是一个自动变量
cout << "请输入最新的名字:";
cin >> temp;
char* pn = new char[strlen(temp) + 1];
strcpy(pn, temp);
return pn;
}
在上述的代码中,数组temp仅当函数getname( )活动时存在,当程序控制权重新回到主函数main( )时,temp使用的内存将被自动释放。
自动变量通常被存储在栈中:
这意味着,当执行代码块时,其中的变量将会以此入栈;当离开代码块时,变量将会出栈(先进后出)。所以,在执行程序的过程中,栈将不断增大或缩小。
2. 静态存储
静态存储:在整个程序执行期间都存在的存储方式。
使变量成为静态的方法:
- 在函数外定义变量;
- 在声明函数时,使用关键字static。如:
static int a = 100;
注:
在 K&R C 中,只能初始化静态数组和静态结构;
在C++Release 2.0(及后继版本) 或 ANSI C 中,还可用初始化自动数组和自动结构。
3. 动态存储
这种存储方式由new与delete提供。这两个关键字管理了一个内存池 —— 自由存储空间 / 堆。
该内存池与静态变量和自动变量使用的内存是分开的。
因为通过 new 开辟的空间只能通过 delete 释放,所以使用这种存储方式的内存,其生命周期不完全受程序或函数的生存时间控制。这种存储方式使程序员对内存拥有了更大的控制权,但相对的,内存也更难管理。
在栈中,因为自动添加与删除的缘故,其使用的内存往往是连续的。但在堆中,new 和 delete 的互相影响可能导致自由存储区不连续,更难跟踪新分配的内存位置。
内存泄漏
之前也提到过,如果通过new开辟的空间没有通过delete进行回收,当指向被开辟空间的指针因为生命周期或者作用域规则而被释放后,就无法再通过指针(即地址)找到被new开辟的空间。
如果内存泄漏发生,那么被泄漏的内存在程序的整个生命周期内将无法被再次使用。如果内存泄漏严重,导致应用程序可用的内存被耗尽,就会发生内存耗尽成为,导致程序崩溃,甚至,可能影响操作系统或者其他的程序。