跳轉到內容

使用 C 和 C++/面向物件程式設計的程式語言概念

來自華夏公益教科書

語言,本質上是與他人分享無數色調影像的活動,只有當參與方想出相似,如果不是完全相同的想法、經驗或設計的描述時才能取得成功。當滿足以下標準時,成功才有可能

  1. 參與方共享一種共同媒介,
  2. 這種共同媒介支援相關的概念。

缺乏這些標準,將使交流變成一場噩夢般的啞劇表演。想象兩個人,他們之間沒有共同媒介,試圖互相交流。太多模糊的空間,不是嗎?事實上,即使雙方說同一種語言,他們的人生觀,讀作“正規化”,也可能使交流成為一項難以忍受的練習。

一個概念,無論是抽象的還是具體的,如果沒有在說話者的心目中佔有一席之地,它就不會在語言中有任何相應的表示。例如,阿拉伯語使用者用同一個詞來指代冰和雪,而愛斯基摩人有幾十個關於雪的詞。然而,這不能被用作智力不足的證明:在描述駱駝的品質時,角色會反轉。

最後一個缺陷用兩種方法來處理:引入一個新詞,可能與已有的詞有關;或者發明一個習語來表達無法表達的東西。前者似乎是更好的選擇,因為後者容易被誤解,因此會導致歧義,這使我們回到原點。它還會模糊它的 - 也就是說,新概念 - 與其他概念之間的關係,並將語言的詞彙變成一片無枝樹的森林。

那麼,程式語言呢?程式語言,像自然語言一樣,是為了與他人交流而設計的:機器語言用於與特定的處理器交流;高階語言用於向程式設計師同事表達我們的解決方案;規範語言將分析/設計決策傳達給其他分析師/設計人員。

如果我們已經有了它 - 也就是說,將我們的想法傳達給她的/他的陛下,計算機 - 作為我們唯一目標,提供一個解決問題的方案,將不會是一項如此困難的任務。但是,我們還有另一個目標,這更值得我們付出智力努力:向其他人解釋我們的解決方案。實現這個更難以捉摸的目標需要採用紀律嚴明的做法和一系列高階概念。前者有助於分析手頭的問題,併為其解決方案提供設計。它使我們更容易發現重複出現的模式,並將經過驗證的解決方案應用於子問題。後者是用來表達自己的詞彙。在這種情況下,使用習語而不是語言結構可能會導致誤解,併成為新人和少數幸運者之間的一道障礙。

另一方面,同化習語不僅會讓你說這種語言,還會讓你成為母語使用者之一。現在,說外語不再是一項枯燥的運用語法規則的練習,而是在他人的思想領域中進行的智力旅程。如果這種旅程沒有被縮短,通常會揭示習語所替代的更多關於概念的資訊;它幫助你[自下而上]建立一個相互關聯的概念網路。下次你踏上旅程時,你之前豎立的路標將幫助你更容易找到自己的路。

那麼,我們應該學習哪些程式語言呢?如果問這個問題的是你 10 歲的表弟,如果他/她從 MS Visual Basic 或其他基於 Web 的指令碼語言開始,也不會是世界末日;如果是一個未來專業人士,他/她將透過編寫程式來謀生,他/她最好更關心概念,而不是某些程式語言的語法。在這種情況下,重要的是能夠建立一個概念基礎,而不是一堆隨機的流行語。出於這個原因,C/C++ 憑藉其慣用性,將成為我們踏上程式語言概念之旅的主要工具。

程式設計(或軟體生產)可以看作是將一個問題(用宣告式語言表達)轉換成一個解決方案(用計算機的機器程式碼表達)的活動。這種轉換中的一些階段由自動程式碼生成翻譯器(如編譯器和彙編器)完成,而另一些階段則由人工代理完成。[1]

除了將負擔轉移到程式碼生成器之外,簡化這種轉換過程的關鍵是將源語言中的概念與目標語言中的概念相匹配。未能做到這一點意味著需要付出額外的努力,並導致使用臨時技術和/或習語。這種方法雖然足以將解決方案傳達給計算機,但並不適合向程式設計師同事解釋你的意圖。然而,在某些情況下,接受這種觀察的有效性並不能幫助你。在這種情況下,你應該努力採用慣用方法,而不是使用臨時技術。這正是我們將在本講義中嘗試實現的目標:我們將建立一種半形式的技術來模擬 C 中的物件概念。如果成功,這種方法將簡化從基於物件的初始模型(通常是邏輯設計規範)到過程模型(C 程式)的過渡。

為了實現我們的目標,我們將採用一種我們從“程式設計級結構”章節的最後兩節中學到的技術:使用較低階的概念來模擬一個概念。由於 C 中沒有類或模組概念,我們將使用一個作業系統概念:檔案。要了解其作用,請繼續閱讀!

堅持廣泛採用的慣例,標頭檔案的內容,或其部分內容,被放在 #ifndef-#endif 對中。這避免了檔案的多次包含。第一次預處理器處理該檔案時,COMPLEX_H 未定義,並且 #ifndef#endif 指令之間的所有內容都將被包含。下次預處理同一個原始檔時,可能由其他包含檔案包含的標頭檔案,COMPLEX_H 已經定義,並且 #ifndef-#endif 對之間的所有內容都將被跳過。

COMPLEX_H 宏之後,包含 General.h 以引入 BOOL 的宏定義。

Complex.h
#ifndef COMPLEX_H
#define COMPLEX_H

#include "General.h"

以下是一個所謂的前向宣告。我們聲明瞭使用名為 struct _COMPLEX 的資料型別的意圖,但沒有透露其結構的細節。我們透過在實現檔案 Complex.c 中提供定義來填寫細節。Complex 資料型別的使用者不需要了解實現的細節。這種方法使實現者能夠靈活地更改底層資料結構,而不會破壞現有的客戶端程式。

我們將定義推遲到實現檔案的原因是,C 中沒有訪問說明符(例如,publicprotectedpublic),就像我們在面向物件的程式語言中一樣。這迫使我們對使用者不應該訪問的一切都保密。

struct _COMPLEX;

注意,Complex 被定義為一個指標。這符合介面應該包含不會改變的內容的規則。無論複數的表示如何,都可以根據實現者的意願進行更改,指向這種表示的指標的記憶體佈局永遠不會改變。因此,我們在函式原型中使用 Complex 而不是 struct _COMPLEX

請注意,介面和實現之間的區別是透過堅持慣例來加強的,而不是由 C 編譯器檢查的某些語言規則來加強的。我們可以將介面和實現合併到一個檔案中,編譯器不會有任何抱怨。

typedef struct _COMPLEX* Complex;

所有以下原型(函式宣告)都用 extern 關鍵字限定。這意味著它們的實現(函式定義)可能出現在另一個檔案中,在本例中是相應的實現檔案。這使得匯出函式成為可能:所有包含當前標頭檔案的檔案都能夠使用這些函式,而無需為它們提供任何實現。從這個角度看,以下原型列表可以被視為一個底層物件的介面,它聲稱提供了一個實現。

定義:介面是一種與物件通訊的抽象協議。

所有 extern 函式都從實現檔案匯出(也就是說,它們對用於構建可執行檔案的其他檔案可見),並由它們的客戶端連結——讀作“匯入”。這樣的匯入函式被稱為具有 *外部連結*。如果它們在另一個檔案中實現,這些函式的地址對編譯器來說是未知的,並在編譯器生成的​​目標檔案中被標記為如此。連結器在構建可執行檔案的過程中會稍後填充這些值。

extern Complex Complex_Create(double, double);
extern void Complex_Destroy(Complex);

請記住,Complextypedef 對指向 struct _COMPLEX 的指標。也就是說,它本質上是一個指標。因此,當使用 const 關鍵字限定時,是被保護免受更改的指標,而不是指標指向的欄位。這種行為類似於 Java 中顯示的行為:當一個物件欄位被宣告為 final 時,被保護免受更改的是控制代碼,而不是底層物件。

根據使用 const 的位置不同,它的含義也不同。

i (可變) int i;
一個 int
i (不可變) const int i;
一個 int
i (可變) *i (不可變) const int *i;
指向 int 的指標 一個 int
i (不可變) *i (可變) int *const i;
指向 int 的指標 一個 int
i (不可變) *i (不可變) const int *const i;
指向 int 的指標 一個 int

另一個值得一提的是所有函式共有的第一個形式引數:const Complex this。它對應於面向物件程式語言中的目標物件(隱式傳遞的第一個引數)。該函式應用於作為第一個引數傳遞的物件,該物件被恰當地命名為 this。雖然物件內容可以改變,但該物件的標識在函式呼叫期間不能改變。這就是為什麼我們使用 const 關鍵字限定引數型別。

extern Complex Complex_Add(const Complex this, const Complex);
extern Complex Complex_Divide(const Complex this, const Complex);
extern BOOL Complex_Equal(const Complex this, const Complex);
extern double Complex_Im(const Complex this);
extern Complex Complex_Multiply(const Complex this, const Complex);
extern void Complex_Print(const Complex this);
extern double Complex_Re(const Complex this);
extern Complex Complex_Subtract(const Complex this, const Complex);

#endif

出於顯而易見的原因,Complex_CreateComplex_Destroy 的簽名構成上述模式的例外。類似建構函式的函式 Complex_Create 為尚未建立的物件分配堆記憶體並對其進行初始化,而類似解構函式的函式 Complex_Destroy 釋放物件使用的堆記憶體,並透過將 NULL 分配給它來使物件指標不可用。

實現

[edit | edit source]
Complex.c
#include <math.h>
#include <stdio.h>
#include <stdlib.h>

#include "General.h"

以下指令乍一看似乎是多餘的。畢竟,為什麼要包含原型列表(加上其他一些內容)呢?畢竟是我們提供了它們的函式體?透過包含此列表,我們讓編譯器同步介面和實現。假設您修改了實現檔案中的函式簽名,並且忘記對介面檔案進行相關更改;具有修改簽名的函式將不可用(因為它未在介面中列出),而介面檔案中的函式將沒有相應的實現(因為預期的實現現在具有不同的簽名)。當我們包含標頭檔案時,編譯器將能夠發現不匹配並讓您知道。

具有諷刺意味的是,這成為可能的原因是 C 中缺乏函式過載。C 編譯器會將實現作為相應宣告的定義,並確保它們匹配。如果我們有函式過載,編譯器會將定義作為宣告的過載例項,並繼續編譯。

與使用“\”作為分隔符的 DOS 不同,UNIX 使用“/”作為路徑名元件之間的分隔符。C 主要在基於 UNIX 的環境中開發,因此使用“/”用於相同目的。我們之前示例之所以能夠正常工作,是因為所用編譯器是 DOS 實現,並且正確解釋了“\”。如果我們想要更可移植的程式碼,我們應該使用“/”而不是“\”。

#include "math/Complex.h"

以下原型在實現檔案中提供,因為它們不是介面的一部分。它們用作實用函式來實現其他函式。如果它們是介面的一部分,我們應該將它們放在相應標頭檔案中,Complex.h。

注意這兩個函式被限定為 static。當全域性變數和函式被宣告為 static 時,它們將被設定為定義它們的​​檔案的本地。[2]也就是說,它們從檔案外部無法訪問。這樣的物件或函式被稱為具有 *內部連結*。

在 C 中,函式、變數和常量預設情況下是 extern。換句話說,除非另有說明,否則它們可以從當前檔案外部訪問。這意味著我們可以省略標頭檔案中所有 extern 的出現。不過,這樣做不可取。它會使將您的程式碼從 C 移植到 C++ 變得困難。例如,C++ 中的常量預設情況下是 static,這與我們在 C 中所擁有的完全相反!

定義: *實現* 是一種具體的資料型別,它透過為介面的每個抽象操作提供精確的語義解釋來支援一個或多個介面。

static Complex Complex__Conjugate(const Complex);
static double Complex__AbsoluteValue(const Complex);

我們提供了標頭檔案中前向宣告的詳細資訊。請注意,這是實現檔案,以下定義僅供實現者檢視。通常,使用者唯一能看到的​​檔案是標頭檔案和目標檔案。

struct _COMPLEX {
  double im, re;
};

以下函式用於建立和初始化一個 Complex 變數,類似於面向物件程式語言中 new 運算子和建構函式的組合。

定義: *建構函式* 是一個特殊的、隱式呼叫的[3]函式,它初始化一個物件。在通常透過 new 運算子[4]成功分配記憶體後,它由編譯器合成的程式碼呼叫。

請注意,類似建構函式的函式必須在我們的案例中顯式呼叫。因為建構函式的概念不是 C 程式語言的一部分。

有時我們需要不止一個這樣的函式。事實上,至少還有兩種方法可以構造複數:從另一個複數和極座標。不幸的是,如果我們想新增另一個建構函式;我們必須想出一個具有新名稱的函式,或者透過單個可變引數函式提供不同的函式定義,因為 C 不支援函式名稱過載。

定義: 函式名稱過載允許提供對不同引數型別執行通用操作的多個函式例項共享一個通用名稱。

Complex Complex_Create(double real, double imaginary) {
  Complex this;

  this = (Complex) malloc(sizeof(struct _COMPLEX));
  if (!this) {
    fprintf(stderr, "Out of memory...\n");
    return(NULL);
  } /* end of if(!this) */

  this->re = real;
  this->im = imaginary;

  return(this);

假設廣泛使用的約定是將返回值儲存在暫存器中,在建構函式完成執行後,我們在下一頁提供了部分記憶體映像。

Partial memory image (constructor)

觀察在堆上分配的記憶體區域的生命週期不受限於區域性指標 this 的生命週期。在函式結束時,this 會自動被丟棄,而堆記憶體仍然有效,這得益於複製到暫存器中的指標。

} /* end of Complex Complex_Create(double, double) */

以下函式用於銷燬和垃圾回收一個 Complex 變數。它類似於面向物件程式語言中的解構函式。

定義: *解構函式* 是一個特殊的、隱式呼叫的函式,它清理物件透過執行其建構函式或透過執行其任何成員函式獲得的任何資源。它通常在呼叫記憶體取消分配函式之前呼叫。

具有垃圾回收的程式語言引入了終結器函式的概念。現在,垃圾回收器回收未使用的堆記憶體,程式設計師不再需要為此操心。但是,檔案、套接字等呢?它們必須以某種方式返回給系統,這就是終結器存在的目的。

我們的解構函式類似的函式非常簡單。我們所要做的就是返回分配給作為函式唯一引數傳遞的Complex變數的堆記憶體。

free返回其引數指向的記憶體,而不是引數本身。另一個提醒:free用於釋放堆記憶體;靜態資料和執行時堆疊記憶體由編譯器合成的程式碼釋放。

無論是堆中的區域還是其他區域,都不應該對已釋放記憶體的內容進行假設。這樣做會導致不可移植的軟體,其行為不可預測。

void Complex_Destroy(Complex this) { free(this); }

Complex Complex_Add(const Complex this, const Complex rhs) {
  Complex result = Complex_Create(0, 0);

  result->re = this->re + rhs->re;
  result->im = this->im + rhs->im;

  return(result);
} /* end of Complex Complex_Add(const Complex, const Complex) */

Complex Complex_Divide(const Complex this, const Complex rhs) {
  double norm = Complex__AbsoluteValue(rhs);

  Complex result = Complex_Create(0, 0);
  Complex conjugate = Complex__Conjugate(rhs);
  Complex numerator = Complex_Multiply(this, conjugate);

  result->re = numerator->re / (norm * norm);
  result->im = numerator->im / (norm * norm);

  Complex_Destroy(numerator);
  Complex_Destroy(conjugate);

  return(result);
} /* end of Complex Complex_Divide(const Complex, const Complex) */

以下函式檢查兩個複數的相等性。請注意,相等性檢查和同一性檢查是兩件不同的事情。這就是我們不使用指標語義進行比較的原因。相反,我們檢查兩個數字的對應欄位是否相等。

示例:同一性檢查和相等性檢查是不同的。

Complex c1 = Complex_Create(2, 3); Complex c2 = Complex_Create(2, 3); Complex c3 = c1;

鑑於上述定義,所有三個物件都相等,而只有 c1 和 c3 是相同的。

BOOL Complex_Equal(const Complex this, const Complex rhs) {
  if (this->re == rhs->re && this->im == rhs->im)
    return(TRUE);
    else return(FALSE);
} /* end of BOOL Complex_Equal(const Complex, const Complex) */

以下函式用作所謂的get-method(或accessor)。我們提供這些函式是為了避免違反資訊隱藏原則。使用者應該透過函式訪問底層結構成員。有時還會提供函式來更改成員的值。這些被稱為set-methods(或mutators)。

定義:資訊隱藏是一種正式機制,用於阻止程式的函式直接訪問抽象資料型別的內部表示。

需要注意的是,還可以為物件的屬性提供訪問器[和變異器],這些屬性沒有由底層結構的成員支援。例如,複數有兩個極座標屬性,可以從它的笛卡爾座標屬性推匯出來:模和角度。

double Complex_Im(const Complex this) { return(this->im); }

Complex Complex_Multiply(const Complex this, const Complex rhs) {
  Complex result = Complex_Create(0, 0);

  result->re = this->re * rhs->re - this->im * rhs->im;
  result->im = this->re * rhs->im + this->im * rhs->re;

  return(result);
} /* end of Complex Complex_Multiply(const Complex, const Complex) */

下一個函式旨在與 Java 的toString功能類似。但是,此函式不返回值;它只是將輸出寫入標準輸出檔案,這比它的 Java 對等方肯定要靈活得多,在 Java 對等方中,返回的是一個String,使用者可以按照自己認為合適的方式使用它:可以將其傳送到標準輸出/錯誤檔案、磁碟檔案或套接字末尾監聽的另一個應用程式。具有這種語義的函式如下所示。

char* Complex_ToString(const Complex this) { double im = this->im; double re = this->re; char *ret_str = (char *) malloc(25 + 1); if(im == 0) { sprintf(ret_str, %g, re); return ret_str; } if(re == 0) { sprintf(ret_str, %gi, im); return ret_str; } sprintf(ret_str, (%g %c %gi), re, im < 0 ? - : +, im < 0 ? im : im); return ret_str; } /* end of char* Complex_ToString(const Complex) */

但是,上述實現的使用者不應忘記返回為儲存字串表示的char*物件分配的記憶體。

char* c1_rep = Complex_ToString(c1); ... // use c1_rep free(c1_rep); // C 中沒有自動垃圾回收!

void Complex_Print(const Complex this) {
  double im = this->im, re = this->re;

  if (im == 0) {printf(%g, re); return;}
  if (re == 0) {printf(%gi, im); return;}
  printf("(%g %c %gi)", re, im < 0 ? - : +, im < 0 ? im : im);
} /* end of void Complex_Print(const Complex) */

double Complex_Re(const Complex this) { return(this->re); }

Complex Complex_Subtract(const Complex this, const Complex rhs) {
  Complex result = Complex_Create(0, 0);

  result->re = this->re - rhs->re;
  result->im = this->im - rhs->im;

  return(result);
} /* end of Complex Complex_Subtract(const Complex, const Complex) */

接下來的兩個函式沒有出現在標頭檔案中。使用Complex資料型別的使用者甚至不知道它們。因此,他們不能[也不應該]直接使用它們。實現者可以隨時選擇更改這些函式和其他隱藏的實體,例如型別的表示。這是應用資訊隱藏原則給我們帶來的靈活性。

static Complex Complex__Conjugate(const Complex this) {
  Complex result = Complex_Create(0, 0);

  result->re = this->re;
  result->im = - this->im;
  
  return(result);
} /* end of Complex Complex__Conjugate(const Complex) */

static double Complex__AbsoluteValue(const Complex this) {
  return(hypot(this->re, this->im));
} /* end of double Complex__AbsoluteValue(const Complex) */

測試程式

[edit | edit source]
Complex_Test.c
#include <stdio.h>

包含 Complex.h 會引入可以在Complex物件上應用的函式的原型。這使 C 編譯器能夠檢查這些函式是否在適當的上下文中正確使用。標頭檔案的另一個目的是作為介面規範,供人類讀者閱讀。

注意,引入的是原型,而不是包含實現的程式碼。外部函式的程式碼由連結器插入。

通常,使用者沒有訪問原始檔的許可權。這樣做的原因是為了保護實現者的智慧財產權。相反,提供的是不可讀的物件檔案。物件檔案是相應原始檔的編譯版本,因此在語義上等同於原始檔。

#include "math/Complex.h"

int main(void) {
  Complex num1, num2, num3;

  num1 = Complex_Create(2, 3);
  printf("#1 = "); 
  Complex_Print(num1); printf("\n");
  num2 = Complex_Create(5, 6);
  printf("#2 = ");
  Complex_Print(num2); printf("\n");

一旦完成下一個賦值命令,我們將得到如下所示的區域性記憶體影像

Partial memory layout (1)

注意堆分配的非連續性。儘管對於此大小的程式,分配的記憶體可能很可能是連續的,但隨著程式變大,這將變得不可能。記憶體分配器的唯一工作是滿足分配需求;分配記憶體的地址無關緊要。為了做到這一點,它可以使用不同的演算法,例如首次適應、最差適應、最佳適應等。

  num3 = Complex_Add(num1, num2);
  printf("#1 + #2: "); Complex_Print(num3); printf("\n");

Complex_Add中,我們在堆上建立了一個複數,並返回指向它的指標作為結果。下次我們使用相同的Complex變數來儲存另一個操作的結果時,儲存上一次操作結果的舊位置將無法訪問。這種無法訪問,因此無法使用的記憶體位置被稱為垃圾。在具有自動垃圾回收的程式語言中,這種未使用的堆記憶體由語言的執行時系統回收。在沒有自動垃圾回收的面向物件程式語言中,這必須由程式設計師透過呼叫delete之類的函式來處理,該函式反過來呼叫一個名為解構函式的特殊函式。在非面向物件程式語言中,必須模擬解構函式,並且程式設計師必須顯式地將這些記憶體區域返回給系統以供重用。在我們的例子中,模擬解構函式的函式名為Complex_Destroy

完成下一行後,我們將得到以下區域性記憶體影像

Partial memory layout (2)

觀察到 num3 仍然指向同一個位置。也就是說,我們仍然可以使用 num3 來操作記憶體的同一區域。但是,沒有保證關於內容。所以,為了讓使用者遠離使用此值的誘惑,最好將 num3 的值更改為不能用來引用物件的任何東西。這個值是 NULL。每次釋放記憶體區域時,指向它的指標應該要麼指向另一個區域,就像在這個測試程式中一樣,要麼使用者應該將 NULL 分配給指標變數。第二種更安全的方法將分配 NULL 的責任交給實現者。問題是我們需要修改指標本身,而不是它指向的區域。可以透過進行以下更改來消除此缺陷

Complex.c

... void Complex_Destroy(Complex* this) { free(*this); *this = NULL; } /* end of void Complex_Destroy(Complex* ) ...

Complex_Test.c

... Complex_Destroy(&num3); ...

Complex_Destroy(num3);
num3 = Complex_Subtract(num1, num2);
	printf("#1 - #2: ");	Complex_Print(num3); printf("\n");

Complex_Destroy(num3);
num3 = Complex_Multiply(num1, num2);
	printf("#1 * #2: ");	Complex_Print(num3); printf("\n");

Complex_Destroy(num3);
num3 = Complex_Divide(num1, num2);
	printf("#1 / #2: ");	Complex_Print(num3); printf("\n");

Complex_Destroy(num3);
Complex_Destroy(num1);
Complex_Destroy(num2);

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

執行測試程式

[編輯 | 編輯原始碼]
gcc –I ~/include –c Complex.c↵ # ~ 代表當前使用者的家目錄;注意 –I 和 ~/include 之間的空格!

上面的命令將生成 Complex.o。注意 –I 和 –c 選項的使用。前者告訴預處理器關於查詢非系統標頭檔案的位置的提示,而後者將導致編譯器在連結之前停止。除非在給定的目錄列表中找不到標頭檔案,否則會在系統目錄中搜索它。

如您所見,我們的程式碼沒有 main 函式。也就是說,它本身不可執行。它只提供複數的實現。此實現稍後將由諸如 Complex_Test.c 之類的程式使用,這些程式操作複數。

gcc –I ~/include –lm –o Complex_Test Complex_Test.c Complex.o↵

上面的命令將編譯 Complex_Test.c 並將其與所需的 obj 檔案連結。連結的輸出將寫入名為 Complex_Test 的檔案中。-l 選項用於連結到庫。[5] 在這種情況下,我們連結到名為 libm.a 的庫,其中 m 代表數學庫。我們需要連結到此庫以確保函式的 obj 程式碼(例如 hypot)包含在可執行檔案中。作為連結到數學庫的結果,只有包含 hypot 實現的檔案的 obj 程式碼包含在可執行檔案中。

程式開發過程

[編輯 | 編輯原始碼]

整個過程可以用下圖來表示。

Producing an executable

圖中黑色區域代表過程的實現者一方。這個盒子裡面發生了什麼與使用者無關;涉及的子過程數量、產生的中間輸出對他們來說無關緊要。事實上,該模組可以用除 C 之外的其他程式語言編寫,只要客戶端和實現者使用相同的二進位制介面,它仍然可以正常工作。他們應該關心的是這個黑盒的輸出,Complex.o,和標頭檔案,Complex.h,這是理解 Complex.o 提供的功能所必需的。

請注意,Complex.o 在語義上等效於 Complex.c。區別在於它們對人類讀者和計算機的可理解性:C 原始碼對人類來說是可理解的,而相應的 obj 檔案則不是。這種不可理解性可以保護實現者的智慧財產權。在專案上花費數月後,實現者將 obj 模組交付給客戶端,其中不包含有關其實現方式的任何提示。

一旦使用者獲得了 obj 模組和相關的標頭檔案,她會按照以下步驟使用這個 obj 模組來構建一個可執行檔案。

  • 編寫程式的原始碼。現在這個程式將引用 Complex.o 中提供的功能,我們必須包含相關的標頭檔案,在本例中是 Complex.h。這將確保正確使用 Complex.o 中提供的功能。
  • 一旦您讓程式編譯,您必須提供實現使用功能的程式碼。此功能在名為 Complex.o 的 obj 模組中提供給您。您只需要將它與測試程式的 obj 程式碼連結起來。
  • 除了 obj 模組外,您還必須訪問 Complex.o 和程式中使用的庫和其他 obj 模組。換句話說,我們可能無法測試我們的程式,除非我們擁有某些檔案。在我們的例子中,這些檔案是標準 C 庫和數學庫。除非我們在磁碟上擁有這些庫,或者實現者將它們提供給我們,否則我們將無法構建可執行檔案。
總結
檔案型別 實現者 使用者 目的
源模組 (*.c) 在軟體開發、升級和維護中由人工使用
目標模組 (*.o, *.a, *.so 或 *.obj, *.lib, *.dll) 由連結器/載入器使用,以在客戶端程式中提供缺失的功能;對人類不可理解;由相應的源模組自動生成,在語義上等效於相應的源模組
標頭檔案 (*.h) 用於作為實現者和使用者之間的契約;由編譯器用於型別檢查,由使用者用於探索 obj 模組中提供的功能
  1. 這些由人工執行的活動中,除一項外,都可以由自動機完成。然而,設計問題的初始模型的行為似乎還將與我們共存一段時間。
  2. 函式在當前檔案的相對地址是在編譯時確定的。換句話說,此類函式的地址是靜態確定的,因此是 static 關鍵字。
  3. 它不是在控制級別結構章節中定義的意義上的隱式呼叫。雖然不是程式設計師進行呼叫,但建構函式呼叫並不在程式設計師的控制範圍之外。程式設計師知道何時以及哪個建構函式將被呼叫。
  4. 在 C++ 中,除了在堆中建立它並透過指標使用之外,還可以將物件嵌入靜態資料區域或執行時堆疊中。也就是說,它們可以在沒有指標的情況下訪問。此類物件遵守 C++ 範圍規則,就像其他變數一樣:它們在相關範圍被進入和退出時自動建立和銷燬。因此,它們不需要呼叫 new 運算子。在 C# 中使用 struct(值型別)中可以看到相同的行為。
  5. 一個 [靜態] 庫基本上是一組 .o(在 MS Windows 中為 .obj)檔案,透過編譯相應的一組 .c 檔案獲得,加上一些元資料。此元資料用於加速 .o 檔案的提取並回答有關庫內容的查詢。通常有一個或多個 .h 檔案,其中包含使用這些 .o 檔案所需的宣告。
華夏公益教科書