前言
c++在创建一个空的类时,可以自动创建哪些函数呢?答案从表面上看来应该至少有六个:构造函数与析构函数,拷贝构造函数与拷贝赋值函数,移动构造函数与移动赋值函数(虽然在实际运行中会被编译器优化,这在后面会提到)。
#include <memory>
class A{ // 一个空类
};
int main(){
A* a = new A(); // 构造函数
A b(*a); // 拷贝构造函数
A c(std::move(*a)); // 移动构造函数
A d;
d = *a; // 拷贝赋值函数
d = std::move(*a); // 移动赋值函数
delete a; // 析构函数
}
接下来我们将对这些函数一一介绍。
1. 构造与析构
1.1 构造函数
简单构造函数就是在实例化对象时,根据传入的参数 来初始化一些参数或值。
#include <memory>
#include <iostream>
using namespace std;
class A{ // 一个空类
public:
A(){
cout << "空的构造函数" << endl;
}; // 空的构造函数
A(int n){
cout << "带参数的构造函数" << endl;
m = n;
}
private:
int m;
};
int main(){
A a; // 调用第一个构造函数
A b(2); // 调用第二个构造函数
}
1.2 析构函数
析构函数就是在对象被销毁时调用的,同基本的数据类型一样,声明在栈上的对象随作用域的结束而自动删除,声明在堆上的对象需要手动调用delete删除。
#include <memory>
#include <iostream>
using namespace std;
class A{
public:
A(){
cout << "空的构造函数" << endl;
}; // 空的构造函数
~A(){
cout << "调用析构函数函数" << endl;
}
};
int main(){
A* a = new A(); // 构造函数
delete a; // 析构函数
}
2.拷贝构造与拷贝赋值
不论是拷贝构造函数拷贝赋值,拷贝的对象都是左值。
2.1 拷贝构造函数
class A{
public:
A(){
cout << "空的构造函数" << endl;
}; // 空的构造函数
A(A& a){ // A& 为左值引用,也就是说拷贝函数传入的是一个左值。
cout << "调用拷贝构造函数" << endl;
}
};
int main(){
A a;
A b(a); // 调用拷贝构造函数
A c = a; // 调用拷贝构造函数
}
2.2 拷贝赋值函数
class A{
public:
A(){
cout << "空的构造函数" << endl;
}; // 空的构造函数
void operator=(A& a){ // A& 为左值引用,也就是说赋值函数传入的是一个左值。
cout << "调用拷贝赋值函数" << endl;
};
int main(){
A a;
A d;
d = a; // 调用拷贝赋值函数
}
3.移动构造与移动赋值
3.1 移动构造函数
class A{
public:
A(){
cout << "空的构造函数" << endl;
}; // 空的构造函数
A(A&& a){ // A&& 为右值引用,也就是说拷贝函数传入的是一个右值。
cout << "调用移动构造函数" << endl;
}
};
int main(){
A a;
A b(std::move(a)); // 调用移动构造函数
A c = std::move(a); // 调用移动构造函数
}
3.2 移动赋值函数
class A{
public:
A(){
cout << "空的构造函数" << endl;
}; // 空的构造函数
void operator=(A&& a){ // A&& 为右值引用,也就是说赋值函数传入的是一个右值。
cout << "调用移动赋值函数" << endl;
}
};
int main(){
A a;
A d;
d = std::move(a); // 调用移动赋值函数
}
4. 深拷贝与浅拷贝
浅拷贝就是按位复制内容,例如类里有指针,指针管理着另外一片空间,浅拷贝只会复制指针的地址。这样两个对象的指针指向同一片地址,在调用类的析构函数时可能会出现错误。
在这里说可能的原因是,这取决于我们指针指向的空间来自哪里以及析构函数中如何处理该指针管理的空间。但这通常是不建议的。
深拷贝就是重新开辟一块指针管理的空间,并把原来空间里的值复制过去。深拷贝后的两个对象没有任何牵连。
- 深拷贝是我们程序员来构建的,编译器自动构建的所有构造和赋值函数都是浅拷贝。
- 假入我们的类里面没有指针,那就不必自己构造深拷贝函数。而智能指针就是用一个对象去管理指针,因此建议在类里面用智能指针来替代原始指针。
4.1 STL是如何看待深拷贝的?
c++的标准库是一个非常值得学习的对象,我们拿vector模板类来举例:
#include <iostream>
#include <vector>
using namespace std;
int main(){
vector<int> m(2,1); // [1,1]
vector<int> n(m); // 调用拷贝构造函数(深拷贝)
n = m; // 调用拷贝赋值函数(深拷贝)
cout << "[" << n[0] << "," << n[1] << "]" << endl;
return 0;
}
通过调试,我们可以看到vector的拷贝构造函数如下:
vector(const vector& __x) // 左值引用的拷贝构造函数
: _Base(__x.size(),
_Alloc_traits::_S_select_on_copy(__x._M_get_Tp_allocator()))
{
this->_M_impl._M_finish =
std::__uninitialized_copy_a(__x.begin(), __x.end(), // 逐一拷贝原始vector的每个值
this->_M_impl._M_start,
_M_get_Tp_allocator());
}
- 在STL中几乎所有的类的拷贝构造函数都是这样的深拷贝。
拷贝赋值函数也是使用的深拷贝。
以下是vector的拷贝赋值函数(为了方便阅读,删除一些不必的代码):
注意:capacity指已经申请的空间大小,size指实际使用的空间大小,始终capacity>size。
operator=(vector& __x)
{
const size_type __xlen = __x.size();
// 这里是判断内存大小是否满足需求
if (__xlen > capacity()) // 原有的capacity较小
{
// 重新开辟一块内存并拷贝值。
pointer __tmp = _M_allocate_and_copy(__xlen, __x.begin(),
__x.end());
// 释放原来的空间
std::_Destroy(this->_M_impl._M_start, this->_M_impl._M_finish,
_M_get_Tp_allocator());
_M_deallocate(this->_M_impl._M_start,
this->_M_impl._M_end_of_storage
- this->_M_impl._M_start);
this->_M_impl._M_start = __tmp;
this->_M_impl._M_end_of_storage = this->_M_impl._M_start + __xlen;
}
else if (size() >= __xlen) // 原有的size较大,不需要重新申请空间
{
std::copy(__x.begin(), __x.end(), begin()); // 首先把值拷贝过来
std::_Destroy(,end(), _M_get_Tp_allocator()); // 清空原来size后面剩下的内容
}
else // 这种情况是size较小,capacity较大。
{
std::copy(__x._M_impl._M_start, __x._M_impl._M_start + size(),
this->_M_impl._M_start);
}
this->_M_impl._M_finish = this->_M_impl._M_start + __xlen;
return *this;
}
以上是vector的拷贝构造函数和拷贝赋值函数,我们已经知道他们都是采用深拷贝的方式。
那么移动构造函数和移动赋值函数呢?
我们在前面的章节已经知道移动构造函数和移动赋值函数传入的都是右值,右值也称为read value, 也就是说通常情况下他们都是一些临时值,或者不需要保存使用的值。
通俗话概括就是 移动构造函数和移动赋值函数传入的都是无关紧要,可以丢弃的值。
我们知道深拷贝是需要额外的时间和内存开销的(重新开辟空间,逐一复制)。而移动构造函数传入的右值又是临时值,很快就要被释放被丢弃,那么我们岂不是可以直接夺取右值的所有权?
重点,重点,重点。敲黑板。。。。。。。。
夺取传入的右值的所有权 这句话很重要,这也是网上很多人说的 s t d : : m o v e ( ) std::move() std::move()的语义是转移所有权。 实际上转移所有权的过程并不是 s t d : : m o v e ( ) std::move() std::move()实现的,而是在类的移动构造函数和移动赋值函数里面实现的,如果没有去做实现,那么 s t d : : m o v e ( ) std::move() std::move()是无法实现所有权的转移的。
那么怎么在移动构造函数中实现 传入右值的所有权转移呢?
很简单,就是浅拷贝的基础上,把传入的右值的指针指向一块空的地址。
哈哈,看看vector怎么做的就知道了:
#include <iostream>
#include <vector>
using namespace std;
int main(){
vector<int> m(2,1); // [1,1]
vector<int> n(std::move(m)); // 调用移动构造函数
cout << "n.size():" << n.size() << endl; // n被清空,所以size()=0
return 0;
}
再看看vector的移动构造函数的源码:
_Vector_impl_data(_Vector_impl_data&& __x) // 传入为右值引用的移动构造函数
: _M_start(__x._M_start), _M_finish(__x._M_finish), // 将新值的值针指向旧值的指针地址。
_M_end_of_storage(__x._M_end_of_storage)
// 将旧值的指针指向空。
{ __x._M_start = __x._M_finish = __x._M_end_of_storage = pointer(); }
移动赋值函数也是如此:
#include <iostream>
#include <vector>
using namespace std;
int main(){
vector<int> m(2,1); // [1,1]
vector<int> n;
n = std::move(m); // 调用移动赋值函数
cout << "n.size():" << n.size() << endl; // n被清空,所以size()=0
return 0;
}
4.2 我们应该怎样构造拷贝函数?
如c++标准库一样,在普通情况下,我们应该尽量保证我们自定义的类中:拷贝构造函数为深拷贝,移动构造函数要实现移动语义。
#include <memory>
#include <iostream>
using namespace std;
class A{
public:
A(){
m = 4;
p = new int(8);
cout << "调用构造函数" << endl;
}
A(A& a){
m = a.m;
p = new int; // 深拷贝。
*p = *a.p;
cout << "调用拷贝构造函数" << endl;
}
A(A&& a){ // A&& 为右值引用,也就是说拷贝函数传入的是一个右值。
p = a.p; // 夺取所有权。
a.p = nullptr; // 将右值的指针指向空的。
cout << "调用移动构造函数" << endl;
}
void print(){
cout << m << endl;
cout << *p << endl;
}
~A(){
delete p;
cout << "调用析构函数函数" << endl;
}
private:
int m;
int* p;
};
int main(){
A a;
A b(a); // 调用拷贝构造函数
a.print();
A c(std::move(a)); // 调用移动拷贝函数
// a.print(); // a已经被转移,不应该再被使用。
}
6. 父类与子类的函数调用顺序
如果一个类的子类和父类都有一个拷贝构造函数,那么他们的调用顺序是什么呢?
#include <memory>
#include <iostream>
using namespace std;
class A{
public:
A(){
m = 4;
p = new int(8);
cout << "调用A构造函数" << endl;
}
A(A& a){
m = a.m;
p = new int;
*p = *a.p;
cout << "调用A拷贝构造函数" << endl;
}
A(A&& a){ // A&& 为右值引用,也就是说拷贝函数传入的是一个右值。
p = a.p;
a.p = nullptr;
cout << "调用A移动构造函数" << endl;
}
void print(){
cout << m << endl;
cout << *p << endl;
}
void operator=(A&& a){ // A&& 为右值引用,也就是说赋值函数传入的是一个右值。
cout << "调用A移动赋值函数" << endl;
}
virtual ~A(){
delete p;
cout << "调用A析构函数函数" << endl;
}
public:
int m;
int* p;
};
class B: public A{
public:
B(){
m = 5;
*p = 9;
cout << "调用B构造函数" << endl;
}
B(B& b){
m = b.m;
*p = *b.p;
cout << "调用B拷贝构造函数" << endl;
}
B(B&& b){ // A&& 为右值引用,也就是说拷贝函数传入的是一个右值。
p = b.p;
b.p = nullptr;
cout << "调用B移动构造函数" << endl;
}
void print(){
cout << m << endl;
cout << *p << endl;
}
void operator=(B&& b){ // A&& 为右值引用,也就是说赋值函数传入的是一个右值。
cout << "调用B移动赋值函数" << endl;
}
~B(){
cout << "调用B析构函数函数" << endl;
}
};
int main(){
B b;
}
上面的输出结果是:
调用A构造函数
调用B构造函数
调用B析构函数函数
调用A析构函数函数
因此子类与父类的构造函数和析构函数是: 父子(构造)子父(析构)。
但是当我们子类调用拷贝构造时:
int main(){
B b;
B c(b);
}
输出结果为:
调用A构造函数
调用B构造函数
调用A构造函数
调用B拷贝构造函数
调用B析构函数函数
调用A析构函数函数
调用B析构函数函数
调用A析构函数函数
可以看到在构造对象c时,父类仍然调用的是构造函数,并没有调用拷贝构造。
要实现子类调用拷贝构造,父类也同时调用拷贝构造,则需要在子类中显示声明:
B(B& b):A(b){ // 显示声明调用父类拷贝构造函数
m = b.m;
p = new int;
*p = *b.p;
cout << "调用B拷贝构造函数" << endl;
}
同样的移动构造函数也是如此:
B(B&& b):A(std::move(b)){ // 显示调用声明
p = b.p;
b.p = nullptr;
cout << "调用B移动构造函数" << endl;
}