C++學習(46)

王小東大將軍發表於2017-06-16

1.      在宣告類時,關鍵字private/public/protected出現任意次數。(正確)

 

2. 最大公約數的最常用的演算法是歐幾里得演算法,也稱為輾轉相除法.

問題定義為求i和j的最大公約數gcd(i,j),其中i和j是整數,不妨設i>j.

 

演算法可以遞迴的表示:

1).如果j能整除i,那麼gcd(i,j)=j;

2).j不能整除i,令r=i%j,那麼gcd(i,j)=gcd(j,r).

使用C語言實現:

 

int gcd(int i, intj)

{int r = i % j;

return r == 0 ? j: gcd(j, r);

}

正確性分析:

演算法的步驟1,顯然成立(最大公約數定義).關鍵是要證明步驟2.

設d是i和j的最大公約數,

那麼i=md,j=nd,m和n互質(否則d不是最大公約數).

由r=i%j可以得到i=kj+r,k=⌊m/n⌋,k≥1(我們前面假設過i>j).

把i=md,j=nd代入得到

md=knd+r

那麼

r=(m-kn)d

m-kn和m也是互質的.

所以得到d是j和r的最大公約數.

 

時間複雜度分析:

逆著看該演算法,最後的餘數是0,倒數第二次餘數是d,倒數第三次是kd,k>1…

由於組成了一個數列,{0,d,kd,nkd+d,…}

數列的n項加上n+1項,比n+2項要小,所以比斐波納契數列增長的要快.

我們已知斐波納契數列增長速度是指數,那麼待分析的數列也是指數增長.

設歐幾里得演算法需要k次,那麼j=O(2^k),則k=O(lg j).

 

所以歐幾里得演算法求最大公約數的時間複雜度是對數量級的,速度非常快.

 

3.棧VS.堆

:在Windows下,棧是向低地址擴充套件的資料結構,是一塊連續的記憶體的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在WINDOWS下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩餘空間時,將提示overflow。因此,能從棧獲得的空間較小。

 

堆是向高地址擴充套件的資料結構,是不連續的記憶體區域。這是由於系統是用連結串列來儲存的空閒記憶體地址的,自然是不連續的,而連結串列的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬記憶體。由此可見,堆獲得的空間比較靈活,也比較大。

 

在C/C++中,記憶體一般分為,堆區,棧區,全域性區,文字長量區,程式程式碼區!在函式中定義的區域性變數是存在在棧區(除static區域性變數,它是存在在全域性區),動態生成的變數存在在堆區,由指標進行讀寫!全域性變數,靜態全域性變數,靜態區域性變數是存放在全域性區的! 堆是程式設計師進行申請和釋放的,因此堆是向上,也就是向高地址方向的!棧是系統進行釋放的,且棧區大小一般是定的2M,因此棧是向下,也就是向低地址方向! 另外說下,靜態區域性變數,靜態全域性變數和全域性變數的區別,靜態變數沒有初始化時,系統會給預設值,而全域性變數不會!全域性變數在整個工程中都是可見的,而靜態全域性變數只在本檔案中可見,靜態區域性變數只在此函式內部可見,但函式結束後不釋放!

 

對於堆,大量的new/delete操作會造成記憶體空間的不連續。

堆容易產生memory leak。

堆的效率比棧要低的多。

棧變數引用容易造成逃逸。

棧生長方向是向下的,即向著記憶體地址降低的方向。

棧區一般由編譯器自動分配釋放,堆區一般由程式設計師分配釋放。

 

4.執行下列程式的結果:未定義行為。

#include<iostream>
#include<cstdlib>
#include<string.h>
using namespacestd;
void test(void) {
    char *str=(char *)malloc(100);
    strcpy(str,"hello");
    free(str);
    if(str!=NULL) {
        strcpy(str,"world");
        printf(str);
    }
}
int main(intargc,char *argv) {
    test();
    return 0;
}

分析:指標釋放儲存空間後沒有置為NULL,變成野指標。

野指標,不能通過簡單的NULL進行判斷,delete或free只是釋放了指標所指向的記憶體區域,並沒有幹掉指標本身,所以指標指向的是“垃圾”指標,所以free或者delete之後要把指標置為NULL。

 

5.分析下列程式:

#include<iostream>
#include<cstdlib>
#include<string.h>
using namespacestd;
class Myclass {
    public:
        Myclass(int i=0) {
            cout<<i;
        }
        Myclass(const Myclass &x) {
            cout<<2;
        }
        Myclass &operator=(const Myclass&x) {
            cout<<3;
            return *this;
        }
        ~Myclass() {
            cout<<4;
        }
};
 
int main(intargc,char *argv) {
    Myclass obj1(1),obj2(2);
    Myclass obj3=obj1;
    //obj3=obj1;
    return 0;
}

分析:拷貝建構函式發生在物件還沒有建立,需要建立時,如obj3;賦值操作符過載僅發生在物件已經執行過建構函式,即已經建立的情況下。

 

首先程式中存在三個MyClass物件。前兩個物件構造時分別輸出1,2;第三個物件是這樣構造的MyClass obj3 = obj1;這裡會呼叫拷貝建構函式,輸出2;然後三個物件依次析構,輸出444;所以最終輸出122444。

 

MyClass obj3 =obj1; obj3還不存在,所以呼叫拷貝建構函式輸出2,如果obj3存在,obj3=obj,則呼叫複製運算子過載函式,輸出03。

 

6.分析下列程式

#include<iostream>
#include<cstdlib>
#include<string.h>
using namespacestd;
class A {
    public:
        virtual void func(int val=1) {
            cout<<"A->"<<val<<endl;
        }
        virtual void test () {
            func();
        }
};
class B :public A{
    public:
        void func(int val=0) {
            cout<<"B->"<<val<<endl;
        }
};
 
int main(intargc,char *argv) {
    B *p =new B;
    p->test();
    return 0;
}

分析:預設引數是靜態繫結的,對於這個特性,估計沒有人會喜歡。所以,永遠記住:

“絕不重新定義繼承而來的預設引數(Never redefine function’s inherited default parameters v)

 

記住:virtual 函式是動態繫結,而預設引數值卻是靜態繫結。意思是你可能會在“呼叫一個定義於派生類內的virtual函式”的同時,卻使用基類為它所指定的預設引數值。

 

結論:絕不重新定義繼承而來的預設引數值!(可參考《Effective C++》條款37)

 

對於本例:

B*p =newB;p->test();

p->test()執行過程理解:

 

(1)由於B類中沒有覆蓋(重寫)基類中的虛擬函式test(),因此會呼叫基類A中的test();

(2)A中test()函式中繼續呼叫虛擬函式 fun(),因為虛擬函式執行動態繫結,p此時的動態型別(即目前所指物件的型別)為B*,因此此時呼叫虛擬函式fun()時,執行的是B類中的fun();所以先輸出“B->”;

(3)預設引數值是靜態繫結,即此時val的值使用的是基類A中的預設引數值,其值在編譯階段已經繫結,值為1,所以輸出“1”;

 

最終輸出“B->1”。所以大家還是記住上述結論:絕不重新定義繼承而來的預設引數值

 

補充:若是在B類中補充一個test()函式,便可以實現輸出B->0。

 

改動(1)

#include<iostream>
#include<cstdlib>
#include<string.h>
using namespacestd;
class A {
    public:
        virtual void func(int val=1) {
            cout<<"A->"<<val<<endl;
        }
        virtual void test () {
            func();
        }
};
class B :public A{
    public:
        void func(int val=0) {
            cout<<"B->"<<val<<endl;
        }
        void test() {
            func();
        }
};
 
int main(intargc,char *argv) {
    A *p =new B;
    p->test();
    return 0;
}
輸出結果是:B->0

 

改動(2)

#include<iostream>
#include<cstdlib>
#include<string.h>
using namespacestd;
class A {
    public:
        virtual void func(int val=1) {
            cout<<"A->"<<val<<endl;
        }
        virtual void test () {
            func();
        }
};
class B :public A{
    public:
        void func(int val=0) {
            cout<<"B->"<<val<<endl;
        }
        void test() {
            func();
        }
       
};
 
int main(intargc,char *argv) {
    A *pa =new B;
    B *p =new B;
    pa->test();
    pa->func();
    p->test();
    p->func();
    return 0;
}
 


8.一個類A,其資料成員如下:

class A {
    private :
        int a;
    public :
         const int b;
         float * &c;
         static const char * d;
         static double * e;
};

則建構函式中,成員變數一定要通過初始化列表來初始化的是:bc

分析:建構函式初始化時必須採用初始化列表一共有三種情況,

1).需要初始化的資料成員是物件(繼承時呼叫基類建構函式)

2).需要初始化const修飾的類成員

3).需要初始化引用成員資料

 

因為static屬於類並不屬於具體的物件,所以 static成員是不允許在類內初始化的,那麼static const 成員是不是在初始化列表中呢?答案是NO.

一是static屬於類,它在未例項化的時候就已經存在了,而建構函式的初始化列表,只有在例項化的時候才執行。

二是static成員不屬於物件。我們在呼叫建構函式自然是建立物件,一個跟物件沒直接關係的成員要它做什麼呢.

 

補充說明:

1),const定義的常量在超出其作用域之後其空間會被釋放,而static定義的靜態常量在函式執行後不會釋放其儲存空間。

2),static表示的是靜態的。類的靜態成員函式、靜態成員變數是和類相關的,而不是和類的具體物件相關的。即使沒有具體物件,也能呼叫類的靜態成員函式和成員變數。一般類的靜態函式幾乎就是一個全域性函式,只不過它的作用域限於包含它的檔案中。

3),C++中,static靜態成員變數不能在類的內部初始化。在類的內部只是宣告,定義必須在類定義體的外部,通常在類的實現檔案中初始化,如:double Account::Rate = 2.25;static關鍵字只能用於類定義體內部的宣告中,定義時不能標示為static.

4),在C++中,const成員變數也不能在類定義處初始化,只能通過建構函式初始化列表進行,並且必須有建構函式

 

const資料成員只在某個物件生存期內是常量,而對於整個類而言卻是可變的。因為類可以建立多個物件,不同的物件其const資料成員的值可以不同。所以不能在類的宣告中初始化const資料成員,因為類的物件沒被建立時,編譯器不知道const資料成員的值是什麼。

 

const資料成員的初始化只能在類的建構函式的初始化列表中進行。要想建立在整個類中都恆定的常量,應該用類中的列舉常量來實現,或者static cosnt.

9.protected不可在類外訪問。

 

10. 編譯器總是根據型別來呼叫類成員函式。但是一個派生類的指標可以安全地轉化為一個基類的指標。這樣刪除一個基類的指標的時候,C++不管這個指標指向一個基類物件還是一個派生類的物件,呼叫的都是基類的解構函式而不是派生類的。如果你依賴於派生類的解構函式的程式碼來釋放資源,而沒有過載解構函式,那麼會有資源洩漏。所以建議的方式是將解構函式宣告為虛擬函式。

 

也就是delete a的時候,也會執行派生類的解構函式。

 

一個函式一旦宣告為虛擬函式,那麼不管你是否加上virtual 修飾符,它在所有派生類中都成為虛擬函式。但是由於理解明確起見,建議的方式還是加上virtual 修飾符。

 

構造方法用來初始化類的物件,與父類的其它成員不同,它不能被子類繼承子類可以繼承父類所有的成員變數和成員方法,但不繼承父類的構造方法)。因此,在建立子類物件時,為了初始化從父類繼承來的資料成員,系統需要呼叫其父類的構造方法。

 

如果沒有顯式的建構函式,編譯器會給一個預設的建構函式,並且該預設的建構函式僅僅在沒有顯式地宣告建構函式情況下建立。

 

構造原則如下:

1.)如果子類沒有定義構造方法,則呼叫父類的無引數的構造方法。

2.)如果子類定義了構造方法,不論是無引數還是帶引數,在建立子類的物件的時候,首先執行父類無引數的構造方法,然後執行自己的構造方法

3.)在建立子類物件時候,如果子類的建構函式沒有顯示呼叫父類的建構函式,則會呼叫父類的預設無參建構函式。

4.)在建立子類物件時候,如果子類的建構函式沒有顯示呼叫父類的建構函式且父類自己提供了無參建構函式,則會呼叫父類自己的無參建構函式。

5.)在建立子類物件時候,如果子類的建構函式沒有顯示呼叫父類的建構函式且父類只定義了自己的有參建構函式,則會出錯(如果父類只有有引數的構造方法,則子類必須顯示呼叫此帶參構造方法)。

6.)如果子類呼叫父類帶引數的構造方法,需要用初始化父類成員物件的方式,

 

 

建構函式是不能被繼承的,但是可以被呼叫,如果父類重新定義了建構函式,也就是沒有了預設的建構函式,子類建立自己的建構函式的時候必須顯式的呼叫父類的建構函式。

 

預設建構函式,拷貝建構函式,拷貝賦值函式,以及解構函式這四種成員函式被稱作特殊的成員函式。這4種成員函式不能被繼承。