目录
[1.1 函数默认参数和函数重载是什么?](#1.1 函数默认参数和函数重载是什么?)
[1.2 默认参数和重载分别解决什么问题?](#1.2 默认参数和重载分别解决什么问题?)
[2.1 什么是函数默认参数?](#2.1 什么是函数默认参数?)
[2.2 默认参数是如何补充的?](#2.2 默认参数是如何补充的?)
[2.3 默认参数必须从右往左设置](#2.3 默认参数必须从右往左设置)
[2.4 默认参数的本质](#2.4 默认参数的本质)
[3.1 函数声明和函数定义的区别](#3.1 函数声明和函数定义的区别)
[3.2 如果有声明,默认参数只能写在声明中](#3.2 如果有声明,默认参数只能写在声明中)
[3.3 为什么不能声明和定义都写默认参数?](#3.3 为什么不能声明和定义都写默认参数?)
[3.4 实际工程如何处理?](#3.4 实际工程如何处理?)
[3.5 没有单独声明时,默认参数可以写在定义中](#3.5 没有单独声明时,默认参数可以写在定义中)
[4.1 什么是函数重载?](#4.1 什么是函数重载?)
[4.2 编译器如何区分重载函数?](#4.2 编译器如何区分重载函数?)
[(2)调用 int 版本](#(2)调用 int 版本)
[(3)调用 float 版本](#(3)调用 float 版本)
[4.3 返回值不能作为函数重载依据](#4.3 返回值不能作为函数重载依据)
[4.4 double、float、int 的重载匹配问题](#4.4 double、float、int 的重载匹配问题)
[4.5 如何解决 double 调用重载函数的问题?](#4.5 如何解决 double 调用重载函数的问题?)
[(1)写成 float 字面量](#(1)写成 float 字面量)
[(2)使用 C 语言风格强制类型转换](#(2)使用 C 语言风格强制类型转换)
[(3)使用 C++ 风格强制类型转换](#(3)使用 C++ 风格强制类型转换)
[4.6 仅保留一个重载函数情况](#4.6 仅保留一个重载函数情况)
[4.7 FUNCSIG 是什么?](#4.7 FUNCSIG 是什么?)
[五、NULL、nullptr 与完整代码总结](#五、NULL、nullptr 与完整代码总结)
[5.1 NULL 和 nullptr 的区别](#5.1 NULL 和 nullptr 的区别)
[5.2 TestNull(123)、TestNull(ptr)、TestNull(NULL)、TestNull(nullptr)](#5.2 TestNull(123)、TestNull(ptr)、TestNull(NULL)、TestNull(nullptr))
[(1)调用 int 版本](#(1)调用 int 版本)
[(2)调用 int* 版本](#(2)调用 int* 版本)
[(3)使用 NULL](#(3)使用 NULL)
[(4)使用 nullptr](#(4)使用 nullptr)
[5.3 完整代码示例](#5.3 完整代码示例)
[6.1 默认参数](#6.1 默认参数)
[6.2 声明和定义中的默认参数](#6.2 声明和定义中的默认参数)
[6.3 函数重载](#6.3 函数重载)
[6.4 double 调用 int 和 float 重载容易报错](#6.4 double 调用 int 和 float 重载容易报错)
[6.5 C++ 中建议使用 nullptr](#6.5 C++ 中建议使用 nullptr)
前言

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