你必须知道的495个C语言问题

978-7-115-37676-3
作者: 【美】Steve Summit
译者: 孙云朱群英
编辑: 傅道坤
分类: C语言

图书目录:

详情

本书以问答的形式组织内容,讨论了学习或使用C语言的过程中经常遇到的一些问题。书中列出了C用户经常问的400多个经典问题,涵盖了初始化、数组、指针、字符串、内存分配、库函数、C预处理器等各个方面的主题,并分别给出了解答,而且结合代码示例阐明要点。

图书摘要

PEARSON

你必须知道的495个C语言问题

[美]Steve Summit 著

孙云 朱群英 译

C Programming FAQs:Frequently Asked Questions

人民邮电出版社

北京

图书在版编目(CIP)数据

你必须知道的495个C语言问题/(美)萨米特(Summit,S.)著;孙云,朱群英译.--北京:人民邮电出版社,2016.4

ISBN 978-7-115-37676-3

Ⅰ.①你… Ⅱ.①萨…②孙…③朱… Ⅲ.①语言—程序设计 Ⅳ.①TP312

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

版权声明

Authorized translation from the English language edition,entitled:C Programming FAQs:Frequently Asked Questions,0201845199 by Steve Summit,published by Pearson Education,Inc.,publishing as Addison-Wesley Professional.Copyright © 1996 by Addison-Wesley Publishing Company,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 &Telecom Press Copyright © 2016.

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

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

版权所有,侵权必究。

◆著 [美]Steve Summit

译 孙云 朱群英

责任编辑 傅道坤

责任印制 张佳莹 焦志炜

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

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

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

北京天宇星印刷厂印刷

◆开本:800×1000 1/16

印张:18.25

字数:393千字  2016年4月第1版

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

著作权合同登记号 图字:01-2008-5465号

定价:45.00元

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

反盗版热线:(010)81055315

内容提要

本书以问答的形式组织内容,讨论了学习或使用C语言的过程中经常遇到的一些问题。书中列出了C用户经常问的400多个经典问题,涵盖了初始化、数组、指针、字符串、内存分配、库函数、C预处理器等各个方面的主题,并分别给出了解答,而且结合代码示例阐明要点。

本书结构清晰,讲解透彻,是各高校相关专业C语言课程很好的教学参考书,也是各层次C程序员的优秀实践指南。

原版序

1979年的某段时间,我听到很多人在谈论C这个当时还挺新的语言和那本刚刚推出的书。我买了一本Brian Kernighan和Denis Ritchie写的The C Programming Language(也称K&R),但它在我的书架上空等了好一阵子,因为当时我并不急着需要它(况且我那时候还是一个余暇无多的大一新生)。后来证明这本书买得很幸运,因为当我最后拿起它以后,就再也没有放下了:从那以后,我就一直在用C语言编程。

1983年我结识了新闻组net.lang.c,这(以及它的后继者comp.lang.c)是一个绝佳的地方,你可以学习C语言的方方面面,发现别人关于C语言的各种疑问,认识到你可能根本还没有掌握关于C语言的一切。C语言尽管表面上很简单,但也还有一些并不显而易见的方面,有些问题不断有人问起。本书根据我从1990年5月开始在comp.lang.c上发布的常见问题(FAQ)列表收集了这样的一些问题,并提供了答案。

然而我得声明,这本书并不是对C语言的批评或诽谤。用户在使用时遇到困难,很容易迁怒于语言(或其他任何工具)或者要求正确设计的工具“应该”防止用户的误用。因此看到书中提及的各种误用以后,很容易将这样的书看作试图显示C语言的先天不足的长篇控诉。这实在是远悖我的本意。

如果我不认为C语言是一门伟大的语言,或者没有在这种语言的编程中获得那么多的乐趣,那我永远也学不到足够的关于C语言的知识来写出本书,而且也不会试图写出本书来让别人更爱用C语言。我很喜欢C语言,我教C的课并花时间参与网上讨论的原因之一,就是希望发现这门语言(或者说编程本身)在哪些方面比较难学,让人不易高效地编程。本书展示了我认识到的部分内容,这些问题毫无疑问就是人们遇到麻烦最多的,而答案则经过多年的反复修正,就是为了消除人们的麻烦。

如果这些答案中有任何错误,那么读者一定会遇到麻烦。尽管审稿人和我都尽力去除所有的错误,但从一部手稿中根除最后一个错误,就跟从程序中去掉最后一个bug一样困难。通过出版社转交或发往我的E-mail地址的任何修正和建议我都感激不尽。同时我也对任何错误的第一个发现者按惯例提供$1.00的报酬。如果你能够访问因特网,你可以在问题20.47提到的ftp和http网址中找到一份勘误表(和错误发现者的积分表)。

希望我已经澄清,这本书并不是对C语言的批评,也不是对我学习过的C语言的书或其作者的批评。从K&R中我不仅学到了C语言,还学会了编程。在试图用自己的贡献来丰富C语言的文献的时候,我唯一遗憾的就是本书没有做到K&R第二版发现的妙处,即“C不是一门复杂的语言,并不值得为它写本厚书”。我希望那些深深地欣赏C语言(及K&R)的简洁和精确的人,看到本书反反复复讲述某些东西,或用3种稍稍不同的方式讲述一个问题的时候不要生气。

尽管封面上只印着我一个人的名字,但这本书的背后却有许许多多的人,简直不知道该从哪里开始感谢。从某种意义上讲,comp.lang.c的每个读者(现在约有320 000人)都做出了贡献:这本书背后的FAQ列表是为comp.lang.c写的,因而本书也保留了comp.lang.c良好的讨论氛围。

我希望这本书也保留了我开始阅读net.lang.c时所学习的正确C语言编程的思想。因此,我要首先感谢我所知的一贯清楚解释这种思想的人:Doug Gwyn、Guy Harris、Karl Heuer、Henry Spencer和Chris Torek。这些绅士们多年来不断耐心、慷慨而睿智地解答各种无穷尽的问题。是我出头写下这些常见问题的,但不要以为是我给出了这些答案。我曾经是个学生(我想正是Guy解答了我提出的问题,现为本书问题5.10),我对走在前面的大师们感激不尽。这本书与其说是我的,不如说是他们的。但对书中的不足和错误我愿一力承担。

在线FAQ在变成本书的过程中增长了3倍,它的增长一度太快也变得有些笨拙了。Mark Brader、Vinit Carpenter、Stephen Clamage、Jutta Degener、Doug Gwyn、Karl Keuer、Joseph Kent和George Leach阅读了部分或全部的手稿,帮我对这一过程施加了一些控制,感谢他们大量的仔细建议和修正。他们的努力都源自一个共同的愿望,期待在编程社区中提高对C语言的整体理解。感谢他们的贡献。

这些审稿人中有3个长期以来也是在线FAQ的贡献者。感谢Jutta Degener和Karl Heuer多年来的帮助,尤其感谢Mark Brader,从5年前我第一次在comp.lang.c上发布FAQ以来,他就一直给予我批评。我不知道他哪里来的毅力提出那么多的建议和修正,其中部分还遭到了我持久顽固的拒绝,即使(正如我最后意识到的)它们实际的确是改进。你可以感谢Mark为本书提供的很多解释的表述形式,而弄糟的部分就责怪我吧。

还要感谢:Susan Cyr设计的封面;Bob Dinse和Eskimo North提供的网络环境,这对这样的项目至关重要;Bob Holland提供的计算机,这本书的大部分内容都是用它写成的;Pete Keleher提供的Alpha文本编辑器;华盛顿大学数学研究和工程图书馆提供的图书查询便利;华盛顿大学海洋学系借给我的磁带驱动器,用来访问我尘封的新闻组旧帖。

感谢Tanmoy Bhattacharya提供的问题11.11中的例子,感谢Arjan Kenter提供的问题13.7中的代码,感谢Tomohiko Sakamoto提供的问题20.37中的代码,感谢Roger Miller提供的问题11.38中的一行文字。

感谢世界各地的人们通过提供建议、修正、建设性的批评或其他支持对FAQ的贡献:Jamshid Afshar,Lauri Alanko,Michael B.Allen,David Anderson,Jens Andreasen,Tanner Andrews,Sudheer Apte,Joseph Arceneaux,Randall Atkinson,Kaleb Axon,Daniel Barker,Rick Beem,Peter Bennett,Mathias Bergqvist,Wayne Berke,DanBernstein,Tanmoy Bhattacharya,John Bickers,Kevin Black,Gary Blaine,Yuan Bo,Mark J.Bobak,Anthony Borla,Dave Boutcher,Alan Bowler,breadbox@muppetlabs.com,Michael Bresnahan,Walter Briscoe,Vincent Broman,Robert T.Brown,Stan Brown,John R.Buchan,Joe Buehler,Kimberley Burchett,Gordon Burditt,Scott Burkett,Eberhard Burr,Burkhard Burow,Conor P.Cahill,D’Arcy J.M.Cain,Christopher Calabrese,Ian Cargill,Vinit Carpenter,Paul Carter,Mike Chambers,Billy Chambless,C.Ron Charlton,Franklin Chen,Jonathan Chen,Raymond Chen,Richard Cheung,Avinash Chopde,Steve Clamage,Ken Corbin,Dann Corbit,Ian Cottam,Russ Cox,Jonathan Coxhead,Lee Crawford,Nick Cropper,Steve Dahmer,Jim Dalsimer,Andrew Daviel,James Davies,John E.Davis,Ken Delong,Norm Diamond,Jamie Dickson,Bob Dinse,dlynes@plenarysoftware,Colin Dooley,Jeff Dunlop,Ray Dunn,Stephen M.Dunn,Andrew Dunstan,Michael J.Eager,Scott Ehrlich,Arno Eigenwillig,Yoav Eilat,Dave Eisen,Joe English,Bjorn Engsig,David Evans,Andreas Fassl,Clive D.W.Feather,Dominic Feeley,Simao Ferraz,Pete Filandr,Bill Finke Jr.,Chris Flatters,Rod Flores,Alexander Forst,Steve Fosdick,Jeff Francis,Ken Fuchs,Tom Gambill,Dave Gillespie,Samuel Goldstein,Willis Gooch,Tim Goodwin,Alasdair Grant,W.Wesley Groleau,Ron Guilmette,Craig Gullixson,Doug Gwyn,Michael Hafner,Zhonglin Han,Darrel Hankerson,Tony Hansen,Douglas Wilhelm Harder,Elliotte Rusty Harold,Joe Harrington,Guy Harris,John Hascall,Adrian Havill,Richard Heafitheld,Des Herriott,Ger Hobbelt,Sam Hobbs,JoelRay Holveck,Jos Horsmeier,Syed Zaeem Hosain,Blair Houghton,Phil Howard,Peter Hryczanek,James C.Hu,Chin Huang,Jason Hughes,David Hurt,Einar Indridason,Vladimir Ivanovic,Jon Jagger,Ke Jin,Kirk Johnson,David Jones,Larry Jones,Morris M.Keesan,Arjan Kenter,Bhaktha Keshavachar,James Kew,Bill Kilgore,Darrell Kindred,Lawrence Kirby,Kin-ichi Kitano,Peter Klausler,John Kleinjans,Andrew Koenig,Thomas Koenig,Adam Kolawa,Jukka Korpela,Przemyslaw Kowalczyk,Ajoy Krishnan T,Anders Kristensen,Jon Krom,Markus Kuhn,Deepak Kulkarni,Yohan Kun,B.Kurtz,Kaz Kylheku,Oliver Laumann,John Lauro,Felix Lee,Mike Lee,Timothy J.Lee,Tony Lee,Marty Leisner,Eric Lemings,Dave Lewis,Don Libes,Brian Liedtke,Philip Lijnzaad,James D.Lin,Keith Lindsay,Yen-Wei Liu,Paul Long,Patrick J.LoPresti,Christopher Lott,Tim Love,Paul Lutus,Mike McCarty,Tim McDaniel,Michael MacFaden,Allen Mcintosh,J.Scott McKellar,Kevin McMahon,Stuart MacMartin,John R.MacMillan,Robert S.Maier,Andrew Main,Bob Makowski,Evan Manning,Barry Margolin,George Marsaglia,George Matas,Brad Mears,Wayne Mery,De Mickey,Rich Miller,Roger Miller,Bill Mitchell,Mark Moraes,Darren Morby,Bernhard Muenzer,David Murphy,Walter Murray,Ralf Muschall,Ken Nakata,Todd Nathan,Taed Nelson,Pedro Zorzenon Neto,Daniel Nielsen,Landon Curt Noll,Tim Norman,Paul Nulsen,David O’Brien,Richard A.O’Keefe,Adam Kolawa,Keith Edward O’hara,James Ojaste,Max Okumoto,Hans Olsson,Thomas Otahal,Lloyd Parkes,Bob Peck,Harry Pehkonen,Andrew Phillips,Christopher Phillips,Francois Pinard,Nick Pitfield,Wayne Pollock,Polver@aol.com,Dan Pop,Don Porges,Claudio Potenza,Lutz Prechelt,Lynn Pye,Ed Price,Kevin D.Quitt,Pat Rankin,Arjun Ray,Eric S.Raymond,Christoph Regli,Peter W.Richards,James Robinson,Greg Roelofs,Eric Roode,Manfred Rosenboom,J.M.Rosenstock,Rick Rowe,Michael Rubenstein,Erkki Ruohtula,John C.Rush,John Rushford,Kadda Sahnine,Tomohiko Sakamoto,Matthew Saltzman,Rich Salz,Chip Salzenberg,Matthew Sams,Paul Sand,David W.Sanderson,Frank Sandy,Christopher Sawtell,Jonas Schlein,Paul Schlyter,Doug Schmidt,Rene Schmit,Russell Schulz,Dean Schulze,Jens Schweikhardt,Chris Sears,Peter Seebach,Gisbert W.Selke,Patricia Shanahan,Girija Shanker,Clinton Sheppard,Aaron Sherman,Raymond Shwake,Nathan Sidwell,Thomas Siegel,Peter da Silva,Andrew Simmons,Joshua Simons,Ross Smith,Thad Smith,Henri Socha,Leslie J.Somos,Eric Sosman,Henry Spencer,David Spuler,Frederic Stark,James Stern,Zalman Stern,Michael Sternberg,Geoff Stevens,Alan Stokes,Bob Stout,Dan Stubbs,Tristan Styles,Richard Sullivan,Steve Sullivan,Melanie Summit,Erik Talvola,Christopher Taylor,Dave Taylor,Clarke Thatcher,Wayne Throop,Chris Torek,Steve Traugott,Brian Trial,Nikos Triantafillis,Ilya Tsindlekht,Andrew Tucker,Goran Uddeborg,Rodrigo Vanegas,Jim Van Zandt,Momchil Velikov,Wietse Venema,Tom Verhoeff,Ed Vielmetti,Larry Virden,Chris Volpe,Mark Warren,Alan Watson,Kurt Watzka,Larry Weiss,Martin Weitzel,Howard West,Tom White,Freek Wiedijk,Stephan Wilms,Tim Wilson,Dik T.Winter,Lars Wirzenius,Dave Wolverton,Mitch Wright,Conway Yee,James Youngman,Ozan S.Yigit和Zhuo Zang。[1]我试图记录下我采纳建议的每一个人,但我担心可能还是遗漏了一些,对那些名字本该出现在这里而没有出现的人,我诚恳地致以道歉。

最后,我要感谢Addison-Wesley的编辑Debbie Lafferty,她有一天发邮件问我是否有兴趣写这本书。我有兴趣,你现在手里拿的就是它。我希望这本书能让你跟我一样觉得C语言编程令人快乐。

Steve Summit

scs@eskimo.com

1995年7月于华盛顿州西雅图市

[1].致谢名单根据最新资料整理。——编者注

前言

你可能在酒吧或聚会上有这样的经历,有人跟你打赌让你做一些看似简单,但最后却限于人体特质或物理规律而根本无法完成的事情。跟你打赌的人知道,他挑战的人越多,他持续获胜的可能性就越大,因为这些特质或规律虽然十分隐晦,却是相当稳定、可以预测的。

同样,如果你让很多人来完成一个复杂任务,如学习C语言,他们肯定会遇到同样的困难,提出同样的问题。在最初设计任务的时候这些困难和问题也许不能预见,而答案也恐怕是“后见之明”,但人们依然会不断遇到同样的困难,也会不断提出同样的问题。这些困难和问题并不表明任务就不能完成,只能说明它比较困难,从而变得很有趣。

毫不奇怪,这些问题在因特网尤其是互动讨论的新闻组上不断被问起。将这些常见问题收集起来的想法是顺理成章的,顺着这一想法形成了常见问题(FAQ)列表的传统。FAQ列表未必总能达成最初设想的减少常见问题发生率的目的,但如果问题是一贯的,那它们被经常问到并纳入FAQ列表的事实说明,它们也许正是你或本书的其他读者要问的问题。

关于本书

多数(关于C语言或其他任何主题的)书都是从作者的角度写成的。它们是用一种作者自己明白的方式来讨论作者认为你应该知道的主题。如果那种方式不适合你(在某种程度上,也不可能适合,因为作者预先已经知道那些内容,而你却全然不知),你很有可能被弄得满头雾水。

而这本书却不一样,它由400多个问题组织而成,所有问题都是人们在学习C语言编程的过程中提出的真实问题。本书不是针对作者认为重要的议题,而是针对真正的读者认为重要的议题,讨论他们提出的问题。如果你在学习或使用C语言,而你遇到的问题在别的书里都找不到答案,那么你很有可能会在这里找到答案。

本书不能保证解答你在C语言编程中遇到的所有问题,因为在编程实践中产生的很多问题都跟你的问题领域有关,而本书只涵盖了C语言本身。正如它不能涵盖每个人试图用C语言解决的每个问题的每个方面,本书也不能涵盖每个人用C语言编程时用的每个操作系统的方方面面或每个人希望用C语言实现的每个算法。具体的问题、具体的操作系统和通用的算法都有专门的书和其他材料进行讨论。不过,某些跟操作系统和算法相关的问题十分常见,因此第19章和第20章对其中一些问题提供了简单的、介绍性的答案,但不要期待它们很完备。

本书中的问题是人们在读完一本C语言入门书或上了一门C语言课程之后常常会提到的。因此本书不是一点点教你学C语言,也不会讨论任何C语言教材都会讨论的基础问题。而且,本书的答案在极大程度上都应该是绝对正确,不会传播任何误解的。因此有些答案初看起来显得有些过于详细,它们要向你提供完整的图景,而不能过于简化而略去了重要的细节。(毕竟,很多这类细节正是本书的问答中提到的诸多错误观点的根源。)在这些详尽的答案中,必要的地方会有捷径和简化处理,而在术语表中你会找到术语的精确定义,帮你准确解释很多问题。当然,这些捷径和简化处理都是安全的,它们不会导致以后的误解,而如果你需要完整的版本,你总是可以找到更详尽的解释或者查到相关的参考文献。

正如我们会在第3章和第11章中看到的那样,C语言的标准定义并没有规定每个写成的C程序的行为。有些程序陷入了各种灰色地带:它们可能在某些系统上能运行,而且严格说来也不非法,但却不能确保在各处都能运行。本书介绍的是可移植的C编程,因此在答案中建议,只要可能就不用不可移植的方法。

本书所基于的在线FAQ列表是一种对话的方式,当人们看不懂的时候,就会直言不讳地提出来。这样的及时反馈非常有利于改善解答的形式。尽管出成书以后就不能这么动态了,但这样的对话方式依然适用:欢迎你的意见、批评和建议。如果你能访问因特网,可以发送意见到scs@eskimo.com,或者寄信让出版社转交。本书的堪误表可以在因特网上获得,并在网上进行维护,具体参见问题20.47中的信息。

问题格式

本书的内容包含一系列的问题及答案。很多答案中还列举了参考书目;有些还有脚注,如果你觉得它们太吹毛求疵,可以略过。

等宽字体用来表示C语法(函数和变量名、关键字等),也用来表示一些操作系统命令(如cc等)。偶尔出现的tty(4)这样的符号表示UNIX Programmer’s Manual第4章的“tty”一节。

代码示例

这是本关于C语言的书,因此必要处给出了许多C程序段。这些例子主要用来清楚地展示。它们不一定总是用最高效的方式写成的,让它们更“快”往往会导致更不清楚。(关于代码效率的信息参见问题20.14。)它们通常都是用现代的ANSI风格的语法写成。如果你还在使用“经典的(classic)”编译器,参见问题11.31关于转换的提示。

作者和出版社欢迎你在自己的程序中使用和修改这些代码片段,当然如果你能提及作者,我们将十分感激。(某些片段来自其他来源,也采用同样的策略。如果你使用这些代码,请感谢对应的贡献者。)较大的例子的源码可以通过匿名ftp从aw.com的cseng/authors/summit/cfaq下载(参见问题18.12)。

为了强调某些要点,我不得不举一些不能那样做的反例子。在答案中,这样的代码片段都用/*WRONG*/这样明确的注释标示出来了,提示你不要模仿。(问题中的代码片段通常没有类似的标示,但从问题本身的提法上应该很容易看出代码片段有问题,因为这些问题通常是“为什么这样做不行”。)

组织

如前所述,本书的问题来自人们在实际的工作或学习中提出的真实问题,这些问题有时不能很好地归类。很多问题涉及多个主题:看似内存分配的问题实际原因却是声明错误。(有些问题在两章中都会出现,以便它们更容易找到。)无论如何,这不是一本必须从头读到尾的书:使用目录、索引和问题之间的交叉索引来找你感兴趣的主题。(如果你有空闲时间从头读到尾,可能会遇到你没有想到过的问题的答案。)

通常,在开始写代码之前要先声明数据结构,因此第1章从声明和初始化开始谈起。C语言的结构、联合和枚举类型足够复杂,值得为它们单独写一章。第2章讨论了它们的声明和使用方法。

程序中多数的工作由表达式语句完成,这是第3章的主题。

第4章到第7章讨论了许多新C程序员最头疼的内容:指针。第4章从总体上讨论指针,第5章专门讨论空指针这一特殊情况,第6章描述了指针和数组的关系,而第7章则探究了指针错乱背后的真正问题:底层的内存分配。

几乎所有的C程序都会操作字符和字符串,但这些类型却在语言的底层实现。程序员通常需要负责正确管理这些类型,第8章收集了管理这些类型时出现的问题。类似地,C语言也没有正式的布尔类型,第9章简单讨论了C语言的布尔表达式以及(在需要的时候)实现用户定义的布尔类型的正确方法。

C语言预处理器(编译器的一部分,负责处理#include和#define指令——实际上负责所有以#开始的行)非常独特,它几乎就是一门独立的语言,因此也单独有一章:第10章。

ANSI C标准委员会(X3J11),在澄清C语言的定义让它简单易懂的过程中,引入了一些新的功能并做出了一些重要的改变。与ANSI标准C相关的问题收集在第11章。如果你用过ANSI之前的C语言(也称“K&R”或“经典”C),你会觉得第11章介绍的差别很有用处。另一方面,如果你已经在顺利地使用ANSI C,那么二者的功能差别可能就没什么意思了。无论如何,第11章中涉及其他主题(如声明、预处理器和库函数等)的所有问题也会在其他章节出现或被交叉引用。

C语言的定义相对简洁,部分原因是许多功能并不是由语言本身而是由库函数提供的。其中最重要的就是“标准I/O”库,或称stdio函数,这在第12章讨论。其他的库函数在第13章讨论。

第14章和第15章讨论了两个更高级的主题:浮点数和可变参数列表。无论使用哪种系统或哪种语言,浮点数运算都颇有技巧。第14章简述了一些一般的浮点数问题和一些C语言特有的问题。函数可以接受可变参数的可能性虽然有人认为没必要而且危险,但有时却很方便而且也是printf函数的核心功能。处理可变参数列表的技巧在第15章讨论。

如果你对前面的内容已经比较熟悉,那么第16章的问题你可能希望最先看到:它们涉及偶尔出现的奇怪问题和程序中冒出的极难跟踪的神秘bug。

当有两种以上编写某个程序的“正确”方法时(通常都有),人们往往根据主观的标准进行选择,这些标准不止跟代码正确编译和运行有关系。第17章讨论了一些编程风格上的问题。

你不能孤立地创建C程序:你需要编译器,可能还需要一些附加的文档、源码或工具。第18章讨论了一些可获得的工具和资源,包括lint,一个快要被遗忘但曾经是检查程序正确性和可移植性的不可或缺的工具。

如前所述,C语言没有规定让一个真正程序运行所需的各个方面。像“怎样从键盘直接读入字符而不用等回车键”和“如何得到文件的大小”这样的问题十分常见,但C语言却没有给定答案,这些操作依赖底层的操作系统提供的工具。第19章提出了一些这样的问题,同时提供了一些在常用操作系统下的答案。

最后,第20章收集了一些不能放入其他章节的杂项问题:位操作、效率、算法、C语言和其他语言的关系,以及一些琐碎的问题。(第20章的介绍对内容有更详尽的划分。)

最后用两个跟本书而不是C语言关系更密切的预备问题来结束这个介绍。

问:既然因特网上有免费版本可以用,我为什么还要花钱买这本书呢?

答:这本书包含的内容大约是发表在comp.lang.c上的内容的三倍,而且尽管电子文档有各种优势,但阅读这么大的信息量用印刷的形式还是更容易一些。(从网上下载再打印这么多内容会花很多时间,而版面也没有这么漂亮。)

问:“FAQ”如何发音?

答:我的发音是“eff ay kyoo”,而且我相信这就是FAQ在“发明”时的最初的发音。很多人现在读作“fack”,这很容易令人联想到单词“fact”。对复数形式,我会读作“eff ay kyooze”,但很多人读作“fax”。这些发音都没有什么严格的对错,“FAQ”是个新词,而流行的用法会在任何新词的演化过程中扮演重要的角色。

(另外,还有一个类似的疑问“FAQ”是仅仅表示某个问题,还是包括该问题和答案,还是全部问题和答案呢?)

现在,开始真正的问题之旅!

第1章 声明和初始化

C语言的声明语法本身实际上就是一种小的编程语言。一个声明包含如下几个部分(但是并非都必不可少):存储类型、基本类型、类型限定词和最终的声明符(也可能包含初始化列表)。每个声明符不仅声明一个新的标识符,同时也表明标识符是数组、指针、函数还是其他任意的复杂组合。基本的思想是让声明符模仿标识符的最终用法。(问题1.21将会更加详细地讨论这种“声明模仿使用”的关系!)

基本类型

让一些程序员惊奇的是,尽管C语言是一种相当低级的语言,但它的类型体系仍然略显抽象。语言本身并没有精确定义基本类型的大小和表示法。

1.1

问:我该如何决定使用哪种整数类型?

答:如果可能用到很大的数值(大于32 767或小于−32 767),就使用long型。否则,如果空间很重要(例如有很大的数组或很多的结构),就使用short型。除此之外,就用int型。如果定义明确的溢出特征很重要而负值无关紧要,或者希望在操作二进制位和字节时避免符号扩展的问题,请使用对应的unsigned类型。(但是,在表达式中混用有符号和无符号值的时候,要特别注意。参见问题3.21。)

尽管字符类型(尤其是unsigned char型)可以当成“小”整数使用,但这样做有时候很麻烦,不值得。编译器需要生成额外的代码来进行char型和int型之间的转换(导致目标代码量增大),而且不可预知的符号扩展也会带来一堆麻烦。(使用unsigned char会有所帮助。类似的问题参见问题12.1。)

在决定使用float型还是double型时也有类似的空间/时间权衡。(很多编译器在表达式求值的时候仍然把所有的float型转换为double型进行运算)。但如果一个变量的地址确定且必须为特定的类型时,以上规则就不再适用。

很多时候,人们错误地认为C语言类型的大小都有精确的定义。事实上,能够确保的只有如下几点:

char 类型可以存放小于等于127的值;[1]

short int和int可以存放小于等于32767的值;

long int可以保存小于等于2147483647的值;

char至少有8位,short int和int至少有16位,而long int则至少有32位。在C99中,long long至少有64位。(各类型的有符号和无符号版本的大小可以确保一致。)

根据ANSI C的规定,可以在头文件<limits.h>中找到特定机器下上述类型的最大和最小值,具体如下表所示。

表中的值是标准能够确保的最小值。很多系统允许更大的值,但可移植的程序不能依赖这些值。

如果因为某种原因需要声明一个有精确大小的变量,确保像C99的<inttypes.h>那样用某种适当的typedef封装这种选择。通常,需要精确大小的唯一的合理原因是试图符合某种外部强加的存储布局。也可参见问题1.3和20.5。

参考资料:[18,Sec.2.2 p.34]

[19,Sec.2.2 p.36,Sec.A4.2 pp.195-196,Sec.B11 p.257]

[8,Sec.5.2.4.2.1,Sec.6.1.2.5]

[11,Secs.5.1,5.2 pp.110-114]

1.2

问:为什么不精确定义标准类型的大小?

答:尽管跟其他的高级语言比起来,C语言是相对低级的,但它还是认为对象的具体大小应该由具体的实现来决定。(在C语言中,唯一能够让你以二进制位的方式指定大小的地方就是结构中的位域。参见问题2.26和2.27。)多数程序不需要精确控制这些大小,那些试图达到这一目的的程序如果不这样做也许会更好。

类型int代表机器的自然字长。这是多数整型变量的当然之选。关于整型的选择,参见问题1.1。另请参见问题12.45和20.5。

1.3

问:因为C语言没有精确定义类型的大小,所以我一般都用typedef定义int16和int32。然后根据实际的机器环境把它们定义为int、short、long等类型。这样看来,所有的问题都解决了,是吗?

答:如果你真的需要精确控制类型大小,这的确是正确的方法。但还是有几点需要注意。

在某些机器上可能没有严格的对应关系。(例如,有36位的机器。)

如果定义int16和int32只是为了表明“至少”这么长,则没有什么实际意义。因为int和long类型已经分别被定义为“至少16位”和“至少32位”。

typedef定义对于字节顺序问题不能提供任何帮助。(例如,当你需要交换数据或者满足外部强加的存储布局时。)

你再也不必自己定义这些类型了,因为标准头文件<inttypes.h>已经定义了标准类型名称int16_t和uint32_t等。

参见问题10.16和20.5。

1.4

问:新的64位机上的64位类型是什么样的?

答:C99标准定义了long long类型,其长度可以保证至少64位,这种类型在某些编译器上实现已经颇有时日了。其他的编译器则实现了类似_ _longlong的扩展。另一方面,也可以实现16位的short、32位的int和64位的long int。有些编译器正是这样做的。

参见问题18.19。

参考资料:[9,Sec.5.2.4.2.1,Sec.6.1.2.5]

指针声明

多数有关指针的问题出现在第4章至第7章,但这里的两个问题和声明的关系特别紧密。

1.5

问:这样的声明有什么问题?

char *p1,p2;

我在使用p2的时候报错了。

答:这样的声明没有任何问题——但它可能不是你想要的。指针声明中的*号并不是基本类型的一部分,它只是包含被声明标识符的声明符(declarator)的一部分(参见问题1.21)。也就是说,在C语言中,声明的语法和解释并非

类型 标识符;

而是

基本类型 生成基本类型的东西;

其中“生成基本类型的东西”——声明符——或者是一个简单标识符,或者是如同*p、a[10]或f()这样的符号,表明被声明的变量是指向基本类型的指针、基本类型的数组或者返回基本类型的函数。(当然,更加复杂的声明符也可以这样组成)。

在问题里的声明中,无论空白的位置暗示了什么,基本类型都是char,而第一个声明符是“*p1”。因为声明符中带有*号,所以这表明p1是一个指向char类的指针。而p2的声明符中却只有p2,因此p2被声明成了普通的char型变量。这可能并非你所希望。在一行代码中声明两个指针可使用如下方式:

char *p1,*p2;

因为*号是声明符的一部分,所以最好像上面这样使用空白;写成char*往往导致错误和困惑。

参见问题1.13。

也可参考Bjarne Stroustrup的意见(http://www.hymnsandcarolsofchristmas.com/santa/virginia’s question.htm )。

1.6

问:我想声明一个指针,并为它分配一些空间,但却不行。这样的代码有什么问题?

char *p;

*p=malloc(10);

答:这里声明的指针是p而不是*p。参见问题4.2。

声明风格

在使用函数和变量之前声明它们并不只是为了消除编译器的警告,它也为编程项目注入了有用的秩序。当项目中的声明安排得井然有序的时候,(类型)不匹配和其他的困难就可以更容易地避免,同时编译器也更容易找到出现的错误。

1.7

问:怎样声明和定义全局变量和函数最好?

答:首先,尽管一个全局变量或函数可以(在多个编译单元中)有多处“声明(declaration)”,但是“定义(definition)”却最多只能允许出现一次。对于全局变量,定义是真正分配空间并赋初值(如果有)的声明。对于函数,定义是提供函数体的“声明”。

例如,这些是声明:

extern int i;

extern int f();

而这些是定义:

int i=0;

int f()

{

return 1;

}

(事实上,在函数的声明中,关键字extern是可选的。参见问题1.11。)

当希望在多个源文件中共享变量或函数时,需要确保定义和声明的一致性。最好的安排是在某个相关的.c文件中定义,然后在头.h(文件)中进行外部声明,在需要使用的时候,只要包含对应的头文件即可。定义变量的.c文件也应该包含该头文件,以便编译器检查定义和声明的一致性。

这条规则提供了高度的可移植性:它和ANSI/ISO C标准一致,同时也兼容大多数ANSI前的编译器和连接器。(UNIX编译器和连接器常常使用允许多重定义的“通用模式”,只要保证最多对一处进行初始化就可以了。这种方式被ANSI C标准称为一种“通用扩展”,没有语带双关的意思。有几个很老的系统可能曾经要求使用显式的初始化来区别定义和外部声明。)

可以使用预处理技巧来使类似

DEFINE(int,i);

的语句在一个头文件中只出现一次,然后根据某个宏的设定在需要的时候转化成定义或声明。但不清楚这样带来的麻烦是否值得,因为尽量减少全局变量的数量往往是个更好的主意。

把全局声明放到头文件绝对是个好主意:如果希望让编译器检查声明的一致性,一定要把全局声明放到头文件中。特别是,永远不要把外部函数的原型放到.c文件中。如果函数的定义发生改变,很容易忘记修改原型,而错误的原型贻害无穷。

参见问题1.24、10.6、17.2和18.7。

参考资料:[18,Sec.4.5 pp.76-77]

[19,Sec.4.4 pp.80-81]

[8,Sec.6.1.2.2,Sec.6.7,Sec.6.7.2,Sec.G.5.11]

[14,Sec.3.1.2.2]

[11,Sec.4.8 pp.101-104,Sec.9.2.3 p.267]

[22,Sec.4.2 pp.54-56]

1.8

问:如何在C中实现不透明(抽象)数据类型?

答:参见问题2.4。

1.9

问:如何生成“半全局变量”,就是那种只能被部分源文件中的部分函数访问的变量?

答:这在C语言中办不到。如果不能或不方便在一个源文件中放下所有的函数,那么有两种常用的解决方案:

(1)为一个库或相关函数的包中的所有函数和全局变量增加一个唯一的前缀,并警告包的用户不能定义和使用除文档中列出的公用符号以外的任何带有相同前缀的其他符号。(换言之,文档中没有提及的带有相同前缀的全局变量被约定为“私有”。)

(2)使用以下划线开头的名称,因为这样的名称普通代码不能使用。(关于更多的信息及对用户命名空间和实现命名空间之间的“无人地带”的描述,参见问题1.30。)

也可以使用一些特殊的连接器参数来调整名称的可见性,但这已经超出了C语言的范围了。

存储类型

我们已经讨论了声明的两个部分:基本类型和声明符。下面的几个问题将讨论存储类型,它决定了所声明对象或函数的可见性和生命周期(又称“作用域”和“持续性”)。

1.10

问:同一个静态(static)函数或变量的所有声明都必须包含static存储类型吗?

答:语言标准并没有严格规定这一点(最重要的是第一个声明必须包含static),但是规则却比较复杂,而且对函数和数据对象的规定不太一致。(这个领域有很多历史变化。)因此,最安全的做法是让static一致地出现在所有的定义和声明中。

外部链接:Jutta Degener 的一篇文章(http://c-faq.com/decl/static.jd.html)解释了静态变量和静态函数的规则中的微妙区别。

参考资料:[8,Sec.6.1.2.2]

[14,Sec.3.1.2.2]

[11,Sec.4.3 p.75]

1.11

问:extern在函数声明中是什么意思?

答:存储类型extern只对数据声明有意义。对于函数声明,它可以用作一种格式上的提示,表明函数的定义可能在另一个源文件中,但在

extern int f();

int f();

之间并没有实质的区别。

参考资料:[8,Sec.6.1.2.2,Sec.6.5.1]

[14,Sec.3.1.2.2]

[11,Secs.4.3,4.3.1 pp.75-76]

1.12

问:关键字auto到底有什么用途?

答:毫无用途,它已经过时了。(它是从C语言的无类型前身B语言中继承下来的。在B语言中,没有像int这样的关键字,声明必须包含存储类型。)参见问题20.43。

参考资料:[18,Sec.A8.1 p.193]

[8,Sec.6.1.2.4,Sec.6.5.1]

[11,Sec.4.3 p.75,Sec.4.3.1 p.76]

类型定义(typedef)

typedef关键字尽管在语法上是一种存储类型,但正如其名称所示,它用来定义新的类型名称,而不是定义新的变量或函数。

1.13

问:对于用户定义类型,typedef和#define有什么区别?

答:一般来说,最好使用typedef,部分原因是它能正确处理指针类型。例如,考虑这些声明:

typedef char *String_t;

#define String_d char *

String_t s1,s2;

String_d s3,s4;

s1、s2和s3都被定义成了char *,但s4却被定义成了char型。这可能并非原来所希望的。(参见问题1.5。)

#define也有它的优点,因为可以在其中使用#ifdef(参见问题10.15)。另一方面,typedef具有遵守作用域规则的优点(也就是说,它可以在一个函数或块内声明)。

参见问题1.17、2.23、11.12和15.11。

参考资料:[18,Sec.6.9 p.141]

[19,Sec.6.7 pp.146-147]

[22,Sec.6.4 pp.83-84]

1.14

问:我似乎不能成功定义一个链表。我试过

typedef struct {

char *item;

NODEPTR next;

} *NODEPTR;

但是编译器报了错误信息。难道在C语言中结构不能包含指向自己的指针吗?

答:C语言中的结构当然可以包含指向自己的指针。[19]的6.5节的讨论和例子表明了这点。

这里的问题在于typedef。typedef定义了一个新的类型名称。在更简单的情况下[2],可以同时定义一个新的结构类型和typedef类型。但在这里不行。不能在定义typedef类型之前使用它。在上边的代码片段中,在next域声明的地方还没有定义NODEPTR。

要解决这个问题,首先赋予这个结构一个标签(“struct node”)。然后,声明“next”域为“struct node *”,或者分开typedef声明和结构定义,或者两者都采纳。以下是一个修正后的版本:

typedef struct node {

char *item;

struct node *next;

} *NODEPTR;

也可以在声明结构之前先用typedef,然后就可以在声明next域的时候使用类型定义NODEPTR了:

struct node;

typedef struct node *NODEPTR;

struct node {

char *item;

NODEPTR next;

};

这种情况下,你在struct node还没有完全定义的情况下就使用它来声明一个新的typedf,这是允许的。

最后,这是一个两种建议都采纳的修改方法:

struct node {

char *item;

struct node *next;

};

typedef struct node *NODEPTR;

使用哪种方式不过是个风格问题。参见第17章。

参见问题1.15和2.1。

参考资料:[18,Sec.6.5 p.101]

[19,Sec.6.5 p.139]

[8,Sec.6.5.2,Sec.6.5.2.3]

[11,Sec.5.6.1 pp.132-133]

1.15

问:如何定义一对相互引用的结构?我试过

typedef struct {

int afield;

BPTR bpointer;

} *APTR;

typedef struct {

int bfield;

APTR apointer;

} *BPTR;

但是编译器在遇到第一次使用BPTR的时候,它还没有定义。

答:与问题1.14类似,这里的问题不在于结构或指针,而在于类型定义。首先,我们定义两个结构标签,然后(不用typedef)定义链接指针:

struct a {

int afield;

struct b *bpointer;

};

struct b {

int bfield;

struct a *apointer;

};

对于结构a中的域定义struct b *bpointer,尽管编译器此时尚未完成结构b(它在此处还处于“未完成”阶段)的定义,但它仍然可以接受。有时候需要在这对定义之前加上这样一行:

struct b;

这个空声明将这对结构声明(如果处于某个内部作用域)同外部作用域的struct b区分开来。

声明了两个带结构标签的结构之后,可以再分别定义两个类型。

typedef struct a *APTR;

typedef struct b *BPTR;

另外也可以先定义两个类型,然后再使用这些类型来定义链接指针域:

struct a;

struct b;

typedef struct a *APTR;

typedef struct b *BPTR;

struct a {

int afield;

BPTR bpointer;

};

struct b {

int bfield;

APTR apointer;

};

参见问题1.14。

参考资料:[19,Sec.6.5 p.140]

[35,Sec.3.5.2.3]

[8,Sec.6.5.2.3]

[11,Sec.5.6.1 p.132]

1.16

问:struct {...} x1;和typedef struct{...} x2;这两个声明有什么区别?

答:参见问题2.1。

1.17

问:“typedef int (*funcptr)();”是什么意思?

答:它定义了一个类型funcptr,表示指向返回值为int型(参数未指明)的函数的指针。它可以用来声明一个或多个函数指针。

funcptr fp1,fp2;

这个声明等价于以下这种更冗长而且可能更难理解的写法:

int (*pf1)(),(*pf2)();

参见问题1.21、4.12和15.11。

const限定词

C语言的声明还包括类型限定词。这是ANSI C中新提出来的。关于限定词的问题收集在第11章。

1.18

问:我有这样一组声明:

typedef char *charp;

const charp p;

为什么是p而不是它指向的字符为const?

答:参见问题11.12。

1.19

问:为什么不能像下面这样在初始式和数组维度值中使用const值?

const int n=5;

int a[n];

答:参见问题11.9。

1.20

问:const char *p、char const *p和char *const p有什么区别?

答:参见问题11.10和1.21。

复杂的声明

C语言的声明可以任意复杂。一旦你熟悉了解读它们的方法,即使最复杂的声明也可以看得明白。不过,首先来说,那些令人眼花缭乱的复杂声明很少是真正必要的。如果你不希望用*(*(a[N])())()这样的神秘声明把你的程序变得混乱不堪,你总是可以像问题1.21的选择(2)那样,用几个类型定义清楚明了地完成。

1.21

问:怎样建立和理解非常复杂的声明?例如定义一个包含N个指向返回指向字符的指针的函数的指针的数组?

答:这个问题至少有以下3种答案:

(1)char *(*(*a[N])())();

(2)用typedef逐步完成声明:

typedef char *pc;      /* pointer to char */

typedef pc fpc();      /* function returning pointer to char */

typedef fpc *pfpc;     /* pointer to above */

typedef pfpc fpfpc();    /* function returning...*/

typedef fpfpc *pfpfpc;   /* pointer to...*/

pfpfpc a[N];         /* array of...*/

(3)使用cdecl程序,它可以在英文描述和C语言源码之间相互翻译。你只需要提供用自然语言描述的类型,cdecl就能翻译成对应的C语言声明:

cdecl>declare a as array of pointer to function returning

pointer to function returning pointer to char

char *(*(*a[])())()

cdecl也可以用于解释复杂的声明(向它提供一个复杂的声明,它就会输出对应的英文解释)。对于强制类型转换和在复杂的函数定义中弄清参数应该进入哪一对括号,cdecl也大有裨益。在comp.sources.unix的第14卷可以找到cdecl的各种版本(参见问题18.20和文献[19])(如同在上述的复杂函数定义中)。参见问题18.1。

C语言中的声明令人困惑的原因在于,它们由两个部分组成:基本类型和声明符,后者包含了被声明的标识符(即名称)。声明符也可以包含字符*、[]和(),表明这个名称是基本类型的指针、数组以及为返回类型的函数或者某种组合[3]。例如,在

char *p;

中,基本类型是char,标识符是pc,声明符是*pc;这表明*pc是一个char(这也正是“声明模仿使用”的含义)。

解读复杂C声明的一种方法是遵循“从内到外”的阅读方法,并谨记[]和()比*的结合度更紧。例如,对于声明

char *(*pfpc )();

我们可以看出pfpc是一个函数(从()看出)的指针(从内部的*看出),而函数则返回char型的指针(从外部的*可以看出)。当我们后来使用pfpc的时候*(*pfpc)()(pfpc所指的函数的返回值指向的值)是一个char型。

另一种分析这种复杂声明的方法是,遵循“声明模仿使用”的原则逐步分解声明:

*(*pfpc)()是一个 char

(*pfpc)()  是一个 指向char的指针

(*pfpc)   是一个 返回char型指针的函数

pfpc    是一个 指向返回char型指针的函数的指针

如果你希望将复杂声明像这样表达得更加清楚,可以用一系列的typedef把上面的分析表达出来,如前文所述的第2种方法所示。

这些例子中的函数指针声明还没有包括函数的参数类型信息。如果参数中又有复杂类型,这时候的声明就真的有些混乱了。(现代版本的cdecl同样会有所帮助。)

参考资料:[19,Sec.5.12 p.122]

[35,3.5ff (esp.3.5.4)]

[8,Sec.6.5ff (esp.Sec.6.5.4)]

[11,Sec.4.5 pp.85-92,Sec.5.10.1 pp.149-150]

1.22

问:如何声明返回指向同类型函数的指针的函数?我在设计一个状态机,用函数表示每种状态,每个函数都会返回一个指向下一个状态的函数的指针。可我找不到任何方法来声明这样的函数——感觉我需要一个返回指针的函数,返回的指针指向的又是返回指针的函数……,如此往复,以至无穷。

答:你不能直接完成这个任务。一种方法是让函数返回一个一般的函数指针(参见问题4.13),然后在传递这个指针的时候进行适当的类型转换:

typedef int (*funcptr)(); /* generic function pointer */

typedef funcptr (*ptrfuncptr)(); /* ptr to fcn returning g.f.p.*/

funcptr start(),stop();

funcptr state1(),state2(),state3();

void statemachine()

{

ptrfuncptr state=start;

while(state !=stop)

state=(ptrfuncptr)(*state)();

}

funcptr start(){

}

return (funcptr)state1;

(第二个类型定义ptrfuncptr隐藏了一些十分隐晦的语法。如果没有这个定义,变量state就必须声明为funcptr(*state)(),而调用的时候就得用(funcptr (*)())(*state)()这样令人困惑的类型转换了。)

另一种方法(由Paul Eggert、Eugene Ressler、Chris Volpe和其他一些人提出)是让每个函数都返回一个结构,结构中仅包含一个返回该结构的函数的指针。

struct functhunk {

struct functhunk (*func)();

};

struct functhunk start(),stop();

struct functhunk state1(),state2(),state3();

void statemachine()

{

struct functhunk state={start};

while(state.func !=stop)

state=(*state.func());

}

struct functhunk start ()

{

struct functhunk ret;

ret.func=state1;

return ret;

}

注意,这些例子中使用了对函数指针较老的显式调用。参见问题4.12和问题1.17。

数组大小

1.23

问:能否声明和传入数组大小一致的局部数组,或者由其他参数指定大小的参数数组?

答:很遗憾,这办不到。参见问题6.15和6.19。

1.24

问:我在一个文件中定义了一个extern数组,然后在另一个文件中使用:

file1.c:           file2.c:

int array[]={1,2,3}; extern int array[];

为什么在file2.c中,sizeof取不到array的大小?

答:未指定大小的extern数组是不完全类型。不能对它使用sizeof,因为sizeof在编译时发生作用,它不能获得定义在另一个文件中的数组的大小。

你有3种选择。

(1)在定义数组的文件中声明、定义并初始化(用sizeof)一个变量,用来保存数组的大小:

file1.c:             file2.c:

int array[]={1,2,3};    extern int array[];

int arraysz=sizeof(array); extern int arraysz;

参见问题6.23。

(2)为数组大小定义一个明白无误的常量,以便在定义和extern声明中都可以一致地使用:file1.h:

#define ARRAYSZ 3

file1.c:              file2.c:

#include "file1.h"        #include "file1.h"

int array[ARRAYSZ];       extern int array[ARRAYSZ];

(3)在数组的最后一个元素放入“哨兵”值(通常是0、−1或者NULL),这样代码不需要数组大小也可以确定数组的长度:

file1.c:              file2.c:

int array[]={1,2,3,-1}; extern int array[];

很明显,选择在一定程度上取决于数组是否已经被初始化。如果已经被初始化,则选择(2)就不太好了。参见问题6.21。

参考资料:[11,Sec.7.5.2 p.195]

声明问题

有时候,无论你觉得已经多么仔细地创建了那些声明,编译器都还是坚持报错。这些问题揭示了一些原因。(第16章收集了一些类似的莫名其妙的运行时问题。)

1.25

问:函数只定义了一次,调用了一次,但编译器提示非法重声明了。

答:在作用域内没有声明就调用(可能是第一次调用在函数的定义之前)的函数被认为声明为:

extern int f();

即未声明的函数被认为返回int型且接受个数不定的参数,但是参数个数必须确定,且其中不能有“窄”类型。如果之后函数的定义不同,则编译器就会警告类型不符。返回非int型、接受任何“窄”类型参数或可变参数的函数都必须在调用前声明。(最安全的方法就是声明所有函数,这样就可以用函数原型来检查参数传入是否正确)。

另一个可能的原因是该函数与某个头文件中声明的另一个函数同名。

参见问题11.4和15.1。

参考资料:[18,Sec.4.2 p.70]

[19,Sec.4.2 p.72]

[8,Sec.6.3.2.2]

[11,Sec.4.7 p.101]

*

1.26

问:main的正确定义是什么?void main正确吗?

答:参见问题11.17。(这样的定义不正确。)

1.27

问:我的编译器总在报函数原型不匹配的错误,可我觉得没什么问题。这是为什么?

答:参见问题11.4。

1.28

问:文件中的第一个声明就报出奇怪的语法错误,可我看没什么问题。这是为什么?

答:参见问题10.9。

1.29

问:为什么我的编译器不允许我定义大数组,如double array[256][256]?

答:参见问题19.28,可能还有问题7.20。

命名空间

命名的问题似乎并非一个问题,可它的确是个问题。为函数和变量命名不像为书、建筑物或者孩子命名那么困难——你不需要考虑公众是否会喜欢你程序中的名称——但你的确需要确保这些名称尚未被占用。

1.30

问:如何判断哪些标识符可以使用,哪些被保留了?

答:命名空间的管理有些麻烦。问题(可能并不总是那么清楚)是你不能使用那些已经被实现使用过的标识符,这会导致一堆“重复定义”错误,或者更坏的情况下,静悄悄地替换了实现的标识符,然后把一切都搞得一团糟。同时你可能也想确保后续版本不会侵占你所保留的名称[4]。(拿一个已经调试的、正常工作的生产程序在新版的编译器下编译、连接,结果却因为命名空间或其他的问题导致编译失败,没有什么比这更令人沮丧了。)因此,ANSI/ISO C标准中包含了相当详尽的定义,为用户和实现开辟了不同的命名空间子集。

要理解ANSI的规则,在我们说一个标识符是否被保留之前,我们必须理解标识符的3个属性:作用域、命名空间和连接类型。

C语言有4种作用域(标识符声明的有效区域):函数、文件、块和原型。(第4种类型仅仅存在于函数原型声明的参数列表中。参见问题11.6。)

C语言有4种命名空间:行标(label,即goto的目的地)、标签(tag,结构、联合和枚举的名称。这3种命名空间相互并不独立,即使在理论上它们可能独立)、结构/联合成员(每个结构或联合一个命名空间),以及标准所谓的其他的“普通标识符”(函数、变量、类型定义名称和枚举常量)。另一个名称集(尽管标准并没有称其为“命名空间”)包括了预处理宏。这些宏在编译器开始考虑上述4种命名空间之前就会被扩展。

标准定义了3种“连接类型”:外部连接、内部连接和无连接。对我们来说,外部连接就是指全局、非静态变量和函数(在所有的源文件中有效);内部连接就是指限于文件作用域内的静态函数和变量;而“无连接”则是指局部变量及类型定义(typedef)名称和枚举常量。

根据文献[35,Sec.4.1.2.1]([8,Sec.7.1.3])的规定,对规则的解释如下。

规则1:所有以下划线打头,后跟一个大写字母或另一个下划线的标识符永远保留(所有的作用域,所有的命名空间)。

规则2:所有以下划线打头的标识符作为文件作用域的普通标识符(函数、变量、类型定义和枚举常量)保留[5]

规则3:被包含的标准头文件中的宏名称的所有用法保留。

规则4:标准库中的所有具有外部连接属性的标识符(即函数名)永远保留用作外部连接标识符。

规则5:在标准头文件中定义的类型定义和标签名称,如果对应的头文件被包含,则在(同一个命名空间中的)文件作用域内保留。(事实上,标准声称“所有作用于文件作用域的标识符”,但规则4没有包含的标识符只剩下类型定义和标签名称了。)

由于有些宏名称和标准库标识符集被保留作“未来使用”,这使得规则3和规则4变得愈加复杂。后续版本的标准可能定义符合特定模式的新名称。下表定义了包含标准头文件时,保留作“未来使用”的名称模式。

[A-Z]表示“任何大写字母”;同样,[a-z]和[0-9]分别表示小写字母和数字。*号表示“任何字符”。例如,如果你包含了<stdlib.h>,则所有的以str打头、后跟一个小写字母的标识符都被保留。

这5条规则到底是什么意思?如果你希望确保安全:

1、2。不要使用任何以下划线开始的名称。

3。不要使用任何匹配标准宏(包括保留作“未来使用”)名称。

4。不要使用任何标准库中已经使用或者保留作“未来使用”的函数和全局变量名称。

(严格地讲,“匹配”是指匹配前6个字符,不分大小写。参见问题11.29。)不要重定义标准库的类型定义和标签名称。

事实上,上面的列表有些保守。如果你愿意,也可以记住下面的例外:

1、2。你可以使用下划线打头、后接一个数字或小写字母的名称来命名函数、块或者原型作用域内的行标和结构/联合成员。

3。如果你不包含定义了标准宏的头文件,可以使用匹配它们的宏名称。

4。可以使用标准库函数名作为静态或局部变量名称。(严格地讲,是用作内部连接或无连接类型的的标识符。)

5。如果你不包含声明标准类型定义和标签的头文件,则可以使用这些名称。

然而,在使用上述“例外”的时候,必须注意有些是非常危险的(尤其是例外3和5,因为你可能在后续版本中意外地包含进相关的头文件。比如,通过一系列的嵌套包含)。其他的,尤其是1、2,是一个用户命名空间和实现保留的命名空间之间的“无人地带”。

提供这些例外的原因之一是允许各种附加库的实现者以某种方式声明他们自己的内部或者“隐藏”标识符。如果你利用这些例外,则不会和标准库发生任何冲突,但可能会和你使用的第三方库发生冲突。(另一方面,如果你是某个第三方附加库的实现者,那么只要足够小心,就可以使用这些名称。)

通常,使用例外4中的标准库函数或匹配保留作“未来使用”模式的函数名称作为函数参数名称或者局部变量名称的确是安全的。例如,“string”就是一个常见而且合法的参数或局部变量名。

参考资料:[35,Sec.3.1.2.1,Sec.3.1.2.3,Sec.4.1.2.1,Sec.4.13]

[8,Sec.6.1.2.1,Sec.6.1.2.2,Sec.6.1.2.3,Sec.7.1.3,Sec.7.13]

[11,Sec.2.5 pp.2103,Sec.4.2.1,p.67,Sec.4.2.4 pp.69-70,Sec.4.2.7 p.78,Sec.10.1 p.284]

初始化

变量的声明当然也可包含对变量的初始化,但是不赋显式的初始值的时候,某种特定的缺省初始化也可能会执行。

1.31

问:对于没有显式初始化的变量的初始值可以作怎样的假定?如果一个全局变量初始值为“零”,它可否作为空指针或浮点零?

答:具有静态(static)生存期的未初始化变量(包括数组和结构)——即在函数外声明的变量和静态存储类型的变量)可以确保初始值为零,就像程序员键入了“=0”或“={0}”一样。

因此,这些变量如果是指针就会被初始化为正确类型的空指针(参见第5章),如果是浮点数则会被初始化为0.0。[6]

具有自动(automatic)生存期的变量(即非静态存储类型的局部变量)如果没有显式地初始化,则包含的是垃圾内容。对垃圾内容不能作任何有用的假定。

这些规则也适用于数组和结构(称为“聚集”)。对于初始化来说,数组和结构都被认为是“变量”。

用malloc和realloc动态分配的内存也可能包含垃圾数据,因此必须由调用者正确地初始化。用calloc获得的内存为全零,但这对指针和浮点值不一定有用(参见问题7.35和第5章)。

参考资料:[18,Sec.4.9 pp.82-84]

[19,Sec.4.9 pp.85-86]

[8,Sec.6.5.7,Sec.7.10.3.1,Sec.7.10.5.3]

[11,Sec.4.2.8 pp.72-73,Sec.4.6 pp.92-93,Sec.4.6.2 pp.94-95,Sec.4.6.3 p.96,Sec.16.1 p.386]

1.32

问:下面的代码为什么不能编译?

int f()

{

char a[]="Hello,world!";

}

答:可能你使用的是ANSI前的编译器,还不支持“自动聚集”(automatic aggregate,即非静态局部数组、结构和联合)的初始化。参见问题11.31。

有4种办法可以完成这个任务:

(1)如果数组不会被写入,或者后续的调用中不需要更新其中的内容,可以把它声明为static(或者也许可以声明成全局变量)。

(2)如果数组不会被写入,也可以用指针代替它:

f()

{

char *a="Hello,world!";

}

初始化局部char *变量,使之指向字符串字面量总是可以的(但请参考1.34)。

(3)如果上边的条件都不满足,你就得在函数调用的时候用strcpy手工初始化了。

f()

{

char a[14];

strcpy(a,"Hello,world!");

}

(4)找一个兼容ANSI的编译器。

参见问题11.31。

1.33

问:下面的初始化有什么问题?编译器提示“invalid initializers”或其他信息。

char *p=malloc(10);

答:这个声明是静态或非局部变量吗?函数调用只能出现在自动变量(即局部非静态变量)的初始式中。

1.34

问:以下的初始化有什么区别?

char a[]="string literal";

char *p="string literal";

当我向p[i]赋值的时候,我的程序崩溃了。

答:字符串字面量(string literal)—–C语言源程序中用双引号包含的字符串的正式名称—–有两种稍有区别的用法:

(1)用作数组初始值(如同在char a[]的声明中),它指明该数组中字符的初始值;

(2)其他情况下,它会转化为一个无名的静态字符数组,可能会存储在只读内存中,这就导致它不能被修改。在表达式环境中,数组通常被立即转化为一个指针(参见第6章)因此第二个声明把p初始化成指向无名数组的第一个元素。

(为了编译旧代码)有的编译器有一个控制字符串是否可写的开关。另外有些编译器则提供了选项将字符串字面量正式转换为const char型的数组(以利于出错处理)。

参见问题1.32、6.1、6.2和6.8。

参考资料:[19,Sec.5.5 p.104]

[8,Sec.6.1.4,Sec.6.5.7]

[14,Sec.3.1.4]

[11,Sec.2.7.4 pp.31-32]

1.35

问:char a{[3]}="abc"; 是否合法?

答:是的。参见问题11.24。

1.36

问:我总算弄清楚函数指针的声明方法了,但怎样才能初始化呢?

答:用下面这样的代码:

extern int func();

int (*fp)()=func;

当一个函数名出现在这样的表达式中时,它就会“退化”成一个指针(即隐式地取出了它的地址),这有点类似数组名的行为。

通常函数的显式声明需要事先知道(也许在一个头文件中),因为此处并没有隐式的外部函数声明(初始式中函数名并非函数调用的一部分)。

参见问题1.25和4.12。

1.37

问:能够初始化联合吗?

答:参见问题2.21。

[1].此处是对非负整数而言。下同。——译者注

[2].在这个简单例子typedef struct{int i;}simplestruct;中,结构名和它的typedef类型名同时被定义为“simplestruct”,同时可以看到这里并没有结构标签。

[3].还有,存储类型(static、register等)也可能和基本类型一起出现,而类型限定词(const、volatile)也可能会点缀在基本类型和声明符之间。参见问题11.10。

[4].这里不仅需要关注公用符号,对实现的内部、私有函数也得小心。

[5].意即这些标识符被编译器用作文件作用域内的普通标识符了。这些规则是从C语言的实现(即编译器)的角度描述的。下同。——译者注

[6].这意味着,在内部使用非零值表示空指针或浮点0的机器的编译器和连接器无法利用未初始化的、以0填充的内存,必须用正确的值进行显式的初始化。

相关图书

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

相关文章

相关课程