前言
关于引用
C++是C语言的继承,它可进行过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行以继承和多态为特点的面向对象的程序设计。引用(reference)就是C++对C语言的重要扩充。引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。引用的声明方法:类型标识符 &引用名=目标变量名;
引用引入了对象的一个同义词。定义引用的表示方法与定义指针相似,只是用&代替了*。
(节选自百度百科)
Tips
在讲引用之前,我想补充一点小知识,个人认为这会对引用和之后的学习有所帮助。
先看一段代码:
void fuc(int i, double d)
{
cout << "void fuc(int i double d)" << endl;
}
void fuc(double d, int i)
{
cout << "void fuc(double d, int i)" << endl;
}
int main()
{
fuc(1, 1.1);
fuc(1.1, 1);
return 0;
}
通过这段代码我们不难发现,fuc函数进行了重载,但是,我们在调用的时候,他似乎能够自动匹配到对应的fuc函数中去,是因为编译器足够智能能够判断吗?又或者是因为现在的技术十分发达,已经可以自动判断匹配了?都不是,想要理解这个问题,我们要简单知道编译器编译代码的过程:
编译器编译分为几个过程:
1.预处理:头文件展开(把头文件的内容拷贝过来放到这个文件中)/宏替换/去掉注释/条件编译
条件编译:#ifdef等
预处理后,test.cpp变成test.i
2.编译:检查语法,无错误生成汇编代码(指令级代码)
编译后生成test.s
3.进行汇编(与汇编代码不同):将汇编代码生成二进制机器码
生成test.o
4.进行链接:合并链接,生成可执行程序
a.out/xxx.exe
编译器编译时会生成符号表,注意,到这一步后,我们要知道,在C语言和C++中,有一个修饰规则叫做函数名修饰规则,C语言的修饰规则简单来说是链接时会用函数名到符号表中找地址,而如果函数名相同符号表就乱了,这也是为什么C语言不支持函数重载的原因。
而C++的修饰规则则更复杂,但也更充分,他会通过函数的调用方式,返回值类型,参数个数甚至参数类型来在符号表中查找函数,但是注意,这里并没有提及利用返回值查找函数,因此,对比C++和C可以发现,C是拿函数名字去找,C++是用修饰后的函数名去找,同时在这里也证明了返回值不同不能构成函数重载。那把函数返回值带入函数名修饰规则,能不能构成重载?答案是不能,因为函数调用的时候不带返回值,会造成调用指向不明确,也会出错。
讲了这么多,对于我们最开始的问题有所帮助吗?当然是有的,总的来说,因为C++的函数名修饰规则,即他查找函数的方式不同,使他能够在调用函数名相同的不同函数时,能够准确地找到对应的函数。
这里再多提一嘴,为什么函数只有声明没有定义会找不到?因为函数编译后才会生成地址,一个函数没有指令就不能生成地址,那拿符号表里的名字去找就找不到
正文
同样的,咱们先从一段代码开始理解
int main()
{
int a = 0;
int& b = a;
cout << &a << endl;
cout << &b << endl;
b++;
a++;
return 0;
}
引用的符号是&,或许刚学完C语言的同学会觉得奇怪,这会不会与取地址的那个&冲突?当然不会,使用场景和使用方法不同,并不会造成冲突,只有在定义类型和变量中间才叫引用,否则就是取地址。
在上面的代码中,我们看到引用最基础的用法是int& b = a;这代表了b是a的别名,这不难理解,我们在日常生活中,不论是朋友还是老师,或是家长,有时候总会给你自己,或别人取一些外号,举个例子:小刚的酒量很好,因此他被朋友们取了个外号叫酒鬼,而小刚在家里有一个小名,叫做小小刚,而小刚的表哥,又喜欢叫他刚子,这“小刚”,“酒鬼”,“小小刚”,“刚子”是不是都指的是小刚?那些名字不过是给小刚取的一个外号,一个别名罢了,他们都可以指代小刚,而这,就是引用。我们创建了一个变量a并给他初始化,现在我不想用a直接进行操作,我想用b来代替a,或者说叫代表a,我想用b来改变a,这个时候我们就可以用引用,让b成为a的别名,让b去代表他进行相关的操作,同时能够将发生的变化转移到a上。所以,a b操控的是同一个变量a。
同时,我们也可以给指针类型变量取别名,引用也有限制:引用必须在定义时初始化,也就是说,我们不能直接用int& b; 当然,一个变量可以有多个引用:
int a = 0;
int& b = a;
int& c = a;
int& d = b;
这与刚刚举的例子相同,一个变量可以有多个别名。
在理解了他的基础用法后,我们再进一步,引用是否可以用在函数当中?我们来看下面的代码
引用传参
void swap(int& x1, int& x2)
{
int tmp = x1;
x1 = x2;
x2 = tmp;
}
int main()
{
int x = 0, y = 0;
swap(x, y);
return 0;
}
可以看到,swap函数没有使用指针,但是传进去的x和y依然发生了交换,这是因为x1,x2是x,y的别名,改变x1,x2自然就会改变x,y,就好比让小小刚去做饭就等于让小刚去做饭。
引用做返回值
int count()
{
int n = 0;
n++;
return n;
}
int main()
{
int ret = count();
return 0;
}
这里我们是传值返回,并没有用到引用,我们知道,一个函数在调用完毕,即出了他的作用域后,就会销毁,而当中定义的局部变量也不例外,因此,一般的传值返回并不是将n返回给ret,出了函数作用域n就没有了,靠的是临时变量拷贝。
(Tips)关于传值返回:在这个例子中,n比较小,将n拷贝到临时变量,用寄存器充当临时变量返回,n比较大,会提前在main的栈帧中,main和函数栈帧的中间开辟一块空间为临时变量进行拷贝。
那我们用上引用,稍微做一下修改
int& count1()
{
int n = 0;
n++;
return n;
}
int main()
{
int ret = count1();
cout << ret << endl;
return 0;
}
打印后我们会发现,这里的值可能是1,可能是随机值,这是为什么?在这里我们将返回类型改为了传引用返回,相当于返回的是n的别名即引用,但是,n在函数调用完毕后就销毁了,那返回的n的引用就相当于野指针:空间销毁后仍然存在,但是指针指向这块空间,这个引用找不到n了,因为空间被销毁后去访问了这个空间,但是为什么值会不同?因为有些空间销毁后会置随机值,有些不会,所以这是编译环境以及编译器的问题
那我们再做测试,在ret处再加一个&试试?
int& count2()
{
int n = 0;
n++;
return n;
}
int main()
{
int& ret = count2();
cout << ret << endl;
cout << ret << endl;
return 0;
}
这次我们发现结果又有所不同,两次打印下来的结果是1和随机值,为什么两次打印的结果会不一样?因为在C++中,cout是函数,需要调用,而调用函数要先传参,参数就传给了ostream流插入,又因为int& ret相当于是n的引用,所以第一个打印出1,但是调用后函数创建的栈帧会覆盖掉count函数的栈帧。第二次调用ret仍是n的引用,但是调用的范围是第一次覆盖后的空间,n不见了,自然就是随机数
注意:如果count函数栈帧够大,n在栈帧的下面,就会覆盖不了
因此,如果函数返回时,出了函数作用域,返回对象还在(没有返回给系统)就可以使用引用返回,否则就要用传值返回
讲到这里,我们可以稍微总结一下引用传参和引用返回:
传引用传参:(任何时候都可以):提高效率,输出型参数(形参的修改,影响的实参)
传引用返回:(出了函数作用域对象还在才可以用):提高效率,可以对参数进行修改
权限问题
int main()
{
const int a = 0;
//int& b = a; error
//int b = a;
const int& c = a;
int x = 0;
const int& y = x;
int i = 0;
//double& d = i;
//error
const double& d = i;
return 0;
}
当引用遇上了const,就会有权限的问题,当变量a加上const后,我们不能对这个变量取别名,因为别名可以修改本来的变量,而const限制了a不能进行修改,所以这里叫做权限的放大,我们要知道,权限是不能放大的,那int b = a可以吗?是可以的,因为这是赋值,而不是取别名,不会对a造成影响,而后面的加上了const的c也可以成为a的别名,这是权限的平移,因为二者都不可更改,权限相同,所以可以取别名。下面的x和y则是权限的缩小,我们将x给了加上const的y,这里就会导致x可以改变,而是x别名的y不能改变。
最后关于const double& d = i;
c c++中规定:发生类型转换或表达式转换等,会产生临时变量,这里不是把i直接给d,而是给临时变量,这个临时变量时double类型,隐式显式都会产生,而临时变量具有常性,因此这里不可以是因为权限的放大,这里用const引用时,就没有权限的放大了,而是权限的平移,同时const会延长变量的生命周期,出了平移后变量的作用域才会销毁
所以,在引用过程中:权限可以平移,可以缩小,但是权限不能放大
实例化分析
了解了引用的用法,我们来举个例子看看他在实际使用时的效果
//C接口设计:
struct SeqList
{
int* a;
int size;
int capacity;
};
int SLAT(struct SeqList* ps, int i)
{
assert(i < ps->size);
return ps->a[i];
}
void SLModify(struct SeqList* ps, int i, int x)
{
assert(i < ps->size);
ps->a[i] = x;
}
//CPP接口设计:
struct SeqList
{
int* a;
int size;
int capacity;
};
int& SLAT(struct SeqList* ps, int i)
{
assert(i < ps->size);
return ps->a[i];
}
int main()
{
struct SeqList s;
SLAT(s, 1) = 0;
cout << SLAT(s, 2) << endl;
return 0;
}
这是顺序表的读取和修改函数,我们可以看到,加上了引用的函数显然要比C语言的函数便捷不少,可读性也增加了不少
总结
在底层代码中,引用相当于空间的拷贝,其实跟本来的方法没有什么区别,但是,底层代码并不是我们考虑并学习的重点,在语法上,引用就是取别名,并没有开空间,请大家多多学习引用,以便于以后的使用,同时希望各位观众姥爷多多点赞支持~