目录
1、什么是菱形继承?菱形继承的问题是什么?如何解决菱形继承的问题?
2、什么是菱形虚拟继承?菱形虚拟继承时如何解决菱形继承中存在的问题的?
3、继承和组合的区别是什么?什么时候用继承?什么时候用组合?
面向对象的三大特征:封装、继承、多态
封装:将事务的属性和行为抽象成具体的数据和方法,使用类对数据和方法进行封装,通过权限访问限定符进行限定,使用者无序关注具体实现(隐藏性),只需通过对象调用类中接口。以类为单位进行管理,提高了代码的复用性和可读性。
一、继承的概念和定义
1、什么是继承?
继承是面向对象的特征之一,是提高代码复用的重要手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类称派生类原有的类称为基类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。
1)如何理解继承的由简单到复杂的层次结构?
2)如何理解封装和继承提高了代码的复用性?
- 封装:通过类实例化出的对象,每个对象都有各自的非静态数据,但是代码是所有对象共享一份的(复用性)。
- 继承:子类继承父类,子类拥有父类的所有数据和方法,同样的子类会将父类的数据保存一份,但是方法也是和父类共享一份的。
2、如何定义继承关系?
1)定义格式
2)继承关系和访问限定符
3)继承基类成员访问方式的变化
总结:无论那种继承方式,派生类的访问方式总是取基类的成员访问方式和继承方式中较小的。例如,public继承中,基类的protected成员在派生类中仍然是protected的。需要特别注意的是,基类的private成员在派生类中是不可见的,但这不可见并不代表没有继承。
思考:基类的private成员可以不继承吗?
不可以,因为可能基类的public成员方法中使用了该private成员,而父类对该public成员进行了继承。如果不继承该private成员,可能会导致程序错误。
注意:
- 派生类继承了基类的大部分成员(构造函数、赋值运算符重载等不会被继承),因此派生类的private成员在派生类中不可见并不是说派生类没有继承,只是不能在派生类中直接访问基类的private成员。
- 如果派生类是以public或者protected方式继承基类的,那么在基类中,protected修饰的成员在派生类中可以直接访问但是不能在派生类和基类之外的地方直接进行访问。
- 使用class定义类时默认继承方式是private,使用struct定义类时默认继承方式是public。
- 实际应用中一般都使用public继承,类的成员访问限定符一般使用public和protected。
4)继承的代码实例演示
class Person
{
private:
int num;//编号(类似学号、教工号)
protected:
std::string name;//姓名
std::string sex;//性别
int age;//年龄
public:
Person(int _num = 1,std::string _name = "XXX",std::string _sex = "男",int _age = 18)
:num(_num), name(_name), sex(_sex), age(_age)
{}
//打招呼
void sayHello()
{
std::cout << "你好啊,我是" <<name<<"我今年"<<age<<"岁了!"<< std::endl;
}
};
//class student : protected Person
//class student : private Person
class student : public Person
{
protected:
int id;//学号
public:
student(int _id = 1)
:id(_id)
{}
void study()
{
std::cout << "我正在学习" << std::endl;
}
};
int main()
{
student st;
st.sayHello();
st.study();
return 0;
}
二、基类和派生类对象赋值转换
1、派生类赋值给基类
派生类的对象可以赋值给基类的对象、基类的指针、基类的引用。我们形象的将其称为切割,即把派生类中从父类继承的那部分切出来赋值给基类对象。
1)将派生类对象赋值给基类类对象代码演示
注意:即使派生类对象赋值给了基类对象,基类对象也只能访问从基类集成的方法和数据。
int main()
{
student st;//派生类:继承了Person类
Person p(2,"张三","男",20);//基类
p.sayHello();
//将派生类对象直接赋值给基类对象
p = st;
p.sayHello();
//派生类对象赋值给基类对象的引用
Person& p1 = st;
p1.sayHello();
std::cout << &p1 << " " << &st;
//派生类对象赋值给基类对象指针
Person* p2 = &st;
p2->sayHello();
//p2->study();//不能访问派生类的独有方法
return 0;
}
2、基类指针赋值给派生类指针(需要强制类型转换)
基类对象不能赋值给派生类对象,但是基类指针可以通过强制类型转换赋值给派生类的指针。
int main()
{
Person p;
//student& st1 = p;//基类对象不能赋值给派生类引用
//student st2 = p;//基类对象不能赋值给派生类对象
//student* st3 = &p;//基类指针不可以直接赋值给派生类指针
student* st4 = (student*)&p;//基类指针可以通过强制类型转换赋值给派生类对象(不安全的)
Person *p1 = &st1;//基类指针指向派生类
student st5 = (student*)p1;//这样才是安全的
st4->study();
st4->sayHello();
return 0;
}
注意:只有当基类的指针是指向派生类对象时,将基类指针通过强制类型转换赋值给派生类才是安全的。
三、继承中的作用域
- 在继承体系中基类和派生类都由独立的作用域
- 子类和父类中有同名成员,子类成员将屏蔽父类的同名成员的直接访问,这种情况叫隐藏(可以通过:基类::基类成员的方式进行访问)。
- 如果是成员函数的隐藏,只需要是函数名相同就构成隐藏。
1、成员变量的隐藏
// 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;//访问父类_num成员
cout<<" 学号:"<<_num<<endl;
}
protected:
int _num = 999; // 学号
};
void Test()
{
Student s1;
s1.Print();
};
2、成员函数的隐藏
// 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();//调用父类中的fun函数
cout << "func(int i)->" <<i<<endl;
}
};
void Test()
{
B b;
b.fun(10);
};
四、派生类的默认成员函数
创建一个类时,即使类中没有写任何内容,编译器也会生成六个默认的成员函数(构造、拷贝构造、赋值运算符重载、析构、取地址和const取地址的重载)。
1、派生类中默认成员函数完成的任务
在继承关系中,派生类的默认成员函数需要完成以下任务:
- 派生类的构造函数必须调用基类的默认构造函数初始化基类的哪一部分成员,如果基类没有默认构造函数则必须在初始化列表中显式的调用基类的构造函数。
- 派生类的拷贝构造函数必须调用基类的拷贝构造函数完成基类的拷贝初始化
- 派生类的operator=必须调用基类的operator=完成基类的赋值。
- 派生类的析构函数会在调用完成后自动调用基类的西沟函数(构造时先构造基类,析构时后析构基类,保证栈的进出顺序)
- 派生类对象初始化时,先调用基类构造对基类成员进行初始化。
2、子类的析构函数和父类的析构函数构成隐藏
析构函数的函数名由~+类名组成,但是编译器会将所有类的西沟函数的函数名都处理成destructor,在继承关系中只要派生类和基类的成员名相同,则派生类中会对基类中相同的成员进行隐藏。因此,子类的析构函数和父类的析构函数构成了隐藏。在实际的项目开发中,最好不要去显式调用父类的析构函数,这样可能会导致先析构父类的问题。
注意:继承下来的父类成员需要调用父类的构造函数进行初始化,不能直接显式对父类成员进行初始化。
五、继承与友元、静态成员的关系
1、继承和友元
友元关系不能继承,即基类的友元不能访问子类的私有或保护成员。
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
void main()
{
Person p;
Student s;
Display(p, s);
}
2、继承和静态成员
对于基类的静态成员,对于整个继承体系来说都使用这一份静态成员。即,无论派生多少个子类,都只有一个一份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()
{
Student s1 ;
Student s2 ;
Student s3 ;
Graduate s4 ;
cout <<" 人数 :"<< Person ::_count << endl;
Student ::_count = 0;
cout <<" 人数 :"<< Person ::_count << endl;
};
六、菱形继承和菱形虚拟继承
1、单继承
如果一个子类只有一个直接父类时,就称为是单继承。
2、多继承
如果一个子类有两个或者两个以上的直接父类时,就称为多继承。
3、菱形继承
菱形继承也是多继承,是多继承的一种特殊情况。
1)菱形继承存在的问题
菱形继承存在数据冗余和二义性问题。
- 数据冗余:同一个类中存在多个相同的成员
- 二义性:访问相同的成员时到底访问的是那个?
2)通过代码查看菱形继承的数据冗余问题和二义性问题
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";
}
数据冗余问题
3)如何解决菱形继承存在的问题?
虚拟继承可以解决菱形继承造成的二义性和数据冗余问题。
4、虚拟继承和虚拟继承解决菱形继承的原理
1)虚拟继承
使用virtual关键字定义虚拟继承。
//菱形虚拟继承,解决了菱形继承的二义性和数据冗余
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 ; // 主修课程
};
2)虚拟继承解决菱形继承的原理
通过下面程序,观察虚拟继承在内存中的分布:
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
class C : 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;
return 0;
}
当不使用虚拟继承时,通过内存窗口可以观察到以下内容:
使用虚拟继承时,结果如下:
首先,使用虚拟继承后我们可以发现,新创建出来的对象会比不使用虚拟继承创建出来的对象大四个字节,这是因为使用虚拟继承后会给基类的冗余数据重新分配一个内存只对冗余数据保存一份。
其次,在原来的位置出,分别保存了一个指针,该指针指向了一个表,这两个指针叫虚表指针这两个表叫虚基表。虚基表中保存的是偏移量,可以通过该偏移量找到最底下保存冗余数据的地址。例如,内存窗口3打开的是C类的徐表指针指向的虚基表,从续表指针位置到冗余数据位置(地址为:0x012FFA98)的偏移量为0x012FFA98 - 0x012FFA8C = 0x0000000c
七、常考面试题
1、什么是菱形继承?菱形继承的问题是什么?如何解决菱形继承的问题?
- 菱形继承:派生类继承了多个基类,而这多个基类又同时继承了同一个类。
- 存在的问题:数据冗余和二义性问题
- 如何解决:使用虚拟继承
2、什么是菱形虚拟继承?菱形虚拟继承时如何解决菱形继承中存在的问题的?
- 虚拟继承:在基类对象前加virtual关键字,定义虚拟继承。
- 将冗余的数据单独开辟空间进行保存(解决了数据冗余问题),使用虚表指针指向虚表,虚表中保存了从基类成员(冗余的成员)到冗余数据保存位置的偏移量。通过该偏移量可以找到冗余的数据,从而解决了二义性问题。
3、继承和组合的区别是什么?什么时候用继承?什么时候用组合?
- 继承:派生类继承基类
- 组合:一个类的成员中包含了另一个类的对象,称为组合
- 区别:继承对于派生类来说,基类的成员是公开的(也称白盒复用);组合对于派生来说,类的成员是隐藏的,不能直接调用,必须通过对象调用。
- 实际中,能用组合绝不用继承,组合的耦合性更低,代码的可维护性更高。
4、你认为C++的缺陷有哪些?
- 多继承就是c++的一个设计不好的地方,使得C++的语法变得更加复杂。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题
- 继承方式也是C++中设计不好的地方,实际开发中很少使用protected和private继承,也很少使用private类型成员(在派生类中不可见),反而使得C++的语法更加复杂。