C++ 類&對象
C++類定義
- 本質上是一個數據類型的藍圖,定義了類的對象包含的信息,以及可以在這個類對象上執行哪些操作。
- 類的定義是以class開頭,后面接類的名稱。
- 類的主體是包含在一個花括號中,類的定義之后,必須跟著一個分號或者一個聲明列表,例子:
- 關鍵字public確定了類成員的訪問屬性,使得在類對象作用域中,公共成員在類的外部是可以被訪問到的。也可以將其定位為private或者protected。
class Box
{public:double length; // 盒子的長度double breadth; // 盒子的寬度double height; // 盒子的高度
};
類&對象詳解
概念 | 描述 |
---|---|
類成員函數 | 類的成員函數是指那些把定義和原型寫在類定義內部的函數,就像類定義中的其他變量一樣。 |
類訪問修飾符 | 類成員可以被定義為 public、private 或 protected。默認情況下是定義為 private。 |
構造函數 & 析構函數 | 類的構造函數是一種特殊的函數,在創建一個新的對象時調用。類的析構函數也是一種特殊的函數,在刪除所創建的對象時調用。 |
C++ 拷貝構造函數 | 拷貝構造函數,是一種特殊的構造函數,它在創建對象時,是使用同一類中之前創建的對象來初始化新創建的對象。 |
C++ 友元函數 | 友元函數可以訪問類的 private 和 protected 成員。 |
C++ 內聯函數 | 通過內聯函數,編譯器試圖在調用函數的地方擴展函數體中的代碼。 |
C++ 中的 this 指針 | 每個對象都有一個特殊的指針?this,它指向對象本身。 |
C++ 中指向類的指針 | 指向類的指針方式如同指向結構的指針。實際上,類可以看成是一個帶有函數的結構。 |
C++ 類的靜態成員 | 類的數據成員和函數成員都可以被聲明為靜態的。 |
類成員函數
- 類的成員函數是指那些把定義和原型寫在類定義內部的函數,就像類定義中的其他變量一樣。類成員函數是類的一個成員,它可以操作類的任意對象,可以訪問對象中的所有成員。
- 需要使用成員函數來訪問類的成員,而不是直接訪問這些類的成員
- 成員函數可以定義在類定義內部,或者單獨使用范圍解析運算符 ::?來定義。在類定義中定義的成員函數把函數聲明為內聯的,即便沒有使用 inline 標識符。按照如下方式定義?Volume()?函數:
class Box
{public:double length; // 長度double breadth; // 寬度double height; // 高度double getVolume(void){return length * breadth * height;}
};
- 也可以在類的外部使用范圍解析運算符 ::?定義該函數,如下所示:?
double Box::getVolume(void)
{return length * breadth * height;
}
- ?:: 運算符之前必須使用類名。調用成員函數是在對象上使用點運算符(.),這樣它就能操作與該對象相關的數據,如下所示:
Box myBox; // 創建一個對象myBox.getVolume(); // 調用該對象的成員函數
訪問修飾符
-
關鍵字public、private和protected稱之為訪問修飾符
class Base {public:// 公有成員protected:// 受保護成員private:// 私有成員};
- 公有成員在程序中類的外部是可訪問的。可以不使用任何成員函數來設置和獲取公有變量的值
- 私有成員變量或函數在類的外部是不可訪問的,甚至是不可查看的。只有類和友元函數可以訪問私有成員。默認情況下,類的所有成員都是私有的。例如在下面的類中,width?是一個私有成員,這意味著,如果沒有使用任何訪問修飾符,類的成員將被假定為私有成員。
- 一般會在私有區域定義數據,在公有區域定義相關的函數,以便在類的外部也可以調用這些函數
#include <iostream>using namespace std;class Box
{public:double length;void setWidth( double wid );double getWidth( void );private:double width;
};// 成員函數定義
double Box::getWidth(void)
{return width ;
}void Box::setWidth( double wid )
{width = wid;
}// 程序的主函數
int main( )
{Box box;// 不使用成員函數設置長度box.length = 10.0; // OK: 因為 length 是公有的cout << "Length of box : " << box.length <<endl;// 不使用成員函數設置寬度// box.width = 10.0; // Error: 因為 width 是私有的box.setWidth(10.0); // 使用成員函數設置寬度cout << "Width of box : " << box.getWidth() <<endl;return 0;
}
-
保護成員變量或函數與私有成員十分相似,但有一點不同,保護成員在派生類(即子類)中是可訪問的。例子中,從父類?Box?派生了一個子類?smallBox。在這里?width?成員可被派生類 smallBox 的任何成員函數訪問。
繼承中的特點
有public, protected, private三種繼承方式,它們相應地改變了基類成員的訪問屬性。
- public 繼承:基類 public 成員,protected 成員,private 成員的訪問屬性在派生類中分別變成:public, protected, private
- protected 繼承:基類 public 成員,protected 成員,private 成員的訪問屬性在派生類中分別變成:protected, protected, private
- private 繼承:基類 public 成員,protected 成員,private 成員的訪問屬性在派生類中分別變成:private, private, private
但無論哪種繼承方式,上面兩點都沒有改變:
- private 成員只能被本類成員(類內)和友元訪問,不能被派生類訪問;
- protected 成員可以被派生類訪問。
- 默認使用protected繼承,在struct中默認使用public繼承
繼承方式 | 基類的public成員 | 基類的protected成員 | 基類的private成員 | 繼承引起的訪問控制關系變化概括 |
---|---|---|---|---|
public繼承 | 仍為public成員 | 仍為protected成員 | 不可見 | 基類的非私有成員在子類的訪問屬性不變 |
protected繼承 | 變為protected成員 | 變為protected成員 | 不可見 | 基類的非私有成員都為子類的保護成員 |
private繼承 | 變為private成員 | 變為private成員 | 不可見 | 基類中的非私有成員都稱為子類的私有成員 |
類構造函數 & 析構函數
類的構造函數
- 類的構造函數是類的一種特殊的成員函數,它會在每次創建類的新對象時執行。
- 構造函數的名稱與類的名稱是完全相同的,并且不會返回任何類型,也不會返回 void。構造函數可用于為某些成員變量設置初始值。
#include <iostream>using namespace std;class Line
{public:void setLength( double len );double getLength( void );Line(); // 這是構造函數private:double length;
};// 成員函數定義,包括構造函數
Line::Line(void)
{cout << "Object is being created" << endl;
}void Line::setLength( double len )
{length = len;
}double Line::getLength( void )
{return length;
}
// 程序的主函數
int main( )
{Line line;// 設置長度line.setLength(6.0); cout << "Length of line : " << line.getLength() <<endl;return 0;
}
帶參數的構造函數
- 默認的構造函數沒有任何參數,但如果需要,構造函數也可以帶有參數。這樣在創建對象時就會給對象賦初始值,如下面的例子所示:
#include <iostream>using namespace std;class Line
{public:void setLength( double len );double getLength( void );Line(double len); // 這是構造函數private:double length;
};// 成員函數定義,包括構造函數
Line::Line( double len)
{cout << "Object is being created, length = " << len << endl;length = len;
}void Line::setLength( double len )
{length = len;
}double Line::getLength( void )
{return length;
}
// 程序的主函數
int main( )
{Line line(10.0);// 獲取默認設置的長度cout << "Length of line : " << line.getLength() <<endl;// 再次設置長度line.setLength(6.0); cout << "Length of line : " << line.getLength() <<endl;return 0;
}
使用初始化列表來初始化字段
- 使用初始化列表來初始化字段:
Line::Line( double len): length(len)
{cout << "Object is being created, length = " << len << endl;
}
- 等效于以下的語法
Line::Line( double len)
{length = len;cout << "Object is being created, length = " << len << endl;
}
- 假設有一個類 C,具有多個字段 X、Y、Z 等需要進行初始化,同理地,可以使用上面的語法,只需要在不同的字段使用逗號進行分隔,如下所示:
C::C( double a, double b, double c): X(a), Y(b), Z(c)
{....
}
類的析構函數
- 類的析構函數是類的一種特殊的成員函數,它會在每次刪除所創建的對象時執行。
- 析構函數的名稱與類的名稱是完全相同的,只是在前面加了個波浪號(~)作為前綴,它不會返回任何值,也不能帶有任何參數。析構函數有助于在跳出程序(比如關閉文件、釋放內存等)前釋放資源。
拷貝構造函數
拷貝構造函數是一種特殊的構造函數,它在創建對象時,是使用同一類中之前創建的對象來初始化新創建的對象。拷貝構造函數通常用于:
- 通過使用另一個同類型的對象來初始化新創建的對象。
- 復制對象把它作為參數傳遞給函數。
- 復制對象,并從函數返回這個對象。
如果在類中沒有定義拷貝構造函數,編譯器會自行定義一個。如果類帶有指針變量,并有動態內存分配,則它必須有一個拷貝構造函數。拷貝構造函數的最常見形式如下:
classname (const classname &obj) {// 構造函數的主體
}
- obj?是一個對象引用,該對象是用于初始化另一個對象的
#include <iostream>using namespace std;class Line{public:int getLength(void);Line(int len);//簡單的構造函數Line(const Line &Obj);//拷貝構造函數~Line();//析構函數private:int *ptr;
};//成員函數的定義,包括析構函數
Line::Line(int len) {cout<<"調用構造函數"<<endl;//為指針分配內存ptr = new int;*ptr = len;
}
Line::Line(const Line &Obj) {cout<<" 調用拷貝構造函數并且為指針ptr分配內存 "<<endl;ptr = new int;*ptr = *Obj.ptr;//拷貝值
}
Line::~Line() {cout<<"釋放內存"<<endl;delete ptr;
}
int Line::getLength(void) {return *ptr;
}
void display(Line obj){cout<<"line 大小"<<obj.getLength()<<endl;
}
int main(){Line line(10);display(line);return 0;
}
- 使用已有的同類型的對象來初始化新創建的對象
int main(){Line line1(10);Line line2 = line1;//這里調用了拷貝構造函數display(line1);display(line2);return 0;
}
C++?友元函數
- 類的友元函數是定義在類外部,但有權訪問類的所有私有(private)成員和保護(protected)成員。盡管友元函數的原型有在類的定義中出現過,但是友元函數并不是成員函數。
- 友元可以是一個函數,該函數被稱為友元函數;友元也可以是一個類,該類被稱為友元類,在這種情況下,整個類及其所有成員都是友元。
- 如果要聲明函數為一個類的友元,需要在類定義中該函數原型前使用關鍵字?friend,如下所示:
class Box
{double width;
public:double length;friend void printWidth( Box box );void setWidth( double wid );
};
例子:
#include <iostream>using namespace std;class Box
{double width;
public:friend void printWidth( Box box );void setWidth( double wid );
};// 成員函數定義
void Box::setWidth( double wid )
{width = wid;
}// 請注意:printWidth() 不是任何類的成員函數
void printWidth( Box box )
{/* 因為 printWidth() 是 Box 的友元,它可以直接訪問該類的任何成員 */cout << "Width of box : " << box.width <<endl;
}// 程序的主函數
int main( )
{Box box;// 使用成員函數設置寬度box.setWidth(10.0);// 使用友元函數輸出寬度printWidth( box );return 0;
}
C++內聯函數
- C++?內聯函數是通常與類一起使用。如果一個函數是內聯的,那么在編譯時,編譯器會把該函數的代碼副本放置在每個調用該函數的地方。
- 對內聯函數進行任何修改,都需要重新編譯函數的所有客戶端,因為編譯器需要重新更換一次所有的代碼,否則將會繼續使用舊的函數。如果想把一個函數定義為內聯函數,則需要在函數名前面放置關鍵字?inline,在調用函數之前需要對函數進行定義。如果已定義的函數多于一行,編譯器會忽略 inline 限定符。在類定義中的定義的函數都是內聯函數,即使沒有用?inline?說明符。
this指針
- 每一個對象都可以通過使用this指針來訪問自己的地址,this是一個所有成員函數的隱含參數,因此,在成員函數內部,可以用來指向調用對象。
- 友元函數沒有this指針,因為友元不是類的成員,只有成員才可以使用this指針。
#include <iostream>using namespace std;class Box
{public:// 構造函數定義Box(double l=2.0, double b=2.0, double h=2.0){cout <<"Constructor called." << endl;length = l;breadth = b;height = h;}double Volume(){return length * breadth * height;}int compare(Box box){return this->Volume() > box.Volume();}private:double length; // Length of a boxdouble breadth; // Breadth of a boxdouble height; // Height of a box
};int main(void)
{Box Box1(3.3, 1.2, 1.5); // Declare box1Box Box2(8.5, 6.0, 2.0); // Declare box2if(Box1.compare(Box2)){cout << "Box2 is smaller than Box1" <<endl;}else{cout << "Box2 is equal to or larger than Box1" <<endl;}return 0;
}
指向類的指針
- 一個指向 C++ 類的指針與指向結構的指針類似,訪問指向類的指針的成員,需要使用成員訪問運算符?->,就像訪問指向結構的指針一樣。與所有的指針一樣,您必須在使用指針之前,對指針進行初始化。
#include <iostream>using namespace std;class Box
{public:// 構造函數定義Box(double l=2.0, double b=2.0, double h=2.0){cout <<"Constructor called." << endl;length = l;breadth = b;height = h;}double Volume(){return length * breadth * height;}private:double length; // Length of a boxdouble breadth; // Breadth of a boxdouble height; // Height of a box
};int main(void)
{Box Box1(3.3, 1.2, 1.5); // Declare box1Box Box2(8.5, 6.0, 2.0); // Declare box2Box *ptrBox; // Declare pointer to a class.// 保存第一個對象的地址ptrBox = &Box1;// 現在嘗試使用成員訪問運算符來訪問成員cout << "Volume of Box1: " << ptrBox->Volume() << endl;// 保存第二個對象的地址ptrBox = &Box2;// 現在嘗試使用成員訪問運算符來訪問成員cout << "Volume of Box2: " << ptrBox->Volume() << endl;return 0;
}
C++?類的靜態成員
- 使用?static?關鍵字來把類成員定義為靜態的。當聲明類的成員為靜態時,這意味著無論創建多少個類的對象,靜態成員都只有一個副本。
- 靜態成員在類的所有對象中是共享的。如果不存在其他的初始化語句,在創建第一個對象時,所有的靜態數據都會被初始化為零。我們不能把靜態成員的初始化放置在類的定義中,但是可以在類的外部通過使用范圍解析運算符?::?來重新聲明靜態變量從而對它進行初始化
靜態成員函數
- 如果把函數成員聲明為靜態的,就可以把函數與類的任何特定對象獨立開來。靜態成員函數即使在類對象不存在的情況下也能被調用,靜態函數只要使用類名加范圍解析運算符?::?就可以訪問。
- 靜態成員函數只能訪問靜態成員數據、其他靜態成員函數和類外部的其他函數。
- 靜態成員函數有一個類范圍,他們不能訪問類的 this 指針。您可以使用靜態成員函數來判斷類的某些對象是否已被創建。
靜態成員函數與普通成員函數的區別:
- 靜態成員函數沒有 this 指針,只能訪問靜態成員(包括靜態成員變量和靜態成員函數)。
- 普通成員函數有 this 指針,可以訪問類中的任意成員;而靜態成員函數沒有 this 指針。
C++?繼承
- 面向對象程序設計中最重要的一個概念是繼承。繼承允許我們依據另一個類來定義一個類,這使得創建和維護一個應用程序變得更容易。這樣做,也達到了重用代碼功能和提高執行效率的效果。
- 當創建一個類時,您不需要重新編寫新的數據成員和成員函數,只需指定新建的類繼承了一個已有的類的成員即可。這個已有的類稱為基類,新建的類稱為派生類。
- 繼承代表了?is a?關系。例如,哺乳動物是動物,狗是哺乳動物,因此,狗是動物,等等。
基類 & 派生類
- 一個類可以派生自多個類,這意味著,它可以從多個基類繼承數據和函數。定義一個派生類,我們使用一個類派生列表來指定基類。類派生列表以一個或多個基類命名,形式如下:
class derived-class: access-specifier base-class
- 其中,訪問修飾符 access-specifier 是?public、protected?或?private?其中的一個,base-class 是之前定義過的某個類的名稱。如果未使用訪問修飾符 access-specifier,則默認為 private。
假設有一個基類?Shape,Rectangle?是它的派生類,如下所示:
#include <iostream>using namespace std;// 基類
class Shape
{public:void setWidth(int w){width = w;}void setHeight(int h){height = h;}protected:int width;int height;
};// 派生類
class Rectangle: public Shape
{public:int getArea(){ return (width * height); }
};int main(void)
{Rectangle Rect;Rect.setWidth(5);Rect.setHeight(7);// 輸出對象的面積cout << "Total area: " << Rect.getArea() << endl;return 0;
}
訪問控制和繼承
- 派生類可以訪問基類中所有的非私有成員。因此基類成員如果不想被派生類的成員函數訪問,則應在基類中聲明為 private。可以根據訪問權限總結出不同的訪問類型,如下所示:
訪問 | public | protected | private |
---|---|---|---|
同一個類 | yes | yes | yes |
派生類 | yes | yes | no |
外部的類 | yes | no | no |
一個派生類繼承了所有的基類方法,但下列情況除外:
- 基類的構造函數、析構函數和拷貝構造函數
- 基類的重載運算符
- 基類的友元函數
繼承類型
當一個類派生自基類,該基類可以被繼承為?public、protected?或?private?幾種類型。繼承類型是通過上面講解的訪問修飾符 access-specifier 來指定的。
幾乎不使用?protected?或?private?繼承,通常使用?public?繼承。當使用不同類型的繼承時,遵循以下幾個規則:
- 公有繼承(public):當一個類派生自公有基類時,基類的公有成員也是派生類的公有成員,基類的保護成員也是派生類的保護成員,基類的私有成員不能直接被派生類訪問,但是可以通過調用基類的公有和保護成員來訪問。
- 保護繼承(protected):?當一個類派生自保護基類時,基類的公有和保護成員將成為派生類的保護成員。
- 私有繼承(private):當一個類派生自私有基類時,基類的公有和保護成員將成為派生類的私有成員。
多繼承
- 多繼承即一個子類可以有多個父類,它繼承了多個父類的特性。
- C++ 類可以從多個類繼承成員,語法如下:
class <派生類名>:<繼承方式1><基類名1>,<繼承方式2><基類名2>,…
{
<派生類類體>
};
其中,訪問修飾符繼承方式是?public、protected?或?private?其中的一個,用來修飾每個基類,各個基類之間用逗號分隔,如上所示。現在一起看看下面的實例:
#include <iostream>using namespace std;// 基類 Shape
class Shape
{public:void setWidth(int w){width = w;}void setHeight(int h){height = h;}protected:int width;int height;
};// 基類 PaintCost
class PaintCost
{public:int getCost(int area){return area * 70;}
};// 派生類
class Rectangle: public Shape, public PaintCost
{public:int getArea(){ return (width * height); }
};int main(void)
{Rectangle Rect;int area;Rect.setWidth(5);Rect.setHeight(7);area = Rect.getArea();// 輸出對象的面積cout << "Total area: " << Rect.getArea() << endl;// 輸出總花費cout << "Total paint cost: $" << Rect.getCost(area) << endl;return 0;
}
另外多繼承(環狀繼承)
- A->D, B->D, C->(A,B)
class D{......};
class B: public D{......};
class A: public D{......};
class C: public B, public A{.....};
- 這個繼承會使D創建兩個對象,要解決上面問題就要用虛擬繼承格式。格式:class 類名: virtual 繼承方式 父類名
class D{......};
class B: virtual public D{......};
class A: virtual public D{......};
class C: public B, public A{.....};
例子
#include <iostream>using namespace std;
//基類class D
{
public:D(){cout<<"D()"<<endl;}~D(){cout<<"~D()"<<endl;}
protected:int d;
};class B:virtual public D
{
public:B(){cout<<"B()"<<endl;}~B(){cout<<"~B()"<<endl;}
protected:int b;
};class A:virtual public D
{
public:A(){cout<<"A()"<<endl;}~A(){cout<<"~A()"<<endl;}
protected:int a;
};class C:public B, public A
{
public:C(){cout<<"C()"<<endl;}~C(){cout<<"~C()"<<endl;}
protected:int c;
};int main()
{cout << "Hello World!" << endl;C c; //D, B, A ,Ccout<<sizeof(c)<<endl;return 0;
}
注意事項
- 與類同名的函數是構造函數。
- ~ 類名的是類的析構函數。
重載運算符和重載函數
- C++ 允許在同一作用域中的某個函數和運算符指定多個定義,分別稱為函數重載和運算符重載。
- 重載聲明是指一個與之前已經在該作用域內聲明過的函數或方法具有相同名稱的聲明,但是它們的參數列表和定義(實現)不相同。
- 當您調用一個重載函數或重載運算符時,編譯器通過把您所使用的參數類型與定義中的參數類型進行比較,決定選用最合適的定義。選擇最合適的重載函數或重載運算符的過程,稱為重載決策。
C++ 中的函數重載
- 在同一個作用域內,可以聲明幾個功能類似的同名函數,但是這些同名函數的形式參數(指參數的個數、類型或者順序)必須不同。不能僅通過返回類型的不同來重載函數。
- 下面的實例中,同名函數?print()?被用于輸出不同的數據類型:
#include <iostream>
using namespace std;class printData
{public:void print(int i) {cout << "整數為: " << i << endl;}void print(double f) {cout << "浮點數為: " << f << endl;}void print(char c[]) {cout << "字符串為: " << c << endl;}
};int main(void)
{printData pd;// 輸出整數pd.print(5);// 輸出浮點數pd.print(500.263);// 輸出字符串char c[] = "Hello C++";pd.print(c);return 0;
}
C++ 中的運算符重載
- 可以重定義或重載大部分 C++ 內置的運算符。這樣,就可以使用自定義類型的運算符。
- 重載的運算符是帶有特殊名稱的函數,函數名是由關鍵字 operator 和其后要重載的運算符符號構成的。與其他函數一樣,重載運算符有一個返回類型和一個參數列表。
Box operator+(const Box&);
聲明加法運算符用于把兩個 Box 對象相加,返回最終的 Box 對象。大多數的重載運算符可被定義為普通的非成員函數或者被定義為類成員函數。如果我們定義上面的函數為類的非成員函數,那么我們需要為每次操作傳遞兩個參數,如下所示:
Box operator+(const Box&, const Box&);
- 下面的實例使用成員函數演示了運算符重載的概念。在這里,對象作為參數進行傳遞,對象的屬性使用?this?運算符進行訪問,如下所示:
#include <iostream>
using namespace std;class Box
{public:double getVolume(void){return length * breadth * height;}void setLength( double len ){length = len;}void setBreadth( double bre ){breadth = bre;}void setHeight( double hei ){height = hei;}// 重載 + 運算符,用于把兩個 Box 對象相加Box operator+(const Box& b){Box box;box.length = this->length + b.length;box.breadth = this->breadth + b.breadth;box.height = this->height + b.height;return box;}private:double length; // 長度double breadth; // 寬度double height; // 高度
};
// 程序的主函數
int main( )
{Box Box1; // 聲明 Box1,類型為 BoxBox Box2; // 聲明 Box2,類型為 BoxBox Box3; // 聲明 Box3,類型為 Boxdouble volume = 0.0; // 把體積存儲在該變量中// Box1 詳述Box1.setLength(6.0); Box1.setBreadth(7.0); Box1.setHeight(5.0);// Box2 詳述Box2.setLength(12.0); Box2.setBreadth(13.0); Box2.setHeight(10.0);// Box1 的體積volume = Box1.getVolume();cout << "Volume of Box1 : " << volume <<endl;// Box2 的體積volume = Box2.getVolume();cout << "Volume of Box2 : " << volume <<endl;// 把兩個對象相加,得到 Box3Box3 = Box1 + Box2;// Box3 的體積volume = Box3.getVolume();cout << "Volume of Box3 : " << volume <<endl;return 0;
}
可重載運算符
雙目算術運算符 | + (加),-(減),*(乘),/(除),%?(取模) |
關系運算符 | ==(等于),!= (不等于),< (小于),> (大于>,<=(小于等于),>=(大于等于) |
邏輯運算符 | ||(邏輯或),&&(邏輯與),!(邏輯非) |
單目運算符 | + (正),-(負),*(指針),&(取地址) |
自增自減運算符 | ++(自增),--(自減) |
位運算符 | | (按位或),& (按位與),~(按位取反),^(按位異或),,<<?(左移),>>(右移) |
賦值運算符 | =, +=, -=, *=, /= , % = , &=, |=, ^=, <<=, >>= |
空間申請與釋放 | new, delete, new[ ] , delete[] |
其他運算符 | ()(函數調用),->(成員訪問),,(逗號),[](下標) |
不可重載運算符
- .:成員訪問運算符
- .*,?->*:成員指針訪問運算符
- :::域運算符
- sizeof:長度運算符
- ?::條件運算符
- #: 預處理符號
運算符重載的案例
序號 | 運算符和實例 |
---|---|
1 | 一元運算符重載 |
2 | 二元運算符重載 |
3 | 關系運算符重載 |
4 | 輸入/輸出運算符重載 |
5 | ++ 和 -- 運算符重載 |
6 | 賦值運算符重載 |
7 | 函數調用運算符 () 重載 |
8 | 下標運算符 [] 重載 |
9 | 類成員訪問運算符 -> 重載 |
一元運算符重載
?
一元運算符只對一個操作數進行操作,下面是一元運算符的實例:
- 遞增運算符( ++ )和遞減運算符( -- )
- 一元減運算符,即負號( - )
- 邏輯非運算符( ! )
一元運算符通常出現在它們所操作的對象的左邊,比如 !obj、-obj 和 ++obj,但有時它們也可以作為后綴,比如 obj++ 或 obj--。下面的實例演示了如何重載一元減運算符( - )。
二元運算符重載
- 二元運算符需要兩個參數,下面是二元運算符的實例。平常使用的加運算符( + )、減運算符( - )、乘運算符( * )和除運算符( / )都屬于二元運算符。就像加(+)運算符。
- 下面的實例演示了如何重載加運算符( + )
#include <iostream>
using namespace std;class Box
{double length; // 長度double breadth; // 寬度double height; // 高度
public:double getVolume(void){return length * breadth * height;}void setLength( double len ){length = len;}void setBreadth( double bre ){breadth = bre;}void setHeight( double hei ){height = hei;}// 重載 + 運算符,用于把兩個 Box 對象相加Box operator+(const Box& b){Box box;box.length = this->length + b.length;box.breadth = this->breadth + b.breadth;box.height = this->height + b.height;return box;}
};
// 程序的主函數
int main( )
{Box Box1; // 聲明 Box1,類型為 BoxBox Box2; // 聲明 Box2,類型為 BoxBox Box3; // 聲明 Box3,類型為 Boxdouble volume = 0.0; // 把體積存儲在該變量中// Box1 詳述Box1.setLength(6.0); Box1.setBreadth(7.0); Box1.setHeight(5.0);// Box2 詳述Box2.setLength(12.0); Box2.setBreadth(13.0); Box2.setHeight(10.0);// Box1 的體積volume = Box1.getVolume();cout << "Volume of Box1 : " << volume <<endl;// Box2 的體積volume = Box2.getVolume();cout << "Volume of Box2 : " << volume <<endl;// 把兩個對象相加,得到 Box3Box3 = Box1 + Box2;// Box3 的體積volume = Box3.getVolume();cout << "Volume of Box3 : " << volume <<endl;return 0;
}
關系運算符重載
- C++ 語言支持各種關系運算符( < 、 > 、 <= 、 >= 、 == 等等),它們可用于比較 C++ 內置的數據類型
- 可以重載任何一個關系運算符,重載后的關系運算符可用于比較類的對象。
- 下面的實例演示了如何重載 < 運算符
輸入/輸出運算符重載
- C++ 能夠使用流提取運算符 >> 和流插入運算符 << 來輸入和輸出內置的數據類型。可以重載流提取運算符和流插入運算符來操作對象等用戶自定義的數據類型。
- 在這里,有一點很重要,我們需要把運算符重載函數聲明為類的友元函數,這樣我們就能不用創建對象而直接調用函數。
- 下面的實例演示了如何重載提取運算符 >> 和插入運算符 <<。
C++多態
- 多態,按照字面意思是指多種形態。當類之間存在層次結構的時候,并且類之間是通過繼承關系關聯的時候,就會遇到多態。多態是指調用成員函數的時候,會根據調用函數的對象的類型不同來執行不同的函數。
- 例子:基類shape會被派生為兩個類
#include <iostream>
using namespace std;class Shape {protected:int width, height;public:Shape( int a=0, int b=0){width = a;height = b;}int area(){cout << "Parent class area :" <<endl;return 0;}
};
class Rectangle: public Shape{public:Rectangle( int a=0, int b=0):Shape(a, b) { }int area (){ cout << "Rectangle class area :" <<endl;return (width * height); }
};
class Triangle: public Shape{public:Triangle( int a=0, int b=0):Shape(a, b) { }int area (){ cout << "Triangle class area :" <<endl;return (width * height / 2); }
};
// 程序的主函數
int main( )
{Shape *shape;Rectangle rec(10,7);Triangle tri(10,5);// 存儲矩形的地址shape = &rec;// 調用矩形的求面積函數 areashape->area();// 存儲三角形的地址shape = &tri;// 調用三角形的求面積函數 areashape->area();return 0;
}//Parent class area
//Parent class area
- 出現上面的錯誤原因是:
調用函數 area() 被編譯器設置為基類中的版本,這就是所謂的靜態多態,或靜態鏈接?- 函數調用在程序執行前就準備好了。有時候這也被稱為早綁定,因為 area() 函數在程序編譯期間就已經設置好了。
-
正確的使用方法:在 Shape 類中,area() 的聲明前放置關鍵字?virtual,如下所示:
class Shape {protected:int width, height;public:Shape( int a=0, int b=0){width = a;height = b;}virtual int area(){cout << "Parent class area :" <<endl;return 0;}
};
虛函數
- 虛函數?是在基類中使用關鍵字?virtual?聲明的函數。在派生類中重新定義基類中定義的虛函數時,會告訴編譯器不要靜態鏈接到該函數。
- 在程序中任意點可以根據所調用的對象類型來選擇調用的函數,這種操作被稱為動態鏈接,或后期綁定。
純虛函數
- 在基類中定義虛函數,以便在派生類中重新定義該函數從而更好地適用于對象,但是在基類中又不能對虛函數給出有意義的實現,這個時候就會用到純虛函數。
- 可以把基類中的虛函數 area() 改寫如下:
class Shape {protected:int width, height;public:Shape( int a=0, int b=0){width = a;height = b;}// pure virtual functionvirtual int area() = 0;
};
-
= 0 告訴編譯器,函數沒有主體,上面的虛函數是純虛函數。
C++?數據抽象
- 數據抽象是指,只向外界提供關鍵信息,并隱藏其后臺的實現細節,即只表現必要的信息而不呈現細節,這個依賴于接口和實現相互分離的編程(設計)技術。
- 舉一個現實生活中的真實例子,比如一臺電視機,用戶可以打開和關閉、切換頻道、調整音量、添加外部組件(如喇叭、錄像機、DVD 播放器),但是不知道它的內部實現細節,也就是說,并不知道它是如何通過纜線接收信號,如何轉換信號,并最終顯示在屏幕上。因此,可以說電視把它的內部實現和外部接口分離開了,無需知道它的內部實現原理,直接通過它的外部接口(比如電源按鈕、遙控器、聲量控制器)就可以操控電視。
- 就 C++ 編程而言,C++ 類為數據抽象提供了可能。它向外界提供了大量用于操作對象數據的公共方法,也就是說,外界實際上并不清楚類的內部實現。例如,程序可以調用?sort()?函數,而不需要知道函數中排序數據所用到的算法。實際上,函數排序的底層實現會因庫的版本不同而有所差異,只要接口不變,函數調用就可以照常工作。
- 在 C++ 中,可以使用類來定義用戶自己的抽象數據類型(ADT)。可以使用類?iostream?的?cout?對象來輸出數據到標準輸出,如下所示:
#include <iostream>
using namespace std;int main( )
{cout << "Hello C++" <<endl;return 0;
}
- 不需要理解?cout?是如何在用戶的屏幕上顯示文本。只需要知道公共接口即可,cout 的底層實現可以自由改變。
訪問標簽強制抽象
在 C++ 中,可以使用訪問標簽來定義類的抽象接口。一個類可以包含零個或多個訪問標簽:
- 使用公共標簽定義的成員都可以訪問該程序的所有部分。一個類型的數據抽象視圖是由它的公共成員來定義的。
- 使用私有標簽定義的成員無法訪問到使用類的代碼。私有部分對使用類型的代碼隱藏了實現細節。
訪問標簽出現的頻率沒有限制。每個訪問標簽指定了緊隨其后的成員定義的訪問級別。指定的訪問級別會一直有效,直到遇到下一個訪問標簽或者遇到類主體的關閉右括號為止。
數據抽象的好處
數據抽象有兩個重要的優勢:
- 類的內部受到保護,不會因無意的用戶級錯誤導致對象狀態受損。
- 類實現可能隨著時間的推移而發生變化,以便應對不斷變化的需求,或者應對那些要求不改變用戶級代碼的錯誤報告。
如果只在類的私有部分定義數據成員,編寫該類的作者就可以隨意更改數據。如果實現發生改變,則只需要檢查類的代碼,看看這個改變會導致哪些影響。如果數據是公有的,則任何直接訪問舊表示形式的數據成員的函數都可能受到影響。
數據抽象的實例
- C++ 程序中,任何帶有公有和私有成員的類都可以作為數據抽象的實例。請看下面的實例:
#include <iostream>
using namespace std;class Adder{public:// 構造函數Adder(int i = 0){total = i;}// 對外的接口void addNum(int number){total += number;}// 對外的接口int getTotal(){return total;};private:// 對外隱藏的數據int total;
};
int main( )
{Adder a;a.addNum(10);a.addNum(20);a.addNum(30);cout << "Total " << a.getTotal() <<endl;return 0;
}
- 上面的類把數字相加,并返回總和。公有成員?addNum?和?getTotal?是對外的接口,用戶需要知道它們以便使用類。私有成員?total?是用戶不需要了解的,但又是類能正常工作所必需的。
設計策略
- 抽象把代碼分離為接口和實現。所以在設計組件時,必須保持接口獨立于實現,這樣,如果改變底層實現,接口也將保持不變。在這種情況下,不管任何程序使用接口,接口都不會受到影響,只需要將最新的實現重新編譯即可。
C++?數據封裝
所有的 C++ 程序都有以下兩個基本要素:
- 程序語句(代碼):這是程序中執行動作的部分,它們被稱為函數。
- 程序數據:數據是程序的信息,會受到程序函數的影響。
總結:
- 封裝是面向對象編程中的把數據和操作數據的函數綁定在一起的一個概念,這樣能避免受到外界的干擾和誤用,從而確保了安全。數據封裝引申出了另一個重要的 OOP 概念,即數據隱藏。
- 數據封裝是一種把數據和操作數據的函數捆綁在一起的機制,數據抽象是一種僅向用戶暴露接口而把具體的實現細節隱藏起來的機制。
- C++ 通過創建類來支持封裝和數據隱藏(public、protected、private)。我們已經知道,類包含私有成員(private)、保護成員(protected)和公有成員(public)成員。默認情況下,在類中定義的所有項目都是私有的。例如:
class Box
{public:double getVolume(void){return length * breadth * height;}private:double length; // 長度double breadth; // 寬度double height; // 高度
};
- 變量 length、breadth 和 height 都是私有的(private)。這意味著它們只能被 Box 類中的其他成員訪問,而不能被程序中其他部分訪問。這是實現封裝的一種方式。
- 為了使類中的成員變成公有的(即,程序中的其他部分也能訪問),必須在這些成員前使用?public?關鍵字進行聲明。所有定義在 public 標識符后邊的變量或函數可以被程序中所有其他的函數訪問。
- 把一個類定義為另一個類的友元類,會暴露實現細節,從而降低了封裝性。理想的做法是盡可能地對外隱藏每個類的實現細節。
-
C++中, 虛函數可以為private, 并且可以被子類覆蓋(因為虛函數表的傳遞),但子類不能調用父類的private虛函數。虛函數的重載性和它聲明的權限無關。一個成員函數被定義為private屬性,標志著其只能被當前類的其他成員函數(或友元函數)所訪問。而virtual修飾符則強調父類的成員函數可以在子類中被重寫,因為重寫的時候并沒有與父類發生任何的調用關系,故而重寫是被允許的。
編譯器不檢查虛函數的各類屬性。被virtual修飾的成員函數,不論他們是private、protect或是public的,都會被統一的放置到虛函數表中。對父類進行派生時,子類會繼承到擁有相同偏移地址的虛函數表(相同偏移地址指,各虛函數相對于VPTR指針的偏移),則子類就會被允許對這些虛函數進行重載。且重載時可以給重載函數定義新的屬性,例如public,其只標志著該重載函數在該子類中的訪問屬性為public,和父類的private屬性沒有任何關系!
純虛函數可以設計成私有的,不過這樣不允許在本類之外的非友元函數中直接調用它,子類中只有覆蓋這種純虛函數的義務,卻沒有調用它的權利。
C++?接口(抽象類)
- 接口描述了類的行為和功能,而不需要完成類的特定實現。
- C++ 接口是使用抽象類來實現的,抽象類與數據抽象互不混淆,數據抽象是一個把實現細節與相關的數據分離開的概念。
- 如果類中有一個函數被聲明為純虛函數,則這個類就是抽象類。純虛函數是通過在聲明中使用 "= 0" 來指定的,如下所示:
class Box
{public:// 純虛函數virtual double getVolume() = 0;private:double length; // 長度double breadth; // 寬度double height; // 高度
};
- 設計抽象類(通常稱為 ABC)的目的,是為了給其他類提供一個可以繼承的適當的基類。抽象類不能被用于實例化對象,它只能作為接口使用。如果試圖實例化一個抽象類的對象,會導致編譯錯誤。
- 因此,如果一個 ABC 的子類需要被實例化,則必須實現每個虛函數,這也意味著 C++ 支持使用 ABC 聲明接口。如果沒有在派生類中重寫純虛函數,就嘗試實例化該類的對象,會導致編譯錯誤。
- 可用于實例化對象的類被稱為具體類。
抽象類的實例
- 請看下面的實例,基類 Shape 提供了一個接口?getArea(),在兩個派生類 Rectangle 和 Triangle 中分別實現了?getArea():
#include <iostream>using namespace std;// 基類
class Shape
{
public:// 提供接口框架的純虛函數virtual int getArea() = 0;void setWidth(int w){width = w;}void setHeight(int h){height = h;}
protected:int width;int height;
};// 派生類
class Rectangle: public Shape
{
public:int getArea(){ return (width * height); }
};
class Triangle: public Shape
{
public:int getArea(){ return (width * height)/2; }
};int main(void)
{Rectangle Rect;Triangle Tri;Rect.setWidth(5);Rect.setHeight(7);// 輸出對象的面積cout << "Total Rectangle area: " << Rect.getArea() << endl;Tri.setWidth(5);Tri.setHeight(7);// 輸出對象的面積cout << "Total Triangle area: " << Tri.getArea() << endl; return 0;
}
設計策略
- 面向對象的系統可能會使用一個抽象基類為所有的外部應用程序提供一個適當的、通用的、標準化的接口。然后,派生類通過繼承抽象基類,就把所有類似的操作都繼承下來。
- 外部應用程序提供的功能(即公有函數)在抽象基類中是以純虛函數的形式存在的。這些純虛函數在相應的派生類中被實現。這個架構也使得新的應用程序可以很容易地被添加到系統中,即使是在系統被定義之后依然可以如此。