<<Thinking in C++>> 是一本非常经典的书,略读了一遍,受益匪浅。
Chapter 1 Introduction to Objects
1. 基类和派生类之间的关系看做是一个“is-a”的关系。
依赖关系:" ... uses a ...";
关联关系:" ... has a ...";
聚合关系:" ... owns a ...";
组合关系:" ... is a part of ...".
2. 我们把处理派生类型就如同处理其基类的过程称为向上类型转换。
3. 多态在运行时决定如何处理。
4. 从技术角度,OOP的论域就是抽象数据类型、继承和多态性。(comment:为什么没有封装?)
5. 为了最大化运行速度,通过将对象存放在栈中或静态存储区域中,存储和生命期可以在编写程序时确定。
对象堆存储是在运行时动态管理的,所以堆上分配存储所需要的时间比栈上创建存储的时间长的多。
6. 异常是一个对象,它在出错的地方被抛出,并且被一段容易处理特定类型错误的异常处理代码所接收。
7. 不论做了多少分析,总有系统的一些问题直到设计时才暴露出来,并且更多的问题是到编程或直到程序完成运行时才出现。因此,迅速进行分析和设计并提出的系统执行测试时相当重要的。
8. 类职责协同(Class-Responsibility-Collaboration, CRC),在CRC卡片上描述一个类的内容:
(1)类的名字(2)类的职责(3)类的协同
9. 开始编程,让一些部分能够运行,这样就可以证明或否定已生成的设计。
11. 提出一个漂亮的方案实际上是一种完全不同水平上的满足,感觉更接近于艺术,而不是技术。
12. C++堵塞了C语言中的许多漏洞,并提供更好的类型检查和编译时的分析。程序员必须先声明函数,是编译器能检查他们的使用。预处理器也限制了值替换和宏,这就减少了查找错误的困难。
Chapter 2 Making &using Objects
1. 大部分的解释器要求一次输入整个源代码。这不仅造成内存空间的限制,而且如果语言不提供设施隔离不同代码段之间的影响,一旦出现错误,就很难调试。
2. C++使用静态类型检查,JAVA也可在程序运行时作部分类型检查[动态类型检查]。动态类型检查和静态类型检查结合使用,比仅仅使用静态类型检查更有效。但它也增加了程序执行的开销。静态类型检查在编译时就告知程序员类型被误用,从而加快了执行时的速度。
3. 声明是向编译器介绍名字-标识符。定义为名字分配存储空间。
int func();在C中表示“一个可带任意参数(任意数目,任意类型)的函数”。这就妨碍了类型检查。而在C++语言中它就意味着“不带参数的函数”。
C++不允许其它类型赋值于Void*, C语言允许。
4. C语言的设计者并不要求函数声明使用extern。
5. C中的”.h”扩展名的库仍然可用,使用它们,即在文件名前加一个字母”c” 。
#include<stdio.h>和#include<stdlib.h>就变成#include<cstdio>和#include<cstdlib>,如果混用这两种形式,会遇到某些问题。
6. 程序启动模块,它包含了对程序的初始化例程。初始化例程是开始执行C/C++程序时必须首先执行首先执行一段程序。初始化例程建立堆栈,并初始化程序的某些变量。
7. Using namespace std; 这就意味着打开std名字空间。在人们费劲心机把名字空间的名字隐藏起来之后,再保留名字空间的所有名字,这看起来是矛盾的。但是using指令仅仅保留当前文件的名字。
8. Cout<<”this ”
“is”
“book”;这样没有错,请记住C/C++是自由格式语言,每个语句结束时才加分号。
9. String具有动态特性,不必担心string的内存分配。String把文件当成单个字符串来处理…
10. Vector不仅仅限于输入和去除,还可以通过使用方括号的下标操作符向vector的任何一个单元赋值。这说明vector是通用、灵活的”暂存器”,用来处理对象集。
Chapter 3 The C in C++
1. Func()在C中表示不确定的参数数目,C++中表示空的参数列表; func(…)在C++中表示不确定的参数数目。Func(void)在C/C++中都表示空的参数列表。
2. C++原型必须指明函数的返回值类型(在C中,如果省略返回值,表示默认为整形)
Chapter 4 Data abstraction
1. 库是他人写好的一些代码按某种方式包装在一起,库大概是改进生产效率的最重要的方法。C++的主要设计目标之一是使库使用起来更加容易。
2. C库的问题之一是必须向用户认真地说明初始化和清除函数的重要性,如果这些函数未被调用,就会出现许多问题。
3. 调用没有声明的函数在C中是可以的(C++中不可以)。如果在头文件中声明一个void func(float),当调用func(int)编译器就知道应当把传入的int转换为float;如果没有声明,C 编译器简单地假设有一个void func(int),错误就产生了,并且这个bug很难找出。
4. 在C中,使用库最大的障碍时名字冲突。无论什么情况,都不允许包含具有相同函数的两个C库。
5. 在C中,Struct内部的标识符不会与全局标识符冲突。
6. 在C中,结构的地址是结构的第一个变量;在C++中,this是struct的地址。
7. C++允许将任何类型的指针赋值给void*(这是void*的最初的意图,它需要足够大,以存放任何类型的指针),但不允许将void指针赋值给任何其它类型的指针。
8. 停留在函数捆绑在数据结构内部的语言是基于对象的,而不是面向对象的。这常常被称为封装。
9. 对象的基本规则之一是每个对象必须有一个唯一的地址,因此,无数据成员的结构总应当有最小的非零长度。
10. 头文件告诉编译器在我们的库中哪些是可以用的,头文件是存放接口规范的地方。C++通过强制正确地使用头文件,保证库中的一致性,并通过在各处强制使用相同的接口,减少程序错误。
11. 如果使用using namespace std;在头文件中,这个文件又是其它文件的头文件,很容易在各处‘关闭’名字空间,不能体现名字空间的好处。简言之,不要在头文件中使用此指令。
12. 将数据和函数捆绑在一起使得库更容易使用(隐藏名字防止名字冲突),但大量的工作可以使C++编程更安全。
Chapter 5 Hiding the Implementation
1. C++不是完全的面向对象语言,而只是一个混合产品。增加friend关键字就是为了用来解决一些实际问题。这也说明了这种语言是不存的。毕竟C++语言设计目的是实用,而不是最求理想的抽象。
2. Struct转换到class是如此重要,因此怀疑Stroustrup偏向于将struct重新定义,但考虑到对C的兼容性而没有这样做。
Chapter 6 Initialization & cleanup
1. 类在库的应用方面做出了重大改进。封装(放入类中)和访问控制(public,private, friend)在改进库的易用性方面取得了重大进展。
2. 在C++中,定义和初始化(构造函数)是基为一体的,不能只取其中之一。在程序中创建和消除一个对象的行为非常特殊,就像出生和死亡,总是由编译器来调用这些函数确保它们被执行;如果由开发者显式的调用构造函数和析构函数,安全性就被破坏了。
3. 一般来说,应该在尽可能靠近变量的使用点处定义变量,并在定义时就初始化。这是出于安全性的考虑,通过减少变量在块中的生命周期,就可以减少变量在块的其他地方被误用的机会。
4. 小作用域是良好设计的指标。
5. goto和switch都可能跳过构造函数调用的序列点,甚至构造函数没有被调用时,这个对象也会在后面的程序块中起作用,所以编译器给出了一条出错信息。
6. 尽管编译器会创建一个默认的构造函数,但是编译器合成的构造函数的行为很少是我们期望的。我们应该把这个特征看成是一个安全网,但尽量少用它。
7. 结构体也有构造函数和析构函数.
8. C++和编译器,谁先发明的?鸡与蛋的问题?
9. 函数调用是先和头文件匹配,然后再到实现地方。
10. 如果void指针指向一个对象的话,就不能正确地将其删除。
Chapter 7 function overloading & default arguments
1. 重载的原因(使函数名方便使用):
1)方便理解,根据传入传出参数就可以确定函数意义,而不用为每种不同参数定义不同函数。即根据上下文可以判断函数意思。
2)构造函数推动了重载,构造函数的需求多样化必须要求重载。
只能通过范围(作用域)和参数来重载。
2. 类、结构体、联合体都有访问限制(public, protect, private),构造函数和析构函数。但union不能在继承时作为基类使用。
3. 枚举的类型名是可选的,不是必须的。
4. 使用Union的首要目的是为了节省空间。Union没有类型名和标识符,这叫做匿名联合。我们访问一个匿名联合的成员就像访问普通的变量一样。唯一的区别在于:该联合的两个变量占用同一内存空间。
5. 默认参数同函数重载一样,给程序员提供了很多方便,它们都使我们可以在不同的场合下使用同一函数名字。不同之处是,利用默认参数,当我们不想亲手提供这些值时,由编译器提供一个默认参数。默认阐述只能放在函数声明中。
默认参数的一个重要应用情况是在开始定义函数时用了一组参数,而使用了一段时间后发现要增加一些参数。通过把这些新增参数都作为默认的参数,就可以保证所有使用这一函数的客户代码不会受到影响。
6. 占位符参数(placeholder)。语法允许把一个参数用作占位符而不去用它。其目的在于以后可以修改函数定义而不需要修改所有的函数调用。
Chapter 8 constants
1. const在变与不变之间画一条界线。在C++程序设计项目中提供了安全性和可控性。
2. 宏定义的工作方式与普通变量类似,而且没有类型信息,这会隐藏一些很难发现的错误。宏定义会使代码易读,易维护。
3. 只有静态常量才能在类声明时就初始化。常量对象只能调用类常量成员函数。
4. C++规定,有const修饰的变量,不但不可修改,还都将具有内部链接属性,也就是只在本文件可见。(这是原来C语言的static修饰字的功能,现在const也有这个功能了。)又补充规定,extern const联合修饰时,extern将压制const这个内部链接属性。于是,extern char const s[]将仍然有外部链接属性,但是还是不可修改的。
5. 在C++中,一个const不必创建内存空间,而在C中,一个const总是需要创建一块内存空间。C++编译器并不为const创建存储空间,相反它把这个定义保存在它的符号表里。但是,加上extern将强制进行存储空间分配,如果取一个const的地址,也要进行存储空间分配。
6. Const和#define比,具有 #define的可以替换性,还可以常量折叠。
7. C++内部连接:const, static, inline函数
8. 对于内部类型来说,按值返回的是否是一个const,是无关紧要的,所以按值返回一个内部类型时,应该去掉const.
9. 有时候,在求表达式值期间,编译器必须创建临时对象。编译器使所有的临时量自动地成为const。
10. Const只能在类构造函数初始化列表中初始化。(Const的值可在编译时初始化。也可在运行时初始化?)
11. 类常量对象只能调用常量成员函数;构造函数和析构函数都不是const成员函数,因为他们在初始化和清除时,总是对对象作修改。
12. 去掉常量性,1.使用const_cast; 2使用关键字mutable.;虽然C++有助于防止错误发生,但如果程序员自己打破这种安全机制,它也是无能为力的。
Chapter 9 inlines
1. 宏定义预处理主要作用:1.代码替代2.字符串定义、字符串拼接和标志粘贴。
2. 在宏定义的各个地方使用括弧来解决传入优先级。
3. 使用宏能降低函数调用开销。内联代码的确占用空间,但假如函数较小,这实际上臂为了一个普通函数调用而产生的代码(参数压栈和执行CALL)占用的空间还少。
4. 如果一个函数是在类声明内定义的,它将自动转换成内联函数,没有必要再函数声明的前面加上关键字inline.构造函数和析构函数也可以是内联的。类内部的内联函数节省了在外部定义成员函数的额外步骤,所以我们一定想在类声明内每一处都使用内联函数。但是,使用内联函数的目的是减少函数调用的开销;如果函数较大,由于需要在调用函数的每一处重复复制代码,这样将使代码膨胀,在速度方面获得的好处就会减少。
5. 内联函数使用攻略:1.需要大量调用地方;2.内联函数很小
6. 宏定义的副作用:
#define BAND(x) (((x)>5 && (x)<10) ? (x) : 0)
BAND(++a) ==>
(((++a)>5 && (++a)<10) ? (++a) : 0)
7. 把类里的内联定义做得简单和精练是非常有用的,这样更容易在一页或一屏里,看起来更方便一些。但是一个真正的工程里,这将造成类接口混乱,使类难以使用。主张所有的定义都放在类外面以保持接口清晰。假如想优化,在外面使用关键字inline.
8. 能够隐藏类的底层实现是关键的,任何危害实现隐藏性的东西都会减少语言的灵活性。
9. 程序开发的原则应该是:首先使用它可以工作,然后优化。
Chapter 10 name control
1. 无论什么时候设计一个保护静态变量的函数时,都应该记住多线程问题。
2. 用户自定义类型的静态对象必须用构造函数来初始化;程序控制第一次转到对象的定一点时,而且只有第一次时,才需要执行构造函数。
3. 静态对象的析构函数,在从main()中退出(main()有时也是调用exit()),或调用exit()时才被调用。如果在析构函数内部使用exit()会导致无穷的递归调用。
4. 一般情况下,在文件作用域内的所有名字对程序中的所有单元来说都是可见的,这就是所谓的外部连接。全局变量和普通函数都有外部连接。
5. 连接只引用那些在连接/装载期间有地址的成员,因此类声明和局部变量并不连接。
6. 可以在局部变量中加入external,说明此局部变量引用于某一全局变量,它是全局的。
7. auto,它指明编译器自动为该变量分配存储空间;register,它告诉编译器这个特殊变量要经常用到。
8. namespace唯一的目的是产生一个新的名字空间,从而对(内部和外部)“连接”做到一定的控制
9. 类内部静态变量定义必须出现在类的外部(不允许内联)而且只能定义一次,因此它通常放在一个类的实现文件中。
10. 局部类,在函数中定义一个类;局部类中不能定义静态变量。局部类很少使用。
11. 静态成员函数不能访问一般的数据成员,只能访问静态数据成员,也只能调用其它的静态函数。一个静态成员函数没有this.
12. 如果C++程序想使用C库,通过重载extern关键字来实现: extern “C” void test();
13. 构造函数和析构函数必须是公有。
Chapter 11, references and copy constructor
1. 按值传递,需要调用拷贝构造函数。
2. 拷贝构造函数的作用:1传值,2.返回值,3对象初始化赋值
3. 成员指针是受限制的,它们仅能被指定给在类中的确定的位置。例如,我们不能像使用普通指针那样增加或比较成员指针。
4. 如果用户必须直接操作成员指针,那么typedef是合适的。
5. 浅拷贝只是赋值,将新对象的指针指向老对象;深拷贝是为新对象开辟一片空间,然后将老对象的值拷贝进来。在浅拷贝情况下,某一对象值改变,另一对象也随之改变。
6. 类中是没有地址的,成员指针如果想指向成员变量,必须在定义时:int Widget::*p = &Widget::a;
7. 常量引用初始化必须有const。 const int &q = 12;
8. 成员指针时指向类中成员,而非类对象的成员。普通指针用确定对象的地址进行初始化,指向一个确定的对象;成员指针用类的成员(注意不是对象的成员,而是类的成员)初始化(也就是只有偏移量的信息,而没有初始地址的信息)。You can choose a particular region of storage (data or function) at runtime. Pointers-to-members just happen to work with class members instead of with global data or functions. You get the programming flexibility that allows you to change behavior at runtime.
9. 成员指针函数就像指针函数一样,可以当做接口。Pointer to member functions can be used in pluggable architectures。
Chapter 12, operator overload
暂时忽略这一章,后面再来总结
Chapter 13, Dynamic object creation
1. malloc()和free()是库函数,不在编译器控制范围之内。
2. 构造函数不能被显式的调用,它在对象创建时由编译器调用。
3. MyTpe *fp = new MyTpye; 它带有内置的长度计算、类型转换和安全检查。
4. delete void*时不会调用析构函数
5. 使用new过程:1.计算对象长度 2.调用malloc分配得到void*内存3.强转为对象类型赋值4.调用构造函数初始化
6. 构造函数不能被显式的调用,它在对象创建时由编译器调用。
7. 如果定义局部new/free(在类内部定义),会调用局部的new/delete
8. 如果不知道数组大小,可以用指针替代。
9. 当创建一个iostream对象时,它们调用new去分配内存。用printf()不会进入死锁状态,因为它不调用new来初始化本身。如果没有涉及到全局的operator new()和delete(),所以使用iostreams是可行的。(comment: ???)
Chapter 14, Inheritancd &Composition
1. 继承,大量的工作由编译器来完成。子类调用父类同名方法,father::set()。 子类初始化父类构造函数方法,MyType::MyType(int i): Bar(i), m(i+1) {}
2. 构造函数的初始化表达式允许我们显式地调用成员对象的构造函数,所有的成员对象在构造函数的左括号之前就被初始化了,我们的精力就可以集中在想要完成的任务上面。这是C++的一个强化机制,它确保了,如果没有调用对象的构造函数,就别想向下进行。
3. 必须显示的在构造函数初始化表达式中调用父类带参构造函数。
4. 构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建。Operator=也不能被继承,因为它完全类似于构造函数的活动。
5. 当开始一个项目时,我们不可能知道所有的答案,如果开始把项目作为一个有机的、可进化的生物来“培养”,而不是完全一次性地构造它,像一个玻璃盒子式的摩天大楼,我们机会获得更大的成功和更直接的反馈。
6. 如果允许编译器为派生类生成拷贝构造函数,它将首先自动地调用基类的拷贝构造函数,然后再是各成员的拷贝构造函数。
7. 确定应当用组合还是继承,最清楚的方法之一是询问是否需要从新类向上类型转换。
8. 静态( static) 成员函数不可以是虚函数(virtual)。
Chapter 15, Polymorphism&virtual Funciton
1. 多态的作用:改善了代码的组织性和可读性,同时也使创建的程序具有可扩展性。
2. 虚函数增强了类型概念,而不是只在结构体内部隐藏地封装代码。
3. 把函数体与函数调用想联系称为“捆绑”
4. 仅需要在基类中声明一个函数为virtual,调用所有匹配基类声明行为的派生类函数都将使用虚机制。
5. 晚捆绑实现:编译器为每个包含虚函数的类创建一个表(VTABLE),在VTABLE中,编译器放置类的虚函数的地址。在每个带有虚函数的类中,编译器秘密地放置一个指针(VPTR),指向这个对象的VTABLE。当通过基类指针做虚函数调用时,编译器静态地插入能取得这个VTR并在VTABLE表中查找函数地址的代码,这样就能调用正确的函数并引起晚捆绑。
6. 获取PTR void * ptr = (void*)*(unsigned long*)p;
7. VPTR在构造函数中作初始化工作,VPTR总是指向相应的VTABLE。
8. 只要有一个函数在类中被声明为纯虚函数,则VTABLE就是不完全的。
9. 为什么不在任何地方使用晚捆绑(virtual)呢?因为它不是相当高效的。Virtual需要调用两条以上的复杂的汇编指令,这既需要代码空间,又需要执行时间。
10. 在C中,效率是最重要的,创造C完全是为了代替汇编语言以实现操作系统;发明C++的主要原因是让C程序员的工作具有更高效率。
11. 建立公共接口的唯一原因是它能对于每个不同的子类有不同的表示。它建立一个基本的格式,用来确定什么是对于所有派生类是公共的,除此之外,别无用途。
12. 注意:纯虚函数禁止对抽象类的函数以传值方式调用。这也是防止对象切片(object slicing)的一种方法。对过抽象类,可以保证在向上类型转换期间总是使用指针或引用。
13. 对象切片实际上是当它拷贝到一个新的对象时,去掉原来对象的一部分,而不是像使用指针或引用那样简单地改变地址的内容。如果在基类中是纯虚函数,编译器不允许我们创建基类对象,它将阻止对象进行“切片”。
14. 可以在实现文件中实现纯虚函数,头文件任然是纯虚(抽象类)。
15. 抽象接口析构函数必须是虚函数,并且必须有函数体。
16. 虚函数多重指派(multiple dispatching),一个单一虚函数调用引起了第二个虚函数调用。
Chapter 16, templates
1. 浅显的过一遍,学习STL时再来精读