跳转到内容

使用 C 和 C++ 的编程语言概念/C 编程入门

来自维基教科书,开放世界中的开放书籍

以下是提供对语言基础知识的介绍的简单 C 程序的精选。为了更好地实现这个目标,示例保持简单和简短。不幸的是,对于如此规模的程序,某些标准变得不那么重要,因为它们通常应该那样。其中一个标准是源代码可移植性,可以通过检查程序的标准合规性来确保。如果从一开始没有认真追求这个标准,它就很难实现。

为了更容易解决这个问题,许多编译器提供了一些帮助。虽然支持的程度及其形式可能有所不同,但值得看一下编译器开关。对于我们将在本课程中使用的编译器,下面列出了部分适用的选项。

选项* 含义 在其中有效
-Wall 对可疑的编程实践提供警告,例如缺少函数返回类型、未使用的标识符、没有默认值的 switch 语句等等。 两者
-std 检查是否符合特定标准。 gcc
-pedantic 检查程序是否严格符合标准,并拒绝使用非标准扩展的所有程序。 gcc
-Tc 类似于 -pedantic MS Visual C/C++
*:MS Visual C/C++ 支持的选项也可以用 '/' 作为前缀。
示例:编译器选项的使用。

使用 gcc,提供确保任何草率编程都不会被忽视所需的编译命令。还要确保您的源代码可以轻松移植到其他开发环境。

第一个要求是通过传递 -Wall 来满足的,而第二个要求是通过使用 -pedantic 选项来满足的。因此,所需的编译命令如下所示。

gcc -Wall -pedantic [其他选项] 文件名

在 MS Visual C/ C++ 中,可以通过以下命令实现相同的目的

cl [其他选项] /Wall /Tc 文件名

C 预处理器

[编辑 | 编辑源代码]

C 预处理器是一个简单的宏处理器——一个 C 到 C 的翻译器——它在 C 编译器读取源程序之前,在概念上处理 C 程序的源文本。通常,预处理器实际上是一个单独的程序,它读取原始源文件并写出一个新的“预处理”源文件,然后可以将其用作 C 编译器的输入。在其他实现中,单个程序在对源文件进行单次扫描时执行预处理和编译。前一种方案的优点是,除了其更模块化的结构之外,还可以使用预处理器为其他编程语言创建翻译器。

预处理器由特殊的预处理器命令行控制,这些命令行是源文件以字符 # 开头的行。

预处理器从源文件中删除所有预处理器命令行,并根据命令对源文件进行额外的转换。

命令的名称必须紧跟在 # 字符之后。[1] 唯一非空格字符是 # 的行在 ISO C 中被称为 *空指令*,其处理方式与空行相同。

定义宏

[编辑 | 编辑源代码]
#define
#define 预处理器命令使一个名称成为预处理器定义的宏。一系列标记,称为宏的 *主体*,与该名称相关联。当在程序源文本或某些其他预处理器命令的参数中识别到宏的名称时,它将被视为对该宏的调用;该名称实际上被一个主体副本替换。如果宏被定义为接受参数,那么跟随宏名称的实际参数将被替换为宏主体中的形式参数。使用的参数传递机制类似于按名称调用。但是,不要忘记,文本替换是由预处理器执行的,而不是由编译器执行的。

此宏指令可以以两种不同的方式使用。

  • 类似对象的宏定义:#define name sequence-of-tokens?,其中 ? 代表前面实体的出现次数为零或一次。换句话说,宏的主体可以为空。
示例:类似对象的宏定义

#define BOOL char /* 不是 #define BOOL=char */ #define FALSE 0 #define TRUE 1 #define CRAY

请注意,没有等号。也不需要分号来终止行。标记序列周围的前导和尾随空格将被丢弃。

在 ISO C 中,允许重新定义宏,前提是新定义与现有定义在标记上完全相同。只有在发出 undef 指令后,才能使用不同的定义重新定义宏。

  • 定义带参数的宏:#define name(name1, name2, ..., namen) sequence-of-tokens?。左括号必须紧跟在宏名称之后,中间没有空格。这种宏定义将被解释为一个类似对象的宏,它以左括号开头。形式参数的名称必须是标识符,不能有两个相同。类似函数的宏可以有空的形式参数列表。这用于模拟没有参数的函数。
示例:带参数的宏
#define increment(n) (n + 1)

当遇到类似函数的宏调用时,整个宏调用将被替换,在参数处理之后,被主体处理后的副本替换。将宏调用替换为其主体处理后的副本的整个过程称为 *宏展开*;主体处理后的副本称为宏调用的展开。因此,对于上述定义,a = 3 * increment(a); 将扩展为 a = 3 * (a + 1);。现在,预处理在编译之前进行,真正的编译器甚至看不到源代码中的标识符 increment。它看到的是宏的展开形式。

示例
#define product(x, y) (x * y)
不幸的是,上面这段看似无害的代码是错误的。它不会为 result = product(a + 3, b); 生成正确的结果。这行代码在预处理后将被转换为 result = (a + 3 * b);。这并不是我们想要的!
相反,我们必须写
#define product(x, y) ((x) * (y))
有了这个定义,之前的示例将被扩展为 result = ((a + 3) * (b));。请注意,程序员看不到多余的括号。她看到的是程序文本的未预处理形式。
示例:错误的宏定义
#define SQUARE(x) x * x
这个定义无法为 a + 3 生成正确的结果。它将生成 a + 3 * a + 3 而不是 (a + 3) * (a + 3)。这可以通过在 x 周围添加括号来解决。
#define SQUARE(x) (x) * (x)
但是,这个第二个版本无法为 result = (long) SQUARE(a + 3); 生成正确的结果,它被扩展为 result = (long) (a + 3) * (a + 3);
在上面的示例中,只有第一个子表达式会被转换为 long。为了将强制转换运算符应用于整个表达式,我们应该在宏的整个主体周围添加括号。因此,宏的正确版本是
#define SQUARE(x) ((x) * (x))
问题到此结束了吗?不幸的是,没有!如果我们以以下方式使用宏会怎么样?这将被扩展为 result = ((x++) * (x++));
result = SQUARE(x++);
此表达式的问题是,由于自增运算符引起的副作用发生了两次,并且 x 被乘以 x + 1,而不是自身。这 [再次] 表明人们必须避免编写产生副作用的表达式。

我们在上述示例中遇到的问题可能是由于所用参数传递机制的文本性质造成的:按名称调用:每次参数名出现在文本中时,它都会被替换为参数的精确文本;每次替换时,它都会被再次求值。

示例

#define swap(x, y) \ { unsigned long temp = x; x = y; y = temp; }

\ 在行尾用于行延续。对于

if (x < y) swap(x, y); else x = y;

我们得到

if (x < y) { unsigned long temp = x; x = y; y = temp; }; else x = y;

右大括号后的分号是多余的,会导致编译时错误。解决方法如下所示。

#define FALSE 0 #define swap(x, y) \ do { unsigned long temp = x; x = y; y = temp; } while(FALSE)

扩展后,我们将有

if (x < y) do { unsigned long temp = x; x = y; y = temp; } while(FALSE); else x = y;

预定义宏

[edit | edit source]
__LINE__ 当前源程序行的行号,用十进制整数常量表示
__FILE__ 当前源文件的名称,用字符串常量表示
__DATE__ 翻译的日历日期,用以下形式的字符串常量表示:"Mmm dd yyyy"
__TIME__ 翻译的时间,用以下形式的字符串常量表示:"hh:mm:ss"
__STDC__ 十进制常量,当且仅当编译器是符合 ISO 标准的实现时
__STDC_VERSION__ 如果实现符合 ISO C 的修正案 1,则此宏被定义,并且值为 199409L

取消定义宏

[edit | edit source]
#undef name
#undef 使预处理器忘记 name 的任何宏定义。取消定义当前未定义的名称不会导致错误。一旦名称被取消定义,就可以在不出现错误的情况下为它赋予一个全新的定义。
示例:取消定义宏。

#define square(x) ((x) * (x)) ... a = square(b); ... ... #undef square ... ... c = square(d++); ...

第一次应用 square 将使用宏定义,而第二次将调用函数。

宏与函数

[edit | edit source]

在以下情况下,选择宏定义而不是函数是有意义的:

  • 效率是首要考虑因素,
  • 宏定义简单且短小,或者在源代码中很少使用,以及
  • 参数只计算一次。

宏定义之所以高效,是因为预处理发生在运行时之前。实际上,它甚至在真正的编译器开始之前就完成了。另一方面,函数调用是一个相当昂贵的指令,它在运行时执行。从这个意义上说,宏可以用来模拟函数的内联。

宏定义应该简单且短小,因为在源代码中替换大量文本会导致代码膨胀,尤其是在它被频繁使用时。

第三个要求的理由在前面已经给出。

另一方面,应该记住,预处理器不会对宏参数进行任何类型检查。

代码包含

[edit | edit source]
#include <文件名>
#include "文件名"
#include 预处理器标记
预处理器命令 #include 会导致指定源文件的所有内容都被处理,就好像这些内容出现在 #include 命令的位置一样。

通常,"文件名" 形式用于引用程序员编写的头文件,而 <文件名> 形式用于引用标准实现文件。第三种形式会进行正常的宏扩展,扩展的结果必须与前两种形式之一匹配。

条件预处理

[edit | edit source]
#if 常量表达式
代码块1
#elif
代码块2
...
#elif
代码块n
#endif
这些命令一起使用,允许有条件地包含或排除源代码行进行编译。当我们需要为不同的架构(例如,Vax 和 Intel)或在不同的模式(例如,调试模式和生产模式)下生成代码时,这很方便。
#ifdef 名称
#ifndef 名称
这两个命令用于测试一个名称是否被定义为预处理器宏。它们等效于 #if defined(名称)#if !defined(名称),分别。

请注意,#if 名称#ifdef 名称 不等效。虽然当 名称 未定义时,它们的工作方式完全相同,但当 名称 被定义为 0 时,这种对等性就会被打破。在这种情况下,前者将为假,而后者将为真。

defined 名称
defined(名称)
运算符 defined 只能在 #if#elif 表达式中使用;它不是预处理器命令,而是预处理器运算符。

发出错误消息

[edit | edit source]
#error 预处理器标记
指令 #error 会生成一个编译时错误消息,其中包含参数标记,这些标记会进行宏扩展。
示例:#error

#if (TABLESIZE > 1000) #error "Table size cannot exceed 1000" #endif

编译时开关

[edit | edit source]

除了使用 #define 命令之外;我们可以使用编译时开关来定义宏。

示例:在命令行中定义宏。
gcc –DTABLESIZE=500 prog_name↵
将把程序视为 #define TABLESIZE 500 是源代码的第一行。
示例:通过编译器开关进行条件预处理。
gcc –DVAX prog_name↵
上面的命令将定义一个名为 VAX 的宏,并(预处理和)编译名为 prog_name 的程序。

#ifdef VAX
VAX 相关的代码
#endif #ifdef PDP11
PDP11 相关的代码
#endif #ifdef CRAY2
CRAY2 相关的代码
#endif

通过编译时开关定义了 VAX,并且源代码中包含了这些定义,只有 VAX 相关的代码会被处理。与其他架构相关的代码将被忽略。当我们想要为不同的架构编译程序时,我们所要做的就是将编译时开关更改为相关的架构。
新手(或恶意)程序员可能会在命令行中定义多个架构宏。
gcc –DVAX –DCRAY2 prog_name↵
在这种情况下,编译后的程序将包含针对两个不同架构的代码。可以使用以下预处理器命令避免这种情况。

#if defined(VAX)
VAX 相关的代码
#elif defined(PDP-11)
PDP11 相关的代码
#elif defined(CRAY2)
CRAY2 相关的代码
#else #error "目标架构未指定/未知!中止编译..." #endif

示例:调试模式编译
prog_name 中,

... #if defined(DEBUG)
调试模式代码
#endif ...

命令 gcc –DDEBUG prog_name↵ 将生成(对象)代码,其中包含一些用于调试的额外语句。一旦调试阶段结束,就可以使用 gcc prog_name↵ 重新编译程序,调试模式代码将不会包含在对象代码中。这样做的优点是,您无需从源代码中删除调试模式代码!

char 数据类型

[edit | edit source]

问题:编写一个程序,打印出字符 'a'..'z'、'A'..'Z' 和 '0'..'9' 以及它们的编码。


Encoding.c

以下预处理器指令将名为 stdio.h 的文件的内容拉入当前文件。包含该文件后,预处理器会解释其内容。

对于我们使用的每个函数,都需要引入其原型,其位置可以通过在基于 UNIX 的环境中找到的 man 命令来确定:man 函数名 将向您提供有关 函数名 的信息,包括其声明所在的头文件。

包含文件通常包含在不同应用程序之间共享的声明。常量声明和函数原型就是例子。在本例中,我们必须包含 stdio.h,以便引入 printf 函数的原型。

一个函数原型包含函数的返回类型、函数名和参数列表。它描述了函数的接口:它详细说明了调用函数时必须提供的参数的数量、顺序和类型,以及函数返回的值的类型。

函数原型帮助编译器确保正确使用特定函数。例如,在 stdio.h 中的声明中,不能将 int 作为第一个参数传递给 printf 函数。

你会经常看到原型和签名这两个词被互换使用。然而,这是不正确的。函数的签名是其参数类型的列表;它不包括函数的返回类型。

#include <stdio.h>

所有可运行的 C 程序都必须包含一个名为 main 的函数。此函数作为程序的入口点,在加载可执行文件后,作为初始化代码的一部分,从 C 运行时内部调用。

将程序从辅助存储复制到主内存以便运行所需的系统软件称为加载器。除了将程序加载到内存之外,它还可以设置保护位、安排虚拟内存将虚拟地址映射到磁盘页面,等等。

以下 main 函数的形式参数列表由单个关键字 void 组成,这意味着它根本不接受任何参数。有时你会看到 C 代码中写着 int main()main(),而不是 int main(void)。在这种情况下,所有这三种方式都具有相同的用途,尽管前两种应该避免使用。在声明/定义中没有返回类型的函数被假定为返回类型为 int 的值。具有空形式参数列表的函数原型是一种旧式声明,它告诉编译器函数的存在,该函数可以接受任意数量的任意类型的参数。它还有未经请求的副作用,即从该点开始关闭原型检查。因此,应该避免此类用法。

int main(void) {

虽然我们将操作字符,但用于保存字符的变量 ch 被声明为 int。这样做是以下原因:在原始语言设计中,没有符号限定符 (signedunsigned),这给了编译器实现者自由地将 char 解释为包含 [0..255] 范围内值的类型(因为字符只能由非负值索引)或包含 [–128..127] 范围内值的类型(因为它是在单个字节中表示的整数类型)。这两种观点基本上将 char 的范围限制为 [0..127]。

一些关于 ASCII 的信息

由于 C 中没有异常,大多数处理字符和字符串的函数需要将异常情况(例如文件结束)表示为不太可能的返回值。这意味着,除了合法的字符之外,我们还应该能够将异常情况编码为不同的值。ASCII,这意味着一个可以容纳 128 + n 个带符号值的表示,其中 n 是要处理的异常情况的数量。结合上一段的结论,可以看出我们需要一个比类型 char 更大的表示。因此,你经常会看到一个 int 变量用于保存类型为 char 的值。

遇到一个字符常量时,C 编译器会用一个整数常量替换它,该常量对应于字符在编码表中的顺序。例如,ASCII 中的 'a' 被替换为 97,它是一个类型为 int 的整数常量。

如果我们使用 char 而不是 int,我们的程序仍然可以正常工作。这是因为我们不必在程序中处理任何异常情况,并且使用的所有 int 常量都在 char 表示的限制范围内。也就是说,所有缩窄的隐式转换(就像在 for 循环的初始化语句中发生的)都是值保持的。

  int ch;
  for(ch = 'a'; ch <= 'z'; ch++)

printf 函数用于格式化并在标准输出文件 stdout 上发送格式化的输出,默认情况下是屏幕。它是一个可变参数函数:也就是说,它接受一个可变长度的参数列表。第一个参数被认为是格式控制字符串,用于确定参数的类型和数量。这是通过使用以 % 开头的特殊字符序列来完成的。遇到此类序列时,如果实际参数可以在字符序列暗示的上下文中使用,则会用相应的实际参数替换它。例如,以下行中的 '%c' 意味着相应的参数必须可解释为一个字符。同样,'%d' 是十进制数的占位符。

请注意,printf 实际上返回一个 int。不将此返回值分配给某个变量意味着它被忽略了。

    printf("Encoding of '%c' is %d\n", ch, ch);
  printf("Press any key to continue\n"); getchar();

  for(ch = 'A'; ch <= 'Z'; ch++)
    printf("Encoding of '%c' is %d\n", ch, ch);
  printf("Press any key to continue\n"); getchar();
  for(ch = '0'; ch <= '9'; ch++)
    printf("Encoding of '%c' is %d\n", ch, ch);

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

在 Linux 命令行中编译和运行程序

[编辑 | 编辑源代码]

假设此程序保存为 Encoding.c,可以使用以下命令编译(和链接)它

gcc –o AlphaNumerics Encoding.c↵
GCC(GNU 编译器集合)顾名思义,是一个针对多种编程语言的编译器集合。 gcc 是 GCC 的 C/C++ 前端,它执行从 C 源文件创建可执行文件所需的预处理、编译、汇编和链接阶段。

gcc 调用 GNU C/C++ 编译器驱动程序,该驱动程序首先获取 C 预处理器处理 Encoding.c,并将转换后的源文件传递给编译器本身。编译器本身的输出(一个汇编程序)随后被传递给汇编器。由汇编器汇编的目标代码文件最终被传递给链接器,链接器将它与标准 C 库链接起来,并将可执行文件存储在磁盘文件中,该文件的文件名通过 -o 选项提供。请注意,你无需告诉驱动程序链接到标准 C 库。这是一个特例。对于其他库,你必须告诉编译器驱动程序要链接的库和目标文件。整个方案基本上创建了一个名为 AlphaNumerics 的新外部命令。在命令行中发出命令

./AlphaNumerics

最终会导致加载器从辅助存储将程序加载到内存并运行它。

使用 Emacs 编译和运行程序

[编辑 | 编辑源代码]
emacs &

此命令会将你带到 Emacs 开发环境中。单击 文件→打开... 并从文件列表中选择 Encoding.c。这将在当前窗口中打开一个新的 C 模式缓冲区。请注意,最底部的第二行显示 (C Abbrev),这意味着 Emacs 已将你的源代码识别为 C 程序。接下来,单击 工具→编译►→编译.... 这将提示你输入编译(和链接)程序所需的命令。此提示将打印在一个名为迷你缓冲区的区域中,该区域通常是框架的最后一行。擦除默认选择并写

gcc –o AlphaNumerics.exe Encoding.c↵

当你按下回车键时,你会看到一个 *编译* 缓冲区弹出,让你知道编译过程的进行情况。希望你没有打错字,并且一切顺利,接下来我们将要运行可执行文件。为此,单击 工具→Shell►→Shell。这将在 Shell 模式缓冲区内打开一个受限制的 shell,从那里可以运行你的可执行文件。在该缓冲区中输入

./AlphaNumerics↵

你会看到与上一节中相同的输出。

如果你遇到编译错误,单击 *编译* 缓冲区中的错误行会将你带到相关的源代码行。

如果您想返回源代码并进行一些更改,请单击 Buffers→Encoding.c。完成更改后,您可以通过单击 Tools→Compile►→Repeat Compilation 再次编译源代码。这将使用上面输入的命令重新编译 Encoding.c。但是,如果您可能想要修改命令,请单击 Tools→Compile►→Compile... 并按照之前的操作进行。

不可移植版本

[编辑 | 编辑源代码]

假设使用 ASCII,您可能会倾向于用相应的整数值替换程序中的所有字符常量。强烈建议不要这样做,因为它会使程序不可移植。


ASCII_Encoding.c
#include <stdio.h>
#include <stdlib.h>

int main(void) {
  int ch;

以下行包含嵌入的常量,这些常量会导致代码不可移植。如果我们想将代码移动到一些使用 EBCDIC 编码字符的环境中会怎么样?因此,应该避免将这种依赖于实现的功能嵌入到程序中,而让编译器完成繁重的工作。

另外,由于编译器会执行相同的操作,因此用整数常量替换字符字面量以加快程序速度的可能动机也毫无根据。

  for(ch = 97; ch <= 122; ch++)
    printf(Encoding of %c is %d\n, ch, ch);
  printf(Press any key to continue\n);
  getchar();

  for(ch = 65; ch <= 90; ch++)
    printf(Encoding of %c is %d\n, ch, ch);
  printf(Press any key to continue\n);
  getchar();

  for(ch = 48; ch <= 57; ch++)
    printf(Encoding of %c is %d\n, ch, ch);

The exit 函数使程序终止,并将其传递的值作为执行程序的结果返回。可以通过从 main 函数返回一个整数值来实现相同的效果。按照惯例,值为 0 表示成功终止,而非零值表示异常终止。

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

使用 MS Visual C/C++ 编译和运行程序

[编辑 | 编辑源代码]

首先,确保您执行 vcvars32.bat,您可以在 MS Visual C/C++ 目录的 bin 子目录中找到它。这将设置一些您需要进行命令行工具正确操作的环境变量。

cl /FeAscii_Enc.exe Ascii_Encoding.c↵

gcc 类似,此命令将经历预处理、编译、汇编和链接阶段。完成成功后,我们可以简单地通过在命令行中输入可执行文件名的名称来运行我们的程序。操作系统 shell 将识别生成的执行文件作为外部命令,并获取加载器将 Ascii_Enc.exe 加载到内存中并最终运行它。

Ascii_Enc↵

使用 DGJPP-rhide 编译和运行程序

[编辑 | 编辑源代码]

启动一个新的 DOS 框,并输入以下命令。

rhide Ascii_Encoding.c↵

这将启动一个基于 DOS 的 IDE,您可以使用它来开发不同编程语言的项目。选择 Compile→Make 或 Compile→Build All 或 Compile→Compile,然后选择 Compile→Link。这将编译源代码并将生成的的目标模块与 C 运行时链接。您现在可以通过单击 Run→Run 或通过选择 File→DOS Shell 并退出到 DOS 来运行可执行文件,并在提示符下输入文件名。(如果您可能从 rhide 中看到意外的行为,请确保文件不在目录层次结构中太深,并且名称不包含空格等特殊字符。)如果您选择第二个选项,您可以在命令提示符下键入 exit 返回 rhide。

问题:编写一个以英语或土耳其语打印问候语的程序。语言应通过编译时开关选择。要问候的人员姓名通过命令行参数传递给程序。


Greeting.c
#include <stdio.h>

以下行检查是否已定义名为 TURKISH 的宏。此宏或任何宏的定义可以在文件内或在命令行提示符处作为编译器开关进行。在此示例中,文件或任何包含文件都没有这样的定义。因此,在命令行提示符处没有这样的宏定义,将导致控制跳转到 #else 部分,并且 #else#endif 之间的语句将包含在源文件中。假设定义是在命令行提示符处完成的,则 #if#else 之间的语句将包含在源文件中。无论包含代码的哪一部分,有一点是肯定的: #if#else 之间的部分或 #else#endif 之间的部分将被包含,而不是两者都被包含;不会出现重复定义的风险。

请注意变量命名的特殊方式。这是所谓的匈牙利命名法。通过在标识符名称之前添加特殊解释的字符,此方法旨在为其他程序员/维护人员提供尽可能多的上下文信息。无需任何关于标识符定义的引用(可能在相隔数页的页面上,甚至在不可访问的另一个源文件中),我们现在只需通过解释前缀即可获取所需的信息。例如,szGreeting 表示以零结尾的字符串(即 C 风格的字符串)。

#if defined(TURKISH)
  char szGreeting[] = "Gunaydin,";
  char szGreetStranger[] = "Her kimsen gunaydin!";
  char szGreetAll[] = "Herkese gunaydin!";
#else
  char szGreeting[] = "Good morning,";
  char szGreetStranger[] = "Good morning, whoever you might be!";
  char szGreetAll[] = "Good morning to you all!";
#endif

C 不允许程序员重载函数名。main 函数是一个例外:它有两种形式。第一个我们已经看到,它不接受任何参数。第二个允许用户将命令行参数传递给应用程序。这些参数以指向字符的指针向量形式传递。如果用户希望将参数解释为属于其他数据类型,则应用程序代码必须进行一些额外的处理。

在 C 中,没有标准方法可以告诉数组的大小。应该使用约定或将大小保存在另一个变量中。C 中的字符串(可以视为字符数组)是前者的一个例子。在此,使用一个哨兵值(NULL 字符)来表示字符串的结尾。在大多数情况下,这种方案是不可能或不可行的。在这种情况下,我们需要将大小(或长度)信息保存在单独的变量中。因此需要第二个变量。

程序名是参数向量的第一个组件。因此,如果参数计数为 1,则表示用户根本没有传递任何参数。

int main(int argc, char *argv[]) {
  switch (argc) {

执行 break 语句将终止最内层的封闭循环(whilefordo while)或 switch 语句。也就是说,它将基本上跳转到循环或 switch 语句之后的下一行。在我们的例子中,控制将被转移到 return 语句。

从基于 Pascal 的背景迁移到 C 的新手必须注意 switch 语句的本质:与 case 语句不同(每个分支都是相互执行的),switch 允许执行多个分支。如果您不希望这样,则必须使用 break 语句分隔这些分支,如下所示。如果没有 break 语句,参数计数为 1 将导致打印所有三个消息。同样,参数计数为 2 将打印匿名消息以及相应的消息。

    case 1: printf("%s\n", szGreetStranger); break;
    case 2: printf("%s %s\n", szGreeting, argv[1]); break;
    default: printf("%s\n", szGreetAll);
  } /* end of switch (argc) */

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

使用编译器开关

[编辑 | 编辑源代码]

将此 C 程序保存为 Greeting.c,并在命令行中输入

gcc Greeting.c –DTURKISH –o Gunaydin↵ # 在 Linux 中

这将生成一个名为 Gunaydin 的可执行文件。此可执行文件将不包含 #else#endif 之间的任何语句的代码对象。同样,如果我们使用以下命令编译程序

gcc Greeting.c –DENGLISH –o GoodMorning↵

我们将获得一个名为 GoodMorning 的可执行文件,其中包含 #if 定义和 #else 排除之间的语句。请注意,在命令行中没有定义任何宏时,将包含英语版本的问候语。

不要将编译时开关与命令行参数混淆。前者传递给预处理器,用于更改要编译的代码,而后者传递给正在运行的程序,用于更改其行为。假设我们按照上面所示构建了可执行文件

./Gunaydin Tevfik↵

将产生以下输出

Gunaydin, Tevfik

然而

./GoodMorning Tevfik Ugur↵

将产生

早上好,各位!

指针运算

[编辑 | 编辑源代码]

问题:编写一个程序来演示指针和地址之间的关系。


Pointer_Arithmetic.c
#include <stdio.h>

int string_length(char *str) {
  int len = 0;

在下面的 for 循环中,第三部分对变量 str 进行增量操作,该变量是指向 char 的指针。如果我们没有意识到地址和指针是两个不同的东西,我们可能会倾向于认为我们所做的只是简单地对地址值进行增量操作。但这将是完全错误的。尽管出于教学目的,我们可能假设指针是一个地址,但它们并不完全相同。当我们对指针进行增量操作时,其中包含的地址值将根据指针指向的值类型的尺寸进行增量操作。但是,就指向 char 的指针而言,对指针进行增量操作和对地址进行增量操作具有相同的效果。这是因为 char 值存储在一个字节中。

定义:指针是一个变量,它包含另一个变量的地址,该变量的内容被解释为属于某种类型。[2]

Effect of incrementing a char*

请注意,虽然对对象的句柄可以被视为一种“类似”指针的东西,但它们是两个不同的概念。除了支持继承等其他差异之外,与指针和地址不同,句柄不参与算术运算。

  for(; *str != '\0'; str++) len++;

  return len;
} /* end of int string_length(char *) */

long sum_ints(int *seq) {
  long sum = 0;

下一行是一个示例,展示了指针和地址之间的区别。在这里,对 seq 进行增量操作将使其中包含的地址值增加 int 的大小。

Effect of incrementing a int*
  for (; *seq > 0; seq++) sum += *seq;

  return sum;
} /* end of long sum_ints(int *) */

int main(void) {

下一行创建并初始化了一个 char 数组。编译器计算此数组的大小。编译器所做的是基本上计算双引号之间的字符数,并相应地保留内存。请注意,编译器也会自动附加一个 NULL 字符。但是,如果我们选择使用聚合初始化,我们需要更加小心。

char string[] = {G, o, k, c , e};

将创建一个包含 5 个字符的数组,而不是 6 个。在字符数组的末尾将不会有 NULL 字符。如果这不是我们真正想要的,我们应该在初始化时添加 NULL 字符,如下所示

char string[] = {G, o, k, c, e, ‘\0};

或恢复到以前的方式。在这两种情况下,数组都在运行时堆栈中分配。

还要注意使用转义序列在字符字符串中嵌入双引号。由于它用于标记结尾,因此无法在字符串文字中插入“”。解决此问题的办法是使用“”,它告诉编译器后面的“”不表示字符字符串的结尾,而是应该按字面意思嵌入字符串中。

  char string[] = "Kind of \"long\" string";
  int i;
  int sequence[] = {9, 1, 3, 102, 7, 5, 0};

下一个 printf 语句的第三个参数是对返回 int 的函数的调用。这个调用更有趣的地方在于其参数的传递方式。虽然我们似乎正在传递一个数组,但实际上在幕后传递的是数组第一个元素的地址;也就是说,&string[0]。无论数组大小如何,都会这样做。这种方案的优点是

  1. 我们只需要传递一个指针,而不是整个数组。随着数组大小的增加,我们在内存方面节省得更多。
  2. 现在我们传递了一个指针,避免了复制整个数组。这意味着既节省了时间又节省了内存。
  3. 我们对数组所做的更改是永久性的;调用者会看到被调用者所做的更改。这是因为传递的是值传递的指针,而不是数组。因此,虽然我们不能更改数组第一个元素的地址,但我们可以修改数组的内容。

缺点是,如果希望数组在调用之间保持不变,则需要制作数组的本地副本。

  printf("Length of \"%s\": %d\n", string, string_length(string));
  printf("Sum of ints ( ");
  for (i = 0; sequence[i] != 0; i++)
    printf("%d ", sequence[i]);
  printf("): %ld\n", sum_ints(sequence));

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

位操作

[编辑 | 编辑源代码]

C 最初是系统编程语言,它为逐位操作数据提供帮助。这包括按位运算以及定义带有位域的结构的功能。

毫不奇怪,语言的这方面被用于机器相关的应用程序,例如实时系统、系统程序[例如设备驱动程序],在这些应用程序中,运行速度是最重要的。鉴于当今编译器所做的复杂优化以及位操作的不可移植性,在存在替代方案的情况下,应该避免在通用编程中使用它们。

按位运算

[编辑 | 编辑源代码]

问题:编写函数来提取 float 参数的指数和尾数。

Extract_Fields.c
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>

#define TOO_FEW_ARGUMENTS 1
#define TOO_MANY_ARGUMENTS 2

C 的规范没有标准化(除了类型 char 之外)整数类型的尺寸。语言规范给出的唯一保证是以下关系

sizeof(short)sizeof(int)sizeof(long)sizeof(long long)

在大多数机器上,int 占用四个字节,而在某些机器上,它占用两个字节。C 系统编程方向的体现是,这种差异是由于 int 的大小被认为等于底层体系结构的字长。因此,如果我们认为四个字节用于表示 int 值,我们可能会偶尔看到我们的程序产生无意义的输出。这是因为用四个字节表示的 int 值在用两个字节表示时可能会导致溢出。

下面的 #if-#else 指令用于规避此问题。 UINT_MAX(在 limits.h 中定义)保存最大的无符号 int 值。在 int 值存储在两个字节的机器上,它将等于 65535。否则,它将是其他值。因此,如果 UINT_MAX 恰好是 65535,我们可以说 int 用两个字节表示。如果不是,则用四个字节表示。

#if UINT_MAX==65535
typedef unsigned long BIG_INT;
#else
typedef unsigned int BIG_INT;
#endif

char *usage = USAGE: Extract_Fields float_number;

下一个函数提取特定 float 变量的指数部分。这是通过隔离指数部分并将结果值向右移位来实现的。我们使用按位与运算符(二进制 &)来隔离数字并向右移位(>>)这个隔离的指数(以某种方式调整数字的右对齐)。

请注意,对负整数进行右移的结果(即最高有效位为 1 的位模式的数字)是未定义的。换句话说,行为取决于实现。在某些实现中,符号位会保留,因此右移实际上是对数字进行符号扩展,而在其他实现中,此位将被 0 替换。[3]

BIG_INT exponent(float num) {
  float *fnump = &num;
  BIG_INT *lnump = (BIG_INT *) fnump;

此处的内存部分图像在图 3 中给出。如果,例如,num 的值为 -1.5,它将通过 fnump 被解释为 -1.5。也就是说,*fnump 将为 -1.5。但是,如果通过 lnump 查看,它将被解释为包含 3,217,031,168!这种差异是由于看待同一事物的不同方式造成的:*fnumpnum 看作符合 IEEE 754/854 标准的 float 值,而 *lnump 将相同四个字节的内存看作一个整数(类型为 unsigned longunsigned int,具体取决于运行程序的机器)使用二进制补码表示法编码。

(在 此处 插入 图表)

问题
*lnump 加 1 后,*fnump 的值是多少?

以下行首先使用位掩码隔离指数部分,然后将其右移,以便指数位占据最低有效位。

(在 此处 插入 图表)

  return((*lnump & 0x7F800000) >> 23);
} /* end of BIG_INT exponent(float) */

下一个函数提取数字的尾数部分。它通过简单地屏蔽数字的符号和指数部分来实现这一点。这是通过返回表达式中的按位与运算符完成的。请注意,我们不需要移位数字,因为它的尾数由表示形式的最低 23 位组成。

BIG_INT mantissa(float num) {
  float *fnump = &num;
  BIG_INT *lnump = (BIG_INT*) fnump;

  return(*lnump & 0x007FFFFF);
} /* end of BIG_INT long mantissa(float) */

以下函数试图理解命令行参数。此类参数的数量为 1 表示用户没有传递任何数字。我们显示一条适当的消息并退出程序。如果参数计数为 2,则第二个参数将被视为我们将提取其组件的数字。否则,用户传递了超出我们处理能力的参数;我们只是通知她并退出程序。

在失败的情况下,建议使用非零值退出。这在程序通过 shell 脚本协同运行时可能非常有用。如果一个程序依赖于另一个程序的成功完成,脚本需要有一种可靠的方法来检查先前程序的结果。使用非描述性值的程序在这种情况下的帮助不大。我们必须确保当我们返回零时,它确实意味着执行成功。否则,就存在问题,这可以通过不同的退出代码进一步说明。

strtod 函数用于将一个可能包含 -/+ 和 e/E 的数字字符串转换为类型为 double 的浮点数。虽然很容易弄清楚第一个参数的功能,即指向要转换的字符串的指针,但第二个参数则不然。从 strtod 返回后,第二个参数将保存指向输入字符串中已转换部分之后字符的指针。由于这一点,我们可以使用 strtod 及其朋友处理字符串的其余部分。

strtod 的朋友
声明 描述 错误情况
long strtol(const char* str, char **ptr, int base): 将字符字符串转换为 long int 返回 0L。
unsigned long strtoul(const char* str, char **ptr, int base): 将字符串转换为 unsigned long int 返回 0L。
double atof(const char* str): str 转换为浮点数,并将转换结果作为 double 返回。 返回 0.0。
int atoi(const char* str): str 转换为整数,并将转换结果作为 int 返回。 返回 0。
unsigned long atol(const char* str): str 转换为整数,并将转换结果作为 unsigned long 返回。 返回 0L。
char* strtok(char* str, const char* set): 使用 set 中的字符作为分隔符来标记 str。将 NULL 传递给第一个参数告诉此函数从上次停止的地方继续。
Parse.c

#include <stddef.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char *argv[]) { char *name, *midterm, *final, *assignment; // 假设 argv[1] 是 “Selin Yardimci: 80, 90, 100”。 name = strtok(argv[1], ":"); // // printf("Name: %s\n", name); midterm = strtok(NULL, ","); // // printf("Midterm: %lu\t", strtoul(midterm, NULL, 10)); final = strtok(NULL, ","); // // printf("Final: %lu\t", strtoul(final, NULL, 10)); assignment = strtok(NULL, "\0"); // // printf("Assignment: %lu\n", strtoul(assignment, NULL, 10)); return(0); } /* int main(int, char**) 的结尾 */

gcc -o Parse.exe Parse.c↵
#需要双引号将字符串视为单个参数。它不是参数字符串的一部分,将在传递给 main 之前被剥离!
Parse "Selin Yardimci: 80, 90, 100"↵
Name: Selin Yardimci
Midterm: 80 Final: 90 Assignment: 100
float number(int argc, char *argv[]) {
  switch (argc) {
    case 1: 
      printf(Too few arguments. %s\n, usage);
      exit(TOO_FEW_ARGUMENTS);
    case 2:
      return((float) strtod(argv[1], NULL));
    default:
      printf(Too many arguments. %s\n, usage);
      exit(TOO_MANY_ARGUMENTS):
  } /* end of switch (argc) */
} /* end of float number(int, char **) */

观察main函数的简洁性。我们只是说明程序的功能,并没有深入研究它是如何实现的。阅读main函数,代码维护者可以轻松地弄清楚它声称要做什么。如果她需要更多细节,则需要检查函数体。这些函数将根据程序的复杂性提供实现的完整细节或将此提供推迟到另一个函数。在复杂的程序中,这种延迟可以扩展到多个级别。

无论手头的问题有多简单或多复杂,无论我们使用什么范式,我们都首先回答“是什么”的问题,然后(可能按程度)继续回答“如何”的问题。换句话说,我们首先分析问题,并为解决方案设计一个方案,然后提供实现。[4] 我们的代码应该反映这一点:它应该首先公开对“是什么”(接口)的答案,然后(对感兴趣的方)公开对“如何”(实现)的答案。

int main(int argc, char *argv[]) {
  float num;

  printf(Exponent of the number is: %x\n, 
            exponent(num = number(argc, argv)));
  printf(Mantissa of the number is: %x\n, mantissa(num));

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

位域

[edit | edit source]

问题:使用位域实现前一个问题。

Extract_Fields_BF.c
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>

...

位域的定义类似于普通的记录域。两者之间的唯一区别是在位域之后跟着的宽度说明。根据以下定义,分数、指数和符号分别占二十三位、八位和一位。但是,其余部分很大程度上取决于编译器的实现。

首先,正如预处理指令所体现的那样,struct SINGLE_FP类型变量的内存布局取决于处理器的字节序。位打包的方式也不保证在不同的硬件之间相同。这两个因素有效地将位域的使用限制在与机器相关的程序中。

struct SINGLE_FP {
#ifdef BIG_ENDIAN /* e.g. Motorola */
  unsigned int sign : 1;
  unsigned int exponent : 8;
  unsigned int fraction : 23;
#else /* if LITTLE_ENDIAN, e.g. Intel */
  unsigned int fraction : 23;
  unsigned int exponent : 8;
  unsigned int sign : 1;
#endif
};

BIG_INT exponent(float num) {
  float *fnump = &num;
  struct SINGLE_FP *lnump = (struct SINGLE_FP *) fnump;

C 中有两个域访问运算符:.->。前者用于访问结构的域,而后者用于通过指向结构的指针访问结构的域。现在lnump 被定义为指向SINGLE_FP 结构的指针,所有声明为 SINGLE_FP 类型的变量都可以通过使用 -> 访问位域。

观察structure->field 等价于 (*structure).field。例如,lnump->exponent 等价于 (*lnump).exponent

  return(lnump->exponent);
} /* end of BIG_INT exponent(float) */

BIG_INT mantissa(float num) {
  float *fnump = &num;
  struct SINGLE_FP *lnump = (struct SINGLE_FP *) fnump;  

  return(lnump->fraction);
} /* end of BIG_INT mantissa(float) */

float number(int argc, char *argv[]) { ... }

int main(int argc, char *argv[]) { ... }

静态局部变量(记忆化)

[edit | edit source]

问题:使用记忆化编写低成本的递归阶乘函数。

类似于缓存,记忆化可用于通过保存已完成计算的结果来加快程序速度。两者之间的区别在于它们的范围。当我们谈论缓存时,我们指的是系统级或应用程序级的优化技术。另一方面,记忆化是一种函数特定的技术。当接收到请求时,我们首先检查是否可以避免从头开始计算结果。否则,从头开始进行计算,并将结果添加到我们的数据库中。换句话说,我们更喜欢空间计算而不是时间计算,并节省了一些宝贵的计算机时间。

将此技术应用于手头的問題,我们将把已经计算的阶乘集合存储在static 局部数组中。现在,对这个数组所做的任何更改都将在不同的调用之间保持持久性,递归的基准条件将更改为达到已计算的阶乘,而不是参数值为 1 或 0。也就是说,我们有

Mathematical definition of the factorial function
阶乘函数的数学定义


Memoize.c
#include <stdio.h>

#define MAX_COMPUTABLE_N 20
#define TRUE 1

unsigned long long factorial(unsigned char n) {

一旦程序开始运行,以下初始化将生效。事实上,由于static 局部变量是在静态数据区域分配的,因此它们在可执行文件的磁盘镜像中包含初始值。

标记用于存储已进行计算的数组的初始化程序。虽然它具有MAX_COMPUTABLE_N 个分量,但在初始化程序中只提供了两个值。其余插槽将填充默认的初始值 0。换句话说,它等价于

static unsigned long long computed_factorials[MAX_COMPUTABLE_N] = {1, 1, 0, 0, , .., 0};

请注意,我们可以提供更多初始值以避免更多初始成本。

static unsigned char largest_computed = 1;
static unsigned long long computed_factorials[MAX_COMPUTABLE_N] = {1, 1};

如果我们已经计算了等于或大于当前参数值的数字的阶乘,我们会检索此值并将其返回给调用者。

请注意,返回值的接收者可以是main 函数或阶乘函数的另一次调用。在第二种情况下,我们进行部分计算并使用先前计算中的一些部分结果。

  if (n <= largest_computed) return computed_factorials[n];

  printf("N: %d\t", n);

一旦计算出新的值,它就会存储在我们的数组中,并且已经计算了阶乘的最大参数值将相应地更新以反映这一事实。

  computed_factorials[n] = n * factorial(n - 1);
  if (largest_computed < n) largest_computed = n;

  return computed_factorials[n];
} /* end of unsigned long long factorial(unsigned char) */

int main(void) {
  short n;

  printf("Enter a negative value for termination of the program...\n");
  do {
    printf("Enter an integer in [0..20]: ");

h 在转换字母 (d) 前表示输入应为 short int。类似地,可以使用 l 指定 long int

    scanf("%hd", &n);
    if (n < 0) break;
    if (n > 20) {
      printf("Value out of range!!! No computations done.\n");

break 一样,continue 用于改变循环内的控制流;它终止最内层封闭的 whiledo whilefor 语句的执行。在我们的例子中,控制将转移到 do-while 语句的开头。

      continue;
    } /* end of if (n > 20) */
    printf("%d! is %Lu\n", n, factorial((unsigned char) n));
  } while (TRUE);

  return(0);
} /* end of int main(void) */
gcc –o MemoizedFactorial.exe Memoize.c↵
MemoizedFactorial↵
输入负值以终止程序
输入 [0..20] 之间的整数:1
1! 为 1
输入 [0..20] 之间的整数:5
N:5 N:4 N:3 N:2 5! 为 120
输入 [0..20] 之间的整数:5
5! 为 120
输入 [0..20] 之间的整数:10
N:10 N:9 N:8 N:7 N:6 10! 为 3628800
输入 [0..20] 之间的整数:-1

文件操作

[edit | edit source]

问题:编写一个程序,该程序可以剥离 C 程序的注释。


Strip_Comments.c
#include <ctype.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BOOL char
#define FALSE 0
#define TRUE 1

#define MAX_LINE_LENGTH 500

#define NORMAL_COMPLETION 0
#define CANNOT_OPEN_INPUT_FILE 11
#define CANNOT_OPEN_OUTPUT_FILE 12
#define TOO_FEW_ARGUMENTS 21
#define TOO_MANY_ARGUMENTS 22

当标识符定义被 const 限定时,它被认为是不可变的。这样的标识符不能作为左值出现。这意味着我们不能将标识符声明为常量,然后对其赋值;常量必须在其声明点提供一个值。换句话说,常量必须被初始化。

全局常量的初始化程序只能包含可以在编译时计算的子表达式。另一方面,局部常量可以包含运行时值。例如,

#include <stdio.h> #include <stdlib.h> void f(int ipar) { const int i = ipar * 3; printf(i: %d\n, i); } /* end of void f(int) */ int main(void) { int i = 6; f(5); f(10); f(i); f(i + 7); exit(0); } /* end of int main(void) */

将产生以下输出

i: 15
i: 30
i: 18
i: 39

这表明对于每次函数调用都会创建常量,它们可以具有不同的值。但它们在整个函数调用过程中仍然无法被修改。

const char file_ext[] = .noc;
const char temp_file[] = TempFile;
const char usage[] = "USAGE: StripComments filename";

在 C 中,所有标识符都必须在使用之前声明!这包括文件名、变量和结构标签。相应的定义可以在同一文件中或不同的文件中在声明之后提供。虽然可以有多个声明,但只能有一个定义。

以下声明是函数的原型,这些函数的定义在程序的后面提供。请注意,参数名称与定义中提供的参数名称不一致。事实上,甚至不必提供名称。但是,您仍然必须列出参数类型以方便类型检查:如果函数的定义或任何对它的使用与它的原型不一致,则会发生错误。

可以通过重新排列定义的顺序来避免使用原型。对于这个例子,将 main 函数放在最后可以消除对原型的需要。

char* filename(int argumentCount, char* argumentVector[]);
void trimWS(const char *filename);
void strip(const char *filename);

用逗号分隔的表达式被认为是一个单一的表达式,它的结果是最后一个表达式的返回值。对用逗号分隔的表达式的计算是严格从左到右的;编译器不能改变这个顺序。

int main(int argc, char* argv[]) {
  char *fname = filename(argc, argv);
  (strip(fname), trimWS(fname));

  return(NORMAL_COMPLETION);
} /* end of int main(int, char**) */

char* filename(int argc, char* argv[]) {

printf 的一个更通用的版本 fprintf 执行输出格式化并将输出发送到作为第一个参数指定的流。因此我们可以将 printf 视为以下内容的等效形式

fprintf(stdin, "...", ...); /* read it as "file printf..." */

您可能希望在输出格式化中考虑的另一个 printf 的朋友是 sprintf 函数。这个函数不是将输出写入某个媒体,而是将其存储在一个字符字符串中。

typedef double currency; currency expenditure = 234.0; ... char *str = (char*) malloc(); ... sprintf(str, %f.2$, expenditure); printf(Total spending: %s, str); /* will print “Total spending: 234.00$” */ ... free(str); ...

  switch (argc) {
    case 1: 
      fprintf(stderr, "No file name passed!\n %s\n", usage);
      exit(TOO_FEW_ARGUMENTS);
    case 2: return(argv[1]);
    default: 
      fprintf(stderr, "Too many arguments!\n %s\n",usage);
      exit(TOO_MANY_ARGUMENTS);
  } /* end of switch(argc) */
} /* end of char* filename(int, char**) */

void trimRight(char *line) {
  int i = strlen(line) - 1;

  do 
    i--; 
  while (i >= 0 && isspace(line[i])) ;

  line[i + 1] = '\n';
  line[i + 2] = '\0';
} /* end of void trimRight(char*) */

void trimWS(const char *infilename) {
  char next_line[MAX_LINE_LENGTH];
  char outfilename[strlen(infilename) + strlen(file_ext) + 1];
  FILE *infile, *outfile;
  BOOL empty_line = FALSE;
MS Visual C/C++ 编译器将为突出显示的行发出编译错误。但是,根据 ISO C,自动(即局部)数组可以具有未知大小,此大小由运行时的初始化程序确定。为了让此程序使用 MS Visual C/C++ 编译,您需要将此数组定义更改为指针定义,并使用 malloc/free 函数在堆中分配/释放数组空间。


以下语句尝试以读取模式打开文件。它将保存在变量 temp_file 中的物理文件的名称映射到名为 infile 的逻辑文件。如果此尝试成功,您对逻辑文件执行的每个操作都将对物理文件执行。可以将 infile 变量视为对真实文件的句柄。句柄与物理文件之间的映射不是一对一的。就像多个句柄可以显示同一个对象一样,一个文件可以有多个句柄指向同一个物理文件。只要所有句柄都以读取模式使用,就没有问题。但是,如果不同的句柄同时尝试修改同一个文件,情况就会变得糟糕。[关键词是操作系统、并发和同步。]

如果打开操作失败,我们就无法获得物理文件的句柄。这反映在 fopen 函数的返回值中:NULL。一个具有 NULL 值的指针意味着我们不能将其用于进一步操作。我们能做的就是将其与 NULL 进行比较。因此,我们首先检查此条件。除非它为 NULL,否则我们继续;否则,我们将有关异常条件性质的内容写入标准错误文件 stderr 并退出程序。

与标准输出一样,标准错误文件默认情况下也映射到屏幕。那么,为什么我们要写入 stderr 而不是 stdio 呢?答案是,我们可以选择将这些标准文件重新映射到不同的物理单元。在这种情况下,如果我们继续将所有内容写入同一个逻辑文件,例如 stdio,错误将混淆有效的输出数据;我们将难以分辨哪一个是哪一个。

  infile = fopen(temp_file, "r");
  if (infile == NULL) {
    fprintf(stderr, "Error in opening file %s: %s\n", temp_file, strerror(errno));
    exit(CANNOT_OPEN_INPUT_FILE);
  } /* end of if (infile == NULL) */

  strcpy(outfilename, infilename); strcat(outfilename, file_ext);
  outfile = fopen(outfilename, "w");
  if (outfile == NULL) {
    fprintf(stderr, "Error in opening file %s: %s\n", outfilename, strerror(errno));
    fclose(infile);
    exit(CANNOT_OPEN_OUTPUT_FILE);
  } /* end of if (outfile == NULL) */

  while (fgets(next_line, MAX_LINE_LENGTH + 1, infile)) {
    trimRight(next_line);
    if (strlen(next_line) == 1) {
      if (!empty_line) fputs(next_line, outfile);
      empty_line = TRUE;
      } else {
        fputs(next_line, outfile);
        empty_line = FALSE;
      } /* end of else */
  } /* end of while (fgets(next_line, …) */

  fclose(infile); fclose(outfile); remove(temp_file);

  return;
} /* end of void trimWS(const char*) */

void strip(const char *filename) {
  int next_ch;
  BOOL inside_comment = FALSE;
  FILE *infile, *outfile;

  infile = fopen(filename, "r");
  if (infile == NULL) {
    fprintf(stderr, "Error in opening file %s: %s\n", filename, strerror(errno));
    exit(CANNOT_OPEN_INPUT_FILE);
  } /* end of if (infile == NULL) */

  outfile = fopen(temp_file, "w");
  if (outfile == NULL) {
    fprintf(stderr, "Error in opening file %s: %s\n", temp_file, strerror(errno));
    fclose(infile);
    exit(CANNOT_OPEN_OUTPUT_FILE);
  } /* end of if (outfile == NULL) */

可以使用以下有限自动机对问题的解决方案进行建模。

Finite automaton providing the solution
提供解决方案的有限自动机

请注意,将基于 FA 的解决方案转换为 C 代码的容易程度。这又是另一个例子,说明这种理论模型尽管看起来可能毫无用处和无聊,但它在实践中却非常有用。

通过将问题的领域表示转换为相应的解决方案领域表示来解决问题。一个人对表示问题的模型了解得越多,她就越容易想出问题的解决方案。

  while ((next_ch = fgetc(infile)) != EOF) {
    switch (inside_comment) {
      case FALSE:
        if (next_ch != '/') { fputc(next_ch, outfile); break; }
        if ((next_ch = fgetc(infile)) == '*') inside_comment = TRUE;
          else { fputc('/', outfile); ungetc(next_ch, infile); }
      break;
      case TRUE:
        if (next_ch != '*') break; 
        if ((next_ch = fgetc(infile)) == '/') inside_comment = FALSE;
    } /* end of switch(inside_comment) */
  } /* end of while ((next_ch = fgetc(infile)) != EOF) */

fclose 将逻辑文件(句柄)与物理文件断开连接。如果程序员没有这样做,C 运行时退出序列中的代码保证在程序结束时所有打开的文件都会关闭。但是,将其留给退出序列有两个缺点。

  1. 应用程序可以同时打开的最大文件数有限。如果我们将关闭文件推迟到退出序列,我们可能更频繁地更快地达到此限制。
  2. 除非你显式地使用 fflush 刷新,否则你写入文件的數據實際上被寫入内存緩存,而不是寫入磁盘。 每當你向文件写入换行符或緩存区满时,它会自动刷新。 也就是说,如果在你可以关闭文件的最早时间和程序结束时退出序列关闭文件之间发生灾难性故障(例如中断),则緩存区中剩余的數據将不会提交到磁盘。 这不是一个完美的成功故事!

所以你应该要么显式地刷新,要么尽早关闭文件。

  fclose(infile); 
  fclose(outfile);

  return;
} /* end of void strip(const char *) */

堆内存分配

[edit | edit source]

问题:编写一个加密程序,从文件读取数据并将编码后的字符写入另一个文件。 使用以下简单的加密方案: 字符 c 的加密形式是 c ^ key[i],其中 key 是作为命令行参数传递的字符串。 该程序以循环方式使用 key 中的字符,直到所有输入都被读取。 使用相同的 key 对编码后的文本进行重新加密会生成原始文本。 如果没有传递 key 或传递空字符串,则不进行加密。

Cyclic_Encryption.c
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define CANNOT_OPEN_INPUT_FILE 11
#define CANNOT_OPEN_OUTPUT_FILE 12
#define TOO_FEW_ARGS 21
#define TOO_MANY_ARGS 22

const char *usage = "USAGE: CyclicEncryption inputfilename [key [outputfilename]]";
const char file_ext[] = ".enc";

struct FILES_AND_KEY {
  char *infname;
  char *outfname;
  char *key;
};

typedef struct FILES_AND_KEY f_and_k;

f_and_k getfilenameandkey(int argc, char **argv) { 
  f_and_k retValue = { "\0", "\0", "\0" };

  switch (argc) {
    case 1:
      fprintf(stderr, "No file name passed!\n %s", usage);
      exit(TOO_FEW_ARGS);

malloc 用于从堆中分配存储空间,堆是程序员自己管理的内存区域。 这意味着程序员有责任将通过 malloc 分配的每字节内存返回到可用内存池。

这种内存是通过指针间接操作的。 不同的指针可以指向同一个内存区域。 换句话说,它们可以共享同一个对象。

使用指针操作堆对象并不意味着指针只能指向堆对象。 也不意味着指针本身是在堆中分配的。 指针可以指向非堆对象,它们可以驻留在静态区域或运行时堆栈中。 事实上,在之前的例子中就是这样。

    case 2:
      retValue.infname = (char *) malloc(strlen(argv[1]) + 1);
      strcpy(retValue.infname, argv[1]);
      retValue.outfname = (char *) malloc(
      strlen(argv[1]) + strlen(file_ext) + 1);
      strcpy(retValue.outfname, argv[1]);
      strcat(retValue.outfname, file_ext);
      return(retValue);
    case 3:
      retValue.infname = (char *) malloc(strlen(argv[1]) + 1);
      strcpy(retValue.infname, argv[1]);
      retValue.key= (char *) malloc(strlen(argv[2]) + 1);
      strcpy(retValue.key, argv[2]);
      retValue.outfname = (char *) malloc(
      strlen(argv[1]) + strlen(file_ext) + 1);
      strcpy(retValue.outfname, argv[1]);
      strcat(retValue.outfname, file_ext);
      break;
    case 4:
      retValue.infname = (char *) malloc(strlen(argv[1]) + 1);
      strcpy(retValue.infname, argv[1]);
      retValue.key = (char *) malloc(strlen(argv[2]) + 1);
      strcpy(retValue.key, argv[2]);
      retValue.outfname = (char *) malloc(strlen(argv[3]) + 1);
      strcpy(retValue.outfname, argv[3]);
      break;
    default:
      fprintf(stderr, "Too many arguments!\n %s", usage);
      exit(TOO_MANY_ARGS);
  } /* end of switch(argc) */

  return retValue;
} /* end of f_and_k getfilenameandkey(int, char**) */

void encrypt(f_and_k fileandkey) {
  int i = 0, keylength = strlen(fileandkey.key);
  int next_ch;
  FILE *infile;
  FILE *outfile;

  if (keylength == 0) return;

以下代码行打开一个可以逐字节(二进制模式)读取的文件,而不是逐字符(文本模式)读取。 在像 UNIX 这样的操作系统中,每个字符都映射到代码表中的一个条目,两者之间没有区别。 但是,在 MS Windows 中,换行符映射为回车符后跟换行符,两者之间存在区别,这可能会让不小心的人员陷入困境。 以下 C 程序演示了这一点。 用多行文件运行它,你会看到需要的 fgetc 的数量将不同。 你读取的字符数将少于你读取的字节数。 这种差异是换行符处理方式不同的结果,当你将你的工作代码从 Linux 移动到 MS Windows 时,它可能会导致很大的麻烦。

Newline.c

#include <errno.h> #include <stdio.h> #include <string.h> #define CANNOT_OPEN_INPUT_FILE 1 int main(void) { int counter; int next_ch; FILE *in1, *in2; in1 = fopen("Text.txt", "r"); if (!in1) { fprintf(stderr, "Error in opening file! %s\n", strerror(errno)); return(CANNOT_OPEN_INPUT_FILE); } /* end of if(!in1) */ counter = 1; while((next_ch = fgetc(in1)) != EOF) printf("%d.ch: %d\t", counter++, next_ch); printf("\n"); fclose(in1); in2 = fopen("Text.txt", "rb"); if (!in2) { fprintf(stderr, "Error in opening file! %s\n", strerror(errno)); return(CANNOT_OPEN_INPUT_FILE); } /* end of if(!in2) */ counter = 1; while((next_ch = fgetc(in2)) != EOF) printf("%d.ch: %d\t", counter++, next_ch); fclose(in2); return(0); } /* end of int main(void) */

Text.txt

First line Second line Third Line Fourth and last line

如果你在基于 Unix 的环境中编译并运行上一页列出的程序,它将为两种模式(文本和二进制)生成相同的输出。 对于 MS Windows 和 DOS 环境,它们将生成以下输出。

gcc –o Newline Newline.c↵
Newline↵
1.ch: 70 ...
...
11.ch: 10 12.ch: 83 ...
...
... 54.ch: 101
1.ch: 70 ...
...
11.ch: 13 12.ch: 10 13.ch: 83 ...
...
...
... 57.ch: 101
  infile = fopen(fileandkey.infname, "rb");
  if (!infile) {
    fprintf(stderr, "Error in opening file %s: %s\n",
    fileandkey.infname, strerror(errno));
    exit(CANNOT_OPEN_INPUT_FILE);
  } /* end of if (!infile) */

  outfile = fopen(fileandkey.outfname, "wb");
  if (!outfile) {
    fprintf(stderr, "Error in opening output file %s: %s\n", 
    fileandkey.outfname, strerror(errno));
    fclose(infile);
    exit(CANNOT_OPEN_OUTPUT_FILE);
  } /* end of if (!outfile) */

  while ((next_ch = fgetc(infile)) != EOF) {
    fprintf(outfile, "%c",(char) (next_ch ^ fileandkey.key[i++]));
    if (keylength == i) i = 0;
  } /* end of while ((next_ch = fgetc(infile)) != EOF) */

  fclose(outfile); fclose(infile);

  return;
} /* end of void encrypt(f_and_k) */

int main(int argc, char *argv[]) {
  f_and_k fandk = getfilenameandkey(argc, argv);

  encrypt(fandk);
  free(fandk.infname);
  fandk.infname = fandk.outfname; 
  fandk.outfname = (char *) 
  malloc(strlen(fandk.infname) + strlen(file_ext) + 1);
  strcpy(fandk.outfname, fandk.infname);
  strcat(fandk.outfname, file_ext);
  encrypt(fandk);

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

指向函数的指针(回调)

[edit | edit source]

问题:编写一个通用的冒泡排序例程。 测试你的代码以对 int 数组和字符字符串进行排序。

General.h
#ifndef GENERAL_H
#define GENERAL_H

...

#define BOOL char
#define FALSE 0
#define TRUE 1
...
typedef void* Object;

以下 typedef 语句将 COMPARISON_FUNC 定义为指向函数的指针,该函数接受两个 Object 类型的参数(即 void*),并返回一个 int。 在此定义之后,在这个头文件中或包含此头文件的任何文件中,我们可以像使用任何其他数据类型一样使用 COMPARISON_FUNC。 实际上,我们在 Bubble_Sort.h 和 Bubble_Sort.c 中就是这么做的。 bubble_sort 函数的第三个参数被定义为 COMPARISON_FUNC 类型。 也就是说,我们可以传递任何符合以下原型函数的地址。

void* 用作通用指针。换句话说,指针指向的数据不假定属于特定类型。从某种意义上说,它与 Java 类层次结构顶部的 Object 类具有相同的目的。就像任何对象都可以被视为属于 Object 类一样,任何值,无论是像 char 这样简单的东西还是像数据库这样复杂的东西,都可以被这个指针指向。但是,这样的指针不能使用 * 或下标运算符进行解引用。在使用它之前,必须先将指针强制转换为适当的类型。

typedef int (*COMPARISON_FUNC)(Object, Object);
...
#endif


Bubble_Sort.h
#ifndef BUBBLE_SORT_H
#define BUBBLE_SORT_H

#include "General.h"

bubble_sort 函数对一个 Object 数组(第一个参数)进行排序,其大小作为第二个参数提供。由于现在没有通用的方法来比较两个组件,而我们希望我们的实现是通用的,所以我们必须能够动态地确定比较两个任意类型项的函数。通过使用指向函数的指针,可以实现对函数的动态确定。将不同的值分配给此指针可以启用使用不同的函数,这意味着对于不同的类型,会有不同的行为。找到对所有可能的​​数据类型通用的参数类型是使指向函数的指针对所有类型都有效的关键。为此,comp_func 接受两个类型为 Object 的参数,即 void*,它可以被解释为属于任何类型。

extern void bubble_sort (Object arr[], int sz, COMPARISON_FUNC comp_func);

#endif


Bubble_Sort.c
#include <stdio.h>

#include "General.h"
#include "algorithms\sorting\Bubble_Sort.h"

在以下定义中,static 限定符将 swap 的可见性限制在此文件中。从某种意义上说,它与 OOPL 中类中的 private 限定符相同。不同之处在于文件是操作系统概念(即由 OS 管理),而类是编程语言概念(即由编译器管理)。后者无疑是更高层次的抽象。在没有更高层次的抽象的情况下,可以使用低层抽象来模拟它(更高层次的抽象)。需要外部代理的干预来提供这种模拟,这会导致更脆弱的解决方案。在这种解决方案中就是这种情况:程序员,干预代理,必须通过使用一些编程约定来模拟这种更高层次的抽象。

static void swap(Object arr[], int lhs_index, int rhs_index) {
  Object temp = arr[lhs_index];
  arr[lhs_index] = arr[rhs_index];
  arr[rhs_index] = temp;

  return;
} /* end of void swap(Object[], int, int) */

void bubble_sort (Object arr[], int sz, COMPARISON_FUNC cmp) {
  int pass, j;

  for(pass = 1; pass < sz; pass++) {
    BOOL swapped = FALSE;

    for (j = 0; j < sz - pass; j++)

我们可以按如下方式编写以下行

if (cmp(arr[j], arr[j + 1]) > 0) {

它仍然会做同样的事情。因此,指向函数的变量可以使用得像函数一样:只需使用它的名称并将参数括在括号中即可。这将导致指向变量的函数被调用。通过指针调用函数的好处是可以简单地通过更改指针变量的值来调用不同的函数。如果将 compare_ints 的地址传递给 bubble_sort 函数,它将调用 compare_ints;如果将 compare_strings 的地址传递给它,它将调用 compare_strings;如果 ...

      if ((*cmp)(arr[j], arr[j + 1]) > 0) {
        swap(arr, j, j + 1);
        swapped = TRUE;
      } /* end of if ((*cmp)(arr[j], arr[j + 1]) > 0) */

    if (!swapped) return; 
  } /* end of outer for loop */
} /* void bubble_sort(Object[], int, COMPARISON_FUNC) */


Pointer2Function.c
#include <stdio.h>
#include <string.h>

#include “General.h”

以下行引入了 bubble_sort 函数的原型,而不是它的源代码或目标代码。

编译器利用此原型对函数的使用进行类型检查。这涉及检查参数的数量、类型、它(函数)是否在正确的上下文中使用。一旦确认了这一点,链接器就会接管并(如果它在单独的源文件中)引入 bubble_sort 的目标代码。所以,

  1. 预处理器通过将指令替换为 C 源代码来预处理源代码。
  2. 编译器使用提供的元信息(例如变量声明/定义和函数原型)检查程序的语法和语义。请注意,当编译器接管时,所有预处理器指令都将被替换为 C 源代码。也就是说,编译器对预处理器一无所知。
  3. 链接器将目标文件组合成单个可执行文件。此可执行文件稍后由加载器加载到内存中,并在操作系统的监督下运行。
#include "algorithms\sorting\Bubble_Sort.h"

typedef int* Integer;

void print_args(char *argv[], int sz) {
  int i = 0;

  if (sz == 0) { 
    printf("No command line args passed!!! Unable to test strings...\n");
    return;
  }
  for (; i < sz; i++)
    printf("Arg#%d: %s\t", i + 1, argv[i]);
  printf("\n");
} /* end of void print_args(char **, int) */

void print_ints(Integer seq[], int sz) {
  int i = 0;

  for (; i < sz; i++) 
  printf("Item#%d: %d\t", i + 1, *(seq[i]));
  printf("\n");
} /* end of void print_ints(Integer[], int) */

接下来的两个函数是为排序算法的实现进行比较所需的。它们比较两个相同类型的​​值。

bubble_sort 函数的实现者无法预先知道要排序的无数个对象类型,并且没有适用于所有类型的通用比较方法。因此,排序算法的用户必须实现比较函数并让实现者了解此函数。实现者利用此函数成功地提供服务。在这样做时,它会“回拨”到用户代码。因此,这种类型的调用称为*回调*。

int compare_strings(Object lhs, Object rhs) {
  return(strcmp((char *) lhs, (char *) rhs));
} /* end of int compare_strings(Object, Object) */

看起来这是一种非常复杂的方式来比较两个 int?没错!但是请记住:我们必须能够比较任何类型的两个对象(值),并且我们必须使用一个函数签名来完成它。比较两个 int 很简单:只需比较值即可。但是,字符字符串呢?比较指针不会产生准确的结果;我们必须比较指针指向的字符字符串。当我们比较包含嵌套结构的两个结构时,它变得更加复杂。解决此问题的办法是将球传给最了解它的人(算法的用户),同时传递指向数据的通用指针 (void *),而不是数据本身。在此过程中,用户将把它强制转换为适当的类型,并相应地进行比较。

int compare_ints(Object lhs, Object rhs) {
  Integer ip1 = (Integer) lhs;
  Integer ip2 = (Integer) rhs;

  if (*ip1 > *ip2) return 1;
  else if (*ip1 < *ip2) return -1;
    else return 0;
} /* end of int compare_ints(Object, Object) */

int main(int argc, char *argv[]) {
  int seq[] = {1, 3, 102, 6, 34, 12, 35}, i;
  Integer int_seq[7];

  for(i = 0; i< 7; i++) int_seq[i] = &seq[i];

  printf("\nTESTING FOR INTEGERS\n\nBefore Sorting\n");
  print_ints(int_seq, 7);

传递给 bubble_sort 函数的第三个参数是指向函数的指针。此指针包含一个可以解释为函数入口点的值。从概念上讲,这种指针与指向某些数据类型的指针之间没有太大区别。唯一的区别在于它们指向的内存区域。后者指向数据段中的某个地址,而前者指向代码段中的某个地址。但是有一点保持不变:地址值始终以[特定方式]解释。

请注意,当您将函数作为参数传递时,您不必应用地址运算符。

  bubble_sort((Object*) int_seq, 7, &compare_ints);
  printf("\nAfter Sorting\n");	print_ints(int_seq, 7);

  printf("\nTESTING FOR STRINGS\n\nBefore Sorting\n");
  print_args(&argv[1], argc - 1);
  bubble_sort((Object*) &argv[1], argc - 1, compare_strings);
  printf("After Sorting\n");	print_args(&argv[1], argc - 1);

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

链接到目标文件

[edit | edit source]
cl /c /ID:\include Bubble_Sort.c↵

/ID:\include 添加到要搜索头文件的目录列表的开头,这些目录最初包括源文件所在的目录以及在 %INCLUDE% 中找到的目录。类似于 Java 的 CLASSPATH,它用于组织头文件。使用此信息,预处理器将 D:\include\algorithms\sorting\Bubble_Sort.h 引入到当前文件(Bubble_Sort.c)中。一旦预处理器完成了它的工作,编译器将尝试编译结果文件并输出一个名为 Bubble_Sort.obj 的目标文件。

cl /FeTest_Sort.exe /ID:\include Bubble_Sort.obj Pointer2Function.c↵

上面的命令编译 Pointer2Function.c,如前一段所述。一旦创建了 Pointer2Function.obj,它将与 Bubble_Sort.obj 链接起来形成名为 Test_Sort.exe 的可执行文件。此链接是引入 bubble_sort 函数的目标代码所需的。请记住:包含 Bubble_Sort.h 引入了原型,而不是目标代码!

模块化编程

[edit | edit source]

问题:用 C 语言编写一个(伪)随机数生成器。保证单个应用程序只使用一个生成器,并且不同的应用程序会反复使用它。

现在我们的生成器将被不同的应用程序使用,我们最好把它放在一个单独的文件中,这样通过链接客户端程序,我们就可以从不同的来源使用它。这类似于(如果不是完全一样的话)Modula-2 等语言中支持的模块的概念。区别在于它们的抽象级别:模块是编程语言提供的实体,所有用户都知道它,而文件是操作系统提供的实体,所有用户都知道它。编程语言编译器(即编程语言规范的实现)作为操作系统提供服务的使用者和概念,这意味着模块概念是一个更高的抽象。

现在,由于 C 语言中没有模块概念(更高的抽象),我们需要使用其他可能更低级别的抽象来模拟它。在这种情况下,我们使用文件(更低的抽象)。这样做,我们无法完全恢复模块带来的所有好处。编译器不知道模块的概念;模块化编程语言中编译器强制执行的某些规则必须由程序员自己检查。例如,程序员必须同步模块的接口和实现,这是一个容易出错的过程。

由于预计所有应用程序都不会使用多个生成器,因此我们可以在静态数据区域创建与生成器相关的字段;为了识别函数作用于哪个生成器,我们不需要传递一个单独的、唯一的句柄。这意味着我们不需要任何创建或销毁函数。我们只需要一个初始化生成器的函数和另一个返回下一个(伪)随机数的函数。

RNGMod.h
#ifndef RNGMOD_H
#define RNGMOD_H

extern void RNGMod_InitializeSeed(long int);
extern double RNGMod_Next(void);

#endif


RNGMod.c
#include "math\RNGMod.h"

static long int seed = 314159;
const int a = 16807;
const long int m = 2147483647;

生成一个(伪)随机数类似于遍历一个值列表:我们基本上从某个点开始,一个接一个地遍历这些值。区别在于生成的这些值是通过一个函数计算出来的,而不是从某个内存位置检索出来的。换句话说,生成器使用时间计算遍历一个列表,而迭代器使用空间计算遍历一个列表。

从这个角度来看,用种子初始化一个(伪)随机数生成器类似于在列表上创建一个迭代器。通过使用不同的种子值,我们可以保证生成器返回不同的值。使用相同的种子值将给出相同的(伪)随机值序列。这种用法可能希望用于针对不同参数重放相同的模拟场景。

void RNGMod_InitializeSeed(long int s) {
  seed = s;

  return;
} /* end of void RNGMod_InitializeSeed(long int) */

以下函数计算序列中的下一个数字。使用一个众所周知的公式计算的数字意味着该序列实际上不是随机的。也就是说,它可以提前知道。这就是为什么这样的数字通常用“伪”这个词来限定。

使序列看起来随机的是它连续生成的不同的值的个数。以下函数中使用的公式将生成 1 到 m - 1 之间的所有值,然后重复序列。超过二十亿个值!

实际上,使用这些大数字没有意义。因此,我们选择 // 将值归一化到 [0..1) 中。

double RNGMod_Next(void) {
  long int gamma, q = m / a;
  int r = m % a;

  gamma = a * (seed % q) - r * (seed / q);
  if (gamma > 0) seed = gamma;
    else seed = gamma + m;

  return((double) seed / m);
} /* end of double RNGMod_Next(void) */


RNGMod_Test.c
#include <stdio.h>

#include "math\RNGMod.h"

int main(void) {
printf("TESTING RNGMod…\n");
printf("Before initialization: %g\n", RNGMod_Next());
RNGMod_InitializeSeed(35000);
printf("After initialization: %g\t", RNGMod_Next());
printf("%g\t", RNGMod_Next());
printf("%g\t", RNGMod_Next());
printf("%g\n", RNGMod_Next());

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

使用 make 构建程序

[edit | edit source]

随着编译/链接程序所需的文件数量增加,跟踪文件之间的相互依赖关系变得越来越困难。用于解决此类情况的一个工具是在 UNIX 环境中找到的 make 实用程序。该实用程序会自动确定大型程序的哪些部分需要重新编译,并发出命令重新编译它们。

make 的输入是一个文件,该文件包含告诉哪些文件依赖哪些文件的规则。这些规则通常采用以下形式

target : prerequisites
TAB command
TAB command
TAB ...

上述规则的解释如下:如果目标已过期,请使用以下命令将其更新。如果目标不存在或比任何先决条件旧(通过比较上次修改时间),则它已过期。目标是指要更新的文件,而先决条件是指用于生成目标文件的其他文件。

Makefile

以下规则告诉 make 实用程序 RNGMod_Test.exe 依赖于 RNGMod.o 和 RNGMod_Test.c。如果 RNGMod_Test.exe 不存在,或者比 RNGMod.o 和 RNGMod_Test.c 旧,则 RNGMod_Test.exe 已过期。如果这两个文件中的任何一个被修改,我们必须使用下一行中提供的命令来更新 RNGMod_Test.exe。

请注意,命令之前的制表符不是为了使文件更易读;用于更新目标的命令必须以制表符开头。

$@ 是一个特殊的变量,用于表示目标文件名。

RNGMod_Test.exe : RNGMod.o RNGMod_Test.c
  gcc -o $@ -ID:\include RNGMod.o RNGMod_Test.c

RNGMod.o : RNGMod.c D:\include\math\RNGMod.h
  gcc -c -ID:\include RNGMod.c

.PHONY 是一个预定义的目标,用于定义虚假目标。它确保 make 实用程序该目标实际上不是要更新的文件,而是一个入口点。在本例中,我们使用 clean 作为入口点,使我们能够删除当前目录中的所有相关目标文件。

.PHONY : clean
clean:
  del RNGMod.o RNGMod_Test.exe

保存此文件后,我们只需要发出 make 命令即可。此命令将在当前目录中查找名为 makefile 或 Makefile 的文件。如果使用 GNU 版本的 make,也会尝试 GNUmakefile。找到此类文件后,make 会尝试从顶部更新第一个规则的目标文件。

make↵
gcc –c –ID:\include RNGMod.c
gcc –o RNGMod_Test.exe –ID:\include RNGMod.o RNGMod_Test.c
Time: 2.263 seconds
RNGMod_Test↵
Testing RNGMod...
Before initialization: 0.458724
After initialization: 0.273923 0.822585 0.186277 0.754617

请注意,make 实用程序完成其任务所需的时间可能因处理器速度及其负载而异。如果可能只修改了 RNGMod_Test.c,我们将看到以下输出。

make↵
gcc –o RNGMod_Test.exe –ID:\include RNGMod.o RNGMod_Test.c
Time: 1.673 seconds

我们有时可能希望 make 实用程序从其他目标开始。在这种情况下,我们必须在命令行中提供目标的名称作为参数。例如,如果我们需要删除当前目录中找到的相关目标文件,则发出以下命令即可完成此操作。

make clean↵
del RNGMod.o RNGMod_Test.exe
Time: 0.171 seconds

以上介绍是对 make 实用程序的有限介绍。有关更多信息,请参阅 GNU make 实用程序的手册。

基于对象的编程(数据抽象)

[edit | edit source]

问题:用 C 语言编写一个(伪)随机数生成器。您的解决方案应该使单个应用程序能够同时使用多个生成器(对这个数字没有上限)。我们还应该满足生成器能够从不同应用程序中使用的要求。

如 [#Modular Programming模块化|编程部分] 中所述,可以通过在单独的文件中提供生成器来满足第二个要求。

为了能够使用多个生成器,其中确切的数字未知,我们必须设计一种方法来根据需要动态创建生成器(类似于 OOPL 中的构造函数所做的)。这种创建方案还必须给我们一些东西来唯一识别每个生成器(类似于构造函数返回的句柄)。我们还应该能够返回不再需要的生成器(类似于 OOPL 中没有自动垃圾收集的析构函数所做的)。

RNGDA.h
#ifndef RNGDA_H
#define RNGDA_H

如要求中所述,我们必须提出一个方案,让用户在同一个程序中拥有多个共存的生成器。我们应该能够以某种方式识别这些生成器的每一个,并将它与其他生成器区分开来。这意味着我们不能使用我们在前面的示例中使用的相同方法。我们必须让相关函数根据特定生成器的状态表现出不同的行为。这种行为差异可以通过传递一个额外的参数来实现,即对生成器当前状态的句柄。此句柄应向用户隐藏生成器的实现细节;它应该具有不可变的特性,我们可以利用这些特性来隐藏生成器的可变属性。听起来像是 Java 中句柄的概念?没错!不幸的是,C 语言不支持句柄。我们需要其他可能不太抽象的概念来模拟它。我们将使用的这个不太抽象的概念是指针的概念:无论数据的大小是多少,它所指的对象的大小都不会改变。

在以下几行中,我们首先对一个名为 struct_RNG 进行前向声明,然后定义一个新类型,作为指向此未定义 struct 的指针。通过使用前向声明,我们不会泄露任何实现细节;我们只是让编译器知道我们打算使用这种类型。通过定义指向此类型的指针,我们在用户和实现者之间设置了一道屏障:用户拥有某种东西(指针,其大小不会改变)来间接访问生成器(底层对象,其大小可以通过实现决策来改变)。改变的自由意味着可以使用生成器而不必参考底层对象,这就是为什么我们将这种方法称为数据抽象,而以这种方式定义的任何类型都是抽象数据类型。请注意传递给函数的指针参数。函数的行为会根据底层对象的改变而改变,底层对象是通过该指针间接访问的。这就是为什么这种编程风格被称为基于对象的编程。

struct _RNG;
typedef struct _RNG* RNG;

extern RNG RNGDA_Create(long int);
extern void RNGDA_Destroy(RNG);
extern double RNGDA_Next(RNG);

#endif


RNGDA.c
#include <stdio.h>
#include <stdlib.h>

#include "math\RNGDA.h"

struct _RNG { long int seed; };

const int a = 16807;
const long int m = 2147483647;

RNG RNGDA_Create(long int s) {
  RNG newRNG = (RNG) malloc(sizeof(struct _RNG));
  if (!newRNG) {
    fprintf(stderr, "Out of memory...\n");
    return(NULL);
  }
  newRNG->seed = s;

  return(newRNG);
} /* end of RNG RNGDA_Create(long int) */

void RNGDA_Destroy(RNG rng) { free(rng); }

double RNGDA_Next(RNG rng) {
  long int gamma;
  long int q = m / a;
  int r = m % a;

  gamma = a * (rng->seed % q) - r * (rng->seed / q);
  if (gamma > 0) rng->seed = gamma;
    else rng->seed = gamma + m;

  return((double) rng->seed / m);
} /* end of double RNGDA_Next(RNG) */


RNGDA_Test.c
#include <stdio.h>
#include "math\RNGDA.h"

int main(void) {
  int i;
  RNG rng[3]; 

  printf("TESTING RNGDA\n");
  rng[0] = RNGDA_Create(1245L);
  rng[1] = RNGDA_Create(1245L);
  rng[2] = RNGDA_Create(2345L);
  for (i = 0; i < 5; i++) {
    printf("1st RNG, %d number: %f\n", i, RNGDA_Next(rng[0]));
    printf("2nd RNG, %d number: %f\n", i, RNGDA_Next(rng[1]));
    printf("3rd RNG, %d number: %f\n", i, RNGDA_Next(rng[2]));
  } /* end of for (i = 0; i < 5; i++)*/
  RNGDA_Destroy(rng[0]);
  RNGDA_Destroy(rng[1]);
  RNGDA_Destroy(rng[2]);

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

说明

[edit | edit source]
  1. ISO C 允许在同一源代码行上的 # 字符之前和之后有空格,但旧的编译器不支持。
  2. 正如我们稍后将看到,void * 是对此的例外。
  3. 在 Java 中,有两个右移运算符:>>>>>:前者符号扩展其操作数,而后者通过零扩展来执行其工作。
  4. 这并不意味着这些阶段不能同时进行。它指的是阶段必须按照一定的顺序开始。例如,一旦初步设计完成,实施者就可以开始实施,但不能在此之前。但这两个团队必须互相联系并提供反馈。当设计师进行更改时,这些更改会传达给实施团队;当实施中出现问题时,这些问题会反馈给设计团队。请注意,软件生产周期中的其他团队之间可能也存在类似的关系。
华夏公益教科书