目录
继承概念及定义
继承概念
继承定义
语法
继承关系和访问限定符
继承基类成员访问方式的变化
规律总结
以公有继承为例测试
基类和派生类对象赋值转换
继承中的作用域
派生类的默认成员函数
构造函数
析构函数
拷贝构造
赋值重载
继承与友元
继承与静态成员
菱形继承以及菱形虚拟继承
单继承和多继承
菱形继承
虚继承解决数据冗余和二义性
虚继承的原理(什么是虚基表)
继承和组合
●继承机制是面向对象程序设计使代码可以复用的重要手段。
●继承在保持原有类特性的基础上可进行扩展,增加功能。这样产生的类,叫做派生类。
●继承呈现了面向对象程序设计的层次结构,继承是类设计层次的复用。
上述图解中,Person是基类,成员变量有姓名和年龄,成员函数有一个Print。基类的成员和方法教师和学生对象都需要,如果不使用继承,学生和教师类就要都写一份。而使用继承的方式,实现了代码的复用,而且,派生类可以增加和扩展新的功能。上述图解也体现了面向对象程序设计的层次结构!
继承后基类(Person)的(成员函数+成员变量)都变成了子类的一部分。这里体现了复用的性质。
class 派生类名 : 继承方式 基类类名
●继承关系有三种:公有(public)继承、保护(protected)继承、私有继承(private)。
●访问限定符有三种:public访问、protected访问、private访问。
a.基类的private成员继承到了派生类中,但是不可见的,无论你用什么继承方式。不可见指的是基类的私有成员继承到了派生类对象中,但是在语法上限制派生类对象不管在类内还是类外面都不能访问。
b.保护限定符专门为继承而生,假设基类成员不想在类外被直接访问,但需要在派生类中访问,就定义基类的成员属性为保护(protected)。
c.基类的私有成员总是派生类不可见的。其余继承方式和访问限定符的组合取某个成员在基类中的访问限定符和继承方式更小的一个(public>protected>private):
基类成员在派生类中的访问方式 = Min(成员在基类的访问限定符,继承方式)。
d.默认情况下,class的默认继承方式是私有继承,struct的默认继承方式是公有继承。不过最后都显示的写出继承方式。
e.公有继承最为常用,原因在于继承的本质是代码复用的一种手段,保护和私有继承下来的成员只能在派生类使用或者不可见。
class Base
{
public:
void Fun1()
{
cout << "公有成员函数" << endl;
}
protected:
void Fun2()
{
cout << "Base保护成员函数" << endl;
}
private:
//私有成员变量
int _id;
};
//公有继承
class Derived : public Base
{
//公有继承:基类的私有成员在派生类中不可见
//公有继承: 基类的保护成员在派生类内可以访问,不可以在类外访问
//公有继承:基类的公有成员在派生类内和类外均可访问。
public:
void FunDerived()
{
//类内访问基类的保护成员
Fun2();
}
private:
};
int main()
{
Derived d1;
d1.Fun1();
d1.FunDerived();
return 0;
}
1.公有继承:基类的私有成员在派生类中不可见 。
2.公有继承: 基类的保护成员在派生类内可以访问,不可以在类外访问。
3.公有继承:基类的公有成员在派生类内和类外均可访问。
1.派生类对象可以赋值给 基类的对象、基类的指针或者基类的引用。这里有个形象的说法叫做切片。
2.基类的对象不能赋值给派生类对象。
3. 切割的过程没有发生类型转换
a.在下述示例中,int类型的引用引用double类型的数据。在这个过程中会先生成一个临时变量,在将临时变量,int& 引用的是产生的临时变量,所以必须用const int &。
b.基类的引用引用派生类
//基类
class Base
{
public:
int _num = 2023;
};
//派生类
class Derived : public Base
{
public:
int _num = 2024;
};
int main()
{
Derived d;
Base& b = d;
return 0;
}
赋值的过程没有产生临时变量。
4.赋值转换测试
class Base
{
public:
int _id = 11;
int _num = 22;
};
class Derived : public Base
{
public:
int _name = 333;
};
int main()
{
Base b;
Derived d;
d._id = 111;
d._num = 222;
//派生类对象赋值给基类的对象
//b = d;
//派生类对象赋值给基类的指针
//Base* dptr = &d;
//派生类对象赋值给基类的引用
//Base& pd = d;
return 0;
}
派生类对象赋值给基类对象
1.在继承体系中基类和派生类都有独立的作用域。
//基类
class Base
{
public:
int _num = 2023;
};
//派生类
class Derived : public Base
{
public:
int _num = 2024;
};
int main()
{
Derived d;
cout << "Derived _num:" << d._num << endl;
cout << "Base _num:" << d.Base::_num << endl;
return 0;
}
类比一下局部变量和全局变量,不同作用域的局部变量可以重名,原因是作用不同。这里也是一样的道理,基类和派生类有不同的作用域。
2.因为子类和父类的作用域不同,所以允许出现同名函数或者成员变量。如果子类和父类中有同名成员,子类成员屏蔽掉父类对同名成员的直接访问,这种情况叫做隐藏(重定义)。
3.同名成员变量,子类成员变量屏蔽掉父类同名成员,子类对象默认访问的是子类中的变量。如果向访问父类的同名变量可以通过父类名::父类成员变量的方式显示访问。
4.同名函数的隐藏,只需要函数名相同就可以。不用担心构成重载,因为构成重载的条件之一就是要在同一作用域中。
5.案例1:下述代码同名函数构成什么关系?
A重载 B重写 C隐藏(重定义) D程序报错
//基类
class Base
{
public:
void Fun()
{
cout << "Base->func()" << endl;
}
};
//派生类
class Derived : public Base
{
public:
void Fun(int i)
{
cout << "Base->func()" << endl;
}
};
int main()
{
Derived dd;
dd.Fun(10);
return 0;
}
分析:在继承中函数同名就构成了隐藏。因为基类和派生类各自有独立的作用域,所以不会构成重载。关于上述程序的运行结果,会调用派生类的Fun函数,基类的同名函数被隐藏。
6.案例2:下述代码同名函数构成什么关系?
A重载 B重写 C隐藏(重定义) D程序报错
//基类
class Base
{
public:
void Fun()
{
cout << "Base->func()" << endl;
}
};
//派生类
class Derived : public Base
{
public:
void Fun(int i)
{
cout << "Base->func()" << endl;
}
};
int main()
{
Derived dd;
dd.Fun();
return 0;
}
分析:两题看似一样,不同点是在调用的时候案例1传了参数,而案例2没有传参。分析逻辑是一样的,继承关系中同名函数构成隐藏,所以在派生类对象调用Fun不传递参数的时候,程序报错。因为基类的Fun()被隐藏了。
●在普通对象中:构造函数对自定义类型的成员调用其自己的构造函数。对于内置类型不处理。
●在派生类对象中:构造函数对属于基类的以部分调用基类的构造函数,如果基类没有提供默认构造,在派生类初始化列表显示的调用。自定义类型和内置类型的处理方式和普通对象相同。
基类提供默认构造:派生类对象初始化先调用基类构造在调用派生类构造。
//基类
class Base
{
public:
Base()
{
cout << "基类提供默认构造" << endl;
}
};
//派生类
class Derived : public Base
{
public:
Derived()
{
cout << "派生类构造" << endl;
}
};
int main()
{
Derived d;
return 0;
}
基类没有提供默认构造:在派生类初始化列表显示的调用。
//基类
class Base
{
public:
Base(int a)
{
cout << "基类没有提供默认构造" << endl;
}
};
//派生类
class Derived : public Base
{
public:
Derived()
:Base(10)
{
cout << "派生类构造" << endl;
}
};
int main()
{
Derived d;
return 0;
}
●在普通对象中:有资源申请的对象要写析构。完成资源清理的工作。对于自定义类型调用其自己的析构。对于内置类型不处理。
●在派生类对象中:
a、派生类的析构函数调用完后,会自动调用基类的析构函数清理基类成员。 这样做的原因是要保证派生类对象先清理派生类成员再清理基类成员的顺序。
b、对于析构函数,由于某种需要,编译器会将析构函数的名称统一设置为destrutor()。所以子类析构和父类的析构构成隐藏关系。
●派生类构造和析构总结
1.生类对象初始化先调用基类的构造函数,再调用派生类的构造函数。
2.派生类对象析构,先调用派生类析构再调用基类的析构。
●普通对象:对于有资源申请的类,需要显示的定义拷贝构造(否则浅拷贝)。默认生成的拷贝构造,对于自定义类型调用其自己的拷贝构造,对于内置类型逐字节拷贝
●派生类对象:调用基类的拷贝构造完成基类部分的拷贝初始化。
●普通对象:默认生成的赋值重载,对于内置类型逐字节拷贝,对于自定义类型调用其对应的赋值重载。
●派生类对象:派生类对象的operator=必须调用基类的operator=完成基类的复制。
●友元关系不能继承!
class Derived;
class Base
{
friend void Print1(const Derived& dd);
private:
string _name = "张三";
int _age = 18;
};
//派生类
class Derived : public Base
{
private:
int _id = 150;
};
void Print1(const Derived& dd)
{
cout << "姓名:" << dd._name << endl;
cout << "年龄:" << dd._age << endl;
}
int main()
{
Derived d;
Print1(d);
return 0;
}
基类友元不能访问派生类私有和保护成员。
class Derived;
class Base
{
friend void Print1(const Derived& dd);
private:
string _name = "张三";
int _age = 18;
};
//派生类
class Derived : public Base
{
private:
int _id = 150;
};
void Print1(const Derived& dd)
{
cout << "id: " << dd._id << endl;
}
int main()
{
Derived d;
Print1(d);
return 0;
}
如果基类定义了static静态成员,则整个继承体系中只有一个这样的成员。无论有多少派生类,都只有一个static成员实例。
//基类
class Base
{
public:
void PrintCount()
{
cout << "count = " << count << endl;
}
public:
static int count;
};
int Base::count = 0;
//派生类1
class Derived1 : public Base
{};
//派生类2
class Derived2 : public Base
{};
int main()
{
Base bb;
Derived1 d1;
Derived2 d2;
bb.PrintCount();
d1.PrintCount();
d2.PrintCount();
bb.count++;
bb.PrintCount();
d1.PrintCount();
d2.PrintCount();
printf("bb:%p\n",&bb.count);
printf("d1:%p\n",&d1.count);
printf("d2:%p\n",&d2.count);
return 0;
}
单继承:一个子类只有一个直接父类。
多继承:一个子类有两个或两个以上的直接父类。
菱形继承是多继承的一种情况,继承关系类似菱形。
菱形继承带来了数据冗余和二义性的问题:
//菱形继承
class Base
{
public:
int _bb;
};
//派生类1
class Derived1 : public Base
{
public:
int _d1;
};
//派生类2
class Derived2 : public Base
{
public:
int _d2;
};
class DDerived3 : public Derived1, public Derived2
{};
int main()
{
DDerived3 dd;
return 0;
}
数据冗余
二义性
dd._bb;
分析:在菱形继承中,以上图为例,基类A的成员在D中有两份,造成了数据的冗余。而且在调用的时候存在二义性问题。
虚拟继承可以解决上述的问题,以上述的继承关系为例,只需要在菱形继承的腰部(Derived1和Derived2)的位置加上virtual关键字即可解决问题。
//菱形虚拟继承
class Base
{
public:
int _bb;
};
//派生类1
class Derived1 : virtual public Base
{
public:
int _d1;
};
//派生类2
class Derived2 : virtual public Base
{
public:
int _d2;
};
class DDerived3 : public Derived1, public Derived2
{};
int main()
{
DDerived3 dd;
dd._bb;
return 0;
}
以上述虚继承代码为例:观察内存窗口
1.虚继承后DDeriver3中只有一份虚基类Base的成员。
2. Derived1和Derived2的部分都多了一个地址。
3.通过内存窗口找到这两个地址,可以发现里面有两个非常显眼的数据。这两个地址,叫做虚基表指针。
4.这两个数据实际上是记录的是找到虚基类的偏移量。这两个表我们称其为虚基表。
5.为什么要通过偏移量找到虚基类成员,当将派生类的对象赋值给基类的对象、基类的指针或者基类的引用时,要发生切片,派生类要能找到属于基类的一部分。
总结:从结构模型的角度分析,虚继承 将虚基类成员放到了对象组成的最下面,虚基类成员同时属于继承于它的派生类,派生类通过指向虚基表的指针可以找到存放偏移量的虚基表。通过偏移量就能找到图中存放在最下面的虚基类成员。
继承
class A
{
private:
int _a1;
int _a2;
};
class B : public A
{
private:
int _b1;
};
组合
//组合
class A
{
private:
int _a1;
int _a2;
};
class B
{
private:
int _b1;
A _aa;
};
继承和组合的对比
●继承更适合is-a的关系,比如:学生是人,狗是动物等等。 组合更适合has-a的关系,比如:头上有双眼睛,车上有个方向盘等等。
●继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很大的影响。派生类和基类间的依赖关系很强,耦合度高。
●组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被封装。