【C++】继承


1. 继承的介绍

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。原有类也叫父类、基类。派生类也叫子类。在这里插入图片描述

问题为什么会有继承?
有两个类(例如student和teacher),他们有相同的成员变量(name,sex,age,tele等),就会有冗余的代码,此时就可以将这些共有信息封装成一个类,然后继承这个类,以此为基础再去定义每个类的特有信息。

class Person
{
public:
	string _name;
	string _sex;
};
class Student :public Person
{
public:
	int _grade;//成绩
};
class Teacher :public Person
{
public:
	string title;//职称
};

Student和Teacher这两个子类继承了父类Person后,父类成员变成子类的一部分。
在这里插入图片描述


2. 继承的定义

在这里插入图片描述
继承方式包括public继承、protected继承和private继承,与访问限定符相同。
在不同的继承方式下,派生类继承不同的基类成员有不同的结果,如下表。

基类成员\派生类继承方式public继承protected继承private继承
public成员派生类的public成员派生类的protected成员派生类的private成员
protected成员派生类的protected成员派生类的protected成员派生类的private成员
private成员派生类中不可见派生类中不可见派生类中不可见
  1. 基类的成员的访问方式,决定了继承后在派生类的成员的访问方式的上限
  2. 基类public成员以什么方式继承,它便是什么成员;基类protected成员就算以public继承,它也只能是protected成员;基类private成员继承后不可见,并不意味着没有继承,而是继承后派生类无法使用(不管是类内还是类外)。
  3. 如果基类成员不想在类外被访问,但可以在派生类中访问,可以写成protected成员。
  4. 实际上应用最多的是public成员,protected成员和private成员在派生类中用得较多。

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

  1. 派生类可以赋值给基类对象,指针和引用。可以理解为将派生类中父类那部分切割给子类。
class Person
{
public:
	string _name;
	string _sex;
};
class Student :public Person
{
public:
	int _grade;
};
void test3()
{
	Student s;
	Person p1 = s;
	Person* p2 = &s;
	Person& p3 = s;
}

在这里插入图片描述
在这里插入图片描述

  1. 将派生类对象赋值给基类对象,不会生成临时变量。有人认为将s赋值给p1,实际上是对s进行强制类型转换。如果是强制类型转换的话,会产生临时变量,就得用const Person& p1 = s。但很显然不是,s只是将父类部分切出,拷贝给p1。

  2. 基类对象不能赋值给派生类对象。因为基类对象中没有派生类特有的成员。


4. 继承作用域和成员隐藏

  1. 基类和派生类有自己独立的作用域。这也就意味着父类和子类可以有同名变量。出现同名变量,编译器会隐藏父类的同名变量,优先考虑子类,这种情况叫做隐藏,也叫重定义。如果要访问父类的同名变量,得用域名::成员
class Person
{
public:
	string _name = "zhangsan";
	string _sex = "nan";
	int _age = 20;
};
class Student :public Person
{
public:
	void Print()
	{
		cout << "姓名:" << _name << endl;
		cout << "性别:" << _sex << endl;
		cout << "成绩:" << _grade << endl;
		//有同名变量_age,Person的_age会被隐藏,除非用(域名::同名变量)访问
		cout << "年龄:" << _age << endl;
		cout << "年龄:" << Person::_age << endl;
	}
	int _grade = 100;
	int _age = 18;
};
void test4()
{
	Student s;
	s.Print();
}

在这里插入图片描述

  1. 子类也可以隐藏父类的同名成员函数,只要子类和父类的成员函数的函数名相同就构成隐藏。
//这又一个有趣的例子,A中的fun和B中的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 test5()
{
	B b;
	b.fun(10);
};

结果
在这里插入图片描述
A中的fun和B中的fun不构成重载,前面我们提到派生类和基类的作用域是独立的,而构成重载的条件之一是在同一作用域中,所以不构成重载。由于函数名相同,所以构成重定义。

  1. 因此,最好不要在继承体系中定义同名变量。

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

class Person
{
public:
	Person(const char* name = "zhangsan")
		:_name(name)
	{
		cout << "Person()" << endl;
	}
	Person(const Person& p)
	{
		_name = p._name;
		cout << "Person(const Person& p)" << endl;
	}
	Person& operator=(const Person& p)
	{
		cout << "Person& operator=(const Person& p)" << endl;
		_name = p._name;
		return *this;
	}
	~Person()
	{
		cout << "~Person()" << endl;
	}
	string _name;
};
class Student :public Person
{
public:
	//1. 构造
	//如果基类没有默认构造,派生类的构造必须调用基类的构造;基类有默认构造,派生类构造时会自动调用
	Student(const char* name, int grade)
		:Person(name)//注意初始化顺序,先父类,再子类
		,_grade(grade)
	{
		cout << "Student()" << endl;
	}
	//2. 拷贝构造
	//子类的拷贝构造必须调用父类的拷贝构造完成父类的拷贝
	Student(const Student& s)
		:Person(s)//刚好利用了派生类和基类赋值切割,s切割出Person那部分赋值
		,_grade(s._grade)
	{
		cout << "Student(const Student& s)" << endl;
		//Person(s);
		//_grade = s._grade;
	}
	//3. =重载
	//派生类的operator=必须要调用基类的operator=完成基类的复制
	Student& operator=(const Student& s)
	{
		cout << "Student& operator=(const Student& s)" << endl;
		//函数名相同,父类函数被隐藏,必须用(域名::函数)名访问
		Person::operator=(s);
		_grade = s._grade;
		return *this;
	}
	//4. 析构
	//析构函数必须满足先析构子类对象,再析构子类中父类部分。原因有二:
	//一是因为构造子类对象前,会先构造父类部分,所以销毁时后构造的先析构;
	//二是如果先析构父类部分,接下里子类还可能访问父类成员。而先析构子类,
	//父类不可能用到子类的成员。
	~Student()
	{
		cout << "~Student()" << endl;
	}
	int _grade = 100;
};
void test6()
{
	Student s1("张三", 100); 
	Student s2(s1);
	Student s3("李四", 90);
	s1 = s3;
}

在这里插入图片描述


6. 继承中的友元与静态成员

  1. 友元关系是不能继承的。父类的友元不是子类的友元,不能调用子类的保护或者私有成员。
    在这里插入图片描述
  2. 父类定义的静态成员,在整个继承体系中不会重新定义。无论派生出多少子类,都只有一个static成员实例。
class Person
{
public:
	Person()
	{
		++_count;
	}
protected:
	string _name = "zhangsan"; // 姓名
	static int _count;//人数
};
int Person::_count = 0;
class Student : public Person
{
public:
	void Print()
	{
		cout << "目前学生人数:" << _count << endl;
	}
protected:
	int _num = 0; // 学号
};
void test7()
{
	Student s1;
	Student s2;
	Student s3;
	Student s4;
	s1.Print();
}

在这里插入图片描述


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

  1. 单继承是指一个子类只有一个父类的继承关系;而多继承是指一个子类至少有两个父类的继承关系。
    在这里插入图片描述
  2. 菱形继承也是多继承的一种。
    在这里插入图片描述
    但菱形继承有两个问题:数据冗余和二义性。
    在这里插入图片描述
class Person
{
public:
	string _name = "zhangsan";//姓名
};
class Classmate:public Person
{
protected:
	int _snum = 1;//座号
};
class Roommate :public Person
{
protected:
	int _bnum = 6;//床号
};
class Friend :public Classmate,public Roommate
{
protected:
	int _tele = 16;//电话
};
void test8()
{
	Friend f;
	//f._name;//×,不知道访问哪个_name,这就是二义性问题
	//得显示指定是哪个父类的成员,可以解决二义性问题,但不能解决数据冗余的问题
	f.Classmate::_name = "nihao";
	f.Roommate::_name = "buhao";
}
  1. 菱形虚拟继承可以同时解决数据冗余和二义性问题。在Classmate和Roommate继承Person时用虚拟继承。注意virtual的位置。
class Classmate : virtual public Person
{
protected:
	int _snum = 1;//座号
};
class Roommate : virtual public Person
{
protected:
	int _bnum = 6;//床号
};

在这里插入图片描述
这是为什么?那两个地址指向的又是什么?

  1. 菱形虚拟继承的原理

未加虚拟继承

class A
{
public:
	int _a;
};
class B :public A
{
public:
	int _b;
};
class C :public A
{
public:
	int _c;
};
class D :public B, public C
{
public:
	int _d;
};
void test9()
{
	D d;
	d.B::_a = 1;
	d._b = 2;
	d.C::_a = 10;
	d._c = 11;
	d._d = 0;
}

在这里插入图片描述
加虚拟继承后
在这里插入图片描述

  1. 实际上虚拟继承很复杂 ,上面只是简单的例子。一般不建议设计多继承,更不建议设计菱形继承。

8. 继承与组合

//这是继承
//继承关系是一种is-a的关系,例如Student is a person.
class Person
{
public:
	string _name;
};
class Student :public Person
{
public:
	int _num;
};
//这是组合
//组合关系是一种has-a的关系,例如Room has a bed.
class Bed
{
public:
	int _size;
};
class Room
{
public:
	Bed b;
	int _NumOfP;
};

问题那么继承和组合哪种更好用?
我的建议是能用组合的情况下,尽量用组合。原因如下:

  1. 在继承中,基类的内部细节对子类是可见的。这在一定程度上破坏了基类的封装,同时如果基类修改,派生类也会受到影响,派生类对基类的依赖性强,耦合度高。这种通过基类生成派生类的复用叫做“白箱复用” 。
  2. 在组合中,类通过组合其他类获得想要的功能,对其他类内部细节不可见。其他类的修改,彼此间的影响低,组合类间的依赖关系不强,耦合度低。这种复用叫做“黑色复用”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值