一、函数的基础
1.什么是函数?(独立的功能单位)
函数是C++中封装代码逻辑的基本单元,用于执行特定任务。
作用:代码复用、模块化、提高可读性。
2、函数的基本结构
返回类型 函数名(参数列表) {// 函数体return 返回值; // 若返回类型非void
}
示例1:最简单的函数
#include<iostream>
using namespace std;
int func(int n)
{
/*求解阶乘
输入:一个正整数n
输出:n的阶乘*/int result = 1;while(n > 1){result *= n--;}return result;
}int main() {int r=func(5);//调用cout<<r<<endl;return 0;
}
调用过程 如下:
函数的调用完成两项工作:一是用实参初始化函数对应的形参,二是将控制权转移给被调用函数。此时,主调函数(calling function)的执行被暂时中断,被调函数(called function)开始执行。
当遇到一条 return 语句时函数结束执行过程。和函数调用一样,return 语句也完成两项工作:一是返回 return 语句中的值(如果有的话),二是将控制权从被调函数转移回主调函数。函数的返回值用于初始化调用表达式的结果,之后继续完成调用所在的表达式的剩余部分。
func等效如下:
int n = 5; // 用字面值 5 初始化 n
int result = 1; //func 函数体内的代码
while (n> 1)
result *= n--;
int r = result; // 用 result的副本初始化 j
3.局部对象
什么是局部对象?
局部对象是指在函数内部或代码块(如{}
包围的语句块)内定义的对象。
特点:
- 作用域(Scope):仅在定义它的函数或代码块内可见。
- 生命周期(Lifetime):从定义处开始,到离开作用域时自动销毁。
局部对象的生命周期
1. 普通局部对象(自动对象)
- 创建:执行到定义语句时创建。
- 销毁:离开作用域时,按定义顺序的逆序自动销毁。
- 存储位置:通常存储在栈(Stack)内存中,由编译器自动管理。
#include<iostream>
using namespace std;
void func(int n)
{int a = n; // 局部对象a,作用域在func内{ // 进入代码块double b = 3.14; // 局部对象b,作用域仅在此代码块内cout << a << " " << b << endl; // 输出a和b}// 离开代码块,b被销毁// a仍有效 cout << a << endl;
}// 离开函数,a被销毁int main() {func(10);return 0;
}
生命周期可视化如下:
2. 局部静态对象
- 用
static
关键字修饰的局部对象。 - 生命周期:从首次执行定义语句开始,到程序结束。
- 初始化:仅第一次执行时初始化一次。
如果有必要令局部变量的生命周期贯穿函数调用及之后的时间,可以将局部变量定义成 static 类型从而获得这样的对象。局部静态对象(local static object)在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。
void counter() {static int count = 0; // 静态局部对象,生命周期为整个程序count++;cout << count << endl;
}int main() {counter(); // 输出1counter(); // 输出2return 0;
}
3、局部对象的作用域限制
局部对象仅在定义的作用域内可见,外部无法访问。
void func1() {int x = 5; // 局部对象x
}void func2() {// cout << x; // 错误!x在func1中,此处不可见
}
4、局部对象与全局对象的对比
特性 | 局部对象 | 全局对象 |
---|---|---|
作用域 | 定义所在的函数或代码块 | 整个程序 |
生命周期 | 作用域内有效 | 程序运行期间始终有效 |
存储位置 | 栈内存(自动管理) | 全局数据区(静态存储) |
访问权限 | 仅在作用域内可访问 | 所有函数可访问 |
初始化 | 每次进入作用域时初始化 | 程序启动时初始化 |
5、注意事项
1. 不要返回局部对象的指针或引用
局部对象在函数返回后会被销毁,返回其指针或引用会导致悬垂指针/引用(Dangling Pointer/Reference)。
// 错误示例:返回局部对象的引用
int& badFunction() {int x = 10; // x是局部对象,函数返回后x被销毁return x; // 返回悬垂引用,未定义行为!
}// 正确做法:返回值而非引用
int goodFunction() {int x = 10;return x; // 返回x的副本
}
2. 对象销毁顺序
局部对象按定义的逆序销毁,依赖其他对象的资源时需注意顺序。
class Logger {
public:Logger() { cout << "Logger created" << endl; }~Logger() { cout << "Logger destroyed" << endl; }
};void test() {Logger log1; // 先创建Logger log2; // 后创建
} // 先销毁log2,再销毁log1(逆序销毁)
3. 避免在局部作用域中创建过大对象
栈内存有限(通常几MB),过大的对象可能导致栈溢出。
解决方案:使用动态内存(堆内存)分配。
void safeFunction() {// 栈内存可能溢出(错误)// int hugeArray[1000000]; // 改用堆内存(正确)int* hugeArray = new int[1000000];delete[] hugeArray; // 需手动释放
}
6、实际应用示例
示例1:局部对象在循环中的使用
#include <iostream>
using namespace std;int main() {for (int i = 0; i < 5; i++) { // i是局部对象,仅在循环内有效string message = "Iteration: " + to_string(i); // message每次循环重新创建cout << message << endl;} // 每次循环结束,message和i的当前副本被销毁return 0;
}
示例2:利用RAII管理局部资源
#include <fstream>
using namespace std;void readFile() {ifstream file("data.txt"); // 局部对象file,打开文件if (file.is_open()) {// 读取文件内容string line;while (getline(file, line)) {cout << line << endl;}} // 离开作用域时,file的析构函数自动关闭文件
}
readFile
函数展示了 C++ 中文件操作的基本用法,以及局部对象的生命周期管理。1. 局部对象
file
的创建
ifstream file("data.txt"); // 局部对象file,打开文件
使用
ifstream
类型创建了一个名为file
的局部对象,用于读取文件"data.txt"
。文件在构造函数中被打开。如果文件不存在或无法打开,
file.is_open()
将返回false
。2. 检查文件是否成功打开
if (file.is_open()) { // 读取文件内容 }
调用
file.is_open()
检查文件是否成功打开。如果文件未成功打开,程序将跳过文件读取部分。
3. 读取文件内容
string line; while (getline(file, line)) { cout << line << endl; }
使用
getline
函数逐行读取文件内容。每次读取一行并存储到字符串变量
line
中。如果文件结束或发生错误,
getline
返回false
,循环结束。4. 局部对象的作用域和析构
} // 离开作用域时,file的析构函数自动关闭文件
当程序执行到
}
时,局部对象file
的作用域结束。在 C++ 中,当局部对象超出作用域时,其析构函数会被自动调用。
对于
ifstream
对象,析构函数会自动关闭文件,因此无需手动调用file.close()
。
总结
这段代码展示了 C++ 中文件读取的基本流程,以及局部对象的生命周期管理:
使用
ifstream
打开文件。检查文件是否成功打开。
使用
getline
逐行读取文件内容。离开作用域时,局部对象的析构函数自动释放资源(如关闭文件)。
这种设计利用了 C++ 的 RAII(Resource Acquisition Is Initialization)机制,确保资源(如文件句柄)在不再需要时能够被正确释放,避免资源泄漏。
小贴士:
C++ 的 RAII(Resource Acquisition Is Initialization,资源获取即初始化) 是一种管理资源的编程机制,核心思想是:通过对象的生命周期来控制资源的获取与释放。
实现方式
构造函数获取资源:对象创建时,在构造函数中申请资源(如内存、文件句柄、网络连接、互斥锁等)。
析构函数释放资源:对象生命周期结束(如离开作用域、被销毁)时,自动调用析构函数释放资源。
核心优势
避免资源泄漏:无论程序正常退出还是因异常终止,对象析构函数都会执行,确保资源释放。
异常安全:若构造函数成功获取资源,后续操作即使抛出异常,析构函数仍会释放资源,保证程序状态正确。
7、小结
- 局部对象的作用域和生命周期是C++高效内存管理的基础。
- 避免返回局部对象的指针或引用,防止未定义行为。
- 合理使用静态局部对象实现跨函数调用的状态保持。
- 栈内存限制要求对大型对象使用动态内存分配。
4、函数组成要素详解
1. 返回类型
void
表示无返回值。- 必须与
return
语句的类型匹配。
void printHello() {std::cout << "Hello!";// 无return语句(允许)
}
2. 参数列表
- 可以是零个或多个参数,用逗号分隔。
- 参数传递方式:值传递、引用传递、指针传递(后文详解)。
3. 函数体
- 包含具体执行的代码。
- 局部变量在函数结束时销毁。
5、函数声明与定义
1、函数声明的作用
函数声明(也称函数原型,Function Prototype)的主要目的是向编译器告知函数的存在,允许在函数定义之前调用它。
核心作用:
-
类型检查:确保调用时参数类型和返回值类型正确。
-
分离编译:支持多文件编程,声明通常放在头文件(
.h
)中。 -
解决依赖:允许函数A调用函数B,而无需考虑定义的先后顺序。
2、函数声明的语法
返回类型 函数名(参数类型列表); // 结尾必须有分号!
-
参数类型列表:只需指定参数类型,形参名可省略(但建议保留以增强可读性)。
-
示例:
// 声明一个加法函数 int add(int, int); // 省略形参名 int multiply(int a, int b); // 保留形参名(推荐)
3、函数声明 vs. 函数定义
特性 | 函数声明 | 函数定义 |
---|---|---|
分号 | 必须有分号结尾 | 无分号 |
函数体 | 无函数体(仅声明接口) | 必须包含函数体(具体实现) |
出现次数 | 可多次声明(需一致) | 只能定义一次(单定义规则) |
示例1:声明与定义分离
// 声明(头文件 math_utils.h)
int add(int a, int b);// 定义(源文件 math_utils.cpp)
int add(int a, int b) {return a + b;
}
4、函数声明的位置
1. 在头文件中声明(推荐)
-
头文件(
.h
)用于集中放置函数声明,供多个源文件共享。 -
示例:
// math_utils.h #ifndef MATH_UTILS_H // 头文件保护,防止重复包含 #define MATH_UTILS_Hint add(int a, int b); double add(double a, double b); // 函数重载声明#endif
2. 在调用前声明
-
在调用函数的代码前直接声明(适用于简单程序)。
-
示例:
#include <iostream> using namespace std;// 声明printMessage函数 void printMessage(const string& message);int main() {printMessage("Hello, World!");return 0; }// 定义printMessage void printMessage(const string& message) {cout << message << endl; }
5、函数声明的注意事项
1. 默认参数的声明规则
-
默认参数只能在函数声明中指定,不能在定义中重复。
-
若声明和定义分离,默认参数应放在声明中(通常位于头文件)。
// 声明(math_utils.h) void log(const string& message, int level = 1);// 定义(math_utils.cpp) void log(const string& message, int level) { // 此处不可写=1// 实现... }
2. 函数重载的声明
-
重载函数的声明必须通过参数列表区分(返回类型不同不算重载)。
// 合法重载 int max(int a, int b); double max(double a, double b);// 错误:仅返回类型不同,无法重载 double getValue(); int getValue(); // 编译错误
3. 函数声明的作用域
-
声明的作用域从声明位置开始,到所在作用域结束。
-
示例:
void outer() {// 错误:inner()未声明// inner(); void inner(); // 声明inner函数inner(); // 合法调用 }void inner() {cout << "Inner function" << endl; }
6、未声明函数的后果
若调用函数前未声明,可能导致:
-
编译错误:编译器无法识别函数。
-
隐式声明风险:C语言允许隐式声明,但C++已废弃此行为,现代编译器会报错。
int main() {int result = add(3, 5); // 若未声明add,编译失败return 0; }
7、实际应用示例
示例1:多文件编程
// ---------- math_utils.h ----------
#ifndef MATH_UTILS_H
#define MATH_UTILS_Hint factorial(int n); // 声明阶乘函数#endif// ---------- math_utils.cpp ----------
#include "math_utils.h"int factorial(int n) { // 定义if (n <= 1) return 1;return n * factorial(n - 1);
}// ---------- main.cpp ----------
#include <iostream>
#include "math_utils.h"int main() {std::cout << factorial(5); // 输出120return 0;
}
示例2:前置声明解决循环依赖
// 前置声明B类
class B;class A {
public:void callB(B& b); // 声明函数,参数为B的引用
};class B {
public:void callA(A& a) { a.callB(*this); } // 定义函数
};// A::callB的定义需在B类定义之后
void A::callB(B& b) { /* ... */ }
8、小结
-
声明先于调用:函数必须在使用前声明。
-
头文件集中管理:声明应放在头文件中,并通过头文件保护避免重复包含。
-
默认参数在声明中指定:确保调用者可见。
-
声明与定义一致:参数列表和返回类型必须严格匹配。
二、参数传递
1、参数传递的基本概念
参数传递是函数调用时,将数据从调用方传递给函数的方式。
核心作用:
- 控制函数内对原始数据的访问权限(是否允许修改原数据)。
- 优化性能(避免不必要的拷贝)。
2、值传递(Pass by Value)
1. 机制
-
将实参的副本传递给函数,函数内修改形参不会影响原始数据。
-
适用场景:基本数据类型(
int
,double
等)或小型结构体。
2. 示例
void increment(int x) {x++; // 修改的是副本
}int main() {int a = 5;increment(a);cout << a; // 输出5(原值未变)
}
3. 特点
-
优点:安全,函数内操作不影响原数据。
-
缺点:对大型对象(如类、数组)会产生拷贝开销。
3、引用传递(Pass by Reference)
1. 机制
- 传递实参的别名(引用),函数内修改形参直接影响原始数据。
- 适用场景:需要修改原数据,或传递大型对象避免拷贝。
2. 示例
void increment(int &x) {x++; // 修改原数据
}int main() {int a = 5;increment(a);cout << a; // 输出6(原值被修改)
}
3. 特点
- 优点:避免拷贝开销,允许修改原数据。
- 缺点:需注意函数可能意外修改数据(可用
const
解决)。
4、指针传递(Pass by Pointer)
1. 机制
- 传递实参的内存地址,函数内通过指针操作原始数据。
- 适用场景:需要显式传递地址,或允许
nullptr
(空指针)的情况。
2. 示例
void increment(int *x) {if (x != nullptr) { // 必须检查空指针!(*x)++; // 解引用后修改原数据}
}int main() {int a = 5;increment(&a); // 传递地址cout << a; // 输出6
}
3. 特点
- 优点:明确表示可能修改原数据,支持动态内存操作。
- 缺点:语法复杂,需手动检查空指针。
5、const
修饰符的应用
1. const
引用
- 禁止通过引用修改原数据,同时避免拷贝。
void printLargeObject(const BigClass &obj) {// 只能读取obj,无法修改// 避免拷贝BigClass的副本
}
2. const
指针
- 禁止通过指针修改数据,或禁止修改指针本身。
void readData(const int *ptr) { // 不能通过ptr修改数据cout << *ptr;
}void readDate(int* const ptr){//不能修改ptr本身cout<<*ptr;
}
6、值传递 vs. 引用传递 vs. 指针传递对比
特性 | 值传递 | 引用传递 | 指针传递 |
---|---|---|---|
修改原数据 | 不允许 | 允许 | 允许 |
拷贝开销 | 有(副本) | 无(别名) | 无(传递地址) |
空值支持 | 不适用 | 不适用 | 支持(nullptr ) |
语法简洁性 | 简单 | 简单 | 较复杂 |
典型用途 | 基本数据类型 | 大型对象/需修改数据 | 动态内存/可选参数 |
7、高级传递方式(C++11+)
1. 移动语义(Move Semantics)
- 通过
std::move
将资源所有权转移,避免拷贝(适用于临时对象)。
void processBigData(BigData &&data) { // 右值引用// 移动资源,避免拷贝
}BigData data;
processBigData(std::move(data)); // 转移所有权
2. 万能引用(Universal Reference)与完美转发
- 使用
auto&&
和std::forward
保留参数类型(模板编程中常见)。
template<typename T>
void relay(T &&arg) {process(std::forward<T>(arg)); // 完美转发
}
8、参数传递的选择指南
- 需要修改原数据 → 使用引用传递(优先)或指针传递。
- 仅读取数据且对象较大 → 使用
const
引用传递。 - 基本数据类型(如
int
) → 值传递或const
引用均可。 - 动态内存或可选参数 → 指针传递(需检查
nullptr
)。 - 避免拷贝临时对象 → 使用移动语义(C++11+)。
9、常见错误与解决方案
1. 返回局部对象的引用或指针
int& badFunction() {int x = 10;return x; // 错误:x在函数结束时被销毁
}
解决:返回值(副本)或传递动态分配的对象。
2. 悬垂引用(Dangling Reference)
int& ref;
{int a = 5;ref = a; // a的引用在代码块外失效
}
cout << ref; // 未定义行为
解决:确保引用的对象生命周期足够长(可以尝试static)。
3. 未初始化指针
int *ptr;
*ptr = 5; // ptr未初始化,指向无效地址
解决:始终初始化指针(如int *ptr = &valid_var
或ptr = new int
)。
10、一个小栗子
#include <iostream>
using namespace std;// 值传递:安全但低效(适合小对象)
void printValue(int val) {cout << "Value: " << val << endl;
}// 引用传递:高效且可修改原数据
void square(int &num) {num *= num;
}// const引用:高效且防止修改(适合大对象)
void printBigObject(const BigData &data) {data.display();
}// 指针传递:允许空值,显式传递地址
void allocateMemory(int **ptr) {*ptr = new int(100); // 修改指针指向的内容
}int main() {int a = 5;square(a); // 引用传递,a变为25printValue(a); // 值传递,输出25int *p = nullptr;allocateMemory(&p); // 指针传递,p指向新内存cout << *p; // 输出100delete p;return 0;
}
11、小结
- 值传递:简单安全,适合小型数据。
- 引用传递:高效灵活,优先用于大型对象或需修改原数据。
- 指针传递:灵活但需谨慎,适合动态内存或可选参数。
-
const
修饰符:保护数据不被意外修改,提升代码健壮性。 - 现代C++特性:移动语义和完美转发可优化资源管理。
三、返回类型和return语句
1、返回类型的作用
返回类型定义了函数返回数据的类型,决定了调用者如何接收和处理结果。
关键点:
- 必须与
return
语句返回的值类型匹配(可隐式转换)。 - 若返回类型为
void
,函数不能返回任何值。
2、基本返回类型与return
用法
1. 返回基本数据类型(int
, double
等)
int add(int a, int b) {return a + b; // 返回int类型值
}double divide(double a, double b) {return a / b; // 返回double类型值
}
2. 返回void
(无返回值)
void printHello() {cout << "Hello";return; // 可省略,函数执行到末尾自动返回
}void earlyExit(bool flag) {if (flag) {return; // 提前退出函数}cout << "Continue...";
}
3、返回引用与指针
1. 返回引用(避免拷贝,但需注意生命周期)
// 返回静态局部变量的引用(安全)
int& getStaticValue() {static int value = 10; // 静态变量,生命周期到程序结束return value;
}// 错误示例:返回局部变量的引用(悬垂引用)
int& badFunction() {int x = 5; // x是局部变量,函数返回后被销毁return x; // 未定义行为!
}
2. 返回指针(需确保内存有效)
// 返回动态内存的指针(调用者需负责释放)
int* createArray(int size) {int* arr = new int[size];return arr;
}// 错误示例:返回局部变量的指针
int* badPointer() {int x = 10;return &x; // x被销毁后指针失效!
}
4、返回对象
1. 返回值对象(触发拷贝构造或移动构造)
class MyClass {
public:MyClass() { cout << "Constructor" << endl; }MyClass(const MyClass&) { cout << "Copy Constructor" << endl; }MyClass(MyClass&&) { cout << "Move Constructor" << endl; }
};MyClass createObject() {MyClass obj;return obj; // 可能触发返回值优化(RVO)
}int main() {MyClass obj = createObject(); // 输出可能仅为"Constructor"(RVO优化)
}
2. 使用移动语义提升效率(C++11+)
MyClass createObject() {MyClass obj;return std::move(obj); // 强制移动语义(可能阻止RVO)
}
5、返回类型与return
的匹配规则
1. 隐式类型转换
return
的值类型可隐式转换为返回类型。
double func() {return 3; // int隐式转换为double
}
2. 列表初始化(C++11+)
std::vector<int> getNumbers() {return {1, 2, 3}; // 返回初始化列表
}
3. 返回auto
(C++14+)
auto add(double a, double b) -> decltype(a + b) {return a + b; // 返回类型由a+b推导
}// 更简化的写法(C++14)
auto multiply(double a, double b) {return a * b; // 自动推导返回类型
}
6、常见错误与解决方案
1. 返回局部对象的引用/指针
int& badRef() {int x = 10;return x; // x被销毁,返回悬垂引用
}
解决:返回静态变量、动态内存或参数中的引用。
2. 返回类型与return
不匹配
int func() {return 3.14; // double转int导致精度丢失(警告)
}
解决:显式转换或修改返回类型。
3. 遗漏return
语句
int badFunc(bool flag) {if (flag) {return 1;}// 未处理flag=false的情况,导致未定义行为
}
解决:确保所有路径都有返回值。
7、高级特性
1. 尾置返回类型(C++11+)
auto getData() -> int (*)[5] { // 返回指向int[5]的指针static int arr[5] = {1,2,3,4,5};return &arr;
}
2. 多返回值(通过结构体或std::tuple
)
#include <tuple>std::tuple<int, double> getValues() {return std::make_tuple(42, 3.14);
}int main() {auto [a, b] = getValues(); // C++17结构化绑定
}
8、小结
- 返回类型需与
return
值匹配,注意隐式转换规则。 - 返回引用/指针时,必须确保对象生命周期足够长。
- 返回对象时,优先依赖编译器优化(RVO),而非显式
std::move
。 - 现代C++特性(如
auto
、移动语义、结构化绑定)可简化代码并提升效率。 - 避免常见错误:悬垂引用、遗漏
return
、类型不匹配。
四、函数重载(Overloading)
1、什么是函数重载?
函数重载允许在同一作用域内定义多个同名函数,但这些函数的参数列表必须不同(参数类型、数量或顺序不同)。
核心目的:提供语义相同的操作,但支持不同类型或数量的参数,提升代码可读性和灵活性。
// 重载示例
int sum(int a, int b) { return a + b; }
double sum(double a, double b) { return a + b; }
int sum(int a, int b, int c) { return a + b + c; }
2、函数重载的规则
- 参数列表必须不同:以下任一条件满足即可:
- 参数类型不同(如
int
vsdouble
)。 - 参数数量不同。
- 参数顺序不同(但需类型不同)。
- 参数类型不同(如
- 返回类型不同不构成重载:仅返回类型不同会导致编译错误。
- 作用域相同:重载函数必须在同一作用域(如全局作用域或同一类中)。
3、函数重载的示例
示例1:参数类型不同
void print(int x) {cout << "Integer: " << x << endl;
}void print(double x) {cout << "Double: " << x << endl;
}int main() {print(5); // 调用print(int)print(3.14); // 调用print(double)
}
示例2:参数数量不同
int sum(int a, int b) {return a + b;
}int sum(int a, int b, int c) {return a + b + c;
}int main() {cout << sum(1, 2); // 输出3cout << sum(1, 2, 3); // 输出6
}
示例3:参数顺序不同
void show(int a, double b) {cout << "int, double" << endl;
}void show(double a, int b) {cout << "double, int" << endl;
}int main() {show(3, 4.5); // 调用第一个函数show(4.5, 3); // 调用第二个函数
}
4、函数重载的解析机制
编译器根据调用时的实参类型和数量选择最匹配的函数,优先级如下:
- 精确匹配(类型完全一致)。
- 隐式转换匹配(如
int
→double
)。 - 用户定义的转换(如类类型转换运算符)。
示例:匹配优先级
void func(int x) { cout << "int" << endl; }
void func(double x) { cout << "double" << endl; }int main() {func(5); // 精确匹配func(int)func(5.0); // 精确匹配func(double)func('a'); // 隐式转换char→int,调用func(int)
}
5、函数重载与const
修饰符
1. 顶层const
不构成重载
void func(int x) { /* ... */ }
void func(const int x) { /* ... */ } // 错误:重复定义
2. 底层const
指针或引用构成重载
void func(int *ptr) { /* ... */ }
void func(const int *ptr) { /* ... */ } // 合法重载void func(int &x) { /* ... */ }
void func(const int &x) { /* ... */ } // 合法重载
示例:const
引用重载
void process(string &str) {str += " (modified)";
}void process(const string &str) {cout << str << " (read-only)" << endl;
}int main() {string s = "Hello";const string cs = "Hi";process(s); // 调用非常量版本process(cs); // 调用常量版本
}
6、函数重载与默认参数
默认参数可能导致二义性调用,需谨慎使用。
void draw(int x, int y = 0) { /* ... */ }
void draw(int x) { /* ... */ }int main() {draw(5); // 错误:两个函数都匹配
}
7、函数重载与模板
1. 函数模板支持隐式重载
template<typename T>
void print(T value) {cout << "Template: " << value << endl;
}void print(int x) {cout << "Non-template: " << x << endl;
}int main() {print(5); // 调用非模板函数(更匹配)print(3.14); // 调用模板函数(T=double)
}
2. 特化模板时需注意重载规则
template<>
void print(int x) { // 显式特化cout << "Specialized: " << x << endl;
}
8、类成员函数的重载
类成员函数可以重载,包括构造函数。
class Rectangle {
public:// 构造函数重载Rectangle() { width = height = 0; }Rectangle(int w, int h) : width(w), height(h) {}// 成员函数重载void scale(double factor) { width *= factor; height *= factor; }void scale(int factor) { width *= factor; height *= factor; }
private:int width, height;
};
9、常见错误与解决方案
1. 二义性调用
void func(float x) {}
void func(double x) {}int main() {func(3.14); // 匹配func(double)func(3.14f); // 匹配func(float)func(5); // 错误:5可转float或double,编译器无法决定
}
解决:显式指定类型,如func(static_cast<float>(5))
。
2. 隐藏基类重载函数
class Base {
public:void func(int x) {}
};class Derived : public Base {
public:void func(double x) {} // 隐藏Base::func(int)
};int main() {Derived d;d.func(5); // 调用Derived::func(double),Base::func(int)被隐藏
}
解决:使用using Base::func;
引入基类重载。
10、小结
- 函数重载的核心是参数列表不同,返回类型不影响重载。
- 优先通过参数类型、数量或顺序实现重载,避免二义性。
const
修饰符在指针或引用参数中可构成重载。- 注意与模板、默认参数和继承的交互关系。
- 合理使用重载提升代码可读性,但避免过度设计。
温馨小贴士:重载和作用域的关系
1、函数重载的基本前提
函数重载要求同名函数必须在同一作用域内,并且参数列表不同。
核心规则:
只有在同一作用域内,编译器才会将同名函数视为重载候选。
不同作用域中的同名函数会触发名称隐藏(Name Hiding),而非重载。
2、局部作用域 vs 全局作用域
局部作用域中声明同名函数会隐藏外部作用域的同名函数
#include <iostream> using namespace std;void func(int x) { cout << "Global func(int)" << endl; }int main() {func(5); // 调用全局func(int){ // 进入局部作用域void func(double x); // 声明局部作用域的funcfunc(3.14); // 调用局部func(double)// func(5); 错误!全局func(int)被隐藏}func(5); // 再次调用全局func(int)return 0; }void func(double x) { cout << "Global func(double)" << endl; }
结果:
Global func(int) Global func(double) Global func(int)
解释:
局部作用域中的
func(double)
声明隐藏了全局作用域的func(int)
。全局
func(double)
实际在局部作用域外定义,但局部作用域内的声明优先。
3、类作用域中的函数重载
类的成员函数重载
class MyClass { public:void print(int x) { cout << "int: " << x << endl; }void print(double x) { cout << "double: " << x << endl; } };int main() {MyClass obj;obj.print(5); // 调用print(int)obj.print(3.14); // 调用print(double) }
合法重载:两个
4、继承中的函数重载与作用域
派生类中同名函数隐藏基类重载
class Base { public:void func(int x) { cout << "Base::func(int)" << endl; }void func(double x) { cout << "Base::func(double)" << endl; } };class Derived : public Base { public:void func(const char* s) { cout << "Derived::func(const char*)" << endl; } };int main() {Derived d;d.func("Hello"); // 调用Derived::func(const char*)// d.func(5); 错误!Base::func(int)被隐藏d.Base::func(5); // 显式调用基类函数 }
解释:
派生类中的
func(const char*)
隐藏了基类的所有func
重载版本。必须通过
Base::
作用域运算符显式调用基类函数。使用
using
声明恢复基类重载class Derived : public Base { public:using Base::func; // 引入基类func的所有重载void func(const char* s) { cout << "Derived::func(const char*)" << endl; } };int main() {Derived d;d.func(5); // 调用Base::func(int)d.func(3.14); // 调用Base::func(double)d.func("Hello"); // 调用Derived::func(const char*) }
5、命名空间作用域与重载
同一命名空间内的函数重载
namespace NS {void log(int x) { cout << "NS::log(int)" << endl; }void log(double x) { cout << "NS::log(double)" << endl; } }int main() {NS::log(5); // 调用log(int)NS::log(3.14); // 调用log(double) }
不同命名空间的同名函数不构成重载
namespace NS1 { void log(int x) { /* ... */ } } namespace NS2 { void log(double x) { /* ... */ } }int main() {NS1::log(5); // 调用NS1::log(int)NS2::log(3.14); // 调用NS2::log(double)// log(5); 错误!未指定命名空间 }
6、函数模板与作用域
模板与非模板函数的重载
void process(int x) { cout << "Non-template" << endl; }template<typename T> void process(T x) { cout << "Template" << endl; }int main() {process(5); // 调用非模板函数(更匹配)process(3.14); // 调用模板函数(T=double) }
规则:非模板函数优先于模板函数匹配。
7、作用域对重载的影响
同一作用域:函数名相同且参数不同 → 合法重载。
不同作用域:内层作用域的同名函数会隐藏外层作用域的所有重载版本。
继承中的重载:派生类函数隐藏基类同名函数,需用
using
声明恢复。解决方案:
使用
using
声明引入外层作用域的重载。通过作用域运算符(如
Base::func
)显式调用隐藏函数。避免在不同作用域中定义同名函数,除非有意隐藏。
关键结论
函数重载仅在相同作用域内有效。
作用域隔离会导致名称隐藏,而非重载。
合理使用
using
声明和作用域运算符管理不同作用域中的函数可见性。
五、内联函数(inline)
1、内联函数的作用
目的:通过将函数代码直接插入调用位置,消除函数调用的开销(压栈、跳转、返回),提高程序运行效率。
适用场景:短小且频繁调用的函数(如简单计算、访问类成员)。
2、内联函数的定义
1. 使用inline
关键字
inline int add(int a, int b) {return a + b;
}
2. 类内定义的成员函数隐式内联
class Calculator {
public:int multiply(int a, int b) { // 隐式内联return a * b;}
};
小对比:
auto lambda = [](int a, int b) { return a + b; }; cout << lambda(2, 3); // 输出5
3、内联函数的底层逻辑
1. 编译器行为
- 建议而非强制:
inline
是给编译器的优化建议,最终是否内联由编译器决定。 - 代码膨胀风险:若函数体较大或频繁调用,内联会导致可执行文件体积增大。
2. 汇编代码对比(示例)
- 普通函数调用:生成
call
指令跳转到函数地址。 - 内联函数:函数体代码直接插入调用位置,无
call
指令。
4、内联函数的优缺点
优点 | 缺点 |
---|---|
减少函数调用开销 | 增加代码体积(多次展开) |
避免跳转指令提升执行效率 | 不适合复杂或递归函数 |
可替代宏(类型安全、易调试) | 编译器可能忽略inline 建议 |
5、内联函数的限制
- 编译器决策权:以下情况编译器通常拒绝内联:
- 函数体包含循环或递归。
- 函数体过长(如超过10行)。
- 虚函数(需动态绑定)。
- 头文件要求:内联函数定义必须放在头文件中,确保所有调用位置可见(否则链接错误)。
6、内联函数 vs 宏
特性 | 内联函数 | 宏(#define ) |
---|---|---|
处理阶段 | 编译期(语法检查) | 预处理期(文本替换) |
类型安全 | 支持(类型检查) | 不支持(易引发错误) |
调试支持 | 支持(可设置断点) | 不支持 |
作用域 | 遵循C++作用域规则 | 全局替换 |
示例:内联函数替代宏
// 宏的隐患
#define SQUARE(x) ((x) * (x))
int a = 5;
int b = SQUARE(a++); // a被递增两次(未定义行为)// 内联函数(安全)
inline int square(int x) { return x * x; }
int a = 5;
int b = square(a++); // a只递增一次
解释一下:
1. 宏定义
SQUARE(x)
#define SQUARE(x) ((x) * (x))
宏定义是一种简单的文本替换机制。在编译之前,预处理器会将所有出现的
SQUARE(x)
替换为((x) * (x))
。示例:
int b = SQUARE(a++); // 被替换为:int b = ((a++) * (a++));
问题:
在表达式
((a++) * (a++))
中,a++
被计算了两次。C++ 标准中明确规定,对于同一个变量,在一个序列点(sequence point)之前对其修改多次是未定义行为。
因此,
a
的值可能会被递增两次,导致结果不可预测。
2. 内联函数
square(int x)
inline int square(int x) { return x * x; }
内联函数是一种编译器优化手段。它告诉编译器尽量将函数调用替换为函数体本身,类似于宏展开,但具有类型检查和作用域的优点。
示例:
int b = square(a++); // 调用时,a++ 只会被计算一次
优点:
参数
a++
只会被计算一次,并将其结果传递给函数square
。避免了宏定义中可能出现的重复计算问题。
具有类型检查和作用域的安全性。
7、例子
示例1:短小工具函数
// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_Hinline int clamp(int value, int min, int max) {return (value < min) ? min : (value > max) ? max : value;
}#endif// main.cpp
#include "math_utils.h"
int main() {int x = clamp(150, 0, 100); // 展开为 (150 < 0) ? 0 : (150 > 100) ? 100 : 150;return 0;
}
示例2:类成员内联函数
class Vector2D {
public:Vector2D(float x, float y) : x(x), y(y) {}inline float magnitude() const {return sqrt(x*x + y*y);}private:float x, y;
};
8、温馨小贴士
- 避免滥用内联:优先考虑函数调用开销与代码体积的平衡。
- 复杂函数非内联:包含循环、递归或大量代码的函数不宜内联。
- 跨模块可见性:内联函数需在头文件中定义,否则导致链接错误。
- 现代编译器优化:编译器自动内联简单函数(即使未标记
inline
)。
9、小结
- 内联函数是性能优化工具:通过消除调用开销提升效率。
- 短小函数适用:适用于简单、高频调用的场景。
- 编译器最终决策:
inline
仅为建议,实际内联由编译器决定。 - 替代宏的更安全方案:提供类型安全和调试支持。
六、默认参数
1、默认参数的作用
默认参数允许在函数声明时为参数指定默认值,调用时可省略该参数。
核心优势:
- 简化函数调用,增强代码灵活性。
- 减少需要重载的函数数量。
2、基本语法与使用
1. 声明默认参数
在函数声明中指定默认值(通常在头文件中):
// 声明函数(默认参数在声明中指定)
void printMessage(const std::string& msg = "Hello, World!");
2. 定义函数
定义时不再重复默认值:
void printMessage(const std::string& msg) {std::cout << msg << std::endl;
}
3. 调用示例
printMessage(); // 输出"Hello, World!"
printMessage("Hi"); // 输出"Hi"
3、默认参数的规则
1. 参数从右向左依次设置默认值
- 右侧参数必须先设置默认值,左侧参数可选。
// 正确:从右向左设置默认值
void draw(int x, int y = 0, int color = 255);// 错误:左侧参数有默认值,右侧无
void errorFunc(int a = 5, int b); // 编译失败
2. 默认值必须是常量或全局可访问的表达式
- 允许使用常量、字面量、全局变量或静态变量。
- 不允许使用局部变量或函数参数。
const int DEFAULT_SIZE = 10;
int globalValue = 100;void init(int size = DEFAULT_SIZE, int value = globalValue); // 合法
3. 默认参数只能在函数声明中指定一次
- 若函数声明和定义分离,默认参数必须在声明中指定,定义中不可重复。
// 头文件(math_utils.h)
int multiply(int a, int b = 2); // 声明时指定默认值// 源文件(math_utils.cpp)
int multiply(int a, int b) { // 定义时不写默认值return a * b;
}
4、默认参数与函数重载
默认参数可简化重载,但需避免歧义。
示例:默认参数替代重载
// 用默认参数替代以下两个重载函数
void log(const std::string& msg, bool addTimestamp);
void log(const std::string& msg);// 合并为一个函数
void log(const std::string& msg, bool addTimestamp = false);
错误示例:二义性调用
void connect(int timeout = 10);
void connect();// 调用connect()时,编译器无法确定调用哪个函数
5、默认参数的实际应用
1. 构造函数初始化对象
class Circle {
public:// 构造函数:radius和color都有默认值Circle(double r = 1.0, int c = 0xFF0000) : radius(r), color(c) {}
private:double radius;int color;
};int main() {Circle c1; // 使用默认半径和颜色Circle c2(5.0); // 半径5.0,颜色默认Circle c3(3.0, 0x00FF00);
}
2. 灵活配置函数行为
// 绘制矩形:默认颜色为红色,边框可选
void drawRect(int width, int height, const std::string& color = "red", bool hasBorder = true);// 调用
drawRect(100, 200); // 使用所有默认值
drawRect(100, 200, "blue"); // 自定义颜色,边框默认
drawRect(100, 200, "green", false); // 自定义所有参数
6、注意事项
- 避免与重载函数冲突:确保默认参数不会导致调用歧义。
- 优先在头文件中声明默认值:确保所有调用代码可见。
- 慎用复杂默认值:默认值应是简单、明确的默认行为。
- 默认参数与C兼容性:C语言不支持默认参数。
7、总结
- 默认参数简化调用:减少冗余代码,提升可读性。
- 从右向左设置默认值:确保左侧参数在调用时可选。
- 声明与定义分离时:默认参数仅在声明中指定。
- 合理替代重载:避免过度设计重载函数。
七、函数指针
1、函数指针的作用
函数指针是指向函数内存地址的变量,允许通过指针动态调用函数。
核心应用:
- 实现回调(Callback)机制。
- 动态选择函数逻辑(如策略模式)。
- 与C语言库交互(如qsort中的比较函数)。
2、函数指针的声明与初始化
1. 声明语法
返回类型 (*指针变量名)(参数类型列表);
示例:
int (*funcPtr)(int, int); // 指向返回int,参数为两个int的函数
2. 初始化与赋值
// 假设存在函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }// 初始化方式1:直接赋值函数名(隐式取地址)
funcPtr = add;// 初始化方式2:显式取地址
funcPtr = ⊂
3、通过函数指针调用函数
1. 直接调用(推荐)
int result = funcPtr(3, 5); // 等价于调用add(3,5)或sub(3,5)
2. 解引用调用
int result = (*funcPtr)(3, 5);
3.用一个完整的例子来看看:
#include <iostream>
using namespace std;
// 假设存在函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }int main(){int add(int a, int b);int sub(int a, int b);int (*ptr1)(int ,int);int (*ptr2)(int ,int);// 初始化方式1:直接赋值函数名(隐式取地址)ptr1 = add;// 初始化方式2:显式取地址ptr2 = ⊂// 使用函数指针调用函数cout << ptr1(5, 3) << endl; // 输出8cout << ptr2(5, 3) << endl; // 输出2return 0;
}
内存图如下:(有点小问题,但需要表达的意思已经达到了)
4、函数指针的典型应用
1. 回调函数示例
#include <iostream>
using namespace std;// 回调函数类型定义
typedef void (*Callback)(const string&);// 执行任务并回调
void doTask(Callback callback) {cout << "Processing task..." << endl;callback("Task completed!");
}// 具体回调函数
void onComplete(const string& message) {cout << "Callback: " << message << endl;
}int main() {doTask(onComplete);return 0;
}
2. 函数指针数组
int (*operations[])(int, int) = {add, sub};int main() {int a = 10, b = 5;cout << operations[0](a, b) << endl; // 15 (add)cout << operations[1](a, b) << endl; // 5 (sub)
}
5、使用typedef
或using
简化声明
1. typedef
简化
typedef int (*MathFunc)(int, int);
MathFunc funcPtr = add; // 更易读
2. C++11的using
别名
using MathFunc = int (*)(int, int);
MathFunc funcPtr = sub;
6、成员函数指针(高级)
1. 声明与调用类成员函数指针
class Calculator {
public:int multiply(int a, int b) { return a * b; }
};int main() {Calculator calc;// 声明成员函数指针int (Calculator::*memFuncPtr)(int, int) = &Calculator::multiply;// 调用int result = (calc.*memFuncPtr)(3, 5); // 输出15return 0;
}
2. 结合对象指针调用
Calculator* pCalc = new Calculator();
int result = (pCalc->*memFuncPtr)(4, 6); // 24
delete pCalc;
7、函数指针与std::function
的对比
特性 | 函数指针 | std::function (C++11+) |
---|---|---|
类型安全 | 弱(无类型擦除) | 强(支持类型检查) |
携带状态 | 不能 | 能(可绑定lambda、函数对象等) |
性能 | 高(直接调用) | 略低(可能有间接开销) |
灵活性 | 仅支持普通函数和静态成员 | 支持函数、lambda、成员函数等 |
8、注意事项
- 类型严格匹配:函数指针类型必须与实际函数签名完全一致。
- 空指针检查:调用前需确保指针非空。
- C++11+替代方案:优先使用
std::function
和lambda表达式,除非需要与C兼容。
9、实际应用示例:排序算法回调
#include <algorithm>
#include <vector>
using namespace std;// 比较函数类型
typedef bool (*CompareFunc)(int, int);// 升序比较
bool ascending(int a, int b) { return a < b; }// 降序比较
bool descending(int a, int b) { return a > b; }// 使用函数指针的排序函数
void customSort(vector<int>& vec, CompareFunc comp) {sort(vec.begin(), vec.end(), comp);
}int main() {vector<int> nums = {3, 1, 4, 1, 5};customSort(nums, ascending); // 升序排序customSort(nums, descending); // 降序排序return 0;
}
解释一下
1. 函数指针类型定义
typedef bool (*CompareFunc)(int, int);
定义了一个名为
CompareFunc
的类型,表示指向返回值为bool
、参数为两个int
类型的函数的指针。这种类型定义简化了后续代码中函数指针的声明和使用。
2. 比较函数
bool ascending(int a, int b) { return a < b; }
bool descending(int a, int b) { return a > b; }
定义了两个比较函数:
ascending
: 判断a
是否小于b
,用于升序排序。
descending
: 判断a
是否大于b
,用于降序排序。这些函数符合
CompareFunc
的定义,可以作为函数指针传递。
3. 自定义排序函数
void customSort(vector<int>& vec, CompareFunc comp)
{ sort(vec.begin(), vec.end(), comp); }
定义了一个通用的排序函数
customSort
,接受以下参数:
vec
: 需要排序的整数向量。
comp
: 比较函数的函数指针。使用 C++ 标准库中的
std::sort
函数进行排序,并将用户提供的比较函数comp
作为第三个参数传递给std::sort
。通过这种方式,
customSort
可以根据不同的比较函数实现升序或降序排序。
4. 主函数调用
int main()
{ vector<int> nums = {3, 1, 4, 1, 5};
customSort(nums, ascending); // 升序排序
customSort(nums, descending); // 降序排序
return 0; }
创建了一个包含整数的向量
nums
。调用
customSort
函数两次:
第一次传递
ascending
比较函数,实现升序排序。第二次传递
descending
比较函数,实现降序排序。
输出结果
假设在每次排序后输出向量内容,程序的输出可能如下:
升序排序后: 1 1 3 4 5
降序排序后: 5 4 3 1 1
10、小结
- 函数指针提供动态函数调用能力,是C/C++灵活性的重要体现。
- 语法复杂但功能强大,需注意类型匹配和空指针问题。
- 现代C++中可结合
std::function
提升灵活性和安全性。 - 成员函数指针需结合对象实例使用,适用于面向对象设计模式。