【C++】继承

目录

  • 前言
  • 继承的概念和定义
    • 继承的概念
    • 继承的定义
  • 基类和派生类对象的赋值兼容转换
  • 继承中的作用域
  • 派生类的默认成员函数
    • 构造和析构
    • 拷贝构造和赋值重载
  • 继承和友元
  • 继承与静态成员
  • 菱形继承

请添加图片描述

前言

我们知道面向对象有三大特性,分别是封装继承多态
封装我们前面说过很多了,可以总结为两个层面的封装

  1. 第一种就是把数据和方法封装起来,能给外界用的定义为公有,不能给外界用的定义为私有。
  2. 第二种就是迭代器的封装,不管迭代器的底层是如何实现的,我们都可以通过typedef为iterator后,遍历获取。

而对于继承和多态,是下面两节要讲的内容。
下面就先来讲讲继承。

继承的概念和定义

继承的概念

  继承(inheritance)机制是面向对象程序设计使得代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,产生新的类,而这新的类就叫派生类,或者叫子类。原来的类就叫基类或者父类。
  所以,在C++中,一个新类(派生类/子类)从已有的类(基类/父类)获得已有的特性,这种现象就叫类的继承
  从已有的类(父类)产生一个新的子类,称为类的派生。
如下面这个例子:

#include<iostream>
using namespace std;

class Person
{
public:
	void Print()
	{
		cout << "name : " << _name << endl;
		cout << "age : " << _age << endl;
	}
protected:
	string _name = "张三";//姓名
	int _age = 20;//年龄
};

class Student : public Person
{
private:
	int _stuid;//学生学号
};

class Teacher : public Person
{
private:
	int _teaid;//老师工号
};

int main()
{
	Student s;
	Teacher t;
	s.Print();
	t.Print();

	return 0;
}

  由代码可以看出,虽然我们没有在Student类和Teacher类中写Print()这个函数,但我们依然能调用它。是因为学生类和老师类都继承了Person这个类,使得Person的成员(包括成员函数+成员变量)都会变成子类的一部分。
在这里插入图片描述
在这里插入图片描述
  通过监视窗口以及汇编可以看出,s对象和t对象中都包含了Person这个变量,而Print函数的地址也都是一样的,这就是继承的特性,对成员函数的复用。

继承的定义

  定义格式:通过上面的介绍,我们可以清楚的知道Person是父类,而Student是子类。
在这里插入图片描述

class 派生类名 : [继承方式] 基类名
{
	//.........
}

  继承方式包括:publicprotectedpublic三种,如果将继承方式省略,则系统会默认是private继承
类中的访问限定符同样也包括:publicprotectedpublic三种。

  • public:公有,自身和外界都可以访问
  • protected:保护,只有自身或者子类对象可以访问
  • private:私有,只有自身可以访问

  所以子类继承父类后,可以获得父类中所有公有和保护的的属性,也就是说,除了父类中私有的内容,其他内容子类都能访问。
对于继承基类成员访问方式的变化,可以看下面一张表:

类成员/继承方式 public继承 protected继承 private继承
基类的public成员 派生类的public成员 派生类的protected成员 派生类的private成员
基类的protected成员 派生类的protected成员 派生类的protected成员 派生类的private成员
基类的private成员 派生类中不可见 派生类中不可见 派生类中不可见

:派生类中不可见是指在派生类中不可访问的意思,但实际上基类的私有成员还是被继承到了派生类中。

在实际运用中一般都是用public继承,对private和protected继承使用较少,也不提倡使用这两种继承。

可以通过简单的写一个类来学习继承方式与访问权限:

class A
{
public:
	int _a;
protected:
	int _b;
private:
	int _c;
};

class B : public A
{
public:
	void Print()
	{
		cout << _a << endl;
		cout << _b << endl;
		cout << _c << endl;
	}
private:
	int _m;
};

在这里插入图片描述
通过报错就可以知道哪些可以访问哪些不可以访问。

基类和派生类对象的赋值兼容转换

派生类对象可以赋值给基类的对象/基类的指针/基类的引用,这也叫做切片或切割。
但是父类对象是不能赋值给子类对象的。
在这里插入图片描述

class Person
{
protected:
	string _name = "张三";
	string _sex = "male";
	int _age = 20;
};
class Student:public Person
{
private:
	int _class;
};
void test1()
{
	Student s;
	//子类对象可以直接赋值给父类对象/指针/引用
	Person p = s;
	Person* ptr = &s;
	Person& ref = s;
}

三者的结果都是一样的:
在这里插入图片描述
  如果对s或者是p或者是ptr或者是ref中的一个的值进行改变,同样会影响到其他,因为这里指向的都是同一部分。
如:

//前提是成员变量是公有才行,这里为了演示就放成了公有。
ptr->_name += "aaaaaaaaaaa";

在这里插入图片描述
  当然,它与类型转换不一样,类型转换如从浮点型转到整型,在此期间会产生临时变量,而切片则不会有此发生,所以效率很高,还是非常好用的。

继承中的作用域

  通过前面的学习,我们知道总共有四个域,分别是:局部域全局域命名空间域以及类域这些域会影响语言编译查找规则,首先回到局部域找,再到全局域找,默认是不会到命名空间域找的,除非将其展开,而影响生命周期的就只有局部域和全局域。

  • 在继承体系中基类和派生类也都有独立的作用域。
  • 如果子类和父类中有同名成员或函数,子类成员会屏蔽父类的同名成员或函数,先访问子类成员或函数,这叫隐藏
class Person
{
protected:
	string _name = "张三";
	string _sex = "male";
	int _age = 20;
	int _num = 10000;
};
class Student:public Person
{
public:
	void Print()
	{
		cout << "name:" << _name << endl;
		cout << "num:" << _num << endl;
	}
private:
	int _class;
	int _num = 9999;
};

  如,Person类和Student类中都有_num这个成员,此时在调用时,构成隐藏,会调用子类的中成员,如果我想访问父类成员中的_num,就需要再访问时加上类域:

void Print()
{
	cout << "name:" << _name << endl;
	cout << "Student num:" << _num << endl;
	cout << "Person num:" << Person::_num << endl;
}

在这里插入图片描述
此时就能很好的访问父类中的_num变量了。
有两点2需要注意的是:

  1. 在继承中,只要命名相同,不管参数和返回值,都会构成隐藏
  2. 所以在继承体系中最好不要定义同名的成员变量或函数。

派生类的默认成员函数

构造和析构

  默认成员函数我们也很熟悉了,就是我们不写编译器会自动帮我们生成的函数。
  同样,父类和子类也是类,在我们没有写的时候编译器也会主动帮我们写,但是由于子类时通过父类继承下来的,因此在进行相关操作时要为父类考虑。
我们首先来看对于构造和析构的一个顺序:

class Person
{
public:
	Person()
	{
		cout << "Person()" << endl;
	}
	~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student:public Person
{
public:
	Student()
	{
		cout << "Student()" << endl;
	}
	~Student()
	{
		cout << "~Student()" << endl;
	}
};

int main()
{
	Student s;
	return 0;
}

在这里插入图片描述

  由此可以看出了子类和父类在构造和析构时顺序的一种关系,虽然我创建的是子类对象,但是编译器会先帮我调用父类的构造,在调用子类构造,析构时则相反,先析构子类在析构父类,如下图所示:
在这里插入图片描述
  在创建子类对象前,先创建父类对象,即调用父类的默认构造,如果父类没有默认构造则会报错。那此时就要在子类初始化列表中声明:

class Person
{
public:
	Person(const char* name)
		:_name(name)
	{
		cout << "Person(const char* name)" << endl;
	}
protected:
	string _name;
};
class Student:public Person
{
public:
	Student(const char* name = " ",int stuid = 1111)
		:Person(name)
		,_stuid(stuid)
	{
		cout << "Student(const char* name = " ",int stuid = 1111)" << endl;
	}
protected:
	int _stuid;//学号
};

int main()
{
	Student s("zhangsan", 1234);
	return 0;
}

规定不能直接用类似于_name(name)直接用成员变量来初始化,而应直接调用父类构造,当然对于声明顺序默认父类先声明。
注意:

  • 在构造时,不能先子后父,因为子类构造初始化可能会用到父类成员,没有初始化父,父类成员就是随机值
  • 在析构时,不能先父后子,因为子类析构时可能会用到父类成员,先父后子,就可能出现问题,子类析构结束后会自动调用父类析构。

拷贝构造和赋值重载

  对于拷贝构造和赋值重载,子类和父类的函数名都是一样的,此时就构成了隐藏,就不会调用父类的拷贝构造和赋值重载,因此我们要写子类时显示调用父类的拷贝构造和赋值重载。

class Person
{
public:
	Person()
	{
		cout << "Person()" << endl;
	}
	Person(const char* name)
		:_name(name)
	{
		cout << "Person(const char* name)" << 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;
		if (this != &p)
		{
			_name = p._name;
		}
		return *this;
	}
	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name;
};
class Student:public Person
{
public:
	Student(const char* name = " ",int stuid = 1111)
		:Person(name)
		,_stuid(stuid)
	{
		cout << "Student(const char* name = " ",int stuid = 1111)" << endl;
	}
	Student(const Student& st)
		:Person(st)//赋值兼容转换
		,_stuid(st._stuid)
	{
		cout << "Student(const Student& st)" << endl;
	}
	Student& operator=(const Student& st)	
	{
		cout << "Student& operator=(const Student& st)" << endl;
		if (this != &st)
		{
			Person::operator=(st);
			_stuid = st._stuid;
		}
		return *this;
	}
	~Student()
	{
		cout << "~Student()" << endl;
	}
protected:
	int _stuid;//学号
};

int main()
{
	Student s("zhangsan", 1234);
	Student s1(s);
	cout << "--------------------------------------" << endl;
	Student s2;
	s2 = s;
	return 0;
}

在这里插入图片描述
  由结果可以看出,无论调用的是子类的拷贝构造还是赋值重载,都能把父类的给调用上。

继承和友元

  友元关系不能继承,也就是说父类友元不能访问子类中的私有和保护成员。
  也很容易理解,就像我爸可以知道我的银行卡密码,但是我爸的朋友是不能知道的。

class Student;//在Person类中使用了Student,所以要先声明
class Person
{
public:
	friend void Print(const Person& p, const Student& st);
protected:
	string _name = "张三";
};

class Student :public Person
{
	friend void Print(const Person& p, const Student& st);
protected:
	int _stuid = 1234;
};

void Print(const Person& p, const Student& st)
{
	cout << p._name << endl;
	cout << st._stuid << endl;
}
int main()
{
	Person p;
	Student s;
	Print(p, s);
	return 0;
}

在这里插入图片描述
  如果我想让其可以访问,则应该在子类中也声明友元,承认他是我的朋友:

//子类中声明:
friend void Print(const Person& p, const Student& st);

继承与静态成员

  在父类中定义了static静态成员,则整个继承体系中只有一个这样的成员。静态成员变量在静态区中,往往只有程序结束时才会销毁它,因此只要父类中存在一个静态成员变量,子类都可以使用它。

class Person
{
public:
	Person() { ++_count; }
protected:
	string _name; // 姓名
public:
	static int _count; // 统计人的个数。
};
int Person::_count = 0;

class Student : public Person
{
protected:
	int _stuNum; // 学号
};

int main()
{
	Person p;
	Student s;

	cout << &Person::_count << endl;
	cout << &Student::_count << endl;

	return 0;
}

由结果可以看出,二者的地址是一样的,也就是二者使用的同一个count。
在这里插入图片描述

菱形继承

对于继承可以分为单继承多继承,前面我们所讲到的都是单继承。

  • 单继承一个子类只由一个父类直接继承时,这就是单继承
  • 多继承一个子类由两个或两个以上的父类直接继承,就叫多继承

而菱形继承就是多继承的一种特殊情况。
可以通过一个图来看:
在这里插入图片描述
  可以看出助理Assistant是由老师和学生继承来的,则这个助理在不同时期扮演不同角色,既可以是学生,也可以是老师。
但这时就会出现一些问题:菱形继承会有数据冗余和二义性的问题。
在这里插入图片描述
  可以看出,Assistant类中包含了两个_name,这就是二义性,在调用时我会不知道要调用哪个里面的_name。

class Person
{
public:
	string _name; // 姓名
};

class Student :  public Person
{
protected:
	int _num; //学号
};

class Teacher : public Person
{
protected:
	int _id; // 职工编号
};

class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

int main()
{
	// 数据冗余和二义性
	Assistant a;
	cout << a._name << endl;
	return 0;
}

这样就会出现问题:

error C2385: 对“_name”的访问不明确

此时可以指定访问哪个父类的成员可以解决二义性问题

a.Student::_name = "小李";
a.Teacher::_name = "李老师";

当然,我们还可以用虚拟继承来解决菱形继承的二义性和数据冗余的问题
虚拟继承也可以叫虚继承,只要在会导致数据冗余的两个类后面继承时加上virtual即可。

class Student : virtual public Person

class Teacher : virtual public Person

注意:虚继承不要在其他地方使用,且建议不要设计出菱形继承。


感谢大家观看,如果大家喜欢,希望大家一键三连支持一下,如有表述不正确,也欢迎大家批评指正。
请添加图片描述

上一篇:跨平台OFD、PDF文档预览UTS插件


下一篇:HCIA(DHCP服务)