深入理解c++特殊成員函數(shù)
在c++中,特殊成員函數(shù)有下面6個(gè):
- 構(gòu)造函數(shù)
- 析構(gòu)函數(shù)
- 復(fù)制構(gòu)造函數(shù)(拷貝構(gòu)造函數(shù))
- 賦值運(yùn)算符(拷貝運(yùn)算符)
- 移動(dòng)構(gòu)造函數(shù)(c++11引入)
- 移動(dòng)運(yùn)算符(c++11引入)
以Widget類為例,其特殊成員函數(shù)的簽名如下所示:
class Widget{
public:
Widget();//構(gòu)造函數(shù)
~Widget();//析構(gòu)函數(shù)
Widget(const Widget& rhs);//復(fù)制構(gòu)造函數(shù)(拷貝構(gòu)造函數(shù))
Widget& operator=(const Widget& rhs);//賦值運(yùn)算符(拷貝運(yùn)算符)
Widget(Widget&& rhs);//移動(dòng)構(gòu)造函數(shù)
Widget& operator=(Widget&& rhs);//移動(dòng)運(yùn)算符
}
每個(gè)方法都有哪些作用,又都有哪些注意點(diǎn)?
本文將針對(duì)這些方法,進(jìn)行詳細(xì)的講解。
構(gòu)造函數(shù)
構(gòu)造函數(shù)的作用是幫助創(chuàng)建對(duì)象的實(shí)例,并對(duì)實(shí)例進(jìn)行初始化。
在c++中,下面兩種形式的語句將會(huì)調(diào)用類的構(gòu)造函數(shù):
Widget widget;
Widget *w = new Widget();
調(diào)用構(gòu)造函數(shù)將會(huì)創(chuàng)建一個(gè)類的實(shí)例對(duì)象。當(dāng)一個(gè)類擁有數(shù)據(jù)成員時(shí),就需要為該類編寫構(gòu)造函數(shù),在構(gòu)造函數(shù)中對(duì)數(shù)據(jù)成員進(jìn)行初始化。
對(duì)于c++98,如果一個(gè)類沒有set方法,那么就需要為其創(chuàng)建含參數(shù)的構(gòu)造函數(shù),如下所示:
#include <iostream>
class Widget
{
public:
Widget(int width, int height):height_(height), width_(width){}
private:
int height_;
int width_;
};
int main()
{
Widget w(1,1);
return 0;
}
倘若此時(shí)不為其創(chuàng)建含參數(shù)的構(gòu)造函數(shù),那么此時(shí)創(chuàng)建的對(duì)象中的成員的值是隨機(jī)的,顯而易見,這樣的創(chuàng)建出的對(duì)象是不好的。
#include<iostream>
using namespace std;
class Widget
{
public:
int getHeight() const{
return height_;
}
int getWidth() const{
return width_;
}
private:
int height_;
int width_;
};
int main()
{
Widget w;
std::cout << w.getHeight()<<std::endl;
std::cout << w.getWidth()<<std::endl;
return 0;
}
但是對(duì)于c++11之后的標(biāo)準(zhǔn),成員的初始值可以在類中定義。
在這種場(chǎng)景下,所有該類創(chuàng)建出的對(duì)象將擁有相同的初始值。如果你希望創(chuàng)建出的對(duì)象的初始值可以是不相同的,那么還是需要添加含參數(shù)的構(gòu)造函數(shù)。
#include<iostream>
using namespace std;
class Widget
{
public:
int getHeight() const{
return height_;
}
int getWidth() const{
return width_;
}
private:
int height_{1};
int width_{1};
};
int main()
{
Widget w;
std::cout << w.getHeight()<<std::endl;
std::cout << w.getWidth()<<std::endl;
return 0;
}
析構(gòu)函數(shù)
構(gòu)造函數(shù)的作用是幫助銷毀一個(gè)實(shí)例。
這很好理解,但是何時(shí)需要自定義析構(gòu)函數(shù)呢?
首先看下面這個(gè)類,這個(gè)類需要寫自定義析構(gòu)函數(shù)嗎?
class Student{
public:
Student(std::string name , int age, int id):name_(name), age_(age), id(id_){};
//需要析構(gòu)函數(shù)嗎?
public:
std::string getName() const{
return name_;
}
int getAge() const{
return age_;
}
int getId() const{
return id_;
}
private:
std::string name_;
int age_;
int id_;
}
答案是否定的,這個(gè)Student類只包含了三個(gè)成員,默認(rèn)的析構(gòu)函數(shù)會(huì)清理掉這些數(shù)據(jù),因此不需要自定義析構(gòu)函數(shù)。
再看看下面這個(gè)例子,需要為其自定義析構(gòu)函數(shù)嗎?
class Student{
public:
Student(const char* s , std::size_t n) :name_(new char[n]);{
memcpy(name_, s, n);
}
//需要析構(gòu)函數(shù)嗎?
public:
char* getName() const{
return name_;
}
private:
char* name_{nullptr};
}
很顯然,該類需要自定義析構(gòu)函數(shù)。默認(rèn)的析構(gòu)函數(shù)只會(huì)將name_置為nullptr,而不會(huì)釋放new所創(chuàng)建的內(nèi)存空間。
因此上面的例子需要改造為下面這樣的形式:
class Student{
public:
Student(const char* s , std::size_t n) :name_(new char[n]);{
memcpy(name_, s, n);
}
~Student(){
if(name_){
delete[] name_;
}
}
//需要析構(gòu)函數(shù)嗎?
public:
char* getName() const{
return name_;
}
private:
char* name_{nullptr};
}
其實(shí)這個(gè)類到目前為止還是有問題的,在下文中提到拷貝操作時(shí)會(huì)解釋為什么。
再看看下面這個(gè)例子,需要為其自定義析構(gòu)函數(shù)嗎?
class AsyncExec{
public:
void exec(std::function<void()>& func){
threadPtr_ = new std::thread(func);
}
//需要析構(gòu)函數(shù)嗎?
private:
std::thread* threadPtr_{nullptr};
}
很顯然,該類也需要自定義析構(gòu)函數(shù)。AsyncExec類的實(shí)例在調(diào)用完Exec方法后,其內(nèi)部包含了一個(gè)指針,并且其成員是std::thread
類型的指針,如果其沒有被detach,那么就必須要進(jìn)行join,否則將會(huì)terminate程序。
因此上面的例子需要改造為下面這樣的形式:
class AsyncExec{
public
~AsyncExec(){
if(threadPtr){
threadPtr->join;
}
delete threadPtr;
}
public:
void exec(std::function<void()>& func){
threadPtr_ = new std::thread(func);
}
//需要析構(gòu)函數(shù)嗎?
private:
std::thread* threadPtr_{nullptr};
}
通過上面兩個(gè)例子,也基本可以發(fā)現(xiàn)這樣的規(guī)律:
通常一個(gè)類需要管理一些資源時(shí)(原始指針,線程,文件描述符等),通常需要為其編寫自定義的析構(gòu)函數(shù),因?yàn)榇藭r(shí)的默認(rèn)的析構(gòu)函數(shù)的行為是不正確的。
接下來需要了解一個(gè)著名的rule of three定理,如果一個(gè)類需要用戶自定義的析構(gòu)函數(shù)、用戶定義的復(fù)制構(gòu)造函數(shù)或用戶定義的賦值運(yùn)算符三者中的一個(gè),那么它幾乎肯定需要這三個(gè)函數(shù)。
例如下面的例子
#include <cstdint>
#include <cstring>
class Student{
public:
Student(const char* s , std::size_t n) :name_(new char[n]){
memcpy(name_, s, n);
}
explicit Student(const char* s = "")
: Student(s, std::strlen(s) + 1) {}
~Student(){
if(name_){
delete[] name_;
}
}
public:
char* getName() const{
return name_;
}
private:
char* name_;
};
int main()
{
Student s1("shirley");
Student s2("tom");
Student s3(s1);//(1)
s2 = s1;//(2)
}
如果使用默認(rèn)的復(fù)制構(gòu)造函數(shù),將會(huì)出現(xiàn)double free的錯(cuò)誤。因?yàn)槟J(rèn)的復(fù)制構(gòu)造函數(shù)是值拷貝,此時(shí)s1和s3的name_成員指向同一處內(nèi)存,s1和s3析構(gòu)時(shí)將重復(fù)析構(gòu)。
如果使用默認(rèn)的賦值運(yùn)算符,不僅會(huì)有double free的問題,還會(huì)有一處內(nèi)存泄漏。由于s2被賦值為了s1,因此s2原來的name_指向的內(nèi)存將不再有指針指向,于是產(chǎn)生了內(nèi)存泄漏。接下來,同理s1和s2的name_成員指向同一處內(nèi)存,s1和s2析構(gòu)時(shí)將重復(fù)析構(gòu)。
正確的寫法就是在添加自定義析構(gòu)函數(shù)的同時(shí),為其添加自定義的賦值構(gòu)造函數(shù)和自定義的賦值運(yùn)算符。
#include <cstdint>
#include <cstring>
class Student{
public:
Student(const char* s , std::size_t n) :name_(new char[n]){
memcpy(name_, s, n);
}
explicit Student(const char* s = "")
: Student(s, std::strlen(s) + 1) {}
~Student(){
if(name_){
delete[] name_;
}
}
Student(const Student& other) // II. copy constructor
: Student(other.name_) {}
Student& operator=(const Student& other) // III. copy assignment
{
if (this == &other)
return *this;
std::size_t n{std::strlen(other.name_) + 1};
char* new_cstring = new char[n]; // allocate
std::memcpy(new_cstring, other.name_, n); // populate
delete[] name_; // deallocate
name_ = new_cstring;
return *this;
}
public:
char* getName() const{
return name_;
}
private:
char* name_;
};
int main()
{
Student s1("shirley");
Student s2("tom");
Student s3(s1);
s2 = s1;
}
賦值運(yùn)算符中的這段代碼的寫法,在effective c++中有提到,這樣做是為了保證異常安全性,這樣的寫法可以確保new的失敗的情況下,不會(huì)對(duì)原有對(duì)象的數(shù)據(jù)進(jìn)行破壞。
std::size_t n{std::strlen(other.name_) + 1};
char* new_cstring = new char[n]; // allocate
std::memcpy(new_cstring, other.name_, n); // populate
delete[] name_; // deallocate
name_ = new_cstring;
復(fù)制構(gòu)造函數(shù)和賦值運(yùn)算符
復(fù)制構(gòu)造函數(shù)的作用是使用一個(gè)已經(jīng)存在的對(duì)象去創(chuàng)建一個(gè)新的對(duì)象。
賦值運(yùn)算符的作用是將一個(gè)對(duì)象的所有成員變量賦值給另一個(gè)已經(jīng)創(chuàng)建的對(duì)象。
二者的區(qū)別在于一個(gè)是創(chuàng)建一個(gè)新對(duì)象,一個(gè)是賦值給一個(gè)已經(jīng)存在的對(duì)象。
在下面的例子中,語法(1)就是調(diào)用復(fù)制構(gòu)造函數(shù), 語法(2)就是調(diào)用賦值運(yùn)算符。
{
Student s1("shirley");
Student s2("tom");
Student s3(s1);//(1)復(fù)制構(gòu)造函數(shù)
s2 = s1;//(2)賦值運(yùn)算符
}
下面我們回顧下面提到的Student類,看下正確的復(fù)制構(gòu)造函數(shù)和賦值運(yùn)算符的編寫需要注意什么。
復(fù)制構(gòu)造函數(shù)的功能相對(duì)簡(jiǎn)單,主要是成員的復(fù)制,如果存在類管理的指針,則需要進(jìn)行深拷貝。
Student(const Student& other) // II. copy constructor
: Student(other.name_) {}
賦值運(yùn)算符的編寫的注意點(diǎn)相對(duì)較多。
- 首先要添加自我賦值判斷。
- 其次由于賦值運(yùn)算符是對(duì)一個(gè)已經(jīng)存在的對(duì)象再次賦值,因此首先需要銷毀原有對(duì)象的成員。
- 接著需要處理成員對(duì)象的賦值,如果存在類管理的指針,則需要進(jìn)行深拷貝。
- 最后需要將
*this
進(jìn)行返回,以便進(jìn)行連續(xù)賦值。
Student& operator=(const Student& other) // III. copy assignment
{
if (this == &other)
return *this;
std::size_t n{std::strlen(other.name_) + 1};
char* new_cstring = new char[n]; // allocate
std::memcpy(new_cstring, other.name_, n); // populate
delete[] name_; // deallocate
name_ = new_cstring;
return *this;
}
當(dāng)你沒有提供自定義的復(fù)制構(gòu)造函數(shù)和賦值運(yùn)算符時(shí),編譯器將創(chuàng)建默認(rèn)的復(fù)制構(gòu)造函數(shù)和賦值運(yùn)算符,其將對(duì)成員進(jìn)行淺拷貝。
如果你的類沒有管理資源,那么淺拷貝可能是合適的。如果你的類是管理某些資源的(原始指針,線程對(duì)象,文件描述符等),那么大概率默認(rèn)的復(fù)制構(gòu)造函數(shù)和賦值運(yùn)算符是不合適的。
但是要注意有時(shí)候,類成員雖然包含原始指針,但是并不代表該原始指針由該類管理。
例如下面的例子中,Client類中擁有handler_指針,但是該指針的生命周期并不由該類管理,該類僅僅是使用該指針,因此在這種場(chǎng)景下,淺拷貝就沒有問題,默認(rèn)的復(fù)制構(gòu)造函數(shù)和賦值運(yùn)算符就可以滿足要求。
#include <memory>
#include <functional>
#include <iostream>
#include <thread>
#include <future>
class IHandler
{
public:
IHandler() = default;
virtual ~IHandler() = default;
public:
virtual void connect() = 0;
};
class TcpHandler :public IHandler
{
public:
TcpHandler() = default;
virtual ~TcpHandler() = default;
public:
void connect(){
std::cout << "tcp connect" << std::endl;
}
};
class UdpHandler : public IHandler
{
public:
UdpHandler() = default;
virtual ~UdpHandler() = default;
public:
void connect() {
std::cout << "udp connect" << std::endl;
}
};
class Client{
public:
Client(IHandler* handler):handler_(handler){};
~Client() = default;
public:
void connect(){
handler_->connect();
}
private:
IHandler* handler_{nullptr};
};
void process(IHandler* handler)
{
if(!handler) return;
Client client(handler);
client.connect();
}
int main()
{
IHandler* handler = new TcpHandler();
process(handler);
delete handler;
handler = nullptr;
handler = new UdpHandler();
process(handler);
delete handler;
handler = nullptr;
}
因此,在設(shè)計(jì)類的時(shí)候,需要注意類是否是管理資源還是僅僅是使用資源。如果是管理資源,那么大概率你需要自定義復(fù)制構(gòu)造函數(shù)和賦值運(yùn)算符。
這里再次會(huì)提到rule of three定理,通常情況下,如果你需要自定義析構(gòu)函數(shù)的時(shí)候,大概率你就需要自定義復(fù)制構(gòu)造函數(shù)和賦值運(yùn)算符。
牢記這個(gè)點(diǎn),當(dāng)你在設(shè)計(jì)一個(gè)類時(shí)需要有這樣的條件反射。
其實(shí)如果當(dāng)你自定義了析構(gòu)函數(shù)之后,默認(rèn)的復(fù)制構(gòu)造函數(shù)和賦值運(yùn)算符就可以被delete,但是在c++98年代,這個(gè)點(diǎn)還沒有被重視。到了c++11年代,因?yàn)榭紤]到舊代碼的遷移困難,這個(gè)點(diǎn)還是沒有繼續(xù)支持。編譯器選擇對(duì)新支持的移動(dòng)構(gòu)造函數(shù)和移動(dòng)運(yùn)算符支持這個(gè)點(diǎn)上的考慮,即如果定義了析構(gòu)函數(shù),則默認(rèn)的移動(dòng)構(gòu)造函數(shù)和移動(dòng)運(yùn)算符將會(huì)delete,這個(gè)點(diǎn)在下面還會(huì)繼續(xù)講解。
移動(dòng)構(gòu)造函數(shù)和移動(dòng)運(yùn)算符
移動(dòng)語義在c++11之后大面積使用,它允許將一個(gè)對(duì)象的所有權(quán)從一個(gè)對(duì)象轉(zhuǎn)移到另一個(gè)對(duì)象,而不需要進(jìn)行數(shù)據(jù)的拷貝。 這種轉(zhuǎn)移可以在對(duì)象生命周期的任意時(shí)刻進(jìn)行,可以說是一種輕量級(jí)的復(fù)制操作。
而移動(dòng)構(gòu)造函數(shù)和移動(dòng)運(yùn)算符就是在類中支持移動(dòng)語義的二個(gè)方法。
關(guān)于如何書寫移動(dòng)構(gòu)造函數(shù)和移動(dòng)運(yùn)算符,這里參考微軟的文檔進(jìn)行理解。
移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值運(yùn)算符
下面的例子是用于管理內(nèi)存緩沖區(qū)的 C++ 類 MemoryBlock。
// MemoryBlock.h
#pragma once
#include <iostream>
#include <algorithm>
class MemoryBlock
{
public:
// Simple constructor that initializes the resource.
explicit MemoryBlock(size_t length)
: _length(length)
, _data(new int[length])
{
std::cout << "In MemoryBlock(size_t). length = "
<< _length << "." << std::endl;
}
// Destructor.
~MemoryBlock()
{
std::cout << "In ~MemoryBlock(). length = "
<< _length << ".";
if (_data != nullptr)
{
std::cout << " Deleting resource.";
// Delete the resource.
delete[] _data;
}
std::cout << std::endl;
}
// Copy constructor.
MemoryBlock(const MemoryBlock& other)
: _length(other._length)
, _data(new int[other._length])
{
std::cout << "In MemoryBlock(const MemoryBlock&). length = "
<< other._length << ". Copying resource." << std::endl;
std::copy(other._data, other._data + _length, _data);
}
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
std::cout << "In operator=(const MemoryBlock&). length = "
<< other._length << ". Copying resource." << std::endl;
if (this != &other)
{
// Free the existing resource.
delete[] _data;
_length = other._length;
_data = new int[_length];
std::copy(other._data, other._data + _length, _data);
}
return *this;
}
// Retrieves the length of the data resource.
size_t Length() const
{
return _length;
}
private:
size_t _length; // The length of the resource.
int* _data; // The resource.
};
為MemoryBlock創(chuàng)建移動(dòng)構(gòu)造函數(shù)
- 1.定義一個(gè)空的構(gòu)造函數(shù)方法,該方法采用一個(gè)對(duì)類類型的右值引用作為參數(shù),如以下示例所示:
MemoryBlock(MemoryBlock&& other)
: _data(nullptr)
, _length(0)
{
}
- 2.在移動(dòng)構(gòu)造函數(shù)中,將源對(duì)象中的類數(shù)據(jù)成員添加到要構(gòu)造的對(duì)象:
_data = other._data;
_length = other._length;
- 3.將源對(duì)象的數(shù)據(jù)成員分配給默認(rèn)值。 這可以防止析構(gòu)函數(shù)多次釋放資源(如內(nèi)存):
other._data = nullptr;
other._length = 0;
為MemoryBloc類創(chuàng)建移動(dòng)賦值運(yùn)算符
- 1.定義一個(gè)空的賦值運(yùn)算符,該運(yùn)算符采用一個(gè)對(duì)類類型的右值引用作為參數(shù)并返回一個(gè)對(duì)類類型的引用,如以下示例所示:
MemoryBlock& operator=(MemoryBlock&& other)
{
}
- 2.在移動(dòng)賦值運(yùn)算符中,如果嘗試將對(duì)象賦給自身,則添加不執(zhí)行運(yùn)算的條件語句。
if (this != &other)
{
}
- 3.在條件語句中,從要將其賦值的對(duì)象中釋放所有資源(如內(nèi)存)。
以下示例從要將其賦值的對(duì)象中釋放 _data 成員:
// Free the existing resource.
delete[] _data;
- 4.執(zhí)行第一個(gè)過程中的步驟 2 和步驟 3 以將數(shù)據(jù)成員從源對(duì)象轉(zhuǎn)移到要構(gòu)造的對(duì)象:
// Copy the data pointer and its length from the
// source object.
_data = other._data;
_length = other._length;
// Release the data pointer from the source object so that
// the destructor does not free the memory multiple times.
other._data = nullptr;
other._length = 0;
- 5.返回對(duì)當(dāng)前對(duì)象的引用,如以下示例所示:
return *this;
完整的MemoryBlock類如下所示:
#include <iostream>
#include <algorithm>
class MemoryBlock
{
public:
// Simple constructor that initializes the resource.
explicit MemoryBlock(size_t length)
: _length(length)
, _data(new int[length])
{
std::cout << "In MemoryBlock(size_t). length = "
<< _length << "." << std::endl;
}
// Destructor.
~MemoryBlock()
{
std::cout << "In ~MemoryBlock(). length = "
<< _length << ".";
if (_data != nullptr)
{
std::cout << " Deleting resource.";
// Delete the resource.
delete[] _data;
}
std::cout << std::endl;
}
// Copy constructor.
MemoryBlock(const MemoryBlock& other)
: _length(other._length)
, _data(new int[other._length])
{
std::cout << "In MemoryBlock(const MemoryBlock&). length = "
<< other._length << ". Copying resource." << std::endl;
std::copy(other._data, other._data + _length, _data);
}
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
std::cout << "In operator=(const MemoryBlock&). length = "
<< other._length << ". Copying resource." << std::endl;
if (this != &other)
{
// Free the existing resource.
delete[] _data;
_length = other._length;
_data = new int[_length];
std::copy(other._data, other._data + _length, _data);
}
return *this;
}
// Move constructor.
MemoryBlock(MemoryBlock&& other) noexcept
: _data(nullptr)
, _length(0)
{
std::cout << "In MemoryBlock(MemoryBlock&&). length = "
<< other._length << ". Moving resource." << std::endl;
// Copy the data pointer and its length from the
// source object.
_data = other._data;
_length = other._length;
// Release the data pointer from the source object so that
// the destructor does not free the memory multiple times.
other._data = nullptr;
other._length = 0;
}
// Move assignment operator.
MemoryBlock& operator=(MemoryBlock&& other) noexcept
{
std::cout << "In operator=(MemoryBlock&&). length = "
<< other._length << "." << std::endl;
if (this != &other)
{
// Free the existing resource.
delete[] _data;
// Copy the data pointer and its length from the
// source object.
_data = other._data;
_length = other._length;
// Release the data pointer from the source object so that
// the destructor does not free the memory multiple times.
other._data = nullptr;
other._length = 0;
}
return *this;
}
// Retrieves the length of the data resource.
size_t Length() const
{
return _length;
}
private:
size_t _length; // The length of the resource.
int* _data; // The resource.
};
值得一提的是,有時(shí)候?yàn)榱藴p少重復(fù)代碼,在移動(dòng)構(gòu)造函數(shù)中也可以調(diào)用移動(dòng)運(yùn)算符,不過需要確保這樣做不會(huì)有什么問題。
// Move constructor.
MemoryBlock(MemoryBlock&& other) noexcept
: _data(nullptr)
, _length(0)
{
*this = std::move(other);
}
下面要介紹的是,如果一個(gè)類自定了析構(gòu)函數(shù),賦值構(gòu)造函數(shù),賦值運(yùn)算符三者之一,則默認(rèn)的移動(dòng)構(gòu)造和移動(dòng)運(yùn)算符就會(huì)被delete。如果使用一個(gè)右值來構(gòu)造對(duì)象,那么編譯器將會(huì)調(diào)用復(fù)制構(gòu)造函數(shù)。
例如,MemoryBlock自定了析構(gòu)函數(shù),賦值構(gòu)造函數(shù),賦值運(yùn)算符,于是默認(rèn)的移動(dòng)構(gòu)造和移動(dòng)運(yùn)算符就會(huì)被delete。
即便你使用了MemoryBlock m2(std::move(m1));
,其仍然調(diào)用的是復(fù)制構(gòu)造函數(shù)。
#include <iostream>
#include <algorithm>
class MemoryBlock
{
public:
// Simple constructor that initializes the resource.
explicit MemoryBlock(size_t length)
: _length(length)
, _data(new int[length])
{
std::cout << "In MemoryBlock(size_t). length = "
<< _length << "." << std::endl;
}
// Destructor.
~MemoryBlock()
{
std::cout << "In ~MemoryBlock(). length = "
<< _length << ".";
if (_data != nullptr)
{
std::cout << " Deleting resource.";
// Delete the resource.
delete[] _data;
}
std::cout << std::endl;
}
// Copy constructor.
MemoryBlock(const MemoryBlock& other)
: _length(other._length)
, _data(new int[other._length])
{
std::cout << "In MemoryBlock(const MemoryBlock&). length = "
<< other._length << ". Copying resource." << std::endl;
std::copy(other._data, other._data + _length, _data);
}
// Copy assignment operator.
MemoryBlock& operator=(const MemoryBlock& other)
{
std::cout << "In operator=(const MemoryBlock&). length = "
<< other._length << ". Copying resource." << std::endl;
if (this != &other)
{
// Free the existing resource.
delete[] _data;
_length = other._length;
_data = new int[_length];
std::copy(other._data, other._data + _length, _data);
}
return *this;
}
// Retrieves the length of the data resource.
size_t Length() const
{
return _length;
}
private:
size_t _length; // The length of the resource.
int* _data; // The resource.
};
int main()
{
MemoryBlock m1(10);
MemoryBlock m2(std::move(m1));
}
因此這就誕生了另一個(gè)著名定理rule of five定理。即如果你需要自定義移動(dòng)構(gòu)造函數(shù)和移動(dòng)運(yùn)算符,那么大概率你需要自定義5個(gè)特殊函數(shù)(析構(gòu)函數(shù),復(fù)制構(gòu)造函數(shù),賦值運(yùn)算符,移動(dòng)構(gòu)造函數(shù),移動(dòng)運(yùn)算符)。
這里順便再提到另一個(gè)rule of zero定理,
- 1.類不應(yīng)定義任何特殊函數(shù)(復(fù)制/移動(dòng)構(gòu)造函數(shù)/賦值和析構(gòu)函數(shù)),除非它們是專用于資源管理的類。
此舉為了滿足設(shè)計(jì)上的單一責(zé)任原則,將數(shù)據(jù)模塊與功能模塊在代碼層面分離,降低耦合度。
class rule_of_zero
{
std::string cppstring;
public:
rule_of_zero(const std::string& arg) : cppstring(arg) {}
};
- 2.基類作為管理資源的類在被繼承時(shí),析構(gòu)函數(shù)可能必須要聲明為public virtual,這樣的行為會(huì)破壞移動(dòng)復(fù)制構(gòu)造,因此,如果基類在此時(shí)的默認(rèn)函數(shù)應(yīng)設(shè)置為default。
此舉為了滿足多態(tài)類在C ++核心準(zhǔn)則中禁止復(fù)制的編碼原則。
class base_of_five_defaults
{
public:
base_of_five_defaults(const base_of_five_defaults&) = default;
base_of_five_defaults(base_of_five_defaults&&) = default;
base_of_five_defaults& operator=(const base_of_five_defaults&) = default;
base_of_five_defaults& operator=(base_of_five_defaults&&) = default;
virtual ~base_of_five_defaults() = default;
};
關(guān)于這個(gè)點(diǎn),還是需要一個(gè)例子來加深印象:
#include <iostream>
#include <algorithm>
#include <vector>
class A{
public:
A() {
std::cout << "A()" << std::endl;
};
~A() = default;
A(const A& other){
std::cout << "A(const A& other)" << std::endl;
}
A& operator=(const A& other){
std::cout << "operator=(const A& other)" << std::endl;
return *this;
}
A(A&& other){
std::cout << "A(A&& other)" << std::endl;
}
A& operator=(A&& other){
std::cout << "operator=(A&& other)" << std::endl;
return *this;
}
};
class DataMgr {
public:
DataMgr(){
val_.reserve(10);
}
virtual ~DataMgr() = default;
// DataMgr(const DataMgr& other) = default;
// DataMgr& operator=(const DataMgr& other) = default;
// DataMgr(DataMgr&& other) = default;
// DataMgr& operator=(DataMgr&& other) = default;
public:
void push(A& a){
val_.emplace_back(a);
}
private:
std::vector<A> val_; //同之前一樣
};
int main()
{
A a1, a2;
DataMgr s1;
s1.push(a1);
s1.push(a2);
std::cout << "========" << std::endl;
DataMgr s2 ;
s2 = std::move(s1);
}
這里的運(yùn)行結(jié)果如下所示:文章來源:http://www.zghlxwxcb.cn/news/detail-679614.html
A()
A()
A(const A& other)
A(const A& other)
========
A(const A& other)
A(const A& other)
盡管使用了s2 = std::move(s1)
這里使用了移動(dòng)語義,然而由于定義了析構(gòu)函數(shù),移動(dòng)操作被delete,導(dǎo)致了調(diào)用了復(fù)制構(gòu)造。試想如果這里的val_的數(shù)據(jù)量很大,那么程序的運(yùn)行效率將會(huì)相差很大。文章來源地址http://www.zghlxwxcb.cn/news/detail-679614.html
總結(jié)
- 特殊成員函數(shù)是編譯器可能自動(dòng)生成的函數(shù),它包括下面六種默認(rèn)構(gòu)造函數(shù),析構(gòu)函數(shù),復(fù)制構(gòu)造函數(shù),賦值運(yùn)算符,移動(dòng)構(gòu)造函數(shù),移動(dòng)運(yùn)算符。
- 對(duì)于構(gòu)造函數(shù)而言,如果需要自定義初始化成員的方式,則不能使用默認(rèn)的構(gòu)造函數(shù),需要編寫自定義構(gòu)造函數(shù)。
- 對(duì)于析構(gòu)函數(shù)而言,如果其內(nèi)部管理了資源(原始指針,文件描述符,線程等等),則通常需要編寫自定義的析構(gòu)函數(shù)。如果只是借用資源,通常使用默認(rèn)析構(gòu)函數(shù)就可以。
- 根據(jù)rule of three,析構(gòu)函數(shù)進(jìn)行了自定義,大概率你也需要自定義復(fù)制構(gòu)造函數(shù)和賦值運(yùn)算符。
- 默認(rèn)移動(dòng)操作僅當(dāng)類沒有顯式聲明移動(dòng)操作,復(fù)制操作,析構(gòu)函數(shù)時(shí)才自動(dòng)生成。如果你定義了析構(gòu)函數(shù)或者復(fù)制操作,此時(shí)的移動(dòng)操作會(huì)調(diào)用復(fù)制構(gòu)造函數(shù)。
- 如果一個(gè)類沒有顯示定義復(fù)制構(gòu)造卻顯示定義了移動(dòng)構(gòu)造,則復(fù)制構(gòu)造函數(shù)被delete。同理如果一個(gè)類沒有顯示定義賦值運(yùn)算符卻顯示定義了移動(dòng)運(yùn)算符,則賦值運(yùn)算符數(shù)被delete。
- 日常開發(fā)中,盡量顯示指明是否使用default的特殊函數(shù)以避免某些成員函數(shù)被delete。如果某些方法不需要生成,則應(yīng)該delete掉。
到了這里,關(guān)于深入理解c++特殊成員函數(shù)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!