#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) {
// 构造函数体为空,完美!
}
};
✔️ 这个写法的规则(必记,你天天用)
子类构造的初始化列表中:
- 先写
父类名(参数)→ 调用父类的构造函数,初始化继承的父类成员- 再写
子类成员(值)→ 初始化子类自己的成员变量- 多个项之间用逗号分隔
这个就是你之前问的代码的「完整版」,也是考试/项目里最标准的写法!
六、额外补充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),成员2(值2){}→ 是构造函数初始化列表;- 和大括号里赋值「功能等价」,但某些场景必须用;
- 子类继承时,初始化列表里先写父类构造,再写自己的成员;
- 写法简洁、规范,是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个必要条件
- 存在继承关系(子类继承父类);
- 父类中声明虚函数(用
virtual关键字); - 子类重写父类的虚函数(函数名、参数、返回值完全一致);
- 用父类的指针/引用指向子类对象(触发多态的关键)。
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(),子类Alipay、WeChatPay重写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; // 记得释放内存
}
五、关键注意点
- 虚析构函数:如果父类有虚函数,一定要加
virtual ~父类名(),否则用父类指针释放子类对象时,子类的析构函数不会执行,导致内存泄漏; - 重写规则:子类重写的函数必须和父类虚函数的函数名、参数类型/个数、返回值完全一致(C++11后可用
override关键字强制检查,避免写错); - 静态多态vs动态多态:静态多态是“编译时确定”,动态多态是“运行时确定”,前者轻量高效,后者灵活扩展。
总结
多态的核心是“一个接口,多种实现”:
- 静态多态(编译时):靠函数/运算符重载实现,简单直接;
- 动态多态(运行时):靠“继承+虚函数+父类指针/引用”实现,是面向对象的核心,能极大提升代码的扩展性和复用性。
你之前写的rectangle和cuboid类,用多态改造后,就能用统一的接口计算面积/体积,这就是多态最典型的应用。
---------
3.触发多态的标准写法
--------
一、先直击核心:BaseCalculator* cal = new AddCalculator; 这句代码的本质
这句是你这段多态代码的灵魂,也是动态多态的核心触发写法,它的本质是:✅ 定义一个「父类类型的指针」,让这个指针指向「子类创建的堆区对象」这行代码能写出来,核心依赖 C++ 的继承兼容性规则:公有继承的子类,是父类的「特殊版」,父类的指针/引用 可以无条件指向子类对象(反过来不行)。
二、逐字拆解这句代码,拆到你完全看懂 【重中之重】
我们把 BaseCalculator* cal = new AddCalculator; 切成 3个独立部分,逐个解释,没有任何难理解的点,都是基础语法的组合:
BaseCalculator* // ① 定义一个指针的「类型」 cal // ② 定义这个指针的「变量名」叫 cal = new AddCalculator; // ③ 在堆区创建子类对象,把对象地址赋值给指针cal
✅ 第一部分:BaseCalculator*
表示:定义一个指针变量,这个指针的「指向类型」是 BaseCalculator(父类),也就是说这个指针理论上应该指向父类的对象。
✅ 第二部分:cal
就是这个指针变量的名字,和你定义 int a 的a、int* p的p 是一个意思,只是个名字。
✅ 第三部分:new AddCalculator;
new 类名()是 C++ 创建堆区对象的语法,会在内存的「堆区」开辟一块空间,创建一个AddCalculator(加法子类)的对象;- 执行
new AddCalculator后,会返回这个子类对象在堆区的首地址; =赋值符号:把这个「子类对象的地址」,交给前面的「父类指针cal」来保存。
✅ 整句话翻译成人话:
创建一个名叫 cal 的指针,它的类型是指向父类BaseCalculator的指针,然后让这个指针,指向「堆区里新建的AddCalculator子类对象」。
三、你必须先懂的「前置核心规则」(你的代码完美满足)
你这段代码是动态多态的标准答案写法,完美满足了动态多态的 4个必要条件,也是你能这么写的前提,我帮你对应上,你就知道你的代码为什么是多态了:
- ✅ 有继承关系:
AddCalculator/SubCalculator都public继承了BaseCalculator; - ✅ 父类有虚函数:父类里写了
virtual int getResult(){return 0;}; - ✅ 子类重写虚函数:两个子类都写了
int getResult() override {...},函数名/参数/返回值和父类完全一致; - ✅ 父类指针指向子类对象:就是你看不懂的这句
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:为什么成员变量是 public 的 m_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的「类型是父类」,编译器只认为它指向父类对象,只能调用「父类中存在的成员」,子类独有的成员看不到。
这个特性刚好保证了代码的安全性,也体现了多态的「统一接口」思想。
九、总结(核心知识点,一句话记死)
BaseCalculator* cal = new AddCalculator;→ 父类指针指向子类对象,动态多态的核心触发写法;- 多态的核心:父类有虚函数,子类重写,父类指针指向子类对象 → 调用虚函数时,执行子类的版本;
- 这么写的意义:新增功能只加子类,不改原有代码,代码扩展性极强;
- 必加虚析构:防止父类指针释放子类对象时的内存泄漏。
你的这段代码是动态多态的完美范例,从语法到逻辑都写的非常标准,你只是看不懂这个指针的写法,现在拆解完,你就彻底理解了👍。

京公网安备 11010502036488号