前言
各位读者朋友们大家好,上期我们讲了C++的部分基础语法,这期我们继续对C++语法进行深入的学习。
目录
- 前言
- 一. 引用
- 1. 引用的概念及定义
- 2. 引用的特性
- 3. 引用的使用
- 4. const引用
- 5. 指针和引用的关系
- 二. inline
- 三. nullptr
一. 引用
1. 引用的概念及定义
引用不是新定义一个变量,而是给已存在的变量取了一个别名,编译器不会为引用变量开辟新的内存空间,它和它引用的变量共用一块内存空间。就像现实生活中的外号或者网名一样。
- 引用的形式
类型& 引用别名 = 引用对象
int main()
{
int a = 0;
int& b = a;
int& c = b;
int& d = c;
int m = 1;
d = m;
cout << a << endl;
cout << b << endl;
cout << c << endl;
cout << d << endl;
cout << m << endl;
return 0;
}
上面代码中,我们定义了一个变量a,又给它取名b,c,d(对其进行引用),因此b,c,d都是a这一变量,然后又定义了变量m,将其赋值给d,因为d是a,所以a,b,c,d都变成了1。
2. 引用的特性
-
引用在定义时必须初始化
-
一个变量可以有多个引用
就像一个人的别称可以有很多个。 -
引用一旦引用一个实体,就不能再引用别的实体(引用的对象不能被改变)
3. 引用的使用
- 引用在实践中的使用主要是用于引用传参和引用作返回值中减少拷贝提高效率和改变引用对象的同时改变被引用对象。
在C语言中,我们想通过函数交换两个数的值,只能通过传指针来改变,有了引用我们可以通过传引用来改变,就减少了传指针过程中的地址拷贝,提高了效率。
我们在Func这个函数中,对引用对象进行了自增操作,在主函数中,a的值也进行了自增,所以,我们可以在改变引用对象的同时改变被引用对象。 - 引用作返回值的场景相对复杂,这里讲一下错误示范,后续深入的内容我们在类和对象部分深入讲解。
int& Func()
{
int a = 0;
return a;
}
这里我们返回了临时变量,当函数调用结束后,a的栈帧就被销毁,我们返回a的引用就类似于返回了一个野指针,因此不能将a的引用作为返回值。
- 引用和指针在实践中相辅相成,功能有重叠性,但是各有特点,互相不可替代。C++的引用和其它语言的引用是有很大区别的,除了用法,最大的特点是C++的引用定义后就不能改变指向。
4. const引用
- 可以引用一个const对象,但是必须用const引用。const引用一可以引用普通的对象,因为对象的访问权限在引用过程中可以缩小,不能放大。
所谓临时对象就是编译器需要一个空间暂存表达式的求值结果时临时创建的一个未命名的对象,C++中把这个未命名对象叫做临时对象。
5. 指针和引用的关系
C++中指针和引用就像两个性格迥异的亲兄弟,指针是哥哥,引用是弟弟,在实践中他们相辅相成,功能有重叠性,但各有各的特点,互相不可替代。
转到汇编看,指针和引用的底层是相似的。
- 语法概念上引用是一个变量的别名不开空间,指针是储存一个变量的地址,要开空间。
- 引用在定义时必须初始化,指针建议初始化,但是语法上不是必须的。
- 引用在初始化时引用一个对象后,就不能再引用其他对象;而指针可以改变指向。
- 引用可以直接访问指向对象,指针需要解引用才能访问指向对象。
- sizeof中含义不同,引用结果为引用类型的大小,但指针始终是4/8个字节。
- 指针很容易出现野指针和空指针的问题,引用很少出现,引用使用起来相对安全。
二. inline
inline是C++的一个关键字,是一种向编译器发出的建议,用于指示编译器尝试将函数体直接插入到每个调用该函数的地方,而不是像通常那样进行函数调用。这样做的目的是减少函数调用的开销,特别是对于那些体积小且频繁调用的函数。
- 用inline修饰的函数叫做内联函数,编译时C++编译器会在调用的地方展开(替换)内联函数,这样在调用内联函数时,就不用建立栈帧了,可以提高效率。相当于宏的作用。
inline int Add(int x, int y)
{
int ret = x + y;
return ret;
}
int main()
{
int ret = Add(1, 2);
return 0;
}
对于上面这段代码,我们转汇编看一下执行指令:
在汇编语言中,call 指令用于从当前代码位置跳转到另一个代码位置(通常是一个函数或过程)执行,并在执行完毕后返回到原来的位置继续执行。这个过程涉及到栈的使用,以确保在函数调用结束后能够正确地返回到调用点。也就是说,有call指令就是函数没有展开。 我们的Add函数是内联函数,为什么没有展开?
- Debug模式下内联默认不展开,这样便于我们调试
如果展开就会像宏一样将代码替换掉,宏是不支持调试的,不能F11进入函数,函数会直接执行完宏到下一条语句,所以为了支持调试,Debug模式下内联函数不会展开。
想要在Debug模式下展开内联函数可以按下面的操作进行设置:
这样就展开了
- inline对于编译器而言只是一个建议,也就是说,加了inline编译器也可以选择在调用的地方不展开,不同的编译器关于inline什么情况下展开各不相同,因为C++标准没有规定这个。inline适用于频繁调用的短小函数,对于递归函数,代码相对多一些的函数,加上inline也会被编译器忽略。
-
- 递归函数本质上是通过函数调用自身来解决问题的。如果编译器尝试将递归函数内联,那么每次递归调用都会尝试内联,这可能导致代码爆炸式增长(即代码膨胀)。这种膨胀不仅会增加编译时间,还可能导致生成的二进制文件异常庞大,并且可能降低程序的性能,因为过多的代码重复和更大的缓存压力。
-
- 对于代码量相对较大的函数,内联可能会导致代码膨胀的问题更加严重。内联函数的目的之一是减少函数调用的开销,但如果函数体本身很大,那么内联后的代码膨胀可能会抵消掉调用开销减少带来的性能提升。
- 对于代码量相对较大的函数,内联可能会导致代码膨胀的问题更加严重。内联函数的目的之一是减少函数调用的开销,但如果函数体本身很大,那么内联后的代码膨胀可能会抵消掉调用开销减少带来的性能提升。
- C语言实现宏函数也会在预处理的时候替换替换展开,但是宏函数的实现很容易出错,且不方便调试,C++设计了inline的目的就是替代C语言的宏函数。
比如要写一个宏定义一个加法函数:
大家可能会写出很多错误的写法,但是有了inline之后就不容易出错了。
- inline不建议声明和定义分离到两个文件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出错。
三. nullptr
NULL实际上是一个宏,在C头文件(stddef.h)中,可以看如下代码:
看下面这段代码运行结果如何:
void Func(int a)
{
cout << "Func(int a)" << endl;
}
void Func(int* ptr)
{
cout << "Func(int* ptr)" << endl;
}
int main()
{
Func(0);
Func(NULL);
return 0;
}
按照C语言的语法,应该是输出Func(int a)和Func(int* ptr),但是实际运行结果并不是这样的
这是因为在C++中,NULL是被定义成字面常量0
因此在调用的时候就调用的第一个函数。
C++11中引入nullptr,nullptr是一个特殊的关键字,nullptr是一种特殊类型的字面常量,它可以转换成任意其它类型的指针类型。使用nullptr定义空指针可以避免类型转换的问题,因为nullptr只能被隐式的转换为指针类型,而不能被转换为整数类型。
所以我们以后再使用空指针时就定义为nullptr了。