继承的概念及定义
概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
C++在设计继承时考虑的比较多,所以子类继承基类时有9种继承方式
总结:
1.基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public>protected>private。
4. 使用关键字class时默认的继承方式private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。
基类和派生类对象赋值转换
- 派生类对象 可以赋值给 基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
- 基类对象不能赋值给派生类对象。
- 基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。
情况一:子类给父类赋值
情况二:父类给子类赋值
情况三:父类指针,指向一块子类的地址,然后强转给子类
情况四:父类指针,指向一块父类的地址,然后强转给子类
继承中的作用域
- 在继承体系中基类和派生类都有独立的作用域。
- 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
- 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
- 注意在实际中在继承体系里面最好不要定义同名的成员。
成员同名:
那我们就是要访问父类的_No呢?没错,指定类域就好。
函数同名:
那么要访问父类的函数呢?解决方法和成员同名一样。
派生类的默认成员函数
6个默认成员函数,“默认”的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢?
- 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
- 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
- 派生类的operator=必须要调用基类的operator=完成基类的复制。
- 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
- 派生类对象初始化先调用基类构造再调派生类构造。
- 派生类对象析构清理先调用派生类析构再调基类的析构。
- 因为后续一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们后面会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destrutor(),所以父类析构函数不加virtual的情况下,子类析构函数和父类析构函数构成隐藏关系。
继承与友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员这里就不做演示,大家有兴趣可以自己试一下
继承与静态成员
基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
从输出的值和地址可以看出,确实只有一份。
复杂的菱形继承及菱形虚拟继承
单继承:一个子类只有一个直接父类时称这个继承关系为单继承
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
菱形继承:菱形继承是多继承的一种特殊情况。
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在D的对象中Base成员会有两份。
那要怎么解决呢?没错用到了虚继承。
为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型。
这里C1先继承所以C1在D中先初始化,加下来我们看一下,C2先继承会怎样,结果是不是我们所分析的那样
下图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将base放到的了对象组成的最下面,这个base同时属于C1和C2,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的base。
继承的总结与反思
1.很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。
2. 多继承可以认为是C++的缺陷之一,很多后来的语言都没有多继承,如Java。
3. 继承和组合
- public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
- 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
- 优先使用对象组合,而不是类继承 。
- 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
- 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
封装。 - 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,可以用组合,就用组合。
继承常见面试题
所有测试代码
#include<iostream>
#include<string>
using namespace std;
//class A
//{
//public:
// A()
// {}
//private:
// string _con;
//};
//int main()
//{
// A a;
// return 0;
//}
//class Person
//{
//public:
// void Print()
// {
// cout << "name:" << endl;
// cout << "age:" << endl;
// }
//protected:
// string name = "小林";
// int age = 19;
//private:
// string BrithDay = "2002/07/15";
//};
class Student : public Person
{
public:
Student()
{
name;
age;
BrithDay;
}
protected:
string ID = "202212905124";//默认ID
};
class Student : protected Person
{
public:
Student()
{
name;
age;
BrithDay;
}
protected:
string ID = "202212905124";//默认ID
};
//class Student : private Person
//{
// public:
// Student()
// {
// name;
// age;
// BrithDay;
// }
//protected:
// string ID = "202212905124";//默认ID
//};
//int main()
//{
// Student s1;
// s1.Print();
// s1.name;
// s1.age;
// s1.BrithDay;
// return 0;
//}
//class Person
//{
//public:
// void Print()
// {
// cout << "name:" << endl;
// cout << "age:" << endl;
// }
//protected:
// string name = "小林";
// int age = 19;
//private:
// string BrithDay = "2002/07/15";
//};
//class Student : public Person
//{
//public:
// Student()
// {}
//protected:
// string ID = "202212905124";//默认ID
//public:
// int _No = 0;
//};
//int main()
//{
// Person p1;
// Student s1;
//
// 子类的对象可以赋值给父类的对象/指针/引用
// //p1 = s1;
// //Person* pp = &s1;
// //Person& rp = s1;
//
// 父类的对象不能给子类的对象
// //s1 = p1;
//
// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
// //Person* pp = &s1;
// //Student* ps1 = (Student*)pp; // 这种情况转换时可以的。
// //ps1->_No = 10;
//
// Person* pp = &p1;
// Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
// ps2->_No = 10;
//
//
//
// return 0;
//}
//class Person
//{
//public:
// void print()
// {
// /*cout << "name:" << endl;
// cout << "age:" << endl;*/
// cout << "print()PersonInfo" << endl;
// }
//protected:
// string name = "小林";
// int age = 19;
// int _No = 10;
//private:
// string BrithDay = "2002/07/15";
//};
//class Student : public Person
//{
//public:
// Student()
// {}
// void print()
// {
// /*cout << "_No=" << Person::_No;*/
// cout << "print()StudentInfo" << endl;
// }
//protected:
// string ID = "202212905124";//默认ID
// int _No = 100;
//};
//int main()
//{
// Student s1;
// s1.Person::print();
// return 0;
//}
//class A
//{
//public:
// A()
// {
// count++;
// }
//private:
// int _a = 0;
//public:
// static int count;
//};
//int A::count = 0;
//class B : public A
//{
//private:
// int _b = 0;
//};
//class C : public A
//{
//private:
// int _c = 0;
//};
//
//int main()
//{
// A a;
// B b;
// C c;
// cout << a.count << endl;
// cout << b.count << endl;
// cout << c.count << endl;
//
// cout << &a.count << endl;
// cout << &b.count << endl;
// cout << &c.count << endl;
// return 0;
//}
//class A
//{
//public:
// A()
// {
// cout << "A()" << endl;
// }
// A(const A& a)
// {
// cout << "A(const A& a)" << endl;
// }
// ~A()
// {
// cout << "~A()" << endl;
// }
// void operator=(const A& a)
// {
// cout << "void operator=(const A& a)" << endl;
// }
//private:
// int _a = 0;
//};
//class B :public A
//{
//public:
// B()
// {
//
// }
// B(const B& b)
// :A(b)
// ,_b(b._b)
// {}
// ~B()
// {
//
// }
// void operator=(const B& b)
// {
// A::operator=(b);
// }
//private:
// int _b = 0;
//};
//int main()
//{
// B b;
// B c(b);
// B d;
// d = b;
// return 0;
//}
class base
{
public:
void print()
{
}
int _n = 0;
};
//class C1 : virtual public base
//{
//public:
// int c1 = 0;
//};
//class C2 : virtual public base
//{
//public:
// int c2 = 0;
//};
class C1 : public base
{
public:
int c1 = 0;
};
class C2 : public base
{
public:
int c2 = 0;
};
class D :public C1,public C2
{
public:
int _d = 0;
};
int main()
{
D d;
d.C1::_n = 1;
d.C2::_n = 2;
d.c1 = 3;
d.c2 = 4;
d._d = 5;
return 0;
}