使用 C 和 C++/面向对象编程的编程语言概念
语言,本质上是与他人分享无数色调图像的活动,只有当参与方想出相似,如果不是完全相同的想法、经验或设计的描述时才能取得成功。当满足以下标准时,成功才有可能
- 参与方共享一种共同媒介,
- 这种共同媒介支持相关的概念。
缺乏这些标准,将使交流变成一场噩梦般的哑剧表演。想象两个人,他们之间没有共同媒介,试图互相交流。太多模糊的空间,不是吗?事实上,即使双方说同一种语言,他们的人生观,读作“范式”,也可能使交流成为一项难以忍受的练习。
一个概念,无论是抽象的还是具体的,如果没有在说话者的心目中占有一席之地,它就不会在语言中有任何相应的表示。例如,阿拉伯语使用者用同一个词来指代冰和雪,而爱斯基摩人有几十个关于雪的词。然而,这不能被用作智力不足的证明:在描述骆驼的品质时,角色会反转。
最后一个缺陷用两种方法来处理:引入一个新词,可能与已有的词有关;或者发明一个习语来表达无法表达的东西。前者似乎是更好的选择,因为后者容易被误解,因此会导致歧义,这使我们回到原点。它还会模糊它的 - 也就是说,新概念 - 与其他概念之间的关系,并将语言的词汇变成一片无枝树的森林。
那么,编程语言呢?编程语言,像自然语言一样,是为了与他人交流而设计的:机器语言用于与特定的处理器交流;高级语言用于向程序员同事表达我们的解决方案;规范语言将分析/设计决策传达给其他分析师/设计人员。
如果我们已经有了它 - 也就是说,将我们的想法传达给她的/他的陛下,计算机 - 作为我们唯一目标,提供一个解决问题的方案,将不会是一项如此困难的任务。但是,我们还有另一个目标,这更值得我们付出智力努力:向其他人解释我们的解决方案。实现这个更难以捉摸的目标需要采用纪律严明的做法和一系列高级概念。前者有助于分析手头的問題,并为其解决方案提供设计。它使我们更容易发现重复出现的模式,并将经过验证的解决方案应用于子问题。后者是用来表达自己的词汇。在这种情况下,使用习语而不是语言结构可能会导致误解,并成为新人和少数幸运者之间的一道障碍。
另一方面,同化习语不仅会让你说这种语言,还会让你成为母语使用者之一。现在,说外语不再是一项枯燥的运用语法规则的练习,而是在他人的思想领域中进行的智力旅程。如果这种旅程没有被缩短,通常会揭示习语所替代的更多关于概念的信息;它帮助你[自下而上]建立一个相互关联的概念网络。下次你踏上旅程时,你之前竖立的路标将帮助你更容易找到自己的路。
那么,我们应该学习哪些编程语言呢?如果问这个问题的是你 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
的宏定义。
#ifndef COMPLEX_H
#define COMPLEX_H
#include "General.h"
以下是一个所谓的前向声明。我们声明了使用名为 struct _COMPLEX
的数据类型的意图,但没有透露其结构的细节。我们通过在实现文件 Complex.c 中提供定义来填写细节。Complex
数据类型的用户不需要了解实现的细节。这种方法使实现者能够灵活地更改底层数据结构,而不会破坏现有的客户端程序。
我们将定义推迟到实现文件的原因是,C 中没有访问说明符(例如,public
、protected
、public
),就像我们在面向对象的编程语言中一样。这迫使我们对用户不应该访问的一切都保密。
struct _COMPLEX;
注意,Complex
被定义为一个指针。这符合界面应该包含不会改变的內容的规则。无论复数的表示如何,都可以根据实现者的意愿进行更改,指向这种表示的指针的内存布局永远不会改变。因此,我们在函数原型中使用 Complex
而不是 struct _COMPLEX
。
请注意,界面和实现之间的区别是通过坚持惯例来加强的,而不是由 C 编译器检查的某些语言规则来加强的。我们可以将界面和实现合并到一个文件中,编译器不会有任何抱怨。
typedef struct _COMPLEX* Complex;
所有以下原型(函数声明)都用 extern
关键字限定。这意味着它们的实现(函数定义)可能出现在另一个文件中,在本例中是相应的实现文件。这使得导出函数成为可能:所有包含当前头文件的文件都能够使用这些函数,而无需为它们提供任何实现。从这个角度看,以下原型列表可以被视为一个底层对象的界面,它声称提供了一个实现。
定义:界面是一种与对象通信的抽象协议。
所有 extern
函数都从实现文件导出(也就是说,它们对用于构建可执行文件的其他文件可见),并由它们的客户端链接——读作“导入”。这样的导入函数被称为具有 *外部链接*。如果它们在另一个文件中实现,这些函数的地址对编译器来说是未知的,并在编译器生成的目标文件中被标记为如此。链接器在构建可执行文件的过程中会稍后填充这些值。
extern Complex Complex_Create(double, double);
extern void Complex_Destroy(Complex);
请记住,Complex
是 typedef
对指向 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_Create
和 Complex_Destroy
的签名构成上述模式的例外。类似构造函数的函数 Complex_Create
为尚未创建的对象分配堆内存并对其进行初始化,而类似析构函数的函数 Complex_Destroy
释放对象使用的堆内存,并通过将 NULL
分配给它来使对象指针不可用。
实现
[edit | edit source]#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);
假设广泛使用的约定是将返回值存储在寄存器中,在构造函数完成执行后,我们在下一页提供了部分内存映像。
观察在堆上分配的内存区域的生命周期不受限于局部指针 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) */
以下函数检查两个复数的相等性。请注意,相等性检查和同一性检查是两件不同的事情。这就是我们不使用指针语义进行比较的原因。相反,我们检查两个数字的对应字段是否相等。
示例:同一性检查和相等性检查是不同的。 |
---|
鉴于上述定义,所有三个对象都相等,而只有 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]#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");
一旦完成下一个赋值命令,我们将得到如下所示的局部内存图像
注意堆分配的非连续性。尽管对于此大小的程序,分配的内存可能很可能是连续的,但随着程序变大,这将变得不可能。内存分配器的唯一工作是满足分配需求;分配内存的地址无关紧要。为了做到这一点,它可以使用不同的算法,例如首次适应、最差适应、最佳适应等。
num3 = Complex_Add(num1, num2);
printf("#1 + #2: "); Complex_Print(num3); printf("\n");
在Complex_Add
中,我们在堆上创建了一个复数,并返回指向它的指针作为结果。下次我们使用相同的Complex
变量来保存另一个操作的结果时,保存上一次操作结果的旧位置将无法访问。这种无法访问,因此无法使用的内存位置被称为垃圾。在具有自动垃圾回收的编程语言中,这种未使用的堆内存由语言的运行时系统回收。在没有自动垃圾回收的面向对象编程语言中,这必须由程序员通过调用delete
之类的函数来处理,该函数反过来调用一个名为析构函数的特殊函数。在非面向对象编程语言中,必须模拟析构函数,并且程序员必须显式地将这些内存区域返回给系统以供重用。在我们的例子中,模拟析构函数的函数名为Complex_Destroy
。
完成下一行后,我们将得到以下局部内存图像
观察到 num3
仍然指向同一个位置。也就是说,我们仍然可以使用 num3
来操作内存的同一区域。但是,没有保证关于内容。所以,为了让用户远离使用此值的诱惑,最好将 num3
的值更改为不能用来引用对象的任何东西。这个值是 NULL
。每次释放内存区域时,指向它的指针应该要么指向另一个区域,就像在这个测试程序中一样,要么用户应该将 NULL
分配给指针变量。第二种更安全的方法将分配 NULL
的责任交给实现者。问题是我们需要修改指针本身,而不是它指向的区域。可以通过进行以下更改来消除此缺陷
... void Complex_Destroy(Complex* this) { free(*this); *this = NULL; } /* end of void Complex_Destroy(Complex* ) ...
... 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 代码包含在可执行文件中。
整个过程可以用下图来表示。
图中黑色区域代表过程的实现者一方。这个盒子里面发生了什么与用户无关;涉及的子过程数量、产生的中间输出对他们来说无关紧要。事实上,该模块可以用除 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 模块中提供的功能 |
- ↑ 这些由人工执行的活动中,除一项外,都可以由自动机完成。然而,设计问题的初始模型的行为似乎还将与我们共存一段时间。
- ↑ 函数在当前文件的相对地址是在编译时确定的。换句话说,此类函数的地址是静态确定的,因此是
static
关键字。 - ↑ 它不是在控制级别结构章节中定义的意义上的隐式调用。虽然不是程序员进行调用,但构造函数调用并不在程序员的控制范围之外。程序员知道何时以及哪个构造函数将被调用。
- ↑ 在 C++ 中,除了在堆中创建它并通过指针使用之外,还可以将对象嵌入静态数据区域或运行时堆栈中。也就是说,它们可以在没有指针的情况下访问。此类对象遵守 C++ 范围规则,就像其他变量一样:它们在相关范围被进入和退出时自动创建和销毁。因此,它们不需要调用
new
运算符。在 C# 中使用struct
(值类型)中可以看到相同的行为。 - ↑ 一个 [静态] 库基本上是一组 .o(在 MS Windows 中为 .obj)文件,通过编译相应的一组 .c 文件获得,加上一些元数据。此元数据用于加速 .o 文件的提取并回答有关库内容的查询。通常有一个或多个 .h 文件,其中包含使用这些 .o 文件所需的声明。