学习->C++篇十:继承

目录

1.什么是继承?

2.切片

3.隐藏(重定义)

4.派生类的默认成员函数

5.继承与友元

6.继承与静态成员

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

8.总结


1.什么是继承?

        利用原有的类产生新的类,新的类不仅有了原有的类的特性,还能拓展新的特性,新的类称派生类,原有的类称为基类。继承是代码复用的重要手段,继承是类设计层次的复用。
        继承使用:在类名后加上 :+ 继承方式 +基类名
例如:

继承方式有三种:public,protected,private

基类的成员访问限定符也有三种,与其组合,继承下来的成员在派生类的访问权限就有九种情况:

        情况较多,简便的记忆方式是基类的private成员在派生类中不可见,其他继承下的成员在派生类的访问权限取继承方式和基类成员访问限定符的较小权限。(public>protected>private)

        上述的不可见是指在派生类中也不可访问,但是基类的成员是继承下来的。基类的protected成员只有在基类和派生类中可以访问,可见是继承中特殊的访问限定符,可以说是为继承而生的限定符。

        因为protected和private继承都改变了基类成员在派生类成员的访问权限,较为复杂,故实际中基本使用public继承。

注:倘若不写继承方式,class默认的继承方式是private,struct则是public。

2.切片

将派生类对象(或指针、引用)直接赋值给基类对象 (或指针、引用) 会发生切片(不是类型转换),形象的理解就是切下基类继承下的成员的那片;反之,将基类对象赋值给派生类则不是切片,而是不安全的行为,因为扩大了 基类指针/引用访问空间(越界访问),编译器也会报错
例如:
int main()
{
	student s;
	person* p = &s;
	return 0;
}
图示:

3.隐藏(重定义)

         在继承体系中,基类和子类有不同的作用域。派生类的成员与基类的成员重名时,在派生类中,将屏蔽基类的成员,这个现象叫做隐藏(或重定义)。隐藏的基类成员无法直接访问,需加上基类类名 + ::来指明基类的作用域,进行访问。
例如:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>

using std::cout;
using std::cin;
using std::endl;
using std::string;

class person
{
public:
	size_t _age=20;
	string _name;
};

class student:public person
{
public:
	void setName()
	{
		_name = "zhangsan";
		person::_name = "lisi";
	}
public:
	std::string _id="00000";
	string _name;
};

int main()
{
	student s1;
	s1.setName();
	cout << s1._name << endl;
	cout << s1.person::_name << endl;
	return 0;
}

输出:

注意:如果是成员函数,当函数名相同时就构成隐藏。

例如:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>

using std::cout;
using std::cin;
using std::endl;
using std::string;

class person
{
public:
	void setAge()
	{
		_age = 20;
	}
	size_t _age=20;
};

class student:public person
{
public:
	void setAge(int age)
	{
		_age = age;
	}
public:
	std::string _id="00000";
};

int main()
{
	student s1;
	s1.setAge(15);//不显示写基类的类域则无法直接调用到基类的同名函数
	cout << s1._age << endl;
	
	s1.person::setAge();
	cout << s1._age << endl;
	return 0;
}

输出:

 隐藏与函数重载的区别是,函数重载是在同一个域中,而隐藏是在派生类域和基类域中。

因为成员同名会带来隐藏的问题,容易让人混淆,故最好不定义同名的成员。

4.派生类的默认成员函数

        前面《类和对象》中提到,类有6个默认构造函数,意味着我们不写,编译器也会自动生成。
在派生类中也类似,因为派生类继承了基类的成员,默认的成员函数会调用基类默认成员函数。
默认构造函数:派生类的默认构造函数会先调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用(否则编译报错)。
例如:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>

using std::cout;
using std::cin;
using std::endl;
using std::string;

class person
{
public:
	person(size_t age = 20)
		:_age(age)
	{
		cout << "person()" << endl;
	}
	size_t _age;
};

class student:public person
{
public:
	student(string id="0000")
		:_id(id)
	{
		cout << "student()" << endl;
	}
	std::string _id;
};

int main()
{
	student s("28937");
	return 0;
}

输出:可见,派生类的默认构造函数先调用了基类的默认构造函数完成继承下的基类成员初始化

拷贝构造函数:派生类的拷贝构造函数需要调用基类的拷贝构造完成基类的拷贝初始化,否则编译器会调用基类的默认构造函数来初始化基类继承下来的成员。
例如:

赋值重载函数:同拷贝构造函数,派生类的operator=必须要先调用基类的operator=完成基类的复制。
可以在派生类的赋值重载函数中如此调用:
    student& operator=(student& s)
	{
		person::operator=(s);//调用方法
		return *this;
	}
析构函数:
派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能
保证派生类对象先清理派生类成员再清理基类成员的顺序。
例如:
class person
{
public:
	person(size_t age = 20)
		:_age(age)
	{
		cout << "person()" << endl;
	}
	~person()
	{
		cout << "~person()" << endl;
	}
	size_t _age;
};

class student:public person
{
public:
	student(string id="0000")
		:_id(id)
	{
		cout << "student()" << endl;
	}

	~student()
	{
		cout << "~student()" << endl;
	}
	std::string _id;
};

int main()
{
	student s("28937");
	return 0;
}

输出: 

在汇编代码下,派生类的代码可以验证:

注意:因为后续在多态的场景下析构函数需要构成重写,重写的条件之一是函数名相同,所以编译器会对析构函数名进行特殊处理,将基类和派生类的函数名都处理成destructor,所以基类析构函数在不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系。

5.继承与友元

友元关系不能继承,意味着基类的友元函数并不是派生类的友元函数,不能访问派生类的私有和保护成员。
例如:下面代码将编译报错,因为基类的友元函数访问不了派生类的私有成员。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>

using std::cout;
using std::cin;
using std::endl;
using std::string;

class student;
class person
{
public:
	friend void printIdPassword(person& p,student& s);
private:
	string secret = "password";
public:
	size_t _age=20;
};

class student:public person
{
private:
	std::string _id="00000";
};

void printIdPassword(person& p,student&s)
{
	cout << p.secret << endl;
	cout << s._id << endl;
}

int main()
{
	person p;
	student s;
	printIdPassword(p,s);
	return 0;
}

6.继承与静态成员

基类定义的static成员,无论实例化多少基类和派生类对象,在整个继承体系中就只有一份实例。
例如:用一个静态成员countPerson来记录创建了基类和派生类的对象总数。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<string>

using std::cout;
using std::cin;
using std::endl;
using std::string;

class person
{
public:
	person()
	{
		++countPerson;
	}
	static int countPerson;
	size_t _age=20;
};
int person::countPerson = 0;

class student:public person
{
private:
	std::string _id="00000";
};


int main()
{
	person p;
	student s;

	printf("%p\n", &p.countPerson);
	printf("%p\n", &s.countPerson);

	cout << p.countPerson << endl;
	cout << s.countPerson << endl;

	person p1;
	person p2;
	person p3;
	person p4;
	student s1;
	student s2;

	cout << p.countPerson << endl;
	cout << s.countPerson << endl;
	return 0;
}

输出:从输出可看出基类的静态成员在继承体系中只有一份实例。

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

单继承:派生类只有一个直接父类(基类)
多继承:派生类有一个以上直接父类(基类)
菱形继承:多继承时,当派生类继承的直接父类直接或间接地继承于同一基类时,继承的图形呈现菱形。如图:

 

例如:
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using std::cout;
using std::cin;
using std::endl;

class A { public: int m_a = 1; };
class B:public A { public: int m_b = 2; };
class C:public B { public: int m_c = 3; };
class D:public B,public C { public: int m_d = 4; };

int main()
{
	D d;

	return 0;
}
D类继承了B和C类,B类和C类都继承了A类的m_a也即继承了两份m_a,会产生数据的二义性问题,还会造成数据冗余造成空间浪费。d的对象模型如图示:

 从内存中亦可验证之:

 要解决数据的二义性和数据冗余问题可以采用虚继承,即让B和C虚继承A即可。

即:

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>

using std::cout;
using std::cin;
using std::endl;

class A { public: int m_a = 1; };
class B:virtual public A { public: int m_b = 2; };
class C:virtual public A { public: int m_c = 3; };
class D:public B,public C { public: int m_d = 4; };

int main()
{
	D d;
	return 0;
}

B和C虚继承A时,将m_a存放在高地址。

B的对象模型是一个虚基表指针和m_b,m_a,虚基表存储了m_a相对B对象起始地址的偏移量。

C的对象模型是一个虚基表指针和m_c,m_a,虚基表存储了m_a相对C对象起始地址的偏移量。

而D继承了B和C,D的对象模型是B的虚基表指针,m_b,C的虚基表指针,m_c,最后是m_a。

从内存中可以验证:

这样一来D的对象模型中就只有一份m_a解决了数据二义性和数据冗余问题,通过虚基表指针来查找虚基表中的偏移量从而确定了m_a的存储位置,这样一来D对象模型中仅多存储了指针,当A对象很大时能节省空间。

8.总结

        C++中的多继承可能会导致菱形继承的问题,需用菱形虚拟继承解决,底层实现较复杂,且多继承的适用场景不多,在C++后续出现的面向对象语言都舍弃了多继承,采用了单继承,多继承可认为是C++的不足。

        继承与组合:继承即是上述所谈及的继承,组合是指对象中存储了另一个对象。

        继承可以认为是is-a的关系,B继承了A可以认为B是A,而组合是has-a的关系,B组合了A是指B对象中有A对象。

        (白箱复用)通常使用的继承(public),基类的的实现细节对派生类可见,继承一定程度破坏了基类的封装性,基类的改变对派生类影响大,派生类和基类的关系紧密,耦合度高。

        (黑箱复用)组合是类复用的另一种手段,类的内部实现细节不可见,类与类之间的依赖关系弱,耦合度低,故组合保持了类的封装性。

        故:优先使用组合而不是继承,组合的耦合度更低,代码维护性更强,但当适合继承的场景也得使用,实现多态时也必须使用继承。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值