使用 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”,它将开始变得更加面向对象。