C++ 继承 8000字精讲

在这里插入图片描述

如果我们这里有两个类,第一个类是学生类,第二个类是老师类,他们都有各自的姓名、年龄、性别、爱好等信息,也有各自特有的学号、工号、职务、学历,所以为了应对这种各种类中有重复的情况,C++出现了继承这个概念,设计一个Person类,类中是学生、老师类共有的信息,这其实是一种设计层面的复用思想
在这里插入图片描述

定义格式

由上例,Person类我们称为父类、基类,Student Teacher类我们称为子类、派生类
在这里插入图片描述
关于继承方式这里会涉及到public继承、protected继承、private继承,继承方式会影响到父类的public、protected、private成员变量、函数,在派生类中是作为public、protected、private中哪一种,父类的三种访问限定符以及三种继承方式,就会涉及到九种情况,听着十分复杂,但在绝大多数情况下无脑使用public继承这种方式即可,并且父类不要设计private,而是使用protected,我们这里是为了讲解,所以大体的聊一聊所有情况
在这里插入图片描述
这张表格的意思我挑两个个来说一下,对于基类是public的成员,我们以public继承方式,在派生类中,就是public的成员,能够被外部访问到,对于基类是protected成员,我们以private继承方式,在派生类中,就是private的成员,不能被外部访问
这张表硬记会很麻烦,我们规律的来讲:基类的private成员在子类中都不可见(语法上就限定了无法访问),基类的其他成员在子类的访问方式(谁的成员):
访问方式 == min(成员在基类的访问限定符, 继承方式),public > protected > private
比如基类是public,protected继承方式,那么取min,该成员在派生类中就是protected成员

外部就可以通过创建子类,来使用父类的变量和函数(当然还需要根据访问限定符和继承方式来确定父类的变量和函数是否有条件被外部使用)

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

可以把派生类对象赋值给基类对象,但是不能把基类对象赋值给派生类对象
在这里插入图片描述
这个转换过程和其他不同类型的赋值,是不会产生临时变量的
在这里插入图片描述
在这个赋值转换的过程中发生了截断/切片,也就是说把_grade数据给丢弃了,只要Person有的变量(子类和基类的成员函数都放在公共代码区了,都只有一份)

关于不会产生临时变量的证明:
在这里插入图片描述
我们是利用引用的性质,允许权限缩小、平移,不允许权限放大的特性,来进行验证
对于普通变量32和35行,把i给到d时会产生临时变量,临时变量具有常性,所以左边需要是const double&类型,但是我们对于Person的p和Student的s,在43行Person&类型下,是可以赋值过去的,所以这个过程是没有产生临时变量的,同时42行因为没有涉及到引用,所以没有权限问题,更能够存储

继承的作用域

在继承体系中基类和派生类都有独立的作用域
子类和父类中有同名的成员时,子类会屏蔽父类的同名成员的直接访问,这种情况我们称为隐藏,也叫重定义,在子类中直接访问到的是子类的变量,只有通过父类的域作用限定符才能够访问到父类

class A
{
public:
	A(int a = 0, int b = 5)
		:_a(a)
		,_b(b)
	{}
	int _a;
	int _b;
};
class B : public A
{
public:
	B(int a = 1)
		:_a(a)
	{}
	int _a;
	void fun()
	{
		cout << _a << endl;// 10
		cout << _b << endl;// 5
		cout << A::_a << endl;// 0
	}
};

void test3()
{
	B b(10);
	b.fun();
}

默认时找变量/函数的顺序:局部域->类域->父类->命名空间->全局域,还找不到就报错
10:局部域fun()中没找到,在类域中找到了,所以输出类域B中的10
5 : 局部域、类域中都没找到,然后在父类中找到了,输出5
0:直接去A中寻找,找到输出0

这里再贴一个同名函数的隐藏例子,以供理解
在这里插入图片描述
这里还需要注意的是,对于成员函数的隐藏,只需要满足函数名相同即可

// B中的fun和A中的fun不是构成重载,因为不是在同一作用域
// B中的fun和A中的fun构成隐藏,成员函数满足函数名相同就构成隐藏。
class A
{
public:
	void fun()
	{
		cout << "func()" << endl;
	}
};
class B : public A
{
public:
	 void fun(int i)
 	{
 		A::fun();
 		cout << "func(int i)->" <<i<<endl;
 	}
};
void Test()
{
	 B b;
 	b.fun(10);
};

不过隐藏这个概念在实际中用的比较少,重名这个事儿,还是尽量避免

派生类的默认构造函数

在这里插入图片描述

构造函数

派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用

class A
{
public:
	A(int a, int b)
		:_a(a)
		, _b(b)
	{}
	int _a;
	int _b;
};
class B : public A
{
public:
	B(int c = 1)
		:A(1, 2)//显示调用
		,_c(c)
	{}
	int _c;
};
void Test4()
{
	B b;
 }

创建b对象的时候先进入B类的构造函数,然后进入初始化列表,进入A的构造(如果有默认构造,使用默认构造的话,也会进入A的构造),然后回到B的构造函数执行完后面的代码,所以也就是先初始化父类,再初始化子类(声明的顺序是初始化顺序,父类子类的声明关系,总是先声明父,再声明子,所以我们宏观的认为先调用A的构造,再调用B的构造)
回顾默认构造:如果程序员没有显示的实现构造函数,编译器就会自动实现一个默认构造函数,如果我们实现的构造函数有参数没有缺省值,也就意味着,在创建对象的时候,必须给没有缺省值的参数一个值,不然编译器会给报错

子类的构造函数中初始化列表不能显式初始化父类的成员变量,这是C++规定的,可以理解为虽然是父子类关系,但是该干的干的事儿还是自己干
在这里插入图片描述

如果没有默认构造,并且参数列表中没有显示调用会直接报错
在这里插入图片描述
如果创建子类对象时想对父类成员变量进行初始化操作,那么应该:

class A
{
public:
	A(int a = 10, int b = 20)
		:_a(a)
		, _b(b)
	{}
	int _a;
	int _b;
};
class B : public A
{
public:
	B(A a = A(), int c = 30)
		:A(a)
		,_c(c)
	{}
	int _c;
};
void Test4()
{
	B b(A(1, 2), 3);
	B b();
 }

拷贝构造

拷贝构造和构造函数很类似,都是在参数列表中,首先调用父类的拷贝构造,然后再执行后序代码,两个点需要注意,第一点:调用父类的拷贝构造,其实也就是给父类传了一个子类参数,那么传参时会发生截断,也就是切片的操作。第二点:如果参数列表中没有显示调用父类拷贝构造,那么会走父类的默认构造函数,只有显示调用父类拷贝构造,才会走父类拷贝构造,所以对于拷贝构造,我们强烈建议显示调用父类的拷贝构造,不然就会走父类的默认构造函数,导致错误

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1, char c = 'a')
		:_year(year), _month(month), _day(day), _c(c)
	{}
	Date(const Date& d)
		:_year(d._year), _month(d._month), _day(d._day), _c(d._c)
	{}

	int _year;
	int _month;
	int _day;
private:
	char _c;
};
class day_time :public Date
{
public:
	day_time(int h = 1, int min = 30)
		:_h(h)
		, _min(min)
	{}
	day_time(const day_time& dt)
		//Date(dt)//自动调用Date的默认构造函数,注意是调用的默认构造函数,不是调用的Date的拷贝构造函数
		:_h(dt._h)
		, _min(dt._min)
	{}
	int _h = 1;
	int _min = 30;
};

void test5()
{
	day_time dt1;
	day_time dt2(dt1);
}

赋值重载

派生类的operator=必须要调用基类的operator=完成基类的复制,这个调用必须是显示调用,必须由程序员进行,程序员不干,它什么也不会做

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1, char c = 'a')
		:_year(year), _month(month), _day(day), _c(c)
	{}
	Date& operator=(const Date& dy)
	{
		if (this != &dy)
		{
			_year = dy._year;
			_month = dy._month;
			_day = dy._day;
			_c = dy._c;
		}
		return *this;
	}
	int _year;
	int _month;
	int _day;
private:
	char _c;
};
class day_time :public Date
{
public:
	day_time(int h = 1, int min = 30)
		:_h(h)
		, _min(min)
	{}
	day_time& operator=(const day_time& dy)
	{
		if (this != &dy)//从地址的角度防止相等  这&dy是取地址
		{
			Date::operator=(dy);//显示调用父类赋值重载,同理于拷贝构造,这里也会发生截断
			_h = dy._h;
			_min = dy._min;
		}
		return *this;
	}
	int _h = 1;
	int _min = 30;
};

析构函数

析构函数编译器会自动调用父类的析构函数,我们程序员不需要处理,其调用顺序为:子类析构-> 父类析构,这个顺序的解释有二:
一:子类中有可能还会访问父类成员,但父类不会使用子类成员,所以先把子类析构了,此时子类就不能够调用到父类,但反过来,先析构父类,子类再访问父类成员,就可能会报错,编译器为了避免我们错误的析构顺序,就直接自动帮我们析构了
二:建立栈帧的顺序,构造时先构造父类,压进栈帧,再构造子类,压进栈帧,所以先析构上层,再析构下层

如果程序员在子类的析构中手动调用了父类的析构,父类会析构两次,所以如果父类中有堆空间的申请,那么多次析构更是会让程序崩溃
所以在子类的析构中,只需要完成对子类的处理

验证:
在这里插入图片描述
在这里插入图片描述
这里父类析构了一次,子类析构了两次,一次是我们手动调用的,一次是编译器自动调用的,

取地址重载

取地址重载我这里弱化说明,有兴趣自行写程序测试,对于取地址重载,如果子类没有实现自己的取地址重载,那么会调用父类的取地址重载,如果子类有取地址重载,那么编译器会优先调用子类的取地址重载,不会去调用父类的取地址重载,二择一,又子类则子类优先,无子类则父类

总结

综上所述,构造函数、拷贝构造函数、析构函数、赋值重载、两个取地址重载,六个默认成员函数中的构造函数、拷贝构造、赋值重载是需要我们手动调用父类对应默认成员函数的,其中拷贝构造、赋值重载几乎所有场景都是需要我们手动调用的,构造函数看需求是否需要在初始化的时候对父类进行手动初始化,其他的默认成员函数程序员不需要处理

继承和友元

这里只需要记得一个点,友元函数无法继承
在这里插入图片描述
_stuNum是子类的成员变量,Display不认识,只能给Student中再添加友元
在这里插入图片描述

在这里插入图片描述

继承与静态成员

静态成员可以继承,但是在子类中不会拷贝一份新的,而是共用同一个静态成员,一般用于数据共享
这个特性具体而言,比如可以用于统计父类和派生类对象个数

class Date
{
public:
	Date(int year = 1900, int month = 1, int day = 1, char c = 'a')
		:_year(year), _month(month), _day(day), _c(c)
	{
		count++;
	}
	static int count;//这里是声明,是模具,还没有具体的count变量
	int _year;
	int _month;
	int _day;
private:
	char _c;
};
class day_time :public Date
{
public:
	day_time(Date d = Date(), int h = 1, int min = 30)
		: Date(d)
		, _h(h)
		, _min(min)
	{}
	int _h = 1;
	int _min = 30;
};
int Date::count = 0;// 定义
void test4()
{
	Date d1;
	Date d2(1900, 2, 28);
	day_time dt;
	cout << Date::count << endl;// 3
}

父类构造函数中用一个静态成员count来++,因为不管创建子类还是父类的对象,都会调用父类的构造,所以就能够记录父类和子类总共创建对象的个数

多继承和菱形继承

多继承和菱形继承这里我打算另开一片博客进行讲解

继承和组合

在这里插入图片描述
继承和组合都是一种复用形式,不过两者有较大的差异
继承:程序员能够根据基类的实现,在public继承方式下使用父类的public protect限定的成员变量、函数,这种复用方式称为白箱复用,也就是说父类的内部细节对子类是可见的,派生类和基类的耦合性较强。
组合:组合是在一个类中定义另一个类的对象,然后使用这个类对外开放的接口,是一种黑箱复用,也就是对象的内部细节不可见,两个类之间的依赖关系较弱。

从暴露程度上来看,比如一个类,有100个成员函数,20个共有,70个保护,10个私有,那么继承能够得到90%的成员函数,而组合能够得到20%的成员函数

什么时候用继承,什么时候用组合,是看需求,但大多数情况下,有一个说法:符合is-a就用继承 符合has-a就用组合
is-a是:植物 - 花的关系 - 子类是一种父类
has-a是:轮胎 - 车的关系 - 一个类是另一个类的一部分

边角料

class Base1
{
public:
	int _b1 = 1;
};
class Base2
{
public:
	int _b2 = 1;
};
class Kid : public Base1, public Base2
{
public:
	int _k = 3;
};
void test7()
{
	Kid k;
	Base1* p1 = &k;
	Base2* p2 = &k;
	Kid* p3 = &k;
}

这里p1和p2拿到&k,是发生了截断,我这里想问的是p1 p2 p3的大小关系(== != > <)
注意到定义Kid时,是class Kid : public Base1, public Base2,先写了public Base1,再写了public Base2,我们其实通过交换书写的先后顺序,也就能够知道,这里的先后会影响到在子类k中是先继承Base1还是先继承Base2
在这里插入图片描述
在这里插入图片描述
也就是说k的首地址,就是继承时,第一个书写,第一个继承的类的首地址,所以对于test7()中即为:p1 == p3 != p2
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

失去梦想的小草

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值