前言
本篇博客讲解c++中的继承
💓 个人主页:普通young man-CSDN博客
⏩ 文章专栏:C++_普通young man的博客-CSDN博客
⏩ 本人giee: 普通小青年 (pu-tong-young-man) - Gitee.com
若有问题 评论区见📝
🎉欢迎大家点赞👍收藏⭐文章
目录
继承的概念及定义
继承的的定义
继承父类成员访问方式
继承类模板
定义宏 ClASSTYPE
父类和子类对象赋值兼容转换
公有继承的对象赋值和类型转换规则
定义 Preson 类
定义 Student 类
main 函数
切片赋值
父类对象赋值给子类对象
总结
继承中的作用域
成员隐藏规则
总结
⼦类的默认成员函数
默认成员函数在子类中的生成规则
实现⼀个不能被继承的类
继承与友元
继承与静态成员
多继承及其菱形继承问题
继承模型
虚继承
IO库中的菱形虚拟继承
继承和组合
白箱 (White-Box)
黑箱 (Black-Box)
总结
在什么情况使用组合,什么情况使用继承?
使用组合的情况
使用继承的情况
何时使用组合?
何时使用继承?
代码示例
使用继承的实现
使用组合的实现
为什么最好使用组合?
继承的概念及定义
继承(inheritance)机制是面向对象程序设计中使代码能够复用的最重要手段。它允许我们在保持原有类特性的基础上进行扩展,通过添加新的方法(成员函数)和属性(成员变量)来创建新的类,这种新类被称为子类。继承呈现了面向对象程序设计的层次结构,并体现了从简单到复杂的认知过程。与以往接触的函数级别的复用不同,继承实现了类设计层面的复用。
-
概念:
- 继承允许我们基于已有的类(父类或基类)创建新的类(子类或派生类)。
- 子类继承父类的特性,并可以添加自己的特性。
-
示例:
- 假设我们有两个类
Student
和Teacher
,它们都有共同的成员变量(如姓名、地址、电话、年龄)以及共同的方法(如身份验证)。为了减少代码冗余,我们可以将这些共有的成员放入一个新的类Person
中。 Student
和Teacher
分别继承info
类,这样它们就可以复用 info类中的成员,而无需重复定义。
- 假设我们有两个类
首先,我们看如果不用继承的话,该咋写:
非继承方法:
//学生
class Student : public info {
public:Student() {_name = "张三";_number = "123456789";_tey = 18;}protected:string _name;//姓名string _number;//电话int _tey;
};
//教师
class Techer :public info {
public:Techer() {_name = "李四";_number = "8888888888888";_tit = "110";}protected:string _name;//姓名string _number;//电话string _tit;
};
这样的代码,如果我类别越来越多,就会冗余
继承方法:
//基本信息
class info {protected:string _name;//姓名string _number;//电话
};//学生
class Student : public info {
public:Student(){info::_name = "张三";info::_number = "123456789";_tey = 18;}private:int _tey;
};
//教师
class Techer :public info {
public:Techer() {info::_name = "李四";info::_number = "8888888888888";_tit = "110";}private:string _tit;
};
继承我们可以把两个类别相同的信息写成一个父类,让他继承给这两个类,就不会让代码冗余
继承的的定义
下⾯我们看到Person是⽗类,也称作基类。Student是⼦类,也称作派⽣类。(因为翻译的原因,所以 既叫⽗类/⼦类,也叫⽗类/⼦类)
继承父类成员访问方式
父类私有成员的不可见性:
- 父类的私有 (
private
) 成员在子类中无论以何种方式继承都是不可见的。这意味着尽管这些成员被继承到了子类对象中,但由于语法上的限制,子类对象无论是在类内部还是外部都无法直接访问这些私有成员。保护成员的用途:
- 如果父类成员不想在类外直接被访问,但需要在子类中访问,那么可以定义为受保护 (
protected
) 成员。保护成员的引入是为了满足继承的需求,使得父类的某些成员可以在子类中访问,但又不让这些成员对外界可见。成员访问方式的总结:
- 父类的私有成员在子类中始终是不可见的。
- 父类的其他成员(即非私有成员)在子类中的访问方式遵循以下规则:
Min(成员在父类的访问限定符, 继承方式)
。
public
>protected
>private
。- 例如,如果父类的一个成员是
protected
,而子类使用public
方式继承,那么该成员在子类中依然是protected
。继承方式的默认设置:
- 使用
class
关键字声明类时,默认的继承方式是private
。- 使用
struct
关键字声明类时,默认的继承方式是public
。- 最好显式地写出继承方式,以增强代码的可读性和明确性。
实际应用中的继承选择:
- 在实际开发中,通常推荐使用
public
继承,因为这提供了最佳的灵活性和扩展性。- 使用
protected
或private
继承并不常见,也不被提倡,因为这些继承方式限制了成员的访问范围,仅限于子类内部,这可能不利于后续的扩展和维护。
//父
class Preson {
public:void Print() {cout << _name << endl;}protected:string _name;
private:int _age;
};//子
class Student : public Preson
{
public:Student() {Preson::_name = "张三";}protected:int _number;};int main() {Student s1;s1.Print();return 0;
}
这个代码的解析:
// 父类 class Preson { public:void Print() {cout << _name << endl;}protected:string _name; // 保护成员,可以在子类中访问,但不能在类外直接访问 private:int _age; // 私有成员,不能在子类中直接访问 };
这里定义了一个名为
Preson
的类,它包含了一个公有方法Print()
用于打印_name
成员变量。_name
是一个受保护的成员变量,意味着它可以在子类中访问,但不能在类的外部直接访问。_age
是一个私有成员变量,这意味着它既不能在类的外部访问,也不能在子类中直接访问。// 子类 class Student : public Preson { public:Student() {Preson::_name = "张三"; // 正确:可以在子类中访问父类的受保护成员}protected:int _number; // 子类的保护成员 };
这是一个名为
Student
的类,它继承自Preson
类。在构造函数中,Preson::_name
被设置为"张三"
,这是正确的,因为_name
是受保护的成员,可以在子类中访问。总结
Preson
类有一个公有方法Print()
和一个受保护成员_name
。Student
类继承自Preson
类,并且可以在构造函数中访问_name
。Student
类的构造函数将_name
设置为"张三"
,并在main
函数中通过调用Print()
方法显示了_name
的值。- 这个age虽然子类不能访问,但是他还是继承了下来
继承类模板
继承类模板是 C++ 中一个非常有用的特性,它允许你创建一个通用的类,该类可以从另一个模板类继承。这种能力对于构建灵活且可复用的代码非常重要。下面我将详细介绍继承类模板的概念和用法。
//继承类模板
#include<vector>
#include<list>
#include<deque>#define ClASSTYPE std::vector
//#define ClASSTYPE std::list
//#define ClASSTYPE std::dequetemplate<class T>
class stack : public ClASSTYPE<T>
{void push(const T& val) {ClASSTYPE<T>::push_back(val);}void pop() {ClASSTYPE<T>::pop_back();}const T& top(){return ClASSTYPE<T>::back();}bool empty(){return ClASSTYPE<T>::empty();}
};int main() {stack<int> s1;s1.push_back(1);s1.push_back(2);s1.push_back(3);for (auto it : s1){cout << it << endl;}return 0;
}
这段代码展示了如何使用类模板来创建一个通用的栈类,该栈类可以使用不同的容器类型(如 std::vector
, std::list
, 或 std::deque
)作为底层存储结构。让我们逐行解析这段代码:
template<class T> class stack : public ClASSTYPE<T> { public:void push(const T& val) {ClASSTYPE<T>::push_back(val);}void pop() {ClASSTYPE<T>::pop_back();}const T& top() const {return ClASSTYPE<T>::back();}bool empty() const {return ClASSTYPE<T>::empty();} };
这里定义了一个名为
stack
的类模板,它继承自ClASSTYPE<T>
类模板。ClASSTYPE
是一个宏,可以根据不同的定义指向std::vector
,std::list
, 或std::deque
。
push
: 向栈中添加一个元素。这里使用了ClASSTYPE<T>::push_back
方法,这意味着栈使用push_back
方法来添加元素。pop
: 从栈中移除顶部元素。这里使用了ClASSTYPE<T>::pop_back
方法。top
: 返回栈顶元素而不移除它。这里使用了ClASSTYPE<T>::back
方法。empty
: 检查栈是否为空。这里使用了ClASSTYPE<T>::empty
方法。定义宏
ClASSTYPE
#define ClASSTYPE std::vector // #define ClASSTYPE std::list // #define ClASSTYPE std::deque
这里定义了一个宏
ClASSTYPE
,它指定了stack
类模板将继承的容器类型。默认情况下,它被定义为std::vector
,但也可以被定义为std::list
或std::deque
。这样就可以很好的复用不一样的类模板
这里我们需要注意父类是类模板是需要指定类域,这里是因为按需实例化的原因(模版是按需实例化,push_back等成员函数未实例化,所以找不到 )
父类和子类对象赋值兼容转换
公有继承的对象赋值和类型转换规则
子类对象赋值给父类对象/指针/引用:
- 子类对象可以赋值给父类的对象、指针或引用,这种操作通常被称为“切片”或“切割”。这意味着子类对象中属于父类的部分会被复制或绑定到父类的对象、指针或引用上,而子类特有的部分会被忽略。
父类对象不能赋值给子类对象:
- 父类对象不能直接赋值给子类对象,因为子类对象需要额外的数据成员或成员函数,而父类对象并不包含这些信息。
父类指针或引用赋值给子类指针或引用:
- 父类的指针或引用可以通过强制类型转换赋值给子类的指针或引用,但这仅在父类指针或引用实际上是指向子类对象时才是安全的。
- 如果父类是多态类型,可以使用RTTI(Run-Time Type Information,运行时类型信息)的
dynamic_cast
来进行识别后进行安全转换。
定义
Preson
类class Preson { protected:string _name; // 姓名string _sex; // 性别int _age; // 年龄 };
这里定义了一个名为
Preson
的类,它包含三个受保护成员变量:_name
(姓名)、_sex
(性别)和_age
(年龄)。定义
Student
类class Student : public Preson { public:int _No; // 学号 };
这是一个名为
Student
的类,它继承自Preson
类,并添加了一个公有成员变量_No
(学号)。
main
函数int main() {Student s1;// 切片(对象,指针,引用)Preson p1 = s1; // 切片赋值Preson* p2 = &s1; // 指针赋值Preson& p3 = s1; // 引用赋值// 父类赋值给子类Preson s2;Student tmp = s2; // 错误:父类对象不能赋值给子类对象return 0; }
在
main
函数中,创建了一个Student
类的对象s1
。切片赋值
Preson p1 = s1;
:这里将s1
对象赋值给Preson
类的对象p1
。这个过程被称为“切片”或“切割”。这意味着s1
对象中属于Preson
类的部分(即_name
,_sex
, 和_age
)会被复制到p1
中,而s1
特有的成员_No
会被忽略。Preson* p2 = &s1;
:这里将s1
对象的地址赋值给Preson
类的指针p2
。这也是一个切片的过程,但这里的切片是指p2
指向了s1
对象中属于Preson
类的部分。Preson& p3 = s1;
:这里创建了一个Preson
类的引用p3
,它绑定到了s1
对象。这也同样是一个切片的过程,p3
引用的是s1
对象中属于Preson
类的部分。父类对象赋值给子类对象
Preson s2;
:这里创建了一个Preson
类的对象s2
。Student tmp = s2;
:试图将s2
对象赋值给Student
类的对象tmp
。这是不允许的,因为s2
对象缺少Student
类特有的成员_No
。因此,这行代码会导致编译错误
总结
- 子类对象赋值给父类:子类对象可以赋值给父类的对象、指针或引用,这种操作通常被称为“切片”或“切割”,意味着子类对象中属于父类的部分会被复制或绑定到父类的对象、指针或引用上。
- 父类对象不能赋值给子类对象:父类对象不能直接赋值给子类对象,因为父类对象缺少子类特有的数据成员或成员函数。
- 父类指针或引用赋值给子类:父类的指针或引用可以通过强制类型转换赋值给子类的指针或引用,但必须确保父类指针或引用实际上是指向子类对象时才是安全的。如果父类是多态类型,可以使用
dynamic_cast
进行安全的类型转换。
继承中的作用域
成员隐藏规则
独立的作用域:
- 在继承体系中,父类和子类都有独立的作用域。这意味着每个类都可以有自己的成员变量和成员函数,即使它们具有相同的名称。
成员隐藏:
- 当子类和父类中有同名成员时,子类成员将屏蔽父类中的同名成员,这种情况称为隐藏。这意味着在子类中直接访问同名成员时,访问的是子类的成员,而不是父类的成员。
- 在子类成员函数中,可以使用作用域解析运算符(
::
)显式访问父类的同名成员。成员函数的隐藏:
- 成员函数的隐藏只需要函数名相同即可构成隐藏。这意味着即使函数签名(参数列表和返回类型)不同,只要函数名相同,也会发生隐藏。
避免同名成员:
- 在实际编码实践中,在继承体系中最好避免定义同名的成员。这样做可以减少混淆和潜在的错误,并提高代码的可读性和维护性。
这边打印得结果是什么?
是不是想不到,这里我们就要想到就近原则,所以这里打印的是:
那如果我们要打印父类得,该咋打印?
A和B类中的两个func构成什么关系()
A.重载 B.隐藏 C.没关系
下⾯程序的编译运⾏结果是什么()
A.编译报错 B.运⾏报错 C.正常运⾏
总结
- 独立的作用域:父类和子类都有独立的作用域,这意味着每个类都可以有自己的成员变量和成员函数,即使它们具有相同的名称。
- 成员隐藏:当子类和父类中有同名成员时,子类成员将屏蔽父类中的同名成员,这种情况称为隐藏。在子类成员函数中,可以使用作用域解析运算符显式访问父类的同名成员。
- 成员函数的隐藏:成员函数的隐藏只需要函数名相同即可构成隐藏,即使函数签名不同。
- 避免同名成员:在实际编码实践中,最好避免在继承体系中定义同名的成员,以减少混淆和潜在的错误,并提高代码的可读性和维护性。
⼦类的默认成员函数
默认成员函数在子类中的生成规则
构造函数:
- 子类的构造函数必须调用父类的构造函数来初始化父类的那一部分成员。如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显式调用父类的构造函数。
//父类 Student(const char* name,int number) :Person(name), _number(number) {cout << "Student : : Student()" << endl; }//子类 Student(const char* name,int number) :Person(name)//显示调用父类的默认构造, _number(number) {cout << "Student : : Student()" << endl; }
Person
类和Student
类都使用了const char* name
作为构造函数的一个参数。这里使用const char*
是因为字符串字面量在 C++ 中是以const char*
形式存在的。当你传递一个字符串字面量给函数时,它实际上是传递了一个指向字符数组的常量指针。例如,字符串"张三"
实际上就是一个const char*
类型的值。
拷贝构造函数:
- 子类的拷贝构造函数必须调用父类的拷贝构造函数来完成父类成员的拷贝初始化。
//父类 Person(const Person& p):_name(p._name) {}//子类 Student(const Student& p) :Person(p)//切片 -- 子类给父类兼容转换 ,_number(p._number) {}
赋值运算符
operator=
:
- 子类的
operator=
必须调用父类的operator=
来完成父类成员的复制。需要注意的是,子类的operator=
隐藏了父类的operator=
,因此需要显式调用父类的operator=
,并指定父类的作用域。//父类 Person& operator=(const Person& p) {if (&p != this){_name = p._name;}return *this; }//子类Student& operator=(const Student& p) {if (&p != this){//存在隐藏关系,需要指定类域Person::operator=(p);//切片 -- 子类给父类兼容转换_number = p._number;}return *this;}
析构函数:
- 子类的析构函数会在被调用完成后自动调用父类的析构函数来清理父类成员。这样可以保证子类对象先清理子类成员,然后再清理父类成员的顺序。
//父类 ~Person() {cout << "Person : : ~Person()" << endl; }//子类 ~Student()//不需要显示调用 {cout << "Student : : ~Student()" << endl; }
这边演示一下如果显示调用该咋用(这个方法会违背对象父子类析构顺序):
这边需要指定类域,因为他们在编译器会对析构函数名进行特殊处理,处理成
destructor()
这里不需要显示调用析构,因为它会自动调用父类的析构
对象初始化顺序:
- 子类对象初始化时,先调用父类构造函数再调用子类构造函数。
对象析构顺序:
- 子类对象析构清理时,先调用子类析构函数再调用父类的析构函数。
析构函数与多态:
- 在多态中,析构函数需要构成重写,重写的条件之一是函数名相同。为了支持多态,编译器会对析构函数名进行特殊处理,处理成
destructor()
。因此,如果父类的析构函数没有加上virtual
关键字,那么子类析构函数和父类析构函数会构成隐藏关系。
总结
- 构造函数:子类构造函数必须调用父类构造函数初始化父类成员。
- 拷贝构造函数:子类拷贝构造函数必须调用父类拷贝构造函数完成父类成员的拷贝初始化。
- 赋值运算符
operator=
:子类的operator=
需要显式调用父类的operator=
来完成父类成员的复制,并需要指定父类的作用域。- 析构函数:子类析构函数会自动调用父类析构函数清理父类成员。
- 对象初始化与析构顺序:子类对象初始化时先调用父类构造函数,析构时先调用子类析构函数。
- 析构函数与多态:为了支持多态,父类的析构函数应声明为虚函数,否则子类析构函数和父类析构函数会构成隐藏关系。
class Person { public://父类的构造Person(const char* name = "张三") :_name(name){cout << "Person : : Person()" << endl;}//父类的拷贝构造Person(const Person& p):_name(p._name){}//父类的赋值重载Person& operator=(const Person& p){if (&p != this){_name = p._name;}return *this;}//父类的析构~Person(){cout << "Person : : ~Person()" << endl;} protected:string _name; };class Student : public Person{//构造//显示调用父类的构造 public:Student(const char* name,int number) :Person(name), _number(number){cout << "Student : : Student()" << endl;}//拷贝Student(const Student& p):Person(p)//切片 -- 子类给父类兼容转换,_number(p._number){}//赋值Student& operator=(const Student& p) {if (&p != this){Person::operator=(p);//切片 -- 子类给父类兼容转换_number = p._number;}return *this;}//析构~Student(){// Person::~Person();cout << "Student : : ~Student()" << endl;} protected:int _number; };int main() {Student s1("李四",18);return 0; }
实现⼀个不能被继承的类
⽅法1:⽗类的构造函数私有,⼦类的构成必须调⽤⽗类的构造函数,但是⽗类的构成函数私有化以后,⼦类看不⻅就不能调⽤了,那么⼦类就⽆法实例化出对象。
⽅法2:C++11新增了⼀个final关键字,final修改⽗类,⼦类就不能继承了。
//实现一个不能继承的类
class Person //final
{
public:int _num;
private:
// Person() {}
};class Student : public Person {
public:void sun() {cout << "Student" << endl;}};
int main() {Student s1;return 0;
}
继承与友元
友元关系不能继承,也就是说父类友元不能访问子类私有和保护成员。
class Person {
public://友元声明friend void fun(const Person& x, const Student& y);
protected:string _name;
};//子类不能继承父类的友元
class Student : public Person{
public:protected:int _number;
};void fun(const Person& x,const Student& y) {cout << x._name << endl;cout << y._number << endl;
}
这个代码会报错,因为友元不能继承
但是可以修改
//前置声明:由于函数向上找值的原因所以需要前置声明,不然就找不到Student
class Student;class Person {
public://友元声明friend void fun(const Person& x, const Student& y);
protected:string _name;
};//子类不能继承父类的友元
class Student : public Person{
public://友元声明friend void fun(const Person& x, const Student& y);
protected:int _number;
};
在两个类里都加上友元,就可以了
继承与静态成员
父类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个子类,都只有⼀个static成员实例。
//继承和静态成员
class Person {
public:static int num ;
protected:string _name;
};int Person::num = 10;
//子类不能继承父类的友元
class Student : public Person {
public:protected:int _number;
};int main() {Student s1;Person s2;cout << s1.num << endl;cout << s2.num << endl;s1.num = 100;cout << s1.num << endl;cout << s2.num << endl;return 0;
}
本来static修饰的变量就存储在静态区的,按这样就很好理解为什么用的都是同一个
多继承及其菱形继承问题
继承模型
继承类型 | 描述 | 内存布局 | 问题 |
---|---|---|---|
单继承 | 一个子类只有一个直接父类。 | - 子类继承父类的成员。 | - 无特殊问题。 |
多继承 | 一个子类有两个或以上的直接父类。 | - 先继承的父类成员在前面。 - 后继承的父类成员在后面。 - 子类成员放在最后面。 | - 数据冗余和二义性问题较少,但仍需注意。 |
菱形继承 | 菱形继承是多继承的一种特殊情况。 | - 中间类(即被多个子类继承的类)的成员会被多次继承。 | - 数据冗余(中间类成员会被多次继承)。 - 二义性(通过不同路径继承的成员可能产生冲突)。 |
这种继承就是单继承,可以理解成你家里人那种代代相传的思想
class Person {
public:string _name;
};class Student : public Person {
public:int _number;
};class info : public Student {
public:int year;
};int main() {info s1;return 0;
}
class Person {
public:string _name;
};class Student {
public:int _number;
};class info : public Student ,public Person
{
public:int year;
};int main() {info s1;return 0;
}
多继承就是多个类的特性继承给你,就像你爸爸继承给了你品德,妈妈继承给你家产,然后你就由他们两个的东西再加上自己的技能就无敌了
这种继承是不建议写的
这样的特性使他很不可控,但是我们的库中有使用过,不建议大家写,是因为库中这个肯定也是调了很多东西才这个样子,从图中就可以看出菱形继承其实就是,两个子类继承了一个父类,然后一个子类有继承了两个刚才继承父类的类,这会出现一个什么问题?你想一下两个类都继承了一个类的特性,他们又把自己继承给一个人,是不是就会出现两份一样的特性
class Person {
public:string _name;
};class Student : public Person {
public:int _number;
};class info : public Person
{
public:int year;
};class Son : public Student, public info {
public:string wang;
};int main() {Son s1;s1._name = "李四"; //E0266 "Son::_name" 不明确 return 0;
}
这边就会直接报错
虚继承
其实这个问题我们也可以解决,需要用到一个新的关键字virtual(虚继承)
先看一下如何写:
这个virtual需要加在,会出现冗余继承的地方,我为什么加在两个类中,而不是一个?这边look一下加了virtual之后的结构
我们要把被继承的类和继承的类看作一个整体,加了virtual之后他在这个整体变成了单独一份,这样就解决了这个问题,但是又出现了另一个问题,假如我想让这几个子类要有不一样的Person咋办?
class Person { public://父类的构造Person(const char* name = "张三") :_name(name){cout << "Person : : Person()" << endl;}//父类的拷贝构造Person(const Person& p):_name(p._name){}//父类的赋值重载Person& operator=(const Person& p){if (&p != this){_name = p._name;}return *this;}//父类的析构~Person(){} protected:string _name; };class Student : virtual public Person{//构造//显示调用父类的构造 public:Student(const char* name,int number = 11) :Person(name), _number(number){}//拷贝Student(const Student& p):Person(p)//切片 -- 子类给父类兼容转换,_number(p._number){}//赋值Student& operator=(const Student& p) {if (&p != this){Person::operator=(p);//切片 -- 子类给父类兼容转换_number = p._number;}return *this;}//析构~Student(){} protected:int _number; };class Teacher : virtual public Person {//构造//显示调用父类的构造 public:Teacher(const char* name, int number = 20):Person(name), _number2(number){cout << "Student : : Student()" << endl;}//拷贝Teacher(const Teacher& p):Person(p)//切片 -- 子类给父类兼容转换, _number2(p._number2){}//赋值Teacher& operator=(const Teacher& p) {if (&p != this){Person::operator=(p);//切片 -- 子类给父类兼容转换_number2 = p._number2;}return *this;}//析构~Teacher(){// Person::~Person();cout << "Student : : ~Student()" << endl;} protected:int _number2; };class info : public Student,public Teacher { public:info(const char* name1, const char* name2, const char* name3):Student(name1),Teacher(name2), Person(name3){} protected:string classname; };int main() {info s1("张三","李四","王五");return 0; }
可以看到为什么我三个类都是王五?因为他们都是继承的父类,父类在这个继承中单独站一份,你可能会问Student和Person不也会去调用?其实他根本就不会去调用,编译器认为你的Person的构造已经构造好了,所以直接跳过(c++作者这样设计,也没办法),所以大家一定尽量不要使用菱形继承
class Base1 { public: int _b1; };class Base2 { public: int _b2; };class Derive : public Base1, public Base2 { public: int _d; };int main(){Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;}
A: p1 == p2 == p3; B: p1 < p2 < p3; C: p1 == p3 != p2; D: p1 != p2 != p3
:
这个题为什么选C,不选A?
别忘了前面的切片
IO库中的菱形虚拟继承
这边c++的输入输出就是用了菱形继承封装了一个类
这些我不做过多讲解,明白他是这个结构就行
template<class CharT, class Traits = std::char_traits<CharT>>
class basic_ostream : virtual public std::basic_ios<CharT, Traits>
{};
template<class CharT, class Traits = std::char_traits<CharT>>
class basic_istream : virtual public std::basic_ios<CharT, Traits>
{};
ps:
菱形继承,他的形状并不代表就是菱形
也有这样的,那这个virtual该加在哪里,才能防止冗余和二义性,哪一个类(公共基类)继承的数据产生了冗余和二义性就加在那两个类当中,注意一定是这种结构
继承和组合
下面这个表格解释的很清楚
继承类型 | 描述 | 复用类型 | 封装影响 | 依赖关系 | 耦合度 | 适用场景 |
---|---|---|---|---|---|---|
Public 继承 | 一个子类对象是一个父类对象。 | 白箱复用 | 父类的内部细节对子类可见,一定程度破坏了封装。 | 强 | 高 | 当子类和父类之间存在 is-a 关系时使用。 |
组合 | B 组合了 A,每个 B 对象中都有一个 A 对象。 | 黑箱复用 | 对象的内部细节是不可见的,只通过接口交互。 | 弱 | 低 | 当需要实现 has-a 关系时使用。 |
多继承 | 一个子类可以继承多个父类。 | 白箱复用 | 父类的内部细节对子类可见,可能导致数据冗余和二义性。 | 强 | 高 | 当需要从多个父类继承特性时使用。 |
菱形继承 | 多继承的一种特殊情况,会导致数据冗余和二义性。 | 白箱复用 | 父类的内部细节对子类可见,可能导致数据冗余和二义性。 | 强 | 高 | 避免设计出菱形继承,除非有明确的需求。 |
“白箱”和“黑箱”是用来描述类之间复用和交互方式的术语。下面是这两个概念的解释:
白箱 (White-Box)
- 定义:白箱复用指的是一个类可以访问另一个类的内部实现细节。在继承的情况下,子类可以访问父类的私有和受保护成员。
- 特点:
- 子类可以看到父类的内部实现,包括私有成员。
- 子类可以扩展或修改父类的行为。
- 这种复用方式允许子类重写父类的方法,甚至可以访问父类的私有成员(通过继承)。
- 优点:
- 提供了一种方便的方式来扩展或修改现有类的行为。
- 有利于代码的重用。
- 缺点:
- 父类的更改可能会影响子类,因为子类依赖于父类的具体实现。
- 父类的内部细节暴露给了子类,可能会导致封装的破坏。
- 子类和父类之间的耦合度较高。
黑箱 (Black-Box)
- 定义:黑箱复用指的是一个类只能通过另一个类的公共接口与其交互,而不能访问其内部实现。这通常指的是对象组合的情况,其中一个类持有另一个类的实例。
- 特点:
- 类之间通过接口交互,不直接访问内部实现。
- 对象之间通过方法调用来通信,而不是直接访问成员变量。
- 优点:
- 提高了封装性,因为内部实现的更改不会影响到其他类。
- 降低了类之间的耦合度,提高了代码的可维护性和可扩展性。
- 缺点:
- 相对于白箱复用,可能需要更多的代码来实现同样的功能。
- 在某些情况下,可能会导致性能上的开销,尤其是当涉及到复杂的对象层次结构时。
总结
- 白箱复用(如通过继承)允许子类访问父类的内部实现,这有助于代码的重用,但可能会破坏封装性和增加耦合度。
- 黑箱复用(如通过组合)强调通过接口进行交互,提高了封装性和降低了耦合度,但可能会需要更多的代码来实现。
在什么情况使用组合,什么情况使用继承?
下面是对这段代码的分析以及何时使用组合与继承的解释:
使用组合的情况
在
Car
类中,包含了四个Tire
类的对象_t1
,_t2
,_t3
, 和_t4
。这里Tire
类和Car
类之间的关系更符合 has-a(拥有)的关系,因为一辆车拥有轮胎。使用继承的情况
BMW
类和Benz
类都继承自Car
类。这里Car
类和BMW
/Benz
类之间的关系更符合 is-a(是)的关系,因为BMW
和Benz
都是一种Car
。(因为车的牌子并不是一辆车,世界上有很多牌子,所以我这里封装一个车,来继承)何时使用组合?
当一个类需要使用另一个类的功能,但它们之间不存在 is-a 的关系时,应该使用组合。例如:
- 轮胎和汽车:轮胎不是一种汽车,而是汽车的一部分。因此,使用组合来表示这种关系更为合适。在本例中,
Car
类包含了Tire
类的对象,表示一辆汽车拥有轮胎。何时使用继承?
当一个类是另一个类的特例时,即存在 is-a 的关系时,应该使用继承。例如:
- 宝马和奔驰与汽车:宝马和奔驰都是汽车的特例。因此,使用继承来表示这种关系更为合适。在本例中,
BMW
和Benz
类都继承自Car
类,表示宝马和奔驰都是汽车的一种。代码示例
class Tire { protected:string _brand = "Michelin"; // 品牌size_t _size = 17; // 尺寸 };class Car { protected:string _colour = "白色"; // 颜色string _num = "陕ABIT00"; // 车牌号Tire _t1; // 轮胎Tire _t2; // 轮胎Tire _t3; // 轮胎Tire _t4; // 轮胎 };class BMW : public Car { public:void Drive() { cout << "好开-操控" << endl; } };class Benz : public Car { public:void Drive() { cout << "好坐-舒适" << endl; } };
- 组合:当一个类需要使用另一个类的功能,但它们之间不存在 is-a 的关系时,使用组合。
- 继承:当一个类是另一个类的特例,即存在 is-a 的关系时,使用继承。
这里还有一种关系就是继承和组合都可以:
这段代码展示了如何使用模板来定义 vector
类和 stack
类,并展示了两种不同的实现方式:一种使用继承,另一种使用组合。下面是对这两种实现方式的分析以及为什么在这种情况下最好使用组合的解释。
使用继承的实现
template<class T>
class vector
{};template<class T>
class stack : public vector<T>
{};
在这个实现中,stack
类继承自 vector<T>
类。这表明 stack
是 vector<T>
的一种特例,即 stack
是 vector<T>
的子类。这里存在 is-a 的关系,即 stack
是一种 vector<T>
。
使用组合的实现
template<class T>
class vector
{};template<class T>
class stack
{
public:vector<T> _v;
};
在这个实现中,
stack
类包含了一个vector<T>
类的对象_v
。这表明stack
拥有一个vector<T>
,即stack
包含了vector<T>
的实例。这里存在 has-a 的关系,即stack
拥有一个vector<T>
。为什么最好使用组合?
黑盒/白盒
尽管
stack
类和vector<T>
类之间的关系既符合 is-a 也符合 has-a,但在大多数情况下,使用组合比使用继承更好,原因如下:
封装性:组合提供了更好的封装性。在组合的情况下,
stack
类和vector<T>
类之间的耦合度较低,因为stack
类只通过接口与vector<T>
类交互,而不是直接访问其内部实现。可维护性和可扩展性:组合使得代码更容易维护和扩展。如果
vector<T>
类的实现发生变化,它不太可能影响到stack
类,因为stack
类通过接口与vector<T>
类交互,而不是直接依赖于其实现细节。灵活性:组合提供了更多的灵活性。如果将来需要使用其他容器类型(如
list
或deque
)来实现stack
类,只需简单地将vector<T>
替换为其他容器类型即可,而无需修改stack
类的继承结构。避免继承带来的问题:使用继承可能会导致一些问题,如菱形继承问题和数据冗余。此外,继承可能会导致封装性的破坏,因为子类可以看到父类的内部实现。