前言:
前文中,我们系统学习了 namespace 机制(有效地解决了命名冲突问题,包含指定访问、部分展开和全部展开三种使用方式),
同时了解了 cin/cout 输入输出流(具备自动类型识别和支持自定义类型两大优势)
以及编写了第一个C++程序,本文将为大家解析C语言与C++在函数方面的主要区别。

一、缺省参数
这是一个非常实用且常用的特性,允许你在定义函数时为参数指定一个“默认值”。如果调用函数时没有传递该参数,编译器就会自动使用这个默认值。
1.1 什么是缺省参数
简单来说,就是备胎, 当你在调用函数时:
-
如果你给了实参,就用你给的。
-
如果你没给,就用函数定义里预先写好的默认值。
基本语法示例:
#include <iostream>
using namespace std;
// 这里 b 被指定为缺省参数,默认值为 10
void printAdd(int a, int b = 10)
{
cout << "a: " << a << ", b: " << b << ", sum: " << a + b << endl;
}
int main()
{
// 情况 1:只传一个参数
// a 变成了 5,b 自动使用默认值 10
printAdd(5); // 输出: a: 5, b: 10, sum: 15
// 情况 2:传两个参数
// a 变成了 5,b 使用传入的 20(覆盖了默认值)
printAdd(5, 20); // 输出: a: 5, b: 20, sum: 25
return 0;
}
1.2 核心规则(非常重要)
1.2.1 规则一:必须“从右往左”依次给出
如果一个参数有了默认值,那么它右边的所有参数都必须也有默认值,你不能跳着给。
错误写法:
void func(int a = 10, int b, int c);
原因:a 有默认值,但右边的 b 和 c 没有,编译器不知道你传的参数是给谁的
错误写法:
void func(int a, int b = 10, int c);
原因:b 有默认值,但右边的 c 没有
正确写法:
void func(int a, int b = 10, int c = 20);
从 b 开始往右,全都有默认值
1.2.2 规则二:声明和定义不能同时给
通常我们会把函数的声明(在 .h 文件中)和定义(在 .cpp 文件中)分开。 但是缺省参数只能出现一次,通常建议写在声明中。
// --- header.h ---
// 建议:在声明中指定默认值
void Func(int a = 10);
// --- main.cpp ---
// 错误:定义中再次指定(即使值一样也不行)
// void Func(int a = 10) { ... }
// 正确:定义中不要写默认值
void Func(int a)
{
cout << a << endl;
}
1.3缺省参数的分类
1.3.1全缺省参数
//全缺省
void Func1(int a = 10, int b = 20, int c = 30)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
//调用
int main()
{
//全缺省的四种调用方式
Func1(); //输出a=10 b=20 c=30
Func1(1); //输出a=1 b=20 c=30
Func1(1, 2); //输出a=1 b=2 c=30
Func1(1, 2, 3); //输出a=1 b=2 c=3
return 0;
}
1.3.2半缺省参数
//半缺省
//缺省参数从右往左,且不能间隔,跳跃传缺省参数
void Func2(int a, int b = 10, int c = 20)
{
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl << endl;
}
int main()
{
//缺省调用,没有缺省值的必须传参
Func2(100); //输出a=100 b=10 c=20
Func2(100, 200); //输出a=100 b=200 c=200
Func2(100, 200, 300); //输出a=100 b=200 c=300
return 0;
}
1.4 常见的“坑”
缺省参数单独使用很棒,但和一个无参函数混合在一起使用,容易造成二义性(Ambiguity),导致编译报错。
#include <iostream>
using namespace std;
// 函数 1:没有参数
void target()
{
cout << "无参版本" << endl;
}
// 函数 2:带缺省参数
void target(int a = 10)
{
cout << "带缺省参数版本: " << a << endl;
}
int main()
{
// target(5); // 没问题,只能调用函数 2
// 报错!二义性错误
// 编译器困惑:是调用函数 1?还是调用函数 2 并使用了默认值?
target();
return 0;
}
二、函数重载
函数重载是 C++ 中一个非常实用且基础的特性。简单来说,它允许你在同一个作用域内,给不同的函数起相同的名字,只要它们的参数列表不同即可。
2.1 什么是函数重载
让我们看一个 print 函数的重载例子:
#include <iostream>
using namespace std;
// 1. 基础函数
void print(int i)
{
cout << "正在打印整数: " << i << endl;
}
// 2. 参数类型不同
void print(double d)
{
cout << "正在打印浮点数: " << d << endl;
}
// 3. 参数个数不同
void print(int i, int j)
{
cout << "正在打印两个整数: " << i << ", " << j << endl;
}
// 4. 参数顺序不同 (注意类型要不同才有意义)
void print(int i, double d)
{
cout << "先整数后浮点: " << i << ", " << d << endl;
}
void print(double d, int i)
{
cout << "先浮点后整数: " << d << ", " << i << endl;
}
int main()
{
print(10); // 调用第 1 个
print(3.14); // 调用第 2 个
print(10, 20); // 调用第 3 个
print(10, 3.14); // 调用第 4 个
return 0;
}
2.2 构成重载的三个条件
编译器区分同名函数的唯一依据是参数列表(也叫函数签名),只要满足以下任意一个条件,就构成重载:
①参数个数不同
②参数类型不同
③参数顺序不同 (指不同类型的参数顺序互换)
例如:同一个 print 函数可以根据参数列表不同,实现不同的函数重载。
1. 基础函数
void print(int i)
2.同一个函数,参数类型不同:
void print(double d)
3.同一个函数,参数个数不同:
void print(int i, int j)
4.同一个函数,参数的类型顺序不同:
void print(int i, double d)
2.3 常见的“陷阱”
2.3.1 情况A:返回值类型不同不构成重载
// 错误示例!编译器会报错
int func(int a)
{
return a;
}
void func(int a) {}
因为在调用函数时,我们经常忽略返回值(例如直接写
func(10);)。
如果两个函数只有返回值不同,编译器看到
func(10)时,根本无法判断你想调用哪一个(它不知道你是否需要那个返回值)。
2.3.2 情况 B:类型转换导致的歧义
有时候你写的重载虽然语法正确,但调用时会让编译器“左右为难”,导致编译错误。
void test(long a) { ... }
void test(double a) { ... }
int main()
{
test(10); // 报错!
}
原因: 由于
10是int类型,所以它既可以转成long,也可以转成double,编译器觉得两者优先级一样,不知道选谁。
2.3.3 情况 C:默认参数导致的歧义
void func(int a) {
cout << "A";
}
// 第二个参数有默认值
void func(int a, int b = 10)
{
cout << "B";
}
int main()
{
func(5); // 报错!
}
原因: 当你调用
func(5)时,既符合第一个函数,也符合第二个函数(因为第二个参数可以省略)。编译器无法区分,直接报错。
三、内联函数
在 C++ 中,内联函数 (Inline Function) 是一个旨在提高程序运行效率的特性。
简单来说,它的作用是向编译器发出一个建议:在调用该函数的地方,不要进行普通的“函数跳转”和“上下文切换”,而是直接将函数的代码“复制粘贴”到调用点。
3.1 核心概念与工作原理
1. 普通函数调用:当程序调用一个普通函数时,CPU 需要保存当前状态(压栈),跳转到函数所在的内存地址执行代码,执行完毕后再跳回来(出栈)。这个过程称为函数调用开销,即该过程为开辟函数栈帧
2. 内联函数:编译器直接将函数体内的代码插入到调用该函数的地方。这样就消除了调用和返回的开销,但会增加最终可执行文件的大小(代码膨胀),这个过程省略了开辟函数栈帧提高了程序运行时的效率。
3.2 语法与使用
使用关键字 inline 来声明内联函数。通常,内联函数的定义(具体实现)必须在头文件中,或者在调用之前可见。
#include <iostream>
using namespace std;
// 使用 inline 关键字
// 只是定义:告诉编译器 add 长什么样,并且建议内联
inline int add(int a, int b)
{
return a + b;
}
int main()
{
int x = 10, y = 20;
// 内联函数在这里展开
// 编译器会将下面这行代码实际上替换为: int result = a + b;
int result = add(x, y);
cout << result << endl;
return 0;
}
3.3 编译器的“拒绝权”
重要的一点是:inline 只是对编译器的一个建议,而不是命令,编译器可以有权忽略这个建议。
以下情况下,编译器通常会拒绝内联,而将其作为普通函数处理:
①函数体过大: 包含复杂的逻辑。
②包含循环语句 (
for,while): 循环执行的时间通常远大于函数调用的开销,内联意义不大。
③包含递归: 递归函数无法无限展开。
④包含 switch 或 goto 语句。
3.4 内联函数与宏定义
C语⾔实现宏函数也会在预处理时替换展开,但是宏函数实现很复杂很容易出错的,且不⽅便调 试,C++设计了inline⽬的就是替代C的宏函数。
例如:实现宏函数ADD 完成两数相加
#define ADD(a,b) ( (a)+(b) )
由该宏函数可能会引出如下问题:
①为什么不能加分号?
进行条件判断和输出时会出现问题,如:if( ADD(2,3) > 0)
②为什么要加外⾯的括号?
运算时的优先级,如:ADD(2,5) * 3 ---> (2) + (5) * 3
③为什么要加⾥⾯的括号?
运算时的优先级,如:ADD(x & y, x | y); ---> ( x & y + x | y )
内联函数实现Add,完成两数相加
nline int Add(int a, int b)
{
return a + b;
}
为什么通过内联函数可以很好的替代宏函数?
因为内联函数真正的函数。有类型安全检查,参数处理正确,且由编译器处理而非预处理器。
3.5 内联函数的注意事项
inline绝对不能声明和定义分离到两个⽂件,分离会导致链接错误。因为inline被展开,就没有函数地址,链接时会出现报错。
详细解释:定义了如下三个文件
文件一:test.h(声明func函数)
//test.h (声明func函数):
inline void func();
文件二: test.cpp(定义func函数)
//test.cpp (定义func函数):
#include "test.h"
inline void func()
{
//...
}
文件三:main.cpp (调用func函数)
//#include "test.h"
int main()
{
func();
return 0;
}
关键分析:当编译器编译 main.cpp 时,发生了什么?
①预处理器把
test.h的内容复制进来了。
②编译器看到了
func()的声明,知道它是一个inline函数。
③关键点:编译器此时看不到
test.cpp里的内容!它找不到func的大括号{ ... }里的代码。
结果:编译器无法进行内联展开(因为没代码可抄),它只能退而求其次,生成一条普通的汇编指令
call func,寄希望于链接器(Linker)稍后能找到这个函数的定义。
核心问题分析:为什么 “找不到” func的函数定义?(链接期问题)
原因①:当编译器去编译 test.cpp文件时,编译器看到了 inline void func() { ... },对于 inline 函数,编译器通常认为:“这个函数是为了给别人内联展开用的,不需要生成一个可以被外部链接调用的‘全局函数符号’(或者说符号是弱符号/仅本地可见)”。
原因②:当你编译 test.cpp 时,编译器是“与世隔绝”的,它不知道 main.cpp 的存在,也不知道 main.cpp 可能会在那边哭着喊着要调用 func()。当它在 test.cpp 里看到一个 inline 函数定义,却发现 test.cpp 自己根本没用它时,为了节省空间,直接把它优化掉,不生成汇编代码。
最终崩溃(链接错误)
当三个文件都编译完成时,链接器将三个文件进行链接:
①它拿着 main.obj,看到里面有一个“欠条”:call func(未解析的外部符号)。
②它去 test.obj 里找 func 的定义。
③找不到! 因为在编译 test.cpp 时,func 作为内联函数并没有生成标准的全局符号。
④报错:LNK2019: unresolved external symbol (未解析的外部符号)。
正确的做法:因为编译器需要看到代码才能进行“复制粘贴”,所以最好将内联函数的定义写在头文件 (.h) 里。
#ifndef TEST_H
#define TEST_H
// 声明和定义都在头文件里!
inline void func()
{
// 具体的实现代码...
}
#endif
既然看到这里了,不妨关注+点赞+收藏,感谢大家,若有问题请指正。


411





