PEARSON
C和C++代码精粹
[美]Chuck Allison 著
董慧颖 译
人民邮电出版社
北京
图书在版编目(CIP)数据
C和C++代码精粹/(美)埃里森(Allison,C.)著;董慧颖译.--北京:人民邮电出版社,2013.10
ISBN 978-7-115-33027-7
Ⅰ.①C… Ⅱ.①埃…②董… Ⅲ.①C语言一程序设计 Ⅳ.①TP312
中国版本图书馆CIP数据核字(2013)第211200号
版权声明
Chuck Allison:C & C++ Code Capsules:A Guide for Practitioners
Copyright©1997 Pearson Education,Inc.
ISBN:0135917859
All rights reserved.No part of this publication may be reproduced,stored in a retrieval system,or transmitted in any form or by any means,electronic,mechanical,photocopying,recording,or otherwise without the prior consent of Addison Wesley.
版权所有。未经出版者书面许可,对本书任何部分不得以任何方式或任何手段复制和传播。
本书中文简体字版由人民邮电出版社经Pearson Education,Inc.授权出版。版权所有,侵权必究。
本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签。无标签者不得销售。
◆著 [美]Chuck Allison
译 董慧颖
责任编辑 傅道坤
责任印制 程彦红 杨林杰
◆人民邮电出版社出版发行 北京市崇文区夕照寺街14号
邮编 100061 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
大厂聚鑫印刷有限责任公司印刷
◆开本:800×1000 1/16
印张:36.5
字数:734千字 2013年10月第1版
印数:1-3000册 2013年10月河北第1次印刷
著作权合同登记号 图字:01-2011-6294号
定价:89.00元
读者服务热线:(010)67132692 印装质量热线:(010)67129223
反盗版热线:(010)67171154
广告经营许可证:京崇工商广字第0021号
本书基于作者备受好评的C/C++ User Journal杂志上的每月专栏,通过大量完全符合ISO标准C++的程序集合,说明了C++真正强大的威力,是C和C++职业程序员的实践指南。
全书分为3篇共20章,分别从指针、预处理器、C标准库、抽象、模板、异常、算法、容器、文件处理、动态内存管理等不同层次的话题展开讨论。书中的精粹代码,对于C和C++程序员具有很好的使用价值和启发意义。
本书可以帮助有一定经验的C和C++程序员深入学习这两种密切相关的语言,对书中代码的参悟和应用,可以帮助他们从根本上提高使用程序的效率。
我第一次听说Chuck是在1993年3月,当时他的位处理类、bitset和bitstring被标准C++库所采纳(bitstring是在后来加入STL时被吸收的)。虽然我本人间或为C++标准加进两三个极小的位,但是他那种通过ANSI/ISO C++委员会非常严格的考验成功地运行一个完整的类的想法给我留下了深刻的印象(我们都是委员会成员,均悉内情)。
尽管计算机业界言过其实的事情不乏其例,以致于我必须尴尬地依靠我的直觉而不是智慧来辨别真相。因此,在这个领域中,给我留下较深刻印象的是那些能用简明扼要和不夸张的方式来阐明问题的人,更确切地说,这样的人就是一位了不起的导师。Chuck就是这样的导师。你可以通过他全力所从事的一系列活动,如写作、教学、编辑、答疑中看出这一点。当我发现这类人——当我见到他们对听众演讲时,我便能对他们深信不疑——因此,我说服他们在软件开发(Software Development)会议上讲演(我在该会议上担任C++和Java的专题主席)。Chuck已经成为会议上一位定期演讲的固定人物,他令双方的听众都满意。
在最后一次SD会议上(1997年秋,美国首都华盛顿),当我们得知当天是Chuck的生日后,我们一伙人便邀他出去共进晚餐。全体就座后,我环顾四周,才意识到我们都是技术图书的作者:Bjarne Stroustrup(C++的创始人和The C++ Programming Language一书的作者)、Dan Saks (C++专栏作家、演说家、顾问和ANSI/ISO C++委员会长期秘书)、Bobby Schmidt(CUJ专栏作家、演讲家)、Marco Cantu(Mastering Delphi一书和C++丛书的作者)、Tim Gooch(Cobb Group出版社C++的编辑,现任Java的编辑)以及我本人,这些人都非常敬重Chuck,所以才请他吃晚餐。
当然,C++教程式的书不在少数。我有时感到自己正坚持不懈地在写这样的一本书(我的最终成果是Thinking in C++)。然而,当你已经理解了基础知识并想更加深入时会如何呢?虽然书是有了,但是常常是以专家的口吻写的(一种令人瞠目结舌的语言)要不然就是它们所覆盖的主题深奥莫测。而这本书架起了通往高深主题的桥梁,它给了你所需要的而又不会在前进的过程中难倒你。
Chuck 的书写得既明了又准确,准确是我非常喜欢的一件事情。一本书的瑕疵太多会令我厌倦(过去我们不得不容忍这样的书,但现在精心编写的C++图书足够多了,因此没有理由再浪费时间)。我非常喜爱这本书的另一个原因是它的每一章都很简练,每章都集中讲一个主题,你可以很快地领会它并且得到完整的概念(我注意力集中的时间可不是很长)。这是一本经得起时间考验的好书,而且会带给你一个又一个顿悟。
Bruce Eckel
http://www.EckelObjects.com
1997年10月
本书适合于那些C和C++的职业程序员。假如你已熟悉这两种语言的语法和基本结构,这本书能够为你创建有效的、实用的程序提供实践性的指导。每一个代码范例或程序范例均标明行之有效的用法和技术,这些用法和技术对C/C++这两种重要编程语言的性能发挥起着重要的作用。
对于那些希望在工作中加强自身技术和提高效率的人来说,本书可以算是一本经验之谈。尽管目前人们对面向对象模式的推崇到了白热状态(本书也包括这方面的丰富内容),可是我没有理由不对C++的基础——C表示尊崇。我发现太多的程序开发者由于培训不当而不能掌握C++,因为他们缺乏对一些基本概念如指针、作用域、连接和静态类型检查的全面理解,也许所有这些之中最大的缺陷就是缺乏对标准C库的熟悉。开发者浪费了大量时间去编写库中已提供的函数确实令人感到悲哀。那些C++的新手因为热衷C++的那些“令人兴奋”的特征,如继承、异常或重载运算符new,甚至当一些特征还没有得到验证的时候就迫不及待地放弃了简单的C语言。我深信每个人都可以从本书中学到一些东西。严格地说,第1章和第13~16章讲的都是C++,第4~6章讲的仅仅是C语言,而所有其他章节则包含了与C和C++各自主题都相关的内容。
可以说,这主要是一本关于 C++的书。当本书(指出版于 1998 年的英文原书)要印刷的时候,C++标准化的努力已进入最后冲刺中,第二届公共委员会草案(CD2)的整套方案已经完成,仅剩下次要的编辑工作。我自1991年初作为该委员会的成员以来,目睹了标准化的文档从200页增加到750多页。我们已在语言中加入了异常、模板、名字空间、运行期类型识别(RTTI)以及其他一些特征,而在库(通常称作标准模板库或STL)中加入了复杂的、相互关联算法的模板化系统、容器以及迭代结构。与其他标准化的努力方向不同,委员会对新发明和标准化现存的实践同等重视。C++的过于复杂引起了一个网上冲浪者发表了下面的观点:“如果 C 语言给了你足够长的绳子让你上吊,那么 C++则给了你更长的绳子,足够让你周围的每一个人上吊,还够你升起一艘小帆船的帆,剩余的还够让你上吊”。我不辞辛苦地用这种方法说明并解释标准 C++和它的库,就是希望你可以更明智地运用你的绳子。
“C++标准的制定”是我对Bjarne Stroustrup采访的摘录,记录了他对C++成为标准的感受。本书的其他部分则分成了3个部分。
第一部分:预备知识
在对C++做了简短的介绍之后,这些章节弥补C程序员在准备使用C++之前可能存在的差距。第2章“指针”基于我在1993年的C Users Journal上发表后反响不错的三个系列,第4章到第6章包含了每个职业程序员都应该知道的标准C库,这是标准C++极其重要的一部分。
第二部分:主要概念
这一部分详尽地揭示和阐述了C++语言的概念和特征。第7章通过类介绍数据抽象;第8章覆盖了由C++模板机制实现的类型抽象,模板对有效地使用C++来说和对象一样重要,甚至可能更重要;第14章不仅论述了继承和多态,也说明了面向对象的设计和重用,如使用当今关系数据库管理系统的用于对象持久性框架的描述。中间的章节深入介绍了许多开发者都容易忽略的重要基本概念。
第三部分:使用标准库
第15章到第20章介绍了如何使用和享受标准C++库卓越的组件,同时还阐明了在第4、5、6章范围之外的标准C库的一些更为复杂的特征;第15章和第16章解释了作为C++库子集的STL库为什么会是这样,以及如何有效地使用它;第19章包含了一个有用的甚至能够处理部分日期的日期组件,可以满足普通的商业数据处理的需要。
总之,这是一本关于如何编程的书,我已尝试通过深度和广度的合理平衡来说明“最好的实践”,引导读者放弃“已经掌握了C++”的想法。这本书在标准委员会通过ISO C++的最终草案仅一周之后就付诸印刷,而我已经谨慎地避免了继续存在的任何死角(所有的语言和环境都有)。我相信本书中的所有材料在未来几年中都是适用的。
致谢
此书的实际写作时间比我的计划多了很多年。写作始于1984年,当时我正担任亚利桑那州图森市皮玛社区学院计算机科学系主任,我的同事Claire Hamlet劝我在那儿开设用C语言编程的第一门课程。从此我开始收集C程序的例子,并与在Hughes Aircraft公司和在盐湖城Church of Jesus Christ of Latter-day Saints的世界总部的一些雇员们共同分享。“代码精粹”(code capsules),即关于特殊主题带有例子的短小程序,源自于我努力使COBOL“受难者”在学习C时感到有趣并且不枯燥。我足够幸运的是在担任C语言支持委员会主席的时候,曾一度得到了委员会管理上的支持,该委员会有一支经验丰富的一流的程序员队伍(David Coombs,John Pearson,Lorin Lords,Kent Olsen,Bill Owens,Drew Terry以及Mike Terrazas),是他们开发了这样顶尖的课程并有效地培训了100多名Church雇员。
“代码精粹”这个名字是我跟Lee Copeland在Church食堂吃早餐时想到的(他使我有了准备行动的路线)。Mike Terrazas审查这些短小程序的早期版本,并建议我把它们发表在C Users Journal上。1992年3月在伦敦举行的C++标准化委员会会议上,我将这些短小程序展示给P.J.Bill是不经意的。作为一位资深的编辑,他提议我成为这一期刊的专栏作者。“代码精粹”专栏从1992年10月开办直至1995年5月,后来我由于时间有限而不得不放弃。令人钦佩的良师益友Bill在鼓励我将这些代码精粹整理成书方面起了很大作用。Bruce Eckel 欣然阅读了本书的部分内容,Pete Becker通览了全部的手稿,去掉了一些错误和不一致的内容。然而,当提及那种所需的无形的精神动力时,我必须像几乎每一位作者那样对我的家人表示感谢,只有Sandy、 James和Kim才能体会到为了将本书出版大家所付出的巨大努力。21 岁的 James 最近从他呆了两年的英格兰来信说:“终于,爸爸完成了他的著作!他从我记事起就一直做这项工作。”
Chuck Allison
http://www.freshsources.com
1997年11月
20世纪80年代初期,C++起源于AT&T,称为带类的 C,当时Bjarne Stroustrup试图用Simula-67编写仿真程序。“类”在Simula中是表示用户定义类型的术语,编写好的仿真程序的关键是能够定义对象反映现实世界。除了把类加到C中使其成为最快的过程语言外,还有什么更好的方法可以得到快速仿真呢?选择 C语言不仅为类提供了有效的工具,并且也提供可移植性。虽然在C++出现之前已经有其他语言可以通过类支持数据抽象,但是, C++现在是应用最广泛的,几乎每个有C语言编译器的主要平台都支持C++。
第一次看C++就可能被它不可抵抗的魅力所吸引。如果有C语言基础,需要将下列术语(然后少许)增加到自己的词汇表中:
抽象类、存取限定符、适配器、(空间)分配器、基类、类、类的作用域、构造函数、复制构造函数、默认参数、默认构造函数、delete 运算符、派生类、析构函数、异常、异常处理器、异常特化、显式构造函数、显式特化、导出、facet、友元、函数对象、继承、内联函数、迭代器、操纵器、成员函数、成员模板、多继承、不定性、名字空间、嵌套类、new 处理器、new 运算符、新风格类型转换、一次定义规则、运算符函数、重载、局部特化、指向成员的指针、多态、私有、保护、公有、纯虚函数、引用、运行期类型识别、静态成员、流、模板、模板特化、this指针、显著特性、try块、类型标识、类型安全连接、using指令、虚基类、虚析构函数、虚函数。
C++的优点在于它是一种能够处理复杂应用的强大的、高效的、面向对象的语言。因此它的缺点是它本身一定有些复杂,并且比 C 语言掌握起来更加困难。当然 C 语言自己本身也是问题的一部分。C++是一个混合的语言,它将面向对象特征与流行的系统编程语言混合在一起。如果不是一个主语言绑定很少内容的话,介绍如此丰富的一组新特征是不可能的。因此与C语言的兼容性是C++设计的一个主要目标,就像1989年Bjarne在ANSI C++委员会的主题演讲中所陈述的那样,C++是“工程上的妥协”,并且必须要使它“越接近C越好,但不能过度”。
C++事实上是一种多范例语言,像C和Pascal那样,它支持传统的过程编程方式;像Ada 一样,它支持数据抽象和通用性(模板);像其他所有面向对象语言一样,它支持继承性和多态性。所有这些可能都或多或少导致了C++成为“不纯”的编程语言,但是这也使C++成为产品化编程中更具实践性的选择。无疑 C++拥有最好的性能,它可以在混合语言环境中很好地运行(不仅和 C 语言,而且也和其他语言),并且不需要像 Smalltalk 和 LISP运行时所需的庞大运行期资源(后者是环境的,不只是编译和连接过程)。
下面将介绍其更多的优点。
在没有完全掌握C++的情况下也可以有效地使用它。事实上,面向对象技术承诺如果供应商为重用、可扩展性提供设计好的类库,那么建立应用程序的工作就很容易了。现有的开发环境,及其应用程序框架和可视化组件,正在兑现这一承诺。
如果觉得必须要掌握这种语言,可以一步步地去做,并且在这一过程中可以取得丰硕的成果。已出现的3个“顶峰”是:
1.更好的C;
2.数据抽象;
3.面向对象编程。
由于C++比C更安全、更富于表达,所以可以将它作为一个更好的C使用。这个顶峰上的特征包括类型安全连接、强制的函数原型、内嵌函数、const限定修饰符(C从C++中借用了它),函数重载、默认参数、引用、动态内存管理的直接语言支持。也需要意识到C++和它前身之间存在着不兼容性。在这章中将探究一些使C++成为更好的C的非面向对象的特征。因为如果不说明基于类的起源就想阐明某些更好的C特征是很困难的,所以我也将解释C++的类机制。
理解C++最重要的部分,也许就是它对于类型安全(type safety)的贡献。上面所提及的其他面向对象语言实质上是无类型的,或最多也只能说是弱类型的,因为它们主要是在程序运行期间执行错误检查,换句话说,C++要求声明每个程序实体的类型,并且在编译期内它要一丝不苟地检查相同用法。正是类型安全而不是其他别的特点,使 C++成为更好的 C,成为常用编程工作的最合理的选择。类型系统的特征包括函数原型、类型安全连接、新风格的类型转换、运行期类型识别(RTTI)(有关类型转换和RTTI的内容请参见第10章)。
{
在C++中,函数原型不是可选的。事实上,在ANSI C委员会采用原型机制以前,它是为C++发明的。在你第一次使用函数前必须声明或定义每个函数,编译器将检查每个函数调用时正确的参数数目和参数类型。此外,在其应用时将执行自动转换。下列程序揭示一个在C中不使用原型时出现的普通错误。
/* convert1.c */
#include <stdio.h>
main(
{
dprint(123);
dprint(123.0);
return 0;
}
dprint(d)
double d; // 老式的函数定义
{
printf("%f\n",d);
}
/* 输出:
0.000000
123.000000
*/
函数dprint要求带有一个double型参数,如果不知道dprint的原型,编译器就不知道调用dprint(123)是个错误。当为dprint提供原型时,编译器自动将123变换成double型:
/* convert2.c */
#include <stdio.h>
void dprint(double); /*原型*/
main()
{
dprint(123);
dprint(123.0);
return 0;
}
void dprint(double d)
{
printf("%f\n",d);
}
/* 输出:
123.000000
123.000000
*/
除类型安全外,在 C++中关键的新特征是类(class),它将结构(struct)机制扩展到除了数据成员之外,还允许函数成员。与结构标记同名的一个成员函数称为构造函数,并且当声明一个对象时,它负责初始化该对象。由于C++允许定义具有与系统预定义类型一样性能的数据类型,因此,对于用户自定义类型也允许隐式转换。下面的程序定义了一个新类型A,它包含了一个double型的数据成员和一个带有一个double型参数的构造函数。
// convert3.cpp
#include <stdio.h>
struct A
{
};
double x;
A(double d)
{
printf("A::A(double)\n");
x = d;
}
void f(const A& a)
{
}
printf("f: %f\n", a.x);
main()
{
A a(1);
f(a);
f(2);
}
// 输出:
A::A(double)
f: 1
A::A(double)
f:2
由于struct A的构造函数期望一个double型参数,编译器自动地将整数1转换为所定义的double型用于a。在main函数的第一行调用f(2)函数产生下面的功能:
1.将2转换为double型;
2.用值2.0初始化一个临时的A对象;
3.将对象传递给f。
换句话说,编译器生成的代码等同于:
f(A(double(2)));
注意到C++的函数风格的强制类型转换。表达式
double(2)
等同于
(double)2
然而,在任一转换序列里只允许有一个隐式用户定义的转换。程序清单 1.1 程序中要求用一个B对象去初始化一个A对象。B对象转而要求一个double型,因为它唯一的构造函数是B::B(double)。表达式
A a(1)
变为
a(B(double(1)))
它只有一个用户定义的转换。然而,表达式f(3)是非法的,这是因为它要求编译器提供两个自动的用户定义转换:
//不能隐式地既做A的转换又做B的转换
f(A(B(double(3))) //非法
表达式f(B(3))是允许的,因为它显式地请求转换B(double(3)),因此编译器仅提供剩余的转换到A。
通过单一参数的构造函数的隐式转换对于混合模式表达式是很方便的。例如,标准的字符串类允许将字符串和字符数组混合,如:
string s1=”Read my lips…”; //初始化s1
string s2=s1+”no new taxes.”; //将s1和常字符连接
程序清单1.1 仅允许一个用户定义的转换
// convert4.cpp
#include <stdio.h>
struct B;
struct A
{
double x;
A(const B& b);
};
void f(const A& a)
{
}
printf("f: %f\n", a.x);
struct B
{
double y;
B(double d) : y(d)
{
}
printf("B::B(double)\n");
};
A::A(const B& b) : x(b.y)
}
printf("A::A(const B&)\n");
main()
{
A a(1);
f(a);
B b(2);
f(b);
// f(3); //将不编译
f(B(3)); // 隐式 B到A的变换
f(A(4));
}
//输出:
B::B(double)
A::A(const B&)
f: 1
B::B(double)
A::A(const B&)
f: 2
B::B(double)
A::A(const B&)
f: 3
B::B(double)
A::A(const B&)
f: 4
第二行等价于:
string s2=s1 + string("no new taxes,");
这是因为标准的字符串类提供了一个带有单一const char * 型参数的构造函数,但有时你可能不希望编译器如此轻松,例如,假设有一个字符串构造函数带有一个单一的数字参数(其实没有),也就是说将字符串初始化为一个具体的空格数,那么下面表达式的结果将会是什么呢?
string s2=s1+5;
上式右边变为s1+string(5),意思是给s1增加5个空格,这多少是一个让人困惑的“特征”。你可以通过声明单参数构造函数explicit来防止这种隐式转换。由于我们假设了字符串的构造函数是这样声明的,上面的语句就是错误的形式。但是string s (5)这个声明是合法的,因为它显式地调用了构造函数,与此类似,如果用
explicit A (double d)
替换程序清单1.3中A的构造函数的声明,编译器将把表达式f(2)按错误处理。
C++甚至可以通过编译单元检测出不正确的函数调用,程序清单 1.2 的程序调用了程序清单1.3中的一个函数。当把它作为C程序编译时,会得到一个错误的输出结果:
f: 0.000000
程序清单1.2 解释程序连接(也见程序清单1.3)
void f(int);
main()
{
}
f(1);
程序清单1.3 要与程序清单1.2连接的函数
#include <stdio.h>
void f(double x)
{
}
printf("f: %f\n",x);
C 无法区分出函数f的不同。常规作法是把正确的函数原型放到所有编译单元都包含的头文件里。然而,在C++里,一个函数的调用仅连接与之有相同标记的函数定义,即函数名称和它的参数类型顺序的组合。当作为一个C++程序进行编译时,在一个流行的编译器中程序清单1.2和程序清单1.3的输出结果是:
Error :undefined symbol f(int) in module safel.cpp
大多数编译器通过把函数标记和函数一起编码来获得这种类型安全连接。这种技巧经常称为函数名编码、名字修饰、或者(我最喜欢的)名字改编。例如,函数f(int)可能以下面的形式出现在连接器中:
f_Fi //f是一个带整型参数的函数
但是函数f(double )则是:
f_Fd // f是一个带双精度型参数的函数
由于名字的不同,在这个例子中连接器不能找到f(int)并报错。
由于C函数的参数是按值传递的,若传递大型结构给函数,既费时又占用空间。大多数C程序员使用指针来代替按值传递,例如,如果struct Foo是一个大型记录结构,可以采用如下方法:
void f (struct Foo * fp)
{
/*通过fp来访问Foo结构*/
fp->x=…
等等.
}
当然,为了使用这个函数,必须传递Foo结构的地址:
struct Foo a;
…
f (&a);
C++的引用机制是符号上的便捷,这样做可以减少采用指针变量的显式间接访问的烦恼。在C++上面的代码可以被描述为:
void f(Foo &fr)
{
/*直接访问Foo的成员*/
fr.x=…
等等.
}
现在可以像这样调用f不使用地址操作符:
Foo a;
...
f(a);
f原型里的&符号指导编译器通过引用来传递参数,这实际上为你处理了所有的间接访问。(对于Pascal程序员而言,引用参数等价于Var参数。)
引用调用意味着对函数参数所做的任何修改也会影响到主调程序中的原始参数。这就是说你可以编写一个实际运行的交换函数(而不是一个宏)(参见程序清单1.4)。如果不打算修改一个引用参数,就可以像我在程序清单 1.1 中所做的那样将它声明为常引用。常引用参数具有安全性、按值调用的符号方便性以及引用调用的有效性。
如程序清单 1.5 所示,也可以通过引用从函数中返回一个对象,在赋值语句的左边是一个函数调用,这看起来有些奇怪,但是这在运算符重载时是方便的(尤其是=和[ ])。
当然每个C程序员都曾经使用过printf的错误格式描述符号。对printf来说没有办法检查所传递的数据项是否与字符串格式匹配。
程序清单1.4 一个说明引用调用的交换函数
// swap.cpp
#include <stdio.h>
void swap(int &, int &);
main()
{
int i = 1, j = 2;swap(i,j);
printf("i == %d, j == %d\n", i, j);
}
void swap(int &x, int &y)
{
int temp = x;
x = y;
y = temp;
}
//输出:
i == 2, j == 1
做如下事情的频率如何?仅仅是在运行时发现问题?
double d;
…
printf("%d\n",d);/*嘿!本应该用%f*/
换句话说,C++流库使用一个对象的类型来决定正确的格式:
double d;
…
cout<<d<<endl;//不会失败的
表达式 cout<<d 翻译成带有流和 double 参数的函数调用(即 operator<<stream&,double),因此输出处理是不会将值传递错的。如果想用定点精度打印浮点型数字,只需这样指明一次:
double x = 1.5, y = 2.5; //从现在起保留小数点后两位
cout.precision(2); //保持小数点后的0
cout.setf(ios::showpoint);
cout<<x<<'\n'; //打印1.50
cout<<y<<'\n'; //打印2.50
C++中有4个预定义的流:cin(标准输入),cout(标准输出),cerr(标准错误),clog (标准错误)。除了cerr外其余都是全缓冲流。就像stderr一样,cerr的行为好象是非缓冲的,但事实上它是单元缓冲的,也就是说它在处理完每一个对象而不是每一个字节后会自动清除缓冲。例如,带有单元缓冲的语句:
cerr<<“hello”;
缓冲处理5个字符,然后清除缓冲区。一个非缓冲处理的流会立即发送每个字符到它的最终目的地。
程序清单1.5 通过引用从函数中返回一个对象
// retref.cpp:返回一个引用
#include <stdio.h>
int & current(); // 返回一个引用
int a[4] = {0,1,2,3};
int index = 0;
main()
{
current() = 10;
index = 3;
current() = 20;
for (int i = 0; i < 4; ++i)
printf("%d ",a[i]);
putchar('\n');
}
int & current()
{
return a[index];
}
//输出:
10 1 2 20
下列程序将标准输入拷贝到标准输出:
// copy1.cpp:将标准输入拷贝到标准输出
#include <iostream>
using namespace std;
main()
{
char c;
while (cin.get(c))
cout.put(c);
}
注意到标准头文件名(即iostream)不再使用一个.h的后缀。几乎所有C++标准库中的内容,包括流,都驻留于名字空间(namespace std)中。一个名字空间就是一个包括声明在内的已命名的范围。上面第二行 using 指令指示编译器在翻译期间查找声明的名字时搜寻std。标准C头文件也存在于C++程序的std标准名字空间中,并以字母c作为前缀。为了包含<stdio.h>,可以这样做:
#include < cstdio >
using namespace std;
或用通常的#include<stdio.h>。
一个从流中读取的函数称为提取器(extractor),而一个输出函数称为插入器(inserter)。get提取器从流中把下一个字节存放到它的char引用参数中,像多数流成员函数一样,get返回流本身。当一个流出现在像上面的while循环的布尔型上下文中,如果数据成功传递,它检验为true;如果有错误,则为false。就像试图过了文件尾还要读文件一样。尽管这样简单的布尔型检验在大多数时间能满足,但你可以在任何时候使用下面这些布尔型成员函数对流的状态进行询问:
bad ( ) 严重错误 (流被误用)
fail ( ) 转换错误 (数据不正确但流正常)
eof ( ) 文件尾
good ( ) 上述都不是
下例程序实现逐行拷贝:
// copy2.cpp: 逐行拷贝
#include <iostream>
using namespace std;
main()
{
const size_t BUFSIZ = 128;
char s[BUFSIZ];
while (cin.getline(s,BUFSIZ))
cout << s << '\n';
}
getline提取器读取BUFSIZ-1个字符给s,如果遇到一个换行符就停下来,添加一个空字节,丢弃换行符。输出流使用左移运算符作为插入器。任何对象,无论是系统预定义的还是用户自定义的,都可以是流中插入链的一部分。你必须自己重载运算符<<用于自己的类中。
程序清单1.6是一个说明用>>运算符来实现提取功能的程序。由于在C中,通常使用stderr作为提示(因为它没有被缓冲),就会在C++中使用cerr:
cerr << “Please enter an integer:” ;
cin >> i;
这在C++中不是必需的,因为,cout与cin是绑定在一起的,当输出请求输入时,一个依赖于输入流的输出流被自动地刷新。如果需要强制刷新,可以使用一个flush成员函数。
程序清单1.6 回应值和地址的整型提示符
// int.cpp:为一个整数提示
#include <iostream>
using namespace std;
main()
{
int i;
cout << "请输入一个整数: ";
cin >> i;
cout << "i == " << i << '\n';
cout << "&i == " << &i << '\n';
}
//例子执行结果:
请输入一个整数:10
i == 10
&i == 0xfff4
物理地址是以定义实现的格式打印的,通常是16进制,当然字符数组是个例外,打印的是字符串的值而不是地址。要想打印C类型字符串的地址,得把它转向void * :
char s[ ] = …;
cout << ( void * ) s<< ‘\n’; // 打印地址
操作符>>默认方式是跳过空格。程序清单1.7的程序利用这个特点来计算文本文件的字数。提取字符串操作类似于scanf中的%s格式化标志。在读取字符时,也可以关闭这种跳过空格的方式(见程序清单1.8)。
在程序清单1.8中ios::skipws是一个格式化标志的例子。格式化标志是位掩码值,该位掩码值可以通过成员函数setf来设置,也可用unsetf复位(见表1.1的完整描述)。
程序清单1.9的程序阐述了数字的格式化。标准流成员函数precision用来指定浮点值显示的小数位数。如果没有设置ios::showpoint标志,那么末尾的零不被显示。要用前置加号来打印正数,就用ios::showpos。在上例中想要以16进制形式显示x 和在指数形式中显示大写e,使用ios::uppercase。
程序清单1.7 计算文本文件中的字数
// wc.cpp:显示字的个数
#include <iostream>
using namespace std;
main()
{
const size_t BUFSIZ = 128;
char s[BUFSIZ];
size_t wc = 0;
while (cin >> s)
++wc;
cout << wc << '\n';
}
//从"wc < wc.cpp”命令输出
34
程序清单1.8 与程序copy1.cpp完全相同,但使用提取运算符读取空格
// copy3.cpp :用>>读取空格符
#include <iostream>
using namespace std;
main()
{
char c;
//不要跳过空格符
cin.unsetf(ios::skipws);
while (cin >> c)
cout << c;
}
一些格式化选项可以具有一定范围的值。例如,用来确定显示整型数基数的ios::basefield可以被设置成10进制、8进制或16进制。(见表1.2中3种格式化域有效的描述)由于这些是位域而不是单个的位,可用带两个参数形式的setf来设置。例如,程序清单1.10的程序设置8进制数模式采用下面语句:
cout.setf ( ios::oct,ios::basefield );
用标志ios::showbase进行设置时,8进制以0开头,16进制以0x开头打印输出(或者以0X开头打印输出,如果ios::uppercase也被设置)。
程序清单1.9 描述数据格式化
// float.cpp :格式化真正的数字
#include <iostream>
using namespace std;
main()
{
float x = 12345.6789, y = 12345;
cout << x << ' ' << y << '\n';
//显示两个十位数
cout.precision(2);
cout << x << ' ' << y << '\n';
//显示末尾的零
cout.setf(ios::showpoint);
cout << x << ' ' << y << '\n';
//显示符号
cout.setf(ios::showpos);
cout << x << ' ' << y << '\n';
//返回符号和默认值的精度
cout.unsetf(ios::showpos);
cout.precision(0);
//使用科学计数法
cout.setf(ios::scientific,ios::floatfield);
float z = 1234567890.123456;
cout << z << '\n';
cout.setf(ios::uppercase);
cout << z << '\n';
}
//输出:
12345.678711 12345
12345.68 12345
12345.68 12345.00
+12345.68 +12345.00
1.234568e+09
1.234568E+09
程序清单1.10 显示整数的基数
// base1.cpp :显示整数的基数
#include <iostream>
using namespace std;
main()
{
int x, y, z;
cout << "输入三个整数: ";
cin >> x >> y >> z;
cout << x << ',' << y << ',' << z << endl;
//在不同基数中打印
cout << x << ',';
cout.setf(ios::oct,ios::basefield);
cout << y << ',';
cout.setf(ios::hex,ios::basefield);
cout << z << endl;
//显示基数前缀
cout.setf(ios::showbase);
cout << x << ',';
cout.setf(ios::oct,ios::basefield);
cout << y << ',';
cout.setf(ios::hex,ios::basefield);
cout << z << endl;
}
//运行结果
输入三个整数:10 010 0x10
10,8,16
10,10,10
0xa,010,0x10
当标识符 endl出现在一个输出流中时,一个换行字符就被插入并且流被刷新。标识符endl是操纵器的一个例子,即为了副效应而插入到流的一个对象。在〈iostream〉中被声明的系统预定义的操纵器列于表 1.3 中。程序清单 1.11 里的程序在功能上与程序清单 1.10的程序等价,但它是用操纵器来代替显式调用setf函数。操纵器经常可以使代码更为高效。
程序清单1.11 用操纵器改变数据基数
// base2.cpp: 显示整数的基数
// (使用操纵器)
#include <iostream>
using namespace std;
main()
{
int x, y, z;
cout << "输入三个整形数:”;
cin >> x >> y >> z;
cout << x << ',' << y << ',' << z << endl;
//在不同基数中显示
cout << dec << x << ','
<< oct << y << ','
<< hex << z << endl;
//显示基数前缀
cout.setf(ios::showbase);
cout << dec << x << ','
<< oct << y << ','
<< hex << z << endl;
}
其他的操纵器带有参数(见表1.4)。在程序清单1.12中的程序是用setw(n)操纵器直接在插入序列里设置输出宽度,这样就不需要单独调用width。ios::width区域是特殊的:它在每次的插入后立即重置为0。当iso::width是0时,值以所需的最少字符数打印,通常,即使它们的空间不够,数字也不会被删掉。
程序清单1.12 利用setw函数设置输出域宽
// adjust.cpp: 调整输出
#include <iostream>
#include <iomanip>
using namespace std;
main()
{
cout << '|' << setw(10) << "hello" << '|' << endl;
cout.setf(ios::left,ios::adjustfield);
cout << '|' << setw(10) << "hello" << '|' << endl;
cout.fill('#');
cout << '|' << setw(10) << "hello" << '|' << endl;
}
//输出:
| hello|
|hello |
|hello#####|
当然你可以用操纵器
…<<setfill('#')<<…
来替换语句
cout.fill('#');
但在这种情况下,这样做似乎很不方便。
提取器通常忽视宽度设置,但C风格的字符串输入是个例外。在对字符数组进行提取操作之前应该先将域宽设置为字符数组的大小,以避免数据溢出。当处理输入行
nowisthetimeforall
时,程序清单1.13程序将输出:
nowisthet,im,eforall
应记住的是,编译器将空白字符默认为分隔符,所以如果输入为:
now is the time for all
那么输出将是:
now,is,the
程序清单1.13 控制输入字符串宽度
// width.cpp: 控制输入字符串的宽度
#include <iostream>
#include <iomanip>
using namespace std;
main()
{
char s1[10], s2[3], s3[20];
cin >> setw(10) >> s1
>> setw(3) >> s2
>> s3;
cout << s1 << ',' << s2 << ',' << s3 << endl;
}
输入输出流也支持新的布尔数据类型(bool),以及格式标识和用于数字或字母文本的操纵器:
bool b=true;
cout<<b<<endl; //打印“1”
cout.setf(ios::boolalpha); //或仅插入操纵器boolalpha
cout<<b<<endl; //打印“true”
你可以通过简单定义一个将流引用作为参数并返回相同引用的函数来建立一个自己的操纵器。例如,下面是一个ASCII码控制铃声的操纵器,当将它插入在任何输出流中时可以发出铃声:
//响铃操纵器
#include <iostream>
ostream& beep(ostream& os)
{
os<<char(7);//ASCII 响铃
return os;
}
使用时,只需插入:
cout<<…<<beep<<…
程序清单 1.4 中的交换函数(swap)只有在交换整数时才有用。如果要交换两个任何系统预定义的数据类型中的对象该么办呢?C++允许定义多个同名函数,只要它们的特征不同。因此就可以为所有系统预定义的数据类型定义一个交换函数:
void swap(char &,char &);
void swap(int &,int &);
void swap(long &,long &);
void swap(float &,float &);
void swap(double &,double &);
等等。
然后就可以调用交换函数用于任何两个系统预定义数据类型对象的交换。然而,假如要实现这些函数中的每一个,不用多久就会发现正在反复地做同一件事而唯一不同的是要交换对象的类型。为了使工作更简洁并减少犯低级错误,可以定义一个模板函数来代替所有的函数。关于模板详细内容请参见第8章。
在C++中你可以重载运算符,例如,定义一个复数的数据类型如下:
struct complex
{
double real, imag;
};
假如能使用中缀符号用于复数加法,那将会相当方便。如:
complex c1,c2;
…
complex c3=c1+c2;
当编译器遇到如c1+c2这样的表达式时,将查找下边两个函数中的一个(只须其中的一个存在):
operator+(const complex&,const complex &); //全局函数
complex::operator+(const complex &); //成员函数
关键字operator是函数名的一部分。为实现两个复数的加法可以将operator+定义为全局类型,如下:
complex operator+(const complex &c1,const complex &c2)
{
complex r;
r.real=c1.real+c2.real;
r.imag=c1.imag+c2.imag;
return r;
}
程序清单1.14 运算符+和运算符<<在复数中的应用
#include <iostream>
using namespace std;
struct complex
{
double real, imag;
complex(double = 0.0, double = 0.0);
};
complex::complex(double r, double i)
{
real = r;
imag = i;
}
inline ostream& operator<<(ostream &os, const complex &c)
{
os << '(' << c.real << ',' << c.imag << ')';
return os;
}
inline complex operator+(const complex &c1, const complex &c2)
{
return complex(c1.real+c2.real,c1.imag+c2.imag);
}
不允许重载系统中预定义的操作,例如,不允许重载两个整型数相加。因此,在重载操作中至少有一个操作数是用户自定义类型。
流库“知道”怎样通过运算符重载来格式化各种系统预定义的数据类型。例如,ostream类中,cout是一个实例,它为所有的系统预定义的数据类型都重载了操作符<<,当编译器看到表达式:
cout<<i;
这里i是整型,它产生以下的函数运算:
cout.operator<<(i); //ostream::operator<<(ostream&,int)
这样可以正确地格式化数据。
程序清单 1.14 表明如何通过重载用于复数的运算符<<来扩展标准流(输出在程序清单1.5中)。编译器将表达式
cout<<c
转换成下面的函数调用(在这里c是一个复数):
operator << ( cout ,c)
这将依次采用operator<<(ostream&,const complex&)将操作分解成格式化系统预定义类型的对象。这个函数也返回流,因此,可以在一个单独的语句中链接多个插入流。如,表达式
cout<<c1<<c2
变为
operator<<(operator<<(cout,c1),c2)
这要求operator<<(ostream&, const complex&)返回流,为了高效这是通过引用来实现的。
在程序清单1.14中所看到的关键字内联(inline)是提示编译器要把相应的代码“内联”。也就是说,直接把代码写入程序中而没有实际函数调用的开销。如果编译器准许了你的要求,它把每一次的函数调用都用相应函数的代码来代替,从而避免了实际函数调用的开销。这种机制不同于类似函数的宏,宏是在程序编译前实现文本的代换。内联函数具有实际函数的类型检查和语义,并且没有函数调用的开销和宏定义副作用的敏感性。例如,假如定义一个宏找出两个数中的较小者:
#define min(x,y) ((x)<(y)?(x) :(y)
当带有增量参数时,这将是失败的,如:
min(x++, y++)
由于内联函数具有真正函数的作用,因而不会出现这种问题。
程序清单1.15 使用复数数据类型
#include <iostream>
#include "complex.h"
using namespace std;
main()
{
complex c1(1,2), c2(3,4);
cout << c1 << " + " << c2 << " == " << c1+c2 << endl;
}
//输出:
(1,2) + (3,4) == (4,6)
然而并不是所有函数都可以或应该进行内联操作,当然一个递归函数是没有资格内联的,在内联时函数体大的函数会极大地增加代码量。内联主要用于代码少而简单的函数。
在一个函数声明中的默认参数用于指示该函数从它的原型中取值。在程序清单1.16中有一个具有原型的函数:
int minutes(int hrs ,int min=0);
最后一个参数后面的“=0”指示编译器给第二个参数提供值0。当调用minutes函数时,可以省略了该参数。这种机制对定义相应的重载函数来说本质上是一种速记的方法。在这种情况下,前面的语句等价于:
int minutes (int hrs,int min);
int minutes (int hrs); //忽略了minutes
程序清单1.14中的复数构造函数采用了默认参数。允许不带参数或带有1个或2个参数来定义复数,例如:
complex c1; //(0,0)
complex c2(1); //(1,0)
complex c3(2,3); //(2,3)
程序清单1.14中operator+的返回语句正是上面第三个语句。
程序清单1.16 说明默认参数
// minutes.cpp
#include <iostream>
using namespace std;
inline int minutes(int hrs, int mins = 0)
{
return hrs * 60 + mins;
}
main()
{
cout << "3 hrs == " << minutes(3) << " minutes" << endl;
cout << "3 hrs, 26 min == " << minutes(3,26) << " minutes" << endl;
}
//输出:
3 hrs == 180 minutes
3 hrs, 26 min == 206 minutes
在C语言中为了用堆栈,需要计算出所要创建的对象的大小:
struct Foo*fp =malloc(sizeof(struct Foo) );
在C++中,运算符new用于计算出对象的大小:
Foo*fp=new Foo;
在C语言中分配数组,需调用不同的函数。
struct Foo*fpa= calloc(n,sizeof(struct Foo));
在C++中,new运算符会知道数组的大小:
Foo*fpa=new Foo[n];
此外,运算符 new 在返回指针之前,自动调用适当的构造函数来初始化对象。例如,在堆栈中创建复数时,编译器会自动将它们初始化,如下:
complex *cp1= new complex; // -> (0,0)
complex *cp2= new complex(1); // -> (1,0)
complex *cp3= new complex(2,3); // -> (2,3)
可以用delete运算符的两种形式之一,把动态内存返还给堆栈,对于单独的对象可以这样做:
delete fp;
delete cp1;
但是,释放一个数组需要不同的句法:
delete [ ] fpa; // 释放数组句法
像 C++的一些其他特性一样,new 和 delete 提高了程序的类型安全性。用户不仅仅是申请一些内存,还有带有适当的类型检查和初始化的对象。如果想了解更多的关于内存管理的内容,请参见第20章。
在C++中,声明可以出现在语句可以出现的任何地方。这就意味着不必在程序块的开始进行一组声明,而可以在第一次使用对象时定义它。例如,程序清单1.17中数组a在整个函数体中都是可见的,但是n直到声明后才有效,而i直到下一行才有效。注意 i 在第二次 for 循环中被再次声明,这说明了在循环中声明的变量的作用域是该循环本身。
程序清单1.17 声明是语句
// declare.cpp
#include <iostream>
using namespace std;
main()
{
int a[] = {0,1,2,3,4};
//打印地址和大小
cout << "a == " << (void *) a << endl;
cout << "sizeof a == " << sizeof a << endl;
//顺序打印
size_t n = sizeof a / sizeof a[0];
for (int i = 0; i < n; ++i)
cout << a[i] << ' ';
cout << endl;
//倒序打印
for (int i = n-1; i >= 0; --i)
cout << a[i] << ' ';
cout << endl;
}
//输出:
a == 0xffec
sizeof(a) == 10
0 1 2 3 4
4 3 2 1 0
本书的第三部分非常详细地说明了标准C++库。除流外,库还提供了大量具体实用的类型和容器类。尽管在早些时候,我定义了自己的复数类型以说明类的某些特点和运算符重载,标准库还是提供了带有一系列强有力的复数数学运算的复数类型。如程序清单1.18所示,complex 是一个类模板,它可以采用任何想要的基本的数值类型(不论是浮点型、双精度型还是长双精度型)。
为了提供强类型检查和面向对象, C++不得不在一些语言方面与 C 不同。如果要把C++作为更好的C使用,就必须留意两种语言间的不同特性。
程序清单1.18 说明复数模板
#include <iostream>
#include <complex>
using namespace std;
main()
{
complex<double> x(1.0, 2.0), y(3.0, 4.0);
cout << "x + y == " << x + y << endl;
cout << "x * y == " << x * y << endl;
cout << "conjugate of x == " << conj(x) << endl;
cout << "normof x == " << norm(x) << endl;
}
//输出:
x + y == (4,6)
x * y == (-5,10)
conjugate of x == (1,-2)
normof x == 5
首先,C++比C有更多的关键字,必须避免使用表1.5中的任何符号作为程序的标识符。可以使用const整数对象和枚举常量定义C++中数组大小,如:
const int SIZE=100;
enum{BIGGER=1000};
int a[SIZE], b[BIGGER];
全局 const声明默认的是内部连接,而在C中它们是外部连接。这意味着在文件作用域内,可以使用const定义取代头文件中#define定义的宏,如果希望一个const对象具有外部连接特性就必须使用关键字extern。
在C中,可以将指向任意类型的指针指向空类型void *或将指针从空类型void *指向任何其他类型,这允许你在没有指针类型转换的情况下使用malloc,如:
#include <stdlib.h>…
char*p=malloc (strlen(s)+1);
而C++的类型系统不允许在没有转换类型的情况从一个空类型指针指向其他类型。上例中,无论如何都应当使用new运算符。
在C中,如果函数定义时漏掉了一些参数,编译器将不会检查你怎样使用该函数(例如,可以向它传递任何数量和类型的参数),在C++中,原型f等价于f(void) ,如果要坚持使用不安全的C行为,可使用f(…)。
最后,在C++中单引用字符常量是char型而不是int型。否则,表达式
cout << ’a’
将输出内部字符码(如ASCII中的97)而不是字母“a”。
要了解更多关于C/C++的兼容特性,请参见附录A。
作为一种多范例式的语言,C++:
1.是更好的C;
2.支持数据抽象;
3.支持面向对象编程。
C++是类型安全语言。
所有函数在第一次使用之前必须声明或定义。
引用参数直接支持引用调用语义。
可以重载函数和运算符。
模板允许创建通用函数。
内联函数将类似于函数的宏的高效与实际函数的安全性相结合。
自由存储运算符new和delete能根据类型计算对象的大小。
声明可以出现在函数可以出现的任意位置。
“分割违规”
“访问违规”
“可疑的指针转换”
“不可移植的指针转换”
“空指针赋值”
这些消息听起来熟悉吗?指针出错是C++程序员必须应付的最令人厌恶的错误。实际上,长时间以来指针和它所提供给开发者的原始功能已经成为人们对C主要的批评。人们说指针太危险了。然而,C和有点低级的C++的哲学是相信程序员。对真相了解得不够以致人们认为这些语言危险,只是一些程序员的借口而已。要安全有效地使用C和C++,掌握指针是必要的。幸运的是,在弄清楚一些基本原则和技术之后,就会很容易地掌握指针。
除了寄存器变量以外,程序中的所有对象都存储在内存中的某处,“某处”有一个地址,在开发平台上内存的每个字节的序号按顺序从0开始,简单地说,地址就是一个字节的顺序号。下面的程序说明了如何找到程序变量的地址:
// address.cpp
#include <cstdio>
#include <iostream>
using namespace std;
main()
{
int i = 7, j = 8;
printf("i == %d, &i == %p\n",i,&i);
cout << "j == " << j << "&j == " << &j << endl;
}
/* 输出:
i == 7, &i == 0012FF88
j == 8, &j == 0x0012ff84
*/
单目运算符&返回一个数据对象的地址。%p格式控制符根据编译器格式的不同来显示地址,通常是十六进制形式。本书所有的例子中,整数和地址都是32位(你的输出可能有所不同)的。
在上面的程序中i和j的内存分布如下:
i和j在内存中碰巧相邻存储这一点并不重要(在某些结构中因对齐的需要而在对象之间存在间隔),注意我的编译器在内存中把i分配在j之后(即i的地址比j的高),这同样不重要。计算机在实际中如何存储一个数的各个位也是随系统而定的,事实上,在 PC 机中,i中的7不是真正存储在i所占内存的最右端。我们可以假设多数情况不是这样的,因为,不管这些位在物理上如何排列,7在逻辑上都是0X00000007。
指针只不过是一个容纳另一个程序实体地址的变量。在大多数情况下,我们不关心一个地址的实际数值,通常只是用它引用感兴趣的对象,程序清单 2.1 中的程序说明了指针的使用。由于指针总是指向某种类型的对象,因此被引用类型通常必须出现在声明中,这样,我们说“指向整型”或者“指向字符型”,等等。声明
int *ip;
说明了*ip是一个整型变量,因此ip是一个指向整型变量的指针。在声明语句之外的表达式中如果指针变量前有星号,结果表示指针变量所指向变量的值。通过指针间接地指向内存的过程叫做间接访问或复引用。它可以出现在赋值语句的任何一边。因此在程序清单2.1中的声明,语句
*ip =9;
等效于
i=9;
程序清单2.1 说明指针和间接访问
// pointer.cpp
#include <iostream>
using namespace std;
main()
{
int i = 7, j = 8;
int* ip = &i;
int* jp = &j;
cout << "Address " << ip
<< " contains " << *ip << endl;
cout << "Address " << jp
<< " contains " << *jp << endl;
*ip = 9;
cout << "Now Address " << ip
<< " contains " << i << endl;
*jp = 10;
cout << "Now Address " << jp
<< " contains " << j << endl;
}
//输出:
Address 0x0012ff88 contains 7
Address 0x0012ff84 contains 8
Now Address 0x0012ff88 contains 9
Now Address 0x0012ff84 contains 10
如果在定义指针时没有说明被引用的类型,表达式*ip将没有意义,间接访问也是不可能的。任何时候都不要忘记指针不仅仅是指向内存中的某处,它还指向某种类型的实体。这个规则的唯一例外是指针不指向任何地方的时候。当给一个指针赋特殊的值0时,就会发生这种情况(这时的指针叫作空指针,在C中由宏NULL表示)。不能复引用一个空指针,只能将它与其他指针做比较。
程序清单2.1的内存分布是:
尽管,地址通常以数字的形式出现,但是不要假设指针类型和整型数据类型之间存在任何关系。指针是一种独特的数据类型并且应该作为一种独特的数据类型来看待。只能对指针进行如下操作:
1.在指针中存储和从其中读取被引用类型的对象的地址;
2.改变或读取该地址中的内容(间接访问);
3.在指针上加或减一个整数(仅限在数组中使用);
4.与另一个指针相减或做比较(当两个指针都指向同一数组时);
5.给指针赋值或把它和空指针做比较;
6.作为参数传递给函数,该函数期望一个指向引用类型的指针作为参数。
由于对象的相应内存位置是不重要的(当然,数组元素除外),通常描述内存的逻辑分布更好,如:
通常说法是“ip指向i”或“jp指向j”。
指针的概念如此简单以至于初学者往往由于要寻找星号之外的意义而使他们陷于紧张不安之中。如果想避免挫折和困惑而浪费没有必要的时间,只需记住:
重要的指针原则1:指针是一个地址
注意,上面所说的是“指针是一个地址”而不是“指针保存一个地址”,当然,两种说法都是正确的。指针是地址就像整型是整数一样。人们经常不说整型“保存”一个整数。我仅仅想强调的是当使用指针时要想到它是“地址”,还有什么更简单的说法呢?
由于所有的对象都有一个地址,因此可以定义指向任何类型对象的指针,包括另一个指针对象。程序清单 2.2 中的程序说明了如何定义指向整型指针的指针。这个程序的拓扑结构是:
如果ipp是指向一个指向整型变量指针的指针,那么*ipp是该指针所指向的指针,为了最终得到i中的整数7,需要另一级别的间接访问,即表达式**ipp,换句话说:
**ipp==*ip==i; //事实上,全部指向同一对象
程序清单2.2 说明指向指针的指针
/* ptr2ptr.cpp: 指向指针的指针*/
#include <iostream>
using namespace std;
main()
{
int i = 7;
int* ip = &i;
int** ipp = &ip;
cout << "Address " << ip << " contains " << *ip << endl;
cout << "Address " << ipp << " contains " << *ipp << endl;
cout << "**ipp == " << **ipp << endl;
}
//输出:
Address 0x0012ff88 contains 7
Address 0x0012ff84 contains 0x0012ff88
**ipp == 7
当一个指针指向一个数组元素时,可以在指针上加一个整数或减一个整数来使它指向同一数组的元素。在这样的指针上加1是通过它所引用类型的数据的字节数来增加它的值,因此,它就指向下一个数组元素。程序清单 2.3 中的程序实现了在一个浮点型数组内完成整数的运算。数组看起来像:
在我的系统平台上,浮点型占4个字节。在p上加1实际上等于在它的值上加4。对两个指向数组元素的指针进行相减可以得到在两个地址之间数组元素个数。换句话说,如果p和q是指向相同类型的指针,那么语句q=p+n意味着q-p==n,或相反。存储两个指针差的便捷方法是把这个差存储在ptrdiff_t中,它在stddef.h中定义,包含在<iostream.h>中。
指针运算规则可以归纳为下面的公式:
1.p±n==(char*)p±n*sizeof(*p)
程序清单2.3 说明指针运算
/* arith.cpp: 举例说明指针运算*/
#include <iostream>
using namespace std;
main()
{
float a[] = {1.0, 2.0, 3.0};
//增加一个指针
cout << "sizeof(float) == " << sizeof(float) << endl;
float* p = &a[0];
cout << "p == " << p << ", *p == " << *p << endl;
++p;
cout << "p == " << p << ", *p == " << *p << endl;
//减去两个指针
ptrdiff_t diff = (p+1) - p;
cout << "diff == " << diff << endl;
diff = (char *)(p+1) - (char *)p;
cout << "diff == " << diff << endl;
}
//输出:
sizeof(float) == 4
p == 0x0012ff80, *p == 1
p == 0x0012ff84, *p == 2
diff == 1
diff == 4
这也就是说“在(从)一个指针上加(减)一个整数n时,指针将在内存中向上(下)移动其所指向的类型的n个单元”。
2.p-q==±n
这里n是p和q之间元素的个数。注意,记住公式2中的n是一个特殊的类型(ptrdiff_t),在一些结构中,你只可以在指针运算中把它作为补充使用,而不能以其他的方式使用(例如,甚至不能打印它)。
因为公式假设一组有序等大小的对象,所以指针运算只在数组内有意义。然而,可以把任何单一的对象解释成字节数组。程序清单 2.4 中的程序通过把整数的地址存放在一个指向字符型的指针中来详细研究了整数,然后通过指针运算来访问每个字节。注意在 cp初始化时的强制类型转换,在给不同类型的指针赋值时,需要有强制类型转换以使编译器确认你知道自己在做什么,否则编译器将怀疑你不知道自己在做什么,因此给出警告消息“可疑的指针转换”,然而,当转换成void指针时则不需要强制类型转换(参见2.5节“普通指针”)。
程序清单2.4 说明指针转换
// convert.cpp: char* 和指针映射
#include <iostream>
using namespace std;
main()
{
int i = 7;
char* cp = (char*) &i;
cout << "The integer at " << &i
<< " == " << i << endl;
//分别打印每个字节的值:
for (int n = 0; n < sizeof i; ++n)
cout << "The byte at " << (void*)(cp + n)
<< " == " << int(*(cp+n)) << endl;
}
//输出:
The integer at 0x0012ff88 == 7
The byte at 0x0012ff88 == 7
The byte at 0x0012ff89 == 0
The byte at 0x0012ff8a == 0
The byte at 0x0012ff8b == 0
程序清单2.4的输出揭示了一个有趣的事实:我的PC的Intel处理器是“从后”存储的,在这种方式下一个对象最没有意义的值被存储在内存地址较低的单元中。这种存储机制叫做“不重要的值结尾”,因为在内存中向上移动时,首先遇到的是一个多字节整数的“小的结尾”。VAX机器同样是“不重要的值结尾”。但是IBM机器是“重要的值结尾”。这在通常的数据处理中并不值得关注的,但是有些时候它将起很大作用。
例如,假设你想有效地存储一个世纪内的日期,需要这样存储:
Year(0-99) 7 bits
Month(1-12) 4 bits
Day(1-31) 5 bits
幸运的是,合在一起是16位,刚好是PC机中一个短整型数的大小。那么,一个将日期存储在短整型中的明显方式是用位段操作如下:
// bit1.cpp: 向整数中压缩数据
#include <iostream>
#include <iomanip>
using namespace std;
main()
{
unsigned short date, year = 92, mon = 8, day = 2;
date = (year << 9) | (mon << 5) | day;
cout << hex << date << endl;
}
// 输出:
b902
日期1992年8月2日(b902)的位逻辑排列的理论期望是:
但是,“不重要的值结尾”机器物理上从后向前存取数据,即;
用下面的位域结构可得到一个可读性更强的程序(参见程序清单2.5)。
struct Date
{
unsigned day:5;
unsigned mon:4;
unsigned year:7;
};
这个结构反映了相反的排列。要用位域结构来表示一个整型,只需简单地将指向整型的指针强制转换成指向Date的指针。现在可以不用移位和屏蔽而用名字来访问日期成员。为了通过指针访问结构成员,需要先对指针进行复引用,然后再命名成员:
(*dp).mon
由于这是个繁锁的句法,其简化的句法为:
dp->mon
程序清单2.5 通过一个位域结构封装短整型数据
// bit2.cpp: 用一个位域结构覆盖一个整数
#include <iostream>
#include <iomanip>
struct Date
{
unsigned day: 5;
unsigned mon: 4;
unsigned year: 7;
};
main()
{
unsigned short date, year = 92, mon = 8, day = 2;
Date* dp = (Date*) &date;
dp->mon = mon;
dp->day = day;
dp->year = year;
cout << hex << date << endl;
}
//输出:
b902
我听过的读法有“dp箭头mon”、“dp指向mon”以及一些其他的读法,在此不再提及。
除非被告知用别的方法,否则C++总是通过值向函数传递参数。这意味着函数是局部地使用了每一个参数的拷贝。这种传递方式的结果是一个函数不可能在所调用的程序中改变对应的实参值。考虑下面的试图交换两个整型变量值的程序段:
void swap(int x, int y)
{
int temp = x;
x = y;
y = temp;
}
诸如swap(a,b)这样的调用对于a和b都不会产任何效果。在退出函数后允许改变函数固定实参值的方式叫做“传递引用”。
程序清单2.6 用指针交换函数中实参的值
// swap2.cpp: 一个有用的交换函数
#include <iostream>
using namespace std;
void swap(int*, int*);
main()
{
int i = 7, j = 8;
swap(&i,&j);
cout << "i == " << i << ", j == " << j << endl;
}
void swap(int* xp, int* yp)
{
int temp = *xp;
*xp = *yp;
*yp = temp;
}
//输出:
i == 8, j == 7
在C语言中,可以通过传递想要改变其值的参数的指针来仿真传引用语义。可以通过指针,改变在调用程序中的变量值(参见程序清单2.6)。传递引用对于大型对象和在面向对象系统中是很普通的,在面向对象操作中,指向一个对象的指针称为“句柄”。正如第1章中所解释的那样,通过引用,C++也直接支持传递引用。
通常编写能接收指向任意类型参数的函数是很方便的。这是很有必要的,例如,用标准的库函数 memcpy,能够从一个地址向另一个地址拷贝一块内存。你也可能想调用memcpy来拷贝自己创建的结构:
struct mystruct a,b;
/*...*/
memcpy(&a,&b,sizeof(struct mystruct));
为了操作任意类型的指针,memcpy把它头两个参数声明为void型指针。可以不需要强制类型转换将任何类型的指针赋予void*类型。也可以在C而不是在C++中将void*赋予其他任何类型的指针。这里说明了void指针的memcpy函数的简洁实现:
void* memcpy(void* target, const void* source, size_t n)
{
char* targetp = (char*) target;
const char* sourcep = (const char*) source;
while (n--)
*targetp++ = *sourcep++;
return target;
}
这个版本的memcpy必须把指向void的指针赋予指向char的指针,这样它就可以每次传递内存块的一个字节,并对这个字节中的数据进行拷贝。试图复引用一个void*是没有意义的,因为它的大小是未知的。
注意 memcpy 函数第二个参数中的 const 关键字。这个关键字告诉编译器此函数将不会改变 source 指向的任何值(除了强制类型转换)。当把指针作为参数传递时,总是合适地使用 const 限定符是一个很好的习惯,它不仅可以防止你无意中错误的赋值,而且还可以防止在作为参数将指针传递给函数时可能会修改了本不想改变的指针所指向的对象的值。例如,如果在程序清单2.6中的声明是:
const int i=7,j=8;
有可能因为下面这条语句而得到警告:
swap(&i,&j);
因为swap的确改变了其参数所指的变量的值。
如果浏览一下你所使用的编译器提供的标准头文件,就会看到const。在一个声明中,当const出现在星号前面的任意位置时,表明所指向的内容不会被改变:
const char *p; //指向常字符的指针
char const *q; //同样是指向常字符的指针
char c;
c=*p; //OK(假设p和q已经初始化了)
*q=c; //错误,不能改变指针所指的内容
也可以通过把const放在星号的后面来声明指针本身不可改变:
char* const p;
*p='a'; //OK,只有指针是常量
++p; //错误,不能改变指针
要禁止改变指针和它所引用的内容,就在星号前和后都使用const:
const char* const p;
char c;
c=*p; //OK-能读出内容
*p='a'; //错误
++p; //错误
程序清单 2.7中的函数inspect说明了如何打印出任何对象的不同字节。因为我并没有改变对象的内容,所以第一个参数是一个指向 const 的指针,而且,在使用它之前要小心地将它转换成一个指向常字符的指针。
程序清单2.7 检查任何对象的函数
// inspect.cpp: 检查对象的字节
#include <iostream>
#include <iomanip>
using namespace std;
void inspect(const void* ptr, size_t nbytes)
{
const unsigned char* p = (const unsigned char*) ptr;
cout.setf(ios::hex, ios::basefield);
for (int i = 0; i < nbytes; ++i)
cout << "byte " << setw(2) << setfill(' ') << I
<< ": " << setw(2) << setfill('0') << int(p[i])
<< endl;
}
main()
{
char c = 'a';
short i = 100;
long n = 100000L;
double pi = 3.141529;
char s[] = "hello";
inspect(&c, sizeof c); cout << endl;
inspect(&i, sizeof i); cout << endl;
inspect(&n, sizeof n); cout << endl;
inspect(&pi, sizeof pi); cout << endl;
inspect(s, sizeof s); cout << endl;
}
//输出:
byte 0: 61
byte 0: 64
byte 1: 00
byte 0: a0
byte 1: 86
byte 2: 01
byte 3: 00
byte 0: 13
byte 1: 7c
byte 2: d3
byte 3: f4
byte 4: d9
byte 5: 21
byte 6: 09
byte 7: 40
byte 0: 68
byte 1: 65
byte 2: 6c
byte 3: 6c
byte 4: 6f
byte 5: 00
在程序清单 2.7 中,会注意到在传递数组 s 时并没有使用它的地址,这是因为 C 和C++在大多数表达式中把数组名转换成指向它第一个元素的指针。自1984年以来,我已经向成百上千的学生讲授了C和C++,我注意到了指针和数组,特别是指针和多维数组之间的关系造成很多迷惑。
这样说似乎很奇怪,但是C++确实不支持数组,至少C++不像支持第一类数据类型如整型或者甚至结构体那样支持数组。考虑以下的语句:
int i=1,j;
int a[4]={0,1,2,3},b[4];
struct pair {int j,int y;};
pair p={1,2},q;
j=i; //OK:整型赋值
q=p; //OK:结构体赋值
b=a; //不能这样做
并不是所有有关数组的操作都是合法的。我们可以进行以下操作,但是它并不是一个“真实”的赋值:
int a[4]={0,1,2,3},*p;
p=a; /*只在p中存储了a[0]的地址*/
除了在声明中或者当一个数组名是 sizeof 运算符或&运算符的操作数之外,编译器总是把数组名解释成指向它的第一个元素的指针。可以将这个原则表达为:
a==&a[0]
或者等价于:
*a==a[0]
使用指针运算的规则,那么当把一个整型变量 i 和一个数组名相加,结果就得到指向数组第i个元素的指针,也就是:
a+i==&a[i]
或者,像我喜欢的表达方式一样:
重要的指针原则 2:*(a+i)==a[i]
程序清单2.8中的程序阐述了原则2以及准备步骤。
由于所有的数组下标是真正的指针运算,可以使用表达式i[a]代替a[i]。这些可直接从原则2中得到:
a[i]==*(a+i)==*(i+a)==i[a]
当然,任何使用了这样极端错误的表达的程序都会被中断而不被执行,而且程序员也会受到严厉的谴责。然而,使用相反的下标也不是完全没有道理,如果一个指针p传递一个数组,就可以使用表达式p[-1]重新得到在*p之前的元素,由于:
p[-1]==*(p-1)
程序清单 2.9极为全面地涵盖了指针和数组符号的结合,它也使用了一个关于数组中元素个数的有用公式:
size_t n=sizeof a/sizeof a[0];
虽然可以在除数中使用任何一个有效的下标,但0是最安全的,这是因为每个数组都有第0个元素。当然,这一习惯只有当原始的数组声明是在生存期内才适用。
对于那些愿意使用C风格字符串的人来说,一个遵循指针和数组符号概念之间相互作用的常用习惯是:
strncpy(s,t,n)[n]='\0';
程序清单2.8 说明数组名是指针
// array1.cpp: 用一个数组名作为一个指针
#include <iostream>
using namespace std;
main()
{
int a[] = {0,1,2,3,4};
int* p = a;
cout << "sizeof a == " << sizeof a << endl;
cout << "sizeof p == " << sizeof p << endl;
cout << "p == " << p << ", &a[0] == " << &a[0] << endl;
cout << "*p == " << *p << ", a[0] == " << a[0] << endl;
p = a + 2;
cout << "p == " << p << ", &a[2] == " << &a[2] << endl;
cout << "*p == " << *p << ", a[2] == " << a[2] << endl;
}
//输出:
sizeof a == 20
sizeof p == 4
p == 0x0012ff78, &a[0] == 0x0012ff78
*p == 0, a[0] == 0
p == 0x0012ff80, &a[2] == 0x0012ff80
*p == 2, a[2] == 2
程序清单2.9 使用索引和指针传递数组
// array2.cpp: 使用索引和指针传递数组
#include <iostream>
using namespace std;
main()
{
int a[] = {0,1,2,3,4};
size_t n = sizeof a / sizeof a[0];
//使用数组索引打印
for (int i = 0; i < n; ++i)
cout << a[i] << ' ';
cout << endl;
//你甚至可以交替a和i(但自己别这么做!)
for (int i = 0; i < n; ++i)
cout << i[a] << ' ';
cout << endl;
//使用指针打印
int* p = a;
while (p < a+n)
cout << *p++ << ' ';
cout << endl;
//和指针一起使用索引符是好的:
p = a;
for (int i = 0; i < n; ++i)
cout << p[i] << ' ';
cout << endl;
//和数组一起使用指针符是好的:
for (int i = 0; i < n; ++i)
cout << *(a+i) << ' ';
cout << endl;
//使用指针向后打印:
p = a + n-1;
while (p >= a)
cout << *p-- << ' ';
cout << endl;
//写在下方的负数是允许的:
p = a + n-1;
for (int i = 0; i < n; ++i)
cout << p[-i] << ' ';
cout << endl;
}
//输出:
0 1 2 3 4
0 1 2 3 4
0 1 2 3 4
0 1 2 3 4
0 1 2 3 4
4 3 2 1 0
4 3 2 1 0
这就把一个字符串拷贝到另一个字符串,同时确保没有溢出并且字符串没有被划上界限(假设n没有超限)——所有这些都在一个简短的语句当中实现。
在指针和数组名之间有另一个区别需要记住:一个数组名是一个不可改变的左值。这就意味着不能改变数组名对应的地址,就像下面的示例所尝试的那样:
int a[5],b[5],*p;
/*下面所有的都不合法*/
a++;
a=p+5;
b=a;
如果这样赋值,就会很轻易地丢失数组在内存中存储的位置(这可不是个好主意!)。
从字面上来说字符串是一组没有名称的字符,可以使用sizeof得到它们的大小并且甚至可以给它们添加下标(见程序清单2.10 和2.11)。注意在清单2.10中我的编译器把“hello”的每一次的出现都作为一个独立的对象,每次都返回不同的地址,有些编译器能够把具有相同字符的字符串“集中起来”以单个形式出现以节省空间。
程序清单2.10 说明一个字符串中的字符是一个匿名数组
// array3.cpp
#include <iostream>
using namespace std;
main()
{
char a[] = "hello";
char* p = a;
cout << "a == " << &a << ", sizeof a == " << sizeof a << endl;
cout << "p == " << (void*)p << ", sizeof p == " << sizeof p << endl;
cout << "sizeof \"hello\" == " << sizeof "hello" << endl;
cout << "address of \"hello\" == " << (void*)"hello" << endl;
cout << "address of \"hello\" == " << (void*)"hello" << endl;
}
//输出:
a == 0x0012ff84, sizeof a == 6
p == 0x0012ff84, sizeof p == 4
sizeof "hello" == 6
address of "hello" == 0x004090d4
address of "hello" == 0x004090f1
程序清单2.11 将字符串中的字符进行索引
// array4.cpp: 将字符串中的字符索引
#include <iostream>
using namespace std;
main()
{
for (int i = 0; i < 10; i += 2)
cout << "0123456789"[i];
}
//输出:
02468
练习 2.1
已知如下声明:
int a[ ] = { 10, 15, 4, 25, 3, -4 };
int *p = &a[ 2 ];
下面表达式的结果是什么?
a. *(p+1)
b. p[-1]
c. p-a
d. a[*p++]
e. *(a+a[2])
当你把数组作为参数传递给一个函数,正如所预期的那样,是传递了指向数组第一个元素的指针。因此,可以在调用的函数中永久地改变数组元素的值。在程序清单2.12的函数f中,地址&a[0]按值传递给指针b,因此表达式b[i]就和表达式a[i]完全是一样的了。不可能按值传一个完整的内置数组。
即使用数组符号定义了参数b,即:
int b[]
它同下面这种写法是完全一样的。
int *b
程序清单2.12 说明作为参数的数组实际上是指针
// array5.cpp: 数组作为参数
#include <iostream>
using namespace std;
void f(int b[], size_t n)
{
cout << "\n*** Entering function f() ***\n";
cout << "b == " << b << endl;
cout << "sizeof b == " << sizeof b << endl;
for (int i = 0; i < n; ++i)
cout << b[i] << ' ';
b[2] = 99;
cout << "\n*** Leaving function f() ***\n\n;
}
main()
{
int a[] = {0,1,2,3,4};
size_t n = sizeof a / sizeof a[0];
cout << "a == " << a << endl;
cout << "sizeof a == " << sizeof a << endl;
f(a,n);
for (int i = 0; i < n; ++i)
cout << a[i] << ' ';
}
//输出:
a == 0x0012ff78
sizeof a == 20
*** Entering function f() ***
b == 0x0012ff78
sizeof b == 4
0 1 2 3 4
*** Leaving function f() ***
0 1 99 3 4
而且,sizeof(b)==4,这是我的操作平台上指针的大小。我们无法在另一个函数中自动地决定一个数组编译时的大小。
指向数组元素的指针参数在文本处理中非常普遍。函数(str_cpy)就可以把一个字符串拷贝到另一个字符串中(除了无返回值外,它与strcpy一样):
void str_cpy(char *s1,const char *s2)
{
while (*s1++ =*s2++);
}
while循环测试不是为了检测值相等,而是检测在赋值后(但是在自增之前)s1的值,循环在拷贝了结束符号‘\0’后停止,表达式*p++是一个很常用的C/C++习惯方式。
习题 2.2
下面的语句通过一系列指针表达式修改字符串 s,当顺序执行时每个表达式重新得到的是什么字符,最后的结果是什么?
char s[ ] = “ desolate”,*p = s;
*p++ = = ?
*(p++)= = ?
(*p)++= = ?
*++p = = ?
*(++p)= =?
++*p = = ?
++(*p)= =?
Strcmp(s,? ) = =0
(感谢Lincoln实验室的Chet Small提供了这个非常聪明的例子)。
有两种方式来描述C风格的字符串数组:(1)指针数组;(2)二维数组。程序清单2.13中的程序说明了第一种方式。内存分布如下:
程序清单2.13 用指向字符的指针数组来实现字符串
// array6.cpp:粗糙的数组
#include <iostream>
#include <cstring>
using namespace std;
main()
{
char* strings[] = {"now","is","the","time"};
size_t n = sizeof strings / sizeof strings[0];
//从粗糙的数组打印
for (int i = 0; i < n; ++i)
cout << "String " << i <<" == \"" << strings[i]
<< "\",\tsize == " << sizeof strings[i]
<< ",\tlength == " << strlen(strings[i])
<< endl;
}
//输出:
String 0 == "now", size == 4, length == 3
String 1 == "is", size == 4, length == 2
String 2 == "the", size == 4, length == 3
String 3 == "time", size == 4, length == 4
由于字符串可以有不同的长度,所以这一类型的数组有时被称为粗糙的(ragged)数组。这一方式仅使用了容纳数据所需的内存数量,再加上指向每个字符串的指针。运行时系统传递给main函数的命令行参数数组argv是一个粗糙的数组。
粗糙的数组方式的一个不利之处是,在大多数环境中,需要动态地为每个字符串分配内存(参见第20章)。如果你不介意浪费一小部分空间,而且如果你也知道会遇到的最长的字符串的长度,就可以使用一个固定大小的区域来存储二维字符数组(每行一个字符串)。程序清单2.14中数组的内存区域分布如下:
程序清单2.14 作为二维字符数组中的行来实现字符串
// array7.cpp: 在二维字符数组中存储字符串
#include <iostream>
#include <cstring>
using namespace std;
main()
{
char array[][5] = {"now","is","the","time"};
size_t n = sizeof array / sizeof array[0];
for (int i = 0; i < n; ++i)
cout << "array[" << i << "] == \"" << array[i]
<< "\",\tsize == " << sizeof array[i]
<< ",\tlength == " << strlen(array[i])
<< endl;
}
//输出:
array[0] == "now", size == 5, length == 3
array[1] == "is", size == 5, length == 2
array[2] == "the", size == 5, length == 3
array[3] == "time", size == 5, length == 4
正如这个程序所表明的,如果多维数组的第一维能从它的初始化中推断出来,那么就不必具体指出多维数组的第一维。
在程序设计语言中 C++在某种程度上是独一无二的,因为在使用数组时仅可以使用其部分下标。就像程序清单2.14中的程序所使用的那样,表达式array[i]是指向第i行的指针。对于一个定义为int a[2][3][4]的数组,a[i]代表的是什么呢?而a[i][j]又是什么呢?请继续读下一节。
实际上,在C++中没有多维数组!至少对多维数组没有直接的支持。人们通常把一个一维数组看作一个向量,把一个二维数组看作一个表或者矩阵,把一个三维数组看作一个长方体。然而,数组的几何模型使明智地使用高维数组变得很困难,取而代之的是C++支持“数组的数组”的概念。例如,如果一个一维的整型数组为
int a[4]={0,1,2,3};
它是一个有索引的整数集合:
我们通常把它描述成一个向量:
对一个二维整型数组,如:
int a[3][4]={{0,1,2,3},{4,5,6,7},{8,9,0,1}}
是像下面这样的向量集合:
或者,如果喜欢的话可写成:
这就是数组的集合。因此,a就是一个“3个具有4个整型元素的数组的数组”,a[0]就是这些具有4个整型元素的数组之一。由于在表达式中使用数组名时,它总是被解释为指向它的第一个元素的指针,所以一个诸如 a+1 之类的表达式就是“指向一个具有4个整型元素的数组的指针”,在这种情况下,该表达式将指向第二行(即a[1])。程序清单2.15中的程序说明了如何声明指向一个数组的指针,这样的指针可以在不改变下标语法的情况下,替换数组名。初学者容易错误地假设:指向整型数据的指针能代替一个整型数组名,就像以下程序:
int a[]={0,1,2,3},*p=a;
/*...*/
p[i]=...
那么,这样指向整型数据的指针的指针将会对二维数组进行同样的操作,就像下面:
int a[][4]={ {0,2,3,4},{4,5,6,7},{8,9,0,1}};
int **p=a; /*受怀疑的指针转换*//*...*/
p[i][j]=...
要弄清以上做法为何不对,考虑一下表达式p[i][j],根据“重要的指针原则2”,这就相当于:
*(p[i]+j)
也相当于:
*(*(p+i)+j)
程序清单2.15 说明在二维数组中指向一维数组的指针
// array8.cpp: 使用一个一维数组指针
#include <iostream>
main()
{
int a[][4] = {{0,1,2,3},{4,5,6,7},{8,9,0,1}};
int (*p)[4] = a; //指向包含4个整型成员的数组
size_t nrows = sizeof a / sizeof a[0];
size_t ncols = sizeof a[0] / sizeof a[0][0];
cout << "sizeof(*p) == " << sizeof *p << endl;
for (int i = 0; i < nrows; ++i)
{
for (int j = 0; j < ncols; ++j)
cout << p[i][j] << ' ';
cout << endl;
}
}
//输出:
sizeof(*p) == 16
0 1 2 3
4 5 6 7
8 9 0 1
由于p是一个指向整型数据的指针,所以表达式 p+i 往 p后面移动的距离等于i个指针的大小,而不是 i 行(我们需要移动 i 行)。显然,我们需要的指针类型是 p 所指向的指针具备一个行的大小,因此 p 必须是一个行指针,也就是说它指向一个大小合适的数组。因此,有趣但又有逻辑性的语法为:
int (*p)[4]=a;
根据p的这个定义,编译器依据下面的步骤来求表达式*(*(p+i)+j)的值:
1.p+i
这一步计算行指针,该行超出行 p 当前所指向的 i 行。
2.*(p+i)
这是具体的行(是一个数组)。
3.由于数组 *(p+i)不是 sizeof 或者 & 运算的操作数,所以它被指向它第一个元素的指针所替代,即 &a[i][0] ,这是一个指向整型(int)的指针。
4.&a[i][0]+j
因为&a[i][0]是一个指向整型(int)的指针,加上j就把这个指针向前移动了j个整型数据单位,结果是&a[i][j]。
5.*&a[i][j]
这就是整型a[i] [j]。
表2.1总结了指针和二维数组的这种关系,注意,不要因为a+1和a[1]有同样的值(0 ×8)而得出以下的结论:
a[i]==a+i // 错误!
它们仅仅在数值上相等,而类型却不一样,因为 sizeof(a+1)==2(一个指针),而sizeof(a[1])= =8(一个有4个整型数的数组)。
我们可以很自然地得出以下结论:一个三维数组是二维数组的集合。
int a[2] [3] [4]={{{0,1,2,3},{4,5,6,7},{8,9,0,1}},
{{2,3,4,5},{6,7,8,9},{0,1,2,3}}};
这个数组的第一个元素是一个“二维数组” a[0](从技术上来说,a[0]是一个由 3 个含有4个整数的数组的数组),为了使指针与数组名a一致,定义:
int (*p) [3] [4] = a;
程序清单2.16是一个应用该指针的程序示例,表2.2是与表2.1相似的三维数组。
其中十六进制值是相对于a的地址偏移量。
程序清单2.16 说明在三维数组内指向二维数组的指针
// array9.c: 使用一个二维数组指针
#include <iostream>
using namespace std;
main()
{
int a[][3][4] = {{{0,1,2,3},{4,5,6,7},{8,9,0,1}},
{{2,3,4,5},{6,7,8,9},{0,1,2,3}}};
int (*p)[3][4] = a;
size_t ntables = sizeof a / sizeof a[0];
size_t nrows = sizeof a[0] / sizeof a[0][0];
size_t ncols = sizeof a[0][0] / sizeof a[0][0][0];
cout << "sizeof(*p) == " << sizeof *p << endl;
cout << "sizeof(a[0][0]) == " << sizeof a[0][0] << endl;
for (int i = 0; i < ntables; ++i)
{
for (int j = 0; j < nrows; ++j)
{
for (int k = 0; k < ncols; ++k)
cout << p[i][j][k] << ' ';
cout << endl;
}
cout << endl;
}
}
//输出:
sizeof(*p) == 48
sizeof(a[0][0]) == 16
0 1 2 3
4 5 6 7
8 9 0 1
2 3 4 5
6 7 8 9
0 1 2 3
其中,十六进制值是相对于a的地址偏移量。
在程序清单2.15和程序清单2.16中的程序表明如何确定一个数组和它所有的子数组的秩(即维数),对于程序清单2.15中的二维数组a,秩是它所包含的行数(一维对象),也就是:
sizeof a /sizeof a[0]
从名称上讲,每一个行的秩就是a中每一行的整数的个数(0维对象):
sizeof a[0] /sizeof a[0][0]
总的来说,如果a是一个n维数组,那么,a为n−1维对象
sizeof a /sizeof a[0]
的集合,a中每一个(n-1)维对象包含n-2维对象
sizeof a[0] /sizeof a[0][0]
每一个(n-2)维对象依次包含(n-3)维对象
sizeof a[0][0] /sizeof a[0][0][0]
等等,直到每一个一维对象中零维对象的个数为
sizeof a[0][0]…[0] /sizeof a[0][0]…[0][0]
{n-1 subscripts} {n-1 subscripts}
练习2.3
模仿表2.1和表2.2,完成下面四维数组的数组指针转换表:
int a[2][3][4][5] =
{
{
{ {0,1,2,3,4},{5,6,7,8,9},{0,1,2,3,4},{5,6,7,8,9} },
{ {0,1,2,3,4},{5,6,7,8,9},{0,1,2,3,4},{5,6,7,8,9} }
{ {0,1,2,3,4},{5,6,7,8,9},{0,1,2,3,4},{5,6,7,8,9} }
},
{
{ {0,1,2,3,4},{5,6,7,8,9},{0,1,2,3,4},{5,6,7,8,9} },
{ {0,1,2,3,4},{5,6,7,8,9},{0,1,2,3,4},{5,6,7,8,9} }
{ {0,1,2,3,4},{5,6,7,8,9},{0,1,2,3,4},{5,6,7,8,9} }
}
};
一个指针可以指向函数也可以指向存储的对象。下面的语句声明fp是一个指向返回值为整型(int)的函数的指针:
int(*fp)( );
*ftp的圆括号是必需的,没有它的语句
int *fp( );
将fp声明为一个返回指向整型(int)指针的函数。这就是将星号与类型声明紧密相连的方式成为逐渐受人们欢迎的方式的原因之一。
int* fp(); //方式说明fp()返回一个指向整型的指针(int * )
当然,这种方式建议你通常应该每条语句只声明一个实体,否则,以下的语句将会使人感到迷惑:
int *ip, jp; //jp不是一个指针!
如果想具体说明fp指向的函数就必须带有一定的参数,如果是一个浮点型(float)和一个字符串型,那么可以这样写:
int (*fp) (float, char * );
然后可以在fp中存储这样一个函数的地址:
extern int g(float,char*);
fp=g;
表达式中函数的名字解析地址可以认为是指向那个函数代码区的开始的地址。下面的“hello,world”程序说明如何通过指针来执行一个函数。
/*hello.c: 通过函数的指针来说hello */
#include <stdio.h>
main()
{
void ( * fp )( )=printf;
fp("hello, world\n");
}
要通过指针来执行一个函数,你可能认为得这样写:
(*fp)("hello world\n");
来复引用指针。事实上,在ANSI C出现以前必须这样做,但是ANSI C 委员会决定容许像我在hello.c中那样使用的普通函数调用句法。由于编译器知道它是一个指向函数的指针,并且它还知道在该环境下所能做的惟一的一件事就是调用函数,因此这里没有任何模糊不清的表达。
当把函数名作为一个参数传递给另一个函数时,编译器实际上给这个函数传递了一个指针(与数组名类似)。但是你为什么曾经想过给另一个函数传递函数指针呢?C标准库中的排序函数 qsort 的使用就是一个例子,采用简单和复合的排序关键字,它可以对由任何类型的元素所组成的数组排序。程序清单2.17中的程序说明怎样排序命令行参数字符串,在这种情况下所需要做的全部事情就是传递给qsort一个知道如何比较字符串的函数。一个函数(像qsort)通过由运行时决定的指针来调用另一个函数(像 comp)的行为叫做返调(callback)。
函数指针的数组在菜单驱动应用程序中很容易见到,假设想将以下的目录展现给用户:
1.返回
2.插入
3.更新
4.退出
程序清单2.18的程序直接把键盘输入作为索引放到指向处理每一个菜单选择函数的指针的数组中。
程序清单2.17 用qsort函数将命令行参数排序
// sortargs.cpp: 排序命令行参数
#include <iostream>
#include <cstring>
#include <cstdlib>
using namespace std;
int comp(const void*, const void*);
main(int argc, char *argv[])
{
qsort(argv+1, argc-1, sizeof argv[0], comp);
while (--argc)
cout << *++argv << endl;
}
int comp(const void* p1, const void* p2)
{
const char *ps1 = * (const char**) p1;
const char *ps2 = * (char**) p2;
return strcmp(ps1,ps2);
}
//从"sortargs *.cpp"命令输出:
address.cpp
arith.cpp
array1.cpp
array2.cpp
array3.cpp
array4.cpp
array5.cpp
array6.cpp
array7.cpp
array8.cpp
array9.cpp
bit1.cpp
bit2.cpp
convert.cpp
inspect.cpp
pointer.cpp
ptr2ptr.cpp
sortargs.cpp
swap1.cpp
swap2.cpp
程序清单2.18 用函数指针数组来处理菜单选择
/* menu.c: 举例说明函数数组*/
#include <stdio.h>
/*你一定要给这些提供定义*/
extern void retrieve(void);
extern void insert(void);
extern void update(void);
extern int show_menu(void); /*返回keypress */
main()
{
int choice;
void (*farray[])(void) = {retrieve,insert,update};
for (;;)
{
choice = show_menu();
if (choice >= 1 && choice <= 3)
farray[choice-1](); /* 进程的需要 */
else if (choice == 4)
break;
}
return 0;
}
如果返调函数是某个类的成员函数将会怎样?获得指向类成员的指针与获得指向非成员实体的指针的方式相似,只存在很小的语法变化。例如,考虑下面类的定义:
class C
{
public:
void f ( ) {cout << "C::f\n";}
void g( ) {cout << "C::g\n";}
};
可以这样定义一个指向C类成员函数的指针:
void (C::*pmf) ( ); // pmf是指向C的成员函数的指针
//不带参数并且没有返回值
可以根据需要来初始化pmf使其指向不同的成员函数,如下面的程序所示:
main( )
{
C c;
void (C::*pmf) ( ) =&C::f;
(c.*pmf) ( );
pmf =&C:: g;
(c.*pmf) ( );
}
//输出:
C::f
C::g
.* 运算符调用了代表其左边操作数所指向的对象的成员函数。如果 cp 是一个指向C 的指针,那么可以使用->*运算符,就像:
( cp->*pmf ) ( );
由于C++定义运算符优先级的方式,所以圆括号是完全确认所调用的函数所必需的。请参见程序清单2.19(程序清单2.18菜单例子的指向成员函数指针的版本)。
程序清单2.19 举例说明对成员的指针
// menu2.cpp: 使用指向函数成员的指针
#include <iostream>
using namespace std;
class Object
{
public:
void retrieve() {cout << "Object::retrieve\n";}
void insert() {cout << "Object::insert\n";}
void update() {cout << "Object::update\n";}
void process(int choice);
private:
typedef void (Object::*Omf)();
static Omf farray[3];
};
Object::Omf Object::farray[3] =
{
&Object::retrieve,
&Object::insert,
&Object::update
};
void Object::process(int choice)
{
if (0 <= choice && choice <= 2)
(this->*farray[choice])();
}
main()
{
int show_menu(); //你所提供的!
Object o;
for (;;)
{
int choice = show_menu();
if (1 <= choice && choice <= 3)
o.process(choice-1);
else if (choice == 4)
break;
}
}
{
好的编程习惯能隐藏用户不必知道的执行细节。例如,要执行一个整数栈,可以为用户提供如下的头文件:
// mystack.h
class StackOfInt ( )
{
public:
StackOfInt ( ) ;
void push ( int );
int pop ( ) ;
int top ( ) const;
int size ( ) const;
private:
enum { MAX_STACK = 100 };
int data [ MAX_STACK];
int stkPtr;
};
因为下面几行的数组和栈指针是私有的,因此用户必须通过你提供的公共接口按照你的方式进行操作(程序清单2.20就是一个例子)。即使成员函数的实现对于用户是隐藏的,但他(或她)可以看一下头文件就很容易地推断出与程序清单2.21相似的内容。如果隐藏所有的实现细节对你来说很重要,你可以通过使用不完全类型增加一个额外的保护层。
不完全类型的大小不能在编译期的时候确定。下面的声明就是一个例子:
extern int a [];
这个声明定义 a 是一个未知长度的数组,所以不能使用 sizeof,否则将得到一个错误信息。这个定义完全不同于:
extern int *a;
这是一个有大小的指针。可以通过在另一个类中隐藏它的实现细节来加强上面栈类型的封装性。在程序清单2.22中,只有一个指向 StackImp的指针出现在StackOfInt的私有部分中。语句
class StackImp;
是 StackImp 的一个不完全声明,它仅仅表明了类的存在。一旦只有一个 StackImp 的指针或引用出现在 stack2.h中,用户就不需要stkimp.h。StackOfInt的成员函数目前只是传递请求到StackImp(见程序清单2.23和程序清单2.24)。
程序清单2.20 StackOfInt 的定义
// tstack.cpp: 使用StackOfInt 类
#include <iostream>
#include "mystack.h"
using namespace std;
main()
{
StackOfInt s;
s.push(10);
s.push(20);
s.push(30);
while (s.size())
cout << s.pop() << endl;
}
//输出:
30
20
10
程序清单2.21 StackOfInt 的实现
// stack.cpp: StackOfInt 类的实现
#include "stack.h"
StackOfInt::StackOfInt()
{
stkPtr = 0;
}
void StackOfInt::push(int i)
{
if (stkPtr == MAX_STACK)
throw "overflow";
data[stkPtr++] = i;
}
int StackOfInt::pop()
{
if (stkPtr == 0)
throw "underflow";
return data[--stkPtr];
}
int StackOfInt::top() const
{
if (stkPtr == 0)
throw "underflow";
return data[stkPtr - 1];
}
int StackOfInt::size() const
{
return stkPtr;
}
程序清单2.22 为了更好地封装而使用不完全类型
// stack2.h: 隐藏堆栈的实现
class StackImp;
class StackOfInt
{
public:
StackOfInt();
~StackOfInt();
void push(int);
int pop();
int top() const;
int size() const;
private:
StackImp* imp;
};
程序清单2.23 StackOfInt类的实现
// stack2.cpp
#include "stack2.h"
#include "stkimp.h"
StackOfInt::StackOfInt()
: imp(new StackImp)
{}
StackOfInt::~StackOfInt()
{
delete imp;
}
void StackOfInt::push(int i)
{
imp->push(i);
}
int StackOfInt::pop()
{
return imp->pop();
}
int StackOfInt::top() const
return imp->top();
}
int StackOfInt::size() const
{
return imp->size();
}
程序清单2.24 堆栈的实现
// stkimp.cpp
#include "stkimp.h"
StackImp::StackImp()
{
stkPtr = 0;
}
void StackImp::push(int i)
{
if (stkPtr == MAX_STACK)
throw "overflow";
data[stkPtr++] = i;
}
int StackImp::pop()
{
if (stkPtr == 0)
throw "underflow";
return data[--stkPtr];
}
int StackImp::top() const
{
if (stkPtr == 0)
throw "underflow";
return data[stkPtr - 1];
}
int StackImp::size() const
{
return stkPtr;
}
C和C++仅仅与那些使用它们的人一样危险。
指针是地址。
可以将任何一个指针赋值成void*。
注意区分一个const指针和一个指向const的指针。
p±n = =(char*)p±n* sizeof (*p)。
p-q = = ±n 。
*(a+i) = = a [i]。
除非在sizeof和&的上下文中,否则一个数组名即是指向它第一个元素的指针。
没有多维数组,只有数组的数组。
仅是指针的存在并不要求它所引用的类型的实现的有效性(这是一个不完全类型)。
如果理解了这些概念,你就正在逐渐地成为一名可信赖的C++程序员。现在去告诉你的老板,她可以指派你去编写真正的程序。
练习答案
练习2.1
已知如下声明:
int a[ ] = { 10, 15, 4, 25, 3, -4 };
int *p = &a[ 2 ];
下面表达式的结果是什么?
a. *(p+1) 25
b. p[-1] 15
c. p-a 2
d. a[*p++ ] 3
e. *(a+a[ 2 ]) 3
练习2.2
下面的语句通过一系列指针表达式修改字符串 s,当顺序执行时每个表达式重新得到什么字符,最后的结果是什么?
char s[ ] = "desolate",*p = s;
*p++ = = d;
*(p++)= = e;
(*p)++= = s;
*++p = = o;
*(++p)= = l;
++*p = = m;
++(*p )= = n;
strcmp(s,"detonate") = = 0;
练习2.3
其中十六进制值是相对于a地址偏移量。
0x0f0 = = 240
0x140 = = 320
0x154 = = 340
0x158 = = 344