继承、封装、多态是 C++ 面向对象语言三大特征,继承可以使代码复用,易于扩展
继承权限表
我们可以发现,如果基类的成员是 private 那么不管以什么样的继承方式在派生类都是不可见的(存在但是不能调用)这才引出看 protected 概念,继承在派生类的权限永远都是取那个“最小权限的” public > protected > private
基类的private在派生类是不能被访问的,不能被访问不是没有继承,继承下来了,但是没有访问的权限
派生类内部都可以访问基类的公有成员和保护成员
派生类对象可以赋值给基类的指针,引用,对象,有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。但是基类一般情况下不能赋值给派生类,除非进行强制类型转换,但是强制类型转换可能会引发问题,如果基类的指针没有指向派生类对象的话,会有越界的问题,所以一般不这么做
赋值兼容规则 —public继承
子类对象可以赋值给父类对象
class Person {
protected:
string _name;
string _sex;
int _age;
};
class Student : public Person {
public:
int _id;
};
void Test() {
Student s1;
s1._id = 10;
//子类对象可以赋值给父类对象,引用,指针
Person* p1 = &s1;
Person& p2 = s1;
Person p3 = s1;
}
父类对象不能赋值给子类对象
如果用父类给子类对象赋值的时候编译器直接报错,为什么?
子类继承于父类,它含有父类的部分,又做了扩展。如果子类对象赋值给父类变量,则使用该变量只能访问子类的父类部分(因为子类含有父类的部分,所以不会有问题)但是,如果反过来,这个子类变量如果去访问它的扩充成员变量,就会访问不到,因为父类不包含该部分,会 内存越界 一般情况,子类在继承了父类的方法和属性外,它还增加了新的属于自己的方法和属性,此时用父类的对象去赋值给子类的对象,这些新的方法和属性要怎么操作呢,C++为了避免这样的现象出现,当你用父类对象给子类赋值时就会报错。但是如果把父类强制转换为子类的类型,可以编译通过。
继承中的作用域
-
在继承体系中基类和派生类都有独立的作用域。
-
子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用基类::基类成员 显示访问)
-
需要注意的是: 如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
-
注意在实际中在继承体系里面最好不要定义同名的成员。
子类成员将屏蔽父类对同名函数、变量的直接访问
class Person {
protected:
int _age = 10;
};
class Student :public Person {
public:
void Print() {
cout << "age:" << _age << endl;
cout << "age:" << Person::_age << endl;
}
public:
int _age = 99;
};
void Test() {
Student s1;
s1.Print();
}
第一个会输出99,第二个输出10
类的默认成员函数是哪六个?
1.构造函数、2. 拷贝构造函数、3.析构函数、4.赋值操作符重载、5.取地址操作符重载 、6. const 修饰的取地址操作符重载
派生类对象的构造和析构过程
- 派生类的构造函数必须先调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
基类存在默认构造函数,所以在调用派生类的构造函数时候会先调用基类构造函数来初始化基类的成员
class Person {
public:
Person()
:_name(10)
{}
int _name;
};
class Student : public Person {
public:
Student(int id)
:_id(id)
{}
protected:
int _id;
};
如果基类没有默认构造函数,则必须在派生类的初始化列表显示调用
class Person {
public:
Person(int name)
:_name(10){}
int _name;
};
class Student : public Person {
public:
Student(int id,int name)
:_id(id)
,Person(name) {}
protected:
int _id;
};
void Test() {
Student t1(10,20);
}
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
class Person {
public:
Person(int name)
:_name(10)
{}
Person(const Person & p)
:_name(p._name)
{}
int _name;
};
class Student : public Person {
public:
Student(int id,int name)
:_id(id)
,Person(name)
{}
Student(const Student & s)
:_id(s._id)
, Person(s)//基类可以直接引用派生类的对象
//这样写的话调用的是基类的构造函数
// Person(s._name)
{}
protected:
int _id;
};
void Test() {
Student t1(10,20);
Student t2(t1);
}
- 派生类的operator=必须要调用基类的operator=完成基类的复制。
Student& operator = (const Student& s) {
if (this != &s) {
//显示的调用基类的赋值操作符
Person::operator = (s);
_id = s._id;
}
return *this;
}
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 派生类对象初始化先调用基类构造再调派生类构造。
- 派生类对象析构清理先调用派生类析构再调基类的析构
实现一个不能被继承的类
在C++98里可以使构造函数的权限设为私有,这样就无法继承了,但在C++11中可以添加关键字 final
class NonInherit{
public:
static NonInherit GetInstance(){
return NonInherit();
}
private:
NonInherit()
{}
};
// C++11给出了新的关键字final禁止继承
class NonInherit final
{};
继承 & 友元
基类中的友元成员不能访问子类中的私有成员和保护成员,这个也是比较好理解的,就相当于你父亲的朋友不会去关注你父亲留给你的东西
class Student;
class Person{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name = "hehe"; // 姓名
};
class Student : public Person {
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s){
cout << p._name << endl;
//下面这条语句无法通过编译,因为并没有继承
//cout << s._stuNum << endl;
}
继承与静态成员
基类定义了 static 静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
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; // 研究科目
};
void TestPerson() {
Person p1;
Student s3;
Graduate s4;
cout << " 人数 :" << Person::_count << endl;
Student::_count = 0;
cout << " 人数 :" << Person::_count << endl;
}
上面大部分实现的是单继承,就是一个子类只有一个父类;多继承是一个子类有多个直接父类,还有一种继承是菱形继承,它是多继承的特殊情况,如下图
菱形继承的缺陷:存在数据二义性和数据冗余
class D 中 有两份 A类的成员,造成了数据的冗余,而且无法区分
虚拟继承可以解决菱形继承的二义性和数据冗余的问题 如上面的继承关系,在 Student 和 Teacher 的继承 Person 时使用虚拟继承,即可解决问题。虚拟继承就是:将父类修饰成为虚基类,子类从父类继承的过程叫做虚继承,可以实现资源共享,并且还可以消除二义性
虚拟继承解决数据冗余和二义性的原理
这里是通过了 B 和 C 的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的 A 类中的成员
class Person{
public :
string _name ; // 姓名
};
class Student : virtual public Person{
protected :
int _num ; //学号
};
class Teacher : virtual public Person{
protected :
int _id ; // 职工编号
};
class Assistant : public Student, public Teacher{
protected :
string _majorCourse ; // 主修课程
};
void Test (){
Assistant a ;
a._name = "peter";
}
所以菱形继承也是继承中的一大缺陷,因为其底层比较复杂,所以一般最好不用设计出多继承
继承和组合的区别以及使用场景?
class Tire {
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};
class Car {
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
Tire _t; // 轮胎
};
继承 vs 组合
这导出了我们的面向对象设计的第二个原则:优先使用对象组合,而不是类继承。
面向对象系统中功能复用的两种最常用技术是类继承和对象组合(object composition)。正如我们已解释过的,类继承允许你根据其他类的实现来定义一个类的实现。这种通过生成子类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,父类的内部细节对子类可见。
对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。
继承和组合各有优缺点。类继承是在编译时刻静态定义的,且可直接使用,因为程序设计语言直接支持类继承。类继承可以较方便地改变被复用的实现。当一个子类重定义一些而不是全部操作时,它也能影响它所继承的操作,只要在这些操作中调用了被重定义的操作。
但是类继承也有一些不足之处。首先,因为继承在编译时刻就定义了,所以无法在运行时刻改变从父类继承的实现。更糟的是,父类通常至少定义了部分子类的具体表示。因为继承对子类揭示了其父类的实现细节,所以继承常被认为“破坏了封装性” 。子类中的实现与它的父类有如此紧密的依赖关系,以至于父类实现中的任何变化必然会导致子类发生变化。当你需要复用子类时,实现上的依赖性就会产生一些问题。如果继承下来的实现不适合解决新的问题,则父类必须重写或被其他更适合的类替换。这种依赖关系限制了灵活性并最终限制了复用性。一个可用的解决方法就是只继承抽象类,因为抽象类通常提供较少的实现。
对象组合是通过获得对其他对象的引用而在运行时刻动态定义的。组合要求对象遵守彼此的接口约定,进而要求更仔细地定义接口,而这些接口并不妨碍你将一个对象和其他对象一起使用。这还会产生良好的结果:因为对象只能通过接口访问,所以我们并不破坏封装性;只要类型一致,运行时刻还可以用一个对象来替代另一个对象;更进一步,因为对象的实现是基于接口写的,所以实现上存在较少的依赖关系。
对象组合对系统设计还有另一个作用,即优先使用对象组合有助于你保持每个类被封装,并被集中在单个任务上。这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物。另一方面,基于对象组合的设计会有更多的对象 (而有较少的类),且系统的行为将依赖于对象间的关系而不是被定义在某个类中。