什麼是類
C++是什麼?C++設計之初就是class with c,所以簡單點說,C++就是帶類的C,那麼什麼是類?
類,簡單點說就是類型,在C++中我們一開始所接觸的類型有如下幾種:
//+-------------------
char,
short,
int,
long,
long long,
float,
double
……
//+------------------
這些類型屬於語言本身的一部分,我們稱之為基本類型,基本類型不可被更改,也不可被創造,更不可被消滅,任何一個程序都是有基本類型搭建起來的,比如,我們想要用一個類型來表示一個學生,那麼我們可以char*,來表示他的名字,用unsigned int來表示他的學號,用double來表示他的成績等等,而這個表示學生信息的類型是由我們自定義而來,所以我們稱之為自定義類型,在C語言裡面,如果我們想要自定義一個類型出來,那麼我們只能用關鍵字struct或者union,常使用的是struct,而union僅僅用於某些特殊的場合,所以我們可以按如下的放下來定義一個自定義類型:
//+-----------------
typedef struct UserType{
int a;
double b;
long long c;
}* __LPUSERTYPE;
//+----------------
這個自定義類型由三個數據段組成,當然如果你要問我幹嘛這麼定義,我想應該必要的時候會這麼定義吧。那麼,既然說C++是帶類的C,那麼在C++裡面擴展一個自定義類型又該如何呢?當然,如果我說class在很多時候等同於struct的話那麼這個問題是不是就不再是問題了呢?ok,如果剛才的那個UserType到底表示什麼都不清楚的話,那麼下面我們嘗試用一種能夠說清楚的類型來闡述自定義類型的定義:
//+---------------
class Point{
public:
double x;
double y;
};
//+---------------
class : C++ 關鍵字,表示接下來要定義一個類型啦。
Point : 類型名,總是跟在class的後面,指明類型名是什麼,class 和 類型名的中間還可以有其他的東西,比如我們在寫com的時候使用的uuid,比如我們要導出一個類時候使用的__declspec(dllexport)等。
{} : class 的代碼段。
在 C++ 裡面,class 是一句完整的C++語句,C++語句都是以";"結束,所以在"}"後面需要要用表示結束的";"號,否則你會遇到各種你預想不到的錯誤,當然,該語法對於C語言的struct也同樣實用。
那麼class和struct又有什麼區別呢?在C語言裡面,struct裡面所定義的數據類型都是可以直接訪問的,簡單點說C語言的struct的數據是共有的,同時C語言裡的struct裡面不可以有成員函數,當然這個限制在C++中已經被摒棄,在C++中,struct和class的唯一區別就是默認權限的區別,在C語言中沒有權限的概念,但C++作為面向對象的編程語言,所以自然提供了權限的概念,以便於數據的封裝,只是struct的默認權限是public,而class的默認權限是private,public顧名思義是公共的,private是私有的,當然除了public和private外還存在一個權限:protected,private和protected所限制的數據都是外部不能夠訪問的,那麼他們的區別是什麼呢?private是純粹的對數據進行封裝,protected不但對數據進行封裝,還對繼承留下一個後門。如你們所見,這裡我們使用的plubic權限,public後面必須跟有":"號,所以在public下面的接口或者數據都是外部能夠直接訪問得到的。
那麼在C++中,我們什麼時候使用struct什麼時候使用class呢?這裡沒有什麼標準規範來限制,所以簡單點說就是凡是使用struct的地方都可以使用class來替換,反之亦然,但是,通常於C++來說有個不成文的規矩,那就是如果僅僅只是簡單的定義一個組合類型的話我們使用struct,否則我們都應該使用class。
構造函數
什麼是構造函數,從名字上面來理解,我們可以簡單的認為就是構造對象的函數,一個類型想要被實例化,那麼它首先調用的便是這個構造函數,而從代碼的角度來理解的話構造函數就是名字和類型名一樣的函數,該函數可以有參數,但沒有返回值,如果該函數沒有參數,那麼該函數被稱為默認構造函數。
Point p;
這句代碼直觀上理解我們可能誰都清楚,但是現在我們想要知道,當我們定義一個Point的對象p的時候我們實際都經歷了些什麼?
第一,在棧上獲取了一塊內存。
第二,調用了Point的構造函數。
什麼?調用了Point的構造函數?Point的構造函數是什麼鬼?說好的和Point同樣名字的函數怎麼沒看到呢?嗯,這就是我們要說的默認構造函數,也就是說,當我們不給我們的類指定構造函數的時候編譯器會為我們生成默認的構造函數,而這個函數什麼,所以雖然我們已經構造出一個Point的對象p出來,但是p裡面的x和y是未初始化的,所以接下來我們需要針對xy進行各自的初始化,所以無論出於什麼樣的理由,我們應該給Point添加相應的構造函數。
//+-----------------
class Point{
public:
Point(double __x = 0.0,double __y = 0.0):x(__x),y(__y){}
private:
double x;
double y;
};
//+-----------------
這次我們不但添加了構造函數,同時還將數據段放在private裡面,我們將通過構造函數對數據進行初始化。該構造函數我們使用兩個double類型作為參數,並且兩個參數都有默認值,所以我們下面的代碼:
Point p;
將等同於:
Point p(0.0,0.0);
此時的x y的值分別都是0.0
:x(__x),y(__y)
在構造函數的括號後面有個冒號,冒號後面跟了一段代碼,這段代碼叫初始化列表,我們的xy便是在這裡進行初始化的,我們使用第一個參數對x進行初始化,使用第二個參數對y進行初始化。當然我們也可以不適用初始化列表:
//+------------------
class Point{
public:
Point(double __x = 0.0,double __y = 0.0){
x = __x;
y = __y;
}
private:
double x;
double y;
};
//+-------------------
這樣的構造函數也是隨處可見的,只是這樣的寫法和上面的寫法有些不同(這不是廢話嗎?只要不瞎一看就是不同),哦!我這裡說的不同是指效率上面的不同,好吧,我們來剖析一下為什麼不會不同,我們先來看看要實例化一個類我們所要經歷的步驟:
第一,構造數據成員
第二,執行構造函數
所以,在執行構造函數之前xy已經被實例化出來了,所以當我們執行 x = __x 時又經歷了一個複雜的過程,這個過程後面細說(但是由於我們此處使用的是基本類型,所以這個過程也就被忽略啦,如果是自定義類型的話這期間又會有各種問題的產生),所以不管怎麼說,我們應該優先選擇使用初始化列表的方法來對數據進行初始化。
我們自定義一個類型目的就是為了使用他所封裝的數據,但是像我們的Point類就是一個鐵公雞,就是說我們可以將數據放進去,但取不出來,嗯,這是一個問題,解決這個問題的方法可以將數據段的private提升為public,這可以,但……
//+-----------------
void dealPoint(const Point& p) {
const_cast
}
int main()
{
Point p{ 100,200 };
dealPoint(p);
std::cout << p.x << std::endl;
std::cin.get();
return 0;
}
//+----------------
p的x的值直接被修改,當然有些時候我們可能想要這麼幹,但是這往往會帶來意向不到的災難性後果,因為你不知道什麼時候哪根神經搭錯了忽然間很想修改這個值。所以合理的做法應該是我們提供有方法直接訪問內部需要訪問的東西。
//+----------------
class Point{
public:
Point(double __x = 0.0,double __y = 0.0):x(__x),y(__y){}
double get_x() const{return x;}
double get_y() const{return y;}
void set_x(double __x){x = __x;}
void set_y(double __y){y = __y;}
void set(double __x,double __y){
x = __x;
y = __y;
}
private:
double x;
double y;
};
int main()
{
Point p(200, 300);
std::cout << p.get_x()<
std::cin.get();
return 0;
}
//+----------------
複製構造函數
如果我們有一個Point,我們想要當前的Point去構造出一個相同的Point的時候我們應該怎麼說呢?
Point p(200, 300);
Point p2(p);
就目前來說,如果我們寫出這樣的代碼,編譯通過是完全沒問題的,同時運行也不會有任何問題。因為上面的第二句代碼執行的並不是默認的構造函數,而是默認的複製構造函數,什麼是複製構造函數呢?
複製構造函數就是函數名和類型一樣,沒有返回類型,而參數是該類型,如果我們不指定複製構造函數的話那麼編譯器會為我們的類升成默認的複製構造函數,所以上面的第二行代碼執行的便是複製構造函數,最終是p == p2.
那麼怎麼編寫複製構造函數呢?如下:
//+---------------
class Point{
public:
Point(double __x = 0.0,double __y = 0.0):x(__x),y(__y){}
Point(const Point& p):x(p.x),y(p.y){}
double get_x() const{return x;}
double get_y() const{return y;}
void set_x(double __x){x = __x;}
void set_y(double __y){y = __y;}
void set(double __x,double __y){
x = __x;
y = __y;
}
private:
double x;
double y;
};
//+------------------
賦值操作符
編譯器會為class生成的不只有默認的構造函數和默認的複製構造函數,同時還會生成默認的賦值操作,正因為有這個默認的賦值操作符,所以我們下面的代碼才會通過編譯:
Point p(200,300); // 調用構造函數
Point p2 = p; // 調用複製構造函數
Point p3; // 調用默認的構造函數
p3 = p2 // 調用默認的賦值操作符
如果我們不使用編譯器為我們準備的默認操作符的話,我們可以自己編寫我們的賦值操作符,賦值操作符是這樣的一個函數:
T& operator=(const T& other);
T 是我們的自定義類型。
所以如果我們自己編寫賦值操作符,應該這樣來:
//+-----------------
class Point{
public:
Point(double __x = 0.0,double __y = 0.0):x(__x),y(__y){}
Point(const Point& p):x(p.x),y(p.y){}
Point& operator=(const Point& other){
if(this == &other)
return *this;
x = other.x;
y = other.y;
return *this;
}
double get_x() const{return x;}
double get_y() const{return y;}
void set_x(double __x){x = __x;}
void set_y(double __y){y = __y;}
void set(double __x,double __y){
x = __x;
y = __y;
}
private:
double x;
double y;
};
//+---------------------
一個空類
//+-----------------
class Empty{};
//+----------------
當我們寫下上面的類的時候,意味著我們寫了些什麼?
1,默認構造函數。
2,默認的複製構造函數
3,默認的賦值操作符
4,默認取地址操作符(該操作符的重載此處不做解釋,熟悉之後自然也就明白了,所以該函數在一般的教科書中是不當作默認實現的函數,因為它本該存在)
5,析構函數
實際等同於:
//+-----------------
class Empty{
public:
Empty(){}
~Empty(){}
Empty(const Empty& other){}
Empty& operator=(const & Empty& other){return *this;}
};
//+-----------------
第一次我們引入析構函數,析構函數和構造函數相對應,構造函數初始化資源,所以析構函數的功能就是清理資源,那麼什麼時候需要我們自己實現構造函數呢?那就是當我們有資源需要我們手動釋放的時候,比如堆上的指針,比如com對象的Release等等,如果說上面我們所舉的Point例子其實是不需要複製操作符和複製構造函數的話(因為默認的就很好),那麼我們現在來說一個我們必須要手賦值制操作符和複製構造函數的例子——字符串處理類,String。
在C++裡面字符串有char*表示,但是純粹的時候char*太過原始,一點都不對象,所以通常都會對char*進行封裝,當然想要做一個完備的字符串類出來可不是一件簡單的事,所以這裡只是作為一個例子,我們僅僅實現一些簡單的操作即可:
//+------------------
//
// 簡單的字符串處理類
//
class String{
public:
//
// 構造函數
//
String(const char* str = "") :mData(nullptr){
int len = strlen(str) + 1;
mData = new char[len];
memset(mData, 0, len );
memcpy(mData, str, len - 1);
}
//
// 析構函數
// 該函數絕不能使用缺省的,我們必須手動釋放資源
//
~String(){
if (mData){
delete[] mData;
mData = nullptr;
}
}
//
// 複製構造函數
// 該函數不能使用缺省的,我們必須手動拷貝資源
//
String(const String& str):mData(nullptr){
int len = str.size();
len += 1;
mData = new char[len];
memset(mData, 0, len);
memcpy(mData, str.mData, len - 1);
}
//
// 賦值操作符
// 該函數不能使用缺省的,我們必須手動拷貝資源
//
String& operator=(const String& str){
if (this == &str){
return *this;
}
if (mData != nullptr){
delete[] mData;
mData = nullptr;
}
int len = str.size();
len += 1;
mData = new char[len];
memset(mData, 0, len);
memcpy(mData, str.mData, len - 1);
return *this;
}
//
// 獲取字符串長度
//
unsigned size() const{
if (mData == nullptr){
return 0;
}
return strlen(mData);
}
//
// 下標操作符
//
char& operator[](unsigned index){
if (mData == nullptr || index >= size()){
throw std::out_of_range("operator[](unsigned index)");
}
return mData[index];
}
const char& operator[](unsigned index) const{
return const_cast
}
//
// 檢查字符串是否為空
//
bool empty() const{
return this->size() == 0;
}
//
// 支持流的輸出
//
friend std::ostream& operator<
if (str.empty()){
return os;
}
os << str.mData;
return os;
}
private:
char*mData{ nullptr };
};
//
// 測試代碼
//
int main(){
String str("Hello World");
std::cout <
String str2 = str;
std::cout << "str2 = " << str2 << std::endl;
String str3;
std::cout << str3.empty() << std::endl;
std::cout << str2.size() << std::endl;
str3 = str2;
str3[2] = 'H';
std::cout << str3.empty() << std::endl;
std::cout << str3 << std::endl;
system("pause");
return 0;
}
//+------------------
字符串的操作屬於最基本的操作,但同時也是最有講究的操作,幾乎每一個相對完善的C++類庫都提供有字符串處理類,比如標準庫中的string,MFC和ATL的CString,Qt的QString,CEGUI的String,DuiLib的DuiString等等,所以字符串的處理雖然是基本的操作,卻也是最為重要的操作,網上流傳的C++面試題中更是將字符串的實現作為一大考點,當然這不足為奇,因為要是現在一個完備的字符串類,需要考慮到方方面面的東西,後續我們會提供一個功能強大的字符串類,那麼餘下的就由各位去思考。
閱讀更多 IT布丁老師 的文章