访问权限:
C++ 通过关键字 public
、protected
和 private
来控制类成员(属性和方法)的访问权限:
访问权限 | 同类内部访问 | 子类访问 | 外部访问 |
---|---|---|---|
public |
✅ | ✅ | ✅ |
protected |
✅ | ✅ | ❌ |
private |
✅ | ❌ | ❌ |
将对象的状态(数据)和行为(方法)打包封装在一起,并控制对外暴露的接口,保护内部数据。
你定义一个银行账户类,把账户余额 balance
设置为 private
,提供 deposit()
和 withdraw()
方法供外部使用,不能直接改余额,避免非法操作。
子类可以“继承”父类的属性和方法,使得子类具有父类的功能,同时还能进行功能扩展或重写。
public
和 protected
成员保持权限不变继承到子类。public 和
protected
成员都变成 protected
。private
,只供内部使用。父类成员权限 | 公有继承 class A : public B |
保护继承 class A : protected B |
私有继承 class A : private B |
---|---|---|---|
public |
保持 public |
变成 protected |
变成 private |
protected |
保持 protected |
保持 protected |
变成 private |
private |
存在,但不可访问 | 存在,但不可访问 | 存在,但不可访问 |
private成员永远不会被子类访问到,但它仍存在于对象中(可以通过父类方法间接使用)
Base 的 private 成员会被继承,但子类看不见,也不能访问!
不过它确实存在,比如构造函数可以初始化它,Base 的成员函数也可以操作它。
举例:
基类定义:
class Base {
public:
int pub = 1;
protected:
int prot = 2;
private:
int priv = 3;
};
公有继承:
class PublicDerived : public Base {
public:
void test() {
pub = 10; // ✅ 可以访问(是 public)
prot = 20; // ✅ 可以访问(是 protected)
// priv = 30; // ❌ 错误,private 不能访问
}
};
void testPublic() {
PublicDerived d;
d.pub = 100; // ✅ 外部也能访问
// d.prot = 200; // ❌ 外部不能访问
}
保护继承:
class ProtectedDerived : protected Base {
public:
void test() {
pub = 10; // ✅ 可以访问(继承后变 protected)
prot = 20; // ✅ 可以访问
// priv = 30; // ❌ 不可以访问
}
};
void testProtected() {
ProtectedDerived d;
// d.pub = 100; // ❌ 外部无法访问(现在是 protected)
}
私有继承:
class PrivateDerived : private Base {
public:
void test() {
pub = 10; // ✅ 可以访问(继承后变 private)
prot = 20; // ✅ 可以访问
// priv = 30; // ❌ 不可以访问
}
};
void testPrivate() {
PrivateDerived d;
// d.pub = 100; // ❌ 外部访问失败(现在是 private)
}
继承方式 | 父类 public 成员变成 | 父类 protected 成员变成 | 外部能否访问 pub 成员 |
---|---|---|---|
公有继承 | public | protected | ✅ 可以 |
保护继承 | protected | protected | ❌ 不可以 |
私有继承 | private | private | ❌ 不可以 |
相同接口(方法名)可以表现出多种行为(即:多种“形态”)。
父类
,运行的是子类的实现。virtual
)。多态是一种允许相同的接口在不同对象上表现出不同行为的机制。根据实现时机的不同,多态分为两种:
编译阶段就决定了调用哪个函数,实现方式:
函数重载(Overload):不是核心多态,不依赖继承
举例:
class Printer {
public:
void print(int i) { std::cout << "int" << std::endl; }
void print(double d) { std::cout << "double" << std::endl; }
void print(std::string s) { std::cout << "string" << std::endl; }
};
程序运行时根据对象的实际类型决定调用哪个函数,实现方式:
函数覆盖(Override):是核心多态,虚函数+继承
virtual
);virtual
声明父类函数,建议子类使用 override
明确声明;允许将子类类型的指针赋值给父类类型的指针,并在调用函数时表现出子类的行为。
举例:
class Base {
public:
virtual void print() {
std::cout << "Base" << std::endl;
}
};
class Derived : public Base {
public:
void print() override {
std::cout << "Derived" << std::endl;
}
};
Base* b = new Derived();
b->print(); // 输出: Derived(即使 b 是 Base 类型)
定义一个基类 Animal 有一个 speak() 方法,猫类 Cat 和狗类 Dog 继承后各自实现不同的 speak(),当我们用 Animal* 调用时,会根据实际对象类型执行正确的方法。
特性 | 目的 | 关键词/机制 |
---|---|---|
封装 | 数据保护,隐藏细节 | private , public , 类 |
继承 | 复用代码,扩展功能 | class 派生 |
多态 | 接口统一,行为多样 | virtual , 重载等 |
一个类可以同时继承多个父类(基类),从而获得多个父类的成员变量和成员函数。
下面,为了便于理解,这里我将类定义为壳子
+内容
:
Animal animal
; → 这里的 animal
是“壳子”,你必须通过它访问内容。eat()
、x
等),这是我们真正要用的功能。class Animal {
public:
void eat() {}
};
class Mammal {
public:
Animal a; // 这是组合
};
这里创建了一个叫 a
的“壳子”,内部装着 Animal
的功能:
Mammal m;
m.a.eat(); // ✅ 必须通过壳子访问
Animal
是一个 成员变量eat()
,必须 m.a.eat()
class Mammal : public Animal {};
这里不需要壳子了,直接把 Animal
的功能全塞到 Mammal
里。
所以:
Mammal m;
m.eat(); // ✅ 直接用,不需要 m.animal.eat()
Animal
的成员全部复制粘贴到 Mammal
的类里animal
这个名字(壳子)Mammal
看起来就像自己定义了 eat()
一样class Printer {
public:
void print() {}
};
class Scanner {
public:
void scan() {}
};
class Copier : public Printer, public Scanner {};
把 Printer
的内容和 Scanner
的内容都剥掉壳子,统统塞到 Copier
里面。
所以:
Copier c;
c.print(); // ✅ 来自 Printer
c.scan(); // ✅ 来自 Scanner
结构上就像:
Copier:
- void print(); // 来自 Printer
- void scan(); // 来自 Scanner
各功能独立、不冲突,Copier
获得 Printer
和 Scanner
两个功能模块的内容,不带壳,直接用。
子类通过多条路径继承了同一个祖先类。
class Animal {
public:
void eat() {}
};
class Mammal : public Animal {};
class Bird : public Animal {};
class Bat : public Mammal, public Bird {};
这里相当于从 Mammal
拆一次壳子嵌进来(内容还是之前 Animal
的内容),从 Bird 又拆一次壳子嵌进来(内容还是之前 Animal
的内容)。两段内容重复了!!!
结果:Bat 里有两个 eat()
函数!来自两个 Animal
内容副本
错误原因:
Bat b;
b.eat(); // ❌ 编译器懵了,不知道你是想用 Mammal::Animal::eat 还是 Bird::Animal::eat
class Mammal : virtual public Animal {};
class Bird : virtual public Animal {};
class Bat : public Mammal, public Bird {};
这里不拆壳子,只要个引用!让 Bat 拆壳子,塞一份就够了,大家共享用它。
在**所有使用 virtual
继承的结构中,只有“最底层的最终派生类”负责真正拆壳子、提供内容。所以,是 Bat
负责真正创建那一份 Animal
内容!
机制解释:
Mammal
和 Bird
都声明了 “我需要一个 Animal
,但我不拆壳,我只是声明我虚继承了它”Bat
,它看到两个路径都需要一个虚基类 Animal
,就必须自己提供一份唯一的实例Bat
的对象内存中只构造一次 Animal
,而且所有路径都引用这个共享版本结果:
Bat:
├── Mammal(只有自己的成员,没有 Animal 内容)
├── Bird(同样没有 Animal 内容)
└── Animal(由 Bat 拆+粘 的,唯一一份内容)
也就是说:
Mammal
和 Bird
的结构中原本该嵌入的 Animal
内容,被抽离了出来Bat
里的那一份 Animal
所以,编译器在它们访问 eat() 的时候,底层操作是:
通过 vbase pointer
→ 找到 Bat
里的那份 Animal
→ 调用 eat()
区域 | 含义 |
---|---|
Mammal 区块 | 自己的成员 + 虚基指针 |
Bird 区块 | 自己的成员 + 虚基指针 |
Animal 区块 | 真正构造出来的唯一一份内容 |
Bat b;
b.eat(); // ✅ 只存在一个 eat,没有冲突
重载是指同一个作用域中,多个函数名称相同,但参数个数不同或参数类型不同。(参数类型必须不一样)
特征:
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
这是典型的重载:函数名都是 add,但参数类型和返回值类型不同(一个是 int,一个是 double)。
重写是指在子类中重新定义一个和父类中同名、同参数的函数,以实现不同的功能。是运行时多态(动态多态)
virtual
;override
关键字(C++11 以后),用于确保是重写;还有其他可能:
情况 | 是否是重写? | 说明 |
---|---|---|
函数名相同,参数不同 | ❌ 不是重写,只是“隐藏” | |
函数名和参数都一样,但父类不是 virtual | ❌ 不是重写,只是重新定义 | |
函数名、参数都一样,父类是 virtual | ✅ 是重写(推荐加 override) |
class Base {
public:
virtual void show(int x) {
cout << "Base::show(int)" << endl;
}
};
class Derived : public Base {
public:
void show() {
cout << "Derived::show()" << endl;
}
};
分析:
show()
与父类的 show(int)
参数不同;Derived
中的 show()
会隐藏掉父类所有同名函数(不管参数对不对);调用效果:
Derived d;
d.show(); // ✅ 调用的是 Derived::show()
d.show(10); // ❌ 编译错误:Derived 隐藏了 Base::show(int)
d.Base::show(10); // ✅ 你可以手动指定调用 Base 的方法
class Base {
public:
void show() {
cout << "Base::show()" << endl;
}
};
class Derived : public Base {
public:
void show() {
cout << "Derived::show()" << endl;
}
};
分析:
Derived::show()
与 Base::show()
签名完全一致;Base::show()
不是 virtual
,这不是重写;调用效果:
Base* p = new Derived();
p->show(); // 输出 Base::show(),因为没有虚函数,静态绑定
即使你用的是父类指针指向子类对象,调用的还是父类的函数,而不是子类的。
总结:
情况 | 名称 | 是否隐藏父类函数? | 是否重写? | 是否支持多态? | 绑定类型 |
---|---|---|---|---|---|
参数相同,父类是 virtual | 重写(override) | ❌ 否 | ✅ 是 | ✅ 是 | 运行时绑定(多态) |
参数不同 | 函数名隐藏 | ✅ 是 | ❌ 否 | ❌ 否 | 编译时绑定 |
参数相同,父类非 virtual | 静态覆盖 | ❌ 否(同名但没隐藏) | ❌ 否 | ❌ 否 | 编译时绑定 |
class Base {
private:
virtual void print() {
cout << "Base class" << endl;
}
};
class Derived : public Base {
public:
void print() override { // ❌ 报错:父类的 print 是 private,看不到
cout << "Derived class" << endl;
}
};
class Base {
public:
virtual void print() {
cout << "Base class" << endl;
}
};
class Derived : public Base {
public:
void print() override {
cout << "Derived class" << endl;
}
};
调用:
Base* b = new Derived();
b->print(); // 输出: Derived class
因为使用了虚函数和 override
,所以调用的是子类的方法,实现了多态。
特性 | 重载(Overload) | 重写(Override) |
---|---|---|
是否需要继承关系 | 否 | 是(必须有父类和子类) |
函数名 | 相同 | 相同 |
参数列表 | 不同 | 必须相同 |
返回值 | 可以不同 | 一般相同(不能仅靠返回值区分) |
调用方式 | 编译时根据参数判断调用 | 运行时根据对象类型判断调用 |
多态类型 | 编译时多态(静态绑定) | 运行时多态(动态绑定) |
在 C++ 中:
virtual
函数时,编译器会为它生成一个虚函数表(vtable);通过 vtable 模拟上述三个例子:
class Base {
public:
virtual void show() { cout << "Base::show()\n"; }
};
class Derived : public Base {
public:
void show() override { cout << "Derived::show()\n"; }
};
Base::show()
是虚函数;
Derived::show()
是合法的重写;
Derived 的 vtable 会替换掉原来指向 Base 的函数地址;
虚函数表模拟:
Base vtable:
[ show() → Base::show() ]
Derived vtable:
[ show() → Derived::show() ] ✅ 替换成功
调用效果:
Base* ptr = new Derived();
ptr->show(); // 输出 Derived::show(),实现多态
class Base {
public:
virtual void show(int x) { cout << "Base::show(int)\n"; }
};
class Derived : public Base {
public:
void show() { cout << "Derived::show()\n"; } // 参数不同
};
Base::show()
是虚函数;
Derived::show()
不是重写,因为参数不同;
所以 Derived 的 vtable 中 还是Base::show(int)
,不会有 Derived 的函数;
虚函数表模拟:
Base vtable:
[ show(int) → Base::show(int) ]
Derived vtable:
[ show(int) → Base::show(int) ] // 注意!Derived::show() 不在 vtable 里
调用效果:
Base* ptr = new Derived();
ptr->show(42); // 输出 Base::show(int)
即使是 Derived 实例,调用的还是 Base 的版本,因为 Derived::show() 是新函数,不在 vtable 中。
class Base {
public:
void show() { cout << "Base::show()\n"; } // 非 virtual
};
class Derived : public Base {
public:
void show() { cout << "Derived::show()\n"; } // 签名一致
};
父类
Base::show()
不是虚函数;
子类也定义了一个新版本,和父类名字完全一样;
没有 vtable,编译器使用静态绑定;
编译期根据指针类型决定调用哪个版本;
没有虚函数表
无 vtable,全靠编译期决定调用哪个函数
调用效果:
Base* ptr = new Derived();
ptr->show(); // 输出 Base::show(),因为是静态绑定
这时用的是名字查找 + 编译期绑定
隐藏就真的生效了!
Derived d;
d.show(); // ✅ 调用子类的 show()
d.show(10); // ❌ 编译错误:Base::show(int) 被隐藏了
编译器看到你用的是 Derived,就只查找 Derived 的作用域,不会管父类有没有其他 show(int),所以你只能用 Derived::show(),不能用 Base::show(int)。
这时如果父类的 show(int) 是 virtual,那它会进入 vtable,即使被隐藏了,也可以调用!
Base* ptr = new Derived();
ptr->show(10); // ✅ 调用 Base::show(int),没问题
虽然 Derived 中写了一个 show(),参数不同,但它没有重写 show(int),所以 Derived 的 vtable 中仍然保留的是 Base::show(int)。
调用方式 | 是否受隐藏影响 | 会不会调用被隐藏的 Base::show(int) |
---|---|---|
Derived d; d.show(10) |
✅ 会受影响 | ❌ 不会,编译错误(名字找不到) |
Base* p = new Derived(); p->show(10) |
❌ 不受影响 | ✅ 会,正常多态调用 Base::show(int) |
C++ 的多态性是通过:虚函数(virtual function)& 虚函数表(vtable)实现的。
多态的作用:允许你使用父类类型的指针或引用指向子类对象,在运行时调用真正属于子类的函数,从而实现 “调用不变,行为多变”。
class Shape {
public:
virtual void draw() const {
// 基类的默认实现
}
};
virtual
表明 draw()
是一个虚函数;Circle
)就可以“重写 override”这个函数;Shape*
指向 Circle
,依旧可以调用到正确的 Circle::draw()
;class Circle : public Shape {
public:
void draw() const override {
// 派生类的实现
}
};
override
关键字(C++11 引入)告诉编译器:这是要重写一个父类虚函数;Shape* shapePtr = new Circle();
shapePtr
是 Shape*
类型,但它实际指向的是 Circle
类型的对象;shapePtr->draw();
// 实际调用的是 Circle::draw()
draw()
是虚函数,运行时通过 vtable 查表找到正确的函数;shapePtr
是 Shape*
,也能调用到 Circle::draw()
。编译器会为每个含有虚函数的类维护一张 虚函数表(vtable):