C++ Primer Plus(第6版)读书笔记【3】

第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:

  1. 头文件: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_
  1. 函数实现文件: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";
}
  1. 主程序文件: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); // 这个函数不会被改名字
}

正文完
 0
评论(没有评论)