C++封装和继承特性
目录
- 一、继承的概念与定义
- 1.1 什么是继承?
- 1.2 继承的访问控制
- 二、基类与派生类对象的赋值转换
- 三、继承中的作用域
- 四、派生类的默认成员函数
- 五、继承与友元
- 六、继承与静态成员
- 七、菱形继承与虚拟继承
- 7.1 菱形继承的问题
- 7.2 虚拟继承(Virtual Inheritance)
- 7.3 虚拟继承原理
一、继承的概念与定义
1.1 什么是继承?
继承是面向对象程序设计中最核心的代码复用机制。它允许我们在不修改原有类的基础上,通过扩展来创建新的类(称为派生类),从而形成类的层次结构。之前我们可能使用到的服复用都是函数复用,而继承就是类的复用
例如:Person 类作为基类,Student 和 Teacher 类作为派生类,复用 Person 中的成员变量和函数。
class Person {
protected:string _name = "liming";int _age = 20;
public:void Print() {cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
};class Student : public Person { //继承Person类,此时Student类中也存在Person类中的成员属性和方法,可以调用基类中的方法
protected:int _stuid; // 学号
};
class Teacher : public Person{protected:int _jobid; //工号
}int main()
{Student st;Teacher te;st.Print();te.Print();}
继承后,基类视为派生类的一部分
1.2 继承的访问控制
C++ 提供了三种继承方式:
- public 继承
- protected 继承
- private 继承
它们影响基类成员在派生类中的访问权限:
基类成员访问权限 | public 继承 | protected 继承 | private 继承 |
---|---|---|---|
public | public | protected | private |
protected | protected | protected | private |
private | 不可见 | 不可见 | 不可见 |
总结:
private
成员在派生类中不可见,但仍被继承。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 都不能去访问它。访问权限 = 访问限定符合继承方式两者中优先级高的。
优先级如下:
private > protected > public
默认继承方式:class 为 private,struct 为 public。在实际中基本都会显示写出继承方式,增加代码可读性
实际开发中常用 public 继承。
二、基类与派生类对象的赋值转换
- 派生类 → 基类:允许赋值(称为“切片”或“切割”),包括对象、指针、引用。可以通俗的认为:基类是派生类的一部分,派生类中一定包含基类的所有成员,因此可以构造出来一个完整的基类
- 基类 → 派生类:不允许直接赋值。与上一种情况相反,基类中没有所有的派生类成员,总会有部分成员没有合适的值去赋值,因此无法赋值
- 基类指针/引用 → 派生类指针/引用:可通过强制类型转换,但不安全,除非基类指针原本就指向派生类对象。
Student s;
Person p = s; // 切片
Person* pp = &s; // 派生类指针给基类指针赋值
Person& rp = s; // 派生类对象初始化基类引用// s = p; // 基类给派生类赋值错误
Student* ps = (Student*)pp; // 强制转换,算然可以,但是会存在越界访问
与隐式类型转换不同,隐式类型转换会用被转换的类型去构造出一个临时变量,然后用该临时变量去赋值或者拷贝构造出一个变量或者对象。而切片不会有临时变量的产生,他直接会将派生类成员拷贝或者调用拷贝构造去给基类成员赋值。
三、继承中的作用域
- 基类和派生类有独立的作用域。
- 如果派生类中有与基类同名的成员,会隐藏(也叫重定义) 基类的成员。
- 可使用 基类::成员 显式访问被隐藏的成员。
class Person {
public:void Show(){cout << "Person::Show()" << endl;}
protected:int _num = 111; // 身份证号
};class Student : public Person {
public:void Print() {cout << Person::_num << endl; // 显式访问基类成员cout << _num << endl; // 访问派生类成员}
protected:int _num = 999; // 学号,隐藏基类的 _num
};class Teacher : public Person {
public:void Show( int a) //与基类中成员函数同名,即使参数不同也构成隐藏{cout << "Teacher::Show(), a = " a << << endl;}
protected:int _name = "wanglaoshi";
};int main()
{Student s;s._num; //访问派生类自身的成员_num(学号)s.Person::_num; //访问基类中的_num(身份证号)Teacher t;t.Show(); //默认调用派生类成员方法t.Teacher::Show(5); //指定调用基类成员方法
}
注意:只要函数名相同也构成隐藏(不要求参数相同)。
四、派生类的默认成员函数
派生类的6个默认成员函数(构造、析构、拷贝构造、赋值、取地址)需注意:
- 构造函数:必须调用基类构造函数(可在初始化列表中调用)。
- 拷贝构造:必须调用基类拷贝构造。
- 赋值操作符:必须调用基类赋值操作符。
- 析构函数:先执行派生类析构,再自动调用基类析构。
- 析构函数会被特殊处理为destructor(),因此与基类析构函数构成隐藏关系。
class Student : public Person {
public:Student(const char* name, int num) //构造函数,必须调用基类构造: Person(name), _num(num) {}Student(const Student& s) //拷贝构造也必须调用基类拷贝构造: Person(s), _num(s._num) {}Student& operator=(const Student& s) { //赋值拷贝也必须调用基类赋值拷贝,但是*this指针是派生类的,也有没基类的任何表示,所以需要指定类作用域 + 显示调用基类赋值拷贝构造if (this != &s) {Person::operator=(s);_num = s._num;}return *this;}~Student() {// 自动调用 ~Person()}
};
在派生类中,直接调用 ~Person() 会调用到派生类的析构,因为析构函数在底层都将析构函数重命名为destructor(),形成了函数隐藏
需要指定作用域在调用。Person::~person() ,这样就可以显示调用到基类析构
注意:在析构函数中,如果我们显示调用基类的析构函数,那么在最后还是会自动的再次调用析构函数,导致重复析构的问题。这是因为要保证先调派生类析构,在调基类析构。
那么为什么要保证这个顺序呢?
如果我们在派生类中显示调用基类析构,此时我们依然在派生类作用域里,还是可以调用基类的属性和方法,若是在我们显示调用析构后我们还使用基类的属性和方法,则会造成一系列问题,为了防止这个问题,编译器强制在派生类析构执行完之后自动调用基类派生,无需我们手动调用。
除了析构函数的顺序,构造函数也有顺序,不过是先基类,在派生类,因此派生类在整个生命周期调用函数顺序如下图:
五、继承与友元
友元关系不能继承。基类的友元函数不能访问派生类的私有或保护成员。
class Person {friend void Display(const Person& p, const Student& s);
protected:string _name;
};class Student : public Person {
protected:int _stuNum;
};void Display(const Person& p, const Student& s) {cout << p._name << endl; // ✅cout << s._stuNum << endl; // ❌ 错误,不能访问 Student 的 protected 成员
}
六、继承与静态成员
基类中定义的 static
成员在整个继承体系中只有一份实例,所有派生类共享。
class Person {
public:static int _count;
};
int Person::_count = 0;class Student : public Person { /* ... */ };
class Graduate : public Student { /* ... */ };// 所有类共享同一个 _count
七、菱形继承与虚拟继承
7.1 菱形继承的问题
菱形继承指一个类继承自两个具有共同基类的类,导致:
- 数据冗余:共同基类的成员在最终派生类中有两份。
- 二义性:访问共同成员时需指定路径。
class Person { string _name; };
class Student : public Person { int _num; };
class Teacher : public Person { int _id; };
class Assistant : public Student, public Teacher { };Assistant a;
a._name = "peter"; // ❌ 二义性
a.Student::_name = "xxx"; // ✅ 指定路径
7.2 虚拟继承(Virtual Inheritance)
使用 virtual继承可解决菱形继承问题:
class Student : virtual public Person { };
class Teacher : virtual public Person { };
class Assistant : public Student, public Teacher { };
7.3 虚拟继承原理
- 虚基类(如 Person)在最终派生类中只保留一份。
- 派生类通过虚基表指针和偏移量来访问共享的基类成员。
virtual继承可解决菱形继承问题:
class Student : virtual public Person { };
class Teacher : virtual public Person { };
class Assistant : public Student, public Teacher { };