一、继承的概念及定义
1.1继承的概念
继承(inheritance)机制是面向对象程序设计 使代码可以复用的最重要的手段,它允许程序员在 保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称 派生类。继承呈现了面向对象 程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;class Person {
public:void Print() {cout << "name:" << _name << endl;cout << "age:" << _age << endl;}protected:string _name = "peter"; // 姓名int _age = 18; // 年龄
};// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了
// Student 和 Teacher 复用了 Person 的成员。下面我们使用监视窗口查看 Student 和 Teacher 对象,可以看到变量的复用。调用 Print 可以看到成员函数的复用。class Student : public Person {
protected:int _stuid; // 学号
};class Teacher : public Person {
protected:int _jobid; // 工号
};int main() {Student s;Teacher t;s.Print(); // 打印 Student 对象的成员变量和继承自 Person 类的成员函数t.Print(); // 打印 Teacher 对象的成员变量和继承自 Person 类的成员函数return 0;
}
1.2 继承定义
1.2.1定义格式
下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类。
1.2.2继承关系和访问限定符
1.2.3继承基类成员访问方式的变化
父类成员的访问控制
父类的public成员允许本类,子类和类外部(非本类和子类)的访问;
父类的protected成员允许本类和子类的访问;
父类的private成员只允许本类访问。
继承方式
1、作用是限定子类,继承是把父类的全部东西都拿到子类中,虽然继承到了,但是不一定继承的东西都能用的到,所以就有继承方式这一说,父类的不同成员在子类中的权限是什么样的,就要结合继承方式来确定。总的来说,类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式)
2、继承方式总共有三种:
- public公有继承
1.基类public和protected成员的访问属性在派生类中不变,并且基类的private成员不可访问。
2.派生类中的成员函数可以直接访问基类中的public和protected成员,但基类的private成员不可被访问。
3.派生类的对象只能访问基类的public成员。
- protected保护继承
1.基类的public和protected成员都以protected成员的身份出现在派生类中,并且基类的private成员不可访问。
2.派生类中的成员函数可以直接访问基类中的public和protected成员,但基类的private成员不可被访问。
3.派生类的对象不能访问基类中的任何成员。
- private私有继承
1.基类的public和protected成员都以private成员的身份出现在派生类中,并且基类的private成员不可访问。
2.派生类中的成员函数可以直接访问基类中的public和protected成员,但基类的private成员不可被访问。
3.派生类的对象不能访问基类中的任何成员。
访问权限和继承方式都是三种,父类成员在子类中的访问权限由两者共同决定
所以也就会有九种不同的情况:
总结:
- 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私 有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 都不能去访问它。
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在 派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
- 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他 成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected > private。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过 最好显示的写出继承方式。
- 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡 使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里 面使用,实际中扩展维护性不强。
二、基类和派生类对象赋值转换
- 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片 或者切割。寓意把派生类中父类那部分切来赋值过去。
- 基类对象不能赋值给派生类对象。
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类 的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(RunTime Type Information)的dynamic_cast 来进行识别后进行安全转换。
RTTI(Run-Time Type Information)是 C++ 的一种特性,用于在运行时获取对象的类型信息。通过 RTTI,我们可以在运行时确定对象的真实类型,而不仅仅是指针或引用的静态类型。这样可以在程序运行时进行类型检查和类型转换,特别适用于多态情况下。
在 C++ 中,dynamic_cast 是 RTTI 的一部分,用于将一个基类指针或引用安全地转换为派生类指针或引用。dynamic_cast 在进行类型转换时会进行类型检查,确保转换的安全性。如果转换合法,则返回指向正确派生类对象的指针或引用;如果转换不合法,且目标类型是指针,则返回空指针;如果目标类型是引用,则抛出 std::bad_cast 异常。
使用 dynamic_cast 进行类型转换需要满足以下条件:
- 基类必须有至少一个虚函数。
- 转换的方向必须是从派生类到基类或者相同类之间。
- 转换只能用于指针或引用类型。
这里先简单介绍下虚函数,
虚函数是 C++ 中一种特殊的成员函数,用于实现多态性。通过在基类中声明虚函数,可以在派生类中进行函数的重写,并使用基类指针或引用调用相应的派生类函数。
虚函数的特点包括:
- 虚函数通过在基类中使用 virtual 关键字进行声明,告诉编译器该函数是一个虚函数。
- 虚函数可以在派生类中进行重写,即在派生类中使用相同的函数名和参数列表重新定义函数体。
- 使用基类指针或引用调用虚函数时,将根据指针或引用所指向的对象的实际类型来决定调用哪个版本的函数。
- 析构函数通常应当声明为虚函数,以确保在删除基类指针时正确调用派生类的析构函数,从而避免内存泄漏。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std;class Base { public:// 基类中的虚函数virtual void func() { cout << "Base::func()" << endl; } };class Derived : public Base { public:// 派生类中重写的虚函数virtual void func() { cout << "Derived::func()" << endl; }// 派生类中新增的成员函数void derived_func() { cout << "Derived::derived_func()" << endl; } }; int main() {// 创建基类指针,指向派生类对象Base* ptr_base = new Derived;// 调用虚函数,根据实际对象类型确定调用版本ptr_base->func(); // 输出 "Derived::func()"// 使用 dynamic_cast 进行类型转换Derived* ptr_derived = dynamic_cast<Derived*>(ptr_base);if (ptr_derived) {// 转换成功,调用派生类的成员函数ptr_derived->derived_func(); // 输出 "Derived::derived_func()"}else {// 转换失败cout << "类型转换失败!" << endl;}// 释放动态分配的内存delete ptr_base;return 0; }
在主函数中,首先创建一个基类指针 ptr_base,并将其指向一个派生类对象。然后,通过 ptr_base->func() 调用虚函数,根据实际对象类型确定调用的版本,输出 "Derived::func()"。接下来,使用 dynamic_cast 将基类指针 ptr_base 转换为派生类指针 ptr_derived。如果转换成功,即原始对象确实是派生类对象,则可以使用 ptr_derived 调用派生类的成员函数 derived_func(),输出 "Derived::derived_func()";如果转换失败,则输出 "类型转换失败!"。最后,通过 delete 释放动态分配的内存,删除 ptr_base 指向的对象。注意,虚函数的关键在于通过基类指针或引用来调用函数,以实现多态性。而类型转换(如 dynamic_cast)则用于在需要时将基类指针或引用转换为派生类指针或引用,以调用派生类的特定函数。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
using namespace std;class Person {
protected:string _name; // 姓名string _sex; // 性别int _age; // 年龄
};class Student : public Person {
public:int _No; // 学号
};void Test() {Student sobj;// 1.子类对象可以赋值给父类对象/指针/引用Person pobj = sobj;Person* pp = &sobj;Person& rp = sobj;//2.基类对象不能赋值给派生类对象// sobj = pobj; // 编译错误,无法将基类对象赋值给派生类对象// 3.基类的指针可以通过强制类型转换赋值给派生类的指针pp = &sobj;Student* ps1 = (Student*)pp; // 这种情况转换时可以的。ps1->_No = 10;pp = &pobj;Student* ps2 = (Student*)pp; /*这种情况转换时虽然可以,但是会存在越界访问的问题pobj 是一个 Person 类型的实例,将其地址赋值给 Student* 类型的指针 ps2 并对其进行访问,实际上是在试图访问 Person 对象中并不包含的 _No 成员变量。这种行为可能导致访问到无效的内存区域,从而引发程序崩溃或产生意想不到的行为。*/ //ps2->_No = 10; // 访问越界,会导致程序崩溃}int main() {Test();return 0;
}
三 、继承中的作用域
- 1. 在继承体系中基类和派生类都有独立的作用域。
- 2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏, 也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
- 3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 4. 注意在实际中在继承体系里面最好不要定义同名的成员。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
class Person {
protected:string _name = "小李子"; // 姓名int _num = 111; // 身份证号
};class Student : public Person {
public:void Print() {cout << "姓名: " << _name << endl;cout << "身份证号: " << Person::_num << endl;/*通过 Person::_num 访问基类 Person 中的 _num 成员变量,以区分基类和派生类的成员。*/cout << "学号: " << _num << endl;}protected:int _num = 999; // 学号
};void Test() {Student s1;s1.Print();
}int main() {Test();return 0;
}
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;//重载(Overloading)是指在同一个作用域内,使用相同的函数名但参数列表不同的多个函数。
// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。class A {
public:void fun() {cout << "func()" << endl;}
};class B : public A {
public:void fun(int i) {A::fun();cout << "func(int i) -> " << i << endl;}
};void Test() {B b;b.fun(10);
}int main() {Test();return 0;
}
四、派生类的默认成员函数
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成,那么在派生类 中,这几个成员函数是如何生成的呢?
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认 的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的operator=必须要调用基类的operator=完成基类的复制。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能 保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 派生类对象初始化先调用基类构造再调派生类构造。
- 派生类对象析构清理先调用派生类析构再调基类的析构。
- 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加 virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
using namespace std; class Person
{
public:Person(const char* name = "peter") // 构造函数,姓名默认为 "peter": _name(name) // 初始化成员变量 _name{cout << "Person()" << endl; }Person(const Person& p) // 拷贝构造函数: _name(p._name) // 初始化成员变量 _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 num) // 构造函数: Person(name) // 调用基类构造函数初始化姓名, _num(num) // 初始化学号{cout << "Student()" << endl; }Student(const Student& s) // 拷贝构造函数: Person(s) // 调用基类的拷贝构造函数, _num(s._num) // 初始化学号{cout << "Student(const Student& s)" << endl; }Student& operator=(const Student& s) // 赋值运算符重载{cout << "Student& operator= (const Student& s)" << endl; if (this != &s) // 避免自我赋值{Person::operator=(s); // 调用基类的赋值运算符重载_num = s._num; // 赋值操作}return *this; // 返回当前对象的引用}~Student() // 析构函数{cout << "~Student()" << endl; }protected:int _num; // 学号
};void Test()
{Student s1("jack", 18); // 创建学生对象 s1Student s2(s1); // 使用拷贝构造函数创建学生对象 s2Student s3("rose", 17); // 创建学生对象 s3s1 = s3; // 调用赋值运算符重载
}int main()
{Test(); // 调用测试函数return 0;
}
- 在Test()函数中,创建了学生对象s1,调用了带有参数的构造函数。首先会调用基类Person的无参构造函数 Person(),输出 "Person()"。然后调用子类Student的带参构造函数 Student(const char* name, int num),输出 "Student()"。
- 接下来,使用拷贝构造函数创建学生对象s2,并且将s1作为参数传入。首先会调用基类Person的拷贝构造函数 Person(const Person& p),输出 "Person(const Person& p)"。然后调用子类Student的拷贝构造函数 Student(const Student& s),输出 "Student(const Student& s)"。
- 创建学生对象s3,调用了带有参数的构造函数。首先会调用基类Person的无参构造函数 Person(),输出 "Person()"。然后调用子类Student的带参构造函数 Student(const char* name, int num),输出 "Student()"。
- 将s3赋值给s1,调用了赋值运算符重载函数。首先会调用子类Student的赋值运算符重载函数 Student& operator= (const Student& s),输出 "Student& operator= (const Student& s)"。在该函数内部,首先会调用基类Person的赋值运算符重载函数 Person operator=(const Person& p),输出 "Person operator=(const Person& p)"。
- 程序执行结束,开始析构阶段。首先按照创建的相反顺序析构学生对象s1、s3和s2。对于每个对象,首先调用子类Student的析构函数 ~Student(),输出 "~Student()",然后调用基类Person的析构函数 ~Person(),输出 "~Person()"。
五、继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;class Student; /*前向声明是指在使用某个类之前提前声明该类的存在,
以便编译器知道这个类的存在而不需要包含完整的类定义。
前向声明通常用于解决类之间相互引用的问题,
避免头文件互相包含导致的循环包含。
通过在Person类之前声明Student类,以及在Student类之前声明Person类,
可以使编译器知道这两个类的存在,从而避免编译错误
*/ class Person {
public:friend void Display(const Person& p, const Student& s);protected:string _name="123"; // 姓名
};class Student : public Person {
protected:int _stuNum; // 学号
};void Display(const Person& p, const Student& s) {cout << p._name << endl;cout << s._stuNum << endl; //基类友元不能访问子类私有和保护成员
}int main() {Person p;Student s;Display(p, s);return 0;
}
解决方法可以将Student类的_stuNum成员变量改为公有或者编写一个公有的获取函数来获取该成员变量,例如:
class Student : public Person {
public:int GetStuNum() const { return _stuNum; } // 获取学号
protected:int _stuNum=1; // 学号
};void Display(const Person& p, const Student& s) {cout << p._name << endl;cout << s.GetStuNum() << endl;
}
六、继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子 类,都只有一个static成员实例 。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>
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; // 研究科目
};void TestPerson()
{Student s1;Student s2;Student s3;Graduate s4;//构造函数会按照继承链的顺序依次调用每个基类的构造函数。cout << "人数: " << Person::_count << endl;Student::_count = 0;cout << "人数: " << Person::_count << endl;
}int main()
{TestPerson();return 0;
}
七、复杂的菱形继承及菱形虚拟继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。 在Assistant的对象中Person成员会有两份。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <string>using namespace std;
class Person {
public:string _name; // 姓名
};
class Student : public Person {
protected:int _num; //学号
};
class Teacher : public Person {
protected:int _id; // 职工编号
};
class Assistant : public Student, public Teacher {
protected:string _majorCourse; // 主修课程
};
void Test() {// 这样会有二义性无法明确知道访问的是哪一个Assistant a;a._name = "peter";// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决a.Student::_name = "xxx";a.Teacher::_name = "yyy";
}
int main()
{Test();return 0;
}
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和 Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地 方去使用。
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";
}
虚拟继承解决数据冗余和二义性的原理
为了研究虚拟继承原理,现给出了一个简化的菱形继承继承体系,借助内存窗口观察对象成员的模型。
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;class A
{
public:int _a;
};// class B : public A
class B : virtual public A
{
public:int _b;
};// class C : public A
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 = 2;d._b = 3;d._c = 4;d._d = 5;cout << "B::_a: " << d.B::_a << endl;cout << "C::_a: " << d.C::_a << endl;cout << "B::_b: " << d._b << endl;cout << "C::_c: " << d._c << endl;cout << "D::_d: " << d._d << endl;return 0;
}
在菱形继承中,如果不使用虚拟继承,类 D 继承了类 B 和类 C,而类 B 和类 C 都各自继承了类 A。这样在类 D 中就会包含两份类 A 的成员变量。当通过对象 d 访问 _a 时,由于类 B 和类 C 都有一个 _a 成员变量,就会造成二义性问题,编译器无法确定应该访问哪一个 _a 成员变量。
通过使用虚拟继承,类 B 和类 C 虚拟继承类 A,这样在类 D 中只包含一份类 A 的成员变量。当通过对象 d 访问 _a 时,就不会有二义性问题,可以明确地访问到类 A 的那份 _a 成员变量。
下图是菱形继承的内存对象成员模型:这里可以看到数据冗余
下图是菱形虚拟继承的内存对象成员模型:
// 为什么D中B和C部分要去找属于自己的A? 为了解决数据冗余问题
/*那么大家看看当下面的赋值发生时,d是
不是要去找出B/C成员中的A才能赋值过去?
*/D d;
B b = d;
C c = d;
对象 d 是类 D 的一个实例。当执行 `B b = d` 时,d 会被隐式转换为类 B 的一个实例,并调用类 B 的拷贝构造函数。类 B 的拷贝构造函数会从对象 d 中抽取出属于类 B 的部分,然后将这些数据复制到对象 b 中。类似地,当执行 `C c = d` 时,d 会被隐式转换为类 C 的一个实例,并调用类 C 的拷贝构造函数。
由于类 D 继承了类 B 和类 C,而类 B 和类 C 都继承了类 A,因此类 D 对象中包含了类 A、类 B 和类 C 的成员。在执行 `B b = d` 时,只会复制类 B 相关的部分,也就是类 A 和类 B 的成员;在执行 `C c = d` 时,只会复制类 C 相关的部分,也就是类 A 和类 C 的成员。因此,不需要去找出 B/C 成员中的 A 才能赋值过去,而是直接从对象 d 中抽取出属于类 B/C 的部分,然后进行赋值操作。
下面是上面的Person关系菱形虚拟继承的原理解释:
八、继承的总结和反思
- C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设 计出菱形继承。否则在复杂度及性能上都有问题。
- 多继承可以认为是C++的缺陷之一,很多后来的面向对象语言都没有多继承,如Java。
- 继承和组合
- public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
- 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
- 优先使用对象组合,而不是类继承 。
- 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称 为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的 内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
- 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象 来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复 用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。 组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被 封装。
- 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有 些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用 继承,可以用组合,就用组合。
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <string> using namespace std;// Car和BMW(奔驰) Car和Benz(宝马)构成is-a的关系class Car { public:string _colour = "白色"; // 颜色string _num = "陕ABIT00"; // 车牌号 };class BMW : public Car { public:void Drive() { cout << "好开-操控" << endl; } };class Benz : public Car { public:void Drive() { cout << "好坐-舒适" << endl; } };int main() {BMW bmw;Benz benz;cout << "BMW车特点:" << endl;cout << "颜色:" << bmw._colour << endl;cout << "车牌号:" << bmw._num << endl;bmw.Drive();cout << endl;cout << "Benz车特点:" << endl;cout << "颜色:" << benz._colour << endl;cout << "车牌号:" << benz._num << endl;benz.Drive();return 0; }
#define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <string> using namespace std;// Tire和Car构成has-a的关系 class Tire { public:string _brand = "Michelin"; // 品牌size_t _size = 17; // 尺寸 };class Car { public:string _colour = "白色"; // 颜色string _num = "陕ABIT00"; // 车牌号Tire _t; // 轮胎 };int main() {Car myCar;cout << "我的汽车信息:" << endl;cout << "颜色:" << myCar._colour << endl;cout << "车牌号:" << myCar._num << endl;cout << "轮胎品牌:" << myCar._t._brand << endl;cout << "轮胎尺寸:" << myCar._t._size << endl;return 0; }
九、笔试面试题
- 什么是菱形继承?菱形继承的问题是什么?
- 什么是菱形虚拟继承?如何解决数据冗余和二义性的
- 继承和组合的区别?什么时候用继承?什么时候用组合?
- 菱形继承是指在类的继承层次结构中,某个类同时继承自两个不同的类,这两个类又间接继承自同一个基类的情况。这样会导致从子类向上转型时,出现了同名成员变量和函数的二义性问题,造成了程序设计上的困难和效率低下。
- 菱形虚拟继承是一种特殊的继承方式,用于解决菱形继承中的数据冗余和二义性问题。通过在虚继承的类之间建立虚基类,可以消除掉重复的成员数据,同时保留从基类继承来的唯一拷贝。在菱形虚拟继承中,派生类需要使用 virtual 继承方式继承虚基类,以确保每个对象只包含一个虚基类子对象。
- 继承和组合都是面向对象编程中的重要概念。继承是指从已有类中派生出新的类,新类继承了父类的属性和方法,并且可以新增或修改其属性和方法。组合是指将多个已有类组合在一起形成新的类,新类包含了组合类的属性和方法,通过调用组合类的方法来实现新类的功能。
一般情况下,当新类需要具有与已有类相似的属性和方法,并且需要新增或修改一些属性和方法时,可以使用继承。如果新类需要包含多个不同的类的属性和方法,并且需要灵活地组合和调用这些类的方法,可以使用组合。在具体设计中,需要根据实际需求来选择使用继承或组合。同时,需要注意继承可能带来的耦合和二义性问题,以及组合可能带来的复杂性和代码重复问题。