C++进阶-->继承(inheritance)

1. 继承的概念及定义

1.1 继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要手段,他允许我们在保证原有类的特性基础上还进行扩展,通过继承产生的类叫做派生类(子类),被继承的类叫做基类(父类)。继承呈现了面向对象程序设计的层次结构。和我们之前Date类那里重载运算符的那块有异曲同工之妙,重载运算符那里的实现是函数的复用,而继承则是类设计层次的复用。

ok, 当我们想创建两个类一个是学生,一个是教师,他们都有一些共同的特点例如名字、地址、电话号码、年龄等等的时候,但他们也有不同的一些特点,例如学生有学号,老师有职称等等,我们就可以用继承来实现,创建一个父类Person储存他们的共同特点,然后再对父类进行继承出新的子类即可;

语法为:

class Person{};

class Teacher: public: Person{};

class Student: public: Person{};

当我们想实现一个Student类和Teacher类的时候会有一些相同的信息;如下代码所示:

cpp 复制代码
#include<iostream>
using namespace std;

class Student
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
	void identity()
	{
		// ...
	}
	// 学习
	void study()
	{
		// ...
	}
protected:
	string _name = "peter"; // 姓名
	string _address; // 地址
	string _tel; // 电话
	int _age = 18; // 年龄
	int _stuid; // 学号
};
class Teacher
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
	void identity()
	{
		// ...
	}
	// 授课
	void teaching()
	{
		//...
	}
protected:
	string _name = "张三"; // 姓名
	int _age = 18; // 年龄
	string _address; // 地址
	string _tel; // 电话
	string _title; // 职称
};
int main()
{
	return 0;
}

那么我们就可以用继承进行解决代码如下:

cpp 复制代码
#include<iostream>
using namespace std;

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()
	{
		// ...
	}
protected:
	int _stuid; // 学号
};

class Teacher : public Person
{
public:
	// 授课
	void teaching()
	{
		//...
	}
protected:
	string title; // 职称
};
int main()
{
	Student s;
	Teacher t;
	s.identity();
	t.identity();
	return 0;
}

这样Student 和Teacher都有Person类的成员例如name、address、tel,而有不需要我们再过多写;

首先又在这里提一嘴就是成员变量被protected修饰的话就代表他的子类或者内部类可以使用,而在类外或者派生类(子类)外都不可以使用,private则是子类和类外都不能使用;


1.2 继承的定义

定义的格式:

如下图可见:

  1. 如果基类(父类)的成员是private的话,在派生类(子类)不可以被访问,但是这些成员也一样会被继承到派生类去,只是不能访问而已。

  2. 基类(父类)的成员是protected的话,在派生类(子类)中可以被访问。而protected关键词其实就是因为继承才出现的。

  3. 继承方式,一般是按最小级别的来,级别如:public > protected > private ,如果说继承方式是public的话,基类成员是private修饰的,那么基类的成员访问限定符按最小级别来即private;如果说继承方式是private的话,无论基类成员是public还是protected,基类的访问限定符都为private。

  4. 使用关键字class时默认的继承方式是private,而struct时默认方式是public;但最好显示写出来。

  5. 一般都是用public,便于维护;


1.3 继承类模板

顾名思义继承一个类模板;如下代码和注释来看:

cpp 复制代码
#include<iostream>
#include<vector>
#include<list>
#include<deque>
#define Container std::vector
using namespace std;

namespace lwt
{


	//template<class T>
	//class vector
	//{};
	// stack和vector的关系,既符合is-a,也符合has-a
	//is-a是说stack底层实现就是vector
	//has-a是说包含关系,即vector可以被stack继承,因为stack的特性包含vector的特性
	template<class T>
	class stack : public std::vector<T>
	{
	public:
		//👇如果说我们直接调用的push_back(x);的话会出错,因为按需实例化的时候
		//push_back在本类里面找不到然后就会进到父类里面找,但都没找到,所以我们要指定一下
		//类域即vector<T>::
		void push(const T& x)
		{
			// 基类是类模板时,需要指定⼀下类域,
			// 否则编译报错:error C3861: "push_back": 找不到标识符
			// 因为stack<int>实例化时,也实例化vector<int>了
			// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
			//push_back(x);
			vector<T>::push_back(x);

		}
		void pop()
		{
			vector<T>::pop_back();
		}
		const T& top()
		{
			return vector<T>::back();
		}
		bool empty()
		{
			return vector<T>::empty();
		}
	};

	//下面是上面的一个新用法
	//我们可以使用宏在头部定义一个Container放置std::vector
	template<class T>
	class stack : public Container<T>
	{
	public:

		void push(const T& x)
		{
			vector<T>::push_back(x);
		}
		void pop()
		{
			vector<T>::pop_back();
		}
		const T& top()
		{
			return vector<T>::back();
		}
		bool empty()
		{
			return vector<T>::empty();
		}
	};

这里再来讲一下里面为什么要用vector<T>::push_back,因为模板是遵守按需实例化的规则来的,class stack : public std::vector<T>,如果我们我们实例化stack<int>,那就只实例化了vector<int>,编译器不知道我们还要实例化里面的功能,这里只是单纯实例化了一个vector<int>对象,所以我们需要指定一下vector<T>域内的push_back即可;


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

  • public继承的派生类(子类) 对象可以给基类(父类) 的对象、指针和引用赋值。但不是把派生类(子类)对象的所有成员都赋值给基类(父类)对象,而是把原先属于基类部分的成员赋值给基类对象;这个过程称为切割或者切片;很好理解,就是把派生类内不是从基类继承来的成员切割掉;

  • 基类的对象不能赋值给派生类的对象;

  • 但是如果那个基类的对象的指针原先就是指向派生类的指针的话,那还是可以通过强制类型转换赋值给派生类对象的,但是这里使用到一个dynamic_cast,这里只是演示一下;如下代码所示:

    cpp 复制代码
    #include<iostream>
    
    using namespace std;
    class Person
    {
    protected:
    	//多态后面会说
    	virtual void func()
    	{}
    public:
    	Person(const string& name, const string& sex, int age)
    		:_name(name)
    		,_sex(sex)
    		,_age(age)
    	{
    		cout << "Person(const string& name, const string& sex, int age)" << endl;
    		cout << "name:> " << _name << endl;
    		cout << "sex:> " << _sex << endl;
    		cout << "age:> " << _age << endl;
    
    	}
    	string _name; // 姓名
    	string _sex; // 性别
    	int _age; // 年龄
    };
    
    class Student :public Person
    {
    public:
    	Student(const string& name="lwt", const string& sex="man", int age=20)
    		:Person(name, sex, age)
    	{}
    
    	int _No; //学号
    };
    
    
    int main()
    {
    	Student sobj;
    
    	//子类对象可以赋值给父类对象/指针/引用
    	//这样就是上面所说的切割,即会把非父类对象的部分切割然后
    	//把是父类对象的部分赋值给父类
    	Person pobj = sobj;
    
    	Person* pp = &sobj;
    	Person& rp = sobj;
    
    	//基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针
    	//是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run - Time Type
    	//Information)的dynamic_cast 来进⾏识别后进⾏安全转换。(ps:这个我们后⾯类型转换章节再
    	//单独专⻔讲解,这⾥先提⼀下)
    	Student* sp1 = dynamic_cast<Student*>(pp);
    	cout << sp1<< endl;
    	cout << pp << endl;
    
    	pp = &pobj;
    	Student* sobj2 = dynamic_cast<Student*>(pp);
    
    	//2.父类对象不能赋值给子类对象,这里会编译报错
    	//sobj = (Student)pobj;
    
    
    	return 0;
    }

    👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇

    运行结果为:

这里需要提一点的就是,在派生类想要初始化基类的话需要调用基类的构造函数,不可以直接使用基类的成员变量在派生类的初始化链表初始化;


3. 继承的作用域

3.1 隐藏

  1. 在继承体系中,基类和派生类都有独立的作用域;

  2. 派生类和基类中如果有同名的成员的话,那么就会屏蔽掉基类的成员访问,这种就称作隐藏;那如果我们在派生类想要访问基类中被隐藏的成员我们就可以通过指定访问域进行访问例如:基类::基类成员即可;

  3. 如果是成员函数的话,构成隐藏的条件只需要一个即函数名相同,即使他们的参数不相同也不会构成重载,照样构成隐藏;如下代码所示:

cpp 复制代码
#include<iostream>
using namespace std;

class Person
{
public:
	void Fun()
	{
		cout << "PersonFunc()" << endl;
	}
	void test(int a)
	{
		cout << "Persontest()" << endl;
	}
protected:

	string _name = "小李子"; // 姓名
	int _num = 111; // 身份证号
};

//隐藏即如果基类和派生类有同名的成员的话
//基类的成员就会被隐藏,只是用派生类的成员
//如果我们想使用基类里面的话我们就需要指定类域
//即可;

//如果说有函数名相同的则会直接隐藏基类的函数,不管他们参数是否一样
//不会构成重载
//只会被隐藏掉

class Student : public Person
{
public:
	void Print()
	{
		cout << _num << endl;
		cout << Person::_num << endl;
	}
	void Func()
	{
		cout << "StudentFunc()" << endl;
	}
	void test()
	{
		cout << "Studenttest()" << endl;
	}

protected:
	int _num = 999; // 学号
};

int main()
{
	Student s1;
	s1.Print();
	s1.Func();
	s1.Person::Fun();
	//报错
	//s1.test(1);
	s1.Person::test(1);
	return 0;
}

👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇

运行结果为:


看完这个知识点我们直接就来两道面试题;如下图

cpp 复制代码
#include<iostream>
using namespace std;
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();
	return 0;
};

答案:A,A;

解析:首先上面说了,只要函数名相同就构成隐藏,不管他参数相同还是不同,都不会构成重载;所以第一题选A。

第二题:由于是构成隐藏关系,那我们想访问那个无参的func的话就必须显式调用即b.A::func();指定一定访问域,不然就报错;


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

默认的意思就是指我们不写,编译器会帮我们自动生成一个;

  1. 基类如果没有默认构造的话,那么在派生类的构造函数里需要调用基类的构造函数对基类的成员进行初始化,调用基类的构造函数就类似于匿名对象一样,但这里不叫匿名对象;如下代码所示:
cpp 复制代码
#include<iostream>
using namespace std;

class Person
{
public:
	Person(const string& name	)
		:_name(name)
	{
		cout << "Person()" << endl;
	}

protected:
	string _name;
};

class Student:public Person
{
public:

	Student(const string& name, int num)
		:Person(name)
		,_num(num)
	{
		cout << "Student()" << endl;
	}
	
protected:
	int _num;
};

int main()
{
	Student s1("lwt", 12);

	return 0;
}

👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇

运行结果为:


  1. 拷贝构造的也是一样,派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的初始化;
cpp 复制代码
#include<iostream>
using namespace std;

class Person
{
public:
	Person(const string& name	)
		:_name(name)
	{
		cout << "Person()" << endl;
	}

	Person(const Person& p)
		:_name(p._name)
	{
		cout << "Person拷贝构造调用" << endl;

	}

protected:
	string _name;
};

class Student:public Person
{
public:

	Student(const string& name, int num)
		:Person(name)
		,_num(num)
	{
		cout << "Student()" << endl;
	}

	// 严格说Student拷贝构造默认生成的就够用了
	// 如果有需要深拷贝的资源,才需要自己实现
	Student(const Student& s1)
		:Person(s1)
		,_num(s1._num)
	{
		cout << "Student拷贝构造调用" << endl;
	}

protected:
	int _num;
};

int main()
{
	Student s1("lwt", 12);
	Student s2(s1);

	return 0;
}

👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇

运行结果为:


  1. 派生类的operator=必须调用基类的operator=完成基类的赋值。但需要注意的是派生类的operator=隐藏了基类的operator=,所以要显示调用基类的operator=。
cpp 复制代码
#include<iostream>
using namespace std;

class Person
{
public:
	Person(const string& name	)
		:_name(name)
	{
		cout << "Person()" << endl;
	}

	Person(const Person& p)
		:_name(p._name)
	{
		cout << "Person拷贝构造调用" << 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 string& name, int num)
		:Person(name)
		,_num(num)
	{
		cout << "Student()" << endl;
	}

	// 严格说Student拷贝构造默认生成的就够用了
	// 如果有需要深拷贝的资源,才需要自己实现
	Student(const Student& s1)
		:Person(s1)
		,_num(s1._num)
	{
		cout << "Student拷贝构造调用" << endl;
	}
	void print()
	{
		cout << _num << endl;
		cout << _name << endl;
	}
	Student& operator=(const Student& s1)
	{
		if (this != &s1)
		{
			//父类和子类的operator=构成隐藏关系
			Person::operator=(s1);
			_num = s1._num;
		}
		return *this;
	}

protected:
	int _num;
};

int main()
{
	Student s1("lwt", 12);
	Student s2(s1);
	Student s3("lll", 15);
	cout << endl;
	s2.print();
	cout << endl;
	s2 = s3;
	s2.print();
	return 0;
}

👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇

运行结果为:

有一个很巧妙的点就在于,你看在Student内重载operator=那里的Person::operator=(s1),如果我们没有学到基类和派生类之间的转换的话我们不懂为什么Person内重载的operator的参数是Person,而在这里我们传了类型为Student的s1进去,这里就造成了切割或切片。


  1. 派生类对象初始化先调用基类构造再调用派生类构造;
cpp 复制代码
#include<iostream>
using namespace std;

class Person
{
public:
	Person(const string& name	)
		:_name(name)
	{
		cout << "Person()" << endl;
	}

	Person(const Person& p)
		:_name(p._name)
	{
		cout << "Person拷贝构造调用" << endl;

	}

protected:
	string _name;
};

class Student:public Person
{
public:

	Student(const string& name, int num)
		:Person(name)
		,_num(num)
	{
		cout << "Student()" << endl;
	}

	// 严格说Student拷贝构造默认生成的就够用了
	// 如果有需要深拷贝的资源,才需要自己实现
	Student(const Student& s1)
		:Person(s1)
		,_num(s1._num)
	{
		cout << "Student拷贝构造调用" << endl;
	}
	void print()
	{
		cout << _num << endl;
		cout << _name << endl;
	}

protected:
	int _num;
};

int main()
{
	Student s1("lwt", 12);
	return 0;
}

👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇

运行结果为:


  1. 析构函数的调用,如果说这里面没有动态申请空间的话,默认的析构函数也够用了,如果动态申请了的话我们就需要写析构函数清理申请的内存,那问题又来了,我们不仅要清理派生类里面的空间,还要清理基类内的空间,那么我们就需要写两个析构函数,但是呢问题又来了,基类的析构和派生类的析构构成隐藏,原因是无论析构函数的名字怎么样,最后都会被编译器转为一个名为destructor()的函数,所以会导致隐藏,那么我们就需要指定一下作用域;如下代码所示:
cpp 复制代码
#include<iostream>
using namespace std;
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()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name; // 姓名
};

class Student : public Person
{
public:
	Student(const char* name, int num, const char* addrss)
		:Person(name)
		, _num(num)
		, _addrss(addrss)
	{}

	// 严格说Student拷贝构造默认生成的就够用了
	// 如果有需要深拷贝的资源,才需要自己实现
	Student(const Student& s)
		:Person(s)
		, _num(s._num)
		, _addrss(s._addrss)
	{
		// 深拷贝
	}

	// 严格说Student析构默认生成的就够用了
	// 如果有需要显示释放的资源,才需要自己实现
	// 析构函数都会被特殊处理成destructor() 
	~Student()
	{
		// 子类的析构和父类析构函数也构成隐藏关系
		// 规定:不需要显示调用,子类析构函数之后,会自动调用父类析构
		// 这样保证析构顺序,先子后父,显示调用取决于实现的人,不能保证
		// 先子后父
		cout<<"~Student()"<<endl;
		Person::~Person();
		delete _ptr;
	}
protected:
	int _num = 1; //学号
	string _addrss = "翻斗花园";

	int* _ptr = new int[10];
};

int main()
{
	Student* ptr = new Student("胡图图",10,"翻斗花园");
	delete ptr;

	return 0;
}

👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇

运行结果为:

这里为什么会有两个~Person呢?原因是其实派生类的析构函数调用后会自动调用基类的析构函数,所以不需要我们写;我们可以把~Student里面的Person::~Person()删掉即可;

👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇

运行结果为:

最后总结一下:一般如果没有深拷贝的话拷贝构造和赋值运算符重载不需要我们自己写,用默认的就行,析构函数也是;然后析构函数的调用顺序是先子后父


5. 实现一个不能被继承的类

方法一:让构造函数的访问限定符为private,这样就无法调用了。

cpp 复制代码
#include<iostream>
using namespace std;

class Person
{
public:

private:
	Person()
	{

	}
};

class Student : public Person
{
public:
};
int main()
{
	Student s1;
	return 0;
}

方法二:c++11新加的关键字final,final加到基类的类名后就不能被继承了。


6. 继承和友元

友元的关系不能被继承,说明基类的友元不能访问派生类的私有保护成员;举个很简单的例子方便记忆,"父亲的朋友不是你的朋友";如下代码所示:


7. 继承与静态成员

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

cpp 复制代码
#include<iostream>
using namespace std;

class Person
{
public:
	string _name;
	static int _count;

};

int Person::_count = 0;

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

int main()
{
	Student s;
	Person p;

	//地址不一样说明已经继承了
	cout << &s._name << endl;
	cout << &p._name << endl;

	//地址一样说明只有一份
	cout << &s._count << endl;
	cout << &p._count << endl;

	return 0;
}

👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇 👇

运行结果为:


7. 多继承以及菱形继承的问题

7.1 继承模型

单继承:一个派生类只有一个基类。

多继承:一个派生类有两个或两个以上的基类,"我爸"有一个Son的基类和Dad的基类,多继承对象在内存中的模型是先继承的基类放在前面,后继承的放后面。

菱形继承:是多继承的一种特殊情况,可能祖师爷那会喝醉了没想到这个情况吧。菱形继承会导致数据冗余和二义性的问题,Assistant里面有两份Person,因为Student里面有一份Person,Teacher里面也有一份Person;

cpp 复制代码
#include<iostream>
using namespace std;

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() = "胡图图";
}

如果我们想解决这个问题的话可以指定访问指定的基类成员就可以了;


7.2 虚继承

有了多继承,菱形继承就无法避免最好不要实现出来,但我们也可以在其中做出一些操作;在两个会继承到同一个基类的派生类处加关键字virtual即可;如下代码所示:

cpp 复制代码
#include<iostream>
using namespace std;

class Person
{
public:
	string _name;
};

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

class Teacher :public virtual Person
{
protected:
	int _id;
};

class Assistant :public Student, public Teacher
{
public:
protected:
	string _majorCourse;
};

int main()
{
	Assistant a;
	a._name = "胡图图";
	//a.Student::_name = "胡图图";
	//a.Teacher::_name = "胡英俊";
	return 0;
}

这串代码就可以执行了;而这个东西的原理就是把那些继承的数据,通过虚基表和虚基指针来管理这些共享的数据,从而避免了数据的冗余;简单点理解就是放在一个公共区域共同使用。


8. 继承和组合

继承是子类继承了父类的特性,可以说子类其实就是一个特殊的父类,而组合则是将另一个类当做自己的成员变量进行使用;

继承是is-a的关系,举个例子,例如狗是一个动物,那么狗就是子类,而动物就是父类,而狗有动物的所有特性,即是有个新的类需要一个类的所有特性的时候就可以使用继承。

而组合是has-a,这里也举个例子就是汽车有一个轮子,那么我们有一个class Car{class wheels{};};,即有个类需要某些特定的功能的时候则可以使用组合。组合可以很好地保护类的安全性不破坏封装;


END!

相关推荐
Algorithm15768 分钟前
JVM是什么,与Java的关系是什么,以及JVM怎么实现的跨平台性
java·开发语言·jvm
Gnevergiveup9 分钟前
2024网鼎杯青龙组Web+Misc部分WP
开发语言·前端·python
尘佑不尘22 分钟前
shodan5,参数使用,批量查找Mongodb未授权登录,jenkins批量挖掘
数据库·笔记·mongodb·web安全·jenkins·1024程序员节
LaoZhangGong12322 分钟前
使用常数指针作为函数参数
c++·经验分享
边疆.23 分钟前
C++类和对象 (中)
c语言·开发语言·c++·算法
yy_xzz25 分钟前
QT编译报错:-1: error: cannot find -lGL
开发语言·qt
你不讲 wood28 分钟前
使用 Axios 上传大文件分片上传
开发语言·前端·javascript·node.js·html·html5
林浔090636 分钟前
C语言部分输入输出(printf函数与scanf函数,getchar与putchar详解,使用Linux ubuntu)
c语言·开发语言
SeniorMao0071 小时前
结合Intel RealSense深度相机和OpenCV来实现语义SLAM系统
1024程序员节
一颗甜苞谷1 小时前
开源一款基于 JAVA 的仓库管理系统,支持三方物流和厂内物流,包含 PDA 和 WEB 端的源码
java·开发语言·开源