天天看點

【讀書筆記:C++ primer plus 第六版 中文版】第7章 函數---C++的程式設計子產品

轉載請注明出處:http://blog.csdn.net/enyusmile/article/details/46604119

本章内容包括:

  • 函數基本知識
  • 函數原型
  • 按值傳遞函數參數
  • 設計處理數組的函數
  • 使用const指針參數
  • 設計處理文本字元串的函數
  • 設計處理結構的函數
  • 設計處理string對象的函數
  • ​調用自身的函數(遞歸)
  • 指向函數的指針

7.1 複習函數的基本知識

  • 要使用C++函數,必須完成如下工作:
    • 提供函數定義
    • 提供函數原型
    • 調用函數
  • 庫函數是已經定義和編譯号的函數,同時可以使用标準庫頭檔案提供其原型,是以隻需正确地調用這種函數即可.
  • 程式清單7.1 calling.cpp

7.1.1 定義函數

  • C++對于傳回值的類型有一定的限制:不能是數組,但可以是其他任何類型—整數,浮點數,指針,甚至可以是結構和對象(有趣的是,雖然C++函數不能直接傳回數組,但可以将數組作為結構或對象組成部分來傳回).
  • 通常,函數通過将傳回值複制到制定的CPU寄存器或記憶體單元中來将其傳回.随後,調用程式将檢視該記憶體單元.傳回函數和調用函數必須就該記憶體單元中存儲的資料的類型達成一緻.
  • 函數原型将傳回值類型告知調用程式,而函數定義指令被調用函數應傳回什麼類型的資料.

7.1.2 函數原型和函數調用

  • 程式清單7.2 protos.cpp
  • 程式說明
    1. 為什麼需要原型
      • 原型描述了函數到編譯器的接口,也就是說,它将函數傳回值的類型(如果有的話)以及參數的類型和數量告訴編譯器.
      • C++的程式設計風格是将main()方在最前面,因為它通常提供了程式的整體結構.
    2. 原型的文法
      • 函數原型是一條語句,是以必須以分号結束.
      • 函數原型不要求提供變量名,有類型清單就足夠了,但在原型的參數清單中,可以包括變量名,也可以不包括.原型中的變量名相當于占位符,是以不必與函數定義中的變量名相同.
      • C++原型與ANSI原型:ANSI C借鑒了C++中的原型,但這兩種語言還是有差別的.其中最重要的差別是,為與基本C相容,ANSI C中的原型是可選的,但在C++中,原型是必不可少的.
    3. 原型的功能
      • 具體來說,原型確定以下幾點:
        1. 編譯器正确處理函數傳回值
        2. 編譯器檢查使用的參數數目是否正确
        3. 編譯器檢查使用的參數類型是否正确.如果不正确,則轉換為正确的類型(如果可能的話).
      • 在編譯階段進行的原型化被稱為靜态類型檢查.可以看出,靜态類型檢查可捕獲許多在運作階段非常難以捕獲的錯誤.

7.2 函數參數和按值傳遞

  • C++通常按值傳遞參數,這意味着将數值參數傳遞給函數,而後者将其賦給一個新的變量.
  • 出于簡化的目的,C++标準使用參數來表示實參,使用參量來表示形參.

7.2.1 多個參數

  • 程式清單7.3 twoarg.cpp

7.2.2 另外一個接受兩個參數的函數

  • 注意:有些C++實作不支援long double類型,如果所用的C++實作是這樣的,請使用double類型.
  • 程式清單7.4 lotto.cpp

7.3 函數和數組

  • 程式清單7.5 arrfun1.cpp

7.3.1 函數如何使用指針來處理數組

  • 當指針指向數組的第一個元素時,本書使用數組表示法;而當指針指向一個獨立的值時,使用指針表示法.例如:在C++中,當(且僅當)用于函數頭或函數原型中,int *arr和int arr[]的含義才是相同的.
  • 記住,将指針(包括數組名)加1,實際上是加上了一個與指針指向的類型的長度(以位元組為機關)相等的值.對于周遊數組而言,使用指針假發和數組下标是等效的.

7.3.2 将數組作為參數意味着什麼

  • 傳遞正常變量時,函數将使用該變量的拷貝;但傳遞數組時,函數将使用原來的數組.實際上,這種卻别并不違反C++按值傳遞的方法,sum_arr()函數仍傳遞了一個值,這個值被賦給一個新變量,但這個值是一個位址,而不是數組的内容.
  • 程式清單7.6 arrfun2.cpp
  • 注意:為将數組類别和元素數量高速數組處理函數,請通過兩個不同的參數來傳遞它們.

7.3.3 更多數組函數示例

  1. 填充數組
  2. 顯示數組及用const保護數組
    • 為防止函數無意中修改數組的内容,可在聲明形參時使用關鍵字const
  3. 修改數組
  4. 将上述代碼組合起來
    • 程式清單7.7 arrfun3.cpp
  5. 程式說明
    • 這種被稱為自下而上的程式設計,因為設計過程從元件到整體進行.這種方法非常适合于OOP—它首先強調的是資料表示和曹總.而傳統的過程性程式設計傾向于從上而下的程式設計,首先制定子產品化設計方案,然後再研究細節.這兩種方法都很有用,最終的産品都是子產品化程式.
  6. 數組處理函數的常用編寫方式

7.3.4 使用數組區間的函數

  • STL方法使用”超尾”概念來制定區間.也就是說,對于數組而言,辨別數組結尾的參數将是指向最後一個元素後面的指針.
  • 程式清單7.8 arrfun4.cpp

7.3.5 指針和const

  • 可以用兩種不同的方式将const關鍵字用于指針.第一種方法是讓指針指向一個常量對象,這樣可以防止使用該指針來修改所指向的值,第二種方法是将指針本身聲明為常量,這樣可以防止改變指針指向的位置.
  • 注意:如果資料類型本身并不是指針,則可以将const資料或非const資料的位址賦給指向const的指針,但隻能将非const資料的位址賦給非const指針.
  • 盡可能使用const:将指針參數聲明為指向常量資料的指針有兩條理由
    1. 這樣可以避免由于無意間修改資料而導緻的程式設計錯誤.
    2. 使用const使得函數能夠處理const和非const實參,否則将隻能接受非const資料.
  • 如果條件允許,則應将指針形參聲明為指向const的指針
int sloth = ;
const int * ps = &sloth;//a pointer to const int
int * const finger = &sloth;//a const pointer to int
           
  • 在最後一個聲明中,關鍵字const的位置與以前不同.這種聲明格式使得finger隻能指向sloth,但允許使用finger來修改sloth的值.中間的聲明不允許使用ps來修改sloth的值,但允許将ps指向另一個位置.簡而言之,finger和*ps都是const,而*finger和ps不是.

7.4 函數和二維數組

7.5 函數和C風格字元串

7.5.1 将C風格字元串作為參數的函數

  • 假設要将字元串作為參數傳遞給函數,則表示字元串的方式有三種:
    1. char數組
    2. 用引号括起的字元串常量(也稱字元串字面值)
    3. 被設定為字元串的位址的char指針
  • 但上述3種選擇的類型都是char指針(準确的說是char*).可以說是将字元串作為參數來傳遞,但實際傳遞的是字元串第一個字元的位址.這意味着字元串函數原型應将其表示字元串的形參聲明為char*類型.
  • 程式清單7.9 strgfun.cpp

7.5.2 傳回C風格字元串的函數

  • 程式清單7.10 strgback.cpp

7.6 函數和結構

  • 可以将一個結構賦給另外一個結構.同樣,也可以按值傳遞結構,就像普通變量那樣.在這種情況下,函數将隻用原始結構的副本.另外,函數也可以傳回結構.
  • 在C語言和C++中,都使用符号&來表示位址運算符.
  • 按值傳遞結構有一個缺點.如果結構非常大,則指派結構将增加記憶體要求,降低系統運作的速度.處于這些原因(同時由于最初C語言不允許按值傳遞結構),許多C程式員傾向于床底結構的位址,然後使用指針來通路結構的内容.C++提供了第三種選擇—按應用傳遞.

7.6.1 傳遞和傳回結構

  • 程式清單7.11 travel.cpp

7.6.2 另一個處理結構的函數示例

  • 程式清單7.12 atrctfun.cpp
  • 注意:有些編譯器僅當被明确訓示後,才會搜尋數學庫.

7.6.3 傳遞結構的位址

  • 程式清單7.13 strctptr.cpp

7.7 函數和string對象

  • 程式清單7.14 topfive.cpp

7.8 函數與array對象

  • 在C++中,類對象是基于結構的,是以結構程式設計方面的有些考慮因素也适用于類.
  • 程式清單7.15 arrobj.cpp

7.9 遞歸

  • C++函數有一種有趣的特點—可以調用自己(然而,與C語言不同的是,C++不允許main()調用自己),這種功能被稱為遞歸.

7.9.1 包含一個遞歸調用的遞歸

  • 程式清單7.16 recur.cpp

7.9.2 包含多個遞歸調用的遞歸

  • 在需要将一項工作不斷分為兩項較小的類似的工作時,遞歸非常有用.

    遞歸方法有時被稱為分而治之政策.

  • 程式清單7.17 ruler.cpp
// ruler.cpp -- using recursion to subdivide a ruler
#include <iostream>
const int Len = ;
const int Divs = ;
void subdivide(char ar[], int low, int high, int level);
int main()
{
    char ruler[Len];
    int i;
    for (i = ; i < Len - ; i++)
        ruler[i] = ' ';
    ruler[Len - ] = '\0';
    int max = Len - ;
    int min = ;
    ruler[min] = ruler[max] = '|';
    std::cout << ruler << std::endl;
    for (i = ; i <= Divs; i++)
    {
        subdivide(ruler,min,max, i);
        std::cout << ruler << std::endl;
        for (int j = ; j < Len - ; j++)
            ruler[j] = ' ';  // reset to blank ruler
    }
    // std::cin.get();
    return ;
}
void subdivide(char ar[], int low, int high, int level)
{
    if (level == )
        return;
    int mid = (high + low) / ;
    ar[mid] = '|';
    subdivide(ar, low, mid, level - );
    subdivide(ar, mid, high, level - ); 
}
           
  • 如果要求的遞歸層次很多,這種遞歸方式将是一種糟糕的選擇;然而,如果遞歸層次較少,浙江是一種精緻而簡單的選擇.

7.10 函數指針

  • 與資料項相似,函數也有位址.函數的位址是存儲其機器語言代碼的記憶體的開始位址.這對使用者來說不重要,對程式員卻很有用.例如:可以編寫将另一個函數的位址作為參數的函數.這樣第一個函數将能夠找到第二個函數,并運作它.與直接調用另一個函數相比,這種方法很笨拙,但它允許在不同的時間傳遞不同函數的位址,這意味着可以在不同的時間使用不同的函數.

7.10.1 函數指針的基礎知識

  1. 擷取函數的位址
    1. 使用函數名即可擷取函數的位址.如:如果think()是一個函數,則think就是該函數的位址.
    2. 要将函數作為參數進行傳遞,必須傳遞函數名.一定要區分傳遞的是函數的位址還是函數的傳回值.
  2. 聲明函數指針
    1. 提示:通常,要聲明指向特定類型的函數的指針,可以首先編寫這種函數的原型,然後用(*pf)替換函數名.這樣pf就是這類函數的指針.
    2. 為提供正确的運算符優先級,必須在聲明中使用括号将*pf括起.

      使用函數指針時,比較棘手的是編寫原型,而傳遞位址則非常簡單.

  3. 使用指針來調用函數
    1. 曆史與邏輯:真實非常棒的文法!為何pf和(*pf)等價呢?一種學派認為,由于pf是函數指針,而*pf是函數,是以應将(*pf)()用作函數調用.另一種學派認為,由于函數名是指向該函數的指針,指向函數的指針的行為應與函數名相似,是以硬體pf()用作函數調用使用.C++進行了折中—這兩種方式都是正确的,或者至少是允許的,雖然他們在邏輯上是互相沖突的.在認為折中折中粗糙之前,應該想到,容忍邏輯上無法自圓其說的觀點正在人類思維活動的特點.

7.10.2 函數指針示例

程式清單7.18 fun_ptr.cpp

// fun_ptr.cpp -- pointers to functions
#include <iostream>
double betsy(int);
double pam(int);
// second argument is pointer to a type double function that
// takes a type int argument
void estimate(int lines, double (*pf)(int));
int main()
{
    using namespace std;
    int code;
    cout << "How many lines of code do you need? ";
    cin >> code;
    cout << "Here's Betsy's estimate:\n";
    estimate(code, betsy);
    cout << "Here's Pam's estimate:\n";
    estimate(code, pam);
    // cin.get();
    // cin.get();
    return ;
}
double betsy(int lns)
{
    return  * lns;
}
double pam(int lns)
{
    return  * lns +  * lns * lns;
}
void estimate(int lines, double (*pf)(int))
{
    using namespace std;
    cout << lines << " lines will take ";
    cout << (*pf)(lines) << " hour(s)\n";
}
           

7.10.3 深入探讨函數指針

  • 程式清單arfupt.cpp
// arfupt.cpp -- an array of function pointers
#include <iostream>
// various notations, same signatures
const double * f1(const double ar[], int n);
const double * f2(const double [], int);
const double * f3(const double *, int);
int main()
{
    using namespace std;
    double av[] = {, , };
    // pointer to a function
    const double *(*p1)(const double *, int) = f1;
    auto p2 = f2;  // C++0x automatic type deduction
    // pre-C++0x can use the following code instead
    // const double *(*p2)(const double *, int) = f2;
     cout << "Using pointers to functions:\n";
    cout << " Address  Value\n";
    cout <<  (*p1)(av,) << ": " << *(*p1)(av,) << endl;
    cout << p2(av,) << ": " << *p2(av,) << endl;
    // pa an array of pointers
    // auto doesn't work with list initialization
    const double *(*pa[])(const double *, int) = {f1,f2,f3};
    // but it does work for initializing to a single value
    // pb a pointer to first element of pa
    auto pb = pa;
    // pre-C++0x can use the following code instead
    // const double *(**pb)(const double *, int) = pa;
    cout << "\nUsing an array of pointers to functions:\n";
    cout << " Address  Value\n";
    for (int i = ; i < ; i++)
        cout << pa[i](av,) << ": " << *pa[i](av,) << endl;
    cout << "\nUsing a pointer to a pointer to a function:\n";
    cout << " Address  Value\n";
    for (int i = ; i < ; i++)
        cout << pb[i](av,) << ": " << *pb[i](av,) << endl;
    // what about a pointer to an array of function pointers
    cout << "\nUsing pointers to an array of pointers:\n";
    cout << " Address  Value\n";
    // easy way to declare pc 
    auto pc = &pa; 
     // pre-C++0x can use the following code instead
    // const double *(*(*pc)[3])(const double *, int) = &pa;
   cout << (*pc)[](av,) << ": " << *(*pc)[](av,) << endl;
    // hard way to declare pd
    const double *(*(*pd)[])(const double *, int) = &pa;
    // store return value in pdb
    const double * pdb = (*pd)[](av,);
    cout << pdb << ": " << *pdb << endl;
    // alternative notation
    cout << (*(*pd)[])(av,) << ": " << *(*(*pd)[])(av,) << endl;
    // cin.get();
    return ;
}
// some rather dull functions
const double * f1(const double * ar, int n)
{
    return ar;
}
const double * f2(const double ar[], int n)
{
    return ar+;
}
const double * f3(const double ar[], int n)
{
    return ar+;
}
           
  • 顯示的位址為數組av中double值的存儲位置.
  • 指向函數指針數組的指針并不少見.實際上,類的虛方法實作通常都采用了這種技術.所幸的是,這些細節由編譯器處理.
  • 感謝auto:C++11的目标之一是讓C++更容易使用,進而讓程式員将主要經理放在設計而不是細節上.程式清單7.19示範了這一點.自動類型推斷功能表明,編譯器的角色發生了改變.在C++98中,編譯器利用其知識幫助您發現錯誤,而在C++11中,編譯器利用其知識幫助您進行正确的聲明.存在一個潛在的缺點.自動類型推斷確定變量的類型與賦給它的初值的類型一緻,但您提供的初始的類型可能不對:auto pc = *pa;上述聲明導緻pc的類型與*pa一緻,在程式清單7.19中,後面使用它時假定其類型與&pa相同,這将導緻編譯錯誤.

7.10.4 使用typedef進行簡化

7.11 總結

7.12 複習題

7.13 程式設計練習

附件:本章源代碼下載下傳位址