使用 C 和 C++/面向物件程式語言概念
企業的成功取決於其競爭力,競爭力通常轉化為以合理的價格在合理的時間內提供易於升級的高質量產品。軟體行業也不例外:越快、越便宜、越靈活越好。實現這一目標的關鍵是避免重複做同樣的事情:重用。
[1]例如,重用程式碼[2] 將節省我們重用模組的開發時間;它通常會導致更快和/或更小的程式碼,因為它已經被分析和微調;它會更可靠,因為它已經被除錯。
在 C 或 Pascal 等程序式程式設計語言中,子程式是重用的自然單元,組合是實現重用的方式:人們透過將簡單的子程式組合起來構建複雜的子程式。面向物件程式語言提供了一種替代方案:繼承。[3] 除了將物件從子物件組合起來,還可以使用繼承從現有類派生一個新類[4],這意味著新類的物件將包含基類的一個子物件。換句話說,組合和繼承基本上歸結為同一件事。區別在於執行該過程的代理:程式設計師和編譯器,分別。
在本章中,在 C 中提供了一個非常簡單的面向物件模擬,希望能提供一些見解。在瀏覽程式碼時,請記住,這不能作為權威來源。它肯定比這個程式碼中提供的要多得多。
此外,還介紹了靜態檢查器 Splint。它可以用來檢查原始碼是否存在語義錯誤,例如記憶體洩漏、無限迴圈、使用未初始化資料等等。這樣的工具和編譯器可以協同使用,使 C 成為更安全的程式語言。
以 _Private 為字尾的標頭檔案包含的資訊通常不應該向模組使用者公開。這個明顯的錯誤是由於 C [和 C++] 的編譯模型造成的,它規定為了定義一個變數,編譯器必須訪問相關型別的定義。如果你使用指向結構的指標而不是普通結構,這不會有太大影響。但是,繼承需要將物件從子物件組合起來,這意味著派生型別需要訪問基型別的結構。現在,基型別和派生型別的實現者很可能是不同的 [組] 程式設計師。必須有一種方法可以在這些方面之間傳遞所需的資訊。在 C 中,唯一可以做到這一點的方法是將型別定義放在標頭檔案中,這就是我們在檔名以 _Private 為字尾的檔案中所做的事情。
#ifndef EMPLOYEE_PRIVATE_H
#define EMPLOYEE_PRIVATE_H
#include “misc/inheritance/Employee.h”
下一個結構定義了 Employee 物件的成員。其中有兩個指向函式的指標,這些指標可以指向程式碼段中的不同位置。換句話說,不同的 Employee 物件可以在這些指標中具有不同的值,這意味著它們可以對相同的請求做出不同的反應。在我們的例子中,我們會說所有 Employee 都獲得工資和獎金,其計算方法取決於一個特定的 Employee 實際上是否是 Engineer 或 Manager。
請注意,我們在標頭檔案中包含了結構的詳細資訊,這違背了我們通常的做法,即推遲到實現檔案。這仍然可以透過將結構定義 (struct _EMPLOYEE) 轉換為指向不完整型別的指標來完成。但是,它不會反映繼承的真實本質:繼承 是由編譯器執行的組合;子類的物件包含——除了它自己的資料成員之外——超類的子物件。[5]
#define EMPLOYEE_PART \
CALC_SALARY calc_salary; \
CALC_BONUS calc_bonus; \
char* name; \
Dept_Type department;
struct _EMPLOYEE {
EMPLOYEE_PART
};
#endif
#ifndef EMPLOYEE_H
#define EMPLOYEE_H
struct _EMPLOYEE;
typedef struct _EMPLOYEE* Employee;
typedef long (*CALC_SALARY)(const Employee);
typedef long (*CALC_BONUS)(const Employee);
typedef struct _ALL_EMP_FUNCS {
CALC_SALARY calc_salary;
CALC_BONUS calc_bonus;
} Employee_Functions;
typedef enum _DEPT { RESEARCH, SALES, MANAGEMENT, PRODUCTION } Dept_Type;
C 為了提高效能,是一種非常寬鬆的程式語言。這通常意味著大多數在許多其他程式語言中推遲到編譯器的簿記工作都落在了程式設計師的肩上。當程式不大和/或程式設計師很熟練時,這就可以了。她可以主動地跳過一些上述東西,這通常意味著更快的和更精簡的程式碼。或者......
處理令人不祥的替代方案的一種方法是使用一個工具來檢測原始碼中的程式設計異常。Splint 就是這樣一個工具,它靜態地檢查 C 程式是否存在潛在的安全漏洞和程式設計錯誤。[6] 這樣做有時可能需要對原始碼進行註釋。這些註釋是風格化的註釋,以 /*@ 開頭,以 @*/ 結尾。[7] 以下原型是對此的示例。
/*@null@*/ 在返回型別之前表示該函式除了返回指向某個記憶體區域的指標之外,還可以返回一個 NULL 值。這種可能性意味著粗心的程式設計師最終可能會嘗試使用不存在的 Employee 物件。我們不希望看到這種情況發生,我們的朋友 Splint 也不會讓它發生。程式設計師必須確保返回的值不是 NULL,或者忍受 Splint 的抱怨。有關確保 Splint 返回的指標永遠不會是 NULL 的不同方法,請檢視這裡。
前一段實際上是對我們在樣本 C 程式章節中提到的“使用前宣告”規則的重新解釋。編譯器對識別符號的宣告感到滿意,[在外部實體的情況下,當相應的定義在當前預處理檔案中找不到時] 將對相應定義是否存在進行控制推遲到連結器,連結器不會讓你使用未定義的識別符號。換句話說,編譯器-連結器組合負責確保未定義的識別符號不被使用。但是,由於堆區域由程式設計師管理——換句話說,物件是由程式設計師動態分配 [定義] 的——編譯器-連結器組合在執行時之前完成工作,因此本質上是靜態的,因此無法對在堆中分配的物件執行此規則。程式設計師必須多走一步!在 Java 等程式語言中,這轉化為防止相關異常,而在 C 中,這意味著檢查 NULL。
一個補充註釋是 /*@notnull@*/,它表示相關識別符號不能具有 NULL 作為其值。[8] 例如,銷燬只能在應用於現有物件時才能發生,這意味著傳遞給解構函式的引數必須是非 NULL 的。以這種方式作為保證,Splint 不會讓你傳遞可能具有 NULL 作為其值的某些變數。
至於 /*@reldef@*/ 註釋,它用於放鬆對相關宣告的定義檢查。用這種方式註釋的儲存被假定在使用時被定義;如果它在返回或作為引數傳遞之前沒有被定義,則不會報告錯誤。
定義:當為物件分配了所需的儲存空間時,該物件(即記憶體區域)被稱為定義的。[9] 當從物件可以到達的所有儲存空間都被定義時,該物件被稱為完全定義的。
typedef struct _STUDENT { char* name; int no; } *Student; ... Student std1 = (Student) malloc(sizeof(struct _STUDENT)); /* 此時,std1 被定義了。然而,它並沒有被完全定義。 */ std1->name = (char*) malloc(20); /* std1 現在被完全定義了。 */
在我們的例子中,一個從Employee派生的物件的構造——Engineer 或 Manager——是在兩個不同的建構函式中完成的。在Engineer_Create 或 Manager_Create 中之一分配完記憶體後,控制權將傳遞給 Employee_Create 來初始化某些欄位。初始化隨後在Employee_Create 的呼叫者中完成。這意味著在進入 Employee_Create 時,會存在未初始化的欄位,這進一步意味著可能存在未定義的欄位。[10]
extern /*@null@*/ Employee Employee_Create
(/*@reldef@*/ Employee, /*@notnull@*/ char*, Dept_Type, Employee_Functions);
extern void Employee_Destroy(/*@notnull@*/ /*@reldef@*/ Employee);
/*@in@*/ 註解用於表示一個與輸入引數相對應的引數,因此必須被完全定義。在我們的例子中,成功地構造一個從Employee 派生的物件,保證了其完全定義。
注意,除非另有說明,Splint 假設所有未加註解的引用——從全域性變數、引數和返回值可以訪問的儲存空間——都被完全定義了。可以透過關閉 impouts 標誌來放寬這一要求。
extern Dept_Type Employee_GetDepartment(/*@in@*/ const Employee);
extern /*@null@*/ char* Employee_GetName(/*@in@*/ const Employee);
#include “misc/inheritance/Employee_Private.h”
#endif
#include <assert.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include “misc/inheritance/Employee.h”
一個Employee 物件只能作為另一個物件的子物件存在。這個包含物件作為第一個引數傳遞給我們的建構函式式函式。如果這個引數值為 NULL,這意味著沒有包含物件,我們將在不建立 Employee 物件的情況下返回。畢竟,Employee 的概念是一個抽象的概念,無法具體化。
然而,Engineer 的概念是一個具體的概念,因此我們可以建立它的物件,而這個物件反過來包含一個 Employee 部分。以下建構函式式函式用於為這部分進行分配和初始化。
Employee Employee_Create(Employee this, char* name, Dept_Type dept, Employee_Functions funcs) {
if (!this) {
fprintf(stderr, “Cannot create Employee object...\n”);
return NULL;
} /* end of if(!this) */
this->name = (char *) malloc(strlen(name) + 1);
以下 if 語句是為了讓 Splint 相信我們意識到了 malloc 可能返回 NULL 的可能性。由於有了這種控制,我們不會受到 Splint 煩人警告的困擾。[11]
如果你確定結果永遠不會是 NULL——或者你根本不在乎——並且你不想寫這段程式碼片段,你可以透過在 this->name 的每個使用處新增 /*@-nullpass@*/ 註解來關閉這種控制。[12]
if(this->name == NULL) {
fprintf(stderr, “Out of memory...\n”);
return NULL;
} /* end of if (this->name == NULL) */
strcpy(this->name, name);
this->department = dept;
this->calc_salary = funcs.calc_salary;
this->calc_bonus = funcs.calc_bonus;
如果以下語句返回的值被分配給一個全域性變數,底層記憶體區域將在該[全域性]變數和實際引數之間共享。
... Employee global_emp; void func(...) { ... global_emp = Employee_Create(receiver_obj, ...); /* global_emp 和 receiver_obj 共享同一個底層物件。 */ ... } /* end of void func(...)
考慮到程式設計師很可能會忘記這一點,並繼續透過引數釋放它,Splint 會介入,給我們一個警告。不在乎這一點,我們透過 /*@-temptrans@*/ 標誌來移除檢查。
/*@-temptrans@*/ return this;
} /* end of Employee Employee_Create(Employee, char*, int, Employee_Functions) */
記住,被銷燬的物件可能是任何從 Employee 派生的物件。也就是說,如果需要,我們可以擴充套件 Employee 的定義,並提出一個新的型別,例如 Manager。然而,我們如何擴充套件基本型別是基本型別本身不知道的。出於這個原因,我們的解構函式式函式只釋放所有 Employee 共有的區域。[13]
void Employee_Destroy(Employee this) { free(this->name); }
返回一個 Engineer 所在的部門與返回一個 Manager 所在的部門沒有區別。同樣地,返回她的姓名也是一樣。無論我們處理的是哪種型別的 Employee 物件——無論是 Engineer、Manager,甚至是一個尚未定義的型別——答案的提供方式總是相同的。因此,與其在所有模組中重複這種不變的行為,不如將這種功能放在一箇中央儲存庫中,在我們的例子中,這個中央儲存庫恰好是基本型別 Employee。
由於這種恆定行為的特性——不像工資和獎金計算函式——這些函式不需要透過指向函式的指標來呼叫,這意味著在執行時之前就可以進行對其中任何一個函式的繫結的呼叫。
定義:在執行時之前繫結的函式呼叫被稱為靜態分派。對於這樣的函式,只需閱讀原始碼,就可以找出在進行函式呼叫時將執行哪個函式定義。
int Employee_GetDepartment(const Employee this) {
return(this->department);
} /* end of int Employee_GetDepartment(const Employee) */
char* Employee_GetName(const Employee this) {
char* ret_str = (char *) malloc(strlen(this->name) + 1);
if(!ret_str) {
fprintf(stderr, “Out of memory...\n”);
return NULL;
} /* end of if(!ret_str) */
strcpy(ret_str, this->name);
return(ret_str);
} /* end of char* Employee_GetName(const Employee) */
子類
[edit | edit source]#ifndef ENGINEER_PRIVATE_H
#define ENGINEER_PRIVATE_H
#include “misc/inheritance/Engineer.h”
#define ENGINEER_PART \
Discipline disc;
一個Engineer 物件由兩部分組成:它的 Employee 子物件和 Engineer 子物件。
struct _ENGINEER {
EMPLOYEE_PART
ENGINEER_PART
};
#endif
#ifndef ENGINEER_H
#define ENGINEER_H
#include “misc/inheritance/Employee.h”
struct _ENGINEER;
typedef struct _ENGINEER* Engineer;
typedef enum _DISCIPLINE { CSE, EE, IE, ME } Discipline;
extern /*@null@*/ Engineer Engineer_Create
(/*@null@*/ Engineer, char*, Dept_Type, Discipline, Employee_Functions);
extern void Engineer_Destroy(/*@reldef@*/ Engineer);
extern long Engineer_CalcSalary(const Employee);
extern long Engineer_CalcBonus(const Employee);
extern Discipline Engineer_GetDiscipline(const Engineer);
#include “misc/inheritance/Engineer_Private.h”
#endif
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include "misc/inheritance/Employee.h"
#include "misc/inheritance/Engineer.h"
Engineer Engineer_Create(Engineer this, char* name, Dept_Type dept, Discipline disc, Employee_Functions funcs) {
Engineer res_obj, temp;
if (!this) {
注意,以下分配保留了足夠大的記憶體來容納 Employee 和 Engineer 子物件。接下來,在記憶體分配之後,我們呼叫基本型別的建構函式式函式,即 Employee_Create。認識到我們是在初始化物件的 Engineer 特定部分之前執行此操作的。背後的邏輯是我們可能會利用 Employee 部分初始化中的某些資訊來完成 Engineer 部分的初始化。然而,它永遠不會反過來:你不會利用 Engineer 部分的資訊來完成 Employee 部分的初始化。
res_obj = (Engineer) malloc(sizeof(struct _ENGINEER));
if (!res_obj) {
fprintf(stderr, “ Cannot create Engineer object…\n ”);
return NULL;
} /* end of if(!res_obj) */
} else res_obj = this;
temp = res_obj;
res_obj = (Engineer) Employee_Create((Employee) res_obj, name, dept, funcs);
if (res_obj == NULL) {
free(/*@-temptrans@*/ temp);
return NULL;
} /* end of if (res_obj == NULL)*/
res_obj->disc = disc;
return res_obj;
} /* end of Engineer Engineer_Create(Engineer, char*, Dept_Type, Discipline, Employee_Functions) */
void Engineer_Destroy(Engineer this) {
Employee_Destroy((Employee) this);
free(this);
} /* end of void Engineer_Destroy(Engineer) */
現在 Engineer 是一個具體的型別,我們必須為工資和獎金計算提供實現。
請注意,函式頭部的唯一引數沒有在函式體中使用。這是一個相當奇怪的情況!不出所料,Splint 同意我們的看法,並報告了潛在的問題來源。我們毫不關心此時此刻的建議,透過解除特定的要求來擺脫煩人的警告。這是透過 /*@-paramuse@*/ 標誌完成的。
long Engineer_CalcSalary(/*@-paramuse@*/ const Employee this) {
return(1000);
} /* end of long Engineer_CalcSalary(const Employee) */
long Engineer_CalcBonus(/*@-paramuse@*/ const Employee this) {
return(300);
} /* end of long Engineer_CalcBonus(const Employee) */
Discipline Engineer_GetDiscipline(const Engineer this) {
return(this->disc);
} /* end of Discipline Engineer_GetDiscipline(const Engineer) */
#ifndef MANAGER_PRIVATE_H
#define MANAGER_PRIVATE_H
#include "misc/inheritance/Manager.h"
#define MANAGER_PART \
Bool bribes;
struct _MANAGER {
EMPLOYEE_PART
MANAGER_PART
};
#endif
#ifndef MANAGER_H
#define MANAGER_H
#include "misc/inheritance/Employee.h"
struct _MANAGER;
typedef struct _MANAGER* Manager;
下一個註釋將型別宣告標記為布林型別。定義為屬於此型別 (Bool) 的識別符號只能在布林上下文中使用——除非使用其他註釋另行指定。
/*@-likelybool@*/ typedef enum _BOOL { FALSE, TRUE } Bool;
extern /*@null@*/ Manager Manager_Create
(/*@null@*/ Manager, char*, Dept_Type, Bool, Employee_Functions);
extern void Manager_Destroy(/*@reldef@*/ Manager);
extern long Manager_CalcSalary(const Employee);
extern long Manager_CalcBonus(const Employee);
extern Bool Manager_GetBribing(const Manager);
extern void Manager_SetBribing(const Manager);
#include "misc/inheritance/Manager_Private.h"
#endif
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include "misc/inheritance/Employee.h"
#include "misc/inheritance/Manager.h"
Manager Manager_Create(Manager this, char* name, Dept_Type dept, Bool bribes, Employee_Functions funcs) {
Manager res_obj, temp;
if (!this) {
res_obj = (Manager) malloc(sizeof(struct _MANAGER));
if (!res_obj) {
fprintf(stderr, “ Cannot create Manager object…\n ”);
return NULL;
} /* end of if(!res_obj) */
} else res_obj = this;
temp = res_obj;
res_obj = (Manager) Employee_Create((Employee) res_obj, name, dept, funcs);
if (!res_obj) {
free(/*@-temptrans@*/ temp);
return NULL;
} /* end of if(!res_obj) */
res_obj->bribes = bribes;
return res_obj;
} /* Manager Manager_Create(Manager, char*, Dept_Type, Bool, Employee_Functions) */
void Manager_Destroy(Manager this) {
Employee_Destroy((Employee) this);
free(this);
} /* end of void Manager_Destroy(Manager) */
long Manager_CalcSalary(/*@-paramuse@*/ const Employee this) {
return(1500);
} /* end of long Manager_CalcSalary(const Employee) */
long Manager_CalcBonus(const Employee this) {
沉迷於一些真正的 C 程式設計,我們發現自己將布林值用作整數。這並不違反 C 的規則,但 Splint 將其視為潛在的錯誤併發出警告。我們應該採取一些糾正措施,如果有任何錯誤,或者放寬 Splint 的規則。我們選擇第二條路徑,並使用 /*@+boolint@*/ 註釋來宣告這一點。
long tot_bonus = 1000 * (2 * /*@+boolint@*/ ((Manager) this)->bribes + 1);
return(tot_bonus);
} /* end of long Manager_CalcBonus(const Employee) */
Bool Manager_GetBribing(const Manager this) { return(this->bribes); }
void Manager_SetBribing(const Manager this) { this->bribes = TRUE; }
測試程式
[edit | edit source]#include <assert.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include "misc/inheritance/Employee.h"
#include "misc/inheritance/Engineer.h"
#include "misc/inheritance/Manager.h"
以下注釋更改了用於標記格式化註釋的開始和結束的特殊字元。從這一點開始,註釋將以 /*! 開頭,並以 !*/ 結尾。
/*@-commentchar !@*/
print_tot_salary 計算支付給傳遞給它的陣列中列出的 Employee 的總報酬(即獎金加基本工資)。陣列的每個元件可以是 Engineer 或 Manager。我們不能確定哪個元件屬於哪個組。但有一點可以肯定:無論其真實型別如何,每個元件都可以被視為 Employee。但是,這也有自己的侷限性:一個 Employee 可以回答可以向所有派生型別物件提出的問題。[14] 事實上,這就是我們所做的:獲取 Employee 的姓名,計算她的工資和獎金。例如,我們不會詢問她的專業。我們也不會問她是否收受賄賂。因為,前者對 Engineer 來說是特殊的,而後者對 Manager 來說是特殊的。
void print_tot_salary(Employee payroll[], int no_of_emps) {
int i;
long tot_payment = 0, bonus, salary;
char* nm;
if (no_of_emps) printf("\nNAME\tSALARY\tBONUS\tTOTAL\n");
for(i = 0; i < no_of_emps; i++) {
下一個函式呼叫將在執行時之前解決——準確地說是在連結時——而接下來的兩個將在執行時解決。這是因為儲存在 calc_salary 和 calc_bonus 中的指標值是在物件建立時確定的,這發生在執行時。
定義:在執行時解析函式的地址稱為動態排程。相同呼叫解析為不同函式的能力稱為多型。
由於 Employee_GetName 返回可能為 NULL 的值,該值隨後傳遞給 printf,因此 Splint 提出異議:你怎麼能確定可以列印可能不存在的字元字串的內容?好吧,我不能,但在這種情況下,這似乎不太可能。因此,我放寬了空值檢查,讓它溜走。[15]
printf("%s\t", /*!-nullpass!*/ (nm = Employee_GetName(payroll[i])));
free(nm);
printf("%d\t", (salary = payroll[i]->calc_salary(payroll[i])));
printf("%d\t", (bonus = payroll[i]->calc_bonus(payroll[i])));
printf("%d\n", salary + bonus);
tot_payment += bonus + salary;
} /* end of for (i = 0; i < no_of_emps; i++)
printf("\nTotal payment: %d\n", tot_payment);
} /* end of void print_tot_salary(Employee**) */
int main(void) {
Employee_Functions eng_funcs = { &Engineer_CalcSalary, &Engineer_CalcBonus };
Employee_Functions mng_funcs = { &Manager_CalcSalary, &Manager_CalcBonus };
Employee emps[4];
請注意,我們將每個 Engineer/Manager 轉換為 Employee。這將允許我們以相同的方式對待它們。好吧,幾乎!使用這些物件呼叫的函式列表將限制為可以使用 Employee 呼叫的函式。但是,呼叫的函式將取決於底層物件的型別。[16]
這裡的 Employee 型別(即指標變數的型別)稱為靜態型別——因為它可以透過讀取原始碼來確定——而底層物件的型別(即指標指向的區域的型別)稱為動態型別。[17]
下一行的 C++ 等效項如下所示,它反映了編譯器的幕後努力。由於 Employee 和 Engineer 之間的繼承關係,該關係透過 ':' 運算子傳遞給編譯器,因此強制轉換現在是隱式的。類似地,NULL 引數表示一個 Engineer 物件——而不是從 Engineer 類繼承的類的物件——現在不再需要。最後,編譯器在看到函式簽名之前的 virtual 關鍵字時,將收集動態排程過程中所需的指標值——並將其插入到稱為vtable 的結構中,該結構又由建立物件的隱式欄位指向,這是執行編譯器合成的程式碼片段的結果。
emps[0] = new Engineer("Eng 1", RESEARCH, EE);
emps[0] = (Employee) Engineer_Create(NULL, "Eng 1", RESEARCH , EE, eng_funcs);
請注意說服 Splint 我們瞭解從建構函式返回的可能為 NULL 的值的各種方法。所有這些都保證 NULL 指標永遠不會被解除引用。在第一種技術中,我們透過斷言返回的指標不為空來實現這一點,這意味著程式在從 Engineer_Create 接收 NULL 時將終止。第二和第三種技術基本上歸結為同一件事:透過 if 語句,透過將返回值顯式地與 NULL 進行比較來實現目標。
assert(emps[0] != NULL);
emps[1] = (Employee) Manager_Create(NULL, "Mng 1", MANAGEMENT, TRUE, mng_funcs);
if (emps[1] == NULL) {
fprintf(stderr, "Cannot create emps[1]...\n");
exit(EXIT_FAILURE);
} /* end of if(emps[1] == NULL) */
emps[2] = (Employee) Manager_Create(NULL, "Mng 2", MANAGEMENT, FALSE, mng_funcs);
if (!emps[2]) {
fprintf(stderr, "Cannot create emps[2]...\n");
exit(EXIT_FAILURE);
} /* end of if(!emps[2]) */
emps[3] = (Employee) Engineer_Create(NULL, "Eng 2", RESEARCH, CSE, eng_funcs);
assert(emps[3] != NULL);
print_tot_salary(emps, 4);
exit(0);
} /* end of int main(void) */
執行測試程式
[edit | edit source]- splint -ID:/include Employee.c↵ # 在 Cygwin 中
- Splint 3.1.1 --- 2003 年 5 月 2 日
- 完成檢查 --- 無警告
- splint -ID:/include Engineer.c↵
- Splint 3.1.1 --- 2003 年 5 月 2 日
- 完成檢查 --- 無警告
- splint -ID:/include Manager.c↵
- Splint 3.1.1 --- 2003 年 5 月 2 日
- 完成檢查 --- 無警告
- splint -ID:/include Inheritance_Test.c↵
- Splint 3.1.1 --- 2003 年 5 月 2 日
- 完成檢查 --- 無警告
- gcc -ID:/include –o InheritanceTest.exe Inheritance_Test.c Employee.c Engineer.c Manager.c↵
- Inheritance_Test↵
- 姓名 工資 獎金 總計
- Eng 1 1000 300 1300
- Mng 1 1500 3000 4500
- Mng 2 1500 1000 2500
- Eng 2 1000 300 1300
總計支付金額:9600
註釋
[edit | edit source]- ↑ 為了複用而複用並不總是能節省寶貴的資源。為了實現這一點,它必須成為組織政策的一部分。
- ↑ 類似的論點可以用來證明覆用分析和設計文件的合理性。
- ↑ 在本章中,當我們說繼承時,我們實際上指的是“繼承加上多型性”,這是我們從對該概念的自然應用中所理解的。但是,正如我們在繼承章節中將要看到的,C++ 允許在沒有多型性的情況下使用繼承。
- ↑ 在多重繼承的情況下,派生可以來自多個類。
- ↑ 當我們討論 C++ 中的虛擬繼承時,我們將不得不重新表述這句話。
- ↑ ref!!!
- ↑ 您可以選擇另一個字元來扮演“@”的角色。例如,檢視測試程式。
- ↑ 除非另有說明,Splint 假設所有指標變數都用
/*@notnull@*/註解。 - ↑ 具有
NULL值的指標是完全定義的。 - ↑ 事實上,
name欄位將是未定義的,因此未初始化的。 - ↑ 這並不是說服 Splint 的唯一方法。有關替代方案,請檢視 Inheritance_Test.c。
- ↑ 支援使用標誌來修改 Splint 的行為。在標誌前面加上
+表示它處於開啟狀態;在標誌前面加上-表示它處於關閉狀態。 - ↑ 它怎麼可能釋放它不知道的區域呢?畢竟,所有
Manager也是Employee,但並非所有Employee都是Manager。 - ↑ 這與說傳送到物件的 messages 會透過其 handle 過濾,意思相同。
- ↑ 在一個更大的程式中,這樣做並非最佳選擇。對 nm 的值進行斷言或將其與
NULL進行比較會更加明智。 - ↑ 記住“傳送到物件的 messages 受到...的限制”。
- ↑ 將“指標變數”讀作“handle”,“指標指向的區域”讀作“object”,它將開始變得更加面向物件。