-
拷貝和移動建構式定義了當用同型別的另一個物件初始化本物件時做什么,拷貝和移動賦值運算子定義了將一個物件賦予同型別的另一個物件時做什么,解構式定義了當此型別物件銷毀時做什么,我們稱這些操作為拷貝控制操作,
-
如果一個建構式的第一個引數是自身型別別的參考,且任何額外引數都有默認值,則此建構式是拷貝建構式,
-
拷貝建構式的第一個引數必須是一個參考型別,雖然我們可以定義一個接受非const參考的拷貝建構式,但此引數幾乎總是一個const的參考,
-
拷貝建構式在幾種情況下都會被隱式地使用,因此,拷貝建構式通常不應該是explicit的,
-
如果我們沒有為一個類定義拷貝建構式,編譯器會為我們定義一個,與合成默認建構式不同,即使我們定義了其他建構式,編譯器也會為我們合成一個拷貝建構式,
-
一般情況,合成的拷貝建構式會將其引數的成員逐個拷貝到正在創建的物件中,編譯器從給定物件中依次將每個非static成員拷貝到正在創建的物件中,而對于某些類來說,合成拷貝建構式用來阻止我們拷貝該型別別的物件,
-
每個成員的型別決定了它如何拷貝:對型別別的成員,會使用其拷貝建構式來拷貝;內置型別的成員則直接拷貝,雖然我們不能直接拷貝一個陣列,但合成拷貝建構式會逐元素地拷貝一個陣列型別的成員,如果陣列元素是型別別,則使用元素的拷貝建構式來進行拷貝,
-
當使用直接初始化時,我們實際上是要求編譯器使用普通的函式匹配來選擇與我們提供的引數最匹配的建構式,當我們使用拷貝初始化時,我們要求編譯器將右側運算物件拷貝到正在創建的物件中,如果需要的話還要進行型別轉換,
-
拷貝初始化通常使用拷貝建構式來完成,但是,如果一個類有一個移動建構式,則拷貝初始化有時會使用移動建構式而非拷貝建構式來完成,
-
拷貝建構式在以下幾種情況下會被使用(發生拷貝初始化):
-
拷貝初始化(用=定義變數)
-
將一個物件作為實參傳遞給一個非參考型別的形參
-
從一個回傳型別為非參考型別的函式回傳一個物件
-
用花括號串列初始化一個陣列中的元素或一個聚合類中的成員
-
某些型別別還會對它們所分配的物件使用拷貝初始化,(初始化標準庫容器或呼叫其insert/push操作時,容器會對其元素進行拷貝初始化)
-
-
拷貝建構式被用來初始化非參考型別別引數,這一特性解釋了為什么拷貝建構式自己的引數必須是參考型別,
-
如果我們希望使用一個explicit建構式,就必須顯式地使用,
vector<int> v1(10); // 正確:直接初始化 vector<int> v2 = 10; // 錯誤:接受大小引數的建構式是explicit的 void f(vector<int>); // f的引數進行拷貝初始化 f(10); // 錯誤:不能用一個explicit的建構式拷貝一個實參 f(vector<int>(10)); // 正確:從一個int直接構造一個臨時vector -
在拷貝初始化程序中,編譯器可以(但不是必須)跳過拷貝/移動建構式,直接創建物件,即,編譯器被允許將下面的代碼改寫:
string null_book = "9-999-9999-9"; // 拷貝初始化 string null_book("9-999-99999-9"); // 編譯器略過了拷貝建構式但是,即使編譯器略過了拷貝/移動建構式,但在這個程式點上,拷貝/移動建構式必須是存在且可訪問的(例如,不能是private的),
-
多載運算子的引數表示運算子的運算物件,某些運算子,包括賦值運算子,必須定義為成員函式,如果一個運算子是一個成員函式,其左側運算物件就系結到隱式的this引數,對于一個二元運算子,例如賦值運算子,其右側運算物件作為顯式引數傳遞,
-
拷貝賦值運算子接受一個與其所在類相同型別的引數:
class Foo { public: Foo& operator=(const Foo&); // 賦值運算子 // ... }; -
賦值運算子通常應該回傳一個指向其左側運算物件的參考,
-
與處理拷貝建構式一樣,如果一個類未定義自己的拷貝賦值運算子,編譯器會為它生成一個合成拷貝賦值運算子,
-
類似拷貝建構式,對于某些類,合成拷貝賦值運算子用來禁止該型別物件的賦值,如果拷貝賦值運算子并非出于此目的,它會將右側運算物件的每個非static成員賦予左側運算物件的對應成員,這一作業是通過成員型別的拷貝賦值運算子來完成的,對于陣列型別的成員,逐個賦值陣列元素,合成拷貝賦值運算子回傳一個指向其左側運算物件的參考,
// 等價于合成拷貝賦值運算子 Sales_data& Sales_data::operator=(const Sales_data &rhs) { bookNo = rhs.bookNo; // 呼叫string::operator= units_sold = rhs.units_sold; // 使用內置的int賦值 revenue = rhs.revenue; // 使用內置的double賦值 return *this; // 回傳一個此物件的參考 } -
解構式釋放物件使用的資源,并銷毀物件的非static資料成員,解構式是類的一個成員函式,名字由波浪號接類名構成,它沒有回傳值,也不接受引數:
class Foo { public: ~Foo(); // 解構式 // ... }; -
由于解構式不接受引數,因此它不能被多載,對一個給定類,只會有唯一一個解構式,
-
解構式有一個函式體和一個析構部分,在一個解構式中,首先執行函式體,然后銷毀成員,成員按初始化順序的逆序銷毀,
-
在一個解構式中,析構部分是隱式的,成員銷毀時發生什么完全依賴于成員的型別,銷毀型別別的成員需要執行成員自己的解構式,內置型別沒有解構式,因此銷毀內置型別成員什么也不需要做,
-
隱式銷毀一個內置指標型別的成員不會delete它所指向的物件,
-
無論何時一個物件被銷毀,就會自動呼叫其解構式:
- 變數在離開其作用域時被銷毀
- 當一個物件被銷毀時,其成員被銷毀
- 容器(無論是標準庫容器還是陣列)被銷毀時,其元素被銷毀
- 對于動態分配的物件,當對指向它的指標應用delete運算子時被銷毀
- 對于臨時物件,當創建它的完整運算式結束時被銷毀
-
當指向一個物件的參考或指標離開作用域時,解構式不會執行,
-
類似拷貝建構式和拷貝賦值運算子,對于某些類,合成解構式被用來阻止該型別的物件被銷毀,如果不是這種情況,合成解構式的函式體就為空,
// 下面的代碼片段等價于Sales_data的合成解構式 class Sales_data { public: // 成員會被自動銷毀,除此之外不需要做其他事情 ~Sales_data() { } // 其他成員的定義,如前 }; -
解構式體自身并不直接銷毀成員,成員是在解構式體之后隱含的析構階段中被銷毀的,在整個物件銷毀程序中,解構式體是作為成員銷毀步驟之外的另一部分而進行的,
-
當我們決定一個類是否要定義它自己版本的拷貝控制成員時,一個基本原則是首先確定這個類是否需要一個解構式,通常,對解構式的需求要比對拷貝建構式或賦值運算子的需求更為明顯,如果一個類需要自定義解構式,幾乎可以肯定它也需要自定義拷貝賦值運算子和拷貝建構式,
-
決定一個類是否要定義它自己版本的拷貝控制成員時,第二個基本原則:如果一個類需要一個拷貝建構式,幾乎可以肯定它也需要一個拷貝賦值運算子,反之亦然——如果一個類需要一個拷貝賦值運算子,幾乎可以肯定它也需要一個拷貝建構式,然而,無論是需要拷貝建構式還是需要拷貝賦值運算子都不必然意味著也需要解構式,
-
我們可以通過將拷貝控制成員定義為
=default來顯式地要求編譯器生成合成的版本, -
當我們在類內用
=default修飾成員的宣告時,合成的函式將隱式地宣告為行內的(就像任何其他類內宣告的成員函式一樣),如果我們不希望合成的成員是行內函式,應該只對成員的類外定義使用=default,class Sales_data { public: // 拷貝控制成員;使用default Sales_data() = default; Sales_data(const Sales_data&) = default; Sales_data& operator=(const Sales_data &); ~Sales_data() = default; // 其他成員的定義,如前 }; Sales_data& Sales_data::operator=(const Sales_data&) = default; -
我們只能對具有合成版本的成員函式使用
=default(即,默認建構式或拷貝控制成員), -
我們可以通過將拷貝建構式和拷貝賦值運算子定義為洗掉的函式來阻止拷貝:我們雖然宣告了它們,但不能以任何方式使用它們,在函式的引數串列后面加上
=delete來指出我們希望將它定義為洗掉的:struct NoCopy { NoCopy() = default; // 使用合成的默認建構式 NoCopy(const NoCopy&) = delete; // 阻止拷貝 NoCopy &operator=(const NoCopy&) = delete; // 阻止賦值 ~NoCopy() = default; // 使用合成的解構式 // 其他成員 }; -
與
=default不同,=delete必須出現在函式第一次宣告的時候,這個差異與這些宣告的含義在邏輯上是吻合的,一個默認的成員只影響為這個成員而生成的代碼,因此=default直到編譯器生成代碼時才需要,而另一方面,編譯器需要知道一個函式是洗掉的,以便禁止試圖使用它的操作, -
與
=default的另一個不同之處是,我們可以對任何函式指定=delete(我們只能對編譯器可以合成的默認建構式或拷貝控制成員使用=default),雖然洗掉函式的主要用途是禁止拷貝控制成員,但當我們希望引導函式匹配程序時,洗掉函式有時也是有用的, -
對于一個洗掉了解構式的型別,編譯器將不允許定義該型別的變數或創建該類的臨時物件,而且,如果一個類有某個成員的型別洗掉了解構式,我們也不能定義該類的變數或臨時物件,
-
對于洗掉了解構式的型別,雖然我們不能定義這種型別的變數或成員,但可以動態分配這種型別的物件,但是,不能釋放這些物件:
struct NoDtor { NoDtor() = default; // 使用合成默認建構式 ~NoDtor() = delete; // 我們不能銷毀NoDtor型別的物件 }; NoDtor nd; // 錯誤:NoDtor的解構式是洗掉的 NoDtor *p = new NoDtor(); // 正確:但我們不能delete p delete p; // 錯誤:NoDtor的解構式是洗掉的 -
對于解構式已洗掉的型別,不能定義該型別的變數或釋放指向該型別動態分配物件的指標,
-
對某些類來說,編譯器將這些合成的成員定義為洗掉的函式:
- 如果類的某個成員的解構式是洗掉的或不可訪問的(例如,是private的),則類的合成解構式被定義為洗掉的,
- 如果類的某個成員的拷貝建構式是洗掉的或不可訪問的,則類的合成拷貝建構式被定義為洗掉的,如果類的某個成員的解構式是洗掉的或不可訪問的,則類合成的拷貝建構式也被定義為洗掉的,
- 如果類的某個成員的拷貝賦值運算子是洗掉的或不可訪問的,或是類有一個const的或參考成員,則類的合成拷貝賦值運算子被定義為洗掉的,
- 如果類的某個成員的解構式是洗掉的或不可訪問的,或是類有一個參考成員,它沒有類內初始化器,或是類有一個const成員,它沒有類內初始化器且其型別未顯式定義默認建構式,則該類的默認建構式被定義為洗掉的,
-
本質上,這些規則的含義是:如果一個類有資料成員不能默認構造、拷貝、賦值或銷毀,則對應的成員函式將被定義為洗掉的,(本質上,當不可能拷貝、賦值或銷毀類的成員時,類的合成拷貝控制成員就被定義為洗掉的,)
-
因為試圖訪問一個未定義的成員會導致一個鏈接時錯誤,通過宣告(但不定義)
private的拷貝建構式,我們可以預先阻止任何拷貝該型別物件的企圖:試圖拷貝物件的用戶代碼將在編譯階段被標記為錯誤;成員函式或友元函式中的拷貝操作將會導致鏈接時錯誤, -
希望阻止拷貝的類應該使用
=delete來定義它們自己的拷貝建構式和拷貝賦值運算子,而不應該將它們宣告為private的, -
通常,管理類外資源的類必須定義拷貝控制成員,為了定義這些成員,我們首先必須確定此型別物件的拷貝語意,一般來說,有兩種選擇:可以定義拷貝操作,使類的行為看起來像一個值或者像一個指標,
- 類的行為像一個值,意味著它應該也有自己的狀態,當我們拷貝一個像值的物件時,副本和原物件是完全獨立的,改變副本不會對原物件有任何影響,反之亦然,
- 行為像指標的類則共享狀態,當我們拷貝一個這種類的物件時,副本和原物件使用相同的底層資料,改變副本也會改變原物件,反之亦然,
-
關鍵概念:賦值運算子
當你撰寫賦值運算子時,有兩點需要記住:
- 如果將一個物件賦予它自身,賦值運算子必須能正確作業,
- 大多數賦值運算子組合了解構式和拷貝建構式的作業,
當你撰寫一個賦值運算子時,一個好的模式是先將右側運算物件拷貝到一個區域臨時物件中,當拷貝完成后,銷毀左側運算物件的現有成員就是安全的了,一旦左側運算物件的資源被銷毀,就只剩下將資料從臨時物件拷貝到左側運算物件的成員中了,
HasPtr& HasPtr::operator=(const HasPtr &rhs) { auto newp = new string(*rhs.ps); // 拷貝底層string delete ps; // 釋放舊記憶體 ps = newp; // 從右側運算物件拷貝資料到本物件 i = rhs.i; return *this; // 回傳本物件 }這樣撰寫賦值運算子是錯誤的
HasPtr& HasPtr::operator=(const HasPtr &rhs) { delete ps; // 釋放物件指向的string // 如果rhs和*this是同一個物件,我們就將從已釋放的記憶體中拷貝資料! ps = new string(*(rhs.ps)); i = rhs.i; return *this; } -
令一個類展現類似指標的行為的最好方法是使用
shared_ptr來管理類中的資源,但是,有時我們希望直接管理資源,在這種情況下,使用參考計數就很有用了,參考計數的作業方式如下:- 除了初始化物件外,每個建構式(拷貝建構式除外)還要創建一個參考計數,用來記錄有多少物件與正在創建的物件共享狀態,當我們創建一個物件時,只有一個物件共享狀態,因此將計數器初始化為1,
- 拷貝建構式不分配新的計數器,而是拷貝給定物件的資料成員,包括計數器,拷貝建構式遞增共享的計數器,指出給定物件的狀態又被一個新用戶所共享,
- 解構式遞減計數器,指出共享狀態的用戶少了一個,如果計數器變為0,則解構式釋放狀態,
- 拷貝賦值運算子遞增右側運算物件的計數器,遞減左側運算物件的計數器,如果左側運算物件的計數器變為0,意味著它的共享狀態沒有用戶了,拷貝賦值運算子就必須銷毀狀態,
-
實作共享計數器的一種方法是將計數器保存在動態記憶體中,
class HasPtr { public: // 建構式分配新的string和新的計數器,將計數器置為1 HasPtr(const string &s = string()) : ps(new string(s)), i(0), use(new size_t(1)) {} // 拷貝建構式拷貝所有三個資料成員,并遞增計數器 HasPtr(const HasPtr &p) : ps(p.ps), i(p.i), use(p.use) { ++*use; } HasPtr &operator=(const HasPtr &); ~HasPtr(); private: string *ps; int i; size_t *use; // 用來記錄有多少個物件共享*ps的成員 }; HasPtr &HasPtr::operator=(const HasPtr &rhs) { ++*rhs.use; // 遞增右側運算物件的參考計數 if (--*use == 0) // 然后遞減本物件的參考計數 { delete ps; // 如果沒有其他用戶 delete use; // 釋放本物件分配的成員 } ps = rhs.ps; // 將資料從rhs拷貝到本物件 i = rhs.i; use = rhs.use; return *this; // 回傳本物件 } HasPtr::~HasPtr() { if (--*use == 0) // 如果參考計數變為0 { delete ps; // 釋放string記憶體 delete use; // 釋放計數器記憶體 } } -
除了定義拷貝控制成員,管理資源的類通常還定義一個名為swap的函式,對于那些與重排元素順序的演算法一起使用的類,定義swap是非常重要的,這類演算法在需要交換兩個元素時會呼叫swap,
-
如果一個類定義了自己的swap,那么演算法將使用類自定義版本,否則,演算法將使用標準庫定義的swap,
class HasPtr { friend void swap(HasPtr&, HasPtr&); // 其他成員定義 }; // 由于swap的存在就是為了優化代碼,我們將其宣告為inline函式, inline void swap(HasPtr &lhs, HasPtr &rhs) { using std::swap; swap(lhs.ps, rhs.ps); // 交換指標,而不是string資料 swap(lhs.i, rhs.i); // 交換int成員 } -
與拷貝控制成員不同,swap并不是必要的,但是,對于分配了資源的類,定義swap可能是一種很重要的優化手段,
-
swap函式應該呼叫swap,而不是std::swap,內置型別是沒有特定版本的swap的,對swap的呼叫會呼叫標準庫
std::swap,但是,如果一個類的成員有自己型別特定的swap函式,呼叫std::swap就是錯誤的了, -
如果存在型別特定的swap版本,swap呼叫會與之匹配,如果不存在型別特定的版本,則會使用
std中的版本(假定作用域中有using宣告), -
定義swap的類通常用swap來定義它們的賦值運算子,這些運算子使用了一種名為拷貝并交換的技術,這種技術將左側運算物件與右側運算物件的一個副本進行交換:
// 注意rhs是按值傳遞的,意味著HasPtr的拷貝建構式... // ...將右側運算物件中的string拷貝到rhs HasPtr& HasPtr::operator=(HasPtr rhs) { // 交換左側運算物件和區域變數rhs的內容 swap(*this, rhs); // rhs現在指向本物件曾經使用的記憶體 return *this; // rhs被銷毀,從而delete了rhs中的指標 } -
使用拷貝并交換的賦值運算子自動就是例外安全的,且能正確處理自賦值,
-
雖然通常來說分配資源的類更需要拷貝控制,但資源管理并不是一個類需要定義自己的拷貝控制成員的唯一原因,一些類也需要拷貝控制成員的幫助來進行簿記作業或其他操作,
-
拷貝賦值運算子通常執行拷貝建構式和解構式中也要做的作業,這種情況下,公共的作業應該放在private的工具函式中完成,
-
拷貝控制示例
-
Folder.cpp
#include <utility> // for move, we don't supply a using declaration for move #include <iostream> using std::cerr; using std::endl; #include <set> using std::set; #include <string> using std::string; #include "Folder.h" void swap(Message &lhs, Message &rhs) { using std::swap; // not strictly needed in this case, but good habit // remove pointers to each Message from their (original) respective Folders for (auto f: lhs.folders) f->remMsg(&lhs); for (auto f: rhs.folders) f->remMsg(&rhs); // swap the contents and Folder pointer sets swap(lhs.folders, rhs.folders); // uses swap(set&, set&) swap(lhs.contents, rhs.contents); // swap(string&, string&) // add pointers to each Message to their (new) respective Folders for (auto f: lhs.folders) f->addMsg(&lhs); for (auto f: rhs.folders) f->addMsg(&rhs); } Folder::Folder(Folder &&f) { move_Messages(&f); // make each Message point to this Folder } Folder& Folder::operator=(Folder &&f) { if (this != &f) { remove_from_Msgs(); // remove this Folder from the current msgs move_Messages(&f); // make each Message point to this Folder } return *this; } void Folder::move_Messages(Folder *f) { msgs = std::move(f->msgs); // move the set from f to this Folder f->msgs.clear(); // ensure that destroying f is harmless for (auto m : msgs) { // for each Message in this Folder m->remFldr(f); // remove the pointer to the old Folder m->addFldr(this); // insert pointer to this Folder } } Message::Message(Message &&m): contents(std::move(m.contents)) { move_Folders(&m); // moves folders and updates the Folder pointers } Message::Message(const Message &m): contents(m.contents), folders(m.folders) { add_to_Folders(m); // add this Message to the Folders that point to m } Message& Message::operator=(Message &&rhs) { if (this != &rhs) { // direct check for self-assignment remove_from_Folders(); contents = std::move(rhs.contents); // move assignment move_Folders(&rhs); // reset the Folders to point to this Message } return *this; } Message& Message::operator=(const Message &rhs) { // handle self-assignment by removing pointers before inserting them remove_from_Folders(); // update existing Folders contents = rhs.contents; // copy message contents from rhs folders = rhs.folders; // copy Folder pointers from rhs add_to_Folders(rhs); // add this Message to those Folders return *this; } Message::~Message() { remove_from_Folders(); } // move the Folder pointers from m to this Message void Message::move_Folders(Message *m) { folders = std::move(m->folders); // uses set move assignment for (auto f : folders) { // for each Folder f->remMsg(m); // remove the old Message from the Folder f->addMsg(this); // add this Message to that Folder } m->folders.clear(); // ensure that destroying m is harmless } // add this Message to Folders that point to m void Message::add_to_Folders(const Message &m) { for (auto f : m.folders) // for each Folder that holds m f->addMsg(this); // add a pointer to this Message to that Folder } // remove this Message from the corresponding Folders void Message::remove_from_Folders() { for (auto f : folders) // for each pointer in folders f->remMsg(this); // remove this Message from that Folder folders.clear(); // no Folder points to this Message } void Folder::add_to_Messages(const Folder &f) { for (auto msg : f.msgs) msg->addFldr(this); // add this Folder to each Message } Folder::Folder(const Folder &f) : msgs(f.msgs) { add_to_Messages(f); // add this Folder to each Message in f.msgs } Folder& Folder::operator=(const Folder &f) { remove_from_Msgs(); // remove this folder from each Message in msgs msgs = f.msgs; // copy the set of Messages from f add_to_Messages(f); // add this folder to each Message in msgs return *this; } Folder::~Folder() { remove_from_Msgs(); } void Folder::remove_from_Msgs() { while (!msgs.empty()) (*msgs.begin())->remove(*this); } void Message::save(Folder &f) { folders.insert(&f); // add the given Folder to our list of Folders f.addMsg(this); // add this Message to f's set of Messages } void Message::remove(Folder &f) { folders.erase(&f); // take the given Folder out of our list of Folders f.remMsg(this); // remove this Message to f's set of Messages } void Folder::save(Message &m) { // add m and add this folder to m's set of Folders msgs.insert(&m); m.addFldr(this); } void Folder::remove(Message &m) { // erase m from msgs and remove this folder from m msgs.erase(&m); m.remFldr(this); } void Folder::debug_print() { cerr << "Folder contains " << msgs.size() << " messages" << endl; int ctr = 1; for (auto m : msgs) { cerr << "Message " << ctr++ << ":\n\t" << m->contents << endl; } } void Message::debug_print() { cerr << "Message:\n\t" << contents << endl; cerr << "Appears in " << folders.size() << " Folders" << endl; }-
Folder.h
#ifndef FOLDER_H #define FOLDER_H #include <string> #include <set> class Folder; class Message { friend void swap(Message&, Message&); friend class Folder; public: // folders is implicitly initialized to the empty set explicit Message(const std::string &str = ""): contents(str) { } // copy control to manage pointers to this Message Message(const Message&); // copy constructor Message& operator=(const Message&); // copy assignment ~Message(); // destructor Message(Message&&); // move constructor Message& operator=(Message&&); // move assignment // add/remove this Message from the specified Folder's set of messages void save(Folder&); void remove(Folder&); void debug_print(); // print contents and it's list of Folders, // printing each Folder as well private: std::string contents; // actual message text std::set<Folder*> folders; // Folders that have this Message // utility functions used by copy constructor, assignment, and destructor // add this Message to the Folders that point to the parameter void add_to_Folders(const Message&); void move_Folders(Message*); // remove this Message from every Folder in folders void remove_from_Folders(); // used by Folder class to add self to this Message's set of Folder's void addFldr(Folder *f) { folders.insert(f); } void remFldr(Folder *f) { folders.erase(f); } }; // declaration for swap should be in the same header as Message itself void swap(Message&, Message&); class Folder { friend void swap(Message&, Message&); friend class Message; public: ~Folder(); // remove self from Messages in msgs Folder(const Folder&); // add new folder to each Message in msgs Folder& operator=(const Folder&); // delete Folder from lhs messages // add Folder to rhs messages Folder(Folder&&); // move Messages to this Folder Folder& operator=(Folder&&); // delete Folder from lhs messages // add Folder to rhs messages Folder() = default; // defaults ok void save(Message&); // add this message to folder void remove(Message&); // remove this message from this folder void debug_print(); // print contents and it's list of Folders, private: std::set<Message*> msgs; // messages in this folder void add_to_Messages(const Folder&);// add this Folder to each Message void remove_from_Msgs(); // remove this Folder from each Message void addMsg(Message *m) { msgs.insert(m); } void remMsg(Message *m) { msgs.erase(m); } void move_Messages(Folder*); // move Message pointers to point to this Folder }; #endif -
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/258877.html
標籤:其他
上一篇:30行代碼實作朋友圈自動點贊
下一篇:Java三大特征詳解
