[C++基础](2) 引用|内联函数|auto|范围for|nullptr

本文详细介绍了C++中的引用,包括其概念、特性、常引用以及在参数和返回值中的应用。接着讨论了内联函数的原理、优点和注意事项,并对比了引用与指针的异同。此外,还探讨了C++11引入的新特性,如auto关键字的用法、基于范围的for循环和nullptr的使用。
摘要由CSDN通过智能技术生成

引用

引用的概念与特性

引用就是给已有的变量取别名,编译器不会给引用开辟新的空间,它和对应的变量共用同一块空间。对引用的操作和对变量直接操作完全一样。

定义方式类型& 引用变量名(对象名) = 引用实体;

注意

  • &符号也可以表示取地址,这里表示引用
  • 引用类型引用实体必须是同类型的。

🌰例子:

void testRef()
{
	int a = 8;
	int& b = a;
	int& c = b;
	cout << &a << endl;
	cout << &b << endl;
	cout << &c << endl;
}
  • b是a的别名。c是b的别名,其实就是a的别名。三者的地址是相同的。
  • 这也说明一个变量可以有多个引用

引用的其他特性:

  1. 引用在定义时必须初始化
  2. 引用一旦引用了一个实体,就不能再引用其他实体。

常引用

如何引用const修饰的变量?

const int a = 10;
int& b = a;
  • 这样是不行的。
const int a = 10;
const int& b = a;
  • 引用的前面必须也加上const
int a = 10;
const int& b = a;
  • 而这样也是可以的。但是此时只能通过a来修改变量值。

由此我们发现一个规则:

  • 对原变量,引用的权限只能平等或缩小,不能放大。

另外也可以对一个常量定义引用,也要在前面加上const

const int& a = 10;

上面提到,引用类型和实体类型必须是同类型的,double类型的变量肯定不能取int类型的别名,但是可以这样:

void testConstRef()
{
	double a = 2.5;
	int c = a;
	const int& b = a;
	cout << a << ' ' << b << endl; // 2.5 2
	cout << &a << ' ' << &b << endl; // 地址不同
}

此时访问a的值仍然是2.5,而b的值却是2。求ab的地址会发现它们俩并没有共用同一块空间。

这是为什么呢?

在学C语言的时候我们就知道,C语言允许隐式类型转换,doubleint会把小数部分截掉。如int c = a;中,a截掉后产生新的值是2,它并不会被直接赋值给a,而是先赋给一个临时变量,然后这个临时变量再把值赋给c

所以b不是a的别名,而是中间产生的临时变量的别名,由于临时变量具有常性,所以前面要加const。并且此时这个临时变量的生命周期变得和引用一样。

实际使用

引用作参数

void swap(int& a, int& b)
{
	int tmp = a;
	a = b;
	b = tmp;
}

int main()
{
	int x = 2;
	int y = 5;
	swap(x, y);// 调用
	return 0;
}

定义和调用都比原来用指针写更方便了。

在传参效率方面,引用和指针传地址是差不多的。只有传值调用效率较低,因为需要拷贝参数。

引用作返回值

首先了解一点,我们原先的传值返回中间也会产生临时变量

如下代码函数运行到return n;时,会先把n的值赋给临时变量,然后函数栈帧销毁,最后把临时变量赋给ret。

所以最后接收的是临时变量,而临时变量具有常性。我们也可以引用接收,但是前面要加const,这也证明了临时变量的存在。

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

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

还要了解一点,static修饰的变量是静态全局变量,只会被定义一次,第二次定义会直接跳过。如下代码,结果为123

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

int main()
{
	cout << count() << endl; // 1
	cout << count() << endl; // 2
	cout << count() << endl; // 3
	return 0;
}

使用引用返回,返回的是n的别名,不会有临时变量的拷贝。如下,nret是同一个地址。

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

int main()
{
	int& ret = count();
	cout << ret << endl; // 1
	cout << "hello world" << endl;
	cout << ret << endl; // 随机值
	return 0;
}

但是这会导致一个问题。比如这两次打印ret,第一次是1,第二次却是随机值。

因为ret的地址还是原来n的地址,而n所在的函数栈帧已经销毁,第一次访问时这块空间还没有被覆盖,所以是1。而打印hello world会创建新的栈帧,将原来的位置的值覆盖,所以第二次打印就是随机值。

所以,想使用引用返回来减少拷贝,要保证变量出了作用域不会被销毁,这里应在int n = 0;前加static

引用与指针的比较

从底层的角度,引用和指针是同样的方式实现的。

如下代码:

int main()
{
	int a = 10;

	int& b = a;
	b = 20;

	int* pb = &a;
	*pb = 20;
	return 0;
}

转到反汇编:

	int& b = a;
00007FF636101FB4  lea         rax,[a]  
00007FF636101FB8  mov         qword ptr [b],rax  
	b = 20;
00007FF636101FBC  mov         rax,qword ptr [b]  
00007FF636101FC0  mov         dword ptr [rax],14h  

	int* pb = &a;
00007FF636101FC6  lea         rax,[a]  
00007FF636101FCA  mov         qword ptr [pb],rax  
	*pb = 20;
00007FF636101FCE  mov         rax,qword ptr [pb]  
00007FF636101FD2  mov         dword ptr [rax],14h  

可见二者的汇编代码是一样的。

引用与指针的区别总结

  1. 引用在定义时必须初始化,指针没有要求。
  2. 引用在初始化时引用了一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型的实体。
  3. 没有NULL引用,但有NULL指针。
  4. sizeof引用的结果是引用类型的大小,而指针始终是地址所占的空间大小(32位平台下4字节,64位平台下8字节)
  5. 引用+1即引用的实体+1,而指针+1则指针向后移动一个类型的大小。
  6. 有多级指针,没有多级引用
  7. 访问实体的方式不同,指针需要手动解引用,引用由编译器自行处理。
  8. 引用比指针使用起来相对更安全。

内联函数

内联函数的概念

[c语言]预处理 #define中介绍了宏函数。

当需要频繁调用短小函数的时候,可以改为使用宏来避免开辟栈帧的开销。

但是它也有许多不足:

  1. 容易因为运算符优先级的问题造成混乱
  2. 函数参数有类型检查,宏参数没有
  3. 宏不支持调试

C++内联函数很好地解决了以上问题。

内联函数即以inline修饰的函数,编译时会在调用内联函数的地方展开,没有函数压栈的开销,提高了程序的运行效率。

🌰例子

inline int add(int a, int b)
{
	return a + b;
}

int main()
{
	int ret = 0;
	ret = add(1, 2);
	return 0;
}

我们知道debug版本为了支持调试,是不会做优化的,所以inline并不会起作用,和普通函数一样。

debug版本下的汇编代码如下,call表示调用函数。

	ret = add(1, 2);
00007FF6901B1872  mov         edx,2  
00007FF6901B1877  mov         ecx,1  
00007FF6901B187C  call        add (07FF6901B12FDh)  
00007FF6901B1881  mov         dword ptr [ret],eax  

编译器默认在debug版本下不展开,我们要想查看inline的效果,可以对编译器进行设置(以vs2022为例):

imgimg

设置好后,转到反汇编,可以看到这条语句的call没了。

	ret = add(1, 2);
00007FF7893B14CA  mov         eax,1  
00007FF7893B14CF  add         eax,2  
00007FF7893B14D2  mov         dword ptr [ret],eax  

注意事项

  1. 代码也占内存,所以内联函数是一种以空间换时间的做法。较长的函数(10行以上)和递归函数都不适合作为内联函数。
  2. inline对于编译器只是个建议==,如果定义的函数过长或者是递归函数,inline会被忽略。
  3. inline不建议声明和定义分离。

第三点的分离指的是,将函数声明写到.h文件,函数定义写到.cpp文件。

我们知道声明的普通函数会生成地址,调用的时候会去.cpp找。而内联函数是直接替换,不会生成地址,这样就导致了链接错误。

因为内联函数本身短小,所以建议直接定义在.h文件里。

auto关键字(C++ 11)

简介

在早期版本中,C++同C语言一样,auto被解释为一个自动存储变量的关键字,主要就是用来声明变量的生命周期为自动。即在全局域定义的变量为全局变量,函数中定义的变量为局部变量。但是这个关键字不怎么使用,因为所有变量默认就是自动存储类型。

C++ 11赋予了auto关键字全新的含义:auto不再是一个存储类型指示符,而是作为一个新的类型指示符来指示编译器,auto声明的变量必须由编译器在编译时期推导而得。

也就是说,使用auto定义变量可以不指定类型,而由编译器来推导。

注意:auto定义变量必须初始化

🌰例子

int f()
{
	return 10;
}
int main()
{
	const int a = 10;
	auto b = &a;
	auto c = 'a';
	auto d = f();

	cout << typeid(b).name() << endl; // int const * __ptr64
	cout << typeid(c).name() << endl; // char
	cout << typeid(d).name() << endl; // int
	return 0;
}
  • aconst int类型,然后取地址,可以推出bint const *类型,const*前面,表示指向的内容不可被修改;'a'是字符,可以推出cchar类型;f函数的返回类型是int,可以推出d的类型是int
  • typeid().name()能以字符串形式返回一个变量的类型名,记住用法即可。

其他用法

1. auto*&结合使用

int main()
{
	int x = 10;
	auto a = &x;
	auto* b = &x;
	auto& c = x;

	cout << typeid(a).name() << endl; // int * __ptr64
	cout << typeid(b).name() << endl; // int * __ptr64
	cout << typeid(c).name() << endl; // int
	return 0;
}
  • 也可以自己写一部分,比如表示指针的*,表示引用的&

这说明auto相当于一个“占位符”,编译时会自动替换为实际的类型。

2. 在一行定义多个变量

auto a = 1, b = 3;   // 正确✔
auto c = 2, d = 2.5; // 错误×

在一行定义多个变量要保证类型相同。

不能推导的情形

1. auto不能作为函数的参数

void add(auto a, auto b) // ❌
{
	return a + b;
}

同理,auto作返回类型也是不可以的

2. auto不能用来声明数组

auto a[] = { 1,2,3 }; // ❌

使用场景

但是像上面这样使用auto没有意义。只有在类型名很长时,使用auto较为方便。

如下场景:

int main()
{
	map<string, string> dict;
	dict["key"] = "value";
	//map<string, string>::iterator it = dict.begin();
	auto it = dict.begin();
	return 0;
}

STL在以后会学到,总之这里的迭代器类型就很长,使用auto就很方便。

基于范围的for循环(C++ 11)

简介

如果要遍历一个大小不确定的数组,我们以前往往会写成这样:

int main()
{
	int a[] = { 1,2,3,4,5 };
	for (int i = 0; i < sizeof(a) / sizeof(a[0]); ++i)
	{
		cout << a[i] << ' ';
	}
	return 0;
}

而C++ 11的for循环可以这样写:

for (auto e : a)
{
	cout << e << ' ';
}
  • 依次自动取a中的数据,赋值给e,并自动判断结束。
  • 这里的auto也可以写成确定的类型int,只是习惯写成auto

基于范围的for循环就是像这样,括号内由冒号:分为两部分:第一部分是范围内用于迭代的变量,第二部分则表示被迭代的范围 。


注意:由于e的值是拷贝过来的,所以修改e不能改变原数组的值。

要想修改可以这样:

int main()
{
	int a[] = { 1,2,3,4,5 };
	for (auto& e : a)
	{
		e *= 2;
	}
	for (auto e : a)
	{
		cout << e << ' ';
	}						// 2 4 6 8 10
	return 0;
}
  • 运用引用。再次遍历时就会发现数组被修改了。

不能使用范围for的情形

1. 范围for的迭代范围必须是确定的。如下场景就不可以🙅‍

void testFor(int a[]) // ❌
{
	for (auto& e : a)
	{
		cout << e << ' ';
	}
}
  • 此时的a已经是个指针,通过它并不能表示整个数组的范围。

指针空值nullptr(C++ 11)

nullptr是C++ 11新增的一个关键字。

int* p1 = NULL;
int* p2 = 0;
int* p3 = nullptr;

初始化空指针,在C++ 11中这三种写法,效果基本上是一样的。

但是,推荐使用nullptr

因为NULL实际上是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

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

也就是说在C++中写NULL0其实是一样的。

我们来看如下代码:

void f(int)
{
	cout << "f(int)" << endl;
}
void f(int*)
{
	cout << "f(int*)" << endl;
}
int main()
{
	f(0);
	f(NULL);
	f(nullptr);
	return 0;
}

结果为f(int) f(int) f(int*)

本意上NULL代表的是空指针,应该是指针类型,而它实际上却是个整型。

注意:

  1. nullptr关键字,不需要包含头文件。
  2. sizeof(nullptr)sizeof((void*)0)相同,都是指针的大小(4/8字节)。
  3. 为提高代码健壮性,建议使用nullptr
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

世真

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值