派生类
类之间有一种层次关系,有父亲类,有孩子类
父类也叫做基类、超类;子类又叫作派生类
继承,说白了就是,我们要先定义一个父类,父类中定义一些公用的成员变量,成员函数;我们通过继承父类来构建新的类:子类。所以,我们只需要写一些和子类相关的一些内容
子类一般比父类更加庞大
写法:class 子类名 : 继承方式 父类名
继承方式:public/protected/private;
// human.h
#ifndef _HUMAN_H_
#define _HUMAN_H
#include<iostream>
class human{
public:
int age;
char name[100];
human();
explicit human(int);
};
#endif // !_HUMAN_H_
//human.cpp
#include"human.h"
using namespace std;
human::human():age(0), name(""){
cout<<"human()"<<endl;
}
human::human(int tmpage):age(tmpage), name(""){
cout<<"human(int)"<<endl;
}
//man.h
#ifndef _MAN_H
#define _MAN_H
#include<iostream>
#include"human.h"
class man:public human //man是human的子类
{
public:
man();
};
#endif // !_MAN_H
//man.cpp
#include"man.h"
using namespace std;
man::man(){
cout<<"man()"<<endl;
}
派生类对象定义时调用构造函数的顺序:先调用父类的构造函数,再调用子类的构造函数
public、protected、private
public
:可以被任意实体所访问
protected
:只允许被本类或者子类的成员函数所访问
private
:只允许本类的成员函数访问
基类中的访问权限 | 子类继承基类的继承方式 | 子类得到的访问权限 |
---|---|---|
public | public | public |
protected | public | protected |
private | public | 子类无权访问 |
public | protected | protected |
protected | protected | protected |
private | protected | 子类无权访问 |
public | private | private |
protected | private | private |
private | private | 子类无权访问 |
总结:
1)子类public继承父类不改变父类的访问权限
2)子类protected继承父类中的public成员变为子类中的protected成员
3)子类private继承父类使得父类的所有成员在子类中的访问权限变为private
4)父类中的private成员不受继承方式的影响,子类永远无法访问
5)对于父类来讲,尤其是父类的成员函数,如果不想让外部访问,就设置为private,如果想让子类访问,就设置为protected,如果想公开,就设置为public
函数遮蔽
子类中如果有一个同名函数,那么父类中,不管有几个同名函数,子类都无法访问到
如果我们确实想要调用父类中的同名函数,在子类的成员函数中,用父类::函数名
强制调用父类函数;或者使用using
,让父类同名函数在子类中可见,用法在子类中using 父类名::函数名
,只能指定函数名,则凡是基类中的public、protected,在子类中都可见,无法让一部分父类中的同名函数可见
基类指针、派生类指针
父类指针可以new一个子类对象human *phuman = new man;
可以;父类类型指针,可以调用父类成员函数,无法调用子类成员函数
子类指针不能new一个父类指针man *pman = new human;
不可以
虚函数
我们想要只定义一个对象指针,就能够访问父类以及子类中的各个同名函数,那么这个对象指针必须是父类类型,必须如human *phuman = new man;
进行定义;并且对于同名函数也有要求,在父类中的同名函数声明时必须要加virtual
参数,定义为虚函数;在子类中也可以加virtual
方便阅读;一旦某个函数的父类中被声明为虚函数,那么所有派生类中他都是虚函数
调用虚函数执行的是动态绑定,动态指的是在我们程序执行时才能知道调用了哪个类的同名函数
一旦定义了虚函数,则human *phuman = new man;
创建的对象指针调用的是子类的同名成员函数,例如human
类和man
类中都有一个相同的eat()
函数,man
类继承human
类,且human
类中的eat()
被声明为virtual
human *phuamn = new man;
phuman->eat(); //调用的是man类的eat()函数
phuman->human::eat() //调用的是human类的eat()函数
override
为了避免在子类中写错虚函数,在c++11中,你可以在函数声明这里添加一个override
关键字,这个关键字用在子类中,而且是虚函数专用,用了这个关键字之后,编译器就会认为子类中的函数覆盖了父类中的同名函数(只有虚函数才存在子类可以覆盖父类中的同名函数的问题)。用法virtual int eat() override;
final
final
也是虚函数专用,是用在父类,如果我们在父类的函数声明中加了final
关键字,那么任何尝试覆盖该函数的操作都将引发错误。用法virtual int eat() final;
子类同名函数中不能使用override
多态性
多态性只针对虚函数,多态性体现在具有继承关系的父类和子类之间,子类重新定义(重写)父类的成员函数,同时父类把该函数声明成virtual
,通过父类指针,只有到了程序运行时期,找到动态绑定在父类指针上的对象,这个对象可能是某个子类对象,也可能是父类对象,然后系统内部要查虚函数表,找到函数的入口地址,从而调用相应的父类或子类函数,这就是运行时期的多态性。
纯虚函数
纯虚函数实在基类中声明的虚函数,但是他在基类中没有定义,要求任何派生类都要定义该虚函数自己的实现方法
基类中实现纯虚函数的方法是在函数原型后增加=0
,如virtual int eat() = 0;
没有函数体,只有函数声明。
一旦一个函数中有了纯虚函数了,那么你就不能生成这个类的对象了,这个类就变为了抽象类
,抽象类的主要目的是用来统一管理子类对象
基类的析构函数一般写成虚函数(虚析构函数)
man *pman = new man;
delete pman;
// human::human() -> man::man() -> man::~man() -> human::~human()
human *phuman = new man;
delete phuman;
// human::human() -> man::man() -> human::~human()
//没有执行子类的析构函数,可能出现问题
用基类指针new子类对象,使用delete来回收,系统不会调用派生类的析构函数
如果把父类的析构函数写为虚函数,那么则有
human *phuman = new man;
delete phuman;
// human::human() -> man::man() -> man::~man() -> human::~human()
//执行完整
public
继承中,基类对其派生类及其对象的操作,只能影响到那些从基类继承下来的成员,如果想要用基类对非继承成员进行操作,则要把基类的这个函数定义为虚函数,析构函数自然也如此;另外基类中的虚属性也会被继承给子类,这样的话子类中的析构函数也就自然而然成为了虚函数;执行delete phuman
的时候,肯定是要调用父类的析构函数,但在父类的析构函数中他要是想要调用子类man
的析构函数,那么human这个类中的析构函数就要声明为virtual
,也就是说c++中为了获得运行时的多态行为,所调用的成员函数必须得是virtual
虚函数会增加内存开销,类里面定义虚函数,编译器就会给这个类增加虚函数表,在这个表里存放虚函数指针
友元关系
友元函数
//man.h
#ifndef _MAN_H
#define _MAN_H
#include<iostream>
#include"human.h"
class man:public human //man是human的子类
{
public:
man();
~man();
void funcpub() const {
cout<<"funcpub()"<<endl;
};
private:
void funcpri() const {
cout<<"funcpri()"<<endl;
};
};
#endif // !_MAN_H
//man.cpp
#include"man.h"
using namespace std;
man::man(){
cout<<"man()"<<endl;
}
man::~man(){
cout<<"~man()"<<endl;
}
//main.cpp
#include “man.h”
void func1(cont man &tmpman){
tmpman.funcpub();
}
void func2(cont man &tmpman){
tmpman.funcpri();
}
int main(){
man m;
func1(m); // 可以访问,funcpub为public属性
func2(m); // 不能访问,funcpub为private属性
}
只要让函数func2
成为类man
的友元函数,那么func2
就可以访问类man
的所有成员(成员变量、成员函数),包括public,protected,peivate
用法:
class man:public human //man是human的子类
{
public:
man();
~man();
void funcpub() const {
cout<<"funcpub()"<<endl;
};
private:
void funcpri() const {
cout<<"funcpri()"<<endl;
};
friend void func2(const man &tmpman); //声明为友元函数
};
因为友元函数不属于类成员,所以友元函数声明不受public、protected、private影响
友元类
类可以把其他的类定义为友元类。如果B类是A类的友元类,那么B类就可以在B类的成员函数中访问A类的所有成员
class A{
private:
int data;
}
class B{
public :
void func(int x, A &a){
a.data = x; //正常情况下不能访问
cout<<a.data<<endl;
}
}
在a类中添加友元类声明
class A{
private:
int data;
friend class B; //虽然类B此时没有定义,但是系统不报错,但一般还是先定义类B
}
class B{
public :
void func(int x, A &a){
a.data = x; //可以访问A的私有成员
cout<<a.data<<endl;
}
}
每个类都负责控制自己的友元类和友元函数
a)友元关系不能被继承
b)友元关系是单向的,比如说类B是类A的友元类,这不代表类A是类B的友元类
c)友元关系没有传递性,比如说类B是类A的友元类,类C是类B的友元类,这不代表类C是类A的友元类
友元成员函数
我们有时不希望整个类成为友元类,只想让类中的某个成员函数成为友元函数
只有public的函数才能成为其他类的友元成员函数
class B{
public :
void func(int x, A &a){
a.data = x;
cout<<a.data<<endl;
}
}
class A{
private:
int data;
friend void B::func(int x, A &a); //友元成员函数声明,必须在B::func声明之后
}
优点:允许在特定情况下,某些非成员函数访问类的protected、private成员,使得访问类的protected、private成员更加灵活
缺点:破坏了类的封装性,降低了类的可靠性和封装性
RTTI(Run Time Type Identification)运行时类型识别
通过RTTI程序可以使用基类的指针或者引用来检查这些指针或者引用所指对象的实际派生类型。
我们可以把RTTI看成是一种系统提供给我们的能力,或者一种功能,通过两个运算符来体现:
1)dynamic_cast
运算符:能够将基类的指针或者引用安全的转换为派生类的指针或者引用
2)typrid运算符:返回指针或者引用对象的实际类型
要想让RTTI的两个运算符能够正常工作,那么基类中必须至少要有一个虚函数,不然这两个运算符工作的结果就可能与我们预测的不同。只有因为虚函数的存在,这两个运算符才会使用指针或者引用所绑定的动态类型
dynamic_cast
如果该运算符能够转换成功,说明这个指针实际上是要转换到的那个类型,这个运算符能够做安全检查
human *phuman = new man;
man *m = (man *)phuman; // c语言风格强制类型转换,不安全
woman *w = (woman *)phuman; // 也可以转,但是可能有问题
man *m2 = dynamic_cast<man *>(phuman); //安全
if(m2 != nullptr){
// 转换成功,可以操作man类的成员变量和成员函数
}
对于引用,如果用dynamic_cast转换失败,则系统会抛出一个std::bad_cast异常
human *phuman = new man;
human &q = *phuman;
try{
man m3 = dynamic_cast<man &>(q);
//转换成功,可以操作man类的成员变量和成员函数
}
catch(std::bad_cast){
// 异常处理
}
基类必须有虚函数
typeid
typeid(类型[指针/引用]),也可以是typeud(表达式)
拿到对象类型信息;typeid就会返回一个常量对象引用,这个常量对象是一个标准库类型type_info
human *phuman = new man;
human &q = *phuman;
cout<<typeid(*phuman).name()<<endl; //class man
cout<<typeid(q).name()<<endl; //class man
typeid主要是为了比较两个指针是否指向同一种类型的对象
1)两个指针定义的类型是否相同
human *phuman = new man;
human *phuman1 = new woman;
if(typeid(phuman) == typeid(phuman2)){
// 等式成立
}
if(typeid(*phuman) == typeid(*phuman2)){
// 等式不成立
}
基类必须有虚函数
type_info类
type_info的一些常用方法
a).name
:返回c风格的字符串,名字
b)==、!=
:判断相等或者不等
RTTI与虚函数表
c++中,如果类中含有虚函数,编译器就会对该类创建一个虚函数表。
虚函数表里有很多项,每一项都是一个指针,每个指针指向的是这个类中的各个虚函数的入口地址。
虚函数表项里,第一个项很特殊,他指向的不是虚函数的入口地址,他指向的实际是这个类所关联的type_info
对象
之前例子中的phuman
对象里有一个我们不可见的指针,指向的是这个对象所在类man
中的虚函数表
评论区