营销型的物流网站模板,男科24小时免费咨询,个人参与防疫工作总结,自己怎么样建网站前言#xff1a; 这篇文章是c入门基础的第一站的中篇,涉及的知识点 函数重载:函数重载的原理--名字修饰 引用:概念、特性、使用场景、常引用、传值、传引用效率比较的知识点 目录 5. 函数重载 #xff08;续#xff09; C支持函数重载的原理--名字修饰(name Mangling) 为什么…前言 这篇文章是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区分函数不同的依据是去找函数的地址(本质也就是第一句指令的地址)找到地址之后将其函数名修饰成特殊函数名: _Z4funcid 意思是四个字节,func(int,double) _Z4funcdi 意思是四个字节,func(double,int) 在符合表里面 用一个独特的符号去代表一个类型跟据类型的个数不同、类型不同、类型的顺序不同修饰出了的名字就是不一样的所以根据这点就可以在函数名相同的情况下区分不同的函数。 vs2019底下 void __cdecl func(int,double) (?funcYAXHNZ) void __cdecl func(double,int) (?funcYAXNHZ) 因为这两个函数虽然函数名字相同但是函数类型的顺序不同所以编译器会根据情况修饰成特殊的函数名 问题 函数名修饰规则带入返回值返回值不能能否构成重载? 不能。 为什么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;//用指针的方式是为了不在不同栈帧内创建ia[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;intb 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;
}
当变量前面有很大一块空间被占用时有可能不会被覆盖 写一个相加两个变量值的代码 代码实现
#includeiostream
#includeassert.h
int 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 time.h
#includeiostreamstruct 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 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();return 0;
} 关于顺序表的读取与修改 c语言接口 struct SeqList
{int a[10];int size;
};//C的接口设计
//读取第i个位置
int SLAT(struct SeqList* ps, int i)
{assert(ips-size);//防止越界//...return ps-a[i];
}
//修改第i个位置的值
void SLModify(struct SeqList* ps, int i, int x)
{assert(i ps-size);// ...ps-a[i] x;
} 可以看待以上代码C语言实现读取和修改结构体成员--数组元素时是非常繁琐的实现功能就要编写一个功能函数但是如果换成c的引用那就可以一个函数实现两个功能 Cpp的接口设计: 代码示例 CPP接口设计
//读 or 修改第i个位置的值
#includeiostream
#includeassert.h
int 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里面有个规定:表达式转换的时候会产生一个临时变量具有常性 以及函数返回的时候也会产生一个临时对象 本章未结束尽快更新下一章完结此篇。