继承、菱形继承与虚拟继承
- 一、概念
- 二、定义格式
- 三、继承方式
- 四、派生类继承基类成员访问方式的变化
- 五、基类和派生类对象赋值转换
- 1、概念
- 2、示意图
- 3、示例代码
- 4、特点
- 六、继承中的作用域
- 1、概念
- 2、示例代码
- 3、运行结果
- 七、派生类的默认成员函数
- 1、调用方法
- 2、示例代码
- 3、运行结果
- 八、友元关系不能继承
- 1、代码
- 2、注意
- 3、运行结果
- 4、错误代码与编译器报错
- 九、继承类别
- 1、单继承
- (1)概念
- (2)示意图
- 2、多继承
- (1)概念
- (2)示意图
- 3、菱形继承
- (1)概念
- (2)示意图
- (3)缺点
- (4)示例代码
- (5)调试结果
- (6)不显式指定时编译器报的错误
- 4、虚拟继承
- (1)定义与作用
- (2)代码
- (3)数据冗余代码
- (4)调试内存窗口
- (5)虚拟继承
- (6)调试内存窗口
- 5、总结
一、概念
- 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,是C++三大特性(封装、继承、多态)的其中一种,同时也是类设计层次的复用。
- 继承允许程序员在保持原有类特性的基础上进行扩展,增加功能。通过这种方法产生出的新类称为派生类,也叫子类;原有类称为基类,也叫父类。
- 继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。
二、定义格式
三、继承方式
- 继承方式一般使用公有继承或者保护继承,不太会使用私有继承。
四、派生类继承基类成员访问方式的变化
- 派生类无论以什么方式继承基类,基类中被private限定符限定的成员,虽然会被继承到派生类中,但派生类不管在类内还是类外都无法访问它。
- 如果基类的成员不想在基类以外直接被访问,又想在继承它的派生类中能被访问,则该基类的成员就需要用protected限定符限定。
- 基类的成员在子类的访问方式为成员在基类的访问限定符和继承方式中,最小权限的那个。访问权限从大到小分别为public、protected、private。
- 虽然关键字class和struct分别有默认的继承方式private和public,不过最好显式地写出继承方式。
- 因为用protected或者private继承方式继承下来的基类成员都只能在派生类的类里面使用,实际中扩展维护性不强。所以,在实际运用中一般使用的都是public继承方式继承,几乎很少和不提倡使用protected或者private继承方式继承。
五、基类和派生类对象赋值转换
1、概念
- 派生类对象可以赋值给基类的对象、指针、引用。这种赋值方式可以形象地称为切片或者切割,即把派生类中父类具有的那部分切割出来并赋值过去或者指向那块空间。
- 基类对象不能赋值给派生类对象,因为派生类中可能会有基类没有的成员,而用基类赋值给派生类时,那部分成员就没有被赋值或者说没被初始化。
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但必须是基类的指针指向派生类对象时才是安全的。
2、示意图
3、示例代码
class Person
{
protected:
string _name = "snowdragon";
size_t _age = 18;
};
class Student :public Person
{
public:
string _id;
};
int main()
{
Student s;
Person p = s;
Person* pp = &s;
Person& rp = s;
//s = p;
Student* ps1 = (Student*)pp;
ps1->_id = "123";
cout << ps1->_id << endl;
pp = &p;
Student* ps2 = (Student*)pp;
//下方为越界访问
//ps2->_id = "123";
//cout << ps2->_id << endl;
return 0;
}
4、特点
- 派生类对象可以赋值给基类对象以及基类对象的指针和引用。
- 基类对象不能赋值给派生类对象。
- 基类的指针可以通过强制类型转换赋值给派生类的指针,如果基类指针指向的是派生类对象,访问不会发生错误;如果基类指针指向的是基类对象,将会存在越界访问的问题。因为派生类中可能有基类没有的成员,而用派生类的指针访问这些成员就是越界访问,在运行时编译器会奔溃。
六、继承中的作用域
1、概念
- 在继承体系中基类和派生类都有各自独立的作用域。
- 派生类和基类中有同名成员函数时,派生类中的该成员函数将屏蔽基类对该同名成员函数的直接访问,这种情况称为隐藏,也称为重定义。但在派生类成员函数中,可以使用基类::基类成员显式地进行访问。
- 如果是成员函数的隐藏,只需要函数名相同就行。
2、示例代码
class Person
{
public:
void Func()
{
cout << "Person::void Func()" << endl;
}
protected:
string _name = "snowdragon";
size_t _age = 18;
};
class Student :public Person
{
public:
void Print()
{
cout << "姓名:" << _name << endl;
cout << "年龄:" << _age << endl;
cout << "id:" << _id << endl;
}
void Func()
{
Person::Func();
cout << "Student::void Func()" << endl;
}
protected:
string _name = "snow";
string _id = "123";
};
int main()
{
Student s;
s.Print();
s.Func();
return 0;
}
3、运行结果
七、派生类的默认成员函数
1、调用方法
- 派生类的构造函数必须调用基类的构造函数初始化基类所具有的成员,即派生类对象初始化时,会先调用基类的构造函数再调用派生类的构造函数。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显式地调用基类的构造函数。
- 派生类的拷贝构造函数必须调用基类的拷贝构造函数完成对象中基类所具有的成员的拷贝初始化。
- 派生类的赋值运算符重载(operator=)必须调用基类的赋值运算符重载(operator=)完成对象中基类所具有的成员的赋值。
- 为了保证派生类对象先清理派生类成员再清理基类成员,派生类的析构函数会在被调用完成后自动调用基类的析构函数去清理对应的基类成员,即派生类对象进行析构清理时,会先调用派生类的析构函数再调用基类的析构函数。
2、示例代码
class Person
{
public:
Person(const char* name = "snowdragon")
:_name(name)
{
cout << "Person()" << 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 = "snow", const char* id = "123")
:Person(name)
,_id(id)
{
cout << "Student()" << endl;
}
Student(const Student& s)
:Person(s)
,_id(s._id) //不能在函数体内赋值
{
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s)
{
cout << "Student& operator=(const Student& p)" << endl;
if (this != &s)
{
Person::operator=(s);
_id = s._id;
}
return *this;
}
~Student()
{
cout << "~Student()" << endl;
}
protected:
string _id;
};
int main()
{
Student s1("dragon", "18");
cout << endl;
Student s2(s1);
cout << endl;
Student s3("snowdragon", "19");
cout << endl;
s1 = s3;
cout << endl;
return 0;
}
3、运行结果
八、友元关系不能继承
1、代码
class Student;
class Person
{
public:
friend void Print(const Person& p, const Student& s);
protected:
string _name = "snowdragon";
private:
string _sex = "man";
};
class Student :public Person
{
public:
string _hairColor = "black";
protected:
string _id = "123";
private:
size_t _age = 18;
};
void Print(const Person& p, const Student& s)
{
cout << "姓名:" << p._name << endl;
cout << "性别:" << p._sex << endl;
cout << "头发颜色:" << s._hairColor << endl;
/*cout << "id:" << s._id << endl;
cout << "年龄:" << s._age << endl;*/
}
int main()
{
Person p;
Student s;
Print(p, s);
return 0;
}
2、注意
- 基类友元函数不能访问子类用私有和保护限定符限定的成员,但因为它是基类的友元,所以它能访问基类的成员。
- 因为上方代码中基类友元函数的参数要用到派生类Student,而派生类在友元函数声明语句的后面才定义,所以需要在友元函数的声明之前,即基类前声明派生类。
3、运行结果
4、错误代码与编译器报错
九、继承类别
1、单继承
(1)概念
对于继承关系,一个派生类只有一个直接的基类。
(2)示意图
2、多继承
(1)概念
对于继承关系,一个派生类同时继承多个(两个或以上)基类。
(2)示意图
3、菱形继承
(1)概念
属于多继承的一种特殊情况,两个不同的派生类继承于同一个基类,又有一个派生类继承于这个两个派生类。
(2)示意图
(3)缺点
- 使用菱形继承有数据冗余和二义性的缺点,即最后一个派生类C会继承两份基类sd的成员。
- 显式指定访问基类的成员可以解决二义性问题,但数据冗余问题无法解决。
(4)示例代码
class Person
{
public:
string _p = "snowdragon";
};
class PerDer1 :public Person
{
protected:
string _pd1 = "snow";
};
class PerDer2 :public Person
{
protected:
string _pd2 = "dragon";
};
class Derive :public PerDer1, public PerDer2
{
protected:
string _d = "snowdragon";
};
int main()
{
Derive d;
//d._p = "sd";
d.PerDer1::_p = "snow sd";
d.PerDer2::_p = "sd dragon";
return 0;
}
(5)调试结果
(6)不显式指定时编译器报的错误
4、虚拟继承
(1)定义与作用
- 在派生类继承时,在继承方式前添加virtual即为虚继承,作用为解决菱形继承的二义性和数据冗余问题。
- 当用继承关系中最后的派生类访问最初的基类的成员时,所有对象访问它时,访问到的都是同一个,即任一对象访问并修改该成员时,该成员的数据都会改变。
(2)代码
class Person
{
public:
string _p = "snowdragon";
};
class PerDer1 :virtual public Person
{
protected:
string _pd1 = "snow";
};
class PerDer2 :virtual public Person
{
protected:
string _pd2 = "dragon";
};
class Derive :public PerDer1, public PerDer2
{
protected:
string _d = "snowdragon";
};
int main()
{
Derive d;
d._p = "sd";
d.PerDer1::_p = "snow sd";
d.PerDer2::_p = "sd dragon";
return 0;
}
(3)数据冗余代码
class Person
{
public:
int _p;
};
class PerDer1 :public Person
//class PerDer1 :virtual public Person
{
public:
int _pd1;
};
class PerDer2 :public Person
//class PerDer2 :virtual public Person
{
public:
int _pd2;
};
class Derive :public PerDer1, public PerDer2
{
public:
int _d;
};
int main()
{
Derive d;
d.PerDer1::_p = 1;
d.PerDer2::_p = 2;
d._pd1 = 3;
d._pd2 = 4;
d._d = 5;
return 0;
}
(4)调试内存窗口
- 地址后面的框中的数字为对象d的地址。
- 基类成员_p有两份,第一份为类PerDer1 继承的,第二份为类PerDer2继承的。
(5)虚拟继承
- 下方为上方数据冗余代码的注释部分,即将类名部分的注释,再将注释部分展开。
- Derive 对象中将Person成员放到的了对象组成的最下面,而Person的成员同时属于PerDer1 和PerDer2。
- PerDer1 和PerDer2 通过各自的虚基表指针,指向各自的虚基表。虚基表中存有偏移量,通过这个偏移量就可以从当前位置找到下面Person的成员。
(6)调试内存窗口
5、总结
- 因为C++有多继承,进而就有了菱形继承,而有了菱形继承就会有菱形虚拟继承。使用它们会使底层实现变得很复杂。所以一般不建议设计出多继承,否则在复杂度和性能上都会有问题。
- public继承是一种is-a的关系。即每个派生类对象都是一个基类对象或者基类扩展后的对象,而在基类中改动被protected和private限定符限定的成员时,可能会影响派生类,即耦合度高。
- 组合是一种has-a的关系。如果B组合了A,则每个B对象中都会有一个A对象。简单点说就是在B类中定义了一个A类的对象。而在基类中改动被protected和private限定符限定的成员时,可能不会影响到派生类,即耦合度低。
- 继承允许根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。虽然在继承方式中,基类的内部细节对子类可见,但也在一定程度上破坏了基类的封装。所以,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
- 对象组合是类继承之外的另一种复用选择,新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于保持每个类的封装。
- 因为要遵循高内聚低耦合的设计理念,所以,类之间的关系可以用组合时,就用组合;不能用组合时,再考虑用继承。因为组合的耦合度低,代码维护性好。
本文到这里就结束了,如有错误或者不清楚的地方欢迎评论或者私信
创作不易,如果觉得博主写得不错,请务必点赞、收藏加关注💕💕💕