在第一篇博客中,我们探讨了C语言与C++在语法上的一些小区别以及C++独有的一些特性。在这节课中,我们将进一步深入学习一些C++的入门基础知识。包括进一步理解引用,以及inline和nullptr。

1.进一步理解引用

在上一篇博客中,我们初步认识了引用的概念,即给一个变量取一个别名,格式为:类型& 引用别名=引用对象。不过,关于引用,还有一些问题我们要注意。

1.1 const引用

设想这样一种情况,假设有一个变量a,且被const修饰,是只读变量,我们定义为const int a=10,接下来,我们想给它取一个别名b,那我们可不可以这样写呢?

int main()
{
	const int a=10;
	int& b = a;
	return 0;
}

结果,编译器报错:

错误	C2440	“初始化”: 无法从“const int”转换为“int &”	

这是因为,因为 a 是 const int 类型,而 int& 允许通过别名修改值,这会破坏 a 的 const 约束,因此编译器禁止这种绑定。必须使用 const int& 保证类型一致。这里相当于是放大了访问权限,造成了冲突,自然就出错了。

因此,我们将代码这样改,就可以正常编译运行了:

int main()
{
	const int a=10;
	const int& b = a;
	return 0;
}

另外注意一点,在C++中,对象的访问权限在引用过程中可以缩小,但是不能放大,例如我们可以这样写:

int main()
{
	int a=10;
	const int& b = a;
	return 0;
}

a可以修改,但它的别名b不行,这样也是可以的。

1.2 临时对象的引用

在说明临时对象的引用这个问题前,我们先要搞清楚C++中的临时对象是什么。

临时对象(Temporary Object)是指在表达式求值过程中由编译器隐式创建的、没有命名的中间对象。这些对象通常会在表达式结束时被自动销毁。典型的场景包括:

  • 函数返回非引用类型值时
  • 类型转换时
  • 构造对象时传递的临时参数

以add()函数为例:

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

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

在这段代码中,我们创建了一个变量a,并将add(1,2)的返回值赋给了a。实际上,编译器会先创建一个临时对象来存储add()函数的计算结果,然后将这个临时对象的值赋给变量a,最后销毁该临时对象。

明白了临时对象是什么,我们可以说回引用了。既然是对象,那么就可以引用。如果我们要引用存储这个返回值的临时对象,聪明的你一定能想到这样写:

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

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

然而,编译器报错:

错误	C2440	“初始化”: 无法从“int”转换为“int &”

别着急,这是因为,C++中规定,临时对象具有常性,也就是说无法被修改。这下恍然大悟,我们只需要给int& a加上const修饰就可以了。C++ 标准规定,非常量引用(T&)不能绑定到临时对象。

另外,除了函数返回非引用类型值时会产生临时对象,在强制类型转换中也会产生临时对象。例如double d = 12.34; int& rd = d; 这样的场景下,实际上是先生成int类型的一个临时对象,然后将d强转过后的值放入临时对象,再引用这个临时对象,而不是直接引用d。而临时对象具有常性,因此这样的写法是错误的,应该用常引用,即const int& rd=d;。


int main()
{
	int a = 10;
	int& ra = a;
	const int& ra = a;//访问权限可以缩小,不能放大

	double b = 5.20;
	//int& rb = b; 不能这样写,因为涉及到了强制类型转换,引用的临时对象具有常性
	const int& rb = b;//改为常引就可以了,但请注意这里引用的不是b,而是一个临时对象
	return 0;
}

注意:编译器默认临时对象用完即销毁,但若该临时对象被引用,那么就先不销毁,而是等引用被销毁后再销毁。

1.3 指针和引用的关系

在C++中,指针和引用都是用于间接访问变量的机制,但它们在语法和使用场景上有显著区别。在实践中他们相辅相成,功能有重叠性,但是也各有自己的特点,互相不可替代。

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

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

• 引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以在不断地改变指向对象。例如如果要用引用实现链表结构时,由于引用不能改变引用对象,就不方便进行插入删除等操作了。

• 引用可以直接访问指向对象,指针需要解引用才是访问指向对象。

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

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

值得注意的一点是,引用的底层也是通过指针实现的。

int main()
{
	int a = 1;
	int* p = &a;
	int& ra = a;
	return 0;
}

这段代码定义了一个变量a以及指向它的指针p和一个引用ra。虽然指针和引用在语法上有所不同,但底层实现机制是相似的。从汇编层面分析这段代码可以更直观地理解其工作原理(C++代码会先编译为汇编代码,再进一步转换为机器码执行。具体实现细节可参考《编译原理》相关教材。)

在Visual Studio中,我们可以通过启动调试模式来查看代码对应的汇编指令。

虽然不需要深入掌握汇编语言,但通过观察可以发现,int* p = &a;int& ra = a;的底层实现确实相同。但这并不意味着指针和引用可以完全互换使用。引用的底层实现较为复杂,我们只需重点理解指针和引用各自的特点及适用场景即可。

2.内联函数(inline)

使用inline修饰的函数称为内联函数。在编译时,C++编译器会将内联函数在调用处直接展开,从而避免了函数调用时的栈帧创建开销,进而提升程序运行效率。

我们来看这样一段程序:

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

int main()
{
	zy(1, 2);
	yy(1, 2);
	return 0;
}

函数zyyy的实现内容完全相同,但yy使用了inline修饰。下面我们通过汇编代码来观察二者的区别。

注意:在VS编译器的Debug版本中,默认不会展开inline函数以便调试。如需在Debug版本中展开inline函数,需进行相应设置,具体方法此处不再详述。

编译后的汇编代码如图:

zy函数编译后出现了call指令,而yy没有。出现call说明没有被展开,没有出现说明被展开了。

不过,inline对编译器来说只是一个优化建议,即使添加了该关键字,编译器仍有权决定是否展开函数调用。不同编译器对inline的处理方式存在差异,因为C++标准并未对此作出明确规定。该关键字最适合用于频繁调用的短小函数,而对于递归函数或代码量较大的函数,即使添加inline也通常会被编译器忽略。所以,inline虽好,可不要滥用哦,因为编译器可能不听你的。

3.nullptr

在学习C语言时,我们会接触到NULL这个概念,NULL通常用于表示空指针。NULL实际是一个宏,在传统的C头文件(stddef.h)中,可以看到如下代码:

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

这段代码的意思是,在C语言中,NULL被定义为无类型指针的常量,即((void *)0),而在 C++ 中,NULL通常是0或0L,其类型是整数而非指针。

这会带来一个问题,前面我们学过,C++支持函数重载,我们以以下代码为例:

#include<iostream>
using namespace std;
void zy(int a) {
	cout << "调用zy(int a)" << endl;
	return;
}
void zy(int* a) {
	cout << "调用zy(int* a)" << endl;
	return;
}

int main()
{
	zy(1);
	zy(NULL);
	return 0;
}

在这个例子中zy(1);是想调用zy(int a),zy(NULL);是想调用zy(int* a),但由于C++中NULL就是常量0,所以编译器不认为NULL是空指针,zy(NULL)调用了zy(int a),因此与程序的初衷相悖。

运行结果:

调用zy(int a)
调用zy(int a)

所以,C++11 引入了 nullptr 关键字,这是一个特殊的字面量常量。它可以隐式转换为任意指针类型,但不会自动转换为整数类型。使用 nullptr 定义空指针能有效避免类型转换带来的问题。所以,我们将程序改为:

#include<iostream>
using namespace std;
void zy(int a) {
	cout << "调用zy(int a)" << endl;
	return;
}
void zy(int* a) {
	cout << "调用zy(int* a)" << endl;
	return;
}

int main()
{
	zy(1);
	zy(nullptr);
	return 0;
}

运行结果:

调用zy(int a)
调用zy(int* a)

这样就成功了。

4.结语

本文探讨了C++基础特性,包括引用的进阶用法、内联函数和nullptr关键字。这是作者第二次写博客,感谢大家的支持哦,也希望大家有什么问题多多和笔者交流。下一篇博客将会介绍C++中的类与对象,拜拜~

Logo

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

更多推荐