C语言编程:库变更、可移植性问题与冷门特性解析
1. 库变更
C语言的库如同语言本身一样,也在不断发展。曾经,“标准”C库是随UNIX操作系统附带的。后来,UNIX操作系统分裂为BSD - UNIX和System V UNIX两个家族,标准库也随之分裂。
当ANSI对C语言进行标准化时,也对库进行了标准化。不过,现在仍能看到使用旧库调用的代码。主要区别如下:
- 旧的K&R C没有
stdlib.h
或
unistd.h
头文件。
- 许多旧函数被重命名或替换。以下是更新后的函数列表:
| K & R函数 | ANSI等效函数 | 说明 |
|---|---|---|
| bcopy | memcpy | 复制数组或结构体 |
| bzero | memset | 将内存置零 |
| bcmp | memcmp | 比较两段内存 |
| index | strchr | 在字符串中查找字符 |
| rindex | strrchr | 从字符串末尾开始查找字符 |
| char *sprintf | int sprintf | K&R函数返回字符串指针,ANSI标准函数返回转换的项数 |
此外,早期的C编译器可能没有一些最新特性,这些特性包括:
-
void
类型
-
const
限定符
-
volatile
限定符
-
stdlib.h
头文件或
unistd.h
头文件
-
enum
类型
在内存分配和释放方面,ANSI C和K&R C也存在差异。在ANSI C中,
malloc
函数定义为:
void *malloc(unsigned long int size);
由于
void *
表示“通用指针”,
malloc
的返回值可以匹配任何指针类型,例如:
struct person *person_ptr;
person_ptr = malloc(sizeof(struct person));
而在一些K&R C编译器中,由于没有
void
类型,
malloc
定义为:
char *malloc(unsigned long int size)
为避免编译器对不同指针类型的警告,需要对
malloc
的输出进行类型转换,例如:
struct person *person_ptr;
person_ptr = (struct person *)malloc(sizeof(struct person));
free
函数也有类似问题。ANSI C中定义为:
int free(void *);
K&R中定义为:
int free(char *);
所以需要将参数转换为字符指针以避免警告。
早期的C编译器缺乏现在常见的错误检查,这使得编程变得困难。为解决这个问题,编写了一个名为
lint
的程序。它可以检查常见错误,如使用错误参数调用函数、函数声明不一致、在变量初始化前使用等。运行
lint
的命令如下:
% lint -hpx prog.c
其中,
-h
选项开启一些启发式检查,
-p
选项检查可能的可移植性问题,
-x
选项检查声明为
extern
但从未使用的变量。需要注意的是,在System V UNIX系统上,
-h
选项的功能相反,应省略该选项。
下面来看一些代码问题及解答:
-
问题19 - 1
:
area
函数的参数类型为一个整数和一个浮点数,但调用时参数类型颠倒,由于使用K&R风格的C语言,C无法识别这种颠倒,导致参数传递混乱,输出结果错误。
-
问题19 - 2
:代码没有
square
函数的原型,C默认该函数返回整数且接受任意数量的参数,但实际返回浮点数,导致结果错误。可以在程序开头添加K&R风格的原型
float square();
来解决,更好的方法是将程序转换为ANSI C,添加真正的原型并修正函数头。
-
问题19 - 3
:函数头的参数类型声明问题导致局部变量隐藏了参数,对未初始化的局部变量求和,返回随机结果。虽然ANSI已禁止这种情况,但许多编译器仍接受此类代码。
-
问题19 - 4
:
strcat
函数需要两个字符串作为参数,但代码中使用了字符,这是非法的。旧风格的C编译器不进行参数类型检查,应将字符替换为字符串。
-
问题19 - 5
:代码中调用
area
函数时只传递了一个参数,C不检查参数数量,会为第二个参数生成随机值,导致结果错误,应正确传递两个参数。
2. 可移植性问题
C程序理论上是可移植的,但C语言包含许多依赖机器的特性,而且UNIX和MS - DOS/Windows之间存在巨大差异,这常常导致程序出现可移植性问题。
2.1 模块化
编写可移植程序的一个技巧是将所有不可移植的代码放在一个单独的模块中。例如,MS - DOS/Windows和UNIX的屏幕处理差异很大,需要编写特定于机器的模块来更新屏幕。以HP - 98752A终端和PC终端为例,它们的功能键发送的代码不同,需要编写
get_code
例程来获取字符或功能键字符串并进行转换。对于HP机器,程序由
main.c
和
hp - tty.c
组成;对于PC,使用
main.c
和
pc - tty.c
。
2.2 字长问题
long int
是32位,
short int
是16位,普通
int
可能是16位或32位,这取决于机器。例如,以下代码在32位UNIX系统上可以正常工作,但移植到MS - DOS/Windows时会失败:
int zip;
zip = 92126;
printf("Zip code %d\n", zip);
因为在MS - DOS/Windows上,
zip
只有16位,无法容纳92126。可以将
zip
声明为32位整数来解决:
long int zip;
zip = 92126;
printf("Zip code %d\n", zip);
然而,这样在PC上仍然可能存在问题,因为
printf
的
%d
用于普通
int
,而不是
long int
,正确的格式说明符应该是
%ld
。
2.3 字节顺序问题
一个
short int
由2个字节组成,对于数字
0x1234
,两个字节的值分别为
0x12
和
0x34
,哪个值存储在第一个字节取决于机器。这在编写可移植的二进制文件时会造成麻烦,不同机器使用不同的字节顺序,如Motorola 68000系列机器使用一种字节顺序(ABCD),而Intel和Digital Equipment Corporation机器使用另一种(BADC)。
解决可移植二进制文件问题的一种方法是避免使用它们,在程序中添加读写ASCII文件的选项。ASCII文件更具可移植性且人类可读,但缺点是文件较大。如果文件太大不适合使用ASCII,可以使用文件开头的魔术数字。例如,假设魔术数字是
0x11223344
,程序读取魔术数字后,可以与正确的数字和字节交换后的版本(
0x22114433
)进行比较,自动修复文件问题:
const long int MAGIC = 0x11223344L;
const long int SWAP_MAGIC = 0x22114433L;
FILE *in_file;
long int magic;
in_file = fopen("data", "rb");
fread((char *)&magic, sizeof(magic), 1, in_file);
switch (magic) {
case MAGIC:
break;
case SWAP_MAGIC:
printf("Converting file, please wait\n");
convert_file(in_file);
break;
default:
fprintf(stderr,"Error:Bad magic number %lx\n", magic);
exit (8);
}
2.4 对齐问题
一些计算机对整数和其他类型数据的地址使用有限制。例如,68000系列要求所有整数从2字节边界开始,如果使用奇数地址访问整数会产生错误。不同处理器的对齐规则不同,有些没有对齐规则,有些则更严格,要求整数在4字节边界对齐。
对齐限制不仅适用于整数,浮点数和指针也需要正确对齐。C语言会隐藏这些对齐限制,但结构的大小会因机器而异。例如,在68000系列上声明的结构体:
struct funny {
char flag;
long int value;
};
在68000上结构体大小是6字节,在8086上是5字节。如果在68000上编写包含100条记录的二进制文件,文件长度为600字节,而在8086上只有500字节。
解决这个问题的一种方法是使用ASCII文件,另一种方法是显式声明填充字节:
struct new_funny {
char flag;
char pad;
long int value;
};
填充字符可以使
value
字段在68000机器上正确对齐,同时使结构体在8086类机器上具有正确的大小。但使用填充字符困难且容易出错,例如
new_funny
在1字节和2字节对齐的机器上可移植,但在4字节整数对齐的机器(如Sun SPARC系统)上不可移植。
2.5 NULL指针问题
许多在VAX计算机上使用UNIX编写的程序和实用工具存在一个问题,即使用空指针作为字符串。例如:
#ifndef NULL
#define NULL ((char *)0)
#endif NULL
char *string;
string = NULL;
printf("String is '%s'\n", string);
这实际上是对字符串的非法使用,空指针不应被解引用。在VAX上,由于程序的第一个字节是0,
string
指向空字符串,但这是巧合而非设计。在其他计算机上,这种代码可能会产生意外结果。许多新编译器会检查
NULL
并输出
String is (null)
,这并不意味着打印
NULL
不是错误,而是编译器为程序员提供了一个安全网。
2.6 文件名问题
UNIX使用
/root/sub/file
指定文件,而MS - DOS/Windows使用
\root\sub\file
。在从UNIX移植到MS - DOS/Windows时,需要更改文件名。例如:
#ifndef __MSDOS__
#include <sys/stat.h>
#else __MSDOS__
#include <sys\stat.h>
#endif __MSDOS__
但如果代码中使用了类似
const char NAME[] = "\root\new\table";
的语句,在MS - DOS/Windows上会出现问题,因为C语言中反斜杠
\
是转义字符,应使用双反斜杠
\\
,即
const char NAME[] = "\\root\\new\\table";
。
2.7 文件类型问题
在UNIX中只有一种文件类型,而在MS - DOS/Windows中有文本和二进制两种类型。MS - DOS/Windows使用
O_BINARY
和
O_TEXT
标志来指示文件类型,这些标志在UNIX中未定义。从MS - DOS/Windows移植到UNIX时,可以使用预处理器定义这些标志:
#ifndef O_BINARY
#define O_BINARY 0
#define O_TEXT 0
#endif
这种方法允许在UNIX和MS - DOS/Windows上使用相同的
open
函数,但从UNIX移植到MS - DOS/Windows时,可能需要添加额外的标志。
3. C语言的冷门特性
C语言还有一些很少在实际编程中使用的特性,例如
do/while
语句。其语法如下:
do {
statement
statement
} while (expression);
程序会先执行循环体,然后测试表达式,如果表达式为假(0)则停止。需要注意的是,这种结构至少会执行一次。在C语言中,
do/while
语句不常使用,大多数程序员更喜欢使用
while/break
组合。
综上所述,虽然可以编写可移植的C程序,但由于C语言运行在多种不同类型的机器和操作系统上,实现程序的可移植性并不容易。在编写代码时,如果时刻考虑可移植性,可以将问题降到最低。
C语言编程:库变更、可移植性问题与冷门特性解析
4. 库变更与可移植性问题的关联及应对策略
库变更和可移植性问题并非孤立存在,它们之间相互影响。例如,库函数的变更可能会导致在不同系统上代码的表现不同,从而引发可移植性问题。下面我们通过一个流程图来展示处理这些问题的一般策略:
graph LR
A[编写代码] --> B{是否使用旧库函数}
B -- 是 --> C{是否需要移植}
B -- 否 --> D[检查可移植性特性]
C -- 是 --> E[更新库函数调用]
C -- 否 --> D
E --> F[处理可移植性问题]
D --> F
F --> G[测试代码]
G -- 通过 --> H[发布程序]
G -- 未通过 --> A
从这个流程图可以看出,在编写代码时,首先要判断是否使用了旧库函数。如果使用了旧库函数,并且代码需要移植到其他系统,那么就需要更新库函数的调用。无论是否使用旧库函数,都要检查代码中的可移植性特性,如字长、字节顺序、对齐等问题。处理完这些问题后,对代码进行测试,若测试通过则可以发布程序,若未通过则需要重新编写代码。
5. 实际案例分析
为了更好地理解上述问题,我们来看一个实际案例。假设我们要编写一个程序,该程序需要在UNIX和MS - DOS/Windows系统上都能正常运行,并且要处理文件操作和内存分配。
#include <stdio.h>
#include <stdlib.h>
#ifdef __MSDOS__
#define O_BINARY 0
#define O_TEXT 0
#endif
// 定义一个结构体
struct data {
int id;
float value;
};
// 函数用于读取文件
void read_file(const char *filename) {
FILE *in_file;
struct data *data_ptr;
// 打开文件
in_file = fopen(filename, "rb");
if (in_file == NULL) {
fprintf(stderr, "%s: file not found\n", filename);
return;
}
// 分配内存
data_ptr = (struct data *)malloc(sizeof(struct data));
if (data_ptr == NULL) {
fprintf(stderr, "Memory allocation failed\n");
fclose(in_file);
return;
}
// 读取数据
fread(data_ptr, sizeof(struct data), 1, in_file);
// 输出数据
printf("ID: %d, Value: %f\n", data_ptr->id, data_ptr->value);
// 释放内存
free(data_ptr);
fclose(in_file);
}
int main() {
#ifdef __MSDOS__
const char filename[] = "\\root\\new\\data.dat";
#else
const char filename[] = "/root/new/data.dat";
#endif
read_file(filename);
return 0;
}
在这个案例中,我们考虑了以下几个方面的问题:
-
文件类型
:通过预处理器定义了
O_BINARY
和
O_TEXT
,以确保在不同系统上文件操作的一致性。
-
文件名
:根据不同的系统使用不同的文件名格式,避免了因反斜杠转义问题导致的错误。
-
内存分配
:使用了
malloc
和
free
函数,并且进行了类型转换,以适应不同编译器对指针类型的要求。
6. 总结与建议
通过对C语言库变更、可移植性问题和冷门特性的分析,我们可以总结出以下几点建议:
-
关注库函数的更新
:随着C语言标准的发展,库函数也在不断更新。在编写代码时,尽量使用新的库函数,避免使用旧的、可能会导致兼容性问题的函数。
-
考虑可移植性
:在设计和编写代码时,要时刻考虑代码的可移植性。将不可移植的代码封装在单独的模块中,使用条件编译来处理不同系统之间的差异。
-
进行充分测试
:在将代码移植到不同系统之前,要进行充分的测试。测试可以帮助发现潜在的问题,如字长、字节顺序、对齐等问题。
-
学习冷门特性
:虽然
do/while
等冷门特性在实际编程中使用较少,但了解这些特性可以让我们对C语言有更全面的认识,在某些特定场景下可能会发挥作用。
总之,C语言是一门强大而灵活的编程语言,但要编写可移植的代码需要我们付出更多的努力。通过遵循上述建议,我们可以减少代码中的问题,提高代码的质量和可维护性。
7. 常见问题解答
为了帮助大家更好地理解和解决在C语言编程中遇到的问题,下面列出一些常见问题及解答:
| 问题 | 解答 |
|---|---|
| 为什么使用旧库函数会导致可移植性问题? | 旧库函数可能在不同的系统上有不同的实现,或者已经被新的库函数所替代。使用旧库函数可能会导致代码在某些系统上无法正常运行。 |
| 如何避免字节顺序问题? | 可以避免使用二进制文件,使用ASCII文件进行数据存储。如果必须使用二进制文件,可以使用魔术数字来检测和修复字节顺序问题。 |
| 对齐问题会对程序产生什么影响? | 对齐问题可能会导致程序在某些系统上崩溃或产生错误的结果。例如,在不满足对齐要求的系统上访问数据可能会引发硬件错误。 |
do/while
和
while/break
有什么区别?
|
do/while
结构至少会执行一次循环体,而
while/break
结构在条件不满足时可能一次都不执行。大多数程序员更喜欢使用
while/break
组合,因为它更灵活。
|
通过这些常见问题的解答,希望能帮助大家更好地理解C语言编程中的各种问题,提高编程能力。
超级会员免费看

被折叠的 条评论
为什么被折叠?



