C++中类的基本操作主要包括:
若我们没有定义,则编译器会为我们默认创建一个拷贝构造函数,实现的是浅拷贝。
浅拷贝默认会这样来实现:
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;
};
拷贝构造函数被调用可能发生在以下几种情况:
Point p1;//调用默认构造函数
Point p2 = p1;//调用拷贝构造函数
void Test(Point point)
{
//即使什么都不做,也会调用一次Point的拷贝构造函数
}
Point Test()
{
Point point;
return point;//调用拷贝构造函数
}
std::vector<Point> points{ Point(1, 2, 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;
};
其实所做的无非就是对于指针类型的成员变量,在拷贝的时候,需要重新申请一块内存,而不是像浅拷贝那样多个对象最后指向了同一块内存。
与拷贝构造函数一样,如果类未定义自己的拷贝构造赋值运算符,编译器会默认创建一个。
小区分:
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;
赋值运算符通常组合了析构函数和构造函数的操作。类似析构函数,赋值操作会销毁左侧运算对象的资源;类似拷贝构造函数,赋值操作会从右侧运算对象拷贝数据。
同样的,如果类的成员变量中没有指针,默认的拷贝赋值运算符即可满足要求。
还是上面的例子重写一遍:
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;
};
可以看出,拷贝赋值运算符重载考虑的其实比拷贝构造函数更多。
大多数赋值运算符组合了析构函数和拷贝构造函数的工作。
析构函数的作用与构造函数刚好相反,构造函数初始化对象的非static数据成员;析构函数释放对象使用的资源,并销毁对象的非static数据成员。
class Point
{
public:
~Point();//析构函数
}
成员销毁时发生什么完全依赖于成员的类型:
当一个对象被销毁,就会自动调用析构函数:
{
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)也被销毁
Point* p1 = new Point();
delete p1;
std::vector<Point> points{ Point(1, 2, 3) };
这里的Point(1, 2, 3)
就是一个临时变量,被塞入points之后,创建它的表达式就结束了,该临时变量会被销毁。
移动针对的都是右值,拷贝的是左值。
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;
}
...
};
只有当一个类没有定义任何自己版本的拷贝控制成员,且它的所有数据成员都能移动构造或移动赋值时,编译器才会为它合成移动构造函数或移动赋值运算符。
所有内置类型的成员,都可以移动。
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));//调用拷贝构造函数,因为定义了拷贝构造,所有不会有默认的移动构造。
//如果定义了移动构造,那么这里就会调用移动构造
如果一个成员函数同时提供拷贝和移动版本,它也能从中受益。
和拷贝/移动构造的参数一样,会有2个版本:
void push_back(const T&);// 拷贝
void push_back(T&&);// 移动
若定义了移动构造,则当参数为右值引用时(比如std::move,就是为了得到一个右值引用)