目录
1、继承的概念及定义
以前我们接触的复用都是函数复用,继承是类设计层次的复用。继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许在保持原有类特性的基础上及逆行扩展,增加功能,这样可以产生新的类,称为派生类。
1.1定义格式
class Student : public Person
派生类 继承方式 基类
{
}
1.2继承基类尘缘访问方式的变化
总结:1、基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,只是语法上限制派生类对象不管在类里面还是在类外面都不能去访问它。
2、如果基类成员在类外不想被访问但是想在派生类内被访问,就定义为pretected,因此,还可以看出保护成员限定符是因继承才出现的。
3、基类的私有成员在子类都是不可见的。基类的其他成员在子类的访问方式==Min(public > protected > private)-----“哪个小就是哪个”
4、使用class默认继承方式是private,使用struct默认继承方式是public。
5、在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡
使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
2、基类和派生类对象赋值转化
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 ; // 这种情况转换时虽然可以,但是会存在越界访问的问题 //因为必须是基类的指针指向派生类对象时才是安全的。ps2 -> _No = 10 ;}
3、继承中的作用域
1、在继承体系中,基类和派生类都有独立的作用域。
2、子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
3、如果是成员函数的隐藏,只需要函数名相同就可以
4、注意 在实际中在继承体系里面最好不要定义同名成员
4、派生类的默认成员函数
1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。3. 派生类的 operator= 必须要调用基类的 operator= 完成基类的复制。4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。5. 派生类对象初始化先调用基类构造再调派生类构造。6. 派生类对象析构清理先调用派生类析构再调基类的析构。7. 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同 ( 这个我们后面会讲解) 。那么编译器会对析构函数名进行特殊处理,处理成 destrutor() ,所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系
5、继承与友元
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; //不能访问_stuNum
}
void main()
{
Person p;
Student s;
Display(p, s);
}
结论:友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员
6、继承与静态成员
基类定义了一个static静态成员,则整个继承体系中只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。
7、复杂的菱形继承及菱形虚拟继承
单继承:一个子类只有一个直接父类的时候这个继承关系称为单继承
class Person
class Person:public Person
class Post:public Student
多继承:一个子类有两个或两个以上直接父类时的继承关系
class Student class Teacher
class Assistant:public Student,public Teacher
菱形继承是多继承的一种特殊情况
菱形继承是有问题的: 先看一段代码:
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";
}
菱形继承有数据冗余的问题和二义性的问题,在Assistant的对象中Person成员会有两份。
按照上面的方法指定访问哪个父类的成员可以解决二义性的问题,但是冗余任然存在。
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在 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";
}
8、继承的总结与反思
1. 很多人说 C++ 语法复杂,其实多继承就是一个体现。有了多继承 ,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设 计出菱形继承。否则在复杂度及性能上都有问题。2. 多继承可以认为是 C++ 的缺陷之一,很多后来的 语言都没有多继承,如 Java 。
// Car和BMW Car和Benz构成is-a的关系
class Car
{
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
};
class BMW : public Car
{
public:
void Drive() {cout << "好开-操控" << endl;}
};
class Benz : public Car
{
public:
void Drive() {cout << "好坐-舒适" << endl;}
};
// Tire和Car构成has-a的关系
class Tire{
protected:
string _brand = "Michelin"; // 品牌
size_t _size = 17; // 尺寸
};
class Car
{
protected:
string _colour = "白色"; // 颜色
string _num = "陕ABIT00"; // 车牌号
Tire _t; // 轮胎
};
public继承是一种is-a的关系,即每个派生类对象都是一个基类对象
组合是一种has-a的关系,假设b组合了a,每个b对象钟都有一个a对象
3、优先使用对象组合,而不是类继承 。4、继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用 (white-box reuse) 。术语 “ 白箱 ” 是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。5、对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用 (black-box reuse) ,因为对象的内部细节是不可见的。对象只以 “ 黑箱 ” 的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。6、实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。
9、笔试面试题
1. 什么是菱形继承?菱形继承的问题是什么?菱形继承是指在多重继承中,某个类(例如类D)从两个类(类B和类C)继承,而这两个类又分别从同一个基类(类A)继承,形成一个菱形结构。菱形继承的问题主要有两个:
数据冗余:当类D继承自类B和类C时,如果类A中有成员变量,那么类D将会分别继承类B和类C中的类A的拷贝,这会导致类A的实例数据在内存中重复存储,从而增加了内存使用。
二义性:在调用类A的方法时,如果通过类D来调用,编译器可能不知道应该执行类B中的方法还是类C中的方法,导致二义性的问题。
2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的通过虚拟继承,类D将只包含一个类A的实例,从而解决了数据冗余的问题。同时,由于只有一个类A的实例,调用类A的方法时不会产生二义性,因为所有的调用都会指向同一个基类实例。3. 继承和组合的区别?什么时候用继承?什么时候用组合?继承和组合是两种不同的代码复用方式。
继承:
- 定义:子类继承父类的属性和方法,是一对“是一个”(is-a)关系。
- 用途:当子类与父类之间有明显的上下级关系时使用,例如“猫是动物”,可以通过继承来实现共享代码。
- 优点:简单直接,可以重用父类的代码。
- 缺点:可能导致紧耦合,子类对父类的变化敏感。
组合:
- 定义:通过将一个或多个对象组合成一个新对象,形成“有一个”(has-a)关系。
- 用途:当需要将不同的功能组合在一起,且这些功能可以独立存在时使用,例如“汽车有一个引擎”。
- 优点:降低耦合度,增加灵活性,便于修改和扩展。
- 缺点:可能需要更多的代码来实现组合的功能。
何时使用继承:
- 当类之间有明确的层次关系,且想要共享实现时使用继承。
何时使用组合:
- 当类之间的关系更适合使用“拥有”而非“是”的关系,或者需要灵活组合不同功能时使用组合。
总结来说,选择继承还是组合取决于具体的设计需求和类之间的关系。