跳轉到內容

使用 C 和 C++ 的程式語言概念/C++ 程式設計入門

來自華夏公益教科書,開放的世界,開放的書籍

遺留 C 程式

[編輯 | 編輯原始碼]

純 C 程式

[編輯 | 編輯原始碼]

由於 C++ 與 C 之間存在獨特的聯絡,大多數 C 程式可以編譯為 C++ 程式,而無需或僅需少量修改。不過,這種做法(即以 C++ 程式的形式傳遞 C 程式)並不推薦;只有在需要在 C++ 程式中使用一些現有的 C 程式碼時才建議這樣做。[1]

C_Commands.cxx
#include <stdio.h>

int main(void) {
printf(An example to the usage of C commands in a C++ program\n);

return 0;

除了 C 風格的註釋之外,C++ 還提供了一種可選的單行註釋,它用雙斜槓與行中的其他內容隔開。[2] 分隔符右側程式行上的所有內容都被視為註釋,編譯器會忽略它們。

} // end of int main(void)

名稱空間中的 C 命令

[編輯 | 編輯原始碼]
C_Commands_In_NS.cxx

C 標頭檔案可以透過兩種不同的方式從 C++ 程式中包含:使用其 C 或 C++ 名稱。下一行是後者的示例。C 標頭檔案的 C++ 名稱以字母 c 為字首,並刪除 .h 檔案字尾。

由於 C++ 庫名稱通常[3] 在名稱空間中宣告,因此從標頭檔案中包含的名稱不可見,除非我們透過 using 指令明確地使它們可見。對於 C 庫標頭檔案,這是 std 名稱空間。如果未能引入此名稱空間中找到的名稱,則必須將所有這些名稱與它們的名稱空間一起使用。也就是說;省略第 2 行需要我們將第 4 行更改為:

std::printf("An example to ...specification\n");

有時,這可能被證明是一種有用的工具。考慮呼叫一個名為 function4 的函式,它碰巧具有與當前名稱空間中的函式相同的名稱和簽名。根據您是否透過某個 using 指令匯入了此函式,簡單地發出名稱將導致呼叫當前名稱空間中的函式或出現編譯時錯誤。透過範圍運算子並明確地在函式名稱之前宣告名稱空間(可能還有類名)可以修復此錯誤。

示例:範圍運算子的使用。

// 沒有 using 指令!!! // 下一行將建立一個 Stack 物件,它恰好位於 CSE224::DS 的巢狀名稱空間中。 CSE224::DS::Stack<int> int_stack; ... ... int_stack.push(5); ... // 假設 Stack 類有一個名為 st_f 的靜態函式。如果沒有使用指令,可以按如下方式呼叫此函式。 ... CSE224::DS::Stack::st_f(...) ...; ...

可以透過在函式和/或類名之前新增範圍運算子來明確引用不在特定名稱空間中的名稱,即全域性名稱空間中的名稱。根據此,如果您想在 C++ 程式中使用 C 風格包含的 printf,則必須編寫以下程式碼

::printf("printf included a la C lives in the global namespace\n");

最後要注意的是,C++ 風格的 C 功能包含並不啟用面向物件風格的程式設計。您仍然必須像在 C 中一樣呼叫函式;沒有辦法向物件傳送訊息。

#include <cstdio>
using namespace std;

int main(void) {
  printf("An example to the usage of C commands in a C++ program with a namespace specification\n");

  return 0;
} // end of int main(void)

C++ 程式

[編輯 | 編輯原始碼]
C++_Commands.cxx

更好的做法是像真正的 C++ 一樣使用 C++!在 C++ 標頭檔案 iostream 中可以找到類似於 C 標頭檔案 stdio.h 中的功能。與標準庫標頭檔案中宣告的其他名稱一樣,iostream 中的名稱在 std 名稱空間中宣告。

#include <iostream>
using namespace std;

int main(void) {

C++ 編譯器在內部將以下行轉換為

operator<<(operator<<(cout, A program with C++ commands), endl);

其中 cout 是一個 ostream 物件,表示標準輸出(類似於 C 中的 stdout),而 endl 是一個 ostream 操縱器,它將一個換行符插入輸出流,然後重新整理 ostream 緩衝區(類似於 C 中的 '\n')。<< 是一個運算子,用於將輸出寫入某個 ostream 物件。從轉換後的行可以看出,它從左到右關聯。因此,下一行將訊息寫入標準輸出,然後將換行符追加到末尾。

請注意,<< 運算子連續呼叫了兩次:一次用於輸出 "A program with C++ commands",另一次用於追加一個行尾字元。也就是說,下一行等效於

cout << "A program with C++ commands"; cout << endl;

它被轉換為

operator<<(cout, "A program with C++ command"); operator<<(cout, endl);

這也說明了為什麼 << 運算子必須返回一個 ostream 物件。

  cout << A program with C++ commands << endl;

  return 0;
} // end of int main(void)

連結 C 和 C++ 程式

[編輯 | 編輯原始碼]

下一個程式是一個 C 程式,它原本應該是一個 C 程式;它不是一個被編譯為 C++ 程式的 C 程式。

C_File.c
#include <stdio.h>

儘管接下來的兩個函式基本上做的是同一件事,但我們既不能將它們合併成一個函式,也不能給它們起相同的名稱。這是因為 C 語言不支援模板或過載。

void fint(int ipar) {
  printf("In C function fint...Value of ipar: %d\n", ipar);
} /* end of void fint(int) */

void fdouble(double dpar) {
  printf("In C function fdouble...Value of dpar: %f\n", dpar);
} /* end of void fdouble(double) */
C++_Linking_With_C.cxx
#include <iostream>
#include <string>
using namespace std;

下面的 extern 宣告將 fintfdouble 宣告為使用 C 連線方式連結的函式。也就是說,除了可能在前面加上一個下劃線之外,不會進行任何名稱改編。或者,我們可以這樣寫

extern "C" void fint(int); extern "C" void fdouble(double);

extern "C" {
  void fint(int);
  void fdouble(double);
}

有兩個名為 f_overloaded 的函式定義:一個接受 int 引數,另一個接受 double 引數。請記住,這是一個 C++ 程式,C++(由於一個名為“名稱改編”的過程)支援函式名過載。這是透過在函式名中編碼引數型別並將此轉換後的名稱傳遞給連結器來實現的。這使得型別安全連結不再成為問題。例如,下面第一個函式的名稱將被[編譯器]轉換為 f_overloaded__Fi,而第二個函式將被轉換為 f_overloaded__Fd[4] 也就是說,連結器會看到兩個不同的函式名。

雖然[不像 Java]返回值型別在區分過載函式時會被考慮,但應該非常謹慎地使用此屬性。這是因為從 C++ 函式返回的值可以被忽略;一個函式可能被呼叫只是為了其副作用。下面的程式碼片段應該可以使這一點更清晰。

int f(int i) { ... } ... void f(int j) { ... } ... // 正常。編譯器可以從上下文中輕鬆推斷出程式設計師的意圖。 int res = f(3); ... // 模稜兩可!!!使用者可能打算呼叫第二個函式或第一個函式以獲得其副作用。 f(5);

void f_overloaded(int ipar) {
  cout << "In C++ function f_overloaded..."
       << "The value of ipar: " << ipar << endl;
} // end of void f_overloaded(int)

void f_overloaded(double dpar) {
  cout << "In C++ function f_overloaded..." 
       << "The value of dpar: " << dpar << endl;
} // end of void f_overloaded(double)

C++ 透過模板機制使型別引數化。[5] 程式設計師對函式介面(引數和返回值型別)中的所有型別或部分型別進行引數化,而函式體保持不變。

與過載不同,模板機制不需要程式設計師提供多個函式定義;函式的例項由編譯器構建。此過程稱為“模板例項化”。它作為呼叫函式模板或獲取函式模板地址的副作用隱式地發生。

template <class Type>
void f_template(Type par) {
  cout << "In C++ function f_template..."
       << "The value of par: " << par << endl;
} // end of void f_template(<class>)

int main(void) {

與 C 不同,C++ 允許將宣告語句與可執行語句混合使用。這意味著現在可以在識別符號的第一個引用點之前宣告它們;不需要在進入相關塊時宣告它們。[6]

  int i = 10;
  fint(i);

  double d = 123.456;
  fdouble(d);
  string s = "A random string";

根據引數型別,將在編譯時解析要呼叫的函式。不要忘記:我們在這裡做的是呼叫一個預先存在的函式,這些函式碰巧具有相同的名稱。

  f_overloaded(i);
  f_overloaded(d);

以下每個呼叫都會導致編譯器構建不同的例項;如果沒有呼叫,編譯器將不會進行任何例項化。

  f_template(i);
  f_template(d);
  f_template(s);

  return 0;
} // end of int main(void)
執行程式
[edit | edit source]

gcc –c C_file.c↵ # 使用 DJGPP-gcc gxx -o C++_Linking_With_C C++_Linking_With_C.cxx C_File.o↵

在其他有 GNU 編譯器集合移植版的環境中,例如 Linux 和 Cygwin,你可能會看到一條訊息,提示命令無法識別。在這種情況下,請嘗試類似 g++gpp 的命令。

C++_Linking_With_C↵ 在 C 函式 fint...ipar 的值為:10 在 C 函式 fdouble...dpar 的值為:123.456000 在 C++ 函式 f_overloaded... ipar 的值為:10 在 C++ 函式 f_overloaded... dpar 的值為:123.456 在 C++ 函式 f_template... par 的值為:10 在 C++ 函式 f_template... par 的值為:123.456 在 C++ 函式 f_template... par 的值為:一個隨機字串

預設引數

[edit | edit source]
Default_Arg.cxx
#include <iostream>
using namespace std;

傳遞給某些函式的引數在大多數情況下可能具有某些預期值,而在特殊情況下,它們可能假設不同的值。對於這種情況,C++ 提供了使用“預設引數”作為選項。例如,考慮一個用於列印整數的函式。為使用者提供列印整數的基數選項似乎很合理,但在大多數程式中,整數將被列印為十進位制整數。使用此功能列印整數的函式將具有以下原型

void print_int(int num, unsigned int base = 10);

只可以為尾部的引數提供預設引數。 retType f(argType1 arg1, ..., argTypen argn = def_value); // 正常 retType f(argType1 arg1, ..., argTypem argm = def_value, ... , argTypen argn); // 錯誤!!!

預設引數的效果也可以透過過載來實現。上面的 print_int 函式可以用以下函式來表達

void print_int(int num, unsigned int base); void print_int(int num);

int greater_than_n(int *ia, int size, int n = 0) {
  int i, count = 0;

  for (i = 0; i < size; i++)
    if (ia[i] > n) count++;

  return count;
} // end of int greater_than_n(int[], int. int)

int main(void) {
  int inta[] = { 1, 2, -3, 6, -10, 0, 7, -2};

  cout << "The count of numbers greater than 5: "
       << greater_than_n(inta, 8, 5) << endl;
  cout << "The count of positive numbers in the sequence: "
       << greater_than_n(inta, 8) << endl;

  return 0;
} // end of int main(void)

C++ 引用

[edit | edit source]
Reference.cxx
#include <iostream>

以下 using 指令與我們之前見過的不同。它不用於引入名稱空間中找到的所有名稱,而是引入名稱空間中的特定類;它將引入 std 名稱空間中的 iostream 類,而同一名稱空間中的所有其他類/函式都將不可見,因此只能在作用域運算子的幫助下使用。

using std::iostream;

以下行是 C++ 中引用用法的一個示例。引用用於提供按引用呼叫語義。[7] 它們幫助程式設計師編寫比使用指標編寫的程式碼更簡潔、更容易理解的程式碼。透過指標模擬的按引用呼叫語義以及所有相關工作都由編譯器完成[就像 Pascal 中的 var 引數或 C# 中的 ref 引數一樣]。

實際上,引用可以看作是另一個變數的別名。在傳遞引數時,形式引數成為對應實際引數的別名。從技術上講,它是一個指向另一個變數的常量指標。因此,引用必須在其定義點進行初始化。[8] 也就是說,

int ivar = 100; ... // 編譯器會將下一行程式碼轉換為 int *const iref = &ivar; // int *const iref = &ivar; int &iref = ivar; // 正確。 int &iref2; // 錯誤

一旦定義為引用,就不能再引用其他變數。 也就是說,根據上面的定義,

int ivar2 = 200; ... iref = var2; // 將轉換為 *iref = var2;

不會使 iref 成為 ivar2 的別名。 它將設定 iref,並透過 iref 設定 ivar200

相應地,以下函式的轉換後的程式碼如下所示

void swap(int *const x, int *const y) { int temp = *x; *x =*y; *y = temp; } // void swap(int *const, int *const) 的結束

void swap(int& x, int& y) {
  int temp = x;
  x = y;
  y = temp;
} // end of void swap(int&, int&)

除了其不可否認的靈活性之外,C/C++ 中陣列和指標的特殊關係有時會導致難以發現的執行時錯誤。 例如,以下等效宣告。

long sum(int arr[5]); long sum(int arr[]); long sum(int *arr);

編譯器最終會將前兩個宣告轉換為第三個宣告,這意味著我們可以傳遞任何長度的陣列。 這與第一個宣告的意圖形成鮮明對比。 因此,程式設計師和使用者都應該更加努力地避免任何可能的執行時錯誤,例如使用越界索引值。

下一行是一個型別定義,我們將使用它來對陣列引數進行更嚴格的型別檢查。 它定義了一個對五個 int 的陣列的引用。 任何聲稱此型別的陣列識別符號不僅會檢查其元件型別,還會檢查其長度。 例如,任何嘗試將大小不為五的陣列傳遞給 sum 的嘗試都將被視為編譯時錯誤。

應該強調的是,這對那些在編譯時可以確定大小的陣列有效。 C++ 編譯器不會將任何執行時檢查(這會降低程式速度,因此與 C/C++ 的設計理念不符)合併到生成的程式碼中。 出於這個原因,以下片段甚至無法編譯。

long sum(int size) { // size 的值取決於傳遞的引數。 因此,la 的長度將在執行時確定。 int la[size]; array_of_five_ints a = la; ... } // long sum(int) 的結束

#define NO_OF_ELEMENTS 5
typedef int (&array_of_five_ints)[NO_OF_ELEMENTS];

long sum(array_of_five_ints arr) {
  long res = 0;
  for (int i = 0; i < NO_OF_ELEMENTS; i++) res += arr[i];

  return res;
} // end of long sum(array_of_five_ints)

既然引用是一個常量指標,那麼下面的內容可以看作是

typedef int (*const rf)(int);

也就是說,以下 typedef 定義了函式引用型別的同義詞 rf,它接受一個 int 並返回一個 int。 換句話說,任何接受一個 int 並返回一個 int 的函式,例如 multiply_with_3raise_to_the_3rd_power,都可以被視為 rf 的例項。

typedef int (&rf)(int);

int multiply_with_3(int i) {
  cout << "Tripling " << i << ": ";
	
  return 3 * i;
} // end of int multiply_with_3(int)

int raise_to_the_3rd_power(int i) {
  cout << "Raising " << i << " to the third power: ";
	
  return i * i * i;
} // end of int raise_to_the_3rd_power(int)

下一個函式展示了在 C++ 中實現回撥機制的另一種方法。 f_caller 生成的副作用取決於傳遞給它的函式作為其引數。

void f_caller(rf f) {
  cout << "In the f_caller..." << f(5) << endl;
} // end of void f_caller(rf)

int main(void) {
  int a = 5, b = 3;

  cout << "TESTING CALL-BY-REFERENCE" << endl;
  cout << "a: " << a << "\tb: " << b << endl;

沒有地址運算子! 編譯器會處理所有事情。[9] 使用者只需要知道的是,函式中發生的副作用是永久性的。

  swap(a, b);
  cout << "a: " << a << "\tb: " << b << endl;

  cout << "TESTING ARRAYS WITH SIZE INFORMATION" << endl;
  int ia[] = {1, 3, 5, 7, 9};
  cout << "Sum of array elements: " << sum(ia);

  cout << "TESTING CALLBACK" << endl;
  f_caller(multiply_with_3);
  f_caller(raise_to_the_3rd_power);

  return 0;
} // end of int main(void)

流操縱器

[edit | edit source]

C++ 中的所有流物件,例如 cout 和 cin,都維護一個狀態資訊,可以用來控制輸入/輸出操作的細節。 這包括精度等屬性浮點數、表格資料的寬度等。 以下是一個簡單的程式來演示 C++ 中的一些操縱器。

Manipulators.cxx
#include <fstream>
#include <iomanip>
#include <iostream>
using namespace std;

int main(void) {
  int i;
  ofstream outf("Output.dat");

  cout << "Enter an int value: "; cin >> i;
  outf << "Number entered: " << i << endl;

一個操縱器修改流物件的內部狀態並導致後續的輸入/輸出以不同的方式執行; 它不會寫入或讀取底層流。 例如,以下語句中的 setw 為下一個引數的輸出預留了與引數中傳遞的值一樣多的字元空間; left 以左對齊方式寫入所有後續輸出(直到它被另一個操縱器(如 right)更改)。

  outf << setw(12) << left << "Hex" 
       << setw(12) << " Octal"
       << setw(12) << " Dec" << endl;

如果生成的輸出沒有填滿為它預留的所有空間,我們選擇用下劃線字元填充剩餘的空位; 寫入十二個字元視窗的任何整數都會以右對齊的方式使用十六進位制表示法寫入,並透過 showbase 操縱器透過在輸出之前新增字首的方式傳遞給使用者。

  outf.fill('_');
  outf << right << setw(12) << hex << showbase << i;
  outf << " " << setw(12) << oct << i;
  outf << " " << setw(12) << setbase(10) << /* noshowbase << */ i << endl;

  bool bool_value = true;
  outf << endl << "bool_value: " << boolalpha << bool_value 
       << '\t' << noboolalpha << bool_value << endl;

下一行使用預設精度值初始化區域性變數精度。 這恰好是 6,這意味著在小數點後寫入六位數字。 如果你想要更高的精度,你可以將它作為引數傳遞給同一個函式,或者以類似的方式使用 setprecision

  int precision = outf.precision();
  double d, divisor;
  do {
    cout << "Enter a double value: "; cin >> divisor;
    if (divisor == 0) break;
    outf << endl << "Double value: " << divisor << endl;
    d = 1 / divisor;
    while (divisor != 0) {
      outf << "Precision: " << precision << "... d: " << fixed << d;
      outf << " Using sci. notn.: " << scientific << uppercase << d << endl;
      cout << "New precision: "; cin >> precision;
      if (precision != 0) {
        outf << "New precision: " << precision;
        outf << setprecision(precision);
      } else break;
    } // end of while(divisor != 0)
    precision = outf.precision();
  } while (divisor != 0);

  return 0;
} // end of int main(void)

執行程式

[edit | edit source]

gxx -o Test_Manipulator.exe Manipulators.cxx↵ # 使用 DJGPP-gcc Test_Manipulator > Output.dat↵ 輸入一個 int 值: 12345↵ 輸入一個 double 值: 5.6↵ 新精度: 15↵ 新精度: 16↵ 新精度: 17↵ 新精度: 18↵ 新精度: 19↵ 新精度: 0↵ 輸入一個 double 值: 4.56↵ 新精度: 18↵ 新精度: 17↵ 新精度: 16↵ 新精度: 15↵ 新精度: 0↵ 輸入一個 double 值: 0↵

Output.dat

輸入的數字: 12345 十六進位制 八進位制 十進位制 ______0x3039 ______030071 _______12345 bool_value: true 1 雙精度值: 5.6 精度: 6... d: 0.178571 使用科學記數法: 1.785714E-01 新精度: 15 精度: 15... d: 0.178571428571429 使用科學記數法: 1.785714285714286E-01 新精度: 16 精度: 16... d: 0.1785714285714286 使用科學記數法: 1.7857142857142858E-01 新精度: 17 精度: 17... d: 0.17857142857142858 使用科學記數法: 1.78571428571428575E-01 新精度: 18 精度: 18... d: 0.178571428571428575 使用科學記數法: 1.785714285714285754E-01 新精度: 19 精度: 19... d: 0.178571428571428575 使用科學記數法: 1.785714285714285754E-01 雙精度值: 4.559999999999999609E+00 精度: 19... d: 0.219298245614035103 使用科學記數法: 2.192982456140351033E-01 新精度: 18 精度: 18... d: 0.219298245614035103 使用科學記數法: 2.192982456140351033E-01 新精度: 17 精度: 17... d: 0.21929824561403510 使用科學記數法: 2.19298245614035103E-01 新精度: 16 精度: 16... d: 0.2192982456140351 使用科學記數法: 2.1929824561403510E-01 新精度: 15 精度: 15... d: 0.219298245614035 使用科學記數法: 2.192982456140351E-01

註釋

[edit | edit source]
  1. 即使在這種情況下,也可能存在更好的解決方案,我們將在後面的“連結 C 和 C++ 程式”中介紹。
  2. 在許多編譯器中得到了廣泛支援,這現在是 C 程式語言的標準功能。
  3. 也就是說,你仍然可以在不將程式設計實體(例如:類、函式等)放在特定名稱空間的情況下編寫 C++ 程式。在這種情況下,這些實體被稱為放置在全域性名稱空間中。然而,這種風格可能會導致名稱衝突問題,這是由於同一個名稱空間中的實體不能擁有相同的名稱。如果你可以訪問原始碼,這個問題可以透過更改相關實體的名稱並使它們唯一來解決。但這在沒有原始碼的情況下是行不通的。在這種情況下,解決方案是使用名稱空間。
  4. 請注意,沒有標準的方法來破壞函式名;編譯器編寫者可以自由選擇自己的方案。
  5. 除了函式外,還可以引數化類。有關更多資訊,請參閱引數化型別章節。
  6. 如果你認為這聽起來不太對,很可能是你一直在使用支援語言擴充套件的 C 編譯器,這意味著移植你的程式碼可能是一項艱鉅的任務。如果你不相信,嘗試將 -pedantic(在 gcc 中)或 /Tc(在 MS Visual C/C++ 中)新增到你的命令列並看看會發生什麼!
  7. 在其他情況下,使用引用可以使生活更輕鬆。有關更多資訊,請參閱基於物件的程式設計章節。還應該注意,正如繼承講義中將要展示的那樣,透過引用和指標可以實現動態分派。
  8. 記住初始化和賦值之間的區別?常量必須在其建立時賦予一個值;它不能在沒有初始值的情況下建立。它也不能被賦予一個新值,這解釋了為什麼引用(由編譯器管理的常量指標)在初始化後不能被修改為引用另一個變數。
  9. 也就是說,編譯器將默默地將此行轉換為 swap(&a, &b);
華夏公益教科書