继承(c++)

目录

1.继承概念

 1.2.继承定义

3.基类和派生类对象 赋值转换

4.继承中的作用域

5.派生类中的默认成员函数

 5.1.默认构造函数:

5.2拷贝构造函数

5.3=赋值

5.4析构函数

6.继承与友元

7.继承与静态成员

8. 菱形继承和菱形虚拟继承

8.1单继承

8.2多继承

8.3菱形继承

总结 


1.继承概念

继承机制是面向对象设计使代码可以复用的一个重要手段,可以在继承原来的类的基础上(基类、父类),产生新的类,即派生类(子类),继承呈现了面向对象程序设计的层次结构。形象的说,继承是类设计层次的复用。

之所以出现继承,很简单就是减少重复性代码,模板已经做了一部分,而继承针对的类设计上的重复,比如相同的成员、只少了一点的成员函数的类,完全可以利用继承减少重复性操作。

 1.2.继承定义

在这之前,先补充下关于类的访问限定符的访问范围知识(我关于类的文章评论区有补充,就不单开一个了)

private: 只能由该类中的函数、其友元函数访问,不能被任何其他访问,该类的对象也不能访问.
protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问
public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问
注:友元函数包括两种:设为友元的全局函数,设为友元类中的成员函数

class是普通类的关键字之一,b是派生类,public是继承方式,a是基类

继承方式:public,private,protected

关于派生类的继承:

1.基类的private成员在派生类中无论什么继承方式,都是不可见的,不可见就是在派生类中不能直接使用(比如在派生类中写了一个函数,在这个函数中用到了基类的某个private成员),最多间接使用(比如,基类里有个函数,函数里调用了基类的private成员,而这个函数是public或protected的,那派生类里可以写个函数调用基类的这个函数,或者直接派生类实例化后的对象直接调用基类的这个函数),但本质上基类private成员还是被继承到了派生类中,只是无法直接使用。

2.proteced继承适合于不想被类外的对象使用但想在派生类中使用的场景。

3.首先假设public>protected>private,那么除了基类的private成员,其他成员在派生类的访问权限==min(成员在基类的访问限定符,继承方式)

4.继承方式可以不写用默认 class b:a。class的默认继承方式是private,struct的默认继承方式是public,不过最好还是自己手动写

5.一般平时用建议:基类(成员是public/protected),派生类(继承方式:public)

因为另外两个继承方式,会使得继承下来的成员最多只能在派生类里使用,扩展维护性不强 

3.基类和派生类对象 赋值转换

1.派生类对象可以把赋值给基类的对象/引用/指针。形象的说就是把派生类中跟基类相同的成员变量赋值给基类

2.基类不能赋值给派生类(有特殊情况,看后面)

3.注意,在赋值转换中,不会产生临时变量(是语法的一个特殊处理,一般来说涉及类型转换、不同类型值比较和函数值返回,会产生临时变量,临时变量具有常性,而引用如果引用了常量必须加const修饰)

注意,引用和*的都是派生类中基类的部分,并不是引用和*派生类

叫赋值兼容转换/切割/切片

4.继承中的作用域

 1.基类和派生类都有各自独立的作用域

2.派生类和基类的同名成员,遵循就近原则,优先访问派生类。

但可以通过指定作用域的方式访问基类的同名成员

子类中 基类::基类成员 显示访问

按照定义,是说子类会隐藏父类的同名成员(屏蔽对父类同名成员的直接访问),也叫做重定义

3.实际使用中尽量不使用同名成员

4.成员函数一旦名字相同就构成隐藏(函数重载是在同一作用域,但基类和派生类是不同的作用域),返回值和参数可以不同

5.派生类中的默认成员函数

注意,模板不会继承,只能继承实例化(模板生成或自己写)的类

 5.1.默认构造函数:

派生类对象在实例化的时候,先对于继承下来的基类成员可以看做一个整体,单独调用基类的构造函数,然后再调用派生类的构造函数。(因为父类肯定比派生类先声明,而初始化列表里是根据声明顺序来进行初始化,所以必然是先调用父类的构造函数)

在我们自己写派生类的构造函数的时候,不要越俎代庖的替基类的成员初始化(会直接报错),简而言之,各干各的。

如果非要初始化,可以显示调用基类的构造函数

注意,如果不加a的构造函数(有参数)不加缺省值,必须在b的初始化列表里就进行显示调用a默认构造函数。如果有缺省值可以b的构造函数体内显示调用a的默认构造函数

5.2拷贝构造函数
 

跟构造函数类似,因为初始化列表遵循声明顺序来初始化,先调用父类的拷贝构造函数,再初始化派生类的成员

#include<iostream>
using namespace std;

class a{
public:
	a(int x = 1)
		:m(x)
	{}
	a(const a&s)
		:m(s.m)
	{}
	int m = 1;
};
class b :public a
{
public:
	b(int c)
		:k(c)
		,a(3)
	{}	
	b(const b& s)
		:k(s.k)
		,a(s)
//注意,这里就用到了前面提到的切片,赋值兼容转换
//a的拷贝构造函数引用的是b的基类成员的一个整体
	{}
	int k = 1;
};
int main() {
	b h(5);
	b q(h);
	cout << q.k << endl;
	cout << q.m << endl;
	//5
	//3
	return 0;
}

5.3=赋值

#include<iostream>
using namespace std;

class a{
public:
	a(int x = 1)
		:m(x)
	{}
	a(const a&s)
		:m(s.m)
	{}
	a& operator=(const a& s)
	{
		cout << "父类赋值" << endl;
		m = s.m;
		return *this;
	}
	int m = 1;
};
class b :public a
{
public:
	b(int c)
		:k(c)
		,a(c+1)
	{}	
	b(const b& s)
		:k(s.k)
		,a(s)
	{}
	b& operator=(const b& s)
	{
		a::operator=(s);
//注意,因为函数名相同,构成隐藏
//要加作用域调用。
//其次,必须显示调用,
//因为构造和拷贝构造要走初始化列表,按声明顺序来初始化
//但赋值和析构都不走初始化列表
//如果不显示调用,就不会自动调用基类赋值
		cout << "子类赋值" << endl;
		k = s.k;
		return *this;
	}
	int k = 1;
};
int main() {
	b h(5);
	cout << h.k << endl;
	cout << h.m << endl;
	//5
	//6
	b q(8);
	cout << q.k << endl;
	cout << q.m << endl;
	//8
	//9
	q = h;
	cout << q.k << endl;
	cout << q.m << endl;
	//5
	//6
	return 0;
}

5.4析构函数

注意,因为涉及多态,所以后面析构函数都会被编译成destrutor(),在父类析构函数不加virtual的情况下这就导致了父类和子类的析构函数同名,也就是说构成了隐藏,如果要在子类的析构函数中显示调用父类的析构函数,必须加作用域

还有,就是析构的顺序是先子后父,因为子类是可以访问继承下来的父类成员的,如果在父类已经析构的情况下(也就是说父类的那片空间被释放了,已经是非法空间了),子类的析构函数里又访问了父类成员,这就会出现非法访问(野指针等),存在了安全隐患。

#include<iostream>
using namespace std;

class a{
public:
	a(int x = 1)
		:m(x)
	{}
	a(const a&s)
		:m(s.m)
	{}
	a& operator=(const a& s)
	{
		cout << "父类赋值" << endl;
		m = s.m;
		return *this;
	}
	~a() {
		cout << "~a" << endl;
	}
	int m = 1;
};
class b :public a
{
public:
	b(int c)
		:k(c)
		,a(c+1)
	{}	
	b(const b& s)
		:k(s.k)
		,a(s)
	{}
	b& operator=(const b& s)
	{
		a::operator=(s);
		cout << "子类赋值" << endl;
		k = s.k;
		return *this;
	}
	~b() {
		a::~a();
		cout << "~b" << endl;
	}
	int k = 1;
};
int main() {
	b h(5);
	//~a
	//~b
	//~a
	return 0;
}

6.继承与友元

 友元关系不能继承,也就是说基类友元不能访问派生类的private和protected成员

友元可以参考我“类与对象”的文章

7.继承与静态成员

 如果基类定义了static静态成员,那么整个继承体系中都只有这样一个成员,因为静态成员是在静态区的,不在类里面,派生类也可以访问这个静态成员,但只是继承了访问权,但并没有生成一个新的静态成员。(跟成员函数有点不同,成员函数虽然是在函数列表,但是也是会完整继承下来)

#include<iostream>
using namespace std;

class a{
public:
	a(int x = 1)
		:m(x)
	{}
	a(const a&s)
		:m(s.m)
	{}
	a& operator=(const a& s)
	{
		m = s.m;
		return *this;
	}
	~a() {
		cout << "~a" << endl;
	}
	int m = 1;
	static int o;
};
int a:: o = 1;
class b :public a
{
public:
	b(int c)
		:k(c)
		,a(c+1)
	{}	
	b(const b& s)
		:k(s.k)
		,a(s)
	{}
	b& operator=(const b& s)
	{
		a::operator=(s);
		k = s.k;
		return *this;
	}
	~b() {
		a::~a();
	}
	int k = 1;
};
int main() {
	b h(5);
	cout << b::o << endl;
	cout << a::o << endl;
	//1
	//1
//都可以访问,访问的是同一个
	return 0;
}

8. 菱形继承和菱形虚拟继承

8.1单继承

一个派生类只有一个直接基类的继承关系,称为单继承

8.2多继承

一个派生类有2个及以上的直接基类的继承关系,称为多继承

8.3菱形继承

菱形继承会造成数据冗余(比如同样的成员,c类会有2两份一模一样的w类里的成员,但这在实际运用中是空间的浪费),二义性(比如w有个name成员,在一些地方,name应当具有唯一性,但是c类会继承2份,那name就不具有唯一性,且如果要准确访问还需要限定作用域,否则编译器会报不明确)

解决方法,便是在a类处:   class a:virtual public w,b类处: class b:virtual public w

即虚拟继承,这个时候,w类的成员只会放一份放在c类对象的最下面(内存中,不一定最下面,但可以肯定是只会放一份),而原先的被重复占用的位置,会放置指针(虚基表指针),指向空间一个位置(虚基表)(a类和b类都会有一个位置),这个位置上存放着偏移量,通过偏移量可以找到同一个变量,假如w类成员有个o,那不管是a::o,和b::o,o三个访问修改都是同一个变量。

#include<iostream>
using namespace std;

class A {
public:
	int a = 1;
};
class B :virtual public A
{
public:
	int b;
};
class C :virtual public A
{
public:
	int c;
};
class D :public B, public C
{
public:
	int d;
};
int main() {
	D d1;
	d1.B::a = 1;
	d1.C::a = 2;
	d1.b = 3;
	d1.c = 4;
	d1.d = 5;
	return 0;
}

注意03上面2行都是一个虚基表指针,04上面2行也是

B类的虚基表

第二行才存A类的成员a的偏移量

0x000000E8636FF8C8+28=0x000000E8636FF8F0

同理C类的虚基表

0x000000E8636FF8D8+18=0x000000E8636FF8F0

虚基表对于某个类来说都是一样的,比如我们实例化了多个D,那么这时候,这多个D里的虚基表指针都是一样的,指向同两个虚基表

在下面的场景中,也是虚基表指针的作用

可以看到b1和c1,都可以修改d1的a,第一个原因是我们知道派生类基类赋值转换是不产生临时变量的,也就是说,b1和c1都指向了d1的A类成员部分。

第二个原因就是b1和c1的虚基表指针指向的虚基表正是许多个D类共有的两个虚基表。

!!注意上面所谓的一份,是针对D类的,如果此时你单独把A类对象和B类对象的a都修改掉,是会呈现不同的,也就是说C类和B类各自还是会放一份A类的成员(存储结构类似刚刚分析的D),但是多继承下的c类,会只留一份继承下来,再辅以上述内容,解决菱形继承的问题。

之所以结构类似,有个原因是

	D d1;
	B *b1 = &d1;
	B* b2 = new B();
B类指针可能指向的是D类型切片,也可能是B类对象
两者虚基表不同,且偏移量不同

 

总结 

1.多继承可以用少用,菱形继承不要用。

2.多继承可以算是c++的缺陷之一。

3.继承和组合

public是一个is-a的关系(基类所有成员都会被继承),也就是说每个派生类对象都是一个基类对象的延伸,人与学生,人与老师

组合是has-a关系,如果B类组合A类,那每个B对象都有一个A对象成员,存在一部分关联,但不一定全是关联。轮胎与车,零件与成品

两者访问一个是直接进去内部成员访问,一个是借助一个对象,再访问对象里的成员

继承是一种白箱复用,基类的内部细节对子类来说是可见的。继承破坏了基类的封装,基类的改变对派生类有很大影响。依赖关系强,高耦合

组合是黑箱复用。B类不清楚A类全部内部细节,而且受到了A类内的类访问限定符限制,只有public成员会影响B类,相比继承public,低耦合,保持了每个类的封装。

尽量使用组合,耦合度低,可维护性好。有些特殊场景继承好,就用继承。实现多态也需要继承。类之间关系如可继承可组合,就优先组合。

  • 7
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值