目录
一、什么是继承?怎么定义继承?
二、继承关系和访问限定符?
三、基类和派生类对象可以赋值转换吗?
四、什么是隐藏?隐藏vs重载?
五、派生类的默认成员函数?
1)派生类构造函数怎么写?
2)派生类拷贝构造函数怎么写?
3)派生类operator=怎么写?
4)派生类析构函数怎么写?☆☆
六、什么是友元?友元关系能继承吗?
七、静态成员和继承的关系?
八、什么是多继承,多继承的坑是什么?
九、如何解决菱形继承的问题?
十、菱形虚拟继承的原理是什么?
一、什么是继承?怎么定义继承?
当多个类之间有许多共同的方法或属性时,可以将这些方法和属性提取出来作为一个父类,让子类通过继承父类来进行代码的复用,同时,子类也可以在父类的基础上做一些扩展。
怎么定义:
class 派生类名:继承方式(public/protected/private) 基类名 { public: int _n; }; //eg: class Student:public Person{ public: int _stuid; };
二、继承关系和访问限定符?
1)首先对于基类的私有成员,无论以什么方式继承,在类外以及派生类内都是不可见的。
2)对于基类的公有成员和保护成员:
公有继承:基类所有成员保持原有访问权限。
保护继承:基类的公有成员访问权限变成保护。
私有继承:基类的公有成员和保护成员访问权限变成私有。
三、基类和派生类对象可以赋值转换吗?
1)向上转换:派生类对象/指针可以赋值给基类的对象/指针/引用,这种情况也叫切片(只看到基类中的部分)。
!!注意:只有公有继承才有可能直接进行基类和派生类之间的赋值转换,私有和保护继承改变了派生类对基类的访问权限,使得无法直接进行类型转换。
2)向下转换:基类对象不能赋值给派生类对象。基类指针类型或基类引用类型如果原本是指向派生类的,那么它可以赋值给派生类指针类型或派生类引用类型,但需要手动强制类型转换。
class Base {
private:
int private1;
};
class Pai1 : public Base{
private:
int p1;
};
int main()
{
Pai1 p;
Base b;
b = p;
Base& rb = p;
Base* pb = &p;
Pai1* pp = (Pai1*)pb;
Pai1& rp = (Pai1&)rb;
}
四、什么是隐藏?隐藏vs重载?
在继承体系中基类和派生类的作用域是独立的,如果在基类和派生类中出现了同名成员,子类将屏蔽父类同名成员,直接访问自己的,这就叫隐藏。如果是成员函数,只要函数名相同,就构成隐藏(也叫重定义)。
如果子类想要访问父类的同名成员,可以指定类域。
class A { public: void fun(){ std::cout << "A" << std::endl; } }; class B : public A{ void fun(){ A::fun(); std::cout << "B" << std::endl; } };
隐藏和重载的区别在于:隐藏是在不同的作用域的,重载是在同一作用域下的。且重载的限制条件更多,不仅要求函数同名,且形参列表必须不同。
五、派生类的默认成员函数?
1)派生类构造函数怎么写?
派生类必须调用父类构造函数来初始化父类成员。
1)父类有默认构造,派生类构造时会自动调用父类默认构造。
2)父类没有默认构造,需要在派生类构造函数的初始列表显示调用父类构造函数。
class Person { public: Person(string name) :_name(name) {} private: string _name; }; class Student :public Person{ public: Student(string name,int num) :Person(name) ,_num(num) {} private: int _num; };
2)派生类拷贝构造函数怎么写?
调用父类的拷贝构造,可以传子类的对象给父类拷贝构造,构成切片。然后再实现自己的部分。
不显示写的话会调父类的默认构造而不是拷贝构造,所以不能不显示调用。
Student(const Student& stu) :Person(stu) ,_num(stu._num) {}
3)派生类operator=怎么写?
要显示调用父类的operator=,再实现自己的部分。
Student& operator=(const Student& stu) { if (this != &stu) { Person::operator=(stu);//调用父类的 _num = stu._num; } return *this; }
4)派生类析构函数怎么写?☆☆
派生类的析构函数不需要我们手动调用,因为它要保证先析构子类,再析构父类。以防出现析构完父类后,子类又用到父类成员的情况。让编译器来做这件事比让人来做更靠谱。
六、什么是友元?友元关系能继承吗?
如果一个函数或一个类想访问某一个类A的保护成员或私有成员,可以在类A中声明一下:
注意:友元关系不能继承!!!
七、静态成员和继承的关系?
静态成员不属于某个类或对象,它是在静态区中保存的,只有一份,但它是受类的访问限定符的约束的。子类继承的是静态成员的使用权。
静态成员变量必须在类内声明,类外定义和初始化
静态成员函数的定义和初始化可以在类内也可以在类外。
class MyClass { public: static int staticVar; // 在类内声明静态成员变量 static void staticFunc() { // 在类内定义静态成员函数,可以直接实现功能 } }; int MyClass::staticVar = 0; // 在类外定义静态成员变量并进行初始化
八、什么是多继承,多继承的坑是什么?
一个人是可能有多重角色的,那么有多个父类也是合理的。
但是,有了多继承,就有可能出现菱形继承,菱形继承会引发很多问题:
① 二义性(Assistant对象中访问的_name到底访问的是从哪个类中继承的?)
② 数据冗余(浪费空间)
九、如何解决菱形继承的问题?
1)二义性:可以通过指定类域来访问,这种方式虽然表面上解决了二义性的问题,但它其实是违背了现实世界的。
2)更根本地解决数据冗余和二义性的做法是:虚继承
给菱形继承中继承了同一个类的类都加上virtual关键字。
class Student:virtual public Person { //... } class Teacher:virtual public Person { //... }
十、菱形虚拟继承的原理是什么?
继承了同一个类的这些类加了virtual关键字后,这些类会多一个成员,这个成员的类型是虚基表的指针,指向的是一张虚基表,虚基表里保存是距离父类的成员的偏移量。
思考一下:D对象赋值给B对象的切片,B*pb = &d; pb->_a可以正常访问吗?
为什么不直接存偏移量?
虚基表除了存偏移量可能还会存别的值,在对象中存虚基表的指针可以保证每个对象只需要多存一个指针大小即可。
在下面的例子中,A的构造会调用几次?ABCD谁先构造?
十一、继承和组合的区别?什么时候用继承,什么时候用组合?
组合是指一个类持有另一个类的实例作为自己的属性,通过调用被组合对象的方法实现功能。组合是一种"has a"关系,即一个对象包含另一个对象。
在选择使用继承还是组合时,通常遵循以下原则:
- 当两个类之间有明显的"is a"关系,即子类是父类的一种特殊形式时,可以使用继承。
- 当两个类之间有“has a”关系,即一个类包含另一个类作为属性时,可以使用组合。
继承时一种白箱复用,其内部的细节对子类是可见的,组合是一种黑箱复用,对象内部细节是不可见的。尽量避免过度使用继承,因为过度继承可能导致类之间的耦合度过高,影响代码的灵活性和可维护性。因此在设计时需要根据具体的情况选择合适的方式来设计类之间的关系。