【C++】继承

继承

1、继承的概念和定义

1.1 继承的概念

继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法,这样产生的新的类称为派生类。继承展现了面向对象程序设计的层次结构体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用

c 复制代码
class Person
{
public:
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}
protected:
	string _name = "张三"; // 姓名
	string _address; // 地址
	string _tel; // 电话
	int _age = 18; // 年龄
};

class Student : public Person
{
public:
	// 学习
	void study()
	{
		// ...
		//_name = "xxx";
	}

protected:
	int _stuid; // 学号
};

class Teacher : public Person
{
public:
	// 授课
	void teaching()
	{
		//...
	}

protected:
	string title; // 职称
};

int main()
{
	Student s;

	Teacher t;
	s.identity();  //输出:void identity() 张三
	t.identity();  //输出:void identity() 张三

	return 0;
}

上面的代码中有三个类,因为Student和Teacher中有部分内容相同,所以把相同的部分写到Person类中,Student和Teacher都继承Person,父类Person中的成员都会变成字类的一部分,所以可以复用这些成员,不需要重复定义了。

Student除了继承父类的成员外有自己特有的_stuid(学号),Teacher除了继承父类的成员外有自己特有的title(职称)

1.2 继承的定义

1.2.1 定义格式

下面看到的Person为基类,也称为父类,Student是派生类,也称为子类

1.2.2 继承方式

  • public继承
  • protected继承
  • private继承
类成员 / 继承方式 public 继承 protected 继承 private 继承
基类的 public 成员 派生类的 public 成员 派生类的 protected 成员 派生类的 private 成员
基类的 protected 成员 派生类的 protected 成员 派生类的 protected 成员 派生类的 private 成员
基类的 private 成员 在派生类中不可见 在派生类中不可见 在派生类中不可见
  • 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但语法上限制派生类对象不管在累里面还是类外面都不能去访问它。
  • 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
  • 基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected >private。
  • 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
  • 在实际运用中⼀般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用
    protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

1.3 继承类模板

c 复制代码
namespace Func
{
	//template<class T>
	//class vector
	//{};
	// stack和vector的关系,既符合is-a,也符合has-a
	template<class T>
	class stack : public vector<T>
	{
	public:
		void push(const T& x)
		{
			// 基类是类模板时,需要指定一下类域,
			// 否则编译报错:error C3861: "push_back": 找不到标识符
			//stack 继承的是模板类 vector<T>,不是普通类。
			//编译器无法自动识别 push_back 来自基类模板,必须写 vector<T>:: 告诉编译器。
			vector<T>::push_back(x);
			//push_back(x);
		}

		void pop()
		{
			vector<T>::pop_back();
		}

		const T& top()
		{
			return vector<T>::back();
		}

		bool empty()
		{
			return vector<T>::empty();
		}
	};
}

int main()
{
	bit::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	}
	return 0;
}

2、基类和派生类之间的转换

通常情况下把一个类型的对象赋值给另一个类型的指针或引用时,存在类型转换,中间会产生临时对象,需要加const,例如:

c 复制代码
int a = 1;
const double& d = a;  // 编译器自动生成一个临时变量来存储a的值,然后d引用这个临时变量,临时变量具有常性,不能修改其值,因此d是一个常引用,需要加上const修饰

string s1 = "11111";
const string& s2 = "11111";

public继承中是一个特殊处理的意外:

  • 派生类对象 可以赋值给基类的指针基类的引用,而不需要加const
  • 基类对象不能赋值给派生类对象
  • 派生类对象赋值给基类对象是通过基类的拷贝构造函数或者赋值重载函数完成的,这个过程被叫做切片

eg:

c 复制代码
// 基类 Person:存3个成员 _name _sex _age
class Person
{
protected:
    string _name;
    string _sex;
    int _age;
};

// 子类 Student:继承Person,有自己独有的 _No(学号)
class Student : public Person
{
public:
    int _No; // 只有Student才有学号,Person没有
};

int main()
{
 Student sobj ;
 //sobj内存为【Person整块:_name _sex _age】+【Student独有:_No】
 
 // 1.派生类对象可以赋值给基类的指针/引用
 Person* pp = &sobj;
 Person& rp = sobj;
 
 Person pobj = sobj;  //这里发生对象切片
 //pobj是独立的Person对象,内存只有Person大小,只能装下_name _sex _age
 //调用拷贝构造时,把子类独有的_No学号切除丢弃,pobj中不存在_No成员
 
 //2.基类对象不能赋值给派生类对象,这里会编译报错
 sobj = pobj;
 
 return 0;
}

赋值过程的核心 相当于将子类的父类部分 切割出来赋值给父类的对象 /父类的指针 /父类的引用

3、继承中的作用域

  • 在继承体系中基类和派生类都有独立的作用域
  • 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。
    (在派生类成员函数中,可以使用基类::基类成员显示访问)

eg:

c 复制代码
class Person
{
protected:
	string _name = "小李子"; // 姓名
	int _num = 111; // 身份证号
};

class Student : public Person
{
public:
	void Print()
	{
		cout << " 姓名:" << _name << endl;
		cout <<" 学号:"<< _num << endl;  //输出999 就近原则,访问的是Student的_num,而不是Person的_num
		cout <<" 身份证号:"<< Person::_num << endl;  //输出111
	}
protected:
	int _num = 999; // 学号
};

int main()
{
	Student s1;
	s1.Print();

	return 0;
};

输出:

姓名:小李子

学号:999

身份证号:111

  • 如果是成员函数的隐蔽,只需要函数名相同就构成隐蔽

eg:

c 复制代码
//A中和B中的两个func函数是什么关系?
//不是重载关系,因为重载关系要求在同一个作用域中,而A和B是两个不同的类,func函数分别在不同的作用域中,所以不是重载关系
//是隐藏关系,B中的func函数隐藏了A中的func函数,只要函数名相同,参数列表不同,就构成隐藏关系,B中的func函数隐藏了A中的func函数
class A
{
public:
	void fun()
	{
		cout << "func()" << endl;
	}
};

class B : public A
{
public:
	void fun(int i)
	{
		cout << "func(int i)" << i << endl;
	}
};

int main()
{
	B b;
	b.fun(10);  //正常运行
	b.fun(); // 编译报错,因为B中的fun函数隐藏了A中的fun函数,所以B中没有无参的fun函数,所以编译报错
  b.A::fun()  //如果想调用A中的fun函数,可以使用作用域限定符A::fun()来调用
	return 0;
};

输出结果:

func(int i)10

func()

子类只要定义了和父类名字一模一样 的成员函数,子类的函数就会隐藏父类所有同名函数;子类对象直接调用时只能看见子类版本,访问父类版本必须加父类名::函数名

注意:在实际中在继承体系里最好不要定义同名的成员

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

6个默认成员函数,默认的意思就是指我们不写,编译器会变我们自动生成⼀个,那么在派生类中,这几个成员函数是如何生成的呢?

4.1 默认构造函数

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

c 复制代码
class Person
{
public:
  //默认构造函数
	Person(const char* name)
		: _name(name)
	{
		cout << "Person()" << endl;
	}

protected:
	string _name; // 姓名
};

class Student : public Person
{
public:
	//默认构造函数
	Student(const char* name, int num, const char* address)
		: Person(name)  //初始化基类,必须显式调用基类的构造函数初始化,把基类当成一个整体
		,_num(num)
		,_address(address)  //先初始化Person,再初始化num,最后初始化address
 {}
 
protected:
	int _num; //学号
	string _address;
};
  1. 创建子类对象时,子类包含一整块父类成员_name,必须先初始化父类部分
  2. 不能直接在初始化列表对父类的成员进行初始化,即在子类初始化列表中_name(name)是不允许的,只能整体初始化,例如Person(name)
  3. 创建对象是必须先把父类的一整块数据初始化完,才能处理子类的_num、_address,所以必须调用基类构造

4.2 拷贝构造函数

派生类拷贝构造必须调用基类拷贝构造完成拷贝初始化

c 复制代码
class Person
{
public:
  //默认构造函数
	Person(const char* name)
		: _name(name)
	{
		cout << "Person()" << endl;
	}

	//拷贝构造函数
	Person(const Person& p)
		: _name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}

protected:
	string _name; // 姓名
};

class Student : public Person
{
public:
	//默认构造函数
	Student(const char* name, int num, const char* address)
		: Person(name)
		,_num(num)
		,_address(address)
		
	//拷贝构造函数	
	Student(const Student& s)
		:Person(s)
		,_num(s._num)
		,_address(s._address)
	{}
	
protected:
	int _num; //学号
	string _address;
};

int main()
{
	Student s1("张三", 1, "北京");
	Student s2(s1);  //使用基类的拷贝构造函数

	return 0;
}
  1. Student s2(s1); 触发子类拷贝构造
  2. Person(s) 调用基类拷贝构造,复制基类的_name;
  3. 复制派生类自有成员_num、_address;
  4. 如果不写Person(s),编译器会调用基类的默认构造函数

4.3 赋值运算符重载函数

派生类的operator=必须要调用基类的operator=完成基类的复制

c 复制代码
class Person
{
public:
  //默认构造函数
	Person(const char* name)
		: _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;
		if (this != &p)
			_name = p._name;
		
		return *this;
	}
		
protected:
	string _name; // 姓名
};

class Student : public Person
{
public:
	//默认构造函数
	Student(const char* name, int num, const char* address)
		: Person(name)
		,_num(num)
		,_address(address)
		
	//拷贝构造函数	
	Student(const Student& s)
		:Person(s)
		,_num(s._num)
		,_address(s._address)
	{}
	
	//赋值运算符重载
	Student& operator=(const Student& s)  //与父类的赋值运算符重载,构成隐藏
	{
		if (this != &s)
		{
			Person::operator=(s);  //不加Person::,编译器会认为是调用Student的operator=,会造成死循环
			_num = s._num;
			_address = s._address;
		}
	
		return *this;
	}
	
protected:
	int _num; //学号
	string _address;
};

int main()
{
	Student s1("张三", 1, "北京");
	Student s2(s1);  //使用基类的拷贝构造函数

	Student s3("李四", 2, "北京");
	s1 = s3;  //调用赋值运算符重载
	
	return 0;
}

注意 :派生类的赋值运算符重载函数中想调用基类的赋值运算符重载函数需要指定类域,例如Person::operator=(s);,不指定类域会产生无穷递归

4.4 析构函数

派生类对象析构清理先调用派生类析构在调基类析构

c 复制代码
class Person
{
public:
  //默认构造函数
	Person(const char* name)
		: _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;
		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 num, const char* address)
		: Person(name)
		,_num(num)
		,_address(address)
		
	//拷贝构造函数	
	Student(const Student& s)
		:Person(s)
		,_num(s._num)
		,_address(s._address)
	{}
	
	//赋值运算符重载
	Student& operator=(const Student& s)  //与父类的赋值运算符重载,构成隐藏
	{
		if (this != &s)
		{
			Person::operator=(s);  //不加Person::,编译器会认为是调用Student的operator=,会造成死循环
			_num = s._num;
			_address = s._address;
		}
	
		return *this;
	}
	
	//析构函数
	//派生类和基类析构构成隐藏关系
	~Student() 
	{
		cout << "~Student()" << endl;
		//Person::~Person();  //注意作用域,显示调用会导致析构两次的问题
		//派生类析构调用后,会自动调用父类析构,所以自己实现析构时不需要显示调用	
		//自动调用父类析构, 才能保证先子后父的析构顺序,所以不用显示调用
	}
	
protected:
	int _num; //学号
	string _address;
};

int main()
{
	Student s1("张三", 1, "北京");
	Student s2(s1);  //使用基类的拷贝构造函数

	Student s3("李四", 2, "北京");
	s1 = s3;  //调用赋值运算符重载
	
	return 0;
}

注意:对于析构函数,为了保证从父类先析构,子类后析构的顺序,编译器会自动调用父类的析构函数,所以无需显示调用,即使显示调用,编译器还是会自动调用父类的析构函数,导致子类中父类部分的资源被释放了两次,会产生问题。

5、继承与友元

友元关系不能继承,基类友元不能访问派生类私有保护成员

c 复制代码
//友元关系不能继承
// 前置声明
class Student; // 前置声明,告诉编译器Student是一个类,后续会定义

class Person
{
public:
	friend void Display(const Person& p, const Student& s);
protected:
	string _name; // 姓名
};

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

void Display(const Person& p, const Student& s)
{
	cout << p._name << endl;
	cout << s._stuNum << endl;  //错误,不是子类的友元,不能在函数中访问子类的成员
}

int main()
{
	Person p;
	Student s;

	Display(p, s);

	return 0;
}

Display函数只是父类Person的友元函数,不是子类Student的友元函数,所以在Display中只能调用父类的成员变量,不能调用子类的成员变量

6、继承与静态成员变量

基类定义了static静态成员,则整个继承体系里只有一个这样的成员,无论派生出多少个派生类,都只有⼀个static成员实例

c 复制代码
class Person
{
public:
	string _name;
	static int _count;
};

int Person::_count = 0;

class Student : public Person
{
protected:
	int _stuNum;
};

int main()
{
	Person p;
	Student s;

	// 这里的运行结果可以看到非静态成员_name的地址是不一样的
	// 说明派生类继承下来了,父派生类对象各有一份
	cout << &p._name << endl;
	cout << &s._name << endl;

	cout << &p._count << endl;
	cout << &s._count << endl;

	// 公有的情况下,父派生类指定类域都可以访问静态成员
	cout << Person::_count << endl;
	cout << Student::_count << endl;

	return 0;
}

输出结果:

000000070DFDFA18

000000070DFDFA58

00007FF7EECF1520

00007FF7EECF1520

0

0

7、复杂的菱形继承及菱形虚拟继承

7.1 继承模型

单继承 :一个子类只有一个直接父类时称这个继承关系为单继承

多继承 :一个子类有两个或两个以上的直接父类时称这个继承关系为多继承

菱形继承:菱形继承是多继承的一种特殊情况

菱形继承存在的问题:从下面的对象成员模型构造可以看出菱形继承有数据冗余和二义性的问题,即在Assistant的对象中Person成员变量会有两份

c 复制代码
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;
	a._name = "peter";

	return 0;
}

这段代码中各个类的关系如图

复制代码
       Person
      /     \
 Student     Teacher
      \     /
     Assistant

运行结果为对"_name"的访问不明确,这就是二义性问题,可以通过指定类域去访问

cpp 复制代码
int main()
{
	//编译报错:error C2385: 对"_name"的访问不明确
	Assistant a;
	//a._name = "peter";
	
	// 需要显示指定访问哪个基类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";

	return 0;
}

上面这种方法解决了二义性问题,但冗余的问题依然存在,因此需要引入虚拟继承来解决

7.2虚拟继承

虚拟继承可以解决菱形继承的二义性和数据冗余的问题

如上面的继承关系,在Student和Teacher继承Person时使用虚拟继承,在继承处加virtual关键字就可以解决问题

cpp 复制代码
class Person
{
public:
	string _name; // 姓名
};

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

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

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

int main()
{
	Assistant a;
	a._name = "peter";

	return 0;
}

使用虚拟继承后_name赋值直接作用到父类Person

原理 :在内存中,本来Person在类Student和类Teacher中都有一份,现在虚拟继承将Person类直接放到类Assistant的一块空间中。再给类Student和类Teacher设置一个Person类的位置偏移量值,方便这两个类访问Person类

7.3 多继承中的指针偏移问题

cpp 复制代码
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{
	Derive d;
	Base1* p1 = &d;
	Base2* p2 = &d;
	Derive* p3 = &d;

	return 0;
}

p1、p2、p3的关系是什么

Derive中继承了Base1和Base2,在Derive的内存中,p3指向了整个内存空间的开头,同样p1也指向内存空间的开头,p2则指向空间中存储Base2的开头,所以三者关系p1 == p3 != p2

如果文章中有错误或不足,欢迎大家指正交流。

相关推荐
栈溢出了1 小时前
Java Lambda 表达式笔记
java·开发语言·intellij-idea
会周易的程序员1 小时前
使用 pybind11 封装 C++ 日志库 microLog 为 Python 模块
java·c++·python·物联网·架构·日志·aiot
WZF-Sang2 小时前
网络基础——2
服务器·网络·c++·学习·网络编程·php
zhixingheyi_tian2 小时前
一份GC日志的解读
java·开发语言
czhaii2 小时前
串口1中断收发-C语言-MODBUS协议
c语言·开发语言
apihz2 小时前
随机驾考题目(C 照科一 / 科四 2000+ 题)免费API调用教程
android·java·c语言·开发语言·网络协议·tcp/ip
matlab代码2 小时前
基于matlab人脸疲劳驾驶检测系统(可使用其它人脸视频)【源码39期】
开发语言·matlab·人脸疲劳检测
-西门吹雪3 小时前
现代C++ 并发编程-学习指南
java·jvm·c++
zenithdev13 小时前
fastcache:为 Go 设计的低 GC 压力内存缓存
开发语言·其他·缓存·golang