迹忆客 专注技术分享

当前位置:主页 > 学无止境 > 编程语言 > 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 是右值而不是左值。

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

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

本文地址:

相关文章

Arduino 中停止循环

发布时间:2024/03/13 浏览次数:444 分类:C++

可以使用 exit(0),无限循环和 Sleep_n0m1 库在 Arduino 中停止循环。

Arduino 复位

发布时间:2024/03/13 浏览次数:315 分类:C++

可以通过使用复位按钮,Softwarereset 库和 Adafruit SleepyDog 库来复位 Arduino。

Arduino 的字符转换为整型

发布时间:2024/03/13 浏览次数:181 分类:C++

可以使用简单的方法 toInt()函数和 Serial.parseInt()函数将 char 转换为 int。

Arduino 串口打印多个变量

发布时间:2024/03/13 浏览次数:381 分类:C++

可以使用 Serial.print()和 Serial.println()函数在串口监视器上显示变量值。

Arduino if 语句

发布时间:2024/03/13 浏览次数:123 分类:C++

可以使用 if 语句检查 Arduino 中的不同条件。

Arduino ICSP

发布时间:2024/03/13 浏览次数:214 分类:C++

ICSP 引脚用于两个 Arduino 之间的通信以及对 Arduino 引导加载程序进行编程。

使用 C++ 编程 Arduino

发布时间:2024/03/13 浏览次数:127 分类:C++

本教程将讨论使用 Arduino IDE 在 C++ 中对 Arduino 进行编程。

Arduino 中的子程序

发布时间:2024/03/13 浏览次数:168 分类:C++

可以通过在 Arduino 中声明函数来处理子程序。

扫一扫阅读全部技术教程

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

最新推荐

教程更新

热门标签

扫码一下
查看教程更方便