2.C++入门基础-续
在第一篇博客中,我们探讨了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;
}
函数zy和yy的实现内容完全相同,但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++中的类与对象,拜拜~
更多推荐
所有评论(0)