第十五篇 -- 研究下指针与内存
学习C++宝典中的内容
一、程序中内存的分配方式
在C++程序中内存分为5个区:栈、堆、自由存储区、全局/静态存储区和常量存储区。
栈:栈区由编译器自动分配和释放,存放函数的参数以及局部变量。其分配运算内置于处理器的指令集中,效率很高。但是可使用的总量有限,一般不会超过1M字节。
堆:堆区中内存的分配和释放由开发者负责。一般用运算符new分配内存,并用运算符delete释放内存。一个new 要对应一个delete,否则会导致内存泄露。如果开发者没有释放,在程序结束的时候操作系统会自动回收。在堆上可分配的内存比栈上大了很多,且使用非常灵活。
自由存储区:和堆类似,但是其内存管理是通过库函数malloc和free等进行的。在C程序中经常使用,虽然在C++程序中仍然可以使用,但不如用堆方便。
全局/静态存储区:存放的是全局变量和静态变量。该存储区分配的内存在这个程序运行期间一直有效,直到程序结束由系统回收。
常量存储区:存储的是常量,通常不允许修改。在程序中定义的常量以及指针字符串都存储在这里。
例1:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
int a = 0; //全局变量,存储在全局/静态存储区 void Storage_study() { std::cout << "\nThis is Storage_study() function\n"; int b; //局部变量,存储在栈上 int *p = new int(); //由运算符new分配的,存储在堆上 static int d; //静态变量,存储在全局/静态存储区 const int e = 0; //常量,存储在常量存储区 delete p; //释放堆中的内存 std::cout << "\n===================Storage_study() end===================\n"; }
C++程序中的内存都要从上面的五个区中分配。不过栈、全局/静态存储区以及常量存储区中的分配是由编译器来进行的,并且在程序运行之前已经分配,因此称之为静态分配;堆以及自由存储区上内存的分配,是在程序运行的过程中进行的,称之为动态内存分配
说明:只有在堆上和自由存储区中分配的内存需要开发者管理。其他存储区中的变量只要定义即可,其内存的分配和释放由编译器负责。
二、在堆上分配内存
在堆上分配内存,要用new关键字,后面跟一个数据类型,为了保存分配出内存的地址,应当使用一个指针指向new的结果int *p = new int;。如果需要对分配的内存进行初始化,则在类型后面加一个括号,并带有初始值。如:int *p = new int(3);
对于不提供显式初始化的情况,如果new的类型是类,则会调用该类的默认构造函数;如果是内部数据类型,则不会被初始化。
用new不但可以为一个变量分配内存,还可以为一个数组分配内存,其方法是在new的类型标识符后面跟一个中括号,其中是数组的长度。如:int *pArr = new int[10];
注意:同数组的定义不同,用运算符new为数组分配内存空间时,其长度可以是变量,而不必是常量。因为动态内存分配在程序运行的时候才分配空间,所以当用new在堆上定义数组的时候,其长度可以是一个变量。
例:
int n = 10;
int *parr = new int[n]; //可以用变量作为动态生成的数组的长度
因此当数组的长度不确定的时候,一般都要使用new的方法定义数组。new数组也有其缺点,那就是不能提供显式初始化值。
三、释放堆上的内存
通过new分配的内存必须由开发者自己去释放,一块内存如果没有被释放,则会一直存在到该应用程序结束。在C++中使用delete来释放内存。对于单个内存,释放语法为:delete 指针名。对于数组,释放语法为:delete []指针名。
如:
int *p = new int;
delete p;
int *pArr = new int [10];
delete []pArr;
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
void release_hand() { std::cout << "\nThis is release_hand() function\n"; int *pCount = new int; std::cout << "Please input the length of the array : " << std::endl; std::cin >> *pCount; std::cout << "Please input the value of the array : " << std::endl; int *pArray = new int[*pCount]; for (int i = 0; i < *pCount; i++) { std::cin >> pArray[i]; } std::cout << "Array's value as below : \n" << std::endl; for (int j = 0; j < *pCount; j++) { std::cout << "pArray[" << j << "] = " << pArray[j] << "\t"; } std::cout << std::endl; delete pCount; delete pArray; std::cout << "\n===================release_hand() end===================\n"; }
上述代码中,因为数组的长度依赖于输入,是个变量,因此需要用new的方法。因为int是C++的内部数据类型,因此[]可以忽略。
当心:new 分配的内存,都需要显式的delete。如果缺少了delete,则会造成内存泄露。含有这种错误的代码每被调用一次就丢失一块内存。也许刚开始时系统的内存充足,其影响还不明显,等到程序运行足够长的时间后,系统内存就会被耗尽,程序也就会崩溃。
但是对于不是用new分配的内存地址,则在其上使用delete是不合法的。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
int i; //局部变量 int *pi = &i; //指针指向变量 string str = "abcdef"; //字符串指针 double *pd = new double(33); //通过new分配的内存 delete str; //错误,str不是一个动态创建的对象 delete pi; //错误,pi指向一个实例变量 delete pd; //正确
对于上面两个错误,编译器能检查出对str的删除是错误的,因为编译器知道str并不是一个指针类型。但是对于第二个错误,编译器并不能断定一个指针指向什么类型的对象,不能在编译时检查出来。因此在程序中new和delete出现的次数必须相同,而且对于一块使用new分配的内存,只能delete一次,否则也会造成系统错误。
四、const与指针
const是一个常量修饰符。使用const修饰一个变量时,将使其值不可改变,从而变成一个常量。const也可以在定义指针时使用,不过此时const不仅可以修饰指针变量,也可以修饰指针所指向的数据。
1. 指向const的指针
指向const的指针,指的是一个指针指向的数据是常量,不可以被修改,但指针变量本身可以被修改。其定义格式是在普通指针定义前加上const关键字。如:const int *p;
在上面的定义中标识符p前面的“*”表明p是一个指针,int表明p是一个指向整形数据的指针,而const则表明p所指的数据是一个常量,不能被修改。严格来讲是不能通过指针p间接修改这个数据的,例如:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
int a = 0; //定义变量 const int * pa; //定义一个const指针 pa = &a; //可以改变指针指向的地址 *pa = 1; //错误,不能通过该const指针修改其值 a = 2; //a是变量,可以修改
在上面的代码中,试图对*pa的修改是错误的,因为pa是一个指向const的指针。但是a本身还是一个变量,因此可以直接对a进行修改。因此,指向const的指针,这个const限制的是通过指针来修改变量的权限,而不是修改变量的权限,该变量还是可以被修改的。对于符号常量,即用const修饰的变量,只能用指向const修饰的变量,只能用指向const的指针来指向,而不能用普通的指针。例如:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
const int a = 10; //const 变量 int *p1 = &a; //错误 const int *p2 = &a; //正确
指向const的指针,只限制了指向变量不可以被修改,但是指针本身的指向是可以修改的:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
int a; int b; const int *p = &a; p = &b; //修改指针的指向
提示:在函数定义中,经常用指针来代替数组,对于只读的数组,经常用指向const的指针来限制。
2.const指针
const指针,指的是指针变量本身是一个常量,只能指向定义时所给的那个数据,而不能指向别处,而对被指向的数据是没有影响的。其定义语法是在指针名前加上const关键字,而不是在最前面。如:int a; int * const p = &a;
当const指针定义之后,可以修改其指向变量的值。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
int a; int * const p = &a; *p = 1; //正确,修改其指向的值 int b; p = &b; //错误,不可以修改其指向
因为不能修改const指针的指向,所以在定义的时候必须给一个初始值。下面的定义是错误的:
int * const p;
3. 指向const的const指针
上面讲述了const在两个位置的不同效果,const还可以同时出现在这两个位置,其定义格式为:const 类型名 * const 指针名;
这是前面两种情况的组合,其指向的变量和指向都不可以被修改,例如:
int a = 0; //定义一个变量
const int * const p = &a; //指向const的const指针
对于这个指针p,下面两种情况的修改都是错误的:
*p = 1; //错误
int b;
p = &b; //错误
p表示一个指向const的指针,因此既不能修改指针的指向,也不能修改指针指向地址的变量。
三种const指针的定义很相似,很难以区分,这里有一个简单的技巧:从标识符的开始处读它,并从里向外读,const靠近那个“最靠近的”。因此根据这个理解,第一个定义表示int不可以被改变,第二个表示p不可以被改变,第三个表示int和p都不可以被改变。
五、引用
虽然指针的使用非常灵活和高效,但使用起来却不是非常方便。如果使用不当,很容易导致某些不易察觉的错误。为此,C++引入了引用。
1. 定义引用
引用也是一种数据类型。不过,引用不能独立存在,而只能依附于一个变量。所以定义一个引用,必须要指明是哪个变量的引用。定义一个引用包括目标变量的数据类型、引用修饰符“&”、引用的标识符以及目标变量的标识符,其语法如下:
类型标识符 &引用名 = 目标变量名;
其中类型标识符是目标变量的类型。“&”是引用修饰符,表示定义的是一个引用。而被引用的变量则通过赋值运算符指定。例如:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
int a; //定义一个变量 int &b = a; //定义一个上述变量的引用
说明:“&”在这里不是取地址运算符,而是一个引用修饰符。
引用一旦定义,则始终跟其目标变量绑定,而不能改变为其他变量的引用。假设b是变量a的引用,则在b的生命周期内,b始终都是a的引用,而不能再改变为其他变量的引用。另外,引用在其生命周期内完全可以替代其目标变量。也就是说,所有施加于引用上的操作,其效果都等同于直接对引用的目标变量进行操作。而且一旦目标变量的值发生了改变,引用的值也会发生同样的变化。
鉴于引用的不可变更性,以及引用与目标变量的等价性,一个变量的引用也可以看做该变量的别名。定义一个引用只不过是给变量另外起了一个名字。这样两个名字拥有一个实体,对一个名字的操作自然也会影响到另外一个名字。
提示:引用的上述行为和特性类似于const指针。首先,const指针也是定义后不能在指向别的变量的;其次,通过const指针可以间接地修改其目标变量,而且对目标变量的修改也会影响const指针。
2. 常引用
定义引用时也可以用const进行修饰,其结果为一个常引用。其语法如下:
const 类型标识符 &引用名 = 目标变量名;
例如:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
int a; //定义一个变量 const int &b = a; //定义一个上述变量的引用 a = 1; //正确,可以修饰变量 b = 2; //错误,不可以通过常饮用修改变量
上面语句为a定义了一个const的引用b,这样,就可以限制通过b来修改目标变量a,相当于给了这个别名只读不写的权限。
一般的引用在定义时必须有一个已经存在的变量,而常引用则没有这样的限制,可以定义一个字面常量的常引用。例如:const int &a = 2;
当心:对于符号常量,即被const修饰的变量,其对应的引用必为常引用。否则对于一个变量实体,其一个名字显示不可修改,而另一个名字显示可以修改,这样显然是矛盾的。
六、引用和指针的区别
引用是C++特有的新类型(与C相比)。在很多情况下,引用提供了与指针操作同等的功能。但是引用和指针还是有一些区别的,在使用时应当根据实际情况进行选择。例如:
1. 引用必须与一个变量绑定,不存在没有任何目标的引用。因此如果在使用的过程中有可能出现什么都不指向的情况下,则应该使用指针,可以把一个空值给指针。而若一个变量肯定指向某个对象,不允许为空,则可以使用引用。
2. 引用仅仅是一个变量实体的别名。因此在使用引用之前,不需要检测其合法性。但指针在使用之前必须检测其指向的对象是否为空,不能对空指针进行取值操作。
3. 指针可以重新赋值以重新指向新的对象,引用在初始化之后就不可以改变指向对象了。
4. 指针可以指向数组的地址来替代数组使用,而引用不可以代替数组,引用只能指向数组中的某一个元素。
5. 指针可以用于在堆上生成的对象,delete的对象只能是一个指针,不能是引用。
一个简单指针指向数组的应用--冒泡排序
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include "pch.h" #include <iostream> using namespace std; #define len 10 //define array length equal 10 //function prototype void Method_one(); void Method_two(); void compare(int &a, int &b); int break_node = 0;//when break_node is true, jump the loop int main() { //Bubble sort //Method_one(); Method_two(); } void Method_one() { //define an array which length is 10 int array_sort[len]; //input array value cout << "Please input " << len << " number: " << endl; for (int i = 0; i < len; i++) cin >> array_sort[i]; //start sort for (int m = len; m > 0; m--) { for (int j = 0; j < m - 1; j++) { compare(array_sort[j], array_sort[j + 1]); } if (break_node) { break_node = 0; } else {//while no change data in inner loop, jump out the external loop break; } } //output sorted array(high-->low) cout << "Sorted array: " << endl; for (int k = 0; k < len; k++) { cout << array_sort[k] << " "; } cout << endl; } void Method_two() { int length = 0; cout << "Please input the length of array : " << endl; cin >> length; cout << "Please input " << length << " values" << endl; int *pArr = new int[length]; //input array value for (int i = 0; i < length; i++) cin >> pArr[i]; //start sort for (int m = length; m > 0; m--) { for (int j = 0; j < m - 1; j++) { compare(pArr[j], pArr[j + 1]); } if (break_node) { break_node = 0; } else {//while no change data in inner loop, jump out the external loop break; } } //output sorted array(high-->low) cout << "Sorted array: " << endl; for (int k = 0; k < length; k++) { cout << pArr[k] << " "; } cout << endl; delete pArr; } void compare(int &a, int &b) { if (a >= b); else { int temp; temp = a; a = b; b = temp; break_node++; } }