C++ 引用与内联函数详情
引用初阶
引用是C++的特性的之一,不过C++没有没有给引用特意出一个关键字,使用了操作符的重载。引用在C++中很常见,常见就意味着它很重要。我们分两个境界来谈谈引用,初阶是我们能在书上看到的.
什么是引用
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间 .所谓的取别名,就是取外号,张三在朋友面前可能叫三子,在长辈面前可能叫三儿,但是无论是叫三子还是三儿,他们叫的就是张三,这是无可否认的.引用的符号&和我们取地址的&操作符一样的,后面我们就会知道这是操作符构成重载的原因.
我们可以理解所谓的引用.
#include <stdio.h> int main() { int a = 10; int& b = a; // b 是 a的一个别名 printf("%p\n", &a); printf("%p\n", &b); return 0; }
为何要有引用
引用有很多优点,其中有一个就是可以简化代码,我们在C语言中写过如何交换两个整型数据,需要借助一维指针,但是这有一点麻烦,使用引用就可以很好的解决这个问题.
#include <stdio.h> void swap(int& a, int& b) { int c = a; a = b; b = c; } int main() { int a = 10; int b = 20; printf("交换前 a = %d b = %d\n", a, b); swap(a, b); printf("交换后 a = %d b = %d\n", a, b); return 0; }
引用指向同一块空间
我们无论给变量取多少外号,但是这些外号所指向的空间是一样的,我们只要修改一个外号的空间,就会导致其他外号值得修改.
#include <iostream> using namespace std; int main() { int a = 10; int& b = a; //b 是 a 的别名 int& c = b; //还可以对c取别名 return 0; }
int main() { int a = 10; int& b = a; //b 是 a 的别名 int& c = b; //还可以对c取别名 c = 20; cout << "a: " << a << endl; cout << "b: " << a << endl; cout << "c: " << a << endl; return 0; }
引用的特性
即使引用很好使用,但是我们还要关注他们的一些特性.
定义时必须初识化
这个要求很严格,我们必须在引用的时候给他初始化,否则就会报错.
#include <iostream> using namespace std; int main() { int a = 10; int& b; return 0; }
一个变量可以多次引用
这个特性我们前面和大家分享过了,我们可以对一个变量多次取外号,每一个外号都是是这个变量的别名,我们甚至可以对外号取外号,不过我们一般不这么干.
引用一旦引用了一个实例,不能在再引用其他的实例
这个特性完美的阐释了引用的专一性,一旦我们给某一个变量起了一个别名,这个别名就会跟着这个变量一辈子,绝对不会在成为其他的变量的别名.这个也是和指针很大的区别,指针比较花心.
这个我们就不通过代码打印各个变量的地址了.,我们观看另外一种现象,通过观察我们看到a的值也被修改了,可以确定 b 仍旧a的别名.
int main() { int a = 10; int& b = a; int c = 20; // 这个一定是赋值 b = c; cout << "a: " << a << endl; return 0; }
引用进阶
我们已经看到引用的优点,但是这是引用的基础用法,要是到那里,我们一般看书都可以做到,现在要看看引用更加详细的知识.
常引用
我们在C语言中,专门分享过const相关的知识,那么我们如何对const修饰的变量取别名呢?这是一个小问题.
const int a = 10;
我们看看下面的方法.
int main() { const int a = 10; int& b = a; //报错 const int& c = a; // 不报错 return 0; }
到这里我们就可以发现,我们取别名的时候也用const修饰就可以了,这是我们从现象中的得出的结论,但是这又是因为什么呢?我们需要知道他们的原理.
权限
不知道大家有没有在学习中看到过这样一种现象,对于一个文件,我们可能存在仅仅阅读的权限,自己无法修改,但是其他的人有可能有资格修改,这就是权限的能力,const修饰变量后,使得变量的加密程度更加高了,我们取别名的时候,总不能把这个权限给过扩大了,编译器这是不允许的,也就是说我们取别名的时候,权限只能往低了走,绝对不能比原来的高.
下面就是把权限往缩小了:
int main() { int a = 10; const int& b = a; return 0; }
给常量取别名必须要是const修饰的,因为常量不能修改.
int main() { const int& a = 10; return 0; }
临时变量具有常属性
这个是语法的一个知识点,我们记住就可以了,现在我们就要看看为什么这个代码会可以运行.
我们常引用 还有最后一个问题,如果我们是double类型的变量,如何给取一个int类型的别名?
需要用const修饰,而且会发生截断:
int main() { double d = 1.2; const int& a = d; //需要用const修饰 cout << a << endl; return 0; }
它的本质不是取别名,而类似于给常量取别名,而且还会发生截断.这个说截断也不太合适,这会产生一个临时变量,我把原理图放在下面.
int main() { double d = 1.2; const int& a = d; cout << "&a :" <<&a << endl; cout << "&d :" <<&d << endl; return 0; }
引用的场景
我们需要来看看引用的使用场景,它主要有两大作用.
- 做参数
- 做返回值
做参数
我们可以使用引用来交换两个整型数据:
#include <stdio.h> void swap(int& a, int& b) { int c = a; a = b; b = c; } int main() { int a = 10; int b = 20; printf("交换前 a = %d b = %d\n", a, b); swap(a, b); printf("交换后 a = %d b = %d\n", a, b); return 0; }
但是使用引用作为参数可能会出现权限不太匹配的错误,所以说我们需要const修饰.
下面就是由于权限问题,我们没有办法来给常量却别名,这就需要const修饰.
void func(int& a) { } int main() { int a = 10; double b = 10.2; func(a); func(10); func(b); return 0; }
返回值
我们先看看返回值的原理,再说说引用做返回值.
返回值的原理:
我们需要谈谈编译器是如何使用返回值的,以下面的代码为例.
int func() { int n = 1; n++; return n; } int main() { int ret = func(); return 0; }
编译器会看这个返回值的空间大不大,如果不大,就把的数据放到一个寄存器中,如果很大,看编译器的机制,有的编译器甚至可能在main函数中开辟这个空间来临时保存数据.
这是由于当函数结束后,函数栈帧会销毁,n的空间也会被释放,所以要有一个寄存器来保存数据.
下面的代码就可以表现出来:
int main() { int& ret = func(); return 0; }
引用做返回值
引用做返回值会有很大的要求,这个和我们普通的返回值可不一样.说实话,我不想和大家分享的那么深,但是已经到这里了,只能这样了.
下面的代码,我们可以理解,就是给静态变量n取一个别名,我们把它返回到了ret.
int& func() { static int n = 1; n++; return n; } int main() { int ret = func(); cout << ret << endl; return 0; }
也就是说我们n取一个别名,把这个别名作为返回值返回给函数调用者.
int& func() { static int n = 1; n++; cout << &n << endl; return n; } int main() { int& ret = func(); // 注意 是 int& cout << &ret << endl; return 0; }
注意事项:
到这里我们就可以看看引用做返回值的注意事项了,我们一定要保证做返回值得引用得数据在函数栈帧销毁后空间不被释放,否则就会发生下面得事情.
我们得到的是一个随机值,我们拿到了变量 n 的别名,但是在func结束后空间就被释放了,下一次函数的调用函数栈帧会覆这篇空间,运气好的话,我们有可能拿到准确值,但是无论如何访问都越界了.
int& func() { int n = 1; n++; return n; } int main() { int& ret = func(); printf("这是一条华丽的分割线\n"); cout << ret << endl; cout << ret << endl; return 0; }
引用不会开辟空间
前面我们说了传参需要有一定的要求,但是这不是说引用做参数不行,我们使用引用传参不会发生拷贝,这极大的提高了代码的效率.
我们定义一个大点的结构体,来看看拷贝传参和引用传参的效率.一般情况下相差大概20倍左右.
typedef struct A { int arr[10000]; } A; void func1(A a) { } void func2(A& a) { } void TestRefAndValue() { A a; // 以值作为函数参数 size_t begin1 = clock(); for (size_t i = 0; i < 100000; ++i) func1(a); size_t end1 = clock(); // 以引用作为函数参数 size_t begin2 = clock(); for (size_t i = 0; i < 100000; ++i) func2(a); size_t end2 = clock(); // 分别计算两个函数运行结束后的时间 cout << "func1(A)-time:" << end1 - begin1 << endl; cout << "func2(A&)-time:" << end2 - begin2 << endl; } int main() { TestRefAndValue(); return 0; }
引用和指针比较
它们有一个本质的区别,在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间 ,但是引用的底层还是开辟空间的,因为引用是按照指针方式来实现.我们看看汇编代码.
- 引用不会开辟空间,但是指针会开辟相应的空间.
底层引用和指针是一样的.
int main() { int a = 10; //语法没有开辟空间 底层开辟了 int& b = a; b = 20; //语法开辟空间 底层也开辟了 int* pa = &a; *pa = 20; return 0;
我们来看看它们其他的比较小的区别,我就不详细举例了.
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
内联函数
以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数压栈的开销,内联函数提升程序运行的效率,本质来说就和我们的宏比较相似.
记住我们不关心他们在那个过程中展开,只需要记住他会展开就可以了.我么看看内联函数的知识点.
为何存在 内联函数
对于一些比较小的函数,我们总是调用函数开销比较的,我们是不是存在一个方法减少这个开销呢?C语言通过宏来实现,C++支持C语言,所以我们可以通过行来实现,那么你给我写一个两数现相加的宏,要是你写的和下面的不一样,就代表你忘记了一部分知识点.
#define ADD(x,y) ((x) + (y)) //不带 ; 括号要带
我们就可以理解了,宏很好,但是写出一个正确的宏很困难,但是写一个函数就不一样了,所以一些大佬就发明了内敛函数,用来替代宏的部分功能.
展开短小的函数
函数内联不内联不是你说了算,我们用inline修饰就是告诉编译器这个函数可以展开,至于是否展开还是看编译器,一般之后展开比较短小的函数,较大的函数不会展开,像递归的那些也不可以.
inline void swap(int& x, int& y) { int ret = x; x = y; y = ret; } int main() { int a = 1; int b = 2; swap(a,b); cout << "a: " << a << endl; cout << "b: " << b << endl; return 0; }
我们来看看内联函数,如果函数不是内联了的,汇编语言会call
void swap(int& x, int& y) { int ret = x; x = y; y = ret; }
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用.
由于我们使用的是VS编译器,这里需要看看内联函数的汇编语言.
在release模式下,查看编译器生成的汇编代码中是否存在call swap,但是编译器会发生优化,我们通过debug模式下,但是需要设置.
inline修饰的较短函数展开了,没有call
inline void swap(int& x, int& y) { int ret = x; x = y; y = ret; }
内联函数的特性
我们需要来看看函数的基本的特性
- inline是一种以空间换时间的做法,省去调用函数额开销。所以代码很长或者有循环 / 递归的函数不适宜使用作为内联函数。
- inline 对于编译器而言只是一个建议,编译器会自动优化,如果定义为inline的函数体内有循环/递归等,编译器优化时会忽略内联。
- inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址了,链接就会找不到。
较大的函数编译器不会发生内联
编译器会自动判别这个函数给不该内联,要是一个函数比较大,里面存在递归,那么还不如不展开呢.一般是10行为依据.
inline void f() { cout << "hello" << endl; cout << "hello" << endl; cout << "hello" << endl; cout << "hello" << endl; cout << "hello" << endl; cout << "hello" << endl; cout << "hello" << endl; cout << "hello" << endl; cout << "hello" << endl; cout << "hello" << endl; cout << "hello" << endl; cout << "hello" << endl; cout << "hello" << endl; cout << "hello" << endl; cout << "hello" << endl; cout << "hello" << endl; cout << "hello" << endl; cout << "hello" << endl; } int main() { f(); return 0; }
声明定义一起
如果我们声明和定义的分离,运行时编译器会找不到这个函数的地址的.
这里我么建议直接把内联函数直接放到自定义的头文件中
inline void f() { }
到此这篇关于C++ 引用与内联函数详情的文章就介绍到这了,更多相关C++ 引用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
最新评论