【C++进阶】继承上 概念及其定义 赋值兼容转换 子类默认成员函数的详解分析

🔥个人主页:爱和冰阔乐

📚专栏传送门:《数据结构与算法》C++

🐶学习方向:C++方向学习爱好者

⭐人生格言:得知坦然 ,失之淡然


🏠博主简介

文章目录


前言

在C++初阶我们学习了面向对象的经典三大特性(封装,继承,多态)之一的封装,今天我们便走进继承,感受其中的奥妙


一、继承的概念及定义

1.1继承的概念

定义:继承(inheritance)机制是⾯向对象程序设计使代码可以复⽤的最重要的⼿段,它允许我们在保持原有类特性的基础上进⾏扩展(类层次的复用),增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类/子类。继承呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复⽤,继承是类设计层次的复⽤

简单来说,原有的类是父类,继承产生的类是子类,儿子站在父亲的肩膀上------先用别人(父类)做好的基础,再在上面添加自己的东西

下⾯我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址/电话/年龄等成员变量,都有identity⾝份认证的成员函数,设计到两个类⾥⾯就是冗余的。当然他们也有⼀些不同的成员变量和函数,⽐如⽼师独有成员变量是职称,学⽣的独有成员变量是学号;学⽣的独有成员函数是学习,⽼师的独有成员函数是授课

cpp 复制代码
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; // 职称 
};

在学生类和老师类中的身份认证,姓名等是相同的,那么可以把这些公共的成员均放到一个语义上更大的人类中(Person类中),学生和老师都属于人类,我们让student和Person均继承person,这样我们就不需要重复定义了,减少冗余

cpp 复制代码
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;
}

这里便就体现出继承了,我们在原有的Person上进行复用与扩展得到了子类------student和teacher类,在student类中并没有identity这个类,但是学生和老师均可以使用,这便是复用

1.2 继承的定义

定义格式:

下⾯我们看到Person是基类,也称作⽗类。Student是派⽣类,也称作⼦类。(因为翻译的原因,所以既叫基类/派⽣类,也叫⽗类/⼦类)



继承基类成员访问⽅式的变化

在父类中用public/protected/private修饰的成员通过不同方式的继承方式会在子类中变成不同的成员,如下9种情况:

  1. 基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指基类的私有成员还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问它

  2. 基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。

  3. 实际上⾯的表格我们进⾏⼀下总结会发现,基类的私有成员在派⽣类都是不可⻅。基类的其他成员在派⽣类的访问⽅式==Min(成员在基类的访问限定符,继承⽅式),public > protected>private(取成员访问限定符和继承方式中最小的权限)

  4. 使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显⽰的写出继承⽅式

  5. 在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实际中扩展维护性不强

这里我们需要注意,在父类中的private成员在子类中存在(但是不可见),但是子类不可以直接使用,这里我们把age私有化

cpp 复制代码
class Person
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证 
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}
protected:
	string _name = "张三"; // 姓名 
	string _address; // 地址 
	string _tel; // 电话 
private:
	int _age = 18; // 年龄 
};
class Student : public Person
{
public:
	// 学习 
	void study()
	{
		// ...
		cout << _age << endl;
	}
	

protected:
	int _stuid; // 学号 
};

经过调试发现,_age在学生类中存在,但是我们访问使用便会报错

类里面都无法访问age,更别提类外面了

虽然私有的不可以直接使用,但是我们可以间接使用,我们父类中调用该成员即可,如在Person类的identity函数中调用age,该函数是共有的,那么子类学生类便可以使用该函数,因此相当于子类间接访问了age

cpp 复制代码
class Person
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证 
	void identity()
	{
		cout << "void identity()" << _name << endl;

		cout << _age << endl;
	}
protected:
	string _name = "张三"; // 姓名 
	string _address; // 地址 
	string _tel; // 电话 
private:
	int _age = 18; // 年龄 
};
int main()
{
	Student s;

	Teacher t;
	s.identity();
	t.identity();
	return 0;
}

1.3 继承类模板

在前面我们学习实现栈使用的是适配器模式,在这里我们可以使用继承,通过栈来继承vector,如下:

cpp 复制代码
namespace hxx
{
	//template<class T>
	//class vector
	//{};
	// stack和vector的关系,既符合is-a,也符合has-a 
	template<class T>
	class stack : public std::vector<T>
	{
	public:
		void push(const T& x)
		{
			// 基类是类模板时,需要指定⼀下类域, 
			// 否则编译报错:error C3861: "push_back": 找不到标识符 
			// 因为stack<int>实例化时,也实例化vector<int>了(stack继承vector)
     // 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到 (在stack和vector<?>中均找不到则报错,)
     //因此需要指定其类域,具体可见gitee源码详解
			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()
{
	hxx::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	}
	return 0;
}

二、父类与子类对象赋值兼容转换

  1. public继承的派⽣类对象可以赋值给基类的指针/基类的引⽤。这⾥有个形象的说法叫切⽚或者切割。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分

  2. 基类对象不能赋值给派⽣类对象

  3. 基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run-Time TypeInformation)的dynamic_cast 来进⾏识别后进⾏安全转换。(ps:这个我们后⾯类型转换章节再单独专⻔讲解,这⾥先提⼀下)

下面我们通过示例来感受下:

cpp 复制代码
class Person
{
protected:
	string _name; // 姓名 
	string _sex; // 性别 
	int _age; // 年龄 
};




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


int main()
{
	Student sobj;
	// 1.派⽣类对象可以赋值给基类的指针/引⽤ 
	Person* pp = &sobj;
	Person& rp = sobj;

	
	Person pobj = sobj;

	//2.基类对象不能赋值给派⽣类对象,这⾥会编译报错 
	//sobj = pobj;

	return 0;
}

因此可以得出基类对象不能赋值给派生类,这里为派生类对象可以赋值给基类的对象是通过调用后面介绍的基类拷贝构造实现的(这里没有发生类型转换,即没有产生临时变量)

三、继承中的作用域

  1. 在继承体系中父类和子类都有独立的作用域(可以理解父类和子类是不同的类,有着独立的类域)
  2. 子类和父类中有同名成员,子类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏。(在子类成员函数中,可以使⽤父类::父类成员显⽰访问)
cpp 复制代码
class Person
{
protected:
    string _name = "小李子";//姓名
    int _num = 111;//身份证

};

class Student :public Person
{
public:
    void Print()
    {
        cout << _num << endl;
        cout
    }
protected:
    //在子类中便有了两个叫num的变量,父类的继承下来了,
    int _num = 999;

};

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

    return 0;
}

在main函数中调用了Print函数,那么打印的是父类num的111,还是子类的999?我们得出是999

那么想访问父类的则如下:

  1. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏

下面我们来看一道有趣的题目:

A和B的两个func构成了:重载 / 隐藏 / 没关系的哪一个

cpp 复制代码
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);
    return 0;
};

乍眼一看,函数名相同,参数不同,如果你秒选构成了重载,那很遗憾的和你说,答案是隐藏,为什么?

因为函数重载要求在同一个作用域中,而继承的父类和子类是两个不同的类域。在该小点中提到继承中成员函数只需函数名相同便是隐藏

这里我们还需要注意一点,在main函数中调用b.func(),是会报错的,因为调用fun是先在B(子类)搜索,在子类的fun参数不匹配,但其不会在父类中去查找,因为隐藏了父类,因此如果想调用必须指定作用域

  1. 注意在实际中在继承体系⾥⾯最好不要定义同名的成员。

四、子类的默认成员函数

4.1 4个常见默认成员函数

在前面类和对象中我们讲到了6个默认成员函数,默认的意思就是指我们不写,编译器会帮我们自动生成一个,那么在子类中,这几个成员函数是如何生成的?

4.2 构造

1. 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤

下面我们依旧通过student类和Person类感受下默认生成的构造函数的行为,其分为3部分:

1.内置类型不确定

2.自定义类型调用其的默认构造

3.继承父类成员看做一个整体对象,要求调用父类的默认构造

cpp 复制代码
class Person
{
public:
	Person(const char* name = "peter")
		: _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:
protected:

	int num;//学号
	string _addrss;


};

int main()
{
	Student s;

	return 0;
}

那么在Person类中没有默认构造,因此我们需要在子类构造函数的初始化列表阶段显⽰调⽤,如下

4.3 拷贝构造

2. 子类的拷⻉构造函数必须调⽤父类的拷⻉构造完成父类的拷⻉初始化

这里的拷贝构造和第一点构造函数一样,也是三种情况:

1.内置类型是值拷贝

2.自定义类型调用其的拷贝构造函数

3.父类会调用其父类的拷贝构造

在上面的Person和Student类中,严格来说,Student生成的拷贝构造够用了,但是如果在该类中有需要深拷贝的资源,才需自己实现拷贝构造

因此我们便可得出,子类的构造需要自己写,拷贝构造/赋值/析构一般都不需要自己写(三者是一体化的),那么如果需要手动写拷贝构造,就需要解决如何拷贝父类的一部分,把父类当成一个整体的对象,会显示调用父类的拷贝构造,但是在Student类中没有Person类的对象,我们就可以通过前面介绍的父类与子类的兼容转换来解决

cpp 复制代码
Student(const Student& s)
	:_num(s._num)
	,_addrss(s._addrss)
	,Person(s)
{
	 //深拷贝
}

4.4 赋值重载

派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域(这里和拷贝构造类似)

那么如果需要我们显示写赋值重载该如何实现?这里我们需要注意的是如果显示调用父类的operator=,因为子类的隐藏了父类的operator

cpp 复制代码
	//赋值重载
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
		//父类和子类的operator构成隐藏关系
			Person::operator=(s);
			_num = s._num;
			_addrss = s._addrss;
		}

		return *this;
	}

4.5 析构

同理,这里的析构也和上面的拷贝/赋值重载类似,严格来说子类生成的析构就够用了,如果有需要显示示范法的资源,才需要自己实现

cpp 复制代码
//析构
~Student()
{
	~Person();
}

我们发现很奇怪:为什么调不动父类的析构?这里就要提到多态的知识了:

因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系。

简单来说就是编译器将父类和子类的析构函数名都处理为destructor(),即构成了隐藏关系,因此还需显示指定作用域

cpp 复制代码
//析构 
~Student()
{
  //子类的析构和父类的析构函数构成隐藏关系
	Person:: ~Person();
}
int main()
{

	Student s1("张三",1,"南京市");

	Student s2(s1);

	Student s3("李四", 2, "成都");
	s1 = s3;
	return 0;
}

咦?这里我们发现值创建了三个对象,结果调用了6次析构,这样会导致delete多次调用析构函数,会出问题,因此在这里析构不需要显示调用

cpp 复制代码
	//析构 
	~Student()
	{ 
	//子类的析构和父类的析构函数构成隐藏关系
	//规定:不需要显示调用,子类析构函数之后,会自动调用父类析构
	//这样就可以保证析构的顺序,先子后父
		//Person:: ~Person();
	}

对象构造时先是构造父类,后是子类,析构时,后定义的需要先析构,这也便是我们不显示写析构的原因,保证析构是先子后父,显示调用则不能保证是先子后父,如下:

cpp 复制代码
	~Student()
	{
		Person::Person();
		delete _ptr;
	}

总结

坚持到这里,已经很棒啦,希望读完本文可以帮读者大大更好了父类与子类之间的关系!!!如果喜欢本文的可以给博主点点免费的攒攒,你们的支持就是我前进的动力🎆

资源分享:继承源码

相关推荐
余辉zmh3 小时前
【C++篇】:LogStorm——基于多设计模式下的同步&异步高性能日志库项目
开发语言·c++·设计模式
艾莉丝努力练剑3 小时前
【C++STL :list类 (二) 】list vs vector:终极对决与迭代器深度解析 && 揭秘list迭代器的陷阱与精髓
linux·开发语言·数据结构·c++·list
寒冬没有雪3 小时前
矩阵的翻转与旋转
c++·算法·矩阵
星竹晨L4 小时前
【C++】深入理解list底层:list的模拟实现
开发语言·c++
什么半岛铁盒5 小时前
C++11 多线程与并发编程
c语言·开发语言·c++
Mr_WangAndy8 小时前
C++设计模式_结构型模式_组合模式Composite(树形模式)
c++·设计模式·组合模式
·心猿意码·13 小时前
C++右值语义解析
开发语言·c++
小龙报13 小时前
《彻底理解C语言指针全攻略(2)》
c语言·开发语言·c++·visualstudio·github·学习方法
zzzsde14 小时前
【c++】深入理解string类(4)
开发语言·c++