第7章 函数,C++的编程模块
- 要提高编程效率,可更深入地学习 STL 和 Boost,先用好工业级标准库,再谈自主研发.
- 数组/字符串/结构的处理:这是函数参数传递的必经之路。理解它们如何传参,本质上就是在学习“值传递”与“指针/引用传递”的内存模型开销。
- 递归(Recursion):培养算法思维的基石。在现代开发中(如处理树状结构、JSON 解析、编译期元编程),递归依然无处不在。
- 函数指针(Function Pointer):这是 C 语言回调机制的巅峰。虽然现代 C++ 更多使用 std::function 和 Lambda 表达式,但函数指针是理解“代码也是一段内存地址”的关键底层纽带。
随着 C++ 标准的飞速演进(C++11/17/20/23),老一辈 Boost 库中许多优秀的功能(如智能指针、正则表达式、文件系统、协程)已经全都被正式吸纳进 C++ 标准库(STL)中了。现代开发者应优先将 STL 学透,再在需要尖端、特定功能(如高级网络库 Asio)时去求助 Boost。
创建自己的函数时,必须自行处理这3个方面一-定义、提供原型和调用。
#include <iostream>
// ==========================================
// 1. 提供原型 (Prototype) - 告诉编译器函数的存在
// ==========================================
double calculate_cube(double side);
int main() {
double box_side = 10.0;
// ==========================================
// 2. 调用 (Calling) - 传入实参并获取返回值
// ==========================================
double volume = calculate_cube(box_side);
std::cout << "边长为 " << box_side << " 的立方体体积为: " << volume << std::endl;
return 0;
}
// ==========================================
// 3. 定义 (Definition) - 函数的具体实现代码
// ==========================================
double calculate_cube(double side) {
// side 是形式参数,用来接收调用时传进来的值
return side * side * side;
}
//边长为 10 的立方体体积为: 1000
函数原型:
- 函数原型是一条语句,因此必须加分号
- 函数原型不需要变量名,有类型列表就足够了,但也可以包括变量名
函数与数组
在 C++ 中,当数组作为函数参数传递时,它会自动退化为指向其第一个元素的指针。因此,将数组传递给函数时,通常需要同时传递数组的长度,以便函数知道何时停止遍历。
#include <iostream>
// 1. 打印数组的函数
// 参数 arr[] 表面上是数组,实际编译器会将其视为 int* arr(指针)
void printArray(const int arr[], int size) {
for (int i = 0; i < size; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
}
// 2. 修改数组元素的函数
// 因为传递的是地址,函数内部的修改会改变主函数中的原数组
void doubleElements(int arr[], int size) {
for (int i = 0; i < size; ++i) {
arr[i] *= 2; // 将每个元素乘以 2
}
}
int main() {
int myArray[] = {1, 2, 3, 4, 5};
// 计算数组长度:总字节数 / 单个元素字节数
int size = sizeof(myArray) / sizeof(myArray[0]);
std::cout << "原数组内容: ";
printArray(myArray, size);
// 数组名 myArray 实际就是指向数组首元素的指针
doubleElements(myArray, size);
std::cout << "修改后的数组: ";
printArray(myArray, size);
return 0;
}
/*
原数组内容: 1 2 3 4 5
修改后的数组: 2 4 6 8 10
*/
强烈建议避免使用传统 C 风格数组,因为它们容易引发内存越界等 Bug。改用标准库容器可以自带长度信息,且传递更安全。
替代方案:动态长度的 std::vector, 适合长度会发生改变的数组,是现代 C++ 中最常用的数组替代品。
#include <iostream>
#include <vector> // 必须引入该头文件
// 通过引用传递,允许在函数内部修改 vector 的内容
void modifyVector(std::vector<int>& vec) {
vec.push_back(60); // 在末尾追加一个新元素
}
int main() {
std::vector<int> myVector = {10, 20, 30};
modifyVector(myVector);
std::cout << "vector 修改后的内容: ";
for (int val : myVector) {
std::cout << val << " ";
}
std::cout << "\n当前 vector 大小: " << myVector.size() << std::endl;
return 0;
}
//vector 修改后的内容: 10 20 30 60
//当前 vector 大小: 4
函数与结构
在 C++ 中,结构体(Struct)作为自定义数据类型,可以作为函数的参数或返回值。与数组不同,结构体作为参数传递时,默认是值传递(会复制整个结构体)。
为了提高效率,在现代 C++ 开发中,通常使用引用传递(Reference)。
#include <iostream>
#include <string>
// 定义一个结构体
struct Player {
std::string name;
int level;
double health;
};
// 场景 1:通过 const 引用传递(推荐)
// 作用:只读访问。加 & 避免内存复制提高效率,加 const 防止函数内部修改数据
void printPlayerInfo(const Player& p) {
std::cout << "玩家: " << p.name
<< " | 等级: " << p.level
<< " | 血量: " << p.health << std::endl;
}
// 场景 2:通过引用传递(修改数据)
// 作用:在函数内部直接修改主函数中的结构体变量,不加 const 可以修改数据
void levelUp(Player& p) {
p.level += 1; // 等级加 1
p.health += 50.0; // 血量上限增加 50
std::cout << p.name << " 升级了!" << std::endl;
}
// 场景 3:结构体作为函数返回值
// 作用:当函数需要同时返回多个不同类型的数据时非常有用
Player createPlayer(std::string name) {
Player newPlayer;
newPlayer.name = name;
newPlayer.level = 1; // 初始等级
newPlayer.health = 100.0; // 初始血量
return newPlayer; // 返回整个结构体
}
int main() {
// 1. 调用函数创建结构体
Player p1 = createPlayer("小明");
std::cout << "--- 初始状态 ---" << std::endl;
printPlayerInfo(p1);
// 2. 调用函数修改结构体
std::cout << "\n--- 触发升级 ---" << std::endl;
levelUp(p1);
// 3. 再次打印查看修改结果
std::cout << "\n--- 升级后状态 ---" << std::endl;
printPlayerInfo(p1);
return 0;
}
/*
--- 初始状态 ---
玩家: 小明 | 等级: 1 | 血量: 100
--- 触发升级 ---
小明 升级了!
--- 升级后状态 ---
玩家: 小明 | 等级: 2 | 血量: 150
*/
- 值传递 vs 引用传递
- 若直接写 void func(Player p),系统会复制一份新的数据。如果结构体很大(包含大量数据或数组),会严重消耗内存和时间。
- 现代 C++ 规范要求:除非常小的结构体,否则一律使用 const Player&(只读)或 Player&(可写)。
- 返回多个值:C++ 函数原生只能返回一个变量。如果你的函数需要同时返回一个 int、一个 double 和一个 string,将它们打包进一个结构体中返回是最优解。
函数和string对象
在 C++ 中,std::string 是一个标准库提供的类(Class)对象,而不是传统的 C 风格字符数组(char[])。因为 std::string 内部会自动管理内存,它作为函数参数或返回值时非常安全、灵活。与结构体类似,为了避免复制整串字符串带来的性能开销,现代 C++ 在传递 string 时有着一套标准的最佳实践。以下是完整的代码示例(Demo),展示了 std::string 在函数中的四种核心用法
#include <iostream>
#include <string> // 必须引入该头文件
// 场景 1:值传递(不推荐,除非需要副本)
// 缺点:会复制整个字符串,如果字符串很长,会浪费内存和时间
void printByValue(std::string str) {
std::cout << "值传递: " << str << std::endl;
}
// 场景 2:const 引用传递(现代 C++ 强烈推荐的标准写法)
// 优点:没有复制开销(高效),且 const 保证了函数内部无法修改原字符串(安全)
void printByConstReference(const std::string& str) {
std::cout << "const 引用传递: " << str << std::endl;
}
// 场景 3:普通引用传递(用于在函数内部修改字符串)
// 作用:函数内部的修改会直接影响主函数中的原字符串
void encryptString(std::string& str) {
for (char& c : str) {
c += 1; // 简单的加密:将每个字符的 ASCII 码加 1
}
}
// 场景 4:string 作为函数返回值
// 现代 C++ 编译器有 RVO(返回值优化),直接返回 string 效率很高,不会发生多余复制
std::string getWelcomeMessage(const std::string& userName) {
std::string message = "欢迎回来, " + userName + "!";
return message;
}
int main() {
std::string text = "Hello你好";
std::cout << "--- 1. 参数传递展示 ---" << std::endl;
// 推荐使用 const 引用传递
printByConstReference(text);
std::cout << "\n--- 2. 函数内修改字符串 ---" << std::endl;
std::cout << "加密前: " << text << std::endl;
encryptString(text); // 传递引用
std::cout << "加密后: " << text << std::endl;
std::cout << "\n--- 3. 字符串作为返回值 ---" << std::endl;
std::string greeting = getWelcomeMessage("Alice");
std::cout << greeting << std::endl;
return 0;
}
/*
--- 1. 参数传递展示 ---
const 引用传递: Hello你好
--- 2. 函数内修改字符串 ---
加密前: Hello你好
加密后: Ifmmp御榾
--- 3. 字符串作为返回值 ---
欢迎回来, Alice!
*/
函数和array对象
与传统数组相比,std::array 作为函数参数时不会自动退化为指针,它是一个完整的对象。这意味着:
- 它自带长度信息,函数内部可以直接调用 .size(),不再需要额外传递大小参数。
- 默认是值传递(会复制整个数组),因此为了效率,通常推荐使用引用传递。
#include <iostream>
#include <array> // 必须引入该头文件
#include <string>
// 场景 1:const 引用传递(最推荐的只读方式)
// 模板参数必须包含:<数据类型, 数组大小>。大小必须在编译时确定
void printArray(const std::array<int, 5>& arr) {
std::cout << "当前数组 (长度 " << arr.size() << "): ";
// 可以直接使用基于范围的 for 循环
for (int val : arr) {
std::cout << val << " ";
}
std::cout << std::endl;
}
// 场景 2:普通引用传递(用于修改原数组)
void doubleElements(std::array<int, 5>& arr) {
for (int& val : arr) {
val *= 2; // 注意这里用 int& 才能修改元素本身
}
}
// 场景 3:结构体或对象数组作为参数
struct Point { int x, y; };
void printPoints(const std::array<Point, 3>& points) {
for (const auto& p : points) {
std::cout << "(" << p.x << ", " << p.y << ") ";
}
std::cout << std::endl;
}
int main() {
// 1. 初始化一个内含 5 个整数的 std::array
std::array<int, 5> numbers = {1, 2, 3, 4, 5};
std::cout << "--- 1. 只读传递 ---" << std::endl;
printArray(numbers);
std::cout << "\n--- 2. 修改数组元素 ---" << std::endl;
doubleElements(numbers);
printArray(numbers); // 再次打印查看修改结果
std::cout << "\n--- 3. 复杂对象数组传递 ---" << std::endl;
std::array<Point, 3> path = { Point{0, 0}, Point{1, 2}, Point{3, 4} };
printPoints(path);
return 0;
}
/*
--- 1. 只读传递 ---
当前数组 (长度 5): 1 2 3 4 5
--- 2. 修改数组元素 ---
当前数组 (长度 5): 2 4 6 8 10
--- 3. 复杂对象数组传递 ---
(0, 0) (1, 2) (3, 4)
*/
递归
函数可以调用自己,但c++中不允许main函数调用自己。
斐波那契数列:1, 1, 2, 3, 5, 8, 13…规律:从第三项开始,每一项等于前两项之和(F(n) = F(n-1) + F(n-2))。
#include "ayzw.top/init.h"
int64 f(int64 n) {
if (n > 2) {
return f(n - 1) + f(n - 2);
}
return 1;
}
int main() {
int position = 10;
for (int p = 1; p <= position; p++) {
std::cout << "斐波那契数列第 " << p << " 项是: " << f(p) << std::endl;
}
return 0;
}
/*
斐波那契数列第 1 项是: 1
斐波那契数列第 2 项是: 1
斐波那契数列第 3 项是: 2
斐波那契数列第 4 项是: 3
斐波那契数列第 5 项是: 5
斐波那契数列第 6 项是: 8
斐波那契数列第 7 项是: 13
斐波那契数列第 8 项是: 21
斐波那契数列第 9 项是: 34
斐波那契数列第 10 项是: 55
*/
函数指针
函数指针(Function Pointer)是指向函数代码起始内存地址的指针变量。通过函数指针,可以把函数像普通变量一样作为参数传递给另一个函数,或者实现动态调用,这是 C++ 实现多态和回调函数(Callback)的基础。本质上,函数指针存放的是代码段中函数的入口地址。
#include <iostream>
// 定义两个简单的函数
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }
// 场景 2:将函数指针作为参数传递(回调函数)
// 这里的 int (*op)(int, int) 就是一个函数指针参数
void calculateAndPrint(int x, int y, int (*op)(int, int)) {
// 通过指针调用函数
int result = op(x, y);
std::cout << "计算结果为: " << result << std::endl;
}
int main() {
// 场景 1:基础语法
// 声明一个函数指针 pFunc,指向一个“接收两个 int 并返回 int”的函数
// 语法:返回值类型 (*指针变量名)(参数类型列表)
int (*pFunc)(int, int) = nullptr;
// 让指针指向 add 函数(函数名本身就是函数的地址)
pFunc = add;
// 调用方式 A:现代 C++ 推荐,直接像普通函数一样调用
std::cout << "指针调用 add: " << pFunc(5, 3) << std::endl;
pFunc = multiply;
// 调用方式 B:传统 C 风格,显式解引用(效果相同)
std::cout << "指针调用 multiply: " << (*pFunc)(5, 3) << std::endl;
std::cout << "\n--- 场景 2:作为函数参数 ---" << std::endl;
// 传入 add 函数作为回调
calculateAndPrint(10, 5, add);
// 传入 multiply 函数作为回调
calculateAndPrint(10, 5, multiply);
return 0;
}
/*
指针调用 add: 8
指针调用 multiply: 15
--- 场景 2:作为函数参数 ---
计算结果为: 15
计算结果为: 50
*/
第8章 函数探幽
内联函数
在 C++ 中,内联函数(Inline Function)是一种以空间换时间的优化机制。当你在函数声明或定义前加上 inline 关键字时,编译器会尝试在每个调用该函数的地方,把整个函数体直接展开(复制)到调用点,而不是像普通函数那样进行跳转、压栈、弹栈等操作。
#include <iostream>
// 在函数名前面加上 inline 关键字
// 适用场景:代码行数极少(通常 1-5 行)、调用极其频繁的函数
inline int square(int x) {
return x * x;
}
// 现代 C++ 规范:普通的类成员函数如果在类内部直接定义,会自动隐式转换为内联函数
class Rectangle {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
// 隐式内联:因为是在类体内部直接实现的
double getArea() const {
return width * height;
}
};
int main() {
int val = 5;
// 编译器在编译时,可能会将下面这行代码:
// int result = square(val);
// 直接替换并展开为:
int result = val * val;
std::cout << val << " 的平方是: " << result << std::endl;
Rectangle rect(10.0, 5.0);
// 同样,这里的 getArea() 也大概率会被直接展开为 10.0 * 5.0
std::cout << "矩形面积: " << rect.getArea() << std::endl;
return 0;
}
//5 的平方是: 25
//矩形面积: 50
引用变量
在 C++ 中,引用(Reference)是一个非常重要且高效的概念。简单来说,引用就是给一个已经存在的变量起一个“别名”(Alias)。引用变量在内存中不会开辟新的空间,它和被引用的原变量共享同一块内存地址。对引用的任何操作,实际上都是直接作用于原变量。
#include <iostream>
#include <string>
// 场景 2:引用作为函数参数(最常用的场景)
// 优点:没有复制开销,且函数内部的修改会直接影响外部的原变量
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
// 场景 3:const 引用(只读别名)
// 既保证了没有复制开销(高效),又保证了函数内不能修改它(安全)
void printMessage(const std::string& msg) {
// msg = "Changed"; // 错误!编译不通过,因为有 const 保护
std::cout << "消息内容: " << msg << std::endl;
}
int main() {
// 场景 1:基础语法
int original = 10;
int& ref = original; // ref 是 original 的引用(别名)
std::cout << "--- 1. 基础特性 ---" << std::endl;
std::cout << "原变量值: " << original << ", 引用值: " << ref << std::endl;
// 修改引用,原变量也会改变
ref = 20;
std::cout << "修改引用后 -> 原变量值: " << original << std::endl;
// 它们的内存地址完全相同
std::cout << "原变量地址: " << &original << std::endl;
std::cout << "引用变量地址: " << &ref << std::endl;
std::cout << "\n--- 2. 函数参数中的应用 ---" << std::endl;
int x = 5, y = 99;
std::cout << "交换前: x = " << x << ", y = " << y << std::endl;
swap(x, y); // 直接传入变量名即可,不需要像指针那样加 & 取地址
std::cout << "交换后: x = " << x << ", y = " << y << std::endl;
std::cout << "\n--- 3. const 引用的应用 ---" << std::endl;
std::string text = "Hello C++";
printMessage(text);
return 0;
}
/*
--- 1. 基础特性 ---
原变量值: 10, 引用值: 10
修改引用后 -> 原变量值: 20
原变量地址: 0x7eaedff768
引用变量地址: 0x7eaedff768
--- 2. 函数参数中的应用 ---
交换前: x = 5, y = 99
交换后: x = 99, y = 5
--- 3. const 引用的应用 ---
消息内容: Hello C++
*/
引用的三大钢性规则:
- 必须在声明时初始化:不能写
int& ref;。因为它是别名,必须在它出生的那一刻告诉编译器它是谁的别名。 - 不能有空引用:引用必须绑定合法的内存对象,不存在类似指针的 nullptr(空引用是非法的)。
- 不能更换“忠诚度”:一旦一个引用指向了某个变量,它这辈子就再也不能改为其他变量的引用了。
int a = 10;
int b = 20;
int& ref = a; // ref 是 a 的别名
ref = b; // 警告:这【不是】让 ref 变成 b 的别名,而是把 b 的值(20)赋值给 a!
引用(Reference) vs 指针(Pointer):
| 特性 | 引用 (&) | 指针 (*) |
|---|---|---|
| 本质 | 变量的别名,不占独立内存空间(逻辑上) | 一个独立的变量,里面存放的是内存地址 |
| 初始化 | 必须在声明时初始化 | 可以先声明,后面再赋值 |
| 可变性 | 一旦初始化,终生不能更改指向 | 可以随时指向其他的内存地址 |
| 空值 | 绝对不能为空(安全) | 可以为 nullptr(不安全,容易引发空指针崩溃) |
| 访问语法 | 直接使用变量名,如 ref = 5; | 需要用 * 解引用,如 *ptr = 5; |
在现代 C++ 开发中,我们遵循 “能用引用,绝不用指针” 的原则。因为引用不需要处理繁琐的指针解引用(*),也不用担心空指针引发的程序闪退,语法更干净,安全性也更高。
函数模板
在 C++ 中,函数模板(Function Template)是实现泛型编程(Generic Programming)的核心机制。它可以让你编写一个与数据类型无关的通用函数。在编译时,编译器会根据你传入的实际参数类型,自动生成对应类型的函数代码。这极大地减少了代码重复(免去了为 int、double、string 重复编写相同逻辑函数的麻烦)。
第9章 内存模型和名称空间
头文件内容
- 结构体与类声明:提供代码结构。
- 函数与类模板:编译器需要看到完整代码来生成实例。
- 内联函数:带有 inline 关键字,允许多次定义。
- 符号常量:使用 const 或 constexpr 修饰的变量。
- 静态成员变量声明:真正的初始化在源文件中。
头文件中严禁包含的内容:
- 普通函数体:会导致链接时报“重复定义”错误。
- 非 const 全局变量:会导致“符号重定义”错误。
- 不要使用#include 来包含源代码文件,这样做将导致多重声明。
头文件使用双引号还是尖括号,决定了编译器去哪里寻找你的头文件。核心区别:
#include "coordin.h"(双引号):编译器先在当前工作目录或源代码目录找。适合你自己写的、项目内部的头文件。如果找不到,才会去系统标准库目录找。#include <iostream>(尖括号)编译器直接去存储标准头文件的系统目录找。适合 C++ 自带的标准库,或者第三方安装的库。
文件组织demo:
- 头文件:coordin.h这个文件只给出定义和声明,不写具体的函数干了什么。
// 【第1步】检查宏 COORDIN_H_ 是否没有被定义过
// 如果是第一次包含此文件,条件成立,编译器会往下读取代码
// 如果是第二次包含,条件不成立,编译器会直接跳到最后的 #endif
#ifndef COORDIN_H_
// 【第2步】立刻定义这个宏
// 这样当后面再次遇到 #ifndef COORDIN_H_ 时,条件就会失败
#define COORDIN_H_
// --- 这里放置头文件的正文内容 ---
// 1. 结构体定义
struct Polar {
double distance; // 距离
double angle; // 角度
};
struct Rect {
double x; // 横坐标
double y; // 纵坐标
};
// 2. 函数原型(声明)
Polar rect_to_polar(Rect rect_pos);
void show_polar(Polar dapos);
// --- 正文内容结束 ---
// 【第3步】结束整个 #ifndef 的范围
// 这里的注释通常写上宏的名字,方便一眼看出这个结尾对应的是哪个开头
#endif // COORDIN_H_
- 函数实现文件:coordin.cpp这个文件负责写出函数的具体功能。注意它使用了双引号来包含刚才的头文件。
#include <iostream>
#include <cmath>
#include "coordin.h" // 包含自定义头文件
// 直角坐标转极坐标
Polar rect_to_polar(Rect rect_pos) {
Polar result;
result.distance = std::sqrt(rect_pos.x * rect_pos.x + rect_pos.y * rect_pos.y);
result.angle = std::atan2(rect_pos.y, rect_pos.x);
return result;
}
// 显示极坐标结果
void show_polar(Polar dapos) {
std::cout << "距离: " << dapos.distance;
std::cout << ", 角度: " << dapos.angle << " 弧度\n";
}
- 主程序文件:main.cpp这是程序的入口,它同样需要包含 coordin.h 来认识 Rect 结构体和相关函数。
#include <iostream>
#include <cmath>
#include "coordin.h" // 包含自定义头文件
// 直角坐标转极坐标
Polar rect_to_polar(Rect rect_pos) {
Polar result;
result.distance = std::sqrt(rect_pos.x * rect_pos.x + rect_pos.y * rect_pos.y);
result.angle = std::atan2(rect_pos.y, rect_pos.x);
return result;
}
// 显示极坐标结果
void show_polar(Polar dapos) {
std::cout << "距离: " << dapos.distance;
std::cout << ", 角度: " << dapos.angle << " 弧度\n";
}
//请输入直角坐标的 X 和 Y 值: 10 10
//距离: 14.1421, 角度: 0.785398 弧度
二进制兼容性问题(ABI 兼容性)
简单来说,不同的编译器在给函数改名字时,制定的“暗号”不一样。C++ 支持函数重载(允许函数同名,但参数不同)。为了区分这些同名函数,编译器在编译时会偷偷把函数名改成一个复杂的长名字,这个过程叫名称修饰(Name Mangling)。
举个例子,假设有一个函数:void show(int x),编译器 A 可能会把它改成:_show_int,编译器 B 可能会把它改成:_xyz_show_i.
如果你的主程序用编译器 A 编译,它就会在打包(链接)时寻找 _show_int。但如果你的库是用编译器 B 编译的,库里面只有 _xyz_show_i。链接器找不到对不上的名字,就会直接报错:无法解析的外部符号(Link Error)。
解决这个问题的三种方法
- 使用同一个编译器:这是最简单的方法。确保项目里的所有 .cpp 文件和第三方库,都用完全一样的编译器(甚至版本也要相同)来编译。
- 用源代码重新编译:如果你能拿到第三方库的源码,用你现有的编译器重新编译一次。这样生成的二进制文件就拥有相同的“名字规则”。
- 使用 extern "C" 关键字:如果你必须使用别人用不同编译器做好的库,可以让函数用 C 语言 的规则来命名。C 语言不支持重载,所以不会修改函数名。在头文件中这样写,就能跨编译器连接:
extern "C" {
void show(int x); // 这个函数不会被改名字
}