作者:[美]ChuckAllison(著)
出版社:信息技术第一出版分社
格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT
C和C++代码精粹试读:
前言
本书适合于那些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 Allisonhttp://www.freshsources.com1997年11月第一部分预备知识第1章更好的C1.1 两种语言简述
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运行时所需的庞大运行期资源(后者是环境的,不只是编译和连接过程)。
下面将介绍其更多的优点。1.2 循序渐进
在没有完全掌握C++的情况下也可以有效地使用它。事实上,面向对象技术承诺如果供应商为重用、可扩展性提供设计好的类库,那么建立应用程序的工作就很容易了。现有的开发环境,及其应用程序框架和可视化组件,正在兑现这一承诺。
如果觉得必须要掌握这种语言,可以一步步地去做,并且在这一过程中可以取得丰硕的成果。已出现的3个“顶峰”是:
1.更好的C;
2.数据抽象;
3.面向对象编程。
由于C++比C更安全、更富于表达,所以可以将它作为一个更好的C使用。这个顶峰上的特征包括类型安全连接、强制的函数原型、内嵌函数、const限定修饰符(C从C++中借用了它),函数重载、默认参数、引用、动态内存管理的直接语言支持。也需要意识到C++和它前身之间存在着不兼容性。在这章中将探究一些使C++成为更好的C的非面向对象的特征。因为如果不说明基于类的起源就想阐明某些更好的C特征是很困难的,所以我也将解释C++的类机制。1.3 类型系统
理解C++最重要的部分,也许就是它对于类型安全(type safety)的贡献。上面所提及的其他面向对象语言实质上是无类型的,或最多也只能说是弱类型的,因为它们主要是在程序运行期间执行错误检查,换句话说,C++要求声明每个程序实体的类型,并且在编译期内它要一丝不苟地检查相同用法。正是类型安全而不是其他别的特点,使 C++成为更好的 C,成为常用编程工作的最合理的选择。类型系统的特征包括函数原型、类型安全连接、新风格的类型转换、运行期类型识别(RTTI)(有关类型转换和RTTI的内容请参见第10章)。1.4 函数原型
在C++中,函数原型不是可选的。事实上,在ANSI C委员会采用原型机制以前,它是为C++发明的。在你第一次使用函数前必须声明或定义每个函数,编译器将检查每个函数调用时正确的参数数目和参数类型。此外,在其应用时将执行自动转换。下列程序揭示一个在C中不使用原型时出现的普通错误。
/* convert1.c */
#include
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
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
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
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)按错误处理。1.5 类型安全连接
C++甚至可以通过编译单元检测出不正确的函数调用,程序清单 1.2 的程序调用了程序清单1.3中的一个函数。当把它作为C程序编译时,会得到一个错误的输出结果:
f: 0.000000
程序清单1.2 解释程序连接(也见程序清单1.3)
void f(int);
main()
{
}
f(1);
程序清单1.3 要与程序清单1.2连接的函数
#include
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)并报错。1.6 引用
由于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 所示,也可以通过引用从函数中返回一个对象,在赋值语句的左边是一个函数调用,这看起来有些奇怪,但是这在运算符重载时是方便的(尤其是=和[ ])。1.7 类型安全I//O
当然每个C程序员都曾经使用过printf的错误格式描述符号。对printf来说没有办法检查所传递的数据项是否与字符串格式匹配。
程序清单1.4 一个说明引用调用的交换函数
// swap.cpp
#include
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< 表达式 cout< double x = 1.5, y = 2.5; //从现在起保留小数点后两位 cout.precision(2); //保持小数点后的0 cout.setf(ios::showpoint); cout< cout< C++中有4个预定义的流:cin(标准输入),cout(标准输出),cerr(标准错误),clog (标准错误)。除了cerr外其余都是全缓冲流。就像stderr一样,cerr的行为好象是非缓冲的,但事实上它是单元缓冲的,也就是说它在处理完每一个对象而不是每一个字节后会自动清除缓冲。例如,带有单元缓冲的语句: cerr<<“hello”; 缓冲处理5个字符,然后清除缓冲区。一个非缓冲处理的流会立即发送每个字符到它的最终目的地。 程序清单1.5 通过引用从函数中返回一个对象 // retref.cpp:返回一个引用 #include 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 using namespace std; main() { char c; while (cin.get(c)) cout.put(c); } 注意到标准头文件名(即iostream)不再使用一个.h的后缀。几乎所有C++标准库中的内容,包括流,都驻留于名字空间(namespace std)中。一个名字空间就是一个包括声明在内的已命名的范围。上面第二行 using 指令指示编译器在翻译期间查找声明的名字时搜寻std。标准C头文件也存在于C++程序的std标准名字空间中,并以字母c作为前缀。为了包含 #include < cstdio > using namespace std; 或用通常的#include 一个从流中读取的函数称为提取器(extractor),而一个输出函数称为插入器(inserter)。get提取器从流中把下一个字节存放到它的char引用参数中,像多数流成员函数一样,get返回流本身。当一个流出现在像上面的while循环的布尔型上下文中,如果数据成功传递,它检验为true;如果有错误,则为false。就像试图过了文件尾还要读文件一样。尽管这样简单的布尔型检验在大多数时间能满足,但你可以在任何时候使用下面这些布尔型成员函数对流的状态进行询问: bad ( ) 严重错误 (流被误用) fail ( ) 转换错误 (数据不正确但流正常) eof ( ) 文件尾 good ( ) 上述都不是 下例程序实现逐行拷贝: // copy2.cpp: 逐行拷贝 #include 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 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.9 格式化 在程序清单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 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 using namespace std; main() { char c; //不要跳过空格符 cin.unsetf(ios::skipws); while (cin >> c) cout << c; }表1.1 格式化标志续表 一些格式化选项可以具有一定范围的值。例如,用来确定显示整型数基数的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 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.2 格式化域 程序清单1.10 显示整数的基数 // base1.cpp :显示整数的基数 #include 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,0x101.10 操纵器 当标识符 endl出现在一个输出流中时,一个换行字符就被插入并且流被刷新。标识符endl是操纵器的一个例子,即为了副效应而插入到流的一个对象。在〈iostream〉中被声明的系统预定义的操纵器列于表 1.3 中。程序清单 1.11 里的程序在功能上与程序清单 1.10的程序等价,但它是用操纵器来代替显式调用setf函数。操纵器经常可以使代码更为高效。表1.3 简单的操纵器(〈iostream〉)续表 程序清单1.11 用操纵器改变数据基数 // base2.cpp: 显示整数的基数 // (使用操纵器) #include 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.4 参数化的操纵器(iomanip) 程序清单1.12 利用setw函数设置输出域宽 // adjust.cpp: 调整输出 #include #include 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#####| 当然你可以用操纵器 …< 来替换语句 cout.fill('#'); 但在这种情况下,这样做似乎很不方便。 提取器通常忽视宽度设置,但C风格的字符串输入是个例外。在对字符数组进行提取操作之前应该先将域宽设置为字符数组的大小,以避免数据溢出。当处理输入行 nowisthetimeforall 时,程序清单1.13程序将输出: nowisthet,im,eforall 应记住的是,编译器将空白字符默认为分隔符,所以如果输入为: now is the time for all 那么输出将是: now,is,the 程序清单1.13 控制输入字符串宽度 // width.cpp: 控制输入字符串的宽度 #include #include 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< cout.setf(ios::boolalpha); //或仅插入操纵器boolalpha cout< 你可以通过简单定义一个将流引用作为参数并返回相同引用的函数来建立一个自己的操纵器。例如,下面是一个ASCII码控制铃声的操纵器,当将它插入在任何输出流中时可以发出铃声: //响铃操纵器 #include ostream& beep(ostream& os) { os< return os; } 使用时,只需插入: cout<<…< 程序清单 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章。1.12 运算符重载 在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 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是整型,它产生以下的函数运算: cout.operator<<(i); //ostream::operator<<(ostream&,int) 这样可以正确地格式化数据。 程序清单 1.14 表明如何通过重载用于复数的运算符<<来扩展标准流(输出在程序清单1.5中)。编译器将表达式 cout< 转换成下面的函数调用(在这里c是一个复数): operator << ( cout ,c) 这将依次采用operator<<(ostream&,const complex&)将操作分解成格式化系统预定义类型的对象。这个函数也返回流,因此,可以在一个单独的语句中链接多个插入流。如,表达式 cout< 变为 operator<<(operator<<(cout,c1),c2) 这要求operator<<(ostream&, const complex&)返回流,为了高效这是通过引用来实现的。1.13 内联函数 在程序清单1.14中所看到的关键字内联(inline)是提示编译器要把相应的代码“内联”。也就是说,直接把代码写入程序中而没有实际函数调用的开销。如果编译器准许了你的要求,它把每一次的函数调用都用相应函数的代码来代替,从而避免了实际函数调用的开销。这种机制不同于类似函数的宏,宏是在程序编译前实现文本的代换。内联函数具有实际函数的类型检查和语义,并且没有函数调用的开销和宏定义副作用的敏感性。例如,假如定义一个宏找出两个数中的较小者: #define min(x,y) ((x)<(y)?(x) :(y) 当带有增量参数时,这将是失败的,如: min(x++, y++) 由于内联函数具有真正函数的作用,因而不会出现这种问题。 程序清单1.15 使用复数数据类型 #include #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.14 默认参数 在一个函数声明中的默认参数用于指示该函数从它的原型中取值。在程序清单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 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 minutes1.15 new和delete 在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)
试读结束[说明:试读内容隐藏了图片]