购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

2.4 使用C语言探究数据格式

在本节中,我们将用C语言编写第一个程序。这些程序演示了数字在内存中的存储方式和人类阅读数字的方式之间的差异。C语言允许我们充分接近硬件来理解核心概念,同时又照顾到了许多底层细节。本书中用到的这些简单的C程序应该不会让你觉得太难,特别是如果你已经有其他编程语言经验。

如果你学习过高级语言编程,比如C++、Java或Python,那么你有可能也懂得面向对象编程。C语言不支持面向对象的范式,它是一种过程式编程语言。C程序被划分为多个函数。函数是一组具名的编程语句。其他编程语言对此也使用术语“过程”(procedure)和“子程序”(subprogram),根据语言的不同,它们之间有一些细微的区别。

2.4.1 C和C++ I/O库

大多数高级编程语言都包含标准库,可以将其视为该语言的一部分。标准库包含各种函数和数据结构,可在语言中用于处理一些常见任务,比如终端I/O——向屏幕写入数据以及从键盘读取数据。C语言提供了C标准库,C++语言提供了C++标准库。

对于终端I/O,C程序员使用stdio库中的函数,C++程序员使用iostream库中的函数。例如,以下C代码从键盘读取一个整数,与100相加,然后将结果写入屏幕:

int x;
scanf("%i", &x);
x += 100;
printf("%i", x);

对应的C++代码如下所示:

int x;
cin >> x;
x +=100;
cout << x;

在这两个例子中,代码从键盘读取字符,每个字符都作为char类型,接着将char类型序列转换为对应的int类型。然后将其与100相加。最后再将int类型的结果转换为char类型序列显示在屏幕上。上述代码中的C或C++ I/O库函数负责执行char类型序列和int类型之间的必要转换。

图2-1展示了C应用程序、I/O库和操作系统之间的关系。

图2-1 C应用程序、I/O库和操作系统
之间的关系

从键盘读取时,scanf库函数首先调用read系统函数(操作系统提供的一个函数)。键盘输入是字符串形式,每个字符都是char类型。scanf库函数为应用程序将该字符串转换为int类型。printf库函数将int类型转换为相应的char类型字符串,并调用write系统函数将每个字符写入屏幕。

如图2-1所示,应用程序可以直接调用read和write函数来读写字符。这个话题留待第16章讨论,届时我们将编写自己的转换函数。尽管C/C++的效果更好,但自己动手实践能让你更好地理解数据是如何在内存中存储以及如何被软件处理的。

注意

如果你尚不熟悉GNU make程序,强烈建议你学习如何使用make来构建程序。目前看来似乎有些大材小用,但使用简单的程序会更容易上手。make提供了多种格式的使用手册(见GNU make官方文档网站),我也在自己的网站上发表了一些使用意见。

2.4.2 编写并执行第一个C程序

大多数编程书都是以一个在屏幕上打印出“Hello, world”的简单程序开篇的,但是我们另辟蹊径,选择从一个读取十六进制值(作为无符号整数以及字符串)的程序开始。参见清单2-1。

清单2-1 展示整数与字符串之间差异的程序
➊ /* intAndString.c
    * Read and display an integer and a text string.
    */
 
➋ #include <stdio.h>
 
➌ int main(void)
   {
   ➍unsigned int anInt;
     char aString[10];
 
   ➎printf("Enter a number in hexadecimal: ");
   ➏scanf("%x", &anInt);
     printf("Enter it again: ");
   ➐scanf("%s", aString);
   ➑printf("The integer is %u and the string is %s\n", anInt, aString);
  
   ➒return 0;
   }

代码最开始是一段文档,给出了文件名➊和程序用途的简要描述。当你自己编写源代码时,也应该把你的名字和编写日期作为文档的一部分(为了节省纸张,我在本书的示例程序中将其省略了)。/*和*/之间的所有内容都是注释。注释仅服务于人类读者,对程序本身没有任何影响。

实际影响程序的第一个操作是包含另一个文件➋,即stdio.h头文件。如你所知,C编译器需要知道传入或传出函数的每项数据的类型。头文件用于提供每个函数的原型,后者指定了编译器所需的数据类型。stdio.h头文件定义了C标准库中众多函数的接口,编译器由此得知在源代码中遇到这些函数调用时应该如何处理。stdio.h头文件已经安装在编译器知晓的位置。

你接下来看到的是main函数的定义➌。所有的C程序都是由各种函数组成的,函数的一般格式如下:

return_data_type function_name(parameter_list)
{
  function_body
}

当执行C程序时,操作系统会先设置C运行时环境(C runtime environment),后者负责安排好运行程序所需的计算机资源。然后C运行时环境调用main函数,这意味着你编写的程序必须包含一个 function_name 为main的函数。main函数则会调用其他函数,其他函数再调用别的函数。不过程序的控制权最后还是会返回main函数,然后再返回C运行时环境。

调用C函数时,调用函数(calling function)可以在调用时提供一系列实际参数(argument,简称“实参”)作为被调用函数(called function)的输入。这些输入对应于被调用函数所执行操作中用到的形式参数(parameter,简称“形参”) 。例如,在清单2-1中,当程序首次启动时,main函数使用一个字符串实参调用printf函数➎。printf函数使用字符串决定在屏幕上显示什么内容。我们将在第14章详细描述实参如何被传给函数以及如何作为函数中的形参使用。清单2-1中的main函数不需要从C运行时环境获取任何数据,这一点可以从其定义中使用void作为 parameter_list 看出。

函数结束后通常会返回调用函数。被调用函数在返回时可以将数据项传给调用函数。main函数应该向C运行时环境返回一个整数,指明程序在运行过程中是否出现错误。因此,main的 return _ data _ type 是int。清单2-1中的main函数向C运行时环境返回整数0➒,后者将该值传给操作系统。值0告诉操作系统一切顺利。

在清单2-1中,我们在main函数的函数体开头定义了两个变量➍,一个是无符号整型变量anInt,另一个是字符串aString。大多数现代编程语言都允许我们在代码的任意位置定义新变量,不过C要求在函数开头处定义它们(该规则存在一些例外情况,不过这超出了本书的范围)。你可以把它想象成在给出操作说明之前要先列出烹饪配方的配料。我们通过指定名称和数据类型来定义变量。语法[10]告诉编译器为aString变量分配能容纳10个char类型的存储空间,这允许我们存储一个长度为9个字符的C风格字符串(第10个字符将是终止字符NUL)。我们将在第17章详细讨论数组。

该程序使用C标准库函数printf在屏幕上显示文本。printf的第一个参数是格式字符串,它由要在屏幕上显示的普通字符(除了%)组成。

最简单的格式字符串只包含要打印的文本➎,不包含变量。如果你想打印变量,可以将格式字符串作为文本模板。需要在变量值出现的位置使用转换说明符(conversion specifier)标记。转换说明符以%字符起始➑。变量名在格式字符串之后列出,其顺序与它们各自的转换说明符在模板中出现的顺序一致。

转换说明符开头的%字符之后紧跟一个或多个转换代码,告诉printf该如何显示变量值。表2-9列出了一些常见的转换说明符。

表2-9 printf和scanf格式字符串常用的转换说明符

转换说明符可以包括属性字符,如字段显示宽度、值在字段中是左对齐还是右对齐等。这里我们就不赘述了。你可以参阅printf的手册页以了解更多信息(在shell中输入man 3 printf)。

C标准库函数scanf的第一个参数也是格式字符串。我们在其中使用同样的转换说明符指示scanf函数该如何解释从键盘上输入的字符➏。我们通过对变量使用取值操作符(&anInt),告诉scanf将输入的整数存储在哪里。将数组名传入函数时,C会直接使用该数组的地址,所以在调用scanf从键盘读取文本时不需要使用&操作符➐。

除了这些转换说明符,scanf格式字符串中出现的其他字符必须与键盘输入严格匹配。例如:

scanf("1 %i and 2 %i", &oneInt, &twoInt);

要求输入为:

1 123 and 2 456

scanf会从键盘读取整数123和456。你可以参阅scanf的手册页以了解更多信息(在shell中输入man 3 scanf)。

最后,main函数向C运行时环境返回0➒,后者将该值传给操作系统。值0告诉操作系统一切正常。

在我的计算机上编译并运行清单2-1的程序会产生以下输出:

$ gcc -Wall -masm=intel -o intAndString intAndString.c
$ ./intAndString
Enter a hexadecimal value: 123abc
Enter it again: 123abc
The integer is 1194684 and the string is 123abc
$

清单2-1中的程序展示了一个重要的概念:十六进制被用作一种便于人类表达位模式的手段。数字本身不分二进制、十进制或十六进制。数字只是一个值而已。任何值都能以这3种数制(基数为2、10、16)等价表示。因为计算机由双态开关组成,故以2为基数来考虑存储在计算机中的数值是有意义的。

动手实践

1.使用C语言编写一个十六进制-十进制转换器程序。该程序允许用户输入一个十六进制数,然后打印出与其等价的十进制数。输出格式类似于这样:0x7b = 123。

2.使用C语言编写一个十进制-十六进制转换器程序。该程序允许用户输入一个十进制数,然后打印出与其等价的十六进制数。输出格式类似于这样:123 = 0x7b。

3.将清单2-1中程序的最后一个printf语句内的%u改为%i。如果输入ffffffff,该程序会打印什么? yg6NsPpw70ujR3QOby8CYWrv3SiyT0fgtRhUy+lPnfGrgfdhUV9N07MEjR3nk4dn

点击中间区域
呼出菜单
上一章
目录
下一章
×