目录
1,什么是继承?
2,继承分类
单一继承:一个派生类只从一个基类派生
多重继承:一个派生从多个基类派生
派生类格式:
3,继承权限
4,继承方式对基类成员在派生类中访问权限的影响
5,类型兼容
1,可用派生类对象向基类对象赋值
2,派生类对象对基类对象引用初始化
3,函数形参为基类对象或引用时,实参可为派生类对象
4,用派生类对象地址对基类指针变量赋值
类型兼容小结:
6,派生类构造函数
总结:
构造函数的调用顺序
派生类析构函数
补充
7,虚函数
延伸:
8,多重继承
在多重继承下派生类的构造函数怎么定义呢?
多重继承下的二义性是什么?
9,虚继承
虚继承的目的:
虚继承在C++标准库中的实际应用:
使用虚基类的派生类的构造函数的调用
虚继承时的构造函数:
分析:
面对对象程序设计有三个主要特点:封装,继承,多态
封装这里就不说了,封装就是将属性和行为作为一个整体,表现生活中的事物
语法格式就是:class 类名。这里不再赘述了
什么是继承呢?比如说父亲和儿子,儿子通过继承,保留了父亲的优良属性并扩充有了新的特征,这个就是继承,父亲就被叫做父类或者基类,儿子就被称为子类或者派生类。
简单来说就是用已有类建立新类,可以理解为一个类从另一个类获取成员变量和成员函数的过程。通过基类和派生类来描述这种继承关系。继承提高了代码的可复用性,实现了代码重用。
需要注意的是一个基类可以派生多个派生类,子类继承父类所有特性。派生类通过对基类的继承,保留了基类原有的属性和方法,并可以增加新的属性和方法。
继承的目的:实现代码重用
派生的目的:当新问题出现,原有程序无法或不能完全解决时,需对原有程序进行改造。
继承可分为单一继承和多重继承
例:
#include
using namespace std;
class Father
{
public:
void F_print()
{
cout << "父类函数调用" << endl;
}
};
class Son :public Father
{
public:
void S_print()
{
cout << "子类函数调用" << endl;
}
};
int main()
{
Son s;
s.S_print();
s.F_print();
}
运行结果
子类函数调用
父类函数调用
例:
#include
using namespace std;
class Father
{
public:
void F_print()
{
cout << "父类函数调用" << endl;
}
};
class Mother
{
public:
void M_print()
{
cout << "第二个父类函数调用" << endl;
}
};
class Son :public Father,public Mother
{
public:
void S_print()
{
cout << "子类函数调用" << endl;
}
};
int main()
{
Son s;
s.S_print();
s.F_print();
s.M_print();
}
输出结果
子类函数调用
父类函数调用
第二个父类函数调用
class 派生类名称:继承方式 基类1名称,...继承方式 基类n名称
{
派生类成员声明
}
注意:
1,派生类不继承基类的构造函数和析构函数,友元函数也不继承
2,除构造,析构函数外,派生类无条件继承基类的所有的数据成员和成员函数
注意:基类的私有成员派生类也能继承,但是派生类不能直接访问,需要使用基类的 方 法才可以访问。
3,派生类除了拥有基类的成员,还可以定义自己的新成员
4,多个派生类可以继承自一个基类
class A{……};
class B:public A{……}; //类B继承自类A
class C:public A{……}; //类C继承自类A
5,可通过派生形成类的层次结构。如类A派生类B,类B派生类C
class A{……};
class B:public A{……}; //类B是类A的派生类
class C:public B{……}; //类C是类B的派生类
说到继承方式,下面就要说一下继承权限了
类定义中的访问限定符,给类成员提供了3类访问权限
类成员的访问权限由高到低依次为:
public->protected->private
public(共有):任何位置,可以被该类的成员函数及类外的其他函数访问
private(私有):自身(默认),类内的函数可以访问,类外不可访问
protected(受保护):自身加派生类,类内和该类的派生类可以访问,类外其他函数不可访问,出现在基类中,只为派生类开放权限,访问权限由派生类的继承方式决定
友元可访问类中的任何成员(友元是单向的),友元后面会说。
注意:
当存在继承关系时,派生类继承自基类的成员的访问权限会发生变化。
可见:
基类成员的访问权限不是简单的被派生类继承的。派生类定义时不同的继承方式, 会影响基类成员在派生类中的访问权限
继承方式有三种:public,protected,private
派生类的成员:从基类继承的成员和派生类的新增成员两部分
访问方式的不同主要表现在一下两个方面:
不同的继承方式会使基类成员的访问权限在派生类中发生变化
派生类继承了基类时,派生类就拥有了基类的所有成员,基类的成员本身就有对应的访 问权限,但是不同的继承方式会改变派生类继承的基类成员的访问权限
口诀是:遇升不升,遇降则降,遇平则平,私有除外
比如基类成员的访问权限是protected,如果派生类继承这个基类的方式为public,那么基类成员在派生类的访问权限不会发生改变,仍是protected,也就是遇升不升;如果派生类继承这个基类的方式为private,那么基类成员在派生类的访问权限将会发生变化,变成private,也就是遇降则降,这样派生类就不能访问基类成员了,只能在基类内访问。
例:
#include
using namespace std;
class Father
{
public:
int a = 10;
protected:
void F_print()
{
cout << "父类函数调用" << endl;
}
};
class Son :private Father
{
public:
void S_print()
{
cout << "子类函数调用" << endl;
cout << a << endl;//未报错,类内可以访问
}
};
int main()
{
Son s;
s.S_print();
//cout << s.a << endl; 报错,派生类继承自基类的成员的访问权限因为私有继承变成private
//所以在son类外不能访问son类中的a成员,在类内则可以访问
}
输出结果
子类函数调用
10
从这里就可以看出来,如果是私有继承,那么son类继承自Father类的成员的访问权限会发生改变,会变成private,类内可以访问,类外访问不了
派生类是从基类继承来到,特别是共有继承的派生类,保持了基类的所有特征。因此,在使用上,一个共有派生类对象可以充当一个基类对象(在用到基类对象时,可用其派生类对象代替,如:可将派生类对象的值赋值给基类对象),反之不成立。
使用派生类对象操作基类数据的四种方法:
1,可用派生类对象向基类对象赋值
2,可用派生类对象对基类引用初始化
3,函数形参为基类对象或引用时,实参可为派生类对象
4,可用派生类对象地址对基类指针变量赋值
例
classA obj_a; //定义基类ClassA对象obj_a
classB obj_b; //定义类ClassA的公有派生类ClassB的对象obj_b
obj_a = obj_b; //用派生类ClassB对象obj_b对基类对象obj_a赋值
派生类对象向基类对象赋值时,将基类数据成员赋值,派生类新增的数据成员值被舍弃, 不存在对成员函数的赋值。由派生类中数据成员的排列情况可知,基类数据成员排列在 最前端,因此可以使用派生类对象向基类对象赋值,基类对象会获取派生类对象中的基 类数据。
注意:
1,赋值后不能通过基类对象访问派生类对象的成员,反之可以
假设:newmember是派生类ClassB中增加的公有数据成员,若有下列代码,则执 行错误:
obj_a.newmember = xxx; //错误,obj_a中不包含派生类中增加的成员
2,派生类型关系是单向的,不可逆。只能用派生类对象对其基类对象赋值,反之 不可。同理,同一基类的不同派生类对象之间也不能赋值。
派生类对象可以替代基类对象,向基类对象的引用进行赋值或初始化。
若已定义了基类ClassA对象obj_a,可以定义obj_a的引用:
classA obj_a; //基类对象
classB obj_b; //公有派生类对象
classA &refa1 = obj_a; //定义基类对象的引用并用基类对象对其初始化
//(refa1是obj_a的别名,两者共享同一块内存空间)
也可以:用派生类对象初始化基类对象的引用:
classA &refa2 = obj_b; //定义基类对象的引用并用派生类对象对其初始化
注意:
此时refa2并不是obj_b的别名,也不与obj_b共享一段存储单元。它只是obj_b中基类部分的别名,refa2与obj_b中基类部分共享同一段存储单元,两者具有相同的起始地址。
如果函数形数是基类对象或基类对象的引用,函数调用时的实参可以是派生类对象。
例如:
void func(ClassA &ref) //形参是基类对象的引用
{
cout<
调用时:函数形参是基类对象的引用,本来实参应该为基类的对象。由于派生类对 象与基类对象赋值兼容,派生类对象能自动转换类型,调用时可以用派生类对象 obj_b作实参:
func(obj_b); //输出派生类对象的基类数据成员num的值。
指向基类对象的指针变量也可以指向派生类对象(只能操作基类成员,不能操作派 生类成员)
公有派生类对象对基类对象赋值兼容
用基类对象或指针只能操作派生类中继承的基类成员,不能操作派生类成员
私有和保护继承方式没有赋值兼容性
构造函数用于完成数据成员的初始化。派生类不会继承基类的构造函数和析构函数。
派生类的成员有两种,一个是新增的数据成员,一个是基类继承的数据成员,大部分基类都有private属性的数据成员,在派生类中无法访问,那么怎么初始话这些数据呢?
思路:派生类中自身数据可用派生类构造函数完成初始化,从基类继承的数据成员必须由基类构造函数完成初始化,在派生类的构造函数中调用基类构造函数
派生类的构造函数除了完成自身数据成员的初始化外,还会调用基类构造函数完成基类成员的初始化。
例:
#include
using namespace std;
class Father
{
private:
int a;
public:
Father(int x)
{
a = x;
cout << "a=" << a<
这里例子就可以看出来创建派生类对象时,首先调用基类的构造函数,然后再调用派生类的构造函数。
注意:
1,派生类构造函数名后面括号内的参数列表中,应包含基类和派生类构造函数中, 需要进行初始化的所有数据成员的参数值。冒号后面的内容是要调用的基类构造函 数及其参数,在这里是对基类构造函数的调用,因此参数为实参,不需要有类型名, 基类构造函数后面的内容还可以是常量、全局变量。
2,若基类没有构造函数或仅存在无参构造函数,则在派生类构造函数的定义中可 以省略对基类构造函数的调用。
3,当基类的构造函数使用一个或多个参数时,派生类中必须定义构造函数,提供 将参数传递给基类构造函数的方法,从而实现对基类数据成员的初始化。此时,派 生类的构造函数体可能为空,仅仅为了向基类传递数据。
1,可以将基类构造函数的调用放在参数初始化表后面
2,派生类构造函数总是先调用基类构造函数再执行构造函数中其他代码
3,函数头部是对基类构造函数的调用,而不是声明,所以括号里的参数是实参, 它们不但可以是派生类构造函数参数列表中的参数,还可以是局部变量、常量等
创建派生类对象时,会先调用基类构造函数,再调用派生类构造函数。
如果继承关系有好几层的话,构造函数的调用顺序是:按照继承的层次自顶向下、 从基类再到派生类的。
例如:A --> B --> C,那么创建 C 类对象时构造函数的执行顺序为:
A类构造函数 --> B类构造函数 --> C类构造函数
注意:派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。
通过派生类创建对象时必须要调用基类的构造函数,这是语法规定。换句话说,定义派生类构造函数时最好指明基类构造函数;如果不指明,就调用基类的默认构造函数(不带参数的构造函数);如果没有默认构造函数,则编译失败。
派生类的析构函数也是在对象生命期结束时完成资源的清理工作,与构造函数类似,派生类也不能继承基类的析构函数,若想完成派生类中新增数据成员的资源释放,需要在派生类中定义析构函数。同样,若派生类中没有显式定义析构函数,编译系统会提供一个默认的析构函数。
析构函数的执行顺序与构造函数的执行顺序相反:
创建派生类对象时,构造函数的执行顺序和继承顺序相同,即先执行基类构造函数, 再执行派生类构造函数。
销毁派生类对象时,析构函数的执行顺序和继承顺序相反,即先执行派生类析构函 数,再执行基类析构函数。
有时候派生类需要根据自身特点改写从基类继承的函数
比如,动物都有叫声,在描述动物的类中可以定义speak()函数,不同的动物,叫声也不同,比如猫、狗都有特定叫声。若定义猫科类,该类继承自动物类,继承了speak()函数,但在猫科类中需要改写speak()函数,用于描述猫特有的叫声。
派生类中重新定义基类同名函数的方法,称为对基类函数的覆盖或改写,覆盖后基类同名函数在派生类中被隐藏。通过派生类对象调用与基类同名的函数时,调用的是自身新定义的函数,基类同名函数不被调用。
注意:
若派生类中存在与基类同名的成员函数,则不论参数类型、数量是否相同,基类函 数在派生类中被隐藏,通过派生类对象只能访问到派生类中定义的函数,若想访问 基类中的同名函数,需要通过作用域运算符“::”明确对基类函数的调用
在main()函数中,定义派生类后,定义基指针变量并让它指向派生类对象,通过派生类的构造函数创建对象。观察通过基类指针调用speak()函数,会有什么结果。
下面看一个例子
#include
using namespace std;
class Animal
{
public:
void speak()
{
cout << "animal叫"<speak();
}
输出结果
Dog叫
animal叫
从这个例子可以看出来,通过基类指针访问了基类的speak函数,由于p指针指向animal 基类,调用了基类的speak函数,若想要调用派生类的函数,则需要通过虚函数实现(将 基类同名函数定义为虚函数,在派生类中重定义与基类同名的函数)。这个多态会说。
如果派生类的成员(包括成员变量和成员函数)和基类的成员重名,则会屏蔽从基类继承过来的成员。也就是说在派生类中使用与基类同名的成员时(包括定义派生类时和通过派生类对象访问时),实际上使用的是派生类新定义的同名成员,而不是从基类继承来的。
前面我们已经介绍了什么是多重继承,我们这里说一下多重继承下派生类构造函数的使用以及多重继承中的二义性问题
与单继承中派生类的构造函数类似,多重继承派生类的构造函数不但要对派生类中 新增成员完成初始化,还要对继承各基类的成员进行初始化。
多重继承派生类构造函数需要调用该派生类的所有基类构造函数。派生类构造函数 对各基类构造函数的调用顺序,按定义派生类时基类名称的先后顺序,依次调用。
格式:
派生类名::派生类构造函数名(参数总表):基类1构造函数名(参数表1), 基 类2构造函数名(参数表2), …
{
派生类构造函数体
}
说明:
“参数总表”包含了其后各基类构造函数需要的所有参数,以及派生类新增 数据成员初始化所需的参数。
强调:
基类构造函数的调用顺序和它们在派生类构造函数列表中出现的顺序无关,而是和定义派生类时基类出现的顺序相同。
下面我们看一个例子
#include
using namespace std;
class A
{
public:
void speak()
{
cout << "A基类函数调用"<
我们把这段代码放到编译器中会发现24行也就是派生类调用speak函数那里报错了,为什么会报错呢?我们看一下报错原因
可以看出,报错原因是D::speak不明确,为什么不明确呢,因为D派生类继承了A,B两个基类,所以D拥有了A,B的两个speak函数,当在派生类调用speak函数,系统不知道调用的是A基类的speak函数还是B基类的speak函数,这个就是多重继承中的二义性
怎么解决呢,显示指明就行,在函数调用前面加上A::或者B::就行
#include
using namespace std;
class A
{
public:
void speak()
{
cout << "A基类函数调用"<
这样就不会报错了,在函数调用前面加上作用限定符”::”明确函数所属类,消除二义性
还有一种二义性是派生类的多个直接基类,又共同继承自一个上层基类(公共基类),则派生类的每个直接基类中都会包含上层基类的一份数据成员,使得派生类中存在公共基类中可访问数据成员的多份拷贝,若派生类访问这种数据成员,则出现二义性问题。
这个也就是典型的菱形继承问题
类A派生出类B和类C,类D继承自类B和类C,这时类A中的成员变量和成员函数继 承到类D中变成了两份,一份来自A-->B-->D这条路径,另一份来自A-->C-->D这条路径。
这样派生类在访问A的数据成员时就会出现二义性。
怎么解决呢?
消除这种二义性问题的方法:是在定义派生类的直接基类时,在继承方式前通过virtual 关键字声明其上层间接基类Animal为虚基类。用virtual修改的继承为虚继承。
让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类,本例中的A就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
注意:虚派生只影响从指定了虚基类的派生类(类B和类C)中进一步派生出来的类(类 D),不会影响派生类(类B和类C)本身。
C++标准库中的iostream类就是一个虚继承的实际应用案例。iostream 从 stream 和ostream 直接继承而来,而istream和ostream又都继承自一个共同的名为 base_ios的类,是典型的菱形继承。此时istream和ostream必须采用虚继承,否 则将导致iostream类中保留两份base_ios类的成员。
对于普通基类来说,派生类构造函数只负责调用其直接基类的构造函数,若直接基类还有它的更上层的基类,则依次调用各层基类的构造函数,但对于虚基类的派生类来说,其构造函数不仅直接调用其直接基类的构造函数,还需要调用虚基类的构造函数。
对于虚基类的派生类的构造函数,C++编译器的调用方法是:由最后定义的派生类, 即类的层次结构中最低层的派生类在定义对象时完成虚基类的构造函数的调用,该派生类的其它基类对虚基类的构造函数的调用被忽略。
在虚继承中,虚基类是由最终的派生类初始化的,换句话说,最终派生类的构造函数必须要调用虚基类的构造函数。对最终的派生类来说,虚基类是间接基类,而不是直接基类。与普通继承不同,在普通继承中,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。
例:
#include
using namespace std;
class A
{
public:
A(int a)
{
cout << "基类A的值为" << a << endl;
}
};
class B:virtual public A
{
public:
B(int a, int b) :A(a)
{
cout << "调用B类函数,值为" << b << endl;
}
};
class C :virtual public A
{
public:
C(int a, int c) :A(a)
{
cout << "调用C类函数,值为" << c << endl;
}
};
class D :public B, public C
{
public:
D(int a, int b, int c, int d) :A(a), B(90,b), C(100,c)
{
cout << "调用D类函数,值为" << d << endl;
}
};
int main()
{
D d(50, 60, 70, 80);
}
输出结果
基类A的值为50
调用B类函数,值为60
调用C类函数,值为70
调用D类函数,值为80
在最终派生类D的构造函数中,除了调用B和C的构造函数,还调用了A的构造函数,这说明D不但要负责初始化直接基类B和C,还要负责初始化间接基类A。而在普通继承中,派生类的构造函数只负责初始化它的直接基类,再由直接基类的构造函数初始化间接基类,用户尝试调用间接基类的构造函数将导致错误。
C++规定必须由最终的派生类D来初始化虚基类A,直接派生类B和C对A的构造函数的调用是无效的。在D d(50, 60, 70, 80)中,先调用A的构造函数,然后调用B的构造函数,可以发现我们给B的传参是B(90,b),但是根据输出结果可以发现,B的构造函数输出了,传给A的参数90并没有输出,即A的构造函数没有调用,同样C也是一样。
所以对于虚继承,必须由最终的派生类D来初始化基类A,派生类B,C对A的构造函数的调用是无效的。