Effective C++面向对象与继承

简介:

1:子类不要覆写父类的非虚函数。

2:子类不要覆写从父类继承过来的默认参数

3:子类与父类之间的赋值问题

 

1:子类不要覆写父类的非虚函数。

为了解释方便,先看一个简单的例子。

复制代码
class A
{
    public: 
        A(int d):data(d){  }

        void print()
        {
            cout<<"A print..."<<data<<endl;
        }

        virtual void test(int i=2)
        {
            cout<<"A test..."<<i<<endl;
        }
    private: 
        int data;
};

class B:public A
{
    public : 
         
        B(int d):A(d){  }
        void print()
        {
            cout<<"B print..."<<endl;
        } 
        virtual void test(int i=4)
        {
            cout<<"B test..."<<i<<endl;
        }
};
  
//测试代码
int main() { 
    {
        B b(5); 
        b.print();
        A *a=&b; 
        a->print();  
        cout<<endl;
        b.test();
        a->test();
        cout<<endl;
        A a1=b;
        a1.test();
    }
     
     getchar();
     return 0;
} 
  
复制代码

运行结果截图:

例子中指针a是指向对象b的,但是他们调用的print方法却不是同一个。这里涉及到静态绑定和动态绑定的问题。a的静态类型是A,a的动态的类型却是B,b的静态类型和动态类型都是B,因为静态类型就是申明时的类型,动态类型是其真正指向的类型。还有一点就是非虚方法是静态绑定,虚拟方法是动态绑定。Print是非虚方法,它是静态绑定,调用的是自己的对象申明类型的方法,所以a调用的是A的print,b调用的是B的print方法。我想我们更想知道C++是怎么实现动态绑定。我们都知道含有虚方法的类都有一个虚拟方法表,每个对象的实例都有一个指针指向这个虚拟方法表,子类会继承父类的virtual方法,也可以覆写父类的虚拟方法,如果子类覆写父类的虚拟方法,那么在虚拟表中对应的指针就指向子类覆写父类的方法,如果子类不覆写父类的虚拟方法,则还是指向父类的方法,这样就形成了动态绑定。不同的子类按照自己的方式覆写父类的虚拟方法,表现出不同的行为这就是多态。在多重继承中,每个对象可能有多个虚拟表,那么它的实例就会有多个指向虚拟表的指针,如果多个父类有一个相同的方法,那么你就不能直接用这个实例调用这个方法,因为编译器根本不知道它该调用哪个方法,你要指定是那个父类的方法,当你指明了哪个父类,编译就可以通过对应的指针调用对应的虚拟表中对应的方法。那么实例调用虚拟方法的过程是怎么样的呢,你有没有想过?其实上面也提到一点,大致三步:

1:根据对象的vptr指针找到其虚拟方法表vtbl;

2:找到被调用方法在vtbl中对应的指针;

3:调用2中指针指向的方法。

 

2:子类不要覆写从父类继承过来的默认参数

这一条其实还是涉及到静态绑定和动态绑定的问题,关于这个问题我想上面已经说得比较清楚了,默认值也是静态绑定,这是毫无疑问的,因为它在编译期就已经确定了,而虚拟方法确实动态绑定,你把静态绑定的东西和动态绑定的东西搅在一起没有问题,但是你还有得寸进尺的在子类中覆写静态的东西就会出问题,对不起,父类不管子类中静态的东西,它只管自己静态的东西,所以当子类不要覆写从父类继承过来的默认参数时,子类就可能出现精神分裂的行为,上面那个列子就是证明。

 

上面更多提到的都是关于虚拟方法的,那么非虚拟方法呢,对象实例时怎么调用非虚拟方法的呢?非虚拟方法是怎么实现的呢?非虚拟方法就像一般的C函数那样被实现的,所以他们的调用不需要像虚拟方法一样先要找到一个指针,然后在通过这个指针调用对应的方法。

 

3:子类与父类之间的赋值问题

首先将父类转换成子类的事最好不要做,因为子类的很多特性父类根本没有,当你把一个从父类转换过来的子类,当做子类来用的话,很可能出问题。接下来我们重点讨论将子类转换成父类。还是通过上面例子来说明问题。

B b(2);

A a=b;//调用copy constructor

a=b;//调用 operator=

上面两行代码,第一行先实例化了一个对象b,第二行将b赋给a,那么是怎么将b赋给a的呢,这里其实调用的不是operator=,而是copy constructor,因为构造一个对象必须调用constructor,或是copy constructor,那么这里肯定是调用copy constructor,operator=只是一个赋值动作,一个对象还没有构造出来怎么给他赋值呢,在operator=可不是用来帮你构造对象的哦,在第三行的时候a已经被构造出来了,那么这里真的就是赋值了调用的就是operator=。总之一句话,一个对象作为左值时,第一次肯定调用的是copy constructor,被初始化后(分配了内存),之后的操作才是赋值。一个对象作为by value形式的参数,那么每次调用的都是copy constructor,而不是operator=,我们一般都会说将实参赋给形参,其实是用实参构造一个形参。

将b赋给a,就是将b的A部分赋给a,a就是一个完全的A了,它对B一无所知,更不会表现出B的任何行为,所以by value是很暴力并且很耗性能的,也不会出现多态的行为。所以要避免使用by value,尽量用by reference。

就此打住,未完待续...

 

Effective C++系列:

      Effective C++构造函数析构函数Assignment运算符

  Effective C++ 类与函数的设计和申明

    Effective C++面向对象与继承

作者:陈太汉

博客:http://www.cnblogs.com/hlxs/



本文转自啊汉博客园博客,原文链接:http://www.cnblogs.com/hlxs/archive/2012/07/28/2612912.html

相关文章
|
25天前
|
【c++】继承(继承的定义格式、赋值兼容转换、多继承、派生类默认成员函数规则、继承与友元、继承与静态成员)
本文深入探讨了C++中的继承机制,作为面向对象编程(OOP)的核心特性之一。继承通过允许派生类扩展基类的属性和方法,极大促进了代码复用,增强了代码的可维护性和可扩展性。文章详细介绍了继承的基本概念、定义格式、继承方式(public、protected、private)、赋值兼容转换、作用域问题、默认成员函数规则、继承与友元、静态成员、多继承及菱形继承问题,并对比了继承与组合的优缺点。最后总结指出,虽然继承提高了代码灵活性和复用率,但也带来了耦合度高的问题,建议在“has-a”和“is-a”关系同时存在时优先使用组合。
70 6
|
3月前
|
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
93 19
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
102 13
|
3月前
|
C++学习之继承
通过继承,C++可以实现代码重用、扩展类的功能并支持多态性。理解继承的类型、重写与重载、多重继承及其相关问题,对于掌握C++面向对象编程至关重要。希望本文能为您的C++学习和开发提供实用的指导。
70 16
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
72 5
|
3月前
|
【C++面向对象——输入输出流】处理二进制文件(头歌实践教学平台习题)【合集】
本任务要求使用C++读取二进制文件并在每行前添加行号后输出到控制台。主要内容包括: 1. **任务描述**:用二进制方式打开指定文件,为每一行添加行号并输出。 2. **相关知识**: - 流类库中常用的类及其成员函数(如`iostream`、`fstream`等)。 - 标准输入输出及格式控制(如`cin`、`cout`和`iomanip`中的格式化函数)。 - 文件的应用方法(文本文件和二进制文件的读写操作)。 3. **编程要求**:编写程序,通过命令行参数传递文件名,使用`getline`读取数据并用`cout`输出带行号的内容。 4. **实验步骤**:参考实验指
89 5
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
61 5
【C++面向对象——类的多态性与虚函数】计算图像面积(头歌实践教学平台习题)【合集】
本任务要求设计一个矩形类、圆形类和图形基类,计算并输出相应图形面积。相关知识点包括纯虚函数和抽象类的使用。 **目录:** - 任务描述 - 相关知识 - 纯虚函数 - 特点 - 使用场景 - 作用 - 注意事项 - 相关概念对比 - 抽象类的使用 - 定义与概念 - 使用场景 - 编程要求 - 测试说明 - 通关代码 - 测试结果 **任务概述:** 1. **图形基类(Shape)**:包含纯虚函数 `void PrintArea()`。 2. **矩形类(Rectangle)**:继承 Shape 类,重写 `Print
61 4
【C++面向对象——类的多态性与虚函数】编写教学游戏:认识动物(头歌实践教学平台习题)【合集】
本项目旨在通过C++编程实现一个教学游戏,帮助小朋友认识动物。程序设计了一个动物园场景,包含Dog、Bird和Frog三种动物。每个动物都有move和shout行为,用于展示其特征。游戏随机挑选10个动物,前5个供学习,后5个用于测试。使用虚函数和多态实现不同动物的行为,确保代码灵活扩展。此外,通过typeid获取对象类型,并利用strstr辅助判断类型。相关头文件如&lt;string&gt;、&lt;cstdlib&gt;等确保程序正常运行。最终,根据小朋友的回答计算得分,提供互动学习体验。 - **任务描述**:编写教学游戏,随机挑选10个动物进行展示与测试。 - **类设计**:基类
51 3
【C++】继承
C++中的继承是面向对象编程的核心特性之一,允许派生类继承基类的属性和方法,实现代码复用和类的层次结构。继承有三种类型:公有、私有和受保护继承,每种类型决定了派生类如何访问基类成员。此外,继承还涉及构造函数、析构函数、拷贝构造函数和赋值运算符的调用规则,以及解决多继承带来的二义性和数据冗余问题的虚拟继承。在设计类时,应谨慎选择继承和组合,以降低耦合度并提高代码的可维护性。
51 1
【C++】继承