C++零基础到工程实战(5.2.5):函数默认参数和函数重载

C++零基础到工程实战(5.2.5):函数默认参数和函数重载 目录前言一、本节学习内容概要1.1 函数默认参数和函数重载是什么1函数默认参数2函数重载1.2 默认参数和重载分别解决什么问题二、函数默认参数的基本使用2.1 什么是函数默认参数2.2 默认参数是如何补充的1只传一个参数2传两个参数3传三个参数2.3 默认参数必须从右往左设置2.4 默认参数的本质三、函数声明、函数定义与默认参数3.1 函数声明和函数定义的区别1函数声明2函数定义3.2 如果有声明默认参数只能写在声明中3.3 为什么不能声明和定义都写默认参数3.4 实际工程如何处理1.h文件2.cpp文件3.5 没有单独声明时默认参数可以写在定义中四、函数重载的基本使用与匹配规则4.1 什么是函数重载4.2 编译器如何区分重载函数1调用无参版本2调用 int 版本3调用 float 版本4调用两个参数版本4.3 返回值不能作为函数重载依据4.4 double、float、int 的重载匹配问题4.5 如何解决 double 调用重载函数的问题1写成 float 字面量2使用 C 语言风格强制类型转换3使用 C 风格强制类型转换4.6 仅保留一个重载函数情况4.7 FUNCSIG 是什么五、NULL、nullptr 与完整代码总结5.1 NULL 和 nullptr 的区别5.2 TestNull(123)、TestNull(ptr)、TestNull(NULL)、TestNull(nullptr)1调用 int 版本2调用 int* 版本3使用 NULL4使用 nullptr5.3 完整代码示例六、总结6.1 默认参数6.2 声明和定义中的默认参数6.3 函数重载6.4 double 调用 int 和 float 重载容易报错6.5 C 中建议使用 nullptr前言在前面的文章中我们已经对C 函数重载做过介绍知道了 C 中允许多个函数使用同一个函数名只要它们的参数类型、参数数量或者参数顺序不同编译器就可以根据调用时传入的实参自动选择对应的函数版本。如果还没有看过前面的内容可以先参考这篇文章一篇搞懂 C 重载函数重载 运算符重载从入门到会用含 、、 实战_一文懂 操作符重载-CSDN博客https://blog.csdn.net/m0_58954356/article/details/155323257?ops_request_miscelastic_search_miscrequest_idf963d69ab4f7c20832b8551ad444c785biz_id0utm_mediumdistribute.pc_search_result.none-task-blog-2~all~ElasticSearch~search_v2-1-155323257-null-null.nonecaseutm_term%E9%87%8D%E8%BD%BDspm1018.2226.3001.4450本节继续围绕函数展开重点讲解两个非常常用、也非常容易混淆的知识点函数默认参数函数重载函数默认参数解决的是调用函数时某些参数不传怎么办函数重载解决的是同一个函数名为什么可以对应多个不同函数除此之外本节还会结合实际代码讲解几个容易出错的地方例如默认参数为什么必须从右往左设置函数声明和函数定义中默认参数应该写在哪里函数重载到底根据什么区分为什么返回值不同不能构成函数重载double、float、int 在重载调用中为什么可能报错NULL 和 nullptr 在函数重载中有什么区别一、本节学习内容概要1.1 函数默认参数和函数重载是什么在 C 中函数比 C 语言更加灵活。除了普通函数定义和调用之外C 还支持函数默认参数和函数重载。1函数默认参数函数默认参数指的是在函数参数列表中提前给某些参数设置默认值。调用函数时如果这些参数没有传入编译器会自动使用默认值补上。例如void TestFuncDec(int x, int y 200, int z 300);调用时可以写TestFuncDec(100); TestFuncDec(110, 210); TestFuncDec(120, 220, 320);虽然三个调用传入的参数数量不同但是都可以正常执行。2函数重载函数重载指的是多个函数可以使用同一个函数名只要参数列表不同即可。名字相同参数列表不同返回值不能单独区分重载。满足重载的几种情况参数类型不同参数个数不同参数顺序不同例如void TestOverload(int x); void TestOverload(float x); void TestOverload(int x, int y);这几个函数名字都叫TestOverload但是参数类型或者参数数量不同所以它们可以同时存在。编译器会根据调用时传入的参数自动选择最合适的函数版本。1.2 默认参数和重载分别解决什么问题默认参数解决的是函数调用时某些参数可以不传。比如一个函数有三个参数但是后两个参数有默认值那么调用时只传一个参数也可以。函数重载解决的是同一个函数名可以处理不同类型或者不同数量的参数。比如同样是TestOverload可以处理int也可以处理float还可以处理两个int。两者虽然都能让函数调用更加灵活但是解决的问题并不一样。二、函数默认参数的基本使用2.1 什么是函数默认参数函数默认参数就是在函数参数列表中给参数设置一个默认值。代码示例#include iostream using namespace std; void TestFuncDec(int x, int y 200, int z 300) { cout TestFuncDec x , y , z endl; } int main() { TestFuncDec(100); TestFuncDec(110, 210); TestFuncDec(120, 220, 320); return 0; }运行结果2.2 默认参数是如何补充的1只传一个参数TestFuncDec(100);对应关系是x 100 y 200 z 300因为y和z没有传入所以使用默认值。它等价于TestFuncDec(100, 200, 300);2传两个参数TestFuncDec(110, 210);对应关系是x 110 y 210 z 300因为z没有传入所以z使用默认值。它等价于TestFuncDec(110, 210, 300);3传三个参数TestFuncDec(120, 220, 320);对应关系是x 120 y 220 z 320三个参数全部传入所以不会使用默认值。2.3 默认参数必须从右往左设置默认参数有一个非常重要的规则默认参数必须从右往左连续设置。正确写法void TestFuncDec(int x, int y 200, int z 300);这表示x 必须传 y 可以不传 z 可以不传错误写法void TestFuncDec(int x 100, int y, int z 300);这种写法是错误的。原因是C 函数传参是按照从左到右的位置匹配的不能跳过中间参数。比如调用TestFuncDec(10);如果x有默认值y没有默认值z有默认值那么编译器就会不知道10 到底应该给 x 还是应该给 y所以 C 规定默认参数必须从右往左连续设置。2.4 默认参数的本质默认参数并不是运行时才决定的而是在编译阶段由编译器完成补充。例如TestFuncDec(100);编译器看到函数声明void TestFuncDec(int x, int y 200, int z 300);于是会把它理解成TestFuncDec(100, 200, 300);所以默认参数的本质可以理解为调用时少传的参数由编译器根据默认值自动补上。三、函数声明、函数定义与默认参数3.1 函数声明和函数定义的区别在 C 中函数一般分为声明和定义。1函数声明函数声明只是告诉编译器这个函数存在。这个函数叫什么名字。这个函数返回值是什么。这个函数需要哪些参数。例如void Test(int x 999);这就是函数声明。2函数定义函数定义是真正写出函数内部要执行的代码。例如void Test(int x) { cout x endl; }简单理解声明告诉编译器有这个函数。 定义真正实现这个函数。3.2 如果有声明默认参数只能写在声明中正确写法#include iostream using namespace std; // 函数声明默认参数写在声明中 void Test(int x 999); // 函数定义定义中不要再写默认参数 void Test(int x) { cout x endl; } int main() { Test(); Test(100); return 0; }运行结果999 100这里void Test(int x 999);表示如果调用Test()时不传参数默认使用999。而函数定义中void Test(int x) { cout x endl; }就不要再写默认参数了。3.3 为什么不能声明和定义都写默认参数下面这种写法是错误的void Test(int x 999); void Test(int x 999) { cout x endl; }虽然两次默认值都是999但是编译器仍然会认为你重复指定了默认参数。原因是默认参数只能在同一个作用域中指定一次。也就是说默认参数不是函数实现的一部分而是函数调用规则的一部分。当编译器看到函数调用Test();它需要根据前面看到的函数声明来决定应该补什么默认值。3.4 实际工程如何处理所以在实际工程中一般这样写1.h文件头文件.h中写声明和默认参数void Test(int x 999);2.cpp文件源文件.cpp中写函数定义不再写默认参数void Test(int x) { cout x endl; }这样既清晰又不会重复定义默认参数。3.5 没有单独声明时默认参数可以写在定义中如果函数没有提前声明函数定义本身也可以写默认参数。例如void TestFuncDec(int x, int y 200, int z 300) { cout x , y , z endl; }因为这个函数定义本身也能起到声明作用所以可以直接写默认参数。但是在实际项目中如果函数声明和定义分开默认参数一般写在声明里。四、函数重载的基本使用与匹配规则4.1 什么是函数重载函数重载指的是函数名相同但是参数列表不同代码示例#include iostream using namespace std; void TestOverload() { cout __FUNCSIG__ endl; } void TestOverload(int x) { cout __FUNCSIG__ x endl; } void TestOverload(float x) { cout __FUNCSIG__ x endl; } void TestOverload(int x, int y) { cout __FUNCSIG__ endl; } int main() { TestOverload(); TestOverload(100); TestOverload(1.3f); TestOverload(1, 2); return 0; }这几个函数名字都叫TestOverload但是它们的参数列表不同所以可以同时存在。4.2 编译器如何区分重载函数编译器会根据调用时传入的实参自动选择最匹配的函数。1调用无参版本TestOverload();匹配void TestOverload();2调用 int 版本TestOverload(100);100是int类型所以匹配void TestOverload(int x);3调用 float 版本TestOverload(1.3f);1.3f是float类型所以匹配void TestOverload(float x);4调用两个参数版本TestOverload(1, 2);这里传入了两个int参数所以匹配void TestOverload(int x, int y);4.3 返回值不能作为函数重载依据下面这种写法是错误的int TestOverload(int x) { return x; } double TestOverload(int x) { return x; }虽然两个函数返回值不同int double但是它们的参数列表完全相同TestOverload(int) TestOverload(int)编译器无法区分它们。比如调用TestOverload(10);编译器不知道你想调用返回int的版本还是返回double的版本。所以一定要记住函数重载只看参数列表不看返回值。4.4 double、float、int 的重载匹配问题这里是一个非常容易出错的地方。先看代码void TestOverload(int x) { cout __FUNCSIG__ x endl; } void TestOverload(float x) { cout __FUNCSIG__ x endl; }如果调用TestOverload(1.3f);没有问题。因为1.3f 是 float 类型所以会匹配void TestOverload(float x);但是如果调用TestOverload(1.3);就可能报错。原因是1.3 默认是 double 类型。当前有两个重载版本void TestOverload(int x); void TestOverload(float x);但是没有void TestOverload(double x);这时编译器发现double 可以转换成 int double 也可以转换成 float但是这两个转换都不是完全匹配编译器不知道应该选择哪个所以会报错。这个错误本质上就是调用重载函数不明确。4.5 如何解决 double 调用重载函数的问题解决方式有三种。1写成 float 字面量TestOverload(1.3f);这里1.3f本身就是float类型所以会直接匹配float版本。2使用 C 语言风格强制类型转换TestOverload((float)1.3);这里把1.3从double强制转换成float然后调用float版本。3使用 C 风格强制类型转换TestOverload(static_castfloat(1.3));这种写法更符合 C 风格。4.6 仅保留一个重载函数情况如果只保留其中一个重载函数例如只保留void TestOverload(float x);那么调用TestOverload(1.3);编译器就可以自动把double转换成float。如果只保留void TestOverload(int x);那么调用TestOverload(1.3);编译器也可以自动把double转换成int但是小数部分会丢失1.3会变成1。所以需要注意当多个重载版本都可以通过类型转换匹配时编译器可能不知道选哪个。4.7FUNCSIG是什么在 Visual Studio 中经常可以看到这样的代码cout __FUNCSIG__ endl;__FUNCSIG__是 Visual Studio 提供的一个宏可以打印当前函数的完整函数签名。例如可能输出void __cdecl TestOverload(void) void __cdecl TestOverload(int) void __cdecl TestOverload(float) void __cdecl TestOverload(int,int)它的作用是方便我们观察当前到底调用了哪个重载函数。注意__FUNCSIG__ 主要在 Visual Studio / MSVC 中使用。如果是 GCC 或 Clang常用的是__PRETTY_FUNCTION__五、NULL、nullptr 与完整代码总结5.1 NULL 和 nullptr 的区别在 C 语言中我们经常使用NULL表示空指针。但是在 C 中尤其是遇到函数重载时NULL容易引起问题。看下面代码void TestNull(int) { cout __FUNCSIG__ endl; } void TestNull(int*) { cout __FUNCSIG__ endl; }这里有两个重载函数TestNull(int) TestNull(int*)一个参数是普通整数int另一个参数是指针int*。5.2 TestNull(123)、TestNull(ptr)、TestNull(NULL)、TestNull(nullptr)1调用 int 版本TestNull(123);123是int类型所以调用void TestNull(int)2调用 int* 版本int* ptr{ 0 }; TestNull(ptr);这里ptr是一个int*指针变量。虽然它保存的值是0表示空地址但是它的类型是int*所以调用void TestNull(int*)3使用 NULLTestNull(NULL);在很多 C 编译器中NULL本质上可能就是0所以它容易被当成普通整数处理从而调用void TestNull(int)这就是 C 语言习惯在 C 函数重载中容易带来的问题。4使用 nullptrTestNull(nullptr);nullptr是 C11 引入的空指针关键字。它不是普通整数0而是专门用来表示空指针。所以它会更加准确地匹配指针版本void TestNull(int*)因此在 C 中建议少用 NULL多用 nullptr。5.3 完整代码示例#include iostream using namespace std; // 一、函数默认参数 void TestFuncDec(int x, int y 200, int z 300) { cout TestFuncDec x , y , z endl; } // 二、函数声明和定义 // 如果有函数声明默认参数一般写在声明中 void Test(int x 999); // 函数定义中不要重复写默认参数 void Test(int x) { cout Test x x endl; } // 三、函数重载 void TestOverload() { cout __FUNCSIG__ endl; } void TestOverload(int x) { cout __FUNCSIG__ x endl; } void TestOverload(float x) { cout __FUNCSIG__ x endl; } void TestOverload(int x, int y) { cout __FUNCSIG__ endl; 声明默认参数一般写在声明中 void Test(int x 999); // 函数定义中不要重复写默认参数 void Test(int x) { cout Test x x endl; } // 三、函数重载 void TestOverload() { cout __FUNCSIG__ endl; } void TestOverload(int x) { cout __FUNCSIG__ x endl; } void TestOverload(float x) { cout __FUNCSIG} // 四、NULL 和 nullptr void TestNull(int) { cout __FUNCSIG__ endl; } void TestNull(int*) { cout __FUNCSIG__ endl; } int main() { // 1. 默认参数 TestFuncDec(100); TestFuncDec(110, 210); TestFuncDec(120, 220, 320); // 2. 声明中的默认参数 Test(); Test(100); // 3. 函数重载 TestOverload(); TestOverload(100); TestOverload(1.3f); TestOverload(1, 2); // 如果同时存在 int 和 float 版本下面可能报错 // 因为 1.3 默认是 double // TestOverload(1.3); TestOverload((float)1.3); TestOverload(static_castfloat(1.3)); // 4. NULL 和 nullptr TestNull(123); int* ptr{ 0 }; TestNull(ptr); TestNull(NULL); TestNull(nullptr); return 0; }六、总结本节主要学习了 C 中的函数默认参数和函数重载。6.1 默认参数默认参数可以让函数调用时少传一些参数。例如void Test(int a, int b 10, int c 20);调用时可以写Test(1); Test(1, 2); Test(1, 2, 3);但是默认参数必须从右往左连续设置。6.2 声明和定义中的默认参数如果函数既有声明又有定义那么默认参数一般写在声明中void Test(int x 999);定义中不要重复写默认参数void Test(int x) { }原因是默认参数只能指定一次。6.3 函数重载函数重载要求函数名相同参数列表不同。参数列表不同可以是参数数量不同 参数类型不同 参数顺序不同但是返回值不同不能构成函数重载。6.4 double 调用 int 和 float 重载容易报错1.3 默认是 double 1.3f 默认是 float如果同时存在void TestOverload(int); void TestOverload(float);那么调用TestOverload(1.3);可能会报错因为编译器不知道应该把double转换成int还是转换成float。可以改成TestOverload(1.3f);或者TestOverload(static_castfloat(1.3));6.5 C 中建议使用 nullptr在 C 中不推荐继续使用NULL表示空指针。因为NULL 本质上可能是 0 nullptr 才是真正的空指针关键字所以建议写int* p nullptr;而不是int* p NULL;默认参数解决“参数可以少传”的问题函数重载解决“同名函数处理不同参数”的问题nullptr 解决“空指针和整数 0 混淆”的问题。