指针与引用总结

指针

若现在指针不知道指向,可以使用NULL,例如int *b = NULL; char *a = NULL;,但是使用的时候若指针指向的是数组、字符串和结构体等,需要提前声明大小。若是int *,则不需要,直接将一个int类型的指针赋值给它即可。

c++中的*&对于初学者来说,确实有点让人搞懵。因为在变量的定义和调用时,*&都会表现出不一样的含义。

* 总结

定义一个指针的三种写法都对:1. int * p; 2. int* p; 3. int *p; 习惯不同而已
定义一个函数指针的三种写法都对:1. int *p(); 2. int * p(); 3. int* p();

用于定义

*在定义时是声明该变量是一个指针,例如

1
int *p; //那p就是一个int型的指针。

例1:

1
2
int a = 0; 
int *p = a; //那p的值就是a的地址。

上面的int a = 0; int *p = a;实际上是int a = 0; int *p = &a;这两者是等价的!!!

原因是int *p = &a;时,c语言本身提供了可以略去&的简写,但是本人不是很喜欢这样的写法,因为这样会误导初学者!

所以大家还是写全比较规范一点,写完int *p = &a,这样比较好。

c++就没有这样的简写机制,大家可以测试一下。

用于调用时

*在调用时是指针指向的那个变量,是取值运算符。

例如:

1
2
3
int a = 0;
int *p = &a;
printf("*p = %d\n", *p);

&总结

用于定义时

&在定义时是定义一个引用

例2:

1
2
int a = 0;
int &b = a;

那么b就是a的引用,即b=a;如果再给a赋值a=10,则b也会变为10;如果给b赋值b=20,则a也会变为20;

关于具体引用的介绍我们将会在下面详细的进行介绍

用于调用时

&在调用时是一个取地址运算符。
例如:

1
2
int a = 0;
printf("&a = %p\n", &a);

会打印出a的地址,这个地址因为变量a在各个计算机的地址的不一样,所以打印的也不一样。

&在调用时还有一种与运算,如:int a = 0; a& = 0; //按位与操作,这个就不细说了

关于int *a; int &a; int & *a; int * &a; (int*) &a

上述的四条语句,前面两个很好理解,而后面两个,大部分C++初学者都会比较困惑,今天我也是查阅了一些资料以后才恍然大悟。下面具体来说明一下:

1
2
3
4
5
6
7
8
9
int i;

int *a = &i;//这里a是一个指针,它指向变量i

int &b = i;//这里b是一个引用,它是变量i的引用,引用是什么?它的本质是什么?下面会具体讲述

int * &c = a;//这里c是一个引用,它是指针a的引用

int & *d;//这里d是一个指针,它指向引用,但引用不是实体,所以这是错误的

注意:(int*) &a为取a的地址,然后进行强制类型转换,转为int类型的指针。

int * &aint & *a

声明

我在写这两句语句时,在int*(&)间空了一格,而后面的&(*)紧跟a。原因是:分析此类语句时,最简单的办法就是从右往左读,离注意:变量名最近的符号对其类型有最直接的影响,即先看a前紧跟的是什么,它决定了a的类型。例如,对于int & *a,此处是*,表示其首先是个指针,指针的类型是一个int型引用。而int后的一个空格是为了解释int *a, b;//a是指针,而b不是

正确的多个指针声明应该为:例:int *a,*b,*c,*d;

使用

在使用过程中,假设有一个 int 类型的变量 apa是指向它的指针,那么*&a&*pa分别是什么意思呢?

*&a可以理解为*(&a)&a表示取变量 a 的地址(等价于 pa),*(&a)表示取这个地址上的数据(等价于 *pa),绕来绕去,又回到了原点,*&a仍然等价于 a。

&*pa可以理解为&(*pa)*pa表示取得 pa 指向的数据(等价于 a),&(*pa)表示数据的地址(等价于 &a),所以&*pa等价于 pa

int **a;

引用

上面初步介绍了引用,下面相信介绍引用的概念、注意事项以及作用

什么是引用

引用,顾名思义是某一个变量或对象的别名,对引用的操作与对其所绑定的变量或对象的操作完全等价

1
语法:类型 &引用名=目标变量名;

&出现在等式左边的定义时。或者当&出现在函数的参数值中的时候是引用。或者在函数定义时,如float &fn2(float r)

特别注意:

  • &不是求地址运算符,而是起标志作用

  • 引用的类型必须和其所绑定的变量的类型相同

    1
    2
    3
    4
    5
    6
    7
    #include<iostream>
    using namespace std;
    int main(){
    double a=10.3;
    int &b=a; //错误,引用的类型必须和其所绑定的变量的类型相同
    cout<<b<<endl;
    }

报错

1
error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'|

  • 声明引用的同时必须对其初始化,否则系统会报错

    1
    2
    3
    4
    5
    6
    #include<iostream>
    using namespace std;
    int main(){
    int &a; //错误!声明引用的同时必须对其初始化
    return 0;
    }
  • 引用相当于变量或对象的别名,因此不能再将已有的引用名作为其他变量或对象的名字或别名

  • 引用不是定义一个新的变量或对象,因此内存不会为引用开辟新的空间存储这个引用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include<iostream>
    using namespace std;
    int main(){
    int value=10;
    int &new_value=value;
    cout<<"value的值为:"<<value<<endl;
    cout<<"new_value的值为:"<<new_value<<endl;
    cout<<"value在内存中的地址为:"<<&value<<endl;
    cout<<"new_value在内存中的地址为:"<<&new_value<<endl;
    return 0;
    }

即可简单的理解为上例中的new_valuevalue

对数组的引用

1
语法:类型 (&引用名)[数组中元素数量]=数组名;

例如

1
2
3
4
5
6
7
8
9
10
#include<iostream>
using namespace std;
int main(){
int a[3]={1,2,3};
int (&b)[3]=a;//对数组的引用
cout<<&a[0]<<" "<<&b[0]<<endl;
cout<<&a[1]<<" "<<&b[1]<<endl;
cout<<&a[2]<<" "<<&b[2]<<endl;
return 0;
}

对指针的引用

1
语法:类型 *&引用名=指针名;//可以理解为:(类型*) &引用名=指针名,即将指针的类型当成类型*

例如:

1
2
3
4
5
6
7
8
9
10
11
#include<iostream>
using namespace std;
int main(){
int a=10;
int *ptr=&a;
int *&new_ptr=ptr;
//int *&new_ptr1 = &a; //error: invalid initialization of non-const reference of type 'int*&' from an rvalue of type 'int*'|
cout<<ptr<<" "<<new_ptr<<endl;
cout<<&ptr<<" "<<&new_ptr<<endl;
return 0;
}

上例可理解为new_ptr为变量ptr的引用,因此两者的地址均相同。

上例中的int *&new_ptr1 = &a;会报错,可以理解为引用的类型只能与绑定的类型一致,因此只能给new_ptr1赋值指针变量。

引用的应用

引用作为函数的参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
using namespace std;
void swap(int &a,int &b){//引用作为函数的参数
int temp=a;
a=b;
b=temp;
}
int main(){
int value1=10,value2=20;
cout<<"-"<<endl;
cout<<"value1的值为:"<<value1<<endl;
cout<<"value2的值为:"<<value2<<endl;
swap(value1,value2);
cout<<"-"<<endl;
cout<<"value1的值为:"<<value1<<endl;
cout<<"value2的值为:"<<value2<<endl;

上述例子可以简单的理解为但value1传入到swap函数中的时候,为int &a = value1,对应上面介绍的引用定义。

特别注意:

  • 当用引用作为函数的参数时,其效果和用指针作为函数参数的效果相当。当调用函数时,函数中的形参就会被当成实参变量或对象的一个别名来使用,也就是说此时函数中对形参的各种操作实际上是对实参本身进行操作,而非简单的将实参变量或对象的值拷贝给形参

  • 通常函数调用时,系统采用值传递的方式将实参变量的值传递给函数的形参变量。此时,系统会在内存中开辟空间用来存储形参变量,并将实参变量的值拷贝给形参变量,也就是说形参变量只是实参变量的副本而已;并且如果函数传递的是类的对象,系统还会调用类中的拷贝构造函数来构造形参对象。而使用引用作为函数的形参时,由于此时形参只是要传递给函数的实参变量或对象的别名而非副本,故系统不会耗费时间来在内存中开辟空间来存储形参。因此如果参数传递的数据较大时,建议使用引用作为函数的形参,这样会提高函数的时间效率,并节省内存空间

  • 使用指针作为函数的形参虽然达到的效果和使用引用一样,但当调用函数时仍需要为形参指针变量在内存中分配空间,而引用则不需要这样,故在C++中推荐使用引用而非指针作为函数的参数

  • 如果在编程过程中既希望通过让引用作为函数的参数来提高函数的编程效率,又希望保护传递的参数使其在函数中不被改变,则此时应当使用对常量的引用作为函数的参数。

  • 数组的引用作为函数的参数:C++的数组类型是带有长度信息的,引用传递时如果指明的是数组则必须指定数组的长度

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include<iostream>
    using namespace std;
    void func(int(&a)[5])
    {
    //数组引用作为函数的参数,必须指明数组的长度
    //函数体
    }
    int main()
    {
    int number[5]={0,1,2,3,4};
    func(number);
    return 0;
    }

常引用

1
语法:const 类型 &引用名=目标变量名;

常引用不允许通过该引用对其所绑定的变量或对象进行修改

1
2
3
4
5
6
7
8
#include<iostream>
using namespace std;
int main(){
int a=10;
const int &new_a=a;
new_a=11;//错误!不允许通过常引用对其所绑定的变量或对象进行修改
return 0;
}

特别注意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
#include<string>
using namespace std;
string func1(){
string temp="This is func1";
return temp;
}
void func2(string &str){
cout<<str<<endl;
}
int main(){
func2(func1());
func2("Tomwenxing");
return 0;
}

运行上面的程序编译器会报错

这是由于func1()和“Tomwenxing”都会在系统中产生一个临时对象(string对象)来存储它们,而在C++中所有的临时对象都是const类型的,而上述func1返回值与"Tomwenxing"均没有与之对应的变量名,而上面的程序试图将const对象赋值给非const对象,这必然会使程序报错。如果在函数func2的参数前添加const,则程序便可正常运行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
#include<string>
using namespace std;
string func1(){
string temp="This is func1";
return temp;
}
void func2(const string &str){
cout<<str<<endl;
//str = "hello"; // 报错,因为const不可更改
}
int main(){
func2(func1());
func2("Tomwenxing");
return 0;
}

或者可以改为下面形式,将上面的常量给赋值成为变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
#include<string>
using namespace std;
string func1(){
string temp="This is func1";
return temp;
}
void func2(string &str){
cout<<str<<endl;
}
int main(){
string one = func1();
func2(one);
string two = "Tomwenxing";
func2(two);
return 0;
}

引用作为函数的返回值

1
语法:类型 &函数名(形参列表){ 函数体 }
注意事项1
  • 引用作为函数的返回值时,&必须在定义函数时在函数名前
注意事项2
  • 用引用作函数的返回值的最大的好处是在内存中不产生返回值的副本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//代码来源:RUNOOB
#include<iostream>
using namespace std;
float temp;
float fn1(float r){
temp=r*r*3.14;
return temp;
}
float &fn2(float r){ //&说明返回的是temp的引用,换句话说就是返回temp本身
temp=r*r*3.14;
return temp;
}
int main(){
float a=fn1(5.0); //case 1:返回值
//float &b=fn1(5.0); //case 2:用函数的返回值作为引用的初始化值 [Error] invalid initialization of non-const reference of type 'float&' from an rvalue of type 'float'
//(有些编译器可以成功编译该语句,但会给出一个warning)
float c=fn2(5.0);//case 3:返回引用
float &d=fn2(5.0);//case 4:用函数返回的引用作为新引用的初始化值
cout<<a<<endl;//78.5
//cout<<b<<endl;//78.5
cout<<c<<endl;//78.5
cout<<d<<endl;//78.5
return 0;
}

上例中4个case的说明解释:

case 1

用返回值方式调用函数(如下图,图片来源:伯乐在线):

返回全局变量temp的值时,C++会在内存中创建临时变量并将temp的值拷贝给该临时变量。当返回到主函数main后,赋值语句a=fn1(5.0)会把临时变量的值再拷贝给变量a

case 2

用函数的返回值初始化引用的方式调用函数(如下图,图片来源:伯乐在线)

这种情况下,函数fn1()是以值方式返回到,返回时,首先拷贝temp的值给临时变量。返回到主函数后,用临时变量来初始化引用变量b,使得b成为该临时变量到的别名。由于临时变量的作用域短暂(在C++标准中,临时变量或对象的生命周期在一个完整的语句表达式结束后便宣告结束,也就是在语句float &b=fn1(5.0);之后) ,所以b面临无效的危险,很有可能以后的值是个无法确定的值。

如果真的希望用函数的返回值来初始化一个引用,应当先创建一个变量,将函数的返回值赋给这个变量,然后再用该变量来初始化引用:

1
2
int x=fn1(5.0);
int &b=x;

case 3

用返回引用的方式调用函数(如下图,图片来源:伯乐在线)

这种情况下,函数fn2()的返回值不产生副本,而是直接将变量temp返回给主函数,即主函数的赋值语句中的左值是直接从变量temp中拷贝而来(也就是说c只是变量temp的一个拷贝而非别名) ,这样就避免了临时变量的产生。尤其当变量temp是一个用户自定义的类的对象时,这样还避免了调用类中的拷贝构造函数在内存中创建临时对象的过程,提高了程序的时间和空间的使用效率。

case 4

用函数返回的引用作为新引用的初始化值的方式来调用函数(如下图,图片来源:伯乐在线)

这种情况下,函数fn2()的返回值不产生副本,而是直接将变量temp返回给主函数。在主函数中,一个引用声明d用该返回值初始化,也就是说此时d成为变量temp的别名。由于temp是全局变量,所以在d的有效期内temp始终保持有效,故这种做法是安全的。

注意事项3

不能返回局部变量的引用。如上面的例子,如果temp是局部变量,那么它会在函数返回后被销毁,此时对temp的引用就会成为“无所指”的引用,程序会进入未知状态。

注意事项4

不能返回函数内部通过new分配的内存的引用。虽然不存在局部变量的被动销毁问题,但如果被返回的函数的引用只是作为一个临时变量出现,而没有将其赋值给一个实际的变量,那么就可能造成这个引用所指向的空间(有new分配)无法释放的情况(由于没有具体的变量名,故无法用delete手动释放该内存),从而造成内存泄漏。因此应当避免这种情况的发生

注意事项5

当返回类成员的引用时,最好是const引用。这样可以避免在无意的情况下破坏该类的成员。

注意事项6

可以用函数返回的引用作为赋值表达式中的左值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
using namespace std;
int value[10];
int error=-1;
int &func(int n){
if(n>=0&&n<=9)
return value[n];//返回的引用所绑定的变量一定是全局变量,不能是函数中定义的局部变量
else
return error;
}

int main(){
func(0)=10;
func(4)=12;
cout<<value[0]<<endl;
cout<<value[4]<<endl;
return 0;
}

用引用实现多态

在C++中,引用是除了指针外另一个可以产生多态效果的手段。也就是说一个基类的引用可以用来绑定其派生类的实例

1
2
3
4
class Father;//基类(父类)
class Son:public Father{.....}//Son是Father的派生类
Son son;//son是类Son的一个实例
Father &ptr=son;//用派生类的对象初始化基类对象的使用

特别注意:
ptr只能用来访问派生类对象中从基类继承下来的成员如果基类(类Father)中定义的有虚函数,那么就可以通过在派生类(类Son)中重写这个虚函数来实现类的多态。

引用总结

  • 在引用的使用中,单纯给某个变量去别名是毫无意义的,引用的目的主要用于在函数参数的传递中,解决大块数据或对象的传递效率和空间不如意的问题

  • 用引用传递函数的参数,能保证参数在传递的过程中不产生副本,从而提高传递效率,同时通过const的使用,还可以保证参数在传递过程中的安全性

  • 引用本身是目标变量或对象的别名,对引用的操作本质上就是对目标变量或对象的操作。因此能使用引用时尽量使用引用而非指针

指针和引用的对比

指针

对于一个类型TT*就是指向T的指针类型,也即一个T*类型的变量能够保存一个T对象的地址,而类型T是可以加一些限定词的,如const、volatile等等。见下图,所示指针的含义:

引用

引用是一个对象的别名,主要用于函数参数和返回值类型,符号X&表示X类型的引用。见下图,所示引用的含义:

区别

首先,引用不可以为空,但指针可以为空。前面也说过了引用是对象的别名,引用为空——对象都不存在,怎么可能有别名!故定义一个引用的时候,必须初始化。因此如果你有一个变量是用于指向另一个对象,但是它可能为空,这时你应该使用指针;如果变量总是指向一个对象,i.e.,你的设计不允许变量为空,这时你应该使用引用。如下图中,如果定义一个引用变量,不初始化的话连编译都通不过(编译时错误):

而声明指针是可以不指向任何对象,也正是因为这个原因,使用指针之前必须做判空操作,而引用就不必

其次,引用不可以改变指向,对一个对象”至死不渝”;但是指针可以改变指向,而指向其它对象。说明:虽然引用不可以改变指向,但是可以改变初始化对象的内容。例如就++操作而言,对引用的操作直接反应到所指向的对象,而不是改变指向;而对指针的操作,会使指针指向下一个对象,而不是改变所指对象的内容。见下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iostream>

using namespace std;

int main(int argc,char** argv)
{

int i=10;
int& ref=i;
ref++;
cout<<"i="<<i<<endl;
cout<<"ref="<<ref<<endl;

int j=20;
ref=j;
ref++;
cout<<"i="<<i<<endl;
cout<<"ref="<<ref<<endl;
cout<<"j="<<j<<endl;
return 0;
}

ref++操作是直接反应到所指变量之上,对引用变量ref重新赋值"ref=j"(此处要注意ref是可以重新在赋值的,但指向并不会发生变化),并不会改变ref的指向,它仍然指向的是i,而不是j。理所当然,这时对ref进行++操作不会影响到j。而这些换做是指针的话,情况大不相同,请自行实验。输出结果如下:

再次,引用的大小是所指向的变量的大小,因为引用只是一个别名而已;指针是指针本身的大小,4个字节。见下图所示:

从上面也可以看出:引用比指针使用起来形式上更漂亮,使用引用指向的内容时可以之间用引用变量名,而不像指针一样要使用*;定义引用的时候也不用像指针一样使用&取址。

最后,引用比指针更安全。由于不存在空引用,并且引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用,因此引用很安全。对于指针来说,它可以随时指向别的对象,并且可以不被初始化,或为NULL,所以不安全。const 指针虽然不能改变指向,但仍然存在空指针,并且有可能产生野指针(即多个指针指向一块内存,free掉一个指针之后,别的指针就成了野指针)。

总而言之,言而总之——它们的这些差别都可以归结为”指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名,引用不改变指向。

指针传递和引用传递比较

在C语言中,如果要实现在函数内部改变外部变量的值的话,就应该传递这个变量的指针。如果要通过指针访问变量,必须使用指针运算符*。这样在源代码中就会显得比较别扭:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void function(int *pval)
{
*pval=100;
//pval=100;先不考虑此处类型转换的错误,该代码只能改变堆栈中临时指针变量的地址,而不能改变指针指向对象的值
}
int main()
{
int x=200;
function(&x);
//或者如下调用
int *refx=&x;
function(refx);
return 0;
}

为了能透明地使用指针来访问变量,C++中引入了“引用”的概念

1
2
3
4
5
6
7
8
9
10
11
12
13
void function(int &refval)
{
refval=100;
}
int main()
{
int x=200;
function(x);
//当然,如下调用也可以。但这样做就失去引入"引用"的原本意义了
int &refx=x;
function(refx);
return 0;
}

这样一来,只要改一下函数声明,就可以在源代码的级别上实现指针访问和一般访问的一致性。可以把“引用”想象成一个不需要"*"操作符就可以访问变量的指针

总结

从概念上讲。指针从本质上讲就是存放变量地址的一个变量,在逻辑上是独立的,它可以被改变,包括其所指向的地址的改变和其指向的地址中所存放的数据的改变。

而引用是一个别名,它在逻辑上不是独立的,它的存在具有依附性,所以引用必须在一开始就被初始化,而且其引用的对象在其整个生命周期中是不能被改变的(自始至终只能依附于同一个变量)。

在C++中,指针和引用经常用于函数的参数传递,然而,指针传递参数和引用传递参数是有本质上的不同的:

指针传递参数本质上是值传递的方式,它所传递的是一个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变量处理,即在栈中开辟了内存空间以存放由主调函数放进来的实参的值,从而成为了实参的一个副本。值传递的特点是被调函数对形式参数的任何操作都是作为局部变量进行,不会影响主调函数的实参变量的值。

而在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址(指针传递参数时,指针中存放的也是实参的地址,但是在被调函数内部指针存放的内容可以被改变,即可能改变指向的实参,所以并不安全,而引用则不同,它引用的对象的地址一旦赋予,则不能改变)。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。

引用传递和指针传递是不同的,虽然它们都是在被调函数栈空间上的一个局部变量,但是任何对于引用参数的处理都会通过一个间接寻址的方式操作到主调函数中的相关变量。而对于指针传递的参数,如果改变被调函数中的指针地址,它将影响不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量,那就得使用指向指针的指针,或者指针引用。即指针传递只是传了一个地址copy, 在函数内部改变形参所指向的地址,不能改变原实参指向的地址,仅可以通过修改形参地址的内容,来达到修改实参内容的目的(原C语言中的通过指针来互换值小函数例子),所以如果想通过被调函数来修改原实参的地址或给重新分配一个对象都是不能完成的,只能使用双指针或指针引用(下面会进行详解)

如以下指针传递的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
using namespace std;
void function(int *pval)
{
int a = 200;
pval = &a;
*pval = 300;
cout<<"内部函数值:"<<*pval<<endl;
//pval=100;先不考虑此处类型转换的错误,该代码只能改变堆栈中临时指针变量的地址,而不能改变指针指向对象的值
}
int main()
{
int x = 100;
function(&x);
cout<<"外部函数值:"<<x<<endl;
return 0;
}

运行结果:

如以下引用传递的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
using namespace std;
void function(int &pval)
{
int a = 200;
pval = a;
cout<<"内部函数值:"<<pval<<endl;
//pval=100;先不考虑此处类型转换的错误,该代码只能改变堆栈中临时指针变量的地址,而不能改变指针指向对象的值
}
int main()
{
int x = 100;
function(x);
cout<<"外部函数值:"<<x<<endl;
return 0;
}

为了进一步加深大家对指针和引用的区别,下面我从编译的角度来阐述它们之间的区别:

程序在编译时分别将指针和引用添加到符号表上,符号表上记录的是变量名及变量所对应地址。指针变量在符号表上对应的地址值为指针变量的地址值,而引用在符号表上对应的地址值为引用对象的地址值。符号表生成后就不会再改,因此指针可以改变其指向的对象(指针变量中的值可以改),而引用对象则不能修改。

最后,总结一下指针和引用的相同点和不同点:

相同点:

  • 都是地址的概念;
    指针指向一块内存,它的内容是所指内存的地址;而引用则是某块内存的别名。

不同点:

  • 指针是一个实体,而引用仅是个别名;
  • 引用只能在定义时被初始化一次,之后不可变;指针可变;引用“从一而终”,指针可以“见异思迁”;
  • 引用没有const,指针有constconst的指针不可变;(具体指没有int& const a这种形式,而const int& a是有的,前者指引用本身即别名不可以改变,这是当然的,所以不需要这种形式,后者指引用所指的值不可以改变)
  • 引用不能为空,指针可以为空;
  • sizeof 引用得到的是所指向的变量(对象)的大小,而sizeof 指针得到的是指针本身的大小;
  • 指针和引用的自增(++)运算意义不一样;
  • 引用是类型安全的,而指针不是 (引用比指针多了类型检查)

指针的指针和指针的引用对比

指针的指针例子详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
#include <stdio.h>
#include <malloc.h>
#include <stdlib.h>

// 指针的指针作为形参,data1是主函数中实参 data1 的地址,*data1 表示实参 data1 的值
void test1(float **data1)
{
// 调用方式一
printf("inside1_data1:%p\n", *data1);
*data1 = (float*)calloc(10, sizeof(float));
printf("inside2_data1:%p\n", *data1);
for(int i=0;i<10;i++)
{
// printf("%p\n",*data1+i);
*(*data1 + i) = i; // *data1 + 1 表示地址偏移
}
}

void test2(float *data2)
{
// 调用方式二
printf("inside1_data2:%p\n", data2);
data2 = (float*)calloc(10, sizeof(float));
printf("inside2_data2:%p\n", data2);
for(int i=0;i<10;i++)
{
*(data2 + i) = i;
}
}

void test3(float **data3)
{
// 调用方式三
printf("inside1_data3:%p\n", data3);

data3 = (float **)calloc(10, sizeof(float*));
for(int i=0;i<10;i++)
{
*(data3+i) = (float *)calloc(10, sizeof(float));
}
// *data3 = calloc(10, sizeof(float));
printf("inside2_data3:%p\n", *data3);
for(int i=0;i<10;i++)
{
// printf("%p\n",*data1+i);
for(int j=0;j<10;j++)
{
*(*(data3 + i)+j) = 5; // *data1 + 1 表示地址偏移
}
}

for(int i=0;i<10;i++)
{
// printf("%p\n",*data1+i);
for(int j=0;j<10;j++)
{
//printf("%f\n", *(*(data3 + i)+j)); // *data1 + 1 表示地址偏移
}
}
for(int i=0;i<10;i++)
{
free(*(data3+i));
}
free(data3);
}


void test4(char *str1)
{
printf("inside1_str1:%p\n", str1);
str1 = (char*)calloc(10, sizeof(char));
printf("inside2_str1:%p\n", str1);
for(int i=0;i<10;i++)
str1[i] = 'a';
}

void test5(char *str2)
{
printf("inside1_str2:%p\n", str2);
for(int i=0;i<10;i++)
str2[i] = 'a';
}

void test6(char *str3)
{
printf("inside1_str3:%p\n", str3);
char *str4 = (char*)calloc(10, sizeof(char));
for(int i=0;i<10;i++)
str4[i] = 'a';
str3 = str4;
printf("inside2_str3:%p\n", str3);
}

int main(int argc, char const *argv[])
{
// 调用方式一,传入指针的地址作为实参,可以更改指针的值
float *data1 = NULL;
printf("outside1_data1:%p\n", data1); // 打印 data1 的值,而非data1的地址
test1(&data1);
free(data1);
printf("outside2_data1:%p\n", data1);
printf("\n");

// 调用方式二,传入指针的值作为实参,不可改变指针的值
float *data2 = NULL;
printf("outside1_data2:%p\n", data2);
test2(data2);
free(data2);
printf("outside2_data2:%p\n", data2);
printf("\n");

// 调用方式三,和调用方式是一的区别,&data1是一维数组,**data3是二维数组
// 在对二维动态数组进行动态内存分配时,要使用逐层内存申请的方式,第一层是指向指针的指针,每个元素是指针的形式,
// 第二层使用for循环进行内存申请,每个元素是基本数据类型(int,float)。
float **data3 = NULL;
// *data3 = NULL;
printf("outside1_data3:%p\n", data3); // 打印 data3 的值,而非data3的地址
test3(data3);
free(data3);
printf("outside2_data3:%p\n", data3);
printf("\n");

// 调用方式四,同调用方式二
char *str1 = NULL;
printf("outside1_str1:%p\n", str1);
test4(str1);
free(str1);
printf("outside2_str1:%p\n", str1);
printf("\n");

// 调用方式五,在函数外部声明大小,指针作为函数的实参
char *str2 = NULL;
printf("outside1_str2:%p\n", str2);
str2 = (char*)calloc(10, sizeof(char));
printf("outside2_str2:%p\n", str2);
test5(str2);
free(str2);
printf("outside3_str2:%p\n", str2);
printf("\n");

// 调用方式六
char *str3 = NULL;
printf("outside1_str3:%p\n", str3);
str3 = (char*)calloc(10, sizeof(char));
printf("outside2_str3:%p\n", str3);
test6(str3);
free(str3);
printf("outside3_str3:%p\n", str3);
printf("\n");

// 以下用法错误
// char **data1 = NULL;
// printf("%p", *data1);
// *data1 = (char*)calloc(10, sizeof(char));
return 0;
}

运行结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
outside1_data1:00000000
inside1_data1:00000000
inside2_data1:00370F00
outside2_data1:00370F00

outside1_data2:00000000
inside1_data2:00000000
inside2_data2:00370F00
outside2_data2:00000000

outside1_data3:00000000
inside1_data3:00000000
inside2_data3:00370F60
outside2_data3:00000000

outside1_str1:00000000
inside1_str1:00000000
inside2_str1:00370F30
outside2_str1:00000000

outside1_str2:00000000
outside2_str2:00370F48
inside1_str2:00370F48
outside3_str2:00370F48

outside1_str3:00000000
outside2_str3:00370F48
inside1_str3:00370F48
inside2_str3:00370F60
outside3_str3:00370F48

  • 对比调用方式二和调用方式四,可以得出float *char *相同,都可以理解为特殊形式的数组。前者是float数组,后者是char数组。

  • 对比调用方式一和调用方式二,当指针的地址作为实参时候,可以更改指针的值;当指针的值作为实参的时候,不可以更改指针的值。

  • 对比调用方式一和调用方式三,调用方式一中&data1是一维数组,调用方式三中**data3是二维数组。

  • 对比调用方式四、调用方式五和调用方式六,在函数外部声明大小,指针的值作为实参,可以更改指针值所指向空间的值,而不可以更改指针的指向(值)。

  • 对比第153行和第10行,第10行*data1是0,而第153的*data1没有值,因此会报错。

总结
由此可见,可以把指针float *data1char *str1看成是一个变量(指针变量),若想在子函数中改变变量的值,需要将变量取地址作为实参传给子函数,而非调用方式二的用法。而因为指针是指针变量,指针的值作为实参的时候,可以更改指针指向地址的内容,而不能更改指针的指向。

对比

在下列函数声明中,为什么要同时使用*&符号?以及什么场合使用这种声明方式?

1
void func1( MYCLASS *&pBuildingElement );

先来看int **ppint *&rp区别。前者是一个指向指针的指针;后者是一个指针的引用。如果这样看不明白的话,变换一下就清楚了:

1
2
3
typedef int * LPINT;
LPINT *pp;
LPINT &rp;

而指针的指针和指针的引用作为传递参数时,如下面的两个函数在被调用时,编译器编译的二进制代码都将传递一个双重指针,只不过两者的调用方法不同:

1
2
3
4
5
6
7
8
9
10
void function1(int **p)
{
**p=100;
*p=NULL;
}
void function2(int *&ref)
{
*ref=100;
ref=NULL;
}

可见,“引用”仅仅是为了给重载操作符提供了方便之门,其本质和指针是没有区别的。所以只要你碰到*&,就应该想到**。也就是说这个函数修改或可能修改调用者的指针,而调用者象普通变量一样传递这个指针,不使用地址操作符&

下面用三个函数onePointerFunc,poiPointerFunc, refPointerFunc举例详解,三个函数均想要在函数调用完毕后可以指向新的对象。

传单指针:

1
2
3
4
5
voidonePointerFunc(MYCLASS *pMyClass) 
{
DoSomething(pMyClass);
pMyClass = // 其它对象的指针
}

调用:MYCLASS* p = new MYCLASS; onePointerFunc(p); 调用onePointerFuncp没有指向新的对象:

第二条语句在函数过程中只修改了pMyClass的值。并没有修改调用者的变量p的值。如果p指向某个位于地址0x008a00的对象,当func1返回时,它仍然指向这个特定的对象。

传双指针:

1
2
3
4
voidpoiPointerFunc(MYCLASS** pMyClass); 
{
*pMyClass = new MYCLASS;
}

调用:MYCLASS* p =new MYCLASS;poiPointerFunc(&p);调用poiPointerFunc之后,p指向新的对象。

BTW,在COM编程中,到处都会碰到这样的用法—例如在查询对象接口的QueryInterface函数中:

1
2
3
4
5
6
interface ISomeInterface { 
   HRESULT QueryInterface(IID &iid, void** ppvObj);
   ……
   };
   LPSOMEINTERFACE p=NULL;
   pOb->QueryInterface(IID_SOMEINTERFACE, &p);

此处,pSOMEINTERFACE类型的指针,所以&p便是指针的指针,在QueryInterface返回的时候,如果调用成功,则变量p包含一个指向新的接口的指针。   

传指针的引用:

1
2
3
4
5
voidrefPointerFunc(MYCLASS *&pMyClass); 
   {
   pMyClass = new MYCLASS;
   ……
   }

其实,它和前面所讲得指针的指针例子是一码事,只是语法有所不同。传递的时候不用传p的地址&p,而是直接传p本身:  
调用:MYCLASS* p = new MYCLASS; refPointerFunc(p);调用refPointerFunc之后,p指向新的对象。

MFC在其集合类中用到的*&作为返回修饰符的例子—CObList,它是一个CObjects指针列表。

1
2
3
4
5
6
class CObList : public CObject { 
   ……
   // 获取/修改指定位置的元素
   CObject*& GetAt(POSITION position);
   CObject* GetAt(POSITION position) const;
   };

这里有两个GetAt函数,功能都是获取给定位置的元素。区别何在呢? 区别在于一个让你修改列表中的对象,另一个则不行。所以如果你写成下面这样:CObject* pObj = mylist.GetAt(pos);pObj是列表中某个对象的指针,

如果接着改变pObj的值: pObj = pSomeOtherObj; 这并改变不了在位置pos处的对象地址,而仅仅是改变了变量pObj.

但是,如果写成下面这样: CObject*& rpObj = mylist.GetAt(pos);。现在,rpObj是引用一个列表中的对象的指针,所以当改变rpObj时,也会改变列表中位置pos处的对象地址—换句话说,替代了这个对象。这就是为什么CObList会有两个GetAt函数的缘故。一个可以修改指针的值,另一个则不能。注意我在此说的是指针,不是对象本身。这两个函数都可以修改对象,但只有*&版本可以替代对象

const

在这里我为什么要提到const关键字呢?因为const对指针和引用的限定是有差别的,下面听我一一到来。

常量指针VS常量引用

常量指针

指向常量的指针,在指针定义语句的类型前加const,表示指向的对象是常量。

定义指向常量的指针只限制指针的间接访问操作,而不能规定指针指向的值本身的操作规定性。

常量指针定义"const int* pointer=&a"告诉编译器,*pointer是常量,不能将*pointer作为左值进行操作。

常量引用

指向常量的引用,在引用定义语句的类型前加const,表示指向的对象是常量。也跟指针一样不能利用引用对指向的变量进行重新赋值操作。

指针常量VS引用常量

在指针定义语句的指针名前加const,表示指针本身是常量。在定义指针常量时必须初始化!而这是引用天生具来的属性,不用再引用指针定义语句的引用名前加const

指针常量定义"int* const pointer=&b"告诉编译器,pointer是常量,不能作为左值进行操作,但是允许修改间接访问值,即*pointer可以修改

常量指针常量VS常量引用常量

常量指针常量

指向常量的指针常量,可以定义一个指向常量的指针常量,它必须在定义时初始化。常量指针常量定义"const int* const pointer=&c"告诉编译器,pointer*pointer都是常量,他们都不能作为左值进行操作。

而就不存在所谓的”常量引用常量”,因为跟上面讲的一样引用变量就是引用常量。C++不区分变量的const引用和const变量的引用。程序决不能给引用本身重新赋值,使他指向另一个变量,因此引用总是const的。如果对引用应用关键字const,起作用就是使其目标称为const变量。即没有:Const double const& a=1;只有const double& a=1

总结

有一个规则可以很好的区分const是修饰指针,还是修饰指针指向的数据——画一条垂直穿过指针声明的星号(*),如果const出现在线的左边,指针指向的数据为常量;如果const出现在右边,指针本身为常量。而引用本身与天俱来就是常量,即不可以改变指向。

或者看离const最近的是什么,例如char * const [指向字符的静态指针],离const最近的是*,则是指针常量,不能更改指针;而const char * [指向静态字符的指针],,离const最近的是char,则是代表字符不能改变,但是指针可以变,也就是说该指针可以指针其他的const char。

如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iostream>
#include<string>
using namespace std;

int main(){
char a[] = "hello";
char aa[] = "haha";

//const 修饰的是 *,则指针指向不可改变,但是其指向的内容可以更改
char * const c = a;
//c = aa; //error: assignment of read-only variable 'c'
a[1] = 'a';

// const 修饰的是char。表示指针指向可更改,但是指向的内容不能改变
const char * d = a;
d = aa;
//d[1] = 'a'; //error: assignment of read-only variable 'c'
cout<<c<<endl;
cout<<d<<endl;
return 0;
}

变量可以转为常量。但是常量不能变为非常量。

1
2
3
const int i = 10;
int &a = i; //错误
const int &b = i; //正确

附件

文章中的visio图的附件在这里

参考

C++:引用的简单理解
C++中引用,指针,指针的引用,指针的指针
c++的*与&简单总结

------ 本文结束------
坚持原创技术分享,您的支持将鼓励我继续创作!

欢迎关注我的其它发布渠道