实际上继承和派生是一个意思,只是说站在不同的角度来看而已。基类派生出派生类,派生类继承自基类。
首先已经定义了一个基类BaseClass,再定义一个类ChildClass,继承自这个基类,格式如下:
class ChildClass: [继承方式] BaseClass{
//子类新增的数据成员和成员函数
};
继承方式有public 、private、protected
,如果不写,默认为private
在派生类中,能够使用的只有public
和protected
修饰的成员。private成员是不能通过对象访问的,不能通过子类的成员函数调用。不同的继承方式会改变基类中的成员在派生类中的权限。总的来说有三点:
①public继承的时候,不改变父类的成员的权限,private不能用,public还是public,protected还是protected;
②private继承的时候,private不能用,public,protected变成private;
③protected继承的时候,private不能用,public,protected变成protected;
总之,一切唯子类继承方式马首是瞻;
由于 private 和 protected 继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以实际开发中我们一般使用 public。
在派生类中访问基类 private 成员的唯一方法就是借助基类的非 private 成员函数,如果基类没有非 private 成员函数,那么该成员在派生类中将无法访问。
在基类中使用using 能够将父类中的public改成private,protected改成public,using 只能改变基类中 public 和 protected 成员的访问权限,不能改变 private 成员的访问权限,因为private根本不可见。
public:
using BaseClass::age; //age从protected变成了public
当派生类定义了和基类一样的成员的时候,不构成重载,而是屏蔽了基类的成员。如果要调用基类的成员,实际上调用的是派生类的。
解决方法:加上类名指明调用基类成员变量/成员函数。
注意:只要函数名相同就会导致屏蔽父类成员,哪怕参数类型不同,个数不同。
这是因为类的作用域是嵌套的,子类的作用域嵌套在父类中,调用某个成员时,先在子类作用域中查找,再到父类作用域中查找。因此会造成覆盖屏蔽;
类的内存模型是这样的:成员变量和成员函数是分开的,变量存在堆/栈区,函数存在代码区。
当涉及到继承的时候,派生类的成员变成了新增的加上父类的成员变量,而函数依然是放在代码区的。在派生类的对象模型中,会包含所有基类的成员变量。
构造函数和析构函数是不能被继承的,可以这么理解:继承了也没有用,首先类名不同,不能用作子类的构造函数/析构函数,其次也不能用作普通的成员变量。因此,如果要在派生类中初始化基类中的成员,得调用基类的构造函数。析构函数不用调用,系统将会自动调用。
如何调用基类的构造函数?
只能在派生类的构造函数的初始化列表中调用。而且只能调用直接基类的构造函数,另外,基类的构造函数总是首先被执行。
事实上,通过派生类创建对象时必须要调用基类的构造函数,如果不指明,则调用基类的默认构造函数,如果基类连默认构造函数都没有,那么就报错。
析构函数和构造函数的执行顺序相反,具体说来:构造函数在生成对象的时候先执行基类的构造函数,析构函数在销毁对象的时候先指向派生类的析构函数,最后再执行基类的析构函数。
每个基类前面都要加上继承方式:public,private还是protected,省略不写默认为private
在派生类中可以调用多个基类的构造函数
#假设classA classB是基类
classC : public classA,public classB
{
//定义构造函数
//这里调用了两个基类的构造函数
classC(int a,int b,char *str):classA(a),classB(b)
{
strcpy(ss,str);
}
};
多继承容易出现问题,例如一个菱形继承的情况:A派生出B和C,然后D从B和C多继承。
假如此时A类中有一个变量a,被B和C都继承了,然后D从B和C各自继承了一个变量a,这样就会导致歧义(ambigious),消除歧义的方法有两种:
①在D类中调用的时候指明该变量来自于哪个类 B::a
或者 C::a
②采用虚继承的方法,B和C继承A 的时候加上virtual
关键字:virtual public A
采用虚继承之后,最终传到D类的变量就只会有一个,而不会产生歧义。能够解决命名冲突和数据冗余的问题。
由于多继承的复杂性已经容易产生的歧义问题,能不用多继承就不用,尽量用单继承解决问题。
另外注意,在虚继承中,派生类可以直接调用间接继承类的构造函数,例如此处的菱形继承D可以调用A的构造函数。
一个指针被定义为指向基类的指针,就算将子类的对象地址赋值给这个指针,调用的函数还是基类的函数(如果基类和子类有相同的函数),这是由于编译的时候就已经决定了指针与基类对象的结合,这叫做静态结合。
如下所示为静态结合
#include
using namespace std;
class Animal{
public:
void show(){
cout<<"Animal.show()"<<endl;
}
};
class Person:public Animal{
public:
void show()
{
cout<<"Person.show()"<<endl;
}
};
int main()
{
Person person = Person();
Animal * p = &person;
p->show(); //Animal.show()
return 0;
}
实现多态性:将基类对象地址赋值给指针能调用基类的函数,将派生类对象的地址赋值给指针能调用派生类的函数,随心所欲,这样就实现了多态性。根据在程序运行过程中所赋给的对象来决定结合关系。这叫做动态结合。
多态是通过虚函数来实现的,如果在父类和子类中有相同的函数,并将父类中的声明为虚函数,也就是加上virtual
关键字。然后定义一个基类指针,将子类对象的地址赋值给该指针(也就是让基类指针指向子类对象),这样就可以实现多态性。可以通过该指针调用子类中的函数。如果该基类指针指向基类对象,可以调用基类中的虚函数。这就是多态性的意义。
注意:
①父类中声明的虚函数,子类中必须得有该函数的定义,可以不加virtual关键字(一般也不加)。
②可以定义基类指针指向基类对象、子类对象。可以定义子类指针指向子类对象,但是不可以指向父类对象。
③如果这个虚函数只在基类中存在,子类中没有,那么基类指针指向子类对象,调用的还是基类的虚函数(没有实现多态性,由于派生类中不存在于是回溯到基类中)。
④如果虚函数在基类中和子类中都存在,那么指针指向基类对象调用基类中的虚函数,指向子类对象调用子类中的虚函数(实现了多态性)
⑤如果虚函数只在子类中存在,基类中没有,如果依然想通过基类指针访问,编译错误。
⑥如果都存在,但是不是虚函数,调用基类函数。(没有实现多态性)
总结:实现多态性必须虚函数在基类和派生类中都存在,如果没定义虚函数不能实现多态性,只能调用基类函数。
如下是实现虚函数的例子
#include
using namespace std;
class Animal{
public:
virtual void show(){
cout<<"Animal.show()"<<endl;
}
};
class Person:public Animal{
public:
void show()
{
cout<<"Person.show()"<<endl;
}
};
int main()
{
Person person = Person();
Animal *p = &person;
p->show(); //Person.show()
//当然这样也是可以的
Person *q = &person;
q->show(); //Person.show()
return 0;
}
问题和思考:对于一个相同的函数在基类定义虚函数和在派生类定义虚函数有区别么?
理解:只需要在基类中定义虚函数即可,派生类中和这个虚函数相同的函数也就是虚函数了。也就是只需要在基类的定义中声明是虚函数即可。
如何判断派生类中的函数是虚函数:
一句话说就是具有相同的函数原型:函数名相同,返回类型相同,参数类型和个数相同,如果具有关键字const(这样的话const对象也可以调用),派生类也必须有。
满足这个条件的情况下,如果基类函数具有关键字virtual,那么可以判定派生类的这个函数也是虚函数。
如何实现多态
利用指针或者引用都可以,但是用对象名来访问虚函数是不能实现多态的。
有的基类只是基类,不用做生成对象。在这样的基类中就不定义函数实体了。
如果一个虚函数在基类定义中没有函数实体,只有一个声明,那么这个 虚函数就是纯虚函数。
纯虚函数的定义,记得加上“=0”
virtual void disp()=0;
在基类中说明的纯虚函数,必须在派生类中给出实体定义。
如果一个类的定义中包含纯虚函数,则不能定义该类的对象:
同样的,对于一个定义了纯虚函数的基类而言,已经失去了创建对象的资格,只能用作继承,如果试图创建对象将发生错误。
包含纯虚函数的类被称为抽象类,抽象类中包含没有函数体定义的函数。
如果在派生类中也没有定义纯虚函数的实体,那么这个派生类也变成了抽象类.
不能将构造函数定义为虚函数
但是可以将析构函数定义为虚函数,虚析构函数。
为了在调用析构函数时实现多态,以便进行所需要的内存释放后处理工作。
virtual ~classname();
1.如果没有定义虚构析构函数,在基类和派生类中都定义了析构函数
并且定义了一个指向基类的指针,将派生类的对象地址赋值给这个指针。
在删除指针p 的时候调用的将是基类的析构函数,无法利用派生类的析构函数进行处理。比如删除派生类的对象。
classA *p=new classB();
...
delete p;
做如下修改
//在基类中:
virtual ~classA();
//在派生类中
~classB();
//也就是定义了一个虚析构函数,这里和一般的虚构函数要求不同;不要求相同的函数名
//只要基类析构函数是virtual的,那么派生类的析构函数就是virtual的
一些注意事项:
1 . 如果调用了派生类的虚析构函数,也将自动调用基类的虚析构函数
2.派生类的析构函数在删除对象的时候被调用了(如果已经定义了虚析构函数)
3.就算没有析构函数也是能够释放申请的对象的,但是如果是申请的空间(使用了new),这样的情况下,申请的内存空间必须由析构函数来释放(delete p),否则将会:对象被释放,但是空间没有释放,空间也不能继续使用,就会再也无法释放了(因为释放对象时,指向空间的指针已经被释放了)内存泄露 memory leak
4.运行出错,指针p所指向的对象是已经定义好的对象,这个时候就不能用delete来删除对象了。用delete只能是用来释放new申请的空间
classB b;//定义了一个派生类对象b,这是定义的对象,有对象名,而不是classB *p=new
classA *p=&b;//定义了基类指针指向b
delete p;//发生错误,delete只能和new对应。
5.指针不是对象,不能自动调用析构函数释放空间