C++/C入门之指针与引用

把指针和引用放在一起说,不表示指针和引用很相似,难以区分,实际上他们的共同点非常少。指针和引用都跟内存中的某块地址绑定在一起。正是因为这个共同点,初学者常常把引用等同于指针。这是灾难性的。

(一)从概念上区分指针和引用

指针可以理解为一种特殊的数据对象。一般的变量值,是由一系列比特位表示的数字或字符。而指针的变量值,当然也是一个长整型数值,但这个长整型值有特殊的涵义,它表示内存块的地址。

如果是非const的指针,它的值是可以变化的。

引用是C++引入的概念,C里面没有。它是一个变量别名。怎么理解?引用初始化的时候,赋给该引用的地址,必须是用过的,即跟一个已知变量绑定的。也就是说,不能给赋给引用没使用过的内存,引用的值也不能为NULL。也就是说,至始至终,引用只是一个已知变量的别名。它不独立存在。在编译器的符号表中,变量本名和别名指向同一地址。

(二)指针和变量的行为差别很大

理解了指针和引用的概念,也就不难理解以下它们的差别。

  1. 指针定义的时候不必初始化,可以是NULL值。引用则都不可以。

  2. 指针的值是可变的。引用一经初始化,它的值是不可更改的。注意,一旦定义了一个引用,访问它和访问变量名本身的方式和效果是一致的。这是理解引用的关键。如:

    int x = 0; int& a = x;

    执行a++,x和a的值都是1。执行x++效果是一样的。

    如果定义 int y = 0; 并让 a = y; 效果是a和x的值变成0,并不是引用a跟变量y的地址绑定。同样的规则对类,结构和数组都适用。

    相应的,如果定义了一个指针p,执行p++,其涵义完全不同。C++对指针的自增运算符做了重载。

  3. 常量指针和指向常量的指针都是C++支持的。由于引用生来就是常量,所以不存在常量引用的说法。但是绑定到常量的引用是有意义的。

  4. 引用的大小由对应的对象大小决定。指针的大小由操作系统的位数来决定。32位操作系统一般4个字节。64位操作系统一般是8个字节。

  5. 引用是类型安全的,而指针不是。后面有会一章专门来介绍指针。

(三)为什么要引入引用的概念?

没有引入引用之前,C中的函数调用,如果要保存更改到输入参数,只能通过指针来实现。这样做,有两个劣势。其一,不够直观。其二,涉及到对象传递,效率低下。下面结合一个引用的例子,来说明引用作为地址参数传递,是如何克服这两缺点的。试看:

#include <iostream>

using namespace std;

void exchange(int& x,int& y){

int temp = x;

x = y;

y = temp;

}

int main(){

int x=1,y=2;

exchange(x,y);

cout << \”x=\” << x <<endl;

cout << \”y=\” << y <<endl;

}

函数exchange交换引用参数x和y的值。调用函数时,直接赋予对应类型的变量为参数即可,没有多余的*或者&,地址的概念被隐藏了。x和y的值最后被交换过来了,很直观吧。

关于效率,如果是传指针给函数,按传值模式来处理函数调用。也就是说,函数要给对应的实参(指针),生成一个副本并入栈,如果指针指向的对象较大,则开销会比较大。如果是传引用给函数,函数被调用的时候,简单地把该地址入栈,读取时,直接按该地址寻址即可,对象的大小就无关痛痒了。

(四)引用的其它用途

由于引用在函数调用时的优势,C++对于运算符的重载,拷贝构造函数的实现,都依赖于引用。这两个概念,后面章节也会介绍到。

C\\C++|指针详述及实例分析

指针是C语言中的精华,也是一把双刃剑,关系到安全和效率。

1 系统内存布局

2 存储变量的内存地址

3 指针定义:变量,地址,类型(宽度)

4 指针声明

5 &与*运算符

6 定义指针与解引用

7 指针初始化

8 指针指向类型长度计算:sizeof(*p)

9 void* 类型指针

10 指针应用:判断系统大小端

11 指针加减运算

12 常量指针与指针常量

13 数组名是一const指针

14 指针与数组关系

15 字符指针

16 二级指针

17 函数指针与指针函数

18 数组指针与指针数组

19 函数不要返回局部变量的指针

20 指针与引用

21 指针引用做函数参数

22 返回指针和指针引用

23 指针使用注意事项

指针其实就是一个变量,和其他类型的变量一样,在32位机器上,它占用四字节(64位上占8个字节),它与其他变量的不同就在于它的值是一个内存地址,指向内存的另外一个地方。

以X86的32位系统为例,如下图所示,系统的内存虚拟地址范围为4GB(0x0-0xFFFFFFFF)。 其中低2GB主要为应用程序使用(Ring3级别),而高2GB为系统内核使用(Ring0级别):

需要注意的是,程序在执行时,传递给CPU的地址是逻辑地址,它由两部分组成,一部分是段选择符(比如cs和ds等段寄存器的值), 另一部分为有效地址(即偏移量,比如eip寄存器的值)。逻辑地址必须经过映射转换变为线性地址, 线性地址再经过一次映射转为物理地址,才能访问真正的物理内存。

变量是存放在内存中的,比如下图中的变量i和a,分别对应一块内存单元首地址的一个命名,变量i和a的地址,可以用&取址运算符获得。

而对于指针p来说,它本身也是一个变量,存放在内存中,只不过它的值是一个内存地址,这个内存地址,可以是其它变量的地址。

如:

如上图所示,内存地址空间是一个线性空间。

数据类型除了表示可以执行的操作(可以使用的运算符)、编码与解码格式以外,还用来表示需要内存空间的长度,如上面的int就是使用4个字节的内存空间,变量a是这个内存空间地址的命名,在C/C++,变量名做右值取得的是其内存空间的二进制位组成的值按数据类型编码规则解析出来的值(变量名做左值可以更新其内存空间的值),其地址可以通过“&\”加变量解析出来。int*p表示p是指向一个int大小的内存空间。

内存的地址可以分为有效地址,即这个所对应的内存是可访问的;还有无效地址,访问无效地址,会导致程序崩溃,比如NULL地址就是一个无效地址。

指针其实就是一个变量,和其他类型的变量一样。它与其他变量的不同就在于它的值是一个内存地址,指向内存的某一个地方。即指针是一种存放另一个变量的地址的变量。

指针含义可以分为3个方面来理解:

I 它是一个变量,所以也占用一定的内存空间(在X86上占用4个字节,X64上占用8个字节)

II 它的值是一个内存地址。这个地址可以是其它变量的地址。

III 它的地址指向的内存空间具有确定的长度。这是指针与地址的本质区别。如果只告诉你一个内存地址,你不会知道从这个地址开始的内存有多长。但如果告诉你一个指针,你会明确的知道从这个内存地址开始的内存有多长。因为指针都是有类型的。知道了指针的类型,就确定了所指向的内存地址对应的长度。

指针声明如同变量声明一样,只是多了一个“*”:

当然需要类型,表示这个地址可以解析的内存空间长度。

其右值只能是一个有效的内存地址:

因为变量是内存地址的一个可以理解的命名标识,这个标识的直接使用是值的访问与更新。通过这个标识我们可以取得其地址,C/C++中,可以使用“&”(取址运算符)来获取某个变量的地址,比如:

*p中,p必须是有效的地址,否则会引发程序崩溃。比如:

int *p = NULL;

*p = 0;//此时,p无NULL地址,会引发程序异常

在编程语言中,许多情况下一个符号会有多种用途,取决于上下文,如“*”即可用于定义指针,又可用于解引用:

“*”既是指针声明符,又是解引用(dereference)运算符,与&运算符互为逆运算。

一个小技巧区分“*”做为指针声明与解引用的上下文:

“*“写在数据类型之后时表示声明指针变量,“*“前没有数据类型时表示解引用。

当定义了一个指针,有多种方法进行初始化(赋值):

① 声明与赋值分开进行

② 声明与赋值分开进行并先赋NULL值

③ 声明与赋值同时进行,也就是声明即初始化

④ 声明并指向堆空间

用于指针初始化的右值的取值空间为:0x0000FFFF-ox7FFF0000,这个值不能由程序员直接给出,因为操作系统统一管理各程序的内存空间,程序中使用的是虚拟内存并通过虚拟内存地址来访问数据和代码的,操作系统再将虚拟内存地址映射成为实际的物理内存的,所以合法的地址只能是通过定义变量或由此产生的偏移来获取,可直接通过malloc()或new申请动态内存来获取。

指针的长度(在32位机器系统上)为4。

当使用sizeof()和数组名(常量指针)求数组的长度时,求的是这个数组整个空间的长度,等于元素个数乘以单个元素大小,字符串数组的长度必须包含字符串的结束标志符’\\0’。(关于数组名做函数参数时退化为指针的细节,见后续)

char *p1 = “Hello, word!” // 字面量 “Hello, word!”存储在静态区.rdata段

p1为字符串指针,所以sizeof (p1) = 4。

char p2[] = “Hello, world”

p2为字符数组并初始化为”Hello, world”。由于字符串的存储特点,总是以’\\0’做为结束标志,因此上面的字符串等价于下面的数组:char p2[] = {‘h’, ‘e’, ‘l’,’l’,’o’, ‘ ‘, ‘w’,’o’,’r’,’l’,’d’,’\\0’},必须包含字符串的结束标志符’\\0’,所以sizeof (p2) = 13。

char p3[] = {‘h’, ‘e’, ‘l’,’l’,’o’, ‘ ‘, ‘w’,’o’,’r’,’l’,’d’}

p3为字符数组,并由12个字符初始化,所以sizeof (p3) = 12。

注意,strlen(p)计算的是字符串中有效的字符数(不含’\\0’)。所以strlen(p)的值为12。考察下面拷贝字符串的代码,看看有什么问题没呢?

显然,由于strlen()计算的不是str的实际长度(即不包含’\\0’字符的计算),所以strbak没有结束符’\\0’,而在C语言中,’\\0’是字符串的结束标志,所以是必须加上的,否则会造成字符串的溢出。所以上面的代码第二句应该是:

char *strbak = (char *)malloc(strlen(str)+1);

既然在这里谈到了sizeof,现在我们就把sizeof运算在下面做一个系统的总结:

1)参数为数据类型或者为一般变量

例如sizeof(int),sizeof(double)等等。这种情况要注意的是不同系统或者不同编译器得到的结果可能是不同的。例如int类型在16位系统中占2个字节,在32位系统中占4个字节。

2)参数为数组或指针

int a[50]; //sizeof(a)=4*50=200; 数组所占的空间大小为200字节。

注意数组做函数参数时,在函数体内计算该数组参数则等同于计算指针的长度。

int *a=new int[50]; // sizeof(a)=4; a为一个指针,sizeof(a)是求指针的大小,在32位系统中,当然是占4个字节。

3)参数为结构或类。

sizeof应用在类和结构的处理情况是相同的。有两点需要注意,第一、结构或者类中的静态成员不对结构或者类的大小产生影响,因为静态变量的存储位置与结构或者类的实例地址无关。第二、没有成员变量的结构或类的大小为1,因为必须保证结构或类的每一个实例在内存中都有唯一的地址。关于更多的结构的sizeof大小计算,请考虑数据对齐。

我们可以使用void*来定义个void *类型的指针:

void *p;

p是void *类型指针,其他类型指针隐式转换成该类型,不能直接使用*p来取值,必须先转换为特定类型再做取值

p可以接受任何类型的指针赋值。

p赋值给其它类型的指针,需要强转。

p不能进行解引用*运算,必须先转换。

比如:

一定条件下,void*可以让数据类型有一定的泛型特征,因为C/C++是强类型语言,如标准库中的很多函数便以void*为参数:

对于int x=0x1; 在小端系统中,低位存放整数的低位,因此低地址的第一个字节的值为01。而大端系统中,低位存放整数的高位,因此低地址的第一个字节为00。如果把这个内存地址所在的第一个字节取出来,就可以区别系统是小端还是大端了。

指针运算一般只有算术和有限的比较运算。通过一个指针与一个整数的加减,形成的内存地址变化或偏移可以视为指针的移动。

指针移动,最常用的就是“++”运算符了,下述表达式注意与解引用区分:

如以下利用指针移动和两个指针的差值求字符串长度的函数:

对于链表,其移动稍有区别:

当const修饰指针声明时,根据const的位置不同,const可以修饰指针本身,也可以修饰指针指向的内容:

1)const int *a; // 指针常量,指针指向的变量不能改变值

2)int const *a; // 指针常量,与const int *a等价

3)int * const a; // 常量指针,指针本身不能改变值

4)const int * const a; // 两者均为常量

小技巧:以“*”为分界,const靠近哪部分,哪部分为常量

数组名所代表的值就是数组的首地址,一旦定义了数组之后,数组名所代表的值就不能再改变。从指针的角度来看,数组名就是一个常量指针,比如:

int a[10];

那么a就是一个常量指针,即:int *const a。因此,不能再用其它的值赋值给a。因为a是常量。

在计算数组长度的时候,我们需要注意数组作为函数的参数,将退化为指针,所以,其长度大小为指针的长度。现在我们来看下面这段代码:

下面来看以下有问题的代码:

分析:函数内的sizeof有问题。根据语法,sizeof如用于数组,只能测出静态数组的大小,无法检测动态分配的或外部数组大小。函数外的str是一个静态定义的数组,因此其大小为6,函数内的str实际只是一个指向字符串的指针,没有任何额外的与数组相关的信息,因此sizeof作用于上只将其当指针看,一个指针为4个字节,因此返回4。

数组名虽然代表了数组的首地址,虽然a与&a的值一样,都是数组的首地址,但是,a与&a在特定的上下文中的含义并不一样。对于一维数组来说:

int a[10];

&a+1中的1代表的是整个数组的长度10*sizeof(int);

a+1中的1代表的是一个元素的长度sizeof(int)。

&a[0]+1中的1也代表的是一个元素的长度。

再看下面实例:

前面已经提到,指针加减法运算,后面的数字表示指针指向的数据类型的大小的倍数。比如&a+1,其中的1就表示指针向前移动1*sizeof(&a)那么多的字节。而&a表示整个数组,所以ptr1 = (int *)(&a+1),ptr1指到了数组的末尾位置。因为ptr1[-1]即为*((int*)ptr1-1),即指针ptr1向低地址移动sizeof(int)个字节,即向后移动4个字节,正好指到a[4]的位置,所以ptr1[-1]为5。

对于语句*ptr2 =(int *)((int)a+1),在这里,我们已经将指针a(地址值)强制转换成了整型,a+1不是指针运算了。(int *)((int)a+1)指向了首地址的下一个字节,如上图所示。

所以,*ptr2所代表的整数(四个字节,小端存储),我们从上图可以看出是:2000000。

对于多维数组来说:

int a[5][10];

a和&a都是数组a[5][10]的首地址。那么它们有什么不同呢?实际上,它们代表的类型不同。a是int a[10]的类型,而&a则是a[5][10]的类型。大家知道,指针运算中的“1”代表的是指针类型的长度。所以a+1和&a+1中的1代表的长度分别为a的类型a[10]即sizeof (int) * 10 和&a的类型a[5][10]即sizeof (int)*10*5。

看下面实例:

大家知道,指针运算中的“1”代表的是指针类型的长度。所以a+1和&a+1中的1代表的长度分别为a的类型a[10],即sizeof (int) * 10 。

&a的类型a[5][10]即sizeof (int)*10*5。

更抽象点的说,如果定义一个数组int a[M1][M2][…][Mn],那么a + 1 = a首地址+M2*M3*…*Mn *sizeof (int);而&a + 1 = a首地址 + M1*M2*…*Mn*sizeof (int)。

现在大家已经明白,数组名,其实是一个常量指针:

int a[10];

a的类型为:int * const a;//a是常量指针

因此在访问数组元素的时候:

a[i], 与*(a+i)都可以访问第i个元素的值。而&a[i]与a+i都是第i个元素的地址。同样,我们也可以定义一个整数指针pa指向数组的首地址:

int *pa=&a[0];

int *pa=a;

因此pa+i也是第i个元素的地址,而*(pa+i)和pa[i]引用的也是a[i]的值。

字符指针的定义是:

char *p;

字符指针,既可以指向字符变量,也可以指向字符串(其实就是字符串中首字符的地址)。比如:

char *str=“hello world”;// 这里str是一个字符指针,它是”hello world”字符串中首字符’h’的地址。

因为字符串是以’\\0’结尾的,所以可以通过字符指针来遍历字符串:

字符指针也可以指向某个字符变量,比如:

C的字符串用一个以\’\\0\’结尾的字符数组来表示。注意以下初始化方式的细微区别:

所谓二级指针,就是指向指针的指针,即该指针的值是另外一个一级指针的地址。与此类似,如果一个指针中存放的是二级指针的地址,那么该指针就是三级指针,与此类推。

如上图所示,pch是一级指针,存放着变量c的地址;ppch是二级指针,存放这一级指针pch的地址。只要画出了上面的关系图,那么一次*运算,就是向右移动一次,两次*运算,就是往右移动两次,即*pch即为c,*ppch为pch,**ppch即为c。

函数指针是指指向一个函数的指针,指针函数是指返回一个指针的函数。

17.1 函数指针

函数名,就是函数的首地址。如果一个指针变量,存放的是函数的地址,那么就把这个指针叫做函数指针。定义函数指针有2中形式:

第一种,首先用typdef定义出函数指针的类型,然后,通过函数指针类型来定义函数指针。

第二种,直接用函数的签名来定义函数指针。

声明函数指针,通常的做法是,先声明一个函数,然后将函数名改为函数指针名,再加一个“*”号,用括号括起来即可。

函数指针通常用做函数参数:

17.2 指针函数

指针函数即返回指针的函数。比如下面的代码中,我们尝试着调用get_memory()获取一个内存,用来存放“hello world“这个字符串,那么就可以将get_memory()设置成为一个返回指针的函数:

注意:指针函数不能返回局部变量的指针(地址),只能返回堆上内存的地址,或者函数参数中的内存地址以及全局变量或静态变量的地址。因为局部变量存放在栈上,当函数运行结束后,局部变量就被销毁了,这个时候返回一个被销毁的变量的地址,调用者得到的就是一个野指针。

与“指针数组”和“数组指针”类似的有“函数指针”与“指针函数”,“常量指针”与“指针常量”。这些概念都符是偏正关系,所以指针数组其实就是数组,里面存放的是指针;数组指针就是指针,这个指针指向的是数组;

函数指针就是指针,这个指针指向的是函数,指针函数就是函数,这个函数返回的是指针;常量指针就是指针,只不过这个指针是常量的,不能再修改值指向别的地方;指针常量,就是指指针本身不是常量指针指向的内存是常量,不能修改。

理解上述声明的含义,关键是要明白[],*,和()(这里的()不是函数声明的90)运算符的优先级:() > [] > *。比如int *a[10],由于[]的运算级别高于*。掌握了这一点,再按下面的思路去分析就行了:

I 找括号(函数参数的括号通常写在最后,除外),做为核心,其它部分是修饰;如char* (*pf) (int i); 核心是指针,其它是修饰,所以是函数指针。

II 优先级高的是核心,其它是修饰,如char* arr[12]; 核心是数组,指针修饰数组,所以是指针数组。

所以现在来分析int (*a[10])(int);就简单了, 核心是*a[3],然后是[3],是一个数组,一个指针数组,一个函数指针数组。

函数一定不要返回局部变量的指针或者引用。如下面的代码:

在func函数中,我们将局部变量c的地址当做一个指针返回,那么在main函数中,我们是不能够再次使用或者访问这个指针所指的内存的。因为局部变量c的生命周期只存在于函数func运行期间。一旦func结束运行之后,那么c就被销毁了,c的地址就是一个无效的内存地址,因此,当在main函数中执行了:

pc=func() ;

pc指向的内存是无效的内存,因此pc是一个野指针,试图访问一个野指针,其后果是未定义的,程序有可能崩溃,有可能访问的是垃圾值。

引用是一种没有指针语法的指针,与指针一样,引用提供对对象的间接访问。引用为所指对象的一个别名(alisas)。如下面的例子:

引用必须初始化,而指针没有这个要求(尽管没有初始化的指针很危险);引用总是指向它最初获得的那个对象,而指针可以被重新赋值。

引用可以理解为由编译器实现了自动解引用的const指针。

如果是主调函数给被调函数传递指针,那么会先复制该指针,在函数内部使用的是复制后的指针,这个指针与原来的指针指向相同的地址,如果在函数内部将复制后的指针指向了另外的新的对象,那么不会影响原有的指针。所以要想在函数中改变指针,必须传递指针的指针或者指针的引用。

使用对象指针作为函数参数要比使用对象作函数参数更普遍一些。因为使用对象指针作函数参数有如下两点好处:

1)实现传址调用。可在被调用函数中改变调用函数的参数对象的值,实现函数之间的信息传递。

2)使用对象指针实参仅将对象的地址值传给形参,而不进行副本的拷贝,这样可以提高运行效率,减少时空开销。

使用对象引用作函数参数要比使用对象指针作函数参数更普遍,这是因为使用对象引用作函数参数具有用对象指针作函数参数的优点,而用对象引用作函数参数将更简单,更直接(无须在函数体内解引用即是对主调函数实参的操作)。

在C语言中经常使用指针,指针的指针,指针的引用做函数的参数。那么它们的区别是什么呢?

1)指针做参数:

void func( MyClass *pBuildingElement );// 指针,不能修改指针本身

通常要求实参是一个变量的地址,函数体内通过操作*pBuildingElement来改变pBuildingElement指向的值。而如果实参是一个指针,函数体内操作pBuildingElement,通常不是函数设计的初衷,没有意义。

2)指针的指针做参数:

void func( MyClass **pBuildingElement );//指针的指针,能修改指针

通常在函数体内操作的是*pBuildingElement

3)指针引用做参数:

void func(MyClass *&pBuildingElement ); // 指针的引用,能修改指针

pBuildingElemen是MyClass*的别名,既是传址,又因为是引用(实现了自动解引用),函数体内pBuildingElemen与MyClass*两者的运算是一致的。

对于一个返回动态内存的函数,另外一种方式就是利用指针函数返回。

对于引用,用做参数时,结合了指针和变量的性质,实参和形参结合时,是传址的特性,引用用在函数体时,无需解引用即是对主调函数的实参的操作。

看下面实例:

返回指针和返回引用都可以做为左值,但返回引用做左值时操作更直观。

总结一下指针、引用用做函数参数和返回值:

首先要理解主调函数和被调函数的关系。如果想保持两者的独立性,当然是用传值的方式进行。如果想让被调函数能够修改主调函数的变量,就使用传址,指针传值因为需要在函数体内解引用,操作不方便,C++引入了引用的语法机制,被调函数既能修改主调函数的实参,在函数体中有无须解引用。

另外可以从抽取函数的角度去理解。如果是引用传址,相对于直接抽取一部分代码封装为函数,实参与形参的相互影响仍在。而指针传址呢?需要在函数体内改写成解引用的形式。如果是传值呢,抽取的是相互没有关联性的代码(一般是一个值的计算,仍返回一个值)。

C语言中最复杂最容易出错的要数指针了。指针让一些初级程序员望而却步,而一些新的开发语言(如Java,C#)干脆就放弃了指针。

大家已经知道,C语言最适合于底层的开发,一个重要的原因就是因为它支持指针,能够直接访问内存和操作底层的数据,可以通过指针直接动态分配与释放内存:

上面的这段代码演示了指针的基本使用方式。在指针声明的时候,最好将其初始化为NULL,否则指针将随机指向某个区域,访问没有初始化的指针,行为为未定义而为程序带来预想不到的结果;指针释放之后,也应该将指针指向NULL,以防止野指针。因为指针所指向的内存虽然释放了,但是指针依然指向某一内存区域。

指针使用注意事项总结:

1)指针在声明的时候最好初始化

指针变量没有被初始化,任何指针变量刚被创建时不会自动成为NULL指针,它的缺省值是随机的,它会随机的指向任何一个地址(即野指针),访问野指针会造成不可预知的后果。所以,指针变量在创建的同时应当被初始化,要么将指针设置为NULL,要么让它指向合法的内存。

2)指针的加减运算移动的是指针所指类型大小

前面已经提到,指针的加法运算p = p + n中,p向前移动的位置不是n个字节,而是n * sizeof(*p)个字节,指针的减法运算与此类似。

3)当用malloc或new为指针分配内存时应该判断内存分配是否成功,并对新分配的内存进行初始化。

用malloc或new分配内存,应该判断内存是否分配成功。如果失败,会返回NULL,那么就要防止使用NULL指针。在分配成功时,会返回内存的地址。这个时候内存是一段未被初始化的空间,里面存在的可能是垃圾数据。因此,需要用memset()等对该段内存进行初始化或直接使用calloc()。

此外,应该防止试图使用指针作为参数,去分配一块动态内存。如果非要这么做,那么请传递指针的指针或指针的引用。

4)如果指针指向的是一块动态分配的内存,那么指针在使用完后需要释放内存,做到谁分配谁释放的原则,防止内存泄漏。

5)指针在指向的动态内存释放后应该重新置为NULL,防止野指针。

野指针不是NULL指针,是指向“垃圾”内存的指针。野指针是很危险的,它可能会造成不该访问的数据或不该改的数据被访问或者篡改。在应用free或者delete释放了指针指向的内存之后,应该将指针重新初始化为NULL。这样可以防止野指针。

分析下面的程序:

分析:上面的代码经常出现在各大外企的笔试题目里,它通过指针的指针分配了一段内存,然后将”hello”拷贝到该内存。使用完后再释放掉。到此为止,代码没有任何问题。但是,在释放之后,程序又试图去使用str指针。那么这里就存在问题了。由于str没有被重新置为NULL,它的值依然指向了该内存。因此后面的程序依然能够打印出”world” 字符串。

6)指针操作不要超出变量的作用范围,防止野指针。

分析下面的代码:

在上面的代码中,func()函数试图返回一个指向局部变量c的指针。然而局部变量的生命期为func()函数执行期,即变量c分配在栈上,func()函数执行完后,c就不存在了。返回的指针就是一个无效的野指针。因此,打印*p时,可能会出现任何一个不可确定的字符。

7) 对于复杂指针的使用,如果做不到“谁分配,谁释放”,那么可以使用引用计数来管理这块内存的使用。 引用计数方式来管理内存,即在类中增加一个引用计数,跟踪指针的使用情况。当计数为0了,就可以释放指针了。 此种方法适合于通过一个指针申请内存之后,会经过程序各种复杂引用的情况。

下面是一个实际例子:

以上实例的目的就是试图封装裸指针以达到指针安全的目的,相当于C++STL的智能指针shared_ptr的雏形。

-End-

本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com

点赞 0
收藏 0

文章为作者独立观点不代本网立场,未经允许不得转载。