前言:
前文中,我们系统学习了 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既然看到这里了,不妨关注+点赞+收藏,感谢大家,若有问题请指正。