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.