读书笔记第二季(item31~60)

十一月 19th, 2009 No Comments »

31.关于成员指针:
成员指针和普通指针有很大不同。普通指针是用确定对象的地址进行初始化的,指向一个确定的对象。成员指针用类的成员(注意不是对象的成员,而是类的成员,也就是只有偏移量的信息,而没有初始地址的信息)初始化。如:
int A::*pm = &A::data;?//定义加初始化格式
此指针只能指向A类对象中的int型成员,而不能指向其它类型对象的int型成员或者A类的其它类型成员。当然,若A类有多个int型成员,pm是可以指向不同int型成员的。pm指向的对象时虚的,存储的是一个偏移量。要想引用必须提供初始地址即具体对象。如果有多个A类对象,是可以利用一个pm访问各自的同一个成员的,不需要重新赋值,原因仍然是pm存储的是一个偏移量。
例程:
#include <iostream>
using namespace std;

class A{
public:
?A(int a,int b,char c)
?{
??num=a;
??num2=b;
??ch=c;
?}
?int num;
?int num2;
?char ch;
};

class B{
public:
?int bb;
};

int main()
{
?A a(1,2,’r');
?A c(3,4,’w');
?int A::*pnum = &A::num;
?cout<<a.*pnum<<endl;
?pnum = &A::num2;
?cout<<a.*pnum<<endl;
?B b;
?cout<<c.*pnum<<endl;
?//pnum = &B::bb;?//error!?can’t convert from int B::* to int A::*
?//pnum = &A::ch;?//error!?can’t convert from char A::* to int A::*
}
成员函数指针与之类似。

32.关于函数指针(含成员函数指针):
1)定义一个普通函数指针,赋值并调用的语法:
int fun(float);?//函数原型
float flt_num = 1.5;?//调用函数所用参数
int (*fp)(float) = NULL;?//定义函数指针,指针名为fp,括号为必须
fp=&fun;??//给函数指针赋值,不写()
int result = (*fp)(flt_num);?//调用
2)定义一个成员函数指针,赋值并调用:
class MyClass{
public:
?int mf(float){}
};?//类和成员函数原型
int(MyClass::*mfp)(float) = NULL;?//定义成员函数指针,指针名为mfp
mfp=&MyClass::mf;?//赋值,同样注意,不写()
MyClass tmc;
MyClass* ptmc = new MyClass;??//分别实例化一个对象和指针用来调用函数指针所指的成员函数
int result2 = (tmc.*mfp)(flt_num);?//用实例调用函数指针
int result3 = (ptmc->*mfp)(flt_num)?//用类指针调用函数指针所指函数
2)注意事项:
a.指向普通函数的指针和指向成员函数的指针之间不兼容。
b.函数指针可以用来判断是否指向某函数:
if(fp==&fun)
if(fp>0)?//检查是否初始化过
c.使用不同调用规范的函数指针不兼容。
d.可以将函数指针作为参数和返回值。
e.->*操作符和.*操作符不仅可用于成员函数指针,同样可用于成员变量指针。

33.关于函数按值返回调用拷贝构造函数时的编译器优化:
本来,我们认为一个函数在按值返回函数结果时会调用拷贝构造函数用存储函数结果的临时对象初始化目标对象。但是某些编译器(如G++)会作出优化从而使得拷贝构造函数函数不被调用,有意思的是,它还要求一定有拷贝构造函数可用(如果声明为私有会报错),然而自己又不去调用(可用打印信息验证)。这种做法由下列C++标准支持:

当一个隐式声明的对象被拷贝到一个同类型的对象时,
编译器可以选择跳过拷贝的过程而直接将拷贝的终点对象作为构造的对象,
而不必考虑因此带来的任何副作用。

In such cases, the implementation treats the source and target of the omitted copy operation as simply two different ways of referring to the same object, and the destruction of that object occurs at the later of the times when the two objects would have been destroyed without the optimization.118)
在这种情况(优化情况)下,编译器实现将原始对象和被拷贝的对象视为对同一对象的两种不同方式的引用,并且在未优化的情况下,编译器会选择这两种引用中生存期较长的一种,在其生存期到期后才进行析构。

原帖地址:http://topic.csdn.net/u/20090412/11/276440f5-8a0b-4295-a9f4-b21137d76d4e.html?seed=9234162&r=60865449#r_60865449

34.继续说函数传值返回。
由于可能出现中断的原因,将函数返回值放在栈里是不安全的,唯一安全的地方是寄存器。C++正是把函数返回值放在寄存器里进行返回的。但对于较大的自定义对象寄存器可能放不下,对于这种情况,C++采取的方法是将返回地址作为调用函数的第一个参数入栈,在得出函数结果后不再放入寄存器,而是直接拷贝到目标地址。
当然很多编译器对此作出了优化,见32。

35.小技巧――如何设计一个随时能知道自己被实例化成多少个对象的类?
加入一个静态int成员,初始化为0,在构造函数中令其自增,析构函数中令其自减。

36.关于函数参数表中的占位符:
如果函数参数表中有的参数在函数定义中没有被引用过,编译器会给出一个警告。如果想保留这个参数留待后续开发(这样,重定义函数时不需要更改调用代码),又想消除警告,解决办法是不给此参数命名,这样就使之成为一个“占位符”,待用到时再命名。

37.printf()的格式化列表直到运行期才会进行评估,编译器是无法发现其格式不匹配之错误的。

38.C++对于C的两个设计原则:
1.一个能在C编译器中通过编译的程序也应该能够通过C++的编译器。C++编译器产生的仅有的错误和警告应该来源于C语言自身的漏洞,而且填补这些漏洞应该仅仅需要局部的修改。(实际上,用C++编译器进行编译往往能帮你发现C程序中隐含的错误。)
2.C程序在被C++编译器重新编译后,其行为不会被私自地改变。

39.提供cin和>>操作符仅仅是为了一种完整性。因为输入时如果遇到非期望的数据类型将导致程序行为的偏移,就像scanf()和printf()。
解决办法是在实际读入数据时,按行读入,待安全存入buffer后再做扫描和操作。这样就不必担心输入被不期望的数据阻塞。
按行读入的两个常见选择是get()和getline(),都是iostreams的成员函数。

40.输入输出流有多种版本,对标准I/O的,对文件的,对内存的,对string的。标准I/O相对特殊(因为它是标准I/O嘛),所以iostream类有预定义好的全局对象cin和cout供使用。对于其它类型的I/O类,由于不存在这种特殊的对象,所以不存在如cin和cout这样的预定义对象。要使用这些类都需要自定义对象。

41.迫使对象析构的小技巧,将对象出现的代码范围用大括号括起来。

42.关于streambuf:
streambuf类是各iostream类的幕后老大。iostream对象在外面接下各式各样的工作,然后交给幕后隐藏的streambuf对象来完成。每个流对象都有一个成员函数rdbuf()来返回指向其streambuf对象的指针。利用此指针便可调用streambuf类的各成员函数。rdbuf()返回的指针会随着streampos的变化而变化。
关于streambuf,有如下两个小用途有必要知道一下。
1)用streambuf指针作为<<的右操作数,可以把其对应的流对象里的所有内容传递到左操作数(即某流对象)中。或者这么说,左操作数喝干了右操作数的内容。如:
fstream fin(“x.txt”);
cout<<fin.rdbuf();
就把x.txt的所有内容输出到屏幕上咯。
2)用streambuf和get()搭配使用。get()有一个版本,用于把主人(即调用此成员函数get()的对象)的内容输入到另一个stream对象。有两个参数,第一个是目标streambuf的引用(对rdbuf()进行解引用得到),第二个是终止符,缺省为’\n’。由此得到将整个文件输出到标准输出的另一个办法:
fstream in(“x,txt”);
while(in.get(*cout.rdbuf()))
?in.ignore();
streambuf有很多成员函数供操作。
至于streambuf的更多知识,后续研究吧。
MSDN on streambuf:

All streambuf objects, when configured for buffered processing, maintain a fixed memory buffer, called a reserve area, that can be dynamically partitioned into a get area for input, and a put area for output. These areas may or may not overlap. With the protected member functions, you can access and manipulate a get pointer for character retrieval and a put pointer for character storage. The exact behavior of the buffers and pointers depends on the implementation of the derived class.

The capabilities of the iostream classes can be extended significantly through the derivation of new streambuf classes. The ios class tree supplies the programming interface and all formatting features, but the streambuf class does the real work. The ios classes call the streambuf public members, including a set of virtual functions.

The streambuf class provides a default implementation of certain virtual member functions. The “Default Implementation” section for each such function suggests function behavior for the derived class.

43.关于如何在流中定位:
streampos是fpos模板的一个实例化,typedef fpos<mbstate_t> streampos;
streampos用整数表示流对象中的位置,如果将streampos定位于流的末尾,则值等于流的字节数。streampos通常用于在流中进行定位。
用到的函数有tell函数和seek函数。
tellg() & seekg() 对应输入流(g for get);tellp() & seekp() 对应输出流(p for put)。
tell函数都是不含参数的,返回流当前的streampos。
seek函数用于定位,返回原流的引用。
有两个版本,第一个版本用于绝对定位,只含一个参数,是这里用到的和streampos配合使用的版本,参数就是streampos型作为定位目标。
另一个版本用于相对定位,含两个参数,第一个为移动距离,可正可负,第二个为移动方向,取以下三值:ios::beg,ios::cur,ios::end。

例程:
?ifstream in(“text.txt”);
?assert(in);
?cout<<in.rdbuf()<<endl<<endl;?//output the whole file
?in.seekg(-200,ios::cur);??//repos the streampos
?cout<<in.rdbuf()<<endl<<endl;?//output the last 200 characters
?in.seekg(0,ios::end);???//end of the file
?streampos sp = in.tellg();??//get the streampos
?in.seekg(sp/2);???//use the streampos to pos the stream
?streampos sp2 = in.tellg();??//get the streampos
?in.seekg(sp2);???//use the streampos to pos the stream
?cout<<in.rdbuf()<<endl<<endl;?//output the last half of the file

44.C中只能对文件进行定位。而C++可以对任何类型的流进行对位。

45.对同一个文件同时进行读写的一个方法。
用ios::in|ios::out标记声称该文件的一个ifstream对象用于读取数据。用该流的streambuf初始化一个ostream对象,用于写入数据。
如此生成的两个对象,get指针会被初始化在流首,put会被初始化在流尾。即作为输入时从头开始,作为输出时从尾开始。当然还可以用streampos进行重定位,便可以覆盖写入了。
例程(from thinking in c++):
//: C02:Iofile.cpp
// Reading & writing one file
#include “../require.h”
#include <iostream>
#include <fstream>
using namespace std;
int main() {
?ifstream in(“Iofile.cpp”);
?assure(in, “Iofile.cpp”);
?ofstream out(“Iofile.out”);
?assure(out, “Iofile.out”);
?out << in.rdbuf(); // Copy file
?in.close();
?out.close();
?// Open for reading and writing:
?ifstream in2(“Iofile.out”, ios::in | ios::out);
?assure(in2, “Iofile.out”);
?ostream out2(in2.rdbuf());
?cout << in2.rdbuf(); // Print whole file
?out2 << “Where does this end up?”;
?out2.seekp(0, ios::beg);
?out2 << “And what about this?”;
?in2.seekg(0, ios::beg);
?cout << in2.rdbuf();
} ///:~

46.strstream 内核格式化
istrstream用于内存格式化作为输入。利用字符串初始化一个istrstream对象,再用>>分别从中提取各个类型的值,是比C库中的atof(),atoi()更一般化更方便的“将字符串转换成类型值”的方法。
如:
istrstream in(“1.414 3 lakers”);
float f;int i;char buf[100];
in>>f>>i>>buf;
>>操作符优先考虑数据类型,而不是空格分隔符。
如in>>i>>f,那么i=1,f=0.414。

47.构造istrstream和ostrstream对象时都可以手工地为其分配内存:
istrstream::istrstream(char* buf);
istrstream::istrstream(char* buf,int size);
第一个构造函数得到的流,我们可以从中提取字符直到遇见0;第二个构造函数得到的流,我们可以一直提取到buf[size],遇到0也不必停。
ostrstream::ostrstream(char*,int,int=ios::out);
第一个参数是要输出的目的缓冲区,第二个是缓冲区大小,用来保证数组尾不被覆盖。第三个默认为ios::out,输出将从缓冲区的首地址开始覆盖,如果第三个参数设置为ios::ate或ios::app,则缓冲区被假定以0结尾,然后新数据输出从0开始,覆盖0。
需要知道的是,向ostrstream输出数据时不会添加通常所需要的0终止符,如果准备好终止输入时,请用ends操作算子添加0。

48.除了手工分配内存外,ostrstream对象还支持自动分配内存(istrstream则不支持)。也就是ostrstream类有一个不带参数的构造函数:
ostrstream A;
A自己负责自己在堆中的存储空间的分配,在9A中想放多少字节就放多少字节。如有必要,它自己可以移动存储块以分配更多的存储空间。
如果不知道需要多少存储空间,这是一个很完美很灵活的办法:
A<<”a string”<<endl<<ends;
cout<<A.rdbuf();
如果想得到A的字符被格式化到的物理地址怎么办?调用str()成员函数:
char *a = A.str();
可是一旦调用了str(),就不能再对A追加输入了(undefined行为),因为那样做有可能导致当前存储空间不够用而引起从新分配,那样a就不一定合法了。
对于自动分配内存的ostrstream对象(比如A),它在堆上的内存是有自己负责释放的,但是一旦调用了str(),ostrstream就不再负责清理存储器了,而要有程序员调用delete来删除:
delete A.str();
还有一个不太常见的方法:以0为参数调用freeze()解冻ostrstream。freeze()是streambuf的成员函数,所以如此调用:
A.rdbuf()->freeze(0);
这样A便解冻了,一切行为一如str()调用之前,只不过经过再次追加输出,先前用str()得到的物理地址可能不再合法。
如果只构造一个ostrstream对象而不对其插入字符串,那么打印其str()返回的指针值,将会是0(NULL指针),说明直到第一次插入字符时存储才被分配。

49.将任何指针强制转换成void*型后,可以打印出只针对应得16进制值:
char* a = “hello,world!”;
cout<<a<<endl;
cout<<(void*)a<<endl;

50.输出流的格式化
将数据输出到某流后,该流会对这些数据进行格式化然后存储,这里讲的是如何控制这些格式化(诸如域宽、填充字符、进制等等)。
有两个途径:1)使用成员函数flags()、setf()、unsetf()来设置。2)使用格式化操纵算子。
格式化操纵算子是直接嵌入<<语句中使用的。格式化算子有无参数和有参数两种。
具体就不细提了,查就好。
操纵算子是可以创建的。

51.<limit.h>
内含各种属性值的定义,如ULONG_MAX代表unsigned long能表示的最大值。可查看MSDN。

52.常量折叠
编译器尽量不为const常量分配内存,而是用值对常量名进行替代,然后将其记录在常量表里。除非遇见取地址等涉及到常量内存地址的行为才会为常量分配内存,再分配内存后更改此地址里的内容仍然不会改变常量的值,这就是“常量折叠”。可以认为这是编译器的一种优化。
另,对于常量表达式,编译器会进行计算将复杂的常量表达式计算出值,而不会把计算过程编辑到程序中去。
分不分配内存和是否折叠没有矛盾。分不分配内存指的是常量定义的地方,是否折叠指的是常量被使用的地方(如折叠,则用值代替)。
参考贴:
http://topic.csdn.net/u/20071016/13/4617d907-23ac-489e-81dc-90a80b118f0b.html
http://topic.csdn.net/u/20090520/09/1a4394ed-e41f-4783-99ed-5daad6735f1f.html
http://hi.baidu.com/lightningyaoyao/blog/item/c95edc583ef460dc9d820475.html

53.关于指针定义格式:
尽管int* i;这样的定义语句比int *i;这样的语句看起来更合理,因为这样int*可以被理解成一个不同于int的数据类型。但是实际不是这样的,*实际是和identifier联系在一起的,看一下例子:
int* i,j;
这样定义的话,i是指针而j是整型。

54.关于指向字符串字面值的指针:
虽然指向字符串字面值的指针不必是pointer to const,但是字符串字面值是编译器创建的且不容许更改的,因此若通过non-const指针更改其内容,虽无任何编译链接错误,却会导致运行期错误。如下:
char* p = “string”;?//可以通过编译,但逻辑上是错误的
*p = ‘a’;??//可以通过编译,但会引起运行期错误

55.有时为了表达式求值,编译器会建立临时对象,它们也需要存储空间,也会被构造和删除,但是我们从来看不到它们――编译器负责它们的去留以及存在的细节。它们自动成为常量。
如:
void f(X& x);
X g();
f(g());?//!error
这里,编译器会创建一个临时对象来保存g()的返回结果,然后用其调用f(),但由于临时对象是自动常量的,而f()取一个non-const-reference做参数,也就是意味着此X对象可能在f()内部被改变。编译器知道临时对象是马上要被销毁的,那样f()对其做的改变就可能丢失,因此给出一个编译期错误。

56.关于将指针作为函数的返回值:
将函数中的局部栈变量的地址返回是不可以的,因为这个地址在函数返回后将失效(栈被清理)。两种情况下可以用指针作为函数的返回值:
1.指向static变量的指针;
2.指向堆上分配的变量的指针;
这两种指针不会随着函数返回失效。

57.关于命令行参数的个数:
总是把程序名字作为argument zero,因此参数个数argc总是比实际从命令行传进来的多一个。

58.关于类的静态成员:
将类的成员声明为static的目的是为了:无论有多少个对象被创建,所有对象的该成员都共享一份内存,这为各个对象之间提供了一种通信机制。如果只有声明而没有定义,连接器会给出一个错误。
声明的时候(在类定义内部)可以给初值,而不会导致内存的分配(因为不是定义),而且这个给过初值声明过的静态成员名,可以作为编译期常量在类定义中使用(如作为buf大小),如此看来,编译器将其放入符号表?不然如何知道这个值呢?
由于静态成员的内存只有单独一份,对静态成员的定义必须放在一个单独而特殊的地方:类定义外,只定义一次,这样一来,应该将其放在实现文件里以免被包含造成多重定义。将其按照一般静态变量进行理解便可看出这么做的一般性。如果声明时已经给出初值,定义就不必再给。

59.如何在进入main()函数之前和离开main()函数之后得到执行代码的机会?
在C++中,全局静态对象的构造函数是在进入main()之前被调用的,自然析构就是在离开main()后被调用的,因此这提供了执行代码的机会――before and after main function。

60.Effective C++中提到的不同编译单元内的静态对象初始化相互依赖问题如何解决?
因为C++不保证各个编译单元的编译顺序,所以真正的编译顺序有可能与各编译单元内的静态对象初始化依赖关系相违背。如何解决?
方法1.Jerry Schwarz在创建iostream库时使用的方法:在库头文件中额外加一个类专门负责静态对象的初始化,该类只在自己的第一个对象被构造的时候才按照指定的顺序初始化静态对象。然后在此库头文件中定义该类的一个static对象,这样每个包含此库头文件的编译单元内都会构造一个此类的对象,而且由于是static的所以连接器不会抱怨多重定义。由于只在第一个被编译的编译单元内该对象被创建后会执行初始化工作(计数器为零,这严重依赖于在所有动态初始化进行前所有静态对象都会被初始化为零),因此不管哪个编译单元先被执行编译都能很好地完成静态对象的初始化工作。

有关内联函数的一些事

十一月 10th, 2009 4 Comments »

有关内联函数的一些事,《Thinking in C++》和《Effective C++》的学习笔记。

内联函数是C++用来替换宏而引入的。
C中的宏在省去函数调用的开销的同时引入了不易发现的BUG,主要是由对参数求值引起的。

一、内联函数如何起作用:
对于普通函数,编译器只把函数名称(对于C++来说也包含了参数类型?)和返回值记录在符号表里,对于内联函数除此之外还在符号表里记录其函数体(究竟存放源代码还是编译后的汇编指令就看编译器的实现了)。当遇到内联函数的调用时,编译器首先检查调用是否正确(参数类型检查,返回结果是否被正确使用――对于普通函数也进行这些检查),检查无误后将内联函数的函数体替换掉对它的调用,从而省去调用函数的开销(参数入栈,汇编CALL等)。

二、如何让函数成为内联:
使用关键字inline指定函数为内联。
声明函数为内联是没有意义也没有必要的,要让函数成为内联需要在定义时指定。
对于类的成员函数有一些特殊,在类的声明内定义的成员函数自动成为内联,在类体外定义的函数就需要显式指定了。
由于内联函数的定义必须被包含在每一个使用它的文件里,而定义的不统一又会造成多重定义的error,因此将内联函数的定义放在头文件里是合适的。
将内联函数的定义放在头文件里的根本原因是,大多数建制环境都是在编译期执行内联的,而编译期要将函数调用替代为函数体,就需要指导函数长什么样。
――>扩展:
这点也和function template相同,函数模板也通常放在头文件里。这是因为大多数编译器在编译期进行函数模板的具现化,因此它需要知道函数模板长什么样子。
所以不应该有“函数模板都是inline”的推论。

三、编译器和内联函数:
有几种情况编译器无法执行函数的内联,遇到这种情况,即使你将函数定义为内联,编译器仍然会将其按照普通函数处理――为函数体分配存储空间。如果这必须在多个编译单元之间进行,这很可能导致多重定义错误,若一定要执行的话(取决于编译器),链接器会被告知忽略多重定义。下面列出这些情况:
1)函数过于复杂。这取决于编译器,但大多数编译器都会选择放弃,这时候即使内联你也得不到任何显著的效率提升。一般,函数体内带有循环或者递归,都会被认为过于复杂。
2)函数被取址时。这就迫使编译器不得不为其分配内存从而产生一个地址,然而在不被取址的地方,内联仍有可能被执行。
另外,对virtual函数的调用也做不到inlining,因为virtual意味着“等待,直到运行期才知道调用哪个函数”,因此编译器无法知道调用的具体函数,也就没法进行代码替换。
一个不应存在的顾虑:
由于类的声明中给出函数的定义时,可能类的全部成员函数没有都完成声明,所以可能会有这样的顾虑:
class A{
??????? int i;
public:
??????? A() :i(0){}
??????? int f() const { return g() + 1; }
??????? int g() const { return i; }
};

main()
{
??????? A a;
??????? a.f();
}
f()定义内调用了g(),而此时g()还没有声明。
其实这种顾虑是不必要的,因为语言定义规定在类中所有内联函数的评估都要在类声明完成时进行。

四、何时使用内联:
内联肯定会提高程序的效率,尽管有时这种提高微乎其微。
内联不一定会带来代码膨胀,如果函数体十分短小,甚至比调用函数所生成的代码都小,那就不会代码膨胀,反而会缩小object code。
这给何时使用内联提供了指导。
inline还有另一个需要考虑的成本:
对于程序开发者,升级一个inline的函数比升级一个outline的函数成本更大,因为inline函数定义在头文件中,因此更新后所有使用到此函数的客户文件都需要重新编译,而对于outline函数,则只需要重新链接即可。如果是动态链接,甚至没有成本。

――The End

Something about const

十一月 10th, 2009 No Comments »

说说const,这也是我学习《thinking in c++》和《effective c++》中关于const内容的学习笔记。
一、最基本用法――定义常量
C++中编译器是尽量避免为const分配内存的,而是在进行类型检查之后将从定义时的初始化式(要求是常量表达式)中计算来的值折叠到代码里(即进行值替代),然后存放到符号表(symbol table)。
C++中const定义的时候必须进行初始化,这是因为:
初始化式用来区分定义和声明,而在C中,无论写
const int x;
还是
extern const int x;
编译器都将其看做是声明,所以不需要初始化。
而在C++中,这么写
const int x;
是不允许的,要声明必须加上extern,表明定义在别处(elsewhere)。
这个规定也是从const的不可赋值性得出的。只能初始化不能定义,又必须有值存在,而初始化又只能发生在定义出(初始化是从无到有,赋值是从有到有)。
前面提到,编译器是尽量避免为const分配内存的,那么什么情况会迫使编译器为const分配内存呢?
以下情况:
1.初始化式的值过于复杂使得编译器无法计算,这样编译器为const分配内存。
2.初始化表达式不是编译期常量,如const int i = cin.get();编译器无法在编译期确定常量的值,因此为其分配内存。
3.const数组,编译器不确定符号表足够盛放整个数组,于是一律分配内存,不在符号表里保留信息。
4.任何对一个const取地址的行为(包括将其传给一个形参为引用的函数这样的不自觉取地址行为)会导致内存分配,但是编译器仍然是知道其值的,符号表里仍然有其信息,编译器仍将它看做编译期常量。这和上面三种情况不同。
1-3情况,编译器在编译期无法得到const的值,也就不能作为常量来使用它,比如不能用其作为定义数组时的大小。

二、const与指针搭配使用
两种使用方式:使指针所指之物为const;使指针所存地址为const。
Pointer to const
如果想让指针所指之物为常量,而指针所含地址可以更改,在定义时将const写在*的左面。然而,这只是说不能通过指针更改其所指之物,此被指之物本身不必是const,也就是不通过指针还是可以更改的。又由于指针本身并不是const,所以定义指针时不必初始化。两种定义格式完全相等,如下:
const int* pi;
int const* pi;
const pointer
如果想让指针为const,即指针不能再指向其它之物,而指针所指之物可以被更改,当然也可以通过指针被更改,那么应将const写在*右面,由于指针是const的,所以定义时必须初始化,如下:
int d = 1;
int* const pi = &d;
也可以让指针同时具有两种属性,还是两种等价的定义格式:
int d = 1;
const int* const p1 = &d;
int const* const p2 = &d;
这样,指针和所指之物就都是const的了。仍然不需要所指之物本身是const的。
可以用non-const之物的地址给指向const之物的指针赋值,不可以把const之物的地址赋给指向non-const之物的指针,除非使用强制转换,那就破坏了const带来的常量性。
关于指向字符串字面值的指针
虽然指向字符串字面值的指针不必是pointer to const,但是字符串字面值是编译器创建的且不容许更改的,因此若通过non-const指针更改其内容,虽无任何编译链接错误,却会导致运行期错误。如下:
char* p = “string”;?//可以通过编译,但技术上是错误的
*p = ‘a’;??//可以通过编译,但会引起运行期错误,实际上是未定义行为,有些机器上也会可以工作
编译器之所以允许non-const指针用字符串字面值初始化,是向大多数依赖于此的C代码的妥协。
要相对字符串字面值作出更改,请使用数组:
char p[] = “string”;

三、用于函数参数和返回值
如果以pass-by-value方式传递函数参数,那么将其声明为const对客户来说毫无意义,参数在函数内部本身就是一个临时对象,它是否改变不影响客户手里的参数原本,因此这个promise毫无意义。所以,这样做是对函数编写者的限制,而不是调用者。为了不迷惑调用者,可以把const的声明从参数列表挪到函数体内,用reference实现:
void f(int i)
{
?const int& j = i;
?j++;?//Illegal — compile-time error
}
对于pass-by-value的函数返回值,存在同样的问题,当写:
const int f();
只是保证函数体内的原返回值不被改变,由于是按照pass-by-value返回的,所以这种保证毫无意义而只会让客户迷惑。
当处理以pass-by-value返回用户自定义类型的函数时,const才显得重要。
对于内置类型的pass-by-value返回的函数,编译器阻止函数调用结果作为左值,而对于用户自定义类型,编译器不做此组织行为,除非将返回值声明为const。用const修饰返回值从而阻止用户自定义类型的返回值作为左值,可以避免if(f()=a)这样的错误(本想做比较而不是赋值)。

如果以指针和引用作为函数参数,那么const参数比non-const参数版本的函数更具一般性:
即可以将non-const指针和变量传递给const参数的函数,反之则不可。
对于临时变量,可以将其传递给接受const引用的函数,而不能传递给接收non-const引用的函数,因为对临时变量的更改是不允许的。
临时变量是不能被取值用于指针传递的。

四、类中的使用
1)类中的编译期常量
首先,不能在类的定义里简单地使用const作为编译期常量定义数组大小了,因为const在每个对象中都会被分配内存,所以编译器在编译期间不知道它的值,这里的“const”只保证在此对象的生命期内该成员不被改变,而不能保证各个对象之间的const成员一致,也就不是编译期常量。
用于产生可以在类中可以使用的编译期常量,有两种方法:
1.使用enum:
enum {size = 10};
int buf[size];
枚举的所有值必须在编译期建立,且不会占用对象中的存储空间,枚举常量在编译期被全部求值。
2.使用static const:
classicA{
private:
?static const int size = 5;
?int buf[size];
};
由于这是类的定义,所以这仅仅是size的声明而非定义。
通常C++要求使用到的东西都有一份定义,但是如果是类中的专属常量,又是static且为整数类型(int,char,bool),则需特殊处理。只要不对其取地址就可以只提供声明而不需定义就能使用。
如果不得不定义,由于声明中已经给了初值,定义中不再需要初值,定义应放在.cpp中而非.h。

2)类中的const成员
如果一个类中有const成员,由于const变量必须在定义时被初始化,所以需要特殊处理:
首先const变量的定义及初始化肯定要发生在类的构造函数里,但是又不能到进入构造函数体内再进行,因为在进入构造函数体内之前,构造函数会调用类中各成员的构造函数来定义各个成员,成员真正的定义时刻发生在此时,构造函数体内发生的是赋值而非初始化。所以正确的做法是在构造函数的初始化列表里提供const成员的初值,用来指导对const变量构造函数的调用。

3)const对象与const成员函数
类和其它内置类型一样,可以声明const的对象。然而对于类这样复杂的类型,编译器如何保证其const性呢?
编译器的做法是只允许const对象调用被声明为const的成员函数,然后再对const成员函数的行为进行限制,从而保证对象的常量性。
声明一个成员函数为const的语法是在函数声明的末尾加上const:
int f() const;
定义时仍要重复此写法否则编译器不将其作为同一个函数。
int A::f() const{
?//do something…
}
构造函数和析构函数都肯定不是const的,因为它们的工作不可避免要更改对象。
const对象只能调用const成员函数,non-const对象则无限制。
如在一个const函数里,想做更改对象的事,怎么办?
还是有办法的:
1.强制转换:在函数内取this指针,这时它是const指针,将其强制转换成non-const指针,即可利用它进行有关操作,编译器不再保护此指针对应的对象的const性:
void Y::f() const{
?((Y*)this)->j++;
}
2.使用关键字mutable,讲一个成员声明为mutable,说明它在const函数中可被改变而不影响对象的常量性。而且可以让用户心中有数。

有关linkage,extern,static的碎碎念

十一月 8th, 2009 No Comments »

为了理解C\C++程序的行为,需要了解linkage。在一个可执行程序中,标示符是以内存中保存变量值或者编译过的函数体的存储块所标示的。linkage描述了这个存储块对于连接器的可见性。有两种linkage:internal linkage和external linkage。

internal linkage意味着这个存储块仅仅被创建用来标示被编译的文件里的标示符。其他文件可以使用相同名字的internal linkage标示符或者全局变量,而不会使连接器发现名字冲突――会为每个标示符创建不同的存储块。internal linkage可以用C/C++关键字static来声明。

external linkage意味着只有一块存储块被创建用来标示所有文件中的同名标示符。这个存储块只被创建一次,然后连接器负责将所有引用指向它。全局变量和函数名具有external linkage。它们可以被其它文件中的代码访问,只要用extern关键字进行声明(说明其定义在别处,然后连接器会负责查找)。在所有函数外定义的变量(C++中的const除外)和函数(即非成员函数)也具有external linkage。你可以通过使用static声明而强制它们具有internal linkage。在C中用const定义一个变量和函数是没有意义的,而在C++中对于const是有必要的(在C中,不能修饰一个函数为const,C++中这么做是为了提供给const对象一个可用的接口;C中const默认external linkage,因此不能放在头文件中被当做编译器常量来使用,否则会导致多重定义错误,C++中相反,const为internal linkage,可以作为编译期常量。这里都指所有大括号外,大括号内是否可定义为extern?不可以,会导致编译错误)。

自动(即局部)变量只是当函数被调用时在栈上临时的存在。连接器不认识自动变量,所以它们具有no linkage。(可以把no linkage也作为一个linkage,也可以认为这是没有linkage。)

C++中为什么将const缺省为internal linkage?
因为const是要放在.h中提供给各个.cpp使用的。而编译器不能永远避免为const分配内存的(没错,编译器尽量避免为const分配内存,而只是将其计算出值并存放在符号表中,但是过于复杂的const表达式将使得编译器不得不为const分配内存,因为编译器不能确定此const的值,否则就可以不为其分配内存从而进行常量折叠――即用其值代替的同时将其存放在符号表内――了;另外,对const取值也会迫使编译器为const分配内存,但是更改此内存中的内容不会影响const的值――已经被常量折叠而替换了),所以如果const是external linkage,会造成多个.cpp文件中有对const的重复定义,从而造成链接错误。
实际上经常存放在.h中的名字(常量、内联函数)都是internal linkage的,以此来避免连接错误的发生――否则连接器面对包含此.h的众多.cpp编译生成的.obj中的各个存储块,不知道该链接哪个。

static和extern是一对关键字。
这对关键字的作用值得讨论。
在不同的作用域内,它们可以分别影响object的编译器可见性和存储类别。
static?单一文件域可见的,静态存储的。
extern?全局文件域可见的,别处定义的。
所谓静态存储,是指生命期从构造开始直到程序结束为止。非静态存储则在栈上,随着函数的调用而建立,函数的技术而释放。
在文件域内(所有大括号外),名字都是静态存储的,static和extern(缺省)用来指定名字对编译器的可见性。
在大括号内,名字都是编译器不可见的,即internal linkage的,static和extern(缺省)用来指定存储类别。static指定为静态存储,extern指定名字为在别处定义的。如不进行任何声明,则缺省为auto(?),在栈上分配空间。
也就是说static和extern既是链接属性说明符又是存储类别说明符。
除了它俩,还有两个存储类别说明符register和auto,暂不表。

static比较有意思,在大括号外它指导符号的可见性为“当前文件可见、外部不可见”;到了大括号内部,它又转而指导存储位置和多次调用(对应于函数大括号)或多个对象(对应于类定义大括号)之间的持续性。

我翻译的”Thinking In C++” Chapter”Iostreams”前三小节

十一月 4th, 2009 No Comments »

读机械工业出版社翻译的thinking in c++ 1/e,翻译实在是差的可以了。于是找到第二版的英文版来看,看的时候突发奇想,不如翻译个试试?于是就试试。有如下体会:

1.读原版的感觉真好,感觉对于描述这种技术性知识,英语要比汉语合适的多。

2.翻译确实不容易,不少中文版中不理解的地方,在原文里虽能理解但是确实很难用中文表达出来。

本打算翻译这一整章呢,无奈本章太长我翻译的又太慢,两天时间就弄出了三小节。罢手吧。

里面还保留了两句原文,我实在是不理解那两句话的意思。

虽然能力不足,但绝对不会像第一版中文版翻译那么不负责!

文中错误漏洞肯定不少,敬请指正啦,虽然不会有多少人看到这里:)

?

2:Iostreams 输入输出流

对于解决通常的I/O问题,你所能做的比仅仅将标准I/O库封装成类要多得多。

如果能把所有与I/O有关的“容器”――诸如标准I/O,文件和内存块都统一起来,这样你就只需记住唯一的一个借口,岂不是很好?这就是输入输出流背后的设计思想。比起使

用C的标准I/O库中各种各样的函数,输入输出流使用起来更简单、更安全,通常也效率更高。

输入输出流通常是新C++程序员所学习的第一个库。本章将探讨输入输出流的使用,这样在后面的章节里我们就可以用它来代替C的I/O函数。在后续章节中还会看到如何建立我们自己的和输入输出流相容的类。

为什么使用输入输出流?

你也许会纳闷C原来的库函数不是很好吗?为什么不把它封装成一个类来使用呢?确实,如果你的目的是使C的I/O库使用起来更加安全和更加容易,那么这么做(将其封装成类)确实是很完美的。比如,如果你想确保一个标准输入输出文件总是能被安全地打开和合适地关闭,而不依赖于程序员记得去调用close()函数:

?//: C02:FileClass.h
?// Stdio files wrapped
?#ifndef FILECLAS_H
?#define FILECLAS_H
?#include <cstdio>
?class FileClass {
??std::FILE* f;
?public:
??FileClass(const char* fname, const char* mode=”r”);
??~FileClass();
??std::FILE* fp();
?};
?#endif // FILECLAS_H ///:~

当你在C语言中操作文件的I/O时,你需要使用一个未经封装的指向FILE结构体的指针,但是这个类封装了这个指针,用构造函数和析构函数来正确的初始化和清除它。构造函数的第二个参数是文件模式,缺省默认为“r”代表读文件。

要想得到这个指针以便使用文件I/O函数,可以使用fp()来获取。下面是成员函数的定义:

?//: C02:FileClass.cpp {O}
?// Implementation
?#include “FileClass.h”
?#include <cstdlib>
?using namespace std;
?FileClass::FileClass(const char* fname, const char* mode){
??f = fopen(fname, mode);
??if(f == NULL) {
???printf(“%s: file not found\n”, fname);
???exit(1);
??}
?}
?FileClass::~FileClass() { fclose(f); }
?FILE* FileClass::fp() { return f; } ///:~

构造函数代替你调用fopen()函数,而且还会检查调用结果是否为0,如果是0说明打开文件失败了,这样构造函数将打印打开失败的文件名字并且调用exit()函数退出。

析构函数负责关闭文件,成员函数fp()能返回文件指针f。下面是一个使用FileClass类的简单例子:

?//: C02:FileClassTest.cpp
?//{L} FileClass
?// Testing class File
?#include “FileClass.h”
?#include “../require.h”
?using namespace std;
?int main(int argc, char* argv[]) {
??requireArgs(argc, 1);
??FileClass f(argv[1]); // Opens and tests
??const int bsize = 100;
??char buf[bsize];
??while(fgets(buf, bsize, f.fp()))
??puts(buf);
?} // File automatically closed by destructor
?///:~

这里我们创造了一个FileClass对象,通过调用fp()函数得到文件指针,藉此通过C的文件I/O函数完成了一些操作。当做完了想做的一切,我们无需记得关闭文件,因为在出作用域的时候析构函数已经帮我们做了。

真正的封装

因为fp()可以返回文件指针,所以尽管文件指针是私有的,这种安全性仍然是不完全的。唯一的作用似乎仅仅是保证了初始化和清除被正确的执行,既然如此为什么不把它声明成公共的呢?或者为什么不用结构体来代替呢?注意,尽管你能通过fp()得到f的一个副本,但是你不能更改f――那是被类控制的。当然,通过调用fp()得到f的值后,用户仍然能够

存取文件的内部元素,所以所谓的安全更多的意义在于提供一个安全的文件指针而不是保护文件内部。

如果我们要求完全的安全,就必须彻底杜绝用户对文件指针的访问。这意味着常用的文件I/O函数必须要以成员函数的面貌出现,如此你在C中做的任何事都可以通过C++类来完成了:

?//: C02:Fullwrap.h
?// Completely hidden file IO
?#ifndef FULLWRAP_H
?#define FULLWRAP_H
?class File {
??std::FILE* f;
??std::FILE* F(); // Produces checked pointer to f
?public:
??File(); // Create object but don’t open file
??File(const char* path,
??const char* mode = “r”);
??~File();
??int open(const char* path,const char* mode = “r”);
??int reopen(const char* path,const char* mode);
??int getc();
??int ungetc(int c);
??int putc(int c);
??int puts(const char* s);
??char* gets(char* s, int n);
??int printf(const char* format, …);
??size_t read(void* ptr, size_t size, size_t n);
??size_t write(const void* ptr, size_t size, size_t n);
??int eof();
??int close();
??int flush();
??int seek(long offset, int whence);
??int getpos(fpos_t* pos);
??int setpos(const fpos_t* pos);
??long tell();
??void rewind();
??void setbuf(char* buf);
??int setvbuf(char* buf, int type, size_t sz);
??int error();
??void clearErr();
?};
?#endif // FULLWRAP_H ///:~

这个类几乎包含了C中所有的文件I/O函数。没有vfprintf(),它仅仅被用来实现printf()。

File类拥有和先前版本一摸一样的构造函数,另外它还有一个缺省构造函数。缺省构造函数在下列情况下显得非常重要:当你想创建一个File对象的数组;或者想把File对象作为另

一个类的成员,这种情况下构造函数是不进行初始话的(但有时在被包含的对象被创建后进行初始化)。

缺省构造函数将作为私有成员的FILE指针设为0。但在对f的任何引用之前都必须保证f不是0。这是有最后一个成员函数F()来完成的,由于它只会被其它成员函数调用所以设置为私有。(在本类中,我们不想让用户可以直接访问FILE结构体内部。)

无论从何种意义上讲,这种解决方法都是不错的。它很有效,你可以想象为标准I/O和内存I/O编写类似的类。

真正使得将C的I/O库封装变得不可行的是在运行期用于解释参数列表的程序。此程序用于在运行期对你的参数格式化以便对其进行解释。它之所以是问题是由于以下四个方面:

?1.?即使我们只会用到解释程序中的一小部分,我们也不得不把它全部装载。比如:
??printf(“%c”,’x');
??整个包都会被装载,包括打印浮点数和string对象所需要的部分。无法减少程序占用的空间。

?2.?Because the interprtation happens an runtime there’s a performance overhead you can’t get rid of.关于格式串的所有的信息在编译阶段就已经存在了,但是直到运行期才可以对其作出评估,这难免令人沮丧。如果我们在编译阶段就可以分析这些参数的话,那么我们就可以进行更具体的函数调用,如此就可能比在运行期才

分析得到更块的速度(机关printf()族函数通常已经被优化的很好)。

?3.?由于参数分析直到运行期才会进行,有一个更加严重的问题可能会发生:没有编译器差错。如果在printf()语句中使用了错误的数字和类型就可能出现错误,若你曾试图查找过此类错误你一定会很熟悉这种情况。C++做了大量的工作来进行编译器检错,以便更早地发现错误以使得工作更加容易。如果单单抛却I/O库不管似乎有些说

不过去,况且I/O库的应拥有如此广泛。

?4.?对C++来说,最重要的问题时printf()函数族不具备可扩展性。设计它们的目的仅仅是用来处理C中的四种基本数据类型(char,int,float,double和它们的变种)。我们也许会想,每次增加一个新类就重载一个printf()函数和scanf()函数(以及它们用于文件和string的函数版本)。但是别忘了,重载函数必须以参数列表中的类型不同以

相区别,而printf()族函数是将类型信息隐藏在格式串里和参变量列表里的。对于一个像C++这样,致力于使添加数据类型变得容易的语言来说,这是一个难堪的限制。

用输入输出流来解决

所有这些问题表明C++的第一个类库应该是处理I/O的库。因为“Hello,world!”是几乎每个学习一门新语言的程序员所写的第一个程序,还因为I/O几乎包含在所有程序中,C++

中的I/O库必须容易使用。这个库还面对这样的挑战,它从不知道所有它必须与之相容的类,但是又必须于所有的新类相容。这样一来,它就注定是一个充满灵感的设计了。

本章不会考察此设计的细节,也不会介绍如何向你自己的类添加I/O功能(后续章节会介绍)。首先,我们需要先学会使用输入输出流。除了让你在处理I/O问题时更有招法且更清晰,我们还会看到一个真正强大的C++库是如何工作的。

略窥操作符重载

在使用输入输出流库之前,我们必须先了解C++的一个新特性,关于此特性的细节要到后续章节中才会介绍。为了使用输入输出流,我们必须明白在C++中操作符是可以有不同的含义的。在本章中,我们着眼于<<和>>两个操作符。“操作符可以用不同的含义”这句话值的进一步探讨。

在第20章中,我们已经了解到函数重载特性允许我们通过相同的函数名和不同的参数列表来做不同的事。现在想象一下每当编译器遇见一个形如“参数 操作符 参数”的表达式,它都会去调用一个函数。也就是活,操作符只不过是语法有所不同的函数而已。

当然了,这是对数据类型很考究的C++。所以必须有一个较早的函数声明来匹配这个操作符和特定的参数类型,否则编译器是不会接受这个表达式的。

关于操作符重载这件事,很多人会立刻产生这样令人沮丧的想法:我们所了解的C中关于操作符那些知识都不管用了。这是绝对错误的。下面列出C++的两个不容置疑的设计原则:

?1.?一个能在C编译器中通过编译的程序也应该能够通过C++的编译器。C++编译器产生的仅有的错误和警告应该来源于C语言自身的漏洞,而且填补这些漏洞应该仅仅需要局部的修改。(实际上,用C++编译器进行编译往往能帮你发现C程序中隐含的错误。)

?2.?C程序在被C++编译器重新编译后,其行为不会被私自地改变。

牢记这两点能帮助回答很多问题;了解到把C移植到C++不会引起不确定的变化这一点,会让这种移植变得容易一些。例如,操作内置类型的操作符不会突然按照另一种方式工作――――它们的意义是不容更改的。重载的操作符仅仅会包含在新数据类型的地方被创建。所以你可以为新创建的类重载操作符,但是原有的表达式

1 << 4;

的含义不会突然改变,而原来错误的表达式

1.414 << 1;

也不会突然变得好用了。

插入操作符和提取操作符

在输入输出流库中,为了让输入输出流使用起来更加容易重载了两个操作符。操作符<<通常被用作输入输出流的插入操作符,而>>通常被用作提取操作符。

流是一个能储存字节并对其格式化的对象。有输入流(istream)也有输出流(ostream)。输入流和输出流也有不同的版本:操作文件的ifstreams和ofstreams,操作内存(内核格式化)的istrstreams和ostrstreams,以及面向C++标准类string的istringstreams和ostringstreams。无论你是在操作文件、标准I/O、内存还是string对象,所有这些流对

象都有统一的接口。你所学到的这唯一的接口同样也能为新定义的类工作。

对于一个能生产字节的流(即输入流),可以用提取操作符从中提取信息。提取操作符获取信息并将其格式化为目标对象所要求的类型。可以用cin对象作为例子,cin是一个流对象,相当于C中的标准输入(stdin)。只要你包含了iostream.h头文件,这个对象就被定义好了。(如此一来,I/O库就可以自动在大多数编译器下协同工作。)

?int i;
?cin >> i;
?float f;
?cin >> f;
?char c;
?cin >> c;
?char buf[100];
?cin >> buf;

对于每一个你可以在I/O语句中作为>>操作符右参数的数据类型,都有一个重载了的>>操作符为其服务。(也可以重载属于我们自己的>>,后面章节会提到。)

要想看到这些变量里都存进去了什么,可以通过插入操作符<<来使用cout对象(对应于标准输出;同样还有一个cerr对象来对应标准出错):

?cout << “i = “;
?cout << i;
?cout << “\n”;
?cout << “f = “;
?cout << f;
?cout << “\n”;
?cout << “c = “;
?cout << c;
?cout << “\n”;
?cout << “buf = “;
?cout << buf;
?cout << “\n”;

如此操作令人乏味,而且不管你是否进行类型检查,似乎也没显示出照比printf()有多大改进。幸运的是,在iostream里重载了的<<和>>被设计得可以连接成一个复杂的表达式

来使用,这样写起来就容易多了:

?cout << “i = ” << i << endl;
?cout << “f = ” << f << endl;
?cout << “c = ” << c << endl;
?cout << “buf = ” << buf << endl;

在接下来的章节里你会明白它的工作原理,现在作为一个类的使用者来说,知道它如此用就足够了。

操纵算子

这里有一个新元素出现:写作endl的操纵算子。操纵算子对流本身起作用;这里它向流中插入了一个新行并且清空此流(将所有储存在流内部尚未输出的字符一次性输出来)。你

也可以仅仅进行清空操作:

?cout << flush;

有的基本操纵算子还可以改变数字的进制:otc(8进制),dec(10进制)和hex(16进制):

?cout << hex << “0x” << i << endl;

还有可以在从流中进行提取的时候吃掉空格的操纵算子:

?cin >> ws;

另还有一个和endl很像的操纵算子,写作ends。它只能用于内存流(strstreams,稍后提到)。以上列出的是<iostream>里的所有操纵算子,但是在<iomanip>中还有其它操纵

算子,后续章节会讲及。

通常用法

尽管cin和>>为cout和<<提供了很好的对应,但是在实践中使用格式化输入,尤其是对于标准输入时,cin面临和scanf()同样的问题。如果输入得到一个非期望的值,进程就会被

偏移,而且很难修正。而且,格式化输入默认是以空格为分隔符的。所以如果我们把上面的代码集合成一个程序:

?//: C02:Iosexamp.cpp
?// Iostream examples
?#include <iostream>
?using namespace std;
?int main() {
?int i;
?cin >> i;
?float f;
?cin >> f;
?char c;
?cin >> c;
?char buf[100];
?cin >> buf;
?cout << “i = ” << i << endl;
?cout << “f = ” << f << endl;
?cout << “c = ” << c << endl;
?cout << “buf = ” << buf << endl;
?cout << flush;
?cout << hex << “0x” << i << endl;
?} ///:~

然后按照这样输入,

?12 1.4 c this is a test

我们期望得到和输入一样的输出

?12
?1.4
?c
?this is a test

然而,输出却并不如此,

?i = 12
?f = 1.4
?c = c
?buf = this
?0xc

buf只得到了第一个单词,因为输入程序是把空格当做分隔符的,而“this”后面恰有一个空格。不仅如此,如果输入的字符串长度超过了buf的存储能力,缓冲区就会发生溢出。

如此看来,提供cin和插入操作符似乎仅仅是为了一种完整性,这么看待它们也确实比较有好处。实践中,我们经常希望能够按行读取输入,然后将它们作为一个字符序列安全地

存入缓冲区后再对其进行扫描和操作。这么做,我们就不必担心输入被不期望的数据阻塞了。

另外关于命令行接口的概念也值得考虑了。曾经当控制台仅仅比一台打字机复杂些的时候,命令行接口确实意义非凡,但是如今的世界已经是被图形用户接口(GUI)主宰的了。

在如今控制台I/O还意味着什么?除了一些很简单的例子和应用,还是将cin忽略掉比较实际,下面提供一些应该遵循的准则:

?1.?如果程序需要输入,我们可以从文件中读取――――我们很快会看到用输入输出流对文件进行操作是非常简单的。文件的输入输出流同样能很好地与GUI一

起工作。

?2.?读入输入时不要试图对其进行转换。等到输入被存放起来,在转换时不会把事情弄糟时,再扫描它们就安全了。

?3.?输出是另一回事。如果正在使用GUI,cout是不能工作的,我们必须将输出送到某个文件(就像送到cout一样),或者使用GUI设备来进行数据显示。如果

使用的不是GUI,那么cout还是有意义的。这两种情况下,输入输出流的输出格式化函数都是很有用的。

按行输入

想要每次得到一行输入,我们有两个成员函数以供选择:get()和getline()。两个函数都有三个参数:一个指向用于存储结果的字符缓冲区的指针,缓冲区的大小(这样就不会溢出

),和告之何时停止读取的终止字符。终止字符有一个缺省值‘\n’,我们经常会用到。两个函数都会在遇到终止字符时在缓冲区里添加一个0值。

那两者有什么区别呢?区别虽微妙但却很重要:get()在输入流中遇见分隔符的时候会停下来,而不把分隔符提取出来。这样的话,如果你再次以相同的分隔符为参数调用get(),

它会立即返回而得不到任何输入。(所以,要么使用一个不同的分隔符,要么就是用其它的输入函数。)而getline()会将分隔符从输入流中提取出来,但是同样不会将其存入结果

缓冲区。

一般来说,如果处理一个按行读入的文本文件,我们应该使用getline()。

get()的重载版本

get()还有另外三个重载版本:一个无参数版本以int类型返回下一个字符;一个通过引用(要想立刻明白此处,需要跳到第二十章阅读)将一个字符放入其中char型参数当中;第

三个版本直接将数据存放到另一个流对象的底层缓冲结构中。本章稍后部分会讨论这点。

读原始字节

如果我们很清楚你所面对的操作对象并且想把字节直接移动到变量,数组或者内存结构中,我们可以使用read()函数。第一个参数是指向目的内存的指针,第二个参数是要读取的

字节数。如果你事先把信息存放到了文件中,那么这个函数是很有用的,对于一个输出流使用相对应的write()成员函数。稍后我们会看到所有这些函数的例子。

出错处理

除了无参数的get()版本返回下一个字节或者EOF外,所有其它版本的get()和getline()都返回它们从中提取字符的输入流。当重新获取一个流对象时,我们可以验证它是否正常。实

际上我们可以调用成员函数good(),eof(),fail(),和bad()来查看任何输入输出流对象是否完好。它们以下列状态信息作为返回值:eofbit(表示缓冲区已经到达末端),failbit

(表示由于格式化或者其它一些不对缓冲区造成影响的问题,某些操作失败了)和badbit(表示缓冲区出错)。

然而,像之前提到的,通常只有在从输入得到的数据类型和期盼的不一样时,输入流才会莫名其妙地被破坏。于是我们就面临要对输入流做什么才能修正它的问题。如果听从我的

建议每次读入一行或者一大块字符(使用read()函数),并且除非在简单的情势下否则不去尝试使用输入格式化函数,那么除了是否到达输入的结尾(EOF)之外你不用再担心任

何事。幸运的是,对是否到达输入结尾的检查很容易而且可以在条件判别式里完成,如while(cin)或者if(cin)。如今我们最好接受这样的事实:在使用输入流对象时,用以标记对象

是否达到输入末尾的值总是能被安全地、正确地甚至魔术般地产生出来。你也可以使用布尔非操作符!,像if(!cin)这样,来表示流对象已经非正常了;这表示你已经达到了输入的

末尾,应该退出对流的读入了。

有这样的情况,尽管我们知道流已经非正常了,但是我们仍然想要继续使用它。比如,你到达了一个输入文件的末尾,eofbit和failbit都已经被设置了,所以基于那个流的条件会

表明此流已经非正常了。然而我们可能还想继续使用这个文件,比如回溯到前面某个继续读取数据。想要纠正条件判别,简单地调用成员函数clear()就能做到。

文件输入输出流

用输入输出流来操纵文件比用C的标准I/O库要简单的多。只需创建一个对象就可以打开一个文件;构造函数会做相应的工作。也不用显式地关闭文件(尽管你也可以这样做,通

过调用成员函数close())因为当对象出生存域时析构函数会关掉它。

要创建一个缺省用于输入的文件,可以创建一个ifstream对象。要创建一个缺省用于输出的文件,则创建ofstream对象。

下面的例子展示了很多到此为止讨论过的特性。留意对<fstream>的包含就声明了文件I/O类;<iostream>同样包含在其中。

?//: C02:Strfile.cpp
?// Stream I/O with files
?// The difference between get() & getline()
?#include “../require.h”
?#include <fstream>
?#include <iostream>
?using namespace std;
?int main() {
??const int sz = 100; // Buffer size;
??char buf[sz];
??{
???ifstream in(“Strfile.cpp”); // Read
???assure(in, “Strfile.cpp”); // Verify open
???ofstream out(“Strfile.out”); // Write
???assure(out, “Strfile.out”);
???int i = 1; // Line counter
???// A less-convenient approach for line input:
???while(in.get(buf, sz)) { // Leaves \n in input
????in.get(); // Throw away next character (\n)
????cout << buf << endl; // Must add \n
????// File output just like standard I/O:
????out << i++ << “: ” << buf << endl;
???}
??} // Destructors close in & out
??ifstream in(“Strfile.out”);
??assure(in, “Strfile.out”);
??// More convenient line input:
??while(in.getline(buf, sz)) { // Removes \n
???char* cp = buf;
???while(*cp != ‘:’)
????cp++;
???cp += 2; // Past “: ”
???cout << cp << endl; // Must still add \n
??}
?} ///:~
在ifstream和ofstream对象被创建后都用assure()检查文件打开是否成功。Here again the object, used in a situation where the compiler expects an integral result,

produces a value that indicates success or failure.(做到这点组要调用自动转换类型的成员函数。20章里会对此进行讨论。)

第一个while循环显示了get()函数的两种用法。第一种将字符读入缓冲区,当遇到第sz-1个字符或者第三个参数代表的分隔符时停止读入,并在末尾添加一个0。get()将分隔符留

在了输入流中,所以此分隔符必须通过调用无参数版本的get()来提取并扔掉,这个版本的get()取一个字符并按int将其返回。也可以调用成员函数ignore(),它有两个参数。第一

个参数是要扔掉的字符个数,缺省为1。第二个参数是使ignore()退出的字符(在提取了它之后),缺省为EOF。

接下来是两个很相似的输出语句:一个输出到cout一个输出到文件流对象out。留意这里显示的方便之处;我们不必困扰于究竟在操作何种对象,因为所有的输出流对象都使用相

同的格式语句进行工作。第一句在标准输出打印一行,第二句将这一行写到新文件里,并且加上一个行号。

演示getline()部分很有意思,我们打开刚刚创建的文件然后去掉里面的行号。为了保证在打开并读它之前该文件已经被合适的关闭,我们有两个办法。我们可以将程序的第一部分

用大括号括起来,这样迫使out对象离开生存域并且在此处调用析构函数关闭文件。也可以调用close()函数关闭两个文件;如果愿意,我们甚至可以通过调用成员函数open()以便

继续使用对象in(还可以在堆上动态地创建和销毁对象,将在20章介绍)。

第二个while循环显示了getline()是如何将分隔符(它的第三个参数,缺省为‘\n’)从输入流中移除的。然而和get()一样,getline()虽也在缓冲区末尾添加0,但同样不会讲分隔

符插入到缓冲区。

打开方式

我们可以通过改变缺省参数来控制打开文件的方式。下表列出了这些控制文件方式的标志:

?Flag???Function
?ios::in??打开输入文件。以此作为ifstream对象的打开方式,
???来防止已存在文件被删除。
――――――――――――――――――――――――――――――――――――
?ios::out??打开输出文件。以此作为ofstream对象的打开方式,
???同时不适用ios::app,ios::ate或者ios::in时,
???ios::trunc被隐含。
――――――――――――――――――――――――――――――――――――
?ios::app??以追加方式打开一个输出文件。
――――――――――――――――――――――――――――――――――――
?ios::nocreate?只当文件存在时才打开。(否则失败。)
――――――――――――――――――――――――――――――――――――
?ios::noreplace?只当文件不存在时才打开。(否则失败。)
――――――――――――――――――――――――――――――――――――
?ios::trunc??打开文件,如果已经存在就删除原文件。
――――――――――――――――――――――――――――――――――――
?ios::binary??以二进制方式打开文件。默认打开文件方式是文本
???方式。
――――――――――――――――――――――――――――――――――――

以上标记可以用位或or进行联合。

读书笔记第一季(item1~30)

十月 29th, 2009 12 Comments »

1.关于读写文件操作:
fopen()在堆上为FILE结构体分配空间,并返回指向FILE结构体的指针,fclose()告诉堆释放这个指针。

2.关于C语言调用未声明的函数:
C语言中,编译器中允许调用未声明的函数,并按照实参猜测函数原型,这是危险的。在C++中不能调用未声明的函数。

3.关于编译和链接:
编译是对语法的检查,链接是对外部引用的替换。由于文件是单独编译的,对于对外部函数的调用在编译时会保留一个引用(因为编译的时候编译器只知道这个函数像什么而不知道是什么,只看到声明未看到定义),在链接时会从别的.obj或.o文件中找到此函数地址,并用之代替对此函数调用的引用。

4.编译器需要知道正在编译的程序是c还是c++,而编译器是不直接编译头文件的,所以无论c还是c++,头文件都是.h,而源文件有.c和.cpp之别。

5.关于为什么把函数作为类的成员的一个直观想法:
为了避免名字冲突,因为struct内部的名字是不会和外部冲突的,而函数名又是稀缺的资源,有些函数又是专门作用在struct上的,并无它用,那为什么不把该函数作为struct的内部成员呢?

6.关于成员函数参数列表中看不见的参数this:
RT,是编译器负责的。

7.关于定义多个对象时,成员函数的代码有多份吗:
当然不是,只有一份。

8.关于::运算符:
C++用把函数作为类和结构的成员的方法解决了名字分解的问题,但是实际上C++的名字是分两部分的:
范围::名字。需要指出名字的范围时,即分解名字时,用它,这也是相对C语言新增添的运算符,叫“范围分解运算符”。
比如,在外部定义成员函数时就用到此操作符。

9.关于外部定义成员函数时,函数体内部对成员变量的操作是否也需要加::操作符:
不用,编译器没那么笨。说明一次就够了,交给编译器吧。
在一个成员函数的内部,可以直接提及任何一个类的成员(包括成员变量和成员函数),编译器在在全局搜寻此名字前会先在局部结构的名字中搜索。
想用该结构地址时,用this即可。

10.关于.h和.cpp:
类定义放在.h,类成员函数定义放在.cpp。

11.C可以用void*指针和其它任何类型指针之间互相赋值。C++只允许用其它类型指针给void*赋值。

12.关于“向对象发送消息”:
object.memberfunction(arglist);
以上语句是调用某对象的成员函数,在OOP中,也叫向某对象发送消息,让它自己完成某项操作,这样的“对象”的概念体现得更明显。OOP要做的事就是设计好对象和消息,然后向对象发送消息。

13.关于没有成员变量的类的sizeof():
会返回一个很小的非零值,VS2005是1,对于既有成员变量又有成员函数的类,sizeof求大小的时候只计算成员变量所占空间,不加此非零值。

14.C++对象的生命期是到定义它所在的程序块的右大括号为止,在新编译器中,编译器默认为每个for循环加上大括号,即使原来并没有。

15.C++编译器如此重视对象的初始化以至于它会检查对象的定义(对于对象来说,定义即初始化,因为在定义时编译器会自动插入构造函数用于初始化对象)是否会因为goto语句或者switch-case语句而被跳过。在VS2005编译器下,goto跳过一个对象的初始化会产生一个警告,switch-case则会产生一个错误。在GCC编译器下,则两者都是错误。

16.malloc(),calloc(),realloc()都从堆上分配内存,意味着需要用free()去释放,free()做的工作实际是告之编译器(?)这些内存可以被再次分配。上述三个分配内存的函数如果调用失败(比如当前堆不够用了)都会返回0,可以用assert()断言。

17.assert()实际是在assert.h中定义的宏,取一个可以判断真假的表达式为参数,“必真,否则打印并推出”。

18.关于类嵌套定义:
类可以嵌套定义,在A类的内部定义B类,并声明B类的一个(或多个)对象为A类的成员。个人认为,此举除了名字范围划分外,和在类外定义一个新类无区别。要想让B类对象访问A类私有成员仍然需要声明其为友元――friend A::B;?所有友元声明都应放在类定义内部,因为这是给编译器看的。多从编译器的角度想问题,很多规则就变得理所当然了。

19.关于存取块:
这里,存取块指的是类定义内部由一个存取控制符(public,protected,private)开始到下一个存取控制符为止,中间的部分。
在一个存取块内的对象在内存中都是相邻的,各存取块之间通常也是按定义顺序存储的,但不绝对,因为有些机器对private提供支持,将其放在特殊位置。

20.关于存取限制及想办法访问私有成员:
存取指定消息(public,protected,private)只能作用到编译阶段,也就是说是对写代码阶段的限制。到了运行阶段存取限制标志已经消失,对象就是一个存储区域,如果有人想通过指针移动和格式转换来读取到私有成员,是可以做到的,C++对此无能为力。

21.要产生一个指向某结构或某类的指针,只需要该结构或类的声明而无需定义,因为指针的大小是确定的,而所指之物的大小不是必须知道的。

22.关于构造函数与析构函数的名字和返回值:
构造函数与析构函数必须与类同名,这是为了编译器能够立刻查找到它们,因为它们是由编译器负责调用的,实际上是由编译器负责在编译阶段将它们插入到原有的代码中,构造函数插入到定义处,析构函数插入到定义所在的代码块的右大括号处。
构造函数与析构函数都没有返回值,这与返回void不同,因为编译器负责调用它们,而编译器不可能知道如何处理返回值,所以它们没有返回值。

23.关于explicit和aggregate:
先说aggregate。aggregate类型可以使用大括号初始化。这种初始化方式,个人认为是调用了拷贝构造函数(见24)。
aggregate类型是指array或者是没有用户定义构造函数,没有私有或保护非静态成员变量,没有基类,没有虚函数的class类型。
在有构造函数的情况下,还有一种情况可能可以用大括号对对象进行初始化。
对于只有一个参数的构造函数,如果在初始化时提供此构造函数所需参数类型的参数,那么系统会自动调用此构造函数对对象进行初始化。这有可能是违背程序员意愿的,阻止方法是将构造函数声明为explicit,这样此构造函数就不能用来进行类型转换了。
在这种情况下,如果用一个大括号初始化一个对象数组是可以成功的。

24.关于拷贝构造函数:
如果你的类没有添加拷贝构造函数,编译器也会为你添加一个默认的拷贝构造函数,用于通过其它对象的const&来构造新的对象编译器默认生成的拷贝构造函数以“位拷贝”的方式工作。一般情况下,使用编译器提供的默认拷贝构造函数就可以了,除非你的类中有指针成员,因为指针成员存储buffer地址,而通过此地址对数据进行操作,如果直接拷贝就会出现两个对象只有一份数据的情况,会造成错误。正确的做法是,重新开辟内存,拷贝指针所指数据信息,给指针成员赋值。

25.关于编译器自动产生的缺省构造函数:
编译器在类定义中没有任何构造函数的情况下提供缺省构造函数。
工作原理如下:
1.全局变量(一般为定义在所有函数之外的变量),静态全局变量和静态变量的默认值为0。
2.局部变量默认值为随机数。
看来编译器自动产生的缺省构造函数还是很不靠谱的。

26.关于如何隐藏私有成员:
C++中类的定义,能够隐藏私有成员的实现,却无法防止类库的使用者看到私有成员的类型,因为必须要提供类定义,而类定义中一般含有类的全貌。解决办法是将私有成员放在结构体里,然后在类定义中只将指向此结构体的指针作为成员。在声明此指针时只需要该结构体的声明而非定义,这样就可以掩盖结构体的内部。
此技术成为handle classes或者Cheshire cat。

27.可以用引用初始化引用。如下列程序:
?int i = 5;
?int &j = i;
?int &k = j;
?k++;
?cout<<i<<endl<<j<<endl<<k<<endl;
输出结果为:
6
6
6

28.关于C++调用函数的传值过程
int f(int x,char c);
int g = f(a,b);
对应的伪汇编:
push b??;这是在进行值拷贝,相当于创建新对象作为函数参数
push a??;c/c++中,参数是从右向左进栈的
call f()?
add sp,4?;这句也是由函数调用产生的,调用代码负责清理栈中的参数
move g,register a?;返回函数结果
;如果a,b,g是全局变量,将被表示成_a,_b,_g,如果是局部变量,编译器将在堆栈指针上对
;其索引。
;通过传值方式传递参数时,编译器简单地将参数拷贝压栈――编译器知道拷贝有多大。

29.不能用const对象来初始化non-const引用,因为引用是直接对所引用存储单元进行操作的,c++出于安全考虑进行此项检查。用const对象初始化const引用或用non-const对象初始化const引用都没有问题。后一种情况(用non-const对象初始化const引用),仍然可以直接更改原对象的值,但不能通过const引用更改。
由此可合理推出,int& i =12;的写法是非法的,因为这试图用一个const对象初始化一个非const引用。

30.调用函数时,如果传递参数和返回函数结果都采用值传递的方式,那么在传递和返回时编译器会自动调用拷贝构造函数来生成局部形参对象和返回结果对象。如果怕编译器提供的拷贝构造函数不能完成拷贝的任务(bitcopy方式),可以自己定义拷贝构造函数。如何避免拷贝构造函数被调用呢?或者说如何避免一个类的对象被按值传递呢?方法是定义一个私有的拷贝构造函数,这样不但屏蔽了编译器的构造函数,而且编译器调用它时会报错。

Hello world!

十月 24th, 2009 2 Comments »
Welcome to chris' world.
 
Today here is nothing.
 
Tomorrow here will be a beautiful world.
 
Now chris me is at the first level.
 
In the far future,i don't know.
 
Which level can i reach?
 
Which level shall i reach?
 
To be a programmer.