• 详解c++---入门(下)


    一.引用

    水浒传这个名著大家应该非常的熟悉,里面有许多生动形象的角色,比如说宋江,李逵等等但是大家如果看的比较多的话会发现这些角色一般都有其他的称号,比如说宋江在这篇名著里面又称为及时雨,李逵在这里面又称为铁牛或者黑旋风,虽然这里有多种不同的称呼但是这些称呼却是指向的同一个人,我们就把这些不同的名称称为外号,在我们生活也有许许多多的外号,我本人叫做叶超凡在初中的时候我们班的人都喜欢叫我炒饭因为超凡这两个字的音跟炒饭特别的像,那在我们的c++里面有外号吗?答案是有的但是我们不叫外号叫引用。

    引用的形式


    这就是我们的引用的基本格式,这里的类型得和被引用的类型一致,当然看到这里想必大家还是不知道我们的引用的使用方式,所以接下来我们来通过几个例子来理解理解。

    引用的例子

    首先我们来创建一个整型的变量a并且将这个变量的值赋值为10,那么我们这里的代码就是这样:

    int main()
    {
    	int a = 10;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    接下来我们就要给这个变量a来取一个别名,因为变量a是一个整型所以我们这里的类型处就填一个int,然后再加上一个取地址符号(&),因为这里取的是a这个变量的别名所以我们这里引用的名字就不能和他相同,那我们这里引用的名字就取ra,再加上一个等于号,等于号的右边就是本体也就是我们上面创建变量a,那么这里的代码就是这样:

    #include
    int main()
    {
    	int a = 10;
    	int& ra = a;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    那么这里的ra就是变量a的别名,我们一开始创建一个整型大小的空间,用一个变量名a来指向这个空间,然后这里的ra就是变量a的别名,他也指向a指向的空间,那么当这个空间有两个不同的变量名的话,我们改变一个变量名的值时,另外一个变量名的值也会跟着改变,比如说下面的代码:

    #include
    int main()
    {
    	int a = 10;
    	int& ra = a;
    	a++;
    	std::cout <<"ra的值为:"<< ra;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    我们来看看运行的结果:
    在这里插入图片描述
    我们发现当我们对a的值加一,ra的值也跟着加一了,这就是因为ra是变量a的别名他们都指向同一块空间,当其中一个变量对该空间的数据进行更改时,该空间的其他引用变量的值也会跟着发生改变。

    引用的一个例子

    我们在初次学习c语言的时候写过这么一个函数这个函数的功能就是交换两个变量的值,我们说函数里面的变量是创建的临时变量对这些临时变量进行改变是不会影响我们的实参的,所以我们这里就得传地址过去这样我们才能改变函数外实参的值比如说下面的代码:

    #include
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    我们将这个代码运行一下就可以发现这两个变量在函数内进行了交换但是在函数外部却没有发生交换:
    在这里插入图片描述
    所以我们这里就将参数改成指针的形式这样我们外面的参数也可以发生改变:

    #include
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    但是大家有没有发现我们这里指针的形式就十分的麻烦,每个参数前都要加个解引用操作符,所以我们这里就可以将我们这里的引用加入进去,因为引用也是一种类型所以我们这里的参数类型可以改成引用:

    void swap(int& x, int& y)
    
    • 1

    既然我们这里参数的类型是引用的话,那么我们这里的x和y就分别是变量a和b的别名,他们都指向的是同一块空间所以我们这里就可以在函数里面直接通过这里的x和y来改变外面的a和b的值,那么我们这里的完整的代码就如下:

    void swap(int& x, int& y)
    {
    	int tem = x;
    	x = y;
    	y = tem;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    那么这样看的话我们这里的使用就简单了不少,当然在我们学习的过程中还遇到过这么一个场景就是在链表的学习过程中我们在main函数中创建了一个phead指针这个指针的作用就是指向这个链表的第一个元素,所以当我们对这个链表进行头插的时候就会对应的改变这个头指针的值,因为这个phead本来就是一个指针所以我们想要在函数的外面改变这个值的话就得传二级指针过去,那我们函数的声明就是这样:

    void SListPushBack(struct ListNode** phead, int x);
    
    • 1

    但是当我们学习了引用之后我们就可以给这个指针取一个别名,这样我们在函数里面使用的时候也不用对其进行解引用,那我们这是声明就成了这样:

    void SListPushBack(struct ListNode*& phead, int x);
    
    • 1

    那么我们第一种形式传过来的phead是指针变量的地址,而第二种形式的phead传过来的是指针变量,那这就是两种传递方式的区别。

    引用的特性

    第一点:引用在定义的时候必须初始化。
    我们在创建一些普通的变量的时候是可以不对其初始化的,这是他里面装的就是一些随机值,比如说我们下面的代码:

    #include
    int main()
    {
    	int a;
    	double b;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们没有对这些值进行初始化但是我们在运行的过程中,编译器却没有报错,并且我们通过调试发现这时这两个变量里面放的都是随机值:
    在这里插入图片描述
    但是我们引用却不能这样,我们的引用在创建的时候就必须对其进行初始化,如果不初始化就会报错:

    int main()
    {
    	int a;
    	double b;
    	int& ra;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们看看下面的提示就可以发现我们这里的引用必须得对其进行初始化,如果不初始化就会报错
    在这里插入图片描述
    第二点:一个变量可以有多个引用。
    既然我们可以跟一个变量取别名的话,那么这个别名就可以不止一个,比如说我们下面的代码:

    #include
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    我们这里创建了一个变量a将其值赋值为10,然后给这个变量取了多个引用变量,当我们对a的值进行修改之后,这些引用变量的值也就都随之改变:
    在这里插入图片描述
    第三点:引用变量一旦引用一个实体就不能再引用其他实体。
    有小伙伴可能会有这么一个问题就是当我们这里创建了两个变量a和b,当我们给这里的变量a取了一个引用变量ra之后,我们能不能对其进行修改将这里的ra指向变量b,让ra成为变量b的别名呢?答案是不行的,但是有小伙伴写出了这样的代码却发现我们这里好像可以引用其他的实体:

    #include
    int main()
    {
    	int a = 10;
    	int b = 20;
    	int& ra = a;
    	ra = b;
    	std::cout << ra;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    将这个代码运行之后发现这里的ra的值变成了20,跟我们的b一模一样,那这是不是说明我们这里ra就成功的指向了b呢?其实并不是我们这里再将a值进行更改就可以发现这里ra的值也同样的发生了更改,比如说下面的代码:

    #include
    int main()
    {
    	int a = 10;
    	int b = 20;
    	int& ra = a;
    	ra = b;
    	std::cout << ra<<std::endl;
    	ra++;
    	std::cout << ra;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    在这里插入图片描述
    这就说明我们这里的ra依然是变量a的引用,那上面的代码在打印的时候为什么会出现打印和b一样的值的情况呢?原因很简单ra = b并不是将引用ra指向b的内存空间,而是将b的值赋值给ra,所以这里就会打印出20,那我们这里引用之所以不能再引用其他实体的原因也非常的简单就是因为我们这里会引发歧义就是ra = b到底是把b的值给他还是把b的空间给他这个是不明确的,所以就会出现上面的情况。

    引用使用的场景

    引用做参数

    当我们函数的参数是引用的话,我们是可以在函数的内部来修改形参的方式来修改函数外的实参,比如说我们之前写的swap函数当该函数的参数是引用时我们可以直接在函数里面来交换函数外两个变量的值:

    void swap(int& x, int& y)
    {
    	int tem = x;
    	x = y;
    	y = tem;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    当然当我们的引用作为参数的时候也可以构成重载,但是大家要注意的一点就是因引用而构成的重载在调用的时候很容易引发歧义,比如说下面的代码:

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    我们将这个代码运行一下就可以发现我们这里却是构成重载(因为参数的类型不同)
    在这里插入图片描述

    但是我们在调用这个函数的时候却发现这里的重载引发了歧义:
    在这里插入图片描述
    所以大家在以引用作为参数的时候得考虑一下因调用时引发的歧义问题。

    引用做返回值

    我们的引用还可以来做函数的返回值,比如说我们下面的两端代码:

    int count1()
    {
    	static int n = 0;
    	n++;
    	return n;
    }
    int count2()
    {
    	int x = 0;
    	x++;
    	return x;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    这是我们创建的两个函数,这两个函数唯一的不同就是创建变量的时候一个放到了内存中的静态区(n)一个放到了函数的栈区(x),但是我们在主函数里面创建对应类型变量a和b来接收这两个函数的返回值时我们发现a和b的值是一模一样的:

    #include
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述
    我们这里函数的返回类型是int,那如果我们将这里的返回类型改成int类型的引用呢?我们再来看看返回的结果如何:

    #include
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    我们来看看这段代码的运行结果如何:
    在这里插入图片描述
    我们发现当我们将返回的类型改成引用的时候,我们这里打印出来的值就大有不同,那这里的原因就是因为我们用引用变量来接收的时候,该变量依然会指向函数里面创建的那个变量的空间,那这里的a指向的就是n的空间,这里的b指向的就是x的空间,但是随着函数的结束,这些函数在栈帧上创建的变量就会随之销毁,比如说count2里面的变量x就会在函数结束的时候进行销毁,该空间里面装的内容也就会随之改变,但是引用b却依然指向该空间,所以当我们打印b的值时就会出现奇奇怪怪的内容,而我们的引用a却不一样,因为a指向的是n的空间,而n是以静态变量进行创建的,他不会随着函数的结束而销毁,所以他打印出来的值依然为1 。那么根据上面的例子我们可以发现一个结论就是:如果函数返回时出来函数作用域,如果返回变量不存在了,就不能使用引用返回,因为引用返回的结果未定义,如果出了函数作用域,返回变量还存在则可以使用引用返回。这里大家还可以看看下面的代码:

    #include
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    我们将这个代码运行一下就可以发现一个非常奇妙的事情就是:
    在这里插入图片描述
    一开始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,那么通过这里的代码大家应该能够发现我们这里的引用返回所带来的一些可能的错误。

    传值和传引用的效率对比

    以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。所以传值和传引用的效率就会有所不同,比如说我们下面的代码:

    #include 
    #include
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    那么我们这里代码的意思就是调用两个函数,这两个函数的参数类型不同一个是传值一个是传引用,参数就是一个结构体该结构体里面是一个整型的数组,并且这个数组的容量是1w,然后我们调用100w次函数,然后比较这两个函数调用的时间,那么我们将这个代码运行一下就可以发现我们这里传引用的效率要比传值的效率高很多:
    在这里插入图片描述

    值和引用的作为返回值类型的性能比较

    其实我们的函数在结束的时候并不是直接将函数里面的变量作为返回值,而是将你想要返回的变量拷贝一份,再将你拷贝的变量放到你主函数进行其他的操作,比如说我们下面的代码:

    #include
    int func()
    {
    	int a = 10;
    	return 10;
    }
    int main()
    {
    	int c = func();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    我们这个func函数在结束的时候是返回a的值,然后在主函数里面创建一个变量c来接收这个函数的返回值,但是我们这里并不是将a直接赋值给c就好比这样:int c =a;而是会创建一个空间,并且这个空间里面装的是跟变量a一样的数据,然后再将这个空间里面的数据赋值给我们这里的c,所以函数在返回的时候也会进行一次数据的拷贝也会消耗性能,所以我们这里就来比较一下: 值和引用的作为返回值类型的性能比较,那么这里的思路就跟我们上面的代码差不多:

    #include 
    #include
    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    在这里插入图片描述
    我们可以看到效率也是差别的非常的的大,那么这里就是我们值和引用的作为返回值类型的性能比较。

    常引用

    我们上面在用引用的时候是这么进行创建的:

    #include
    int main()
    {
    	int a = 10;
    	int& ra = a;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们这样进行初始化是什么问题都没有的,但是我们之前学过const,我们如果在创建变量的时候在前面加上const的话,这个变量是无法进行修改的,那我们的引用能不能加上const呢?答案是可以的,比如说上面的ra在创建的时候就可以在前面加上const:

    #include
    int main()
    {
    	int a = 10;
    	const int& ra = a;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    那根据const的性质我们知道这里的ra的值是无法改变的,但是我们的a能够发生改变吗?答案是可以的,比如说我们下面的代码:

    #include
    using namespace std;
    int main()
    {
    	int a = 10;
    	const int& ra = a;
    	a++;
    	cout << a << endl;
    	cout << ra<< endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    我们这里就可以对a进行更改,并且ra的值也跟着随之改变:
    在这里插入图片描述
    但是我们这里却不能直接对ra进行操作:

    #include
    using namespace std;
    int main()
    {
    	int a = 10;
    	const int& ra = a;
    	ra++;
    	cout << a << endl;
    	cout << ra << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    我们这里运行的时候就发现下面报出了错误:
    在这里插入图片描述
    那么我们上面的这个操作就称之为权限的缩小,我们的本体不是const修饰的变量,但是我们给这个本体创建的引用却是const修饰的所以我们这里就交权限的缩小,既然有缩小那也就有权限的平移,那这个就很好的理解如果我们的本体没有被const修饰,而他创建的引用变量也没有const修饰那这就是权限的平移,同样的道理如果本体被const修饰了,而他创建的引用变量也被const修饰那这也是权限的平移,比如说下面的代码:

    int main()
    {
    	int a = 10;
    	int ra = a;
    	const int b = 20;
    	const int& rb = b;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    那么我们这里的两个引用就是权限的平移。既然平移有了权限的缩小也有了权限的平移那对应的也就应该有权限的放大,那这个也就非常的好理解,当我们的本体是const修饰的话,我们给他创建的引用变脸却不是const修饰的,那这样的话我们就称为权限的放大,比如说下面的代码:

    int main()
    {
    	const int a = 10;
    	int& ra = a;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    这就属于权限的放大,在我们的语法规则中是不允许权限的放大的所以我们将上述代码运行起来就可以看到报出了错误:
    在这里插入图片描述
    那么看了上面的介绍大家应该可以总结一条规律出来就是我们在引用初始化的时候,权限可以缩小或者不变,但是不能放大,当然我们这里是在创建引用变量时情况是这样的,当然在其他背景下的情况也是如此比如说我们的函数传参:如果我们的函数在声明当中,参数是没有const修饰的,而我们在调用函数时传的却是const修饰的变量的话,那这就属于权限的放大,比如说下面的代码:

    void func(int& a)
    {
    	;
    }
    int main()
    {
    	const int b = 10;
    	func(b);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    func中的引用变量a没有用const修饰,而我们调用时传的却是const修饰的变量这样的话就属于权限的放大,我们可以看看这段代码运行之后的报出的错误:
    在这里插入图片描述
    所以当我们以后写函数时,函数本身不会对参数进行修改的话我们就在参数的前面加上const来进行修饰,这样的话我们就不会出现因传参时而导致的权限放大的问题,因为使用这个函数的人,他可能会传const修饰的变量也可能会传普通的没有const修饰的变量,所以这么要么出现权限的缩小,要么出现权限的平移不会出现权限放大的问题,因为我们不知道使用者会传什么样的参数所以我们这里就只好加上一个const来修饰我们的参数,这样不管使用者怎么传都不会出现问题,当然如果在这个函数里面要对这些参数进行修改的话我们就不能加const,这里大家要注意一下,不是所有的函数都要加const来修饰参数。我们的引用在初始化的时候可以用一个变量来对其初始化,其实也可以使用一个常量来对其进行初始化,但是必须得在引用的前面加上一个const,因为常量具有常性他是不允许被修改的,所以得加const,比如说我们下面的代码:

    #include
    int main()
    {
    	const int& a = 10;
    	const int& b = 20;
    	std::cout << a << std::endl;
    	std::cout << b << std::endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述
    如果不加const的话我们这里就会报出错误:
    在这里插入图片描述
    那知道这一点之后我们来思考一个问题就是我们的引用在作为参数的时候可以作为缺省参数吗?就好比这样:

    #include
    void func(int& a = 10)
    {
    	std::cout<<a<<std::endl;
    }
    int main()
    {
    	func();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    答案好像是不行的,因为我们运行这段代码的时候发现报出了错误:
    在这里插入图片描述
    但是这里并不是不能作为缺省参数,而是他在作为缺省参数的时候必须得加const来进行修饰道理很简单我们上面就说了,那我们的代码就成了这样:

    #include
    void func(const int& a = 10)
    {
    	std::cout<<a<<std::endl;
    }
    int main()
    {
    	func();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    在这里插入图片描述
    同样的道理我们再来思考一个问题就是我们在创建一个变量的时候,可以用不同的变量来进行初始化比如说,我先创建了一个double类型的变量b将其值赋值为3.8,然后我又创建了一个整型的变量a,但是我在对其进行初始化的时候是可以用b来a进行初始化的,比如说我们下面的代码:

    #include
    int main()
    {
    	double b = 3.8;
    	int a = b;
    	printf("%d", a);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在这里插入图片描述
    那问题来了我们的引用也可以像上面那样用不同类型的值来进行初始化吗?比如说下面的代码:

    #include
    int main()
    {
    	double b = 3.8;
    	int& a = b;
    	printf("%d", a);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们来看看这段代码的运行结果:
    在这里插入图片描述
    有些小伙伴们看到这个错误就觉得我们的引用不能像上面那样用不同类型的值来进行初始化,可事实是这样的吗,我们再在引用变量的前面加个const看看能否成功:

    #include
    int main()
    {
    	double b = 3.8;
    	const int& a = b;
    	printf("%d", a);
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们运行一下发现是可以成功的:
    在这里插入图片描述
    那这里之所以加个const修饰就可以成功的原因就是我们这里是不同形式的赋值,会出现所谓的截断现象,而这个阶段并不是将这里b的值直接改成了3,而是再创建出来一个临时空间,这个空间里面装的被截断之后的值也就是3,最后将这个临时空间的值赋值给我们这里的a,但是问题就出在我们这个创建出来的临时空间具有常性他是不能被修改的,所以我们不加const修饰的话就会出现权限放大的情况,所以就会出现上述的错误,当然不止这种情况会创建出来临时的空间我们在使用强制类型转换操作符的时候也会出现类似的情况:

    int main()
    {
    	double b = 3.8;
    	int a = (int)b;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们这里在强制类型转化的时候也会创建出来一个临时空间这个空间里面放着变量b被强制类型转换之后的结果,我们的函数也是这样,当我们的函数要返回一个值的时候也会创建出来一个临时空间这个空间装的就是你要返回的值或者变量的值,那么这些临时空间都是有常性的不能被改变,比如说下面的代码:

    int func()
    {
    	int a = 10;
    	return a;
    }
    int main()
    {
    	int& b = func();
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    我们这里在用引用变量来接收这个函数的返回值时就会报错,因为在接收的时候这个函数的栈帧已经销毁了赋值给引用变量b的是一块临时空间。所以我们也得在前面加上一个const这样才不会报错。

    引用和指针的区别

    1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
    2. 引用在定义时必须初始化,指针没有要求。
    3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体。
    4. 没有NULL引用,但有NULL指针。
    5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)。
    6. 引用自加即引用的实体增加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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    1. 有多级指针,但是没有多级引用。
    2. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理。
    3. 引用比指针使用起来相对更安全。

    二.内联函数

    为什么会有内联函数

    大家都知道函数在调用的时候会创建函数栈帧,但是栈帧的创建时会消耗性能的,而且当我们的函数非常的简单,而你却要开辟栈帧的话是不是就十分的浪费性能啊,尤其是哪种还要大量的调用的小函数,所以我们c语言就给出了一个解决办法就是宏,他可以直接在使用处进行替换,并不会调用函数创建栈帧,但是我们的宏他也是有缺点的啊比如说:

    1.不方便调试宏。(因为预编译阶段进行了替换)
    2.导致代码可读性差,可维护性差,容易误用。
    3.没有类型安全的检查 。

    所以为了解决上述的问题,我们的c++就给出了一个新的概念内联函数。

    内联函数的概念

    以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率。

    内联函数的使用方法

    那根据上面的概念我们知道这里就直接在函数的声明前面加个inline就可以了,比如我们下面的代码:

    inline int add(int left, int right)
    {
    	return left + right;
    }
    int main()
    {
    	int ret = 0;
    	ret = add(1, 2);
    	return 0}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    我们直接在函数的生面的最前面加个inline就可以了。

    如何来判断我们这里确实没有调用函数

    我们首先来看看不是内联函数的调用的汇编代码长什么样,那么下面是我们的代码:

    int add(int left, int right)
    {
    	return left + right;
    }
    int main()
    {
    	int ret = 0;
    	ret = add(1, 2);
    	return 0}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    我们来转到反汇编的页面:
    在这里插入图片描述
    我们看到这里使用了call指令,根据之前的学习我们知道这个call指令就是函数调用的意思,那么这里出现了call指令那这就说明我们这里调用了函数,创建了函数的栈帧,那我们再来看看上面的内联函数的反汇编的代码如何:
    在这里插入图片描述
    我们发现这个跟上面的不是内联函数的一模一样,那这是为什么呢?因为我们这是在debug的环境下,我们程序员得在这个环境下得进行调试啊,所以当我们这里进行替换的话我们就不好进行调试了,所以我们这里就得对环境做出一下改变:
    在这里插入图片描述
    当我们把环境改成这样之后我们在进行反汇编就可以看到成了这样:
    在这里插入图片描述
    这时就没有call这条指令,那也就说明我们这里没有进行函数的调用。

    特性

    第一点:
    inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建
    议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不
    是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
    第二点:
    inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会
    用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运
    行效率。那这里我们可以这么理解,当我们的一个函数里面的代码有70行,我们这里要调用1w次这个函数,如果我们这里的函数可以展开的话我们这里就会变成70w行代码,而如果我们不对其进行展开的话我们这里也就只有1w加70行代码,而代码越多我们占的内存也就越多也就是我们所谓的安装包越大。
    第三点:
    .inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址
    了,链接就会找不到。所以我们这里建议把内联函数放到头文件里面。这样就不会链接问题。

  • 相关阅读:
    许可分析 license分析 第十一章
    湖南省副省长秦国文一行调研考察亚信科技
    SpringBoot3自动配置流程及原理、SpringBootApplication注解详解
    开源项目在线化 中文繁简体转换/敏感词/拼音/分词/汉字相似度/markdown 目录
    (论文阅读31/100)Stacked hourglass networks for human pose estimation
    【图神经网络论文整理】(七)—— Graph Transformer Networks:GTNs
    Spring MVC的常用注解
    [C++基础]-继承
    HTML期末学生大作业——基于html+css+javascript+jquery+bootstrap响应式音乐酒吧网站
    Spark中宽依赖、窄依赖、Job执行流程
  • 原文地址:https://blog.csdn.net/qq_68695298/article/details/127518511