C++中类的基本操作主要包括:
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数
- 移动赋值运算符
- 析构函数
文章目录
1. 拷贝构造函数
1.1 定义
若我们没有定义,则编译器会为我们默认创建一个拷贝构造函数,实现的是浅拷贝。
浅拷贝默认会这样来实现:
- 对类类型的成员,会适用其拷贝构造函数来拷贝;
- 内置类型的成员则直接拷贝;
- 数组,取决于数组元素的类型,采用方式1或者方式2来进行拷贝。
class Point
{
public:
// 默认构造函数
Point() :m_x(0), m_y(0), m_z(0)
{
std::cout << "Point默认构造" << std::endl;
}
// 拷贝构造函数
Point(const Point& other) :m_x(other.m_x), m_y(other.m_y), m_z(other.m_z)
{
std::cout << "Point拷贝构造" << std::endl;
}
private:
unsigned int m_x;
unsigned int m_y;
unsigned int m_z;
};
1.2 调用时机
拷贝构造函数被调用可能发生在以下几种情况:
- 用=来定义变量时;
Point p1;//调用默认构造函数
Point p2 = p1;//调用拷贝构造函数
- 将一个对象作为实参传递给一个非引用类型的形参
void Test(Point point)
{
//即使什么都不做,也会调用一次Point的拷贝构造函数
}
- 从一个返回类型为非引用类型的函数返回一个对象
Point Test()
{
Point point;
return point;//调用拷贝构造函数
}
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
std::vector<Point> points{ Point(1, 2, 3) };//有参构造和拷贝构造
1.3 什么时候需要定义自己的拷贝构造函数
如果一个类中的成员变量没有指针,那么默认的拷贝构造函数就可以了(即使是浅拷贝)。
我们来看下面的例子:
class Line
{
public:
// 有参构造函数
Line(const Point& startPoint, const Point& endPoint)
:m_startPoint(new Point(startPoint)), m_endPoint(new Point(endPoint))
{
std::cout << "Line有参构造" << std::endl;
}
~Line()
{
delete m_startPoint;
delete m_endPoint;
}
private:
Point* m_startPoint;
Point* m_endPoint;
};
int main()
{
Point p0;
Point p1;
Line line1(p0, p1);
Line line2 = line1;
}
程序结束的时候,line1
和line2
都会自动被销毁,这个时候就会抛出异常,因为两者的成员变量m_startPoint
其实指向了同一块内存,所以在被delete第二次时候当然抛出异常,m_endPoint
也是如此。这个时候我们就需要深拷贝,也就需要实现自己的拷贝构造函数了:
class Line
{
public:
// 有参构造函数
Line(const Point& startPoint, const Point& endPoint)
:m_startPoint(new Point(startPoint)), m_endPoint(new Point(endPoint))
{
std::cout << "Line有参构造" << std::endl;
}
// 拷贝构造函数
Line(const Line& other)
:m_startPoint(new Point(*other.m_startPoint)),
m_endPoint(new Point(*other.m_endPoint))
{
std::cout << "Line拷贝构造" << std::endl;
}
~Line()
{
delete m_startPoint;
delete m_endPoint;
}
private:
Point* m_startPoint;
Point* m_endPoint;
};
其实所做的无非就是对于指针类型的成员变量,在拷贝的时候,需要重新申请一块内存,而不是像浅拷贝那样多个对象最后指向了同一块内存。
2. 拷贝赋值运算符
2.1 定义
与拷贝构造函数一样,如果类未定义自己的拷贝构造赋值运算符,编译器会默认创建一个。
小区分:
Point p1;
Point p2 = p1;
Point p3;
p3 = p2;
第二行并没有调用拷贝赋值运算符,而是一个拷贝初始化,所以这一行调用的是拷贝构造函数;
p3已经在第三行完成了初始化(调用了默认构造函数),在第四行调用了拷贝赋值运算符。
class Point
{
public:
// 默认构造函数
Point() :m_x(0), m_y(0), m_z(0)
{
std::cout << "Point默认构造" << std::endl;
}
// 拷贝构造函数
Point(const Point& other) :m_x(other.m_x), m_y(other.m_y), m_z(other.m_z)
{
std::cout << "Point拷贝构造" << std::endl;
}
// 拷贝赋值运算符重载
Point& operator=(const Point& other)
{
std::cout << "Point拷贝赋值运算符" << std::endl;
m_x = other.m_x;
m_y = other.m_y;
m_z = other.m_z;
return *this;
}
private:
unsigned int m_x;
unsigned int m_y;
unsigned int m_z;
};
注意:赋值运算符重载中,最后返回的是一个此对象的引用return * this;
2.2 何时需要定义自己的拷贝赋值运算符
赋值运算符通常组合了析构函数和构造函数的操作。类似析构函数,赋值操作会销毁左侧运算对象的资源;类似拷贝构造函数,赋值操作会从右侧运算对象拷贝数据。
同样的,如果类的成员变量中没有指针,默认的拷贝赋值运算符即可满足要求。
还是上面的例子重写一遍:
class Line
{
public:
// 有参构造函数
Line(const Point& startPoint, const Point& endPoint)
:m_startPoint(new Point(startPoint)), m_endPoint(new Point(endPoint))
{
std::cout << "Line有参构造" << std::endl;
}
// 拷贝构造函数
Line(const Line& other)
:m_startPoint(new Point(*other.m_startPoint)),
m_endPoint(new Point(*other.m_endPoint))
{
std::cout << "Line拷贝构造" << std::endl;
}
~Line()
{
delete m_startPoint;
delete m_endPoint;
}
private:
Point* m_startPoint;
Point* m_endPoint;
};
int main()
{
Point p0;
Point p1;
Line line1(p0, p1);
Line line2(p0, p1);
line2 = line1;
}
最后一行调用默认的拷贝赋值运算符(浅拷贝),程序结束析构line1
和line2
的时候,导致m_startPoint
和m_endPoint
都被delete两次,然后抛异常。
class Line
{
public:
// 有参构造函数
Line(const Point& startPoint, const Point& endPoint)
:m_startPoint(new Point(startPoint)), m_endPoint(new Point(endPoint))
{
std::cout << "Line有参构造" << std::endl;
}
// 拷贝构造函数
Line(const Line& other)
:m_startPoint(new Point(*other.m_startPoint)),
m_endPoint(new Point(*other.m_endPoint))
{
std::cout << "Line拷贝构造" << std::endl;
}
// 拷贝赋值运算符重载
Line& operator=(const Line& other)
{
Point* newStart = new Point(*other.m_startPoint);
Point* newEnd = new Point(*other.m_endPoint);
delete m_startPoint;//删除旧内存(不可以先delete再new,防止other和*this是同一个对象)
delete m_endPoint;
m_startPoint = newStart;//从右侧运算符拷贝数据到本对象
m_endPoint = newEnd;
return *this;//返回本对象
}
~Line()
{
delete m_startPoint;
delete m_endPoint;
}
private:
Point* m_startPoint;
Point* m_endPoint;
};
可以看出,拷贝赋值运算符重载考虑的其实比拷贝构造函数更多。
大多数赋值运算符组合了析构函数和拷贝构造函数的工作。
3. 析构函数
3.1 定义
析构函数的作用与构造函数刚好相反,构造函数初始化对象的非static数据成员;析构函数释放对象使用的资源,并销毁对象的非static数据成员。
class Point
{
public:
~Point();//析构函数
}
成员销毁时发生什么完全依赖于成员的类型:
- 销毁类类型的成员需要执行成员自己的析构函数
- 内置类型没有析构函数,无需执行操作
3.2 调用时机
当一个对象被销毁,就会自动调用析构函数:
- 变量在离开其作用域时被销毁;
{
Point p1;
}
- 当一个对象被销毁时,其成员被销毁;
- 容器(无论是标准库容器还是数组)被销毁时,其元素被销毁;
Point p1;
Point p2;
{
std::vector<Point> points;
points.reserve(2);
points.push_back(p1);//拷贝构造
points.push_back(p2);//拷贝构造
}
//离开大括号作用域后,points对象被销毁,points容器内的p1'和p2'(不是p1和p2)也被销毁
- 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁;
Point* p1 = new Point();
delete p1;
- 对于临时对象,当创建它的完整表达式结束时被销毁;
std::vector<Point> points{ Point(1, 2, 3) };
这里的Point(1, 2, 3)
就是一个临时变量,被塞入points之后,创建它的表达式就结束了,该临时变量会被销毁。
4. 移动构造函数
4.1 定义
移动针对的都是右值,拷贝的是左值。
class Point
{
public:
...
// 移动构造函数
Point(Point&& other) :m_x(other.m_x), m_y(other.m_y), m_z(other.m_z)
{
std::cout << "Point移动构造" << std::endl;
}
// 移动赋值运算符
Point& operator=(Point&& other)
{
std::cout << "Point移动赋值运算符" << std::endl;
m_x = other.m_x;
m_y = other.m_y;
m_z = other.m_z;
return *this;
}
...
};
4.2 合成的移动操作
只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符。
所有内置类型的成员,都可以移动。
struct X
{
int i;
std::string s;
};
struct hasX
{
X mem;
};
X x1;
X x2 = std::move(x1);//会使用默认的移动构造函数来初始化x2
hasX h1;
hasX h2 = std::move(h2);//会使用默认的移动构造函数来初始化h2
class Foo
{
public:
Foo() = default;
Foo(const Foo&);
};
Foo x;
Foo y(x);//调用拷贝构造函数,x是一个左值
Foo z(std::move(x));//调用拷贝构造函数,因为定义了拷贝构造,所有不会有默认的移动构造。
//如果定义了移动构造,那么这里就会调用移动构造
4.3 右值引用和成员函数
如果一个成员函数同时提供拷贝和移动版本,它也能从中受益。
和拷贝/移动构造的参数一样,会有2个版本:
- const Foo&(拷贝)
- Foo&&(移动)
例如,定义了push_back的标准库容器提供2个版本:一个版本有一个右值引用参数,而另一个版本有一个const左值引用:
void push_back(const T&);// 拷贝
void push_back(T&&);// 移动
4.4 调用时机
若定义了移动构造,则当参数为右值引用时(比如std::move,就是为了得到一个右值引用)