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 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 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);
}
- 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁;
Point* p1 = new Point();
delete p1;
- 对于临时对象,当创建它的完整表达式结束时被销毁;
std::vector<Point> points{ Point(1, 2, 3) };
这里的Point(1, 2, 3) 就是一个临时变量,被塞入points之后,创建它的表达式就结束了,该临时变量会被销毁。
|