1.引用的情势
这就是我们的引用的根本格式,这里的类型得和被引用的类型同等。否则这里会出现错误好比下面的代码:
- #include<iostream>
- using namespace std;
- int main()
- {
- double d = 3.14;
- int& a = d;
- cout << a << endl;
- return 0;
- }
复制代码 对不同的类型举行引用便会出现报出这样的错误:
那么要想办理这个题目就得利用常量应用来办理,这个内容本文章后面会举行介绍。
引用的第一个例子
起首创建一个整型的变量a而且将这个变量的值赋值为10
- int main()
- {
- int a = 10;
- return 0;
- }
复制代码 接下来我们就要给这个变量a来取一个别名,因为变量a是一个整型以是这里的类型处就填一个int,然后再加上一个取地址符号(&),因为这里取的是a这个变量的别名以是引用的名字就不能和他相同,那我们这里引用的名字就取ra,再加上一个等于号,等于号的右边就是本体也就是我们上面创建变量a
- #include<iostream>
- int main()
- {
- int a = 10;
- int& ra = a;
- return 0;
- }
复制代码 那么这里的ra就是变量a的别名,我们一开始创建一个整型大小的空间用来存放变量a假设空间的地址为001EFBD0,用变量名a来指向该空间,那么这里的ra就是a的别名ra也能代表空间001EFBD0,以是我们改变一个变量名的值时,另外一个变量名的值也会跟着改变,因为他们指向的是同一个空间,空间的内容变了,那么ra和a的值肯定也会跟着发生改变,好比说下面的代码:
- #include<iostream>
- int main()
- {
- int a = 10;
- int& ra = a;
- a++;
- std::cout <<"ra的值为:"<< ra;
- return 0;
- }
复制代码 我们来看看运行的结果:
我们发现当我们对a的值加一,ra的值也跟着加一了,这就是因为ra是变量a的别名他们都指向同一块空间。
引用的第二个例子
swap函数相比大家已经见过或者写过不少,我们说函数的参数是拷贝过来的称之为形参而调用函数时用的参数我们称之为实参,形参时实参的拷贝以是形参的改变不会影响实参,以是要想在函数内里改变表面的实参就得通过传地址的情势好比说下面的代码:
- #include<stdio.h>
- void swap(int a, int b)
- {
- int temp = a;
- a = b;
- b= temp;
- printf("函数里a的值为:%d\n", a);
- printf("函数里b的值为:%d\n", b);
- }
- int main()
- {
- int a = 10;
- int b = 20;
- swap(a, b);
- printf("a的值为:%d\n", a);
- printf("b的值为:%d\n", b);
- return 0;
- }
复制代码 我们将这个代码运行一下就可以发现这两个变量在函数内举行了交换但是在函数外部却没有发生交换:
以是我们这里就将参数改成指针的情势这样我们表面的参数也可以发生改变:
- #include<stdio.h>
- void swap(int* a, int* b)
- {
- int temp = *a;
- *a = *b;
- *b = temp;
- }
- int main()
- {
- int a = 10;
- int b = 20;
- swap(&a, &b);
- printf("a的值为:%d\n", a);
- printf("b的值为:%d\n", b);
- return 0;
- }
复制代码 那么知道引用之后我们是不是可以利用引用来实现swap函数
- void swap(int& x, int& y)
复制代码 这里参数的类型是引用,那么x和y就分别是变量a和b的别名,他们都指向的是同一块空间以是我们这里就可以在函数内里直接通过这里的x和y来改变表面的a和b的值:
- void swap(int& x, int& y)
- { int tem = x; x = y; y = tem;}
复制代码 那么这样看的话我们这里的利用就简朴了不少,当然在我们学习的过程中还碰到过这么一个场景就是在链表的学习过程中我们在main函数中创建了一个phead指针这个指针的作用就是指向这个链表的第一个元素,以是当我们对这个链表举行头插的时间就会对应的改变这个头指针的值,因为phead本来就是一个指针以是我们想要在函数的表面改变这个值的话就得传二级指针过去,那我们函数的声明就是这样:
- void SListPushBack(struct ListNode** phead, int x);
复制代码 但是当我们学习了引用之后我们就可以给这个指针取一个别名,那我们这是声明就成了这样:
- void SListPushBack(struct ListNode*& phead, int x);
复制代码 第一种传过来的phead是指针变量的地址,而第二种情势的phead传过来的是指针变量的别名,不管哪种都可以对函数外的指针变量举行修改,第一种是通过解引用的情势,第二种是通过直接修改的情势,那么显而易见还是第二种更加的简朴。
2.引用的特性
第一点:引用在界说的时间必须初始化。
我们在创建一些平凡的变量的时间是可以不对其初始化的,这是他内里装的就是一些随机值,好比说我们下面的代码:
- #include<stdio.h>
- int main()
- {
- int a;
- double b;
- return 0;
- }
复制代码 我们没有对这些值举行初始化但是我们在运行的过程中,编译器却没有报错,而且我们通过调试发现这时这两个变量内里放的都是随机值:
但是引用不能这样引用在创建的时间就必须对其举行初始化,假如不初始化就会报错:
- int main()
- {
- int a;
- double b;
- int& ra;
- return 0;
- }
复制代码 我们看看下面的提示就可以发现我们这里的引用必须得对其举行初始化,假如不初始化就会报错
第二点:一个变量可以有多个引用。
可以给一个变量取别名的话,那么别名就可以不止一个好比说下面的代码:
- #include<iostream>
- int main()
- {
- int a = 10;
- int& ra = a;
- int& rra = a;
- int& rrra = a;
- int& rrrra = a;
- int& rrrrra = a;
- a++;
- std::cout << ra << std::endl;
- std::cout << rra << std::endl;
- std::cout << rrra << std::endl;
- std::cout << rrrra << std::endl;
- std::cout << rrrrra << std::endl;
- return 0;
- }
复制代码 我们这里创建了一个变量a将其值赋值为10,然后给这个变量取了多个引用变量,当我们对a的值举行修改之后,这些引用变量的值也就都随之改变:
第三点:引用变量一旦引用一个实体就不能再引用其他实体。
有小搭档可能会有这么一个题目我们这里创建了两个变量a和b,当我们给这里的变量a取了一个引用变量ra之后,我们能不能对其举行修改将这里的ra指向变量b让ra成为变量b的别名呢?答案是不行的,引用变量一旦引用一个实体就不能再引用其他实体,好比说下面的代码:
- #include<iostream>
- using namespace std;
- int main()
- {
- int a = 10;
- int b = 20;
- int& ra = a;
- cout << &a << endl;
- cout << &ra << endl;
- cout << &b << endl;
- ra = b;
- cout << a << endl;
- cout << ra << endl;
- cout << b << endl;
- cout << &a << endl;
- cout << &ra << endl;
- cout << &b << endl;
- return 0;
- }
复制代码 运行的结果如下:
3.引用利用的场景
引用做参数
当函数的参数是引用的话,是可以在函数的内部来修改形参的方式来修改函数外的实参,好比说我们之前写的swap函数当该函数的参数是引用时我们可以直接在函数内里来交换函数外两个变量的值:
- void swap(int& x, int& y)
- { int tem = x; x = y; y = tem;}
复制代码 当然当我们的引用作为参数的时间也可以构成重载,但是大家要留意的一点就是因引用而构成的重载在调用的时间很轻易引发歧义,好比说下面的代码:
- void swap(int x, int y)
- {
- int tem = x;
- x = y;
- y = tem;
- }
- void swap(int &x, int& y)
- {
- int tem = x;
- x = y;
- y = tem;
- }
- int main()
- {
- int a = 10;
- int b = 20;
- return 0;
- }
复制代码 我们将这个代码运行一下就可以发现我们这里却是构成重载(因为参数的类型不同)
但是我们在调用这个函数的时间却发现这里的重载引发了歧义:
以是大家在以引用作为参数的时间得思量一下因调用时引发的歧义题目。
引用做返回值
引用还可以来做函数的返回值,好比说下面的两端代码:
- int count1()
- {
- static int n = 0;
- n++;
- return n;
- }
- int count2()
- {
- int x = 0;
- x++;
- return x;
- }
复制代码 这是创建了两个函数,两个函数唯一的不同就是创建变量的时间一个放到了内存中的静态区(n)一个放到了函数的栈区(x),在主函数内里创建对应类型变量a和b来接收这两个函数的返回值时可以看到a和b的值是一模一样的:
- #include<iostream>int count1()
- {
- static int n = 0;
- n++;
- return n;
- }
- int count2()
- {
- int x = 0;
- x++;
- return x;
- }
- int main(){ int a = count1(); int b = count2(); std::cout << a << std::endl; std::cout << b << std::endl; return 0;}
复制代码
函数的返回类型是int,那假如我们将这里的返回类型改成int类型的引用呢?再来看看返回的结果如何:
- #include<iostream>
- int& count1()
- {
- static int n = 0;
- n++;
- return n;
- }
- int& count2()
- {
- int x = 0;
- x++;
- return x;
- }
- int main()
- {
- int& a = count1();
- int& b = count2();
- std::cout << a << std::endl;
- std::cout << b << std::endl;
- return 0;
- }
复制代码 我们来看看这段代码的运行结果如何:
我们发现当我们将返回的类型改成引用的时间,这里打印出来的值就大有不同,原因就是因为我们用引用变量来接收的时间,该变量依然会指向函数内里创建的那个变量的空间,那这里的a指向的就是n的空间,这里的b指向的就是x的空间,但是随着函数的结束这些函数在栈帧上创建的变量就会随之烧毁,好比说count2内里的变量x就会在函数结束的时间举行烧毁,该空间内里装的内容也就会随之改变,但是引用b却依然指向该空间,以是当我们打印b的值时就会出现奇希奇怪的内容,而引用a却不一样,因为a指向的是n的空间而n是以静态变量举行创建的,他不会随着函数的结束而烧毁,以是他打印出来的值依然为1 。那么根据上面的例子我们可以发现一个结论就是:函数的调用结束时,假如返回变量的生命周期结束了就不能利用引用返回,因为引用时变量的别名变量的生命走起都结束了别名也会跟着结束,假如出了函数作用域返回变量还存在则可以利用引用返回。这里大家还可以看看下面的代码:
- #include<iostream>
- using namespace std;
- int& Count()
- {
- int n = 0;
- cout << &n << endl;
- n++;
- return n;
- }
- void Func()
- {
- int x = 100;
- cout << &x << endl;
- }
- int main()
- {
- int& ret = Count();
- cout << ret << endl;
- cout << ret << endl;
- Func();
- cout << ret << endl;
- cout << &ret << endl;
- return 0;
- }
复制代码 我们将这个代码运行一下就可以发现一个非常奇妙的事情就是:
一开始ret的值是1,但是我们再一次打印这里ret的值时却打印了一个随机值,然后调用一下func函数之后我们再打印一下ret的值时却又酿成了100,那这是怎么回事呢?我们这里就可以一步一步的分析:起首第一次打印出现了1 这个就非常的好明白,我们将count的返回值用来作为ret的初始化,而conut函数返回的是变量n而且还是引用类型,以是我们这里打印的ret的值就是变量n对应的那个空间内里装的值,既然这里打印的是1的话那么这就说明我们count函数虽然结束了,但是并没有将这个空间的值举行改变,而第二次打印这个ret的值是随机值的原因就是:cout也是一个函数的调用,但是在利用cout之前我们这里会先取出ret对应的值,然后再将该值传给cout函数来举行该函数调用,以是第一次我们能够正常的打印出1,但是n对应的空间颠末cout函数栈帧的创建和烧毁之后就改成了其他的值,以是第二次再打印的时间就酿成了其他的值,再调用func时创建了一个变量x将其值初始化为100,但是变量x对应的空间是原来n的空间,而且函数调用之后这个空间的数据跟原来的一样并没有被修改,以是当我们再次打印ret的值时就又酿成了100,那么通过这段代码大家应该能够发现我们这里的引用返回所带来的一些可能的错误。
4.传值和传引用的服从对比
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接通报实参或者将变量本身直接返回,而是通报实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型服从好坏常低下的,好比说我要通报一个二叉树,这个树内里有一万个数据,那么在调用函数举行传参时就得将这一个个数据全部举行拷贝还得维持他们之前的逻辑结构以是服从就十分的低,以是在调用函数举行传参时我们更加乐意利用传引用而不是传值,好比说我们下面的代码:
- #include <time.h>
- #include<iostream>
- using namespace std;
- struct A
- {
- int a[10000];
- };
- void TestFunc1(A a)
- {
- }
- void TestFunc2(A& a)
- {
- }
- void TestRefAndValue()
- {
- A a;
- // 以值作为函数参数
- size_t begin1 = clock();
- for (size_t i = 0; i < 1000000; ++i)
- TestFunc1(a);
- size_t end1 = clock();
- // 以引用作为函数参数
- size_t begin2 = clock();
- for (size_t i = 0; i < 1000000; ++i)
- TestFunc2(a);
- size_t end2 = clock();
- // 分别计算两个函数运行结束后的时间
- cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
- cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
- }
- int main()
- {
- TestRefAndValue();
- return 0;
- }
复制代码 这里代码的意思就是调用两个函数,并用clock函数来记录两段传参所耗费的时间吗,两个函数的参数类型不同一个是传值一个是传引用,参数就是一个结构体该结构体内里是一个整型的数组,而且这个数组的容量是1w,然后我们调用100w次函数,然后比力这两个函数调用的时间,这里代码运行一下就可以发现传引用的服从要比传值的服从高许多:
5.值和引用的作为返回值类型的性能比力
其实我们的函数在结束的时间并不是直接将函数内里的变量作为返回值,而是将你想要返回的变量拷贝一份老师成一个临时变量,再将临时变量拷贝到主函数举行其他的利用,好比说下面的代码:
- #include<iostream>
- int func()
- {
- int a = 10;
- return 10;
- }
- int main()
- {
- int c = func();
- return 0;
- }
复制代码 func函数在结束的时间返回a的值,然后在主函数内里创建一个变量c来接收这个函数的返回值,但是我们这里并不是将a直接赋值给c就好比这样:int c =a;而是会创建一个空间,而且这个空间内里装的是跟变量a一样的数据,然后再将这个空间内里的数据赋值给我们这里的c,以是函数在返回的时间也会举行两次数据的拷贝也会斲丧性能,以是我们这里就再来比力一下: 值和引用的作为返回值类型的性能比力:
- #include <time.h>
- #include<iostream>
- using namespace std;
- struct A
- {
- int a[10000];
- };
- A a;
- // 值返回
- A TestFunc1()
- {
- return a;
- }
- // 引用返回
- A& TestFunc2()
- {
- return a;
- }
- void TestReturnByRefOrValue()
- {
- // 以值作为函数的返回值类型
- size_t begin1 = clock();
- for (size_t i = 0; i < 100000; ++i)
- TestFunc1();
- size_t end1 = clock();
- // 以引用作为函数的返回值类型
- size_t begin2 = clock();
- for (size_t i = 0; i < 100000; ++i)
- TestFunc2();
- size_t end2 = clock();
- // 计算两个函数运算完成之后的时间
- cout << "TestFunc1 time:" << end1 - begin1 << endl;
- cout << "TestFunc2 time:" << end2 - begin2 << endl;
- }
- int main()
- {
- TestReturnByRefOrValue();
- return 0;
- }
复制代码
我们可以看到服从也是差异的非常的的大,那么这里就是我们值和引用的作为返回值类型的性能比力。
6.常引用
我们上面在用引用的时间是这么举行创建的:
- #include<iostream>
- int main()
- {
- int a = 10;
- int& ra = a;
- return 0;
- }
复制代码 通过上面的学习我们知道通过对引用的修改是可以改变原变量的值的,那如原变量是const修饰的常量呢?这里依然能够举行修改吗?很显然是不能的,以是平凡的引用是不能指向常量,以是c++给出了另外一个东西就是常引用,情势就是引用前面加一个const:
- #include<iostream>
- int main()
- {
- int a = 10;
- const int& ra = a;
- return 0;
- }
复制代码 根据const的性质我们知道这里ra的值是无法改变的,但是a的值是可以的,好比说下面的代码:
- #include<iostream>
- using namespace std;
- int main()
- {
- int a = 10;
- const int& ra = a;
- a++;
- cout << a << endl;
- cout << ra<< endl;
- return 0;
- }
复制代码 我们这里就可以对a举行更改,而且ra的值也跟着随之改变:
但是我们这里却不能直接对ra举行利用:
- #include<iostream>
- using namespace std;
- int main()
- {
- int a = 10;
- const int& ra = a;
- ra++;
- cout << a << endl;
- cout << ra << endl;
- return 0;
- }
复制代码 我们这里运行的时间就发现下面报出了错误:
那利用常引用对平凡变量举行引用时就可以称之为权限的缩小,变量本体不是const修饰的变量,但是我们给这个本体创建的引用却是const修饰的以是我们这里就叫权限的缩小,既然有缩小那也就有权限的平移,那这个就很好的明白假如我们的本体没有被const修饰,而他创建的引用变量也没有const修饰那这就是权限的平移,同样的原理假如本体被const修饰了,而他创建的引用变量也被const修饰那这也是权限的平移,好比说下面的代码:
- int main()
- {
- int a = 10;
- int ra = a;
- const int b = 20;
- const int& rb = b;
- return 0;
- }
复制代码 那么我们这里的两个引用就是权限的平移。既然平移有了权限的缩小也有了权限的平移那对应的也就应该有权限的放大,那这个也就非常的好明白,当我们的本体是const修饰的话,我们给他创建的引用变脸却不是const修饰的,那这样的话我们就称为权限的放大,好比说下面的代码:
- int main()
- {
- const int a = 10;
- int& ra = a;
- return 0;
- }
复制代码 这就属于权限的放大,在我们的语法规则中是不允许权限的放大的以是我们将上述代码运行起来就可以看到报出了错误:
那么看了上面的介绍大家应该可以总结一条规律出来就是我们在引用初始化的时间,权限可以缩小或者不变,但是不能放大,当然我们这里是在创建引用变量时情况是这样的,当然在其他配景下的情况也是如此好比说我们的函数传参:假如我们的函数在声明当中,参数是没有const修饰的,而我们在调用函数时传的却是const修饰的变量的话,那这就属于权限的放大,好比说下面的代码:
- void func(int& a)
- {
- ;
- }
- int main()
- {
- const int b = 10;
- func(b);
- }
复制代码 func中的引用变量a没有用const修饰,而我们调用时传的却是const修饰的变量这样的话就属于权限的放大,我们可以看看这段代码运行之后的报出的错误:
以是当我们以后写函数时,函数本身不会对参数举行修改的话我们就在参数的前面加上const来举行修饰,这样的话就不会出现因传参时而导致的权限放大的题目,因为利用这个函数的人,他可能会传const修饰的变量也可能会传平凡的没有const修饰的变量,以是这里要么出现权限的缩小,要么出现权限的平移不会出现权限放大的题目,因为我们不知道利用者会传什么样的参数以是我们这里就只好加上一个const来修饰参数,这样不管利用者怎么传都不会出现题目,当然假如在这个函数内里要对这些参数举行修改的话我们就不能加const,这里大家要留意一下,不是所有的函数都要加const来修饰参数。我们的引用在初始化的时间可以用一个变量来对其初始化,其实也可以利用一个常量来对其举行初始化,但是必须得在引用的前面加上一个const,因为常量具有常性他是不允许被修改的,以是得加const,好比说我们下面的代码:
- #include<iostream>
- int main()
- {
- const int& a = 10;
- const int& b = 20;
- std::cout << a << std::endl;
- std::cout << b << std::endl;
- return 0;
- }
复制代码
假如不加const的话我们这里就会报出错误:
7.引用的留意事项
引用和缺省参数
我们来思考一个题目就是引用在作为参数的时间可以作为缺省参数吗?就好比这样:
- #include<iostream>
- void func(int& a = 10)
- {
- std::cout<<a<<std::endl;
- }
- int main()
- {
- func();
- return 0;
- }
复制代码 答案好像是不行的,因为我们运行这段代码的时间发现报出了错误:
但是这里并不是不能作为缺省参数,而是他在作为缺省参数的时间必须得加const用常引用来接受值,因为缺省参数都是常量,好比下面的代码:
- #include<iostream>
- void func(const int& a = 10)
- {
- std::cout<<a<<std::endl;
- }
- int main()
- {
- func();
- return 0;
- }
复制代码
不同类型之间的引用
我先创建了一个double类型的变量b将其值赋值为3.8,然后我又创建了一个整型的变量a,但是我在对其举行初始化的时间是可以用b来a举行初始化的,好比说我们下面的代码:
- #include<stdio.h>
- int main()
- {
- double b = 3.8;
- int a = b;
- printf("%d", a);
- return 0;
- }
复制代码
那题目来了我们的引用也可以像上面那样用不同类型的值来举行初始化吗?好比说下面的代码:
- #include<stdio.h>
- int main()
- {
- double b = 3.8;
- int& a = b;
- printf("%d", a);
- return 0;
- }
复制代码 我们来看看这段代码的运行结果:
有些小搭档们看到这个错误就觉得我们的引用不能像上面那样用不同类型的值来举行初始化,可事实是这样的吗,我们再在引用变量的前面加个const看看能否成功:
- #include<stdio.h>
- int main()
- {
- double b = 3.8;
- const int& a = b;
- printf("%d", a);
- return 0;
- }
复制代码 我们运行一下发现是可以成功的:
这里之以是加个const修饰就可以成功的原因就是我们这里是不同情势的赋值,会出现所谓的截断征象,而这个阶段并不是将这里b的值直接改成了3,而是再创建出来一个临时空间,这个空间内里装的被截断之后的值也就是3,末了将这个临时空间的值赋值给我们这里的a,但是题目就出在我们这个创建出来的临时空间具有常性他是不能被修改的,以是我们不加const修饰的话就会出现权限放大的情况,以是就会出现上述的错误,当然不止这种情况会创建出来临时的空间我们在利用强制类型转换利用符的时间也会出现雷同的情况:
- int main()
- {
- double b = 3.8;
- int a = (int)b;
- return 0;
- }
复制代码 这里在强制类型转化的时间也会创建出来一个临时空间这个空间内里放着变量b被强制类型转换之后的结果
引用和函数返回值
我们的函数也是这样,当函数要返回一个值的时间也会创建出来一个临时空间这个空间装的就是你要返回的值或者变量的值,那么这些临时空间都是有常性的不能被改变好比说下面的代码:
- int func()
- {
- int a = 10;
- return a;
- }
- int main()
- {
- int& b = func();
- return 0;
- }
复制代码 我们这里在用引用变量来接收这个函数的返回值时就会报错,因为在接收的时间这个函数的栈帧已经烧毁了赋值给引用变量b的是一块临时空间,该空间中装的时返回值,以是我们也得在前面加上一个const这样才不会报错。
8.引用和指针的区别
- 引用概念上界说一个变量的别名,指针存储一个变量地址。
- 引用在界说时必须初始化,指针没有要求。
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时间指向任何一个同类型实体。
- 没有NULL引用,但有NULL指针。
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
- 引用自加即引用的实体增长1,指针自加即指针向后偏移一个类型的大小的地址。
- int main()
- {
- int a = 10;
- int& ra = a;
- cout<<"&a = "<<&a<<endl;
- cout<<"&ra = "<<&ra<<endl;
- return 0;
- }
- int main()
- {
- int a = 10;
- int& ra = a;
- ra = 20;
- int* pa = &a;
- *pa = 20;
- return 0;
- }
复制代码
- 有多级指针,但是没有多级引用。
- 访问实体方式不同,指针必要显式解引用,引用编译器本身处理。
- 引用比指针利用起来相对更安全。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |