day4 4.9(向上转型)派生类赋值给基类

向上转型(将派生类赋值给基类)

在 C/C++ 中经常会发生数据类型的转换,例如将 int 类型的数据赋值给 float 类型的变量时,编译器会先把 int 类型的数据转换为 float 类型再赋值;反过来,float 类型的数据在经过类型转换后也可以赋值给 int 类型的变量;

数据类型转换的前提是,编译器知道如何对数据进行取舍;大的变小,直接丢弃就可以,小的变大的多出来的内存不知道如何填充

类其实也是一种数据类型,也可以发生数据类型转换,不过这种转换只有在基类和派生类之间才有意义,并且只能将派生类赋值给基类,包括将派生类对象赋值给基类对象、将派生类指针赋值给基类指针、将派生类引用赋值给基类引用,这在 C++ 中称为向上转型(Upcasting)。相应地,将基类赋值给派生类称为向下转型(Downcasting);

  1. 向上转型非常安全,可以由编译器自动完成;
  2. 向下转型有风险,需要程序员手动干预。本节只介绍向上转型,向下转型将在后续章节介绍;

将派生类对象赋值给基类对象

#include 

using namespace std;

class A {
public:
    A(int a);
    void print() const;
protected:
    int m_a;
};

A::A(int a) : m_a(a) {}
void A::print() const {
    printf("class A: m_a = %d\n", m_a);
}

class B : public A {
public:
    B(int a, int b);
    void print() const;
private:
    int m_b;
};

B::B(int a, int b) : A(a), m_b(b) {}
void B::print() const {
    printf("class B: m_a = %d, m_b = %d\n", m_a, m_b);
}

int main() {
    A a(1);
    B b(11, 22);
    a.print();
    b.print();

    printf("----------------\n");

    a = b;//向上转型,大的变成小的
    a.print();
    b.print();

    return 0;
}

运行结果:

class A: m_a = 1
class B: m_a = 11, m_b = 22
----------------
class A: m_a = 11
class B: m_a = 11, m_b = 22

赋值的本质是将现有的数据写入已分配好的内存中,对象的内存只包含了成员变量,所以对象之间的赋值是成员变量的赋值,成员函数不存在赋值问题。运行结果也有力地证明了这一点,虽然有a = b;这样的赋值过程,但是 a.print() 始终调用的都是 A 类的 print() 函数。换句话说,对象之间的赋值不会影响成员函数,也不会影响 this 指针;

将派生类对象赋值给基类对象时,会舍弃派生类新增的成员,也就是“大材小用”;

这种转换关系是不可逆的,只能用派生类对象给基类对象赋值,而不能用基类对象给派生类对象赋值。理由很简单,基类不包含派生类的成员变量,无法对派生类的成员变量赋值。同理,同一基类的不同派生类对象之间也不能赋值。

要理解这个问题,还得从赋值的本质入手。

  1. 赋值实际上是向内存填充数据,当数据较多时很好处理,舍弃即可;本例中将 b 赋值给 a 时(执行a = b;语句),成员 m_b 是多余的,会被直接丢掉,所以不会发生赋值错误。
  2. 但当数据较少时,问题就很棘手,编译器不知道如何填充剩下的内存;如果本例中有b = a;这样的语句,编译器就不知道该如何给变量 m_b 赋值,所以会发生错误;

将派生类指针赋值给基类指针

除了可以将派生类对象赋值给基类对象(对象变量之间的赋值),还可以将派生类指针赋值给基类指针(对象指针之间的赋值);

我们先来看一个多继承的例子:基类 A 拥有成员 m_a;中间派生类 B 继承于 A,并添加了新成员 m_b;基类 C 拥有成员 m_c;最终派生类 D 继承 B 和 C;

#include 

using namespace std;

class A {
public:
    A(int a);
    void print() const;
protected:
    int m_a;
};

A::A(int a) : m_a(a) {}
void A::print() const {
    printf("class A: m_a = %d\n", m_a);
}

class B : public A {
public:
    B(int a, int b);
    void print() const;
protected:
    int m_b;
};

B::B(int a, int b) : A(a), m_b(b) {}
void B::print() const {
    printf("class B: m_a = %d, m_b = %d\n", m_a, m_b);
}

class C {
public:
    C(int c);
    void print() const;
protected:
    int m_c;
};

C::C(int c) : m_c(c) {}
void C::print() const {
    printf("class C: m_c = %d\n", m_c);
}

class D : public B, public C {
public:
    D(int a, int b, int c, int d);
    void print() const;
private:
    int m_d;
};

D::D(int a, int b, int c, int d) : B(a, b), C(c), m_d(d) {}
void D::print() const {
    printf("class D: m_a = %d, m_b = %d, m_c = %d, m_d = %d\n", m_a, m_b, m_c, m_d);
}

int main() {
    A *pa = new A(1);
    pa -> print();
    B *pb = new B(11, 22);
    pb -> print();
    C *pc = new C(3);
    pc -> print();

    D *pd = new D(110, 114, 119, 120);
    pd -> print();
    printf("--------------------------\n");

    pa = pd;//pa是A类型指针,基类
    pa -> print();
    pb = pd;
    pb -> print();
    pc = pd;
    pc -> print();

    printf("---------------------------\n");
    printf("pa = %p\npb = %p\npc = %p\npd = %p\n", pa, pb, pc, pd);

    return 0;
}

运行结果:

class A: m_a = 1
class B: m_a = 11, m_b = 22
class C: m_c = 3
class D: m_a = 110, m_b = 114, m_c = 119, m_d = 120
--------------------------
class A: m_a = 110
class B: m_a = 110, m_b = 114
class C: m_c = 119
---------------------------
pa = 0xb61aee4090
pb = 0xb61aee4090
pc = 0xb61aee4098
pd = 0xb61aee4090

本例中定义了多个对象指针,并尝试将派生类指针赋值给基类指针。与对象变量之间的赋值不同的是,对象指针之间的赋值并没有拷贝对象的成员,也没有修改对象本身的数据,仅仅是改变了指针的指向;

  1. 通过基类指针访问派生类的成员
    将派生类指针 pd 赋值给了基类指针 pa,从运行结果可以看出,调用 print() 函数时虽然使用了派生类的成员变量,但是 print() 函数本身却是基类的;也就是说,将派生类指针赋值给基类指针时,通过基类指针只能使用派生类的成员变量,但不能使用派生类的成员函数,pb、pc 也是一样的情况;
    这是因为,调用哪个类的函数不是由指针指向的数据为判断标准,而是决定于指针的类型,如果指针的类型是A *,那么就调用类 A 的函数,如果是D *,就调用类 D 的函数;
    概括起来说就是:编译器通过指针来访问成员变量,指针指向哪个对象就使用哪个对象的数据;编译器通过指针的类型来访问成员函数,指针属于哪个类的类型就使用哪个类的函数。

  2. 赋值后值不一致的情况
    本例中我们将最终派生类的指针 pd 分别赋值给了基类指针 pa、pb、pc,按理说它们的值应该相等,都指向同一块内存,但是运行结果却有力地反驳了这种推论,只有 pa、pb、pd 三个指针的值相等,pc 的值比它们都大。也就是说,执行pc = pd;语句后,pc 和 pd 的值并不相等,这是因为在赋值前,编译器会进行某些处理,就如同将 float 类型的 3.14 赋值给 int 类型的变量时,编译器会直接舍弃小数点后的值,最终变为 3;对象指针之间的赋值也是这个道理;因为A->B,B->D,C->D,B->D,所以A,B,D理所应当同一块内存,C与A肯定存在偏移,我们通过继承类的对象内存模型可以推导。

将派生类引用赋值给基类引用

引用在本质上是通过指针的方式实现的,既然基类的指针可以指向派生类的对象,那么我们就有理由推断:基类的引用也可以指向派生类的对象,并且它的表现和指针是类似的;

最后需要注意的是,向上转型后通过基类的对象、指针、引用只能访问从基类继承过去的成员(包括成员变量和成员函数),不能访问派生类新增的成员;

将派生类指针赋值给基类指针时到底发生了什么

通过上节最后一个例子我们发现,将派生类的指针赋值给基类的指针后,它们的值有可能相等,也有可能不相等;

我们通常认为,赋值就是将一个变量的值交给另外一个变量,这种想法虽然没错,但是有一点要注意,就是赋值以前编译器可能会对现有的值进行处理;

例如将 double 类型的值赋给 int 类型的变量,编译器会直接抹掉小数部分,导致赋值运算符两边变量的值不相等;

将派生类的指针赋值给基类的指针时也是类似的道理,编译器也可能会在赋值前进行处理;

要明确的一点是,对象的指针必须要指向对象的起始位置;这也是第二个代码例子里C的地址与其他不一样的原因。

  1. 对于 A 类和 B 类来说,它们的子对象的起始地址和 D 类对象一样,所以将 pd 赋值给 pa、pb 时不需要做任何调整,直接传递现有的值即可;
  2. 而 C 类子对象距离 D 类对象的开头有一定的偏移,将 pd 赋值给 pa 时要加上这个偏移,这样 pc 才能指向 C 类子对象的起始位置;
    也就是说,执行pc = pd;语句时编译器对 pd 的值进行了调整,才导致 pc、pd 的值不同;

你可能感兴趣的:(#,C++基础内容)