14.1 C++类-成员函数、对象复制与私有成员
14.2 C++类-构造函数详解、explicit与初始化列表
14.3 C++类-inline、const、mutable、this与static
14.4 C++类-类内初始化、默认构造函数、“=default;”和“=delete;”
14.5 C++类-拷贝构造函数
14.6 C++类-重载运算符、拷贝赋值运算符与析构函数
14.7 C++类-子类、调用顺序、访问等级与函数遮蔽
14.8 C++类-父类指针、虚/纯虚函数、多态性与析构函数
14.9 C++类-友元函数、友元类与友元成员函数
14.10 C++类-RTTI、dynamic_cast、typeid、type-info与虚函数表
14.11 C++类-基类与派生类关系的详细再探讨
14.12 C++类-左值、右值、左值引用、右值引用与move
14.13 C++类-临时对象深入探讨、解析与提高性能手段
14.14 C++类-对象移动、移动构造函数与移动赋值运算符
14.15 C++类-继承的构造函数、多重继承、类型转换与虚继承
14.16 C++类-类型转换构造函数、运算符与类成员指针
很多类之间有一种层次关系,有父亲类(简称父类/基类/超类),有孩子类(简称子类/派生类)。例如卡车和轿车,它们都是车,既然是车,就有一些共性,比如说都烧油,都有轮子,都在机动车道上行驶。
细想一想,可以定义一个车的类,把这个车的类当成父类,从这个父类派生出卡车、轿车等,那么,卡车类、轿车类就属于子类。
有父类,有子类,这种层次关系就叫“继承”(也叫“继承性”)!也就是说子类能够从父类那里继承很多东西,“继承”这种概念(或者称为“性质”)是面向对象程序设计的核心思想之一。在13.1.1节也曾提及过。
这种继承需要先定义一个父类,父类中主要是定义一些公共的成员变量和成员函数,然后通过继承这个父类来构建新的类,这个新的类称为子类,通过继承,父类中的很多成员变量和成员函数就自动继承到了子类中,那么,在书写子类的时候就减少了很多编码工作量——只写子类中独有的一些内容即可。所以,通常来讲,子类会比父类有更多的成员变量以及成员函数,换句话说,子类一般比父类更庞大(创建子类对象时占用的内存空间更多)。
现在来定义一个父类,名字为Human。专门创建Human.h和Human.cpp来定义和实现这个父类。
Human.h中,内容如下:
#ifndef __HUMAN__
#define __HUMAN__
class Human
{
public:
Human();
Human(int);
public:
int m_Age;
char m_Name[100];
}
#endif
Human.cpp中,内容如下(记得要把Human.cpp加入到项目中来):
Human::Human()
{
std::cout << "执行了Human::Human()构造函数" << std::endl;
}
Human::Human(int age)
{
std::cout << "执行了Human::Human(int age)构造函数" << std::endl;
}
再来定义一个子类,名字为Men。专门创建Men.h和Men.cpp来定义和实现这个子类。注意观察子类的写法。
Men.h中,内容如下:
#ifndef __MEN__
#define __MEN__
class Men : public Human
{
public:
Men();
};
#endif
Men.cpp中,内容如下(记得要把Men.cpp加入到项目中来):
#include "Men.h"
#include
Men::Men()
{
std::cout << "执行了Men::Men()构造函数" << std::endl;
}
编译一下整个项目,没有什么错误,一切正常。
现在,请注意Men.h中的下面这行代码:
class Men : public Humen
上面这行代码的含义是定义一个子类Men,派生自父类Human。定义子类的一般形式为:
class 子类名:继承方式 父类名
· 继承方式(访问等级/访问权限):public、protected、private之一,后面会详细介绍。
· 父类名:已经定义了的一个类名。一个子类可以继承自多个父类,但比较少见。一般的继承关系都是子类只继承自一个父类。所以这里就先只研究继承自一个父类的情形。
在MyProject.cpp的开头部分包含Men.h文件:
#include "Men.h"
main主函数中,编写如下代码:
读者可以任意设置断点并进行逐行跟踪,可以看到,整个程序的运行结果是先执行父类构造函数的函数体,再执行子类构造函数的函数体。这个执行顺序,请牢记,如图14.9所示。
前面曾经讲解过public和private这两种访问权限(访问等级/访问权限),专门用于修饰类中的成员,public表示“公共”的意思,private表示“私有”的意思,当时没有提及protected,其实protected是“保护”的意思,访问等级介于public和private之间,一般有父子关系的类谈protected才有意义。
现在对这三种访问权限修饰符(专用于修饰类中的成员变量、成员函数)进行一下总结:
· public:可以被任意实体所访问。
· protected:只允许本类或者子类的成员函数来访问。
· private:只允许本类的成员函数访问。
现在再来看看刚刚讲到的类继承时的继承方式(专用于子类继承父类),也依然是这三种:
· public继承。
· protected继承。
· private继承。
针对三种访问权限以及三种继承方式,笔者总结了一张比较详细的表14.1,这个表不要求死记硬背,但要求能够理解,在需要的时候随时查阅即可。
总结:
(1)子类public继承父类,则父类所有成员在子类中的访问权限都不发生改变;
(2)protected继承将父类中public成员变为子类的protected成员;
(3)private继承使得父类所有成员在子类中的访问权限变为private;
(4)父类中的private成员不受继承方式的影响,子类永远无权访问;
(5)对于父类来讲,尤其是父类的成员函数,如果你不想让外面访问,就设置为private;如果你想让自己的子类能够访问,就设置为protected,如果你想公开,就设置为public。
现在有了表14.1,可以在Human类的定义中增加一些代码以方便进一步学习研究。修改Human.h文件以定义一些protected和private修饰的成员变量和成员函数,修改后的Human.h文件看起来如下:
class Human
{
public:
Human();
Human(int);
public:
int m_Age;
char m_Name[100];
void funcpub() {};
protected:
int m_pro1;
void funcpor() {};
private:
int m_priv1;
void funcpriv() {};
};
而后,可以将Men类对Human类的继承方式分别从原来的public修改为protected、private,并在main主函数中书写一些代码,测试一下对类中各种成员变量、成员函数的访问权限。
举个例子:假如Men类继承Human类的继承方式是protected,也就是如下:
class Men : Protected Human{...}
那么,如果在main主函数中书写如下测试代码,是否会有问题?
{
Men men;
men.m_Age = 10;
cout << men.m_Age << endl;
men.m_priv1 = 15;
cout << men.m_priv1 << endl;
}
上面的代码针对m_Age成员变量的访问和针对m_priv1成员变量的访问都有问题。因为:
(1)m_Age在Human中被定义为public类型,而Men类是用protected继承方式来继承Human类的。查表14.1中间部分(竖着查看每一列),父类的访问权限为public,子类的继承方式为protected,从而得到子类的访问权限是protected,继续查表14.1左上角,protected访问权限只允许在本类或者子类的成员函数中访问,不允许在main主函数中直接访问,也就是说,如下两行语句都是有问题的:
men.m_Age = 10;
cout << men.m_Age << endl;
(2)同理,m_priv1在Human中被定义为private类型,而Men类是用protected继承方式来继承Human类的。查表14.1,父类的访问权限为private,子类的继承方式为protected,从而得到子类无权访问,这意味着m_priv1就好像完全不存在于子类Men中(无法用Men类对象访问m_priv1),所以,在main主函数中出现下面两行代码是绝不可以的:
men.m_priv1 = 15;
cout << men.m_priv1 << endl;
正常情况下,父类中的成员函数只要是用pubic或者protected修饰的,子类只要不采用private继承方式来继承父类,那么子类中都可以调用。
但是,在C++的类继承中,子类会遮蔽父类中的同名函数,不论此函数的返回值、参数。也就是说,父类和子类中的函数只要名字相同,子类中的函数就会遮蔽掉父类中的同名函数。
看如下代码,在父类Human.h的Human类定义中,增加两个public修饰的成员函数的声明:
public:
void samenamefunc();
void samenamefunc(int);
在Human.cpp中,增加这两个成员函数的实现代码:
void Human::samenamefunc()
{
std::cout << "执行了Human::samenamefunc()" << std::endl;
}
void Human::samenamefunc(int)
{
std::cout << "执行了Human::samenamefunc(int)" << std::endl;
}
在子类Men.h的Men类定义中,增加一个public修饰的成员函数的声明:
public:
void samenamefunc(int);
在Men.cpp中,增加这个成员函数的实现代码:
void Men::samenamefunc(int)
{
std::cout << "执行了void Men::samenamefunc(int)" << std::endl;
}
在main主函数中,增加如下代码:
{
Men men;
men.samenamefunc();//报错,无法调用父类中有不带参数的samenamefunc函数
men.samenamefunc(1); //遗憾,只能调用子类中带一个参数的samenamefunc函数,无法调用父类带一个参数的samenamefunc函数
}
通过上面的范例可以看到,只要子类中有一个和父类同名的成员函数,那么,通过子类对象,完全无法调用(访问)父类中的同名函数。
如果确实想调用父类中的同名函数,能办到吗?可以,可以借助子类的成员函数samenamefunc来调用父类的成员函数samenamefunc,只要在子类的成员函数samenamefunc中使用“父类::成员函数名(…)”的方式就可以调用父类的samenamefunc函数。例如,修改Men.cpp中Men类的成员函数samenamefunc的代码:
void Men::samenamefunc(int)
{
Human::samenamefunc(); //可以调用父类的无参的samenamefunc函数
Human::samenamefunc(120); //可以调用父类的带一个参数的samenamefunc函数
std::cout << "执行了void Men::samenamefunc(int)" << std::endl;
}
当然,如果子类Men是以public继承方式来继承Human父类,那么也可以在main主函数中用“子类对象名.父类名::成员函数名(…)”的方式来调用父类的同名成员函数。看如下代码:
{
Men men;
men.Human::samenamefunc(); //调用父类中不带参数的samenamefunc函数
men.Human::samenamefunc(160); //调用父类中带一个参数的samenamefunc函数
}
另外,通过13.2.1节的学习已经知道,using namespace一般用于声明(使用)命名空间。在C++11中,还可以通过using这个关键字让父类同名函数在子类中可见。换句话说就是“让父类的同名函数在子类中以重载方式使用”。
现在,在Men.h中的Men类内书写如下代码行,注意用public修饰符:
public:
using Human::samenamefunc; //using声明让父类函数在子类可见
现在,在main主函数中写如下代码:
{
Men men;
men.samenamefunc(1); //执行子类的samenamefunc函数,虽然父类也有带一个参数的该函数,但子类函数还是覆盖了父类同名函数
men.samenamefunc(); //执行父类的不带参函数的samenamefunc函数
}
通过上面的代码可以看到,可以直接调用父类的samenamefunc不带参数的方法了。有几点说明:
(1)这种using声明只能指定函数名,不能带形参列表,并且父类中的这些函数必须都是public或者protected(只能在子类成员函数中调用),不能有private的,否则编译会出错。换句话说,是让所有父类的同名函数在子类中都可见,而无法只让一部分父类中的同名函数在子类中可见。
(2)using声明这种方法引入的主要目的是实现可以在子类实例中调用到父类的重载版本的函数。
再回忆一下重载函数的概念:重载函数就是函数名字相同,但函数的参数类型或者参数个数并不相同。
如果子类中的成员函数和父类中的同名成员函数参数个数、参数类型完全相同,那么是无法调用到父类中的该函数的。例如,上面men.samenamefunc(1);是无法调用到父类的带一个参数的成员函数samenamefunc的。这时如果要在main主函数中调用父类的带一个参数的成员函数samenamefunc,就应该这样调用:
men.Humen::samenamefunc(160);
如果是在Men子类的成员函数中调用父类的带一个参数的成员函数samenamefunc,就应该这样调用:
men.Humen::samenamefunc(120);
虽然子类确实可以调用父类的同名函数,但这样做的实际意义值得商榷,如果子类覆盖了父类的同名成员函数,一般来讲子类对象都应该不想调用父类的同名成员函数吧!