首页 > 编程语言 >第18 章探讨 C++新标准 移动语义和右值引用

第18 章探讨 C++新标准 移动语义和右值引用

时间:2024-08-09 20:24:18浏览次数:17  
标签:vstr 右值 对象 18 语义 C++ Useless 移动 构造函数

第18 章探讨 C++新标准 移动语义和右值引用

第18 章探讨 C++新标准 移动语义和右值引用

文章目录


18.2 移动语义和右值引用

现在介绍本书前面未讨论的主题。C++11支持移动语义,这就提出了一些问题:什么是移动语义?
C++11如何支持它?为何需要移动语义?下面首先讨论第一个问。
18.2.1 为何需要移动语义
先来看 C++11之前的复制过程。假设有如下代码:

vectorsatring> vstri
// build up a vector of 20,000 strings, each of 1000 characters...
vector<string>vstr copyl(vstr);// make vstr copyi a copy of vstr

vector和 string 类都使用动态内存分配,因此它们必须定义使用某种 new 版本的复制构造函数。为初始化对象vstr_copyl,复制构造函数 vcctor将使用new给20000个sting对象分配内存,而每个string对象又将调用 suing的复制构造函数,该构造函数使用new为1000个字符分配内存。接下来,全部20000000 个字符都将从 vstr 控制的内存中复制到vstrcopy控制的内存中。这里的工作量很大,但只要妥当就行。但这确实妥当吗?有时候答案是否定的。例如,假设有一个函数,它返回一个vector对象:

vectorestring>allcaps(const veetorestring>6 vs)
{
vector<string> temp;// code that stores an all-uppercase version of vs in temp
return temp;
}

接下来,假设以下面这种方式使用它:

vector<string> vstr;// build up a vector of 20,000 strings, each of 1000 characters
veetor<string> vstr copyi(vstr};// #3
vectorestring>vstr copy2(allcaps{vstr)};// #2

从表面上看,语句#】和#2类似,它们都使用一个现有的对象初始化一个 vector对象。如果深入探索这些代码,将发现 allcaps()创建了对象tcmp,该对象管理着20000000个字符:vector和string的复制构造函数创建这 20000000个字符的副本,然后程序删除allcaps()返回的临时对象(迟钝的编译器甚至可能将temp复制给一个临时返回对象,删除temp,再删除临时返回对象)。这里的要点是,做了大量的无用功。考虑到临时对象被删除了,如果编译器将对数据的所有权直接转让给vstr_copy2,不是更好吗?也就是说,不将20000000个字符复制到新地方,再删除原来的字符,而将字符留在原来的地方,并将vstr_copy2与之相关联。这类似于在计算机中移动文件的情形:实际文件还留在原来的地方,而只修改记录。这种方法被称为移动语义(movesemantics)。有点悖论的是,移动语义实际上避免了移动原始数据,而只是修改了记录。
要实现移动语义,需要采取某种方式,让编译器知道什么时候需要复制,什么时候不需要。这就是右值引用发挥作用的地方。可定义两个构造函数。其中一个是常规复制构造函数,它使用const左值引用作为参数,这个引用关联到左值实参,如语句#1中的vstr;另一个是移动构造函数,它使用右值引用作为参数,该引用关联到右值实参,如语句#2中 allcaps(vstr)的返回值。复制构造函数可执行深复制,而移动构造函数只调整记录。在将所有权转移给新对象的过程中,移动构造函数可能修改其实参,这意味着右值引用参数不应是const。
18.2.2 一个移动示例
下面通过一个示例演示移动语义和右值引用的工作原理。程序清单 18.2定义并使用了 Uscless 类,这个类动态分配内存,并包含常规复制构造函数和移动构造雨数,其中移动构造函数使用了移动语义和右值引用。为演示流程,构造函数和析构函数都比较啰嗦,同时Useless类还使用了一个静态变量来跟踪对象数量。另外,省略了一些重要的方法,如赋值运算符。

程序清单18.2 useless.cpp

// useless.cpp -- an otherwise useless class with move semantics
#include <iostream>
using namespace std;

// interface
class Useless
{
private:
    int n;          // number of elements
    char * pc;      // pointer to data
    static int ct;  // number of objects
    void ShowObject() const;
public:
    Useless();
    explicit Useless(int k);
    Useless(int k, char ch);
    Useless(const Useless & f); // regular copy constructor
    Useless(Useless && f);      // move constructor
    ~Useless();
    Useless operator+(const Useless & f)const;
// need operator=() in copy and move versions
    void ShowData() const;
};

// implementation
int Useless::ct = 0;

Useless::Useless()
{
    ++ct;
    n = 0;
    pc = nullptr;
    cout << "default constructor called; number of objects: " << ct << endl;
    ShowObject();
}

Useless::Useless(int k) : n(k)
{
    ++ct; 
    cout << "int constructor called; number of objects: " << ct << endl;
    pc = new char[n];
    ShowObject();
}

Useless::Useless(int k, char ch) : n(k)
{
    ++ct;
    cout << "int, char constructor called; number of objects: " << ct << endl;
    pc = new char[n];
    for (int i = 0; i < n; i++)
        pc[i] = ch;
    ShowObject();
}

Useless::Useless(const Useless & f): n(f.n) 
{
    ++ct;
    cout << "copy const called; number of objects: " << ct << endl;
    pc = new char[n];
    for (int i = 0; i < n; i++)
        pc[i] = f.pc[i];
    ShowObject();
}

Useless::Useless(Useless && f): n(f.n) 
{
    ++ct;
    cout << "move constructor called; number of objects: " << ct << endl;
    pc = f.pc;       // steal address
    f.pc = nullptr;  // give old object nothing in return
    f.n = 0;
    ShowObject();
}

Useless::~Useless()
{
    cout << "destructor called; objects left: " << --ct << endl;
    cout << "deleted object:\n";
    ShowObject();
    delete [] pc;
}

Useless Useless::operator+(const Useless & f)const
{
    cout << "Entering operator+()\n";
    Useless temp = Useless(n + f.n);
    for (int i = 0; i < n; i++)
        temp.pc[i] = pc[i];
    for (int i = n; i < temp.n; i++)
        temp.pc[i] = f.pc[i - n];
    cout << "temp object:\n";
    cout << "Leaving operator+()\n";
    return temp;
}

void Useless::ShowObject() const
{ 
    cout << "Number of elements: " << n;
    cout << " Data address: " << (void *) pc << endl;
}

void Useless::ShowData() const
{
    if (n == 0)
        cout << "(object empty)";
    else
        for (int i = 0; i < n; i++)
            cout << pc[i];
    cout << endl;
}

// application
int main()
{
    {
        Useless one(10, 'x');
        Useless two = one;          // calls copy constructor
        Useless three(20, 'o');
        Useless four(one + three);  // calls operator+(), move constructor
        cout << "object one: ";
        one.ShowData();
        cout << "object two: ";
        two.ShowData();
        cout << "object three: ";
        three.ShowData();
        cout << "object four: ";
        four.ShowData();
    }
    // cin.get();
}

其中最重要的是复制构造函数和移动构造函数的定义。首先来看复制构造函数(删除了输出语句);

Useless::Useless(constUseless &f):n(f.n)
++Ct;
pc =new charn];
for(int i=0;i <n; i++)
peli]= f.peli];

它执行深复制,是下面的语句将使用的构造函数:Useless two one;引用f将指向左值对象one。接下来看移动构造函数,这里也删除了输出语句:

Useless::Useless(Useless && f):n(f.n)
//ca11s copy constructor
++Ct;
pC =f.pe;//steal address
f.pe =nullptr;//give old obiect nothing in return
f.n= 0;

它让 pc指向现有的数据,以获取这些数据的所有权。此时,pc和fpc 指向相同的数据,调用析构函数时这将带来麻烦,因为程序不能对同一个地址调用dclete[]两次。为避免这种问题,该构造函数随后将原来的指针设置为空指针,因为对空指针执行deete[]没有问题。这种夺取所有权的方式常被称为窃取pilfering)。上述代码还将原始对象的元素数设置为零,这并非必不可少的,但让这个示例的输出更一致。注意,由于修改了f对象,这要求不能在参数声明中使用const。
在下面的语句中,将使用这个构造函数:

Useless four(one +three)://calls move constructor

表达式 one+three 调用 Useless::operator+O,而右值引用f将关联到该方法返回的临时对象。下面是在Microsoft VisualC++2010中编译时,该程序的输出:
在这里插入图片描述

注意到对象two是对象 one 的副本:它们显示的数据输出相同,但显示的数据地址不同(006F4B68 和006F4BB0)。另一方面,在方法Uscess:operator+()中创建的对象的数据地址与对象four 存储的数据地址相同(都是006F4C48),其中对象four是由移动复制构造函数创建的。另外,注意到创建对象fur 后为临时对象调用了析构函数。之所以知道这是临时对象,是因为其元素数和数据地址都是0。如果使用编译器 g++4.5.0和标记-std=c++11编译该程序(但将nullptr 替换为0),输出将不同,这很有趣:

注意到没有调用移动构造函数,且只创建了4个对象。创建对象four 时,该编译器没有调用任何构造函数;相反,它推断出对象four 是 operator+()所做工作的受益人,因此将 operator+()创建的对象转到 four的名下。一般而言,编译器完全可以进行优化,只要结果与未优化时相同。即使您省略该程序中的移动构造函数,并使用g++进行编译,结果也将相同。

标签:vstr,右值,对象,18,语义,C++,Useless,移动,构造函数
From: https://blog.csdn.net/zhyjhacker/article/details/141023516

相关文章

  • 【C++进阶学习】第十二弹——C++ 异常处理:深入解析与实践应用
    前言:在C++编程语言中,异常处理是一种重要的机制,它允许程序员在运行时捕获和处理错误或异常情况。本文将详细介绍C++异常处理的相关知识点,包括异常的定义、抛出与捕获、异常处理的原则、以及在实际编程中的应用。目录1.异常处理的基本概念1.1异常的定义1.2异常的抛出......
  • 用了两个月苹果iOS 18后 这是我最满意的几个地方
    自从六月的苹果全球开发者大会(WWDC)上发布了首个开发者版本以来,我便在我的iPhone15ProMax上开始试用iOS18。笔者发现,iOS18包含了许多引人注目的新特性。对于外界来说,iOS18目前可能看起来还不算完全成熟的产品,因为一些最令人期待的新功能尚未正式亮相。深色图标多年来......
  • c++入门这一篇就够了!!!
    c++简介“c++”中的++来自于c语言中的递增运算符++,该运算符将变量加1。c++起初也叫”cwithclsss”.通过名称表明,c++是对C的扩展,因此c++是c语言的超集,这意味着任何有效的c程序都是有效的c++程序。c++程序可以使用已有的c程序库。     为什么c++不叫++c呢?因为它虽然对......
  • 【C++】模板(相关知识点讲解 + STL底层涉及的模板应用)
    目录模板是什么?模板格式模板本质函数模板格式介绍显式实例化模板参数匹配原则类模板类模板的实例化非类型模板参数模板特化——概念函数模板特化类模板的特化全特化半特化偏特化三种类特化例子(放一起比较)模板分离编译STL中比较经典的模板应用(不包含argus)......
  • 移情别恋c++ ദ്ദി˶ー̀֊ー́ ) ——4.模板
    1.泛型编程如何实现一个通用的交换函数呢?voidSwap(int&left,int&right){inttemp=left;left=right;right=temp;}voidSwap(double&left,double&right){doubletemp=left;left=right;right=temp;}voidSwap(char&left,char&right)......
  • Windows图形界面(GUI)-MFC-C/C++ - 树形视图(Tree Control) - CTreeCtrl
    公开视频-> 链接点击跳转公开课程博客首页-> ​​​链接点击跳转博客主页目录树形视图(TreeControl)-CTreeCtrl创建和初始化添加和删除项获取和设置项属性操作项项选择变化项双击项展开示例代码树形视图(TreeControl)-CTreeCtrl创建和初始化Subclas......
  • Windows图形界面(GUI)-MFC-C/C++ - 列表视图(List Control) - CListCtrl
    公开视频-> 链接点击跳转公开课程博客首页-> ​​​链接点击跳转博客主页目录列表视图(ListControl)-CListCtrl创建列表视图设置列表视图属性成员函数注意事项示例代码列表视图(ListControl)-CListCtrl创建列表视图在对话框编辑器中,从工具箱中拖拽一个Li......
  • Visual C++ 官方版下载及安装教程必装(微软常用运行库合集|DLL报错必装)
    前言MicrosoftVisualC++Redistributable(简称MSVC,VB/VC,系统运行库)是Windows操作系统应用程序的基础类型库组件。此版VisualC++运行库组件合集(微软常用运行库合集)由国内封装爱好者@Dreamcast打包而成,整合VisualC++组件安装包运行库所有版本,提供图形安装界面,可自选更新V......
  • 《信息学奥赛一本通编程启蒙》3031-3050(Scratch、C、C++、python)
    3031:练7.3买图书(C、C++、python)3031:练7.3买图书(C、C++、python)-CSDN博客3032:练7.4梯形面积(C、C++、python)3032:练7.4梯形面积(C、C++、python)-CSDN博客3033:【例8.1】人民币支付(Scratch、C、C++、python)3033:【例8.1】人民币支付(Scratch、C、C++、python)-CSDN博客3......
  • 在国产芯片上实现YOLOv5/v8图像AI识别-【2.3】RK3588上使用C++启用多线程推理更多内容
    本专栏主要是提供一种国产化图像识别的解决方案,专栏中实现了YOLOv5/v8在国产化芯片上的使用部署,并可以实现网页端实时查看。根据自己的具体需求可以直接产品化部署使用。B站配套视频:https://www.bilibili.com/video/BV1or421T74f基础背景对于国产化芯片来说,是采用NPU进......