C++描述的數據結構和算法(一)

  • 參數傳遞方式(如傳值、引用和常量引用)。
  • 函數返回方式(如返值、引用和常量引用)。
  • 模板函數。
  • 遞歸函數。
  • 常量函數。
  • 內存分配和釋放函數:new與delete。
  • 在檢查程序的時候我們應該關注以下這些點:

在程序開發過程中通常需要做到如下兩點:一是高效地描述數據;二是設計好的算法。在程序的開發過程中,要重點關注這些信息:

  • 它正確嗎?
  • 它容易讀懂嗎?
  • 它有完善的文檔嗎?
  • 它容易修改嗎?
  • 它在運行時需要多大內存?
  • 它的運行時間有多長?
  • 它的通用性如何?能不能不加修改就可以用它來解決更大範圍的問題?
  • 它可以在多種機器上編譯和運行嗎?或者說需要經過修改才能在不同的機器上運行嗎?

上述問題的相對重要性取決於具體的應用環境。比如,如果正在編寫一個只需運行一次即可丟棄的程序,那麼主要關心的應是程序的正確性、內存需求、運行時間以及能否在一臺機器上編譯和運行。不管具體的應用環境是什麼,正確性總是程序的一個最重要的特性。一個不正確的程序,不管它有多快、有多麼好的通用性、有多麼完善的文檔,都是毫無意義的(除非它變正確了)。儘管我們無法詳細地介紹提高程序正確性的技術,但可以爲大家提供一些程序正確性的驗證方法以及公認的一些良好的程序設計習慣,它們可以幫助你編寫正確的代碼。

算法的目標是如何開發正確的、精緻的、高效的程序。

2 函數與參數

2.1 傳值參數

考察函數func(),該函數用來計算表達式 a+b+b*c+(a+b-c)/(a+b) + 4,其中a,b和c是整數,結果也是一個整數。

int func(int a, int b, int c)
{
    return a+b+b*c+(a+b-c)/(a+b)+4;
}

形式參數a、b和c 都是傳值參數(value parameter)。運行時,與傳值形式參數相對應的實際參數的值將在函數執行之前被複制給形式參數,複製過程是由該形式參數所屬數據類型的複製構造函數(copy constructor)完成的。如果實際參數與形式參數的數據類型不同,必須進行類型轉換,從實際參數的類型轉換爲形式參數的類型,當然,假定這樣的類型轉換是允許的。

當函數運行結束時,形式參數所屬數據類型的析構函數(destructor)負責釋放該形式參數。當一個函數返回時,形式參數的值不會被複制到對應的實際參數中。因此,函數調用不會修改實際參數的值。

2.2 模板函數

編寫一段通用的代碼,將參數的數據類型作爲一個變量,不明確指定其數據類型,而是由編譯器來確定。利用模板函數計算一個表達式代碼如下:

template<class T>
T Abc(T a, T b, T c)
{
    return a+b+b*c+(a+b-c)/(a+b)+4;
}

通過把T替換爲int,編譯器可以立即構造出func的整形版本,而把T替換爲double或long,編譯器又可以構造出函數func的雙精度型版本和長整型版本。

2.3 引用參數

上述模板函數形式參數的用法會增加程序的運行開銷。假定a, b 和c 是傳值參數,在函數被調用時,類型T的複製構造函數把相應的實際參數分別複製到形式參數a、b和c之中,以供函數使用;而在函數返回時,類型T的析構函數會被喚醒,以便釋放形式參數a,b和c。假定數據類型爲用戶自定義的類類型,那麼它的複製構造函數將負責複製其所有元素,而析構函數則負責逐個釋放每個元素,這都是非常大的消耗。

一種優化方案是,將a、 b和c改用引用參數。如果用語句func(x,y,z)來調用函數,其中x、y和z是相同的數據類型,那麼這些實際參數將被分別賦予名稱a,b和c,因此在函數func執行期間,x、y和z被用來替換對應的a,b和c。與傳值參數的情況不同,在函數被調用時,本程序並沒有複製實際參數的值,在函數返回時也沒有調用析構函數。

template<class T>
T Abc(T& a, T& b, T& c)
{
    return a+b+b*c+(a+b-c)/(a+b)+4;
}

2.4 常量引用參數

更進一步,C++還提供了另外一種參數傳遞方式——常量引用(const reference),這種模式指出函數不得修改引用參數。

template<class T>
T func(const T& a, const T& b, const T& c)
{
    return a+b+b*c+(a+b-c)/(a+b)+4;
}

使用關鍵字const來指明函數不可以修改引用參數的值。對於簡單數據類型,當函數不會修改實際參數值的時候我們可以
採用傳值參數,對於其他複雜的數據類型(包括模板類型),當函數不會修改實際參數值的時候可以採用常量引用參數。

可以得到上述代碼的一個更通用的版本。在新的版本中,每個形式參數可能屬於不同的數據類型,函數返回值的類型與第一個參數的類型相同。

template<class Ta, class Tb, class Tc >
Ta func(const Ta& a, const Tb& b, const Tc& c)
{
    return a+b+b*c+(a+b-c)/(a+b)+4;
}

2.5 返回值

函數可以返回值、引用或常量引用。在前面的例子中,函數func返回的都是一個具體值,在這種情況下,被返回的對象均被複制到調用(或返回)環境中。對於函數func的所有版本來說,這種複製過程都是必要的,因爲函數所計算出的表達式的結果被存儲在一個局部臨時變量中,當函數返回時,這個臨時變量(以及所有其他的臨時變量和傳值形式參數)所佔用的空間將被釋放,其值當然也不再有效。爲了避免丟失這個值,在釋放臨時變量以及傳值形式參數的空間之前,必須把這個值從臨時變量複製到調用該函數的環境中去。

如果需要返回一個引用,可以爲返回類型添加一個前綴 &。如:

T& X(int i, T& z)

如果在函數名之前添加關鍵字const,那麼函數將返回一個常量引用,例如:

const T& X (int i, T& z)

除了返回的結果是一個不變化的對象之外,返回一個常量引用與返回一個引用是相同的。

2.6 遞歸函數

遞歸函數(recursive function)是一個自己調用自己的函數。涉及數學的兩個相關概念——數學函數的遞歸定義以及歸納證明。在數學中經常用一個函數本身來定義該函數。例如階乘函數f(n)=n!的定義如下:

對於函數f(n)的一個遞歸定義,要想使它成爲一個完整的定義,必須滿足如下條件:

  • 定義中必須包含一個基本部分,返回值必須是直接定義的(即非遞歸)。
  • 在遞歸部分中,右側所出現的所有f的參數都必須有一個比n小,以便重複運用遞歸部分來改變右側出現的f,直至出現f的基本部分。

例程1 以下程序給出了一個計算階乘n!的C++函數。

int Factorial (int n)
{
    //計算n!
    if (n<=1) return 1;
    else return n * Factorial(n-1);
}

例程2 模板函數Sum和RSum統計元素a[0]至a[n-1]的和,一種是傳統的累加,一種是遞歸調用的方式。

template<class T>
T Sum(T a[], int n)
{ 
    //累加方式計算a[0: n-1]的和
    T tsum=0;
    for(int i = 0; i < n; i++)
        tsum += a[i];
    return tsum;
}

template<class T>
T Rsum(T a[], int n)
{
    //遞歸方式計算a[0: n-1]的和
    if (n > 0)
        return Rsum(a, n-1) + a[n-1];
    return 0;
}

例程3 以下使用遞歸函數生成排列

template<class T>
void Perm(T list[], int k, int m)
{ 
    //生成list [k: m ]的所有排列方式
    int i;
    if (k == m) {
        //輸出一個排列方式
        for (i = 0; i <= m; i++)
            cout << list [i];
        cout << endl;
    }
    else // list[k: m ]有多個排列方式
        // 遞歸地產生這些排列方式
        for (i=k; i <= m; i++) {
            Swap (list[k], list[i]);
            Perm (list, k+1, m);
            Swap (list [k], list [i]);
        }
}

3 動態存儲分配

3.1 操作符new

C++操作符new可用來進行動態存儲分配,該操作符返回一個指向所分配空間的指針。例如,爲了給一個整數動態分配存儲空間,可以使用下面的語句來說明一個整型指針變量:

int *y ;

當程序需要使用該整數時,可以使用如下語法來爲它分配存儲空間:

y = new int;

操作符new分配了一塊能存儲一個整數的空間,並將指向該空間的指針返回給 y,y是對整數指針的引用,而*y則是對整數本身的引用。爲了在剛分配的空間中存儲一個整數值,可以使用如下語法:

*y = 10;

我們可以把上述三步進行適當的合併,如下例所示:

int *y = new int;

*y = 10;

int *y = new int (10);

int *y;

y = new int (10);

3.2 一維數組

使用一維或二維數組時,這些數組的大小在編譯時可能是未知的,事實上,它們可能隨着函數調用的變化而變化。因此,對於這些數組必須進行動態存儲分配。

爲了在運行時創建一個一維浮點數組x,首先必須把x說明成一個指向float的指針,然後爲數組分配足夠的空間。例如,一個大小爲n的一維浮點數組可以按如下方式來創建:

float *x = new float [n];

操作符new分配n個浮點數所需要的空間,並返回指向第一個浮點數的指針。可以使用如下語法來訪問每個數組元素:x[0], ..., x[n-1]。

3.4 操作符delete

動態分配的存儲空間不再需要時應該被釋放,所釋放的空間可重新用來動態創建新的結構。可以使用C++操作符delete來釋放由操作符new所分配的空間。下面的語句可以釋放分配給 *y的空間以及一維數組x:

delete y;

delete [ ] x;

3.5 二維數組

雖然C++提供了多種機制用來說明二維數組,但其中的多數機制都要求在編譯時明確地知道每一維的大小。而且,在使用這些機制時,很難編寫出一個允許形式參數是一個第二維大小未知的二維數組的函數。之所以如此,是因爲當形式參數是一個二維數組時,必須指定其第二維的大小。

例如,a[ ][10]是一個合法的形式參數,而a[ ][ ]不是。克服這種限制的一條有效途徑就是對於所有的二維數組使用動態存儲分配。當一個二維數組每一維的大小在編譯時都是已知時,可以採用類似於創建一維數組的語法來創建二維數組。例如,一個類型爲char的7×5數組可用如下語法來定義:

char c[7][5];

如果在編譯時至少有一維是未知的,必須在運行時使用操作符new來創建該數組。一個二維字符型數組,假定在編譯時已知其列數爲 5,可採用如下語法來分配存儲空間:

char (*c)[5];
try {
    c = new char [n][5];
}

catch (xalloc) {
    //僅當n e w失敗時纔會進入
    cerr << "Out of Memory" << endl;
    exit (1);
}

例程:爲一個二維數組分配存儲空間

template <class T>
void Make2DArray ( T ** &x, int rows, int cols)
{
    //創建一個二維數組
    //創建行指針
    x = new T * [rows];
        
    //爲每一行分配空間
    for(int i = 0; i < rows; i++)
        x[i] = new int [cols];
}

例程:按照申請空間的相反順序釋放空間

//釋放二維數組的空間
template <class T>
void Delete2DArray(T ** &x, int rows)
{
    //釋放爲每一行所分配的空間
    for (int i = 0; i < rows; i++)
            delete [] x[i];
    
    //刪除行指針
        delete[] x;

    x = nullptr;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章