拷贝构造函数和拷贝赋值运算符
拷贝构造函数定义如下:
class User { string username; string phonenumbers[10]; public: User(){ cout << "direct constructor !!! " << endl; }; User(string username) : username(username) { cout << "direct constructor (params: username ) !!!" << endl; }; User(const User& user):username(user.username) { int index = 0; for_each(begin(phonenumbers),end(phonenumbers),[&](string &p) mutable { p = user.phonenumbers[index++]; }); cout << "copy constructor !!! " << endl; } };
其中User(constUser& user):username(user.username) 是拷贝构造函数(copy constructor)的定义。这里需要注意的是copyconstructor 的参数一定是const 引用,原因是因为如果是普通的类型 进入函数会拷贝相关的类,而且相关的类的拷贝构造函数正是你调用的这个函数,会出现循环拷贝现象。同时需要注意的是:由于数组是无法拷贝的,所以我们应该将元素逐一抽出然后相应拷贝。
下面是关于什么情况会调用拷贝初始化
User userA = string("TONY"); //某些编译器是属于拷贝初始化,某些是跳过拷贝初始化,然后直接初始化。 User userB = userA; //拷贝初始化 User userC("YAN"); //直接初始化 User userD(userC); //拷贝初始化
合成拷贝构造函数一般情况下,如果我们不去定义拷贝构造函数,系统会帮我们合成定义一个合成拷贝构造函数,合成拷贝构造函数(synthesizedcopy constructor)会将我们的非static 成员逐一拷贝,如果是数组也会将数组中的元素逐一拷贝(由于数组是不能拷贝的)
拷贝赋值运算符定义如下:
class User { string username; string phonenumbers[10]; public:
.....中间省略1万字..... User& operator=(const User& user){ int index = 0; for_each(begin(phonenumbers),end(phonenumbers),[&](string &p) mutable { p = user.phonenumbers[index++]; }); this->username = user.username; cout << "copy-assignment operator !!! " << endl; }; };
其中User&operator=(const User& user) 的定义就是拷贝赋值运算符(copy_assignment operator)的定义,函数体基本上和拷贝构造函数类似。
下面是什么情况会调用拷贝赋值运算符:
User userA = string("TONY"); //某些编译器是属于拷贝初始化,某些是跳过拷贝初始化,然后直接初始化。 User userB = userA; //拷贝初始化 userA = userB; //使用拷贝赋值运算符
合成拷贝赋值运算符(synthesizedcopy_assignment operator),一般情况下如我们没有定义相关的拷贝赋值运算符,会生成一个合成拷贝赋值运算符,而且会将我们的非static成员逐一拷贝,如果是数组也会将数组中的元素逐一拷贝(由于数组是不能拷贝的)
当然有些情况我们不希望合成一个拷贝构造函数或者运算符等,在新的标准下我们可以使用delete关键字定义一个函数为删除的,在旧的标准之下我们可以使用声明一个private的函数而不去定义,实现类似于delete的功能。
class MyObject { public : string text = "XXXXXX"; MyObject() = default; MyObject(const MyObject &) = delete; MyObject &operator=(const MyObject &) = delete; ~MyObject() = delete; };
由于一些类对象不希望使用拷贝的机制,例如IO流。
所以旧标准下我们可以通过声明一个private的拷贝构造函数去实现类似于delete的效果,但是由于友元和成员函数依然可以调用,所以我们也只做声明而不去定义:
class MyObject { MyObject(const MyObject &); //声明而不定义 MyObject &operator=(const MyObject &); ~MyObject(); public : string text = "XXXXXX"; MyObject() = default; };
当成员函数和友元尝试调用的时候,会报错函数未定义,从而调用失败。
析构函数定义如下:
class User { string username; string phonenumbers[10]; public: ~User(){ cout << "destructor !!!" << endl; }; };
合成成员函数可能会被定义删除的可能有以下几种情况:
1、类中成员析构函数定义为删除或者是不可访问的(private)--- 合成析构函数定义为删除;
2、类中成员拷贝构造函数定义为删除的(或不可访问)、或者成员析构函数定义为删除(或不可访问)----合成拷贝构造函数定义为删除;
3、类中成员拷贝赋值运算符定义为删除(或不可访问)、或者类中有const或者引用成员---合成拷贝赋值运算符为删除;
4、类中成员析构函数定义为删除(或不可访问)、或者类中有引用或者const成员,且没有类内初始器----- 合成默认构造函数为删除;
在旧标准下定义为private从而禁止拷贝构造函数以及拷贝赋值运算符:
class TestObject { TestObject(const TestObject &); TestObject &operator=(const TestObject &); public: string text = ""; //类内初始化 TestObject() = default; };
需要注意的是:private定义在友元和成员函数当中仍然是可以使用的,所以我们只是进行声明而不定义,当友元和成员函数调用时会出错。
下面展示两个例子,一个是类似shared_ptr的类,一个是类似unique_ptr的类,其中类似unique_ptr的支持拷贝但是会进行值拷贝而非指针拷贝。由于还没有学习模板技术,所以指针中的数据是定义为string
MyValue_ptr:拷贝值互相独立,使用是堆内存所以在赋值过程会先释放之前的内存,然后重新获得新内存,而且还有一个数据修改以及访问计数器。
class MyValue_ptr { private: string *value; //数据值使用动态内存 int counter; //记录读取和修改次数 public: MyValue_ptr(const string &value) : value(new string(value)), counter(0) {}; //拷贝构造函数 MyValue_ptr(const MyValue_ptr &myValue_ptr) : value(new string(*myValue_ptr.value)), counter(0) {}; //拷贝赋值运算符 MyValue_ptr&operator=(const MyValue_ptr& myValue_ptr){ string *new_value = new string(*myValue_ptr.value); if(this->value){ delete this->value; } this->value = new_value; this->counter = myValue_ptr.counter; return *this; } void swap(MyValue_ptr &ptrA,MyValue_ptr &ptrB){ using std::swap; swap(ptrA.value,ptrB.value); swap(ptrA.counter,ptrB.counter); } //重载赋值运算符 MyValue_ptr&operator=(const string& value){ if(this->value){ delete this->value; } this->value = new string(value); this->counter++; return *this; } //获得字符串 string& getValue(){ this->counter++; return *this->value; } //析构函数 ~MyValue_ptr(){ delete this->value; } };
MyShared_ptr:自建引用计数器和堆内存指针,在析构函数中需要检查其引用的数量,如果引用数量为零则释放内存。
class MyShared_ptr { string *value; int *reference_count; public: //构造函数,添加引用计数器并设置为1 MyShared_ptr(const string &value) : value(new string(value)),
reference_count(new int(1)) {}; //拷贝构造函数,并在引用计算器+1 MyShared_ptr(const MyShared_ptr &myShared_ptr) : value(myShared_ptr.value),
reference_count(myShared_ptr.reference_count) { (*this->reference_count)++; } //拷贝赋值运算符 MyShared_ptr &operator=(const MyShared_ptr &myShared_ptr) { auto new_value = myShared_ptr.value; auto new_reference_count = myShared_ptr.reference_count; (*new_reference_count)++; (*this->reference_count)--; if (this->reference_count != nullptr && *this->reference_count <= 0) { cout << "delete value " << *this->value << endl; delete this->value; delete this->reference_count; } this->reference_count = new_reference_count; this->value = new_value; return *this; } //赋值string只改变内容,不改变指针 MyShared_ptr &operator=(const string &value) { *this->value = value; return *this; } //获得数据等同于解引用 string &getValue() { return *this->value; } //对象析构时引用计数器-1 如果没有其他引用后销毁堆内存 ~MyShared_ptr() { (*reference_count)--; if (*reference_count <= 0) { cout << "delete value " << *this->value << endl; delete reference_count; delete value; } } };
有时我们可以利用swap进行对象转换,下面是关于MyValue_ptr类赋值运算符使用swap的写法:
MyValue_ptr&operator=(MyValue_ptr myValue_ptr){ swap(*this,myValue_ptr); return *this; } void swap(MyValue_ptr &ptrA,MyValue_ptr &ptrB){ using std::swap; swap(ptrA.value,ptrB.value); swap(ptrA.counter,ptrB.counter); }
提示:因为在传参过程中调用了拷贝构造函数,然后创建了一个函数中的myValue_ptr的局部变量,然后通过交换函数,将数据进行交换。然后myValue_ptr带着旧的数据在离开作用域后自动销毁并调用了myValue_ptr的析构函数。
根据上述的提示,MyShared_ptr也可以改成这样:
MyShared_ptr &operator=(MyShared_ptr myShared_ptr) { swap(*this, myShared_ptr); return *this; } void swap(MyShared_ptr &ptrA, MyShared_ptr &ptrB) { using std::swap; swap(ptrA.value, ptrB.value); swap(ptrA.reference_count, ptrB.reference_count); }
代码比MyShared_ptr&operator=(const MyShared_ptr& myShared_ptr) 简单了不少。
对象的移动
对象移动是新标准的一新特性,通过使用对象移动减少对象拷贝这种性能代价。但是需要注意,对象被移动后,需要确保不再使用原对象。
这里涉及到右值引用的概念:
右值引用是绑定到右值对象的引用使用&&来获得右值引用,右值引用最重要的特性--只能绑定在一个将要销毁的对象。因此我们可以使用右值引用的资源移动到一个对象当中。
关于右值和左值:这个概念原话是:一般而言,一个左值表达式的是一个对象的身份,而一个右值表达式表示的是对象的值。
一般读完都会有点懵逼,其实可以这样理解,左值是持久的,右值是短暂的。这样之间的区别就更加明显了,左值有持久状态,例如一个变量。而右值是字面量,或者是表达式求值过程中创建的临时对象。
下面几个就属于左值对象:
1、函数返回的临时对象
2、通过表达式计算后得出的值
3、字面量
在我们之前使用的所有左值对象引用只能通过const&的引用方式进行引用。以下是关于右值引用的例子:
int i = 42; int &r = i; //正确:左值引用,引用了一个左值对象 int &&rr = i; //错误:无法将一个左值对象 引用绑定到右值引用 int &r2 = i * 10; //错误:无法使用左值引用,绑定一个右值对象 const int &r3 = i * 12; //正确:可以将一个右值对象绑定到const左值引用当中 int &&rr2 = i * 13 // 正确:可以将一个右值对象绑定到右值引用当中。
int &&rr3 = i// 错误:变量是左值的。
右值引用只能绑定到临时对象当中,所以产生一下特点:
1、所引用的对象将要销毁
2、该对象没有其他用户
需要注意:通常我们使用const&左值引用绑定右值对象的时候,我们是不能对其进行修改的,但是如果我们使用&&右值引用是可以对其绑定进行修改的。
很多情况下我们需要将一个变量显性转换为右值:
int i=10; int &&rr = std::move(i); // 注意:这里使用的是std::move
需要注意的是:使用move函数之后我们只能销毁i或者对i这边变量从新赋值之外,我们不能做任何假设性操作。
对于类似string这种类型的拷贝是基本上产生了很多无谓的开销,string和其他内置类型都支持移动构造函数和移动赋值运算符:
string text = "TONY"; string new_text = std::move(text);
可以做一个简单的测试,理解移动特性
text.push_back('A'); cout << text << endl; //输出A 这里需要注意的是,一般情况下移动之后的原对象除了重新赋值和销毁就不做其他操作了 cout << new_text << endl; //输出TONY
个人觉得是在底层移动了指针,从而达到移动的特性。其实就是复制其指针,由于在看C++11 primer 的例子中也是复制其指针的,以下我们使用MyShared_ptr定义一个移动构造函数:
//移动构造函数 MyShared_ptr(MyShared_ptr &&myShared_ptr) : value(myShared_ptr.value), reference_count(myShared_ptr.reference_count) { myShared_ptr.value = nullptr; myShared_ptr.reference_count = nullptr; }
为了达到正常析构的目的,所以需要将析构函数改成:
//对象析构时引用计数器-1 如果没有其他引用后销毁堆内存 ~MyShared_ptr() { if (reference_count) { //判断计数器指针是否有效 (*reference_count)--; if (*reference_count <= 0) { cout << "delete value " << *this->value << endl; delete reference_count; delete value; } } }
当然如果是非指针类型的,就需要直接调用std::move调用其成员的移动构造函数。
下面是之前写的MyMessage和Folder的实验,添加其移动构造函数:
MyMessage::MyMessage(MyMessage &&message) : content(std::move(message.content)){ //移动string message.remove_self_in_folders(); this->folders = std::move(message.folders); //移动set this->save_self_in_folders(); message.folders.clear(); //确保其message对象能够正常析构 }
[注:最后会贴出完成代码实现]
然而还有就是移动赋值运算符:
MyMessage& MyMessage::operator=(MyMessage &&message) { message.remove_self_in_folders(); this->remove_self_in_folders(); this->folders = std::move(message.folders); message.folders.clear(); this->save_self_in_folders();
return *this; }
当然一般情况下如我们不定义移动构造函数和移动赋值运算符都会有一个合成版本的,合成版本的会逐一移动成员,但是以下几点就导致合成定位为删除的:
1、类成员有自定义的拷贝构造函数或者类成员无法合成移动构造函数,将定义为删除
2、类成员的移动构造函数或者移动赋值运算符无法访问 --- 移动构造函数和移动赋值运算符都会定义为删除
3、如果类的析构函数被定义为删除 --- 移动构造函数和移动赋值运算符都会定义为删除
4、如果类成员有const 或者 引用 --- 移动构造函数和移动赋值运算符都会定义为删除
关于移动和拷贝的重载匹配
如果是右值对象有限匹配为移动,如果是左值对象匹配为const&。其实右值对象也可以匹配为const& 的拷贝函数,但是需要进行转型为cosnt,所以&&的移动函数匹配更加精准。
但是在实际测试上,在移动构造函数需要显示使用std::move才会成功调用,否则就是算右值对象也一样调用拷贝构造函数:
MyMessage messageA("Message_A"); MyMessage messageB = messageA; //使用拷贝构造函数 MyMessage &&rrMessage = getMyMessage(); //函数返回对象属于右值对象 MyMessage messageC(getMyMessage()) ; //使用移动构造函数 MyMessage messageD(std::move(getMyMessage())); //使用移动构造函数 messageA = messageB; //传入左值使用拷贝运算符 messageC = getMyMessage(); //赋值运算符,根据传入的是右值 所以使用移动赋值运算符
移动迭代器:
一般的迭代器解引用后都是右值的对象,当然我们可以显性使用std::move函数对对象进行转换,但是可以使用移动迭代解引用返回对象是右值,从而可以移动迭代器的对象。
下面是关于一个自定义的vector实验,将其中的扩容函数,修改成移动迭代器,从而使用移动的方式去将字符元素从旧的空间移动到新的空间当中,而不是去拷贝元素:
void StrVector::checkCap(int desired) { if (first_free + desired <= cap) { return; } auto cap_size = (first_free + desired) - elements; auto new_cap_size = cap_size * 2; string *new_memory_start = str_allocator.allocate(new_cap_size); string *new_first_free = uninitialized_copy(make_move_iterator(elements),
make_move_iterator(first_free),new_memory_start); this->free(); first_free = new_first_free; elements = new_memory_start; cap = elements + new_cap_size; }
以下是等价方式:
void StrVector::checkCap(int desired) { if (first_free + desired <= cap) { return; } auto cap_size = (first_free + desired) - elements; auto new_cap_size = cap_size * 2; string *new_memory_start = str_allocator.allocate(new_cap_size); auto new_memory_iterator = new_memory_start; for (auto item = elements; item != first_free; item++) { str_allocator.construct(new_memory_iterator++, std::move(*item)); } this->free(); first_free = new_memory_iterator; elements = new_memory_start; cap = elements + new_cap_size; }
关于类的右值成员函数
一般情况下我们没有区分调用函数的是右值还是左值,所以以下表达式是成立的:
string a = "tony"; string b = "yan"; auto n = (a + b).find('a'); //使用右值调用成员函数 a + b = "hahaha"; //为一个右值对象赋值
对于上述傻B行为可以得出一个结论,上面都是然并卵的。所以有时我们需要对这些行为加以阻止,或者对右值的调用只是做一些有意义的操作。
class RightValueTest { string value; public: RightValueTest(string value) : value(value) {}; //调用的对象是一个非const的左值对象 RightValueTest &operator=(const RightValueTest &value) & { this->value = value.value; return *this; } };
在函数后面加上&符号代表接受左值对象调用,如果是右值调用会出错:
int main() { RightValueTest testC("TEST_C"); getRightValueTest() = testC; }
错误如下:
C:\Users\TONY\ClionProjects\startLearing\main.cpp:15:25:error: passing 'RightValueTest' as 'this' argument discards qualifiers[-fpermissive]
getRightValueTest() = testC;
同样来说右值函数也可以和左值重载,但是需要注意的是不能两个都非const其中一个函数必须是const否则无法重载,以下是重载的方式:
class RightValueTest { string value; public: RightValueTest(string value) : value(value) {}; char getValueChar(int index) const &{ cout << "left value" << endl; return *(this->value.begin() + index); } char getValueChar(int index) &&{ cout << "right value" << endl; char tmp = *(this->value.begin() + index); return std::move(tmp); } };
和constthis的重载一样,考究的是调用的对象是一个const左值 还是一个 右值对象,下面是调用的结果:
RightValueTest getRightValueTest() { RightValueTest testA("TEST_A"); return testA; } int main() { const RightValueTest testB("TEST_C"); char resultA = getRightValueTest().getValueChar(5); //使用右值版本 char resultB = testB.getValueChar(5); //使用const左值版本 }
自定义StrVector代码如下:
StrVector.h
#ifndef UNTITLED_STRVECTOR_H #define UNTITLED_STRVECTOR_H #include <string> #include <initializer_list> using namespace std; class StrVector { string *elements; string *first_free; string *cap; allocator<string> str_allocator; void checkCap(int desired); void free(); public: StrVector(); StrVector(initializer_list<string> initializerList); StrVector(const StrVector& strVector); StrVector& operator=(const StrVector& strVector); string* push_back(string value); string* push_back(initializer_list<string> initializerList); int size(); string* begin(); string* end(); string* begin() const; string* end() const; ~StrVector(); void clear(); void show(); }; #endif //UNTITLED_STRVECTOR_H
StrVecto