传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
- 做参数比较
#include <time.h> struct A { int a[10000]; }; void TestFunc1(A a) {} void TestFunc2(A& a) {} void TestRefAndValue() { A a; // 以值作为函数参数 size_t begin1 = clock(); for (size_t i = 0; i < 10000; ++i) TestFunc1(a); size_t end1 = clock(); // 以引用作为函数参数 size_t begin2 = clock(); for (size_t i = 0; i < 10000; ++i) TestFunc2(a); size_t end2 = clock(); // 分别计算两个函数运行结束后的时间 cout << "TestFunc1(A)-time:" << end1 - begin1 << endl; cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl; } int main() { TestRefAndValue(); return 0; }
- 做返回值比较
#include <time.h> struct A { int a[10000]; }; A a; // 值返回 A TestFunc1() { return a; } // 引用返回 A& TestFunc2() { return a; } void TestReturnByRefOrValue() { // 以值作为函数的返回值类型 size_t begin1 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc1(); size_t end1 = clock(); // 以引用作为函数的返回值类型 size_t begin2 = clock(); for (size_t i = 0; i < 100000; ++i) TestFunc2(); size_t end2 = clock(); // 计算两个函数运算完成之后的时间 cout << "TestFunc1 time:" << end1 - begin1 << endl; cout << "TestFunc2 time:" << end2 - begin2 << endl; } int main() { TestReturnByRefOrValue(); system("pause"); }
通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。
引用和指针的区别
在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。
int main() { int a = 10; int& ra = a; cout<<"&a = "<<&a<<endl; cout<<"&ra = "<<&ra<<endl; return 0; }
在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。那就看下以下操作吧
int main() { int a = 10; int* p1 = &a; int& ref = a; return 0; }
我们来看下引用和指针的汇编代码对比
可能有的同学不太看的懂汇编,但是我们会发现他们的汇编指令一模一样.
那我简单解释一下这几个指令:
- lea是取地址的意思,先把把a的地址放到eax寄存器中。
- dword ptr:表示操作数的大小,这里表示一个双字(32位)。
- mov:表示将一个值从源操作数复制到目标操作数。将寄存器 eax 中的值复制到内存地址
p1 指向的位置,由于 eax 是一个32位寄存器,因此会将32位的值存储到该地址。下面的两行同样意思.
那有的的同学还有疑惑,那对他们进行 +±-的操作还会一样吗,我们继续来看看
int main() { int a = 0; int* p1 = &a; int& ref = a; ++(*p1); ++ref; return 0; }
我们发现他们的汇编代码也都是一样的,我在带大家解释一下这几个指令。
- mov eax, dowrd ptr [p1] :: p1存的是一个地址,把这个地址放在eax寄存器上。
- mov ecx, dword ptr [eax]:: 寄存器加个[] 就是解引用的意思,把这个解引用的值放在ecx
上,
- add ecx,1 :: 对ecx的值加1
- mov edx ,dword ptr [p1] :: 在将p1的地址放在edx寄存器里面
- mov dword ptr [edx],ecx:: 把ecx的值放回 edx里面
下面的ref的操作也是一模一样的
底层的汇编语言级别,确实没有直接对应引用的概念。底层汇编语言通常只提供了指针的概念和相关指令,而没有引用的语法糖。- 所有我们从底层角度里面看,我们只有指针没有引用,
- 但引用和指针还是有点区别的:
1.引用概念上定义一个变量的别名,指针存储一个变量地址。
2.初始化:引用必须在创建时进行初始化,并绑定到某个对象。指针可以在任何时候被初始化,包括创建后再指向一个对象。
3.空值:引用不能为null,它必须始终引用一个有效的对象。指针可以为null,表示它当前没有指向任何对象。
4.重新赋值:引用一旦初始化后,无法更改其绑定的对象。指针可以通过重新赋值来指向不同的对象。
5.空引用和空指针:引用不能表示不存在的对象,它始终引用一个有效的对象。指针可以为空,表示它目前没有指向任何对象。
6.访问对象:通过引用可以直接访问和操作对象,而指针需要使用解引用操作符(*)来访问指向的对象。
7..引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
8.有多级指针,但是没有多级引用
9.在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32
位平台下占4个字节)
10.引用比指针使用起来相对更安全
总的来说,引用提供了方便的别名机制,用于直接访问对象,而指针则提供了更灵活的内存操作,可以指向不同的对象,并允许对对象的位置进行更多的控制。
C++函数重载
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型不同的问题。
函数重载的规则如下:
- 函数名称必须相同。
- 参数列表必须不同,要么是参数类型不同,要么是参数个数不同,要么是参数顺序不同。
- 返回类型可以相同也可以不同,返回类型不参与函数重载的决策。
- 参数类型不同
#include<iostream> using namespace std; // 参数类型不同 int Add(int left, int right) { cout << "int Add(int left, int right)" << endl; return left + right; } double Add(double left, double right) { cout << "double Add(double left, double right)" << endl; return left + right; }
- 参数个数不同
void f() { cout << "f()" << endl; } void f(int a) { cout << "f(int a)" << endl; }
- 参数类型顺序不同
void f(int a, char b) { cout << "f(int a,char b)" << endl; } void f(char b, int a) { cout << "f(char b, int a)" << endl; }
C++支持函数重载的原理–名字修饰
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预处理、编译、汇编、链接。具体的讲解我已经写在该文章了【C语言】 程序员的自我修养之(程序编译过程)
这里我们直接总结一下:
- C++与C找不到函数地址的报错信息
为什么他们报错信息都不一样呢?接下来让我们看看吧!
1.实际项目通常是由多个头文件和多个源文件构成,而通过C语言阶段学习的编译链接,我们
可以知道,【当前a.cpp中调用了b.cpp中定义的Add函数时】,编译后链接前,a.o的目标
文件中没有Add的函数地址,因为Add是在b.cpp中定义的,所以Add的地址在b.o中。那么
怎么办呢?
2.所以链接阶段就是专门处理这种问题,链接器看到a.o调用Add,但是没有Add的地址,就
会到b.o的符号表中找Add的地址,然后链接到一起。(老师要带同学们回顾一下)
3.那么链接时,面对Add函数,链接接器会使用哪个名字去找呢?这里每个编译器都有自己的
函数名修饰规则。
4.由于Windows下vs的修饰规则过于复杂,而Linux下g++的修饰规则简单易懂,下面我们使
用了g++演示了这个修饰后的名字。
5.通过下面我们可以看出gcc的函数修饰后名字不变。而g++的函数修饰后变成【_Z+函数长度+函数名+类型首字母】
- 采用C语言编译器编译后结果
结论:在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变。C语言是静态链接的语言,函数的调用是通过函数名进行匹配的。由于函数名在编译阶段必须唯一,因此在C语言中,同一个作用域内不能存在同名的函数。
- 采用C++编译器编译后
结论:在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参数类型信息添加到修改后的名字中。
对比Linux会发现,windows下vs编译器对函数名字修饰规则相对复杂难懂,但道理都是类似的.
6.通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修
饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
7.如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办
法区分。
C++内联函数
C++内联函数(inline function)是一种特殊类型的函数,以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调用建立栈帧的开销,内联函数提升程序运行的效率
如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的调用。
查看方式:
1.在release模式下,查看编译器生成的汇编代码中是否存在call Add
2.在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不
会对代码进行优化,以下给出vs2013的设置方式)
1.inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会
用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运
行效率。
2.inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建
议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不
是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。下图为
《C++prime》第五版关于inline的建议:
3.inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址
了,链接就会找不到
// F.h #include <iostream> using namespace std; inline void f(int i); // F.cpp #include "F.h" void f(int i) { cout << i << endl; } // main.cpp #include "F.h" int main() { f(10); return 0; } // 链接错误:main.obj : error LNK2019: 无法解析的外部符号 "void __cdecl f(int)" (?f@@YAXH@Z),该符号在函数 _main 中被引用
在通常情况下,将内联函数的声明和定义放在同一个头文件中是比较推荐的做法。这是因为内联函数在编译时被展开,没有实际的函数地址,所以链接器无法正确地解析和连接分离的内联函数的定义。
如果将内联函数的声明放在头文件中,而将定义放在源文件中,那么在链接时,其他源文件无法找到该函数的定义,从而导致链接错误。
为了避免链接错误,通常的做法是在头文件中同时包含内联函数的声明和定义,并且将这些函数声明为inline,以确保函数被正确地展开和内联。