前言

本篇分享两个内容—— 引用和inline函数。

1、引用的概念

在我们日常生活里,我们会给某些事物取别名,比如我们《西游记》里面的孙悟空我们常称为猴哥,猴子…我们在看《西游记》时,我们叫猴哥,都知道是代表的孙悟空。

而在C++里面也有一个操作符能够实现该类似的效果——&(引用)。

引用就是给一个变量取别名。它是给已存在变量取了⼀个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同⼀块内存空间。

代码示例如下:

#include<iostream>
using namespace std;
int main()
{
	int a = 10;
	int &b = a;
	//a的地址和b的地址是一样的
	cout << "&a=" << &a << endl;
	cout << "&b=" << &b << endl;
	//a的值和b的值是一样的
	cout << "a=" << a << endl;
	cout << "b=" << b << endl;
	return 0;
}

1.1 引用的特点

1.引用必须要初始化。

2.一个变量可以取多个别名,就像孙悟空的别名有猴子,猴哥等等。

int main()
{
	int a = 10;
	//int& b;错误必须要初始化
	int& b = a;
	int& c = a;
	int& d = a;
	//abcd的地址是一样的--一个变量可以有多个引用
	cout << "&a=" << &a << endl;
	cout << "&b=" << &b << endl;
	cout << "&c=" << &c << endl;
	cout << "&d=" << &d << endl;
	//abcd的值是一样的
	cout << "a=" << a << endl;
	cout << "b=" << b << endl;
	cout << "c=" << c << endl;
	cout << "d=" << d << endl;
	
	return 0;
}

3.引用一旦成为一个变量的别名,就不能成为其他变量的别名。

int main()
{
	int a = 10;
	int c = 0;
	int& b = a;
	b = c;//一个别名只能对应一个变量(引用不会改变指向) 这里是赋值操作 相当于b=0 
	cout << "a=" << a << endl;//a=0
	cout << "b=" << b << endl;//b=0
	return 0;
}

1.2引用的意义

引用在一些场景能够优化代码,提高效率,提高代码可读性,下面我们提高例子来认识。

1.引用可以做函数参数,做为形参可以影响实参

void Swap(int& a, int& b)//a就是a1的别名对a的改变就是对a1的改变 b同理
{
	int tmp = a1;
	a1 = a2;
	a2 = tmp;
}

int main()
{
	int a1 = 10;
	int a2 = 20;
	//交换前
	cout << "交换前" << endl;
	cout << "a1=" << a1 << endl;
	cout << "a2=" << a2 << endl;
	//a1=10 a2=20

	Swap(a1, a2);

	//交换后
	cout << "交换后"<< endl;
	cout << "a1=" << a1 << endl;
	cout << "a2=" << a2 << endl;
	//a1=20 a2=10
	return 0;
}

2.提供代码可读性,减少拷贝

void swap(SL** pa, SL** pb)
{
	SL** tmp = pa;
	*pa = *pb;
	*pb = *tmp;
}
void swap(SL*& pa, SL*& pb)//提供代码可读性 pa就是指向a地址的指针 对pa操作会影响实参
{
	SL* tmp = pa;
	pa = pb;
	pb = tmp;
}
int main()
{	
	SL a;
	SL b;
	SL* pa = &a;
	SL* pb = &b;
	//交换前
	cout << pa << endl;
	cout << pb << endl;
	swap(pa, pb);
	//交换后
	cout << pa << endl;
	cout << pb << endl;
		return 0;
}

例如:下面运用引用,在单链表中,可以避免使用二级指针,提高代码可读性。
如下:pphead就是plist的别名,对pphead的改变就是对plist的改变。

typedef struct ListNode
{
	int data;
	struct ListNode* next;
}LTNode, * PLTNode;
//相当于 typedef struct ListNode LTNode
//相当于 typedef struct ListNode *PLTNode

//void LTNodePushBack(LTNode** pphead, int x) 

//void LTNodePushBack(PLTNode& pphead, int x)//PLTNode等价于LTNode*
void LTNodePushBack(LTNode*& pphead, int x)
{
	PLTNode newnode = (PLTNode)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		//...
	}
	else
	{
		newnode->data = x;
		newnode->next = NULL;
	}
}

int main()
{
	LTNode* plist = NULL;
	LTNodePushBack(plist,1);
	return 0;
}

2.减少拷贝,提高效率

typedef struct A
{
	int arr[100];
}A;
void func(SL& a)//引用能够减少拷贝
{
}
void func(SL* a)//指针也能减少拷贝
{
}
int main()
{
	//减少拷贝,提供效率	
	SL a;
	func(a);
	func(&a);
	return 0;
}

  1. 引用做返回值,减少拷贝,提高效率
  2. 引用做返回值,修改返回对象
#include<assert.h>
typedef struct SeqList
{
	int* arr;
	int size;
	int capacity;
}SL;

void SLInit(SL* phead, int n = 4)
{
	phead->arr = (int*)malloc(n * sizeof(int));
	phead->capacity = phead->size = 0;
}
void SLPushBack(SL& sl, int x)
{
	//扩容
	sl.arr[sl.size] = x;
	sl.size++;
}

int& SLFind(SL& sl, int i)
{
	assert(i < sl.size);
	return sl.arr[i];
}
int SLFind(SL* sl, int i)
{
	assert(i < sl->size);
	return sl->arr[i];
}

int main()
{
	SL sl;
	SLInit(&sl);
	SLPushBack(sl, 1);
	SLPushBack(sl, 2);
	SLPushBack(sl, 3);
	SLPushBack(sl, 4);

	int ret1 = SLFind(sl, 1);
	cout << ret1 << endl;
	cout << ++SLFind(sl, 1) << endl;//传引用返回的可以修改

	int ret2 = SLFind(&sl, 1);
	cout << ret1 << endl;
	cout << ++SLFind(sl, 1) << endl;//传值返回的不可以修改 临时变量具有常性
}

如图所示:
传值返回的临时变量是上不能修改的,
传引用返回产生的别名是可以修改的。

在这里插入图片描述

注意:用引用返回时,减少了代码的拷贝,但更加需要关注变量是否是局部的,是否被销毁。

int& func()
{
	static int ret = 10;
	return ret;
}

int main()
{
	int& x = func();
	return 0;
}

在这里插入图片描述

1.3 const引用

const引用主要关注的是,变量的权限问题,权限可以缩小,但是不能放大。

如下:

int main()
{
	int a = 0;
	int& x = a;
	//权限可以缩小
	const int& y = a;//变量y被const修饰,不能通过改变y来改变a

	const int b = 0;
	//int& y = b;//err 权限不能放大 b变量自己都不能改变 更何况他的别名y呢	     	    正确的:const int& r = b;

	int z = y;
	//权限可以缩小
	const int* p1 = &y; //const修饰y,p1不能改变y,权限缩小
	// 权限不能放大
	 //int* p2 = p1; 不能通过p2改变y的值 正确的:	const int* p2 = p1;
	int* p3 = &z;
	// 权限可以缩小 
	const int* p4 = p3; //const修饰p3所指的z 不能通过p4改变z的值
	return 0;
}

除此之外,还有一些特别的例子
1.不同类型转化时,转化为另一种类型时产生临时变量
2.一些*,/等操作符时,会将计算完之后的值储存在临时变量里面在赋值
由于临时变量具有常性,所以在形参处需要加const修饰,否则会涉及权限的放大

void func(const int& r)
{

}

int main()
{
	int x = 1;
	const int y = 2;
	double d = 1.1;

	const int& r1 = x;
	const int& r2 = y;
	const int& r3 = 10;

	const int& r4 = x * 10;
	const int& r5 = d;

	func(x);
	func(y);
	func(r4);
	//传的x*10 是一个临时变量 x*10先计算保持在一个临时变量里面在赋值给r4 所以形参需要const修饰
	func(x*10);
	//传的d会转化为int类型 把它存在一个临时变量里面 也需要const修饰形参
	func(d);
	return 0;
}

1.4 引用和指针的关系

指针和引⽤的关系
1.C++中指针和引⽤就像两个性格迥异的亲兄弟,指针是哥哥,引⽤是弟弟,在实践中他们相辅相成,功能有重叠性,但是各有⾃⼰的特点,互相不可替代。

2。语法概念上引⽤是⼀个变量的取别名不开空间,指针是存储⼀个变量地址,要开空间。

3.引⽤在定义时必须初始化,指针建议初始化,但是语法上不是必须的。

4.引⽤在初始化时引⽤⼀个对象后,就不能再引⽤其他对象;⽽指针可以在不断地改变指向对象。

5.引⽤可以直接访问指向对象,指针需要解引⽤才是访问指向对象。

6.sizeof中含义不同,引⽤结果为引⽤类型的⼤⼩,但指针始终是地址空间所占字节个数(32位平台下占4个字节,64位下是8byte)

7.指针很容易出现空指针和野指针的问题,引⽤很少出现,引⽤使⽤起来相对更安全⼀些。

这里等等写引用和指针的底层关系结合inline

inline——内联函数,在调用处直接展开,直接建立函数栈帧,不需要call该函数的地址,提高效率

在c语言里面,我们写一个宏函数,可能会因为某些细节出现很多问题。
例如,我们写一个ADD宏函数,需要考虑以下细节

// 宏函数
// 缺点:复杂/类型安全检查/不能调试
// 优点:预处理阶段替换,不用建立栈帧,本质是一种提效。

//#define ADD(int a, int b) return a + b; err 宏函数定义不需要类型

//#define ADD(a, b) a + b;
if (ADD(10, 20) > 0) //err 相当于 if ( a + b; > 0)  会出现报错


//#define ADD(a, b) a + b
cout<<ADD(1,2)*5<<endl; //err 相当于 1+2*5 得不到预期结果


//#define ADD(a, b) (a + b)
int x = 1, y = 2;
ADD(x & y, x | y); // ->(1 & 2 + 2 | 1)  运算符优先级问题 得不到预期结果

//正确的:
#define ADD(a,b) ((a)+(b))

//由此可见,宏函数对细节的把控要很到位
//而inline函数则不需要,就相当于写一个普通的Add函数
//inline函数也可以达到宏函数的效果
inline int Add(int a, int b)
{
	return a + b;
}
int main()
{
	int r = Add(10, 20);
	cout << r << endl;
	return 0;

1用inline修饰的函数叫做内联函数,编译时C++编译器会在调⽤的地⽅展开内联函数,这样调⽤内联函数就直接建⽴栈帧了,就可以提⾼效率。
注意:vs2022需要自己调设置成inline展开
在这里插入图片描述

这里通过实际代码和汇编代码来展示。

#include<iostream>
int Add(int x, int y)
{
	int ret = x + y;
	return ret;
}
using namespace std;
int main()
{
	int r = Add(10, 20);
	cout << r << endl;
	return 0;
}

这个代码的汇编如下:我们看到在调用函数后,会出现call指令(跳转到执行函数的代码),随后会进行Add函数栈帧,如图所示。
在这里插入图片描述
采用inline展开

inline int Add(int x, int y)
{
	int ret = x + y;
	return ret;
}
using namespace std;
int main()
{
	int r = Add(10, 20);
	cout << r << endl;
	return 0;
}

汇编指令如下:
在这里插入图片描述
通过对比可以知道,在一次调用Add函数时,inline展开(在编译阶段完成)的指令小于非inline的情况,也就是说在这种情况下,inline展开能够体现减少代码提高效率的优势。

既然inline函数这么好,那么我们所有的函数都要用inline展开吗?
答:错误。

我们来看一个例子
在这里插入图片描述
当代码数量足够多时,会出现以下情况,为什么呢?

这是因为inline对于编译器⽽⾔只是⼀个建议,也就是说,你加了
inline编译器也可以选择在调⽤的地⽅不展开,不同编译器关于inline什么情况展开各不相同,因为C++标准没有规定这个。

inline适⽤于频繁调⽤的短⼩函数,对于递归函数,代码相对多⼀些的函数,加上inline也会被编译器忽略。

这里我们来解释其原因,这是理论分析。

在这里插入图片描述
1.inline函数的声明定义也与普通函数不同
下面通过代码来举例
在这里插入图片描述
运行该代码时,会出现链接错误,代码中在 main 函数里调用了 f1(int) 函数,但链接器(Linker)在链接所有目标文件(.obj)时,找不到 f1 函数的定义。——具体解释看下面解释图
在这里插入图片描述
在这里插入图片描述

除此之外,我们不能想到一个问题,如果这个inline函数在.h文件里面因为函数语句过多导致编译器使其不变成inline函数呢,不会因为该函数在预处理阶段在两个.cpp文件同时存在,不会包重定义错误吗?
如下:
在这里插入图片描述

这里就不得不提一个规定了。
C++ 标准规定:inline 函数可以在多个编译单元中存在完全相同的定义,不会报重定义错误。
此时,即使编译器没把 f1 内联展开,它依然是带 inline 关键字的函数,因此遵循「inline 函数的多定义规则」,多个文件包含头文件时,不会报重定义错误。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐