第12课 特殊成员函数的生成机制

摘要:
1、 特殊成员功能(一)概述1。特殊成员函数是指C++自身生成的成员函数。有六种主要类型:默认构造函数、析构函数、复制构造函数、复制赋值函数、移动构造函数和移动赋值函数。2.默认生成的特殊成员函数具有公共访问权限,并且是内联非虚拟函数。请注意,当当前声明复制赋值或析构函数时,仍将生成默认的复制构造函数,但此行为将被逐渐放弃。2.在任何情况下,成员函数模板都不会禁止生成特殊的成员函数。

一. 特殊成员函数

(一)概述

  1. 特殊成员函数指C++会自行生成的成员函数,主要有6种:默认构造函数、析构函数、复制构造函数、复制赋值函数、移动构造函数和移动赋值函数

  2. 默认生成的特殊成员函数都具有public访问权限且是inline的非虚函数(除析构例外)。通常,如果这些函数不被相关代码使用,编译器不会为其产生真正的函数代码

  3. 如果基类析构函数是虚函数,则编译器为派生类生成的析构函数也是个虚函数。

  4. 默认的复制或移动都是指对非静态成员的操作,它们都是“按成员复制”或“按成员移动”的而“按成员移动”实际上更像是按成员移动的请求,因为有些类型不具备移动操作,对于这些对象会通过其复制操作来实现“移动”。

(二)C++11中的生成机制

第12课 特殊成员函数的生成机制第1张 

  1. 默认构造函数:仅当类中不包含用户声明的构造函数时(含无参/带参构造函数、复制构造和移动构造)才生成。

  2. 析构函数:仅当基类的析构函数为virtual时,派生类的析构函数才是虚的。其与C++98的机制基本相同。唯一区别在于析构函数默认为noexcept。

  3. 复制构造函数:生成条件(该类未声明复制构造;②该类未声明移动操作;③该类未声明复制赋值或析构函数)。注意,在当前声明复制赋值或析构函数时,仍会生成默认的复制构造函数,但该行为会被逐渐废弃。

  4. 复制赋值函数:生成条件(① 该类未声明复制赋值;②该类未声明移动操作;③该类未声明复制构造或析构函数)。注意,在当前声明复制构造或析构函数时,仍会生成默认的复制赋值函数,但该行为会被逐渐废弃。

  5. 移动构造和移动赋值函数:仅当类中不包含用户声明的复制操作、移动操作和析构函数3个条件满足时才生成。

二、理解生成机制

(一)生成机制背后的思想

  1. 复制操作是彼此独立的,即声明了其中一个,并不会阻止编译器生成另一个。

    早期编译器认为默认的复制构造和复制赋值都是按成员复制的。因此,它仍愿意提供这种最简单的“浅拷贝”方式以供调用(如遇到成员变量是引用或常量时,就可以有机会提示报错信息)。而如果需要进行“深拷贝”则须由用户自行定义相应的函数。

  2. 移动操作并不彼此独立,即声明了其中一个,就会阻止编译器生成另一个。

    假设如果声明一个移动构造函数,实际上表示移动操作与编译器生成的按默认“按成员移动”是不同的。而如果“按成员移动”操作不符合要求时,按成员进行的移动赋值也极及可能不符合要求。

  3. 复制操作与移动操作会相互影响

    ①如果声明复制操作(无论是复制构造或复制赋值)的行为,这表明对象按成员复制不符合要求。编译器认为,既然按成员复制不符合要求,那么“按成员移动”也极可能不符合要求。

    ②反之,如果声明了移动操作(移动构造或移动赋值),就表示“按成员移动”不符合要求,也就没理由期望按成员复制符合要求。

(二)大三律及推论

  1. 大三律(Rule of Three)如果声明了复制构造、复制赋值或析构函数中的任何一个,就得同时声明所有这三个

    ①在一种复制操作中进行资源管理,也极有可能在另一种复制操作中也需要进行。

    ②该类的析构函数也会参与到该资源的管理中(释放之)。

  2. 推论

    ①如果声明的析构函数,则平凡的按成员复制不适于该类。即复制操作不会被自动生成,因为它们的行为都是不正确的。但在C++11中仍保留生成复制操作函数,仅仅是出于兼容C++98的遗留代码而己,这种行为将逐渐被废弃。

    ②如果声明了析构函数就不会生成移动操作(因为析构函数会抑制复制操作,而复制操作又会阻止生成移动操作)。

(三)注意事项

  1. 为了消除因相互抑制而产生的依赖关系,在C++11中可以通过=default来显式指示编译器生成默认版本的特殊成员函数。

  2. 成员函数模板在任何情况下都不会抑制特殊成员函数的生成。

  3. 在己经显式声明析构函数时,会阻止生成移动操作,而但仍会生成复制操作(将逐渐被废弃)。这可能会产生副作用,即原来移动操作可能会变成复制操作

  4. 根据大三律原则,当声明析构函数时,一般应同时提供复制操作和移动操作函数。

【编程实验】特殊成员函数生成机制

#include <iostream>
#include <map>

using namespace std;

//1. 演示特殊成员函数之间的抑制关系
//1.1 自定义析构函数会阻止默认移动操作(含移动构造和移动赋值)
class Foo
{
private:
    int i;
public:
    Foo() = default;
    Foo(const Foo& f) = delete;    //删除复制构造函数!让移动操作只会去找移动操作的函数,而不是被复制取代!
    Foo& operator=(const Foo&) = delete; //理由同上!

    ~Foo() {};  //这里自定义析构函数!
};

//1.2 自定义析构函数使“移动操作”变为“复制操作”!
class StringTable
{
    std::map<int, std::string> values;
    
    void makeLogEntry(std::string s){}; //日志记录
public:
    StringTable()
    {
        makeLogEntry("Creating StringTable Object");    
    }
    
    ~StringTable()   //注意,这里自定义了析构函数,会产生副作用!
    {
        makeLogEntry("Destroying StringTable Object");
    }
};

//2. 大三律
class Bar
{
public:
    virtual ~Bar() = default;  //自定义了析构函数,则应同时提供复制和移动操作!
    Bar(Bar&&) = default;     //提供移动操作的支持
    Bar& operator=(Bar&&) = default;

    Bar(const Bar&) = default;//提供移动操作的支持
    Bar& operator=(const Bar&) = default;
};

//3. 特种成员函数模板
class Widget
{
public:
    template<typename T>
    Widget(const T& rhs) { cout <<"Widget(const T& rhs)" << endl; }

    template<typename T>
    Widget& operator=(const T& rhs) 
    { 
        cout << "Widget& operator=(const T& rhs)" << endl;
        return *this; 
    }
    
public:
    Widget() = default;    
};

int main()
{
    //1. 自定义析构函数,会阻止移动操作
    Foo f;
    //Foo f1 = std::move(f);   //编译失败! 由于自定义了析构函数,默认移动操作被删除。
    //f1 = std::move(w);       //编译失败! 原因同上。
    
    StringTable st1;
    StringTable st2 = std::move(st1); //这里本意是调用移动构造函数,但因该类的析构函数阻止生成
                                      //默认的移动操作,转而变为复制操作。而对于values对象的复
                                      //制,而td::map<int, std::string>的复制往往比移动慢很多!

    //3. 特种成员函数模板不会抑制编译器生成默认的特殊成员函数
    Widget w;
    Widget w1 = w; //调用编译器生成的默认复制构造函数
    Widget w2 = 2; //调用函数模板:Widget(const T& rhs)
    
    w1 = w2; //调用编译器生成的默认复制赋值函数
    w1 = 2;  //调用函数模板:Widget& operator=(const T& rhs)

    return 0;
}
/*输出结果
Widget(const T& rhs)
Widget& operator=(const T& rhs)
*/

免责声明:文章转载自《第12课 特殊成员函数的生成机制》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇ubuntu-16.04使用MDK3伪造wifi热点和攻击wifi热点至死Navicat for sqlserver 破解版安装下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Android TimePickerDialog样式配置与TimePicker模式选择

习惯性的,把要说的内容先总结一下: TimePicker有两种模式:spinner 和clock,可通过如下方式配置: <TimePicker android:timePickerMode = "spinner" android:layout_width="match_parent" andr...

使用Intent实现Activity的显式跳转

【正文】 这里以按钮实现活动跳转为例,为实现这个功能,我们需要三个步骤: 1.点击按钮才发生页面跳转,因此,第一步我们先要找到要点击的按钮 如何拿到按钮对象呢?通过资源id,前面我们提到过,在R.id.xxx 中会有我们的资源id,但button按钮是在layout 中创建的,系统不会为其创建资源id,所以我们需要在layout 设置 button 时自己...

C++的隐式类型转换

C++是一种复杂的语言,其中有许多“好玩”的特性,学习C++的过程就像在海边捡一颗颗石头,只要坚持不懈,也许一颗颗小石头也能建起你自己小小的城堡。 废话完后,讲讲自己捡到的石头:隐式类型转换  学习出处:《Effective C++》 lostmouse大人翻译 class TestInt{ public:    int GetData()const{...

JS的继承

对于继承的实际运用还没有很好的理解,这里就先说说JS中继承的实现。 类式继承 作为基于原型的语言,JS也可以模仿类式继承。首先声明一个父类 function Person(name, age) { this.name = name; this.age = age; } Person.prototype.sayHello = function...

ASP.NET Core 如何使用Mvc相关技术建立Controller、Tag Helper (下)

上篇文章介绍了通过定义两个接口和服务类,分别实现这两个接口,并且进行了一个服务的注册。 今天就来建立Controller 接下来就是在控制器中通过构造函数的方式注入接口(见代码块6行) 代码块2行的意思是在构造函数中注入之后,赋值给一个本地只读的变量。 从代码块11行往后就是建立acttion了,一个是做部门查询用的Index方法,一个是做新增部门用的A...

显式等待-----Selenium快速入门(十)

  上一篇说了元素定位过程中的隐式等待,今天我们来探讨一下显示等待。显式等待,其实就是在使用WebDriverWait这个对象,进行等待。显式等待对比隐式等待,多了一些人性化的设置,可以说是更细化的隐式等待。   WebDriverWait 类继承自泛型类 FluentWait<T> ,而这个泛型类,又是泛型接口 Wait<T> 的...