C++ 内联函数,c++引入内联函数的原因
博客明星评选
在写之前,今天先来分享一下关于参考的知识。下面是一些基础知识。以后,我们可以在课堂上进一步使用reference。让我们一起努力。
主引用是C的特点之一,但是C没有给引用一个关键字,使用了运算符的重载。引号在C中很常见,常见意味着它很重要。先说两个境界的报价。第一阶段是我们在书上能看到的。
什么是参考?引用不是变量的新定义,而是给现有变量的别名。编译器不会为被引用的变量开放内存空间,但它会与其被引用的变量共享相同的内存空间。所谓别名就是外号。张三可能在朋友面前叫三子,在长辈面前叫三儿,但不管是叫三子还是三儿,都叫张三,这是不可否认的。引用的符号与我们的地址操作符相同。
我们可以理解所谓的引用。
# include stdio . h int main(){ int a=10;int b=a;//b是A的别名,printf (%p \ n ,printf (%p \ n ,返回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=20Printf(交换前,a=%d b=%d\n ,a,b);互换(a,b);Printf(交换后,a=%d b=%d\n ,a,b);返回0;}
当我们引用同一个空间时,无论我们给变量取多少个昵称,这些昵称都指向同一个空间。如果只是修改一个昵称的空间,其他昵称就值得修改了。
#使用命名空间std包含iostreamint 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=20cout a: a endl;cout b: a endl;cout c: a endl;返回0;}
即使引用很容易使用,我们也应该注意它们的一些特性。
这个要求很严格。我们必须在报价时初始化它,否则我们将报告一个错误。
#使用命名空间std包含iostreamint main(){ int a=10;int返回0;}
一个变量可以多次引用这个特性。正如我们之前与您分享的,我们可以多次给一个变量起绰号,每个绰号都是该变量的别名。我们甚至可以给外国号码起外号,但我们一般不会这么做。
一个实例一旦被引用,就不可能再引用其他实例。这一特点完美地解释了指称的特异性。一旦我们给了一个变量一个别名,这个别名就会跟随这个变量一辈子,永远不会成为其他变量的另一个别名。这也和指针很不一样,指针更花哨。
我们不会按代码打印每个变量的地址。再来看另一个现象。通过观察A的值也被修改,我们可以确定B仍然是A的别名.
int main(){ int a=10;int b=a;int c=20//这一定是赋值b=c;cout a: a endl;返回0;}
高级引用我们已经看到了引用的优势,但这是引用的基本用法。如果我们去那里,我们通常可以通过看书来完成。现在让我们来看看更详细的引文知识。
我们经常被引用分享C语言中与const相关的知识,那么如何给const修改的变量别名呢?这是个小问题。
const int a=10我们来看下面这个方法。
int main(){ const int a=10;int b=a;//error const int c=a;//无错误返回0;}
这里可以发现,我们取别名的时候,也可以用const来修改。这是我们从现象得出的结论,但这是为什么呢?我们需要知道它们的原理。
不知道大家在学习中有没有看到这样的现象。对于一个文件,我们可能有权利只是阅读,但我们自己不能修改,但其他人可能有资格修改。这就是权限的能力。const修改变量后,变量的加密程度更高。当我们取别名时,我们不能扩展这个权利,这是编译器不允许的。也就是说我们取别名的时候,权限只能往下走。
以下是缩小权限。这里
int main(){ int a=10;const int b=a;返回0;}
常数的别名必须用const修饰,因为常数不能修改。
int main(){ const int a=10;返回0;}
临时变量有常量属性,这是语法的一个知识点。我们可以记住它。现在我们要看看这段代码为什么能运行。
我们经常提到最后一个问题。如果我们是double类型的变量,怎么给int类型的别名?
它需要用const来修饰,会发生截断,
int main(){ double d=1.2;const int a=d;//cout a endl需要用const修饰;返回0;}
它的本质不是取别名,而是对常数取别名,同样会被截断。说截断不太合适,会产生一个临时变量。下面我放示意图。
int main(){ double d=1.2;const int a=d;cout a: a endl;cout d: d endl;返回0;}
我们需要看一下reference的使用场景,它主要有两个作用。
作为参数,作为返回值,作为参数,我们可以用引用来交换两个整数数据。
#include stdio.h void swap(int a,int b){ int c=a;a=b;b=c;} int main(){ int a=10;int b=20Printf(交换前,a=%d b=%d\n ,a,b);互换(a,b);Printf(交换后,a=%d b=%d\n ,a,b);返回0;}
但是,使用引用作为参数可能会导致权限不匹配的错误,所以我们需要const修饰。
下面是因为权限问题,我们没有办法别名常量,需要const修饰。
void func(int a){ } int main(){ int a=10;双b=10.2func(a);func(10);func(b);返回0;}
我们先来看看返回值的原理,然后把引用称为返回值。
返回值原理我们需要讲一下编译器是如何使用返回值的。以下面的代码为例。
int func(){ int n=1;n;返回n;}int main(){ int ret=func()。返回0;}编译器会看这个返回值的空间大不大。如果它不大,它会把数据放在一个寄存器中。如果它很大,取决于编译器的机制,一些编译器甚至可能在主函数中开辟这个空间来临时存储数据。
这是因为当函数结束时,函数的堆栈框架会被破坏,N的空间会被释放,所以需要一个寄存器来存放数据。
可以显示下面的代码,
int main(){ int ret=func();返回0;}
引用返回值会有很多要求,和我们普通的返回值不一样。说实话,我也不想跟你分享的那么深,但它就在这里,我能做的也就这些了。
下面这段我们能理解的代码,是给静态变量n一个别名,我们把它返回给ret。
int func(){ static int n=1;n;返回n;}int main(){ int ret=func()。cout ret endl返回0;}
也就是说,我们取一个别名,作为返回值返回给函数调用方。
int func(){ static int n=1;n;cout n endl返回n;}int main(){ int ret=func()。//注意是int cout ret endl返回0;}
注意这里,我们可以把注释的引用看作一个返回值。我们必须保证函数栈框架被破坏后,值得引用的数据的空间不会被释放,否则会发生下面的事情。
我们得到的是一个随机值。我们得到了变量n的别名,但是空格是在func结束后释放的。下次调用函数时,函数堆栈框架将覆盖这个空间。如果幸运的话,我们可能会得到准确的值,但无论如何访问是不允许的。
int func(){ int n=1;n;返回n;}int main(){ int ret=func()。Printf(这是一条华丽的分界线\ n );cout ret endlcout ret endl返回0;}
引用不会打开空间。前面我们说引用要有一定的要求,但这并不是说引用作为参数就不能用。当我们使用引用传递参数时,不会出现复制的情况,大大提高了代码的效率。
让我们定义一个更大的结构来看看复制和引用参数的效率。一般相差20倍左右。
typedef struct A { int arr[10000];} A;void func 1(A A){ } void func 2(A A){ } void TestRefAndValue(){ A A;//取值为函数参数size _ t begin 1=clock();for(size _ t I=0;我100000;I)func 1(a);size _ t end 1=clock();//以引用为函数参数size _ t begin 2=clock();for(size _ t I=0;我100000;I)func 2(a);size _ t end 2=clock();//分别计算两个函数运行后的时间cout func 1(a)-time: end 1-begin endl;cout func 2(A)-time: end 2-begin 2 endl;} int main(){ TestRefAndValue();返回0;}
引用和指针有本质的区别。在语法概念中,指称是别名,没有独立的空间,与其指称实体共享同一个空间。但是底层引用还是打开了空间,因为引用是通过指针实现的。让我们看看汇编代码。
引用不会打开空间,但是指针会打开相应的空间。基础引用和指针是相同的。
int main(){ int a=10;//语法不开空间。底层开起来int b=a;b=20//语法打开空间。底层也开了int * pa=* pa=20返回0;
再来看看他们其他的小区别,我就不详细举例了。
定义引用时,必须对其进行初始化。指针不需要引用。初始化时引用一个实体后,不能再引用其他实体,指针可以随时指向同类型的任何实体。没有空引用,但是sizeof中有一个含义不同的空指针:引用结果是引用类型的大小,然而,指针始终是地址空间占用的字节数(32位平台上为4字节)。引用自加的实体是指引用加1,指针自加是指指针的后向偏移量。有多层次的指针,但没有多层次的引用。访问实体的方式不同,指针需要显式解引用,引用编译器自己处理引用。它比指针使用起来更安全。内联函数称为内联函数。编译时,C编译器会部署在调用内联函数的地方,没有堆栈函数的开销。内联函数提高了程序运行的效率,本质上类似于我们的宏。
记住我们不关心他们在那个过程中的扩张,只要记住他们会扩张。我们来看看内联函数的知识点。
为什么我们有内联函数?对于一些较小的函数,我们总是称之为函数成本比较。我们有办法降低这个成本吗?C语言是用宏实现的,C支持C语言,所以我们可以用行来实现。然后给我写一个两个数相加的宏。如果你写的和下面不一样,说明你忘记了一些知识点。
#define ADD(x,y)((x)(y))//without;我们可以用括号来理解。宏是好的,但是写一个正确的宏很难,但是写一个函数就不一样了,所以有些老板发明了内向函数来代替宏的一些功能。
扩展短函数不是你能决定的。当我们使用内联时,我们告诉编译器这个函数是可以扩展的。至于要不要扩展,就看编译器了。一般来说,短函数展开后,更大的函数就不会展开了,即使是递归的。
inline void swap(int x,int y){ int ret=x;x=y;y=ret} int main(){ int a=1;int b=2;互换(a,b);cout a: a endl;cout b: b endl;返回0;}
让我们来看看内联函数。如果函数不是内联的,汇编语言将调用
void swap(int x,int y){ int ret=x;x=y;y=ret}
如果在上述函数前添加inline关键字,将其改为内联函数,编译器会在编译时用函数体替换函数调用。
由于我们使用的是VS编译器,所以这里需要看一下内联函数的汇编语言。
在发布模式下,检查编译器生成的汇编代码中是否存在调用交换,但编译器会进行优化。我们传递了调试模式,但是需要设置它。
内联修改的更短的函数是扩展的,没有调用。
inline void swap(int x,int y){ int ret=x;x=y;y=ret}
内联函数的特性我们需要看看函数的基本特性。
内联是一种用空间换时间的方式,节省了调用函数的开销。因此,具有长代码或循环/递归的函数不适合内联函数。内联只是给编译器的一个建议,编译器会自动优化。如果在定义为内联的函数体中有循环/递归,编译器在优化时将忽略内联。内联不建议声明和定义分离,这样会导致链接错误。因为inline是扩展的,没有函数地址,链接也找不到。大型函数编译器不会被内联。编译器会自动决定这个函数是否应该内联。如果一个函数很大,里面有递归,最好不要展开。一般来说,它基于10种行为。
inline void f(){ cout hello endl;cout hello endlcout hello endlcout hello endlcout hello endlcout hello endlcout hello endlcout hello endlcout hello endlcout hello endlcout hello endlcout hello endlcout hello endlcout hello endlcout hello endlcout hello endlcout hello endlcout hello endl} int main(){ f();返回0;}
如果我们把声明和定义分开,运行时编译器将找不到这个函数的地址。
这里,我们建议将内联函数直接放入用户定义的头文件中。
内嵌void f(){}
转载请联系作者取得授权,否则将追究法律责任。评论0发表评论。
wx604d7ad249574
2022-05-10 20:20
有问题请在评论区留言,我看到会马上回复。
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。