C++中类和对象的一些注意事项 --- 多态

1. 一些继承中的问题

1.1 多继承中父类含有重名成员问题

如下:

#include <iostream>
#include <string>
using namespace std;

class father1 {
public:
    father1() {
        class_name = "father1";
    }
    string class_name;
};

class father2 {
public:
    father2() {
        class_name = "father2";
    }
    string class_name;
};

class public_inherited_son : public father1, public father2 {
public:
};

int main() {
    public_inherited_son public_inherited_son_instance1;
    cout << public_inherited_son_instance1.class_name << endl;
    return 0;
}

此时father1, father2都含有class_name成员, 而子类不含, 所以子类实例化的对象中如果要使用class_name, 必须要加上作用域, 否则以上代码编译后就会如下:

修改后代码如下:

#include <iostream>
#include <string>
using namespace std;

class father1 {
public:
    father1() {
        class_name = "father1";
    }
    string class_name;
};

class father2 {
public:
    father2() {
        class_name = "father2";
    }
    string class_name;
};

class public_inherited_son : public father1, public father2 {
public:
};

int main() {
    public_inherited_son public_inherited_son_instance1;
    cout << public_inherited_son_instance1.father1::class_name << endl;
    cout << public_inherited_son_instance1.father2::class_name << endl;
    return 0;
}

结果为:

或者让子类用同名成员覆盖该成员, 代码如下:

#include <iostream>
#include <string>
using namespace std;

class father1 {
public:
    father1() {
        class_name = "father1";
    }
    string class_name;
};

class father2 {
public:
    father2() {
        class_name = "father2";
    }
    string class_name;
};

class public_inherited_son : public father1, public father2 {
public:
    public_inherited_son() {
        class_name = "public_inherited_son";
    }
    string class_name;
};

int main() {
    public_inherited_son public_inherited_son_instance1;
    cout << public_inherited_son_instance1.class_name << endl;
    return 0;
}

结果如下:

1.2 菱形继承的问题

也叫钻石继承, 即两个派生类同时继承于同一个基类, 又有一个派生类同时继承于以上两个派生类.

如下代码所示:

#include <iostream>
#include <string>
using namespace std;

class grandfather {
public:
    int member_value;
};

class father1 : public grandfather {
public:
};

class father2 : public grandfather{
public:
};

class public_inherited_son : public father1, public father2 {
public:
};

这时候father1, father2类都继承了grandfaher类的member_value属性, 那么public_inherited_son在多继承father1和father2类的时候就会不清楚用谁的成员, 用以下测试代码:

int main() {
    public_inherited_son public_inherited_son_instance1;
    public_inherited_son_instance1.member_value = 10;
    cout << public_inherited_son_instance1.member_value << endl;
    return 0;
}

编译发现果然有二义性:

这种情况当然可以通过作用域来区分, 但是如果实际情况是这份数据只需要一份的话, 菱形继承之后会造成资源浪费, 那么就可以用virtual继承来解决这样的问题.

2 虚继承

2.1 普通继承的内存模型

当不加虚继承关键字的时候, 继承中内存的情况如下图:

2.2 虚继承实现

复用1.2中的例子, 如果修改继承方式如下:

#include <iostream>
#include <string>
using namespace std;

class grandfather {
public:
    int member_value;
};

class father1 : virtual public grandfather {
public:
};

class father2 : virtual public grandfather{
public:
};

class public_inherited_son : public father1, public father2 {
public:
};

int main() {
    public_inherited_son public_inherited_son_instance1;
    public_inherited_son_instance1.member_value = 10;
    cout << "public_inherited_son_instance1.member_value = 10" << endl;
    cout << "current public_inherited_son_instance1.member_value is" << public_inherited_son_instance1.member_value << endl;
    cout << "current public_inherited_son_instance1.father1::member_value is" << public_inherited_son_instance1.father1::member_value << endl;
    cout << "current public_inherited_son_instance1.father2::member_value is" << public_inherited_son_instance1.father2::member_value << endl << endl;
    public_inherited_son_instance1.father1::member_value = 20;
    cout << "change public_inherited_son_instance1.father1::member_value to 20" << endl;
    cout << "current public_inherited_son_instance1.member_value is" << public_inherited_son_instance1.member_value << endl;
    cout << "current public_inherited_son_instance1.father1::member_value is" << public_inherited_son_instance1.father1::member_value << endl;
    cout << "current public_inherited_son_instance1.father2::member_value is" << public_inherited_son_instance1.father2::member_value << endl << endl;
    public_inherited_son_instance1.father2::member_value = 30;
    cout << "change public_inherited_son_instance1.father1::member_value to 30" << endl;
    cout << "current public_inherited_son_instance1.member_value is" << public_inherited_son_instance1.member_value << endl;
    cout << "current public_inherited_son_instance1.father1::member_value is" << public_inherited_son_instance1.father1::member_value << endl;
    cout << "current public_inherited_son_instance1.father2::member_value is" << public_inherited_son_instance1.father2::member_value << endl << endl;
    return 0;
}

这样编译就没问题了, 运行结果为:

原因是, 当使用虚继承之后, 该数据成员便只有一份, 此时该内存模型为:

虚继承后, 子类继承的是一个指针, 这个指针指向的是一个虚继承表, 表内记录了继承的数据的位置与本身的偏移量(即用的是基类的数据, 这样保证了数据的唯一性), 这样做就解决了菱形问题.

3 多态

3.1 静态多态和动态多态

函数重载, 运算符重载等等, 地址早绑定, 以下是一个静态继承的例子:

#include <iostream>
#include <string>
using namespace std;

class father {
public:
    void printClassName() {
        cout << "this is class father" << endl;
    }
};

class son1 : public father {
public:
    void printClassName() {
        cout << "this is class son1" << endl;
    }
};

void PrintClassName(father &fatherInstance) {
    fatherInstance.printClassName();
}

void test1() {
    son1 son1Instance1;
    PrintClassName(son1Instance1);
}

int main() {
    test1();
    return 0;
}

运行时发现:


这就是因为静态多态早绑定的性质, 因为在编译阶段, 其father中的printClassName函数就已经确定了函数地址了, 就是father实例化对象的printClassName, 哪怕是通过子类进行类型转换的, 其作为参数传进PrintClassName中时, 也会退化为传入其父类的空间.

如果我们按照以下方法做:

#include <iostream>
#include <string>
using namespace std;

class father {
public:
    virtual void printClassName() {
        cout << "this is class father" << endl;
    }
};

class son1 : public father {
public:
    void printClassName() {
        cout << "this is class son1" << endl;
    }
};

void PrintClassName(father &fatherInstance) {
    fatherInstance.printClassName();
}

void test1() {
    son1 son1Instance1;
    PrintClassName(son1Instance1);
}

int main() {
    test1();
    return 0;
}

运行发现:

即在father类的printClassName函数前面加virtual, 这样的话就会实现与静态多态早绑定相对应的晚绑定, 即在运行时才给函数赋值. 如需动态多态, 子类需要重写父类的虚函数(区别于重载, 重写为函数返回值类型, 函数名称, 参数列表需完全相同). 这样做就能达到父类的指针或引用指向子类对象时, 调用其虚函数会调用子类对象对应的重写函数.

3.2 动态多态的实现原理

当在父类的函数前加上virtual关键字, 其就会作为一个函数指针存储在类中, 指向虚函数表, 当运行时, 从虚函数表中获取出当前的函数地址并运行(如果发生重写, 会换成继承后的地址).

3.3 多态各类情况总结

用以下测试类:

class father {
public:
    void printFuncName() {
        cout << "this is father's function" << endl << endl;
    }
    virtual void virtualPrintFuncName() {
        cout << "this is father's virtual function" << endl << endl;
    }
};

class virtualInheritedSon : virtual public father {
public:
    void printFuncName() {
        cout << "this is virtualInheritedSon's function" << endl << endl;
    }
    void virtualPrintFuncName() {
        cout << "this is virtualInheritedSon's virtual function" << endl << endl;
    }
};

class normalInheritedSon : public father {
public:
    void printFuncName() {
        cout << "this is normalInheritedSon's function" << endl << endl;
    }
    void virtualPrintFuncName() {
        cout << "this is normalInheritedSon's virtual function" << endl << endl;
    }
};

3.3.1 普通继承

如下代码测试:

void normalInheritedTest() {
    normalInheritedSon normalInheritedSonInstance;
    cout << "now normalInheritedSonInstance printFuncName" << endl << endl;
    normalInheritedSonInstance.printFuncName();
    cout << "now normalInheritedSonInstance virtualPrintFuncName" << endl << endl;
    normalInheritedSonInstance.virtualPrintFuncName();
    cout << "now fatherInstance = normalInheritedSonInstance" << endl << endl;
    father fatherInstance = normalInheritedSonInstance;
    cout << "now fatherInstance printFuncName" << endl << endl;
    fatherInstance.printFuncName();
    cout << "now fatherInstance virtualPrintFuncName" << endl << endl;
    fatherInstance.virtualPrintFuncName();
    cout << "now rFatherInstance = normalInheritedSonInstance" << endl << endl;
    father &rFatherInstance = normalInheritedSonInstance;
    cout << "now rFatherInstance printFuncName" << endl << endl;
    rFatherInstance.printFuncName();
    cout << "now rFatherInstance virtualPrintFuncName" << endl << endl;
    rFatherInstance.virtualPrintFuncName();
}

结果为:

总结如下:

  • 普通继承的子类实例化的对象无论是调用普通成员函数或者是虚成员函数, 都会调用子类自己的.
  • 用父类等号取出其子类实例化对象中的父类调用普通成员函数或者是虚成员函数, 都会调用父类自己的.
  • 用父类引用取出其子类实例化对象中的父类调用普通成员函数, 会调用父类自己的, 如果是虚函数, 会调用子类的.

3.3.2 虚继承

用如下代码测试:

void normalInheritedTest() {
    normalInheritedSon normalInheritedSonInstance;
    cout << "now normalInheritedSonInstance printFuncName" << endl << endl;
    normalInheritedSonInstance.printFuncName();
    cout << "now normalInheritedSonInstance virtualPrintFuncName" << endl << endl;
    normalInheritedSonInstance.virtualPrintFuncName();
    cout << "now fatherInstance = normalInheritedSonInstance" << endl << endl;
    father fatherInstance = normalInheritedSonInstance;
    cout << "now fatherInstance printFuncName" << endl << endl;
    fatherInstance.printFuncName();
    cout << "now fatherInstance virtualPrintFuncName" << endl << endl;
    fatherInstance.virtualPrintFuncName();
    cout << "now rFatherInstance = normalInheritedSonInstance" << endl << endl;
    father &rFatherInstance = normalInheritedSonInstance;
    cout << "now rFatherInstance printFuncName" << endl << endl;
    rFatherInstance.printFuncName();
    cout << "now rFatherInstance virtualPrintFuncName" << endl << endl;
    rFatherInstance.virtualPrintFuncName();
}

结果为:

总结如下:

  • 虚继承的子类实例化的对象无论是调用普通成员函数或者是虚成员函数, 都会调用子类自己的.
  • 用父类等号取出其子类实例化对象中的父类调用普通成员函数或者是虚成员函数, 都会调用父类自己的.
  • 用父类引用取出其子类实例化对象中的父类调用普通成员函数, 会调用父类自己的, 如果是虚函数, 会调用子类的.

3.3.3 虚析构

如下例子:

#include <iostream>
using namespace std;

class father {
public:
    father() {
        cout << "calling father's constructor" << endl;
    }
    ~father() {
        cout << "calling father's destructor" << endl;
    }
    virtual void printClassName() = 0;
};

class son : public father {
public:
    son(int value) {
        cout << "calling son's constructor" << endl;
        m_pvalue = new int(value);
    }
    ~son() {
        cout << "calling son's destructor" << endl;
        if (m_pvalue != NULL) {
            delete m_pvalue;
            m_pvalue = NULL;
        }
    }
    void printClassName() {
        cout << "this is class son" << endl;
        cout << "my value is " << *m_pvalue << endl;
    }
    int *m_pvalue;
};

void test1() {
    father* pfatherInstance = new son(10);
    pfatherInstance->printClassName();
    delete pfatherInstance;
}

int main() {
    test1();
    return 0;
}

这样运行结果为:

发现并没有调用子类的析构函数, 也就是说当用父类去引用或用父类指针指向子类对象时, 父类本身析构过程并不会调用子类析构函数, 这样容易导致内存泄漏(以上son的构造过程中m_pvalue指向了堆区申请的空间而没有释放掉).

我们把父类的析构函数设置为虚析构函数, 如下代码:

class father {
public:
    father() {
        cout << "calling father's constructor" << endl;
    }
    virtual ~father() {
        cout << "calling father's destructor" << endl;
    }
    virtual void printClassName() = 0;
};

这样就可以避免此问题, 且虚析构函数相较于其他虚函数, 其本身父类的函数也会走, 如下结果:

如果父类的析构函数声明为纯虚析构, 如下代码:

class father {
public:
    father() {
        cout << "calling father's constructor" << endl;
    }
    virtual ~father() = 0;
    virtual void printClassName() = 0;
};

那么编译会出现如下错误:

因为父类的纯虚函数是必须要走的, 所以其可以在类的实现文件中实现一下, 加入如下代码:

father::~father() {
    cout << "calling father's destructor" << endl;
}

这样编译就没问题了, 运行结果也和上面一样.

3.3.4 构造函数不可以是虚函数

同析构函数不同的是, 构造函数不可以设置为虚函数, 因为构造函数调用时虚函数表还没有初始化. 而且虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。 而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数, 没有这个必要.

3.3.5 静态函数不可以是虚函数

虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable. 对于静态成员函数,它没有this指针,所以无法访问vptr. 这就是为何static函数不能为virtual.

4 重载和多态

主要是重载, 覆盖, 隐藏的区别:

重载: 同一个类中(两个函数都在父类, 或者都在子类, 这样用父类指针或者用子类调用不会有什么争议)

覆盖: 父类虚函数被子类同名, 同参数函数覆盖, 这就是简单的多态,需要注意,覆盖需要子类的方法的返回值小于等于父类的返回值,访问权限大于父类的访问权限。

隐藏: 父类与子类存在同名, 参数不同函数, 无论父类函数是否为虚函数, 都会被隐藏.

         父类与子类存在同名, 参数相同参数, 父类函数不是虚函数, 也会被隐藏.

         隐藏函数可以通过父类作用域调用.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值