• Index

详述继承中的默认函数

Reads: 7

继承中的构造函数

我们知道当一个类没有显式地写明构造函数、复制构造函数、移动构造函数、析构函数、复制赋值运算符、移动赋值运算符的话,那么编译器就会为类添加这些特殊函数的默认版本,而派生类也不会例外。虽然之前教程中说过公共继承会继承所有公共成员函数,但是以上这些特殊成员函数是例外的。

当派生类没有显式写出构造函数时,编译器将会添加默认的构造函数,从而覆盖掉基类所有的构造函数,所以此时只剩下无参数的构造函数。如果自己定义的派生类需要和基类同样功能的构造函数,就需要自己再定义一次:

基础示例

#include <iostream>
#include <string>

class u32stringex : public std::u32string
{
public:
    u32stringex(void) = default; // 重载一
    u32stringex(const u32stringex &x); // 重载二
    u32stringex(u32stringex &&x); // 重载三
    u32stringex(size_type n, char32_t c); // 重载四
};

int main(void)
{
    u32stringex text(5, U'啊');
    std::cout << std::boolalpha << text.empty() << std::endl;
    return 0;
}

u32stringex::u32stringex(const u32stringex &x)
    : std::u32string(x) // 调用父类的复制构造函数
{
}

u32stringex::u32stringex(u32stringex &&x)
    : std::u32string(std::move(x)) // 调用父类的移动构造函数
{
}

u32stringex::u32stringex(size_type n, char32_t c)
    : std::u32string(n, c) // 调用父类的这个构造函数
{
}

基础讲解

字符串类有很多个构造函数,而我们现在只需要上面代码中的四个构造函数。由于上面代码中的派生类就只有这么多构造函数,所以基类的其他构造函数将被屏蔽而不能使用。因此在设计派生类时可以根据需要,在派生类的构造函数中作出相应的改变再调用基类的构造函数。

由于基类的成员也是需要初始化的,尤其是基类的私有成员,派生类是不能操作父类的私有成员的,所以全部构造函数都调用了基类相应的构造函数来初始化基类的成员。

先看无参构造函数:因为我们定义了多个构造函数,而且我们也需要无参数的构造函数,所以要显式地把无参数构造函数写出来。由于上面的派生类没有自己的成员变量需要初始化,所以使用默认构造函数就可以了。或许你会问,不重写无参数的构造函数怎样初始化基类的成员?这个问题比较有意思。震精!!派生类的默认构造函数竟然会做这种事!!:就是去调用基类的无参构造函数或者默认构造函数,让它去初始化基类的成员。所以如果派生类不需要手动初始化而且基类有无参构造函数或者默认构造函数时,派生类就可以使用默认构造函数。

再看复制构造函数和移动构造函数:我们可以看到,我们调用基类的复制和移动构造函数时,传入的值是u32stringex类型的,而基类的这两个函数只接收std::u32string,按照之前的讲解,应该是调用失败。然而实际并不是这样,因为派生类是可以转换成基类的,但是必须用基类的引用或者指针来操作派生类对象。这是因为基类对象和派生类对象内存大小不一样,数据也不一样,如果直接复制会发生错误,因此也会编译报错;而使用指针或引用由于不需要复制对象,而且派生类继承的部分和基类是一样的,所以是允许用基类的指针或引用来操作派生类对象。当转换成基类后,用基类的对象可以对派生类的对象进行操作,但只局限于基类自身的成员。上面代码中复制和移动构造函数调用基类的复制和移动构造函数时,在传入自身的对象的时候发生了隐式转换,所以可以调用成功。

最后一个重载的构造函数:它调用基类的对应的构造函数,这样就可以使用基类的这个函数了。

我们再看一下调用父类的构造函数:我们看到父类构造函数的调用是在初始化列表中,所以会先执行父类的构造函数,再执行子类的构造函数,子类的几个默认构造函数都是这样的。这样的调用顺序可以保证派生类初始化数据不会造成错乱。我们知道委托构造函数之后,初始化列表就不能再初始化其他成员变量,由于委托构造函数的定义是调用自身类名的构造函数,而这里是调用基类的构造函数,所以不是委托,因此,调用基类的构造函数之后,初始化列表可以再初始化派生类的成员变量。

注意:如果在构造函数里的{}调用基类的构造函数,这就不是初始化了,而是创建了一个基类的右值或者是一个函数声明,请观察清楚。

继承中的赋值运算符重载

当不显式写出赋值运算符重载时,编译器也会贴心地给你添加默认的赋值运算符重载。默认的赋值运算符重载函数只接收自身类型的对象,所以当传入字符串时就会编译报错。此时也是需要显式写出赋值运算符重载。

基础示例

先看例子(为了简化代码长度所以暂时不写构造函数):

#include <iostream>
#include <string>

class u32stringex : public std::u32string
{
public:
    u32stringex & operator=(const std::u32string &x);
    u32stringex & operator=(std::u32string &&x);
};

int main(void)
{
    u32stringex text1;
    text1 = U"尐古銀のс++教程徔繼承";
    u32stringex text2;
    text2 = std::move(text1);
    std::cout << std::boolalpha << text1.empty() << std::endl;
    std::cout << std::boolalpha << text2.empty() << std::endl;
}

u32stringex & u32stringex::operator=(const std::u32string &x)
{
    // 显式写明我需要调用的是父类的成员函数operator=
    // 如果不写明父类, 那么按照规则将优先调用自身类里的同名函数
    // 即自己调用自己, 就成了没有条件限制的递归函数了
    std::u32string::operator=(x);
    return *this;
}

u32stringex & u32stringex::operator=(std::u32string &&x)
{
    // 显式写明我需要调用的是父类的成员函数operator=
    // 如果不写明父类, 那么按照规则将优先调用自身类里的同名函数
    // 即自己调用自己, 就成了没有条件限制的递归函数了
    std::u32string::operator=(std::move(x));
    return *this;
}

基础讲解

重载的赋值运算符让它可以赋值std::u32string的字符串,这样直接赋值字符串就不会报错了。而在成员函数里面调用基类同名的成员函数,就必须加上基类的说明,如上面的std::u32string::,这样就会调用基类的成员函数而不是派生类的成员函数。

由于类u32stringex没有自身的成员变量,所以不需要重新写赋值,采用默认的赋值运算符即可。由于重载的赋值运算符的参数不是u32stringex,所以,参数为u32stringex的默认赋值运算符重载成员函数依然有效,因此代码中的text2 = text1;是没有错误的。

和构造函数一样,派生类的默认复制赋值运算符重载和默认移动赋值运算符重载都会分别调用基类的复制赋值运算符重载和默认移动赋值运算符重载。所以就算我不显式把赋值运算符重载写出来,也可以使用复制和转移。


Comments

Make a comment

  • Index

WARNING: You are using an old browser that does not support HTML5. Please choose a modern browser (Chrome / Microsoft Edge / Firefox / Sarafi) to get a good experience.