指针被誉为“C语言的灵魂”。C语言之所以强大,很大一部分原因在于其灵活的指针运用。在这篇文章中,我就对C/C++中指针这一重要的语言特性进行简要的总结。
一、给指针分配一个绝对地址会发生什么?
1 2 3 |
|
我们可以给指针分配一个绝对地址,但是这样的做法是非常危险的。一旦我们试图改变它指向的内容,程序就会崩溃。上述代码的第二行可以执行,但是会在第三行崩溃。
二、指针和引用有什么区别?
- 非空区别,一个引用必须总是指向某些对象,而指针可以为空,所以在使用指针之前需要测试其合法性,防止其为空。
- 二是可修改区别,引用总是指向初始化时被指定的对象,之后引用所指的对象就不能改变(但是所指对象的内容可以改变),但是指针可以被重新赋值,指向另外的对象。
- 三是初始化时的区别,引用在声明的时候必须初始化,而指针在声明的时候可以不初始化,但是这样它会指向一个不确定的值,在这种情况下就改变其指向的内容是不合理的。
三、指针、引用与const
const
修饰符意味着一个变量或函数是只读的。const
修饰指针,一般分为以下三种情况:
1 2 3 |
|
其中第一种和第二种情况是等价的,都是说明指针a
指向的是一个const int
,这种情况下不允许对指针所指向的内容进行修改操作,但是指针本身的值可以修改;第三种情况中const
位于星号右侧,说明指针a
本身是一个常量,该指针指向的位置是不能变化的,但是该指针所指向的内容可以修改。
另外在前两种情况中,我们在声明指针a
的同时可以不对其进行初始化;而第三种情况中,因为指针a
本身是常量,我们必须在声明指针a
的同时对其进行初始化(声明常量时必须对其进行初始化)。
const
修饰引用时,有一点需要注意:在声明引用的时候,我们需要用一个变量对其进行初始化。而在声明const
引用时,不仅可以利用变量进行初始化,还允许用一个字面值,甚至是一个表达式作为初始值。此时编译器会创建一个临时量,然后将该const
引用绑定到临时量上。参考下面的代码:
1 2 3 |
|
const
指针与引用的“自以为是”:我们不能通过指向常量的指针或引用修改其指向的内容,但是可以通过其他途径修改。所谓常量指针或引用,其实只是指针或引用自以为是,它们觉得自己指向了常量,所以自觉不去改变所指对象的值,但是这个值还是可以通过其他途径改变的。比如下面的代码:
1 2 3 4 5 6 7 8 9 10 |
|
四、char*
与char[]
字符串有什么区别?
观察下面的代码,并寻找其中潜在的危险:
1 2 3 4 |
|
在上述代码中,函数体中的str
以数组的形式存储,这个数组的空间是在栈中分配的,且str
指向的是这个数组在栈中的首地址。在函数调用完成之后,栈恢复到调用函数之前的状态,调用函数时的临时空间(包括函数内部的临时变量)被回缩,str
所对应的地址已经不再属于应该被访问到的范围了。如果将str
的类型改为char*
则是正确的。通过数组方式分配的字符串位于栈上,在函数执行完毕后,该空间就被回缩;通过指针方式分配字符串位于内存中全局区域的文字常量区,该空间是一直存在的。
char*
与char[]
的区别还包括:①我们可以修改char[]
字符串的内容,但是char*
字符串存储在全局区域中的文字常量区,我们无法修改该字符串的内容,如果试图修改,程序会在运行时崩溃。②对于char str[]
,在存储字符串的内容之外,没有额外存储一个指向该数组的指针,因为str
本身可以当作指向该数组第一个元素的指针来使用;而对于char* str
,除了在文字常量区存储字符串的内容之外,在栈中还会利用4字节的空间存储指向该字符串的指针。
五、指针与动态分配内存
在C++中,我们经常利用操作符new
动态分配一块内存,这块内存位于堆中。需要注意两点:①使用new
动态分配的内存要使用delete
释放掉,否则会造成内存泄漏。②在我们delete
了一个new
返回的指针后,它对应的内存空间已经被释放,但是该指针本身仍然存在且不指向任何有效的空间,成为了悬空指针。如果在没有对该指针重新赋值的情况下就要修改它指向的空间,会造成程序崩溃。
那么,C++中的new
和C语言中的malloc
有什么区别呢?
首先,动态销毁malloc
创建的内存要用到free
,而new
则对应delete
;另外,对于我们自定义的类型,我们需要在动态创建对象的时候执行该类型的构造函数,在动态销毁对象的时候执行该类型的析构函数,malloc/free
则无法完成这一点。
六、this
指针
this
指针是一种特殊的指针,在类成员函数的调用过程中,它时时刻刻指向类的实例本身。关于这个特别的指针,我们有一些要点需要了解:
①如果类的一个非静态成员函数中访问到了类的非静态成员,那么编译器会对该函数进行一些处理:将对象本身的地址作为一个隐含参数传递给函数。实际上,对于类T
,成员函数默认的第一个参数都是T* const this
。比如成员函数int func(int a)
,在编译器看来其实应该是int func(T* const this, int a)
。
②this
指针的生命周期同任何一个成员函数的参数是一样的,在成员函数的开始前构造,在成员函数的结束后清除。当然,this
指针作为参数的传递效率一般比其他参数要高,可能会使用寄存器传递,而不是通过栈。
③一个对象的this
指针并不是对象本身的一部分,不会影响对该对象使用sizeof
的结果。其实,所有成员函数的参数都不会占用对象本身的空间,它们只会在参数传递的时候占用栈空间,或者直接通过一个寄存器进行传递。
④静态函数中不能使用this
指针,因为它可以不通过类的实例,而是类对象本身进行调用。
七、函数指针
1 2 |
|
在上述代码中,我们声明了一个名称为p
的函数指针,它的参数表为(int, int)
,返回值为int
。需要注意的是,绝对不能将*p
外面的括号省略掉,否则p
就成为了一个返回值为int*
类型的函数。然后我们令指针p
指向一个函数func
。当我们把函数名称作为一个变量使用时,该变量会自动转化为指针,所以没必要写成&func
。
1 2 |
|
函数指针类型通常比较冗长,我们可能想通过typedef
为它起一个别名。对函数指针类型使用typedef
的语法如上述代码所示。
八、数组指针
1 2 |
|
在上述代码中,我们声明了一个名称为p
的数组指针,它指向的是长度为3的int
类型数组。与函数指针类似,我们绝对不能将*p
外面的括号省略掉,否则p
就成为了一个长度为3的int*
类型数组(这里涉及到数组指针与指针数组的区别。数组指针是指向数组类型的指针,指针数组是一个数组,其中的元素都是指针类型)。然后我们令指针p
指向了一个数组a
,a
的类型是int[3]
,p
的类型则为int(*)[3]
。
※这里有一个误区,由于数组的名称可以当作指向数组第一个元素的指针来使用,很多人都认为a
的类型为int*
。实际上a
的类型为int[3]
,只不过可以为int*
类型的指针赋值而已。
数组指针的加减运算:正如对int*
类型的指针使用自增时,该指针会移动sizeof(int)
字节的长度一样,我们对int(*)[3]
类型的指针使用自增时,该指针会移动sizeof(int[3])
,也就是12字节的长度。观察下面的代码:
1 2 3 |
|
a
是int[5]
类型,&a
则是int(*)[5]
类型,将该类型的指针+1
,即移动20个字节,指向a
最后一个元素的下一个位置。然后我们将该int(*)[5]
类型的指针强制转化为int*
类型,并对其-1
,即前移4字节,此时该指针指向a
的最后一个元素。所以输出的内容应该是5。
九、智能指针auto_ptr
使用C++中的智能指针auto_ptr
可以方便管理单个堆内存对象,它解决了因为动态分配的对象忘记delete
而导致的内存泄漏问题。比如下面的代码将不会导致T
类型对象的内存泄漏,无论函数是正常退出还是因为异常跳出,pt
都会将对应的内存空间自动释放掉。智能指针调用成员函数的方法与普通指针基本一致。
1 2 3 |
|
智能指针的get()
函数:该函数返回被该智能指针管理的裸指针,在上述代码中,pt->get()
返回的就是T*
类型的指针。
智能指针的release()
函数:该函数让出该智能指针对裸指针的管理权(注意只是让出管理权,而不是释放对应的内存空间),一旦管理权被让出,那么该智能指针就不会自动释放内存空间了,需要我们手动delete
。
注意:智能指针不能相互赋值!将pt1
赋值给pt2
会使pt2
完全夺取pt1
的内存管理所有权,导致pt1
悬空,之后再对pt1
指向的内存空间进行访问时就会程序崩溃。所以,智能指针绝对不能使用赋值操作符,并且由智能指针管理的对象也不能放入vector
等容器中。