C++之中this指針與類的六個默認函數小結

       我們先來看看this指針。之前看過一篇關於this指針的文章,覺得寫的很好,今天決定自己來寫一寫,順便總結一下C++裏面關於類的一些內容。

       什麼是this指針呢?簡單的說它是一個指向類的實例的指針,就好像當我們在進入一個房子之後,可以看見房子裏的桌子,椅子、地板等, 但是看不到房子的全貌。對於一個類的實例來說,你可以看到它的成員函數、成員變量,但是實例本身呢?this是一個指針,它時時刻刻指向這個實例。

來看看this指針的特性:

1)this指針的類型是一個類類型 * const, 這表示什麼呢?如果你想在你的成員函數之中改變你的this指針的指向,很顯然,做不到!

2)不過當你在使用sizeof操作符的時候千萬要注意,不要把this指針的大小考慮進去,這是爲什麼呢?this指針本身不佔用大小,它並不是對象的一部分,因此不會影響sizeof的結果。

3)this指針是類成員函數的第一個默認隱含參數,因此不需要你顯示地傳入,你在類成員函數之中可以直接使用this指針。

4)this指針的作用域是在非靜態函數的內部,在成員函數(非靜態)開始前構造,在成員函數(非靜態)結束後消除(下面會分析爲什麼是在非靜態函數內部才能使用)

5)this指針的傳入方式,這裏不得不提函數的調用約定_thiscall,如果參數確定,那麼this指針是通過ecx傳遞給被調用者的,若參數不確定,那麼this指針在所有參數壓入之後在壓入。

(_thiscall的調用約定如下:

這 是 C++ 語言特有的一種調用方式,用於類成員函數的調用約定。如果參數確定,this 指針存放於 ECX 寄存器,函數自身清理堆棧;如果參數不確定,this指針在所有參數入棧後再入棧,調用者清理棧。__thiscall 不是關鍵字,程序員不能使用。參數按照從右至左的方式入棧。


6)this指針不能再初始化列表之中使用,原因是在初始化列表之中,類的對象還沒有創建,編譯器不知道對象的結構,因此不知道應該爲其分配多大的空間。

接下來就進入C++之中關於類的六個默認函數。先來看一張圖:


我們就按順序說起:
                                                                                           一、構造函數

       什麼是構造函數呢?但凡想要理解一個事物,必須要從它的定義入手。構造 函數它是一個特殊的成員函數,1)他沒有返回值。2)它的名字與類的名字相同。3)創建類類型對象的時候由編譯器自動去調用。4)在對象的生命週期之內且 只調用一次,從而保證每一個都有一個合適的初始值。

再來看一下構造函數的特性:

       首先構造函數允許重載, 最爲常見的構造函數的重載就是我們所熟知的拷貝構造函數。不但如此,你也可以顯示的定義你的構造函數,如:Date(){} 裏面可以什麼也不寫,這就是一個最簡單的構造函數(什麼也不寫的構造還函數,你可以去看彙編代碼,還是做了些事情的),只要你滿足重載的條件(不過要注意 會不會產生二義性),你可以定義多個構造函數(通過參數可以選出你所要調用的構造函數)。當然如果你什麼也沒有寫,那麼編譯器就會幫你自動生成一個構造函 數,當然這個構造函數也不會幫你做什麼。

       其次構造函數允許定義缺省構造函數, 比如上面定義的那個什麼也不寫的構造函數就是一個缺省構造函數,缺省構造函數可以分爲兩種,第一種就是參數列表之中什麼也不寫的,稱爲無參的構造函數。第 二中就是參數列表之中的每一個參數都對應有一個默認值,稱爲全缺省構造函數。但是要注意的是這兩個構造函數之能顯示的給出一個,否則如果你什麼參數也不 寫,兩個構造函數都可以調用,但是編譯器不知道到底應該調用哪一個,因此會產生二義性問題。

       還有就是構造函數不允許使用const修飾,因爲構造函數需要改變參數的內容,所以它是要修改參數(this指針指向的對象中的內容)。

       最後一點就是關於默認函數可以使用初始化列表, 但是要注意初始化列表是按照類中原有的對象的順序進行初始化的,與變量在初始化列表中的寫的順序無關,因此特別要注意最好不要用變量來初始化另一個變量, 當然如果你的參數沒有全部在初始化列表中去初始化,那麼,沒有出現的變量也會初始化(初始化爲0xcccccccc),千萬不要以爲只有初始化列表裏列出 來的 成員變量纔在執行構造函數體之前進行初始化的。事實上,即使沒有在初始化列表中出現,所有成員變量仍然是在初始化這一步驟(也就是在執行構造函數的函數體 前)完成初始化的。所以,我們常常在構造函數的函數體內對變量進行初始化,實際是非常浪費和降低效率的。應該養成用初始化列表進行賦初值的習慣。

        順便在這裏提一下什麼情況下使用初始化列表呢?有三種情況必須使用初始化列表

1)非靜態const數據成員(即沒有static修飾的)

2)引用數據成員

3)類類型對象(該類之中沒有缺省構造函數)

最後說一下構造函數的作用:1)構造對象   2)初始化對象   3)類型轉化

                                                                    默認構造函數

       上面其實也提到了默認構造函數,其實默認構造函數就是,如果你沒有顯示的定義一個構造函數,那麼編譯器就會幫你生成一個構造函數。不過需要注意的是,如果 你這個生成的默認構造函數什麼也不做,那麼編譯器會對其進行優化,你會發現你無法在彙編之中看到代碼,那並不是沒有產生默認構造函數,只不過是編譯器進行 了優化。(比如你的一個Date類的對象之中有一個time類的對象,而time類對象有缺省構造函數的時候,哪怕你在Date類之中沒有構造函數u,編 譯器會給你生成一個默認構造函數,而且你可以在彙編之中看到,因爲這個默認構造函數是有意義的)

       還有一點要說的就是,只要你顯式定義了構造函數,即使該構造函數什麼也不做,編譯器也不會爲該類合成默認的構造函數編譯器生成的默認構造函數使用與變量初始化相同的規則來初始化成員,具有類類型的成員通過運行各自的默認構造函數來進行初始化。內置和符合類型的成員如指針、數組,只對定義在全局作用域中的對象初始化,當對象定義在局部作用域時,內置和符合類型的成員不進行初始化。在某些情況下,默認構造函數是由編譯器隱式使用的。

                                                         二、拷貝構造函數   

       只有單個形參,而且該形參是對本類類型對象的引用(常用const修飾),這樣的構造函數稱爲拷貝構造函數。拷貝構造函數是特殊的構造函數,創建對象時使 用已存在的同類對象來進行初始化,由編譯器自動調用。要注意的是,拷貝構造函數參數傳遞的是引用,如果參數不是引用,那麼如果採用值傳遞,那麼在傳遞過程 之中又會產生一個臨時變量,而這個臨時變量的產生又需要使用拷貝構造函數,於是一個無限遞歸問題就產生了。
      關於拷貝構造其實沒什麼好多說了,重點是知道什麼失手我們使用到了拷貝構造函數。下面來看看一些使用到了
拷貝構造函數的場景。

1)利用對象進行傳參的時候,如:

    void Fun(const Date date)  
    {}  

2)利用對象實例化另一個對象的時候,如:

    Date d1(1999, 1, 1);  
    Date d2(d1);  

3)利用參數作爲返回值的時候,有時候會返回一個類的對象。


                                                                      三、析構函數

        析構函數,顧名思義,與構造函數的功能相反,析構函數是在對象被銷燬時,由編譯器自動調用,完成類的一些資源清理和汕尾工作。

來看一看析構函數的特點:

1)一個類裏面只有一個,對象被銷燬的時候只調用一次。

2)不能有參數,也不能有返回值,因此析構函數不能重載

3)如果沒有顯示的給出,編譯器會默認生成一個。

4)在對象生命週期結束的時候,由編譯器自動調用。

5)析構函數在函數體內並不是刪除對象,而是做一些清理工作。這裏有一點不得不提一 下:用delete或free來銷燬對象時,會調用其析構函數,並將所佔的全局堆內存空間返回。但從銷燬對象到程序退出該作用域,對象的指針還存在於棧 中,並指向對象本來的位置。顯然,這種情況下調用指針是非常危險的。Win32平臺下訪問這種指針,結果有三種可能情況:訪問違例、取得無意義值、取得其 他對象。第一種會導致進程崩潰,後兩種雖然不會立即崩潰,但是可能會有不可預測的行爲操作或造成對象不必要的變化,需要謹慎避免。

6)析構順序:利用棧的特性,先進後出,所以對後進的對象先進行析構,與構造函數生成對象的順序相反。

                                                          四、運算符的重載(後面三個默認函數)

      在這裏提到了運算符重載,於是我把後面的三個重載放在一起講。先來了解一下什麼叫做運算符重載(或者叫做操作符重載),重載操作符是具有特殊函數名的函 數,關鍵字operator後面接需要定義的操作符符號。操作符重載也是一個函數,具有返回值和形參表。它的形參數目與操作符的操作數目相同,函數調用操 作符可以接受任意數目的操作數。

(在這裏還要說明一下,一般來說,我們在操作符重載的時候,既可以將其寫成一個類的友元函數,同樣也可以將其寫成一個類的成員函數。這裏具體情況具體分析,一般來說,像+、-、*、/我習慣於寫成友元函數,而像++、--之類的我習慣於寫成類的成員函數,一般將算術操作符定義爲非成員函數(友元函數),將賦值運算符定義成員函數

格式:返回類型 operate 操作符(參數列表);

 可以被重載的操作符:


 不可以被重載的操作符:

 1、不能通過連接其他符號來創建新的操作符:比如operator@;
   void operator @(){}

2、重載操作符必須有一個類類型或者枚舉類型的操作數

	1. int operator +(const int _iNum1 , const int _iNum2 )   // 報錯  
	2. {  
	3.     return ( _iNum1 + _iNum2);  
	4. }  
	5.   
	6. typedef enum TEST {one ,two ,three };  
	7. int operator+(const int _iNum1 , const TEST _test )  
	8. {  
	9.      return _iNum1;  
	10. }  


3、用於內置類型的操作符,其含義不能改變,例如:內置的整型+,不能改變其含義

4、重載前後操作符的優先級和結合性是不變的


5、不在具備短求值特性重載操作符不能保證操作符的求值順序,在重載&&和||中,對每個操作數
   都要進行求值,而且對操作數的求值順序不能做規定,因此:重載&&、 ||和逗號操作符不是好的做法。

6、作爲類成員的重載函數,其形參看起來比操作數數目少1成員函數的操作符有一個默認的形參this,限定爲第一個形參。

    CTest operator+(const CTest test1, const CTest test2)const   // 報錯  
    {  
         return test1;  
    }  
      
    CTest operator+(const CTest test1)const  
    {  
         return test1;  
    }  
7、一般將算術操作符定義爲非成員函數,將賦值運算符定義成員函數
8、操作符定義爲非類的成員函數時,一般將其定義爲類的友元
9、== 和 != 操作符一般要成對重載
10、下標操作符[]:一個非const成員並返回引用,一個是const成員並返回引用
11、解引用操作符*和->操作符,不顯示任何參數
13、自增自減操作符
    前置式++/--必須返回被增量或者減量的引用
    後綴式操作符必須返回舊值,並且應該是值返回而不是引用返回
14、輸入操作符>>和輸出操作符<<必須定義爲類的友元函數

【建議】
   使用重載操作符,可以令程序更自然、更直觀,而濫用操作符重載會使得類難以理解,在實踐中很少發生明顯的操作符重載濫用。但有些程序員會定義 operator+來執行減法操作,當一個重載操作符不明確時,給操作符取一個名字更好,對於很少用的操作,使用命名函數通常比用操作符好,如果不是普通 操作,沒有必要爲簡潔而用操作符。

        

順便在這裏分析一下static、const、extern的相關內容。

                                                                         “詭異”的static

        首先來看static,同樣的先來看看static是什麼?static是一個關鍵字,被他修飾的變量稱爲靜態變量,聲明爲static的類成員(成員數據或成員函數)稱爲類的靜態成員。

來看一看static的特性

1)static在類中只是聲明,必須要在類外初始化,並且要加上類的作用域(此時不需要再帶上static關鍵字)。

2)靜態成員爲所有類對象所共享,不屬於某個具體的實例。存放於靜態區。

3)類靜態成員即可用類名::靜態成員或者對象.靜態成員來訪問。

4)類的靜態成員函數沒有默認的this指針,因此在它裏面不能使用任何非靜態成員。

5)靜態成員和類的普通成員一樣,也有public、protected、private3種訪問級別,也可以具有返回值,const修飾符等參數。

6)靜態成員函數調用方式爲_cdcal。

7)靜態成員函數不能訪問非靜態成員變量,同樣也不能調用非靜態成員函數,原因是靜態 成員函數裏面不能使用this指針,如果你非要訪問,那麼你可以把對象作爲參數傳進來,利用這個對象去訪問非靜態成員變量。(但是非靜態成員函數卻可以調 用靜態成員函數,因爲本質上靜態成員函數還是一個成員函數,可以通過this指針去訪問到它。)

                                                                      “嚴格”的const

        再來看一看const,還是先從定義入手,如const int a 這句語句,在C++之中表示a是一個常量,但是在C語言中則表示a是一個不可修改的變量。簡單的說就是給一個變量賦予常屬性。

再來看一看const的特性

1)const修飾形參,一般和引用同時使用。(一般只出現在類的賦值函數中,目的是爲了實現鏈式表達
2)const修飾返回值。(這裏有必要說一下:若函數的返回值是指針,且用const修飾,則函數返回值指向的內容是常數,不可被修改,此返回值僅能賦值給const修飾的相同類型的指針。如果函數返回值採用“值傳遞方式”,由於函數會把返回值複製到外部臨時的存儲單元中,加const 修飾沒有任何價值。)
3)const修飾類數據成員,必須在構造函數的初始化列表中初始化。
4)const修飾類成員函數,實際修飾隱含的this,表示在類中不可以對類的任何成員進行修改。
5)在const修飾的成員函數中要對類的某個數據成員進行修改,該數據成員定義聲明是必須加mutable關鍵字。
6)c語言中const修飾的變量爲不可修改的變量。

來看一下下面一些const使用場景問題:

1.const對象可以調用非const成員函數和const成員函數嗎?非const對象可以調用非const成員函數和const成員函數嗎?

       這是不可以的,const類型使用的時候能做的有限,可以理解爲權利較小,但是非const修飾的對象權利較大,可能一不小心就改了裏免得內容,因 此,const對象只能訪問const成員函數。因爲const對象表示其不可改變,而非const成員函數可能在內部改變了對象,所以不能調用。而非 const對象既能訪問const成員函數,也能訪問非const成員函數,因爲非const對象表示其可以改變。

2.const成員函數內可以調用其它的const成員函數非const成員函數嗎?非const成員函數內可以調用其它的const成員函數非const成員函數嗎?
       同樣的與上面的情況類似,const成員函數表示裏面的內容不能修改,權利很小,但是非const權利較大,可能會修改掉const成員函數裏面的內容, 因此 const成員函數是不會改變類的數據成員的值的 但是非const 成員 函數是會改變的 因此 const 成員 函數是不能調用 非const 成員的。只能調用const成員函數。而非const成員函數既能調用const成員函數,也能調用非const成員函數,因爲非const對象表示其可以改變。

                                                                          

                                                                           “神奇”的extern

        老樣子,還是從定義入手,extern在源文件A裏定義的函數,在其它源文件裏是看不見的(即不能訪問)。爲了在源文件B裏能調用這個函數,應該在B的頭部加上一個外部聲明


1. 聲明外部實體

聲明外部全局變量或對象,一般用於頭文件中,表示在其它編譯單元內定義的變量,鏈接時進行外部鏈接,如:
extern int ivalue;
此時的extern是必須的,省略了extern編譯器將視爲定義而不是聲明,一般地在源代碼中定義變量並進行初始化,在頭文件中使用extern聲明變量。

類似地用於聲明外部全局函數,表示該函數在其它編譯單元中定義,如:
extern void func( void );此時的extern可以省略。

extern   函數原型;  
  這樣,在源文件B裏也可以調用那個函數了。  
  注意這裏的用詞區別:在A裏是定義,在B裏是聲明。一個函數只能(也必須)在一個源文件裏被定義,但是可以在其它多個源文件裏被聲明。定義引起存儲分配, 是真正產生那個實體。而聲明並不引起存儲分配。打一個粗俗的比方:在源文件B裏聲明後,好比在B裏開了一扇窗,讓它可以看到A裏的那個函數。

 

#include "stdafx.h"
  1.extern用在變量聲明中常常有這樣一個作用,你在*.c文件中聲明瞭一個全局的變量,這個全局的變量如果要被引用,就放在*.h中並用extern來聲明。
  2.如果函數的聲明中帶有關鍵字extern,僅僅是暗示這個函數可能在別的源文件裏定義,沒有其它作用。即下述兩個函數聲明沒有區別:
  extern int f(); 和int f();
  ================================
  如果定義函數的c/cpp文件在對應的頭文件中聲明瞭定義的函數,那麼在其他c/cpp文件中要使用這些函數,只需要包含這個頭文件即可。
  如果你不想包含頭文件,那麼在c/cpp中聲明該函數。一般來說,聲明定義在本文件的函數不用“extern”,聲明定義在其他文件中的函數用“extern”,這樣在本文件中調用別的文件定義的函數就不用包含頭文件
  include “*.h”來聲明函數,聲明後直接使用即可。
  ================================
  舉個例子:
  

 //extern.cpp內容如下:  
    
  // extern.cpp : Defines the entry point for the console application.  
  //  
    
  #i nclude "stdafx.h"  
  extern print(char *p);  
  int main(int argc, char* argv[])  
  {  
   char *p="hello world!";  
   print(p);  
   return 0;  
  }  
  //print.cpp內容如下  
  #i nclude "stdafx.h"  
  #i nclude "stdio.h"  
  print(char *s)  
  {  
   printf("The string is %s/n",s);  
  }  


  結果程序可以正常運行,輸出結果。如果把“extern”去掉,程序依然可以正常運行。
  
  由此可見,“extern”在函數聲明中可有可無,只是用來標誌該函數在本文件中定義,還是在別的文件中定義。只要你函數在使用之前聲明瞭,那麼就可以不用包含頭文件了。
  
    VC++6.0中常出現的"unexpected end of file while looking for precompiled header directive"的問題?

    如何解決:"fatal error C1010:VC++6.0中常出現的"unexpected end of file while looking for precompiled header directive"的問題?

    我想大家在VC6.0中經常回遇到這樣的問題,如何解決呢?

 1、看看是否缺少“;”,“}”  
 如:類,結構體後面的分號
 隱藏得深的是宏、.h文件的問題就要費點心思了

 2、一定是你在類的部分定義被刪除了,M$在每個類中定義一些特殊的常量,是成對的,如下:

    .h:  
    #if !defined(AFX_CHILDFRM_H__54CA89DD_BA94_11D4_94D7_0010B503C2EA__INCLUDED_)  
    #define AFX_CHILDFRM_H__54CA89DD_BA94_11D4_94D7_0010B503C2EA__INCLUDED_  
    .......  
    //{{AFX_INSERT_LOCATION}}  
    // Microsoft Visual C++ will insert additional declarations immediately before the previous line.  
      
    #endif // !defined(AFX_MAINFRM_H__54CA89DB_BA94_11D4_94D7_0010B503C2EA__INCLUDED_)   

 你可以新建一個類,然後把這些拷貝過去或補上就可以了。  
 3、在頭部加入 #include "stdafx.h"

 4、在CPP文件第一行加上#include "stdafx.h"。
 或者Rebuild All. 

 5、

 (1). [Project] - [Settings] - [C/C++] - [Category]
 (2). 選擇 [Precomplied Headers]
 (3). 單選 [Not Using Precomplied Headers]
 (4). [OK]


 如果以上不能解決問題,那麼就請看以下內容.引起這樣的錯誤,有可能你只是增加了一個.H和.CPP的文件.這時你就要按上面所說.
名含"stdafx.h"即可.如果還要在多個文件裏同時使用結構類型,你就要繼續向下看了.一定會有不少收穫的.

 類型的定義和類型變量的定義不同,
 類型定義只是描述一個類型,
 是給編譯器看的,
 不會產生可執行代碼。
 變量定義是指在執行文件中真實得存在這麼一塊內容。

 因爲每個.c裏都要寫清楚類型定義很麻煩,
 所以一般都把類型定義寫在.h裏
 ,而在.c裏採用簡單的寫法,如struct A a;
 這樣定義變量,
 不需把整個類型的描述再寫一遍。

 ------------------------------------------------------------------------
 所以,struct類型定義放到 XX.h裏面,
 XX.cpp 里加struct str st_r;
 XXXXX.cpp加上#i nclude "XX.h"
 然後直接使用extern struct str st_r;


2. 聲明函數的編譯和鏈接方式

extern 後可以跟”C”或”C++”用於聲明全局函數的編譯和鏈接方式,例如:
extern “C” void add( int a, int b);
extern “C++” void sum(int* ia, int leng);
void sum(int* ia, int leng);
其中的extern “C++”可以省略,它是在C++中默認的鏈接方式,即後面兩種聲明方式是等效的。這種聲明有兩種含義:首先,聲明這些函數使用外部鏈接方式,其實現不在 本編譯單元之內;另一種含義,則是告訴編譯器編譯方式,如extern “C”則是告訴編譯器使用C語言的編譯方式編譯該函數。

C++支持函數重載,所以參數不同在編譯後生成的函數名也不同,如:
int max(int a, int b);
int max(float a, float b);
在編譯時生成的函數名可能分別爲_max_int_int、_max_float_float,通過在函數名後加上參數類型來區分不同的函數,如果使用C 語言方式,則生成的函數名中不包含參數信息,只生成_max,所以無法實現重載,也就是說在extern “C”中不能出現函數名重載,例如:

extern “C”{
int max(int a, int b);
int max(float a, float b);
}


非法,編譯器將報錯。而C++標準中並沒有定義extern “C”與extern “C++”的具體實現方式,不同編譯器生成的符號規則可能不同。

需要注意的是,如果函數聲明使用了extern “C”,則函數定義必須使用C編譯器編譯,或者使用extern “C”來修改函數的編譯方式,一般地將extern “C”聲明的函數的定義所在的源程序擴展名使用.c即可,而C++代碼放在.cpp文件中。如果將extern “C”聲明的函數實現也放在.cpp中,則需要使用extern “C”來聲明函數編譯方式,例如:
extern “C” {
int max( int a, int b) { return a > b ? a : b; }
}


只有在C++中使用C語言的庫或者兩種語言混合編程的時候纔會用到extern “C”,而在C語言中是不支持extern “C”的,所以爲了頭文件通用,需要使用宏來控制,例如:
#ifndef MAX_H // 防止重複引用
#define MAX_H
#ifdef __cplusplus
extern "C" {
#endif
int max (int a, int b);
#ifdef __cplusplus
}
#endif
#endif
其中__cplusplus爲C++定義的宏,凡是C++的編譯器都定義了該預編譯宏,通過它來檢測當前編譯器是否使用的是C++編譯器。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章