1. 继承的简单介绍
1.1 继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行扩展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称派生类。
继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。
简单来说,被继承的类叫做父类(或基类),继承自父类的类叫做子类(或派生类)。
子类拥有父类的所有成员,在此基础之上可以对父类进行拓展。
1.2 子类的定义方式
class 子类名 : 访问限定符 父类名
{// 拓展内容
}
通常来说,父类和子类具有类别上的包含关系。
例如,老师和同学不仅具有人的基本特点,还在人的基础之上有了自己的拓展,而老师和同学都属于人。
我们在用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(){// ...}
protected:int _stuid; // 学号
};
老师类(子类):
class Teacher : public Person
{
public:// 授课void teaching(){//...}
protected:string title; // 职称
};
1.3 继承方式
访问限定符限定的是继承的方式,不同访问限定符下的继承方式如下:
类成员 / 继承方式 | public | protected | private |
父类的public成员 | 子类的public成员 | 子类的protected成员 | 子类的private成员 |
父类的protected成员 | 子类的protected成员 | 子类的protected成员 | 子类的private成员 |
父类的private成员 | 子类无法直接显式访问 | 子类无法直接显式访问 | 子类无法直接显式访问 |
子类成员的访问权限:public > protected > private > 父类中被修饰为private
其中,protected访问限定符是伴随着继承的出现而出现的。被它修饰的成员,意味着无法在类外部进行访问,而可以在类内部或其子类中被访问。
无论子类以何种方式继承,在父类中被修饰为private的成员子类都不可直接显式访问。
继承方式用于限定继承下来的成员访问权限不能高于继承方式。
当不指定继承方式时,class子类默认为private方式继承,struct子类默认为public方式继承。
在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类类里使用,实际中扩展维护性不强。
可以采用protected/private继承的一个例子:
#include<vector>
#define CONTAINER std::vectortemplate<class T>
class stack : private 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()
{stack<int> st;st.push(1);st.push(2);st.push(3);while (!st.empty()){cout << st.top() << " ";st.pop();} return 0;
}
这里采用了继承vector的方式来实现stack,但我希望用户只用stack的接口而不直接访问vector的成员函数,此时就可以采取protected/private的继承方式。
1.4 继承类模板
相信细心的小伙伴已经发现了,在上面stack的例子中,我们每次调用vector的接口时都对其类域进行了指定,否则会发生编译报错:
error C3861: “push_back”: 找不到标识符
这是因为,模板是按需实例化的,当类模板中的函数没有被调用时,其就不会实例化。
我们在实例化stack<T>对象时,vector<T>对象也跟着实例化。但是vector<T>中,只有成员变量和构造函数被实例化了。
我们在调用push_back()函数时,由于this指针为stack<T>类型,所以编译器并不会将vector模板中的push_back()函数实例化,而是直接去寻找其定义或声明。
所以,继承模板类时,调用父类函数要注意指定类域。
2. 父类和子类的转换
1. public继承的子类对象可以赋值给父类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把子类中父类那部分切出来,父类指针或引用指向的是子类中切出来的父类那部分。
2. 父类对象不能赋值给子类对象。
3. 父类的指针或者引用可以通过强制类型转换赋值给子类的指针或者引用。但是必须是父类的指针是指向子类对象时才是安全的。(这里父类如果是多态类型,可以使用RTTI(Run-Time TypeInformation)的dynamic_cast 来进行识别后进行安全转换)
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;
}
3. 继承中的作用域及"隐藏"规则
隐藏规则:
1. 在继承体系中父类和子类都有独立的作用域。
2. 子类和基类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏。(在子类成员函数中,可以使用"父类::父类成员"显式访问)
// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是⾮常容易混淆
class Person
{
protected:string _name = "⼩李⼦"; // 姓名int _num = 111; // ⾝份证号
};class Student : public Person
{
public:void Print(){cout << " 姓名:" << _name << endl;cout << " ⾝份证号:" << Person::_num << endl;cout << " 学号:" << _num << endl;}
protected:int _num = 999; // 学号
};int main()
{Student s1;s1.Print();return 0;
};
这里,程序运行的结果为
3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同(分别在父类和子类中定义的函数只会触发隐藏而不会触发函数重载)就构成隐藏。
4. 注意在实际中在继承体系里面最好不要定义同名的成员
class A
{
public:void fun(){cout << "func()" << endl;}
};class B : public A
{
public:void fun(int i){cout << "func(int i)" << i << endl;}
};
此时,B中的fun函数会隐藏掉A中的fun函数,而不会构成重载。
4. 子类的默认成员函数
子类的默认成员函数主要是用于处理父类没有的成员成员变量,父类自身的成员交由其自己的默认成员函数去处理。
4.1 构造函数
子类在构造函数的初始化列表中可以显式调用父类的构造函数对父类成员变量进行初始化,若未显式调用则会调用父类的默认构造函数。
如果没有显式调用父类的构造函数,且父类没有默认构造函数,那么此时就会发生报错。
显式调用父类构造函数的方式是:
父类名(参数列表)
Student(const char* name, int num):Person(name), _num(num)
{cout << "Student()" << endl;
}
由于父类和子类的可以发生转换,所以在子类的拷贝构造中,将子类对象直接传给父类的构造函数即可调用父类的拷贝构造:
Student(const Student& s):Person(s), _num(s._num)
{cout << "Student(const Student& s)" << endl;
}
4.2 赋值重载
必须要在子类的operator=中显式调用父类的operator=才能按照预期正常的对父类成员进行拷贝,否则只会完成浅拷贝。
要注意,子类的operator=会隐藏父类的operator=,调用父类的operator=需要指定类域。
Student& operator = (const Student& s)
{cout << "Student& operator= (const Student& s)" << endl;if (this != &s){// 构成隐藏,所以需要显⽰调⽤Person::operator =(s);_num = s._num;} return* this;
}
4.3 析构函数
父类的析构函数会在子类的析构函数被调用之后自动调用,无需显式调用。