C++类的基本操作

MirrorYuChen
MirrorYuChen
发布于 2024-12-09 / 19 阅读
0
0

C++类的基本操作

1.C++类的基本操作

​ 一个 C++类的基本操作包括如下几种:

class X {
public:
  X();                            // 默认构造函数
  X(sometype);                    // 普通构造函数
  X(const X &);                   // 拷贝构造函数
  X(X &&);                        // 移动构造函数
  X &operator=(const X &);        // 拷贝赋值操作符
  X &operator=(X &&);             // 移动赋值操作符
  ~X();                           // 析构函数
  // ...
};

​ 赋值语句通过拷贝或移动赋值操作符,而在其它情况下,将使用移动构造函数或拷贝构造函数。默认情况下,编译器会根据需要来生成上面这些成员函数,当然,普通构造函数除外,若程序员希望显式地使用这些函数的默认实现,可以使用关键字 =default,如:

class Y {
public:
  Y(sometype);
  Y(const Y&) = default;         // 使用默认生成的拷贝构造函数
  Y(Y &&) = default;             // 使用默认生成的移动构造函数 
  // ...
};

​ 当类中含有指针成员时,最好显式指定拷贝操作和移动操作,若不这么做,当编译器生成默认函数尝试 delete指针对象时,系统会发生错误。

​ 与 =default一样,还有 =delete符号用于声明不生成目标操作函数,例如,类层次结构的基类通常不允许拷贝:

class Shape {
public:
  Shape(const Shape&) = delete;
  Shape &operator=(const Shape&) = delete;
  // ...
};

2.C++类的拷贝操作

​ 拷贝的默认含义是逐成员地复制,即依次复制每个成员。对于简单的具体类型而言,逐元素复制的方式通常符合拷贝操作的本来语义,然而对于复杂的具体类型,逐成员复制的方式通常是不正确的,抽象类型更是如此。如:

#pragma once

#include <iostream>
#include <cassert>

class Vector {
public:
  Vector(int s) : elem {new double[s]}, sz{s} {
    std::cout << "Constructor called." << std::endl;
  }
  ~Vector() {
    delete[] elem;
    std::cout << "Destructor called." << std::endl;
  }

  Vector(const Vector &other) : elem{new double[other.sz]},
                                sz{other.sz} {
    for (int i = 0; i < sz; ++i) {
      elem[i] = other.elem[i];
    }
    std::cout << "Copy constructor called." << std::endl;
  }
  Vector &operator=(const Vector &other) {
    elem = new double[other.sz];
    sz = other.sz;
    for (int i = 0; i < sz; ++i) {
      elem[i] = other.elem[i];
    }
    std::cout << "Copy assignment operator called." << std::endl;
    return *this;
  }

  double &operator[](int i) {
    assert(i >= 0 && i < sz);
    return elem[i];
  }
  const double &operator[](int i) const {
    assert(i >= 0 && i < sz);
    return elem[i];
  }

  int size() const {
    return sz;
  }

private:
  double *elem;
  int sz;
};

​ 拷贝语义正确定义应该首先为指定数量的元素分配空间,然后把元素复制到空间中,这样在复制过程中,每个 Vector就拥有了自己元素拷贝了。

3.C++类的移动操作

​ 我们可以通过定义拷贝构造函数和拷贝赋值操作符来控制拷贝过程,但对于大容量容器而言,拷贝过程可能消耗巨大。当给函数传递参数时,可用引用类型来减少拷贝对象的代价,但无法返回局部对象的引用。如:

Vector operator+(const Vector &a, const Vector &b) {
  if (a.size() != b.size()) {
    throw Vector_size_mismatch{};
  }
  Vector res(a.size());
  for (int i = 0; i < a.size(); ++i) {
    res[i] = a[i] + b[i];
  }
  return res;
}

void func(const Vector &x, const Vector &y, const Vector &z) {
  Vector r;
  // ...
  r = x + y + z;
  // ...
}

​ 调用函数 func时,至少需要拷贝 Vector对象两次(每个 +操作一次),若 Vector容量比较大,上述过程将会让人头疼不已,最不合理的地方就是 operator+中res在拷贝后就不再使用,这里可以给 Vector添加移动构造函数,执行从函数中移出返回值的任务。

#pragma once

#include <iostream>
#include <cassert>

class Vector {
public:
  Vector(int s) : elem {new double[s]}, sz{s} {
    std::cout << "Constructor called." << std::endl;
  }
  ~Vector() {
    delete[] elem;
    std::cout << "Destructor called." << std::endl;
  }

  Vector(const Vector &other) : elem{new double[other.sz]},
                                sz{other.sz} {
    for (int i = 0; i < sz; ++i) {
      elem[i] = other.elem[i];
    }
    std::cout << "Copy constructor called." << std::endl;
  }
  Vector &operator=(const Vector &other) {
    elem = new double[other.sz];
    sz = other.sz;
    for (int i = 0; i < sz; ++i) {
      elem[i] = other.elem[i];
    }
    std::cout << "Copy assignment operator called." << std::endl;
    return *this;
  }

  Vector(Vector &&other) : elem {other.elem}, sz {other.sz} {
    other.elem = nullptr;
    other.sz = 0;
    std::cout << "Move constructor called." << std::endl;
  }

  Vector &operator=(Vector &&other) {
    elem = other.elem;
    sz = other.sz;
    other.elem = nullptr;
    other.sz = 0;
    return *this;
  }

  double &operator[](int i) {
    assert(i >= 0 && i < sz);
    return elem[i];
  }
  const double &operator[](int i) const {
    assert(i >= 0 && i < sz);
    return elem[i];
  }

  int size() const {
    return sz;
  }

private:
  double *elem;
  int sz;
};

​ &&的意思是右值引用,右值的意思与左值正好相反,左值的大致含义为:能出现在赋值操作符左侧的内容。 右值的大概意思则是:无法为其赋值的值, 如,函数调用返回的一个整数就是一个右值。进一步地,右值引用地含义就是引用了一个别人无法赋值地内容,所以我们可以安全地窃取它的值。

​ 移动构造函数不接受 const实参,因为移动构造函数最终要删除它实参中的值,移动赋值操作符的定义与之类似。这里需要说一下标准库中的 std::move()函数,它并不会移动什么,而是负责返回我们能移动函数实参的引用——右值引用,这是一种强制类型转换。

4.参考资料

  • [1]斯特劳斯特卢普.C++之旅:英文版[M].电子工业出版社,2016.

评论