一、引用
1.1 引用概念
引用不是用于新定义变量,而是对已存在变量取别名。对于引用变量来说,编译器不会为其开辟空间(底层实际开辟),它与被引用变量共同占用同块内存空间
举个例子:李逵,在家称为"铁牛",江湖上人称"黑旋风"。这三个名称都是指向同个对象
1.2 引用语法
类型说明符& 引用对象名 =引用实体(引用类型必须和引用实体是同种类型)
void TestPef() { int a=10; int& pa=a;//pa是a的别名 printf("&a==%p\n", &a); printf("&pa==%p\n", &pa); //从地址上,可以得出它和它引用的变量共用同一块内存空间 }
1.3 引用特性
第一个:
- 它和被引用的变量共用同一块内存空间
void TestPef() { int a=10; int& pa=a;//pa是a的别名 a++; printf("a==%d pa==%d\n", a,pa);//11 11 pa++; printf("a==%d pa==%d\n", a,pa);//12 12 }
说明:修改对象a或pa的也会影响对象pa或a
第二点:
- 引用在定义时必须初始化(否则在编译阶段会报错)
第三点:
- 引用一旦引用一个实体,再不能引用其他实体
//引用定义后,不能改变指向(这一点很重要) int a = 10; int& b = a; int c = 10; b = c;//不是改变指向,而是b(a)赋值为10
第四点:
- 一个变量可以有多个引用,引用变量也可以取别名
//一个变量可以有多个别名,别名也有别名 int a = 10; int& b = a; int &d=b ;//d是b的别名,b是a的别名,则d是a的别名 return 0;
1.4 引用权限(常引用)
对于权限可以缩小,但是不能放大
void TestConstRef() { int a=0; int& b=a; const int& c=a; //支持->权限缩小 const int x=10; int& y=x;//不支持-权限放大(此时的x只有读权限,没有写权限) const int& y=x;//支持权限相等 //表达式的返回值是临时对象,而临时对象具有常性 int& n= a+x = 临时对象 //这里是属于权限放大 const int& n = a+x = 临时对象; //支持权限相等 }
1.5 引用使用场景
1.5.1 引用作为输出型参数(减少拷贝,提高效率)
说明:如果参数是指针类型,使用时需要对其解引用操作,但是使用引用可以避免解引用操作
1.5.2 引用作为返回值(减少拷贝,提高效率)
int& func() { int a = 0; return a; } int main() { int ret = func();//第一种 int& ret = func();//第二种 cout << ret << endl; return 0; }
说明:
- 第一种情况,当函数运行结束,函数内存被系统收回。引用作为返回值类型,返回临时局部变量a被引用接收。那么对象
ret
接收到这块空间是什么?这里需要根据编译器是否对这块函数栈帧清空,但是操作操作是不具备安全性 - 第二种情况,使用引用接收这块已经被回收的空间。会导致野引用,访问未知名空间可以看作租房子,房子合同到期,如果非法进入会出现不安全的影响。
【有趣的现象】:
关于编译器是否对该函数栈帧清空,可以看一个有意思的东西
说明:对于两次ret
结果值为0,表示编译器没有对函数栈帧清空;如果当调用func()
函数后,ret
值为随机值表示函数栈帧已经清空。间接说明引用做返回值尽量不要返回局部变量
引用做返回值使用场景:
//没有使用引用做返回值 void SLModity(Seqlist& s1, int pos, int x) { assert(pos >= 0); assert(pos < s1.size); s1.a[pos] = x; } int SLGet(Seqlist& s1, int pos) { assert(pos >= 0); assert(pos < s1.size); return s1.a[pos]; } //使用引用 读写返回值 int& Get(int pos) { assert(pos >= 0); assert(pos < s1.size); return a[pos]; }
1.6 引用做参数与返回值效率
说明:由于一般在实参传递或返回值传递过程需要创建临时对象,而对于引用与被引用对象共用同块空间,减少了拷贝,提高效率。通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。
1.7 引用和指针的区别
在语法概念上,引用是一个别名,没有独立空间,同其引用实体共用同一块空间,但是在底层实现上,实际引用是有开辟空间的,由于引用是按照指针方式实现的。
int main() { int a=10; int& ra=a; cout<<"&a"<<&a<<endl; cout<<"&ra"<<&ra<<endl; return 0; }
1.7.1 引用和指针的汇编代码对比
引用相较于指针来说,更加适合使用比较复杂的场景进行一些替换,使得代码更简单易懂(不代表引用完全替代指针),比如:引用定义后不能改变指向,而指针可以改变指向。
1.7.2 引用与指针不同点
- 引用概念上定义一个变量的别名,指针存储一个变量地址
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但是有NULL指针
- 在
sizeof
中含义不同:引用结果为引用类型的大小,但是指针始终是地址空间所占字节个数(32位平台下占4个字节) - 引用自加既引用的实体增加1,指针自加即指针后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显示解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
二、内敛函数
2.1 内敛函数概念
内敛函数是以关键字inline
修饰的函数,编译时C++编译器会在调用内敛函数位置展开函数体(函数体替换函数的调用)。函数调用建立栈帧会有开销,内敛函数虽然可以提升程序运行的效率,但是内敛函数也是需要付出代价。
【查看方式】:
内敛函数本身是一种代码优化手段
- 在release模式下,查看编译器生成的汇编代码中是否存在call Add
- 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不会对代码进行优化,以下给出
vs2013
的设置方式
2.2 内敛函数特性
第一点:
inline
是一种以空间换时间的做法,如果编译器将函数当成内敛函数处理,在编译阶段,会用函数体代替函数调用.
说明:内敛函数展开,编译程序空间变大(指令变多),空间是编译好的可执行程序,并不是我们理解的内存。虽然避免了函数调用开销提高程序运行效率,但是可能会使目标文件变大
【给出场景】:当1000行需要调用swap函数,函数体假设10行代码
- 如果swap不是内敛函数,合计需要10 + 1000
- 如果swap是内敛函数,合计需要10 * 1000,导致目标文件变大
第二点:
inline
对于编译器而言只是一个建议,不同编译器关于inline
实现机制可能不同- 一般建议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不是递归、且频繁调用的函数采用
inline
修饰,否则编译器会忽略inline
特性
第三点:
inline
不建议声明和定义分离,分离会导致链接错误。因为inline
被展开,就没有函数地址了,链接就会找不到
2.3 关于头或源文件同时定义函数措施
假设这里所指函数为Add函数。如果想要头文件中定义Add函数会导致预处理阶段出现命名冲突。
【使得头文件定义Add函数两种方法】:
第一种:
使用static
关键字将函数的外部链接属性转为内部链接属性,只在当前文件可见,简单来说就是不进符号表,不会产生没有命名冲突。
第二种:
使用内敛函数,假设在.cpp
文件定义内敛函数,由于内敛函数没有函数地址,意味着不会进去符号表。对于其他文件中需要该函数,会发生链接错误。推荐内敛函数的声明和定义放在一块,在头文件展开并且链接时不会出现命名冲突
【推荐】:
- 如果是大函数,可以使用声明和定义分离,
static
修饰; - 如果是小函数,可以使用内敛。
【C++】C++特性揭秘:引用与内联函数 | auto关键字与for循环 | 指针空值(二)https://developer.aliyun.com/article/1617291