• C++:超越C语言的独特魅力


    W...Y的主页😊

    代码仓库分享💕 


     🍔前言:

    今天我们依旧来完善补充C++,区分C++与C语言的区别。上一篇我们讲了关键字、命名空间、C++的输入与输出、缺省参数等知识点。今天我们继续走进C++的世界。

    目录

    函数重载

    函数重载概念

     C++支持函数重载的原理--名字修饰(name Mangling)

    引用

    引用概念

    引用特性

    常引用

    使用场景

     做参数

    做返回值

     传值、传引用效率比较 

    值和引用的作为返回值类型的性能比较 

    引用和指针的区别

    内联函数 

    概念

    特性


    函数重载

    函数重载有点像“一词多义”,我们汉语的博大精深就经常会出现一词多义的现象,就如同一个笑话一样:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个
    是男足。前者是“谁也赢不了!”,后者是“谁也赢不了!”

    函数重载概念

    函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这
    些同名函数的形参列表(参数个数 或 类型 或 类型顺序)不同,常用来处理实现功能类似数据类型
    不同的问题。

    C语言是不支持同名函数的,这就是C语言的一个“坑”。比如我们想要实现整数的相加与浮点数的相加:

    1. int Add(int left, int right)
    2. {
    3. cout << "int Add(int left, int right)" << endl;
    4. return left + right;
    5. }
    6. double Add(double left, double right)
    7. {
    8. cout << "double Add(double left, double right)" << endl;
    9. return left + right;
    10. }

    当我们在C语言中调用此函数时就会报错,但是在C++中就不会。在C++中函数重载支持的条件是:函数名相同,参数不同。而函数名相同,参数不同的情况有三种:

    1.类型不同。2.个数不同。3.顺序不同。

    举三个不同例子:

    1. #include
    2. using namespace std;
    3. // 1、参数类型不同
    4. int Add(int left, int right)
    5. {
    6. cout << "int Add(int left, int right)" << endl;
    7. return left + right;
    8. }
    9. double Add(double left, double right)
    10. {
    11. cout << "double Add(double left, double right)" << endl;
    12. return left + right;
    13. }
    14. // 2、参数个数不同
    15. void f()
    16. {
    17. cout << "f()" << endl;
    18. }
    19. void f(int a)
    20. {
    21. cout << "f(int a)" << endl;
    22. }
    23. // 3、参数类型顺序不同
    24. void f(int a, char b)
    25. {
    26. cout << "f(int a,char b)" << endl;
    27. }
    28. void f(char b, int a)
    29. {
    30. cout << "f(char b, int a)" << endl;
    31. }
    32. int main()
    33. {
    34. Add(10, 20);
    35. Add(10.1, 20.2);
    36. f();
    37. f(10);
    38. f(10, 'a');
    39. f('a', 10);
    40. return 0;
    41. }

    这串代码很好诠释了三种不同,让我们更容易理解。

    注意:如果两个函数构成重载,不传参数时使用函数参数的缺省参数就会存在二义性!!! 

    1. #include
    2. using namespace std;
    3. void f()
    4. {
    5. cout << "f()" << endl;
    6. }
    7. void f(int a = 0)
    8. {
    9. cout << "f(int a)" << endl;
    10. }
    11. int main()
    12. {
    13. f();
    14. return 0;
    15. }

     上述代码就是一个二义性代码,当函数不传参时两个函数都可以调用,这时就非常危险。所以我们要避免这种情况的发生!

    函数重载不同点全部在函数参数的地方,却在返回值处没有做过多工作,这时为什么呢?我们使用反证法假设返回值不同,但是函数调用却不知道调用谁,所以只能是参数不同才可以区分。

     C++支持函数重载的原理--名字修饰(name Mangling)

    为什么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语言编译器编译后结果

    采用C++编译器编译后结果 

    结论:在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参
    数类型信息添加到修改后的名字中。

    Windows下名字修饰规则 :

    对比Linux会发现,windows下vs编译器对函数名字修饰规则相对复杂难懂,但道理都
    是类似的,我们就不做细致的研究了。
    下面是对函数名修饰:
    C/C++的调用约定icon-default.png?t=N7T8http://blog.csdn.net/lioncolumn/article/details/10376891
    6. 通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修
    饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
    7. 如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办
    法区分。 

    所以C语言不支持函数重载,C++支持函数重载!

    引用

    引用概念

    引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空
    间,它和它引用的变量共用同一块内存空间。比如:水浒传中的林冲,被称为“豹子头”。

    类型& 引用变量名(对象名) = 引用实体;

    1. void TestRef()
    2. {
    3.   int a = 10;
    4.   int& ra = a;//<====定义引用类型
    5.   printf("%p\n", &a);
    6.   printf("%p\n", &ra);
    7. }

    注意:引用类型必须和引用实体同种类型

    引用特性

    1. 引用在定义时必须初始化
    2. 一个变量可以有多个引用
    3. 引用一旦引用一个实体,再不能引用其他实体

    1. void TestRef()
    2. {
    3.  int a = 10;
    4.  // int& ra;  // 该条语句编译时会出错
    5.  int& ra = a;
    6.  int& rra = a;
    7.  printf("%p %p %p\n", &a, &ra, &rra);
    8. }

     别名与原变量的地址相同,所以a创建了变量,b是a的别名,所以b++就是a++的结果。

    1. int main()
    2. {
    3. int a = 0;
    4. int& b = a;
    5. a++;
    6. b++;
    7. cout << a << endl << b << endl;
    8. return 0;
    9. }

    注意:也可以给别名取别名!!! 

    当我们创建一个函数时,参数是指针时,我们也可以用引用进行表示。

    1. void Swap(int& left, int& right)
    2. {
    3. int temp = left;
    4. left = right;
    5. right = temp;
    6. }

    所以在无哨兵位链表时,我们可以用引用代替二级指针!

    引用与指针用法非常相似,但是引用能完全替代指针吗?我们来看一段代码: 

    1. int main()
    2. {
    3. int a = 0;
    4. int& c = a;
    5. int b = 1;
    6. //c变成b的别名还是给c赋值呢?
    7. c = b;
    8. printf("%d %d %d \n", a, b, c);
    9. return 0;
    10. }

     上述代码是c变成b的别名还是给c进行赋值呢?如果c变成b的别名就是改变指向的?如果b给c进行赋值就不能改变指向。很明显不能改变指向。只是单纯的赋值,所以引用与指针的区别出现了,引用无法替代指针。

    常引用

    什么是常引用,就是对不可修改的常量做引用,这样做与#define宏定义有点类似,但是底层逻辑是不相同的,宏定义是在预处理阶段进行替换,所以常引用可以进行调试出理,但是宏定义就不行。

     那怎样进行常引用定义呢?

    1. void TestConstRef()
    2. {
    3.   const int a = 10;
    4.   //int& ra = a;  // 该语句编译时会出错,a为常量
    5.   const int& ra = a;
    6.   // int& b = 10; // 该语句编译时会出错,b为常量
    7.   const int& b = 10;
    8.   double d = 12.34;
    9.   //int& rd = d; // 该语句编译时会出错,类型不同
    10.   const int& rd = d;
    11. }

    以上是常引用定义的错误点,就是因为使用了常引用将本没有权限的内容变得有权限,导致不合理,所以出现错误。

    总结:常引用可以将定义的内容权限缩小,但是不能进行放大。

    使用场景

     做参数

    1. void Swap(int& left, int& right)
    2. {
    3.  int temp = left;
    4.  left = right;
    5.  right = temp;
    6. }

    做参数在上文已经提到,这里就不做过多的解释。

    做返回值

    1. int& Count()
    2. {
    3.  static int n = 0;
    4.  n++;
    5.  // ...
    6.  return n;
    7. }

    我们可以将返回值设置为int&,但是我们没有进行取别名的操作,为什么还可以进行返回呢?因为计算机可以帮我们自动取一个别名(我们不知道)然后进行函数返回值的返回。我们一般int的类型的返回值是将返回内容进行拷贝一份进行返回,因为在函数栈帧调用后函数内存就会被销毁!那就有人会闻,函数栈帧都被销毁了,取的别名传的内容就是一个错误的内容吗?

    这就会引出我们下面的问题,下面代码会输出什么结果?

    1. int& Add(int a, int b)
    2. {
    3.   int c = a + b;
    4.   return c;
    5. }
    6. int main()
    7. {
    8.   int& ret = Add(1, 2);
    9.   Add(3, 4);
    10.   cout << "Add(1, 2) is :"<< ret <
    11.   return 0;
    12. }

     那为什么是7,不应该是3吗?

    注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用
    引用返回,如果已经还给系统了,则必须使用传值返回。

    这里无论是3还是7都是不严谨的,有的程序还会返回错误值,这都取决于内存是否环给系统。但是我们这里给ret赋值一次,但是却出现了第二次调用的值,就是因为ret是计算机给出c的别名,地址都是相同的,所以只要调用一次函数就会更新ret中的值!上面函数栈帧图非常清楚,我们可以理解一下!!! 

    所以用引用作为返回值去返回临时变量是非常危险的,所以我们应该返回static静态变量! 

     传值、传引用效率比较 

    以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直
    接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效
    率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。

    1. #include
    2. struct A{ int a[10000]; };
    3. void TestFunc1(A a){}
    4. void TestFunc2(A& a){}
    5. void TestRefAndValue()
    6. {
    7. A a;
    8. // 以值作为函数参数
    9. size_t begin1 = clock();
    10. for (size_t i = 0; i < 10000; ++i)
    11. TestFunc1(a);
    12. size_t end1 = clock();
    13. // 以引用作为函数参数
    14. size_t begin2 = clock();
    15. for (size_t i = 0; i < 10000; ++i)
    16. TestFunc2(a);
    17. size_t end2 = clock();
    18. // 分别计算两个函数运行结束后的时间
    19. cout << "TestFunc1(A)-time:" << end1 - begin1 << endl;
    20. cout << "TestFunc2(A&)-time:" << end2 - begin2 << endl;
    21. }

    当我们进行大规模传参时,传值需要进行拷贝,而传引用却不需要,所以根据上述代码我们可以看到传引用比传值的效率高很多。 

    值和引用的作为返回值类型的性能比较 

    1. #include
    2. struct A{ int a[10000]; };
    3. A a;
    4. // 值返回
    5. A TestFunc1() { return a;}
    6. // 引用返回
    7. A& TestFunc2(){ return a;}
    8. void TestReturnByRefOrValue()
    9. {
    10. // 以值作为函数的返回值类型
    11. size_t begin1 = clock();
    12. for (size_t i = 0; i < 100000; ++i)
    13. TestFunc1();
    14. size_t end1 = clock();
    15. // 以引用作为函数的返回值类型
    16. size_t begin2 = clock();
    17. for (size_t i = 0; i < 100000; ++i)
    18. TestFunc2();
    19. size_t end2 = clock();
    20. // 计算两个函数运算完成之后的时间
    21. cout << "TestFunc1 time:" << end1 - begin1 << endl;
    22. cout << "TestFunc2 time:" << end2 - begin2 << endl;
    23. }

     返回值也是如此,传值时需要将返回值进行拷贝而传引用不用,所以传引用相率高。

     总结:通过上述代码的比较,发现传值和指针在作为传参以及返回值类型上效率相差很大。

    引用和指针的区别

    在语法概念上引用就是一个别名,没有独立空间,和其引用实体共用同一块空间。

    1. int main()
    2. {
    3. int a = 10;
    4. int& ra = a;
    5. cout<<"&a = "<<&a<
    6. cout<<"&ra = "<<&ra<
    7. return 0;
    8. }

    上述代码所打印出的地址相同,所以从语法层面来看引用是没有空间开辟的。 

    在底层实现上实际是有空间的,因为引用是按照指针方式来实现的。

    1. int main()
    2. {
    3. int a = 10;
    4. int& ra = a;
    5. ra = 20;
    6. int* pa = &a;
    7. *pa = 20;
    8. return 0;
    9. }

    上述代码我们转成汇编语言进行观看:

    转成汇编语言后,发现引用与指针的逻辑是相同的。 

     引用和指针的不同点:
    1. 引用概念上定义一个变量的别名,指针存储一个变量地址。
    2. 引用在定义时必须初始化,指针没有要求
    3. 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何
    一个同类型实体
    4. 没有NULL引用,但有NULL指针
    5. 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32
    位平台下占4个字节)
    6. 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小

    7. 有多级指针,但是没有多级引用
    8. 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
    9. 引用比指针使用起来相对更安全

    内联函数 

    概念

    以inline修饰的函数叫做内联函数,编译时C++编译器会在调用内联函数的地方展开,没有函数调
    用建立栈帧的开销,内联函数提升程序运行的效率。

    如果在上述函数前增加inline关键字将其改成内联函数,在编译期间编译器会用函数体替换函数的
    调用。

    查看方式:
    1. 在release模式下,查看编译器生成的汇编代码中是否存在call Add
    2. 在debug模式下,需要对编译器进行设置,否则不会展开(因为debug模式下,编译器默认不
    会对代码进行优化,以下给出vs2013的设置方式)

     

    特性

    1. inline是一种以空间换时间的做法,如果编译器将函数当成内联函数处理,在编译阶段,会
    用函数体替换函数调用,缺陷:可能会使目标文件变大,优势:少了调用开销,提高程序运
    行效率。
    2. inline对于编译器而言只是一个建议,不同编译器关于inline实现机制可能不同,一般建
    议:将函数规模较小(即函数不是很长,具体没有准确的说法,取决于编译器内部实现)、不
    是递归、且频繁调用的函数采用inline修饰,否则编译器会忽略inline特性。下图为
    《C++prime》第五版关于inline的建议:
    3. inline不建议声明和定义分离,分离会导致链接错误。因为inline被展开,就没有函数地址
    了,链接就会找不到 

     所以inline的特性与register非常相似,是一种“请求”,程序会根据实际进行选择优化!!!


    以上就是本次博客全部内容,感谢大家观看,一键三连支持一下博主吧!!! 

  • 相关阅读:
    大数据Hadoop核心架构HDFS+MapReduce+Hbase+Hive内部机理详解
    辅助知识-第2 章 项目合同管理
    《JAVA筑基100例》导读
    2.简单的搭建后端,一步一步从基础开始(2023-9-20优化更新第一次)
    1.11 小红书起号必看,如何找到适合自己的对标账号?【玩赚小红书】
    LLM - 大模型速递 InternLM-20B 快速入门
    激励合作伙伴的8个想法
    SpringBoot2基础篇(三)—— 整合第三方技术
    flask学习笔记
    听GPT 讲Rust源代码--library/std(13)
  • 原文地址:https://blog.csdn.net/m0_74755811/article/details/133874519