公司动态

【走进C++ (3)】缺省参数 函数重载 引用

📅 2026/7/15 3:07:37
【走进C++ (3)】缺省参数  函数重载  引用
文章目录一、缺省参数1、概念2、分类和使用2.1 全缺省参数2.2 半缺省参数2.3 参数传递特性2.4 总结3、声明和定义二、函数重载1、概念2、构成函数重载的三种情况3、不能构成函数重载的情况4、可以构成重载但会引起歧义的情况5、函数重载的原理拓展学习三 、引用1、概念2、特性3、使用场景3.1 做参数3.2 做返回值4、传值、传引用效率比较5、引用和指针的区别一、缺省参数1、概念缺省参数是声明或定义函数时为函数的参数指定一个缺省值默认值。在调用该函数时如果没有指定实参则采用该形参的缺省值否则使用指定的实参。简而言之当函数调用时提供了实参则使用实参如果没有提供对应实参则使用缺省值。具体的代码实现如下#include iostream using namespace std; void Func(int a 0) { cout a endl; } int main() { Func(); // 打印0 Func(5); // 打印5 return 0; }第一次调用Func时没有指定实参就会使用缺省值 0 作为参数的值。第二次调用Func时指定了实参所以照常传入这里指定的是 5参数就使用传入的值也就是 5。2、分类和使用2.1 全缺省参数函数中所有的参数都有缺省值。#include iostream using namespace std; void Func(int a 1, int b 10, int c 20) { cout a a ; cout b b ; cout c c endl; } int main() { Func(); // 一个都不传 Func(0); // 只传1个 Func(0, 1); // 只传2个 Func(0, 1, 2); // 全部都传 return 0; }运行结果如下在这个程序中执行的流程是这样的第一次调用Func()时没有指定实参就会使用缺省值作为参数的值。第二次调用Func(0)时传入了一个实参所以a采用传入的值其他的参数则继续使用缺省值作为参数的值。第三次调用Func(0,1)时传入了两个实参所以a/b采用传入的值c则继续使用缺省值作为参数的值。第四次调用Func(0,1,2)时全部参数都传入参数所以都是采用传入的值作为参数值缺省值则不起作用。由此也可以看到在有缺省参数的时候调用该函数传参时传参的顺序是从左到右依次传入的。2.2 半缺省参数半缺省参数要求缺省值必须从参数列表的右侧连续开始设置。半缺省并不是缺省一半的参数而是缺省一部分可以是1个也可以是多个。#include iostream using namespace std; void Func(int a, int b, int c 20) { cout a a ; cout b b ; cout c c endl; } int main() { // 因为a和b参数不带缺省值所以调用时一定要传该参数 // Func(); // 错误 // Func(0); // 错误 Func(0, 1); // 没缺省的参数一定要传值 Func(0, 1, 2); // 全部都传 return 0; }运行结果如下2.3 参数传递特性既然可以全缺省也可以半缺省那可以中间缺省吗#include iostream using namespace std; void Func(int a 10, int b, int c 20) { cout a a ; cout b b ; cout c c endl; } int main() { Func(, 1, ); return 0; }运行结果如下可以发现编译器报错。这是因为按照语法要求缺省参数的设置只能从右到左而且函数调用时参数的传递也是从左到右Func(, 1, )本身也是错误的调用方式因此不可以这么传递。2.4 总结1缺省值必须是常量或者全局变量通常使用常量因为缺省参数必须是一个在编译阶段能够确定的表达式。2半缺省参数必须从右侧连续设置不能跳过中间参数。3C语言不支持缺省参数编译器不支持。3、声明和定义缺省参数不能在函数声明和定义中同时出现即同一个作用域下不能同时重复指定在实际开发中通常建议将缺省参数写在函数声明中而函数定义中不再设置缺省值。原因如下因为C采用单文件独立编译机制编译器在处理函数调用点时只能看到声明接口若将缺省值写在定义中调用者将无法获知默认值而报错。同时C标准严格禁止在声明和定义中同时设置缺省值这会导致二义性即使两处设置的值完全相同编译器也不会去比对而是直接报重定义默认参数的语法错误。当函数的声明与定义位于同一个文件中且定义在调用点之前时才允许直接在定义处设置缺省值。#include iostream using namespace std; // 声明和定义一起而且也在调用点之前可以直接设置缺省值 void Func1(int a 1) { cout a a endl; } // 声明中设置缺省值 void Func(int a 10, int b 20, int c 30) int main() { Func(); Func1(); return 0; } // 定义中不再设置任何缺省值 void Func(int a, int b, int c) { cout a a ; cout b b ; cout c c endl; }二、函数重载1、概念自然语言中一个词可以有多重含义人们可以通过上下文来判断该词真实的含义即该词被重载了。比如以前有一个笑话国有两个体育项目大家根本不用看也不用担心。一个是乒乓球一个是男足。前者是“谁也赢不了”后者是“谁也赢不了”函数重载是函数的一种特殊情况函数重载是指在同一作用域中可以定义多个同名函数这些函数通过【参数列表不同】进行区分。2、构成函数重载的三种情况函数重载要求函数参数列表不同分别是参数数量、类型或类型顺序不同也只有这三种情况才能构成函数重载。1参数数量不同#include iostream using namespace std; void f(int a, int b) { cout f(int a, int b) endl; } void f(int a, int b, int c) { cout f(int a, int b, int c) endl; } int main() { f(1, 2); f(1, 2, 3); return 0; }运行结果如下2参数类型不同#include iostream using namespace std; int Add(int left, int right) { cout Add(int left, int right) endl; return left right; } double Add(double left, double right) { cout Add(double left, double right) endl; return left right; } int main() { Add(1, 2); Add(1.1, 2.2); return 0; }运行结果如下3参数类型顺序不同参数列表中的类型顺序不同本质也属于参数类型不同的一种情况。#include iostream using namespace std; // 类型顺序不同本质也是类型不同 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; } int main() { f(1, a); f(a, 1); return 0; }运行结果如下3、不能构成函数重载的情况有些情况下比如返回值不同、缺省值不同就不能构成函数重载。1返回值不同运行会报错调用函数时无法根据参数列表确定调用哪个函数因此无法构成重载。#include iostream using namespace std; void f(int a, int b) { cout void f(int a, int b) endl; } int f(int a, int b) { cout int f(int a, int b) endl; } int main() { f(1, 2); // 报错不知道调用谁 return 0; }2缺省值不同编译器同样无法区分两个函数因此也会运行报错。#include iostream using namespace std; void f(int a) { cout f(int a) endl; } void f(int a 1) { cout f(int a 1) endl; } int main() { f(10); // 报错不知道调用谁 return 0; }4、可以构成重载但会引起歧义的情况f()没有传参此时既可以走无参f()也可以走缺省参数的f(int a 1)此时会产生歧义编译器无法确定因此会报错。#include iostream using namespace std; void f() { cout f() endl; } void f(int a 1) { cout f(int a 1) endl; } int main() { f(); // 报错两个函数都可以调用存在歧义不知道调用谁 f(10); // 成功调用f(int a 1) return 0; }5、函数重载的原理拓展学习C支持函数重载的本质原理就是名字修饰(name Mangling)。在C/C中一个程序要运行起来需要经历以下几个阶段预处理、编译、汇编、链接。1当前a.cpp中调用了b.cpp中定义的Add函数时编译后链接前a.o的目标文件中并没有Add函数的具体地址因为Add是在b.cpp中定义的函数Add地址保存在b.o中。那么怎么办呢2所以链接阶段就是专门处理这种问题链接器看到a.o中存在对Add的引用但是没有对应的函数定义就会到b.o的符号表中查找Add的符号信息并将两者链接起来。3那么链接时面对Add函数链接接器会使用哪个名字去找呢这里每个编译器都有自己的函数名修饰规则。4由于Windows下vs的修饰规则过于复杂而Linux下g的修饰规则简单易懂下面我们使用了g演示了这个修饰后的名字。5通过下面可以看出在Linux下采用gcc编译时函数符号名通常不会发生改变而采用g编译时编译器会对函数名进行修饰将函数参数类型等信息加入到修饰后的名字中【_Z函数长度函数名类型首字母】。其意思是_Z 表示 C 名字修饰 3 表示函数名 Add 的长度 Add 表示函数名 ii 表示两个 int 类型参数。gcc和g编译器分别编译后结果结论在linux下采用gcc编译完成后函数名字的修饰没有发生改变采用g编译完成后函数名字的修饰发生改变编译器将函数参数类型信息添加到修改后的名字中。而Windows下名字修饰规则如下对比Linux会发现windows下vs编译器对函数名字修饰规则相对复杂难懂但道理都是类似的。6通过这里就理解了C语言没办法支持重载因为同名函数没办法区分。而C是通过函数修饰规则来区分只要参数列表不同经过名字修饰后生成的符号名通常就不同因此 C 可以支持函数重载。7如果两个函数的函数名和参数列表完全相同只是返回值不同则不能构成函数重载因为函数调用时编译器不会根据返回值进行区分。三 、引用1、概念引用不是新定义一个变量而是给已存在变量取了一个别名从语法层面来看引用不会创建新的对象它和引用实体表示的是同一个对象。比如张三可以叫成小张。语法上类型 引用变量名(引用名) 引用实体。这里的并不是取地址运算符而是引用声明符用于定义引用。。更具体的代码如下#include iostream using namespace std; int main() { int a 0; int b a; // b就是a的别名 // 观察两者的地址可以发现是相同的 cout b endl; cout a endl; b; a; cout a b endl; // 两者的值也会同时改变 return 0; }运行结果如下从运行结果可以看到变量和引用取地址得到的是同一个地址。在语法层面可以将引用理解为原变量的另一个名字而不是新创建了一个对象。2、特性1引用在定义时必须初始化否则编译无法通过。int main() { int a 0; int b; b a; // 报错必须在定义时初始化 return 0; }2一个变量可以有多个引用对该变量的引用起引用也是对这个变量的引用。int main() { int a 0; int b a; // 引用1 int c a; // 引用2 int d b; // 也是对a的引用 cout a endl; cout b endl; cout c endl; cout d endl; return 0; }运行结果如下可以发现它们的地址都相同因此无论是直接引用a还是对引用b再次建立引用本质上都是引用同一个对象a。3引用一旦引用一个实体就不能引用其他实体。int main() { int a 0; int b a; int c 1; b c; // 实际这一步是进行赋值操作无法改变引用指向 return 0; }执行b c后并不会改变引用的绑定关系而是把c的值赋给b所引用的对象也就是a。所以可以得出结论在定义别名后就是谁的别名而不能重新变成其他人的别名。需要注意的是指针是可以改变指向的但当我们查看编译后的汇编代码时可以发现引用的操作和指针的操作的汇编代码是一样的因此可以认为大多数编译器会采用指针的方式实现引用但这属于编译器实现细节C标准并没有规定引用必须采用指针实现。int main() { int a 1; int r a; int b 2; int* p b; int c 3; p c; // 改变指针指向 return 0; }4常引用临时对象右值默认只能绑定到const引用因此通常使用常引用来引用常量、临时对象以及类型转换产生的临时对象临时变量具有常性。int main() { const int a 10; //int ra a; // 该语句编译时会出错a为常量 const int ra a; // int b 10; // 该语句编译时会出错b为常量 const int b 10; double d 12.34; //int rd d; // 该语句编译时会出错类型不同 const int rd d; return 0; }3、使用场景3.1 做参数引用做参数有两层价值1做输出型参数可以直接修改实参。2减少对象拷贝提高传参效率尤其适用于较大的对象或自定义类型。虽然使用指针同样可以实现这两种效果但引用的写法更加简洁也更加符合普通变量的使用习惯。下面以交换两个数为例void Swap(int a, int b) { int tmp a; a b; b tmp; } int main() { int x 0, y 1; Swap(x, y); cout x y endl; return 0; }在调用Swap()时参数a和b分别是x、y的引用相当于分别成为了它们的别名因此在函数内部修改a、b实际上就是修改x、y。如果采用普通值传递那么函数内部修改的是形参的副本无法影响实参如果采用指针传参则需要传递地址并通过解引用进行访问void Swap(int* a, int* b) { int tmp *a; *a *b; *b tmp; }相比之下引用不需要取地址和解引用也不存在多级指针的问题因此在不需要使用空指针或修改绑定关系的情况下引用通常比指针更加简洁、易读。3.2 做返回值1错误做法引用也可以作为函数返回值但必须保证被引用对象在函数返回后仍然存在否则就会产生野引用。int Func() { int a 10; return a; } // 用于演示代码在Func的栈空间中覆盖此代码使ret的值发生改变 // 因此代码的结构和Func保持一致 int Func1() { int a 20; return a; } int main() { int ret Func(); cout ret endl; Func1(); cout Func1() endl; return 0; }函数中的变量a是局部变量它存储在函数栈帧中。当Func()执行结束后函数栈帧被销毁局部变量a的生命周期也随之结束因此返回的引用已经失效形成了野引用。在某些环境下由于该栈空间暂时还没有被其他数据覆盖第一次输出可能仍然是10。但当继续调用Func1()时新的函数调用很可能会复用之前释放的栈空间导致原来保存10的那块栈空间被新的局部变量20覆盖。此时再次访问ret就有可能输出20。需要注意的是这种现象只是某一种可能的结果。由于程序访问的是生命周期已经结束的对象因此其行为属于未定义行为错误的逻辑不同的编译器、不同的优化选项甚至同一程序在不同运行环境下其结果都可能不同。2正确做法引用返回值要求返回对象必须在函数结束后依然存在例如可以返回全局变量、静态局部变量、堆空间对象等int Func() { static int a 10; return a; } int main() { // 返回的是对象本身而不是副本 // 因此Func()即变量a可以被直接赋值 Func() 100; cout Func() endl; }由于static修饰的局部变量生命周期贯穿整个程序运行期间因此返回它的引用是安全的。因此也可以看出引用做返回值最大的作用就是避免对象拷贝提高效率如果返回的是非常量引用还可以直接修改返回对象。4、传值、传引用效率比较以值作为参数或者返回值类型在传参和返回期间函数不会直接传递实参或者将变量本身直接返回而是传递实参或者返回值通常需要构造返回对象的副本因此当参数或返回值是较大的对象时采用值传递值返回会产生对象拷贝效率通常低于引用传递引用返回。为了便于观察差异建议在关闭编译器优化的情况下对下面代码进行测试#include iostream #include time.h using namespace std; struct A { int a[10000]; }; void Func1(A a) {} void Func2(A a) {} A b; A Func3() { return b; } A Func4() { return b; } int main() { A a; // 以值作为函数参数 size_t begin1 clock(); for (size_t i 0; i 10000; i) Func1(a); size_t end1 clock(); // 以引用作为函数参数 size_t begin2 clock(); for (size_t i 0; i 10000; i) Func2(a); size_t end2 clock(); // 以值作为函数的返回值类型 size_t begin3 clock(); for (size_t i 0; i 100000; i) Func3(); size_t end3 clock(); // 以引用作为函数的返回值类型 size_t begin4 clock(); for (size_t i 0; i 100000; i) Func4(); size_t end4 clock(); cout Func1(A)-time: end1 - begin1 endl; cout Func2(A)-time: end2 - begin2 endl; cout Func3-time: end3 - begin3 endl; cout Func4-time: end4 - begin4 endl; return 0; }运行结果如下通过上述代码运行结果可以发现对于较大的对象采用引用作为参数或返回值通常比值传递和值返回效率更高因为可以避免对象拷贝。但对于内置类型如int、char、double等由于对象本身很小值传递与引用传递的性能差异通常可以忽略更应优先考虑代码的可读性。5、引用和指针的区别1语法上1、引用是别名语法层面不开空间指针是地址需要开空间存地址2、引用必须初始化指针可以初始化也可以不初始化3、引用不能改变绑定对象指针可以4、引用相对更安全没有空引用但是有空指针容易出现野指针但是不容易出现野引用做返回值可能会出现5、没有NULL引用但有NULL指针6、在sizeof中含义不同引用结果为引用对象的大小但指针始终是地址空间所占字节个数(32位平台下占4个字节)7、引用自加即引用的实体增加1指针自加即指针向后偏移一个类型的大小8、指针可以形成多级指针而引用没有多级引用访问对象时指针需要显式解引用而引用可以像普通变量一样直接使用由编译器完成相关处理。2底层上在大多数编译器中引用通常采用指针的方式实现因此在汇编层面可以看到引用操作最终会转换成类似指针的操作。不过这属于编译器实现细节C标准并没有规定必须这样实现。