前言:
这篇文章是c++入门基础的第一站的中篇,涉及的知识点 函数重载:函数重载的原理–名字修饰 引用:概念、特性、使用场景、常引用、传值、传引用效率比较的知识点
目录
5. 函数重载 (续)
C++支持函数重载的原理–名字修饰(name Mangling)
为什么C++支持函数重载,而C语言不支持函数重载呢?
6. 引用
引用概念
关于引用的应用:
引用特性
引用在定义时必须初始化
一个变量可以有多个引用
引用一旦引用一个实体,再不能引用其他实体
使用场景
传值、传引用效率比较
值和引用的作为返回值类型的性能比较
关于顺序表的读取与修改
c语言接口
Cpp的接口设计:
常引用
5. 函数重载 (续)
C++支持函数重载的原理—名字修饰(name Mangling)
编译器是如何编译的?
Test.cpp
预处理头文件展开/宏替换/去掉注释/条件编译
Test.i
编译检查语法,生成汇编代码(指令级代码) — 右击鼠标打开反汇编
Test.s
汇编将汇编代码生成二进制机器码
Test.o
链接合并链接,生成可执行程序(a.out / xxx.exe)
在整个编译的过程中涉及到的一个问题是什么呢?
在一个项目里面写了一个stack.h(栈定义的各种接口)和stack.cpp ,这些各种接口不在Test.o内,而在stack.o内,那么怎么去这里找呢。那就涉及到名字去找地址,在链接的时候怎么用名字去找地址呢?
C语言的特点呢 — 直接用函数名去充当函数的名字
这样的后果就是自己都区分不开来,所以C语言是不允许重名的。
那么C++是如何这块的问题呢?如何把两个同名的但参数类型顺序不一样的函数区分开来呢?
那就是函数名修饰规则解决这个问题
当函数只有声明没有定义的时候,就会出现以下的链接错误
当函数只有定义的时候,没有实现的时候,它就没有一堆汇编指令,没有指令就不能生成地址(就没有建立函数栈帧的过程,寄存器没有存地址)。所以在符号表里面拿这个名字去找的时候就找不到,以下是修饰以后的函数名,本质上它是用类型带入这个名字里面去了(函数修饰规则)
而C语言是直接用函数名去找:
在linux环境底下去看:
以下两张图均是函数名修饰规则
Linux底下:c++区分函数不同的依据是去找函数的地址(本质也就是第一句指令的地址),找到地址之后,将其函数名修饰成特殊函数名:
意思是:四个字节,func(int,double)
意思是:四个字节,func(double,int)
在符合表里面:
用一个独特的符号去代表一个类型,跟据类型的个数不同、类型不同、类型的顺序不同,修饰出了的名字就是不一样的,所以根据这点,就可以在函数名相同的情况下区分不同的函数。
vs2019底下:
void __cdecl func(int,double)” (” />HN@Z)
void __cdecl func(double,int)” (?func@@YAXNH@Z)
因为这两个函数虽然函数名字相同,但是函数类型的顺序不同,所以编译器会根据情况修饰成特殊的函数名
问题:
函数名修饰规则带入返回值,返回值不能能否构成重载? 不能。
为什么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+函数长度+函数名+类型首字母】
- 采用
结论:在linux下,采用gcc编译完成后,函数名字的修饰没有发生改变。
- 采用C语言编译器编译后结果
结论:在linux下,采用g++编译完成后,函数名字的修饰发生改变,编译器将函数参 数类型信息添加到修改后的名字中。
- Windows下名字修饰规则
对比Linux会发现,windows下vs编译器对函数名字修饰规则相对复杂难懂,但道理都 是类似的,我们就不做细致的研究了。 6. 通过这里就理解了C语言没办法支持重载,因为同名函数没办法区分。而C++是通过函数修 饰规则来区分,只要参数不同,修饰出来的名字就不一样,就支持了重载。
7. 如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办法区分。
6. 引用
引用概念
引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。
现实生活来说:比如:李逵,在家称为“铁牛”,江湖上人称“黑旋风”。
C++为了在拓展语法的过程中,为了防止创新符号太多不好记忆,直接沿用C语言的符号,让其一个符号赋予了多重意思,&在这边不是取地址的意思,而是引用操作符
当b++时,a也会同时++,当两条语句都++时,那么这个值就变为2
代码实现:
int main(){int a = 0;int& b = a;//引用cout << &a << endl;cout << &b << endl;return 0;}
输出:
注意: 引用类型必须和引用 实体是 同种类型的
关于引用的应用:
1️⃣简单的应用,值的交换:
void swap(int& x1, int& x2) { int tmp = x1; x1 = x2; x2 = tmp; } int main() { int size; int x = 0, y = 1; swap(x,y); printf("%d %d", x, y); }
执行:
2️⃣二叉树前序遍历的应用
int TreeSize(struct TreeNode* root) { //写法一 if (root == NULL) return 0;//写法二 return TreeSize(root->left) + TreeSize(root->right) + 1; } void _preorder(struct TreeNode* root, int* a, int& pi) { if (root == NULL) return; //用指针的方式是为了不在不同栈帧内创建i a[pi] = root->val;pi++; _preorder(root->left, a, pi); _preorder(root->right, a, pi); } int* preorderTraversal(struct TreeNode* root, int& returnSize) { int size = TreeSize(root); int* a = (int*)malloc(sizeof(int) * sizeof(int)); int i = 0; _preorder(root,a,i); return a; } int main() { int size = 0; preorderTraversal(nullptr, size); }
执行: 3️⃣关于单链表的链接 前后代码对比:
引用特性
引用在定义时必须初始化
一个变量可以有多个引用
int main(){int a = 0;int& b = a;int& c = a;int& d = b;//给别名取别名,实际上是同一块空间。int x = 1;//赋值b = x;return 0;}
代码执行变化:
引用一旦引用一个实体,再不能引用其他实体
int main(){int a = 0;int& b = a;int x = 1;int&b = x;return 0;}
代码执行:
使用场景
传引用返回和传值返回:
以下的两者返回的方式有什么区别呢?
答:这两种情况的区别,在于传值调用在函数销毁时有寄存器,传引用调用没有寄存器保存值,因为是同一个空间,引用不同于传值和传址,既然直接传的就是这个空间本身,因此既不用创建临时拷贝,也不需要传变量地址。而是直接变量进行赋值。
先来看传值返回:
用了一个全局的寄存器eax把返回值保存起来,待Count函数栈帧销毁后,回到主函数main,再将寄存器里面的值赋值给ret
传引用返回:
这个n的别名,出了作用域就销毁(这意味着返回的值是对已被销毁的变量的引用),还给操作系统了,在还给操作系统的时候,可能将这块空间里面的值给清理了,变成随机值了。由于Count
函数的返回值是无效的(引用已被销毁),所以打印ret
的值将导致未定义的行为。它可能打印1,也可能打印随机值,或者可能导致程序崩溃。
那如果对代码再修改一下呢:
这段代码是非法的。当函数Count()
执行完毕后,局部变量n
将被销毁,引用ret
将会成为悬空引用(dangling reference),它指向了已经归还给操作系统的空间,该空间里面的值可能已经被初始化为随机值。因此,将对n
的引用返回给调用函数是无效的,导致未定义的行为。
总结:
传引用的第一个示例还是第二个示例中,代码都是非法的,并且会导致未定义的行为
如果函数返回时,离开函数作用域后,其栈上空间已经还给系统,因此不能用栈上的空间作为引用类型返回。如果以引用类型返回,返回值的生命周期必须不受函数的限制(即比函数生命周期长)。
只有当出了这个作用域,这个对象还在的情况下,才可以加引用比如便用static将变量设成静态变量,或是全局变量,函数生命周期就不会影响到引用
代码示例:
//传引用调用int& Count(){ int n = 0; n++;return n;}int main(){int& ret = Count();//这里打印的结果可能是1,也可能是随机值cout << ret << endl;cout << ret << endl;return 0;}//传值调用int Count(){int n = 0;n++;return n;}int main(){int ret = Count();cout << ret << endl;cout << ret << endl;return 0;}
当变量前面有很大一块空间被占用时,有可能不会被覆盖:
写一个相加两个变量值的代码:
代码实现:
#include#includeint& Add(int a,int b){int c = a + b;return c;}int main(){int& ret = Add(1, 2);Add(3, 4);cout << "Add(1,2) is :" << ret << endl;}
解析:
注意:如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用 引用返回,如果已经还给系统了,则必须使用传值返回。
传值、传引用效率比较
以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直接返回,而是传递实参或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效 率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
传引用传参(任何时候都可以用)
1、提高效率
2、输出型参数(形参的修改,影响的实参)
#include #includestruct 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();return 0;}
值和引用的作为返回值类型的性能比较
传引用返回(出了函数作用域对象还在才可以用)– static修饰的,全局变量,堆空间等等
1、提高效率
2、修改返回对象
#include 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();return 0;}
关于顺序表的读取与修改
c语言接口
struct SeqList{int a[10];int size;};//C的接口设计 //读取第i个位置int SLAT(struct SeqList* ps, int i){assert(isize);//防止越界//...return ps->a[i];}//修改第i个位置的值void SLModify(struct SeqList* ps, int i, int x){assert(isize);// ...ps->a[i] = x;}
可以看待以上代码,C语言实现读取和修改结构体成员–数组元素时,是非常繁琐的,实现功能就要编写一个功能函数,但是如果换成c++的引用,那就可以一个函数实现两个功能
Cpp的接口设计:
代码示例:
CPP接口设计//读 or 修改第i个位置的值#include#includeint& SLAT(struct SeqList& ps, int i){assert(i < ps.size);return(ps.a[i]);}int main(){struct SeqList s;s.size = 3;SLAT(s, 0) = 10;SLAT(s, 1) = 20;SLAT(s, 2) = 30;cout << SLAT(s, 0) << endl;cout << SLAT(s, 1) << endl;cout << SLAT(s, 2) << endl;return 0;}
特别注意:
常引用
在引用的过程中:
1.权限可以平移
2.权限可以缩小
3.权限不能放大!!!
int main(){const int a = 0;//权限的放大int& b = a;//这个是不行的!!!//权限的平移const int& c = a;//权限的缩小//形象地理解: int x = 0;//齐天大圣const int& y = x;//戴上紧箍咒的孙悟空return 0;}
赋值:
int b = a;//可以的,因为这里是赋值拷贝,b修改不影响a
类型转换:
在c/c++里面有个规定:表达式转换的时候会产生一个临时变量,具有常性
以及函数返回的时候也会产生一个临时对象
本章未结束,尽快更新下一章完结此篇。