C语言解惑

978-7-115-35114-2
作者: 【美】Alan R. Feuer
译者: 杨涛王建桥杨晓云
编辑: 傅道坤
分类: C语言

图书目录:

详情

本书脱胎于作者在C语言的摇篮——贝尔实验室教授C语言的讲稿,几乎涵盖了C语言各个方面的难点,并包含了一些其他书籍很少分析到的问题。在每个谜题后面都有详尽的解题分析,使读者能够清晰地把握C语言的构造与含义,学会处理许多常见的限制和陷阱,是一本绝佳的C语言练习册。 本书结构清晰,循序渐进,适合于C语言的初学者,可用作高校计算机相关专业的辅助教材,同时也可供具有一定C语言编程经验的读者复习提高之用。

图书摘要

PEARSON

C语言解惑

The C Puzzle Book

[美]Alan R.Feuer 著

杨涛 王建桥 杨晓云 译

人民邮电出版社

北京

图书在版编目(CIP)数据

C语言解惑/(美)富勒(Feuer,A.R.)著;杨涛,王建桥,杨晓云译.--北京:人民邮电出版社,2016.3

ISBN 978-7-115-35114-2

Ⅰ.①C… Ⅱ.①富…②杨…③王…④杨… Ⅲ.①C语言—程序设计 Ⅳ.①TP312

中国版本图书馆CIP数据核字(2015)第007124号

版权声明

Authorized translation from the English language edition,entitled:The C Puzzle Book,published by Pearson Education,Inc.,publishing as Addison-Wesley Professional,Copyright © 1999 by Alan R.Feuer.

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 © 2016.

本书中文简体字版由Pearson Education Asia Ltd.授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

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

版权所有,侵权必究。

◆著 [美]Alan R.Feuer

译 杨涛 王建桥 杨晓云

责任编辑 傅道坤

责任印制 张佳莹 焦志炜

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

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

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

北京鑫正大印刷有限公司印刷

◆开本:800×1000 1/16

印张:10.25

字数:189千字  2016年3月第1版

印数:1-3000册  2016年3月北京第1次印刷

著作权合同登记号 图字:01-2007-0544号

定价:35.00元

读者服务热线:(010)81055410 印装质量热线:(010)81055316

反盗版热线:(010)81055315

内容提要

本书脱胎于作者在C语言的摇篮——贝尔实验室教授C语言的讲稿,几乎涵盖了C语言各个方面的难点,并包含了一些其他书籍很少分析到的问题。在每个谜题后面都有详尽的解题分析,使读者能够清晰地把握C语言的构造与含义,学会处理许多常见的限制和陷阱,是一本绝佳的C语言练习册。

本书结构清晰,循序渐进,适合于C语言的初学者,可用作高校计算机相关专业的辅助教材,同时也可供具有一定C语言编程经验的读者复习提高之用。

前言

C语言并不大——如果以参考手册的篇幅作为衡量标准的话,C语言甚至可以归为一种“小”语言。不过,这种“小”并不意味着C语言的功能不够强大,而是说明了C语言里的限制性规则比较少。C语言本身的设计非常简洁精妙,这一点相信C语言的使用者早已有所体会。

不过,C语言的这种精妙对C语言的初学者来说,似乎是故作神秘。因为限制较少,C语言可以写成内容丰富的表达式,这可能会被初学者认为是输出错误。C语言的紧凑性允许以简洁凝炼的方式实现常见的编程任务。

学用C语言的过程,与学用其他的程序设计语言一样,大致可以分为三个阶段(这样的分段想必读者在其他的教科书里已见过很多次了)。第一阶段是理解这种语言的语法,这至少需要达到编译器不再提示程序存在语法性错误的程度。第二阶段是了解编译器将赋予正确构造的结构什么含义。第三阶段是形成一种适合这种语言的编程风格;只有到了这一阶段,才能编写出清晰简洁而又正确的程序。

本书中的谜题是我们为了帮助广大读者迅速通过C语言学习过程中的第二阶段而准备的。它们不仅可以检验读者对C语言语法规则的掌握程度,还可以引导读者接触一些很少涉及的问题,绕过一些常规的限制,跳过几个打开的陷阱。(我们必须承认,C语言也有一些需要一定的编程经验才能掌握的难点,在这方面与其他程序设计语言没有什么两样。)

请不要把本书的谜题视为优秀的代码范例,事实上,本书的某些代码相当不容易理解。但这也正是我们编写本书的目的之一。编写失当的程序往往却能成为一个有意义的谜题:

表达含混,必须参照一本语法手册才能看懂;

结构过于复杂,数据结构和程序结构不够清晰,难以记忆和理解;

某些用法晦涩难懂,在运用某些概念的时候不遵守有关的标准。

本书中的谜题全部基于ANSI标准的C语言,涉及的某些功能可能有某些编译器不支持。不过,因为ANSI C是绝大多数C语言版本的超集,所以即使你们的编译器不支持书中涉及的某项功能,它也很可能会以另外一种方式实现。

如何使用这本书

本书是标准C语言教材[1]的绝佳配套读物。本书分为9章,每章探讨一个主题,各章均由C语言代码示例构成,分别对该章主题的各个方面进行探讨。在那些代码示例里有许多print语句,而本书的主要工作就是分析每段示例代码的输出到底是什么。书中的示例程序都是彼此独立的,但后面的谜题需要把前面的谜题搞清楚之后才容易理解。

每个程序的输出紧接在相应的程序代码的后面给出。这些程序都已经在“Sun工作站+Unix操作系统”和“PC + MS/DOS操作系统”环境下调试通过。少数例子在这两种平台上有不同的输出,我们分别给出这两种输出。

本书的大部分篇幅是一步一步地解释各类谜题的答案,相关的C语言编程技巧就穿插在解释内容里。

做谜题的一般顺序是这样的:

阅读C语言教科书里该主题的相关内容。

阅读本书与该主题相关的章节里的每一段示例程序:

—做各段示例程序相关的谜题;

—把你的答案与书中给出的程序输出进行对照;

—阅读本书对解决方案的解释。

致谢

本书脱胎于我在C语言的诞生地——贝尔实验室教授C语言的讲稿。来自听课人员的踊跃反应使得我有勇气把这些谜题及其答案整理成书。有许多朋友和同事对本书的草稿提出了宝贵的建议和指教,他们是:Al Boysen,Jr.、Jeannette Feuer、Brian Kernighan、John Linderman、David Nowitz、Elaine Piskorik、Bill Roome、Keith Vollherbst和Charles Wetherell。最后,我要感谢贝尔实验室为我提供的有利环境和大力支持。

Alan Feuer

[1].推荐人民邮电出版社即将出版的美国主流教材《C程序设计:现代方法》(K.N.King著)。——译者注

第1章 操作符

C语言程序由语句构成,而语句由表达式构成,表达式又由操作符和操作数构成。C语言中的操作符非常丰富——本书的附录B所给出的操作符汇总表就是最好的证据。正是因为这种丰富性,为操作符确定操作数的规则就成为了我们理解C语言表达式的核心和关键。那些规则——即所谓的“优先级”和“关联性”——汇总在本书附录A的操作符优先级表里。请使用该表格来解答本章中的谜题。

谜题1.1 基本算术操作符

请问,下面这个程序的输出是什么?

输出:

解惑1.1 基本算术操作符

1.1.1

关于printf:printf是C语言标准函数库提供的一个格式化输出函数。printf函数的第一个参数是一个格式字符串,它描述了后面的参数将如何输出。“%”引出参数的输出规范。在谜题1.1这段程序里,“%d”将使得printf函数对第二个参数进行分析然后将其输出为一个十进制整数。printf函数也可以用来输出字面字符。在这个程序里,还输出了一个换行符,这需要在格式字符串里给出换行符的名字(\n)。

1.1.2

1.1.3

1.1.4

关于编程风格:正如我们在前言里讲的那样,本书里的程序并不是应当仿效的范例。这些程序只是为了让大家开动脑筋去思考C语言的工作机制而设计的。不过,既然也是程序,这些谜题本身当然也会涉及编程风格的问题。一般来说,如果一段代码总是需要程序员求助于参考手册才能读懂的话,那它要么是编写得不够好,要么是需要增加一些注释来提供缺少的细节。

本谜题的这组题传达了这样一个信息:在复杂的表达式里,使用括号有助于读者搞清楚操作符与操作数之间的关联关系。

谜题1.2 赋值操作符

请问,下面这个程序的输出是什么?

输出:

解惑1.2 赋值操作符

1.2.1

关于define:本谜题的程序以

#define PRINTX printf("%d\n", x)

这条语句开头。在C程序里,以“#”字符开头的代码行都是一条C预处理器语句。预处理器的工作之一是把一个记号字符串替换为另一个。这个程序里的define语句,告诉预处理器把代码里的“PRINTX”记号全部替换为字符串“printf("%d\n",x)”。

1.2.2

1.2.3

1.2.4

谜题1.3 逻辑操作符和增量操作符

请问,下面这个程序的输出是什么?

输出:

解惑1.3 逻辑操作符和增量操作符

1.3.1

再谈define:这个程序里的define语句与前一个程序里的define语句有所不同:这个程序里的“PRINT”是一个带参数的宏。在遇到带参数的宏时,C语言的预处理器将分两步进行替换:先把宏定义里的形式参数替换为实际参数,再把宏调用替换为宏定义体。

在这个程序里,“PRINT”宏有一个形式参数int。“PRINT(x)”是使用实际参数“x”进行的“PRINT”调用。在扩展“PRINT”调用的时候,C语言预处理器会先把宏定义里的所有“int”替换为“x”,再把宏调用“PRINT(x)”替换为结果字符串“printf("%d\n",x)”。请注意,形式参数int并不是匹配单词“printf”里的“int”字符的。这是因为宏定义里的形式参数是标识符——具体到这个例子,形式参数int只对标识符int进行匹配和替换。

1.3.2

1.3.3

1.3.4

关于记号:计算机程序的源代码文本是由一系列记号构成的,而编译一个程序的第一项工作就是把那些记号一个一个地分解开。这在绝大多数场合都没有什么困难,但有些字符序列可能会让人感到困惑。比如说,如果上面这个例子里的表达式有一部分根本没有空格——就像下面这样:

x+++++y

那么,这个没有空格的表达式与有空格的表达式还会是等价的吗?

为避免产生二义性,如果一个字符串能够解释为多个操作符,C语言编译器将按照“构成操作符的字符个数越多越好”的原则来作出选择。根据这一原则,“x+++++y”解释为:

x++ ++ + y

而这已经不是一个合法的表达式了。

1.3.5

初始值:x=3,z=0

z = x / ++ x

z = x / (++ x)

z = (x / (++ x))

(z = (x / (++ x)))

如果你还像以前那样按照从内到外的顺序对这个表达式进行求值,即先对x进行递增,然后作为除数、用x作被除数去进行除法计算。问题是:作为被除数的x到底是几?是3还是4?换个问法,被除数到底是递增前的x值,还是递增后的x值?请注意,C语言并没有对这种“副作用”作出明确的规定,而是由C编译器的编写者决定的[1]。这个例子的教训是:如果你无法断定会不会产生副作用,那么就尽量不要写这样的表达式。

谜题1.4 二进制位操作符

请问,下面这个程序的输出是什么?

输出:

解惑1.4 二进制位操作符

1.4.1

PRINT宏:这个程序里的“PRINT”宏需要用到C语言预处理器的“#”操作符和字符串合并操作。这个“PRINT”宏有一个形式参数int。在扩展的时候,形式参数int将被替换为宏调用里的实际参数。在形式参数的前面加上一个“#”字符作为前缀,将使得实际参数被括在一对双引号里。于是

PRINT(x | y & z)

将被扩展为:

printf("x | y & z" " = %d\n", x | y & z)

C语言预处理器会自动将相邻的字符串合并,所以这又等价于:

printf("x | y & z = %d\n", x | y & z)

1.4.2

1.4.3

1.4.4

1.4.5

初始值:x=01

! x | x

((! x) | x)

((! TRUE) | x)

(FALSE | 01)

(0 | 01)

01

1.4.6

1.4.7

1.4.8

1.4.9

1.4.10

初始值:y= -08

y >>= 3

y = -08 >> 3

这道谜题的答案似乎显而易见:y=-1,可惜情况并非总是如此——有些计算机在进行右移操作时不保留操作数的符号位,而且C语言本身也不保证移位操作的结果在数学意义上肯定是正确的。不管怎样,还是使用更为清晰的除以8的表达方式,即“y/8”。

谜题1.5 关系操作符和条件操作符

请问,下面这个程序的输出是什么?

输出:

解惑1.5 关系操作符和条件操作符

1.5.1

1.5.2

1.5.3

1.5.4

1.5.5

谜题1.6 操作符的优先级和求值顺序

初始值:x= -1,y= -1,z= -1

请问,下面这个程序的输出是什么?

输出:

解惑1.6 操作符的优先级和求值顺序

1.6.1

1.6.2

1.6.3

初始值:x=1,y=1,z=1

++ x && ++ y && ++z

(((++ x) && (++ y)) && (++z))

((2 && 2) && (++z)),此时x=2,y=2

(TRUE && (++z))

(TRUE && TRUE),此时z=2

TRUE,即1

1.6.4

1.6.5

初始值:x= -1,y= -1,z= -1

++ x||++ y && ++z

((++ x)||((++ y) && (++z)))

(FALSE||((++ y) && (++z))),此时x=0

(FALSE||(FALSE && (++z))),此时y=0

(FALSE||(FALSE)

FALSE,即0

1.6.6

++ x && ++ y && ++z

(((++ x) && (++ y)) && (++z))

((FALSE&&(++ y)) && (++z)),此时x=0

(FALSE && (++z))

FALSE,即0

关于逻辑操作符的副作用:正如你们现在已经体会到的那样,C语言里的逻辑表达式的求值有一定的难度,因为是否需要对逻辑操作符的右操作数求值取决于其左操作数的求值结果。这种根据具体情况来决定是否对右操作数求值的做法是逻辑操作的一个有用的属性。可是,如果在逻辑表达式的右半部分隐藏着副作用,那么就难免会留下隐患——那些副作用可能会发作,也可能不会发作。一般说来,谨慎对待副作用总是没错的,这在逻辑表达式中就更为重要了。

[1].这里所说的“副作用”是指在执行一条本身并无语法错误的语句时会产生的难以确定的后果。C程序的副作用几乎都与变量的值(比如上面这个例子里的递增操作或一个赋值操作的计算结果)无法预料有关。

相关图书

代码审计——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版)

相关文章

相关课程