迹忆客 专注技术分享

当前位置:主页 > 学无止境 > 编程语言 > C++ >

C++ 中的移动语义

作者:迹忆客 最近更新:2023/08/22 浏览次数:

在本文中,我们将讨论 C++ 中的移动语义:

  • 我们将讨论深拷贝和浅拷贝的相关概念。
  • 我们将快速讨论左值和右值的概念。
  • 我们将尝试通过示例来理解移动语义。

C++ 对象创建预备知识

让我们使用以下简单示例快速了解创建对象副本的机制:

class T{
  int x;
  public:
    T (int x=1){    this->x = x;    }
    int getX() const{    return x;    }
    void setX(int x){    this->x = x;    }
};
int main(){
  T o1;
  T o2(o1);
  cout << o1.getX()    << '\t'    << o2.getX()    << '\n';
  o2.setX(5);
  cout << o1.getX()    << '\t'    << o2.getX()    << '\n';
  return 0;
}

在这个简单的示例中,类 T 只有一个数据成员:x。 我们为 x 编写了一个带有默认值的参数化构造函数和两个工厂方法。

在 main() 中,我们使用可用的构造函数创建了对象 o1。 在第二行中,我们创建了另一个对象 o2,它是 o1 的副本。

该对象将通过每个类中默认存在的复制构造函数创建。 此默认构造函数对数据成员进行按成员的复制。

在第 11 行中,我们打印两个对象的数据成员,而第 4 行更改了 o2 的数据成员 x 的值。 最后,我们再次显示数据成员以查看修改对两个对象的影响。

这是该代码的输出:

1    1
1    5

在第一个输出行中,两个数据成员的值均为 1,因为第一个对象是使用默认值 1 的参数化构造函数创建的。由于第二个对象是 o1 的副本,因此两个值相同。

然而,稍后,o2 调用 setter 函数来修改其数据成员的值。 第二行输出表明,更改 o2 数据成员的值不会影响 o1 数据成员的值。

浅拷贝

不幸的是,如果数据成员是指向动态内存(在堆上创建)的指针,则上述复制机制不起作用。 在这种情况下,复制器对象指向由前一个对象创建的相同动态内存位置; 因此,复制器对象被称为具有另一个对象的浅表副本。

如果对象或数据成员是只读的,则该解决方案可以正常工作; 否则,可能会造成严重的问题。 我们先通过下面的代码示例来理解浅拷贝的概念:

class T{
      int *x, size;
  public:
      T (int s=5){
      size = s;
      x = new int[size];
      x[0]=x[1]=x[2]=x[3]=x[4]=1;
    }
      void set(int index, int val){    this->x[index] = val;    }
      void show() const{
      for (int i=0;i<size;i++)
        cout << x[i] << ' ';
      cout << '\n';
    }
};
int main(){
  T o1;
  T o2(o1);
  o1.show();
  o2.show();
  o2.set(2,5);
  o1.show();
  o2.show();
  return 0;
}

在此示例中,类 T 有两个数据成员:指针 x 和大小。 同样,我们有一个带有默认值的参数化构造函数。

我们已将默认参数值分配给构造函数体内的数据成员大小。

构造函数中的第 7 行和第 8 行声明了一个大小为 length 的动态数组,并将 1 分配给所有数组元素。 set 方法将 val 的值放置到动态数组的索引位置。

在main()中,我们创建了o1和o2,其中o2是复制对象。 接下来,我们打印这两个对象。

同样,我们正在修改 o2 中数组的第三个元素的值并打印对象。

这是该代码的输出:

1 1 1 1 1
1 1 1 1 1
1 1 5 1 1
1 1 5 1 1

现在,输出可能不符合您的预期。 前两行显示精确的副本; 然而,在第三行和第四行中,两个对象中有相同的元素,而我们只修改了一个。

这是因为默认的复制构造函数通过将第一个对象中的指针值(这是动态分配的数组的地址)分配给第二个对象的指针来创建浅复制。

同样,如果我们有只读值,那么这个浅拷贝就可以了; 否则,输出会清楚地显示更新风险。

深拷贝

与浅拷贝相反,在深拷贝中,我们为每个对象分配单独的动态内存。 然后我们对堆内的每个元素进行成员级复制。

这本质上是通过重载复制构造函数和赋值运算符来实现的。 请参阅编码示例,其中我们重载了复制构造函数:

T (const T &t){
  size = t.size;
  x = new int[size];
  for (int i=0;i<size;i++)
    x[i]=t.x[i];
}

请注意,在第二行中,新动态分配的地址被分配给x。 在第 3 行和第 4 行中,我们复制了所有动态数组元素以创建深层副本。

以下是添加复制构造函数后上一个示例中编写的主要代码的输出:

1 1 1 1 1
1 1 1 1 1
1 1 1 1 1
1 1 5 1 1

第 3 行和第 4 行显示第二个对象对第一个对象没有修改效果。 这意味着更改仅是第二个对象所固有的,因为现在 2 是 o1 的深层副本。

现在,让我们快速讨论一下左值和右值的概念。

左值和右值

左值或左值是指接收值的赋值运算符左侧的操作数。 相反,右值是指提供值的赋值运算符右侧的操作数。

因此,简单变量既可以用作左值,也可以用作右值。 常量或变量常量只能用作右值。

表达式是右值。 例如,我们可以写a = b + c,但不能写b + c = a,或者不能写2 = a。


C++ 中的移动语义

最后,我们开始讨论移动语义。 在这里,我们将讨论有关复制构造函数的概念。

首先,请参阅以下对类 T 的复制构造函数的调用:

T o2(o1);
T o3(o1-o2);

如果我们深入分析上面的代码,只有第一行需要深度复制,稍后我们可以在其中检查 o1。 此外,我们可以在复制构造函数中使用一条语句来修改 o1 的内容(例如,像 t.set(2,5) 这样的语句)。

因此,我们说 o1 是左值。

然而,表达式 o1-o2 不是左值; 相反,它是一个右值,因为它是一个一致(没有名称)的对象,并且我们不需要再次访问 o1-o2。 因此,我们可以说右值代表在执行所考虑的语句后不久就被销毁的临时对象。

C++0x 引入了一种称为 RValue Reference 的新方法,用于引用右值类型。 这种新方法允许在函数重载中匹配右值参数。

为此,我们必须编写另一个带有右值引用参数的构造函数。

T(T&& t){
    x = t.data;
    t.data = nullptr;
}

首先,请注意新的构造函数(称为移动构造函数而不是复制构造函数),我们在其中添加了一个 & 符号,它是右值的引用。

在第 2 行中,我们没有对动态数组进行深层复制,而是将现有对象的地址分配给新对象。 为了进一步安全,我们将 nullptr 分配给现有对象的指针。

理由是我们不需要访问临时源对象。 因此,在移动构造函数中,我们将资源(即动态数组)从现有(临时)对象移动到新的调用对象。

为了更好地理解它,让我们看一下另一个重载赋值运算符的代码:

T& operator = (T t){
  size = t.size;
  for (int i=0;i<size;i++)
    x[i] = t.x[i];
  return *this;
}

我们在这里没有使用右值引用。 在C++0x中,编译器将检查参数是右值还是左值,并相应地调用移动或复制构造函数。

因此,如果我们调用赋值运算符o1=o2,复制构造函数将初始化t。 然而,如果我们调用赋值运算符 o3 = o2-o1,移动构造函数将初始化 t。

原因是 o2-o1 是右值而不是左值。

最后,我们得出结论,复制构造函数可用于创建深层复制,以保存保持源对象安全以供进一步访问的选项。 移动构造函数将现有对象的动态内存分配给调用对象的指针,因为以后不需要访问右值对象。

上一篇:在 C++ 中抛出异常

下一篇:没有了

转载请发邮件至 1244347461@qq.com 进行申请,经作者同意之后,转载请以链接形式注明出处

本文地址:

相关文章

在 C++ 中抛出异常

发布时间:2023/08/21 浏览次数:200 分类:C++

C++抛出异常是C++的一个强大功能,可用于处理错误和意外事件。 它主要用于终止程序的执行或将控制权转移到程序的不同部分。在 C++ 中抛出异常

C++ 中抛出超出范围的异常

发布时间:2023/08/21 浏览次数:176 分类:C++

This article discusses how to throw an out of range exception in C++. It also discusses the possible errors while throwing out of range exception in C++.

在 C++ 中抛出异常消息

发布时间:2023/08/21 浏览次数:192 分类:C++

它是通过在程序中可能出现问题的地方抛出异常来执行的。 C++ 中有几个异常处理关键字,但本文将介绍如何使用可变消息引发异常。使用标准 C++ 异常抛出带有消息的异常 - 无效参数

C++ 中的 A Declaration Shadows a Parameter 错误

发布时间:2023/08/21 浏览次数:188 分类:C++

每个对象或变量总是有一些边界、范围或作用域来访问其他类成员,例如由 C++ 中的访问说明符定义为 public、private 或 protected 的数据成员或成员函数。当我们在程序的特定范围或块中多次定义

C++ 中的错误 Error ID Returned 1 Exit Status

发布时间:2023/08/21 浏览次数:130 分类:C++

C++ [Error]: Id returned 1 exit status 不是常见错误。 这通常意味着程序崩溃了,并且在不查看堆栈跟踪的情况下很难确定原因。

C++ 中错误 Too Many Arguments to Function

发布时间:2023/08/21 浏览次数:178 分类:C++

我们在编写一段代码时会遇到很多错误。 解决错误是编程中最关键的部分之一。本文将讨论我们在 C++ 中遇到的一个错误:Too Many Arguments to Function。

处理 C++ 中的错误

发布时间:2023/08/21 浏览次数:197 分类:C++

本文讨论了 C++ 中的错误和异常处理。C++ 中的错误处理 C++ 程序中可能存在多种类型的错误。 有些错误需要向用户发出提示。

C++ 中错误 Function Returns the Address of a Local Variable

发布时间:2023/08/21 浏览次数:119 分类:C++

根据作用域,C 和 C++ 中的变量分为局部变量和全局变量。 虽然可以从程序的任何部分访问全局变量,但局部变量却不然。让我们讨论一下为什么会出现这个错误以及如何修复它。

C++ 错误 Invalid Conversion of Int* to Int

发布时间:2023/08/21 浏览次数:154 分类:C++

这个简短的教程将讨论错误消息 Invalid conversation of int* to int 。 首先,让我们回顾一下 C++ 中的指针。

扫一扫阅读全部技术教程

社交账号
  • https://www.github.com/onmpw
  • qq:1244347461

最新推荐

教程更新

热门标签

扫码一下
查看教程更方便