什么是继承
--场景举例
一个游戏里,简单设置两个类 —— NPC、主角.
这两个类中,可能会有许多重复的成员 —— 例如名字/年龄等,
当然也有各自独特的成员。如何更好处理这些重复的成员呢?
以前写函数时,快速排序/冒泡排序/堆排序,都要交换变量,都要实现交换逻辑,
就可以把这段相同的逻辑提取出来,放到一个单独的函数中,让以上的排序函数去调用交换函数.
--相关定义
对于这两个类,我们把这些重复的成员提取出来,放到一个公共的类Role中,
让这两个类一起复用公共类的成员.这种复用方式就是继承.
( 1 ) 共有的数据/方法都提取到一个类中,这个类叫父类/基类
( 2 ) 对父类进行继承的类,叫子类/派生类
--继承的基本格式
class 子类:继承方式 父类{//子类独有成员}
class NPC:public Role
{
//NPC的独有成员函数和成员变量
}
其实和定义一个普通类差不多,主要增加了 继承方式 和 要继承的父类.
简单举例:
暂时把成员全部定义为public
class Role
{
public:
string _name = "ori";
int _age = 18;
};
// 子类是NPC,这里的public是继承方式,用公有继承方式继承Role
class NPC : public Role
{
public:
void print()
{
cout << _name << ":" << _age << ":" << friendly << endl;
}
protected:
bool friendly = true; //是否友好
};
void test1()
{
NPC ori;
ori.print();
}
NPC类用 公有继承方式 继承 Role类,此时定义一个子类对象,
子类对象里有 父类的成员变量 和 子类的专有成员变量
【类对象只会存成员变量,不存储成员函数】
继承方式
--引出
一个类中的成员,被三种访问限定符限制.
public成员:使用类对象,在类内/类外都可以访问.
protected/private成员:使用类对象,在类外不能访问,在类里可以访问.
那继承下来的成员,它们在子类的访问限定符会变成什么?
由继承方式来确定.
--三种继承方式
继承方式的关键字和访问限定符的关键字相同.
public继承、protected继承、private继承
父类的成员,用不同的继承方式继承,它们在子类的访问限定符会变成什么?
( 1 ) 一般用public继承
( 2 ) 父类中的private成员,不管用什么方式继承,在子类都是不可见的【特殊】
class Role
{
private:
int _pri;
};
class NPC : public Role
{
//尝试访问继承下来的 父类private成员
void visit_Pri()
{
//_pri;都会报错
//Role::_pri;
}
protected:
bool friendly = true; //是否友好
};
void test2()
{
NPC child;
cout << sizeof(child) << endl;//8
}
不可见:父类的私有成员仍然被继承到了子类对象内部,仍然会占用大小
但子类对象在类内类外,都无法访问到该成员.
【父类某些成员不想被子类继承下来,就声明为private】
( 3 ) 访问权限大小:public > protected > private
父类中的protected/public成员,用3种方式分别继承,
继承下来的成员在子类的访问权限 = min( 成员在父类的访问限定符,继承方式 )
例:
父类的protected成员,用public方式继承,
那在子类中的访问限定符就是protected.
父类的public成员,用private方式继承,
在子类中的访问限定符就是private.
--特殊
可以不显式写继承方式.
class的默认继承方式是private
struct的默认继承方式是public
继承中的隐藏
--域的基本知识
类定义了类域,域会影响访问.
同一个域不能定义同名的变量,
不同的域可以定义同名的变量.
函数特殊,同一个域可以定义构成函数重载的函数.
父类和子类有各自的类域.
--同名成员构成隐藏
子类和父类中如果有同名成员,子类成员将屏蔽对父类同名成员的直接访问,这种现象叫隐藏.
( 1 ) 子类和父类有相同名称的变量:
使用子类对象时,默认访问子类自己的成员变量,而不是父类的同名变量.
需要指定域才能访问到同名的父类成员变量.
( 2 ) 成员函数的隐藏,只要函数名相同就构造隐藏.
函数重载的前提是在同一个域中.
class Role
{
public:
int showAge()
{
return _age;
}
int _age = 5;
};
class NPC :public Role
{
public:
int showAge()
{
return _age;
}
int _age = 100;
};
void test2()
{
NPC ori;
//默认调用子类对象的成员
ori.showAge();
cout << ori._age << endl;
//调用父类对象的同名成员,指定类域
ori.Role::showAge();
cout << ori.Role::_age;
}
继承中的切割/切片
--形式
( 1 ) 子类对象可以直接赋值给父类对象,
即把子类对象中,父类部分的数据,交给父类对象.
( 2 ) 父类对象可以直接引用子类对象.
父类对象引用的数据是 子类对象内部父类的那一部分.
( 3 ) 父类对象的指针,可以直接指向子类对象的地址,
也是指向 子类对象内部父类的那一部分 数据地址.
--代码演示
void test3()
{
NPC child;
child._age = 10;
child._name = "child";
//把子类对象直接赋值给父类对象
//子类对象中,把父类那一部分数据给父类对象
Role father = child;
//父类对象的指针直接指向子类对象
Role* rolePtr = &child;
//父类对象直接引用子类对象
Role& fatherRef = child;
}
--注意事项
( 1 ) 一般而言,不同类型的对象相互赋值,都需要隐式类型转换或强制类型转换.
但切割是语法天然支持,没有进行强制类型转换和隐式类型转换.
否则Role& fatherRef = child已经编译报错.
( 2 ) 切割/切片 建立在public继承的前提下,否则仍然会发生强制类型转换.
子类的默认成员函数
在继承体系中,子类的默认成员函数会做什么?
和普通类有什么区别?
子类的成员变量有两个部分:一部分是继承下来的,另一部分是自己独有的成员.
--构造函数
编译器默认生成的构造函数:
( 1 ) 继承下来的成员,调用父类的默认构造函数初始化.
( 2 ) 对于自己独有的成员,内置类型不处理,自定义类型去调用自定义类型的默认构造函数.
class Role
{
public:
Role() { cout << "调用父类的构造函数" << endl; }
//给初始化列表的缺省值
int _age = 10;
string _name = "ori";
};
class NPC :public Role
{
public:
string _child;//自定义类型
int _friendly;//内置类型
};
void test4()
{
NPC a;
cout << "子类对象的内置类型_friendly:" << a._friendly << endl;
cout << "子类对象的自定义类型_child:" << a._child << endl << endl;
cout << "子类对象中父类部分的成员_age:" << a._age << endl;
cout << "子类对象中父类部分的成员_name:" << a._name << endl;
}
在子类中显式写构造函数
子类对象内部,继承下来的成员 是被当作一个整体,统一调用父类的构造函数进行初始化.
因此,在初始化列表中,默认会调用父类的默认构造函数,初始化父类的成员,
也可以通过传参来调用指定的父类构造函数.
class Role//父类有两个不同的构造函数
{
public:
Role() { cout << "调用父类的构造函数Role()" << endl; }
Role(int age, const string& name)
:_age(age)
,_name(name)
{
cout << "调用父类的构造函数Role(int,const string&)" << endl;
}
//给初始化列表的缺省值
int _age = 10;
string _name = "ori";
};
class NPC :public Role
{
public:
NPC()
:_friendly(true)
,Role(5,"kua")//根据传参不同,调用父类的构造函数
{}
bool _friendly;
};
void test5()
{
NPC a;
}
--拷贝构造函数
编译器默认生成的拷贝构造
( 1 ) 继承下来的成员,调用父类的拷贝构造初始化.
( 2 ) 自己独有的成员,内置类型完成浅拷贝,自定义类型去调用自定义类型的拷贝构造.
class Role
{
public:
Role()
:_age(10)
,_name("ori")
{}
//写了拷贝构造函数,相当于有了构造函数,编译器不会再默认生成构造函数,因此要显式写默认构造函数
Role(const Role& r2)
{
cout << "调用父类拷贝构造Role(const Role& r)" << endl;
_name = r2._name;
_age = r2._age;
}
int _age;
string _name;
};
class NPC :public Role
{ public: };
void test6()
{
NPC a;
a._age = 18;
//使用子类默认生成的拷贝构造函数
NPC b = a;
}
显式写子类的拷贝构造函数
在初始化列表,要让 继承下来的成员 调用父类的拷贝构造,用到切割/切片.
class NPC :public Role
{
public:
NPC()
:_friendly(true)
,Role()
{}
NPC(const NPC& npc)
:_friendly(npc._friendly)
,Role(npc)//要初始化 【继承下来的成员】,本该传父类对象
//用npc中的父类那部分成员,来初始化 当前对象的父类部分
{}
bool _friendly;
};
--赋值运算符重载
编译器默认生成的赋值运算符重载
( 1 ) 继承下来的成员,调用父类的赋值运算符重载.
( 2 ) 自己独有的成员,内置类型浅拷贝,自定义类型调用自定义类型的赋值.
显式写子类的赋值运算符重载
只有构造函数有初始化列表,所以子类的赋值运算符重载,
要在函数体内调用父类的赋值运算符重载,赋值 继承下来的成员.
class Role
{
public:
Role()
:_age(10)
,_name("ori")
{}
int _age;
string _name;
};
class NPC :public Role
{
public:
NPC& operator=(const NPC& npc)
{
if (this != &npc)
{
_friendly = npc._friendly;
//1 将npc的父类成员部分,赋值给 当前对象 的 父类成员部分 【切割】
//2 注意指定类域
Role::operator=(npc);
}
return *this;
}
bool _friendly;
};
void test6()
{
NPC a;
a._age = 18;
NPC b;
cout << b._age << endl;
b = a;
cout << b._age;
}
--析构函数
编译器默认生成的析构函数
( 1 ) 子类独有的成员,内置类型不处理,自定义类型去调用自定义类型的析构
( 2 ) 继承下来的成员,去调用父类的析构函数处理
显式写子类的析构函数
编译器仍然会自动调用父类的析构函数,清理 继承的成员.
不需要在函数体内手动调用父类析构函数.
否则,重复析构可能导致程序崩溃.(例:delete[]某个野指针)
class Role
{
public:
~Role()
{
//显式调用string的析构函数清理_name
_name.~basic_string();
cout << "~Role父类析构" << endl;
}
int _age;
string _name;
};
class NPC :public Role
{
public:
~NPC(){}//没有手动去调用父类的析构函数
bool _friendly;
};
void test7()
{
NPC a;
}
为什么编译器仍会自动调用父类的析构函数?
( 1 ) 栈帧中的对象,必须满足后定义的,先析构
( 2 ) 创建一个子类对象:
会先调用父类的构造函数初始化 继承成员,
再初始化子类独有的成员.
( 3 ) 为了保证 后定义的先析构,
即先清理子类独有成员,
再调用父类的析构函数清理 继承成员;
子类的析构函数后面,都会自动调用父类的析构函数,清理 继承成员
class Role
{
public:
Role() { cout << "调用父类构造Role()" << endl; }
~Role() { cout << "调用父类析构~Role" << endl; }
};
class NPC :public Role
{
public:
NPC() { cout << "调用子类构造函数NPC()" << endl; }
~NPC() { cout << "调用子类析构~NPC()" << endl; }
};
void test8()
{
NPC a;
}
--小结
子类的以上默认成员函数:
若由编译器默认生成:
( 1 ) 子类独有的成员,子类单独处理;
( 2 ) 继承下来的成员,调用父类的对应默认成员函数处理.
显式写:
( 1 ) 子类独有的成员,子类单独处理;
( 2 ) 继承下来的成员,被作为一个整体,调用父类的对应成员函数处理.
注意:子类的析构函数,不需要显式调用父类的析构函数.
继承关系
单继承:一个子类只有一个直接父类
多继承:一个子类有两个或两个以上的直接父类
--多继承中子类对象空间分布
( 1 ) 先继承的父类,先调用构造函数初始化对应父类部分.
同时也是最后调用对应的析构函数.
class A1
{
public:
A1() { _a1 = 1; cout << "A1()" << endl; }
~A1() { cout << "~A1()" << endl; }
int _a1;
};
class A2
{
public:
A2() { _a2 = 2; cout << "A2()" << endl; }
~A2() { cout << "~A2()" << endl; }
int _a2;
};
class B :public A1, public A2
{
public:
B() { _b = 3; cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
int _b;
};
void test9()
{
B b;
}
( 2 ) 对象空间
--菱形继承
子类继承两个父类,但这两个父类又继承了同一个类.
是多继承的一种特殊情况,如下图所示:
定义一个C类对象时,里面会保存两份 A类成员变量.
一份是B1类成员里面的A类成员变量,
另一份是B2类成员里面的A类成员变量.
因此菱形继承有数据冗余和二义性的问题.
class A
{
public:
int _a;
};
class B1:public A
{
public:
B1()
:A()
,_b1(1){}
int _b1;
};
class B2 :public A
{
public:
B2()
:A()
,_b2(2){}
int _b2;
};
class C :public B1, public B2
{
public:
int _c;
};
void test10()
{
C c;
c.B1::_a = 10;//指定是哪个_a
c.B2::_a = 20;
c._b1 = 1;
c._b2 = 2;
c._c = 3;
}
指定类域只能解决二义性问题,但数据冗余仍然会浪费空间.
同时解决数据冗余(某些数据重复多余)和二义性 —— 虚继承
--虚继承
虚继承用于解决菱形继承的数据冗余和二义性问题.
若出现菱形继承,在普通的继承下,实例化C类对象,A类的成员会重复出现多份.
使用实例
B1和B2类对A类都进行虚继承,可以保证C类对象,只有一份A类成员.
A类被称为虚基类
class A
{
public:
int _a;
};
//B1 和 B2 对A类进行虚继承
class B1:virtual public A
{
public:
B1()
:A()
,_b1(1){}
int _b1;
};
class B2 :virtual public A
{
public:
B2()
:A()
,_b2(2){}
int _b2;
};
class C :public B1, public B2
{
public:
int _c;
};
void test11()
{
C c;
//它们的地址都是相同的,A类成员只有一份
cout << &(c._a) << endl;
cout << &(c.B1::_a) << endl;
cout << &(c.B2::_a) << endl;
}
如果发生菱形继承,在子类对象中,某一个父类成员会出现两份,
那么就让该父类的直接子类都对它进行虚继承.
菱形虚拟继承的对象模型
以上述的A、B1、B2、C为例,
创建一个C类对象:
A类成员整体只有一份,且被放到公共的位置(VS放到对象末尾)
即B1、B2类共享一份A类成员,且各自保存一个指针.
该指针指向一个表(虚基表),
虚基表中保存【公共的A类成员】离【保存虚基表指针】位置的偏移量.
访问过程
如果是C类对象或C类对象的指针,直接.或->可以找到公共的A类成员.
以前的切片很完整,类成员之间是先后紧密挨着的,但这里的A类成员被放到公共位置,
切片时要通过虚基表找到公共的A类成员,才能切片给B1和B2类对象/引用/指针.
void test12()
{
C c;
B1* pb1 = &c;
//不是直接找到_a,
// pb1先通过虚基表找到pb1离公共类A成员的偏移量
//然后 *(pb1 + 该偏移量) 才能找到_a
pb1->_a = 1;
B2* pb2 = &c;
pb2->_a = 2;
}
虚拟继承的子类模型
如果只有两个类,子类虚继承父类
class A
{
public:
int _a = 0;
};
//B1对A类进行虚继承
class B1:virtual public A
{
public:
B1()
:A()
,_b1(1){}
int _b1;
};
void test13()
{
B1 b1;
}
虚基类成员整体仍然会被放到公共位置,
本该保存A类成员的位置,保存虚基表指针.
和菱形虚拟继承的模型保持一致