C++中的父继子承:继承方式实现栈及同名隐藏和函数重载的本质区别, 派生类的4个默认成员函数
🎬 胖咕噜的稞达鸭:个人主页
学习完本文,你将知道:(各位大佬预知答案几何请移步文章结尾!)
1. 当子类继承了父类,父类的私有成员在子类中是不可见的,所以父类的私有成员在子类中有没有被继承下来?
2. 子类对象一定比父类大?
3. 函数重载和函数隐藏的区别是什么?同名了有什么影响?
4. 派生类构造函数初始化列表的位置必须显式调用基类的构造函数,已完成基类部分成员的初始化?
5. 派生类构造函数先初始化子类成员,再初始化基类成员?派生类对象构造函数先调用子类构造函数,在调用基类构造函数?
接着来步入今天的正文:
面向对象三大特性:封装,继承,多态
我们之前学过了封装,类的定义是一个封装,迭代器实现也是一个封装,屏蔽了底层的实现细节。模板的使用也是一个封装。接下来讲解面向对象第二大特性:继承。
继承的定义:
假设大学学生和大学的老师,作为一个人的共性,都有姓名,住址和电话号码,但是不同的是,老师授课有职称,学生有学号,这是老师和学生不同的地方。所以我们可以将姓名,住址和电话号码封装在一个大类Person
里面,将职称和学号分别封装在一个小的类Student
,teacher
里面。下面来实现代码:
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(){// ...}
protected:int _stuid;// 学号
};
class Teacher : public Person
{
public:// 授课void teaching(){//...}
protected:string title;// 职称};
int main()
{Student s;Teacher t;
}
定义格式:
-
父类的
private
成员在子类中无论以什么方式继承都是不可见的。这里的不可见是指父类的私有成员还是被继承到了子类对象中,但是语法上限制子类对象不管在类外面还是类里面都不能去访问它。
比如说:上方代码中,age在父类中被定义为私有成员,想要在子类中访问私有成员,代码会出现报错,所以父类的private
在子类中不可见。 -
父类的
private
成员在子类中是不能被访问的,如果父类成员不想在类外直接被访问,但是需要在子类中能访问,就定义为protected
。可见保护成员限定符是因为继承才出现的。 -
总结:父类的私有成员在子类中是不可见的,父类的其他成员在子类的访问方式==Min(成员在父类的访问限定符,继承方式),public>protected>private。
-
在实践中,一般都是
public
继承,很少使用protected/private
继承。 -
使用class时默认的继承方式是private,使用struct时默认的继承方式是public
继承的类模板
之前用容器适配器实现了一个栈,在这里我们也可以用继承的类模板去实现一个栈。
如果在push成员函数中不指定类域会怎么样?
主函数中实例化了stack,同时也就实例化了vector,Keda::stack<int>st
;只会实例化栈的构造函数,这里构造函数编译器自动产生了,所以在stack<int>
实例化的时候,同时也实例化了vector <T>
,将vector
实例化为int
类型了,vector<int>
,模板需要按需实例化,如果在void push(const T& x)
中写push_back(x)
会报错,push_back
会向上找父类,没有找到就会报错,所以父类是类模板的时候,需要指定一下类域,否则编译器会报错,“push_back找不到标识符”。将push_back(x)
改为vector<int>::push_back(x)
.
这里也就涉及模板的按需实例化。
也即是说在测试中没有写栈的删除测试,而在成员变量中没有在pop()
下面指定类域,反而不会报错。如果对删除进行测试,但是没有指定类域,就会出现问题。编译器用到了那个地方,就会调用哪个地方。
#include<vector>
using namespace std;
namespace Keda
{template<class T>class stack :public std::vector<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();}};
}int main()
{Keda::stack<int>st;st.push(1);st.push(2);while (!st.empty()){cout << st.top() << " ";st.pop();}return 0;
}
用继承的方式实现一个栈,实现vector的栈,实现list的栈,实现deque的栈,我们可以用宏来进行替换。宏的原理就是一种替换,预处理之后就没有
CONTAINER
了,预处理之后就直接是相对应的栈了。想要实现vector就是vector,list就是list,deque就是deuqe.,不会存在CONTAINER。
//#define CONTAINER std::vector
//#define CONTAINER std::list
#define CONTAINER std::deque#include<vector>
#include<list>
#include<deque>
using namespace std;
namespace Keda
{template<class T>class stack :public CONTAINER<T>{public:void push(const T& x){CONTAINER<T>::push_back(x);}void pop(){CONTAINER<T>::pop_back();}const T& top(){return CONTAINER<T>::back();}bool empty(){return CONTAINER<T>::empty();}};}int main()
{Keda::stack<int>st;st.push(1);st.push(2);while (!st.empty()){cout << st.top() << " ";st.pop();}return 0;
}
父类和子类对象赋值兼容转换
- 前提是公有继承条件下,把子类对象中的父类对象的一部分切割出来拷贝给父类对象,也可以给父类的指针或者引用,引用会变成子类当中父类对象一部分的别名,也叫切片或者切割。切出来让父类去引用。
- 父类对象是不能赋值给子类对象的。
- 父类的指针或者解引用可以通过强制类型转换赋值给子类的指针或者引用,但是必须是父类的指针是指向子类对象时才是安全的。
继承中的作用域
#include<iostream>
#include<string>
using namespace std;class Person
{
protected:string _name = "Keda";//姓名int _num = 111;};
class Student:public Person
{
public:void Print(){cout << _num << endl;//999//cout << Person::_num << endl;//111}
protected:int _num = 999;
};
int main()
{Student s;s.Print();return 0;
}
上面代码会打印出999,而不是111,为什么?
如果子类和父类中有同名成员,这段代码中是_num,在Person中有,在Student中也是有的,子类成员将屏蔽父类对同名成员的直接访问,这种情况就叫隐藏。那如何打印出111,那就需要在子类中加:cout<<Person::_num<<endl
.本质影响的是编译时的查找规则。
函数重载,要求在同一作用域,同一个域里面不能有同名的变量和函数,不同的域里面可以有不同的变量和函数,list中有Push_back,vector中也有push_back,不会互相影响。因为他们在不同的域里面。
同名隐藏:如果是成员函数的隐藏,只需要函数名相同就构成隐藏,不管参数,因为在不同的作用域。如果想调就需要指定作用域。
所以尽量不要定义同名成员!!!
来一道常考的选择题:
class A
{
public:void fun(){cout << "func()" << endl;}
};
class B : public A
{
public:void fun(int i){cout << "func(int i)" << i << endl;//func(int i)10}
};
int main()
{B b;b.fun(10);b.fun();return 0;
};
问题:
1.题目中AB的关系是什么?
隐藏关系,在同一个类中,出现同名函数就是同名隐藏,此时派生类会隐藏基类。
2.编译结果?
在类B指定一个对象b,用对象b去调func函数,初始化为10,结果打印出来是func(int i)10
:
因为基类被隐藏了,编译器调用的是派生类中的。
b.fun()
会报错:这里想要去调用的是基类中的func(),由于被隐藏了,掉不出来就会报错,所以最好指定类域:修改成b.A::func();
,程序才不会报错!!!
子类的默认成员函数
- 子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员。如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显示调用
什么意思?
类的默认生成的构造函数的行为:
1.内置类型,默认构造函数不会对其进行初始化,其值是不确定的;
2.对于自定义类型:默认构造函数会调用该自定义类型的默认构造函数来进行初始化;
3.对于继承自父类的成员,默认构造函数会将父类成员视为一个整体,调用父类的默认构造 函数来进行初始化。
Student(const char* name, int num,const char* address): Person(name), _num(num), _address(address)
{}
- 拷贝构造时,对于内置类型会去调用值拷贝,对于自定义类型调用自定义类型的拷贝构造,对于父类调用父类的拷贝构造。父类调用父类的构造。
严格说student拷贝构造默认生成的就够用了。如果有深拷贝的资源才需要自己实现。
Student(const Student& s): Person(s)//把子类对象传给父类的引用, _num(s._num)
{cout << "Student(const Student& s)" << endl;
}
赋值兼容规则:
在这一块子类对象要传给父类对象的引用,调用父类的拷贝构造,就要传父类的对象过去,但是在父类中只有_num,没有这个对象,所以我们传过去它也会自然切的。这里就是赋值兼容规则的体现。
- 那么我们要是在父类的初始化中写上Person(const char* name =
- _name(name)
{
cout << “Person()” << endl;
}
在拷贝构造的时候初始化列表不写Person(s)
,是达不到拷贝构造的目标的,因为在传递的时候没有进行对于父类调用父类的拷贝构造。
"xxxxxx"
)- 子类的Operator=必须要调用父类的operator=完成父类的赋值。需要注意的是子类的operator=隐藏了父类的operator=,需要指定父类作用域。
严格说student赋值默认生成的就够用了。如果有深拷贝的资源才需要自己实现。
Student& operator = (const Student& s)
{cout << "Student& operator= (const Student& s)" << endl;if (this != &s){// 构成隐藏,所以需要显示调用Person::operator =(s);//这里要是不写Person::会报错!!!_num = s._num;}return *this;
}
父类和子类的operator赋值会构成同名隐藏,所以要在operator=(s)前加Person::,指定类域。
- 析构,严格来说,子类析构默认生成的就够用了,自定义类型会调用自己的析构,父类可以看作一个特殊的自定义类型进行析构。
父类中已经有了析构,在子类中继续析构有问题:析构函数都会被特殊处理成destructor(),所以在子类中的析构~Student()和 ~Person()会被处理成destructor(),这就构成了隐藏,所以子类的析构和父类的析构也构成隐藏关系。
规定: 不需要显示调用,子类析构之后,会自动调用父类进行析构。这样就保证了析构顺序,先子后父,显示调用取决于实现的人,不能保证。基类先初始化,派生类再初始化,析构的时候先析构派生类,接着析构基类。
后定义的需要先析构,
5. 子类的初始化先调用父类构造再调子类构造;
6. 子类对象析构清理先调用子类析构再调父类的析构;子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员,因为这样才可以保证子类对象先清理子类成员再清理父类成员的顺序。
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){}Student(const Student& s): Person(s)//把子类对象传给父类的引用, _num(s._num){cout << "Student(const Student& s)" << endl;}Student& operator = (const Student& s){cout << "Student& operator= (const Student& s)" << endl;if (this != &s){// 构成隐藏,所以需要显示调用Person::operator =(s);_num = s._num;}return *this;}~Student(){//~Person();}
protected:int _num=1;//学号
};
int main()
{Student s1();//Student s2(s1);//Student s3("rose", 17);//s1 = s3;return 0;
}
总结这一部分:构造,拷贝构造,赋值重载都要显示调用父类,但是析构不需要,因为析构顺序是先子后父,先定义的后析构
问答答案揭晓:
1. 答:私有的成员继承下来了,但是在子类中不可见。基类私有成员不能直接访问不是没有被继承,而是权限问题
2. 答:不一定,有可能子类只是改写父类的方法而已,并没有增加其自身的数据成员,则大小一样,故错误
3. 答:函数重载:在同一个类里面,函数必须是同名函数,但是函数参数类型不同,且成员变量不能同名,即使类型不同
函数隐藏:在不同类中使用同名函数,在基类和派生类中都使用同名,但是参数不同,派生类中会隐藏基类中同名的函数名
4. 如果父类有默认构造函数,此时就不需要
5. 顺序相反,先初始化父类,再是子类,在派生类对象构造时,先调用基类构造函数,后调用子类构造函数