一、C++類與對象
1.C++類定義
們使用關鍵字 class 定義 Box 數據類型,如下所示:
class Box
{
public:
double length; // 盒子的長度
double breadth; // 盒子的寬度
double height; // 盒子的高度
};
關鍵字 public 確定了類成員**的訪問屬性。**在類對象作用域內,公共成員在類的外部是可訪問的。
2.定義 C++ 對象
聲明類的對象,就像聲明基本類型的變量一樣。下面的語句聲明瞭類 Box 的兩個對象:
Box Box1; // 聲明 Box1,類型爲 Box
Box Box2; // 聲明 Box2,類型爲 Box
對象 Box1 和 Box2 都有它們各自的數據成員。
3.訪問數據成員
類的對象的公共數據成員可以使用直接成員訪問運算符 (.) 來訪問,需要注意的是,私有的成員和受保護的成員不能使用直接成員訪問運算符 (.) 來直接訪問。
實例:
class Box
{
public:
double length; // 長度
double breadth; // 寬度
double height; // 高度
};
int main( )
{
Box Box1; // 聲明 Box1,類型爲 Box
Box Box2; // 聲明 Box2,類型爲 Box
double volume = 0.0; // 用於存儲體積
// box 1 詳述
Box1.height = 5.0;
Box1.length = 6.0;
Box1.breadth = 7.0;
// box 2 詳述
Box2.height = 10.0;
Box2.length = 12.0;
Box2.breadth = 13.0;
4.類成員函數
類的成員函數是指將定義和原型寫在類定義內部的函數,類成員函數是類的一個變量,它可以操作類的任意對象,可以訪問對象中所有成員。
成員函數可以定義在類定義內部,或者單獨使用範圍解析運算符::來定義,在類定義中定義的成員函數把函數聲明爲內聯的,即便沒有使用 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(); // 調用該對象的成員函數
我們使用上面提到的概念來設置和獲取類中不同的成員的值
#include <iostream>
using namespace std;
class Box
{
public:
double length; // 長度
double breadth; // 寬度
double height; // 高度
// 成員函數聲明
double getVolume(void);
void setLength( double len );
void setBreadth( double bre );
void setHeight( double hei );
};
// 成員函數定義
double Box::getVolume(void)
{
return length * breadth * height;
}
void Box::setLength( double len )
{
length = len;
}
void Box::setBreadth( double bre )
{
breadth = bre;
}
void Box::setHeight( double hei )
{
height = hei;
}
// 程序的主函數
int main( )
{
Box Box1; // 聲明 Box1,類型爲 Box
Box Box2; // 聲明 Box2,類型爲 Box
double volume = 0.0; // 用於存儲體積
// box 1 詳述
Box1.setLength(6.0);
Box1.setBreadth(7.0);
Box1.setHeight(5.0);
// box 2 詳述
Box2.setLength(12.0);
Box2.setBreadth(13.0);
Box2.setHeight(10.0);
// box 1 的體積
volume = Box1.getVolume();
cout << "Box1 的體積:" << volume <<endl;
// box 2 的體積
volume = Box2.getVolume();
cout << "Box2 的體積:" << volume <<endl;
return 0;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
Box1 的體積: 210
Box2 的體積: 1560
5.類訪問修飾符
數據封裝是面向對象編程的一個重要特點,它防止函數直接訪問類類型的內部成員。類成員的訪問限制是通過在類主體內部對各個區域標記 public、private、protected 來指定的。關鍵字 public、private、protected 稱爲訪問修飾符。
一個類可以有多個 public、protected 或 private 標記區域。每個標記區域在下一個標記區域開始之前或者在遇到類主體結束右括號之前都是有效的。成員和類的默認訪問修飾符是 private。
(1)公有(public)成員
公有成員在程序中類的外部是可訪問的。您可以不使用任何成員函數來設置和獲取公有變量的值,如下所示:
#include <iostream>
using namespace std;
class Line
{
public:
double length;
void setLength( double len );
double getLength( void );
};
// 成員函數定義
double Line::getLength(void)
{
return length ;
}
void Line::setLength( double len )
{
length = len;
}
// 程序的主函數
int main( )
{
Line line;
// 設置長度
line.setLength(6.0);
cout << "Length of line : " << line.getLength() <<endl;
// 不使用成員函數設置長度
line.length = 10.0; // OK: 因爲 length 是公有的
cout << "Length of line : " << line.length <<endl;
return 0;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
Length of line : 6
Length of line : 10
(2)私有成員變量或函數在類的外部是不可訪問的,甚至是不可查看的**。只有類和友元函數可以訪問私有成員**。
默認情況下,類的所有成員都是私有的。例如在下面的類中,width 是一個私有成員,這意味着,如果您沒有使用任何訪問修飾符,類的成員將被假定爲私有成員:
class Box
{
double width;
public:
double length;
void setWidth( double wid );
double getWidth( void );
};
實際操作中,我們一般會在私有區域定義數據,在公有區域定義相關的函數,以便在類的外部也可以調用這些函數,如下所示:
#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;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
Length of box : 10
Width of box : 10
(3)保護(protected)成員
保護成員變量或函數與私有成員十分相似,但有一點不同,保護成員在派生類(即子類)中是可訪問的。可以看到下面的實例中,我們從父類 Box 派生了一個子類 smallBox。下面的實例與前面的實例類似,在這裏 width 成員可被派生類 smallBox 的任何成員函數訪問。
#include <iostream>
using namespace std;
class Box
{
protected:
double width;
};
class SmallBox:Box // SmallBox 是派生類
{
public:
void setSmallWidth( double wid );
double getSmallWidth( void );
};
// 子類的成員函數
double SmallBox::getSmallWidth(void)
{
return width ;
}
void SmallBox::setSmallWidth( double wid )
{
width = wid;
}
// 程序的主函數
int main( )
{
SmallBox box;
// 使用成員函數設置寬度
box.setSmallWidth(5.0);
cout << "Width of box : "<< box.getSmallWidth() << endl;
return 0;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
Width of box : 5
(4)承中的特點
有public, protected, private三種繼承方式,它們相應地改變了基類成員的訪問屬性;
1.>public 繼承:基類 public 成員,protected 成員,private 成員的訪問屬性在派生類中分別變成:public, protected, private
2.>protected 繼承:基類 public 成員,protected 成員,private 成員的訪問屬性在派生類中分別變成:protected, protected, private
3.>private 繼承:基類 public 成員,protected 成員,private 成員的訪問屬性在派生類中分別變成:private, private, private
但無論哪種繼承方式,上面兩點都沒有改變:
1.private 成員只能被本類成員(類內)和友元訪問,不能被派生類訪問;
2.protected 成員可以被派生類訪問。
public 繼承
class A{
public:
int a;
A(){
a1 = 1;
a2 = 2;
a3 = 3;
a = 4;
}
void fun(){
cout << a << endl; //正確
cout << a1 << endl; //正確
cout << a2 << endl; //正確
cout << a3 << endl; //正確
}
public:
int a1;
protected:
int a2;
private:
int a3;
};
class B : public A{
public:
int a;
B(int i){
A();
a = i;
}
void fun(){
cout << a << endl; //正確,public成員
cout << a1 << endl; //正確,基類的public成員,在派生類中仍是public成員。
cout << a2 << endl; //正確,基類的protected成員,在派生類中仍是protected可以被派生類訪問。
cout << a3 << endl; //錯誤,基類的private成員不能被派生類訪問。
}
};
int main(){
B b(10);
cout << b.a << endl;
cout << b.a1 << endl; //正確
cout << b.a2 << endl; //錯誤,類外不能訪問protected成員
cout << b.a3 << endl; //錯誤,類外不能訪問private成員
system("pause");
return 0;
}
protected 繼承
class A{
public:
int a;
A(){
a1 = 1;
a2 = 2;
a3 = 3;
a = 4;
}
void fun(){
cout << a << endl; //正確
cout << a1 << endl; //正確
cout << a2 << endl; //正確
cout << a3 << endl; //正確
}
public:
int a1;
protected:
int a2;
private:
int a3;
};
class B : protected A{
public:
int a;
B(int i){
A();
a = i;
}
void fun(){
cout << a << endl; //正確,public成員。
cout << a1 << endl; //正確,基類的public成員,在派生類中變成了protected,可以被派生類訪問。
cout << a2 << endl; //正確,基類的protected成員,在派生類中還是protected,可以被派生類訪問。
cout << a3 << endl; //錯誤,基類的private成員不能被派生類訪問。
}
};
int main(){
B b(10);
cout << b.a << endl; //正確。public成員
cout << b.a1 << endl; //錯誤,protected成員不能在類外訪問。
cout << b.a2 << endl; //錯誤,protected成員不能在類外訪問。
cout << b.a3 << endl; //錯誤,private成員不能在類外訪問。
system("pause");
return 0;
}
private 繼承
using namespace std;
class A{
public:
int a;
A(){
a1 = 1;
a2 = 2;
a3 = 3;
a = 4;
}
void fun(){
cout << a << endl; //正確
cout << a1 << endl; //正確
cout << a2 << endl; //正確
cout << a3 << endl; //正確
}
public:
int a1;
protected:
int a2;
private:
int a3;
};
class B : private A{
public:
int a;
B(int i){
A();
a = i;
}
void fun(){
cout << a << endl; //正確,public成員。
cout << a1 << endl; //正確,基類public成員,在派生類中變成了private,可以被派生類訪問。
cout << a2 << endl; //正確,基類的protected成員,在派生類中變成了private,可以被派生類訪問。
cout << a3 << endl; //錯誤,基類的private成員不能被派生類訪問。
}
};
int main(){
B b(10);
cout << b.a << endl; //正確。public成員
cout << b.a1 << endl; //錯誤,private成員不能在類外訪問。
cout << b.a2 << endl; //錯誤, private成員不能在類外訪問。
cout << b.a3 << endl; //錯誤,private成員不能在類外訪問。
system("pause");
return 0;
}
6.構造函數&析構函數
類的構造函數是類的一種特殊的成員函數,它會在每次創建類的新對象時執行。
構造函數的名稱與類的名稱是完全相同的,並且不會返回任何類型,也不會返回 void。構造函數可用於爲某些成員變量設置初始值。
(1)構造函數實例:
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;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
Object is being created
Length of line : 6
(2)帶參數的構造函數
默認的構造函數沒有任何參數,但如果需要,構造函數也可以帶有參數。這樣在創建對象時就會給對象賦初始值,如下面的例子所示:
#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;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
Object is being created, length = 10
Length of line : 10
Length of line : 6
使用初始化列表來初始化字段:
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)
{
....
}
(3)類的析構函數
類的析構函數是類的一種特殊的成員函數,它會在每次刪除所創建的對象時執行。析構函數的名稱與類的名稱是完全相同的,只是在前面加了個波浪號(~)作爲前綴,它不會返回任何值,也不能帶有任何參數。析構函數有助於在跳出程序(比如關閉文件、釋放內存等)前釋放資源。
下面的實例有助於更好地理解析構函數的概念:
#include <iostream>
using namespace std;
class Line
{
public:
void setLength( double len );
double getLength( void );
Line(); // 這是構造函數聲明
~Line(); // 這是析構函數聲明
private:
double length;
};
// 成員函數定義,包括構造函數
Line::Line(void)
{
cout << "Object is being created" << endl;
}
Line::~Line(void)
{
cout << "Object is being deleted" << 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;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
Object is being created
Length of line : 6
Object is being deleted
7.C++拷貝構造函數
拷貝構造函數是一種特殊的構造函數,它在創建對象時,是使用同一類中之前創建的對象來初始化新創建的對象。拷貝構造函數通常用於:
通過使用另一個同類型的對象來初始化新創建的對象。
複製對象把它作爲參數傳遞給函數。
複製對象,並從函數返回這個對象。
如果在類中沒有定義拷貝構造函數,編譯器會自行定義一個。如果類帶有指針變量,並有動態內存分配,則它必須有一個拷貝構造函數。拷貝構造函數的最常見形式如下:
classname (const classname &obj) {
// 構造函數的主體
}
在這裏,obj 是一個對象引用,該對象是用於初始化另一個對象的。
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(void)
{
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;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
調用構造函數
調用拷貝構造函數併爲指針 ptr 分配內存
line 大小 : 10
釋放內存
釋放內存
下面的實例對上面的實例稍作修改,通過使用已有的同類型的對象來初始化新創建的對象:
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(void)
{
cout << "釋放內存" << endl;
delete ptr;
}
int Line::getLength( void )
{
return *ptr;
}
void display(Line obj)
{
cout << "line 大小 : " << obj.getLength() <<endl;
}
// 程序的主函數
int main( )
{
Line line1(10);
Line line2 = line1; // 這裏也調用了拷貝構造函數
display(line1);
display(line2);
return 0;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
調用構造函數
調用拷貝構造函數併爲指針 ptr 分配內存
調用拷貝構造函數併爲指針 ptr 分配內存
line 大小 : 10
釋放內存
調用拷貝構造函數併爲指針 ptr 分配內存
line 大小 : 10
釋放內存
釋放內存
釋放內存
8.C++友元函數
類的友元函數是定義在類外部,但有權訪問類的所有私有(private)成員和保護(protected)成員。儘管友元函數的原型有在類的定義中出現過,但是友元函數並不是成員函數。
友元可以是一個函數,該函數被稱爲友元函數;友元也可以是一個類,該類被稱爲友元類,在這種情況下,整個類及其所有成員都是友元。
如果要聲明函數爲一個類的友元,需要在類定義中該函數原型前使用關鍵字 friend,如下所示:
class Box
{
double width;
public:
double length;
friend void printWidth( Box box );
void setWidth( double wid );
};
聲明類 ClassTwo 的所有成員函數作爲類 ClassOne 的友元,需要在類 ClassOne 的定義中放置如下聲明:
friend class ClassTwo;
請看下面的程序:
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;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
Width of box : 10
9.C++中的this指針
在 C++ 中,每一個對象都能通過 this 指針來訪問自己的地址。this 指針是所有成員函數的隱含參數。因此,在成員函數內部,它可以用來指向調用對象。
友元函數沒有 this 指針,因爲友元不是類的成員。只有成員函數纔有 this 指針。
下面的實例有助於更好地理解 this 指針的概念:
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 box
double breadth; // Breadth of a box
double height; // Height of a box
};
int main(void)
{
Box Box1(3.3, 1.2, 1.5); // Declare box1
Box Box2(8.5, 6.0, 2.0); // Declare box2
if(Box1.compare(Box2))
{
cout << "Box2 is smaller than Box1" <<endl;
}
else
{
cout << "Box2 is equal to or larger than Box1" <<endl;
}
return 0;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
Constructor called.
Constructor called.
Box2 is equal to or larger than Box1
10.指向類的指針
一個指向 C++ 類的指針與指向結構的指針類似,訪問指向類的指針的成員,需要使用成員訪問運算符 ->,就像訪問指向結構的指針一樣。與所有的指針一樣,您必須在使用指針之前,對指針進行初始化。
下面的實例有助於更好地理解指向類的指針的概念:
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 box
double breadth; // Breadth of a box
double height; // Height of a box
};
int main(void)
{
Box Box1(3.3, 1.2, 1.5); // Declare box1
Box Box2(8.5, 6.0, 2.0); // Declare box2
Box *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;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
Constructor called.
Constructor called.
Volume of Box1: 5.94
Volume of Box2: 102
11.C++中的靜態成員
我們可以使用 static 關鍵字來把類成員定義爲靜態的。當我們聲明類的成員爲靜態時,這意味着無論創建多少個類的對象,靜態成員都只有一個副本。
靜態成員在類的所有對象中是共享的。如果不存在其他的初始化語句,在創建第一個對象時,所有的靜態數據都會被初始化爲零。我們不能把靜態成員的初始化放置在類的定義中,但是可以在類的外部通過使用範圍解析運算符 :: 來重新聲明靜態變量從而對它進行初始化,如下面的實例所示。
下面的實例有助於更好地理解靜態成員數據的概念:
class Box
{
public:
static int objectCount;
// 構造函數定義
Box(double l=2.0, double b=2.0, double h=2.0)
{
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
// 每次創建對象時增加 1
objectCount++;
}
double Volume()
{
return length * breadth * height;
}
private:
double length; // 長度
double breadth; // 寬度
double height; // 高度
};
// 初始化類 Box 的靜態成員
int Box::objectCount = 0;
int main(void)
{
Box Box1(3.3, 1.2, 1.5); // 聲明 box1
Box Box2(8.5, 6.0, 2.0); // 聲明 box2
// 輸出對象的總數
cout << "Total objects: " << Box::objectCount << endl;
return 0;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
Constructor called.
Constructor called.
Total objects: 2
靜態成員函數
如果把函數成員聲明爲靜態的,就可以把函數與類的任何特定對象獨立開來。靜態成員函數即使在類對象不存在的情況下也能被調用,靜態函數只要使用類名加範圍解析運算符 :: 就可以訪問。
靜態成員函數只能訪問靜態成員數據、其他靜態成員函數和類外部的其他函數。
靜態成員函數有一個類範圍,他們不能訪問類的 this 指針。您可以使用靜態成員函數來判斷類的某些對象是否已被創建。
靜態成員函數與普通成員函數的區別:
靜態成員函數沒有 this 指針,只能訪問靜態成員(包括靜態成員變量和靜態成員函數)。
普通成員函數有 this 指針,可以訪問類中的任意成員;而靜態成員函數沒有 this 指針。
下面的實例有助於更好地理解靜態成員函數的概念:
class Box
{
public:
static int objectCount;
// 構造函數定義
Box(double l=2.0, double b=2.0, double h=2.0)
{
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
// 每次創建對象時增加 1
objectCount++;
}
double Volume()
{
return length * breadth * height;
}
static int getCount()
{
return objectCount;
}
private:
double length; // 長度
double breadth; // 寬度
double height; // 高度
};
// 初始化類 Box 的靜態成員
int Box::objectCount = 0;
int main(void)
{
// 在創建對象之前輸出對象的總數
cout << "Inital Stage Count: " << Box::getCount() << endl;
Box Box1(3.3, 1.2, 1.5); // 聲明 box1
Box Box2(8.5, 6.0, 2.0); // 聲明 box2
// 在創建對象之後輸出對象的總數
cout << "Final Stage Count: " << Box::getCount() << endl;
return 0;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
Inital Stage Count: 0
Constructor called.
Constructor called.
Final Stage Count: 2
二、C++ 繼承
面向對象程序設計中最重要的一個概念是繼承。繼承允許我們依據另一個類來定義一個類,當創建一個類時,您不需要重新編寫新的數據成員和成員函數,只需指定新建的類繼承了一個已有的類的成員即可。這個已有的類稱爲基類,新建的類稱爲派生類。
繼承代表了 is a 關係。例如,哺乳動物是動物,狗是哺乳動物,因此,狗是動物,等等。
1.基類 & 派生類
一個類可以派生自多個類,這意味着,它可以從多個基類繼承數據和函數。定義一個派生類,我們使用一個類派生列表來指定基類。類派生列表以一個或多個基類命名,形式如下:
class derived-class: access-specifier base-class
其中,訪問修飾符 access-specifier 是 public、protected 或 private 其中的一個,base-class 是之前定義過的某個類的名稱。如果未使用訪問修飾符 access-specifier,則默認爲 private。
假設有一個基類 Shape,Rectangle 是它的派生類,如下所示:
// 基類
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;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
Total area: 35
2.訪問控制和繼承
派生類可以訪問基類中所有的非私有成員。因此基類成員**如果不想被派生類的成員函數訪問,則應在基類中聲明爲 private。**我們可以根據訪問權限總結出不同的訪問類型,如下所示:
訪問 | public | protected | private |
---|---|---|---|
同一個類 | yes | yes | yes |
派生類 | yes | yes | no |
外部的類 | yes | no | no |
一個派生類繼承了所有的基類方法,但下列情況除外:
基類的構造函數、析構函數和拷貝構造函數;基類的重載運算符;基類的友元函數。
3.繼承類型
當一個類派生自基類,該基類可以被繼承爲 public、protected 或 private 幾種類型。繼承類型是通過上面講解的訪問修飾符 access-specifier 來指定的。
我們幾乎不使用 protected 或 private 繼承,通常使用 public 繼承。當使用不同類型的繼承時,遵循以下幾個規則:
公有繼承(public):當一個類派生自公有基類時,基類的公有成員也是派生類的公有成員,基類的保護成員也是派生類的保護成員,基類的私有成員不能直接被派生類訪問,但是可以通過調用基類的公有和保護成員來訪問。
保護繼承(protected): 當一個類派生自保護基類時,基類的公有和保護成員將成爲派生類的保護成員。
私有繼承(private):當一個類派生自私有基類時,基類的公有和保護成員將成爲派生類的私有成員。
多繼承
多繼承即一個子類可以有多個父類,它繼承了多個父類的特性。
C++ 類可以從多個類繼承成員,語法如下:
class <派生類名>:<繼承方式1><基類名1>,<繼承方式2><基類名2>,…
{
<派生類類體>
};
其中,訪問修飾符繼承方式是 public、protected 或 private 其中的一個,用來修飾每個基類,各個基類之間用逗號分隔,如上所示。現在讓我們一起看看下面的實例:
// 基類 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;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
Total area: 35
Total paint cost: $2450
三、C++ 重載運算符和重載函數
C++ 允許在同一作用域中的某個函數和運算符指定多個定義,分別稱爲函數重載和運算符重載。
重載聲明是指一個與之前已經在該作用域內聲明過的函數或方法具有相同名稱的聲明,但是它們的參數列表和定義(實現)不相同。
當您調用一個重載函數或重載運算符時,編譯器通過把您所使用的參數類型與定義中的參數類型進行比較,決定選用最合適的定義。選擇最合適的重載函數或重載運算符的過程,稱爲重載決策。
1.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;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
整數爲: 5
浮點數爲: 500.263
字符串爲: Hello C++
2.C++ 中的運算符重載
您可以重定義或重載大部分 C++ 內置的運算符。這樣,您就能使用自定義類型的運算符。
重載的運算符是帶有特殊名稱的函數,函數名是由關鍵字 operator 和其後要重載的運算符符號構成的。與其他函數一樣,重載運算符有一個返回類型和一個參數列表。
Box operator+(const Box&);
聲明加法運算符用於把兩個 Box 對象相加,返回最終的 Box 對象。大多數的重載運算符可被定義爲普通的非成員函數或者被定義爲類成員函數。如果我們定義上面的函數爲類的非成員函數,那麼我們需要爲每次操作傳遞兩個參數,如下所示:
Box operator+(const Box&, const Box&);
下面的實例使用成員函數演示了運算符重載的概念。在這裏,對象作爲參數進行傳遞,對象的屬性使用 this 運算符進行訪問,如下所示:
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,類型爲 Box
Box Box2; // 聲明 Box2,類型爲 Box
Box Box3; // 聲明 Box3,類型爲 Box
double 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;
// 把兩個對象相加,得到 Box3
Box3 = Box1 + Box2;
// Box3 的體積
volume = Box3.getVolume();
cout << "Volume of Box3 : " << volume <<endl;
return 0;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
Volume of Box1 : 210
Volume of Box2 : 1560
Volume of Box3 : 5400
3.可重載運算符/不可重載運算符
下面是可重載的運算符列表:
雙目算術運算符 | + (加),-(減),*(乘),/(除),% (取模) |
---|---|
關係運算符 | ==(等於),!= (不等於),< (小於),> (大於>,<=(小於等於),>=(大於等於) |
邏輯運算符 | (邏輯或),&&(邏輯與),!(邏輯非) |
單目運算符 | + (正),-(負),*(指針),&(取地址) |
自增自減運算符 | ++(自增),–(自減) |
位運算符 | (按位或),& (按位與),~(按位取反),^(按位異或),,<< (左移),>>(右移) |
賦值運算符 | =, +=, -=, *=, /= , % = , &=, |
空間申請與釋放 | new, delete, new[ ] , delete[] |
其他運算符 | ()(函數調用),->(成員訪問)(下標),(逗號) |
下面是不可重載的運算符列表:
.:成員訪問運算符
., ->:成員指針訪問運算符
:::域運算符
sizeof:長度運算符
?::條件運算符
#: 預處理符號
四、C++ 多態
多態按字面的意思就是多種形態。當類之間存在層次結構,並且類之間是通過繼承關聯時,就會用到多態。
C++ 多態意味着調用成員函數時,會根據調用函數的對象的類型來執行不同的函數。
下面的實例中,基類 Shape 被派生爲兩個類,如下所示:
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;
// 調用矩形的求面積函數 area
shape->area();
// 存儲三角形的地址
shape = &tri;
// 調用三角形的求面積函數 area
shape->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;
}
};
修改後,當編譯和執行前面的實例代碼時,它會產生以下結果:
Rectangle class area
Triangle class area
此時,**編譯器看的是指針的內容,而不是它的類型。**因此,由於 *tri 和 rec 類的對象的地址存儲在 shape 中,所以會調用各自的 area() 函數。
正如您所看到的,每個子類都有一個函數 area() 的獨立實現。這就是多態的一般使用方式。有了多態,您可以有多個不同的類,都帶有同一個名稱但具有不同實現的函數,函數的參數甚至可以是相同的。
2.虛函數
虛函數是在基類中使用關鍵字 virtual 聲明的函數。在派生類中重新定義基類中定義的虛函數時,會告訴編譯器不要靜態鏈接到該函數。
我們想要的是在程序中任意點可以根據所調用的對象類型來選擇調用的函數,這種操作被稱爲動態鏈接,或後期綁定。
(1)純虛函數
您可能想要在基類中定義虛函數,以便在派生類中重新定義該函數更好地適用於對象,但是您在基類中又不能對虛函數給出有意義的實現,這個時候就會用到純虛函數。
我們可以把基類中的虛函數 area() 改寫如下:
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
// pure virtual function
virtual int area() = 0;
};
告訴編譯器,函數沒有主體,上面的虛函數是純虛函數。
3.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;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
Total 60
上面的類把數字相加,並返回總和。公有成員 addNum 和 getTotal 是對外的接口,用戶需要知道它們以便使用類。私有成員 total 是用戶不需要了解的,但又是類能正常工作所必需的。
設計策略
抽象把代碼分離爲接口和實現。所以在設計組件時,必須保持接口獨立於實現,這樣,如果改變底層實現,接口也將保持不變。
在這種情況下,不管任何程序使用接口,接口都不會受到影響,只需要將最新的實現重新編譯即可。
4.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++ 程序中,任何帶有公有和私有成員的類都可以作爲數據封裝和數據抽象的實例。請看下面的實例:
#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;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
Total 60
上面的類把數字相加,並返回總和。公有成員 addNum 和 getTotal 是對外的接口,用戶需要知道它們以便使用類。私有成員 total 是對外隱藏的,用戶不需要了解它,但它又是類能正常工作所必需的。
設計策略
通常情況下,我們都會設置類成員狀態爲私有(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():
// 基類
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;
}
當上面的代碼被編譯和執行時,它會產生下列結果:
Total Rectangle area: 35
Total Triangle area: 17
從上面的實例中,我們可以看到一個抽象類是如何定義一個接口 getArea(),兩個派生類是如何通過不同的計算面積的算法來實現這個相同的函數。
設計策略
面向對象的系統可能會使用一個抽象基類爲所有的外部應用程序提供一個適當的、通用的、標準化的接口。然後,派生類通過繼承抽象基類,就把所有類似的操作都繼承下來。
外部應用程序提供的功能(即公有函數)在抽象基類中是以純虛函數的形式存在的。這些純虛函數在相應的派生類中被實現。
這個架構也使得新的應用程序可以很容易地被添加到系統中,即使是在系統被定義之後依然可以如此。