C++学习记录——이 C++入门(2)


1、引用

1、概念

上一篇比较久远的C++笔记末尾提到了引用,这里重新写一下。C语言代码编写中,指针是个很重要的概念,同样也有点复杂。指针有一,二,三级等指针,在初阶数据结构中,比如链表,最一开始为了改变链表结构,我们需要用二级指针来接收。像这种问题,我们很容易搞错。但指针对于代码的编写很有帮助,许多功能也要靠它来完成。所以C++为了解决指针的问题,出现了引用的概念。

引用如果简单来说是一个起别名的作用,不过究其本质,它还是用指针来实现的。创建一个变量a后,引用另一个变量名b,仍然指向a,如果改变了b的值,a的值也改变了,相当于一个标签。

#include <iostream>
using namespace std;

int main()
{
	int a = 1;
	int& b = a;
	int& c = a;

	cout << &a << endl;
	cout << &b << endl;
	cout << &c << endl;
}

在这里插入图片描述

地址完全一样,说明指向的都是同一个地址。使用场景上,除了像这样

在这里插入图片描述
也有在数据结构上,代替指针的。

现在我们继续看其他的使用场景来理解引用。

2、引用特性和使用场景

1、返回值

先看一段代码

int Count()
{
	int n = 0;
	n++;
	return n;
}

int main()
{
	int ret = Count();
	return 0;
}

以及

int Count()
{
	static int n = 0;
	n++;
	return n;
}

n是如何返回的?main函数有main的栈帧,在执行Count函数前,main栈帧里会创建一个ret变量。执行Count函数后,系统也开辟了Count栈帧,里面有一个n变量。n++后,n为1,那么此时n就可以返回了?当n++后,Count函数已经调用完成了,函数栈帧销毁,此时n在之前Count栈帧这块空间里,但是这时候已经销毁了,栈帧销毁也就意味着这块空间已经还给了系统,交给系统自由支配,那么这时候n就无法保证还是之前的值了。系统为了能够得到n的值,会把n的值给一个临时变量,ret去找这个临时变量要值即可。这个临时变量一般在寄存器中运行,在调用Count函数之前,就在main的栈帧里创建这个临时变量。

加上static后,n变量是在静态区的,这个和Count栈帧没关系,所以不会存在上述问题,但是计算机懒得判断,虽然是静态区,但还是先给一个临时变量,再由临时变量把值返回过去。

所以这样是不是太傻瓜了,有点多此一举。明明在静态区,干嘛还要再创建一个变量?在之前函数传参的时候也可以用引用,用引用是否可以解决这个问题?

int& Count()
{
	static int n = 0;
	n++;
	return n;
}

int main()
{
	int ret = Count();
	cout << ret << endl;
	return 0;
}

当用引用来返回值的时候,程序返回的就是一个引用类型的变量,这个引用是n的引用。所以当函数结束的时候,系统就会通过传过来的n的引用来得到n的值,而不需要创建临时变量,这样的函数传返回值是传引用调用,这里也体现引用的一个特性

减少拷贝

看下一段代码

#define N 10
typedef struct Array
{
	int a[N];
	int size;
}AY;

int& At(AY& ay, int i)
{
	assert(i < N);
	return ay.a[i];
}

int main()
{
	AY ay;
	for (int i = 0; i < N; i++)
	{
		At(ay, i) = i * 4;
	}
	for (int i = 0; i < N; i++)
	{
		cout << At(ay, i) << " ";
	}
	cout << endl;
	return 0;
}

在这里插入图片描述
返回的是ay.a[i]的引用,我们可以直接对它进行赋值。这也就是引用的第二个特性

可修改返回对象

在前面写的函数结束,销毁栈帧中可以知道n此时已经归系统管理了,我们无法控制这个n,而后面两个引用的例子中,静态区是一个单独的区域,不受影响,main里的局部变量则更不受影响,只要我们不删除代码。所以引用的条件就是

引用的对象必须是操作者可以控制的对象

如果不是,那么此时它的值可就不确定了。不确定是一个问题,但主要的错误就是我们没有权限访问这个变量。栈帧销毁,空间已经还给系统,变量确实还在那个空间里,我们貌似也确实可以给它赋值,操作它,但这就形成了非法访问。非法访问编译器不一定会报错,但其不确定性很影响代码的实现。所以如果用到引用,就要先观察变量的走向。

关于之前不能使用引用的例子,我们再看一段错误代码

int& Add(int a, int b)
{
	int c = a + b;
	return c;
}

int main()
{
	int& ret = Add(1, 2);
	Add(3, 4);
	cout << "Add(1, 2) is :" << ret << endl;
	cout << "Add(1, 2) is :" << ret << endl;
	return 0;
}
int& ret = Add(1, 2);

这行代码,程序调用了Add函数,Add栈帧空间里有个c变量,数值为3,当函数结束时,空间还给系统,c也是,这时候c完全由系统管理,我们也不清楚系统做了什么。函数结束的时候给我们传回了c的引用,然后用一个引用类型的ret变量来接收,也就是说返回了c的别名,ret又是c的别名,所以c是多少,ret就是多少。再次调用Add,c应当为7,然后还是一样,交由系统全权管理,我们还是不知道系统会干什么,所以下面两个输出不同人的电脑会呈现不同的结果,这取决于系统,比如我的就是随机值

在这里插入图片描述

当出了函数的这个作用域,对于main函数内部来说,这就是另一个空间的变量,相当于我们创建了两个函数,都有各自的变量,只不过这里是引用,我们可以访问到它,但是结果是未定义的,编译器会报错未定义。这就是错误使用引用的例子。

2、传值、传引用效率对比

这个其实没什么可写的,结果就是引用更快,不过也就是相差几毫秒,可以用下面两段代码进行测试。

#include <time.h>
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 < 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;
}
#include <time.h>
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;
}

3、引用和指针的区别

1、权限可小不可大
int main()
{
	int a = 1;
	int& b = a;

	const int c = 2;
	int& d = c;
}

这段代码中,c那里会出错。

引用和指针都有一个特性,赋值或者初始化的时候,权限可以缩小,但不可以放大。

c已经被限制,不能更改,但是d却没有限制,这就是权限放大。指针也一样

	const int* a = NULL;
	int* b = a;

也不能给到b。想解决这个问题那么d和b前面就要加上const,这叫做权限保持。如果c是正常,d是受const限制,那么没有问题,因为这是权限缩小。

int Count()
{
	int n = 0;
	n++;
	return n;
}

还是这个Count函数,main函数里这样写

int& ret = Count();

无法编译通过。返回的不是n,是临时变量,这个变量其实是有常属性的,不能被改变,所以int&之前加一个const就行。另外的解决办法就是让Count函数变成int&类型。

下一个代码

	int i = 0;
	double& j = i;

编不过,但是前面加上一个const就好了。这是因为类型转换的时候会产生临时变量。强制类型转换也是如此,强制转换的变量还是那个类型,但是打印出来的只是临时变量。不管是显式还是隐式,转换时产生的都是临时变量,原变量没有变。

2、关于引用空间的简单理解

引用在语法上来讲,是不开空间的,和其引用实体共用一块空间,而指针则是开空间的。

如果以底层实现来讲,引用是按照指针形式实现的。

int main()
{
	int a = 10;
	int& b = a;
	b = 20;

	int* pa = &a;
	*pa = 20;

	return 0;
}

同样操作的指针和引用,我们查看一下反汇编。

在这里插入图片描述

仔细看,引用和指针在底层实现的代码是一样的。int& b = a那里,a的值给了rax,下一行取出b的地址,把rax的值放进去;b = 20,编译器会把b的地址给到rax,下一行解引用rax,把20这个值的16进制形式给rax。下面也一样,所以这样简单地查看后会发现,引用也是按照指针形式做的,只是语法上我们可以看作是不开空间,起别名。

3、其他区别

引用在定义时必须初始化,指针无要求

引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体

没有NULL引用,但有NULL指针

在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32 4, 64 8)

引用自加,即引用的实体加1,指针自加即指针向后偏移一个类型的大小

有多级指针,但没有多级引用

访问实体方式不同,指针需要显式解引用,引用编译器自己处理

引用比指针使用起来更安全

2、内联

1、内联与宏的对比

C语言中,有宏这个概念。

在C++中,一般用const和enum替代宏常量,inline去替代宏函数。

我们先看宏,宏有一些缺点,不能调试,没有类型安全的检查,有些场景下实现起来也非常复杂。所以C++要解决这个问题。

宏函数是用宏写函数,C++用inline来替代它。宏函数是不开栈帧的,它是直接替换,所以也不能调试。C++则这样写

inline int Add(int x, int y)
{
	int z = x + y;
	return z;
}

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

这个就是内联。内联是在使用函数的时候编译器自己优化一下代码,然后在使用处展开,但不会有栈帧,可以调试。这就避开了宏的缺点。在release模式下可查看内联的反汇编。

但是显而易见,内联有缺点。内联是一种空间换时间的做法,在某处展开代码,如果有多处使用函数的地方,那么代码量就会增加很多,编译出来的可执行程序就会变大。

这里借用《C++ primer》的一段话

内联说明只是向编译器发出的一个请求,编译器可以选择忽略这个请求

一般来说,内联机制用于优化规模较小、流程直接、频繁调用的函数。很多编译器都不支持内联递归函数,而且一个很多行的函数也不大可能在调用点内联地展开

2、声明和定义分离

内联不建议声明和定义分离,会导致链接错误。

func.h

#include <iostream>
using namespace std;

inline void f(int i);

func.cpp

#include "func.h"

void f(int i)
{
	cout << i << endl;
}

test.cpp

#include "func.h"

int main()
{
	f(1);
	return 0;
}

结果就会这样报错

在这里插入图片描述
如果不加inline,是一个很正常的程序。程序运行的时候,main函数里调用了f这个函数,在反汇编中就是一个call指令,后面跟一个地址,通过这个地址可以跳到一个jmp指令,而函数的地址也就是jmp指令的地址,jmp前面括号里的地址,找到这个地址,也就找到了函数。

但是内联函数不需要调用地址等等,内联只是要直接展开,不开辟栈帧,所以在上面程序中,我们调用的时候就只会展开一个声明,找不到定义,而且在程序汇编完后进入链接阶段后,内联函数不产生地址,所以链接无法通过。

下一篇继续写C++入门的知识。

结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值