C和指针

978-7-115-52268-9
作者: [美]肯尼斯·里科(Kenneth Reek)
译者: 徐波
编辑: 傅道坤郭泳泽
分类: C语言

图书目录:

详情

《C和指针》提供与C语言编程相关的全面资源和深入讨论。本书通过对指针的基础知识和高级特性的探讨,帮助程序员把指针的强大功能融入到自己的程序中去。 全书共18章,覆盖了数据、语句、操作符和表达式、指针、函数、数组、字符串、结构和联合等几乎所有重要的C编程话题。书中给出了很多编程技巧和提示,每章后面有针对性很强的练习,附录部分则给出了部分练习的解答。 《C和指针》适合C语言初学者和初级C程序员阅读,也可作为计算机专业学生学习C语言的参考。

图书摘要

版权信息

书名:C和指针

ISBN:978-7-115-52268-9

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。


著    [美]肯尼斯·里科(Kenneth Reek)

译    徐 波

责任编辑 傅道坤

人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

读者服务热线:(010)81055410

反盗版热线:(010)81055315


Authorized translation from the English language edition, entitled Pointers on C, 9780673999863 by Kenneth Reek, published by Pearson Education, Inc, publishing as Addison Wesley Professional, Copyright © 1997 Pearson Education, Inc.

All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc.

CHINESE SIMPLIFIED language edition published by PEARSON EDUCATION ASIA LTD., and POSTS & TELECOMMUNICATIONS PRESS Copyright © 2020.

本书封面贴有Pearson Education (培生教育出版集团) 激光防伪标签。无标签者不得销售。


本书提供与C 语言编程相关的全面资源和深入讨论。本书通过对指针的基础知识和高级特性的探讨,帮助程序员把指针的强大功能融入到自己的程序中去。

全书共18 章,覆盖了数据、语句、操作符和表达式、指针、函数、数组、字符串、结构和联合等几乎所有重要的C 编程话题。书中给出了很多编程技巧和提示,每章后面有针对性很强的练习,附录部分则给出了部分练习的解答。

本书适合C 语言初学者和初级C 程序员阅读,也可作为计算机专业学生学习C 语言的参考。


市面上已经有了许多讲述C语言的优秀图书,为什么我们还需要这一本呢?我在大学里教授C语言编程已有10个年头,但至今尚未发现一本书是按照我所喜欢的方式来讲述指针的。许多图书用一章的篇幅专门讲述指针,而且这部分内容往往出现在全书的后半部分。但是,仅仅描述指针的语法,并用一些简单的例子展示其用法是远远不够的。我在授课时,很早便开始讲授指针,而且在以后的授课过程中也经常讨论指针。我会描述它们在各种不同的上下文环境中的有效用法,展示使用指针的编程惯用法(programming idiom)。我还讨论了一些相关的课题,如编程效率和程序可维护性之间的权衡。指针是本书的线索所在,融会贯通于全书之中。

指针为什么如此重要?我的信念是:正是指针使C语言威力无穷。有些任务用其他语言也可以实现,但C语言能够更有效地实现;有些任务无法用其他语言实现,如直接访问硬件,但C语言却可以。要想成为一名优秀的C语言程序员,对指针有一个深入而完整的理解是先决条件。

然而,指针虽然很强大,与之相伴的风险却也不小。跟指甲锉相比,链锯可以更快地切割木材,但链锯更容易让人受伤,而且伤害常常来得极快,后果也非常严重。指针就像链锯一样,如果使用得当,它们可以简化算法的实现,并使其更富效率;如果使用不当,它们就会引起错误,导致细微而令人困惑的症状,并且极难发现原因。对指针只是略知一二便放手使用是件非常危险的事。如果那样的话,它给你带来的总是痛苦而不是欢乐。本书提供了你所需要的深入而完整的关于指针的知识,足以使你避开指针可能带来的痛苦。

为什么C语言依然如此流行?历史上,由于种种原因,业界选择了C,其中最主要的原因就在于它的效率。优秀C程序的效率几乎和汇编语言程序一样高,但C程序明显比汇编语言程序更易于开发。和许多其他语言相比,C给予程序员更多的控制权,如控制数据的存储位置和初始化过程等。C缺乏“安全网”特性,这虽有助于提高它的效率,但也增加了出错的可能性。例如,C对数组下标引用和指针访问并不进行有效性检查,这可以节省时间,但在使用这些特性时就必须特别小心。如果在使用C语言时能够严格遵守相关规定,就可以避免这些潜在的问题。

C提供了丰富的操作符集合,它们可以让程序员有效地执行一些底层的计算(如移位和屏蔽)等,而不必求助汇编语言。C的这个特点使很多人把C称为“高层”的汇编语言。但是,当需要的时候,C程序可以很方便地提供汇编语言的接口。这些特性使C成为实现操作系统和嵌入式控制器软件的良好选择。

C流行的另一个原因是它的普遍存在性。C编译器已经在许多机器上得以实现。另外,ANSI标准提高了C程序在不同机器之间的可移植性。

最后,C是C++的基础。C++提供了一种和C不同的程序设计和实现的观点。然而,如果你对C的知识和技巧(如指针和标准库等)成竹在胸,将非常有助于你成为一名优秀的C++程序员。

本书并不是一本关于编程的入门图书,它所面向的读者应该已经具备了一些编程经验,或者是一些想学习C,但又不想被诸如为什么循环很重要以及何时需要使用if语句等肤浅问题耽误进程的人。

另外,本书并不要求读者以前学习过C。本书涵盖了C语言所有方面的内容,这种内容的广泛覆盖性使得本书不仅适用于学生,也适用于专业人员。也就是说,本书适用于首次学习C的读者和那些经验更丰富但希望进一步提高语言使用技巧的用户。

优秀的C++图书把关注点集中在与面向对象模型有关的课题上(如类的设计),而不是专注于基本的C技巧,这样做是对的。但C++是建立在C基础之上的,C的基本技巧依然非常重要,特别是那些能够实现可复用类的技巧。诚然,C++程序员在阅读本书时可以跳过一些熟悉的内容,但他们依然会在本书中找到许多有用的C工具和技巧。

本书是按照教程的形式来组织的,它所面向的读者是已经具有编程经验的人。它的编写风格类似于导师在你的身后注视着你的工作,时不时给你一些提示和警告。本书的写作目标是把通常需要多年实践才能获得的知识和观点传授给读者。这种组织形式也影响到书中内容的顺序——通常在一个地方引入一个话题,并进行完整的讲解。因此,本书也可以当作参考手册。

在这种组织形式中,存在两个显著的例外之处。首先是指针,它贯穿全书,会在许多不同的上下文环境中进行讨论。然后就是第1章,它对语言的基础知识提供了一个快速的介绍。这种介绍有助于你很快掌握编写简单程序的技巧。第1章所涉及的主题将在后续章节中深入讲解。

较之其他图书,本书在许多领域着墨更多,这主要是为了让每个主题更具深度,向读者传授通常只有实践才能获得的经验。另外,本书使用了一些在现实编程中不太常见的例子,虽然有些不太容易理解,但这些例子显示了C在某些方面的趣味所在。

本书使用的ANSI C是由ANSI/ISO 9899-1990[ANSI 90]进行定义并由[KERN 89]进行描述的。之所以选择这个版本的C,有两个原因:首先,它是旧式C(有时称为Kernighan和Ritchie[KERN 78],或称为K&R C)的后继者,并已在根本上取代了后者;其次,ANSI C是C++的基础。本书中的所有例子都是用ANSI C编写的,因此本书经常会把“ANSI C标准文档”简称为“标准”。

语法描述格式如下:

if( expression )
    statement
else
    statement

本书在语法描述中使用了4种字体,其中必需的代码(如上例中的关键字if)将如上所示设置为Courier New字体。必要代码的抽象描述(如上例中的expression)用Courier New表示。有些语句具有可选部分,如果决定使用可选部分(如此例中的else关键字),它将严格按上面的例子以粗体Courier New表示。可选部分的抽象描述(如第2个statement)将以粗斜体 Courier New表示。每次引入新术语时,本书将以黑体表示。

完整的程序将标上号码,以“程序0.1”这样的格式显示。标题给出了程序的名称,包含源代码的文件名则显示在右下角。

文中有“提示”部分。这些提示中的许多内容都是对良好编程技巧的讨论——就是使程序更易编写、更易阅读并在以后更易理解。当一个程序初次写成时,稍微做些努力就可以节约以后修改程序的大量时间。其他一些提示能帮助你把代码写得更加紧凑或更有效率。

另外还有一些提示涉及软件工程的话题。C的诞生远早于现代软件工程原则的形成。因此,有些语言特性和通用技巧不为这些原则所提倡。这些话题通常涉及某种特定结构的效率与代码的可读性、可维护性之间的利弊权衡。这方面的讨论将向你提供一些背景知识,帮助你判断效率上的收益是否抵得上其他质量上的损失。

当看到“警告”时就要特别小心:这里将要指出的是C程序员新手(有时甚至是老手)经常出现的错误之一,或者代码将不会如你所预想的那样运行。这个警告标志将使提示内容不易被忘记,而且以后回过头来寻找也更容易一些。

K&R C”表示本书正在讨论ANSI C和K&R C之间的重要区别。尽管绝大多数以K&R C写成的程序仅需极微小的修改即可在ANSI C环境下运行,但有时仍可能碰到一个ANSI之前的编译器,或者遇到一个更老的程序。如此一来,两者的区别便至关重要。

本书每章的最后一节是问题和编程练习。问题难简不一,从简单的语法问题到更为复杂的问题(诸如效率和可维护性之间的权衡等),不一而足。编程练习按等级区分难度:的练习最为简单;的练习难度最大。这些练习有许多作为课堂测验已沿用多年。问题或编程练习前如果有一个符号,表示在附录中可以找到它的参考答案。

尽管无法列出对本书做出贡献的所有人,但依然向他们表示感谢。我的妻子Margaret对我的写作鼓励有加,为我提供精神上的支持,而且她默默承受着由于我写作本书而给她带来的生活上的孤独。

感谢我在RIT的同事Warren Caithers教授,他阅读并审校了本书的初稿。他真诚的批评帮助我从一大堆讲课稿和例子中生成了一份清晰、连贯的手稿。

非常感谢我的C语言编程课程的学生,他们帮助我发现录入错误,提出改进意见,并在学习过程中忍受着草稿形式的教材。他们对我的作品的反应向我提供了有益的反馈,帮助我进一步改进了本书的质量。

还要感谢Steve Allan、Bill Appelbe、Richard C.Detmer、Roger Eggen、Joanne Goldenberg、Dan Hinton、Dan Hirschberg、Keith E.Jolly、Joseph F.Kent、Masoud Milani、Steve Summit和Kanupriya Tewary,他们在本书出版前对它作了评价。他们的建议和观点对我进一步改进本书的表达形式助益颇多。

最后,我要向Addison-Wesley的编辑Deborah Lafferty女士、产品编辑Amy Willcutt女士表示感谢。正是由于她们的帮助,本书才从一本手稿成为一本正式的图书。她们不仅给了我很多有价值的建议,而且鼓励我改进原先自我感觉良好的排版。现在我已经看到了结果,她们的意见是正确的。

现在是开始学习的时候了,预祝大家在学习C语言的过程中找到快乐!


本书由异步社区出品,社区(https://www.epubit.com/)为您提供相关资源和后续服务。

作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。

当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。

我们的联系邮箱是contact@epubit.com.cn。

如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。

如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线投稿(直接访问www.epubit. com/selfpublish/submission即可)。

如果您是学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。

如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。

“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。

“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。

异步社区

微信服务号


从头开始介绍一门编程语言总是显得很困难,因为有许多细节还没有介绍,很难让读者在头脑中形成一幅完整的图。在本章中,我将向大家展示一个例子程序,并逐行讲解它的工作过程,试图让大家对C语言的整体有一个大概的印象。这个例子程序同时展示了你所熟悉的过程在C语言中是如何实现的。这些信息再加上本章所讨论的其他主题,一起构成了C语言的基础知识,这样你就可以自己编写有用的C程序了。

我们所要分析的这个程序是从标准输入读取文本并对其进行修改,然后把它写到标准输出中。程序1.1首先读取一串列标号。这些列标号成对出现,表示输入行的列范围。这串列标号以一个负值结尾,作为结束标志。剩余的输入行被程序读入并打印,然后输入行中被选中范围的字符串被提取出来并打印。注意,每行第1列的列标号为零。例如,如果输入如下:

则程序的输出如下:

这个程序的重要之处在于它展示了当你开始编写C程序时所需要知道的绝大多数基本技巧。

/*
** 这个程序从标准输入中读取输入行并在标准输出中打印这些输入行,
** 每个输入行的后面一行是该行内容的一部分。
**
** 输入的第1行是一串列标号,串的最后以一个负数结尾。
** 这些列标号成对出现,说明需要打印的输入行的列的范围。
** 例如,0 3 10 12 –1表示第0列到第3列,第10列到第12列的内容将被打印。
*/


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define    MAX_COLS    20            /* 所能处理的最大列号 */
#define    MAX_INPUT    1000            /* 每个输入行的最大长度 */

int    read_column_numbers( int columns[], int max );
void    rearrange( char *output, char const *input,
   int n_columns, int const columns[] );

int main( void )
{
   int     n_columns;            /* 进行处理的列标号 */
   int     columns[MAX_COLS];    /* 需要处理的列数 */
   char input[MAX_INPUT];        /* 容纳输入行的数组 */
   char output[MAX_INPUT];        /* 容纳输出行的数组 */

   /*
   ** 读取该串列标号
   */
   n_columns = read_column_numbers( columns, MAX_COLS );

   /*
   ** 读取、处理和打印剩余的输入行。
   */
   while( gets( input ) != NULL ){
      printf( "Original input : %s\n", input );
      rearrange( output, input, n_columns, columns );
      printf( "Rearranged line: %s\n", output );
   }

   return EXIT_SUCCESS;
}

/*
** 读取列标号,如果超出规定范围则不予理会。
*/
int read_column_numbers( int columns[], int max )
{
   int    num = 0;
   int    ch;

   /*
   ** 取得列标号,如果所读取的数小于0则停止。
   */
   while( num < max && scanf( "%d", &columns[num] ) == 1
      && columns[num] >= 0 )
       num += 1;

   /*
   ** 确认已经读取的标号为偶数个,因为它们是以对的形式出现的。
   */
   if( num % 2 != 0 ){
      puts( "Last column number is not paired." );
      exit( EXIT_FAILURE );
   }

   /*
   ** 丢弃该行中包含最后一个数字的那部分内容。
   */
   while( (ch = getchar()) != EOF && ch != '\n' )
      ;

   return num;
}

/*
** 处理输入行,将指定列的字符连接在一起,输出行以NUL结尾。
*/
void rearrange( char *output, char const *input,
   int n_columns, int const columns[] )
{
   int    col;            /* columns数组的下标 */
   int    output_col;    /* 输出列计数器 */
   int    len;            /* 输入行的长度 */

   len = strlen( input );
   output_col = 0;

   /*
   ** 处理每对列标号。
   */
   for( col = 0; col < n_columns; col += 2 ){
      int    nchars = columns[col + 1] - columns[col] + 1;

      /*
      ** 如果输入行结束或输出行数组已满,就结束任务。
      */
      if( columns[col] >= len ||
         output_col == MAX_INPUT - 1 )
           break;

      /*
      ** 如果输出行数据空间不够,只复制可以容纳的数据。
      */
      if( output_col + nchars > MAX_INPUT - 1 )
         nchars = MAX_INPUT - output_col - 1;

      /*
      ** 复制相关的数据。
      */
      strncpy( output + output_col, input + columns[col],
         nchars );
      output_col += nchars;
   }

   output[output_col] = ’\0’;
}

程序1.1 重排字符          rearrang.c

现在,让我们仔细观察这个程序。首先需要注意的是程序的空白:空行将程序的不同部分分隔开来;制表符(tab)用于缩进语句,更好地显示程序的结构。C是一种自由格式的语言,并没有规则要求你必须怎样书写语句。然而,如果你在编写程序时能够遵守一些约定还是非常值得的,它可以使代码更加容易阅读和修改,千万不要小看了这一点。

清晰地显示程序的结构固然重要,但告诉读者程序能做些什么以及怎样做则更为重要。注释(comment)就是用于实现这个功能。

/*
** 这个程序从标准输入中读取输入行并在标准输出中打印这些输入行,
** 每个输入行的后面一行是该行内容的一部分。
**
** 输入的第一行是一串列标号,串的最后以一个负数结尾。
** 这些列标号成对出现,说明需要被打印的输入行的列范围。
** 例如,0 3 10 12 –1表示第0列到第3列,第10列到第12列的内容将被打印。
*/

这段文字就是注释。注释以符号/*开始,以符号*/结束。在C程序中,凡是可以插入空白的地方都可以插入注释。然而,注释不能嵌套,也就是说,第1个/*符号和第1个*/符号之间的内容都被看作是注释,不管里面还有多少个/*符号。

在有些语言中,注释有时用于把一段代码“注释掉”,也就是使这段代码在程序中不起作用,但并不将其真正从源文件中删除。在C语言中,这可不是个好主意,如果你试图在一段代码的首尾分别加上/*和*/符号来“注释掉”这段代码,则不一定能如愿。如果这段代码内部原先就有注释存在,这样做就会出问题。要从逻辑上删除一段C代码,更好的办法是使用#if指令。只要像下面这样使用:

#if 0
  statements
#endif

在#if和#endif之间的程序段就可以有效地从程序中去除,即使这段代码之间原先存在注释也无妨,所以这是一种更为安全的方法。预处理指令的作用远比你想象的要大,本书将在第14章详细讨论这个问题。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define  MAX_COLS    20     /* 能够处理的最大列号 */
#define  MAX_INPUT    1000     /* 每个输入行的最大长度 */

这5行称为预处理指令(preprocessor directive),因为它们是由预处理器(preprocessor)解释的。预处理器读入源代码,根据预处理指令对其进行修改,然后把修改过的源代码递交给编译器。

在我们的例子程序中,预处理器用名叫stdio.h的库函数头文件的内容替换第1条#include指令语句,其结果就仿佛是stdio.h的内容被逐字写到源文件的那个位置。第2、3条指令的功能类似,只是它们所替换的头文件分别是stdlib.h和string.h。

stdio.h头文件使我们可以访问标准I/O库(Standard I/O Library)中的函数,这组函数用于执行输入和输出。stdlib.h定义了EXIT_SUCCESS和EXIT_FAILURE符号。我们需要string.h头文件提供的函数来操纵字符串。

 

提示:

如果你有一些声明需要用于几个不同的源文件,这个技巧也非常方便——在一个单独的文件中编写这些声明,然后用#include指令把这个文件包含到需要使用这些声明的源文件中。这样,只需要这些声明的一份副本,无须在许多不同的地方进行复制,这就避免了在维护这些代码时出现错误的可能性。

 

 

提示:

另一种预处理指令是#define,它把名字MAX_COLS定义为20,把名字MAX_INPUT定义为1000。当这个名字以后出现在源文件的任何地方时,它就会被替换为定义的值。由于它们被定义为字面值常量,所以这些名字不能出现于有些普通变量可以出现的场合(比如赋值符的左边)。这些名字一般都大写,用于提醒它们并非普通的变量。#define指令和其他语言中符号常量的作用类似,出发点也相同。如果以后你觉得20列不够,可以简单地修改MAX_COLS的定义,这样就用不着在整个程序中到处寻找并修改所有表示列范围的20,那样有可能漏掉一个,也可能把并非用于表示列范围的20也修改了。

 

int    read_column_numbers( int columns[], int max );
void    rearrange( char *output, char const *input,
     int n_columns, int const columns[] );

这些声明被称为函数原型(function prototype)。它们告诉编译器这些以后将在源文件中定义的函数的特征。这样,当这些函数被调用时,编译器就能对它们进行准确性检查。每个原型以一个类型名开头,表示函数返回值的类型。跟在返回类型名后面的是函数的名字,再后面是函数期望接受的参数。所以,函数read_column_numbers返回一个整数,接受两个类型分别是整型数组和整型标量的参数。函数原型中参数的名字并非必需的,这里给出参数名的目的是提示它们的作用。

rearrange函数接受4个参数。其中第1个和第2个参数都是指针(pointer)。指针指定一个存储于计算机内存中的值的地址,类似于门牌号码指定某个特定的家庭位于街道的何处。指针赋予C语言强大的威力,本书将在第6章详细讲解指针。第2个和第4个参数被声明为const,这表示函数将不会修改函数调用者所传递的这两个参数。关键字void表示函数并不返回任何值,在其他语言里,这种无返回值的函数被称为过程(procedure)。

 

提示:

假如这个程序的源代码由几个源文件所组成,那么使用该函数的源文件都必须写明该函数的原型。把原型放在头文件中并使用#include指令包含它们,可以避免由于同一个声明的多份副本而导致的维护性问题。

 

int main( void )
{

这几行构成了main函数定义的起始部分。每个C程序都必须有一个main函数,因为它是程序执行的起点。关键字int表示函数返回一个整型值,关键字void表示函数不接受任何参数。main函数的函数体包括左花括号和与之相匹配的右花括号之间的任何内容。

请观察一下缩进是如何使程序的结构显得更为清晰的。

         int     n_columns;            /* 进行处理的列标号 */
         int     columns[MAX_COLS];    /* 需要处理的列数 */
         char input[MAX_INPUT];        /* 容纳输入行的数组 */
         char output[MAX_INPUT];       /* 容纳输出行的数组 */

这几行声明了4个变量:一个整型标量、一个整型数组以及两个字符数组。所有4个变量都是main函数的局部变量,其他函数不能根据它们的名字访问它们。当然,它们可以作为参数传递给其他函数。

    /*
     ** 读取该串列标号
     */
     n_columns = read_column_numbers( columns, MAX_COLS );

这条语句调用函数read_column_numbers。数组columns和MAX_COLS所代表的常量(20)作为参数传递给这个函数。在C语言中,数组参数是以引用(reference)形式进行传递的,也就是传址调用,而标量和常量则是按(value)传递的(分别类似于Pascal和Modula中的var参数和值参数)。在函数中对标量参数的任何修改都会在函数返回时丢失,因此,被调用函数无法修改调用函数以传值形式传递给它的参数。然而,当被调用函数修改数组参数的其中一个元素时,调用函数所传递的数组就会被实际地修改。

事实上,关于C函数的参数传递规则可以表述如下:

所有传递给函数的参数都是按值传递的。

但是,当数组名作为参数时就会产生按引用传递的效果,如上所示。规则和现实行为之间似乎存在明显的矛盾之处,第8章会对此做出详细解释。

         /*
         ** 读取、处理和打印剩余的输入行。
         */
         while( gets( input ) != NULL ){
             printf( "Original input : %s\n", input );
             rearrange( output, input, n_columns, columns );
             printf( "Rearranged line: %s\n", output );
         }

         return EXIT_SUCCESS;
}

用于描述这段代码的注释看上去似乎有些多余。但是,如今最大的软件开销并非在于编写,而是在于维护。在修改一段代码时所遇到的第1个问题就是要搞清楚代码的功能。所以,如果你在代码中插入一些东西,能使其他人(或许就是你自己!)在以后更容易理解它,那就非常值得这样做。但是,要注意书写正确的注释,并且在你修改代码时要注意注释的更新。注释如果不正确那还不如没有!

这段代码包含了一个while循环。在C语言中,while循环的功能和它在其他语言中一样。它首先测试表达式的值,如果是假的(0)就跳过循环体。如果表达式的值是真的(非0),就执行循环体内的代码,然后再重新测试表达式的值。

这个循环代表了这个程序的主要逻辑。简而言之,它表示:

while 我们还可以读取另一行输入时
    打印输入行
    对输入行进行重新整理,把它存储于output数组
    打印输出结果

gets函数从标准输入读取一行文本并把它存储于作为参数传递给它的数组中。一行输入由一串字符组成,以一个换行符(newline)结尾。gets函数丢弃换行符,并在该行的末尾存储一个NUL字节[1](一个NUL字节是指字节模式为全0的字节,类似'\0'这样的字符常量)。然后,gets函数返回一个非NULL值,表示该行已被成功读取[2]。当gets函数被调用但事实上不存在输入行时,它就返回NULL值,表示它到达了输入的末尾(文件尾)。

在C程序中,处理字符串是常见的任务之一。尽管C语言并不存在“string”数据类型,但在整个语言中,存在一项约定:字符串就是一串以NUL字节结尾的字符。NUL作为字符串终止符,它本身并不被看作是字符串的一部分。字符串常量(string literal)就是源程序中被双引号括起来的一串字符。例如,字符串常量:

"Hello"

在内存中占据6字节的空间,按顺序分别是H、e、l、l、o和NUL。

printf函数执行格式化的输出。C语言的格式化输出比较简单,如果你是Modula或Pascal的用户,肯定会对此感到愉快。printf函数接受多个参数,其中第一个参数是一个字符串,描述输出的格式,剩余的参数就是需要打印的值。格式常常以字符串常量的形式出现。

格式字符串包含格式指定符(格式代码)以及一些普通字符。这些普通字符将按照原样逐字打印出来,但每个格式指定符将使后续参数的值按照它所指定的格式打印。表1.1列出了一些常用的格式指定符。如果数组input包含字符串Hi friend!,那么下面这条语句

printf( "Original input : %s\n", input);

的打印结果是:

Original input : Hi friends!

后面以一个换行符终止。

表1.1 常用printf格式代码

格  式

含  义

%d

以十进制形式打印一个整型值

%o

以八进制形式打印一个整型值

%x

以十六进制形式打印一个整型值

%g

打印一个浮点值

%c

打印一个字符

%s

打印一个字符串

\n

换行

例子程序接下来的一条语句调用rearrange函数。后面3个参数是传递给函数的值,第1个参数则是函数将要创建并返回给main函数的答案。记住,这种参数是唯一可以返回答案的方法,因为它是一个数组。最后一个printf函数显示输入行重新整理后的结果。

最后,当循环结束时,main函数返回值EXIT_SUCCESS。该值向操作系统提示程序成功执行。右花括号标志着main函数体的结束。

/*
** 读取列标号,如果超出规定范围则不予理会。
*/
int 
read_column_numbers( int columns[], int max )
{

这几行构成了read_column_numbers函数的起始部分。注意,这个声明和早先出现在程序中的该函数原型的参数个数和类型以及函数的返回值完全匹配。如果出现不匹配的情况,编译器就会报错。

在函数声明的数组参数中,并未指定数组的长度。这种格式是正确的,因为不论调用函数的程序传递给它的数组参数的长度是多少,这个函数都将照收不误。这是一个伟大的特性,它允许单个函数操纵任意长度的一维数组。这个特性不利的一面是函数没法知道该数组的长度。如果确实需要数组的长度,它的值必须作为一个单独的参数传递给函数。

当本例的read_column_numbers函数被调用时,传递给函数的其中一个参数的名字碰巧与上面给出的形参名字相同。但是,其余几个参数的名字与对应的形参名字并不相同。和绝大多数语言一样,C语言中形式参数的名字和实际参数的名字并没有什么关系。你可以让两者相同,但并非一定要这样做。

int    num = 0;
int    ch;

这里声明了两个变量,它们是该函数的局部变量。第1个变量在声明时被初始化为0,但第2个变量并未初始化。更准确地说,它的初始值将是一个不可预料的值,也就是垃圾。在这个函数里,它没有初始值并不碍事,因为函数对这个变量所执行的第1个操作就是对它赋值。

   /*
   ** 取得列标号,如果所读取的数小于0则停止。
   */
   while( num < max && scanf( "%d", &columns[num] ) == 1
      && columns[num] >= 0 )
        num += 1;

这又是一个循环,用于读取列标号。scanf函数从标准输入读取字符并根据格式字符串对它们进行转换——类似于printf函数的逆操作。scanf函数接受几个参数,其中第1个参数是一个格式字符串,用于描述期望的输入类型。剩余几个参数都是变量,用于存储函数所读取的输入数据。scanf函数的返回值是函数成功转换并存储于参数中的值的个数。

 

警告:

对于这个函数,你必须小心在意。由于scanf函数的实现原理,所有标量参数的前面必须加上一个“&”符号。关于这点,第8章会解释清楚。数组参数前面不需要加上“&”符号[3]。但是,数组参数中如果出现了下标引用,也就是说实际参数是数组的某个特定元素,那么它的前面也必须加上“&”符号。第15章会解释在标量参数前面加上“&”符号的必要性。现在,你只要知道必须加上这个符号就行了,因为如果没有它们的话,程序就无法正确运行。

 

 

警告:

第二个需要注意的地方是格式代码,它与printf函数的格式代码颇为相似却又并不完全相同,所以很容易引起混淆。表1.2粗略列出了一些你可能会在scanf函数中用到的格式代码。注意,前5个格式代码用于读取标量值,所以变量参数的前面必须加上“&”符号。使用所有格式码(除%c之外)时,输入值之前的空白(空格、制表符、换行符等)会被跳过,值后面的空白表示该值的结束。因此,用%s格式码输入字符串时,中间不能包含空白。除了表中所列之外,还存在许多格式代码,但表1.2中的这几个格式代码对于应付我们现在的需求已经足够了。

 

我们现在可以解释表达式:

scanf("%d", &columns[num] )

格式码%d表示需要读取一个整型值。字符是从标准输入读取,前导空白将被跳过。然后这些数字被转换为一个整数,结果存储于指定的数组元素中。我们需要在参数前加上一个“&”符号,因为数组下标选择的是一个单一的数组元素,它是一个标量。

while循环的测试条件由3个部分组成:

num < max

这个测试条件确保函数不会读取过多的值,从而导致数组溢出。如果scanf函数转换了一个整数之后,它就会返回1这个值。最后,

columns[num] >= 0

这个表达式确保函数所读取的值是正数。如果两个测试条件之一的值为假,循环就会终止。

表1.2 常用scanf格式码

格  式

含  义

变 量 类 型

%d

读取一个整型值

int

%ld

读取一个长整型值

long

%f

读取一个实型值(浮点数)

float

%lf

读取一个双精度实型值

double

%c

读取一个字符

char

%s

从输入中读取一个字符串

char型数组

 

提示:

标准并未硬性规定C编译器对数组下标的有效性进行检查,而且绝大多数C编译器确实也不进行检查。因此,如果需要进行数组下标的有效性检查,则必须自行编写代码。如果此处不进行num < max这个测试,而且程序所读取的文件包含超过20个列标号,那么多出来的值就会存储在紧随数组之后的内存位置,这样就会破坏原先存储在这个位置的数据——可能是其他变量,也可以是函数的返回地址。这可能会导致多种结果,程序很可能不会按照你预想的那样运行。

 

&&是“逻辑与”操作符。要使整个表达式为真,&&操作符两边的表达式都必须为真。然而,如果左边的表达式为假,右边的表达式便不再进行求值,因为不管它是真是假,整个表达式总是假的。在这个例子中,如果num到达了它的最大值,循环就会终止[4],而表达式

columns[num]

便不再被求值。

 

警告:

此处需要小心。当实际上想使用&&操作符时,千万不要误用了&操作符。&操作符执行“按位与”操作,虽然有些时候它的操作结果和&&操作符相同,但很多情况下都不一样。第5章将讨论这些操作符。

 

scanf函数每次调用时都从标准输入读取一个十进制整数。如果转换失败,不管是因为文件已经读完还是因为下一次输入的字符无法转换为整数,函数都会返回0,这样就会使整个循环终止。如果输入的字符可以合法地转换为整数,那么这个值就会转换为二进制数存储于数组元素columns[num]中。然后,scanf函数返回1。

 

警告:

用于测试两个表达式是否相等的操作符是==。如果误用了=操作符,虽然它也是合法的表达式,但其结果肯定和你的本意不一样:它将执行赋值操作而不是比较操作!但由于它也是一个合法的表达式,所以编译器无法为你找出这个错误[5]。在进行比较操作时,千万要注意你所使用的是两个等号的比较操作符。如果你的程序无法运行,请检查一下所有的比较操作符,看看是不是这个地方出了问题。相信我,你肯定会犯这个错误,而且可能不止一次,我自己就曾经犯过这个错误。

 

接下来的一个&&操作符确保在scanf函数成功读取了一个数之后,才对这个数进行是否赋值的测试。语句

num += 1;

使变量num的值增加1,它相当于下面这个表达式

num = num + 1;

以后我将解释为什么C语言提供了两种不同的方式来增加一个变量的值[6]

/*
** 确认已经读取的标号为偶数个,因为它们是以成对的形式出现的。
*/
if( num % 2 != 0 ){
    puts( "Last column number is not paired." );
    exit( EXIT_FAILURE );
}

这个测试检查程序所读取的整数是否为偶数个,这是程序规定的,因为这些数字要求成对出现。%操作符执行整数的除法,但它给出的结果是除法的余数而不是商。如果num不是一个偶数,它除以2之后的余数将不是0。

puts函数是gets函数的输出版本,它把指定的字符串写到标准输出并在末尾添上一个换行符。程序接着调用exit函数,终止程序的运行,EXIT_FAILURE这个值被返回给操作系统,提示出现了错误。

/*
 ** 丢弃该行中包含最后一个数字的那部分内容。
 */
 while( (ch = getchar()) != EOF && ch != '\n' )
      ;

当scanf函数对输入值进行转换时,它只读取需要读取的字符。这样,该输入行中包含了最后一个值的剩余部分仍会留在那里,等待被读取。它可能只包含作为终止符的换行符,也可能包含其他字符。不论如何,while循环将读取并丢弃这些剩余的字符,防止它们被解释为第1行数据。

下面这个表达式

(ch = getchar() ) != EOF && ch != '\n'

值得花点时间讨论。首先,getchar函数从标准输入读取一个字符并返回它的值。如果输入中不再存在任何字符,函数就会返回常量EOF(在stdio.h中定义),用于提示文件的结尾。

从getchar函数返回的值被赋给变量ch,然后把它与EOF进行比较。在赋值表达式两端加上括号用于确保赋值操作先于比较操作进行。如果ch等于EOF,整个表达式的值就为假,循环将终止。若非如此,再把ch与换行符进行比较,如果两者相等,循环也将终止。因此,只有当输入尚未到达文件尾并且输入的字符并非换行符时,表达式的值才是真的(循环将继续执行)。这样,这个循环就能剔除当前输入行最后的剩余字符。

现在让我们进入有趣的部分。在大多数其他语言中,我们将像下面这样编写循环:

ch = getchar();
while( ch != EOF && ch != '\n' )
     ch = getchar();

它将读取一个字符,接下来如果我们尚未到达文件的末尾或读取的字符并不是换行符,它将继续读取下一个字符。注意,这里两次出现了下面这条语句:

  ch = getchar();

C可以把赋值操作蕴含于while语句内部,这样就允许程序员消除冗余语句。

 

提示:

例子程序中的那个循环的功能和上面这个循环相同,但它包含的语句要少一些。无可争议,这种形式可读性差一点。仅仅根据这个理由,你就可以理直气壮地声称这种编码技巧应该避免使用。但是,你之所以会觉得这种形式的代码可读性较差,只是因为你对C语言及其编程的习惯用法不熟悉。经验丰富的C程序员在阅读(和编写)这类语句时根本不会出现困难。如果没有明显的好处,你应该避免使用影响代码可读性的方法。但在这种编程习惯用法中,同样的语句少写一次带来的维护方面的好处要更大一些。

 

一个经常问到的问题是:为什么ch被声明为整型,而我们事实上需要它来读取字符?答案是EOF是一个整型值,它的位数比字符类型要多,把ch声明为整型可以防止从输入读取的字符意外地被解释为EOF。但同时,这也意味着接收字符的ch必须足够大,足以容纳EOF,这就是ch使用整型值的原因。正如第3章所讨论的那样,字符只是小整型数而已,所以用一个整型变量容纳字符值并不会引起任何问题。

 

提示:

对这段程序最后还有一点说明:这个while循环的循环体没有任何语句。仅仅完成while表达式的测试部分就足以达到我们的目的,所以循环体就无事可干。你偶尔也会遇到这类循环,处理它们应该没问题。while语句之后的单独一个分号称为空语句(empty statement),它就是应用于目前这个场合,也就是语法要求这个地方出现一条语句但又无须执行任何任务的时候。这个分号独占一行,这是为了防止读者错误地以为接下来的语句也是循环体的一部分。

 

   return num;
}

return语句就是函数向调用它的表达式返回一个值。在这个例子里,变量num的值被返回给调用该函数的程序,后者把这个返回值赋值给主程序的n_columns变量。

/*
** 处理输入行,将指定列的字符连接在一起,输出行以NUL结尾。
*/
void 
rearrange( char *output, char const *input,
   int n_columns, int const columns[] )
{
   int    col;     /* columns数组的下标 */
   int    output_col;  /* 输出列计数器 */
   int    len;     /* 输入行的长度 */

这些语句定义了rearrange函数并声明了一些局部变量。此处最有趣的一点是:前两个参数被声明为指针,但在函数实际调用时,传给它们的参数却是数组名。当数组名作为实参时,传给函数的实际上是一个指向数组起始位置的指针,也就是数组在内存中的地址。正因为实际传递的是一个指针而不是一份数组的拷贝,才使数组名作为参数时具备了传址调用的语义。函数可以按照操纵指针的方式来操纵实参,也可以像使用数组名一样用下标来引用数组的元素。第8章将对这些技巧进行更详细的说明。

但是,由于它的传址调用语义,如果函数修改了形参数组的元素,它实际上将修改实参数组的对应元素。因此,例子程序把columns声明为const就有两方面的作用。首先,它声明该函数的作者的意图是这个参数不能被修改。其次,它导致编译器去验证是否违背该意图。因此,这个函数的调用者不必担心例子程序中作为第4个参数传递给函数的数组中的元素会被修改。

len = strlen( input );
output_col = 0;

/*
** 处理每对列标号。
*/
for( col = 0; col < n_columns; col += 2 ){

这个函数的真正工作是从这里开始的。我们首先获得输入字符串的长度,这样如果列标号超出了输入行的范围,我们就忽略它们。C语言的for语句跟它在其他语言中不太像,它更像是while语句的一种常用风格的简写法。for语句包含3个表达式(顺便说一下,这3个表达式都是可选的)。第一个表达式是初始部分,它只在循环开始前执行一次。第二个表达式是测试部分,它在循环每执行一次后都要执行一次。第三个表达式是调整部分,它在每次循环执行完毕后都要执行一次,但它在测试部分之前执行。为了清楚起见,上面这个for循环可以改写为如下所示的while循环:

col = 0;
while( col < n_columns ) {
   /*
   ** 循环体
   */
   col += 2;
}

int    nchars = columns[col + 1] - columns[col] + 1;

   /*
   ** 如果输入行结束或输出行数组已满,就结束任务。
   */
   if( columns[col] >= len ||
     output_col == MAX_INPUT - 1 )
      break;

   /*
   ** 如果输出行数据空间不够,只复制可以容纳的数据。
   */
   if( output_col + nchars > MAX_INPUT - 1 )
     nchars = MAX_INPUT - output_col - 1;

   /*
   ** 复制相关的数据。
   */
   strncpy( output + output_col, input + columns[col],
     nchars );
   output_col += nchars;

这是for循环的循环体,它一开始计算当前列范围内字符的个数,然后决定是否继续进行循环。如果输入行比起始列短,或者输出行已满,它便不再执行任务,使用break语句立即退出循环。

接下来的一个测试检查这个范围内的所有字符是否都能放入输出行中,如果不行,它就把nchars调整为数组能够容纳的大小。

 

提示:

在这种只使用一次的“一次性”程序中,不执行数组边界检查之类的任务,只是简单地让数组“足够大”从而使其不溢出的做法是很常见的。然而,这种方法有时也应用于实际产品代码中。这种做法在绝大多数情况下将导致大部分数组空间被浪费,而且即使这样有时仍会出现溢出,从而导致程序失败[7]

 

最后,strncpy函数把选中的字符从输入行复制到输出行中可用的下一个位置。strncpy函数的前两个参数分别是目标字符串和源字符串的地址。在这个调用中,目标字符串的位置是输出数组的起始地址向后偏移output_col列的地址,源字符串的位置则是输入数组起始地址向后偏移columns[col]个位置的地址。第3个参数指定需要复制的字符数[8]。输出列计数器随后向后移动nchars个位置。

 }
 output[output_col] = '\0';
}

循环结束之后,输出字符串将以一个NUL字符作为终止符。注意,在循环体中,函数必须经过精心设计,确保数组仍有空间容纳这个终止符。然后,程序执行流便到达了函数的末尾,于是执行一条隐式的return语句。由于不存在显式的return语句,因此没有任何值返回给调用这个函数的表达式。在这里,不存在返回值并不会有问题,因为这个函数被声明为void(也就是说,不返回任何值),并且当它被调用时,并不对它的返回值进行比较操作或把它赋值给其他变量。

本章的例子程序描述了许多C语言的基础知识。但在亲自动手编写程序之前,你还应该知道一些东西。首先是putchar函数,它与getchar函数相对应,它接受一个整型参数,并在标准输出中打印该字符(如前所述,字符在本质上也是整型)。

同时,在函数库里存在许多操纵字符串的函数。这里将简单地介绍几个最有用的。除非特别说明,这些函数的参数既可以是字符串常量,也可以是字符型数组名,还可以是一个指向字符的指针。

strcpy函数与strncpy函数类似,但它并没有限制需要复制的字符数量。它接受两个参数:第2个字符串参数将被复制到第1个字符串参数;第1个字符串原有的字符将被覆盖。strcat函数也接受两个参数,但它把第2个字符串参数添加到第1个字符串参数的末尾。在这两个函数中,它们的第1个字符串参数不能是字符串常量。而且,确保目标字符串有足够的空间是程序员的责任,函数并不对其进行检查。

在字符串内进行搜索的函数是strchr,它接受两个参数:第1个参数是字符串;第2个参数是一个字符。这个函数在字符串参数内搜索字符参数第1次出现的位置,如果搜索成功就返回指向这个位置的指针,如果搜索失败就返回一个NULL指针。strstr函数的功能类似,但它的第2个参数也是一个字符串,它搜索第2个字符串在第1个字符串中第1次出现的位置。

编译和运行C程序的方法取决于所使用的系统类型。在UNIX系统中,要编译一个存储于文件testing.c的程序,可以使用以下命令:

cc testing.c
a.out

在PC中,你需要知道所使用的是哪一种编译器。如果是Borland C++,在MS-DOS窗口中,可以使用下面的命令:

bcc testing.c
testing

本章的目的是描述足够的C语言的基础知识,使你对C语言有一个整体的印象。有了这方面的基础,在接下来的学习中,你会更加容易理解所讲内容。

本章的例子程序说明了许多要点。注释以/*开始,以*/结束,用于在程序中添加一些描述性的说明。#include预处理指令可以使一个函数库头文件的内容由编译器进行处理,#define指令允许你给字面值常量取个符号名。

所有的C程序必须有一个main函数,它是程序执行的起点。函数的标量参数通过传值的方式进行传递,而数组名参数则具有传址调用的语义。字符串是一串由NUL字节结尾的字符,并且有一组库函数以不同的方式专门用于操纵字符串。printf函数执行格式化输出,scanf函数用于格式化输入,getchar和putchar分别执行非格式化字符的输入和输出。if和while语句在C语言中的用途跟它们在其他语言中的用途差不太多。

通过观察例子程序的运行之后,你或许想亲自编写一些程序。你可能觉得C语言所包含的内容应该远远不止这些,确实如此。但是,这个例子程序应该足以让你上手了。

1.在scanf函数的标量参数前未添加&字符。

2.机械地把printf函数的格式代码照搬于scanf函数。

3.在应该使用&&操作符的地方误用了&操作符。

4.误用=操作符而不是==操作符来测试相等性。

1.使用#include指令避免重复声明。

2.使用#define指令给常量值取名。

3.在#include文件中放置函数原型。

4.在使用下标前先检查它们的值。

5.在while或if表达式中蕴含赋值操作。

6.如何编写一个空循环体。

7.始终要进行检查,确保数组不越界。

1.C是一种自由形式的语言,也就是说,并没有规则规定它的外观究竟应该怎样[9]。但本章的例子程序遵循了一定的空白使用规则。你对此有何想法?

2.把声明(如函数原型的声明)放在头文件中,并在需要时用#include指令把它们包含于源文件中,这种做法有什么好处?

3.使用#define指令给字面值常量取名有什么好处?

4.依次打印一个十进制整数、字符串和浮点值,你应该在printf函数中分别使用什么格式代码?试编一例,让这些打印值以空格分隔,并在输出行的末尾添加一个换行符。

5.编写一条scanf语句,它需要读取两个整数,分别保存于quantity和price变量,然后再读取一个字符串,保存在一个名叫department的字符数组中。

6.C语言并不执行数组下标的有效性检查。你觉得为什么这个明显的安全手段会从语言中省略?

7.本章描述的rearrange程序包含下面的语句

 strncpy( output + output_col, 
    input + columns[col], nchars );

strcpy函数只接受两个参数,所以它实际上所复制的字符数由第2个参数指定。在本程序中,如果用strcpy函数取代strncpy函数会出现什么结果?

8.rearrange程序包含下面的语句

 while( gets( input ) != NULL ) {

你认为这段代码可能会出现什么问题?

1.“Hello world!”程序常常是C编程新手所编写的第1个程序。它在标准输出中打印Hello world!,并在后面添加一个换行符。当你希望摸索出如何在自己的系统中运行C编译器时,这个小程序往往是一个很好的测试例。

2.编写一个程序,从标准输入读取几行输入。每行输入都要打印到标准输出上,前面要加上行号。在编写这个程序时要试图让程序能够处理的输入行的长度没有限制。

3.编写一个程序,从标准输入读取一些字符,并把它们写到标准输出中。它同时应该计算checksum(校验和)值,并写在字符的后面。

checksum用一个singed char类型的变量进行计算,它初始为-1。当每个字符从标准输入读取时,它的值就被加到checksum中。如果checksum变量产出了溢出,这些溢出就会被忽略。当所有的字符均被写入后,程序以十进制整数的形式打印出checksum的值,它有可能是负值。注意,在checksum后面要添加一个换行符。

在使用ASCII码的计算机中,在包含“Hello world!”这几个词并以换行符结尾的文件上运行这个程序应该产生下列输出:

   Hello world!
   102

4.编写一个程序,一行行地读取输入行,直至到达文件尾。算出每行输入行的长度,然后把最长的那行打印出来。为了简单起见,你可以假定所有的输入行均不超过1000个字符。

5.rearrange程序中的下列语句

   if( columns[col] >= len ... )
        break;

当字符的列范围超出输入行的末尾时就停止复制。这条语句只有当列范围以递增顺序出现时才是正确的,但事实上并不一定如此。请修改这条语句,即使列范围不是按顺序读取时,也能正确完成任务。

6.修改rearrange程序,去除输入中列标号的个数必须是偶数的限制。如果读入的列标号为奇数个,函数就会把最后一个列范围设置为最后一个列标号所指定的列到行尾之间的范围。从最后一个列标号直至行尾的所有字符都将被复制到输出字符串。

[1] NUL是ASCII字符集中‘\0’字符的名字,它的字节模式为全0。NULL指一个其值为0的指针。它们都是整型值,其值也相同,所以它们可以互换使用。然而,你还是应该使用适当的常量,因为它能告诉阅读程序的人不仅使用0这个值,而且还能告诉他使用这个值的目的。

[2] 符号NULL在头文件stdio.h中定义。另一方面,并不存在预定义的符号NUL,所以如果你想使用它而不是字符常量‘\0’,就必须自行定义。

[3] 即使在它前面加上一个“&”也没有什么不对,所以如果你喜欢,也可以加上它。

[4] “循环终止”(the loop break)这句话的意思是循环结束而不是它突然出现了毛病。这句话源于break语句,我们将在第4章讨论它。

[5] 有些较新的编译器在if和while表达式中发现使用赋值符时会发出警告信息,其理论是在这样的上下文环境中,用户需要使用比较操作的可能性要远大于赋值操作。

[6] 加上前缀和后缀++操作符,事实上共有4种方法用来增加一个变量的值。

[7] 聪明的读者会注意到,如果遇到特别长的输入行,我们并没有办法防止gets函数溢出。这个漏洞确实是gets函数的缺陷,所以应该换用fgets(将在第15章描述)。

[8] 如果源字符串的字符数少于第3个参数指定的复制数量,目标字符串中剩余的字节将用NUL字节填充。

[9] 但预处理指令则有较严格的规则。


相关图书

代码审计——C/C++实践
代码审计——C/C++实践
C/C++代码调试的艺术(第2版)
C/C++代码调试的艺术(第2版)
大规模C++软件开发 卷1:过程与架构
大规模C++软件开发 卷1:过程与架构
C/C++程序设计竞赛真题实战特训教程(图解版)
C/C++程序设计竞赛真题实战特训教程(图解版)
C/C++函数与算法速查宝典
C/C++函数与算法速查宝典
C程序设计教程(第9版)
C程序设计教程(第9版)

相关文章

相关课程