C++引用,野引用,常引用简易介绍使用和复习

目录

1,引用就是取别名

2,引用必须初始化

3,引用的作用之一,传参数

野引用

先来个拷贝复制的预热

引用作为返回值

使用静态变量作引用返回

常引用

情况一:权限放大

情况二:权限缩小

情况三:权限平移

情况四:啥事也没有

情况五:跨类型引用

指针和引用底层浅谈


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……

但我们在使用过程中依旧会认为其是没有开空间的。

以上便是本次博客的学习内容,内容较浅,还请各位大佬指点,如果内容有误,还望各位大佬指出小生的问题之处,以便斧正,谢谢浏览阅读!

  • 32
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值