第十五章 面形对象程序设计

1. 派生类的成员将隐藏同名的基类成员;

2. 除了覆盖继承而来的虚函数之外,派生类最好不要重用其他定义在基类中的名字;

3. 函数调用的解析过程对于理解继承重要;

假如调用obj->func( ),将依次执行以下4个步骤:

  • 首先确定对象obj的静态类型;因为我们调用的是一个成员,所以该类型必然是类类型;
  • 在obj的静态类型对应的类中函数func;如果找不到,则依次在直接基类中不断查找直至到达继承链的顶端,如果找遍了该类及其基类仍然找不到,则编译器会报错;
  • 一旦找到了对应函数,就进行常规的类型检查,以确认对于当前找到的函数,本次调用是否合法;
  • 假设调用合法,则编译器将根据调用的是是否是虚函数而产生不同的代码:
    如果func是虚函数而且我们是通过引用或指针进行的调用,则编译器产生的代码将在运行时确定运行该虚函数的哪个版本,依据是对象的动态类型,因为可能子类函数覆盖了继承的虚函数;
    反之,如果func不是虚函数或者我们是通过对象(而非引用或指针)进行的调用,则编译器将产生一个常规的函数调用;

注意:名字查找先于类型查找
声明在内层作用域的函数并不会重载声明在外层作用域的函数(因为并没重载,只是隐藏掉了,内层调用不了外层,但是外层的还是可以调用外层的函数),因此,定义派生类中的函数也不会重载其基类中的成员。和其他作用域一样,如果派生类的成员与基类的某个成员同名,则派生类将在其他作用域隐藏该基类成员;

贴上代码:

struct Base {
    int func();
};

struct Derived : Base {
    int func(int);
};

int main(int argc, const char * argv[]) {
    Derived d;
    Base b;
    
    d.func(10); // 正确
    d.func(); // 报错
    d.Base::func(); //正确
    
    return 0;
}

这就是为什么基类和派生类的虚函数必须有相同的形参列表(否则隐藏,但没重载!!)

4. 使用using声明改变派生类继承的某个名字的访问级别,或者重载成员;

贴上代码:

#include <iostream>
using namespace std;

class Base {
public:
    size_t size() const { return n; }
    
protected:
    size_t n;
};

class Derived : private Base { //使用了private继承,所以继承而来的成员默认都是子类的私有变量
public:
    //保持对象尺寸相关的成员的访问级别
    using Base::size;
    
protected:
    using Base::n;
};

再次提醒:

  • private 只能被类的成员和友元访问;
  • protected 能被成员、友元,派生类访问;
  • public 能被所有元素访问;

5. 如果基类的析构函数不是虚函数,则delete一个指向派生类对象的基类指针将产生未定义的行为;

例如:

class Base {
public:
    Base() {};
    virtual ~Base() {
        cout << "Delete in Base" << endl;
    };
    
    virtual void DoSomething() {
        cout << "Do in Base" << endl;
    };
};

class Derived : public Base {
public:
    Derived(){};
    virtual ~Derived() {
        cout << "Delete in Derived" << endl;
    };
    
    virtual void DoSomething() {
        cout << "Do in Derived" << endl;
    };
};


int main(int argc, const char * argv[]) {
    Base * obj = new Derived; // 定义了基类的指针并指向派生类的对象
    obj->DoSomething();
    delete obj;
    
    return 0;
}

基类的析构函数加上virtual后的结果:

如果没有加上virtual,表明不会派生类的析构函数,因为基类没有可以继承的;运行结果:

6. 虚析构函数将阻止合成移动操作;

只有当一个类没有定义任何自己版本的拷贝控制成员,而且它的所有数据成员都能移动构造或者移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符

和拷贝操作不同,移动操作永远不会隐式定义为删除的函数,但是,如果我们显式的要求编译器生成=default的移动操作,则编译器会将移动操作定义为删除的函数,

7. 在默认的情况下,基类默认构造函数初始化初始化派生类对象的基类部分。如果我们想拷贝(或者移动)基类部分,则必须在派生类的构造函数初始值列表中显示地使用基类的拷贝(或移动)构造函数。

贴上代码:

#include <iostream>
using namespace std;

class Base {
public:
    Base() {
        name = "Base";
    };
    
    string name;
};

class Derived : public Base {
public:
    Derived(){
        tag = "tag";
    };
    Derived(const Derived& d): Base(d){ //拷贝基类成员
        tag = d.tag; //初始D的成员
    }
    
    Derived(Derived&& d): Base(move(d)) { //移动基类成员
        tag = d.tag; //初始D的成员
        
        d.tag = ""; //将基类对象的数据成员分配给默认值,防止析构函数多次释放内存
    }
    
    void printInfo() {
        cout << "name: " << name << endl;
        cout << "tag: " << tag << endl;
    }
    
    string tag;
};


int main(int argc, const char * argv[]) {
    Derived obj;
    
    Derived d1(obj);  //拷贝构造函数
    d1.printInfo();
    
    Derived d2(move(obj)); //移动构造函数
    d2.printInfo();
    obj.printInfo();
    
    return 0;
}

运行结果:

注意的是,在移动构造函数中,需要将源对象中的类数据成员分配给默认值,这样可以防止析构函数多次释放资源。

8. 基类的运算符正确的处理自赋值的情况,如果赋值命令正确的话,则基类运算符将释放掉其左侧运算对象的基类部分的旧值,然后利用对象参数为其赋一个新值,接着为派生类成员赋值

贴上代码:

Derived& operator=(const Derived& other) {
    if (this != &other) {
        name = other.name;
        tag = other.tag;
    }
    
    return *this;
}

9. 和构造函数以及赋值运算符不同的是,派生类析构函数只负责销毁由派生类自己分配的资源,因为基类的析构函数会自动调用执行

关于移动构造函数和移动赋值函数:
贴上代码:

class MemoryBlock
{
public:
    
    // Simple constructor that initializes the resource.
    explicit MemoryBlock(size_t length)
    : _length(length)
    , _data(new int[length])
    {
        std::cout << "In MemoryBlock(size_t). length = "
        << _length << "." << std::endl;
    }
    
    // Destructor.
    ~MemoryBlock()
    {
        std::cout << "In ~MemoryBlock(). length = "
        << _length << ".";
        
        if (_data != nullptr)
        {
            std::cout << " Deleting resource.";
            // Delete the resource.
            delete[] _data;
        }
        
        std::cout << std::endl;
    }
    
    // Copy constructor.
    MemoryBlock(const MemoryBlock& other)
    : _length(other._length)
    , _data(new int[other._length])
    {
        std::cout << "In MemoryBlock(const MemoryBlock&). length = "
        << other._length << ". Copying resource." << std::endl;
        
        std::copy(other._data, other._data + _length, _data);
    }
    
    // Copy assignment operator.
    MemoryBlock& operator=(const MemoryBlock& other)
    {
        std::cout << "In operator=(const MemoryBlock&). length = "
        << other._length << ". Copying resource." << std::endl;
        
        if (this != &other)
        {
            // Free the existing resource.
            delete[] _data;
            
            _length = other._length;
            _data = new int[_length];
            std::copy(other._data, other._data + _length, _data);
        }
        return *this;
    }
    
    // Retrieves the length of the data resource.
    size_t Length() const
    {
        return _length;
    }
    
private:
    size_t _length; // The length of the resource.
    int* _data; // The resource.
};

10. 如果构造函数或析构函数调用了某个虚函数,则我们应该执行与构造函数或析构函数所属类型相对应的虚函数版本

比如执行基类的构造函数时,不能调用派生类部分,因为其并未初始化;

11. “继承”构造函数

一个类只初始化它的直接基类。类不能继承默认、拷贝和移动构造函数,如果类没有直接定义这些构造函数,则编译器将为派生类合成它们。

使用using简化:

using Base::Base; //继承基类的构造函数

其不会改变构造函数的访问级别,基类的私有构造函数在派生类中还是一个私有构造函数等;

其在编译器中会默认生成构造函数:

Derived(params) : Base(args){}

有两种情况不会继承构造函数:

  • 派生类可以继承一部分构造函数,而为其它构造函数定义自己的版本;如果派生类定义的构造函数和基类的构造函数具有相同的参数列表,则该函数不会被继承,其会替换基类对应构造函数。
  • 默认、拷贝和移动构造函数不会被继承。

文章上面是从第15章的后面做的笔记,下面是前面做的笔记;

12.

原文地址:https://www.cnblogs.com/George1994/p/6399892.html