1.继承的概念及定义
1.1继承的概念
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许我们在保持原有类特性的基础上进行拓展,增加方法(成员函数)和属性(成员变量),这样产生性的类称子类。继承呈现了面向对象的程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。
下面我们看到没有继承之前我们设计了两个类Student和Teacher,Student和Teacher都有姓名/地址/电话/年龄等成员变量,都有identity身份认证的成员函数,设计到两个类里面就是冗余的。当然他们也有一些不同的成员函数和变量,比如老师独有的成员变量是职称,学生的独有成员变量是学号;学生的独有成员函数是学习,老师的独有成员函数是授课。
//学生类
class Student
{
public://进入校园/图书馆/实验室刷二维码等身份确认void identity(){//...}//学习void study(){//...}
protected:string _name = "peter"; //姓名string _address; //地址string _tel; //电话int _age; //年龄int _stuid; //学号
};
//教师类
class Teacher
{
public://进入校园/图书馆/实验室刷二维码等身份确认void identity(){//...}//授课void teaching(){//...}
protected:string _name = "zhangsan"; //姓名string _address; //地址string _tel; //电话int _age; //年龄string _title; //职称
};
下面我们将公共的成员都放到Person类中,Student和Teacher都继承Person,就可以复用这些成员,不需要重复定义了,省去了很多麻烦
class Person
{
public://进入校园/图书馆/实验室刷二维码等身份确认void identity(){//...}protected:string _name = "zhangsan"; //姓名string _address; //地址string _tel; //电话int _age; //年龄};
//学生类
class Student:public Person
{
public://学习void study(){//...}
protected:int _stuid; //学号
};//教师类
class Teacher:public Person
{
public://授课void teaching(){//...}
protected:string _title; //职称
};
1.2继承的定义
1.2.1定义格式
class 派生类 : 继承方式 基类
{
//子类内部自定义属性...
};
下面我们看到Person是父类,也称作基类。Student是子类,也称作派生类。(因为翻译的原因,即叫父类/子类,也叫基类/派生类)
1.2.2继承父类成员访问方式的变化
继承关系和访问限定符
1.2.3继承基类成员访问方式的变化
类成员/继承方式 | public继承 | protected继承 | private继承 |
父类的public成员 | 子类的public成员 | 子类的protected成员 | 子类的private成员 |
父类的protected成员 | 子类的protected成员 | 子类的protected成员 | 子类的private成员 |
父类的private成员 | 在子类中不可见 | 在子类中不可见 | 在子类中不可见 |
1.父类private成员在子类中无论以什么方式继承都是不可见的。这里的不可见是指父类的私有成员是被继承到子类中,但是语法上限制不管是在类里面还是类外面都不能去访问它
2.父类private成员在子类中是不能访问的,如果父类成员不想在类外直接被访问,但需要在子类中能访问,就定义为protected。可以看出保护成员限定符是因为继承才出现的
3.实际上面的表格我们总结一下会发现,父类的私有成员在子类中都是不可见的。父类的其他成员在子类的访问方式==Min(成员在父类的访问限定符,继承方式),public > protected >private。
4.使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
5.在实际运用中一般使用都是public继承,几乎很少用protected/private 继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员只能在子类的类里面使用,实际中扩展维护性不强。
1.3继承类模板
父类是类模板时,需要指定一下类域否者编译器报错
#include<iostream>
#include<string>
#include<vector>
using namespace std;//实现一个栈
namespace Stack
{template<class T>
//stack继承于现有的vectorclass stack :public std::vector<T>{public:void push(const T& x){push_back(x);}};}int main()
{Stack::stack<int> s;s.push(1);s.push(1);s.push(1);s.push(1);return 0;
}
如上代码报错,push_back找不到标识符,父类vector有push_back为什么还会找不到呢?
这是因为模板都是按需实例化,这里我们虽然实例化了一个s 是stack对象,仅仅是调用了他的默认构造,实例化了默认构造,但是push_back并没有实例化,在子类父类都找不到push_back就会报错此时我们只要指定一下类域就可以使用,告诉编译器要使用父类中的push_back使其实例化
void push(const T& x)
{vector<T>::push_back(x);
}
1.4模板补充知识
注意:类模板是按需实例化,编译器对模板函数的检查不会很仔细,虽然vs版本在更新不断优化但是仍然不够严谨,没有实例化一般不检查
如下代码中fun()函数不存在,而且已经实例化出s是int类型,push里面用int类型的变量去调用一个不存在的函数编译器也不报错
#include<iostream>
#include<string>
#include<vector>
using namespace std;namespace Stack
{template<class T>class stack :public std::vector<T>{public:void push(const T& x){x.fun();}void pop(){vector<T>::pop_back();}const T& top(){vector<T>::back();}bool empty(){return vector<T>::empty();}};}
没有使用push,编译器不会对push实例化,编译器也不检查
int main()
{Stack::stack<int> s;return 0;
}
s对象调用了push,编译器实例化了push,编译器检查报错
int main()
{Stack::stack<int> s;s.push(1);return 0;
}
2.父类和子类对象赋值兼容转换
public继承的子类对象可以赋值给父类的对象/父类的指针/父类的引用。这里有个形象的说法叫切割或切片。寓意把子类中父类那部分切割出来赋值过去
- 赋值
指针切片
引用切片
#include<iostream>
#include<string>
#include<vector>
using namespace std;class Person
{
protected:string _name; // 姓名string _sex; // 性别int _age; // 年龄
};
class Student : public Person
{
public:int _No; // 学号
};
int main()
{Student sobj;//子类对象可以赋值给父类对象/指针/引用 ,中间不会产生临时变量Person pobj = sobj;Person* po = &sobj;Person& bj = sobj;return 0;
}
父类对象不能赋值给子类对象,这里不能强转,则会个要和基本类型强转区分开来
//父类不能赋值给子类对象,这里会编译报错
sobj = pobj;//不支持强转
// sobj =(Student)pobj;
父类的指针或者引用可以通过强制类型转换赋值给子类的指针或者引用。但是必须是父类的指针是指向子类对象时才是安全的。这里父类如果是多态类型,可以使用RTTI的dynamic_cast来进行识别后进行安全转换
3.继承中的作用域
3.1隐藏规则
1.在继承体系中父类和子类都有独立的作用域
2.子类和父类中有同名成员时,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏。(在子类成员函数中,可以使用 父类::父类成员 显示访问)
#include<iostream>
#include<string>
#include<vector>
using namespace std;class Person
{
protected:string _name="小李"; // 姓名string _sex; // 性别int _age=999; // 年龄
};
class Student : public Person
{public:void Print(){cout << _age << endl;}int _age = 19;int _No; // 学号
};
int main()
{Student s;s.Print();return 0;
}
运行结果是19,父类和子类有不同的域,屏蔽了父类中同名成员的影响
访问父类中的成语就要指定类域
void Print(){cout << Person::_age << endl;}
3.需要注意的是如果是成员函数的隐藏,只需要求函数名相同就构成隐藏。
class Person
{
public:void fun(){cout << "fun()" << endl;}
};
class Student : public Person
{
public:void fun(){cout << "fun()" << endl;}
};
Student继承于Person,函数重载要求在同一作用域,他们两个的域都不在一起所以首先排除重载,即使同名,不在同一作用域也没影响,所以构成隐藏,编译也不报错,可正常执行
同理下面也构成隐藏
class Person
{
public:void fun(){cout << "fun()" << endl;}
};
class Student : public Person
{
public:void fun(int i){cout << "fun(int i)" << endl;}
};
函数调用也一样
int main()
{Student s;//调用Student中的funs.fun(1);//调用Person中的funs.Person::fun();return 0;
}
4.注意在实际中继承体系里面最好不要定义同名的成员
4.子类的默认成员函数
4.1 4个常见默认成员函数
6个默认成员函数,默认的意思是我们不写,编译器会帮我们自动生成一个,那么在子类中,这几个成员函数是如何生成的呢?
1.子类的构造函数必须调用父类的构造函数初始化父类的那一部分成员。如果父类没有默认的构造函数,则必须在子类构造函数的初始化列表阶段显示调用。
#include<iostream>
#include<string>
#include<vector>using namespace std;class Person
{
public:Person(const char* name = "peter"):_name(name)
{cout << "Preson()" << 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;
}
~Person()
{cout << "~Person()" << endl;
}
protected:string _name;//姓名};
class Student : public Person
{
public://初始化StudentStudent(const char* name, int num, const char* addrss):_name(name)//初始化父类的name,_num(num),_adderss(addrss){}int _num;//学号string _adderss;
};
int main()
{Student s("张三",1,"景德镇");cout << endl;return 0;
}
编译器报错
不允许在子类中直接初始化父类成员,初始化父类成员只能调用父类的默认构造,但是下面这样就可以,在初始化列表匿名调用Person的默认构造初始化_name
//初始化StudentStudent(const char* name, int num, const char* addrss):Person(name)//初始化父类的name,_num(num),_adderss(addrss){}
2.子类的拷贝构造函数必须调用父类的拷贝构造完成父类的拷贝构造完成父类的拷贝初始化
#include<iostream>
#include<string>
#include<vector>
using namespace std;class Person
{
public:
//默认构造Person(const char* name = "peter"):_name(name){cout << "Preson()" << 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;}
//析构~Person(){cout << "~Person()" << endl;}
protected:string _name;//姓名};
class Student : public Person
{
public:
//严格来说Student拷贝构造默认生成的就够用了
//如果有需要深拷贝的资源,才需要自己实现//内置类型int _num;//学号string _adderss;
};
int main()
{Student s;
Student t= s;return 0;
}
_num内置类型初始化不确定,s中父类的_name调用父类的默认构造初始化成peter ,t=s调用了父类中的拷贝构造初始化父类部分
//默认生成的构造函数
//1/内置类型->不确定
//2.自定义类型->调用默认构造
//3.继承父类成员看作一个整体,要求调用父类的默认构造
这里调用了父类的拷贝构造完成父类中的初始化
class Person
{
public:Person(const char* name = "peter"):_name(name){cout << "Preson()" << endl;}Person(const Person& p):_name(p._name){cout << " Person(const char* name)" << endl;}protected:string _name;//姓名};
class Student : public Person
{
public://初始化StudentStudent(const char* name, int num, const char* addrss):Person(name),_num(num),_adderss(addrss){}Student(const Student& s):Person(s),_num(s._num),_adderss(s._adderss) {}int _num;//学号string _adderss;
};
int main()
{Student s("张三",1,"景德镇");Student s1(s);cout << endl;return 0;
}
如果不写拷贝系统会调用其他最合适的默认构造
class Person
{
public:Person(const char* name = "peter"):_name(name){cout << "Preson()" << endl;}Person(const Person& p):_name(p._name){cout << " Person(const char* name)" << 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 Student& s)
//不初始化父类中的name,系统调用默认构造:_num(s._num),_adderss(s._adderss){}int _num;//学号string _adderss;
};
只有Person(const char* name = "peter")给了缺省值可以初始化,编译器调用这个默认构造
3.子类的拷贝构造函数必须调用父类的operator=完成父类的赋值。需要注意的是子类的operator=隐藏了父类的operator=。所以显示调用父类的operator=时,需要指定父类作用域。
4.3析构和默认构造的调用顺序
注意:默认构造的调用顺序和声明的顺序一致,析构的顺序与释放内存的顺序一致。
//严格来说Student析构默认生成的就够用了
//如果有需要显示释放的支援==资源才需要自己实现
//析构函数都会被特殊处理成destructor
~Student()
{//子类的析构和父类的析构构成隐藏关系Person::~Person();
}
4.子类的析构函数会在被调用完成后自动调用父类的析构函数清理父类成员。因为这样才能保证子类对象先清理再清理父类成员的顺序
5.子类对象初始化先调用父类构造再调用子类构造
6.子类对象析构清理先调用子类析构再调用父类析构