讨论C++继承
- 继承
- 定义
- 继承方式和访问限定符
- 基类和派生类的赋值转换
- 继承中的作用域
- 派生类的默认成员函数
- 继承和友元
- 继承和静态成员
- 菱形继承
- 虚拟继承
继承是面向对象程序设计中,使代码可以复用的重要手段,它允许程序员在保持原有类特性的基础上进行扩展。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。
继承
定义
class Person {
public:void print() {cout << "这是一个人类" << endl;}
protected:string name;int age;
};class Student : public Person{
private:int stuId;
};int main() {Student s;Person p;s.print();p.print();return 0;
}
上述代码,
Person
是基类,Student
是派生类。使用
:
来实现继承。
继承方式和访问限定符
基类/继承方式 | public继承 | protected继承 | private继承 |
---|---|---|---|
public成员 | 派生类的public成员 | 派生类的protected成员 | 派生类的private成员 |
protected成员 | 派生类的protected成员 | 派生类的protected成员 | 派生类的private成员 |
private成员 | 在派生类中不可见 | 在派生类中不可见 | 在派生类中不可见 |
- 基类使用
private
修饰的成员在派生类中无论以什么方式继承,都不可见。不可见是指还是会继承,只是不能直接使用,因为private
修饰的成员在类外不可使用。 protected
修饰的成员不可在类外使用,但是存在继承关系的,派生类可以直接访问基类的成员。- 使用关键字
class
时,默认的继承方式是private
,使用struct
时,默认的继承方式是public
。
基类和派生类的赋值转换
派生类对象可以直接赋值给基类的对象、指针、引用。
基类的对象不能赋值给派生类对象。
基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用,但必须是基类的指针指向派生类的对象时才安全。
class Person {
public:void print() {cout << "这是一个人类" << endl;}
protected:string name;int age;
};class Student : public Person{
private:int stuId;
};int main() {Student s;Person* ps = &s;Person& rps = s;Person p = s;return 0;
}
- 派生类对象赋值给基类对象,基类对象只能访问基类拥有的成员变量,不能访问派生类特有的成员变量。
- 派生类赋值给基类对象时,不产生临时变量。
继承中的作用域
- 在继承体系中,基类和派生类的作用域是独立的。
- 当基类和派生类中有同名成员时,派生类成员将屏蔽基类同名成员的直接访问,称为隐藏或重定义。
- 成员函数只需要函数名相同即可达成隐藏。
class Person {
protected:string name = "张三";int age = 10;
};class Student : public Person{
public:void print() {cout << "姓名:" << name << endl;cout << "年龄:" << age << endl;cout << "学号:" << stuId << endl;}
private:int stuId;int age;
};int main() {Student s;s.print();return 0;
}
- 上述代码,
Student
类中的age
和Person
类中的age
构成隐藏。Student
对象使用自己类域中的age
成员变量,因此是随机值。 - 要想使用
Person
类中的age
,需要Person::age
。
class Student : public Person{
public:void print() {cout << "姓名:" << name << endl;cout << "年龄:" << Person::age << endl;cout << "学号:" << stuId << endl;}
private:int stuId;int age;
};
派生类的默认成员函数
- 派生类构造时必须调用基类的构造函数初始化基类的部分成员,如果基类没有默认构造函数,必须在派生类构造函数初始化列表中显示调用。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的
operator=
必须调用基类的operator=
完成基类的赋值。- 派生类的析构函数会在调用完成后自动调用基类的析构函数清理基类成员。
- 派生类对象初始化先调用基类构造函数再调用派生类构造函数。
- 派生类对象析构时先调用派生类析构函数再调用基类析构函数。
class Person {
public:Person(const char *name = "张三"): _name(name) {cout << "Person(name)" << endl;}Person(const Person &p): _name(p._name) {cout << "Person(const Person &p)" << endl;}Person& operator=(const Person& p) {cout << "Person& operator=(const Person& p)" << endl;if(this != &p) {_name = p._name;}return *this;}~Person() {cout << "~Person()" << endl;}protected:string _name;
};class Student : public Person {
public:Student(const char *name, int stuId = 110): Person(name), _stdId(stuId) {cout << "Student(name, stuId)" << endl;}Student(const Student &s): Person(s), _stdId(s._stdId) {cout << "Student(const Student &s)" << endl;}Student& operator=(const Student& s) {cout << "Person& operator=(const Person& p)" << endl;if(this != &s) {Person::operator=(s);_stdId = s._stdId;}return *this;}~Student() {cout << "~Student()" << endl;}private:int _stdId;
};int main() {Student s1("李四", 111);cout << "====================" << endl;Student s2(s1);cout << "====================" << endl;Student s3("王五", 112);s2 = s3;cout << "====================" << endl;return 0;
}
- 根据规则,派生类构造前应完成基类构造,因此在创建派生类对象,一定会初始化基类,析构时先析构派生类,再析构基类。
继承和友元
友元关系不能被继承。
class Student;
class Person {
public:friend void print(const Person& p, const Student& s);
public:Person(const char *name = "张三"): _name(name) {cout << "Person(name)" << endl;}Person(const Person &p): _name(p._name) {cout << "Person(const Person &p)" << endl;}Person& operator=(const Person& p) {cout << "Person& operator=(const Person& p)" << endl;if(this != &p) {_name = p._name;}return *this;}~Person() {cout << "~Person()" << endl;}protected:string _name;
};class Student : public Person {
public:Student(const char *name, int stuId = 110): Person(name), _stdId(stuId) {cout << "Student(name, stuId)" << endl;}Student(const Student &s): Person(s), _stdId(s._stdId) {cout << "Student(const Student &s)" << endl;}Student& operator=(const Student& s) {cout << "Person& operator=(const Person& p)" << endl;if(this != &s) {Person::operator=(s);_stdId = s._stdId;}return *this;}~Student() {cout << "~Student()" << endl;}protected:int _stdId;
};void print(const Person& p, const Student& s) {cout << p._name << endl;cout << s._name << s._stdId << endl;
}int main() {Student s("李四", 111);print(s, s);return 0;
}
- 上述代码会报错,
error: '_stdId' is a protected member of 'Student'
。证明友元函数没有被继承,因为Student
类使用private
和protected
修饰的成员变量不能在类外使用。
继承和静态成员
基类定义的
static
静态成员,整个继承体系中只有一个这样的成员。
class Person {
public:Person() {++n;}
public:static int n;
};
int Person::n = 0;
class Student : public Person {
};int main() {Student s;cout << Person::n << endl;s.n = 10;cout << Person::n << endl;return 0;
}
菱形继承
- 单继承:一个派生类只有一个直接基类。
Java
只支持单继承。
- 多继承:一个派生类继承于多个基类。
C++
支持多继承。
- 菱形继承:它是多继承的一个特殊情况。
菱形继承所带来的问题是,最初继承的类内成员会存在两份,即数据冗余和二义性问题。
class A {
public:int _a;
};class B : public A{
public:int _b;
};class C : public A {
public:int _c;
};class D : public B, public C {
public:int _d;
};int main() {D d;return 0;
}
- 想要直接通过
d
对象去修改_a
是不被允许的,因为编译器不知道想要修改的是B
类和C
类中哪个类的_a
。 - 只能通过
d.B::a = 10
这种指定类域的方式来修改或赋值。这种方式可以解决二义性问题,但无法解决数据冗余问题。
虚拟继承
- 使用
virtual
关键字建立虚拟继承,可以解决数据冗余和二义性问题。 - 当不使用虚拟继承时,内存空间是这样的。
class A {
public:int _a;
};class B : public A{
public:int _b;
};class C : public A {
public:int _c;
};class D : public B, public C {
public:int _d;
};int main() {D d;d.B::_a = 1;d.C::_a = 9;d._b = 2;d._c = 3;d._d = 4;return 0;
}
- 使用虚拟继承,
_a
就只存在一份,可以直接赋值。
class A {
public:int _a;
};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.B::_a = 1;d.C::_a = 9;d._b = 2;d._c = 3;d._d = 4;return 0;
}
clang
编译器通过B
和C
的两个指针,指向一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存在偏移量。通过偏移量可以找到A
。