#include <iostream>
using namespace std;

class BaseCalculator {
    public:
        int m_A;
        int m_B;
        // write your code here......
    virtual int getResult(){
        return 0;
    }
    virtual ~BaseCalculator() {}
};

// 加法计算器类
class AddCalculator : public BaseCalculator {
    // write your code here......
    public:
    int getResult() override {
        return m_A+m_B;
    }
};

// 减法计算器类
class SubCalculator : public BaseCalculator {
    // write your code here......
     public:
    int getResult() override {
        return m_A-m_B;
    }
    
};


int main() {

    BaseCalculator* cal = new AddCalculator;
    cal->m_A = 10;
    cal->m_B = 20;
    cout << cal->getResult() << endl;
    delete cal;

    cal = new SubCalculator;
    cal->m_A = 20;
    cal->m_B = 10;
    cout << cal->getResult() << endl;
    delete cal;

    return 0;
}

1.初始化列表的写法

类名(参数):成员1(值1),成员2(值2){} → 是构造函数初始化列表;完美替换函数写法-> cuboid(int x, int y, int z) : rectangle(x, y){ height = z; }

cuboid(int x,int y,int z):rectangle(x, y),height(z){// 构造函数体为空,完美!}

------------------------

一、核心结论:这行代码是【C++ 构造函数的「成员初始化列表」】完整写法

你看到的 Cuboid(int l, int w, int h) : len(l), wid(w), hei(h) {} 是 C++ 里给类的成员变量初始化的标准语法,和你之前写的子类继承里的 cuboid(int x,y,z):rectangle(x,y){height=z;}同一个语法,只是那个是「调用父类构造+子类成员赋值」,这个是「纯子类成员初始化」。

你之前看不懂,是因为这个语法把「成员赋值」的操作,写到了构造函数的外面、大括号之前,用:开头,,分隔,咱们彻底拆透,保证你看完就懂!

二、先看你能看懂的「等价写法」(你熟悉的方式)

你最开始学的给成员变量赋值,是在构造函数的大括号内部写赋值语句,比如这个写法你一定能看懂:

class Cuboid {
private:
    int len, wid, hei;
public:
    // 写法1:构造函数体 内部赋值(你熟悉的写法)
    Cuboid(int l, int w, int h) {
        len = l;   // 把参数l的值 赋给 成员变量len
        wid = w;   // 把参数w的值 赋给 成员变量wid
        hei = h;   // 把参数h的值 赋给 成员变量hei
    }
};

✅ 这个写法和你问的 : len(l), wid(w), hei(h) {}实现的功能完全一模一样

传入3个参数l,w,h,给类里的3个私有成员len,wid,hei赋值。

区别仅仅是:赋值的位置不同、语法形式不同,效果完全等价。

三、逐字拆解:Cuboid(int l, int w, int h) : len(l), wid(w), hei(h) {}

我们把这行代码切成 4个核心部分,逐个解释,没有任何难点:

Cuboid(int l, int w, int h)  // 第一部分:构造函数的标准头部,接收3个int参数
:                            // 第二部分:【冒号】是初始化列表的开始标记,固定语法
len(l), wid(w), hei(h)       // 第三部分:【成员初始化项】,逗号分隔,一个成员写一项
{}                           // 第四部分:构造函数体,这里为空(因为初始化做完了)

重点解释第三部分:成员名(参数名) 是什么意思?

len(l) → 给类的成员变量len,初始化赋值为构造函数的参数lwid(w) → 给类的成员变量wid,初始化赋值为构造函数的参数whei(h) → 给类的成员变量hei,初始化赋值为构造函数的参数h

语法规则(死记,简单)

初始化列表格式:构造函数名(参数列表) : 成员1(值1), 成员2(值2), 成员3(值3) { 函数体 }

四、为什么要有「初始化列表」?(为什么不一直写大括号里的赋值?)

你肯定会问:既然两种写法效果一样,为什么要多此一举搞个初始化列表?这是C++的重点,分 2个核心原因,优先级从高到低:

✅ 原因1:【必须用】—— 某些场景下,不用初始化列表会直接编译报错

这是初始化列表最核心的存在意义!在以下3种场景中,只能用初始化列表初始化成员,在大括号里赋值会编译失败,没有任何商量余地:

场景①:类里有 const 修饰的成员变量

class A {
private:
    const int num; // const修饰的变量:只能初始化,不能赋值!
public:
    // ❌ 错误写法:大括号里赋值(const变量不能被赋值)
    A(int n) { num = n; } 
    
    // ✅ 正确写法:初始化列表初始化const成员
    A(int n) : num(n) {}
};

知识点:const 变量的特性是「只能在创建的时候赋值一次」,大括号里是「赋值」,初始化列表里是「初始化」。

场景②:类里有「引用类型」的成员变量

class A {
private:
    int # // 引用类型变量:也必须初始化,不能赋值!
public:
    // ✅ 只能用初始化列表
    A(int &n) : num(n) {}
};

场景③:类里有「没有无参构造函数的成员对象」

比如你之前写的 cuboid 继承 rectangle 的代码,还有组合类(类里套类),这个你之前已经遇到过了。

✅ 原因2:【推荐用】—— 初始化列表比 函数体赋值 效率更高

对于int/char/double这种基础类型(比如你的len,wid,hei),两种写法效率几乎没区别;但对于string/数组/自定义类这种复杂类型,初始化列表能少走一步赋值流程,直接创建成员变量,效率更高。

简单理解:

  • 初始化列表:创建成员变量时,直接给值 → 一步到位
  • 函数体赋值:先创建成员变量(赋默认值),再给新值 → 两步操作

五、补充:你之前写的【子类继承+初始化列表】完整写法(你最常用的场景)

这个是你最高频的使用场景,必须重点讲!你之前写的长方体继承矩形的代码,原来的写法是:

class cuboid:public rectangle{
private:
    int height;
public:
    // 你写的写法:调用父类构造 + 函数体里赋值height
    cuboid(int x, int y, int z) : rectangle(x, y){
        height = z;   
    }
};

✅ 优化写法:把子类自己的成员也放进初始化列表

既然初始化列表可以初始化成员,那我们可以把 height 也加到:后面,和父类构造写在一起,这是更规范、更推荐的写法,也是你以后要写的写法:

class cuboid:public rectangle{
private:
    int height;
public:
    // 子类的初始化列表:父类构造 在前,子类成员 在后,逗号分隔
    cuboid(int x, int y, int z) : rectangle(x, y), height(z) {
        // 构造函数体为空,完美!
    }
};

✔️ 这个写法的规则(必记,你天天用)

子类构造的初始化列表中:

  1. 先写 父类名(参数) → 调用父类的构造函数,初始化继承的父类成员
  2. 再写 子类成员(值) → 初始化子类自己的成员变量
  3. 多个项之间用逗号分隔

这个就是你之前问的代码的「完整版」,也是考试/项目里最标准的写法!

六、额外补充2个小知识点(避坑+加分)

✔️ 知识点1:初始化的顺序,和列表顺序无关!

初始化列表里写的顺序,不代表成员初始化的顺序。真正的初始化顺序 = 类里「成员变量声明的顺序」比如:

class Cuboid {
private:
    int len;  // 声明顺序1
    int wid;  // 声明顺序2
    int hei;  // 声明顺序3
public:
    // 列表顺序是 hei, wid, len,但初始化顺序还是 len→wid→hei
    Cuboid(int l, int w, int h) : hei(h), wid(w), len(l) {}
};

这个知识点是面试/笔试的高频坑点,记下来就行。

✔️ 知识点2:初始化列表里可以写「常量/表达式」

不一定非要写构造函数的参数,也可以直接写固定值、表达式,比如:

// 给成员赋固定值
Cuboid() : len(2), wid(3), hei(4) {}

// 给成员赋表达式的值
Cuboid(int l) : len(l), wid(l*2), hei(l*3) {}

七、和你之前代码呼应:完整的长方体类(标准写法)

结合你之前写的继承代码,给你一个最标准的完整版,你以后就这么写:

#include<bits/stdc++.h>
using namespace std;
class rectangle{
	private:
		int length,width;
	public:
		rectangle(int x,int y) : length(x), width(y) {} // 父类也用初始化列表
		void set(int x,int y){
			length=x;
			width=y;
		}
		int area(){
			return length*width;
		}
};
class cuboid:public rectangle{
	private:
		int height;
	public:
		// 子类标准写法:先调用父类构造,再初始化自己的成员
		cuboid(int x, int y, int z) : rectangle(x, y), height(z) {}
		
		int getvolume(){
			return area() * height;
		}
};

八、总结(一句话记死,永不忘记)

  1. 类名(参数):成员1(值1),成员2(值2){} → 是构造函数初始化列表
  2. 和大括号里赋值「功能等价」,但某些场景必须用
  3. 子类继承时,初始化列表里先写父类构造,再写自己的成员
  4. 写法简洁、规范,是C++的标准写法,以后尽量都用这种写法!

这个语法是C++的基础重点,你吃透之后,以后看任何类的构造函数都不会懵了👍

---------------------------

2.多态是什么?

多态是一种面向对象的特性,可以赋予一个接口多用的特性。一个父类的虚函数,可以被多个子类继承并重写,实例化不同的子类可以实现不同的功能。

----------------------------------

一、多态的核心含义

多态(Polymorphism)是面向对象编程(OOP)的三大特性(封装、继承、多态)之一,字面意思是**“多种形态”**。核心定义:同一接口(函数/方法),不同的实现逻辑 —— 调用同一个函数名,根据调用者的“身份”不同,执行不同的代码逻辑。

C++中的多态分两类:

静态多态(编译时)

早绑定

编译阶段

函数重载、运算符重载

动态多态(运行时)

晚绑定

程序运行阶段

继承 + 虚函数(virtual)

下面结合例子讲清楚两类多态的含义和应用,重点讲动态多态(OOP核心)。

二、静态多态(编译时多态)—— 简单易理解,你已接触过

1. 核心逻辑

编译器在编译阶段就确定要调用哪个函数,核心是“同名不同参”的函数重载/运算符重载。

2. 应用例子(你熟悉的场景)

例子1:函数重载(构造函数/普通函数)

比如你之前写的Time类构造函数重载,就是静态多态:

class Time {
public:
    // 重载1:无参构造
    Time() { hours=0; minutes=0; }
    // 重载2:带参构造(同名、不同参数)
    Time(int h, int m) { hours=h; minutes=m; }
};

// 编译时确定调用哪个构造:
Time t1;      // 调用无参构造(编译时确定)
Time t2(2,20); // 调用带参构造(编译时确定)

例子2:运算符重载(你之前写的Time::operator+
Time operator+(const Time& other) { ... }
// 编译时确定:t1 + t2 调用的是Time类的operator+,而非其他类型

三、动态多态(运行时多态)—— OOP核心,重点掌握

1. 核心逻辑

程序运行阶段才确定调用哪个函数,核心是“父类虚函数 + 子类重写 + 父类指针/引用指向子类对象”。核心价值:写一次代码,适配多个子类,极大提升代码扩展性。

2. 动态多态的4个必要条件

  1. 存在继承关系(子类继承父类);
  2. 父类中声明虚函数(用virtual关键字);
  3. 子类重写父类的虚函数(函数名、参数、返回值完全一致);
  4. 父类的指针/引用指向子类对象(触发多态的关键)。

3. 应用例子(结合你熟悉的“矩形/长方体”场景)

比如做一个“形状计算”程序,父类是Shape(形状),子类是Rectangle(矩形)、Cuboid(长方体),统一调用calc()函数,自动计算面积/体积:

#include<bits/stdc++.h>
using namespace std;

// 父类:形状(基类)
class Shape {
public:
    // 虚函数:声明为virtual,允许子类重写
    virtual double calc() {
        return 0.0; // 基类默认返回0
    }
    // 虚析构函数:避免子类对象析构不完整(必加!)
    virtual ~Shape() {}
};

// 子类1:矩形(继承Shape)
class Rectangle : public Shape {
private:
    int len, wid;
public:
    Rectangle(int l, int w) : len(l), wid(w) {}
    // 重写父类虚函数:计算矩形面积
    double calc() override { // override关键字显式标记重写(可选,但推荐)
        return len * wid;
    }
};

// 子类2:长方体(继承Shape)
class Cuboid : public Shape {
private:
    int len, wid, hei;
public:
    Cuboid(int l, int w, int h) : len(l), wid(w), hei(h) {}
    // 重写父类虚函数:计算长方体体积
    double calc() override {
        return len * wid * hei;
    }
};

// 测试多态:统一接口,不同实现
void calculate(Shape& shape) { // 父类引用接收子类对象
    cout << "计算结果:" << shape.calc() << endl;
}

int main() {
    Rectangle rect(2, 3);   // 矩形:长2,宽3
    Cuboid cub(2, 3, 4);    // 长方体:长2,宽3,高4

    // 核心:调用同一个calculate函数,传入不同子类对象,执行不同逻辑
    calculate(rect); // 输出:计算结果:6(矩形面积)
    calculate(cub);  // 输出:计算结果:24(长方体体积)

    // 也可以用父类指针实现(等价效果)
    Shape* p1 = ▭
    Shape* p2 = &cub;
    cout << p1->calc() << endl; // 6
    cout << p2->calc() << endl; // 24

    return 0;
}

4. 例子解析(为什么是“多态”?)

  • 调用的都是calc()函数(同一接口),但传入Rectangle时计算面积,传入Cuboid时计算体积(不同实现);
  • 程序运行时,根据shape引用/指针指向的实际对象类型(而非父类类型),决定调用哪个子类的calc()
  • 如果去掉virtual,则会调用父类的calc()(返回0),多态失效。

四、多态的典型应用场景

1. 统一接口管理(最常用)

比如:

  • 游戏中“角色攻击”:父类Role有虚函数attack(),子类Warrior(战士)、Mage(法师)重写attack(),调用role.attack()时自动执行对应职业的攻击逻辑;
  • 支付系统:父类Pay有虚函数pay(),子类AlipayWeChatPay重写pay(),统一调用pay()即可适配不同支付方式。

2. 代码扩展(开闭原则)

新增功能时,无需修改原有代码,只需新增子类并重写虚函数。比如上面的例子,新增Sphere(球体)类,只需继承Shape并重写calc()计算体积,calculate()函数完全不用改。

3. 容器存储不同子类对象

用父类指针的容器(如vector<Shape*>)存储不同子类对象,遍历调用虚函数:

vector<Shape*> shapes;
shapes.push_back(new Rectangle(2,3));
shapes.push_back(new Cuboid(2,3,4));
for (auto p : shapes) {
    cout << p->calc() << endl; // 依次输出6、24
    delete p; // 记得释放内存
}

五、关键注意点

  1. 虚析构函数:如果父类有虚函数,一定要加virtual ~父类名(),否则用父类指针释放子类对象时,子类的析构函数不会执行,导致内存泄漏;
  2. 重写规则:子类重写的函数必须和父类虚函数的函数名、参数类型/个数、返回值完全一致(C++11后可用override关键字强制检查,避免写错);
  3. 静态多态vs动态多态:静态多态是“编译时确定”,动态多态是“运行时确定”,前者轻量高效,后者灵活扩展。

总结

多态的核心是“一个接口,多种实现”:

  • 静态多态(编译时):靠函数/运算符重载实现,简单直接;
  • 动态多态(运行时):靠“继承+虚函数+父类指针/引用”实现,是面向对象的核心,能极大提升代码的扩展性和复用性。

你之前写的rectanglecuboid类,用多态改造后,就能用统一的接口计算面积/体积,这就是多态最典型的应用。

---------

3.触发多态的标准写法

--------

一、先直击核心:BaseCalculator* cal = new AddCalculator; 这句代码的本质

这句是你这段多态代码的灵魂,也是动态多态的核心触发写法,它的本质是:✅ 定义一个「父类类型的指针」,让这个指针指向「子类创建的堆区对象」这行代码能写出来,核心依赖 C++ 的继承兼容性规则公有继承的子类,是父类的「特殊版」,父类的指针/引用 可以无条件指向子类对象(反过来不行)。

二、逐字拆解这句代码,拆到你完全看懂 【重中之重】

我们把 BaseCalculator* cal = new AddCalculator; 切成 3个独立部分,逐个解释,没有任何难理解的点,都是基础语法的组合:

BaseCalculator*  // ① 定义一个指针的「类型」
cal               // ② 定义这个指针的「变量名」叫 cal
= new AddCalculator; // ③ 在堆区创建子类对象,把对象地址赋值给指针cal

✅ 第一部分:BaseCalculator*

表示:定义一个指针变量,这个指针的「指向类型」是 BaseCalculator(父类),也就是说这个指针理论上应该指向父类的对象

✅ 第二部分:cal

就是这个指针变量的名字,和你定义 int aaint* pp 是一个意思,只是个名字。

✅ 第三部分:new AddCalculator;

  1. new 类名() 是 C++ 创建堆区对象的语法,会在内存的「堆区」开辟一块空间,创建一个AddCalculator(加法子类)的对象;
  2. 执行new AddCalculator后,会返回这个子类对象在堆区的首地址
  3. = 赋值符号:把这个「子类对象的地址」,交给前面的「父类指针cal」来保存。

✅ 整句话翻译成人话:

创建一个名叫 cal 的指针,它的类型是指向父类BaseCalculator的指针,然后让这个指针,指向「堆区里新建的AddCalculator子类对象」。

三、你必须先懂的「前置核心规则」(你的代码完美满足)

你这段代码是动态多态的标准答案写法,完美满足了动态多态的 4个必要条件,也是你能这么写的前提,我帮你对应上,你就知道你的代码为什么是多态了:

  1. ✅ 有继承关系:AddCalculator/SubCalculatorpublic 继承了 BaseCalculator
  2. ✅ 父类有虚函数:父类里写了 virtual int getResult(){return 0;}
  3. ✅ 子类重写虚函数:两个子类都写了 int getResult() override {...},函数名/参数/返回值和父类完全一致;
  4. ✅ 父类指针指向子类对象:就是你看不懂的这句 BaseCalculator* cal = new AddCalculator;

补充:override 关键字是C++11的语法,作用是强制检查是否正确重写了父类的虚函数,写错了编译器会报错(比如函数名写错),不加也能运行,但是加上更规范,你写的非常好!

四、为什么这么写,就能触发「多态」?【核心原理,通俗讲】

你的核心疑问一定是:指针是父类的,指向的是子类对象,调用函数时,到底听谁的?

✅ 多态的核心规则(死记,永不忘记)

如果父类的函数被 virtual 修饰(虚函数),当父类指针/引用 指向子类对象时,调用这个虚函数,执行的是「子类重写的版本」,而不是父类的版本。这个函数的调用,是程序运行时才确定的 → 这就是「动态多态/运行时多态」。

✅ 反例(加深理解)

如果父类的getResult() 去掉 virtual 关键字,变成普通函数:

// 去掉virtual,不是虚函数
int getResult(){ return 0; }

那么执行 cal->getResult() 时,会执行父类的版本,返回0,多态直接失效!

五、完整走一遍你的代码运行流程【从头到尾,一步不落】

你的main函数代码分两段执行,我用「加法段」+「减法段」分开讲,结合你的代码,你跟着看一遍,就彻底通透了,以输入执行顺序为准:

✅ 第一段:执行加法计算

// 1. 父类指针cal 指向 堆区的AddCalculator子类对象
BaseCalculator* cal = new AddCalculator;
// 2. 通过父类指针给对象的成员赋值:m_A=10,m_B=20
//    能直接赋值是因为m_A/m_B是父类的公有成员,子类继承下来了
cal->m_A = 10;
cal->m_B = 20;
// 3. 调用虚函数getResult() → 触发多态,执行子类AddCalculator的重写版本
//    计算10+20=30,输出30
cout << cal->getResult() << endl;
// 4. delete释放堆区的AddCalculator对象,避免内存泄漏
//    父类写了虚析构virtual ~BaseCalculator() {},子类析构会正常执行
delete cal;

✅ 第二段:执行减法计算

// 1. 父类指针cal 重新指向 堆区的SubCalculator子类对象
//    注意:指针变量cal本身还在,只是保存的地址变了,指向了新的子类对象
cal = new SubCalculator;
// 2. 赋值:m_A=20,m_B=10
cal->m_A = 20;
cal->m_B = 10;
// 3. 调用虚函数getResult() → 触发多态,执行子类SubCalculator的重写版本
//    计算20-10=10,输出10
cout << cal->getResult() << endl;
// 4. 释放堆区的SubCalculator对象
delete cal;

六、补充你代码里的2个「优秀细节」+「必知知识点」

你的代码写的非常标准,有两个细节处理的特别好,我提出来,你要知道为什么这么写:

✔️ 细节1:为什么父类要写 virtual ~BaseCalculator() {} 虚析构函数?

这是动态多态的必加语法,也是新手最容易忘的坑:

  • 当用「父类指针」指向「子类对象」,并用delete释放指针时,如果父类的析构函数不是虚函数,编译器只会调用「父类的析构函数」,子类的析构函数不会执行 → 导致内存泄漏
  • 加上virtual后,析构函数也变成虚函数,释放时会调用「子类的析构函数」+「父类的析构函数」,内存被完整释放。

✔️ 细节2:为什么成员变量是 publicm_A/m_B

因为你的父类指针cal指向子类对象时,父类指针只能直接访问「父类中存在的成员」(子类独有的成员访问不了),而m_A/m_B是父类定义的公有成员,子类继承后也拥有,所以可以通过cal->m_A直接赋值,这是合理的写法。

七、这种写法的「核心价值」:为什么不用直接创建子类对象?

你肯定会问:我直接写 AddCalculator cal; cal.m_A=10; 不是更简单吗?为什么要搞父类指针这么麻烦?

✅ 多态的终极意义:【写一次代码,适配所有子类,扩展性拉满】

这是面向对象的精髓,也是多态的核心价值,举个例子你就懂了:如果现在你需要新增一个「乘法计算器」,你只需要新增一个子类,不用修改任何main函数的代码:

// 新增乘法子类,仅此一步
class MulCalculator : public BaseCalculator {
public:
    int getResult() override {
        return m_A * m_B;
    }
};

然后在main函数里加一行:

cal = new MulCalculator;
cal->m_A=10; cal->m_B=20;
cout << cal->getResult() << endl; // 输出200
delete cal;

✅ 核心好处:原有代码一行不改,新增功能只加代码 → 这就是面向对象的「开闭原则」,也是多态的核心魅力。

如果不用多态,你就要写 AddCalculator cal1; SubCalculator cal2; MulCalculator cal3;,新增一个功能就要改main函数,代码会越来越臃肿。

八、补充一个「你一定会遇到的小疑问」

Q:父类指针指向子类对象,能调用子类独有的函数吗?

答:不能。比如你给AddCalculator加一个独有的函数:

class AddCalculator : public BaseCalculator {
public:
    int getResult() override { return m_A+m_B; }
    void showAdd() { cout << "这是加法计算器" << endl; } // 子类独有函数
};

然后执行 cal->showAdd();编译报错。原因:父类指针cal的「类型是父类」,编译器只认为它指向父类对象,只能调用「父类中存在的成员」,子类独有的成员看不到。

这个特性刚好保证了代码的安全性,也体现了多态的「统一接口」思想。

九、总结(核心知识点,一句话记死)

  1. BaseCalculator* cal = new AddCalculator;父类指针指向子类对象,动态多态的核心触发写法;
  2. 多态的核心:父类有虚函数,子类重写,父类指针指向子类对象 → 调用虚函数时,执行子类的版本;
  3. 这么写的意义:新增功能只加子类,不改原有代码,代码扩展性极强;
  4. 必加虚析构:防止父类指针释放子类对象时的内存泄漏。

你的这段代码是动态多态的完美范例,从语法到逻辑都写的非常标准,你只是看不懂这个指针的写法,现在拆解完,你就彻底理解了👍。