面向对象程序设计中最重要的一个概念就是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行时间的效果。
当创建一个类时,不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类,又称为父类,新建的类称为派生类,又称为子类。
一、派生类的定义格式和声明:
class Base
{
public:
void fun1()
{
cout << _b << endl;
}
private:
int _b;
};
class Derive :public Base//声明派生类
{
public:
void fun2()
{
cout << _d << endl;
}
private:
int _d;
};
可以看到,有三种继承关系,分别是:公有继承、保护继承和私有继承,(其中如果不给出继承权限,则class默认为私有继承,struct默认为公有继承)
二、三种形式的继承:
那这三种继承关系对类中的访问限定符有什么影响吗?来看看一个表就可以很清楚的知道
注:这里仅仅表达基类的成员,被public,protected,private三种方式继承后,在原基类为public,protectedc,private的成员在继承类里类型为表格里的内容
1、公有(public)继承
class Base
{
public:
Base()
{
cout << "Base()" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
int _pub;
protected:
int _pro;
private:
int _pri;
};
class Derive :public Base
{
public:
Derive()
{
cout << "Derive()" << endl;
}
~Derive()
{
cout << "~Derive()" << endl;
}
void fun()
{
cout << "_pub=" << _pub << endl;
cout << "_pro=" << _pro << endl;
cout << "_pri=" << _pri << endl;//报错 基类的私有成员在派生类中无法访问
}
};
void FunTest()
{
Derive d1;
d1._pub = 1;
d1._pro = 2;//报错 在类外无法访问公有继承的保护成员
d1._pri = 3;//报错 在类外无法访问公有继承的私有成员
}
2.保护(protected)继承
class Base
{
public:
Base()
{
cout << "Base()" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
int _pub;
protected:
int _pro;
private:
int _pri;
};
class Derive :protected Base
{
public:
Derive()
{
cout << "Derive()" << endl;
}
~Derive()
{
cout << "~Derive()" << endl;
}
void fun()
{
cout << "_pub=" << _pub << endl;
cout << "_pro=" << _pro << endl;
cout << "_pri=" << _pri << endl;
}
};
void FunTest()
{
Derive d1;
d1._pub = 1;
d1._pro = 2;
d1._pri = 3;
}
运行代码后发现下图错误
之前可以在类外访问的公有成员也不能访问了,而在派生类中还可以继续访问说明在保护继承里,公有成员继承之后以及变为了保护成员
3、私有(private)继承
class Base
{
public:
Base()
{
cout << "Base()" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
int _pub;
protected:
int _pro;
private:
int _pri;
};
class Derive :private Base
{
public:
Derive()
{
cout << "Derive()" << endl;
}
~Derive()
{
cout << "~Derive()" << endl;
}
void fun()
{
cout << "_pub=" << _pub << endl;
cout << "_pro=" << _pro << endl;
cout << "_pri=" << _pri << endl;
}
};
void FunTest()
{
Derive d1;
d1._pub = 1;
d1._pro = 2;
d1._pri = 3;
}
在私有继承中,从基类继承下来的东西全部就变为了派生类私有的,所以你在外部完全访问不了,或许有人认为在外部访问不了和protected继承好像没什么区别,你从再次创建一个派生类来访问它的protected成员时是可以访问的,但是private成员就访问不了。
三、派生类中的构造函数和析构函数
class Base
{
public:
Base(int b)
:_b(b)
{
cout << "Base()" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
private:
int _b;
};
class Derive :private Base
{
public:
Derive(int b, int d)
:Base(b)
, _d(d)
{
cout << "Derive()" << endl;
}
~Derive()
{
cout << "~Derive()" << endl;
}
private:
int _d;
};
void FunTest()
{
Derive d1(1, 2);
Derive d2(3, 4);
}
int main()
{
FunTest();
system("pause");
return 0;
}
对程序进行调试之后会得到如下结果:
从图中可以看出创建一个派生类的对象调用构造函数的顺序为:
基类构造函数—>派生类中对象构造函数—>派生类构造函数
实际并不是如此,一步步进行调试的话就会发现真正的调用如下:
派生类构造函数(在初始化列表进行跳转)—>基类构造函数—>派生类中对象构造函数—>派生类构造函数体
同样的,析构函数调用顺序如下:
派生类析构函数—>派生类包含成员对象析构函数—>基类析构函数
对于派生类的构造函数再补充一下几点:
1.基类没有缺省的构造函数时,派生类必须要在初始化列表中显式给出基类名和参数列表
2.基类没有定义构造函数,则派生类也可以不用定义,全部使用缺省的构造函数
3.基类若定义了带有形参表的构造函数,派生类就一定要定义构造函数
四、友元函数和静态成员的继承
在之前的博客中介绍过有关友元函数的概念,在这里就不再说明。接下来验证一下友元关系是不能继承的
class Base
{
friend void fun();//在基类中定义一个友元函数
public:
Base(int b)
:_b(b)
{
cout << "Base()" << endl;
}
~Base()
{
cout << "~Base()" << endl;
}
private:
int _b;
};
class Derive :private Base
{
public:
Derive(int b, int d)
:Base(b)
, _d(d)
{
cout << "Derive()" << endl;
}
~Derive()
{
cout << "~Derive()" << endl;
}
private:
int _d;
};
void FunTest()
{
Derive d1(0, 0);
d1.fun();//尝试用派生类对象去访问基类中的友元函数
}
发现不能访问fun函数,说明了友元函数不能继承。
再来看看静态成员
class Base
{
public:
Base()
{
++_count;
}
protected:
int _a;
public:
static int _count;
};
int Base::_count = 0;
class Derive1 : public Base
{
protected:
int _b;
};
class Derive2 :public Derive1
{
protected:
int _c;
};
void FunTest()
{
Derive1 d1;
Derive1 d2;
Derive1 d3;
Derive2 d;
cout << "_count=" << Base::_count << endl;
Derive1::_count = 0;
cout << "_count=" << Base::_count << endl;
}
int main()
{
FunTest();
system("pause");
return 0;
}
在这段代码和结果中,我们可以看到的是静态成员是可以被继承下来的,一旦基类定义了一个静态成员,则整个继承体系中就只有一个这样的成员,无论派生出多少个子类都只有这一个static成员
五、继承体系中的作用域
先来看一段代码
class Base
{
public:
int _a;
int _b;
};
class Derive : public Base
{
public:
int _a;
int _c;
};
void FunTest()
{
Derive d1;
d1._a = 2;
}
我们会发现在基类和派生类中都有一个_a的数据成员,那用派生类对象去访问_a调用的是基类的还是派生类的呢?我们试着把派生类的成员改为私有的,发现再次去访问_a时出现错误,所以得到以下结论:
基类的同名成员在派生类中被屏蔽,成为“不可见”的(在子类成员函数中可以通过 基类::基类成员 来访问)
注:不同的成员函数,只有在函数名和参数个数相同,类型匹配的情况下才发生同名隐藏
六、赋值兼容规则—public继承
1、子类对象可以赋值给父类对象
2、父类对象不能赋值给子类对象
3、父类的指针或引用可以指向子类对象
4、子类的指针或引用不能指向父类对象(可以通过强制类型转换来实现,但不安全)
七、继承的分类
- 单继承
一个子类只有一个直接父类,如下图
在前面将继承的一些注意方面时,用的都是单继承,所以在这里就不再说明。
- 多继承
一个子类有两个或两个以上直接父类,如下
多继承需要注意的一点就是,在定义派生类的时候,每个继承下来的基类都要加上继承权限,否则就会默认为private继承。
- 菱形继承
菱形继承也是多重继承的一种方式,在这里主要来看一下菱形继承,如上图,是菱形继承的基本方式,上一个简单的例子来看看:
class B
{
public:
int _b;
};
class D1 :public B
{
public:
int _d1;
};
class D2 : public B
{
public:
int _d2;
};
class D :public D1, public D2
{
public:
int _d;
};
我们来考虑一个问题,最后的这个派生类D的大小是多少呢?
运行下面这个测试函数来看看结果:
void FunTest()
{
D d;
int a = sizeof(d);
cout << "派生类D的大小为:" << a << endl;
}
可以看到这个派生类的大小为20,可以分析一下:
1.D1继承了B中的成员_b
2.D2继承了B中的成员_b
3.D继承了D1中的成员_b和_d1
4.D继承了D2中的成员_b和_d2
5.D中有一个自己本身的成员_d占四个字节
所以在D中就有两份成员_b,一份是D1继承B的,一份是D2继承B的,这样D的大小为20 就可以解释了。但是这样就会有一个问题,来看看下面的图:
在这里我想利用d去访问每个成员并对它们进行赋值,却发生了访问不明确的错误。编译器无法知道你想赋值的_b是在D1中还是D2中,这就是菱形继承的缺陷:二义性和数据冗余问题
我们可以来看看菱形继承的对象模型来具体看看D中的成员都是如何存储的(刚刚的赋值问题,我们在前面加上相应的作用域就可以解决)
通过给每个成员赋值,并在内存窗口观察各个成员所处的位置,可以得出菱形继承的对象模型如下图:
其中D1类和D2类是按照继承顺序排列的,从图中也可以看出,D类中有两份_b,那么除了刚刚在访问该成员时加上对应作用域的方法,还有别的方法吗?我们再来看看另一种继承方式。
- 虚继承
来看一个虚继承的例子:
class B
{
public:
int _b;
};
class D1 :virtual public B
{
public:
int _d1;
};
class D2 :virtual public B
{
public:
int _d2;
};
class D :public D1, public D2
{
public:
int _d;
};
虚继承的书写方式,只需要在类派生列表前加上virtual关键字。需要注意的是,如果要解决菱形继承的二义性和数据冗余问题,virtual关键字必须加在D1类和D2类的继承上,将D1类和D2类变成虚基类,虚基类可以在继承共同基类时只保留一份成员。
同样的,我们再来看看变成虚继承之后,D类的对象模型会发生什么变化,先来看看D的大小有没有发生变化呢?
void FunTest()
{
D d;
cout << "D类的大小:" << sizeof(D) << endl;
d._b = 0;
d._d1 = 1;
d._d2 = 3;
d._d = 4;
}
我们会看到,D类的大小不但没有减少,还增加了四个字节。再来看看在内存中的情况:
观察上图,我们会发现,多了两个字节存放了两个地址,我们再来看看这两个地址中的内容:
我们发现,这两个地址都存放的0,在这里解释一下:其实这两个地址中分别存放的是D1类和D2类相对于_b的偏移量,因为我在测试的过程中,每个类都只给了一个成员变量,所以偏移量就为0,读者可以自己多加几个成员变量来测试一下。这样就可以得出虚继承中D类的对象模型如下图:
我们可以看到虚继承只有一份_b,但是在别的类中增加了四个字节来获取基类成员相对于别的类的偏移量。这样虽然解决了在菱形继承的二义性问题,但是也引入了程序性能下降的问题,在这里就不做详细的说明。