《Accelerated C++》笔记
书籍ISBN:978-7-111-22404-4
Chapter 8
P123 typename关键字
这里举了一个这样的例子:
template <class T> T median (vector<T> v){ typedef typename vector<T>::size_type vec_sz; ... }
对typename关键字的说明是:这里的typename是要告诉编译器,vector<T>::size_type是一个类型的名字,虽然编译器现在不知道类型T表示什么。无论什么时候,如果你使用依赖于模板参数的类型的一个成员时,必须在整个名字之前加上typename来单独指出这是一个类型的名字。
Chapter 9
P145 默认初始化与值初始化
说到初始化,那就要从类的构造函数开始谈起,书中一共列出了三种情况,具体规则如下:
1、如果一个对象的类型是定义了一个或多个构造函数的类的话,那么是当的构造函数会完全控制这个类的对象的初始化过程。
2、如果一个对象的类型是内置类型的话,那么值初始化会把它设置为0,而默认初始化会提供一个不明确的值。
3、否则,这个对象的类型只能是没有定义任何构造函数的类。如果是这样的话,对这个对象进行值初始化或者默认初始化就会对它的每个数据成员进行适当的值初始化或默认初始化。如果这个对象的任何一个数据成员的类型是一个类,那么初始化的过程就会按照上述规则递归的进行。
那么这里的值初始化与默认初始化究竟是怎么回事呢?
默认初始化(第3.1节,P32):当我们部位一个变量提供一个初始值时,我们就隐式的依赖于默认初始化(default initialization),默认初始化取决于变量的类型。对于类对象来说,雷辉自己说明如何进行默认初始化。但是对于内置类型的局部变量来说,这种初始化是不明确的,也就是说变量的值可以使任意随机的废弃值,它常常是一个无效值。
值初始化(第7.2节,P108)。当我们使用一个从来没见过的键来做map的索引时,map就会自动地创建一个含有这个键的新元素,而且这个元素是值初始化的。比如对于简单的int类型来说,值初始化会把int类型的值置为0。
Chapter 11
P170 P198 explicit关键字
这个关键字只在带有一个参数的构造函数的定义中有意义。当我们说一个构造函数是explicit,就是说只有在用户明确地调用这个构造函数的地方,编译器才能使用这个构造函数。从而可以防止自动的隐式转换。比如定义了一个template<class T> class Vec的构造函数为:
explicit Vec(std::size_t n){...}
那么在初始化一个vec对象时,就会有以下规定:
Vec<int> vi(100); //可以,显式调用上述构造函数
Vec<int> vi = 100; //不可以,不能进行隐式转换并调用上述构造函数
P176 模板生存空间内外的函数原型
使用模板类作为参数或是返回值时,应当确定此时是否在模板的生存空间之内,C++允许在模板的生存空间以内省略模板参数类型。比如,在类定义的内部对某个方法进行声明时,函数原型是:
vec& operator= (const vec&);
而在类定义外部,对这个方法进行定义时,我们需要这样的函数原型:
template <class T>
vec<T>& vec<T>::operator= (const vec& v){...}
在定义中,参数列表的参数已经算是在模板的生存空间之内了,所以只需要把类型简单的写为vec&即可。而其中的类限定符也应当从类内部的vec::改为类外部的vec<T>::。
复制构造函数在类内部的原型为:Vec(const Vec &);
复制构造函数在类外部的原型为:Vec<T>::Vec<T>(const Vec& v){...}
P179 默认操作与默认构造函数
如果类的作者没有定义类的创建、复制、赋值或者销毁的操作,编译器就会生成这些未定义操作的默认版。默认版的函数会定义为递归操作,针对内置类型会使用适当的规则完成这些操作,类型为类的成员会调用自己的构造函数、复制构造函数、赋值操作符和析构函数来执行相应的操作。需要注意的是,内置类型的析构函数什么都不会做,所以如果这个内置类型是一个指针,那么在这种情况下,它指向的内存空间就不会被释放。
另外需要注意的是,如果类明确地定义了任何一个构造函数,即便是一个复制构造函数,编译器也不会为这个类再合成一个默认构造函数。仅当这个类中没有定义任何构造函数时,默认构造函数才会被合成,这个合成的构造函数不带任何参数,并且会递归的去初始化每一个数据成员,并根据上下文来决定进行默认初始化或值初始化。
P180 复制构造函数,赋值操作符与析构函数三者的关系
对于管理资源(比如内存)的类,默认的构造函数一般是无法满足这样的类的。所以必须定义一个复制构造函数,来完成对资源的复制。并且需要同时定义对资源进行正确管理的、不是采用默认的方式的虚构函数,那么这些类几乎都需要一个显式定义赋值操作符,并且在其中完成对这些资源的正确处理。
在涉及资源管理的类中,复制构造函数、赋值操作符和析构函数它们之间的关系是三者不可缺一的。
P183 <memory>头文件中的allocator<T>类
使用内置的new与delete操作来管理内存,会带来很多过度的开销,因为它不仅会分配内存空间,而且还会把这些内存初始化。如果使用new在构造函数中完成内存管理,那么当用户使用new来创建类的实例时,那就会把对象使用的内存初始化两次:一次是在申请内存空间时,使用默认的构造函数进行初始化,另一次是使用用户提供的值来初始化。
<memory>头文件中提供了一个类,叫做allocator<T>,它可以分配用来保存T类型对象的整块内存,而且不需要初始化,并且它会返回一个指针,指向这块内存的首元素。类中包含4个成员函数:
template <class T> class allocator{ public: T* allocate (size_t); void deallocate (T*, size_t); void construct (T*, const T&); void destroy (T*); // ... };
allocate成员函数可以分配指定类型但是未进行初始化的内存。指定类型是指我们会使用T*来指向这个内存空间,未初始化是指这块内存中没有构造任何对象。
deallocate可以释放未初始化的内存。
construct会在这块未初始化的内存中构造一个单独的对象。
destroy会销毁它参数说指向的T类型的对象,使这块内存再次成为未初始化的状态。
另外还有两个非成员函数,用于初始化allocate成员函数分配空间中的元素:
template <class In, class Fo> Fo uninitialized_copy(In, In, Fo);
template <class Fo, class T> void uninitialized_fill(Fo, Fo, const T&);
P185 类不变式(Class Invariant)
无论何时,只要有一个有效的Vec对象,就要满足一下4个条件:
1)如果对象有元素的话,data指向的是首元素,否则就为零。
2)data <= avail <= limit
3)区间[data, avail)中的元素被构造。
4)区间[avail, limit)中的元素没有被构造。
可以把这些条件称为类不变式。如果在构造对象时满足了这4个条件,并且可以确保没有任何成员违背类不变式,就可以保证不变式为真。在Vec<T>类中,改变data、avail或limit的值就可以使不变式为假。但是没有任何一个成员可以把这个不变式变为假。
在Vec<T>类中引入一个allocator<T>类的私有成员变量alloc,下边是Vec<T>类中的几个私有成员函数,他们主要在alloc上使用allocate<T>类的成员函数来实现:
template <class T> void Vec<T>::create() { data = avail = limit = 0; } template <class T> void Vec<T>::create(size_type n, const T& val) { data = alloc.allocate(n); limit = avail = data + n; uninitialized_fill(data, limit, val); } template <class T> void Vec<T>::create(const_iterator i, const_iterator j) { data = alloc.allocate(j - i); limit = avail = uninitialized_copy(i, j, data); } template <class T> void Vec<T>::uncreate(){ if(data){ iterator it = avail; while(it != data) alloc.destory(--it); alloc.deallocate(data, limit - data); } data = limit = avail = 0; } template <class T> void Vec<T>::grow(){ size_type new_size = max(2 * (limit - data), ptrdiff_t(1)); iterator new_data = alloc.allocate(new_size); iterator new_avail = uninitialized_copy(data, avail, new_data); uncreate(); data = new_data; avail = new_avail; limit = data + new_size; } template <class T> void Vec<T>::unchecked_append(const T& val){ alloc.construct(avail++, val); }
Chapter 12
P191 将其他类型的对象转换成用户定义类型的对象(带其他类型参数的构造函数)
在string类中定义一个带有const char*参数的构造函数,就可以完成char*到string类型的转换,并且这个构造函数一定不能是explicit的。比如在string类中定义如下的构造函数:
string(const char* cp){
std::copy(b, e, std::back_inserter(data));
}
那么当我们在之后编写string s = "hello";语句时,编译器首先会使用上面的构造函数,根据给定的字符串,创建一个string类型的无名的局部临时对象,然后调用string类的赋值操作符(用户定义的或是编译器合成的),把这个临时对象赋给变量s。
P199 将用户定义类型的对象转换为其他类型的对象(类型转换操作符)
类的作者可以显式的定义类型转换操作符(conversion operator),这种操作符可以说明如何把这个类的对象转换为目标类型。类型转换操作符必须被定义为类的成员。类型转换操作符的名字是通过在关键字operator后跟上目标类型来构成的。比如:
class student_info {
public:
operator double() const;
//...
};
这个成员会说明如何从这个类的对象来创建一个double类型的值。
P200 void*通用指针
指向void的指针叫做通用指针,这是因为这种指针可以指向任何类型的对象。但是不能对这种指针解引用,原因是它指向的对象的类型还是未知的。我们可以通过强制转换确定指针指向的类型后再做解引用操作。
Chapter 13
P211 静态绑定与动态绑定
首先假设有两个类,Core类和继承Core类的Grad类。然后我们编写一个函数:
bool compare_grades(const Core& c1, const Core& c2){ return c1.grade() < c2.grade(); }
只要在Core类中定义一个virtual的grade函数,并在Grad中对grade函数进行覆盖,那么在调用上面的函数时,无论传递Core类的对象,还是Grad类的对象,都可以由动态绑定技术在运行阶段来决定到底要调用哪个类的grade函数。
但是,如果把compare_grades函数的定义改写为下边的样子:
bool compare_grades(const Core c1, const Core c2){ return c1.grade() < c2.grade(); }
由于定义的这些参数是Core类的对象,所以对grade函数采用的其实是静态绑定。如果在调用compare_grades函数时传入的参数是Grad类的对象,实际上发生的情况就会是,我们只传递了对象的基类部分,作为参数的Grad对象会被裁剪为他的Core部分。因此,即使Core类的grade函数是virtual的而且在Grad类中进行了重写,我们也无法调用Grad类的grade函数。
想要实现动态绑定,除了要定义虚函数,很重要的另一点是,一定要通过引用或者指针来完成对函数的调用。
P212 虚函数必须定义
对于一个需要实例化的类,必须定义这个类里的每一个虚函数,而不管程序是否会调用它们。非虚函数可以只声明、不定义,只要程序不调用它们就不会有问题。但是一个类如果没有定义一个或多个虚函数的话,很多编译器就会报告奇怪的错误。