c++继承

195 阅读13分钟

什么是继承

--场景举例

一个游戏里,简单设置两个类 —— 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类,此时定义一个子类对象,

子类对象里有 父类的成员变量 和 子类的专有成员变量

【类对象只会存成员变量,不存储成员函数】

image.png

继承方式

--引出

一个类中的成员,被三种访问限定符限制.

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 ) 父类对象的指针,可以直接指向子类对象的地址,

也是指向 子类对象内部父类的那一部分 数据地址.

image.png

--代码演示

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继承的前提下,否则仍然会发生强制类型转换.

image.png

子类的默认成员函数

在继承体系中,子类的默认成员函数会做什么?

和普通类有什么区别?

子类的成员变量有两个部分:一部分是继承下来的,另一部分是自己独有的成员.

--构造函数

编译器默认生成的构造函数:

( 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;
}

image.png

在子类中显式写构造函数

子类对象内部,继承下来的成员 是被当作一个整体,统一调用父类的构造函数进行初始化.

因此,在初始化列表中,默认会调用父类的默认构造函数,初始化父类的成员,

也可以通过传参来调用指定的父类构造函数.

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;
}

image.png

--小结

子类的以上默认成员函数:

若由编译器默认生成:

( 1 ) 子类独有的成员,子类单独处理;

( 2 ) 继承下来的成员,调用父类的对应默认成员函数处理.

显式写:

( 1 ) 子类独有的成员,子类单独处理;

( 2 ) 继承下来的成员,被作为一个整体,调用父类的对应成员函数处理.

注意:子类的析构函数,不需要显式调用父类的析构函数.

继承关系

单继承:一个子类只有一个直接父类

image.png

多继承:一个子类有两个或两个以上的直接父类

image.png

--多继承中子类对象空间分布

( 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;
}

image.png

( 2 ) 对象空间

image.png

image.png

--菱形继承

子类继承两个父类,但这两个父类又继承了同一个类.

是多继承的一种特殊情况,如下图所示: image.png

定义一个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;
}

image.png

指定类域只能解决二义性问题,但数据冗余仍然会浪费空间.

同时解决数据冗余(某些数据重复多余)和二义性 —— 虚继承

--虚继承

image.png

虚继承用于解决菱形继承的数据冗余和二义性问题.

若出现菱形继承,在普通的继承下,实例化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;
}

image.png

如果发生菱形继承,在子类对象中,某一个父类成员会出现两份,

那么就让该父类的直接子类都对它进行虚继承.

菱形虚拟继承的对象模型

以上述的A、B1、B2、C为例,

创建一个C类对象:

image.png

A类成员整体只有一份,且被放到公共的位置(VS放到对象末尾)

即B1、B2类共享一份A类成员,且各自保存一个指针.

该指针指向一个表(虚基表),

虚基表中保存【公共的A类成员】离【保存虚基表指针】位置的偏移量.

image.png

访问过程

如果是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类成员的位置,保存虚基表指针.

和菱形虚拟继承的模型保持一致

image.png