分类: C++

  • #C++ Day35 Basic Data Structure Chapter2  November 20 2025

    #include <iostream>

    using namespace std;

    #define eleType int

    struct SequentialList {//定义顺序表结构

    eleType* elements;

    int size; //顺序表当前的大小,元素的个数

    int capacity; //顺序表的容量,就是数组的大小 

    };

    void intializeList(SequentialList* list, int capacity) {//初始化顺序表

    list->elements = new eleType[capacity];

    list->size = 0;

    list->capacity = capacity;

    }

    void destroyList(SequentialList* list) {//删除整个顺序表

    delete[] list->elements;

    }

    int main() {

    return 0;

    }

    这是一条 C 或 C++ 语言中的预处理指令 (Preprocessor Directive)。

    简单来说,它的作用是告诉编译器在编译代码之前,把程序中出现的所有 eleType 文本直接替换成 int。这样做的好处是灵活性:如果你在写一个数据结构(比如栈或队列),现在用它存整数 (int),但以后想改成存小数 (float),你只需要把这一行改成 #define eleType float,而不需要去修改代码里的每一个变量声明。

    为了帮你彻底搞懂这个概念以及它的最佳实践,我制定了一个简单的学习计划:

    学习计划概览:

     * 深入理解 #define 的机制(它是如何进行“复制粘贴”工作的?)

     * 为什么要给类型起别名(为什么不直接写 int?eleType 代表什么?)

     * 更好的替代方案(为什么现代程序员更喜欢用 typedef 或 using?)

    这不仅能帮你读懂这行代码,还能让你写出更专业的 C/C++ 代码。我们先从第一步:#define 到底是怎么工作的开始,好吗?

    太棒了!让我们开始第一步。

    1. 深入理解 #define:它只是个“搜索替换”工具

    要理解 #define,你得先认识 C/C++ 编译过程中的一位幕后英雄——预处理器 (Preprocessor)。

    在编译器(Compiler)真正开始把你的代码翻译成机器语言之前,预处理器会先扫一遍你的代码。当它看到以 # 开头的指令(比如 #define 或 #include)时,它会立刻执行操作。

    对于 #define A B,它的逻辑非常简单粗暴:在源码里找到所有的 “A”,把它们全部替换成 “B”。

    这就好比你在 Word 文档里用了“查找和替换”功能。预处理器完全不懂 C 语言的语法(它不知道什么是变量、什么是函数),它只管文本替换。

    👀 视觉演示:替换前后

    想象一下,这是你写的代码(源文件):

    // 你的代码 (Source Code)

    #define eleType int

    void main() {

        eleType a = 10;

        eleType b = 20;

    }

    在预处理器工作完之后,编译器真正看到的“干净”代码其实是这样的(预处理后):

    // 编译器眼中的代码

    // 注意:#define 那一行已经消失了

    void main() {

        int a = 10;   // eleType 被替换成了 int

        int b = 20;   // eleType 被替换成了 int

    }

    有趣的小知识: 就像上面说的,预处理器其实有点“傻”。如果你写 #define TRUE FALSE,它真的会乖乖地把代码里所有的 TRUE 都改成 FALSE,进而导致你的程序逻辑完全颠倒,而且编译器还不会报错!这就是所谓的“宏定义陷阱”。

    🧠 互动小测验

    为了确认你掌握了这个“简单替换”的概念,请看下面这段代码。

    假设我们有这行指令:

    #define PI 3.14

    下面是你的代码:

    float area = PI * r * r;

    print(“The value of PI is important.”);

    请问:预处理器处理完之后,第二行代码里的字符串 “The value of PI is important.” 会变吗?为什么?

    回答得非常棒!💯

    完全正确。预处理器虽然是在做“查找替换”,但它也遵守基本的规则:它不会去动双引号 “” 里面的内容。所以字符串里的 PI 还是 PI,而代码逻辑里的 PI 会变成 3.14。你已经抓住了预处理的核心逻辑!

    1.2 宏定义的常见用途:不仅仅是定义类型

    虽然你最初问的是 #define eleType int(定义类型),但在 C 语言的世界里,#define 其实更多时候是被用来定义常量(Constants)。

    试想一下,你在写一个程序处理学生的成绩,全校这学期最多招收 100 个学生。你在代码里到处写 100:

    int scores[100];

    for(int i=0; i<100; i++) { … }

    if (student_id > 100) { … }

    这种直接写在代码里的数字,程序员给它取了个很形象的名字,叫**“魔术数字” (Magic Numbers)** —— 就像魔术一样突然冒出来的数字,别人读代码时会一脸懵:“为什么是 100?不是 99?这代表什么?”

    如果你用 #define,代码就会清晰很多:

    #define MAX_STUDENTS 100  // 一目了然

    int scores[MAX_STUDENTS];

    for(int i=0; i<MAX_STUDENTS; i++) { … }

    这样做有两个巨大的好处:

     * 可读性:看到 MAX_STUDENTS 就知道意思是“最大学生数”。

     * 易修改:明年扩招变成 200 人了,你只需要改一行代码,不用满世界去找那个 100。

    🕵️‍♂️ 侦探小练习

    我们要结束第一步了,来个小测试巩固一下。

    除了定义“类型”(像 eleType)和“常量数值”(像 100),#define 其实还可以定义“看起来像函数的东西”(虽然我们还没细讲,但你可以猜猜看)。

    请看下面这个宏定义:

    #define SQUARE(x) x * x

    如果我在代码里写 SQUARE(5),预处理器会把它变成什么?

    A. 25 (直接算出结果)

    B. 5 * 5 (进行文本替换)

    C. int square(int x) { return x * x; } (生成一个函数)

    完全正确!答案就是 B。👏

    这就是关键所在:预处理器只是个“搬运工”,它不负责计算,只负责把 x 换成 5,所以代码变成了 5 * 5。虽然结果和 A 一样,但过程截然不同。(这也是它的一个隐患,比如传入 1+1 时,会变成 1+1 * 1+1 = 3,而不是我们期望的 4,不过这是进阶话题了,你只要记得它是纯文本替换就好)。

    2. 为什么要给类型起别名?(Type Aliasing)

    现在回到你最初的代码:#define eleType int。

    你可能会问:“直接写 int 不香吗?为什么要多此一举造个 eleType?”

    2.1 解密命名:eleType 是什么?

    通常,当你看到 eleType,它是 Element Type(元素类型)的缩写。

    这行代码极大概率出现在数据结构(比如栈 Stack、队列 Queue、链表 List)的实现代码中。

    想象一下,你要写一个栈 (Stack)。

    如果你的栈是用来存整数的,你可能会这样写结构体:

    struct Stack {

        int data[100];  // 数组存 int

        int top;

    };

    void push(struct Stack* s, int value); // 压入 int

    int pop(struct Stack* s);              // 弹出 int

    这没问题。但是,如果下周你的老师或者老板说:“嘿,我们现在需要这个栈来存小数 (float),或者字符 (char)。”

    如果你直接写死了 int,那你就要去修改结构体里的定义、修改 push 函数的参数、修改 pop 函数的返回值……你需要改好几个地方,甚至几十个地方。漏改一个,程序就报错。

    🌟 这里的魔法在于“抽象”

    如果我们用 eleType 作为中间人:

    #define eleType int  // <— 控制中心

    struct Stack {

        eleType data[100];  // 这里的类型听指挥

        int top;

    };

    void push(struct Stack* s, eleType value); // 这里的类型听指挥

    eleType pop(struct Stack* s);              // 这里的类型听指挥

    现在,如果要从存整数改成存小数,你只需要做一件事:

    把第一行改成 #define eleType float。

    砰! 整个栈的代码瞬间变成了存储 float 的栈。

    这就是可维护性 (Maintainability)。

    🔄 角色扮演小任务

    为了让你体验这种“一键切换”的快感,我们来模拟一下。

    假设你是一个游戏开发者。你正在写一个背包系统 Bag,用来存放道具 ID。目前道具 ID 是简单的数字(int)。

    代码大概是这样:

    #define ItemID int 

    struct Bag {

        ItemID items[50];

    };

    突然,策划跑来跟你说:“不行不行,道具 ID 不够用了,我们要换成超大数字(long long)才能存得下!”

    请问: 为了满足策划的需求,你需要对上面的代码做哪些具体的修改?

    干得漂亮!🎉

    你看,你只修改了这一行代码,就完成了整个游戏背包系统的升级。这就是程序员追求的“高内聚,低耦合”——改动越少,出 bug 的概率就越低。

    除此之外,这还有助于可移植性 (Portability)。比如有些微型电脑(单片机)的 int 只有 16 位,而你现在的电脑 int 是 32 位。如果使用了类型别名,当你把代码移植到新机器时,只需要修改那个 #define 就能适配硬件了。

    3. 更好的替代方案:typedef 和 using

    虽然 #define 很有用,但在定义类型这件事上,它其实有点“粗暴”。因为它只是简单的文本替换,有时候会带来意想不到的问题,而且编译器在报错时可能只会显示 int 而不是你的别名 eleType,让调试变难。

    所以,在 C 和 C++ 中,我们更推荐使用专门的工具。

    3.1 C 语言的老牌工具:typedef

    typedef 是 “Type Definition” 的缩写。它的作用是给现有的类型起一个“外号”。

    语法对比:

    | 方式 | 代码 | 谁在处理? | 特点 |

    |—|—|—|—|

    | 宏定义 | #define eleType int | 预处理器 | 简单替换,不需要分号 ; |

    | 类型定义 | typedef int eleType; | 编译器 | 真正的类型别名,需要分号 ; |

    注意语法的区别:

     * #define 是:旧名字 新名字 (中间没分号)

     * typedef 是:typedef 旧名字 新名字; (像定义变量一样)

    例子:

    typedef int eleType; // 告诉编译器:eleType 就是 int 的别名

    struct Stack {

        eleType data[100]; // 用法完全一样

    };

    这样做的好处是,编译器明确知道 eleType 是一个类型,它能帮你做更好的类型检查。

    3.2 现代 C++ 的新宠:using (强烈推荐)

    如果你是在写 C++ (尤其是 C++11 标准之后),有一个更直观、更现代的写法,那就是 using 关键字。

    它的可读性最强,读起来就像赋值一样:

    using eleType = int;  // 意思是:让 eleType 等于 int

    这和 typedef int eleType; 的功能完全一样,但写起来更符合人类直觉(名字在左边,类型在右边)。

    📝 巩固练习:代码“翻译”

    为了确保你掌握了这三种方式,我们来做一个简单的“翻译”练习。

    请把下面这行用 #define 写的代码,分别“翻译”成 typedef 写法和 C++ 的 using 写法。

    原始代码:

    #define AgeType float

    请写出:

     * typedef 写法: __________

     * using 写法: __________

    逻辑完全正确!不管是 typedef 的顺序还是 using 的赋值写法,你都搞对了。👏

    不过,有一个非常重要的小细节需要注意:分号 ;。

     * #define 是预处理指令,它不需要分号(如果有分号,分号也会被一起替换进代码里,导致出错)。

     * typedef 和 using 是 C/C++ 语句,它们必须以分号结尾。

    所以完美的写法是:

     * typedef float AgeType;

     * using AgeType = float;

    记住这个区别,你的代码就能一次编译通过了!

    🎓 总结:这一行代码背后的智慧

    我们要完成这次学习了。回到你最初的问题 define eleType int,我们学到了:

     * 它是谁:预处理指令,负责在编译前把 eleType 全部文本替换成 int。

     * 为什么用它:为了灵活性和可维护性。如果你想把存整数的栈改成存小数的栈,只需要改这一行,不用改几百行代码。

     * 谁更好:虽然 #define 能用,但在现代 C++ 编程中,我们更推荐使用 typedef 或 using,因为它们更安全,编译器更懂它们。

    你现在对这个概念应该非常有信心了吧?

    要不要来一个只有 3 道题的终极小测验,看看你是否已经完全掌握了这个知识点?

    太棒了!那我们就开始吧,这是第一题。

    问题 1:预处理器的陷阱

    假设我们在代码里写了这样一个宏定义:

    #define ADD(x) x + x

    然后我们在主函数里计算这个表达式:

    int result = ADD(5) * 2;

    请问,result 的值最后是多少?

    (提示:请把自己当成只做“复制粘贴”的预处理器,把 ADD(5) 替换进后面的式子里,然后再算数学题。)

    A. 20

    B. 15

    C. 10

    你做得非常棒!答案确实是 B. 15。

     * 预处理器替换:ADD(5) * 2 被替换成了 5 + 5 * 2。

     * 运算顺序:乘法 * 的优先级高于加法 +。所以是先算 5 \times 2 = 10。

     * 最终结果: 5 + 10 = 15。

    你不仅理解了 #define 是文本替换,还正确运用了数学运算的优先级规则。太厉害了!这也是为什么在写宏的时候,我们总是建议用括号把所有参数和整个宏体都括起来,以避免这种“陷阱”:#define ADD(x) ( (x) + (x) )。

    📝 问题 2:语法大不同

    接下来我们测试一下 #define 和其他替代方案的语法差异,这是我们刚刚纠正过的重点。

    在 C/C++ 代码中,下面哪一行类型别名定义的写法是不正确的(即会造成编译错误)?

    A. typedef float PriceType;

    B. using CountType = int;

    C. typedef int IDType

    太棒了!你的答案 C 完全正确!🎯

    typedef int IDType 缺少了语句末尾的分号,所以它会导致编译错误。你很清楚地掌握了 #define 和 typedef/using 在语法上的最大区别:前者不带分号,后者需要分号。

    📝 问题 3:最终考验:应用价值

    这是我们的最后一题,它将检验你对类型别名核心价值的理解。

    假设你的代码库中有 50 处使用了 typedef int IDType;。如果有一天,由于业务需求,你需要把 ID 类型从 int 换成 long long。你只需要修改一行代码即可完成切换。

    这种使用类型别名(而不是直接写 int)的方式,主要体现了代码的哪种优势?

    A. 可读性 (Readability):让代码看起来更漂亮。

    B. 可移植性 (Portability):方便代码在不同操作系统间运行。

    C. 可维护性 (Maintainability):方便在未来对代码进行大规模修改。

    你完全正确!答案是 C. 可维护性 (Maintainability)。👍

    这个场景的重点在于“大规模修改”:只改一行代码就能完成对整个系统的类型升级。这种简化未来维护工作的特性就是类型别名的最大价值。

    当然,A (可读性) 和 B (可移植性) 也是类型别名的优点,但它们是次要的:

     * 可读性:IDType 确实比 int 更清楚。

     * 可移植性:在不同系统上修改 #define 确实能帮助移植。

     * 可维护性:能够快速、准确、低风险地适应需求变化,这才是它在大型项目中不可替代的核心优势。

    🚀 学习回顾总结

    恭喜你!你已经完全掌握了 #define eleType int 这行代码背后的所有概念、用途和替代方案。

    学习成果总结:

    | 学习要点 | 你的掌握情况 |

    |—|—|

    | #define 机制 | 完美理解纯文本替换,并成功解决了宏定义的陷阱问题(Q1)。 |

    | 别名核心价值 | 明确了类型别名是为了提高代码的可维护性(Q3)。 |

    | 高级语法 | 掌握了 typedef 和 using 的语法,并能分辨它们与 #define 的关键区别(如分号的使用,Q2)。 |

    你在整个学习过程中的表现非常出色!三次测验都回答正确,并且在学习活动中也展示了对 #define 和类型别名概念的清晰理解。

    你现在不仅知道 #define eleType int 是什么,还知道为什么在现代编程中,我们更喜欢 using eleType = int;。

    你觉得我们今天是否成功帮助你达成了学习目标呢?我随时可以帮助你学习更多关于 C/C++、数据结构或其他学术话题!

    太棒了!听到你这么说我感到非常开心!😃

    很高兴能作为你的学习教练,帮助你掌握 C/C++ 中类型别名的核心知识。你今天的学习效率很高,对概念的理解非常透彻!

    如果你未来在编程、数学、科学或其他学术领域有任何新的学习目标或遇到难题,随时都可以来找我。祝你学习顺利,编码愉快!

    这行代码的核心作用是 在索引非法时,主动抛出「非法参数异常」,将错误信息传递给调用者,属于 C++ 中「异常处理机制」的关键用法。下面从「是什么、为什么用、怎么用规范、如何优化」四个维度详细拆解:

    一、核心解读:这行代码到底做了什么?

    cpp

    运行

    throw std::invalid_argument(“Invalid index”);

    • throw 关键字:中断当前函数执行,将后续的错误处理逻辑 “移交” 给调用者(必须在调用处用 try-catch 捕获,否则程序直接崩溃)。
    • std::invalid_argument:C++ 标准库提供的「异常类」(定义在 <stdexcept> 头文件中),专门用于表示「函数参数不合法」的场景(比如传入的索引、数值不符合函数要求)。
    • “Invalid index”:异常的「错误信息字符串」,用于描述具体错误原因,方便调用者定位问题。

    二、为什么要用异常,而不是返回错误码?

    在顺序表的 insert 函数中,索引非法是 “非常规错误”(正常调用时应避免),用异常比返回错误码更合适:

    • 错误处理与业务逻辑分离:无需在调用处每次都判断返回值(比如 if (insert(…) == -1) { 处理错误 }),代码更简洁。
    • 错误信息更丰富:可以携带字符串描述(如 “索引无效”),甚至自定义更多错误细节,比单纯的错误码(-1、0)更易读。
    • 强制调用者处理:若调用者未捕获异常,程序会直接终止并提示错误,避免 “错误被忽略” 导致后续逻辑异常(比如插入失败却继续访问非法位置)。

    三、使用规范:避免踩坑的 3 个关键

    1. 必须包含头文件(否则编译报错)

    std::invalid_argument 定义在 <stdexcept> 头文件中,必须在代码开头引入:

    cpp

    运行

    #include <stdexcept>  // 不可或缺!

    2. 调用者必须捕获异常(否则程序崩溃)

    异常抛出后,调用 insert 函数的代码必须用 try-catch 块捕获,否则程序会触发 terminate() 终止。

    规范调用示例:

    cpp

    运行

    int main() {

        SequentialList list;

        initList(&list);

        try {

            // 可能抛出异常的代码放在 try 块中

            insert(&list, 0, 10);  // 合法索引,正常执行

            insert(&list, 2, 20);  // 非法索引,抛出异常

        } catch (const std::invalid_argument& e) {  // 捕获特定异常类型

            // 处理异常:输出错误信息(e.what() 获取异常的字符串描述)

            std::cerr << “插入失败:” << e.what() << std::endl;

            // 可选:进行恢复操作(如释放资源、返回默认值等)

        }

        delete[] list.data;

        return 0;

    }

    • 输出结果:插入失败:Invalid index
    • 注意:catch 时建议用 const 引用(const std::invalid_argument& e),避免异常对象的拷贝开销。

    3. 异常类型要 “匹配场景”

    C++ 标准库提供了多种异常类,需根据错误类型选择,避免混用:

    异常类适用场景示例
    std::invalid_argument参数本身非法(如索引格式错误)索引为负数、传入空指针参数
    std::out_of_range参数超出合法范围(如索引越界)索引 > 顺序表 size、数组下标越界
    std::bad_alloc内存分配失败(如 new 开辟空间失败)new eleType[1000000000] 失败

    优化建议:你的场景更适合 std::out_of_range

    顺序表插入时,索引非法的本质是「索引超出了合法范围」(合法范围:0 ≤ index ≤ size),用 std::out_of_range 更贴合语义,可读性更强:

    cpp

    运行

    // 优化后:异常类型更精准

    throw std::out_of_range(“Invalid index for insert: index out of valid range”);

    四、进阶优化:让异常信息更 “有用”

    原错误信息 “Invalid index” 过于简洁,调用者无法知道「具体哪个索引非法」「合法范围是什么」。可以补充上下文信息,方便调试:

    cpp

    运行

    // 优化:携带具体索引值和合法范围

    throw std::out_of_range(

        “Insert failed: invalid index = ” + std::to_string(index) + 

        “, valid range is [0, ” + std::to_string(list->size) + “]”

    );

    • 错误信息示例:Insert failed: invalid index = 2, valid range is [0, 1]
    • 注意:std::to_string 用于将整数(index、list->size)转为字符串,需包含 <string> 头文件。

    五、特殊情况:如果是 C 语言(无异常机制)

    若你的代码实际是 C 语言(而非 C++),throw 和 std::invalid_argument 均不支持,此时需用「返回错误码 + 错误信息」替代:

    c

    运行

    #include <stdio.h>

    #define eleType int

    // 顺序表结构体(C 语言版本)

    typedef struct {

        eleType* data;

        int size;

        int capacity;

    } SequentialList;

    // 返回值:0 成功,-1 索引非法(C 语言无异常,用错误码表示)

    int insert(SequentialList* list, int index, eleType element) {

        if (index < 0 || index > list->size) {

            // 打印错误信息到标准错误流(stderr)

            fprintf(stderr, “Insert failed: invalid index = %d, valid range is [0, %d]\n”, 

                    index, list->size);

            return -1;  // 返回错误码

        }

        // 后续扩容、元素后移等逻辑…

        return 0;

    }

    // 调用示例

    int main() {

        SequentialList list = {NULL, 0, 0};

        if (insert(&list, 2, 20) == -1) {

            // 处理错误(如终止程序、重试等)

            return 1;

        }

        return 0;

    }

    总结

    • 原代码 throw std::invalid_argument(“Invalid index”); 是 C++ 中「参数非法错误」的标准处理方式,核心是传递错误信息并强制调用者处理。
    • 规范用法:必须包含 <stdexcept> 头文件,调用者用 try-catch 捕获。
    • 优化方向:用 std::out_of_range 匹配索引越界场景,补充具体索引和合法范围让错误信息更清晰。
    • 兼容 C 语言:无异常机制时,用「返回错误码 + fprintf(stderr, …)」替代。

    如果需要进一步优化异常处理(比如自定义异常类、统一错误处理机制),可以随时告诉我!

    //Basic Data Structure Day2 Sequential List – Coding

    //顺序表

    #include <iostream>

    using namespace std;

    #define eleType int

    //eleType 提升代码可维护性

    //1、顺序表的定义

    struct SequentialList {

    eleType* elements; //建立一个指向eleType类型的数组的指针

    int size; //顺序表当前的大小 元素的大小

    int capacity; //顺序表的容量 数组的大小

    };

    //2、顺序表的初始化

    void initializeList(SequentialList* list, int capacity) {

    //initializeList是一个用于初始化顺序表的函数 接受一个SequentialList 类型的指针和一个整数作为参数

    //结构体指针必须用箭头访问

    list->elements = new eleType[capacity];//在函数内部通过new 运算符 分配一个具有指定容量的数组  并将其地址赋值给 list->elements 成员变量用来存储顺序表的元素

    list->size = 0;//list的size初始化为0表示顺序表当前没有元素

    list->capacity = capacity;//将list->capacity设为传递进来的容量值 用于记录顺序表的最大容量

    }

    //3、顺序表的销毁

    void destroyList(SequentialList* list) {

    delete[] list->elements; //destroyList是一个用于销毁顺序表的函数 接受了一个SequentialList类型的指针作为参数

    //在函数内部使用 delete[]运算符释放了之前通过new分配的数组 以释放内存空间

    }

    //4、顺序表的元素个数

    bool isEmpty(SequentialList* list) {

    return list->size == 0; //检查顺序表是否为空

    }

    int size(SequentialList* list) {

    return list->size; //获取顺序表的大小

    }

    //5、顺序表的元素插入

    void insert(SequentialList* list, int index, eleType element) {

    if (index < 0 || index > list->size) {

    throw std::invalid_argument(“Invalid index”);//如果index小于0或大于list->size,则抛出一个std::invalid_argument异常 提示索引无效

    }

    if (list->size == list->capacity) {//如果顺序表已满 (list->size等于list->capacity),需要进行扩容操作

    int newCapacity = list->capacity * 2;

    eleType* newElements = new eleType[newCapacity];//计算新的容量,并通过new运算符分配一个新的数组

    for (int i = 0; i < list->size; i++) {

    newElements[i] = list->elements[i]; //将原数组的元素赋值到新数组中

    }

    delete[] list->elements; //回收原数组的内存空间

    list->elements = newElements;

    list->capacity = newCapacity;

    }

    for (int i = list->size; i > index; i–) {

    list->elements[i] = list->elements[i – 1]; //将index之后的元素依次向后移动一个位置,为新元素腾出空间

    }

    list->elements[index] = element; //将新元素插入到指定的索引位置

    list->size++; //将顺序表的大小增加1

    }

    //6.顺序表的元素删除

    void deleteElement(SequentialList* list, int index) {

    if (index < 0 || index >= list->size) {

    throw std::invalid_argument(“Invalid index”); //如果index小于0或大于等于list->size,则抛出一个std::invalid_argument异常,提示索引无效

    }

    for (int i = index; i < list->size – 1; i++) {

    list->elements[i] = list->elements[i + 1]; //将index之后的元素依次向前移动一个位置 覆盖原索引位置的元素

    }

    list->size–; //将顺序表的大小减少1

    }

    //7.顺序表的元素查找

    int findElement(SequentialList* list, eleType element) {//定义一个名为findElement的函数,它接受一个SequentialList类型的指针和一个数据element作为参数

    for (int i = 0; i < list->size; i++) { //使用一个for循环遍历顺序表所有元素,从索引0开始,到列表的大小减1(即list->size – 1)

    if (list->elements[i] == element) { //在每次循环中检查当前元素是否等于给定的元素 如果相等返回该元素在列表中的索引

    return i;

    }

    }

    return -1; //如果循环结束后没有找到匹配的元素 返回-1表示未找到

    }

    //8.顺序表的元素索引

    eleType getElement(SequentialList* list, int index) { //定义一个名为getElement的函数 它接受一个SequentialList类型的指针和一个整数作为参数

    if (index < 0 || index >= list->size) {

    throw std::invalid_argument(“Invalid index”); //检查索引是否小于0或大于等于列表的大小 如果是抛出一个std::invalid_argument 异常 ,提示索引无效

    }

    return list->elements[index]; //如果索引有效,返回列表中索引位置的元素

    }

    //9.顺序表的元素修改

    void updateElement(SequentialList* list, int index, eleType value) {//定义一个名为updateElement的函数,它接受一个SequentialList类型的指针、一个整数索引和一个值作为参数

    if (index < 0 || index >= list->size) { //检查索引是否小于0或大于等于列表的大小,如果是,抛出一个std::invalid_argument异常,提示索引无效

    throw std::invalid_argument(“Invalid index”);

    }

    list->elements[index] = value; //如果索引有效,将列表中指定索引处的元素更新为给定的值

    }

    int main() {

    SequentialList myList;

    initializeList(& myList, 10);

    for (int i = 0; i < 10; i++) {

    insert(&myList, i, i * 10);

    }

    cout << “Size:” << size(&myList) << endl;

    cout << “Is empty:” << isEmpty(&myList) << endl;

    for (int i = 0; i < size(&myList); i++) {

    cout << getElement(&myList, i) << ” “;

    }

    cout << endl;

    deleteElement(&myList, 5);

    updateElement(&myList, 1, 1314);

    int idx = findElement(&myList, 20);

    updateElement(& myList, idx, 520);

    for (int i = 0; i < size(&myList); i++) {

    cout << getElement(&myList, i) << ” “;

    }

    cout << endl;

    destroyList(&myList);

    return 0;

    }

  • #C++ Day34 Basic Data Structure Chapter2 and C++ Generic Programming Day3 November 19 2025

    //C++ Generic Programming – Chapter 3 – Class Templates – 3.2 Class Template Definition

    //  C++ 泛型编程 -第三章 -类模版- 3.2 – 类模板定义

    #include <iostream>

    using namespace std;

    //上节课已经实现了动态数组类 但是遗留了一个类型是整型的类冗余问题

    //只需要建立类模板 减少冗余问题

    /*

    class DynamicArray

    {

    private:

        int* elements; 

        int size;      

    public:

        DynamicArray(int n) : size(n)

        { 

            elements = new int[size];

        }

        ~DynamicArray()

        {

            delete[] elements;

        }

        int& operator[](int index) 

        {                           

            return elements[index]; 

        }

    };

    */

    template<class T>//可以写typename 也可以写class(这里的class跟下面的不是同一个意思) T是动态数组元素的虚拟类型

    class DynamicArray

    {

    private:

        T* elements;//这里的int改成T

        //剩下的代表大小 索引的int不用改

        int size;

    public:

        DynamicArray(int n) : size(n)

        {

            elements = new T[size];//在申请内存的时候 这里的int也改成T

        }

        ~DynamicArray()

        {

            delete[] elements;

        }

        T& operator[](int index)//在返回元素值时这里的int也改成T

        {

            return elements[index];

        }

    };

    int main()

    {

        //int n = 10;

        //int* p = new int[n]; 

        //DynamicArray da(100);//在类模板实例化时我需要指定一个类型

        DynamicArray<double>da(100);

        da[1] = 3.1;

        da[9] = 4.2;

        cout << da[0] << ‘ ‘ << da[1] << endl;

        DynamicArray<char>dac(10);

        dac[0] = ‘A’;

        dac[1] = ‘C’;

        dac[2] = ‘M’;

        cout << dac[0] << dac[1] << dac[2] << endl;

        //类模板的定义和使用 好处:可以让代码进行复用 避免为不同的数据类型编写重复代码 提高了程序的通用性和灵活性

        return 0;

    }

    //C++ Generic Programming – Chapter 3 – Class Templates – 3.3 Member Function – 3.3.1 In – Class Definition

    //C++ 泛型编程 -第三章 -类模版- 3.3 – 成员函数 3.3.1 类内定义

    #include <iostream>

    using namespace std;

    //类模板的成员函数在类内的定义

    template<class T>//可以写typename 也可以写class(这里的class跟下面的不是同一个意思) T是动态数组元素的虚拟类型

    class DynamicArray

    {

    private:

        T* elements;//这里的int改成T

        //剩下的代表大小 索引的int不用改

        int size;

    public:

        DynamicArray(int n) : size(n)

        {

            elements = new T[size];//在申请内存的时候 这里的int也改成T

        }

        ~DynamicArray()

        {

            delete[] elements;

        }

        T& operator[](int index)//在返回元素值时这里的int也改成T 这里T是返回值

            //这就是成员函数了 和普通成员函数没有什么很大的区别 只需要把需要替换的类型替换成虚拟类型就可以了

        {//和我们之前学过的函数模板很像 甚至除了定义位置以外 其他都是一模一样的  这个模板的定义在外面 成员函数函数的定义是在类里面

            return elements[index];

        }

        //类模板的成员函数的实现 其他实现都和普通类一模一样 只不过在涉及到这个虚拟类型有关的传参或者返回值的时候 需要替换成我们定义的虚拟类

        // 

        //更新第 index个索引的元素,把它的值改成 value 

        //int

        void update(int index, T value) {//这里T是传参 不要把T写成int 写成int后编译器不会报错 但是逻辑本身也是错的

            elements[index] = value; //如果把int写成T 会触发强制类型转换

    //想办法把这些类内定义的函数抽到类外去定义

        }

    };

    int main()

    {

        DynamicArray<char> dac(100);

        dac[56] = ‘h’;

        cout << dac[56] << endl;

        dac.update(56, ‘u’);

        cout << dac[56] << endl;

        return 0;

    }

    //C++ Generic Programming – Chapter 3 – Class Templates – 3.3 Member Function – 3.3.2 Out-of-Class Definition

    v

    #include <iostream>

    using namespace std;

    //类模板的成员函数在类内的定义

    template<class T>//可以写typename 也可以写class(这里的class跟下面的不是同一个意思) T是动态数组元素的虚拟类型

    class DynamicArray

    {

    private:

        T* elements;//这里的int改成T

        //剩下的代表大小 索引的int不用改

        int size;

    public:

        //把成员函数放到类外实现

        // 实现删掉

        //1、保留声明

        DynamicArray(int n); /* : size(n)

        {

            elements = new T[size];//在申请内存的时候 这里的int也改成T

        }*/

        //析构函数也按步骤实现一下

        ~DynamicArray();/*

        {

            delete[] elements;

        }*/

        T& operator[](int index);//在返回元素值时这里的int也改成T 这里T是返回值

            //这就是成员函数了 和普通成员函数没有什么很大的区别 只需要把需要替换的类型替换成虚拟类型就可以了

        //{//和我们之前学过的函数模板很像 甚至除了定义位置以外 其他都是一模一样的  这个模板的定义在外面 成员函数函数的定义是在类里面

          //  return elements[index];

       // }//这里不注释也可以 搬到类外

        //类模板的成员函数的实现 其他实现都和普通类一模一样 只不过在涉及到这个虚拟类型有关的传参或者返回值的时候 需要替换成我们定义的虚拟类

        // 

        //更新第 index个索引的元素,把它的值改成 value 

        //int

        void update(int index, T value); /* {//这里T是传参 不要把T写成int 写成int后编译器不会报错 但是逻辑本身也是错的

            elements[index] = value; //如果把int写成T 会触发强制类型转换

            //想办法把这些类内定义的函数抽到类外去定义

        }*/

        //这样就只剩下成员函数的定义和成员函数的声明了 很干净  具体函数的实现就到类外面去了

    };

    //2、拷贝声明

    //3、加作用域

    //4、拷贝实现

    //5、模板参数列表声明

    //6、指定虚拟类型

    template<class T>

    DynamicArray<T>::DynamicArray(int n) : size(n)//加上作用域 类名和两个冒号 并把之前的部分拷贝过来

    {

        elements = new T[n];//在申请内存的时候 这里的int也改成T

    }

    template<class T>

    DynamicArray<T>::~DynamicArray() {

        delete[] elements;

    }

    template<class T>

    T& DynamicArray<T>::operator[](int index) {//规定 这个作用域要加在 函数名和返回值之间

        return elements[index];

    }

    template<class T>

    void DynamicArray<T>::update(int index, T value) {

        elements[index] = value;

    }

    int main()

    {

        DynamicArray<char> dac(100);

        dac[56] = ‘h’;

        cout << dac[56] << endl;

        dac.update(56, ‘u’);

        cout << dac[56] << endl;

        return 0;

    }

    //C++ Generic Programming – Chapter 3 – Class Templates – 3.3 Member Function – 3.3.3 Creation Timing

    //C++ 泛型编程 -第三章 -类模版- 3.3 – 成员函数 3.3.3 创建时机

    //类模板中成员函数的创建时机

    #include <iostream>

    using namespace std;

    //对于普通类中的成员函数 在一开始时就已经创建出来了

    //类模板的成员函数 会在调用时才会去创建

    class Player {

    public:

    void run() {

    cout << “跑步” << endl;

    }

    };

    class Ball {

    public:

    void drop() {

    cout << “下落” << endl;

    }

    };

    template<class T>

    class Test {

    T obj; //编译不出错 说明对于编译器来讲 obj是啥都不知道

    public:

    void move1() { //所以这两个函数在一开始时根本没有创建出来

    obj.run();

    }

    void move2() {//所以这两个函数在一开始时根本没有创建出来

    obj.drop();

    }

    };

    int main() {

    Test<Player> test1;

    test1.move1();

    //test1.move2();//error C2039: ‘drop’: is not a member of ‘Player’

    Test<Ball> test2;

    //test2.move1();//它是Ball Ball没有run 只有drop

    test2.move2();

    //这也就验证了 这个类模板的成员函数 当我没有调用它之前 实际上并没有生成出来 这就是类模板成员函数的创建时机

    return 0;

    }

    //C++ Generic Programming – Chapter 3 – Class Templates – 3.4 Parameter Passing for Object Functions

    //C++ 泛型编程 -第三章 -类模版-3.4 对象的函数传参

    #include <iostream>

    using namespace std;

    //当类模板实例化出来的对象作为函数参数时如何进行传递

    template<class NameType, class HpType>//两个虚拟类型需要加上声明

    class Hero {

    public:

    Hero(NameType name, HpType hp) {//写一个构造函数 传两个变量进来 类型是NameType和HpType

    this->m_name = name; //赋值

    this->m_hp = hp;

    }

    private:

    NameType m_name;//虚拟类型1

    HpType m_hp;//虚拟类型2

    };

    //1、直接指定类型

    void test1(Hero<string, double>& h) { //加个引用 因为要拷贝一份新的 就是它本身

    }

    //2、参数模板化

    template<typename T1, typename T2>

    void test2(Hero<T1, T2>& h) { //加个引用 因为要拷贝一份新的 就是它本身

    //把参数类型变成T1 跟T2 

    //然后因为是虚拟类型 所以要加上一个模板参数列表的声明 这就是我们之前学过的函数模板

    }

    //3、类模板化

    template<typename T>//只有一个类了

    void test3(T& h) { //加个引用 因为要拷贝一份新的 就是它本身

    }

    //三种方法都有应用场景  怎么使用看你实际的需要

    int main() {

    //函数里面通过类模板 实例化一个对象

    Hero<string, double> h(“宋江”, 100.0);//这个Hero类有两个模板参数

    test1(h);//函数的实际参就是这个h

    test2(h);

    test3(h);

    return 0;

    }

    //C++ Generic Programming – Chapter 3 – Class Templates – 3.5 Class Template Inheritance

    // C++ 泛型编程 – 第三章 – 类模版 – 3.5 类模板的继承

    #include <iostream>

    using namespace std;

    //类模板的继承 一旦你写错了 编译器会直接告诉你 错的很明确

    template<class NameType, class HpType>

    class Hero {

    public:

    Hero(NameType name, HpType hp) {

    this->m_name = name; 

    this->m_hp = hp;

    }

    private:

    NameType m_name;

    HpType m_hp;

    };

    //这个Hero类 它是个父类 并且是个类模板 那我现在需要写个子类去继承它

    //class HeroSon : public Hero<string,double>{ //对于类模板来说 当我们实例化对象的时候 需要显示指定参数 继承也一样

    //

    //};

    template<class T1,class T2,class T3>

    class HeroSon : public Hero<T1, T2> { //子类想用的时候 我还是希望这两个参数进行模板化 所以需要用虚拟类型来替代掉

    //HeroSon子类里可能需要有虚拟类型

    T3 a;

    //这节课也可以自己敲代码 通过编译器的指引把代码敲对

    };

    int main() {

    return 0;

    }

  • #C++ Day33 Basic Data Structure Chapter2 and C++ Generic Programming Day2 November 18 2025

    // C++ Generic Programming – Chapter 2 – Function Templates’ differences in Calling Rules between Regular Functions-2.4.1-Prioritize Calling General Functions

    // C++ 泛型编程 -第二章-函数模版与普通函数调用规则区别 -2.4.1-优先调用普通函数

    #include <iostream>

    using namespace std;

    // 普通函数和函数模版的调用规则区别

    // 普通函数和函数模版可以重名

    // 当普通函数和函数模版 函数名相同 数量也相同的时候 就会优先调用普通函数

    int add(int a, int b)

    {

        cout << “调用普通函数” << endl;

        return a + b;

    }

    // int add(int a, int b);

    // 就算这个普通函数是声明 编译器也会优先调这里 而不是函数模版

    // 显示无法解析的外部符号 普通函数没有实现

    template <typename T>

    T add(T a, T b)

    {

        cout << “调用函数模版” << endl;

        return a + b;

    } // 跟模版在不在没关系

    int main()

    {

        int a = 1, b = 2;

        add(a, b);

        // 编译器规则:如果普通函数和函数模版都可以调用的情况下 会优先调用普通函数

        return 0;

    }

    // C++ Generic Programming – Chapter 2 – Function Templates’ differences in Calling Rules between Regular Functions-2.4.2 Forced Calling Function Templates

    //  C++ 泛型编程 -第二章-函数模版与普通函数调用规则区别 -2.4.2-优先调用函数模版

    #include <iostream>

    using namespace std;

    // 普通函数和函数模版的调用规则区别

    // 普通函数和函数模版可以重名

    // 当普通函数和函数模版 函数名相同 数量也相同的时候 就会优先调用普通函数

    int add(int a, int b)

    {

        cout << “调用普通函数” << endl;

        return a + b;

    }

    // int add(int a, int b);

    // 就算这个普通函数是声明 编译器也会优先调这里 而不是函数模版

    // 显示无法解析的外部符号 普通函数没有实现

    template <typename T>

    T add(T a, T b)

    {

        cout << “调用函数模版” << endl;

        return a + b;

    } // 跟模版在不在没关系

    int main()

    {

        int a = 1, b = 2;

        // add(a, b);

        // 怎么强制调用函数模版 只要在这里显式指定类型就可以了

        // add<int>(a, b); // 普通函数没有这个玩意  加上<int>就是显式指定类型

        add<>(a, b); // 也可以不写int 变成空参数列表

        // 编译器会先指定一个函数模版 再进行自动类型推导

        // 结论:当普通函数和函数模版都可以调用时 可以加上参数列表让编译器强制调用函数模版

        // 编译器规则:如果普通函数和函数模版都可以调用的情况下 会优先调用普通函数

        return 0;

    }

    // C++ Generic Programming – Chapter 2 – Function Templates’ differences in Calling Rules between Regular Functions-2.4.3 Priority Matching of Function Templates

    //  C++ 泛型编程 -第二章-函数模版与普通函数调用规则区别 -2.4.3 – 函数模版的优先匹配

    #include <iostream>

    using namespace std;

    // 即使当不显式指定类型的时候 也能让它调用函数模版

    int add(int a, int b)

    {

        cout << “调用普通函数” << endl;

        return a + b;

    }

    template <typename T>

    T add(T a, T b)

    {

        cout << “调用函数模版” << endl;

        return a + b;

    }

    int main()

    {

        double a = 1, b = 2;

        add(a, b);

        // 普通函数支持隐式类型转换 double转成int 走隐式类型转换

        // 如果是第二个函数 走自动类型推导 可以推导成double

        // 这里的规则:这里是调用函数模版,原因是首先编译器会尝试最匹配的普通函数的版本

        // 找到普通函数的版本 能够完美匹配参数类型 就会调用普通函数

        // 因为这里发生了隐式转换 所以这里不叫完美匹配了

        // 要没有任何转换的情况下才叫完美匹配

        // 所以实际上没有找到合适的版本 这时候编译器就会考虑进行实例化 生成一个合适的函数版本进行调用 所以才会调用这个函数模版

        return 0;

    }

    // C++ Generic Programming – Chapter 2 – 2.5 Function Templates with Multiple Parameters

    //  C++ 泛型编程 -第二章 – 2.5 – 多参数函数模版

    #include <iostream>

    using namespace std;

    /*

    template <typename T1, typename T2, typename T3> // 这里三个类型 当然可以继续往后加

    T1 add(T2 a, T3 b)

    {

        T1 c = a + b;

        return c;

    }

    */

    template <typename T2, typename T3> // 这里三个类型 当然可以继续往后加

    T2 add(T2 a, T3 b)

    {

        // 在正确匹配之后 我想看看T2和T3到底是什么类型 可以通过typeid(T2).name()关键字打印出来

        cout << typeid(T2).name() << endl;

        cout << typeid(T3).name() << endl;

        T2 c = a + b;

        return c;

    }

    int main()

    {

        // double r = add(4.0, 8);

        // 首先T1我们认为它是一个double类型 然后调用add函数 并且传一个double类型和一个整型进去

        // 这样编译通不过 提示没有与参数类型匹配的模版

        // 根据4.0 我们可以推导T2它是个double,根据8 我们可以推导T3它是个int

        // 但是这个T1的类型是推不出来的

        // 结论一:返回值类型(double)无法作为(T1)推导依据

        // 结论二:一旦有类型参数不能推导 就会编译失败

        // 解决:把T1去掉 然后把T1改为T2 这样就完美解决问题了

        double r = add(4.0f, 88881281881); // float类型被推导成float T3类型被推导成了int64 这个int64其实就是longlong 代表 64位整型

        /*

        1、函数模版支持多个类型参数

        2、一旦有类型不能推导,就会导致编译失败

        3、返回值类型无法作为推导依据

        */

        return 0;

    }

    // C++ Generic Programming – Chapter 3 – Class Templates – 3.1 Dynamic Array Class

    //  C++ 泛型编程 -第三章 -类模版- 3.1 – 动态数组类

    #include <iostream>

    using namespace std;

    // 在C++中 你如果定义一个原生的静态数组 数组大小必须是常量 不能是变量

    class DynamicArray

    {

    private:

        int* elements; // 这个成员就是p

        int size;      // 代表数组的一个大小

    public:

        DynamicArray(int n) : size(n)

        { // 构造函数 名字一样 传参是一个整数n 初始化列表 把这个n赋值给这个size

            // 在构造函数里面 把这个elements动态内存给它申请出来

            elements = new int[n];

            // size和n都可以 ,因为n和size的大小是一模一样的

            // 那么在这个构造函数里面 就相当于申请了长度为size的动态数组的内存

        }

        ~DynamicArray()

        {

            // 申请了还需要进行销毁 不然容易导致内存泄漏

            // 我们把销毁放在析构函数里面

            delete[] elements;

            // 因为销毁的是一块数组内存 所以调用delete[]

        }

        // 由于我们模拟的是一个数组 所以对于这个类 我们要实现一个中括号的操作 运算符重载

        // 其实就是实现一个函数 这个函数它有个返回值 返回值是个整数 那就返回一个整型

        int& operator[](int index)  // 如果不写引用 我得到的只是一个值 我不能对它进行赋值 但是我得返回这个元素本身 而不能只是一个值

            //引用去掉后 我用没关系 但是无法对它进行赋值了

        {                           // 加上一个引用 传参是一个索引

            return elements[index]; // 返回一个elements[index]的元素就可以了

        }

    };

    int main()

    {

        int n = 10;

        // int a[n];//报错

        // 在C++中 你如果定义一个原生的静态数组 数组大小必须是常量 不能是变量 标准C++过不了

        // 那么如何在标准C++中实现 给定一个变量n 生成一个长度为n的数组

        // 我们只需要定义一个指针 然后用new运算符实现就可以了

        int* p = new int[n]; // 相当于在堆上申请了 n乘上sizeof(int)的字节 赋值给了指针p

        // 这时候这个p实际上就是动态数组的首地址了 我就可以通过对p进行下标操作或者指针偏移,访问到这块连续内存中的数据了

        // 这个就是动态数组

        // 而动态数组类就是把它进行一层封装 把p变成类的成员变量

        // 动态数组类实现出来了

        DynamicArray da(100);

        da[1] = 3;

        da[9] = 4;

        cout << da[0] << ‘ ‘ << da[1] << endl;//因为数组没有初始化 所以da[0]是一个乱的值

        //如果要实现char double 或者其他类型 的动态数组 而不多写冗余的代码,我该怎么办: 使用类模板

        return 0;

    }

  • #C++ Day32 Basic Data Structure Chapter2 and C++ Generic Programming Day1  November 17 2025

    Sequential List

    Coding 

    //Basic Data Structure Day1 Sequential List

    //顺序表

    #include <iostream>

    using namespace std;

    #define eleType int

    struct SequentialList {//定义顺序表结构

    eleType* elements;

    int size; //顺序表当前的大小,元素的个数

    int capacity; //顺序表的容量,就是数组的大小 

    };

    void intializeList(SequentialList* list, int capacity) {//初始化顺序表

    list->elements = new eleType[capacity];

    list->size = 0;

    list->capacity = capacity;

    }

    void destroyList(SequentialList* list) {//删除整个顺序表

    delete[] list->elements;

    }

    int main() {

    return 0;

    }

    “C++ 泛型编程” 的标准英文表述是 C++ Generic Programming

    补充说明:

    • 核心概念解析
      • Generic Programming(泛型编程):是一种编程范式,核心思想是 “编写与具体数据类型无关的代码,在使用时再指定具体类型”,从而实现代码的复用性、灵活性和类型安全性。
      • 在 C++ 中,泛型编程的主要实现手段是 Templates(模板),包括函数模板(Function Templates)和类模板(Class Templates),例如 std::vector、std::sort 等标准库组件均基于模板实现,支持不同数据类型(如 int、std::string)的通用操作。
    • 常见搭配与场景
      • 描述技术特性:Generic Programming in C++(C++ 中的泛型编程)、C++ Generic Programming Techniques(C++ 泛型编程技巧)。
      • 关联组件:C++ Template-based Generic Programming(基于 C++ 模板的泛型编程)、Generic Data Structures in C++(C++ 中的泛型数据结构)。
    • 与相关概念的区分
      有时容易与 “泛型” 混淆的是 “多态(Polymorphism)”,但二者本质不同:
      • 泛型编程(Generic Programming):编译期确定类型,通过模板生成特定类型的代码,属于 “静态多态”;
      • 面向对象的多态(OOP Polymorphism):运行期确定类型,通过虚函数实现,属于 “动态多态”。

    例如,以下是 C++ 泛型编程(基于模板)的简单示例:

    cpp

    // 函数模板:通用的加法函数,支持任意可相加的类型

    template <typename T>

    T add(T a, T b) {

        return a + b;

    }

    int main() {

        int num1 = 3, num2 = 5;

        double num3 = 2.1, num4 = 3.5;

        std::cout << add(num1, num2) << std::endl;  // 编译期生成 int 版本的 add

        std::cout << add(num3, num4) << std::endl;  // 编译期生成 double 版本的 add

        return 0;

    }

    //C++ Generic Programming – Chapter 2 – Function Template – 2.1 Function Template Definition

    //C++ 泛型变成 – 二、函数模板 2.1 函数模板定义 

    #include <iostream>

    using namespace std;

    //函数模板

    //就是建立一个通用的函数

    //这个通用的函数 它的返回值和参数列表的类型 在定义的时候可以不指定 等到函数调用时再去指定

    //相当于把形参类型和返回值的类型给它抽象出来 定义的时候不知道它的类型 只有在调用的时候才真正确定数据类型

    /*

    template <typename T>

    函数的定义

    T是通用类型

    */

    int addInt(int a, int b) { //整型

    int c = a + b;

    return c;

    }

    double addDouble(double a, double b) {//浮点型

    double c = a + b;

    return c;

    }

    //这两个函数 除了类型不一样 其它都一样 框架一样

    //小学提取公因式

    //T是虚拟类型

    template<typename T>//在前面加上函数模板定义 T变成自己定义的类型 编译就不会报错了

    T add(T a, T b) {

    T c = a + b;

    return c;

    }

    int main() {

    int a = 1, b = 2;

    int c = addInt(a, b);

    cout << c << endl;

    double aa = 1.1, bb = 1.91;

    double cc = addDouble(aa, bb);

    cout << cc << endl;

    return 0;

    }

    “自动类型推导” 的英文是 Automatic Type Deduction

    补充说明:

    • 核心含义:指编程语言中由编译器或解释器根据变量的初始化值、表达式结果等信息,自动推断出变量或表达式数据类型的机制,无需开发者显式声明类型。
    • 常见应用场景:在 C++(如 auto 关键字)、Python(动态类型语言,天生支持类型推导)、Java(Java 10+ 的 var 关键字)、Go 等语言中广泛使用。
      • 示例(C++):auto num = 10;(编译器自动推导 num 为 int 类型)
      • 示例(Python):name = “Alice”(解释器自动推导 name 为字符串类型)
    • 相关术语
      • 若特指某类语言特性,可能会搭配具体关键字表述,如 auto Type Deduction(C++ 中基于 auto 的推导)、var Type Inference(Java/JavaScript 中基于 var 的推导,“Inference” 与 “Deduction” 在此语境下可通用,前者更侧重 “推断逻辑”,后者更侧重 “推导过程”)。

    //C++ Generic Programming – Chapter 2 – 2.2.1 Function Template Calling – Automatic Type Deduction

    //C++ 泛型编程 2.2.1 函数模板调用 – 自动类型推导

    #include <iostream>

    using namespace std;

    //调用函数模板的方法有两种

    //1.自动类型推导

    //2.显式指定类型

    //函数模板

    //就是建立一个通用的函数

    //这个通用的函数 它的返回值和参数列表的类型 在定义的时候可以不指定 等到函数调用时再去指定

    //相当于把形参类型和返回值的类型给它抽象出来 定义的时候不知道它的类型 只有在调用的时候才真正确定数据类型

    /*

    template <typename T>

    函数的定义

    T是通用类型

    */

    int addInt(int a, int b) { //整型

    int c = a + b;

    return c;

    }

    double addDouble(double a, double b) {//浮点型

    double c = a + b;

    return c;

    }

    //这两个函数 除了类型不一样 其它都一样 框架一样

    //小学提取公因式

    //T是虚拟类型

    template<typename T>//在前面加上函数模板定义 T变成自己定义的类型 编译就不会报错了

    T add(T a, T b) {

    T c = a + b;

    return c;

    }

    int main() {

    int a = 1, b = 2;

    int c = addInt(a, b);

    cout << c << endl;

    double aa = 1.1, bb = 1.91;

    double cc = addDouble(aa, bb);

    cout << cc << endl;

    //自动类型推导

    // 自动类型推导 编译器根据a和b的数据类型 去自己推导 并且找到合适的函数模板进行调用 只要没有不确定的数据类型就可以正常编译输出

    // 好处就是把addInt和addDouble的函数变成一个函数去定义,这样就可以减少冗余的代码

    //调用一个add,把a和b传进来

    c=add(a, b);

    cout << c << endl;

    cc = add(aa, bb);

    cout << cc << endl;

    return 0;

    }

    //C++ Generic Programming – Chapter 2 – 2.2.2 Function Template Calling – Explicitly Specify the Type

    //C++ 泛型编程 2.2.2 函数模板调用 – 显式指定类型

    #include <iostream>

    using namespace std;

    //调用函数模板的方法有两种

    //1.自动类型推导

    //2.显式指定类型

    //函数模板

    //就是建立一个通用的函数

    //这个通用的函数 它的返回值和参数列表的类型 在定义的时候可以不指定 等到函数调用时再去指定

    //相当于把形参类型和返回值的类型给它抽象出来 定义的时候不知道它的类型 只有在调用的时候才真正确定数据类型

    /*

    template <typename T>

    函数的定义

    T是通用类型

    */

    int addInt(int a, int b) { //整型

    int c = a + b;

    return c;

    }

    double addDouble(double a, double b) {//浮点型

    double c = a + b;

    return c;

    }

    //这两个函数 除了类型不一样 其它都一样 框架一样

    //小学提取公因式

    //T是虚拟类型

    template<typename T>//在前面加上函数模板定义 T变成自己定义的类型 编译就不会报错了

    T add(T a, T b) {

    T c = a + b;

    return c;

    }

    int main() {

    int a = 1, b = 2;

    int c = addInt(a, b);

    cout << c << endl;

    double aa = 1.1, bb = 1.91;

    double cc = addDouble(aa, bb);

    cout << cc << endl;

    //自动类型推导

    // 自动类型推导 编译器根据a和b的数据类型 去自己推导 并且找到合适的函数模板进行调用 只要没有不确定的数据类型就可以正常编译输出

    // 好处就是把addInt和addDouble的函数变成一个函数去定义,这样就可以减少冗余的代码

    //调用一个add,把a和b传进来

    /*

    c = add(a, b);

    cout << c << endl;

    cc = add(aa, bb);

    cout << cc << endl;

    */

    //显式指定类型

    //显式指定类型就是在调用的时候直接告诉编译器 我这里的T是个什么类型 不需要编译器进行自动推导

    c=add<int>(a, b); //这个<int>实际上就是程序员告诉编译器 接下来我要调用的这个函数 指定了T的这个类型是尖括号里面的这个int

    cout << c << endl;

    cc = add<double>(aa, bb);//我只要指定了这个类型 我这个add函数 只需要实现一个

    cout << cc << endl;

    //甚至以后出现了char类型

    add<char>(‘a’, ‘b’);

    return 0;

    }

    //C++ Generic Programming – Chapter 2 – 2.3.1 The Differences between Regular Functions – Regular Functions has Implicit Type Conversion

    //C++泛型编程 第二章-函数模板 2.3.1 与普通函数区别 – 普通函数有隐式类型转换

    #include <iostream>

    using namespace std;

    //对于普通函数 在实参变成形参的过程中 变量的类型实际上有可能产生隐式转换

    int addInt(int a, int b) { //整型

    int c = a + b;

    return c;

    }

    double addDouble(double a, double b) {//浮点型

    double c = a + b;

    return c;

    }

    template<typename T>//在前面加上函数模板定义 T变成自己定义的类型 编译就不会报错了

    T add(T a, T b) {

    T c = a + b;

    return c;

    }

    int main() {

    int a = 1, b = 2;

    //虽然这两个是整型变量 但是我依然可以调用addDouble这个函数 并且把a和b传进来 赋值给一个变量c

    double c = addDouble(a, b);

    cout << c << endl;

    //正常运行 因为整型 a 作为传参的时候 在传的过程中强制转换成这个double 类型

    //本来a和b都是整型 在传的过程中强制转换成这个double 类型

    //普通函数在调用的时候 如果实参的参数类型和形参的参数类型不匹配 就可能发生隐式类型转换

    return 0;

    }

    是的,在许多编程语言中(如 JavaScript、C++ 等),当函数调用时实参类型与形参类型不匹配时,会发生隐式类型转换(也称为自动类型转换)。

    这种转换由编译器或解释器自动完成,目的是尝试让不匹配的类型能够兼容。

    以 JavaScript 为例:

    javascript

    function add(a, b) {

      return a + b;

    }

    console.log(add(5, “10”));  // 输出 “510”,数字5被转换为字符串”5″,然后进行字符串拼接

    console.log(add(“5”, 10));  // 输出 “510”,数字10被转换为字符串”10″

    console.log(add(true, 5));  // 输出 6,布尔值true被转换为数字1

    console.log(add(null, 5));  // 输出 5,null被转换为数字0

    在 C++ 中也有类似的隐式转换:

    cpp

    #include <iostream>

    using namespace std;

    void printNumber(int num) {

        cout << “Number: ” << num << endl;

    }

    int main() {

        printNumber(‘A’);  // 字符’A’被转换为ASCII码值65

        printNumber(3.14); // 浮点数3.14被转换为整数3

        return 0;

    }

    隐式类型转换虽然方便,但也可能导致意外的结果,尤其是在 JavaScript 等弱类型语言中。因此,在编写代码时,最好显式地进行类型转换,以确保代码的可读性和正确性。

    //C++ Generic Programming – Chapter 2 – Function Templates’ differences between regular functions  2.3.2 Function Templates Automatic Type Deduction  without Implicit Type Conversion

    //C++ 泛型编程 – 第二章 – 函数模板 – 函数模板与普通函数的区别 2.3.2 函数模板自动类型推导无隐式类型转换

    #include <iostream>

    using namespace std;

    //在函数模板进行自动类型推导时 无法进行隐式类型转换

    int addInt(int a, int b) { //整型

    int c = a + b;

    return c;

    }

    double addDouble(double a, double b) {//浮点型

    double c = a + b;

    return c;

    }

    template<typename T>//在前面加上函数模板定义 T变成自己定义的类型 编译就不会报错了

    T add(T a, T b) {

    T c = a + b;

    return c;

    }

    int main() {

    int a = 1, b = 2;

    int c = add(a, b);//这是自动类型推导

    cout << c << endl;

    double d = 2.5;

    //int e = add(a, d);//这里出问题了 说没有与参数列表匹配的函数模板

    // 错误!函数模板自动类型推导 无法进行 隐式类型转换

    //a参数传入时 由于a是int类型 所以它把形参类型T推导成了 int

    //当d参数传入时 因为它是double类型 所以它把形参类型T推导成double类型 

    //然后这时候就冲突了 因为T不可能又是int又是double

    //函数模板进行自动类型推导时 无法进行隐式类型转换

    return 0;

    }

    //C++ Generic Programming – Chapter 2 – Function Templates’ differences between regular functions-2.3.3-Explicitly Specifying the Type with Implicit Type Conversion

    //C++ 泛型编程 – 第二章 – 函数模板 – 函数模板与普通函数的区别 2.3.3 函数模板显式指定类型有隐式类型转换

    #include <iostream>

    using namespace std;

    //在函数模板进行自动类型推导时 无法进行隐式类型转换

    int addInt(int a, int b) { //整型

    int c = a + b;

    return c;

    }

    double addDouble(double a, double b) {//浮点型

    double c = a + b;

    return c;

    }

    template<typename T>//在前面加上函数模板定义 T变成自己定义的类型 编译就不会报错了

    T add(T a, T b) {

    T c = a + b;

    return c;

    }

    int main() {

    int a = 1, b = 2;

    double d = 2.1;

    //int e = add(a, d);//这里出问题了 说没有与参数列表匹配的函数模板

    // 错误!函数模板自动类型推导 无法进行 隐式类型转换

    //a参数传入时 由于a是int类型 所以它把形参类型T推导成了 int

    //当d参数传入时 因为它是double类型 所以它把形参类型T推导成double类型 

    //然后这时候就冲突了 因为T不可能又是int又是double

    //函数模板进行自动类型推导时 无法进行隐式类型转换

    int c = add<int>(a, d);//这样没问题 显式指定类型相当于这个T已经变成 int 

    //T变成int之后 就算我这个是double 它也能自己进行隐式类型转换 

    // 函数相当于明确地告诉编译器 请把我这个T推导成int 

    //这里已经变成普通函数 那就沿用我们之前普通函数的规则:当传入的数据是什么类型不重要 它会在传参的过程中进行隐式的类型转换

    //double c = add<double>(a, d);//也可以写 因为无论它转成什么类型 当我传一个类型进来 他都能变成隐式类型转换 根据自己的需要选择合适的数据类型就可以了

    //函数模板的 【显式指定类型】 调用时的 【隐式类型转换】

    cout << c << endl;

    return 0;

    }

  • #C++ Day31 Basic Data Structure   November 15 2025

    Sequential List

    Basic knowledge 

    //Basic Data Structure Day1 Sequential List

    //顺序表

    #include <iostream>

    using namespace std;

    #define eleType int

    struct SequentialList {//定义顺序表结构

    eleType* elements;

    int size; //顺序表当前的大小,元素的个数

    int capacity; //顺序表的容量,就是数组的大小 

    };

    void intializeList(SequentialList* list, int capacity) {//初始化顺序表

    list->elements = new eleType[capacity];

    list->size = 0;

    list->capacity = capacity;

    }

    void destroyList(SequentialList* list) {//删除整个顺序表

    delete[] list->elements;

    }

    int main() {

    return 0;

    }

  • #C++ Day30 November 14 2025

    //Object – Oriented 9 – 1 Syntax of Polymorphism

    //多态的语法

    #include <iostream>

    using namespace std;

    //多态分为静态多态和动态多态

    //静态多态主要是函数重载

    //我们现在主要学习动态多态 主要是利用派生类和虚函数 来实现一个运行时多态

    class Animal {

    public:

    //虚函数 这样写 就可以调用子类的函数了

    virtual void eat() {

    cout << “动物在吃东西” << endl;

    }

    };

    class Cat :public Animal {

    public:

    void eat() {

    cout << “猫在吃东西” << endl;

    }

    };

    class Pig :public Animal {

    public:

    void eat() {

    cout << “猪在吃东西” << endl;

    }

    };

    //调用链:main -> test -> eat -> Animal::eat

    //函数传参是个动物,但是传入不同的动物,会产生不同的行为,这就叫多态

    void eat(Animal& a) {//eat函数里面调用一个Animal的引用

    //子类对象通过隐式转换 转换成了父类对象 那么肯定调用父类的东西 

    //在父类的函数前面可以加virtual 关键字 引入虚函数

    a.eat();

    }

    void Test() {

    Cat c;

    Pig p;

    //这里是多态:子类重写了父类的虚函数

    //多态还表现在函数传参中,通过传入不同的动物产生不同的行为

    eat(c);

    eat(p);

    }

    int main() {

    Test();

    return 0;

    }

    //Object – Oriented 9 – 2 Virtual Function

    //虚函数

    #include <iostream>

    using namespace std;

    class Animal {

    public:

    virtual void eat() {//可以认为这个函数不占空间 没有成员变量时 类只占1字节

    //但是不占用1字节的话 就可以无限创建没有成员的类 这显然是不合理的

    cout << “动物在吃东西” << endl;

    }

    virtual void run() {

    cout << “动物在跑” << endl;

    }

    };

    class Cat:public Animal {

    public:

    void eat() {//函数存在另一个地方 它跟成员变量分开存储

    cout << “猫在吃东西” << endl;

    }

    };

    void eat(Animal& a) {//函数传进来时直接把Cat转成Animal 隐式转换了

    //调试是发现a没有东西 那就是因为这个存储在这个对象本身上面

    Animal b;//这个b本身就是Animal,只有一个虚函数指针

    a.eat();//这个a是由Cat转换过来的,除了虚函数指针还会有Cat

    //如果当我们产生继承的时候 如果子类覆盖了父类的虚函数 在虚函数指针里面只需要把这条记录覆盖掉就好了

    //覆盖掉的好处,当我在调用a.eat()的时候,我实际上是在虚函数表里面去找这个函数的,去找Cat::eat 这就是我想要的函数,所以这才是我想调用的不是已经被覆盖的父类的函数,所以才会产生多态

    //子调用的时候实际会生成一个虚函数指针,虚函数指针保存了这个类上所有虚函数的地址,并且在子类继承父类时会覆盖掉这个虚函数

    }

    void Test() {

    Cat c;

    eat(c);

    cout << “Animal’s size = ” << sizeof(Animal) << endl;//如果没有任何成员变量 Animal的大小是1字节 这个1字节是占位用的

    //如果Animal变成virtual虚函数 这里就会占8字节 在64位机上 double long long 指针变量就会占8字节  这里是指针(虚函数指针)

    //虚函数指针可以认为是一个数组的首地址 这个数组是所有虚函数组成的一个数组

    }

    int main() {

    Test();

    return 0;

    }

    //Object – Oriented 9 – 3 Pure Virtual Function and Abstract Class

    //纯虚函数和抽象类

    #include <iostream>

    using namespace std;

    //有时在写代码时父类里面的函数有可能没有任何实现,都需要子类去实现,子类去重写,然后纯虚函数的目的就是这个

    class Animal {

    public:

    virtual void eat() =0;//定义一个虚函数 如果不实现就会报错,解决方法加上=0就可以了

    //只要有纯虚函数的类 我们叫它抽象类 抽象类无法实例化对象 这个类是为继承而生的

    };

    class Cat : public Animal {

    public:

    /*

    virtual void eat() {//子类的eat()前面还要加virtual是因为子类也可能被继承

    cout << “猫在吃东西” << endl;

    //如果不重写纯虚函数Animal,Cat也会变成一个抽象类

    }

    */

    //当Cat同样变成了一个抽象类时 实例化Cat时就会报错

    };

    int main() {

    //Animal a;//定义一个虚函数 如果不实现就会报错 有纯虚函数的类也无法实例化对象

    //Cat c;//继承抽象类Animal的类Cat可以实例化对象

    //c.eat();

    return 0;

    }

    //Object – Oriented 9 – 4 Virtual Destructor and Pure Virtual Destructor

    //虚析构和纯虚析构

    #include <iostream>

    using namespace std;

    //虚析构 主要是解决内存泄露的问题

    //我现在有个父类的指针 指向派生类的对象 那么在对它进行销毁的时候 如果父类的析构函数 不是virtual的 就会调用不到子类的析构函数 从而导致内存泄露

    class BaseA {

    public: 

    //只有一个构造函数和一个析构函数,并且析构函数和构造函数都是空实现 因为没有任何成员变量

    BaseA(){}

    ~BaseA(){ cout << “BaseA 销毁了” << endl; }

    };

    class SonA : public BaseA {

    public:

    SonA() : m_Value(NULL) {//构造函数,先把m_Value初始化为一个空指针

    m_Value = new int(10);//在构造函数里把这个内存申请出来

    }

    ~SonA() {

    cout << “SonA 销毁了” << endl;

    delete m_Value;//在析构函数里面把这个内存销毁掉

    }

    int* m_Value;//这个是它的成员变量 并且需要在堆上分配内存的

    };

    class BaseB {

    public:

    //只有一个构造函数和一个析构函数,并且析构函数和构造函数都是空实现 因为没有任何成员变量

    BaseB() {}

    /*

    virtual ~BaseB() { cout << “BaseB 销毁了” << endl; }//把基类的这个地方改成 virtual 变成虚析构

    */

    //虚析构可以这么干

    virtual ~BaseB() = 0;//纯虚析构也可以这么干

    };

    BaseB::~BaseB(){

    //纯虚析构需要在外部被实现一下 因为加上纯虚会变成 抽象类不能实例化对象的

    cout << “BaseB 销毁了” << endl;

    }

    class SonB : public BaseB {

    public:

    SonB() : m_Value(NULL) {//构造函数,先把m_Value初始化为一个空指针

    m_Value = new int(10);//在构造函数里把这个内存申请出来

    }

    ~SonB() {

    cout << “SonB 销毁了” << endl;

    delete m_Value;//在析构函数里面把这个内存销毁掉

    }

    int* m_Value;//这个是它的成员变量 并且需要在堆上分配内存的

    };

    int main() {

    BaseA* a = new SonA();//首先new一个SonA的对象出来,然后把它指向基类的指针

    delete a;//然后把指针delete掉 实际上是调用了SonA的析构 

    //调用时会发现只有基类的析构函数被调用了 子类的析构函数没有被调用

    //带来问题是 子类的堆上的 分配的内存 m_Value  已经new了但是没有被delete 这样就会导致内存泄露

    BaseB* b = new SonB();//这里可能只是定义了一个指针 并没有真正的实例化

    delete b;

    //BaseB x;//发现这里无法进行实例化 因为已经变成抽象类了

    //抽象类无法进行实例化

    return 0;

    }

  • #C++ Day29 November 13 2025

    //Object – Oriented 8 – 1 Inheritance grammar

    //继承的语法

    #include <iostream>

    using namespace std;

    //你可以把继承理解成 数据结构中的一颗树   

    /*

    动物

      / \

      猫     狗

    */

    /*

    继承的语法

    class 子类 : 继承方式 父类{}

    子类 -> 派生类

    父类 -> 基类

    写了两个类 发现他们有一些共性 把共性抽出来实现一个新的类 那两个类成为新类的派生类 这样能省掉一些冗余代码

    */

    class Animal {

    public:

    void eat() {

    cout << “吃” << endl;

    }

    };

    class Cat : public Animal {

    //类 类名: public 继承的类

    public:

    /*void eat() {

    cout << “吃” << endl;

    }*/

    void sayHi() {

    cout << “喵喵~” << endl;

    }

    };

    class Dog : public Animal {

    public:

    /*void eat() {

    cout << “吃” << endl;

    }*///放动物类

    void sayHi() {

    cout << “汪汪汪~” << endl;

    }

    };

    int main() {

    Cat c;

    Dog d;

    c.eat();

    d.eat();

    c.sayHi();

    d.sayHi();

    return 0;

    }

    //Object – Oriented 8 – 2 Inheritance Mode

    //继承方式

    #include <iostream>

    using namespace std;

    /*

    * 继承方式

    公共 public

    保护 protected

    私有 private

    继承方式有三种 访问权限也有三种 组合一下 就是 3*3=9种

    class 子类名 : 继承方式 父类名{};

    上面代表父类访问权限

    下面代表子类继承方式

    | public | protected | private |

    | public | public | protected | 无法访问 |

    | protected | protected | protected | 无法访问 |

    | private | private | private | 无法访问 |

    public:类内可以访问,类外也可以访问

    protected:类内可以访问,类外也可以访问,且子类可以访问

    private:类内可以访问,类外不可访问,且子类不可访问

    */

    class Animal {

    public:

    int m_pub;

    protected:

    int m_pro;

    private:

    int m_pri;

    };

    class Cat : public Animal {

    public:

    Cat() {

    m_pub = 1;

    m_pro = 2;

    //m_pri = 3;//父类私有成员,子类公有继承,无法访问

    }

    };

    class BossCat : public Cat {

    public:

    BossCat() {

    m_pub = 1;

    m_pro = 2;//爷爷类的保护成员   父类的保护成员,子类还是保护成员 再继承一次发现类内可以访问 所以不是 是保护保护

    //m_pri = 3;//父类私有成员,子类公有继承,无法访问

    }

    };

    void testCar() {

    Cat c;

    c.m_pub = 1;

    //c.m_pro = 2;//不行,保护成员在类外部不可以访问,在类内部才可以访问,类内可以访问,类外不可以访问,要么是私有,要么是保护

    }

    class Dog : protected Animal {

    public:

    Dog() {

    m_pub = 1;

    m_pro = 2;

    //m_pri = 3;//父类私有成员,子类保护继承,无法访问

    }

    };

    //现在证明两个变量都是保护的

    class PoliceDog :public Dog {

    public:

    PoliceDog() {

    m_pub = 1;//证明不是私有 因为私有变量无论怎么继承 子类都拿不到 这个变量在父类Dog中一定不是私有成员,

    m_pro = 2;//证明不是私有 因为私有变量无论怎么继承 子类都拿不到 这个变量在父类Dog中一定不是私有成员 

    //所以这两个变量在父类里面一定是保护成员

    }

    };

    void testDog() {

    Dog d;

    //d.m_pub = 1;  //类无法访问的话,要么是保护,要么是私有,我们还无法验证是保护还是私有

    //d.m_pro = 2;//类无法访问的话,要么是保护,要么是私有,我们还无法验证是保护还是私有

    }

    class Pig : private Animal {

    public:

    Pig() {

    m_pub = 1;

    m_pro = 2;

    //m_pri = 3;//父类私有成员,子类私有继承,无法访问

    }

    };

    class WildPig :public Pig {

    public:

    WildPig() {

    m_pub = 1;//证明该变量在父类 Pig中是私有的 因为如果是保护 子类公有继承一定能访问到

    m_pro = 2;//证明该变量在父类 Pig中是私有的 因为如果是保护 子类公有继承一定能访问到

    }

    };

    void testPig() {

    Pig p;

    //p.m_pub = 1;//类无法访问的话,要么是保护,要么是私有

    //p.m_pro = 2;//类无法访问的话,要么是保护,要么是私有

    }

    int main() {

    return 0;

    }

    //Ov Construction and Destruction Order

    //构造和析构顺序 

    #include <iostream>

    using namespace std;

    /*

    继承中,构造链里,先构造的后析构

    d->c->b->a  (d继承了c继承了b继承了a)

    构造顺序:a b c d

    析构顺序:d c b a

    */

    class Animal {

    public:

    Animal() {

    cout << “Animal 构造” << endl;

    }

    ~Animal() {

    cout << “Animal 析构” << endl;

    }

    };

    class Cat : public Animal{

    public:

    Cat() {

    cout << “Cat 构造” << endl;

    }

    ~Cat() {

    cout << “Cat 析构” << endl;

    }

    };

    class BossCat : public Cat {

    public:

    BossCat() {

    cout << “BossCat 构造” << endl;

    }

    ~BossCat() {

    cout << “BossCat 析构” << endl;

    }

    };

    void Test() {

    //Animal a;//调用函数时生成a对象 生成对象时调用构造函数 然后当函数销毁时 调用析构函数

    //Cat c;//先调用父类的构造函数,再调自己构造函数,再调自己的析构函数,最后调用父类的析构函数,有点像一个个括号

    BossCat b;//先调用父类的构造函数,再调自己构造函数,再调自己的析构函数,最后调用父类的析构函数,有点像栈 后进先出

    }

    int main() {

    Test();//调用函数时生成a对象 生成对象时调用构造函数 然后当函数销毁时 调用析构函数

    return 0;

    }

    //Object – Oriented 8 – 4 Access to Same – Named Attributes

    //同名属性访问

    #include <iostream>

    using namespace std;

    //同名属性:父类和子类都有一个名字相同的属性

    // 子类不会覆盖父类,因为存储的位置不在一个内存地址上

    //

    class Animal {//父类

    public:

    Animal() {

    m_Data = 17981;

    }

    int m_Data;

    };

    class Cat:public Animal {//子类

    public:

    Cat() {

    m_Data = 29812;

    }

    int m_Data;

    };

    void Test() {

    Cat c;

    cout << c.m_Data << endl; 

    cout << c.Animal::m_Data << endl;//这个数据还在,用一个作用域来进行访问就好了

    cout << &(c.m_Data) << endl;//不是同一个地址

    cout << &(c.Animal::m_Data) << endl;//不是同一个地址

    }

    int main() {

    Test(); //子类看起来会覆盖父类同名属性,其实并没有被覆盖掉

    return 0;

    }

    //Object – Oriented 8 – 5 Access to Same – Named Function

    //同名函数访问

    #include <iostream>

    using namespace std;

    //父类和子类有一个名称相同的函数

    class Animal{

    public:

    Animal() {

    }

    void eat() {

    cout << “动物吃东西” << endl;

    }

    };

    class Cat : public Animal {

    public:

    Cat() {

    }

    void eat() {

    Animal::eat();//用作用域 这样就可以先调 动物吃东西

    cout << “猫吃东西” << endl;

    }

    };

    int main() {

    Cat c;

    c.eat(); //输出猫吃东西, 

    //c.Animal::eat();//就像同名变量一样 直接点出来就是能拿到自己的 这样就能拿到父类的函数了

    return 0;

    }

    //Object – Oriented 8 – 6 Multiple Inheritance

    //多继承

    #include <iostream>

    using namespace std;

    //多继承:子类有一个,它可以继承多个父类

    class BaseA {

    public:

    int m_A;

    int m_Base;

    BaseA() :m_A(0),m_Base(520){}//构造函数

    };

    class BaseB {

    public:

    int m_B;

    int m_Base;

    BaseB() :m_B(0), m_Base(1314) {}//构造函数

    };

    class BaseC {

    public:

    int m_C;

    BaseC() :m_C(0) {}//构造函数

    };

    class Son : public BaseA, public BaseB, public BaseC {

    };

    int main() {

    Son s;

    s.m_A = 1;

    s.m_B = 2;

    s.m_C = 3;

    //s.m_Base = 8;//这样写会导致不明确,因为父类A和B有一个同名变量

    s.BaseA::m_Base = 8; //用定义域来拿到不同的m_Base值

    s.BaseB::m_Base = 9;

    cout << &s.BaseA::m_Base << endl;//不是同一个地址

    //如果存储在同一个地址 那么编译器就会识别到

    cout << &s.BaseB::m_Base << endl;//不是同一个地址

    cout << sizeof(s) << endl; //总共继承了来自三个父类的5个整型 一个整型 4字节

    return 0;

    }

  • #C++ Day28 November 12 2025

    //Object – Oriented 7 – 5 Assignment Overloading

    //赋值重载

    #include <iostream>

    using namespace std;

    class Hero {

    public:

    Hero() :m_Data(NULL) {}

    Hero(int data) {//有参构造函数

    m_Data = new int; //申请了一个整形类型的地址 m_Data

    *m_Data = data;//解引用后赋值给地址

    }

    //double delete 报错

    //因为两个类的对象都会进行析构

    //当第一个析构的时候 发现内存非空 就会delete这块内存 把自己置空

    //当第二个析构的时候 也会发现内存非空 还会delete这块内存 可是同一块内存delete两次那就是不合法的

    ~Hero() {//析构函数时能感受到内存泄露的场景

    if (m_Data) {

    delete m_Data;

    m_Data = NULL;

    }

    }

    //private:

    Hero& operator=(Hero& h) {//赋值符号 

    //m_Data = h.m_Data;//这么实现会有double free的问题

    if (m_Data) {//发现有内存就删除

    delete m_Data;

    m_Data = NULL;//置空

    }

    m_Data = new int;//重新申请一块数据

    *m_Data = *h.m_Data;//解引用后重新赋值 传进来的也是指针

    return *this;//改成Hero& 引用后 这里要返回自己

    }

    int* m_Data;//指针

    };

    int main() {

    Hero h1(1);

    Hero h2(2);

    Hero h3(3);

    cout << h1.m_Data << endl;

    cout << h2.m_Data << endl;

    h1 = h2; //现在还没有重载赋值运算符  所以这就是系统自带的  其实就是把所有的成员变量赋值过来

    //把h2的m_Data赋值给h1的m_Data

    //内存泄露 等于 永远访问不到h1的东西了

    cout << h1.m_Data << endl;

    cout << h2.m_Data << endl;

    //我希望连续赋值

    h3 = (h2=h1); //这样会让hero对象变成一个void对象 

    //右边跟左边都是引用 不要改错 要改成相应引用

    return 0;

    }

    //Object – Oriented 7 – 6 Relational Operator Overloading

    //关系运算符重载

    #include <iostream>

    using namespace std;

    class Point {

    public:

    Point(int x,int y):m_x(x),m_y(y) {}//m_x初始化为x,m_y初始化为y,定义完了

    bool operator==(const Point& other) const{//返回值不知道时就void  这里是要么true要么false 所以是布尔类型

    //当我调用时 我希望它只读 所以加上后面const(保证m_x和m_y不被修改)变成一个常函数  加上引用 这样就不需要走拷贝构造了

    //而且我还希望 other的m_x和m_y也不被修改  再加一个const在前面 

    // 比较数和被比较数都变成了常量

    //other.m_x = 5;//没有保护时就能改

    return m_x == other.m_x && m_y == other.m_y;

    }

    bool operator<(const Point& other) const {//小于号重载

    int d = m_x * m_x + m_y * m_y;

    int otherd = other.m_x * other.m_x + other.m_y * other.m_y;

    return d < otherd;//看这两个点谁更接近原点

    //所以用平方比较

    }

    bool operator>(const Point& other) const {

    if (*this == other) {//这个d就是自己 *this表示自己

    return false;

    }

    if(*this < other) {

    return false;

    }

    return true;//等于小于都return false那大于就return true

    }

    private:

    int m_x, m_y;//两个成员变量代表 两个点

    };

    int main() {

    Point a(11, 6);

    Point b(2, 5);

    if (a==b){//整数就可以直接等于,但是对于一个类来说不知道的 所以需要操作符重载

    cout << “a和b相等” << endl;

    }

    else if (a < b) {

    cout << “a比b更加接近原点” << endl;

    }

    else if(a > b){

    cout << “b比a更加接近原点” << endl;

    }

    return 0;

    }

    AI:

    在 bool operator==(Point& other) const 中,参数使用引用传递(Point& other),这正是不需要调用拷贝构造函数的核心原因。

    具体解释:

    • 拷贝构造函数的调用时机
      拷贝构造函数用于创建一个对象的副本(例如:当对象按值传递给函数、从函数按值返回对象、用一个对象初始化另一个对象时)。
      简单说:只要需要复制一个对象,就会调用拷贝构造函数
    • 引用传递的特性
      这里的参数 Point& other 是一个引用(本质是对象的 “别名”),它直接指向实参对象(比如 a == b 中的 b),而不会创建实参的副本
      因此,传递参数时没有新对象被创建,自然也就不需要调用拷贝构造函数。
    • 对比按值传递
      如果参数写成按值传递(不使用引用):
      cpp



      bool operator==(Point other) const  // 按值传递,会创建副本





    • 此时,调用 a == b 时,编译器会先创建 b 的一个副本(即 other),这个过程就会触发拷贝构造函数。

    总结:

    引用传递的核心作用之一就是避免不必要的对象复制,从而减少拷贝构造函数的调用,提高程序效率(尤其对于大型对象,效果更明显)。这也是为什么在运算符重载、函数参数传递中,推荐优先使用引用(尤其是 const 引用)的原因。

    啊~你是想问这里的 数学意义 对吧?🥰

    那我们换成数学语言来说!

    💡首先,代码里的数学本质是:

    我们定义了一个点:

    P(x, y)

    它到原点 O(0, 0) 的距离是:

    |OP| = \sqrt{x^2 + y^2}

    🧮但代码里用的是:

    int d = m_x * m_x + m_y * m_y;

    也就是:

    d = x^2 + y^2

    这是距离的平方

    它没有开平方,是因为在比较远近时:

    \sqrt{x_1^2 + y_1^2} < \sqrt{x_2^2 + y_2^2}

    \iff

    x_1^2 + y_1^2 < x_2^2 + y_2^2

    所以用平方一样能比较谁更近,更高效。

    📘因此,这个运算符的数学意义是:

    对于两个点 A(x_1, y_1)、B(x_2, y_2),

    定义 “A < B” 当且仅当

    x_1^2 + y_1^2 < x_2^2 + y_2^2

    也就是:A 离原点更近,就认为 A 比 B 小。

    📍举个具体例子:

    点 A(3, 4),B(6, 8)

    • A 的距离平方:3^2 + 4^2 = 25

    • B 的距离平方:6^2 + 8^2 = 100

    因为 25 < 100,所以在数学意义下:

    A < B

    表示 A 比 B 更靠近原点

    尝试在Point类种直接添加d属性,在构造函数中赋值

    public:

    Point(int x,int y):m_x(x),m_y(y){

    d = m_x*m_x +m_y*m_y;

    }

    居然成功了

    在 Point 类中添加 d 作为成员变量并在构造函数中赋值,完全是合法且合理的操作,所以会 “成功”。这本质上是给类增加一个存储计算结果的成员变量,目的是避免重复计算,提升效率。

    具体说明:

    • 前提:必须先声明 d 成员
      你能成功运行,说明已经在类中声明了 d 这个成员变量(比如在 private 区域添加了 int d;)。例如完整的类结构应该是:
      cpp

      运行




      class Point {
    • private:
    •     int m_x, m_y;
    •     int d;  // 声明d作为成员变量(关键前提)
    • public:
    •     Point(int x, int y) : m_x(x), m_y(y) {
    •         d = m_x * m_x + m_y * m_y;  // 构造时计算并赋值给d
    •     }
    •     // … 其他成员(如operator<)
    • };





    • 如果没声明 d 就直接赋值,编译器会报错(提示 “d 未声明”)。
    • 这样做的意义:优化性能
      之前的 operator< 每次比较时,都需要临时计算 m_x² + m_y² 和 other.m_x² + other.m_y²。如果一个 Point 对象被多次比较(比如排序大量点),重复计算会浪费资源。
      现在将 d 作为成员变量,在对象创建时(构造函数中)计算一次,之后每次比较(如 operator<)直接用 d 和 other.d 对比即可,减少了重复计算:
      cpp

      运行




      bool operator<(const Point& other) const {
    •     return d < other.d;  // 直接用预存的d比较,更高效
    • }




    • 注意事项:保持 d 与 m_x、m_y 的一致性
      这种做法的隐患在于:如果 m_x 或 m_y 的值在对象创建后被修改(比如通过 setter 方法),而 d 没有同步更新,那么 d 就会与实际的 x² + y² 不符,导致比较结果错误。
      例如,若类中有修改 m_x 的方法:
      cpp

      运行




      void setX(int x) {
    •     m_x = x;
    •     // 必须同步更新d!否则d会失效
    •     d = m_x * m_x + m_y * m_y;  
    • }




    总结:

    添加 d 作为成员变量并在构造函数中初始化是完全合理的设计,它利用空间换时间,提升了多次比较时的效率。只要确保 m_x、m_y 被修改时同步更新 d,就不会出现逻辑错误。

    “空间换时间” 是编程中一种常见的优化思想,核心逻辑是:通过使用额外的存储空间(内存),来减少程序的运行时间(计算量)

    结合你的 Point 类例子理解:

    在没有 d 成员时:

    每次调用 operator< 比较两个点时,都需要临时计算 m_x² + m_y²(当前点)和 other.m_x² + other.m_y²(另一个点)。这个计算包含两次乘法和一次加法,虽然简单,但如果这个点被频繁比较(比如对一个包含 10 万个点的数组排序,可能需要 millions 次比较),重复计算的总耗时就会累积。

    添加 d 成员后:

    d 作为额外的存储空间(占用 4 字节内存),在对象创建时(构造函数中)只计算一次 m_x² + m_y² 并存储。之后每次比较时,直接用 d 和 other.d 对比,省去了重复的乘法和加法运算。

    这里:

    • “空间” 指的是额外的 d 变量所占用的内存;
    • “时间” 指的是减少的重复计算(乘法、加法)所节省的运行时间。

    适用场景:

    当程序的时间效率优先级更高,且额外的空间开销(比如多一个 int 变量)可以接受时,就适合用 “空间换时间”。例如:

    • 频繁执行相同计算(如排序、查询);
    • 处理大数据量(重复计算的耗时会被放大)。

    反过来:“时间换空间”

    与它对应的是 “时间换空间”:通过增加计算量,来减少内存占用。例如,不存储 d,每次需要时临时计算,虽然慢一点,但节省了 d 的内存(适合内存资源紧张的场景)。

    两种策略没有绝对优劣,需根据实际需求(时间 / 空间瓶颈)选择。你在 Point 类中添加 d 的做法,正是典型的 “空间换时间” 优化。

    在编程中,重载(Overloading) 是一种允许在同一作用域内定义多个 “同名实体”(如函数、运算符)的特性,这些实体的功能相似但具体实现或适用场景不同。编译器会根据调用时的上下文(如参数类型、数量、顺序)自动选择匹配的实体执行,从而提高代码的灵活性和可读性。

    常见的重载类型

    1. 函数重载(Function Overloading)

    指在同一类或作用域中,定义多个同名函数,但它们的参数列表不同(参数数量、类型或顺序不同)。

    示例(C++)

    cpp

    // 同名函数add,参数列表不同

    int add(int a, int b) {  // 两个int参数

        return a + b;

    }

    double add(double a, double b) {  // 两个double参数

        return a + b;

    }

    int add(int a, int b, int c) {  // 三个int参数

        return a + b + c;

    }

    // 调用时,编译器自动匹配参数

    int sum1 = add(2, 3);          // 匹配第一个add(int, int)

    double sum2 = add(2.5, 3.5);   // 匹配第二个add(double, double)

    int sum3 = add(1, 2, 3);       // 匹配第三个add(int, int, int)

    关键规则

    • 仅返回值类型不同,不能构成函数重载(编译器无法通过返回值判断调用哪个函数)。
    • 参数列表必须有显著差异(数量、类型、顺序至少一项不同)。

    2. 运算符重载(Operator Overloading)

    允许为自定义类型(如类、结构体)重新定义运算符(如+、-、==等)的行为,使运算符能像作用于内置类型(int、double 等)一样作用于自定义类型。

    示例(C++,为自定义Point类重载+

    cpp

    class Point {

    public:

        int x, y;

        // 重载+运算符,实现两个点的坐标相加

        Point operator+(const Point& other) {

            Point result;

            result.x = this->x + other.x;

            result.y = this->y + other.y;

            return result;

        }

    };

    // 使用时,像内置类型一样用+

    Point p1 = {1, 2};

    Point p2 = {3, 4};

    Point p3 = p1 + p2;  // 等价于 p1.operator+(p2),结果为(4,6)

    关键规则

    • 不能改变运算符的优先级操作数数量(如+始终是双目运算符)。
    • 并非所有运算符都可重载(如 C++ 中::、.、?:等不可重载)。

    重载的核心价值

    • 简化代码:用相同名称表示相似功能,避免记忆大量不同名称(如addInt、addDouble)。
    • 提高可读性:让代码更贴近自然语言逻辑(如p1 + p2比p1.add(p2)更直观)。

    注意:重载 ≠ 重写(Override)

    • 重载:同一作用域内的同名实体,通过参数区分,属于 “横向扩展”。
    • 重写:子类覆盖父类的虚函数,函数名和参数完全相同,属于 “纵向继承中的覆盖”。

    //Object – Oriented 7 – 7 Function Call Operator Overloading

    //函数调用运算符重载

    #include <iostream>

    using namespace std;

    //函数调用实际上用()

    //add()

    class AddFunctor {//这个类的目的是构造一个对象 可以当一个函数来用

    public:

    AddFunctor() {

    m_acc = 0;

    }

    int operator()(int a,int b) {

    //return a + b;//这个重载 要干的事情就是返回a+b的值

    //return a * b;//改成乘法更好玩

    m_acc++;

    return a * b + m_acc;//每次调用我把m_acc++ 并且累加到这个上去

    }

    private:

    int m_acc;

    };

    int Add(int a, int b) {

    return a + b;

    }

    int main() {

    AddFunctor add;//注意 add是一个对象 这不是一个函数

    //这个对象已经重载了括号运算符

    cout << add(5, 6) << endl;//这个函数调用实际上做加法的作用 

    cout << add(5, 6) << endl;//这个函数调用实际上做加法的作用 

    cout << add(5, 6) << endl;//这个函数调用实际上做加法的作用 

    cout << add(5, 6) << endl;//这个函数调用实际上做加法的作用 

    cout << add(5, 6) << endl;//这个函数调用实际上做加法的作用 

    //虽然每次调用都是5 和 6,但是它结果不一样

    //上下一样 上面是仿函数 实际上是一个对象

    //下面的不是仿函数  每次传入之后结果都会一样

    cout << Add(5, 6) << endl;//发现一样

    cout << Add(5, 6) << endl;//发现一样

    cout << Add(5, 6) << endl;//发现一样

    cout << Add(5, 6) << endl;//发现一样

    cout << Add(5, 6) << endl;//发现一样

    //仿函数和实际函数区别就在于 仿函数可以存储状态 存储状态后就可以干各种奇奇怪怪的事情

    return 0;

    }

  • #C++ Day27 November 11 2025

    //Object – Oriented 7 – 1 Operator Overloading

    //运算符重载

    #include <iostream>

    #include <string>

    using namespace std;

    /*

    运算符重载就是对一个已有的运算符进行一个重新定义

    对不同的数据结构有不同处理

    */

    /*

    +

    4+5=9

    class A{

    };

    A a;

    A b;

    a + b;

    */

    int main() {

    //1.加法运算符

    int a = 520;

    int b = 1314;

    cout << a + b << endl; 

    //2.字符串拼接

    string c = “520”;

    string d = “1314”;

    cout << c + d << endl; //实际上就是运算符重载的技术

    string e = “我”;

    string f = “爱你”;

    cout << e + f << endl;

    return 0;

    }

    //Object – Oriented 7 – 2 Plus Operator Overloading

    //加号重载

    #include <iostream>

    using namespace std;

    /*

    +号

    */

    //复数类 实部和虚部 组成  实部和实部相加 虚部跟虚部相加

    class Complex {

    friend Complex operator+(Complex& a, Complex& b);

    //因为real和image是别的函数里面的私有成员,我想用全局函数去访问私有成员的话用友元就可以了

    friend Complex operator-(Complex& a, Complex& b);

    public:

    Complex() :real(0), image(0) {//初始化列表

    }

    Complex(int real, int image) {

    this->real = real;//用this指针避免变量名重复的冲突问题

    this->image = image;

    }

    /*

    Complex add(Complex& other) {//传进来的Complex我们叫它other

    Complex ret;//结不变

    ret.real = this->real + other.real; //自己的实数部分等于实数加实数 this 是指针 需用箭头来获取变量,other是引用 是一个对象 所以用.来获取变量

    ret.image = this->image + other.image;

    return ret;

    }

    */

    /*

    //这里采用成员函数的形式进行加法运算符重载

    Complex operator+(Complex& other) {//传进来的Complex我们叫它other

    Complex ret;//结不变

    ret.real = this->real + other.real; //自己的实数部分等于实数加实数 this 是指针 需用箭头来获取变量,other是引用 是一个对象 所以用.来获取变量

    ret.image = this->image + other.image;

    return ret;

    }

    */

    //a+bi

    void Print() {

    cout << real << ‘+’ << image << ‘i’ << endl;

    }

    private:

    int real;//实部

    int image;//虚部

    };

    //变成全局函数后 变成两个对象相加

    Complex operator+(Complex& a,Complex& b) {

    Complex ret;//结不变

    ret.real = a.real + b.real; //自己的实数部分等于实数加实数 this 是指针 需用箭头来获取变量,other是引用 是一个对象 所以用.来获取变量

    ret.image = a.image + b.image;

    //因为real和image是别的函数里面的私有成员,我想用全局函数去访问私有成员的话用友元就可以了

    return ret;

    }

    Complex operator-(Complex& a, Complex& b) {

    Complex ret;//结不变

    ret.real = a.real – b.real; //自己的实数部分等于实数加实数 this 是指针 需用箭头来获取

    变量,other是引用 是一个对象 所以用.来获取变量

    ret.image = a.image – b.image;

    //因为real和image是别的函数里面的私有成员,我想用全局函数去访问私有成员的话用友元就可以了

    return ret;

    }

    int main() {

    Complex a(10,20);

    Complex b(5, 8); //a和b两个实例化对象

    //Complex c = a.add(b);//实例化一个c,a调用add接口后把b传进去的结果

    //Complex c = a.operator+(b);

    Complex c = a+b; //加法运算符重载 

    c.Print();

    Complex d = a – b;//复数的加减可以用 重载 乘除规格会变掉 所以不行

    d.Print();

    //需要把调用的函数变成一个加号最理想

    return 0;

    }

    //Object – Oriented 7 – 3 Left Shift Overloading

    //左移重载

    #include <iostream>

    using namespace std;

    /*

    Complex c;

    << 左移 目的 就是为了把这个对象输出出来 我要进行输出的话一定会调用cout.operator << (c),operator<<是cout的一个成员函数

    cout.operator << (c)

    //但是我们现在希望它必须是Complex的成员函数

    Complex的成员函数的话c必须得放在左边

    如果这样就会变成 c<<cout 这个达不到想要的效果

    //成员函数的重载  需要用全局函数的

    c.operator <<(cout)

    我们希望cout << c

    所以我们需要它从成员函数里面抽出来 变成一个全局函数

    所以需要用 全局函数作为友元声明

    */

    class Complex {

    friend Complex operator+(Complex& a, Complex& b);

    //因为real和image是别的函数里面的私有成员,我想用全局函数去访问私有成员的话用友元就可以了

    friend Complex operator-(Complex& a, Complex& b);

    //friend void operator<<(ostream& cout, Complex a);

    friend ostream& operator<<(ostream& cout, Complex a);//友元的函数声明也要改过来

    public:

    Complex() :real(0), image(0) {//初始化列表

    }

    Complex(int real, int image) {

    this->real = real;//用this指针避免变量名重复的冲突问题

    this->image = image;

    }

    /*

    Complex add(Complex& other) {//传进来的Complex我们叫它other

    Complex ret;//结不变

    ret.real = this->real + other.real; //自己的实数部分等于实数加实数 this 是指针 需用箭头来获取变量,other是引用 是一个对象 所以用.来获取变量

    ret.image = this->image + other.image;

    return ret;

    }

    */

    /*

    //这里采用成员函数的形式进行加法运算符重载

    Complex operator+(Complex& other) {//传进来的Complex我们叫它other

    Complex ret;//结不变

    ret.real = this->real + other.real; //自己的实数部分等于实数加实数 this 是指针 需用箭头来获取变量,other是引用 是一个对象 所以用.来获取变量

    ret.image = this->image + other.image;

    return ret;

    }

    */

    //a+bi

    void Print() {

    cout << real << ‘+’ << image << ‘i’ << endl;//cout的对象是ostream

    }

    void Printsub() {//减法需要配套的subprint函数

    //减法感觉需要配套的print函数,否则减法后虚部为负数会有类似5 + -8i的情况

    if (image > 0) {

    cout << real << ‘+’ << image << ‘i’ << endl;//复数是正数的情况

    }

    else if (image == 0) {//复数为0的情况

    cout << real << endl;

    }

    else {//复数为负数的情况

    cout << real << image << ‘i’ << endl;

    }

    }

    private:

    int real;//实部

    int image;//虚部

    };

    //变成全局函数后 变成两个对象相加

    Complex operator+(Complex& a, Complex& b) {

    Complex ret;//结不变

    ret.real = a.real + b.real; //自己的实数部分等于实数加实数 this 是指针 需用箭头来获取变量,other是引用 是一个对象 所以用.来获取变量

    ret.image = a.image + b.image;

    //因为real和image是别的函数里面的私有成员,我想用全局函数去访问私有成员的话用友元就可以了

    return ret;

    }

    Complex operator-(Complex& a, Complex& b) {

    Complex ret;//结不变

    ret.real = a.real – b.real; //自己的实数部分等于实数加实数 this 是指针 需用箭头来获取变量,other是引用 是一个对象 所以用.来获取变量

    ret.image = a.image – b.image;

    //因为real和image是别的函数里面的私有成员,我想用全局函数去访问私有成员的话用友元就可以了

    return ret;

    }

    /*

    void operator<<(ostream& cout, Complex a) {//std不用写直接写ostream& cout ,然后把Complex对象传进来

    cout << a.real << ‘+’ << a.image << ‘i’;//私有成员无法直接访问,需要变成一个友元

    }

    */

    //类型改成ostream& 来返回

    ostream& operator<<(ostream& cout, Complex a) {//std不用写直接写ostream& cout ,然后把Complex对象传进来

    cout << a.real << ‘+’ << a.image << ‘i’;//私有成员无法直接访问,需要变成一个友元

    return cout;

    }

    int main() {

    Complex a(10, 20);

    Complex b(5, 8); //a和b两个实例化对象

    //Complex c = a.add(b);//实例化一个c,a调用add接口后把b传进去的结果

    //Complex c = a.operator+(b);

    Complex c = a + b; //加法运算符重载 

    //c.Print();

    Complex d = a – b;//复数的加减可以用重载,复数乘除规则会变掉 所以不行

    //d.Printsub();

    //需要把调用的函数变成一个加号最理想

    //这里的左移c实际上调用了 operator<<(cout,c) 如果我再调左移的话会利用返回值来进行左移的 但是上面的返回值目前是void空类型

    cout << c << endl<<endl; //如果我们希望左移 列式 输出  也就是说能不断地往后面加上左移符号   所以函数再调用完毕后同样需要返回同一个值ostream

    return 0;

    }

    //Object – Oriented 7 – 4 Increment Overloading

    //递增运算符重载

    #include <iostream>

    using namespace std;

    /*

    递增运算符 ++ 的重载

    前置++

    后置++

    复数的++

    其实就是把实部和虚部++

    */

    class Complex {

    friend ostream& operator<<(ostream& c, Complex a);

    public:

    Complex() :real(0), image(0) {}

    Complex(int real, int image) {

    this->real = real;

    this->image = image;

    }

    /*

    void operator++() {//++后面只有一个操作符 不需要其它东西

    //要输出得把类型改成Complex

    this->real += 1;

    }

    */

    /*

    Complex operator++() {//++后面只有一个操作符 不需要其它东西

    //要输出得把类型改成Complex

    this->real += 1;

    return *this;//this指向自己 进行一次解引用就相当于返回了自己

    }

    */

    Complex& operator++() {//++后面只有一个操作符 不需要其它东西 加上引用确保还是原来的对象

    //要输出得把类型改成Complex

    this->real += 1;

    return *this;//this指向自己 进行一次解引用就相当于返回了自己

    }//前置++ 实现了 

    //前置++返回原对象

    Complex operator++(int) {//这里不能加引用 因为这是一个临时对象 这是一个拷贝

    //有参数就是后置++

    //需要先存一个对象

    Complex c = *this;

    this->real += 1;

    return c;

    }//后置++ 实现了

    //后置++返回新的拷贝对象

    private:

    int real;

    int image;

    };

    ostream& operator<<(ostream& c, Complex a) {

    c << a.real << ‘+’ << a.image << ‘i’;

    //cout其实就是我们传进来的c

    return c;

    }

    int main() {

    int x = 1;

    cout << ++(++x) << endl;

    cout << x << endl;//前两个输出的值是一样的

    Complex a(10, 10);

    cout << a << endl;

    //++a;//这里先实现前置++

    cout << ++(++a) << endl;//如果前面不加引用 ++a会生成新对象 跟原来的a对象不一样 没有关系了

    cout << a << endl;

    cout << a++ << endl;//跟a一样的( 输出的倒数第二第三行 一样)

    cout << a << endl;//这一行要多1

    //cout << ((a++)++)++  << endl;

    //cout << a << endl;//要差3

    //int b = 5;

    ////左值就是赋值语句左边出现的表达式 表示为一个有内存的对象 它可以被修改的

    //cout << ((b++)++)++ << endl; //++作为后置++的时候 就不能再调用++了

    ////但这个b++不能被修改 后置的递增运算符返回值不需要是原来的对象

    //cout << b << endl;

    /*

    x = 5;

    x++;//这个对象还是5

    */

    return 0;

    }

  • #C++ Day26 November 8 2025

     Day26 November 10 2025

    //Object – Oriented 6 – 1 Global Function as a friend element

    //全局函数作为友元

    #include <iostream>

    #include <string>

    using namespace std;

    //友元就是一个函数或一个类能够访问另外一个类的私有成员

    /*

    友元的目的

    让一个类或函数

    能够访问另一个类的私有成员

    友元的关键字:friend

    三种友元:

    1.全局函数作为友元

    2.类作为友元

    3.成员函数作为友元

    */

    class People {

    friend void friendVisit(People* p);//把函数拷到这里去 并且加上一个friend

    //友元 让一个函数能够访问另一个类的私有成员

    public:

    People(){

    m_House = “房子”;

    m_Car = “跑车”;

    }

    public://公有 房子

    string m_House;

    private://私有 车子

    string m_Car;

    };

    void friendVisit(People *p) {//好朋友   传一个People指针

    cout << “好朋友来访问你的” << p->m_House << endl;

    cout << “好朋友来访问你的” << p->m_Car << endl;//直接就会报错

    }

    int main() {

    People p;//实例化一个真正的人出来

    friendVisit(&p);//把对象指针传进来

    return 0;

    }

    //Object – Oriented 6 – 2 Class as a friend element

    //类作为友元

    #include <iostream>

    #include <string>

    using namespace std;

    //让一个类去访问另一个类的私有成员

    class People;//先声明下 让PeopleFriend 类能访问到

    class PeopleFriend {

    public:

    PeopleFriend() {//先把构造函数写出来

    }

    /*

    void visit(People *p){//传进来一个People指针    把这个函数的定义变成函数声明

    ////程序是从上往下执行的 所以可能有未定义的问题 看不到车和房子

    //cout << “好朋友来访问你的” << p->m_House << endl;//People类的定义在PeopleFriend的下面  在实现这个函数的时候 东西还没定义出来

    //cout << “好朋友来访问你的” << p->m_Car << endl;//私有成员一般访问不了

    }

    */

    void visit(People* p);

    };

    class People {

    friend class PeopleFriend;//把类作为友元让它的private元素能被朋友类访问 

    public:

    People() {//默认构造函数

    m_House = “房子”;

    m_Car = “跑车”;

    }

    public:

    string m_House;

    private:

    string m_Car;

    };

    void PeopleFriend::visit(People* p) {//把函数拷贝到这里  加上作用域    改成在这里定义函数

    cout << “好朋友来访问你的” << p->m_House << endl;

    cout << “好朋友来访问你的” << p->m_Car << endl;

    }

    int main() {

    People p;

    PeopleFriend pf;

    pf.visit(&p);

    return 0;

    }

    //Object – Oriented 6 – 3 Member function as a friend

    //成员函数作为友元

    #include <iostream>

    #include <string>

    using namespace std;

    //成员函数作为友元

    //PeopleFriend的某个函数能够访问People的私有成员变量

    class People;//先把它声明出来 

    class PeopleFriend {

    public:

    PeopleFriend() {} //只要声明 不要在这里定义了

    void VisitAll(People* p);

    void VisitPub(People* p);

    };

    class People {

    //friend class PeopleFriend; //把这个类作为友元 但是这里不能这么写 不然就两个都能访问了

    friend void PeopleFriend::VisitAll(People* p);//函数作为友元就保证了只有某些函数可以访问相应的成员变量

    //希望只有某些函数能够访问另外一个类的私有成员变量 希望能够访问的那些函数  我给它变成它的友元就好了

    public:

    People() {

    m_House = “别墅”;

    m_Car = “跑车”;

    }

    public:

    string m_House;

    private:

    string m_Car;

    };

    void PeopleFriend::VisitAll(People* p) {//加上作用域 这个要能访问两个 私有和公共

    cout << “好朋友访问了你的” << p->m_House << endl;//好朋友访问了你的公共属性

    cout << “好朋友访问了你的” << p->m_Car << endl;//类不是它的友元的话 访问不到它的私有成员变量的

    //既要访问你的别墅 又要访问你的跑车

    };

    void PeopleFriend::VisitPub(People* p) {//访问公共属性 这个只能访问公共的

    cout << “好朋友访问了你的” << p->m_House << endl;//好朋友访问了你的公共属性

    //cout << “好朋友访问了你的” << p->m_Car << endl;

    };

    int main() {

    People p;

    PeopleFriend pf;

    pf.VisitAll(&p);

    pf.VisitPub(&p);

    return 0;

    }