1.当我们看到赋值符号时,请小心,因为"="也可以用来调用copy构造函数
Widget w3 = w2; //调用copy构造函数,而不是copy赋值操作符
2.不明确的行为:
int *p = 0; // p是个null指针
cout<<*p; // 对一个null指针取值是不明确的行为
3.常量定义式通常被放在头文件中,以便被不同的源码含入。
有两种特殊情况需要注意:
(1)定义常量指针
有必要将指针(而不是指针所指之物)声明为const。 const char* const authorName = "Bob";
(2)class的专属常量
为了将常量的作用域限制于class内,你必须让它成为class的一个成员;而为确保此常量最多只有一份实体,你必须让它成为一个static成员。
class GamePlayer{
private:
static const int NumTurns = 5; // 常量声明式
int scores[NumTurns]; // 使用该常量
}
注意:我们看到的NumTurns是声明式而非定义式。 通常C++要求你对你所使用的任何东西提供一个定义式,但如果它是个class专属常量又是static且为整数类型(如int,char,bool),则需特别处理。
只要不取它们的地址,你就可以使用它们而无需定义式。
如果我们要取某个class专属常量的地址,你就必须提供如下定义式:
const int GamePlayer::NumTurns; // 把各个式子放到实现文件而非头文件。由于class常量声明时就获得初值,所以定义时不需要再设初值。
4. the enum hack补偿
如果你的编译器不允许“static整数型class常量”完成类内初值设定,
class GamePlayer{
private:
static const int NumTurns = 5; // 常量声明式,但有的编译器不允许此种类型在类内设置初值
int scores[NumTurns]; // 使用该常量
}
那么可以这样:
class GamePlayer{
private:
enum { NumTurns = 5 }; // "the enum hack"令NumTurns成为5的一个记号名称。
int scores[NumTurns];
}
5.对于形似函数的宏,最好改用inline函数代替#define
因为预处理定义出来的函数,经常逻辑混乱,容易出错。
6.const面对函数声明时的应用。在一个函数声明式内,const可以和函数返回值、各参数、函数自身产生关联。
(1)令函数的返回值为cosnt 考虑如下:
class Rational {...};
const Rational operator*(const Rational &lhs,const Rational &rhs);
为什么返回一个const对象呢? 原因是为了防止这样一些暴行:
Rational a,b,c;
(a*b) = c; // 客户可能是想写(a*b) == c 进行比较行为,但手误写成了一个赋值。这种动作是无意义的。
所以,我们声明函数的返回值为const,预防了这种无意义的行为。
(2)令函数本身为const(即const成员函数)
const成员函数之所以如此重要是因为:
第一,它使得class接口更加清晰:我们很容易知道哪个函数可以改动对象内容而哪个函数不行。
第二,因为const对象只能调用const成员函数。所以,它使得操作const对象成为可能。
mutable的使用。有时const成员函数需要改变对象的某一些数据,但这又违反了const成员函数的规则,通过将这些数据声明为mutable改善这种情况。
当const和non-const的版本有着等价的实现时,用const版本来实现non-const版本来避免代码重复。 如:
class TextBlock{
public:
...
const char& operator[](size_t position) const
{
...
return text[position];
}
char& operator[](size_t position)
{
return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
}
}
7.确定对象被使用前已被初始化
我们知道,使用了未被初始化的对象,将会导致未定义的行为。
最好的办法是使用初始值列表进行初始化,这比在构造函数体中的赋值行为更为高效,而且避免了对象的成员是const或引用引发的问题。
(因为成员变量是const或reference一定需要初值而不能被赋值。)
有时候,对象的数据成员太多了,放在初始值列表中太长,我们可以将这些成员中的内置类型的成员的初始化放在一个函数中(通常是private的),供所有构造函数调用。
C++有十分固定的初始化顺序,基类,然后派生类。而成员也是按照声明的顺序初始化的,与它们在初始值列表的顺序无关。
尽管如此,还是一个地方需要注意:当使用一个编译单元中的外部变量来初始化另一个编译单元的变量时,它用到的这个变量可能未被初始化。
因为C++对于定义于不同编译单元的对象的初始化次序并无明确定义。
eg:
//FileSystem.cpp
class FileSystem{
....
size_t numDisks() const;
...
};
extern FileSystem tfs;
//Directory.cpp
class Directory{
public:
Directory( params );
...
};
Directory::Directory(params)
{
...
size_t disks = tfs.numDisks(); // 使用tfs对象
...
}
Directory tempDir(params); //创建一个对象
这时,除非tfs在tempDir之前先被初始化,否则tempDir的构造函数将用到未初始化的tfs
有一种类似单例模式的方法来解决这个问题:
class FileSystem{...};
FileSystem &tfs() // 将这个对象用类似这种的函数代替。由于函数体很简单,非常适合定义为inline函数
{
static FileSystem fs;
return fs;
}
class Directory{...};
Directory::Directory(params)
{
...
size_t disks = tfs().numDisks();
...
}
原理是:函数内的static对象会在该函数调用期间,首次遇到该对象的定义式时被初始化。(static对象会在首次用到它的地方进行唯一一次的初始化)
这样,我们就保证了返回的引用永远是一个已经初始化了的对象。
8.当我们的类没有显式定义copy构造函数、copy赋值运算符、析构函数时,编译器会自动为我们定义。
但并不总是这样。考虑如下:
class NamedObject{
public:
NamedObject(string &name,const int &value); // 构造函数
... //未声明operator=
private:
string &nameValue; // 引用
const int objectValue; // const对象
}
string newDog("Bob");
string oldDog("Tony");
NamedObject p(newDog,2);
NamedObject s(oldDog,36);
p = s; // 发生什么?
这段代码将编译错误,我们将s赋值给p,由于C++不允许让引用指向不同的对象,所以,编译器不知道生成什么样的operator=来完成这个赋值,报错。
同样,const的对象也不允许指向其他对象。
如果你打算在一个包含引用、const成员的类内支持赋值操作,就必须自己定义copy赋值运算符,而不能指望编译器生成。
还有一种情况是,如果基类将copy赋值运算符声明为private,则编译器拒绝为其派生类生成一个copy赋值运算符。
因为派生类生成的copy赋值运算符想象中可以处理基类中的成员,但基类中的copy赋值运算符是私有的,它们无法调用。
9.我们知道,通过把copy构造函数、copy赋值运算符声明(而不定义)为private,可以阻止拷贝动作,包括成员函数和友元函数也不行。
如果试图拷贝将会导致运行时的链接错误(linkage error)。
我们可以将链接期错误移至编译期:通过将copy构造函数、copy赋值运算符在一个专门为了阻止拷贝动作的基类内声明(而不定义)为private来完成。
如:
class Uncopyable{
protected:
Uncopyable(){}
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
为了阻止Foo对象被拷贝,我们唯一要做的是继承Uncopyable:
class Foo : private Uncopyable{
....
};