目录
继承的基本概念
定义
使用方法
内存空间
继承下构造·析构执行的顺序
构造函数
析构函数
继承的优点
继承方式
public
protected
private
隐藏
回顾函数重载
隐藏
父类指针指向子类对象
优点
出现的问题
类成员函数指针
普通的函数指针
类成员函数指针
类成员函数与普通的函数的区别
举例
解决问题
继承的基本概念
定义
被继承的类叫做基类(父类),继承的类叫派生类(子类),在子类类名后面加:继承方式 父类
class CFather {
};
class CSon :public CFather {
};
使用方法
通过继承关系,子列可以使用父类的成员。如果子类和父类有同名的成员,默认使用子类的成员,如果想要使用父类成员,需要在成员名前加上类名::用于显式的指定区分。
父类:
class CFather {
public:
int m_fa;
int m_money;
void funFather() {
cout << __FUNCTION__ << endl;
}
void fun() {
cout << "CFather::fun" << endl;
}
};
子类:
class CSon :public CFather {
public:
int m_money;
CSon() {
m_fa = 1;
CFather::m_money = 100;//父类的m_money
CSon::m_money = 200;//子类的m_money
m_money = 300; //默认使用的是子类的成员
}
void funSon() {
cout << m_fa << endl;
funFather();
}
void fun() {
cout << "CSon::fun" << endl;
}
};
测试:
创建一个子类对象,通过子类成员函数调用父类的成员属性,在子类中的构造函数中也可给父类的成员属性初始化值。当子类与父类有同名成员时,可以通过类名作用域来区分,默认使用的是当前类对象的成员。
int main()
{
CSon son;
son.funSon();
cout << son.CFather::m_money << endl; //100
cout << son.CSon::m_money << endl; //300
son.fun();//默认子类
son.CFather::fun();//父类
son.CSon::fun();//子类
return 0;
}
内存空间
子类继承父类,相当于父类的成员包含到自己的类里,所以定义子类对象所占用的空间大小除了子类自身的成员,还包括父类的成员。成员在内存空间分布为:先父类成员后子类成员,而每个类中的成员分布与在类中声明的顺序一致。
内存大小:
cout << sizeof(CFather) << " " << sizeof(CSon) << endl; //8 12
内存分配:
CSon son;
cout << &son << endl;//00D4FC6C
cout << &son.m_fa << " " << &son.CFather::m_money << " " << &son.m_money << endl;//00D4FC6C 00D4FC70 00D4FC74
继承下构造·析构执行的顺序
构造函数
执行的顺序:父构造->子构造(函数体代码)
构造顺序说明:在子类创建对象的时候,会先跳转到子类的构造函数(注意这里并不是直接先执行父类的构造函数),先执行构造的初始化参数列表,初始化顺序看在类中声明的先后顺序(等同于成员在内存排布的先后顺序),由于父类在前,子类成员在后,所以先初始化父类成员,调用父类的构造函数。初始化完毕后,再回到子类初始化参数列表,按照顺序继续初始化其他成员,所有成员初始化完毕后,再执行子类自己的构造函数体代码。
注意1:在初始化列表中会默认调用父类的无参构造初始化父类成员,如果父类只有带参数的构造,在子类的初始化列表中必须显示的指定父类构造进行初始化。这有点像之前说的组合关系形式。
注意2:不能在子类构造函数的初始化参数列表中直接对父类中的成员属性初始化,更不能在子类构造函数的函数体代码中初始化(实际是赋值操作),因为如果在创建对象时要创建父类对象,那么将不会调用子类的构造函数,而父类中又没有构造函数。
默认调用父类无参构造:
class CFather {
public:
int m_fa;
CFather() :m_fa(3) {
cout << __FUNCTION__ << endl;
}
};
class CSon :public CFather {
public:
int m_son;
CSon():/*CFather(),编译器会默认调用父类的无参构造*/m_son(1)/*,m_fa(0)*/ {
m_fa = 0;//赋值操作
cout << __FUNCTION__ << endl;
}
};
手动显示指定调用父类的构造:
class CFather {
public:
int m_fa;
CFather() :m_fa(3) {
cout << __FUNCTION__ << endl;
}
};
class CSon :public CFather {
public:
int m_son;
CSon() :CFather(),/* 手动显示指定调用父类的构造 */m_son(1) {
cout << __FUNCTION__ << endl;
}
};
父类中没有无参构造时,必须手动显式指定出来:
class CFather {
public:
int m_fa;
CFather() :m_fa(3) {
cout << __FUNCTION__ << endl;
}
};
class CSon :public CFather {
public:
int m_son;
CSon(int a) :CFather(a), m_son(1) {
}
};
如果父类中存在多个构造函数,想使用带参数的构造函数,也需要手动指定,否则会调用无参的父类构造了
class CFather {
public:
int m_fa;
CFather() :m_fa(3) {
cout << __FUNCTION__ << endl;
}
CFather(int a) :m_fa(a) {
cout << __FUNCTION__ << endl;
}
};
class CSon :public CFather {
public:
int m_son;
CSon(int a) :CFather(a), m_son(1) {
}
};
析构函数
执行的顺序:子析构->父析构
析构顺序说明:子类对象的生命周期结束后,因为是子类所以自动调用子类析构,当析构执行完了,才会回收对象分配的空间(和构造的顺序相反),当然这个空间包含创建的父类成员,那么回收父类匿名对象前,会自动调用父类的析构,再回收父类对象本身。如果是new出来的子类对象,同理。
class CTest {
public:
int m_tst;
public:
CTest(int a) :m_tst(a) {
cout << __FUNCTION__ << endl;
}
~CTest() {
cout << __FUNCTION__ << endl;
}
};
class CFather {
public:
int m_fa;
public:
CFather(int a) :m_fa(a) {
cout << __FUNCTION__ << endl;
}
~CFather() {
cout << __FUNCTION__ << endl;
}
};
class CSon :public CFather {
public:
CTest tst;
int m_son;
public:
CSon(int a) :tst(a + 2), CFather(a + 1), m_son(a) {
cout << __FUNCTION__ << endl;
}
~CSon() {
cout << __FUNCTION__ << endl;
}
};
int main() {
CSon son(1);
return 0;
}
上三行为构造函数,下三行为析构函数,构造函数和析构函数执行的顺序相反。
继承的优点
可以将一些功能比较相似的类中公共的成员,抽离出来形成一个类,即是一个父类,这些子类继承父类,可以使用父类成员,公共的成员在父类中只维护一份代码即可,如果要添加新的公共的成员,只需要在父类中添加一份即可。增加新的子类公共的成员不必再重复写了,增加了程序代码的复用性、扩展性、灵活性,减少代码冗余。
class CPeople {
public:
int m_money;
CPeople():m_money(100){}
void cost(int n) {
cout << __FUNCTION__ << endl;
m_money -= n;
}
void play() {
cout << "溜溜弯" << endl;
}
void drink() {//如果增加公共的功能,属性,只需要在父类中增加一份即可
cout << "喝饮料" << endl;
}
};
class CWhite : public CPeople{
public:
//CWhite():m_money(100){}
void eat() {
cout << "吃牛排" << endl;
}
};
class CYellow : public CPeople {
public:
void eat() {
cout << "小烧烤" << endl;
}
};
class CBlack : public CPeople {
public:
void eat() {
cout << "西瓜炸鸡" << endl;
}
};
class CBlue : public CPeople {
public:
void eat() {
cout << "吃另一个阿凡达" << endl;
}
};
继承方式
继承方式描述了父类成员在子类中的访问控制,即所能使用的一个范围。继承方式与访问修饰符共同决定了父类成员在子类中所表现的属性。
三种继承方式:public、protected、private
创建一个父类:
class CFather {
public:
int m_pub;
protected:
int m_pro;
private:
int m_pri;
public:
CFather():m_pub(10),m_pro(20),m_pri(30){}
int GetPri() { return m_pri; }
};
public
父类 子类
public -> public
protected -> protected
private -> 不可访问
class CSon : public CFather {
public:
void funPublic() {
cout << m_pub << endl; //公共的
cout << m_pro << endl; //保护的
//cout << m_pri << endl; //父类中私有的不可访问
cout << GetPri() << endl;
}
};
protected
父类 子类
public -> protected
protected -> protected
private -> 不可访问
class CSon : protected CFather {
public:
void funPublic() {
cout << m_pub << endl; //保护的
cout << m_pro << endl; //保护的
//cout << m_pri << endl; //父类中私有的不可访问
cout << GetPri() << endl;
}
};
private
父类 子类
public -> private
protected -> private
private -> 不可访问
class CSon : private CFather {
public:
void funPublic() {
cout << m_pub << endl; //私有的
cout << m_pro << endl; //私有的
//cout << m_pri << endl; //父类中私有的不可访问
cout << GetPri() << endl;
}
};
隐藏
回顾函数重载
对应于函数重载而言,我们调用的时候,可以根据参数类型、参数个数,编译器自动匹配调用哪个函数。
同样如果在一个类中存在两个同名函数(参数列表不同),那么也可以根据调用者传递的参数自动的区分执行哪个函数,因为也是一个函数重载的关系。
正常重载:
void fun() {
cout << __FUNCTION__ << endl;
}
void fun(int a) {
cout << __FUNCTION__ << " " << a << endl;
}
fun();
fun(10);
类内和类外的重载:
由于类中的成员函数是由对象调用的,所以两者也互不影响
class CSon : public CFather {
public:
void fun() {
cout << __FUNCTION__ << endl;
}
void fun(int a) {
cout << __FUNCTION__ << " " << a << endl;
}
};
CSon son;
son.fun();
son.fun(20);
隐藏
对于父类和子类中,如果有同名的函数但是参数列表不同,则不能够自动区分匹配,因为他们之间的关系并不是函数重载的关系,作用域不同,必须使用类名作用域去区分该调用哪个函数。这种关系称之为隐藏。
简单来说就是子类和父类中同名的成员互为隐藏的关系,当子类的同名成员出现时,父类的成员被隐藏,要想使用需要显示指定父类类名作用域。
class CFather {
public:
void fun(/* CFather* const this */int a, int b) {
cout << __FUNCTION__ << " " << a << " " << b << endl;
cout << this << endl;
}
};
class CSon : public CFather {
public:
void fun() {
cout << __FUNCTION__ << endl;
}
void fun(int a) {
cout << __FUNCTION__ << " " << a << endl;
}
};
CSon son;
son.fun();
son.fun(20);
//son.fun(30, 40); //fun优先匹配子类,子类无对应的,报错
son.CFather::fun(30, 40);//匹配父类的
子类同名的成员注释掉后,父类成员显现:
class CFather {
public:
void fun(/* CFather* const this */int a, int b) {
cout << __FUNCTION__ << " " << a << " " << b << endl;
cout << this << endl;
}
};
class CSon : public CFather {
public:
//void fun() {
// cout << __FUNCTION__ << endl;
//}
//void fun(int a) {
// cout << __FUNCTION__ << " " << a << endl;
//}
};
CSon son;
son.fun(10, 20); //子类同名的成员注释掉后,父类成员显现
测试:父类指针指向子类的地址和父类中函数成员自身的this指针指向的地址相同。
CSon son;
son.fun(10, 20);
CFather* const pthis = &son;
cout << pthis << endl;
父类指针指向子类对象
刚才我们也说到,父类的指针可以不用强转直接指向子类对象,保证了子类对象能够成功调用父类函数,这么做的好处是:父类的指针可以统一多个类的类型,提高代码的复用性、扩展性。
那么我们还是用刚才举过的例子来测试:
class CPeople {
public:
int m_money;
CPeople():m_money(100){}
void cost(int n) {
cout << __FUNCTION__ << endl;
m_money -= n;
}
void play() {
cout << "溜溜弯" << endl;
}
void drink() {//如果增加公共的功能,属性,只需要在父类中增加一份即可
cout << "喝饮料" << endl;
}
};
class CWhite : public CPeople{
public:
//CWhite():m_money(100){}
void eat() {
cout << "吃牛排" << endl;
}
};
class CYellow : public CPeople {
public:
void eat() {
cout << "小烧烤" << endl;
}
};
class CBlack : public CPeople {
public:
void eat() {
cout << "西瓜炸鸡" << endl;
}
};
优点
我们先来测试一下父类指针指向子类对象的好处:
当要创建一个家庭类时,如果已知家里有两个人,但是不明都是什么人,要是通过子类指针创建的话,就需要6个指针:
class CFamily {
public:
// 2个人 需要6个指针
CWhite* pW1;
CWhite* pW2;
//...
//...
};
但是如果通过父类指针创建,就可以 只创建两个指针,并且可以合并成数组。
class CFamily {
public:
CPeople* arrPeo[2];
};
出现的问题
我们如果想要创建函数,然后在函数中通过子类指针调用子类对象中的成员函数,那么会出现代码冗余的现象:
void fun(CWhite *pw) {
pw->cost(10);
pw->eat();
pw->play();
pw->drink();
}
void fun(CYellow* pw) {
pw->cost(10);
pw->eat();
pw->play();
pw->drink();
}
fun(new CWhite);
fun(new CYellow);
所以我们会想到用父类指针指向子类对象的成员函数,这样就可以统一多种子类类型,提高代码复用性,扩展性。
void fun(CPeople* pPeo) {
pPeo->cost(10);
//(CWhite*)pPeo->eat(); //不是一个通用的方法,待解决
pPeo->play();
pPeo->drink();
}
fun(new CWhite);
fun(new CYellow);
不过这里会出现一个问题,就是不可以通过父类指针调用子类中不统一的函数,如果强转成子类指针也不是一个通用的方法,所以应该怎么解决呢?
所以要引入一个新的知识点:类成员函数指针
类成员函数指针
调用函数的两种方式:通过函数名直接调用、通过函数指针间接调用。
通过函数指针调用的好处:真正的函数指针可以将实现同一功能的多个模块统一起来标识,使系统结构更加清晰,后期更容易维护。或者归纳为:便于分层设计、利于系统抽象、降低耦合度以及使接口与现实分开,提高代码的复用性、扩展性。
普通的函数指针
void fun() {
cout << __FUNCTION__ << endl;
}
void (*p_fun) () = &fun;
(*p_fun)();
定义函数指针有时看起来比较繁琐、可读性差一些,我们可以用typedef进行优化。
typedef void (*P_FUN) ();
P_FUN p_fun2 = &fun;
(*p_fun2)();
类成员函数指针
class CTest {
public:
void fun(/* CTest * const this */) {
cout << __FUNCTION__ << endl;
}
};
void (CTest::*p_fun3) () = &CTest::fun;
(tst .* p_fun3)();
CTest* pTst = new CTest;
(pTst->*p_fun3)();
类成员函数与普通的函数的区别
1.所属的作用域不同,类成员函数标识了所属的类,必须通过对象调用(虽然可以是空指针对象,但必须得有)。
2.类成员函数编译器会默认加上一个隐藏的参数:this指针,而普通函数没有。
所以定义类成员函数的指针与普通的函数指针肯定会有所区别:C++提供了三种运算符::*、.*、->*用于定义和使用类成员函数指针。
注意:
定义类成员函数指针,格式: 类名 整体操作符(::*) 指针名
.* 和 ->* 都是C++提供的整体操作符
如果创建的是正常的类对象,那么对象就用.*调用类成员函数指针 指向函数
如果创建的是指针类型的对象,那么对象就用->*调用类成员函数指针 指向函数
举例
先定义一个父类和子类:
class CFather {
public:
};
class CSon : public CFather {
public:
void fun() {
cout << __FUNCTION__ << endl;
}
};
如果要通过父类指针指向并调用子类对象中特有的成员函数时,我们首先要创建一个类成员函数指针:
CFather* pFa = new CSon;
//pFa->fun();
//-------------------------------
void(CFather :: * p_fun)() = (void(CFather :: * )())&CSon::fun;
(pFa->*p_fun)();
这里要注意的是:我们创建的是父类成员函数指针,指向的是子类成员函数的地址,由于两边的类型不同,所以要将右边的子类成员函数地址强转为父类成员函数指针类型。(子类转父类是统一的过程,而父类转子类则是发散的过程)。
这么写代码过于复杂,可以通过typedef来简化一下:
typedef void(CFather ::* P_FUN)();
P_FUN p_fun2 = (P_FUN)&CSon::fun;
(pFa->*p_fun2)();
解决问题
在学过类成员函数指针后,我们就可以来解决之前父类指针不能调用子类对象的特有成员函数的问题了。
我们先用typedef来创建一个父类的类成员函数指针类型:
typedef void(CPeople::* P_FUN)();
然后在函数中就可以多创建一个类成员函数指针的形参,这样传入子类的类成员函数地址后,就可以通过父类指针调用类成员函数指针指向子类成员的函数了。
void fun(CPeople* pPeo,P_FUN p_fun) {
pPeo->cost(10);
//(CWhite*)pPeo->eat(); //不是一个通用的方法,待解决
(pPeo->*p_fun)();
pPeo->play();
pPeo->drink();
}
int main() {
fun(new CYellow, (P_FUN)&CYellow::eat);
fun(new CBlack, (P_FUN)&CBlack::eat);
return 0;
}
注意别忘了传入实参时要将子类成员函数的地址强转为父类的类成员函数指针类型。
这里父类指针给人的感觉就是:同一根指针在不同的调用情况它具有了不同子类的形态,那就是一个多态。