learn C++ NO.2 ——认识引用、auto关键字

1.引用

1.1 引用的概念

引用并不是定义一个新的变量,而是给已经存在的变量起的一个别名。从语言的层面上,编译器并不会为了引用而去开辟新的内存空间。引用和被它引用的变量是共用一块内存空间的。举个生活中引用的例子,西游记中,孙悟空,他的别名有很多。像齐天大圣、弼马温、美猴王等。下面通过一个简单的代码看看什么是引用。

#include<iostream>

using namespace std;

int main()
{
    
    
	int a = 10;
	int& ra = a;

	cout << &a << endl;
	cout << &ra << endl;

	return 0;
}

在这里插入图片描述
运行上一段代码可以看见,引用的变量和被引用的变量是共享同一块空间的。那么对ra++是否影响a呢?答案是会影响的。

#include<iostream>

using namespace std;

int main()
{
    
    
	int a = 10;
	int& ra = a;
	
	ra++;

	cout << a << endl;

	return 0;
}

在这里插入图片描述
在这里插入图片描述
需要注意的是引用的类型和被引用的类型需要保持一致,这里简单举一个样例。

int main()
{
    
    
	double d = 1.23;
	int& ri = d;//这样引用是错误的
	return 0;
}

在这里插入图片描述

1.2 引用的特性

1、引用在定义时,必须初始化。
2、一个变量可以有多个引用。
3、一个引用一旦引用了一个实体就不能再继续引用其他实体。

引用在定义时,必须初始化。否则编译器也不知道你具体是谁的别名。

int main()
{
    
    
	double a = 10;
	int& ri;//这样引用是错误的
	return 0;
}

在这里插入图片描述

int main()
{
    
    
	int a = 5;
	int& b = a;
	int& c = b;
	int& d = a;

	cout << a << endl;
	cout << b << endl;
	cout << c << endl;
	cout << d << endl;

	return 0;
}

在这里插入图片描述
这里的c虽然是b的别名,但它的本质还是a的别名。
在这里插入图片描述

int main()
{
    
    
	int a = 5;
	int& b = a;
	int& c = b;
	int& d = a;

	int x = 10;
	d = x;//赋值操作,而非引用操作

	return 0;
}

上面的代码中,在前面的基础上多定义了一个变量x,d = x这句代码的意思是将x的值赋给引用变量d,这里不会改变d的指向,d依旧是变量a的别名。所以,这句话后a,b,c,d变量的值都变成10。这也说明了C++中一个引用变量只能引用一个实体。

1.3 引用的使用场景

1.3.1 引用做参数

在之前的C语言学习中,当需要写一个交换两个局部变量值的函数时,通常需要一个两个变量类型的指针变量来做参数。当学习引用之后,便可以感受引用做参数带来的好处。

//指针做参数
void Swap(int* p1, int* p2)
{
    
    
	int tmp = *p1;
	*p1 = *p2;
	*p2 = tmp;
}
//引用做参数
void Swap(int& x, int& y)
{
    
    
	int tmp = x;
	x = y;
	y = tmp;v
}

int main()
{
    
    
	int a = 10,b = 20;
	int x = 3, y = 5;
	Swap(&a,&b);
	Swap(x,y);
	std::cout << a << " " << b << std::endl;
	std::cout << x << " " << y << std::endl;
	return 0;
}

在这里插入图片描述
通过两种方式实现Swap函数的比较可以发现,使用引用做函数的参数和使用指针做函数参数相比,使用引用做函数参数在函数调用和函数的实现都比较方便。因为,在调用时不必传变量的地址,在实现时不再需要解引用操作。所以,引用通常可以用做输出型的参数。引用做参数还可以减少传参时的拷贝从而提升效率。下面我通过举例和原理来进行说明。

#include<iostream>
#include <time.h>

using namespace std;

struct A {
    
     int a[100000]; };
void TestFunc1(A a) {
    
    }
void TestFunc2(A& a) {
    
    }
void TestRefAndValue()
{
    
    
	A a;
	// 以值作为函数参数
	size_t begin1 = clock();
	for (size_t i = 0; i < 10000; ++i)
		TestFunc1(a);
	size_t end1 = clock();
	// 以引用作为函数参数
	size_t begin2 = clock();
	for (size_t i = 0; i < 10000; ++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;
}

在这里插入图片描述
从上述样例中可以看到,当传参为大对象(即sizeof值较大的对象)时,在函数栈帧开辟时,传结构体拷贝实参的性能损耗较大。而传引用,传的是结构体的别名,拷贝的损耗几乎可以忽略。所以,引用做参数传参的效率是更高的。

1.3.2 引用做返回值

在讲引用做返回值之前,我先简单普及一个概念。就是函数的返回值是怎么带回的。
在这里插入图片描述
引用做函数返回值就相当于上图中的返回值n是调用处ret的别名。

int& Add(int x, int y)
{
    
    
	int n = x + y;
	return n;
}

int main()
{
    
    
	int& ret = Add(1, 2);
	cout << ret << endl;
	return 0;
}

在这里插入图片描述

在这里插入图片描述
但是,这段代码是错误的,因为局部变量n随时函数栈帧的销毁,所属空间使用权归还给操作系统。这里获取到的数是3是恰好的,因为Add函数的栈帧没有被破坏。如果加上一次函数调用,那么原属于Add函数的栈帧会用于调用其它函数,从而导致引用返回的变量被覆盖。
在这里插入图片描述

总结

1、任何场景下都可以使用引用做参数。
2、慎用引用做返回值,如果变量出了局部作用域就销毁,使用引用做返回值就有可能会产生不可预知的错误。尽量使用存储在静态区、全局空间、堆区等等出了局部作用域不销毁的变量上做引用返回。

1.4 常引用

常引用就是对引用的变量前加上const修饰,使得引用具有常数性。

1.4.1 常引用的概念

int main()
{
    
    
	int a = 10;
	const int& ra = a;
	//ra++;//常属性变量不能被修改
	a++;
	//由于ra是a的别名,a的改变会影响ra
	cout << ra << endl;
	return 0;
}

在这里插入图片描述

在这里插入图片描述

1.4.2 引用的权限放大问题

样例

int Func()
{
    
    
	int n = 10;
	return n;
}

int main()
{
    
    
	int& ret = Func();
	return 0;
}

在这里插入图片描述

前面我们说到,函数调用结束后,返回值会存在一个临时空间里,然后带回给调用处。这里由于临时变量是具有常属性的。所以不能直接赋给ret,因为涉及引用权限放大问题。此时const修饰一下引用,使引用也具有常属性就可以。
在这里插入图片描述

int Func()
{
    
    
	int n = 10;
	return n;
}

int main()
{
    
    
	const int& ret = Func();
	return 0;
}

在这里插入图片描述

1.4.3 引用的权限平移和缩小

int main()
{
    
    
	//权限的平移
	const int a = 10;
	const int&ra = a;

	int b = 20;
	int& rb = b;

	//权限的缩小
	int* c = NULL;
	const int*& rc = c;

	return 0;
}

1.5 引用和指针的区别

1、定义引用是不需要额外的开辟空间,而定义指针变量是需要额外开辟空间来存储的。
2、引用必须初始化,指针可以不初始化。
3、引用在初始化指向一个实体后,便不能在更改指向。而指针变量指向一个实体后,可以继续更改自己的指向。
4、没有空引用,但是有空指针。
5、 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位平台下占8个字节)。
6、引用自加1,表示引用的实体对象的值加1。指针变量自加1,表示指针变量向后偏移一个自身类型大小的距离。
7、没有多级引用的概念,但是指针是有分多级,如一级指针变量的地址就得用二级指针变量存储。
8、访问实体的方式不同,引用由编译器来处理,指针需要解引用操作。
9、引用和指针比起来,使用起来更加安全。

1.5.1 引用的汇编指令实现

在这里插入图片描述
通过调试可以看到,VS2019编译器下,引用和指针在汇编指令实现的方式是类似的,都是将实体的值通过保存在寄存器上,再拷贝给编译器开辟的特定空间存储。所以,引用在汇编指令的层面上的实现其实是会开辟内存的。但是,在学习引用这个概念的时候还是要从C++的语法层面看待这一现象,即引用是不开辟空间的,这样有助于学习和理解。、

2.auto关键字

auto关键字由c++11标准引入。是一个根据右表达式的类型来推导左表达式类型的关键字。auto在实际中最常见的优势用法就是跟C++11提供的新式for循环,还有lambda表达式等进行配合使用。

2.1 auto关键字的概念

int main()
{
    
    
	int a = 10;
	int b = a;
	auto c = a;
	auto d = 1 + 1.11;
	cout << typeid(c).name() << endl;
	cout << typeid(d).name() << endl;
	return 0;
}

在这里插入图片描述

需要注意的是使用auto关键字必须初始化。如果只是声明,编译器不能推导出他具体的类型。所以auto并不是一种类型的声明,而是类型的‘占位符’。

2.2 auto 与指针和引用

auto自动推导指针类型时,auto后是否带(*)指针标识符都是可以的。但是,auto自动推导为引用时,auto后必须带(&)引用标识符。

int main()
{
    
    
	int a = 10;
	auto* b = &a;//ok
	auto& c = a;//ok
	auto* d = 10;//error
	auto e = &a;//ok
	return 0;
}

在这里插入图片描述

2.3 auto不能推导的场景

2.3.1 auto做函数形式参数

因为编译器无法对auto作为形式参数的类型进行推导。

void test(auto a)
{
    
    
	\\...
}

2.3.2 auto不能定义数组

void TestAuto()
{
    
    
	int a[] = {
    
    1,2,3};
	auto b[] = {
    
    456};
}

3.基于范围的for循环

在C++11之前,遍历一个数组可以用以下方式

int main()
{
    
    
	int array[] = {
    
     1, 2, 3, 4, 5 };

	for (int i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
	{
    
    
		array[i] *= 2;
	}

	for (int* p = array; p < array + sizeof(array) / sizeof(array[0]); ++p)
	{
    
    
		cout << *p << endl;
	}

	return 0;
}

也许是觉得这个语法太麻烦了,c++11给出了一个甜点语法即(范围for)。


int main()
{
    
    
	int array[] = {
    
     1, 2, 3, 4, 5 };
	
	for (auto& e : array)
	{
    
    
		e++;
	}

	for (auto e : array)
	{
    
    
		cout<<e<<" ";
	}
	cout << endl;

	return 0;
}

在这里插入图片描述
当你使用了这种for循环遍历数组,你还会想用C语言的方式来写吗?当然是不会的。不过这种用法下,范围for的迭代条件必须是明确的。对于数组而言,范围for的遍历范围就是从第一个元素到最后一个元素。对于类而言,应该提供begin和end的
方法,begin和end就是for循环迭代的范围。

//错误样例
void test(int arr[])
{
    
    
	for(auto& e : arr)
	{
    
    
		cout<< e<<endl;
	}
}

范围for的范围不确定所以会报错。最后就是迭代的对象要实现++和==的操作。这个由于我现在还没有学习这一块的知识,只能等以后学了才知道。

4.nullptr关键字(C++11)

在c/c++编程的学习过程中,我们不免得要和指针打交道。我们需要养成良好指针使用习惯。那就是初始化指针变量。初始化指针变量的好处可以让指针变量的指向更安全。

int main()
{
    
    
	int *pa =NULL;
	int* pb = 0;
	return 0;
}

而在传统的C语言头文件(stddef.h)中对于NULL这个宏是这样定义的

#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif

在这里插入图片描述

可以看到当以cpp格式编译时,NULL就会被展开成0值,这其实是有些许缺陷的。当然,从语言发展的角度来看,这么做也许是祖师爷的迫于无奈。在C++11标准定义中,nullptr关键字引入,这样也就方便了我们去初始化指针。当然sizeof(nullptr)的大小和sizeof((void*)0)的大小是一样的。为了提高程序的健壮性,推荐使用nullptr关键字。

猜你喜欢

转载自blog.csdn.net/m0_71927622/article/details/130228434