您的位置:首页 > 编程语言 > C语言/C++

Thinking in C++读书摘要

2015-03-16 14:53 232 查看
第一章 对象的演化
1.对象 = 特性 + 行为;
2.已存在的数据类型的设计动机:为了描述机器的存储单元;
3.一个已经习惯于寻找复杂解的、训练有素的头脑,往往会被问题的简单性难住;

第二章 收据抽象
1.面向对象程序设计中的主要活动就是向对象发消息(向对象调用成员函数)

第三章 隐藏实现
1.存取控制,C++使用关键字private,public,protected,friend 进行存取控制。
2.嵌套定义(nested class)的类对包含它的类(enclosing class)的变量的存取与定义自己
时前面的存取控制符有关,而与自己声明的位置无关。

第四章 初始化和清除
1.类的初始化和清除是如此重要,以至于Stroustrup在设计C++时定义了构造函数和析构
函数,其中前者的名字和类相同,而后者在前面加~符号。
2.上述二者都没有返回值,而且只有前者有初始化使用的参数。可以想一下,如果函数
有返回值的话,那么要么编译器知道返回值应该传递给谁,要么我们要显式的调用它
们。而这二者均是编译器自动调用的。
3.缺省构造函数:是不带任何参数的构造函数。当编译器需要创建一个对象而又不知道
任何细节时,缺省的构造函数就会被调用。所以,若一个类型中没有缺省构造函数的
话,编译器会创造一个。

第五章 函数重载和缺省参数
1. 关于函数重载,可以对比一下自然语言中我们用同一个词儿表示不同的意思,具体的解释还要参考上下文语境。重载一个很重要的应用就是构造函数。
2. 为什么不能仅根据返回值不同进行函数重载呢?因为在函数调用时可以调用一个函数而忽略它的返回值,这时编译器就无法确定我们要用哪个函数了。
3. 缺省参数:只有参数列表的后部参数才可是缺省的,意即不能在一个缺省参数后跟一个非缺省参数。
4. 为什么引入缺省函数:使函数的调用方便,可以把最不可能调整的参数放到参数表的最后;另一个原因是当需要在一个函数中增加参数时,只要增加为缺省参数即可保证调用此函数的代码没有麻烦。

第六章 输入输出流
1. C++的目标是要能特别容易的增加一种数据类型。

第七章 常量
1. 在把const和指针连起来用时,有一个技巧:在标志符的开始处读它并从里向外读。比如:const int* x: x是一个指针,它指向const int,同理:int const * x;
int* const x: x是一个const指针,指向int型变量;
2. 传递const值:变量值不会被函数修改;
代替方案:可以在函数内部用const限定参数优于在参数表里用const限定参数。
void f(int ic)
{
const int& i = ic;
i++; // illegal – compile time error
}
3. 返回const值:对于内部数据类型来说,返回值是否是常量没有关系;对于用户定义类型,那么这个函数的返回值不能是左值。
4. enum的用法,可以在类中表示常量。如下所示:
class X
{
enum { const_wanted = 0.1 };
}
之后在类的内部就可以使用const_wanted了。Ogre中有很多这种用法,以后自己写程序就不用在程序开始建全局的常量了,这个是局部的。枚举常量在编译器全部被求值。
5. 和内建数据一样,用户定义类型也可以定义一个const对象,它在对象寿命周期内不被改变。于是可以声明一个函数为const。通过这种方式告诉编译器可以为一个const对象调用这个函数。
6. this 是一个const指针。通过将this指针强制转换成non-const指针可在const函数中修改常对象成员变量。第二种方式是将变量声明成mutable。

第八章 内联函数
1. 在C++中,内联函数是为取代宏定义而存在的。任何在类中定义的函数自动成为内联函数,也可以使用inline关键字放在类外定义的函数前使之成为内联函数。
2. 内联如何有效:对于任何函数,编译器在它的符号表里放入函数类型(名字、参数类型、返回类型)。另外,编译器看到内联函数和内联函数的分析没有错时,函数代码(源程序还是汇编取决于编译器)也被放入函数表。调用时,若所有的函数类型信息符合调用上下文的话,内联函数代码会直接替换函数调用,消除了调用开销。
3. inline不支持大函数体的原因有两个:一是这时花费在函数体内的时间相对于出入函数的消耗比较大,提高效率不多;二是由于内联后会在函数每一个函数被调用的地方作代码复制,使程序变得冗余。
4. Dan Saks指出在工程中,在.h文件中定义函数体会造成接口混乱,所以他建议将所有函数定义在类外面,并使用inline关键字。

第九章 命名控制
1. static基本含义是指“位置不变的某种东西”,这里则指内存中的物理位置或文件中的可见性。在固定的地址(全局数据区)上分配/对特定的编译单元是本地的
2. 如果没有为一个预定义类型(char int float double,这些都是通过编译器预定义的C++内置类型,其它的都是用户自定义类型)的static 变量初始化,编译器会将其初始化为0;
3. 全局变量的构造函数在main()之前就被执行。
4. 函数后跟随throw()是为了告诉编译器这个函数有可能会抛出异常。
5. extern 和 static:前者对所有的编译单元都是可见的,而后者只是局部变量。
全局变量:int a = 0;隐含为静态存储类,所以前述定义和extern int a = 0;是等价的。
6. auto 和register:前者告诉编译器这是一个局部变量,后者告诉编译器这是一个会经常用到的变量,最好把它一直保存在寄存器中,做代码优化时使用。
7. static成员变量:定义必须出现在类的外部而且只能定义一次,因此通常放到类的实现文件中。即我们通常所说的cpp文件的开头;当然也可以在定义文件的最后;
static成员函数:可以调用静态成员变量,不能调用非静态成员变量;而非静态成员函数既可以调用静态成员函数又可以调用非静态成员函数。
想一下:静态成员函数是属于类的,而不属于某个对象,如果调用成员变量/函数,调用谁的呢?既然非静态成员函数可以调用静态成员变量,那么就可以调用静态成员函数。
8. nested class can have static data members; local class cannot have static data members.
9. static const 代替之前的enum,因为enum的常量用法使枚举丧失了本来的作用,而且只能定义整型值。
10. 通常当前对象的地址this被隐含的传递到被调用的函数,但静态成员函数没有this,所以他无法访问一般的成员函数。
11. C++编译器会将函数名字变成_f_int_int之类的东西以支持函数重载,但是c编译器通常不会这么做,内部名通常为f。这样连接器将无法解决C++对f()的调用。extern “C” float f(int, int) 这就告诉编译器f()是C连接,这样就不会转换函数名。如果头文件中都是声明的话,可以通过extern “C” { #include “cHeader.h” }实现;

第十章 引用和拷贝构造函数
1.引用是支持C++运算符重载语法的基础,也为函数传入和传出的控制提供了便利。引用的思想来自于algol语言。
2.规则:引用被创建时必须被初始化(指针可以在任何时候被初始化);引用被初始化指向一个对象后就不能指向另一个对象(指针可随时更改自己指向的对象);不能有NULL引用;
3.C语言中,想改变指针本身而不是指针指向内容时,需要使用指针的指针。而C++中,使用指针的引用取代指针的指针;
4.->* 指向成员对象的指针

第十一章 运算符重载
1.不能改变优先级;不能改变操作符元数;不能创造操作符;”.”不能被重载;
2.由于运算符重载可以看成函数调用,所以++a与a++区别的方法可以用函数名搞定,如:
Prefix: const myclass& operator++() { i++; return *this;}
Postfix: const myclass operator++(int) { myclass before(*this); i++; return before;}
对于后者,编译器为int参数传递一个哑元常量值用来为后缀版本产生不同的署名。
3. 通常运算符重载有两个版本,一个是可以将其声明全局的函数,然后作为类的友元函数;另一个就是直接作为类成员函数。
4.运算符‘=’仅允许作为成员函数。为什么呢?
5.返回效率比较:
a) return integer(left.i+right.i); 仅构造;到返回值外面的空间;
b) intger tmp(left.i+right.i); return tmp; 构造、拷贝、析构; 局部空间,后拷贝到外部;
6. 拷贝构造函数和赋值运算符:
foo B; // 调用B的构造函数
foo A = B; // 在A的对象未创建时,调用的拷贝构造函数;
A = B; // 在A的对象创建后,调用A的赋值操作符;
7. 构造函数转换,这里的例子特别好:
class one {public: one() {}};
void f(two);
class two {public: two(const one&)}; // 此时调用f(one)无错
class two {public: explicit two(const one&){}}; // 此时调用f(one)出错,必须f(two(one));
8. 类X可以使用operator Y() const 将它本身转换到类Y;
9. 所谓的自动类型转换就是指在构造函数中写相应的构造函数。

第十二章 动态对象创建
1. C++中把创建一个对象的所有工作都放到new运算符里。空间分配、长度计算、类型转换和安全检查,最后返回this指针。这使得堆上分配空间和栈上一样容易。
2. 重载new和delete:
C++new中的内存分配方案是为通用目的而设计的,在特殊情况下不能满足用户需要:
a) 内存可能还有,但都是碎片,找不到足够大的空间;
b) 可能要在内存中制定的位置上放置一个对象。面向硬件的内嵌系统;
c) 希望调用new时,在不同的内存分配器中选择;
3. 虽然编译器在调用new时会分配空间并调用构造函数,但在重载new时仅能改变内存分配方式;
Operator new(size_t sz); // 需要有大小的参数;
Operator delete(void* ptr); // 需要指向那片内存的指针;
Operator new[](size_t); // 对数组而言的new[]和delete[]
Operator delete [](void* ptr);

第十三章 继承和组合
1. 如果基类中有一个函数名被重载几次,在派生类中重定义这个函数名会掩盖所有的基类版本。更有效的方式是用virtual声明它们。
2. 基类的private对外界和派生类都隐藏,而protected只对外界隐藏,对派生类可见。这是我们设计成员可见性的一个标准。
3. 构造函数和析构函数、operator=也不能被继承。
4. 私有继承成员的公有化:只需要在派生类中的public选项声明它们的名字即可。通过这种方式可以隐藏基类的部分功能。
5. 从某种意义上来说私有继承和被保护继承只是为了维护语言的完整性。
6. 向上映射(upcast)和向下映射:任何将子类的对象、引用和指针转变成父类的对象、引用和指针的动作称为向上映射。向上映射是安全的,所以C++总是允许不需要显示的说明或做其它的标记。此种特性与virtual关键字联系起来作用很大;

第十四章 多态和虚函数
1. binding 把函数体和函数调用相联系称为捆绑。早捆绑、晚捆绑;c++中的晚绑定机制是通过virtual关键字是实现的。
2. 每个包括虚函数的类被编译器创建一个VTABLE,而类中保存一个指向这个表的指针。这一点可以通过查看包含虚函数的类和普通类的大小验证,可以看到含虚函数的类比普通类多了一个void指针长度。
3. 如果一个类的对象总是没有意义的,c++中就可以将其声明成纯抽象基类(包含一个纯虚函数),可以防止用户创建其的对象;而此时,VTABLE就是不完全的,既然是不完全的,就不可能创建一个对象。
4. 在声明为纯虚函数后,还可以为其提供定义,因为我们希望此函数中的一块代码对于一些或所有派生类都能使用。在子类中,可以通过类名和域操作符调用它。
5. 如果派生类中定义了自己的虚函数,则通过基类指针的虚函数表的地址偏移无法准确获得我们所需的虚函数;但若我们确切知道了派生类类型时,就可以调用。
6. 包含虚函数的对象被创建时,必须初始化它的Vptr指向相应的Vtable,这个必须在任何有关虚函数调用之前完成。于是,构造函数承担了这部分工作。
7. 所有基类构造函数总是在继承类构造函数中被调用。因为构造函数本身就是为了保证对象被正确的创建,派生类并不能初始化基类的成员,这项工作通过调用基类构造函数完成。如果可能,我们应该尽量在构造函数的初始化列表中初始化所有成员对象。
8. upcasting 是自动自发的,不需强制,因为它是绝对安全的。downcasting是不安全的,因为没有关于实际类型的编译信息(参见RTTI)。这里的向上和向下都是以基类为继承树的根讨论的。
9. 虚析构函数:使用基类指针删除子类对象时,有了虚析构函数,会调用子类析构函数,彻底删除堆里面的对象;否则只会调用基类的;

第十五章:模板和包容器类
1. 继承和组合允许重用对象代码,但不能解决有关重用的所有问题,模版为编译器提供了一种在类和函数体中代换类型名的方法,以此重用代码;
2. 包容器的概念;

第十六章 多重继承
1. ios, istream, ostream, iostream是多重继承的一个应用实例;
2. 最晚派生类(most derived)是当前所在的类,打算使用虚基类时,最晚派生类的构造函数的职责是对虚基类进行初始化;因为既然继承关系走了一个菱形过程,必然有一层编译器无法分清楚调用谁的构造函数;
3. 指针法术(pointer magic)?
4. 可以知道,为了保证虚继承机制的实现,编译器给一个类增加了不少指针;
5. 避免使用多重继承:有必要使用两个类的公共接口?需要向上映射到两个基类吗?
6. 引入多重继承后必须解决子对象重叠问题。MI被称为90年代的goto;

第十七章 异常处理 Exception Handling
1. C中的出错处理: setjmp(): 存储当前程序正常状态;
longjmp(): 用上述状态作为参数,恢复;
上述两个函数相当于non-local 的goto语句,由于它跳出范围时不调用析构函数,所以c++中这种方法不可行;
2. 通常在出错的当前上下文中获取不到异常处理的足够信息,可以将当前错误信息封装成错误对象发送至更大的上下文对象。称为异常抛出。
3. 异常处理两种模式:终止、恢复;如果错误是致命性的,则调用c++终止模式结束异常状态;恢复模式则希望在异常处理后能继续正常执行程序;
4. 异常规格说明:
Void f( void ) throw(); //函数不会抛出异常;
Void f( void ) throw( myexcep1, myexcep2, …); //会抛出这些异常;
Void f( void ); //可能抛出任何类型的异常,也可能不会;
5. set_terminate( void terminate() ): 未被捕获的异常;
set_unexpected( void unexpected() ): 实际抛出的异常类型和函数定义时的异常规格说明不一样;
6. 调用构造函数时发生异常,相应的析构函数能够被顺利的调用吗?
7. 用引用而不是值去捕获异常:当对象通过值被捕获时,它被转化成一个base对象(通过构造函数);而通过引用被捕获时,仅仅地址被传递所以对象不会被切片,所以行为表现出其在派生类中的真实情况;
8. 使用一个构造函数为调用成功的对象是十分危险的,所以需要在构造函数中抛出异常;而析构函数会在抛出其它异常时被调用,所以不要打算在其中再抛出另一个异常了。一旦后者发生,意味着在已存在的异常到达引起捕获之前抛出了一个新异常,这会导致对teminate()的调用;
9. 为了使用新特性,必然有开销;c++的所有新特性都是如此。异常处理器(catch块)完成任务后,异常对象被相应销毁;

第十八章 运行时类型识别 RTTI
1. 通常我们并不需要知道一个类的确切类型,虚函数机制可以实现那种类型的正确行为。RTTI即在只有一个指向基类的指针或引用时确定一个对象的准确类型;
2. RTTI的两种用法:typeid(返回一个type_info对象)和安全类型向下映射(使用dynamic_cast);
3. 典型的RTTI是通过在vtable中放入一个额外指针实现的,这个指针指向一个描述该特定类型的typeinfo结构(每个新类型只有一个typeinfo实例);
4. 如果自己写RTTI,从本质上说,只需要两个函数就行了,一个用来指定类的准确类型的虚函数;
5. static_cast: 所有的良定义转换,包括安全转换和次安全转换,前者是指编译器就可以进行的转换,后者包括窄化(丢失)信息、用void*的强制变换、类层次的静态导航;
6. const_cast: 把一个const转换成non_cast, volatile 到non_volatile;
7. reinterpret_cast: 不安全;其假设一个对象仅仅是一个比特模式,它可以被当作完全不同的对象对待;什么时候用呢?
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: