引入
在软件开发中,我们常常会遇到多个类拥有相似的成员或功能的情况。比如:
class Cat {
public:
void eat() { cout << "Cat is eating" << endl; }
void sleep() { cout << "Cat is sleeping" << endl; }
};
class Dog {
public:
void eat() { cout << "Dog is eating" << endl; }
void sleep() { cout << "Dog is sleeping" << endl; }
};
可以看到,Cat 和 Dog 中有大量重复代码。为了避免代码冗余、提升可维护性,我们可以将共同的部分提取出来,放到一个更通用的类中(比如 Animal),然后让 Cat 和 Dog 去继承这个基类。
继承是 C++ 面向对象编程三大特性之一(封装、继承、多态),它的主要作用包括:
代码复用:派生类自动拥有基类的成员函数和成员变量。
建立层次关系:通过 is-a 关系组织类结构。
实现多态基础:继承 + 虚函数 实现运行时多态(如虚函数表机制)。
使用继承,我们可以从通用到具体地构建系统,例如:
class Animal {
public:
void eat() { cout << "Animal is eating" << endl; }
};
class Cat : public Animal {
public:
void meow() { cout << "Meow!" << endl; }
};
class Dog : public Animal {
public:
void bark() { cout << "Bark!" << endl; }
};
现在,Cat 和 Dog 不再重复 eat(),并且还能定义各自特有的行为,结构更清晰、扩展更方便。
一、继承的基本语法:
#include <iostream>
class Base {
public:
void show() { std::cout << "Base" << std::endl; }
};
class Derived : public Base {
// 派生类没有定义任何成员,但自动继承了 Base 的成员函数
};
int main() {
Derived d;
d.show(); // 调用的是 Base 中的 show()
return 0;
}
语法结构解释:
class 派生类名 : 继承方式 基类名 {
// 派生类定义
};
Base 是基类(父类)
Derived 是派生类(子类)
public Base 表示 公有继承,也就是 继承方式是 public
二、继承方式和基类成员
继承的类型:公有继承 public、保护继承 protected、私有继承 private
解释:protected 是对子类开放的封装,private 是只对当前类开放的封装。
class Base {
public:
int x = 1;
protected:
int y = 2;
};
class Derived : protected Base {
public:
void test1() {
x = 10; // x 变成 protected,可以访问
y = 20; // y 本来就是 protected,可以访问
}
};
class SubDerived : public Derived {
public:
void test2() {
x = 30; // x 是 protected,可以访问
y = 40; // y 是 protected,可以访问
}
};
上面是 protected 继承,子类 SubDerived 能访问 x 和 y,虽然它们对外部是隐藏的。
class Derived2 : private Base {
public:
void test1() {
x = 10; // x 变成 private,Derived2 自己能访问
y = 20; // OK
}
};
class SubDerived2 : public Derived2 {
public:
void test2() {
// x = 30; // 错误,x 是 private,不可访问
// y = 40; // 错误
}
};
上面是 private 继承,子类 SubDerived2 无法访问 x 和 y,它们被“锁死”在 Derived2 中。
(2)基类和派生类对象赋值转换
2.1 基类对象赋值给派生类对象(不允许)
基类和派生类之间的赋值通常是不允许的,因为派生类包含了基类之外的成员,直接赋值可能会丢失派生类中的信息。
#include<iostream>
using namespace std;
class Base {
public:
int baseValue;
Base(int v) : baseValue(v) {}
};
class Derived : public Base {
public:
int derivedValue;
Derived(int v1, int v2) : Base(v1), derivedValue(v2) {}
};
int main() {
Base b(10);
Derived d1(20, 30);
// 这行代码会报错
// d1 = b; // 错误:不能将基类对象赋值给派生类对象
return 0;
}
基类 Base 和派生类 Derived 在内存布局上不同。派生类 Derived 除了继承自基类 Base 外,还可能有额外的数据成员。
直接赋值基类对象到派生类对象,会丢失派生类的额外成员,因此编译器不允许这么做。
2.2派生类对象赋值给基类对象(允许,但只拷贝基类部分)
派生类对象赋值给基类对象(允许,但只拷贝基类部分)
#include<iostream>
using namespace std;
class Base {
public:
int baseValue;
Base(int v) : baseValue(v) {}
};
class Derived : public Base {
public:
int derivedValue;
Derived(int v1, int v2) : Base(v1), derivedValue(v2) {}
};
int main() {
Derived d(20, 30); // 派生类对象
Base b(0);
b = d; // 派生类对象赋值给基类对象
cout << "Base value: " << b.baseValue << endl;
// 基类部分被赋值了,输出: Base value: 20
return 0;
}
在这个例子中,d 是 Derived 类型的对象,它被赋值给 b,即基类类型的对象。
赋值后,b.baseValue 被正确地赋值为 d.baseValue,但是 d.derivedValue 不能拷贝给 b,因为 b 并不包含 derivedValue 成员。
2.3基类指针或引用指向派生类对象(允许)(多态章节讲)
基类的指针或引用可以指向派生类对象,这种赋值称为 多态,在运行时通过虚函数实现动态绑定。这样可以使基类指针或引用操作派生类的对象。
#include<iostream>
using namespace std;
class Base {
public:
virtual void show() {
cout << "Base class" << endl;
}
};
class Derived : public Base {
public:
void show() override {
cout << "Derived class" << endl;
}
};
int main() {
Base* basePtr;
Derived d;
// 基类指针指向派生类对象
basePtr = &d;
basePtr->show(); // 调用的是 Derived 类中的 show() 方法,输出: Derived class
return 0;
}
基类的指针 basePtr 可以指向派生类的对象 d。
通过基类指针调用虚函数 show() 时,实际上调用的是派生类 Derived 中的重写函数,因为虚函数实现了动态绑定。
三、继承中名字隐藏
作用域是独立的:
父类和子类有自己各自的成员变量和函数,即使重名也互不影响,但子类优先访问自己的作用域。
函数名相同就构成隐藏:
子类一旦定义了一个同名函数,哪怕参数不同,也会屏蔽所有父类同名函数,想要使用需要指定类域。
#include<iostream>
using namespace std;
class Base {
public:
int x = 1;
void func() {
cout << "Base::func()" << endl;
}
void func(int n) {
cout << "Base::func(int): " << n << endl;
}
};
class Derived : public Base {
public:
int x = 2; // 与 Base::x 同名,隐藏
void func() { // 同名函数,隐藏了 Base::func 和 Base::func(int)
cout << "Derived::func()" << endl;
}
void test() {
cout << "x = " << x << endl; // 访问的是 Derived::x
cout << "Base::x = " << Base::x << endl; // 显示访问基类成员
func(); // 调用 Derived::func()
Base::func(); // 显示调用 Base::func()
Base::func(100); // 调用重载版本 Base::func(int)
}
};
int main() {
Derived d;
d.test();
return 0;
}
四、派生类的六个默认成员函数
(4.1)派生类的构造函数必须调用基类构造函数
默认情况下,派生类的构造函数会自动调用基类的默认构造函数。
如果基类没有默认构造函数,必须显式调用某个构造函数。
#include<iostream>
using namespace std;
class Base {
public:
Base(int x) {
cout << "Base构造函数: " << x << endl;
}
};
class Derived : public Base {
public:
// Derived 默认构造函数,必须显式初始化 Base
Derived() : Base(100) {
cout << "Derived构造函数" << endl;
}
};
int main() {
Derived d;
}
(4.2)拷贝构造函数自动调用基类的拷贝构造函数
基类如果有显示的拷贝构造:
编译器不会自动调用基类的拷贝构造函数;
你必须显式地在初始化列表中调用;
否则只会默认使用基类的默认构造函数,或者报错(如果没有默认构造函数)。
#include <iostream>
using namespace std;
class Base {
public:
Base() {}
Base(const Base&) {
cout << "Base 拷贝构造函数" << endl;
}
};
class Derived : public Base {
public:
Derived() {}
// 显式调用 Base 的拷贝构造
Derived(const Derived& d) : Base(d) {
cout << "Derived 拷贝构造函数" << endl;
}
};
int main() {
Derived d1;
Derived d2 = d1; // 输出两个拷贝构造函数
}
没有手动调用 Base 的拷贝构造
#include <iostream>
using namespace std;
class Base {
public:
Base() {
cout << "默认构造" << endl;
}
Base(const Base&) {
cout << "Base 拷贝构造函数" << endl;
}
};
class Derived : public Base {
public:
Derived() {}
Derived(const Derived& d) {
cout << "Derived 拷贝构造函数" << endl;
}
};
int main() {
Derived d1;
Derived d2 = d1;
}
(4.3)为什么派生类重载了 operator= 后,需要手动调用基类的 operator=
在 C++ 中,如果我们在派生类中重载了 operator=,很多人以为编译器会自动帮我们调用基类的赋值运算符,其实并不会。
#include <iostream>
using namespace std;
class Base {
public:
int a = 10; // 有数据成员
};
class Derived : public Base {
public:
int b = 20;
Derived& operator=(const Derived& d) {
b = d.b;
cout << "Derived::operator= 被调用" << endl;
return *this;
}
void show() {
cout << "Base::a = " << a << ", Derived::b = " << b << endl;
}
};
int main() {
Derived d1;
d1.a = 111;
d1.b = 222;
Derived d2;
d2 = d1;
d2.show(); // Base::a 没有被赋值
}
此时的基类到底有没有编译器生成的operator=? 有,调试的时候虽然是没有直接进入这个基类,因为你在 Derived 中手动写了 operator=,编译器不会自动帮你调用 Base::operator=,这是你自己的职责。所以你必须手动调用。
看下一个测试用例:我们手动调用基类的operator =
Derived& operator=(const Derived& d) {
Base::operator=(d); // 手动调用
b = d.b;
return *this;
}
此时就是明显的观察到调用了基类的operator= ,并且编译器是生成了operator= 这个功能。
(4.4)析构函数:先析构派生类,再析构基类
#include <iostream>
using namespace std;
class Base {
public:
Base() {
cout << "Base 构造" << endl;
}
};
class Derived : public Base {
public:
Derived() {
cout << "Derived 构造" << endl;
}
};
int main() {
Derived d;
}
(4.5)构造顺序:先构造基类,再构造派生类
class Base {
public:
~Base() {
cout << "Base 析构" << endl;
}
};
class Derived : public Base {
public:
~Derived() {
cout << "Derived 析构" << endl;
}
};
int main() {
Derived d;
}
五、友元与继承
子类不能访问父类私有成员,除非父类主动“邀请”子类成为友元;同时,友元关系是不传递、不继承的。
(5.1)子类不是父类的友元
class Base {
private:
int a = 1;
};
class Derived : public Base {
public:
void func() {
// a = 10; // 编译错误,子类不能访问 Base 的私有成员
}
};
(5.2)友元关系不能继承
如果 A 是 B 的友元,那么 A 的子类或 B 的子类都不会自动拥有这个友元关系。
class A {
friend class B;
private:
int x = 10;
};
class B {
public:
void f(A& a) {
a.x = 20; //B 是 A 的友元
}
};
class B_Derived : public B {
public:
void f2(A& a) {
// a.x = 30; //错误,友元关系不会继承
}
};
(5.3)友元类可以是派生类,也可以是基类
class Derived;
class Base {
friend class Derived; //Derived 可访问 Base 的私有成员
private:
int x = 1;
};
class Derived : public Base {
public:
void f() {
x = 100; //友元访问私有成员
}
};
(5.4)解决办法:声明
#include <iostream>
using namespace std;
class Derived; // 提前声明
class Base {
friend void friendFunc(Base& b); // friendFunc 是 Base 的友元
protected:
int base_protected = 1;
private:
int base_private = 2;
};
class Derived : public Base {
protected:
int derived_protected = 3;
private:
int derived_private = 4;
};
// 定义友元函数
void friendFunc(Base& b) {
cout << b.base_protected << endl; // Base 的保护成员
cout << b.base_private << endl; // Base 的私有成员
// cout << b.derived_protected << endl; // 错误:friendFunc 不是 Derived 的友元
}
Derived 类也加上 friend class FriendClass;,FriendClass 能访问 Derived 的私有和受保护成员
六、静态变量与继承
(6.1)子类会继承静态变量(但不是拷贝)
静态变量只有一份存储空间,在继承中不会为每个子类再创建新的副本。
子类可以访问父类的静态变量(只要权限允许:public 或 protected)。
#include<iostream>
using namespace std;
class Base {
public:
static int count;
};
int Base::count = 0;
class Derived : public Base {};
int main() {
Base::count = 1;
Derived::count = 2;
cout << Base::count << endl; // 输出 2
cout << Derived::count << endl; // 输出 2
}
(6.2)如果子类定义了同名静态变量
#include<iostream>
using namespace std;
class Base {
public:
static int count;
};
int Base::count = 0;
class Derived : public Base {
public:
static int count; // 隐藏了 Base::count
};
int Derived::count = 100;
int main() {
cout << Base::count << endl; // 输出 0
cout << Derived::count << endl; // 输出 100
}
那么这个时候,子类会隐藏基类的静态变量。
七、菱形继承及菱形虚拟继承
(7.1)什么是菱形继承
A
/ \
B C
\ /
D
类 B 和 C 都从 A 派生;
类 D 同时继承自 B 和 C;
这就导致:D 继承了两份 A 的成员。
(7.2)引发二义性
#include<iostream>
using namespace std;
class A {
public:
int x;
};
class B : public A {};
class C : public A {};
class D : public B, public C {};
int main() {
D d;
d.x = 10;// 不明确是 B::A::x 还是 C::A::x
d.B::x = 10; // 可以用作用域限定符解决,但不优雅
}
(7.3)虚拟继承(Virtual Inheritance)解决重复继承问题
通过在继承时加上 virtual 关键字,告诉编译器:这个基类只保留一份副本。
#include<iostream>
using namespace std;
class A {
public:
int x;
};
class B : virtual public A {}; // 注意 virtual
class C : virtual public A {};
class D : public B, public C {};
int main() {
D d;
d.x = 10;
}
B 和 C 虚继承了 A;
D 只包含一份 A 的子对象;
这个机制通过 虚基表指针(vptr) 保证只构造一份基类子对象。
(7.4)虚拟继承的底层原理
上段代码,B类里面有基类A吗?
类 B 虽然写了 : virtual public A,但它:
不包含 A 的独立子对象(子对象指内存中独立的一份 A);
它只是声明了“我依赖一个虚基类 A”,这意味着它知道将来某个派生类会真正提供这份 A 的实体。
所以:B 有继承关系上的 A,但没有真正的 A 成员对象。
#include <iostream>
using namespace std;
class A {
public:
int x;
};
class B : virtual public A {
public:
int b;
};
class C : virtual public A {
public:
int c;
};
class D : public B, public C {
public:
int d;
};
int main() {
D d;
d.x = 1;
d.b = 2;
d.c = 3;
d.d = 4;
cout << "x = " << d.x << endl;
cout << "b = " << d.b << endl;
cout << "c = " << d.c << endl;
cout << "d = " << d.d << endl;
return 0;
}
地址 值
0x008FF754 48 9b 4b 00 ; 偏移0:B的vbptr(虚基表指针)或其它继承指针
0x008FF758 02 00 00 00 ; 偏移4:B::b = 2
0x008FF75C 54 9b 4b 00 ; 偏移8:C的vbptr(虚基表指针)
0x008FF760 03 00 00 00 ; 偏移12:C::c = 3
0x008FF764 04 00 00 00 ; 偏移16:D::d = 4
0x008FF768 01 00 00 00 ; 偏移20:A::x = 1(虚基类成员)
把 48 9b 4b 00 转成地址 0x004B9B48
0x004B9B48 00 00 00 00
0x004B9B4C 14 00 00 00 存储的B的偏移量
14 00 00 00转换为十进制就是20
D 对象内存布局:
+---------------------+
| B::vbptr ---------->|
| B::b |
+---------------------+
| C::vbptr ---------->|
| C::c |
+---------------------+
| D::d |
+---------------------+
| 共享的 A::x | <-- 只存在一份!由 D 初始化
+---------------------+