一直对指针有一种恐惧心理,写程序都不怎么敢用指针,但感觉这样不太行,这东西就算不深入,但还是得知道怎么用才对。由于C++还有个引用,因此也把这家伙一并“谈谈”。水平不够,自然只能浅谈,如有错误,还请见谅。
空悬指针、野指针
空悬指针指向已经销毁的对象或已经回收的地址。
比如用free
和delete
把申请的内存释放了,或者指向的对象超过了作用域被安排了。
注意后一种情况还可以正常用这个指针。比如下面这个例子,p指向的对象已经被安排了,但是依然可以输出指针指向的地址保存的值,即数据未被清除。因此在编程的时候要小心这种情况,因为既然对象被安排了,那么对应的内存空间在之后就有可能被重新利用,从而被覆盖。
int* p = nullptr;
if (1) {
int a = 1;
p = &a;
}
cout << *p << endl;
野指针指的是未经初始化的指针。
int *p;
cout << *p << ' ' << p << endl;
报错:
空指针
空指针不指向任何对象。
生成空指针的方法有以下几种
//方法1:用nullptr来初始化指针,这是C++11新标准引入的一种方法
//nullptr是一种特殊类型的字面值
int *p1 = nullptr;
//方法2:直接将指针初始化为字面值0
int *p2 = 0;
//方法3:这是一种老方法,NULL是一个预处理变量,定义在cstdlib中,其值就是0
int *p3 = NULL;
注意虽然可以直接用字面值0初始化指针,但不能搞个值为0的int变量赋值给指针。
指针与数组
对指针和数组混在一起,再搞些什么加减法,考虑优先级,容易让人头晕,这也是我不爱玩指针的原因。干写原理很没劲,干脆想些情况分析。
先看点简单的。
int a[] = { 1,3,5 };
int* p = a;
cout << *p << endl;//输出1
cout << *p + 1 << endl;//输出2
cout << *(p + 1) << endl;//输出3
指针p
指向数组a
的首地址,直接取值(*p
)得到1。*p + 1
是先取值再加1,输出2。*(p + 1)
是指针加1再取值,指针加1,那么p就指向了a的第二个元素,取值就是3了。
再看下面两种情况。
int a[] = { 1,3,5 };
int* p = a;
cout << ++*p << endl;//输出2
int a[] = { 1,3,5 };
int* p = a;
cout << *p++ << endl;//输出1
cout << *p << endl;//输出3
这里要考虑一个优先级以及结合顺序的问题。
翻了翻《C++ Primer》发现后置递增和递减运算符的优先级要比前置的高,前置的递增和递减运算符的优先级和解引用运算符的优先级相同。二者的结合顺序都是右结合。由此,上面一种情况是对解引用的结果递增,结果当然是2。后面一种情况*p++
是先递增,但是后置的递增和递减运算符返回的值等于对象递增前的值,因此解引用的还是指向数值首元素的指针,所以输出1。后面因为p已经递增,因此其指向数组第二个元素,对其解引用,输出结果是3。
这里提一个要注意的点就是数组名是个常量,因此不能拿来自增自减。
再看一个
int a[][3] = { {1,3,5},{7,9,11},{13,15,17} };
cout << a << endl;
cout << a + 1 << endl;
cout << *a + 1 << endl;
cout << *(a + 1) << endl;
cout << *(a + 1) + 1 << endl;
cout << **a + 1 << endl;
cout << *(*a + 1) << endl;
cout << *(*a + 1) + 1 << endl;
cout << *(*(a + 1) + 1) << endl;
cout << *(*(a + 1)) + 1 << endl;
输出如下:
a
是数组首元素的地址。
a+1
,二维数组其实就是几个一维数组成员组成的一维数组,这里+1,就跳到了下一个一维数组成员,a+1
是下一行首元素的地址。
*a + 1
,就是先对a解引用,然后+1,对a解引用的结果还是首元素的地址,不过+1就不再是跳到下一行了,因为对其解引用其实就是取了第一个一维数组成员,所以+1的结果是同一行的下一个元素的地址。
*(a + 1)
,就是a+1取值,当然依旧是第二行首元素的地址。
*(a + 1) + 1
,第二行第二个元素的地址。
后面我不想写了,差不多套路,一般分析这种问题可以画个图。
注意,下面这样是不对的
int a[][3] = { {1,3,5},{7,9,11},{13,15,17} };
int** p = a;
vs提示:
下面这样是可以的。
int a[][3] = { {1,3,5},{7,9,11},{13,15,17} };
int(*p)[3] = a;
注:写这块的时候很多东西我不明白,输出的结果是没错的,但分析的过程所以可能有些错误,今后勿将上面的分析奉为真理。
带上const
首先看一个问题,考虑如下代码将输出什么。
int i = 0;
int& v1 = ++i;
int& v2 = ++i;
cout << v1 << ' ' << v2 << endl;
这个答案很简单,由于v1
和v2
都是i
的别名,经过两次自加后i
的值为2,因此输出2 2.
之后来看下面一个。
int i = 0;
const int& v1 = i++;
const int& v2 = ++i;
cout << v1 << ' ' << v2 << endl;
判断这段代码的输出要了解两个知识点,一个是前后++的区别,另一个就是const
的影响。如果不考虑这两点,就认为v1
和v2
都是i
的别名,那么将产生错误的判断。
前置的++和–运算符返回的是左值,后置的++和–运算符返回的是右值。后置++和–的原理是创建一个临时对象,把值放在这个临时对象后,再增加源变量的值,之后返回的也是这个临时对象,注意返回的是个常量。因此这里引用的对象是值为0的整型常量,而非变量i。当然,非常量引用的初始值必须是左值,不能是常量,vs中如果把const
去掉就会提示错误。另外,允许常量引用绑定非常量对象。
最后再来想这段代码是输出就很清楚了,v1绑定的是一个整型常量,所以输出0,v2绑定的是变量i,所以输出2.
现在再想,如果后面再让i自加呢。
int i = 0;
const int& v1 = i++;
const int& v2 = ++i;
++i;
cout << v1 << ' ' << v2 << endl;
可以看到由于i的自加,其变为了3,由于v2绑定了i,因此v2的值也是3。
但是由于常量引用,就不能直接对引用进行运算了。
尾声:不做这个笔记不知道原来我对指针及引用的认知是如此得浅薄,以至于让我觉得有必要再看看基础书籍,把里面的相关概念及相关示例吃透。