c++比较珍惜关键字,所以没有定义引用的关键字,所以选择共用符号来表示引用:&
引用就是给变量定义一个别名,引用符号放在类型的后面,变量名的前面。这是一个新的语法。
int a = 0;
int& b = a;
int& c = b;
a、b和c的地址是相同的。
引用的价值,在函数传指针,对数据进行修改的时候,可以使用引用
void swap(int& x, int& y)//c++的引用,指向的对象不可修改;jave的引用,指向的对象可以修改
{
int tmp = x;
x = y;
y = tmp;
}
int main()
{
int a = 0;
int b = 10;
swap(a, b);
cout << a << " " << b << endl;
return 0;
}
还有一种情况
typedef struct SinListNode
{
SLTDateType val;
struct SinListNode* next;
}SinListNode, *pSinListNode;//*pSinListNode是struct SinListNode对应的指针类型,就是SinListNode对应的指针类型
//这里的*pSinListNode是类型名称,不是变量
void SinListPushBack_C(SinListNode** pphead, int x)//这是正常的C语言单链表尾插
{
SinListNode* newnode = (SinListNode*)malloc(sizeof(SinListNode));
if(*pphead == NULL)
{
*pphead = newnode;
}
}
void SinListPushBack_CPP(SinListNode*& phead, int x)//c++引用版本的单链表尾插
//还有一种形式:void SinListPushBack_CPP(pSinListNode& phead, int x),这是同时利用typedef和引用的版本。
{
SinListNode* newnode = (SinListNode*)malloc(sizeof(SinListNode));
if(phead == NULL)
{
phead = newnode;
}
}
int main()
{
SinListNode* phead = NULL;
//SinListPushBack_C(&phead, 1);
SinListPushBack_CPP(phead, 1);
return 0;
}
引用的特性:
1.引用必须在定义的时候初始化,也就是说不能定义一个引用,然后在后面确定它指向的位置。
2.一个变量可以有多个引用
3.引用一旦选择一个实体,就不能引用其他实体。
int main()
{
int a = 0;
int& b = a;
const int x = 200;
const int& y = x;//int 类型取int类型的别名,const int类型取const int类型的别名是合理的
int c = 20;
const int& d = c;//但是,int类型取const int类型的别名也是可以通过的
return 0;
}
这就涉及里取别名的一个原则:对引用的变量,权限只能缩小,不能放大
const是修饰常变量的,也就是说,const修饰的是不可更改的。权限从可读取可修改到只能读取,权限变小了,所以编译时不会报错。如果const int& y = x前没有const,y引用x时,前面没有const,可以对x值进行修改,权限从只读到可修改可读取,权限放大,语法报错。
下面来介绍一些神奇的代码:
int main()
{
const int& c = 20;//常量也是可以取别名的,但是前面必须加const,因为常量不可修改
double a = 2.22;
int x = a;
const int& b = a;//int& b = a;是有语法错误的
return 0;
}
虽然会丢失数据,但为什么加上const编译就可以通过?
c++延伸了C语言的特性,存在隐式类型转换,因为浮点数和整数储存机制的不同,所以两者只能转换,不能简单粗暴的提升,截断。所以double类型赋值给int类型,从语法的层面上来讲,会产生一个临时变量,取出double的整数部分,放到临时变量中。然后再将临时变量给int类型的变量。临时变量具有常属性,不能被修改。所以int& b = a;编译不通过的原因在于,b引用的是中间的临时变量。可以调试一下,b引用的是中间的临时变量,b的地址和a的地址是不一样的。b不是对a的引用。
为什么int x = a;是正常的?
因为变量之前的赋值只是值的拷贝,不会影响原来的值;而指针和引用是可以影响对象的。x拷贝的是中间的临时变量,x不会对临时变量产生影响,而int& b = a;中的b是可以对临时变量产生影响的,所以语法报错。
具体用途在类和对象的时候会讲到(类和对象需要频繁的使用引用),这里简单的提一下,在函数传参的时候会用到。
void func(int& x)
{
}
当参数为double类型,常量,const修饰的整型变量等,就无法进行函数传参。
修改为这样就可以正常传参:
void func(const int& x)
{
}
后面在学到模板时也会用到。
引用的价值主要在作参数和作返回值方面
当引用参数时:
void swap(int& a, int& b)
{
int tmp = a;
a = b;
b = tmp;
}
int main()
{
int a = 10;
int b = 20;
swap(a, b);
return 0;
}
在书写上可以节省一点时间,在功能上产生优化(比如不需要对参数开辟空间了,以及其他的方面的优化,引用传参比普通传参的效率高很多很多,和指针传参的效率差不多,在其他方面比指针更优)
当作返回值时:
要理解引用作返回值首先要理解一般情况下的返回值
int reta()
{
static int a = 1;
return a;
}
int main()
{
int ret = reta();
return 0;
}
由前面的知识可知,a是由寄存器保存的(相当于int tmp = a)。也就是寄存器充当中间变量的作用,中间变量的类型就是返回值的类型,将a先传给中间变量,再被接收。设置中间变量的原因:函数结束,函数中的变量就销毁了,静态变量有很多限制,要少用。在ret前加上&,编译会不通过,原因在于临时变量具有常属性,权限放大了。这样也能证明临时变量的存在。
int& reta()
{
static int a = 1;
return a;
}
int main()
{
int ret = reta();
return 0;
}
返回值类型为int&的意义在于,产生的中间变量类型变成了int&。相当于int& tmp = a。此时tmp没有开辟空间。tmp再给ret就相当于a直接给ret。传引用返回的意义就是,返回的是a的别名。如果ret前加上&,ret和a的地址就是一样的。这里只考虑传值返回和传引用返回的区别,不考虑变量空间销不销毁。
int& reta()
{
int a = 1;
return a;
}
int main()
{
int& ret = reta();
return 0;
}
这个代码肯定是不行的,a已经销毁了。正常情况下只有static修饰的情况下才有引用返回(感觉有点鸡肋)
但是还有这种情况:
int& reta()
{
int a = 1;
return a;
}
int main()
{
int ret = reta();
return 0;
}
这样也会报警告,a是一个局部变量。引用的本质是指针,相当于返回局部变量地址了。
如果函数函数返回时,出了函数作用域,返回对象还在,没有还给系统,就可以使用引用返回;如果已经还给系统了,就必须使用传值返回。
下面来用一个样例来说明:
int& add(int x, int y)
{
int c = x + y;
return c;
}
int main()
{
int& ret = add(1, 2);
add(3,4);
cout << "add(1 ,2) :" << ret << endl;
return 0;
}
这段代码的结果是打印出:add(1 ,2) :7
原因在于返回的是c的引用,也就是说ret的值是add函数中c的那块空间所对应的值。在add(3,4)函数中,c的那块空间的值被改成了7,所以打印出来的是7。如果多调用几次cout << "add(1 ,2) :" << ret << endl;打印出来的将会是随机值。原因在于:第一次调用cout函数取参数ret,为7,打印7。然后第二次取参数,ret所在空间已经被cout函数栈帧覆盖了一次,ret空间中的数据产生了变化,也就打印出随机值。
这个样例只是说明返回对象还给系统后再使用引用返回是不安全的。在int c = x + y;前加上static也只是ret的值稳定,不会被后面的函数栈帧覆盖,add(3,4)还是会让ret变成7,因为对应空间存储的值改变了。
如果返回值超过寄存器的大小,会在上一层的栈帧中找一个位置储存下来。了解一下就可以。
总结一下:
int& add(int x, int y)
int& ret = add(1, 2);
根据返回值和接受变量有无&,共4种情况:
1.
int& add(int x, int y)
int& ret = add(1, 2);
正常,ret指向的空间就是返回值所在的空间,返回值在函数结束后如果销毁,会报警告。ret对应值所在空间会因被后面函数栈帧覆盖而产生变化。
2.
int add(int x, int y)
int& ret = add(1, 2);
报错,函数返回值的中间变量具有常属性,给ret就发生权限变大的现象了
3.
int& add(int x, int y)
int ret = add(1, 2);
如果返回值在函数结束后销毁,报警告,返回的是已经销毁的变量。ret是变量已经被赋值就不会产生变化。
4.
int add(int x, int y)
int ret = add(1, 2);
正常的函数使用。
第二种情况,在int& ret前加上const就不会报错(中间变量是寄存器,那ret引用的不就是寄存器的空间吗?那ret的值是不是会变化?),const修饰的变量可能在静态区,可能在栈区,也可能在只读区,具体情况不知道。
引用和指针的不同点:
1. 引用在定义时必须初始化,指针没有要求
2. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
3. 没有NULL引用,但有NULL指针
4. 在sizeof中含义不同:引用结果为引用类型的大小(const int&大小为4),但指针始终是地址空间所占字节个数(32位占4字节)
5. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
6. 有多级指针,但是没有多级引用
7. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
8. 引用比指针使用起来相对更安全
9.引用是一个变量的别名,指针是一个地址
引用可以说是对指针的改进。在汇编代码中引用和指针是一样的实现方式。
lea——load effective address加载有效地址
ra确实有进行a地址的储存的步骤,但是引用变量在使用时,编译器会自动访问地址对应的值。所以ra中存储的才是a的值,而不是a的地址。
应用要到类和对象中了。