《C++Primer》第十二章-類-學習筆記(2)-作用域&構造函數

《C++Primer》第十二章-類-學習筆記(2)-作用域&構造函數

日誌:
1,2020-03-03 筆者提交文章的初版V1.0

作者按:
最近在學習C++ primer,初步打算把所學的記錄下來。

傳送門/推廣
《C++Primer》第二章-變量和基本類型-學習筆記(1)
《C++Primer》第三章-標準庫類型-學習筆記(1)
《C++Primer》第八章-標準 IO 庫-學習筆記(1)
上一篇
《C++Primer》第十二章-類-學習筆記(1)

類作用域

類作用域(class scope):每個類定義一個作用域。類作用域比其他作用域複雜得多——在類的定義
體內定義的成員函數可以使用出現在該定義之後的名字。
每個類都定義了自己的新作用域和唯一的類型。在類的定義體內聲明類成員,將成員名引入類的作用域。兩個不同的類具有兩個的類作用域。即使兩個類具有完全相同的成員列表,它們也是不同的類型。每個類的成員不同於任何其他類(或任何其他作用域)的成員。

class First {
public:
	int memi;
	double memd;
};
class Second {
public:
	int memi;
	double memd;
};
First obj1;           
Second obj2 = obj1; // error: obj1 and obj2 have different types

使用類的成員

在類作用域之外,成員只能通過對象指針分別使用成員訪問操作符 .-> 來訪問。這些操作符左邊的操作數分別是一個類對象或指向類對象的指針。
跟在操作符後面的成員名字必須在相關聯的類的作用域中聲明:

Class obj; // Class is some class type
Class *ptr = &obj;  //指向類對象的指針
// member is a data member of that class
ptr->member; // fetches member from the object to which ptr points  指針通過-> 來訪問成員
obj.member; // fetches member from the object named obj
// memfcn is a function member of that class
ptr->memfcn(); // runs memfcn on the object to which ptr points
obj.memfcn(); // runs memfcn on the object named obj

一些成員使用成員訪問操作符來訪問,另一些直接通過類使用作用域操作符::來訪問。
一般的數據或函數成員必須通過對象來訪問。定義類型的成員,如 Screen::index,使用作用域操作符來訪問

作用域與成員定義

儘管成員是在類的定義體之外定義的,但成員定義就好像它們是在類的作用域中一樣。回憶一下,出現在類的定義體之外的成員定義必須指明成員出現在哪個類中

double Sales_item::avg_price() const
{
	if (units_sold)
	return revenue/units_sold;
	else
	return 0;
}

我們用完全限定名 Sales_item::avg_price 來指出這是類Sales_item 作用域中的 avg_price 成員的定義。一旦看到成員的完全限定名,就知道該定義是在類作用域中。因爲該定義是在類作用域中,所以我們可以引用revenue 或 units_sold,而不必寫 this->revenue 或 this->units_sold。

形參表和函數體處於類作用域中

在定義於類外部的成員函數中,形參表和成員函數體都出現在成員名之後。這些都是在類作用域中定義,所以可以不用限定而引用其他成員。例如,類Screen 中 get 的二形參版本的定義:

char Screen::get(index r, index c) const
{
index row = r * width; // compute the row location
return contents[row + c]; // offset by c to fetch specified character
}

該函數用 Screen 內定義的 index 類型來指定其形參類型。因爲形參表是在 Screen 類的作用域內,所以不必指明我們想要的是 Screen::index。我們想要的是定義在當前類作用域中的,這是隱含的。同樣,使用 index、width 和contents 時指的都是 Screen 類中聲明的名字。

函數返回類型不一定在類作用域中

與形參類型相比,返回類型出現在成員名字前面。如果函數在類定義體之外定義,則用於返回類型的名字在類作用域之外。如果返回類型使用由類定義的類型,則必須使用完全限定名。例如,考慮 get_cursor 函數:

class Screen {
public:
	typedef std::string::size_type index;
	index get_cursor() const;
};
inline Screen::index Screen::get_cursor() const   //比如這個Screen::index 
{
	return cursor;
}

該函數的返回類型是 index,這是在 Screen 類內部定義的一個類型名。如果在類定義體之外定義 get_cursor,則在函數名被處理之前,代碼在不在類作用域內。當看到返回類型時,其名字是在類作用域之外使用。必須用完全限定的類型名 Screen::index 來指定所需要的 index 是在類 Screen 中定義的名字。

類作用域中的名字查找

名字查找指:尋找與給定的名字使用相匹配的聲明的過程

  1. 首先,在使用該名字的塊中查找名字的聲明。只考慮在該項使用之前聲明的名字。
  2. 如果找不到該名字,則在包圍的作用域中查找。
  3. 如果找不到任何聲明,則程序出錯。在 C++ 程序中,所有名字必須在使用之前聲明。

類作用域也許表現得有點不同,但實際上遵循同一規則。可能引起混淆的是函數中名字確定的方式,而該函數是在類定義體內定義的。
類定義實際上是在兩個階段中處理:

  1. 首先,編譯成員聲明;
  2. 只有在類的所有成員出現之後,才編譯它們的定義本身

當然,類作用域中使用的名字並非必須是類成員名。類作用域中的名字查找也會發生在其他作用域中聲明的名字。在名字查找期間,如果類作用域中使用的名字不能確定爲類成員名,則在包含該類或成員定義的作用域中查找,以便找到該名字的聲明。

類成員聲明的名字查找

按以下方式確定在類成員的聲明中用到的名字。

  1. 檢查出現在名字使用之前的類成員的聲明。
  2. 如果第 1 步查找不成功,則檢查包含類定義的作用域中出現的聲明以及出現在類定義之前的聲明。
    例如:
typedef double Money;  //出現在類定義之前的聲明
class Account {
public:
	Money balance() { return bal; }
private:
	Money bal;
// ...
};

在處理 balance 函數的聲明時,編譯器首先在類 Account 的作用域中查找Money 的聲明。編譯器只考慮出現在 Money 使用之前的聲明。因爲找不到任何成員聲明,編譯器隨後在全局作用域中查找 Money 的聲明。只考慮出現在類Account 的定義之前的聲明。找到全局的類型別名 Money 的聲明,並將它用作函數 balance 的返回類型和數據成員 bal 的類型。

必須在類中先定義類型名字,才能將它們用作數據成員的類型,或者成員函數的返回類型或形參類型。

編譯器按照成員聲明在類中出現的次序來處理它們。通常,名字必須在使用之前進行定義。而且,一旦一個名字被用作類型名,該名字就不能被重複定義:

typedef double Money;
class Account {
public:
	Money balance() { return bal; } // uses global definition of Money
private:
// error: cannot change meaning of Money   //這裏重複定義了類型名字,出錯。
	typedef long double Money;  
	Money bal;
// ...
};

類成員定義中的名字查找

按以下方式確定在成員函數的函數體中用到的名字

  1. 首先檢查成員函數局部作用域中的聲明。
  2. 如果在成員函數中找不到該名字的聲明,則檢查對所有類成員的聲明。
  3. 如果在類中找不到該名字的聲明,則檢查在此成員函數定義之前的作用域
    中出現的聲明。

類成員遵循常規的塊作用域名字查找

下面的函數使用了相同的名字來表示形參和成員,這是通常應該避免的。這樣做的目的是展示如何確定名字:

// Note: This code is for illustration purposes only and reflects bad practice
// It is a bad idea to use the same name for a parameter and a member
int height;
class Screen {
public:
	void dummy_fcn(index height) {
	cursor = width * height; // which height? The parameter  指的是函數形參的height
	}
private:
	index cursor;
	index height, width;     
};

查找 dummy_fcn 的定義中使用的名字 height 的聲明時,編譯器首先在該函數的局部作用域中查找。函數的局部作用域中聲明瞭一個函數形參。dummy_fcn的函數體中使用的名字 height 指的就是這個形參聲明。

在本例中,height 形參屏蔽名爲 height 的成員。儘管類的成員被屏蔽了,但仍然可以通過用類名來限定成員名或顯式使用 this 指針來使用它。
如果我們想覆蓋常規的查找規則,應該這樣做:

// bad practice: Names local to member functions shouldn't hide member names
void dummy_fcn(index height) {
cursor = width * this->height; // member height
// alternative way to indicate the member   這兩句效果一樣的
cursor = width * Screen::height; // member height
}

函數作用域之後,在類作用域中查找

如果想要使用 height 成員,更好的方式也許是爲形參取一個不同的名字:

// good practice: Don't use member name for a parameter or other local
variable
void dummy_fcn(index ht) {
cursor = width * height; // member height
}

現在當編譯器查找名字 height 時,它將不會在函數內查找該名字。編譯器接着會在 Screen 類中查找。因爲 height 是在成員函數內部使用,所以編譯器在所有成員聲明中查找。儘管 height 是先在 dummy_fcn 中使用,然後再聲明,編譯器還是確定這裏用的是名爲 height 的數據成員。

類作用域之後,在外圍作用域中查找

如果編譯器不能在函數或類作用域中找到,就在外圍作用域中查找。在本例子中,出現在 Screen 定義之前的全局作用域中聲明瞭一個名爲 height 的全局聲明。然而,該對象被屏蔽了。
儘管全局對象被屏蔽了(被形參屏蔽了),但通過用全局作用域確定操作符來限定名字,仍然可以使用它。

// bad practice: Don't hide names that are needed from surrounding scopes
void dummy_fcn(index height) {
cursor = width * ::height;// which height? The global one 全局作用域確定操作符
}

在文件中名字的出現處確定名字

當成員定義在類定義的外部時,名字查找的第 3 步不僅要考慮在 Screen類定義之前的全局作用域中的聲明,而且要考慮在成員函數定義之前出現的全局作用域聲明。例如:

class Screen {
public:
// ...
	void setHeight(index);
private:
	index height;
};
Screen::index verify(Screen::index);  //全局函數 verify
void Screen::setHeight(index var) {
// var: refers to the parameter
// height: refers to the class member
// verify: refers to the global function
	height = verify(var);
}

注意,全局函數 verify 的聲明在 Screen 類定義之前是不可見的。然而,名字查找的第 3 步要考慮那些出現在成員定義之前的外圍作用域聲明,並找到全局函數 verify 的聲明

構造函數

構造函數是特殊的成員函數,只要創建類類型的新對象,都要執行構造函數。
構造函數的工作保證每個對象的數據成員具有合適的初始值。展示瞭如何定義構造函數:

class Sales_item {
public:
// operations on Sales_itemobjects
// default constructor needed to initialize members of built-in type
Sales_item(): units_sold(0), revenue(0.0) { } //構造函數初始化列
private:
std::string isbn;   //isbn 成員由 string 的`默認構造函數` `隱式初始化`爲空串。
unsigned units_sold;
double revenue;
};

這個構造函數使用構造函數初始化列表來初始化 units_sold 和 revenue成員。isbn 成員由 string 的默認構造函數 隱式初始化爲空串。
構造函數的名字與類的名字相同,並且不能指定返回類型。像其他任何函數一樣,它們可以沒有形參,也可以定義多個形參。

構造函數可以被重載

可以爲一個類聲明的構造函數的數量沒有限制,只要每個構造函數的形參是唯一的。我們如何才能知道應該定義哪個或多少個構造函數?一般而言,不同的構造函數允許用戶指定不同的方式來初始化數據成員。
例如,邏輯上可以通過提供兩個額外的構造函數來擴展 Sales_item 類:一個允許用戶提供 isbn 的初始值,另一個允許用戶通過讀取 istream 對象來初化對象:

class Sales_item;
// other members as before
public:
// added constructors to initialize from a string or an istream
Sales_item(const std::string&);
Sales_item(std::istream&);
Sales_item();
};

實參決定使用哪個構造函數
我們的類現在定義了三個構造函數。在定義新對象時,可以使用這些構造函數中的任意一個:

// uses the default constructor:
// isbn is the empty string; units_soldand revenue are 0
Sales_item empty;
// specifies an explicit isbn; units_soldand revenue are 0
Sales_item Primer_3rd_Ed("0-201-82470-1");
// reads values from the standard input into isbn, units_sold, and
revenue
Sales_item Primer_4th_ed(cin);

用於初始化一個對象的實參類型決定使用哪個構造函數。在 empty 的定義中,沒有初始化式,所以運行默認構造函數。接受一個 string 實參的構造函數用於初始化 Primer_3rd_ed;接受一個 istream 引用的構造函數初始化Primer_4th_ed。

構造函數自動執行

只要創建該類型的一個對象,編譯器就運行一個構造函數:

// constructor that takes a string used to create and initialize variable
Sales_item Primer_2nd_ed("0-201-54848-8");
// default constructor used to initialize unnamed object on the heap 
Sales_item *p = new Sales_item();

第一種情況下,運行接受一個 string 實參的構造函數,來初始化變量Primer_2nd_ed。第二種情況下,動態分配一個新的 Sales_item 對象。假定分配成功,則通過運行默認構造函數初始化該對象。

用於 const 對象的構造函數

構造函數不能聲明爲 const

class Sales_item {
public:
Sales_item() const; // error  出錯
};

const 構造函數是不必要的。創建類類型的 const 對象時,運行一個普通構造函數來初始化該 const 對象。構造函數的工作是初始化對象。不管對象是否爲 const,都用一個構造函數來初始化化該對象。

構造函數初始化式

與任何其他函數一樣,構造函數具有名字、形參表和函數體。與其他函數不同的是,構造函數也可以包含一個構造函數初始化列表(constructor initializer list)

// recommended way to write constructors using a constructor initializer
Sales_item::Sales_item(const string &book):
isbn(book), units_sold(0), revenue(0.0) { }

構造函數初始化列表(constructor initializer list)以一個冒號開始,接着是一個以逗號分隔的數據成員列表,每個數據成員後面跟一個放在圓括號中的初始化式。構造函數初始化列表負責構造函數中顯式初始化的工作
這個構造函數將 isbn成員初始化爲 book 形參的值,將 units_sold 和 revenue 初始化爲 0。與任意的成員函數一樣,構造函數可以定義在類的內部或外部。構造函數初始化只在構造函數的定義中而不是聲明中指定。
構造函數初始化列表難以理解的一個原因在於,省略初始化列表在構造函數的函數體內對數據成員賦值是合法的。例如,可以將接受一個 string 的Sales_item 構造函數編寫爲:

// legal but sloppier way to write the constructor:
// no constructor initializer
Sales_item::Sales_item(const string &book)  //這裏沒有構造函數初始化列表,變量使用的是隱式初始化
{
isbn = book;
units_sold = 0;
revenue = 0.0;
}

這個構造函數給類 Sales_item 的成員賦值,但沒有進行顯式初始化。不管是否有顯式的初始化式,在執行構造函數之前,要初始化 isbn 成員。這個構造函數隱式使用默認的 string 構造函數來初始化 isbn。執行構造函數的函數體時,isbn 成員已經有值了。該值被構造函數函數體中的賦值所覆蓋。
從概念上講,可以認爲構造函數分兩個階段執行
(1)初始化階段;(2)普通的計算階段。計算階段由構造函數函數體中的所有語句組成。
不管成員是否在構造函數初始化列表中顯式初始化,類類型的數據成員總是在初始化階段初始化。初始化發生在計算階段開始之前。
在構造函數初始化列表中沒有顯式提及的每個成員,使用與初始化變量相同的規則來進行初始化。運行該類型的默認構造函數,來初始化類類型的數據成員。
內置或複合類型的成員的初始值依賴於對象的作用域:在局部作用域中這些成員不被初始化,而在全局作用域中它們被初始化爲 0。
在本節中編寫的兩個 Sales_item 構造函數版本具有同樣的效果:無論是在構造函數初始化列表中初始化成員,還是在構造函數函數體中對它們賦值,最終結果是相同的。構造函數執行結束後,三個數據成員保存同樣的值。
不同之外在於,使用構造函數初始化列表的版本初始化數據成員,沒有定義初始化列表的構造函數版本在構造函數函數體中對數據成員賦值。這個區別的重要性取決於數據成員的類型。

有時需要構造函數初始化列表

如果沒有爲類成員提供初始化式,則編譯器會隱式地使用成員類型的默認構造函數。如果那個類沒有默認構造函數,則編譯器嘗試使用默認構造函數將會失敗。在這種情況下,爲了初始化數據成員,必須提供初始化式。
有些成員必須在構造函數初始化列表中進行初始化。對於這樣的成員,在構造函數函數體中對它們賦值不起作用。沒有默認構造函數的類類型的成員,以及 const 或引用類型的成員,不管是哪種類型,都必須在構造函數初始化列表中進行初始化
因爲內置類型的成員不進行隱式初始化,所以對這些成員是進行初始化還是賦值似乎都無關緊要。除了兩個例外,對非類類型的數據成員進行賦值或者使用初始化式在結果和性能上都是等價的。
例如,下面的構造函數是錯誤的:

class ConstRef {
public:
ConstRef(int ii);
private:
	int i;
	const int ci;
	int &ri; 
};
// no explicit constructor initializer: error ri is uninitialized
ConstRef::ConstRef(int ii)
{ // assignments:
	i = ii; // ok
	ci = ii; // error: cannot assign to a const  
	//const 成員必須在構造函數初始化列表中進行初始化
	ri = i; // assigns to ri which was not bound to an object   //引用類型的成員必須在構造函數初始化列表中進行初始化
}

記住,可以初始化 const 對象或引用類型的對象,但不能對它們賦值。在開始執行構造函數的函數體之前,要完成初始化。初始化 const 或引用類型數據成員的唯一機會是構造函數初始化列表中。編寫該構造函數的正確方式爲

// ok: explicitly initialize reference and const members
ConstRef::ConstRef(int ii): i(ii), ci(i), ri(ii) { }

必須對任何 const 或引用類型成員以及沒有默認構造函數的類類型的任何成員使用初始化式。

成員初始化的次序

每個成員在構造函數初始化列表中只能指定一次,這不會令人驚訝。畢竟,給一個成員兩個初始值意味着什麼?也許更令人驚訝的是,構造函數初始化列表僅指定用於初始化成員的值,並不指定這些初始化執行的次序。成員被初始化的次序就是定義成員的次序。第一個成員首先被初始化,然後是第二個,依次類推。
初始化的次序常常無關緊要。然而,如果一個成員是根據其他成員而初始化,則成員初始化的次序是至關重要的。
考慮下面的類:

class X {
int i;
int j;
public:
// run-time error: i is initialized before j
	X(int val): j(val), i(j) { }  //i根據j初始化的
};

在這種情況下,構造函數初始化列表看起來似乎是用val 初始化 j,然後再用 j 來初始化 i。然而,i 首先被初始化。這個初始化列表的效果是用尚未初始化的 j 值來初始化 i!
如果數據成員在構造函數初始化列表中的列出次序與成員被聲明的次序不同,那麼有的編譯器非常友好,會給出一個警告。
如何解決
按照與成員聲明一致的次序編寫構造函數初始化列表是個好主意。此外,儘可能避免使用成員來初始化其他成員。
一般情況下,通過(重複)使用構造函數的形參而不是使用對象的數據成員,可以避免由初始化式的執行次序而引起的任何問題。
例如,下面這樣爲 X 編寫構造函數可能更好:

X(int val): i(val), j(val) { }

在這個版本中,i 和 j 初始化的次序就是無關緊要的。

初始化式可以是任意表達式

一個初始化式可以是任意複雜的表達式。例如,可以給 Sales_item 類一個新的構造函數,該構造函數接受一個 string 表示 isbn,一個 usigned 表示售出書的數目,一個 double 表示每本書的售出價格:

Sales_item(const std::string &book, int cnt, double price):
isbn(book), units_sold(cnt), revenue(cnt * price) { }   //revenue(cnt * price)使用了初始化式

revenue 的初始化式使用表示價格和售出數目的形參來計算對象的 revenue 成員

類類型的數據成員的初始化式

初始化類類型的成員時,要指定實參並傳遞給成員類型的一個構造函數。可以使用該類型的任意構造函數。例如,Sales_item 類可以使用任意一個 string構造函數來初始化 isbn。也可以用 ISBN 取值的極限值來表示isbn 的默認值,而不是用空字符串。可以將 isbn 初始化爲由 10 個 9 構成的串:

// alternative definition for Sales_item default constructor
Sales_item(): isbn(10, '9'), units_sold(0), revenue(0.0) {}

這個初始化式使用 string 構造函數,接受一個計數值和一個字符,並生成一個 string,來保存重複指定次數的字符。

默認實參與構造函數

再來看看默認構造函數和接受一個 string 的構造函數的定義:

Sales_item(const std::string &book):isbn(book), units_sold(0), revenue(0.0) { }
Sales_item(): units_sold(0), revenue(0.0) { }

這兩個構造函數幾乎是相同的:唯一的區別在於,接受一個 string 形參的構造函數使用該形參來初始化 isbn。默認構造函數(隱式地)使用 string 的默認構造函數來初始化 isbn。
可以通過爲 string 初始化式提供一個默認實參將這些構造函數組合起來:

class Sales_item {
public:
// default argument for book is the empty string
Sales_item(const std::string &book = ""):isbn(book), units_sold(0), revenue(0.0) { }
Sales_item(std::istream &is);
// as before
};

在這裏,我們只定義了兩個構造函數,其中一個爲其形參提供一個默認實參。
對於下面的任一定義,將執行爲其 string 形參接受默認實參的那個構造函數:

Sales_item empty;
Sales_item Primer_3rd_Ed("0-201-82470-1");

在 empty 的情況下,使用默認實參,而 Primer_3rd_ed 提供了一個顯式實參。
類的兩個版本提供同一接口:給定一個 string 或不給定初始化式,它們都將一個 Sales_item 初始化爲相同的值。我們更喜歡使用默認實參,因爲它減少代碼重複。

默認構造函數

只要定義一個對象時沒有提供初始化式,就使用默認構造函數所有形參提供默認實參的構造函數也定義了默認構造函數。

合成的默認構造函數

一個類哪怕只定義了一個構造函數,編譯器也不會再生成默認構造函數。這條規則的根據是,如果一個類在某種情況下需要控制對象初始化,則該類很可能在所有情況下都需要控制。

只有當一個類沒有定義構造函數時,編譯器纔會自動生成一個默認構造函數。

合成的默認構造函數(synthesized default constructor)使用與變量初始化相同的規則來初始化成員。具有類類型的成員通過運行各自的默認構造函數來進行初始化。內置和複合類型的成員,如指針和數組,只對定義在全局作用域中的對象才初始化。當對象定義在局部作用域中時,內置或複合類型的成員不進行初始化。

如果類包含內置或複合類型的成員,則該類不應該依賴於合成的默認構造函數。它應該定義自己的構造函數來初始化這些成員。

此外,每個構造函數應該爲每個內置或複合類型的成員提供初始化式。沒有初始化內置或複合類型成員的構造函數,將使那些成員處於未定義的狀態。除了作爲賦值的目標之外,以任何方式使用一個未定義的成員都是錯誤的。如果每個構造函數將每個成員設置爲明確的已知狀態,則成員函數可以區分空對象具有實際值的對象
//內置類型,類類型,符合類型的構造函數與初始化的關係是啥?

使用默認構造函數

初級 C++ 程序員常犯的一個錯誤是,採用以下方式聲明一個用默認構造函數初始化的對象:

// oops! declares a function, not an object
Sales_item myobj();

編譯 myobj 的聲明沒有問題。然而,當我們試圖使用 myobj 時

Sales_item myobj(); // ok: but defines a function, not an object
if (myobj.same_isbn(Primer_3rd_ed)) // error: myobj is a function

編譯器會指出不能將成員訪問符號用於一個函數!問題在於 myobj 的定義被編譯器解釋爲一個函數的聲明,該函數不接受參數並返回一個 Sales_item 類型的對象——與我們的意圖大相徑庭!使用默認構造函數定義一個對象的正確方式是去掉最後的空括號

// ok: defines a class object ...
Sales_item myobj;

另一方面,下面這段代碼也是正確的:

// ok: create an unnamed, empty Sales_itemand use to initialize myobj
Sales_item myobj = Sales_item();

在這裏,我們創建並初始化一個 Sales_item 對象,然後用它來按值初始化myobj。編譯器通過運行 Sales_item 的默認構造函數來按值初始化一個Sales_item。

隱式類類型轉換

C++ 語言定義了內置類型之間的幾個自動轉換。也可以定義如何將其他類型的對象隱式轉換爲我們的類類型,或將我們的類類型的對象隱式轉換爲其他類型。爲了定義到類類型的隱式轉換,需要定義合適的構造函數。

可以用單個實參來調用的構造函數定義了從形參類型到該類類型的一個隱式轉換。

conversion constructor(轉換構造函數):可用單個實參調用的非 explicit 構造函數。隱式使用轉換構造函數將實參的類型轉換爲類類型。
讓我們再看看定義了兩個構造函數的 Sales_item 版本:

class Sales_item {
public:
// default argument for book is the empty string
Sales_item(const std::string &book = ""):
isbn(book), units_sold(0), revenue(0.0) { }
Sales_item(std::istream &is);
// as before
};

這裏的每個構造函數都定義了一個隱式轉換。因此,在期待一個 Sales_item類型對象的地方,可以使用一個 string 或一個 istream:

string null_book = "9-999-99999-9";
// ok: builds a Sales_itemwith 0 units_soldand revenue from
// and isbn equal to null_book
item.same_isbn(null_book); //該函數期待一個 Sales_item 對象作爲實參
//這段程序使用一個 string 類型對象作爲實參傳給 Sales_item 的same_isbn 函數。

編譯器使用接受一個 string 的 Sales_item 構造函數從 null_book 生成一個新的Sales_item 對象。新生成的(臨時的)Sales_item 被傳遞給 same_isbn。
這個行爲是否我們想要的,依賴於我們認爲用戶將如何使用這個轉換。在這種情況下,它可能是一個好主意。book 中的 string 可能代表一個不存在的ISBN,對 same_isbn 的調用可以檢測 item 中的 Sales_item 是否表示一個空的 Sales_item。另一方面,用戶也許在 null_book 上錯誤地調用了 same_isbn。
更成問題的是從 istream 到 Sales_item 的轉換:

// ok: uses the Sales_item istream constructor to build an object
// to pass to same_isbn
item.same_isbn(cin);

這段代碼將 cin 隱式轉換爲 Sales_item。這個轉換執行接受一個 istream的 Sales_item 構造函數。該構造函數通過讀標準輸入來創建一個(臨時的)Sales_item 對象。然後該對象被傳遞給 same_isbn。
這個 Sales_item 對象是一個臨時對象。一旦 same_isbn 結束,就不能再訪問它。實際上,我們構造了一個在測試完成後被丟棄的對象。這個行爲幾乎肯定是一個錯誤。

抑制由構造函數定義的隱式轉換

可以通過將構造函數聲明爲 explicit,來防止在需要隱式轉換的上下文中使用構造函數:

class Sales_item {
public:
// default argument for book is the empty string
explicit Sales_item(const std::string &book = ""):isbn(book), units_sold(0), revenue(0.0) { }
explicit Sales_item(std::istream &is);
// as before
};

explicit 關鍵字只能用於類內部的構造函數聲明上。在類的定義體外部所做的定義上不再重複它:

// error: explicit allowed only on constructor declaration in class header
explicit Sales_item::Sales_item(istream& is)
{
	is >> *this; // uses Sales_iteminput operator to read the members
}

現在,兩個構造函數都不能用於隱式地創建對象。前兩個使用都不能編譯:

item.same_isbn(null_book); // error: string constructor is explicit
item.same_isbn(cin); // error: istream constructor is explicit

當構造函數被聲明 explicit 時,編譯器將不使用它作爲轉換操作符

爲轉換而顯式地使用構造函數

只要顯式地按下面這樣做,就可以用顯式的構造函數來生成轉換:

string null_book = "9-999-99999-9";
// ok: builds a Sales_itemwith 0 units_soldand revenue from
// and isbn equal to null_book
item.same_isbn(Sales_item(null_book)); //顯式的構造函數來生成轉換

在這段代碼中,從 null_book 創建一個 Sales_item。儘管構造函數爲顯式的,但這個用法是允許的。顯式使用構造函數只是中止了隱式地使用構造函數。
任何構造函數都可以用來顯式地創建臨時對象
通常,除非有明顯的理由想要定義隱式轉換,否則,單形參構造函數應該爲 explicit。將構造函數設置爲explicit 可以避免錯誤,並且當轉換有用時,用戶可以顯式地構造對象。

類成員的顯式初始化

儘管大多數對象可以通過運行適當的構造函數進行初始化,但是直接初始化簡單的非抽象類的數據成員仍是可能的。對於沒有定義構造函數並且其全體數據成員均爲 public 的類,可以採用與初始化數組元素相同的方式初始化其成員

struct Data {
int ival;
char *ptr;
};
// val1.ival = 0; val1.ptr = 0
Data val1 = { 0, 0 };
// val2.ival = 1024;
// val2.ptr = "Anna Livia Plurabelle"
Data val2 = { 1024, "Anna Livia Plurabelle" };

根據數據成員的聲明次序來使用初始化式。例如,因爲 ival 在 ptr 之前聲明,所以下面的用法是錯誤的:

// error: can't use "Anna Livia Plurabelle" to initialize the int ival
Data val2 = { "Anna Livia Plurabelle" , 1024 };

這種形式的初始化從 C 繼承而來,支持與 C 程序兼容。顯式初始化類類型對象的成員有三個重大的缺點:

  1. 要求類的全體數據成員都是 public。
  2. 將初始化每個對象的每個成員的負擔放在程序員身上。這樣的初始化是乏味且易於出錯的,因爲容易遺忘初始化式或提供不適當的初始化式。
  3. 如果增加或刪除一個成員,必須找到所有的初始化並正確更新。

定義和使用構造函數幾乎總是較好的。當我們爲自己定義的類型提供一個默認構造函數時,允許編譯器自動運行那個構造函數,以保證每個類對象在初次使用之前正確地初始化。

參考資料

【1】C++ Primer 中文版(第四版·特別版)

註解

本文許可證

本文遵循 CC BY-NC-SA 4.0(署名 - 非商業性使用 - 相同方式共享) 協議,轉載請註明出處,不得用於商業目的。
CC BY-NC-SA 4.0

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章