1.C++是一个同时支持过程形式、面向对象形式、函数形式、泛型形式、元编程形式的语言。
条款02:
1.宁可以编译器替换预处理器
#define ASPECT_RATIO 1.653
记号名称ASPECT_RATIO也许从未被编译器看见,在编译器开始处理源码之前它就被移走了。于是ASPECT_RATIO可能没有记号表内。当运行此常量获得错误信息时,错误信息提到1.653而不是ASPECT_RATIO。
解决方法是用常量替换宏:
const double AspectRatio = 1.653;
2.关于指针为const和指针所指之物为const
const char* const authorName="Scott Meyers";
第一个const修饰指针所指物,第二个const修饰指针。
3.为了将常量的作用域限制于class内,你必须让它成为class的一个成员;而为确保此常量至多只有一份实体,必须让它成为一个static成员:
class GamePlayer{
private:
static const int NumTurns = 5; //常量声明式
int scores[NumTurns]; //使用该常量
};
然而上面的NumTurns是声明式而不是定义式。通常C++要求对所使用的任何东西提供一个定义式,但如果是个class专属常量又是static且为整数类型,则需特殊处理。
只要不取他们的地址,你可以声明并使用它们而无需提供定义式。但如果取某个class专属常量的地址,就必须另外提供定义式如下:
const int GamePlayer::NumTurns;
由于class常量已在声明时获得初值,因此定义时不可以再设初值。
不用#define创建一个class专属常量,因为其不重视作用域。一旦宏被定义,它在其后的编译过程中有效。
4.如果class内部有non-static const常量,不能在声明时赋值。
class GamePlayer{
private:
const int NumTurns = 5; //错误,企图在类中初始化const数据成员
int scores[NumTurns]; //使用该常量
};
const数据成员初始化只能在类构造函数的初始化成员列表中进行。
5.也可以在class类中只声明,不赋初值,在定义式中赋初值:
class GamePlayer{
private:
static const int NumTurns; //
//int scores[NumTurns]; //使用该常量
};
const int GamePlayer::NumTurns = 5;
int main()
{
GamePlayer g;
return EXIT_SUCCESS;
}
此时,就不能再在类中使用NumTurns来设置数组大小了。
6.一个属于枚举类型的数值可权充ints被使用。
class GamePlayer{
private:
enum{NumTurns=5}; //令NumTurns成为5的记号
int scores[NumTurns]; //使用该常量
};
取一个const的地址是合法的,但取一个enum或#define的地址通常不合法。
如果不想别人获得一个pointer或reference指向你的某个整数常量,enum可以实现这个约束。
7.#define的另一个误用情况是以它实现宏,宏看起来像函数,但不会招致函数调用带来的额外开销。
#define CALL_WITH_MAX(a,b) f((a)>(b)?(a):(b))
//以a和b的较大值调用f
如果做下面的调用,就会出问题:
int a=5,b=0;
CALL_WITH_MAX(++a,b); //a被累加二次
CALL_WITH_MAX(++a,b+10); //a被累加一次
上面这种问题,用inline函数就可以解决,也不会招致函数调用带来的额外开销。
条款03:
1.关键字const出现在星号左边,表示被指物是常量;如果出现在星号右边,表示指针自身是常量;如果出现在两边,表示被指物和指针两者都是常量。
下面两种写法等价:
void f1(const Widget* pw); //指针指向对象为const
void f1(Widget const* pw);
2.声明STL迭代器为const就像声明指针为const一样,表示这个迭代器不得指向不同的东西,不能自增和自减,但是其所指的对象可以改变。
如果希望迭代器所指的东西不可被改动,需要的是const_iterator:
const std::vector<int>::iterator iter=vec.begin(); //相当于T* const
std::vector<int>::const_iterator cIter=vec.begin(); //相当于const T*
3.两个成员函数如果只是常量性不同(一个有const,一个没有),则可以被重载。
#include<iostream>
#include <string>
using namespace std;
class TextBlock{
public:
TextBlock(std::string _text) :text(_text){}
const char& operator[](std::size_t position) const //const
{
return text[position];
}
char& operator[](std::size_t position) //non-const
{
return text[position];
}
private:
std::string text;
};
int main()
{
TextBlock tb("Hello");
std::cout << tb[0]; //调用non-const
const TextBlock ctb("World");
std::cout << ctb[0]; //调用const
return EXIT_SUCCESS;
}
4.有const修饰的成员函数,只能读取数据或成员,不能改变数据成员(non-static);没有const修饰的成员函数,对数据成员则是可读可写的。
const对象只能调用const成员函数,非const对象可以调用const成员函数和非const成员函数。
上面
char& operator[](std::size_t position) //non-const
返回的是reference to char,所以可以执行:tb[0]='x';
如果返回的只是一个char,则上面的句子就无法通过编译。因为如果函数的返回类型是个内置类型,那么改动函数返回值从来就不合法。即使合法,C++以by value返回对象意味着被改动的其实是tb.tetx[0]的一个副本,不是其本身。
5.如果有成员变量是指针,其所指物不属于对象,也就是说,const成员函数可以修改该指针所指物。#include<iostream>
#include <string>
using namespace std;
class CTextBlock{
public:
CTextBlock(char* _text) :pText(_text){}
char& operator[](std::size_t position) const
{
return pText[position];
}
private:
char* pText;
};
int main()
{
const CTextBlock cctb("Hello");
char *pc = &cctb[0];
*pc = 'J';
cout << cctb[0] << endl;
return EXIT_SUCCESS;
}
上面这段程序照理说是可以改变值的,可是运行到8pc='J'那句是会出现写内存错误。
6.mutable释放掉non-static成员变量的bitwise constness约束:成员函数只有在不更改对象之任何成员变量(static除外)时才可以说是const。
class CTextBlock{
public:
CTextBlock(char* _text) :pText(_text){}
std::size_t length() const;
private:
char* pText;
mutable std::size_t textLength; //这些成员即使在const函数内也可能被修改
mutable bool lengthIsValid;
};
7.如果有一个const成员函数和一个对应的非const成员函数(如3中operator函数),为了避免重复,可以使用常量性转移:令其中一个调用另外一个。
可以用non-const调用const函数,但是中间需要一个转型动作。
class TextBlock{
public:
TextBlock(std::string _text) :text(_text){}
const char& operator[](std::size_t position) const //const
{
return text[position];
}
char& operator[](std::size_t position) //non-const
{
return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
}
private:
std::string text;
};
其中const TextBlock指明调用的是const operator[],否则调用operator[],会递归调用自己。这是第一次转型(安全转型,使用static_cast)。
第二次是从const operator[]的返回值中移除const(只能由const_cast完成)。
不能反过来:在const成员函数内调用非const函数,这样曾经承诺不改变的那个对象被改动了。
条款04:
1.使用初始化成员列表完成初始化。
2.当在成员初值列中条列各个成员时,最好总是以其声明次序为次序。
3.编译单元:当一个C或CPP文件在编译时,预处理器首先递归包含头文件,形成一个含有所有信息的单个源文件,这个源文件就是一个编译单元。
4.问题:至少含有两个源码文件,每一个内含至少一个non-local static对象,如果某编译单元内的某个non-local static对象的初始化动作使用了另一编译单元内的某个non-local static对象,它所用到的这个对象可能尚未初始化,这样会出问题。
class FileSystem{
public:
std::size_t numDisks() const;
};
extern FileSystem tfs;
现在客户建立一个class用以处理文件系统内的目录:
class Directory{
public:
Directory(params);
};
Directory::Directory(params)
{
std::size_t disks = tfs.numDisks(); //使用tfs对象
}
现在客户决定创建一个Directory对象:
Directory tempDir(params);
初始化次序的重要性出来了:除非tfs在tempDir之前先被初始化,否则tempDir的构造函数会用到未初始化的tfs。
5.用单例模式来解决上面问题:将每个non-local static对象搬到自己的专属函数内(该对象在此函数内被声明为static)。这些函数返回一个reference指向它所含的对象。然后用户调用这些函数,而不直接指涉这些对象。(non-local static对象被local static对象替换了。)
class Filesystem{};
Filesystem& tfs()
{
static Filesystem fs;
return fs;
}
class Directory{};
Directory::Directory(params)
{
std::size_t disks = tfs.numDisks(); //使用tfs对象
}
Directory& tempDir()
{
static Directory td;
return td;
}
6.竞速形式:如果一个函数
void func()
{
static A a;
}
在多线程情况下编译实际会变成类似:
void func()
{
if (第一次进入函数func)
{
初始化a;
注册退出程序时调用a的析构;
标记为非第一次进入;
}
}
但这段代码不是安全的,两个线程可能同时进入导致都认为是第一次进入。
7.函数内含static对象的事实使他们在多线程系统中带有不确定性。处理这个麻烦的一种做法是:在程序的单线程启动阶段手工调用所有reference-returnning函数,这可消除与初始化有关的竞速形式(在单线程下初始化,就不会同时有两个线程进入且都认为自己是第一次进入了)。