继承
为什么需要继承?
- 代码复用
- 对事物进行分类
- 增量开发
单继承
cpp
```cpp
#include <iostream>
#include <cstring>
//错误声明
// class Undergraduated_Student : public Student;
//正确声明
class Undergraduated_Student ;
using namespace std;
class Student
{ int id;
public:
char nickname[16];
void set_ID(int x) { id = x; }
void SetNickName(char *s) { strcpy(nickname,s);}
void showInfo(){ cout << nickname << ":" << id <<endl; }
private:
void show_private(){cout << "hi" << endl;}
};
class Undergraduate_Student : public Student
{ int dept_no;
public:
void setDeptNo(int x) { dept_no = x; }
void showInfo()
{ cout << dept_no << ": "<< nickname <<endl; }
// using Student::show_private;会报错
private:
using Student::nickname;
using Student::set_ID;
void SetNickName (){strcpy(nickname," ");};
};
int main(){
Undergraduate_Student undergraduateStudent;
// undergraduateStudent.setNickName();会报错
// undergraduateStudent.nickname;会报错
Student student;
student.nickname;
student.SetNickName("123");
student.showInfo();
}
继承方式
public、protected、private
都是将访问权限降级。
using可以改变控制访问权限
private继承时,可以用using将某些数据成员或者函数成员权限放开
也可以将权限缩小
可否将基类private成员进行权限放宽?
派生类的内存空间
基类部分加上派生类新增部分,using不改变空间布局,只改变访问权限(这是编译器部分的检查,因此不会改变内存权限)
bash
Undergraduate_Student 对象的内存布局:
┌─────────────────────────────────────────────┐
│ 基类Student部分 │
├─────────────────────────────────────────────┤
│ int id (4字节) [Student私有] │
├─────────────────────────────────────────────┤
│ char nickname[16] (16字节) [Student公有] │
├─────────────────────────────────────────────┤
│ 派生类新增部分 │
├─────────────────────────────────────────────┤
│ int dept_no (4字节) [Undergraduate_Student私有]│
└─────────────────────────────────────────────┘
总大小:4 + 16 + 4 = 24字节
前向声明
名掩盖问题
cpp
using namespace std;
class Base {
public:
void func(int x) { cout << "Base::func(int)" << endl; }
void func(double x) { cout << "Base::func(double)" << endl; }
void func(int x, int y) { cout << "Base::func(int,int)" << endl; }
};
class Derived : public Base {
public:
void func() { // 只定义了一个无参版本
cout << "Derived::func()" << endl;
}
// using Base::func; 解决名掩盖问题
};
int main() {
Derived d;
d.func(); // 正确:Derived::func()
//d.func(1); // 错误!Base的所有func都被隐藏了
//d.func(1.0); // 错误!
//d.func(1,2); // 错误!
}
一旦派生类中有一个同名函数,基类中任何其他同名函数都不可以再用。
如何解决:
- 使用using将基类方法引入派生类的名空间
using和名掩盖问题的实质:
cpp
// 当编译器看到 d.func(1) 时:
名称查找流程:
1. 进入 Derived 类作用域
2. 查找名称 "func"
3. ✅ 找到 Derived::func() 如果没找到,会到基类中继续寻找。
4. 🛑 立即停止查找!不再去Base类查找
5. 检查参数匹配:Derived::func() 需要0个参数,但提供了1个
6. ❌ 编译错误:参数不匹配
using Base::func;的作用:
- 将Base类中所有名为func的声明注入到Derived类作用域
using的作用实际上就是注入声明(不是注入定义)
派生类对象的初始化
由基类和派生类共同完成。
为什么需要共同完成?
- 基类部分:包含基类的数据成员,需要基类构造函数初始化
- 派生类部分:包含新增的数据成员,需要派生类构造函数初始化
创建Derived对象:
- 基类构造函数 ← 第1步:基类
- 派生类成员对象构造函数(声明顺序) ← 第2步:对象成员
- 派生类构造函数 ← 第3步:派生类自身
Derived对象即将销毁:
- 派生类析构函数 ← 第1步:派生类自身
- 派生类成员对象析构函数(声明顺序) ← 第2步:对象成员
- 基类析构函数 ← 第3步:基类
基类构造函数的调用
- 如果缺省则执行基类默认构造函数
- 若要执行基类的非默认构造函数,必须要派生类构造函数的成员初始化表中指出
cpp
#include <iostream>
using namespace std;
// 基类A
class A {
int x;
public:
// 默认构造函数
A() {
x = 0;
}
// 带参构造函数
A(int i) {
x = i;
}
};
// 派生类B,公有继承A
class B : public A {
int y;
public:
// 情况1:缺省调用基类默认构造函数
B() {
y = 0;
}
// 情况1:缺省调用基类默认构造函数
B(int i) {
y = i;
}
// 情况2:显式调用基类非默认构造函数
B(int i, int j) : A(i) { // 在成员初始化表中指出
y = j;
}
};
int main() {
B b1; // 执行A::A()和B::B()
B b2(1); // 执行A::A()和B::B(int)
B b3(0,1); // 执行A::A(int)和B::B(int,int)
return 0;
}
除此之外,还可以使用using继承构造函数
cpp
class A {
int x;
public:
A() { x = 0; cout << "A默认构造" << endl; }
A(int i) { x = i; cout << "A带参构造: " << x << endl; }
A(int i, int j) { x = i + j; cout << "A双参构造: " << x << endl; }
};
class B : public A {
int y;
public:
using A::A; // 继承A的所有构造函数
// B会自动拥有以下构造函数:
// B() : A() {}
// B(int i) : A(i) {}
// B(int i, int j) : A(i, j) {}
};
虚函数------实现多态的关键
类型相容
问题:a、b是什么类型时,a = b 合法?
- b是a的派生类对象
直接值传递会造成对象切片问题。
cpp
class A { // 基类
int x, y; // 私有数据成员
public:
void f(); // 公有成员函数
};
class B : public A { // 派生类,公有继承A
int z; // 派生类新增数据成员
public:
void f();
void g(); // 派生类新增成员函数
};
A a;
B b;
a = b; //OK,
b = a; //Error
a.f(); //A::f()
a.g(); //Error
//引用和指针也是一样
A &r_a=b; //OK
A *p_a=&b; //OK
B &r_b=a; //Error
B *p_b=&a; //Error
动态绑定和静态绑定
调用的是A的f还是B的f。
cpp
void func1(A& a) {
a.f(); // 这里调用的是A::f还是B::f?
}
void func2(A* pa) {
pa->f(); // 这里调用的是A::f还是B::f?
}
int main() {
B b;
func1(b); // 实际调用:A::f
func2(&b); // 实际调用:A::f
return 0;
}
为什么,普通的成员函数是静态绑定,在编译时就进行绑定。
以func1(b)为例,调用func1时,首先会进行一次传参:A& a = b,此处就将其绑定为了A类。
- 前期绑定(默认)
- 编译时刻、静态类型、效率高、灵活性低
- 动态绑定
- 运行时刻、实际类型、效率低、灵活性高
动态绑定需要加上virtual关键字,则根据实际引用和指向的对象类型。
加在哪?
cpp
class A
{ ...
public:
virtual void f();
};
class B
{ ...
public:
void f();
};
如基类中被定义为虚成员函数,则派生类中对其重定义的成员函数均为虚函数。
限制:
-
类的成员函数才可以是虚函数
-
静态成员函数不能是虚函数(静态成员函数属于类,不能多态)
-
内联成员函数不能是虚函数(这句话有点问题)
这段代码能跑通,但是内联是编译期的建议,虚函数往往不会被内联cpp#include <iostream> using namespace std; class Base { public: virtual inline void func() { cout << "Base::func()" << endl; } }; int main() { Base base; base.func(); } -
构造函数不能是虚函数。(虚函数根据实际对象来执行,但是构造函数执行时对象都没被创建)
-
析构函数可以(往往)是虚函数(每一种派生类需要释放的资源可能不一样)。
虚函数的实现
cpp
class A {
public:
int x, y;
virtual void f(); // 虚函数
virtual void g(); // 虚函数
void h(); // 非虚函数
};
class B : public A {
public:
int z;
void f(); // 重写 f()
void h(); // 重写 h()
};
A a; // 基类对象
B b; // 派生类对象
A* p; // 基类指针
// p = &a;
// p = &b;
每个含虚函数的类,都会有一张虚函数表
| 类 | vtable 内容 |
|---|---|
| A | { A::f, A::g } |
| B | { B::f, A::g } |
含虚函数的对象,其内存空间会多一个指向对应类虚函数表的指针。
bash
+-------------------+
| vptr → A_vtable | ← 指向 A 的虚函数表
+-------------------+
| x |
+-------------------+
| y |
+-------------------+
bash
+-------------------+
| vptr → B_vtable | ← 指向 B 的虚函数表
+-------------------+
| x | ← 继承自 A
+-------------------+
| y | ← 继承自 A
+-------------------+
| z | ← B 自己的成员
+-------------------+
具体调用过程如下:
cpp
A* p = &a;
p->f();
- p 指向 a
- 读取 a 的 vptr → 得到 A_vtable 地址
- 在 A_vtable 中查找第一个函数(f)→ 获取 &A::f
- 调用 A::f()
p->f()等价于(**((char *)p-4))(p) 这是一种过时并且错误的写法,简单了解一下
- (char *)p
将 p(比如 A*)强制转换为字节指针 char*。
目的:按字节进行地址运算。 - ((char *)p - 4)
向低地址方向偏移 4 字节。获得虚函数表的地址。 - *((char *)p - 4) → 等价于 *(...)
解引用,得到 vptr 的值(即 vtable 的地址)。 - **((char )p - 4)
再次解引用:从 vtable 中取出 第一个函数指针(即第一个虚函数的地址)。
此时得到的是一个 函数指针,比如 void (*)(f)。 - (**((char *)p - 4))(p)
把 p 作为参数(即 this 指针)传给该函数并调用。
虚函数的调用规则
- 不要在构造函数中使用虚函数。
- 虚函数要在对象创建后才能正常使用
cpp
class A
{ public:
A() { f();}
virtual void f();
void g();
void h() { f(); g(); }
};
class B: public A
{ public:
void f(){g()}; //void f(B* const this){this->g()};
void g();
};
B b;
A* p = &b;
p->f(); // → 调用 B::f()
p->g(); // → 调用 A::g()
p->h(); // → 调用 A::h(),B::f(),B::g(),A::g()
- 虚函数才看实际类型,否则就看this类型。
override和final
- override
- 显式声明一个函数是重写一个
虚函数 - 编译器会进行检查,如果没有正确重写会报错
- 显式声明一个函数是重写一个
- final
- 阻止函数被重写,阻止类被继承
cpp
struct B {
virtual void f1(int) const ;
virtual void f2 ();
void f3 () ;
virtual void f5 (int) final;
};
struct D : B {
void f1(int) const override; // ✅ 正确:匹配 f1 的 const 和参数
void f2(int) override; // ❌ 错误:B 中没有形如 f2(int) 的函数
void f3() override; // ❌ 错误:f3 不是虚函数,不能 override
void f4() override; // ❌ 错误:B 中没有名为 f4 的函数
void f5(int); // ❌ 错误:f5 已被声明为 final,不能重写
};
重写虚函数也可以改变访问权限:
cpp
struct B {
protected:
virtual void f() {}
};
struct D : B {
public:
void f() override {} // 放宽为 public
};
int main() {
D d;
d.f(); // ✅ 正确:D::f 是 public,可以直接调用
B* pb = &d;
pb->f(); // ❌ 错误:编译期按 B::f 检查,B::f 是 protected,类外不可访问
}
友元没有传递性
基类的友元不是派生类的友元,派生类的友元不是基类的友元
cpp
class Base {
protected:
int prot_mem; // protected 成员
};
class Sneaky : public Base {
public:
friend void clobber(Sneaky&); // 可以访问 Sneaky 的 private 和 protected 成员
friend void clobber(Base&); // 只能访问 Base 的成员(但不能访问 protected)
int j; // 默认 private
};
void clobber(Sneaky &s) {
s.j = 0; // ✅ 正确:clobber 是 Sneaky 的友元 → 可访问 Sneaky 的成员
s.prot_mem = 0; // ✅ 正确:prot_mem 继承自 Base,是 protected → Sneaky 可访问
}
void clobber(Base &b) {
b.prot_mem = 0; // ❌ 错误:clobber 不是 Base 的友元 → 无法访问 protected 成员
}
protected是具有传递性的。
但是protected的传递性是指派生类继承了基类的这个变量,可以访问派生类自身的这个变量,如果作为参数传入一个基类,是不能访问的。
cpp
class Base {
protected:
int x;
};
class Derived : public Base {
// 可以访问 x
};
class GrandDerived : public Derived {
public:
void foo() {
x = 10; // ✅ 合法!GrandDerived 可以访问 Base 的 protected 成员 x
}
void bad(Base& b) {
b.x = 10; // ❌ 编译错误!
}
void good(GrandDerived& gd) {
gd.x = 20; // ✅ 合法:gd 是同类对象(或派生类对象)
}
void alsoGood(Derived& d) {
d.x = 30; // ❌ 编译错误!
}
};
纯虚函数和抽象类
- 纯虚函数
- 声明时在函数原型后面加上 = 0
- 必须在派生类中重写,否则派生类也是抽象类
- 抽象类
- 至少包含一个纯虚函数
- 不能用于创建对象
cpp
class AbstractClass {
public:
virtual int f() = 0; // 纯虚函数
};
AbstractClass a; // ❌ 错误:抽象类
虚析构函数
cpp
class B {
public:
virtual ~B() = default;
};
class D : public B {
public:
mystring* name = new mystring();
~D() {
delete name;
cout << "D::~D()\n";
}
};
int main() {
B* p = new D;
delete p; // ✅ 正确:会调用 D::~D() 然后 B::~B()
}
public 继承表示"is-a"关系
正方形和矩形,鸟和企鹅。看似是is-a关系,但是正方形没有长和宽的关系,企鹅不会飞。如果只是想要代码复用,用组合!
is-a关系要能完全替代,比如工厂模式,A工厂和B工厂都能生产组件C,生产工艺可能不一样,但是两者完全可以互相替代。
"子类型必须能够替换它们的基类型,而不破坏程序的正确性。"
------ 里氏替换原则(Liskov Substitution Principle, LSP)
用组合实现代码复用
cpp
class Square {
private:
Rectangle r;
public:
void setLength(int l) {
r.setHeight(l);
r.setWidth(l);
}
// 其他接口
};
不要定义与继承而来的非虚成员函数同名的成员函数
名掩盖问题。
private继承------实现代码复用的一种方式
在派生类内,将派生类当作基类的派生类,在派生类外,派生类和基类没有关系。
cpp
class Base {
public:
void f() {}
};
class Derived : private Base {
public:
void test() {
f(); // ✅ OK:在成员函数内可访问基类成员
Base* p = this; // ✅ OK:在成员函数内可转换为基类指针
}
};
void func(Base& b) {}
int main() {
Derived d;
d.f(); // ❌ 错误!外部不能调用 Base 的 public 成员
func(d); // ❌ 错误!不能将 Derived 转换为 Base&
Base* p = &d; // ❌ 错误!不能取地址转为 Base*
d.test(); // ✅ OK:内部可以
}
PPT上的例子
cpp
class CHumanBeing { ... };
class CStudent: private CHumanBeing { ... };
void eat(const CHumanBeing& h)
{ ... }//访问CHumanBeing的成员
CHumanBeing a; CStudent b;
eat(a);
eat(b); //出错,类型都不相容
不要重定义缺省参数值
cpp
#include "iostream"
using namespace std;
class A {
public:
virtual void f(int x = 0) { cout << "A::f(" << x << ")\n"; }
};
class B : public A {
public:
virtual void f(int x = 1) { cout << "B::f(" << x << ")\n"; }
};
int main() {
A* p = new B;
p->f();
B b;
b.f();
}
为什么?
cpp
obj.f(); // 没有传参
编译器会将其转化为:
cpp
obj.f(0); // 假设缺省参数是 0
这个行为发生在编译时期,为静态绑定!
多继承
cpp
class 派生类名 : [继承方式1] 基类名1, [继承方式2] 基类名2, ... {
// 成员表
};
- 继承方式
- public、private 、protected
- 继承方式及访问控制的规定同单继承
- 派生类拥有所有基类的所有成员
多继承要点
-
一个类可同时继承多个基类:
class D : public B1, public B2 { ... }; -
构造顺序 :按基类声明从左到右(递归调用);析构顺序相反。
-
内存分配,和单继承类似,按照声明顺序。
- 先是基类的变量再是派生类的变量。
-
若多个基类有同名成员 → 访问歧义 ,需用
B1::member显式指定。cpp#include <iostream> using namespace std; class A { public: int value = 10; }; class B { public: int value = 20; }; class C : public A, public B { public: void print() { // cout << value << endl; // ❌ 错误:歧义!A::value 还是 B::value? cout << A::value << endl; // ✅ 显式指定 cout << B::value << endl; // ✅ 显式指定 } }; int main() { C c; // cout << c.value << endl; // ❌ 歧义 cout << c.A::value << endl; // ✅ OK cout << c.B::value << endl; // ✅ OK } -
菱形继承问题:两个基类共有一个祖先 → 导致祖先被重复继承,重复构造。
cppclass A { public: int val = 0; }; class B : public A {}; class C : public A {}; class D : public B, public C {}; int main() { D d; d.B::val = 1; d.C::val = 2; cout << d.B::val << ", " << d.C::val; // 输出:1, 2 }A会被构建几次?
两次!不是三次!
内存空间?
cpp+------------------+ | B 的部分 | | └─ A 的部分 | | int val | +------------------+ | C 的部分 | | └─ A 的部分 | | int val | +------------------+最顶上不会有A!
-
解决方法 :使用 虚继承 (
virtual public Base),确保公共基类只有一份。cppclass A { public: int val = 0; }; class B : virtual public A {}; class C : virtual public A {}; class D : public B, public C {};A只会被构建一次。
内存空间?
派生类前会多一个虚基类指针,指向共享虚基类。注意虚基类统一放在最后(即使有多个)
cpp+-----------------------------+ | B 的部分 | | vbptr_B →──────────────┐ | +--------------------------+ | C 的部分 | | vbptr_C →────────────┐ | +------------------------+ | [padding: 4 bytes] | +------------------------+ | 共享的 A 虚基类对象 | | int val | +------------------------+ -
虚基类构造
-
虚基类的构造函数由最新派生出的类的构造函数调用
-
在虚继承体系中,无论中间有多少层派生类,只有最派生类(most-derived class)。中间类(如 B、C)即使写了对虚基类的初始化(如 A(10)),也会被忽略。
cpp#include <iostream> using namespace std; class A { public: A(int x) { cout << "A(" << x << ")\n"; } }; class B : virtual public A { public: B() : A(10) { cout << "B()\n"; } // ❌ 这个 A(10) 会被忽略! }; class C : virtual public A { public: C() : A(20) { cout << "C()\n"; } // ❌ 这个 A(20) 也会被忽略! }; class D : public B, public C { public: D() : A(99), B(), C() { // ✅ 只有这里的 A(99) 生效 cout << "D()\n"; } }; int main() { D d; }输出:
cppA(99) B() C() D()
-
-
虚基类的构造函数优先非虚基类的构造函数执行
-
无论声明前后,虚基类构造函数一定优先非虚基类的构造函数执行
在下面的例子V会被优先构造:
cpp#include <iostream> using namespace std; class V { public: V() { cout << "V (virtual base)\n"; } }; class N1 { public: N1() { cout << "N1 (non-virtual base)\n"; } }; class N2 { public: N2() { cout << "N2 (non-virtual base)\n"; } }; class Mid : public N1, virtual public V { public: Mid() { cout << "Mid\n"; } }; class Final : public N2, public Mid { public: Final() { cout << "Final\n"; } }; int main() { Final f; }输出
cppV (virtual base) N2 (non-virtual base) N1 (non-virtual base) Mid Final -
虚函数内部则按照深度优先遍历,从左到右
-
-