指针与引用
指针
内存中有很多内存单元组成。这些内存单元用于存放各种类型的数据。为了标识内存单元,计算机对内存的每个单元都进行了编号,这个编号就称为内存地址,地址决定了内存单元在内存中的位置。程序员并不需要记住这些内存地址,C++的编译器让我们通过名字来访问这些内存位置。
指针本身就是一个变量,其符合变量定义的基本形式,它存储的值是地址。对于一个基本类型T,T* 是“到T的指针”类型,一个类型为T*的变量能够保存一个类型T的对象的地址。
通过一个指针访问它所指向的地址的过程称为间接访问或者引用指针。这个用于执行间接访问的操作符是单目操作符*
int a = 1;
float b = 3.14;
//指针定义
int* c = &a;
float* d = &b;
cout<<c<<endl; //输出a变量的地址
//间接访问
cout<<(*c)<<endl; //等同于cout<<a<<endl;
cout<<(*d)<<endl;//等同于cout<<b<<endl;
左值与右值
字符串本身就是一个字符数组,但是字符串还可以用指针来表示
int main()
{
//两种字符串表示
char strHello[] = {"hello"};
char* pStrHello = "hello";
pStrHello = strHello; //正确,指针变量的值可以改变
strHello = pStrHello; //错误,数组变量的值不允许改变
return 0;
}
strHello不可改变,strHello[index]的值可以改变
pStrHello可以改变,pStrHello[index]的值能否改变取决于所指的存储区域是否可变
这里就涉及到了左值与右值的概念
左值:编译器为其单独分配了一块存储空间,可以取其地址的,左值可以放在赋值运算符左边;
右值:指的是数据本身,不能取到自身地址,右值只能放在赋值运算符右边
左值最常见的情况就是函数和数据成员的名字。
右值是没有标识符、不可取地址的表达式,一般也称为“临时对象”
a = b + c;
&a是允许的操作,a是一个左值
&(b+c)不能通过编译,b+c是一个右值
C++的原始指针
一般类型指针T*
T是一个泛型,泛指任何一种类型
int i = 4;
int* iP = &i;
cout<<(*iP)<<endl;
double d = 3.14;
double* dP =&d;
cout<<(*dP)<<endl;
不论T是什么类型,T* 这个指针的内存空间都是一样的,为4个字节
指针的数组与数组的指针
指针的数组 T* t[]:指针的数组仍然是数组,里面每个值是个指针(arrao of pointers)
数组的指针 T(*t)[] :一个指针,指向一个数组(a pointer to an array)
int* a[4];
int c[4] = {1,2,3,4};
int(*b)[4];
b = &c; //数组的个数一定要匹配
//注意,[]的优先级比较高
for(unsigned int i = 0;i<4;i++)
{
a[i] = &(c[i]);
}
cout<<*(a[0])<<endl; //先取数组下标得到地址,然后做间接访问
cout<<(*b)[3]<<endl; //b是指针,先间接访问取值得到数组,然后取数组下标
const pointer和pointer to const
char strHello[] = {"helloworld"};
char const *pStr1 = "helloworld"; //修饰char,指针指向的地址可变,但是存储的区域中的内容不可变
char* const pStr2 = "helloworld"; //修饰char*,指针指向的地址不可变
char const * const pStr3 = "helloworld"; //地址和空间中的内容都不允许改变
pStr1 = strHello;
//pStr2 = strHello; //错误,pStr2地址不可改
//pStr3 = strHello; //错误,pStr3地址不可改
//pStr1[index] = 'a'; //错误,存储区内的char不可改变
pStr2[index] = 'a';
//pStr3[index] = 'a'; //错误,存储区内的char不可变
关于const修饰
- 看左侧最近的部分
- 如果左侧没有,则看右侧
指向指针的指针
int a = 123;
int* b = &a;
int** c = &b;
*操作符具有从右向左的结合性,**c 这个表达式相当于 *(*c),必须从里向外逐层求值
*c得到的是c指向的位置,即b
**c相当于 *b,得到变量a的值
表达式 | 值 |
---|---|
a | 123 |
b | &a |
*b | a,123 |
c | &b |
*c | b,&a |
**c | *b,a,123 |
未初始化的指针和非法指针
int* a;
*a = 12;//错误
上述操作并没有对指针a进行初始化,也就是说我们并不知道a最终会指向哪里。运气好的话定位到一个非法地址(程序不能访问的地址),程序会出错从而终止。最坏的情况下,a定位到了一个可以访问的地址,这样我们就无意间修改了它,这样的错误难以捕捉,引发的错误与原先用来操作的代码毫不相干,我们根本无法定位。
用指针进行间接访问之前,一定要确保它已经初始化,并且被恰当的赋值。
NULL指针
NULL指针是一个特殊的指针变量,表示不指向任何东西。
int* a = NULL;
NULL指针的概念非常有用,它给了一种方法,来表示特定的指针目前未指向任何东西。
对于一个指针,如果已经知道将被初始化为什么地址,那么请给他赋值,否则把它设置为NULL,这样可以有效避免不可确定性访问的问题。
在对一个指针间接引用前,先判断这个指针的值是否为NULL。
指针使用完成后也请重新赋值为NULL
野指针
野指针是指向“垃圾”内存的指针。if等判断对它们不起作用,因为没有置为NULL;
一般情况下有三种情况被称为野指针
- 指针变量没有初始化
- 已经释放不用的指针没有置为NULL,如delete和free之后的指针
- 指针操作超越了变量的作用域范围(指针指向具有一定生命周期的空间)
没有初始化的,不用的或者超出范围的指针,请一定置为NULL
指针基本运算
&和*操作符
char ch = ‘a’;
char* cP = &ch;
&操作符不能做左值,&操作编译器做是事情是把变量的地址位置取出来,然后放在内存空间中。但是他本身并不是变量自身,仅仅是一块空间存储着变量地址,这块空间我们的程序是没办法获取到的。
间接引用操作当用作左值的时候,实际的操作是把变量ch当前的位置取出来(取空间),这种操作我们可以对这块空间进行操作,比如赋值操作。
当我们把他当作右值时,实际的操作取的就不是存储空间,而是存储空间中的值。
*cp + 1首先得到cp中的值,得到a,做+1操作就是对ASCII码进行操作,得到b。但是这个操作还是由编译器创造一块空间取值,我们得不到这个变量的地址,不能做左值。这个+1的操作是按照cp的类型来做加法的,移动的是cp这个类型的大小。
*(cp+1)操作我们先做了+1,而cp本身是个指针,我们做的是指针的加法,得到的是ch这个变量的地址的后面那个地址(做这个操作前要确定cp指向的地址后面的内容是可以访问的)。这个操作也是可以用作左值和右值,左值就是取地址,右值就是取空间中存储的值。
int main()
{
char ch = 'a';
//&操作符
&ch = 97; //错误,&ch左值不合法
char* cp = &ch; //&ch右值
&cp = 97; //错误,&左值不合法
char** cpp = &cp; //&cp右值
//*操作符
*cp = 'a'; //*cp左值取变量ch的位置
char ch2 = *cp; //*cp右值取变量ch存储的值
*cp + 1 = 'a'; //错误,*cp+1左值不合法的位置
ch2 = *cp + 1; //*cp+1右值取到的字符做ASCII码+1操作
*(cp+1) = 'a'; //左值,语法上合法,访问到cp后面的位置,赋值为a.一定要保证这个位置是可以访问的,这种操作有风险
ch2 = *(cp+1); //右值操作,取ch后面的位置的值
}
++和–操作符
char* cp2 = ++cp;
//汇编代码:
mov eax,dword ptr [cp] //eax是寄存器,dwptr存储cp指针。把指针内容放置寄存器内
add eax,1 //寄存器数据+1
mov dword ptr [cp],eax //把寄存器内容存回cp中
mov ecx,dword ptr [cp] //把cp的内容放置在ecx寄存器
mov dword ptr [cp2],ecx //把寄存器ecx内容放置在cp2中
char* cp3 = cp++;
//汇编代码:
mov eax,dword ptr[cp] //把cp指针内容放置在eax寄存器中
mov dword ptr[cp3],eax //把eax内容直接放在cp3指针
mov ecx,dword ptr[cp] //把cp信息放置在exc寄存器中
add ecx,1 //ecx+1操作
mov dword ptr[cp],ecx //ecx内容写入cp指针
前置操作先做加法再赋值,后置操作先赋值后做加法操作。
自减操作符和自增操作符相同,前置操作先做减法再赋值,后置操作先赋值再做减法。
自增自减操作获得的地址不能当作左值,它只是个地址的副本,没有明确存储的位置。
++操作符优先级高于*
++++和----等运算符
编译器程序分解符号的方法是:一个字符一个字符的读入,如果该字符可能组成一个符号,那么读入下一个字符,一直到读入的字符不能组成一个有意义的符号。这个处理过程称为“贪心法”。
int a = 1,b=2;
int c;
int d;
c = a+++b; //相当于a++ +b。连续读取,当读取到两个+号时不能再组成新符号了
d = a++++b; //相当于a++ ++b。这个是错误的 ,不构成任何运算
引用
引用在本质上仍然是是指针,只不过自身比较特殊,是不允许修改的指针。
在指针使用上,我们会遇到一些问题:
- 空指针
- 野指针
- 不知不觉改变了指针的值,我们却仍然在使用
使用引用,我们可以避免这些问题:
- 不存在空引用;
- 引用必须被初始化;
- 一个引用永远指向它初始化的那个对象,不允许被修改。
引用可以认为是指定变量的别名,使用时可以认为是变量本身:
int x1 = 1,x2 = 3;
int& rx = x1; //定义引用,可以认为rx是x1的别名
rx = 2;
cout<<x1<<rx<<endl; //x1和rx都是2
rx = x2; //引用一旦被初始化就不能更改,所以这里不是赋值rx为x2,而是x1=x2
cout<<x1<<x2<<endl; //都是3
当我们在函数中需要操作形参并且返回时一并返回,这时候我们就可以传递引用。
void swap(int& a,int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int a=3,b=4;
swap(a,b);
}
C++为什么要同时存在指针和引用?在java语言中我们直接使用引用,传统C语言我们都使用指针。C++可以认为是夹在C和java之间的一种。之所以要使用引用是为了支持函数的运算符重载。而C++为了兼容C语言不能摒弃指针。
在函数传递参数的时候,对于内置基础类型(int、double等)而言,在函数中传递值更高效(pass by value);在面向对象中自定义类型而言,在函数中传递const引用更高效(pass by reference to const)。