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

目录

前言

一、本节学习内容概要

[1.1 函数默认参数和函数重载是什么?](#1.1 函数默认参数和函数重载是什么?)

(1)函数默认参数

(2)函数重载

[1.2 默认参数和重载分别解决什么问题?](#1.2 默认参数和重载分别解决什么问题?)

二、函数默认参数的基本使用

[2.1 什么是函数默认参数?](#2.1 什么是函数默认参数?)

[2.2 默认参数是如何补充的?](#2.2 默认参数是如何补充的?)

(1)只传一个参数

(2)传两个参数

(3)传三个参数

[2.3 默认参数必须从右往左设置](#2.3 默认参数必须从右往左设置)

[2.4 默认参数的本质](#2.4 默认参数的本质)

三、函数声明、函数定义与默认参数

[3.1 函数声明和函数定义的区别](#3.1 函数声明和函数定义的区别)

(1)函数声明

(2)函数定义

[3.2 如果有声明,默认参数只能写在声明中](#3.2 如果有声明,默认参数只能写在声明中)

[3.3 为什么不能声明和定义都写默认参数?](#3.3 为什么不能声明和定义都写默认参数?)

[3.4 实际工程如何处理?](#3.4 实际工程如何处理?)

(1).h文件

(2).cpp文件

[3.5 没有单独声明时,默认参数可以写在定义中](#3.5 没有单独声明时,默认参数可以写在定义中)

四、函数重载的基本使用与匹配规则

[4.1 什么是函数重载?](#4.1 什么是函数重载?)

[4.2 编译器如何区分重载函数?](#4.2 编译器如何区分重载函数?)

(1)调用无参版本

[(2)调用 int 版本](#(2)调用 int 版本)

[(3)调用 float 版本](#(3)调用 float 版本)

(4)调用两个参数版本

[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++ 中允许多个函数使用同一个函数名,只要它们的参数类型、参数数量或者参数顺序不同,编译器就可以根据调用时传入的实参自动选择对应的函数版本。

如果还没有看过前面的内容,可以先参考这篇文章:

一篇搞懂 C++ 重载:函数重载 + 运算符重载,从入门到会用(含 ++、<<、== 实战)_一文懂 操作符重载-CSDN博客https://blog.csdn.net/m0_58954356/article/details/155323257?ops_request_misc=elastic_search_misc&request_id=f963d69ab4f7c20832b8551ad444c785&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~ElasticSearch~search_v2-1-155323257-null-null.nonecase&utm_term=%E9%87%8D%E8%BD%BD&spm=1018.2226.3001.4450本节继续围绕函数展开,重点讲解两个非常常用、也非常容易混淆的知识点:

  • 函数默认参数
  • 函数重载

函数默认参数解决的是:调用函数时,某些参数不传怎么办?

函数重载解决的是:同一个函数名,为什么可以对应多个不同函数?

除此之外,本节还会结合实际代码讲解几个容易出错的地方,例如:

  1. 默认参数为什么必须从右往左设置?
  2. 函数声明和函数定义中,默认参数应该写在哪里?
  3. 函数重载到底根据什么区分?
  4. 为什么返回值不同不能构成函数重载?
  5. double、float、int 在重载调用中为什么可能报错?
  6. 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)函数重载

函数重载****指的是:多个函数可以使用同一个函数名,只要参数列表不同即可。

名字相同,参数列表不同;返回值不能单独区分重载。

满足重载的几种情况:

  1. 参数类型不同

  2. 参数个数不同

  3. 参数顺序不同

例如:

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

因为 yz 没有传入,所以使用默认值。

它等价于:

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)函数声明

函数声明只是告诉编译器:

  1. 这个函数存在。
  2. 这个函数叫什么名字。
  3. 这个函数返回值是什么。
  4. 这个函数需要哪些参数。

例如:

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);

100int 类型,所以匹配:

cpp 复制代码
void TestOverload(int x);

(3)调用 float 版本

cpp 复制代码
TestOverload(1.3f);

1.3ffloat 类型,所以匹配:

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.3double 强制转换成 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);

123int 类型,所以调用:

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;

  1. 默认参数解决"参数可以少传"的问题;
  2. 函数重载解决"同名函数处理不同参数"的问题;
  3. nullptr 解决"空指针和整数 0 混淆"的问题。
相关推荐
不负岁月无痕1 小时前
STL-- C++ list类 模拟实现
开发语言·c++·list
江屿风1 小时前
C++OJ题经验总结(竞赛)3
开发语言·c++·笔记·算法
NiceCloud喜云1 小时前
Anthropic 发布 Project Glasswing:未公开模型 Mythos 已挖出 10000+ 漏洞,含 OpenBSD 27 年老 bug
android·java·数据库·c++·python·docker·bug
香蕉鼠片1 小时前
八股C++(二)
开发语言·c++
格发许可优化管理系统2 小时前
解决Mentor许可冲突,让您的业务无缝运行
运维·服务器·c语言·c++·人工智能
思麟呀2 小时前
在C++基础上理解CSharp-4
开发语言·jvm·c++·c#
Brilliantwxx2 小时前
【算法题】 面试级别的二叉树题目OJ复习(上)
数据结构·c++·笔记·算法·面试
颖火虫盟主2 小时前
Conan C++ 包管理工具深度解析
java·jvm·c++
神仙别闹2 小时前
基于C++ OpenGL 绘制太阳系
开发语言·c++