目录
一、继承的概念和定义
1、什么是继承?
2、如何定义继承关系?
二、基类和派生类对象赋值转换
1、派生类赋值给基类
2、基类指针赋值给派生类指针(需要强制类型转换)
三、继承中的作用域
1、成员变量的隐藏
2、成员函数的隐藏
四、派生类的默认成员函数
1、派生类中默认成员函数完成的任务
2、子类的析构函数和父类的析构函数构成隐藏
五、继承与友元、静态成员的关系
1、继承和友元
2、继承和静态成员
六、菱形继承和菱形虚拟继承
1、单继承
2、多继承
3、菱形继承
4、虚拟继承和虚拟继承解决菱形继承的原理
七、常考面试题
1、什么是菱形继承?菱形继承的问题是什么?如何解决菱形继承的问题?
2、什么是菱形虚拟继承?菱形虚拟继承时如何解决菱形继承中存在的问题的?
3、继承和组合的区别是什么?什么时候用继承?什么时候用组合?
4、你认为C++的缺陷有哪些?
面向对象的三大特征:封装、继承、多态
封装:将事务的属性和行为抽象成具体的数据和方法,使用类对数据和方法进行封装,通过权限访问限定符进行限定,使用者无序关注具体实现(隐藏性),只需通过对象调用类中接口。以类为单位进行管理,提高了代码的复用性和可读性。
继承是面向对象的特征之一,是提高代码复用的重要手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类称派生类原有的类称为基类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。
1)如何理解继承的由简单到复杂的层次结构?
2)如何理解封装和继承提高了代码的复用性?
1)定义格式
2)继承关系和访问限定符
3)继承基类成员访问方式的变化
总结:public继承和private继承时,基类成员的访问方式为继承方式和访问限定符中的”小者“(public > protected > private)。例如:public继承基类的protected成员的访问方式为protected。基类的private成员在派生类中不可见。
注意:
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 << "你好啊,我是" <
派生类的对象可以赋值给基类的对象、基类的指针、基类的引用。我们形象的将其称为切割,即把派生类中从父类继承的那部分切出来赋值给基类对象。
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;
}
基类对象不能赋值给派生类对象,但是基类指针可以通过强制类型转换赋值给派生类的指针。
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;
}
注意:只有当基类的指针是指向派生类对象时,将基类指针通过强制类型转换赋值给派生类才是安全的。
// Student的_num和Person的_num构成隐藏关系
class Person
{
protected :
string _name = "小李子"; // 姓名
int _num = 111; // 身份证号
};
class Student : public Person
{
public:
void Print()
{
cout<<" 姓名:"<<_name<< endl;
cout<<" 身份证号:"<
// 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)->" <
创建一个类时,即使类中没有写任何内容,编译器也会生成六个默认的成员函数(构造、拷贝构造、赋值运算符重载、析构、取地址和const取地址的重载)。
在继承关系中,派生类的默认成员函数需要完成以下任务:
析构函数的函数名由~+类名组成,但是编译器会将所有类的西沟函数的函数名都处理成destructor,在继承关系中只要派生类和基类的成员名相同,则派生类中会对基类中相同的成员进行隐藏。因此,子类的析构函数和父类的析构函数构成了隐藏。在实际的项目开发中,最好不要去显式调用父类的析构函数,这样可能会导致先析构父类的问题。
注意:继承下来的父类成员需要调用父类的构造函数进行初始化,不能直接显式对父类成员进行初始化。
友元关系不能继承,即基类的友元不能访问子类的私有或保护成员。
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);
}
对于基类的静态成员,对于整个继承体系来说都使用这一份静态成员。即,无论派生多少个子类,都只有一个一份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)通过代码查看菱形继承的数据冗余问题和二义性问题
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)如何解决菱形继承存在的问题?
虚拟继承可以解决菱形继承造成的二义性和数据冗余问题。
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