巨同升
(山东理工大学 山东淄博 255049)
“C语言程序设计”这门课程在国内高校普遍开设已有近三十年,课程的建设和研究取得了长足的进步,涌现出了数量众多、各具特色的C语言教材。尽管如此,在许多C语言教材中还或多或少地存在着一些不准确甚至是值得商榷的说法。下面将对国内教材中常见的几种说法进行辨析,并期望与广大同行商榷。
有的教材中说:“C语言中没有输入输出语句,只有输入输出函数。”果真如此吗?
完整的C语言是由语言标准和标准库两部分组成的[1]。语言标准相当于C语言的内核,标准库(主体是库函数)相当于C语言的扩展部分。这种设计极大地增强了C语言实现的灵活性和程序的可移植性,因为可以根据某类计算机的硬件特点而单独修改C语言标准库部分的实现。
在C语言的语言标准部分的确没有输入输出语句,也没有输入输出函数。不过在C语言的标准库部分,明确地定义了各种输入输出函数,从而可以在C语言程序中以语句的形式来调用这些输入输出函数。
既然这种函数调用语句可以实现输入输出的功能,将它们称为“输入输出语句”(而不必称为输入输出函数调用语句)也是顺理成章的。
因此正确的说法是:在C语言的语言标准部分没有输入输出语句,但在C语言的标准库部分有输入输出函数,从而在完整的C语言中有输入输出语句。
有的教材中说:“在C语言中,八进制和十六进制整数只有正数,没有负数。”这种说法是有问题的。实际上,在C语言中,八进制和十六进制整数既可以使用正数,也可以使用负数。下面的程序就是一个很好的证据。
#include <stdio.h>
int main(void)
{int a,b,c,d;
a=0127;
b=-0127;
c=0x1af;
d=-0x1af;
printf("a=%d,b=%d,c=%d,d=%d ",a,b,c,d);
printf("a=%o,b=%o,c=%x,d=%x ",a,b,c,d);
return 0;
}
该程序的运行结果如下图所示。
从该程序的运行结果可以发现,程序中可以使用八进制和十六进制的负整数,但是不能以负数形式输出八进制和十六进制的整数。
因此正确的说法是:在C语言中,以八进制和十六进制形式输出整数时,只能输出正数和0,不能输出负数。
有的教材中说:“在C语言中,整数是以二进制补码的形式在内存中存储的。”其实,这种说法是不准确的。在C语言中,有符号整数的确是以二进制补码形式在内存中存储的,其最高位为符号位,用以表示该整数的正负。但是对于无符号整数来说,由于不存在负数,并不需要表示正负的符号位,因而它的每一位都是数值位,这种表示形式应称为“无符号二进制形式”。
因此正确的说法是:在C语言中,有符号整数是以二进制补码形式在内存中存储的,无符号整数是以无符号二进制形式在内存中存储的。
国内绝大多数的C语言教材都说:“在C语言中,后自增(减)运算符的优先级与前自增(减)运算符相同。”其实,这种说法是有问题的。
在传统C语言(即C89标准之前的C语言)规范中,后自增(减)的优先级的确是与前自增(减)相同的,都是2级。但是,从C89标准开始,已经将后自增(减)的优先级调整为1级,而前自增(减)的优先级依然为2级,从而使得后自增(减)的优先级高于前自增(减)[1-2]。遗憾的是,这种调整并未在国内的绝大多数C语言教材中反映出来。
为什么scanf函数中的第二个参数只能是变量的地址,而不能是变量名本身呢?大多数教材中并未给出解释,而有的教材则认为是为了提高编程的灵活性,使得scanf函数中的第二个参数既可以是若干个变量的地址,也可以是指向某些内存单元的指针。其实,真正的原因在于C语言中函数参数的单向传递规则,即只能将实参的值传递给对应的形参,而不能将形参的值传递给对应的实参[3]。
在程序中调用scanf函数完成数据的输入,是通过执行该函数的函数体语句实现的。不过在执行该函数的函数体时所输入的数据,首先存放于函数体中定义的局部变量中。调用scanf函数时,若直接以待存储数据的变量名作为实参,则函数体中局部变量(包括形参)的值并不能直接传递给实参;若改用待存储数据的变量的地址作为实参,就可以通过跨函数间接引用的方式,将函数体中局部变量的值传递给主调函数中的变量了。
在大多数教材中,在调用malloc函数时,总是要对其返回值进行强制类型转换。例如:
int*p;
p=(int*)malloc(sizeof(int));
这些教材中认为这种强制类型转换是必不可少的,其实并非如此。在传统C语言(即C89标准之前的C语言)规范中,malloc函数的返回值为char*类型,由于这种类型与其他的指针类型都不是赋值兼容的,因此不能直接进行赋值运算,而必须先进行强制类型转换,再进行赋值运算[2]。
不过,从C89标准开始,已经将malloc函数的返回值类型改为void*类型。void*是C89标准中定义的通用指针类型,通用指针与所有其他类型的指针都是赋值兼容的,即可以不经过强制类型转换而直接相互赋值[2]。例如:
int*p;
p=malloc(sizeof(int));
此外,calloc函数和realloc函数的用法与malloc函数是类似的。
C语言中的各类文件打开方式均包括两种,如读方式包括“r”和“rb”,写方式包括“w”和“wb”。在大多数C语言教材中,将文件的这两种打开方式称为“打开文本文件”与“打开二进制文件”。
这种叫法很容易使人误解为用第一种打开方式创建的就是文本文件,用第二种打开方式创建的就是二进制文件。其实一个新创建的文件是文本文件还是二进制文件的决定因素,是向文件中写入数据的函数,而不是文件的打开方式[3]。一般而言,使用fprintf、fputc和fputs等函数创建的文件,是文本文件;而使用fwrite函数创建的文件,则是二进制文件。相应地,fscanf、fgetc和fgets等函数用于读取文本文件;而fread函数则用于读取二进制文件。
既然如此,为什么还要将文件的打开方式区分为这两种方式呢?其实,这源于两类操作系统对于回车换行的不同处理方式。在第一类操作系统(如UNIX和Linux)的文本编辑软件中,采用与C语言相同的处理方式,只需用一个换行符,即可实现回车换行。而在第二类操作系统(如DOS和Windows)的文本编辑软件中,则采用与C语言不同的处理方式,需要用一个回车符和一个换行符的组合,方可实现回车换行。
因此,在第二类操作系统中,当用第一种方式打开文件并对文件进行写入操作时,将会把每一个换行符替换为一个回车符和一个换行符的组合;当用第一种方式打开文件并对文件进行读出操作时,将会进行相反的替换。然而,当用第二种方式打开文件并对文件进行写入和读出操作时,将不会对字符进行任何替换。
而在第一类操作系统中,第一种打开方式与第二种打开方式是没有区别的。不论用哪种方式打开文件,在对文件进行写入和读出操作时,都不会对字符进行任何替换。可见,这两种打开方式均是既可以打开文本文件,也可以打开二进制文件,区别在于对回车换行的处理方式不同。因此,正确叫法应该是“文本打开方式”与“二进制打开方式”[3]。