面向对象的三要素:封装继承多态
面向对象:首先要找到对象,一个是大象对象,一个是冰箱对象,如果想要把大象这个对象装进去,首先需要让冰箱这个对象把门打开让大象这个对象进入到冰箱内部,在让冰箱这个对象把冰箱门关上。
对象是什么,对象其实就是类的实例,什么是类呢,就是具有相同特征的元素的集合,比如说能够制冷、能够保险的就称之为冰箱
面向对象就是找到具有相同特征或特点的一类事物,然后把它抽象成一个类,然后再给这个类创建实例,有了这个实例之后我们就可以基于这个实例进行相关的操作,这个实例有属性和方法,属性就是它的特点,方法就是它对应的行为
什么是面向对象
面向过程一般省略主语
面向过程下面每句话就对应着一个操作函数
所谓的面向对象编程就是先要把属性和行为剥离出来,剥离出来之后看它属于谁,然后再基于这个行为属性去定义一个类,这个类就是若干个行为和属性的抽象,得到这个整体也就是这个类的时候,我们再给这个整体去创建它的实例,也就是对象,有了这个对象之后就可以通过这个对象来调用这个类里面定义的行为和属性了,这种编程方式就叫做面向对象
面向对象的三要素:封装继承多态
封装
所谓的封装就是把一些属性也就是成员变量还有一系列的行为也就是成员函数把它们封装到一个类里面,然后再给这个类创建实例,得到这个实例之后就可以通过这个实例来的调用类内部的成员函数
将属性设置成私有或受保护的就是让类成员变量或者函数在类外部不可见
第一个版本
无注释版
cpp#include <iostream> #include <string> using namespace std; # if 1 class NiuLang { public: void searchNiu(); void meeting(string name); void getMarried(string name); string getNiu(); string getName(); private: string m_niu; }; class ZhiNv { public: void searchFangChe(); void meeting(string name); void getMarried(string name); string getFangChe(); string getName(); private: string m_fangChe; }; class HongNiang { public: void doSomething(string niu, string fangChe); void seekHelp(string name); // 求助:找牛郎借牛,找织女借纺车 }; /* 故事梗概: 很久很久以前,天上的织女思凡下界,看上了在河边放牛的牛郎。 柳树精看穿了织女的心思,于是想成全他们。 柳树精偷偷牵走了牛郎的牛,将其栓在河边吃草。 柳树精偷偷偷走了织女的纺车,将其放到了牛郎的牛车上。 牛郎去找牛。。。 织女去找纺车。。。 牛郎和织女见面,一见钟情! 牛郎和织女结婚了! */ int main() { //////////////////////// 面向过程 ////////////////////// qianNiu("HongNiang", "XiaoHeBian"); zhuangChe("HongNiang", "FangChe"); searchNiu("NiuLang"); searchFangChe("ZhiNv"); meet("NiuLang", "ZhiNv"); getMarried("NiuLang", "ZhiNv"); //////////////////////// 面向对象 ////////////////////// NiuLang* niuLang = new NiuLang; ZhiNv* zhiNv = new ZhiNv; HongNiang* hongNiang = new HongNiang; string niu = niuLang->getNiu(); string fangChe = zhiNv->getFangChe(); hongNiang->doSomething(niu, fangChe); hongNiang->seekHelp(niuLang->getName()); hongNiang->seekHelp(zhiNv->getName()); niuLang->searchNiu(); zhiNv->searchFangChe(); niuLang->meeting(zhiNv->getName()); zhiNv->meeting(niuLang->getName()); niuLang->getMarried(zhiNv->getName()); zhiNv->getMarried(niuLang->getName()); return 0; } #endif有注释版
cpp// ============================================================================ // 包含头文件 // ============================================================================ // 包含标准输入输出流头文件 // 用于 cout、endl 等控制台输出 #include <iostream> // 包含字符串头文件 // 用于 std::string 类型 #include <string> // 使用标准命名空间(简化 std:: 前缀) using namespace std; // ============================================================================ // 条件编译指令 // ============================================================================ // #if 1 表示下面的代码会被编译 // 如果改成 #if 0,这段代码会被预处理器跳过(相当于注释掉) // 常用于调试时临时屏蔽大段代码 # if 1 // ============================================================================ // NiuLang(牛郎)类 // ============================================================================ class NiuLang { public: // ---- 公有方法(对外接口) ---- // 找牛 void searchNiu(); // 见面(传入对方名字) void meeting(string name); // 结婚(传入对方名字) void getMarried(string name); // 获取牛(返回牛的字符串描述) string getNiu(); // 获取名字(返回牛郎的名字) string getName(); private: // ---- 私有成员变量 ---- // 牛的描述(如 "老黄牛") string m_niu; }; // ============================================================================ // ZhiNv(织女)类 // ============================================================================ class ZhiNv { public: // ---- 公有方法(对外接口) ---- // 找纺车 void searchFangChe(); // 见面(传入对方名字) void meeting(string name); // 结婚(传入对方名字) void getMarried(string name); // 获取纺车(返回纺车的字符串描述) string getFangChe(); // 获取名字(返回织女的名字) string getName(); private: // ---- 私有成员变量 ---- // 纺车的描述(如 "金纺车") string m_fangChe; }; // ============================================================================ // HongNiang(红娘)类 // ============================================================================ class HongNiang { public: // ---- 公有方法(对外接口) ---- // 做某事:接收牛和纺车,进行配对 // 参数:niu - 牛的描述,fangChe - 纺车的描述 void doSomething(string niu, string fangChe); // 求助:找人帮忙 // 参数:name - 需要帮助的人的名字 // 注释说明:找牛郎借牛,找织女借纺车 void seekHelp(string name); }; /* // ============================================================================ // 故事梗概(多行注释) // ============================================================================ 故事梗概: 很久很久以前,天上的织女思凡下界,看上了在河边放牛的牛郎。 柳树精看穿了织女的心思,于是想成全他们。 柳树精偷偷牵走了牛郎的牛,将其栓在河边吃草。 柳树精偷偷偷走了织女的纺车,将其放到了牛郎的牛车上。 牛郎去找牛。。。 织女去找纺车。。。 牛郎和织女见面,一见钟情! 牛郎和织女结婚了! */ // ============================================================================ // 主函数:程序入口 // ============================================================================ int main() { // ======================================================================== // 面向过程方式(被条件编译包含,会被编译) // 特点:用函数调用来描述流程,数据和处理分离 // 这些函数需要提前定义,但代码中没有给出实现 // ======================================================================== // 红娘牵牛到小河边 qianNiu("HongNiang", "XiaoHeBian"); // 红娘装车放纺车 zhuangChe("HongNiang", "FangChe"); // 牛郎找牛 searchNiu("NiuLang"); // 织女找纺车 searchFangChe("ZhiNv"); // 牛郎和织女见面 meet("NiuLang", "ZhiNv"); // 牛郎和织女结婚 getMarried("NiuLang", "ZhiNv"); // ======================================================================== // 面向对象方式(核心演示部分) // 特点:数据(成员变量)和方法(成员函数)封装在对象内部 // 通过对象之间的协作来完成故事流程 // ======================================================================== // ---- 1. 创建三个对象 ---- // 在堆上创建牛郎对象,返回指针 NiuLang* niuLang = new NiuLang; // 在堆上创建织女对象,返回指针 ZhiNv* zhiNv = new ZhiNv; // 在堆上创建红娘对象,返回指针 HongNiang* hongNiang = new HongNiang; // ---- 2. 获取数据 ---- // 牛郎把自己的牛拿出来 // 假设 getNiu() 返回 "老黄牛" string niu = niuLang->getNiu(); // 织女把自己的纺车拿出来 // 假设 getFangChe() 返回 "金纺车" string fangChe = zhiNv->getFangChe(); // ---- 3. 红娘做事 ---- // 红娘拿着牛和纺车去做媒(配对) hongNiang->doSomething(niu, fangChe); // 红娘主动帮助牛郎(求助) hongNiang->seekHelp(niuLang->getName()); // 红娘主动帮助织女(求助) hongNiang->seekHelp(zhiNv->getName()); // ---- 4. 各自寻找 ---- // 牛郎去找自己的牛 niuLang->searchNiu(); // 织女去找自己的纺车 zhiNv->searchFangChe(); // ---- 5. 见面 ---- // 牛郎和织女见面 // 注意:这里调用了两次见面(双向),可能重复 niuLang->meeting(zhiNv->getName()); zhiNv->meeting(niuLang->getName()); // ---- 6. 结婚 ---- // 牛郎和织女结婚 // 注意:这里调用了两次结婚(双向),可能重复 niuLang->getMarried(zhiNv->getName()); zhiNv->getMarried(niuLang->getName()); // ---- 7. 程序结束 ---- // 返回 0 表示程序正常结束 return 0; } // #endif 结束条件编译 #endif继承
继承是怎么回事呢,就是我们在定义类的时候,子类可以继承父类的一些属性,它只能够继承父类公共的成员函数或者是成员变量以及受保护的成员函数或者成员变量,私有的函数和变量是继承不了的,关于继承不了是从使用层面来说的,不是从内存角度来说,从内存角度来说子类依然继承了父类的私有属性,但是无法使用。
通过继承可以实现代码的重用。
使用面向对象封装之后函数的参数相较于面向过程的函数参数会变少,因为这个参数可以被封装到类内部作为成员变量被成员函数直接使用。
代码
无注释版
cpp#include <iostream> using namespace std; #if 2 class Person { public: void meeting(string name) { cout << "遇见了" << name << endl; } void getMarried() { cout << "哈哈,成亲了,么么哒!" << endl; } }; class NiuLang :public Person { public: void searchNiu() { cout << "牛郎去找牛了..." << endl; } string getNiu() { return m_niu; } string getName() { return "二牛"; } private: string m_niu; }; class ZhiNv :public Person { public: void searchFangChe() { cout << "织女去找纺车了..." << endl; } string getFangChe() { return m_fangChe; } string getName() { return "巧儿"; } private: string m_fangChe; }; class HongNiang { public: HongNiang(NiuLang* niuLang, ZhiNv* zhiNv) : m_niuLang(niuLang), m_zhiNv(zhiNv) {} void doSomething() { cout << "牵牛到河边" << m_niuLang->getNiu() << endl; cout << "纺车装牛车上" << m_zhiNv->getFangChe() << endl; } void seekHelp() { m_niuLang->searchNiu(); m_zhiNv->searchFangChe(); } private: NiuLang* m_niuLang = nullptr; ZhiNv* m_zhiNv = nullptr; }; int main() { NiuLang* niuLang = new NiuLang; ZhiNv* zhiNv = new ZhiNv; HongNiang* hongNiang = new HongNiang(niuLang, zhiNv); hongNiang->doSomething(); hongNiang->seekHelp(); niuLang->meeting(zhiNv->getName()); zhiNv->meeting(niuLang->getName()); niuLang->getMarried(); zhiNv->getMarried(); return 0; } #endif有注释版
cpp// ============================================================================ // 包含头文件 // ============================================================================ // 包含标准输入输出流头文件 // 用于 cout、endl 等控制台输出 #include <iostream> // 使用标准命名空间(简化 std:: 前缀) using namespace std; // ============================================================================ // 条件编译指令 // ============================================================================ // #if 2 表示下面的代码会被编译 // 数字只是标识,非 0 即编译 // 可以改为 #if 0 来临时屏蔽整段代码 #if 2 // ============================================================================ // Person(人类)基类 // 作为牛郎和织女的共同基类 // 提取了共同的属性和行为 // ============================================================================ class Person { public: // ---- 见面方法 ---- // 功能:输出见面信息 // 参数:name - 对方的名字 // 注意:这个方法没有加 virtual,但足够演示继承的基本用法 void meeting(string name) { // 输出:遇见了[对方名字] cout << "遇见了" << name << endl; } // ---- 结婚方法 ---- // 功能:输出结婚信息 // 注意:子类 NiuLang 和 ZhiNv 都继承了此方法 // 所以两个对象都可以调用 getMarried() void getMarried() { // 输出结婚信息 cout << "哈哈,成亲了,么么哒!" << endl; } }; // ============================================================================ // NiuLang(牛郎)类 // 公有继承自 Person // 拥有 Person 的所有公有方法,并扩展了自己的专属方法 // ============================================================================ class NiuLang : public Person { public: // ---- 找牛方法 ---- // 功能:输出找牛的信息 void searchNiu() { cout << "牛郎去找牛了..." << endl; } // ---- 获取牛 ---- // 功能:返回牛的描述 // 返回值:string 类型的 m_niu // 注意:当前 m_niu 从未被赋值,返回的是空字符串 string getNiu() { return m_niu; } // ---- 获取名字 ---- // 功能:返回牛郎的名字 // 返回值:"二牛" string getName() { return "二牛"; } private: // ---- 私有成员变量 ---- // 牛的描述(如 "老黄牛") // ⚠️ 问题:这个变量从未被赋值,始终为空字符串 string m_niu; }; // ============================================================================ // ZhiNv(织女)类 // 公有继承自 Person // 拥有 Person 的所有公有方法,并扩展了自己的专属方法 // ============================================================================ class ZhiNv : public Person { public: // ---- 找纺车方法 ---- // 功能:输出找纺车的信息 void searchFangChe() { cout << "织女去找纺车了..." << endl; } // ---- 获取纺车 ---- // 功能:返回纺车的描述 // 返回值:string 类型的 m_fangChe // ⚠️ 注意:当前 m_fangChe 从未被赋值,返回的是空字符串 string getFangChe() { return m_fangChe; } // ---- 获取名字 ---- // 功能:返回织女的名字 // 返回值:"巧儿" string getName() { return "巧儿"; } private: // ---- 私有成员变量 ---- // 纺车的描述(如 "金纺车") // ⚠️ 问题:这个变量从未被赋值,始终为空字符串 string m_fangChe; }; // ============================================================================ // HongNiang(红娘)类 // 负责撮合牛郎和织女 // 这是"中介者模式"的简单体现 // ============================================================================ class HongNiang { public: // ---- 构造函数 ---- // 功能:接收牛郎和织女的指针,并保存到成员变量 // 参数:niuLang - 牛郎对象指针,zhiNv - 织女对象指针 // 使用初始化列表初始化成员变量 // 相当于:红娘"认识"了牛郎和织女 HongNiang(NiuLang* niuLang, ZhiNv* zhiNv) : m_niuLang(niuLang) , m_zhiNv(zhiNv) { // 构造函数体为空,所有初始化在初始化列表中完成 } // ---- 做某事方法 ---- // 功能:红娘牵线搭桥 // 1. 把牛郎的牛牵到河边 // 2. 把织女的纺车装到牛车上 void doSomething() { // 从牛郎那里获取牛,并输出 // ⚠️ 注意:因为 m_niu 未赋值,这里会输出空字符串 cout << "牵牛到河边" << m_niuLang->getNiu() << endl; // 从织女那里获取纺车,并输出 // ⚠️ 注意:因为 m_fangChe 未赋值,这里会输出空字符串 cout << "纺车装牛车上" << m_zhiNv->getFangChe() << endl; } // ---- 求助方法 ---- // 功能:红娘让牛郎和织女各自去找自己的东西 // 相当于:红娘发布了指令,牛郎去找牛,织女去找纺车 void seekHelp() { // 让牛郎找牛 m_niuLang->searchNiu(); // 让织女找纺车 m_zhiNv->searchFangChe(); } private: // ---- 私有成员变量 ---- NiuLang* m_niuLang = nullptr; // 牛郎对象指针 ZhiNv* m_zhiNv = nullptr; // 织女对象指针 }; // ============================================================================ // 主函数:程序入口 // ============================================================================ int main() { // ---- 1. 创建三个对象 ---- // 在堆上创建牛郎对象 NiuLang* niuLang = new NiuLang; // 在堆上创建织女对象 ZhiNv* zhiNv = new ZhiNv; // 在堆上创建红娘对象,传入牛郎和织女的指针 // 红娘从此"认识"了他们两个 HongNiang* hongNiang = new HongNiang(niuLang, zhiNv); // ---- 2. 红娘撮合 ---- // 红娘牵牛、装纺车 hongNiang->doSomething(); // 红娘求助,让牛郎织女各自寻找 hongNiang->seekHelp(); // ---- 3. 牛郎和织女见面 ---- // 牛郎和织女互相见面 // 调用了从 Person 继承的 meeting() 方法 niuLang->meeting(zhiNv->getName()); // 牛郎遇见了织女("巧儿") zhiNv->meeting(niuLang->getName()); // 织女遇见了牛郎("二牛") // ---- 4. 牛郎和织女结婚 ---- // 双方都调用了从 Person 继承的 getMarried() 方法 niuLang->getMarried(); zhiNv->getMarried(); // ---- 5. 程序结束 ---- // ⚠️ 注意:这里有内存泄漏!new 了 3 个对象但没有 delete // 应该添加:delete niuLang; delete zhiNv; delete hongNiang; return 0; } // #endif 结束条件编译 #endif类继承关系图
cpp┌─────────────────────────────────────────────────────────────────┐ │ Person(基类) │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ + meeting(string name) // 见面 │ │ │ │ + getMarried() // 结婚 │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ 公有继承 │ 公有继承 ▼ ▼ ┌────────────────────────────────┐ ┌────────────────────────────────┐ │ NiuLang(牛郎) │ │ ZhiNv(织女) │ │ ┌──────────────────────────┐ │ │ ┌──────────────────────────┐ │ │ │ + searchNiu() │ │ │ │ + searchFangChe() │ │ │ │ + getNiu() │ │ │ │ + getFangChe() │ │ │ │ + getName() │ │ │ │ + getName() │ │ │ │ - m_niu │ │ │ │ - m_fangChe │ │ │ └──────────────────────────┘ │ │ └──────────────────────────┘ │ └────────────────────────────────┘ └────────────────────────────────┘ │ │ │ │ └──────────────┬─────────────────────┘ │ 被红娘聚合 ▼ ┌────────────────────────────────┐ │ HongNiang(红娘) │ │ ┌──────────────────────────┐ │ │ │ + doSomething() │ │ │ │ + seekHelp() │ │ │ │ - m_niuLang (指针) │ │ │ │ - m_zhiNv (指针) │ │ │ └──────────────────────────┘ │ └────────────────────────────────┘核心知识点
概念 在这段代码中的体现 继承 NiuLang和ZhiNv都继承自Person多态 通过基类指针可以调用子类重写的方法 封装 m_niu和m_fangChe是私有的,通过getXxx()访问组合 HongNiang包含NiuLang*和ZhiNv*指针依赖注入 红娘在构造函数中接收牛郎和织女的指针 继承和组合的区别是什么?
NiuLang 继承 Person(is-a 关系)
HongNiang 组合 NiuLang* 和 ZhiNv*(has-a 关系)
meeting() 和 getMarried() 为什么不需要在子类中重新实现?
因为子类继承了基类的公有方法,可以直接使用
红娘为什么要用指针而不是直接包含对象?
因为牛郎和织女是独立创建的对象,红娘只是"引用"它们,不是"拥有"它们 。
多态
多态就是同名函数具有不同的状态,怎么能够有不同的状态呢,就是函数内部的处理动作不一样
多态有几要素,首先需要发生继承,即有子类和父类,然后在父类里面需要有虚函数,子类继承父类的时候需要在子类重写父类的虚函数,什么是重写,就是把函数体重新定义一遍,如果有需求就重新定义,如果没有需求就不需要,如果这个父类有多个子类,比如说有两个,那么我们在这两个子类里分别重写了父类的虚函数,并且处理的动作不一样,这样同名函数在调用的时候行为就不一样了,第三步就是通过父类的指针或者是引用,让它们指向子类的对象,通过父类指针或引用去调用子类重写也就是从父类继承过来的虚函数。
所谓多态就是在编译器处理的时候做了一个动态绑定,这个动态绑定就是在最终调用的时候才去确定这个指针指向的对象是谁,而不是在编译的时候去指定最终要调用的是谁
编译期就确定调用哪个函数体→ 静态绑定;运行时才确定调用哪个函数体->动态绑定。
原始版
cpp#include <iostream> using namespace std; #if 1 class Person { public: void meeting(Person* person) { //相互见面 if (m_person == nullptr) { m_person = person; person->meeting(this); } } void getMarried() { if (m_flag) { m_flag = false; marry(); m_person->getMarried(); } } //这些虚函数都是纯虚函数 //如果把这些虚函数定义成纯虚函数 //意味着这些虚函数迫切要求在子类中进行重写 //如果子类不重写这些纯虚函数的话 //子类不能够进行对象的实例化,这个子类也是个抽象类 //所谓抽象类就是指类的内部有纯虚函数 //含有纯虚函数的类就称之为抽象类 //如果是抽象类,那么这个类是不能够实例化的 //也就是不能够给这个抽象类去new一个实例对象 //如果这个虚函数不是纯虚函数,这个虚函数可以不在子类进行重写 virtual string getName() = 0; virtual void search() = 0; virtual string getCaiChan() = 0; virtual void marry() = 0; protected: //受保护的指针可以被子类继承 Person* m_person = nullptr; bool m_flag = true; }; class NiuLang :public Person { public: //override的作用,在函数后面加override可以检测函数重写父类的虚函数名字对不对,如果没加override子类名字写错不会报错,加了override可以检测出 void search() override { cout << "我的牛丢了,我去找牛..." << endl; } string getCaiChan()override { return m_niu; } string getName() override { return "二牛"; } void marry()override { cout << "我要娶织女了,没想到还能娶到这么漂亮的媳妇儿..." << endl; } private: string m_niu = "五彩神牛"; }; class ZhiNv :public Person { public: void search() override { cout << "我的纺车丢了,我去找纺车..." << endl; } string getCaiChan()override { return m_fangChe; } string getName()override { return "巧儿"; } void marry()override { cout << "从天上跑下来结了个婚,不知道什么时候被带回去。。。" << endl; } private: string m_fangChe = "七彩虹纺车"; }; class HongNiang { public: HongNiang(Person* niuLang, Person* zhiNv):m_niuLang(niuLang), m_zhiNv(zhiNv){} void doSomething() { cout << "牵牛到河边" << m_niuLang->getCaiChan() << endl; cout << "纺车装牛车上" << m_zhiNv->getCaiChan() << endl; } void seekHelp() { m_niuLang->search(); m_zhiNv->search(); } private: Person* m_niuLang; Person* m_zhiNv; }; int main() { NiuLang* niuLang = new NiuLang; ZhiNv* zhiNv = new ZhiNv; HongNiang* hongNiang = new HongNiang(niuLang, zhiNv); hongNiang->doSomething(); hongNiang->seekHelp(); niuLang->meeting(zhiNv); niuLang->getMarried(); return 0; } #endif有注释版
cpp/** * ============================================================ * 文件:MarriageSimulation.cpp * 功能:模拟牛郎织女相遇结婚的完整流程 * 核心考点:纯虚函数、抽象类、多态、虚函数表、向上转型 * 设计模式:依赖倒置原则(红娘依赖Person抽象接口) * ============================================================ */ // 包含输入输出流库,提供cout、endl等标准输入输出功能 #include <iostream> // 使用标准命名空间std,避免每次调用标准库时加std::前缀 // 注意:大型项目中不推荐使用,容易引起命名冲突 using namespace std; // 条件编译指令:#if 1 表示编译以下代码块,#if 0 则跳过 // 常用于调试开关或跨平台代码的版本控制 #if 1 /** * 【抽象基类】Person * 定义了"人"的统一接口,包含4个纯虚函数 * 面试核心:含有纯虚函数的类称为抽象类,不能实例化 */ class Person { public: /** * 【双向绑定函数】两个人见面建立联系 * 参数:person - 见到的另一个人(基类指针) * 功能:实现A认识B的同时,B也认识A的双向绑定 * 面试坑:存在递归调用,必须用if防止无限循环 */ void meeting(Person* person) { // 相互见面 // 检查自己是否已有伴侣(防止三角恋和无限递归) if (m_person == nullptr) { // 步骤1:把对方记录为自己的伴侣 m_person = person; // 步骤2:调用对方的meeting,传入自己(this指针) // 关键:让双向绑定成立 // 危险:如果没有外层if保护,这里会互相调用导致栈溢出 person->meeting(this); } // 如果m_person != nullptr,说明已有伴侣,忽略这次见面 } /** * 【结婚流程】触发结婚仪式 * 改进:使用成员变量m_flag替代static变量,每个对象独立 * 问题:m_person->getMarried()没有判空保护,可能崩溃 */ void getMarried() { // 检查是否还未结婚(m_flag为true表示未婚) if (m_flag) { // 标记为已结婚/正在结婚,防止重复执行 m_flag = false; // 【多态调用点】调用marry()虚函数 // 运行时通过vptr找到实际对象的marry()函数 marry(); // 让伴侣也执行结婚(递归调用) // ⚠️ 潜在风险:如果m_person为nullptr,这里会崩溃! m_person->getMarried(); } // 如果m_flag == false,说明已经结婚了,直接返回 } // ==================== 纯虚函数(接口契约) ==================== /** * 以下4个是纯虚函数(=0),构成抽象类 * 子类必须全部实现,否则子类仍是抽象类,无法实例化 * 设计意图:强制每个具体的人提供姓名、寻物、财产、婚礼仪式 */ /** 纯虚函数:获取姓名 */ virtual string getName() = 0; /** 纯虚函数:寻找行为(如找牛、找纺车) */ virtual void search() = 0; /** 纯虚函数:获取财产(如神牛、纺车) */ virtual string getCaiChan() = 0; /** 纯虚函数:结婚仪式(不同人有不同的誓言) */ virtual void marry() = 0; /** * 【虚析构函数】确保派生类正确析构 * 面试必问:防止delete基类指针时只调用基类析构,导致子类资源泄漏 */ virtual ~Person() {} protected: /** * 【受保护成员变量1】伴侣指针 * protected修饰:可以被子类继承和访问 * 初始化为nullptr,表示单身状态 */ Person* m_person = nullptr; /** * 【受保护成员变量2】结婚状态标志 * ✅ 改进:从static bool改为成员变量 * 每个对象有自己的m_flag,互不干扰,可服务多对情侣 */ bool m_flag = true; }; /** * 【派生类1】牛郎 * 继承自Person,实现所有纯虚函数 * override关键字:检测函数签名是否正确,防止写错函数名 */ class NiuLang : public Person { public: /** * override的作用: * 不加override:如果函数名写错(如seach),编译器认为这是新函数 * 加了override:编译器检查父类是否有同签名虚函数,没有则报错 * 推荐在重写虚函数时总是加上override */ void search() override { // 牛郎特有的寻物行为:找牛 cout << "我的牛丢了,我去找牛..." << endl; } /** 获取牛郎的财产:返回五彩神牛 */ string getCaiChan() override { return m_niu; } /** 获取牛郎的名字:二牛 */ string getName() override { return "二牛"; } /** 牛郎的结婚誓言(幽默风格) */ void marry() override { cout << "我要娶织女了,没想到还能娶到这么漂亮的媳妇儿..." << endl; } private: /** 牛郎的私有财产:五彩神牛 */ string m_niu = "五彩神牛"; }; /** * 【派生类2】织女 * 继承自Person,实现所有纯虚函数 * 不同子类有不同的行为,体现多态 */ class ZhiNv : public Person { public: /** 织女特有的寻物行为:找纺车 */ void search() override { cout << "我的纺车丢了,我去找纺车..." << endl; } /** 获取织女的财产:七彩虹纺车 */ string getCaiChan() override { return m_fangChe; } /** 获取织女的名字:巧儿 */ string getName() override { return "巧儿"; } /** 织女的结婚誓言(带点无奈和幽默) */ void marry() override { cout << "从天上跑下来结了个婚,不知道什么时候被带回去。。。" << endl; } private: /** 织女的私有财产:七彩虹纺车 */ string m_fangChe = "七彩虹纺车"; }; /** * 【红娘类】负责牵线搭桥 * 面试核心:依赖抽象(Person*)而非具体实现 * 符合依赖倒置原则和开闭原则 */ class HongNiang { public: /** * 【构造函数】接收两个Person指针 * 面试核心:这里发生了【向上转型(Upcasting)】 * 传入NiuLang*和ZhiNv*(子类指针) * 接收Person*(基类指针) * 这是多态的前提条件! * * 初始化列表:比在函数体内赋值更高效 */ HongNiang(Person* niuLang, Person* zhiNv) : m_niuLang(niuLang), m_zhiNv(zhiNv) { // 构造函数体为空 } /** * 【做事情】帮助双方展示财产 * 面试核心:通过基类指针调用虚函数,触发【动态绑定】 * 虽然m_niuLang是Person*类型,但指向NiuLang对象 * 所以调用getCaiChan()时,实际执行NiuLang::getCaiChan() */ void doSomething() { // 【多态调用】m_niuLang->getCaiChan() // 运行时通过vptr找到NiuLang::getCaiChan(),返回"五彩神牛" cout << "牵牛到河边" << m_niuLang->getCaiChan() << endl; // 【多态调用】m_zhiNv->getCaiChan() // 运行时通过vptr找到ZhiNv::getCaiChan(),返回"七彩虹纺车" cout << "纺车装牛车上" << m_zhiNv->getCaiChan() << endl; } /** * 【寻求帮助】让双方各自寻找自己的东西 * 再次展示多态:同一个search()调用,产生不同行为 */ void seekHelp() { // 牛郎找牛(执行NiuLang::search()) m_niuLang->search(); // 织女找纺车(执行ZhiNv::search()) m_zhiNv->search(); } private: /** 基类指针指向男方(实际类型是NiuLang*) */ Person* m_niuLang; /** 基类指针指向女方(实际类型是ZhiNv*) */ Person* m_zhiNv; }; /** * 【主函数】程序入口 * 展示完整的创建、交互、结婚流程 */ int main() { // ========== 1. 创建对象 ========== // 创建牛郎对象(堆内存),不能创建Person(抽象类) NiuLang* niuLang = new NiuLang; // 创建织女对象(堆内存) ZhiNv* zhiNv = new ZhiNv; // 【多态的关键】创建红娘,传入子类指针 // 发生向上转型:NiuLang* → Person*,ZhiNv* → Person* HongNiang* hongNiang = new HongNiang(niuLang, zhiNv); // ========== 2. 红娘帮忙 ========== // 展示财产(多态调用getCaiChan) // 输出:牵牛到河边五彩神牛 // 纺车装牛车上七彩虹纺车 hongNiang->doSomething(); // 各自寻找(多态调用search) // 输出:我的牛丢了,我去找牛... // 我的纺车丢了,我去找纺车... hongNiang->seekHelp(); // ========== 3. 牛郎织女见面 ========== // 双向绑定:牛郎记住织女,织女记住牛郎 // meeting不是虚函数,不涉及多态 niuLang->meeting(zhiNv); // ========== 4. 结婚 ========== // 牛郎发起结婚,内部多态调用marry() // 输出:我要娶织女了... // 从天上跑下来结了个婚... niuLang->getMarried(); // ⚠️ 内存泄漏:没有任何delete语句! // 应该使用智能指针或在程序结束前delete return 0; } #endif完整执行流程图
cpp┌─────────────────────────────────────────────────────────────────┐ │ main() 开始 │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌───────────────────────────────┐ │ 1. 创建对象 │ │ new NiuLang() → niuLang │ │ new ZhiNv() → zhiNv │ │ new HongNiang(niuLang,zhiNv)│ └───────────────────────────────┘ │ ▼ ┌───────────────────────────────┐ │ 2. 红娘展示财产 │ │ hongNiang->doSomething() │ │ ├─ getCaiChan() [多态] │ │ │ → NiuLang::getCaiChan() │ │ └─ getCaiChan() [多态] │ │ → ZhiNv::getCaiChan() │ └───────────────────────────────┘ │ ▼ ┌───────────────────────────────┐ │ 3. 红娘帮助寻找 │ │ hongNiang->seekHelp() │ │ ├─ search() [多态] │ │ │ → NiuLang::search() │ │ └─ search() [多态] │ │ → ZhiNv::search() │ └───────────────────────────────┘ │ ▼ ┌───────────────────────────────┐ │ 4. 牛郎织女见面 │ │ niuLang->meeting(zhiNv) │ │ ├─ m_person = zhiNv │ │ └─ zhiNv->meeting(niuLang) │ │ └─ m_person = niuLang │ └───────────────────────────────┘ │ ▼ ┌───────────────────────────────┐ │ 5. 牛郎发起结婚 │ │ niuLang->getMarried() │ │ ├─ m_flag = false │ │ ├─ marry() [多态] │ │ │ → NiuLang::marry() │ │ └─ m_person->getMarried() │ │ ├─ m_flag = false │ │ ├─ marry() [多态] │ │ │ → ZhiNv::marry() │ │ └─ m_person->getMarried()│ │ (牛郎m_flag=false返回)│ └───────────────────────────────┘ │ ▼ ┌─────────────┐ │ return 0 │ │ 程序结束 │ └─────────────┘ ⚠️ 注意:缺少delete,存在内存泄漏!多态
cpp【编译期】编译器看到 Person* 类型,但不关心它指向谁 ↓ 【运行期】niuLang->meeting(zhiNv) 执行时: ↓ 1. niuLang 是 NiuLang*,但 meeting() 继承自 Person 2. meeting() 内部调用 marry() ------ 注意!marry() 是虚函数 3. 编译器生成代码:通过 this 指针的 vptr 找到 vtable 4. 因为 this 实际指向 NiuLang 对象,所以 vptr 指向 NiuLang 的 vtable 5. vtable 中 marry 槽位存储的是 NiuLang::marry() 的地址 6. 执行 NiuLang::marry() ------ 多态生效! ↓ 【关键证据】输出 "我要娶织女了..." ------ 这是 NiuLang 的版本,不是 Person 的多态的完整调用链
cpp【内存布局】 栈/堆 代码段 ┌─────────────────────────┐ ┌──────────────────────────────┐ │ NiuLang 对象 (niuLang) │ │ NiuLang 的 vtable │ │ ┌─────────────────────┐ │ │ ┌──────────────────────────┐ │ │ │ vptr ────────────────┼─┼─────────────>│ │ [0] → NiuLang::getName │ │ │ ├─────────────────────┤ │ │ │ [1] → NiuLang::search │ │ │ │ m_niu = "五彩神牛" │ │ │ │ [2] → NiuLang::getCaiChan│ │ │ └─────────────────────┘ │ │ │ [3] → NiuLang::marry │ │ └─────────────────────────┘ │ └──────────────────────────┘ │ ↑ └──────────────────────────────┘ │ (向上转型,vptr不变) ┌─────────────────────────┐ │ Person* pNiu = niuLang │ ← 编译期只知道这是 Person*,运行时才解析 └─────────────────────────┘ │ │ 调用 pNiu->marry() ▼ ┌─────────────────────────────────────────────────────────────┐ │ 运行时步骤: │ │ 1. 读取 pNiu 指向的对象头部的 vptr │ │ 2. 根据 vptr 找到 NiuLang 的 vtable │ │ 3. 从 vtable 的第 3 个槽位取出 marry 函数地址 │ │ 4. 跳转到 NiuLang::marry() 执行 │ │ 5. 输出 "我要娶织女了..." │ └─────────────────────────────────────────────────────────────┘最终面试回答模板
面试官:"你的代码中多态是怎么实现的?能讲一下底层原理吗?"
你的回答:
"多态的实现依赖于继承、虚函数和基类指针/引用三个要素。
在我的代码中,
HongNiang类持有Person*指针,通过构造函数接收NiuLang*和ZhiNv*子类对象,完成向上转型。底层原理是:每个对象头部有一个虚指针(vptr) ,指向所属类的虚函数表(vtable) 。当我通过
Person*调用search()或getCaiChan()等虚函数时,编译器会生成通过 vptr → vtable → 函数地址 间接调用的代码。因为vptr在构造时被初始化为指向实际对象所属类的虚表,所以运行时能正确找到并执行子类的函数版本。这实现了**'一个接口,多种实现'** 的效果,使得红娘类可以服务于所有
Person的子类,无需修改代码,符合开闭原则。"面试高频考点清单
序号 考点 代码体现 面试回答要点 1 纯虚函数 virtual string getName() = 0;子类必须实现,否则子类仍是抽象类 2 抽象类 class Person含有纯虚函数不能实例化,只能作为接口 3 多态三要素 继承+虚函数+基类指针指向子类 三个条件缺一不可 4 向上转型 HongNiang(Person* n, Person* z)子类指针隐式转为基类指针 5 动态绑定 通过 Person*调用search()运行时根据实际对象类型决定调用 6 虚函数表 每个有虚函数的类有一张vtable vptr指向vtable,存储虚函数地址 7 override void search() override检查重写的函数签名是否正确 8 虚析构函数 virtual ~Person()防止删除基类指针时内存泄漏 9 静态绑定vs动态绑定 普通函数vs虚函数 编译期确定vs运行期确定 10 依赖倒置 HongNiang依赖Person抽象面向接口编程,不依赖具体实现 11 开闭原则 新增子类无需修改 HongNiang对扩展开放,对修改封闭 12 内存泄漏 new没有delete使用智能指针或栈对象 13 空指针保护 m_person->getMarried()调用前检查 m_person != nullptr14 递归调用 meeting()互相调用必须有终止条件防止栈溢出
niuLang->meeting(zhiNv)到底发生了什么?执行流程拆解
cppniuLang->meeting(zhiNv);调用前状态:
cppniuLang 对象:{ m_person = nullptr, m_flag = true } zhiNv 对象:{ m_person = nullptr, m_flag = true }执行过程(重点看递归):
cpp第1步:niuLang 调用 meeting(zhiNv) ↓ 第2步:进入 Person::meeting() if (m_person == nullptr) // niuLang.m_person == nullptr ✅ { m_person = person; // niuLang.m_person = zhiNv ✅ 牛郎记住织女 person->meeting(this); // 调用 zhiNv->meeting(niuLang) 🔄 递归! } ↓ 第3步:zhiNv 调用 meeting(niuLang) ↓ 第4步:再次进入 Person::meeting() if (m_person == nullptr) // zhiNv.m_person == nullptr ✅ { m_person = person; // zhiNv.m_person = niuLang ✅ 织女记住牛郎 person->meeting(this); // 调用 niuLang->meeting(zhiNv) 🔄 又递归! } ↓ 第5步:niuLang 调用 meeting(zhiNv)(第二次) ↓ 第6步:进入 Person::meeting() if (m_person == nullptr) // niuLang.m_person == zhiNv(不为空)❌ { // 条件不成立,直接跳过! } // 函数结束,不再递归 ✅ 递归终止!最终结果
cpp执行完 meeting() 后: ┌─────────────┐ ┌─────────────┐ │ niuLang │ │ zhiNv │ │ m_person ───┼────────>│ 织女对象 │ │ m_flag=true │ │ m_person ───┼────────>│ 牛郎对象 │ └─────────────┘ │ m_flag=true │ └─────────────┘ └─────────────┘ 牛郎记住了织女 织女记住了牛郎为什么没有任何输出?
因为
meeting()函数内部只有赋值操作 ,没有任何cout输出语句。它只是建立"双向绑定"关系,不输出任何信息。面试追问:如果我想看到见面过程,可以加输出:
cppvoid meeting(Person* person) { if (m_person == nullptr) { cout << getName() << " 认识了 " << person->getName() << endl; // 加输出 m_person = person; person->meeting(this); } }
niuLang->getMarried()到底发生了什么?执行流程拆解
cppniuLang->getMarried();调用前状态:
cppniuLang 对象:{ m_person = zhiNv, m_flag = true } zhiNv 对象:{ m_person = niuLang, m_flag = true }执行过程:
cpp【第1层】niuLang.getMarried() ↓ 进入 Person::getMarried() if (m_flag) // niuLang.m_flag == true ✅ { m_flag = false; // niuLang.m_flag = false (牛郎标记已结婚) marry(); // 👉 调用虚函数 marry() // 因为 this 指向 niuLang(NiuLang对象) // 通过 vptr 找到 NiuLang::marry() // 输出:"我要娶织女了..." // 【这是多态!】 m_person->getMarried(); // 👉 调用 zhiNv->getMarried() // 因为 m_person 指向 zhiNv // 进入【第2层】 } ↓ 【第2层】zhiNv.getMarried() ↓ 进入 Person::getMarried() if (m_flag) // zhiNv.m_flag == true ✅ { m_flag = false; // zhiNv.m_flag = false (织女标记已结婚) marry(); // 👉 调用虚函数 marry() // 因为 this 指向 zhiNv(ZhiNv对象) // 通过 vptr 找到 ZhiNv::marry() // 输出:"从天上跑下来结了个婚..." // 【这是多态!】 m_person->getMarried(); // 👉 调用 niuLang->getMarried() // 因为 m_person 指向 niuLang // 进入【第3层】 } ↓ 【第3层】niuLang.getMarried()(第二次) ↓ 进入 Person::getMarried() if (m_flag) // niuLang.m_flag == false ❌(第1层已经设为false) { // 条件不成立,直接跳过! } // 函数结束 ✅ 递归终止!输出顺序
cpp我要娶织女了,没想到还能娶到这么漂亮的媳妇儿... ← 牛郎的 marry() 从天上跑下来结了个婚,不知道什么时候被带回去。。。 ← 织女的 marry()关键理解:为什么
marry()是动态绑定?
cppvoid getMarried() { if (m_flag) { m_flag = false; marry(); // 👈 这里的 marry() 是虚函数 // 虽然 getMarried() 在 Person 类中定义 // 但调用时 this 指向的是实际对象(niuLang 或 zhiNv) // 所以通过 this->vptr 找到的是子类的 marry()! m_person->getMarried(); } }图解:
cppniuLang->getMarried() │ ├─> this 指向 niuLang(NiuLang对象) │ └─> this->marry() │ └─> 通过 vptr 找到 NiuLang 的虚表 │ └─> 执行 NiuLang::marry() ← 多态! │ └─> m_person->getMarried() │ └─> m_person 指向 zhiNv(ZhiNv对象) └─> this 指向 zhiNv └─> this->marry() └─> 通过 vptr 找到 ZhiNv 的虚表 └─> 执行 ZhiNv::marry() ← 多态!关键概念总结
1.
meeting()------ 建立关系(无输出)
cppvoid meeting(Person* person) { if (m_person == nullptr) { m_person = person; // 自己记住对方 person->meeting(this); // 让对方也记住自己(递归) } }作用 :双向绑定,让两个人互相认识。
特点:普通函数(非虚),所有 Person 行为相同,不需要多态。
2.
getMarried()------ 执行婚礼(有输出)
cppvoid getMarried() { if (m_flag) { m_flag = false; // 防止重复执行 marry(); // 自己的婚礼誓言(多态!) m_person->getMarried(); // 对方的婚礼(递归) } }作用 :触发婚礼流程,依次执行双方的
marry()。特点 :普通函数(非虚),但内部调用虚函数
marry(),触发多态。3.
marry()------ 婚礼誓言(多态核心)
cpp// Person 中定义为纯虚函数 virtual void marry() = 0; // NiuLang 中重写 void marry() override { cout << "我要娶织女了..." << endl; } // ZhiNv 中重写 void marry() override { cout << "从天上跑下来结了个婚..." << endl; }作用 :每个子类有自己的婚礼誓言。
特点:纯虚函数,必须在子类中重写,通过动态绑定调用。
niuLang->meeting(zhiNv)和niuLang->getMarried()这两句是怎么配合工作的?答:"
meeting()负责建立双向绑定关系。当niuLang->meeting(zhiNv)被调用时,牛郎先记住织女,然后递归调用zhiNv->meeting(niuLang),让织女也记住牛郎。这个递归因为有if (m_person == nullptr)保护,只执行两次就终止,没有任何输出。
getMarried()负责触发婚礼。niuLang->getMarried()被调用后,通过m_flag防止重复执行,然后调用虚函数marry()。因为当前对象是牛郎,通过vptr动态绑定到NiuLang::marry(),输出牛郎的誓言。接着通过m_person->getMarried()递归调用织女的getMarried(),织女同样执行自己的marry(),输出织女的誓言。最后织女调用牛郎的getMarried()时,因为牛郎的m_flag已经是false,递归终止。整个流程的核心是多态 ------
marry()虽然是基类中定义的纯虚函数,但在不同对象上调用时,通过虚函数表找到并执行各自子类的版本,实现了 '一个接口,多种行为'。"
设计模式学习笔记
丰锋ff2026-08-28 16:35
相关推荐
罗西的思考16 分钟前
【OpenClaw具身硬件】ZeroClaw 源码阅读笔记(1)— 总体我命由我1234530 分钟前
财务学习 - 配比原则BFT白芙堂42 分钟前
Franka & DROID :面向真实场景的机器人操作数据集loopne1 小时前
AI网文写作实验笔记(十三):系列总结——12 篇实验、8 条核心结论,把“AI 写小说“每一步拆开验证M78佐菲1 小时前
Linux多线程:创建、回收与互斥同步米啦啦.1 小时前
设计模式/sunoo-2292 小时前
【Linux 系统编程】学习第七天:线程属性 | 互斥锁 | 死锁 | 信号量 核心知识点 + 踩坑实战Romantic_love_2 小时前
USART串口初始化源码解析小雪崩2 小时前
嵌入式学习 day33:线程进阶




