读书笔记第二季(item31~60)
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)
在这种情况(优化情况)下,编译器实现将原始对象和被拷贝的对象视为对同一对象的两种不同方式的引用,并且在未优化的情况下,编译器会选择这两种引用中生存期较长的一种,在其生存期到期后才进行析构。
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的所以连接器不会抱怨多重定义。由于只在第一个被编译的编译单元内该对象被创建后会执行初始化工作(计数器为零,这严重依赖于在所有动态初始化进行前所有静态对象都会被初始化为零),因此不管哪个编译单元先被执行编译都能很好地完成静态对象的初始化工作。