美柑の部屋

涙は見せないと誓った。

Loading…

C++中指针的简要总结

指针被誉为“C语言的灵魂”。C语言之所以强大,很大一部分原因在于其灵活的指针运用。在这篇文章中,我就对C/C++中指针这一重要的语言特性进行简要的总结。

一、给指针分配一个绝对地址会发生什么?

1
2
3
int* a;
a = (int*)0x8000;
*a = 10;

我们可以给指针分配一个绝对地址,但是这样的做法是非常危险的。一旦我们试图改变它指向的内容,程序就会崩溃。上述代码的第二行可以执行,但是会在第三行崩溃。

二、指针和引用有什么区别?

  1. 非空区别,一个引用必须总是指向某些对象,而指针可以为空,所以在使用指针之前需要测试其合法性,防止其为空。
  2. 二是可修改区别,引用总是指向初始化时被指定的对象,之后引用所指的对象就不能改变(但是所指对象的内容可以改变),但是指针可以被重新赋值,指向另外的对象。
  3. 三是初始化时的区别,引用在声明的时候必须初始化,而指针在声明的时候可以不初始化,但是这样它会指向一个不确定的值,在这种情况下就改变其指向的内容是不合理的。

三、指针、引用与const

const修饰符意味着一个变量或函数是只读的。const修饰指针,一般分为以下三种情况:

1
2
3
const int* a = &b;
int const* a = &b;
int* const a = &b;

其中第一种和第二种情况是等价的,都是说明指针a指向的是一个const int,这种情况下不允许对指针所指向的内容进行修改操作,但是指针本身的值可以修改;第三种情况中const位于星号右侧,说明指针a本身是一个常量,该指针指向的位置是不能变化的,但是该指针所指向的内容可以修改。
另外在前两种情况中,我们在声明指针a的同时可以不对其进行初始化;而第三种情况中,因为指针a本身是常量,我们必须在声明指针a的同时对其进行初始化(声明常量时必须对其进行初始化)。

const修饰引用时,有一点需要注意:在声明引用的时候,我们需要用一个变量对其进行初始化。而在声明const引用时,不仅可以利用变量进行初始化,还允许用一个字面值,甚至是一个表达式作为初始值。此时编译器会创建一个临时量,然后将该const引用绑定到临时量上。参考下面的代码:

1
2
3
int& a = 42; //错误,不能将非常量引用绑定到字面量上。
const int& b = 42; //正确。
const int& c = 2 * b; //正确。

const指针与引用的“自以为是”:我们不能通过指向常量的指针或引用修改其指向的内容,但是可以通过其他途径修改。所谓常量指针或引用,其实只是指针或引用自以为是,它们觉得自己指向了常量,所以自觉不去改变所指对象的值,但是这个值还是可以通过其他途径改变的。比如下面的代码:

1
2
3
4
5
6
7
8
9
10
int b = 500;
const int* a = &b; //这句是合法的,允许非常量转化为常量。
*a = 600; //错误,不能通过a改变其内容。
b = 600; //正确,a指向的内容已经被改变成了600。

int i = 42;
const int& a = i;
int& b = i;
a = 20; //错误。
b = 20, i = 20; //均正确,a的内容已经被改变成了20。

四、char*char[]字符串有什么区别?

观察下面的代码,并寻找其中潜在的危险:

1
2
3
4
char* getStr(){
    char str[] = 0123456789; //如果想得到正确的结果,可以将char数组改为char*。
    return str;
}

在上述代码中,函数体中的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指针,因为它可以不通过类的实例,而是类对象本身进行调用。

一道关于this指针的奇葩问题展开

七、函数指针

1
2
int func(int, int);
int (*p)(int, int) = func;

在上述代码中,我们声明了一个名称为p的函数指针,它的参数表为(int, int),返回值为int。需要注意的是,绝对不能将*p外面的括号省略掉,否则p就成为了一个返回值为int*类型的函数。然后我们令指针p指向一个函数func。当我们把函数名称作为一个变量使用时,该变量会自动转化为指针,所以没必要写成&func

1
2
typedef int (*Func)(int, int);
Func q = func;

函数指针类型通常比较冗长,我们可能想通过typedef为它起一个别名。对函数指针类型使用typedef的语法如上述代码所示。

八、数组指针

1
2
int a[3] = {1, 2, 3};
int (*p)[3] = &a;

在上述代码中,我们声明了一个名称为p的数组指针,它指向的是长度为3的int类型数组。与函数指针类似,我们绝对不能将*p外面的括号省略掉,否则p就成为了一个长度为3的int*类型数组(这里涉及到数组指针与指针数组的区别。数组指针是指向数组类型的指针,指针数组是一个数组,其中的元素都是指针类型)。然后我们令指针p指向了一个数组aa的类型是int[3]p的类型则为int(*)[3]
这里有一个误区,由于数组的名称可以当作指向数组第一个元素的指针来使用,很多人都认为a的类型为int*。实际上a的类型为int[3],只不过可以为int*类型的指针赋值而已。

数组指针的加减运算:正如对int*类型的指针使用自增时,该指针会移动sizeof(int)字节的长度一样,我们对int(*)[3]类型的指针使用自增时,该指针会移动sizeof(int[3]),也就是12字节的长度。观察下面的代码:

1
2
3
int a[] = {1, 2, 3, 4, 5};
int* ptr = (int*)(&a+1);
cout << *(ptr-1) << endl;

aint[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
void func(){
    auto_ptr<T> pt(new T());
}

智能指针的get()函数:该函数返回被该智能指针管理的裸指针,在上述代码中,pt->get()返回的就是T*类型的指针。
智能指针的release()函数:该函数让出该智能指针对裸指针的管理权(注意只是让出管理权,而不是释放对应的内存空间),一旦管理权被让出,那么该智能指针就不会自动释放内存空间了,需要我们手动delete
注意:智能指针不能相互赋值!pt1赋值给pt2会使pt2完全夺取pt1的内存管理所有权,导致pt1悬空,之后再对pt1指向的内存空间进行访问时就会程序崩溃。所以,智能指针绝对不能使用赋值操作符,并且由智能指针管理的对象也不能放入vector等容器中。

评论