前文中,我们系统学习了 namespace 机制(有效地解决了命名冲突问题,包含指定访问、部分展开和全部展开三种使用方式),
同时了解了 cin/cout 输入输出流(具备自动类型识别和支持自定义类型两大优势)
以及编写了第一个C++程序,本文将为大家解析C语言与C++在函数方面的主要区别。
这是一个非常实用且常用的特性,允许你在定义函数时为参数指定一个“默认值”。如果调用函数时没有传递该参数,编译器就会自动使用这个默认值。
简单来说,就是备胎, 当你在调用函数时:
基本语法示例:
#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;
}如果一个参数有了默认值,那么它右边的所有参数都必须也有默认值,你不能跳着给。
错误写法:
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 开始往右,全都有默认值
通常我们会把函数的声明(在 .h 文件中)和定义(在 .cpp 文件中)分开。 但是缺省参数只能出现一次,通常建议写在声明中。
// --- header.h ---
// 建议:在声明中指定默认值
void Func(int a = 10);
// --- main.cpp ---
// 错误:定义中再次指定(即使值一样也不行)
// void Func(int a = 10) { ... }
// 正确:定义中不要写默认值
void Func(int a)
{
cout << a << endl;
}//全缺省
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;
}//半缺省
//缺省参数从右往左,且不能间隔,跳跃传缺省参数
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;
}缺省参数单独使用很棒,但和一个无参函数混合在一起使用,容易造成二义性(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++ 中一个非常实用且基础的特性。简单来说,它允许你在同一个作用域内,给不同的函数起相同的名字,只要它们的参数列表不同即可。
让我们看一个 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;
}编译器区分同名函数的唯一依据是参数列表(也叫函数签名),只要满足以下任意一个条件,就构成重载:
①参数个数不同
②参数类型不同
③参数顺序不同 (指不同类型的参数顺序互换)
例如:同一个 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)// 错误示例!编译器会报错
int func(int a)
{
return a;
}
void func(int a) {}因为在调用函数时,我们经常忽略返回值(例如直接写
func(10);)。 如果两个函数只有返回值不同,编译器看到func(10)时,根本无法判断你想调用哪一个(它不知道你是否需要那个返回值)。
有时候你写的重载虽然语法正确,但调用时会让编译器“左右为难”,导致编译错误。
void test(long a) { ... }
void test(double a) { ... }
int main()
{
test(10); // 报错!
}原因: 由于
10是int类型,所以它既可以转成long,也可以转成double,编译器觉得两者优先级一样,不知道选谁。
void func(int a) {
cout << "A";
}
// 第二个参数有默认值
void func(int a, int b = 10)
{
cout << "B";
}
int main()
{
func(5); // 报错!
}原因: 当你调用
func(5)时,既符合第一个函数,也符合第二个函数(因为第二个参数可以省略)。编译器无法区分,直接报错。
在 C++ 中,内联函数 (Inline Function) 是一个旨在提高程序运行效率的特性。
简单来说,它的作用是向编译器发出一个建议:在调用该函数的地方,不要进行普通的“函数跳转”和“上下文切换”,而是直接将函数的代码“复制粘贴”到调用点。
1. 普通函数调用:当程序调用一个普通函数时,CPU 需要保存当前状态(压栈),跳转到函数所在的内存地址执行代码,执行完毕后再跳回来(出栈)。这个过程称为函数调用开销,即该过程为开辟函数栈帧
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;
} 重要的一点是:inline 只是对编译器的一个建议,而不是命令,编译器可以有权忽略这个建议。
以下情况下,编译器通常会拒绝内联,而将其作为普通函数处理:
①函数体过大: 包含复杂的逻辑。 ②包含循环语句 (
for,while): 循环执行的时间通常远大于函数调用的开销,内联意义不大。 ③包含递归: 递归函数无法无限展开。 ④包含 switch 或 goto 语句。
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;
}为什么通过内联函数可以很好的替代宏函数?
因为内联函数真正的函数。有类型安全检查,参数处理正确,且由编译器处理而非预处理器。
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既然看到这里了,不妨关注+点赞+收藏,感谢大家,若有问题请指正。