文章目录
1 继承的概念和定义
1.1 概念
继承就是对类(的成员)的复用。被复用的类叫做父类(基类),复用其他类的类叫做子类(派生类)。
继承的作用:子类中不用重复写父类的成员,减少代码冗余。
例如,以下代码中,Student
子类和Teacher
子类就继承了Person
父类:
class Person
{
public:
void print()
{
cout << "name: " << _name << endl;
cout << "age: " << _age << endl;
}
protected:
string _name;
int _age;
};
// Student子类,通过public方式继承Person
class Student : public Person
{
private:
int _stuid; // 学号
};
// Teacher子类,通过public方式继承Person
class Teacher : public Person
{
private:
int _jobid; // 工号
};
继承之后,父类的所有成员,包括成员函数和成员变量,都会成为子类的一部分。也就是说,子类Student
和Teacher
复用了父类Person
的成员。
继承之后,以子类Student
为例,它的内部可以简单地理解为(但并不严谨):
class Student
{
public:
void print()
{
cout << "name: " << _name << endl;
cout << "age: " << _age << endl;
}
protected:
string _name;
int _age;
private:
int _stuid; // 学号
};
1.2 定义
1.2.1 定义格式
继承的定义格式如下:
tip:继承方式可以省略不写。不写的时候,对于class
类,继承方式默认private
;对于struct
类,继承方式默认public
。
比如class Student : public Person
这一句代码中,Student
是子类,public
是继承方式,Person
是父类。
1.2.2 继承方式
我们知道,访问限定符有3个:
public
访问protected
访问private
访问
继承方式和访问限定符类似,也分为:
public
继承protected
继承private
继承
1.2.3 继承到子类后访问方式的变化
不同的继承关系决定了继承后,子类中通过继承得到的父类成员是否可见(子类内能否访问,而不是肉眼能不能看见),以及访问限定符是否改变,改变成什么。
基本规则如下:
- 对于父类的
private
成员,无论是什么继承方式,在子类中都不可见(被继承,但是无论子类内还是子类外都无法访问)。 - 对于父类的非
private
成员,子类继承之后,在子类中会被修改访问限定方式:根据public
>protected
>private
的规则,被继承的父类的非private
成员,在子类中访问限定符会修改为父类的访问限定方式和继承方式的最小值。(比如:父类的protected
成员被子类用public
继承后,在子类中访问限定符依然是protected
;反之,父类的public
成员被子类用protected
继承后,在子类中访问限定符被修改为protected
。)
父类成员访问限定符\子类继承方式 | public | protected | private |
---|---|---|---|
pulic | public | protected | private |
protected | protected | protected | private |
private | 在派生类不可见 | 在派生类不可见 | 在派生类不可见 |
tip:
protected
成员继承后只能在子类内访问,不能在子类外访问。因此,protected
访问限定符是为继承而生的。- 一般使用
public
继承方式。
2 基类和派生类对象赋值兼容转换
子类对象可以赋值给父类的对象/指针/引用。这个过程叫做切片(切割),因为会把子类中父类那一部分进行切割并赋值过去。
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name;
int _age;
};
class Student : public Person
{
public:
void func()
{
cout << _name << endl;
cout << _age << endl;
}
private:
int _stuid;
};
例如,对于以下父类及其子类:
class Person
{
protected:
string _name;
size_t _age;
};
// 学生子类
class Student : public Person
{
private:
size_t _id; // 学号
};
可以有如下逻辑:
// 子类对象赋值给父类
Student s;
Person p = s; // 子类对象赋值给父类对象
Person& ref = s; // 子类对象赋值给父类引用
Person* ptr = &s; // 子类对象的地址赋值给父类指针
父类的指针也可以赋值给子类指针:
// 父类指针赋值给子类指针
Student s;
Person p;
Person* pp = &s;
Student* ps1 = (Student*)pp; // 父类指针赋值给子类指针
pp = &p;
Student* ps2 = (Student*)pp; // 父类指针赋值给子类指针。这种情况下,访问子类成员时会越界访问
3 继承中的作用域
在继承体系中基类和派生类都有独立的作用域。
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问(可以通过作用域限定符间接访问),这种情况叫隐藏(重定义)。
例如,对于以下代码,演示了成员的隐藏和访问:
class Person
{
public:
void func()
{
cout << _id << endl;
}
protected:
int _id = 1;
};
class Student : public Person
{
public:
void func()
{
cout << _id << endl;
cout << Person::_id << endl;
}
protected:
int _id = 2;
};
int main()
{
Student s;
s.func(); // 2 1
s.Person::func(); // 1
return 0;
}
函数重载和函数隐藏(重定义)的区别:
- 函数重载:同一作用域的同名函数、不同参数列表(参数个数、类型不同)。
- 函数隐藏:分别存在于父子类中的同名函数。
4 子类的默认成员函数
子类的默认成员函数规则,只是多了父类的那一部分,父类那部分调用父类的函数去完成。
构造:先父后子;析构:先子后父(自动调用)。
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的赋值重载函数必须要调用基类的赋值重载函数完成基类的复制。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
class Person
{
public:
Person(const string& name = "none")
: _name(name)
{
cout << "父类构造" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "父类拷贝构造" << endl;
}
Person& operator=(const Person& p)
{
cout << "父类赋值重载" << endl;
if (this != &p)
{
_name = p._name;
}
return *this;
}
~Person()
{
cout << "父类析构" << endl;
}
protected:
string _name;
};
class Student : public Person
{
public:
Student(string name = "none", size_t id = 0)
: Person(name) //调用父类的构造函数初始化父类的那一部分成员
, _id(id)
{
cout << "子类构造" << endl;
}
Student(const Student& s)
: Person(s) //调用父类的拷贝构造函数初始化父类的那一部分成员
, _id(s._id)
{
cout << "子类拷贝构造" << endl;
}
Student& operator=(const Student& s)
{
cout << "子类赋值重载" << endl;
if (this != &s)
{
Person::operator=(s); // 调用父类的赋值重载函数完成父类成员的赋值,发生切片。注意父子类的赋值重载函数构成隐藏
_id = s._id;
}
return *this;
}
~Student()
{
cout << "子类析构" << endl;
// 子类的析构函数会在被调用完成后自动调用基类的析构函数
}
private:
size_t _id;
};
int main()
{
Person p1;
Person p2(p1);
p2 = p1;
cout << "---------------------------------" << endl;
Student s1;
Student s2(s1);
s2 = s1;
cout << "---------------------------------" << endl;
return 0;
}
总结:构造:先父后子(需要自己写);析构:先子后父(不用自己写)。
tip:父子类的赋值重载函数、析构函数(因为析构函数名都被特殊处理为destructor()
)构成隐藏,在子类中使用父类的函数需要指定类域。
5 继承与友元
友元关系不能继承。想让友元函数访问哪个类,就需要在这个类加上友元。
#include <iostream>
using namespace std;
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
size_t _id; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl; // 在Person类中声明friend才可以使用类内成员
cout << s._id << endl; // 在Student类中声明friend才可以使用类内成员
}
int main()
{
Person p;
Student s;
Display(p, s);
}
6 继承与静态成员
静态成员在静态区。若父类当中定义了一个static
静态成员变量,则在整个继承体系里面只有一个该静态成员。无论派生出多少个子类,都只有一个static
成员实例。我们可以利用这个特性,来统计这个继承体系中类的个数。
#include <iostream>
using namespace std;
class Person
{
public:
Person() { ++_count; }
protected:
string _name;
public:
static int _count; // 统计人的个数。可以通过此类以及子类访问
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum;
};
class Graduate : public Student
{
protected:
string _seminarCourse; // 研究科目
};
int main()
{
Student s1, s2, s3;
Graduate g1;
cout << " 人数:" << Person::_count << endl; // 人数:4
Student::_count = 0;
cout << " 人数:" << Person::_count << endl; // 人数:0
cout << &Person::_count << ' ' << &Student::_count << ' ' << &Graduate::_count << endl; // 发现是同一个
return 0;
}
7 单继承、多继承、菱形继承、菱形虚拟继承
7.1 定义
- 单继承:一个子类只有一个直接父类。
- 多继承:一个子类继承自两个或多个直接父类。
- 菱形继承:一个子类继承自两个或多个直接父类,并且这些直接父类继承自同一个父类。
7.2 数据存储位置
为了分析单继承、多继承、菱形继承的数据存储位置,我们先写出如下代码,包含4个类,总体构成菱形继承。
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;
};
class E
{
public:
int _e;
};
class F
{
public:
int _f;
};
class G : public E, public F
{
public:
int _g;
};
继承关系如图所示:
7.2.1 单继承
我们在main
函数写下以下代码:
int main()
{
B b;
b._a = 1;
b._b = 2;
return 0;
}
B
类的成员变量在内存中存储顺序如图所示:
即:
通过调试,发现单继承的成员变量存放顺序:先存父类的成员变量,再存子类的成员变量。
7.2.2 多继承
我们在main
函数写下以下代码:
int main()
{
E e;
e._a = 1;
e._b = 2;
e._e = 3;
return 0;
}
G
类的成员变量在内存中存储顺序如图所示:
即:
通过调试,发现多继承的成员变量存放顺序:按照继承关系的声明顺序,依次存放父类成员变量,最后存放自己的成员变量。
7.2.3 菱形继承
我们在main
函数写下以下代码:
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
D
类的成员变量在内存中存储顺序如图所示:
即:
通过调试,发现菱形继承的成员变量存放顺序:按照继承关系的声明顺序,依次存放直接父类的成员变量,最后再存放自己的成员变量。
菱形继承的问题:继承了两次最终父类A
的成员变量,存在数据冗余和二义性(使用_a
成员时,需要指定类域)。
为了解决数据冗余和二义性,我们可以使用菱形虚拟继承。
7.2.4 菱形虚拟继承
菱形虚拟继承,需要使用virtual
关键字对最终父类的下一级进行修饰。也就是对B
和C
两个类进行修饰。
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;
};
继承关系如图所示:
我们在main
函数写下以下代码:
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;
return 0;
}
D
类的成员变量在内存中存储顺序如图所示:
即:
通过调试,发现菱形虚拟继承的成员变量存放顺序:和菱形继承类似,但有两点不同。
- 原来存放最终父类的位置用来存放一个指针,叫做虚基表指针,它指向的前四个字节是为多态的虚表预留的存偏移量的位置(这里我们不必关心),第二个数据就是当前类的对象的地址距离最终父类的首个成员变量存放位置的偏移量。
- 最终父类的成员变量存放在最终子类之后。
7.2.5 虚拟继承
既然菱形虚拟继承会把virtual
修饰的子类的直接父类的成员变量放在内存末尾,并且在非虚拟继承时本该存放直接父类成员变量的位置存放虚基表指针。那么是不是只要是虚拟继承,就有这两个特点?
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
int main()
{
B b;
b._a = 1;
b._b = 2;
return 0;
}
通过调试,我们确认:只要是虚拟继承,就有这两个特点。
8 继承和组合
继承是一种is-a的关系,也就是说每个派生类对象都是一个基类对象;而组合是一种has-a的关系,若是B组合了A,那么每个B对象中都有一个A对象。
例如,宝马是车,它们之间就是is-a的关系,它们之间适合使用继承。
class Car
{
protected:
string _num; //车牌号
};
class BMW : public Car
{
private:
size_t _speed;
};
例如,车有轮胎,它们之间就是has-a的关系,它们之间则适合使用组合。
class Tyre
{
protected:
size_t _size; //尺寸
};
class Car
{
protected:
string _num; //车牌号
Tyre _t; //轮胎
};
对于既适合继承又适合组合的两个类,推荐使用组合的方式。因为组合符合低耦合、高内聚,代码维护性好。
9 相关题目
1、什么是菱形继承?菱形继承的问题是什么?
菱形继承是多继承的一种特殊情况,两个子类继承同一个父类,而又有子类同时继承这两个子类,我们称这种继承为菱形继承。
菱形继承因为子类对象当中会有两份父类的成员,因此会导致数据冗余和二义性的问题。
2、什么是菱形虚拟继承?如何解决数据冗余和二义性?
菱形虚拟继承是指在菱形继承的肩部使用虚拟继承(virtual
)的继承方式,菱形虚拟继承对于D
类对象当中重复的A
类成员只存储一份,然后采用虚基表指针和虚基表使得D
类对象当中继承的B
类和C
类可以找到自己继承的A
类成员,从而解决了数据冗余和二义性的问题。
3、继承和组合的区别?什么时候用继承?什么时候用组合?
继承是一种is-a的关系,而组合是一种has-a的关系。如果两个类之间是is-a的关系,使用继承;如果两个类之间是has-a的关系,则使用组合;如果两个类之间的关系既可以看作is-a的关系,又可以看作has-a的关系,则优先使用组合。