【继承基础概念】
继承可以让本类使用另一个类的非私有成员,提供共用成员的类称为父类或基类,使用共用成员的类称为子类或派生类,子类创建对象时会包含继承自父类的成员。
继承的优势是减少重复定义数据,当本类需要在另一个类的基础上开发新功能时可以使用继承,这样可以简化代码并节省内存。
若父类中的某些成员禁止被外界使用,可以定义为私有成员,若在禁止外界使用时却允许被子类使用,可以定义为保护成员。
重设访问权限
子类继承自父类的成员需要重新设置在子类的访问权限,重设关键词如下:
public,继承的成员保持原有访问权限,公有成员还是公有权限,保护成员还是保护权限。
private,继承的成员设置为私有权限。
protected,继承的成员设置为保护权限。
若不重设访问权限,则默认为私有权限。
子类创建对象
子类创建对象时,并非将继承自父类的成员合并到子类然后创建合并后的对象,而是会首先创建父类对象,然后创建子类对象,在子类对象中调用父类成员时会转换为调用自动创建的父类对象成员,虽然子类不会继承父类私有成员,但是自动创建的父类对象包含父类私有成员,这是为了被父类公有成员调用,每个子类对象都有自己专用的父类对象。
自动创建父类对象时,其构造函数由子类构造函数调用,若父类有构造函数而子类没有,编译器会为子类自动创建一个构造函数,作用只是调用父类构造函数执行,子类构造函数会首先调用父类构造函数执行,之后再执行子类构造函数自身代码,若两个构造函数功能有冲突,则以最后执行的子类构造函数为准。
若父类构造函数无参数,则子类构造函数无需手动管理父类构造函数,编译器会自动在子类构造函数中调用父类构造函数。
若父类构造函数有参数,则子类构造函数需要为父类构造函数参数赋值,此时必须手动定义子类构造函数并为父类构造函数参数赋值,编译器自动生成的子类构造函数无法为父类构造函数参数赋值。
子类对象使用完毕后,子类析构函数负责调用父类析构函数,若父类有析构函数而子类没有则编译器自动创建一个子类析构函数,作用只是调用父类析构函数执行。
子类析构函数会在自身代码执行完毕后调用父类构造函数,若两者功能有冲突则以最后执行的父类析构函数为准。
子类创建对象时不能直接赋值,因为原理上子类需要同时为继承自父类的成员数据赋值,但这会与父类构造函数产生冲突,即使父类没有构造函数也是如此,这样限制的目的是统一语法规则。
子类对象可以使用如下方式赋值:
1.为成员数据设置默认值。
2.使用构造函数赋值。
3.使用其它公有函数赋值,创建子类对象后手动执行此函数。
在C++中有3种对象不能在创建时直接赋值:
1.包含私有成员数据的对象。
2.有构造函数的对象。
3.子类对象。
#include <iostream>
class base
{
protected:int a,b;public:base(int i1, int i2){a = i1;b = i2;printf("父类构造函数\n");}~base(){printf("父类析构函数\n");}void add() const{printf("a+b=%d\n", a+b);}
};/* derive继承base类,继承成员保持原有访问权限 */
class derive : public base
{
public:/* 父类构造函数有参数,需要手动调用父类构造函数,并为其参数赋值 */derive(int i1, int i2) : base(0,0){a = i1;b = i2;printf("子类构造函数\n");}~derive(){printf("子类析构函数\n");}void mul() const{printf("a*b=%d\n", a*b);}
};int main()
{derive derobj(1,2); //创建子类对象,观察父类与子类构造函数与析构函数的执行顺序derobj.add();derobj.mul();return 0;
}
对象类型转换
子类对象可以转换为父类类型,转换后的子类对象将丢失自建成员,只保留继承自父类的成员,所以父类对象可以引用子类对象赋值,编译器会自动将子类对象转换为父类类型。
父类对象不能转换为子类类型,因为父类中不包含子类成员,子类对象不能引用父类对象赋值。
若需要手动转换子类对象类型,可以使用如下语法: (类型名)对象名。
#include <iostream>
class base
{
public:base(){printf("base构造函数\n");}~base(){printf("base析构函数\n");}void output(){printf("父类\n");}
};class derive : public base
{
public:derive(){printf("derive构造函数\n");}~derive(){printf("derive析构函数\n");}void output() //子类可以定义与父类同名的函数{printf("子类\n");}
};int main()
{derive derobj;derobj.output(); //调用子类同名函数((base)derobj).output(); //转换为父类类型,调用父类同名函数return 0;
}
子类对象转换为父类类型后,会额外执行一遍父类析构函数,上述代码中,base析构函数会执行两遍。
对象指针类型转换
子类类型指针,可以自动转换为父类类型,但是此时不能通过指针调用子类对象成员,可以将指针再次转换为子类类型从而调用子类对象成员。
父类类型指针,不能自动转换为子类类型,因为父类对象不包含子类成员,若使用代码手动转换则通过指针调用子类成员时会出错。
【多继承】
间接多继承
继承可以一直传递下去,比如A派生B,B派生C,那C也会间接继承A,创建C对象时会自动创建A、B两个对象,C调用B构造函数,B调用A构造函数。
#include <iostream>
class baseA
{
protected:int a,b;public:baseA(int i1, int i2){a = i1;b = i2;printf("baseA构造函数\n");}~baseA(){printf("baseA析构函数\n");}void add() const{printf("a+b=%d\n", a+b);}
};class baseB : public baseA
{
public:baseB(int i1, int i2) : baseA(0,0){a = i1;b = i2;printf("baseB构造函数\n");}~baseB(){printf("baseB析构函数\n");}void sub() const{printf("a-b=%d\n", a-b);}
};class derive : public baseB
{
public:derive(int i1, int i2) : baseB(0,0){a = i1;b = i2;printf("derive构造函数\n");}~derive(){printf("derive析构函数\n");}void mul() const{printf("a*b=%d\n", a*b);}
};int main()
{derive derobj(1,2);derobj.add();derobj.sub();derobj.mul();return 0;
}
间接多继承时,可继承成员由直接继承的父类决定,而非更上层的父类决定。
#include <iostream>
class baseA
{
public:void baseAput(){printf("baseA\n");}
};class baseB : private baseA //继承成员重设为私有权限
{
public:void baseBput(){printf("baseB\n");}
};class derive : public baseB
{
public:void derput(){//baseAput(); //错误,虽然baseAput定义为公有权限,但是被baseB重设为私有权限,derive继承baseB,不能调用baseB私有成员baseBput();}
};int main()
{derive derobj;derobj.derput();return 0;
}
直接多继承
一个类可以直接继承多个类,此时每个父类都可以单独重设成员访问权限,子类创建对象时会自动创建所有父类的对象,每个父类对象的构造函数都由此子类构造函数调用,析构函数同理。
#include <iostream>
class baseA
{
protected:int a,b;public:baseA(int i1, int i2){a = i1;b = i2;printf("baseA构造函数\n");}int add() const{return a+b;}
};class baseB
{
protected:float a,b;public:baseB(float f1, float f2){a = f1;b = f2;printf("baseB构造函数\n");}float add() const{return a+b;}
};class derive : public baseA, public baseB
{
public:derive(int i1, int i2, float f1, float f2) : baseA(0,0), baseB(0,0){baseA::a = i1;baseA::b = i2;baseB::a = f1;baseB::b = f2;printf("derive构造函数\n");}//......
};int main()
{derive derobj(1, 2, 1.3, 1.5);printf("整数加法结果:%d\n""小数加法结果:%f\n",((baseA)derobj).add(), ((baseB)derobj).add());return 0;
}
直接多继承很容易导致混乱,尤其是在多层继承关系中,在大型项目中经常搞不清一个类的所有上级父类又直接继承了多少个类,类成员的管理非常麻烦,很多高级编程语言都会禁用直接多继承,若你非常注重程序性能,其实使用C语言更合适,而非使用C++直接多继承。
菱形继承
菱形继承是一种复杂的多继承,继承关系网组成一个菱形,具体方式为:A派生出B和C,D又同时继承B和C,D创建对象时会自动创建B、C两个对象,B、C又会各自创建一个A对象,此时D对象就有两个可以使用的A对象,这将导致混乱。
为了解决混乱问题,C++规定在菱形继承关系中B和C继承A时需要添加virtual关键词定义为虚继承,此时创建D对象时只会创建一个A对象,并且三个父类的构造函数都将由D负责调用,B和C的构造函数不再调用A构造函数,等同于将代码转换为D直接继承A、B、C三个类,但是单独创建B或C对象时不受影响。
#include <iostream>
class baseA
{
public:baseA(){printf("baseA构造函数\n");}//......
};class baseB : virtual public baseA //虚继承
{
public:baseB(){printf("baseB构造函数\n");}//......
};class baseC : virtual public baseA //虚继承
{
public:baseC(){printf("baseC构造函数\n");}//......
};class derive : public baseB, public baseC
{
public:derive(){printf("derive构造函数\n");}
};int main()
{derive derobj;return 0;
}
【虚函数】
C语言通过函数实现程序功能模块化,程序经常需要使用函数指针调用不同的模块,C++将函数封装在类中管理,函数指针只能指向本类的成员函数,无法像C语言那样使用函数指针随意调用函数,为此C++增加了虚函数功能,虚函数的作用是通过指针调用同源继承关系中所有类的成员函数。
虚函数是父类中定义的特殊函数,子类继承后可以直接使用,也可以重写内部代码,但是不能改变虚函数的参数和返回值,否则就不能实现使用函数指针统一调用。
虚函数使用父类对象指针调用执行,编译器会转换为通过函数指针调用虚函数(而普通函数会转换为直接调用),父类指针可以赋值为子类对象地址,赋值为哪个子类对象地址就会调用哪个子类重写的虚函数,同时虚函数的this参数也会赋值为所在类的对象地址,若子类没有重写虚函数则调用父类定义的虚函数。
虚函数定义方式如下:
1.父类在函数返回值之前使用virtual关键词定义虚函数。
2.子类重写虚函数时可以添加virtual关键词,也可以不添加,为了方便识别虚函数一般会加上。
3.子类重写虚函数时可选在参数之后添加override关键词,用于强制编译器检查重写的虚函数,若重写的虚函数与原型不同(参数、返回值不同),则编译报错。
注:虚函数可以当做普通函数使用,直接使用函数名调用它,此时等于不使用虚函数机制,与使用普通函数无异。
#include <iostream>
class base
{
public:/* 父类定义虚函数 */virtual void f1(){//......printf("通用模块\n");}/* 父类定义虚析构函数,原因之后介绍 */virtual ~base(){}
};class deriveA : public base
{
public:/* 子类重写虚函数 */virtual void f1() override{//......printf("业务模块1\n");}
};class deriveB : public base
{
public:virtual void f1() override{//......printf("业务模块2\n");}
};class deriveC : public base
{
public:virtual void f1() override{//......printf("业务模块3\n");}
};int main()
{base baseobj;deriveA derAobj;deriveB derBobj;deriveC derCobj;base * p1; //定义父类对象指针int i;scanf("%d", &i); //模块调用变量if(i == 1){p1 = &derAobj;}else if(i == 2){p1 = &derBobj;}else if(i == 3){p1 = &derCobj;}else{p1 = &baseobj;}p1->f1(); //使用父类指针调用虚函数return 0;
}
上述C++代码功能等同于如下C语言代码:
#include <stdio.h>
struct k
{//......
};
void f0(struct k * this)
{//......printf("通用模块\n");
}
void f1(struct k * this)
{//......printf("业务模块1\n");
}
void f2(struct k * this)
{//......printf("业务模块2\n");
}
void f3(struct k * this)
{//......printf("业务模块3\n");
}
int main()
{void (*p1)();int i;scanf("%d", &i);if(i == 1){p1 = f1;}else if(i == 2){p1 = f2;}else if(i == 3){p1 = f3;}else{p1 = f0;}p1(); //使用函数指针统一调用模块return 0;
}
禁止虚函数重写
虚函数机制会随继承关系遗传下去,若一个类间接继承了提供虚函数的父类,则此类也会有虚函数,也可以使用虚函数机制。
若一个类希望虚函数机制到此为止,其子类不再重写、不再使用虚函数,可以在此类的虚函数中添加final关键词。
virtual void f1() override final { //...... }
虚析构函数
若使用虚函数机制的子类对象需要使用new申请内存进行存储,将此子类对象地址赋值给父类类型指针后,对象使用完毕执行delete释放内存时会出现如下情况。
#include <iostream>
class base
{
public:~base(){printf("base析构函数\n");}virtual void f1(){printf("base虚函数\n");}
};class derive : public base
{
public:~derive(){printf("derive析构函数\n");}virtual void f1() override{printf("derive虚函数\n");}
};int main()
{derive *p1 = new derive;base *p2 = p1;p2->f1();delete p2; //通过父类指针释放子类对象,只会执行父类析构函数return 0;
}
上面代码中,子类类型指针p1赋值给父类类型指针p2,两个指针指向同一个对象,使用delete释放内存时,原理上通过任何一个指针释放都可以,但是释放对象之后还会涉及到自动执行析构函数的问题,p2是父类类型,通过p2释放内存时编译器只会调用父类析构函数,不会执行子类析构函数,所以实际上只能通过子类类型指针释放内存,或者将父类类型指针强制转换为子类类型再释放,这限制了C++代码的灵活性,为此C++规定使用虚函数机制时父类需要定义一个虚析构函数(即使此函数什么也不做),此时通过父类类型指针释放子类对象时会调用子类析构函数。
#include <iostream>
class base
{
public:/* 父类定义虚析构函数 */virtual ~base(){printf("base析构函数\n");}virtual void f1(){printf("base虚函数\n");}
};class derive : public base
{
public:~derive(){printf("derive析构函数\n");}virtual void f1() override{printf("derive虚函数\n");}
};int main()
{derive *p1 = new derive;base *p2 = p1;p2->f1();delete p2; //通过父类类型指针释放子类对象,会调用子类析构函数return 0;
}
纯虚函数
使用虚函数机制时,若父类不需要创建对象,父类虚函数也不需要直接调用,父类存在的唯一作用就是提供父类类型指针,从而统一调用子类重写的虚函数,此时可以将父类虚函数定义为纯虚函数,纯虚函数只有函数主体代码,没有任何内容,定义有纯虚函数的类称为抽象类。
纯虚函数与虚函数的区别如下:
1.包含纯虚函数的类不能创建对象。
2.纯虚函数没有内容,子类必须重写纯虚函数,否则子类的虚函数也是纯虚函数,子类也不能创建对象。
class base
{
public:/* 定义纯虚函数,=0表示纯虚函数 */virtual void f1() = 0;/* 抽象类也需要定义虚析构函数 */virtual ~base(){printf("base析构函数\n");}
};