六、引用
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
通俗地讲,可以理解为一个人能够拥有多个称呼,这些所有的称呼都是表示这一个人的。
用法:类型& 引用变量名(对象名) = 引用实体;
注意:引用类型与引用实体的类型必须一致!
引用使用的注意事项
①引用在定义时必须初始化
②引用初始化后,无法再改变指向
③一个引用实体可以拥有多个引用/别名
下面是关于注意事项的图例:
引用使用举例
交换函数Swap:
那么问题来了:既然能够使用引用来替换指针的一些用途,引用能否完全替换掉指针呢?
不能的!C++设计出引用,是对指针使用复杂的场景进行一些替换,让代码简单易懂,并不能完全替换指针,引用不能够完全替换指针的根本原因在于引用不能够改变其指向。
对于Java/Python而言,是不存在指针的,这两门语言的引用能够改变指向,因此替换掉了指针。
也正是因为C++的引用不能够改变指向,对于链表、二叉树等基于指针链接前后物理不连续的空间的数据结构,也是不能使用引用替换掉指针的!
引用的使用场景
1.做参数
①输出型参数;②对象比较大时,使用引用能够减少拷贝、提高效率。
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
2.做返回值
①修改返回对象;②减少拷贝,提高效率。
错误示例
首先,C语言中学到过,当我们在函数中返回临时变量/局部变量的值时,由于函数栈帧的回收,会导致返回的该值是一个随机值,指针中讲到的野指针也是这样类似的情况。
以下是错误的示例:
上图就返回了函数中创建的一个临时变量的值,这样做是错误的。
那么同样的,我们也不能够在上图的返回类型使用引用类型,同样为错误示例,如下所示:
上图函数Func返回类型int&,引用类型的返回,同样不能够作用在临时变量/局部变量上!否则相当于对一块未经允许访问的空间进行了访问,本质是野引用!将这个未经访问的空间a中的值赋给ret,那么ret就是随机值。
同样,ret是别名的错误示例:
返回变量出了函数作用域,生命周期到头,需要销毁(栈帧回收) ,因此不能够使用引用返回!
如局部变量与临时变量。
正确示例
引用不能作用于临时变量、局部变量,那么引用一般作用于什么呢?
引用能够作用于静态变量(如static修饰)、全局变量、堆区开辟空间的变量。
首先在C++中,变量一般被称之为对象,同时C++将结构体升级成了类:结构体中不再只能够定义变量/对象,还能够定义函数。这意味着对于一些数据结构的实现,不再需要采取结构定义与实现函数分离的形式,而是可以直接在类中定义结构和函数进行操作。
类中定义的函数名也较C语言简洁,因为该函数存在于哪个类,就属于哪个类的函数,不需要用复杂的名称来与其他的结构中的函数进行区分!
拿顺序表简单地举个例子:
顺序表的类:
//正确示范
//C++中的结构体中能够定义对象和函数,结构体一般称之为类
//同时C++不需要使用typedef去除struct,允许直接使用struct后名称
struct SeqList
{int* a;int size;int capacity;//相较于C语言的函数操作更为简便void Init()//初始化{int* tmp = (int*)malloc(sizeof(int) * 4);if (tmp == NULL){perror("malloc fail");return;}a = tmp;size = 0;capacity = 4;}void PushBack(int x)//尾插{//CheckMemorySize(省略)a[size++] = x;}int& Get(int pos)//获取pos下标的数据、使用int&返回别名,也能够修改pos处的数据{assert(pos >= 0);assert(pos < size);return a[pos];}void Destroy()//销毁{free(a);a = NULL;}
};
如上我们可以看见,在SeqList类中直接定义初始化、尾插、获取任意位置数据并修改等函数,在形参部分和整体代码量相较于C语言简洁便利许多。同时C++不需要使用typedef去除struct,直接使用类型名即可。
同时,对于获取pos下标处数据与修改,在C语言中本是两个函数分别完成,但是现在可以使用int&的引用类型作为返回值,一个函数就能够搞定,因为它返回的是该下标处数据的别名,对其的修改能够直接体现在数据的修改上。
传值返回,返回的是变量的临时拷贝;传引用返回,返回的是变量的别名。
正因如此,传值无法做到直接改变目标变量,但是传引用却能够轻松完成。
简单插入4个数据对其进行一些数据操作如下图:
如果我们将Get函数的返回类型改为int,那么就会返回目标下标数据的拷贝,即一份临时变量。
临时变量具有常性,即常量性,无法被修改!
如上图,使用引用作为返回值是能够起到很重要的作用的。
引用与指针的区别
引用在底层转换为指针的解释如下图:
我们对比引用与指针在汇编层面上的指令,发现引用经过编译过程后也被转换为了指针,那么就说明,在底层上,引用是会开空间的,语法层面上引用是别名,不开空间。
引用底层使用指针实现的,引用的语法含义与底层实现是背离的!
通俗点,就像鱼香肉丝中没有鱼,老婆饼也不是老婆做的一样。
七、内联函数
如果调用函数次数过多、需要创建的函数栈帧过多,那么为了提高效率,C语言中通过宏函数的方式来替换函数;而C++通过内联函数的方式使函数在外部直接展开而不用创建栈帧。
对于宏而言,需要注意:1.宏并不是函数;2.宏属于预处理指令,末尾不需要分号,本质是一种替换;3.额外重视括号!括号控制优先级。
宏在预处理阶段就会被替换!
对于一个加法的Add函数,写成宏函数如下:
为什么x、y要单独括号括起来?
因为x、y不一定代表一个值,可能代表一个表达式!如果x、y是表达式,且表达式中的运算符优先级低于+号,那么替换进去就会出现问题!举例如下:
因此我们需要加括号确保绝对的正确顺序。
宏的优缺点?
优点:1.增强代码的复用性;2、提高效率/性能
缺点:1、宏在预处理阶段就被替换,不方便调试;2、可读性差,可维护性差,容易误用;3、没有类型安全的检查。
C++替代宏的技术?
1、常量定义---换用const enum;2、短小函数定义,换用内联函数。
内联函数的概念
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调
用建立栈帧的开销,内联函数提升程序运行的效率。
未使用inline修饰时,调用Add函数的反汇编代码如下图:
在汇编层面上,我们看到编译器会去找函数Add的指令首地址,从而调用函数。
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用,不调用函数,也就不用创建函数栈帧,消除了函数栈帧创建的开销:
release下:直接查看反汇编下汇编代码是否存在call Add
debug下:我们需要将属性-常规-调试信息格式改为程序数据库(/Zi),属性-优化-内联函数扩展改为只适用于_inline(/Ob1),这样就能够调试-反汇编查看了
那么使用inline修饰Add函数,使其成为内联函数后,内联函数展开的反汇编如下:
通俗来讲,内联函数就是将函数里面的运算逻辑灵活地在外面实现,而不去创建调用函数所需要的栈帧空间。
内联特点
内联本质上是向编译器发出请求,当函数较大,编译器就会忽略内联的请求,较小函数能够正常内联。
因此较小函数的多次调用,我们可以使用内联inline,较大函数则使用静态static。
1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会用函数体替换函数调用。缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运行效率。
2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。
3. inline不建议声明与定义分离,分离的情况会导致链接错误,inline展开函数,就不会在符号表中生成该函数的地址,链接时就无法找到对应函数。
#pragma once解决头文件重复引入的问题,但是并不能解决两个.cpp文件中都包含相同头文件,该头文件又包含某个函数的定义,如下图所示,那么就会存在函数的重定义问题。
有三种方式能够解决这一问题:
①声明与定义分离
如果不采用声明与定义分离,只通过定义,那么可以使用static或者inline进行操作。
②采用static静态链接,只在当前文件可见
③采用inline内联,同理只在当前文件展开
八、auto关键字---C++11
auto:自动推导类型。
对于auto而言,必须要初始化,不然怎么自动推导呢?
实际意义:对于较长类型,可以使用auto简便替代:
如下图的函数指针类型:
再如其他较长的类型:
auto的利弊
优点:简化较长的类型。
缺点:对不熟悉当前代码的人而言,一定程度上影响代码可读性。
注:①C++规定auto不能定义数组。②慎用auto,C++目前允许auto作返回值,但是不建议。
③同一行使用auto声明多个变量,这些变量必须同类型,否则编译器不会通过,编译器只对第一个类型进行推导,然后用该类型定义后面的变量。
上图中auto a = 9 , k = 'c' ;就无法编译通过,因为a与k类型不同但是他俩想用一个auto。
auto不能推导的场景
①auto不能作为函数的参数
②auto不能直接用来声明数组
九、范围for循环---C++11
范围for循环的使用
for(auto e : array)
{cout << e ;
}
cout << end;
auto自动推导数组中数据类型,也可以自己填入数组数据类型;e是数组数据的依次的临时拷贝。
范围for循环会自动迭代、自动判断结束。
范围for循环可以使我们的数组遍历过程变得非常简便:
范围for循环的使用条件
①for循环迭代的范围必须确定。
数组即第一个元素到最后一个元素的范围;对于类而言,应该提供begin和end的方法,begin和end就是for循环迭代的范围。
②迭代的对象要实现++和==的操作
十、指针空值nullptr---C++11
在C++中,NULL可能被定义为字面常量0,或者被定义为无类型指针(void*)的常量。
因此C++11中创建了nullptr关键字,用于表示指针空值,就不用NULL来表示指针空值了,以避免可能出现的差错。
注:
①在使用nullptr表示指针空值时,不需要包含头文件,因为nullptr是C++11作为新关键字引入的 ②在C++11中,sizeof(nullptr) 与 sizeof((void*)0)所占的字节数相同。 ③为了提高代码的健壮性,在后续表示指针空值时建议最好使用nullptr。