C++ 程式設計/類/成員函式
成員函式可以(也應該)用於與使用者定義型別中包含的資料進行互動。使用者定義型別在程式編寫中的"分治"方案中提供了靈活性。換句話說,一個程式設計師可以編寫一個使用者定義型別並保證一個介面。另一個程式設計師可以使用那個預期的介面編寫主程式。這兩部分被組合在一起並編譯以供使用。使用者定義型別提供了在面向物件程式設計 (OOP) 正規化中定義的封裝。
在類中,為了保護資料成員,程式設計師可以定義函式來對這些資料成員執行操作。成員函式和函式是用來指代類的同義詞。函式原型在類定義中宣告。這些原型可以採用非類函式的形式,也可以採用適合類的原型。函式可以在類定義中宣告和定義。但是,大多數函式可能具有非常大的定義,使得類難以閱讀。因此,可以使用範圍解析運算子"::"在類定義之外定義函式。這個範圍解析運算子允許程式設計師在其他地方定義函式。這可以允許程式設計師提供一個包含類定義的標頭檔案.h,以及從包含函式定義的編譯後的.cpp 檔案生成的.obj 檔案。這可以隱藏實現並防止篡改。使用者必須重新定義每個函式才能更改實現。類中的函式可以訪問和修改(除非函式是常量)資料成員,而無需宣告它們,因為資料成員已經在類中宣告。
簡單示例
檔案: Foo.h
// the header file named the same as the class helps locate classes within a project
// one class per header file makes it easier to keep the
// header file readable (some classes can become large)
// each programmer should determine what style works for them or what programming standards their
// teacher/professor/employer has
#ifndef FOO_H
#define FOO_H
class Foo{
public:
Foo(); // function called the default constructor
Foo( int a, int b ); // function called the overloaded constructor
int Manipulate( int g, int h );
private:
int x;
int y;
};
#endif
檔案: Foo.cpp
#include "Foo.h"
/* these constructors should really show use of initialization lists
Foo::Foo() : x(5), y(10)
{
}
Foo::Foo(int a, int b) : x(a), y(b)
{
}
*/
Foo::Foo(){
x = 5;
y = 10;
}
Foo::Foo( int a, int b ){
x = a;
y = b;
}
int Foo::Manipulate( int g, int h ){
x = h + g*x;
y = g + h*y;
}
成員函式可以被過載。這意味著同一個作用域中可以存在多個同名成員函式,但它們的簽名必須不同。成員函式的簽名由成員函式的名稱以及成員函式的引數型別和順序組成。
由於名稱隱藏,如果派生類中的成員與基類中的成員同名,它們將對編譯器隱藏。要使這些成員可見,可以使用宣告從基類作用域引入它們。
建構函式和其他類成員函式(除了解構函式)可以被過載。
建構函式是一個特殊的成員函式,每當建立一個類的新的例項時就會呼叫它。編譯器在新的物件在記憶體中分配後呼叫建構函式,並將那個“原始”記憶體轉換為適當的型別化物件。建構函式的宣告方式與普通成員函式非常相似,但它將與類同名,並且沒有返回值。
建構函式負責幾乎所有類操作所需的執行時設定。它的一般目的通常是在物件例項化(宣告物件時)定義資料成員,它們也可以有引數,如果程式設計師願意的話。如果建構函式有引數,那麼在使用new 運算子時,也應該將它們新增到該類任何其他物件的宣告中。建構函式也可以被過載。
Foo myTest; // essentially what happens is: Foo myTest = Foo();
Foo myTest( 3, 54 ); // accessing the overloaded constructor
Foo myTest = Foo( 20, 45 ); // although a new object is created, there are some extra function calls involved
// with more complex classes, an assignment operator should
// be defined to ensure a proper copy (includes ''deep copy'')
// myTest would be constructed with the default constructor, and then the
// assignment operator copies the unnamed Foo( 20, 45 ) object to myTest
使用new 和建構函式
Foo* myTest = new Foo(); // this defines a pointer to a dynamically allocated object
Foo* myTest = new Foo( 40, 34 ); // constructed with Foo( 40, 34 )
// be sure to use delete to avoid memory leaks
建構函式可以委託給另一個(在 C++ 11 中引入)。還建議減少使用預設引數,如果維護者必須編寫和維護多個建構函式,可能會導致程式碼重複,從而降低可維護性,因為可能會引入不一致,甚至導致程式碼膨脹。
- 預設建構函式
預設建構函式是可以不帶引數呼叫的建構函式。最常見的是,預設建構函式不帶任何引數宣告,但如果所有引數都賦予了預設值,則帶引數的建構函式也可以是預設建構函式。
為了建立一個類型別的物件陣列,該類必須有一個可訪問的預設建構函式;C++ 沒有語法來指定陣列元素的建構函式引數。
當例項化一個類的物件時,類編寫者可以提供各種建構函式,每個建構函式都有不同的用途。一個大型類將擁有許多資料成員,其中一些可能在物件例項化時被定義,也可能不被定義。無論如何,每個專案都會有所不同,因此程式設計師應該在提供建構函式時調查各種可能性。
這些都是類 myFoo 的建構函式。
myFoo(); // default constructor, the user has no control over initial values
// overloaded constructors
myFoo( int a, int b=0 ); // allows construction with a certain 'a' value, but accepts 'b' as 0
// or allows the user to provide both 'a' and 'b' values
// or
myFoo( int a, int b ); // overloaded constructor, the user must specify both values
class myFoo {
private:
int Useful1;
int Useful2;
public:
myFoo(){ // default constructor
Useful1 = 5;
Useful2 = 10;
};
myFoo( int a, int b = 0 ) { // two possible cases when invoked
Useful1 = a;
Useful2 = b;
};
};
myFoo Find; // default constructor, private member values Useful1 = 5, Useful2 = 10
myFoo Find( 8 ); // overloaded constructor case 1, private member values Useful1 = 8, Useful2 = 0
myFoo Find( 8, 256 ); // overloaded constructor case 2, private member values Useful1 = 8, Useful2 = 256
建構函式初始化列表(或成員初始化列表)是使用非預設建構函式初始化資料成員和基類的唯一方法。成員的建構函式包含在引數列表和建構函式主體之間(用冒號與引數列表隔開)。使用初始化列表不僅效率更高,而且是保證在進入建構函式主體之前完成所有資料成員初始化的最簡單方法。
// Using the initialization list for myComplexMember_
MyClass::MyClass(int mySimpleMember, MyComplexClass myComplexMember)
: myComplexMember_(myComplexMember) // only 1 call, to the copy constructor
{
mySimpleMember_=mySimpleMember; // uses 2 calls, one for the constructor of the mySimpleMember class
// and a second for the assignment operator of the MyComplexClass class
}
這比在建構函式主體內部將值賦給複雜資料成員效率更高,因為在這種情況下,變數將使用其相應的建構函式初始化。
注意,提供給成員建構函式的引數不需要是類建構函式的引數;它們也可以是常量。因此,可以為包含沒有預設建構函式的成員的類建立一個預設建構函式。
示例
MyClass::MyClass() : myComplexMember_(0) { }
在建構函式中使用此初始化列表來初始化成員很有用。這使讀者清楚地知道建構函式沒有執行邏輯。初始化的順序應該與定義基類和成員的順序相同。否則,您可能會在編譯時收到警告。一旦開始初始化成員,請確保將所有成員都儲存在建構函式中,以避免混淆和可能的 0xbaadfood。
使用與成員名稱相同的建構函式引數是安全的。
示例
class MyClass : public MyBaseClassA, public MyBaseClassB {
private:
int c;
void *pointerMember;
public:
MyClass(int,int,int);
};
/*...*/
MyClass::MyClass(int a, int b, int c):
MyBaseClassA(a)
,MyBaseClassB(b)
,c(c)
,pointerMember(NULL)
,referenceMember()
{
//logic
}
注意,此技術也適用於普通函式,但現在已過時,在此情況下被歸類為錯誤。
解構函式
[edit | edit source]解構函式與建構函式類似,宣告方式與普通成員函式相同,但名稱與類名相同,以“~”開頭作為區分,不能接收引數,也不能過載。
解構函式在類物件銷燬時被呼叫。解構函式對於避免資源洩漏(透過釋放記憶體)和實現 RAII 慣用法至關重要。在類建構函式中分配的資源通常在該類的解構函式中釋放,以便在類不再存在後將系統恢復到已知或穩定的狀態。
解構函式在物件銷燬時被呼叫,例如在宣告它們的函式返回後、使用 **delete** 運算子時或程式結束時。如果派生型別物件被銷燬,首先執行最派生物件的解構函式,然後成員物件和基類物件以它們對應的建構函式完成的相反順序遞迴銷燬。與結構體一樣,如果類沒有使用者宣告的解構函式,編譯器會隱式地將其宣告為類的內聯公共成員。
物件的動態型別將從最派生型別開始,隨著解構函式的執行而改變,與建構函式執行時的變化方式對稱。這會影響構造和銷燬過程中虛擬呼叫所呼叫的函式,並導致常見的(且合理的)建議,避免在建構函式或解構函式中直接或間接呼叫物件的虛擬函式。
在成員函式方面,我們之前在行內函數介紹中看到的概念得到了擴充套件,但也有一些額外的考慮因素。
如果成員函式定義包含在類的宣告中,該函式預設情況下會隱式地被標記為內聯。編譯器選項可能會覆蓋此行為。
如果物件的型別在編譯時未知,則無法內聯對虛擬函式的呼叫,因為我們不知道要內聯哪個函式。
**static** 關鍵字可以用四種不同的方式使用
靜態成員函式
[edit | edit source]宣告為靜態的成員函式或變數在物件型別的所有例項之間共享。這意味著對於任何物件型別,成員函式或變數都只有一個副本。
- 無需物件即可呼叫的成員函式
當在類函式成員中使用時,該函式不會將例項作為隱式this引數,而是像自由函式一樣工作。這意味著可以在不建立類例項的情況下呼叫靜態類函式。
class Foo {
public:
Foo() {
++numFoos;
cout << "We have now created " << numFoos << " instances of the Foo class\n";
}
static int getNumFoos() {
return numFoos;
}
private:
static int numFoos;
};
int Foo::numFoos = 0; // allocate memory for numFoos, and initialize it
int main() {
Foo f1;
Foo f2;
Foo f3;
cout << "So far, we've made " << Foo::getNumFoos() << " instances of the Foo class\n";
}
命名建構函式
[edit | edit source]命名建構函式是使用靜態成員函式的一個很好的例子。命名建構函式是指用於建立類物件而無需(直接)使用其建構函式的函式。這可能用於以下目的:
- 繞過建構函式只能在簽名不同時才可過載的限制。
- 透過將建構函式設為私有,使類不可繼承。
- 透過將建構函式設為私有,阻止棧分配。
宣告一個使用私有建構函式建立物件並返回物件的靜態成員函式。(它也可以返回指標或引用,但這似乎沒有用,並將此變成工廠模式,而不是傳統的命名建構函式。)
以下是一個用於儲存可以在不同溫度刻度中指定的溫度的類的示例。
class Temperature
{
public:
static Temperature Fahrenheit (double f);
static Temperature Celsius (double c);
static Temperature Kelvin (double k);
private:
Temperature (double temp);
double _temp;
};
Temperature::Temperature (double temp):_temp (temp) {}
Temperature Temperature::Fahrenheit (double f)
{
return Temperature ((f + 459.67) / 1.8);
}
Temperature Temperature::Celsius (double c)
{
return Temperature (c + 273.15);
}
Temperature Temperature::Kelvin (double k)
{
return Temperature (k);
}
const
[edit | edit source]這種型別的成員函式不能修改類的成員變數。它同時向程式設計師和編譯器暗示給定的成員函式不會更改類的內部狀態;但是,任何宣告為mutable的變數仍然可以修改。
例如
class Foo
{
public:
int value() const
{
return m_value;
}
void setValue( int i )
{
m_value = i;
}
private:
int m_value;
};
這裡value()顯然不會更改 m_value,因此可以且應該為 const。但是setValue()確實修改了 m_value,因此不能為 const。
另一個經常被忽略的細節是const成員函式不能呼叫非 const 成員函式(如果你嘗試這樣做,編譯器會報錯)。由於const成員函式不能更改成員變數,而非 const 成員函式可以更改成員變數,因此,我們假設非 const 成員函式確實會更改成員變數,因此const成員函式被認為永遠不會更改成員變數,因此不能呼叫更改成員變數的函式。
以下程式碼示例解釋了const可以在不同位置使用它所產生的影響。
class Foo
{
public:
/*
* Modifies m_widget and the user
* may modify the returned widget.
*/
Widget *widget();
/*
* Does not modify m_widget but the
* user may modify the returned widget.
*/
Widget *widget() const;
/*
* Modifies m_widget, but the user
* may not modify the returned widget.
*/
const Widget *cWidget();
/*
* Does not modify m_widget and the user
* may not modify the returned widget.
*/
const Widget *cWidget() const;
private:
Widget *m_widget;
};
訪問器和修飾符(Setter/Getter)
[edit | edit source]- 什麼是訪問器?
- 訪問器是不修改物件狀態的成員函式。訪問器函式應該宣告為const.
- **Getter** 是訪問器的另一個常見定義,因為這類成員函式的命名方式為(GetSize())。
- 什麼是修飾符?
- 修飾符(也稱為修改函式)是指更改至少一個數據成員值的成員函式。換句話說,修改物件狀態的操作。修飾符也被稱為“mutators”。
- **Setter** 是修飾符的另一個常見定義,因為其命名方式為(SetSize( int a_Size ))。
動態多型性(覆蓋)
[edit | edit source]到目前為止,我們已經瞭解到可以透過繼承向類新增新的資料和函式。但是,如果我們想讓派生類繼承基類的方法,但希望它有不同的實現呢?這就是我們所說的多型性,它是 OOP 程式設計中的一個基本概念。
正如我們在程式設計正規化部分中所見,多型性分為兩個概念:靜態多型性和動態多型性。本部分重點介紹動態多型性,它在 C++ 中應用於派生類覆蓋基類中宣告的函式時。
我們透過在派生類中重新定義方法來實現此概念。但是,在這樣做時,我們需要考慮一些因素,因此現在必須介紹動態繫結、靜態繫結和虛擬函式的概念。
假設我們有兩個類:A 和 B。B 派生自 A,並重新定義了類 A 中存在的方法 c() 的實現。現在假設我們有一個類 B 的物件 b。如何解釋指令 b.c()?
如果 b 在堆疊中宣告(不是作為指標或引用宣告),編譯器將應用靜態繫結,這意味著它會(在編譯時)解釋為我們引用的是 B 中存在的方法 c() 的實現。
但是,如果我們以類 A 的指標或引用宣告 b,編譯器在編譯時無法知道要呼叫哪個方法,因為 b 可以是 A 或 B 型別。如果在執行時解析,將呼叫 B 中存在的方法。這稱為動態繫結。如果在編譯時解析,將呼叫 A 中存在的方法。這同樣是靜態繫結。
虛成員函式
[edit | edit source]在virtual成員函式方面,概念相對簡單,但常常被誤解。這個概念是設計類層次結構(涉及子類化類)時不可或缺的一部分,因為它決定了特定上下文中覆蓋方法的行為。
虛成員函式是類成員函式,可以在從宣告它們的那個類派生的任何類中被覆蓋。然後,成員函式體將被派生類中的一組新的實現所取代。
透過在方法宣告之前放置關鍵字virtual,我們表明當編譯器必須決定是應用靜態繫結還是動態繫結時,它將應用動態繫結。否則,將應用靜態繫結。
再次,這應該透過一個例子更清楚地說明。
class Foo
{
public:
void f()
{
std::cout << "Foo::f()" << std::endl;
}
virtual void g()
{
std::cout << "Foo::g()" << std::endl;
}
};
class Bar : public Foo
{
public:
void f()
{
std::cout << "Bar::f()" << std::endl;
}
virtual void g()
{
std::cout << "Bar::g()" << std::endl;
}
};
int main()
{
Foo foo;
Bar bar;
Foo *baz = &bar;
Bar *quux = &bar;
foo.f(); // "Foo::f()"
foo.g(); // "Foo::g()"
bar.f(); // "Bar::f()"
bar.g(); // "Bar::g()"
// So far everything we would expect...
baz->f(); // "Foo::f()"
baz->g(); // "Bar::g()"
quux->f(); // "Bar::f()"
quux->g(); // "Bar::g()"
return 0;
}
我們對兩個物件中f()和g()的第一次呼叫很簡單。然而,當我們的 baz 指標是一個指向 Foo 型別的指標時,事情變得有趣了。
f()不是virtual,因此對f()的呼叫將始終呼叫與指標型別相關聯的實現 - 在這種情況下,來自 Foo 的實現。
虛擬函式呼叫在計算上比普通函式呼叫更昂貴。虛擬函式使用指標間接,呼叫,並且需要比普通成員函式多執行幾個指令。它們還要求包含虛擬函式的任何類/結構的建構函式初始化一個指向其虛擬成員函式的指標表。
所有這些特性都將表明效能和設計之間的權衡。應該避免在沒有現有結構性需求的情況下先發制人地宣告函式為虛擬函式。請記住,只能在執行時解析的虛擬函式不能內聯。
純虛成員函式
[edit | edit source]還有一種有趣的情況。有時我們根本不想提供函式的實現,而是希望要求對我們的類進行子類化的人自行提供實現。這就是“純”虛擬函式的情況。
為了表示純virtual函式而不是實現,我們只需在函式聲明後新增一個“= 0”。
再次 - 一個例子
class Widget
{
public:
virtual void paint() = 0;
};
class Button : public Widget
{
public:
void paint() // is virtual because it is an override
{
// do some stuff to draw a button
}
};
因為paint()是 Widget 類中的一個純virtual函式類,因此我們必須在所有具體子類中提供實現。如果我們不提供,編譯器將在構建時給我們錯誤。
這對於提供介面很有幫助 - 這是我們對基於特定層次結構的所有物件期望的內容,但我們想要忽略實現細節。
- 那麼為什麼這有用呢?
讓我們以我們上面的例子為例,其中我們有一個用於繪畫的純virtual。在很多情況下,我們希望能夠對小部件做一些事情,而不必擔心它是什麼型別的小部件。繪畫就是一個簡單的例子。
假設我們的應用程式中有一些東西在小部件變得活躍時重新繪製它們。它只會使用指向小部件的指標,即Widget *activeWidget() const可能是一個可能的函式簽名。因此我們可能會做一些類似的事情
Widget *w = window->activeWidget();
w->paint();
我們希望實際上呼叫“真實”小部件型別相應的 paint 成員函式 - 而不是Widget::paint()(這是一個“純”virtual,如果使用虛擬排程呼叫它會導致程式崩潰)。透過使用virtual函式,我們確保呼叫子類(在本例中為Button::paint())的成員函式實現。
協變返回型別
[edit | edit source]協變返回型別是指派生類中的虛擬函式能夠返回指向自身例項的指標或引用,前提是基類中的方法版本也這樣做。例如
class base
{
public:
virtual base* create() const;
};
class derived : public base
{
public:
virtual derived* create() const;
};
這允許避免強制轉換。
虛擬建構函式
[edit | edit source]有一個以基類Foo為基礎的類層次結構。給定一個屬於層次結構的物件bar,希望能夠執行以下操作
- 建立一個與baz相同類別的物件bar(例如,類Bar),使用該類的預設建構函式進行初始化。通常使用的語法是
- Bar* baz = bar.create();
- 建立一個與baz相同類別的物件bar它是bar的副本。通常使用的語法是
- Bar* baz = bar.clone();
在類Foo中,方法Foo::create()和Foo::clone()宣告如下
class Foo
{
// ...
public:
// Virtual default constructor
virtual Foo* create() const;
// Virtual copy constructor
virtual Foo* clone() const;
};
如果Foo用作抽象類,這些函式可以是純虛擬的
class Foo
{
// ...
public:
virtual Foo* create() const = 0;
virtual Foo* clone() const = 0;
};
為了支援建立預設初始化的物件和建立複製物件,每個類Bar在層次結構中必須具有公共預設建構函式和複製建構函式。的虛擬建構函式定義如下Bar
class Bar : ... // Bar is a descendant of Foo
{
// ...
public:
// Non-virtual default constructor
Bar ();
// Non-virtual copy constructor
Bar (const Bar&);
// Virtual default constructor, inline implementation
Bar* create() const { return new Foo (); }
// Virtual copy constructor, inline implementation
Bar* clone() const { return new Foo (*this); }
};
上面的程式碼使用協變返回型別。如果你的編譯器不支援Bar* Bar::create(),使用Foo* Bar::create(),類似地,對於clone().
使用這些虛擬建構函式時,您必須手動透過呼叫delete baz;來釋放建立的物件。如果使用智慧指標(例如std::unique_ptr<Foo>)作為返回型別而不是普通Foo*.
,可以避免這種麻煩。請記住,無論Foo是否使用動態分配的記憶體,您必須定義解構函式virtual ~Foo ()並使其virtual來處理使用指向祖先型別的指標的物件的釋放。
虛擬解構函式
[edit | edit source]特別重要的是要記住,即使在任何基類中定義一個空虛擬解構函式,因為如果不這樣做,將導致預設的編譯器生成的解構函式不是虛擬的,這將導致問題。
在派生類中重新定義時不會覆蓋虛擬解構函式,每個解構函式的定義是累積的,它們從最後一個派生類開始到第一個基類結束。
純虛擬解構函式
[edit | edit source]每個抽象類都應該包含純虛擬解構函式的宣告。
純虛擬解構函式是純虛擬函式的一種特殊情況(旨在在派生類中被覆蓋)。它們必須始終被定義,並且該定義應始終為空。
class Interface {
public:
virtual ~Interface() = 0; //declaration of a pure virtual destructor
};
Interface::~Interface(){} //pure virtual destructor definition (should always be empty)
三法則
[edit | edit source]“三法則”並不是真正的法則,而是一個指導原則:如果一個類需要顯式宣告的複製建構函式、複製賦值運算子或解構函式,那麼它通常需要這三者。
這條規則有一些例外(或者,換句話說,是細化)。例如,有時顯式宣告解構函式只是為了使其成為virtual;在這種情況下,沒有必要宣告或實現複製建構函式和複製賦值運算子。
大多數類不應該宣告任何“三大”操作;管理資源的類通常需要這三者。
