类型兼容
不同类型数据之间在一定条件下可以进行类型的转换,比如int n = ‘a’由于字符和整型兼容,可以对整型变量n赋值为字符’a’。基类与派生类对象之间也具有赋值兼容的关系,可以进行类型间的转换。
派生类是从它的直接和间接基类继承而来,尤其是公有继承的派生类保持了基类的所有特征。因此,在语法上,一个公有派生类总是可以充当一个基类对象,可以将派生类的值赋给基类对象,在用到基类对象的时候可以用其派生类对象代替。反之不成立,因为派生类是对基类的继承和发展,其中既包含基类的成员又包含派生类增加的新成员。
在了解使用派生类对象操作基类数据之前,先来学习派生类中成员的内存分布情况。C++中类对象的内存空间大小完全取决于类的数据成员,成员函数不是某个对象独有,因此基本不影响对象的空间大小。对象所占空间中,按照其所属类中各数据成员的声明顺序依次排列。一般情况下,若基类派生出新类,则新类的数据成员排列于基类成员之后。
接下来通过一个案例观察派生类中数据成员的内存排列,如例1所示。
例1
1 #include <iostream>
2 using namespace std;
3
4 class Base //定义基类Base
5 {
6 protected:
7 int n_base; //定义数据成员n_base
8 };
9 class Derive:public Base //定义公有派生类Derive
10 {
11 public:
12 //定义disp_addr()成员函数,显示继承自基类的数据成员和派生类私有数据成员的地址信息
13 void disp_addr() {
14 cout << "n_base addr:" << &n_base << endl;
15 cout << "n_derive addr:" << &n_derive << endl;
16 }
17 private:
18 int n_derive;
19 };
20 int main()
21 {
22 Derive obj; //定义派生类对象
23 obj.disp_addr(); //调用disp_addr()函数
24 //通过sizeof运算符求obj占用的字节数
25 cout << "sizeof(obj) = " << sizeof(obj) << endl;
26 system("pause");
27 return 0;
28 }
程序运行结果如图1所示。
图1 例1运行结果
观察例1运行结果可知,基类数据成员n_base排列在派生类成员n_derive之前,且相邻存放。由此可知,对于一般继承情况来说,派生类对象中会首先存放基类的数据成员,派生类成员紧随其后,依次排列。另外,我们通过sizeof求取了obj对象占用的字节数,数值为8,刚好和基类及派生类中所有数据成员占用的字节数相同。图2描述了派生类对象obj中数据成员的内存分布情况。
图2 派生类对象数据成员布局
了解了派生类和基类成员的排列情况,下面学习派生类对象操作基类对象的四种方法。
1、派生类对象可以向基类对象赋值。
可以用派生类对象向基类对象赋值,若有如下代码,操作正确:
ClassA obj_a; //定义基类ClassA对象obj_a
ClassB obj_b; //定义类ClassA的公有派生类ClassB的对象obj_b
obj_a = obj_b; //用派生类ClassB对象obj_b对基类对象obj_a赋值
派生类对象向基类对象赋值时,将基类数据成员赋值,派生类新增的数据成员值被舍弃,不存在对成员函数的赋值。由派生类中数据成员的排列情况可知,基类数据成员排列在最前端,因此可以使用派生类对象向基类对象赋值,基类对象会获取派生类对象中的基类数据。
下图描述了派生类对象向基类对象的赋值情况,如图3所示。
图3 派生类对象向基类对象赋值
使用派生类对象向基类对象赋值时,需要注意以下两点:
(1)赋值后不能通过基类对象obj_a访问派生类对象obj_b的成员,因为obj_b的成员与obj_a的成员不同。假设newmember是派生类ClassB中增加的公有数据成员,若有下列代码,执行错误。
obj_a.newmember = xxx; //错误,obj_a中不包含派生类中增加的成员
(2)派生类型关系是单向的,不可逆。ClassB是ClassA的派生类,只能用派生类对象对其基类对象赋值,而不能用基类对象对其派生类对象赋值,原因显而易见,因为基类对象不包含派生类的成员,无法对派生类的成员赋值。同理,同一基类的不同派生类对象之间也不能赋值。
2、派生类对象可以替代基类对象向基类对象的引用进行赋值或初始化。
若已定义了基类ClassA对象obj_a,可以定义obj_a的引用:
ClassA obj_a; //定义基类ClassA对象obj_a
ClassB obj_b; //定义公有派生类ClassB对象obj_b
ClassA &refa = obj_a; //定义基类ClassA对象的引用变量refa,并用obj_a对其初始化
引用refa是obj_a的别名,refa和obj_a共享同一段存储单元。可以用子类对象初始化引用refa,将上面最后一行改为:
//定义基类ClassA对象的引用变量refa并用派生类ClassB对象obj_b对其初始化
ClassA &refa = obj_b;
需要注意的是,此时refa并不是obj_b的别名,也不与obj_b共享同一段存储单元。它只是obj_b中基类部分的别名,refa与obj_b中基类部分共享同一段存储单元,refa与obj_b具有相同的起始地址。图4描述了基类引用变量refa的操作对象。
图4 派生类对象向基类引用变量赋值
3、如果函数的参数是基类对象或基类对象的引用,函数调用时的实参可以是派生类对象。
定义函数func(),其形参为基类引用,具体代码如下所示:
void func(ClassA &ref) //形参是类ClassA的对象的引用
{
cout<<ref.num<<endl; //输出该引用所代表的对象的数据成员num
}
函数的形参是类ClassA对象的引用,本来实参应该为ClassA类的对象。由于派生类对象与基类对象赋值兼容,派生类对象能自动转换类型,在调用func()函数时可以用派生类ClassB的对象obj_b作实参:
func(obj_b); //输出类ClassB的对象obj_b的基类数据成员num的值。
3、 派生类对象的地址可以赋值给基类指针变量。
指向基类对象的指针变量也可以指向派生类对象。接下来通过一个案例来学习将派生类对象地址赋值给基类指针变量时,基类指针变量的操作方式,如例2所示。
例2
1 #include <iostream>
2 using namespace std;
3
4 class Animal //定义基类Animal
5 {
6 public:
7 //获取m_nAge属性值的函数
8 int get_age(){ return m_nAge; }
9 //获取m_nWeight属性值的函数
10 int get_weight(){ return m_nWeight; }
11 //设置m_nAge属性的函数
12 void set_age(int param_age){ m_nAge = param_age; }
13 //设置m_nWeight属性的函数
14 void set_weight(int param_weight){ m_nWeight = param_weight; }
15 //定义动物说话的函数
16 void speak(){ cout << "animal language!" << endl; }
17 private:
18 int m_nWeight;
19 int m_nAge;
20 };
21
22 class Cat:public Animal //定义派生类Cat
23 {
24 public:
25 //设置m_strName属性的函数
26 void set_name(string param_name);
27 //定义猫说话的函数,该函数与基类中的speak()函数同名
28 void speak(){ cout << "cat language: miaomiao!" << endl; }
29 private:
30 string m_strName;
31 };
32 int main()
33 {
34 Cat cat; //定义派生类对象cat
35 Animal *panimal = &cat; //定义基类指针并初始化为cat地址
36
37 panimal->set_age(5); //基类指针调用基类set_age()函数
38 cout << "base type: age = "
39 << panimal->get_age() << endl; //指针指针调用基类get_age()函数
40 cat.speak(); //派生类对象调用speak()函数
41 panimal->speak(); //基类指针调用speak()函数
42
43 system("pause");
44 return 0;
45 }
程序运行结果如图5所示。
图5 例2运行结果
在例2中,main()函数中定义了派生类对象cat及基类指针panimal,虽然用派生类对象地址对基类指针进行了初始化,但通过panimal只能访问基类公有成员,如代码第37、39行,访问了基类成员函数set_age()、get_age(),运行结果正确。另外本例中,基类、派生类定义了同名函数speak()成员函数,派生类中的函数将基类中的函数覆盖,代码第40行,cat对象调用了自身的speak()函数,运行结果显示“cat language: miaomiao!”。代码第41行,panimal是基类指针,它调用基类的speak()函数,运行结果显示出了基类成员函数信息。
虽然基类指针存入的是派生类对象地址,但基类指针不可调用派生类成员函数,若main()函数中有如下代码,编译出错:
panimal->set_name("Persian");
编译错误信息,如图6所示。
图6 基类指针操作派生类成员函数时的编译错误信息
通过本例可以看到:用基类指针变量指向派生类对象是合法的、安全的,不会出现编译上的错误。但在应用上却不能完全满足人们的要求,有时希望通过使用基类指针调用派生类中与基类同名的函数。这个问题,在介绍完“多态”和“虚函数”后会得到解决。