目录
1,引用就是取别名
int a=1;
int b=a;
int &c = a;
此刻叫a就相当于叫c,a改变不会影响b改变,但是a改变会影响c改变,引用所引的地址都是一样的。
当然取别名可以循环取别名
int &c=a;
int &d=c;
int &e=d;
2,引用必须初始化
一个刚出生的小孩要想取别名,其本身就要有主名,即引用必须初始化。
引用必须初始化,必须要让它知道是谁的别名,即必须要有一个引用的实体。
在知道了引用之后,我们便来分析一下,引用跟赋值
int main(){
int a=0;
int &c=a;
int d=1;
c=d;
}
在上面那串代码中,我们便会思考,究竟是d赋值给c,还是c变成d的别名?
答案是:只是赋值,并不是传地址
以前传地址的时候,我们通常是要用指针来传地址的,繁琐的很
void swap(int* a,int* b){
int tmp = *a;
*a=*b;
*b=tmp;
}
int main(){
int a1=1;
int b1=2;
swap(&a1,&b1);
}
所以现在出现了引用
3,引用的作用之一,传参数
void swap(int& a,int& b){
int tmp = a;
a=b;
b=tmp;
}
int main(){
int a1=1;
int b1=2;
swap(a1,b1);
}
形参是实参的别名,对比于用指针传递实参来改变一些功能来看,传引用显然是较为简便的
另外,传引用比传指针的效率更高。
传指针相当于拿着要改变值的地址来去找这个值
而引用则相当于我就换个名,就站在你面前,任你修改
那么说出来引用比指针好的一个优点之后,缺点呢
引用有野的吗?答案是肯定的
野引用
先来个拷贝复制的预热
int Count(){
int n=0;
n++;
return n;
}
int main(){
int ret = Count(); //Count()的返回值会是什么,n?
return 0;
}
count的返回值会是什么,n吗
那么返回的应该是什么呢,返回的是n的拷贝,那么n的拷贝又是什么呢?
当n是比较小的内容时,一般会寄存在寄存器里
当n是比较大的内容时,它会找一块空间存储起来
再拷贝给ret
引用作为返回值
声明:以下举出的代码均有不同程度的错误,在实际中请勿使用
当我们想要强制运用引用返回的时候,会发生什么呢
int& Count(){ //返回的是n的别名
int n=0;
n++;
return n;
}
int main(){
int ret = Count(); //Count()的返回值会是什么,n?
return 0;
}
分析这个,我们就要知道引用返回的意义是什么,返回的是别名
在此刻返回的就是n的别名,假设别名是编译器取的,甚至可以理解成就是返回n
但是!!!注意的是,在其他编译器中,n在函数结束的时候栈帧就已经销毁了,随之存在的空间也是清理了,所以返回的n是一个野的,结果是不确定的,我用的VS返回的依然是1
根据这个案例,再来一个花活
int& Add(int a, int b) {
int c = a + b;
return c;
}
int main() {
int& ret = Add(1, 2);
cout << ret<<endl; //3 or 随机值,此处选择了3,取决于栈帧是否清理
Add(3, 4);
cout << ret<<endl; //7 or 随机值,此处选择了7
Add(5, 6);
cout << ret << endl; //11 or 随机值,此处选择了11
Add(7, 8);
cout << ret << endl; //15 or 随机值,此处选择了15
Add(ret, 1);
cout << ret << endl; //16 or 随机值,此处选择了16
return 0;
}
会有人想第一个问题,第一次执行打印语句的时候我能够理解,为什么第二次执行打印语句的时候却不是3了呢,分明在编程语言上就已经没有关联了啊?
答案肯定不是的:
第一次Add(1, 2)的时候,返回的是1+2=3,ret的别名是3,实际上返回的是c的别名,或者这样说,ret是函数返回值c的别名的别名,即双重别名,但在本质上还是c的值,所以c的值改变,一定会引起作为别名的别名的ret跟着改变。
但值就具有不确定性了,因为这需要看每个编译器是否会保留释放空间后的c的值了,返回这个值——c的别名——野值/编译器帮你保留的值。
那么第二个问题便是:调用了这么多次Add函数,凭什么每次创建的c都是ret的别名的别名呢?
答案是:c的值一直在那块地方,只不过c的官方使用空间被收回去了,收回去之后,它可能还叫c,也可能不再叫c了,但它所存的内容就还是原来c内容。
就类似于退房子,但是房子退了依旧可以再用。
要记住的是:空间是可以重复利用的,销毁空间并不是字面意义上的把那个空间给扣掉,而是把原本属于那个数据的空间归还给系统,可以理解为销毁的是数据的空间,现在这种行为就是倒回去在把这个数据的空间从系统那里租回来。
虽然Add老是清理空间,但是Add依旧是那么大的空间,系统依旧会把原来的那间房给Add使用,函数中的野东西(因为还了空间)c依旧放在那里,等到Add再来使用这块空间的时候,c就再次被Add使用,以此类推
但这只是VS上的存在,如果是其他编译器,则就可以能是随机值了,不帮你补存了。
也许大家可能会不信,说,怎么这么荒谬,为什么不是随机分配地址,来,那么我们来验证一下
void func() {
int c = 0;
cout << &c << endl;
}
int main(){
func();
func();
func();
}
那么,是不是只有同一个函数才能共用原来的空间呢?
void func1() {
int c = 0;
cout << &c << endl;
}
void func2() {
int b = 1;
cout << &b << endl;
}
int main(){
func1();
func2();
func1();
func2();
return 0;
}
答案,显然是不是的,因为func1()与func2()两个函数所需要的空间是一样大的,所以系统就给他们匹配的是同样的空间了
说了那么多,想要得出的结论就是,出了作用域,返回对象就要销毁的,就不能用引用返回,否则结果就是不确定的
那么应该如何防止引用变野呢?
使用静态变量作引用返回
因为静态变量并不在栈区,所以就算你的栈区空间怎么闹也影响不到我
静态变量的一个特性就是静态变量只会被初始化一次,第一次调用的时候就会初始化了,但可以给值多次。
上两串验证一下:
int& func1(int a,int b){
static int c = a + b;
return c;
}
int& func1(int a,int b){
static int c ;
c = a + b;
return c;
}
int main(){
//依次调用两个不同的func1函数
int& ret = func1(1,2);
cout<< ret<<endl;
func1(3,4);
cout<<ret<<endl;
//依次调用上述两种不同的func1函数
//第一次是3,第二次是3
//第一次是3,第二次是7
return 0;
}
第一次是因为一开始就给它初始化了,即static int c = a + b ; 你第二次进去不可能在初始化了,所以只能是两次3。
但是,第2块函数是static int c ; c= a+b ; 不同的是,他并没有再次初始化,只是进行赋值而已。
常引用
情况一:权限放大
下面给出代码,看看是否可以运行
int a = 0;
int &b = a;
const int a =0;
int &b = a; //???
第一种情况是可以的,但是第二种情况不可。
这就涉及到引用的一个重要性质:
取别名的原则是一个,不能放大权限,可以平移
但是第二种就是不行了,因为加上const后,a就不能被修改了,如果此时用b来做a的别名,也就意味着a是可以修改的,显然是不行的
其本质上就是做了一次,权限放大,把a变成了可以修改的值,缩小可以,放大可不行。
所以要做出更改的话
const int a = 0;
const int &b = a;
int &e = 10;
此种情况就不可以了,因为10只是常量,而你赋予了e 可读可写的功能,与常量冲突,所以不行,这是属于权限的放大。
情况二:权限缩小
int c = 20;
const &d = c;
此种情况是可以的,c是可读可写,我d变成了只可读,但并没有超出我原来的名字c的权限,所以是可以的,这就是权限的缩小。
情况三:权限平移
const int &e = 10;
此种权限是可以的,可以给常量别名,常量是不可变的,此种变换取别名是权限的平移
情况四:啥事也没有
const int a = 10;
int b = a;
a是常量没问题,此处是将a的值拷贝给b,但其并不是权限的放大,b的改变并不影响a,也就是a能不能修改并不关b什么事的,所以,没事的,朋友。
情况五:跨类型引用
首先我们先看一次常识性东西
int i=1;
double j = i;
这个只是普通的类型转换
但再看看这个
int i = 1;
double& rj = i;
这个在编译过程中是不允许的;
原因并不是它类型不一样,而是出现在类型转换上
始终记得,类型转换,无论是隐式类型转换也好,还是强制类型转化,中间都会产生一个临时变量 而临时变量是一个常量,所以归根起来还是权限问题;
所以我们想要改正的时候只需:
int i = 1;
const double j = i;
指针和引用底层浅谈
引用在语法上并不开空间,但也只是在语法上
底层实现中是开的,这个用反汇编就能证明
int a = 0;
int& b = a;
int* ptr = &a;
很明显,其跟用指针在底层逻辑的实现过程是一样的。而且开的空间跟指针一样,即占用空间为4/8,引用变量大小跟类型而定,char为1,int为4……
但我们在使用过程中依旧会认为其是没有开空间的。
以上便是本次博客的学习内容,内容较浅,还请各位大佬指点,如果内容有误,还望各位大佬指出小生的问题之处,以便斧正,谢谢浏览阅读!