本章一共有四个条款:
- 视c++为一个语言联邦
- 尽量以const,enum,inline替换#define
- 尽可能使用const
- 确定对象被使用前已被初始化
视c++为一个语言联邦
这个条款介绍c++总共由四个次语言部分组成,分别为:
-
C part of c++:这一点表示c++是以c为基础的,其中的区块(blocks)、语句(statements)、数组(arrays)和指针(pointers)都来自c。注意,c语言部分和专属于c++部分的初始化方式不一样。
-
Object-oriented c++: 这一部分即面向对象部分,包括封装、继承、多态和虚函数等等。
-
Template C++ :这是c++的泛型编程部分。
-
STL
尽量以const, enum, inline替换#define
本条款也可称之为“宁以编译器替换预处理器”。#define与前三者的差别在于,它不被视为语言的一部分,在编译器处理源码之前就由预处理器来处理了,其所定义的名称不记入记号表。
- 对于单纯常量,最好以const对象或enums替换#define
- 对于形似函数的宏,最好改用inline函数替换#define
尽可能使用const
对于关键字const,考虑其对指针的修饰:
const char* p=greeting; //(1)
char const* p=greeting; //(2)
char* const p=greeting; //(3)
如果const出现在星号左边表示*p所指的字符串是常量,即不能对*p重新赋值,如(1)(2)式所示,且(1)(2)式的意义完全一样。如果const出现在星号右边表示指针p是常指针,即不能对p重新赋值使其指向其他的字符串。值得注意的是,在STL中迭代器是以指针为根据塑模出来的,所以迭代器的作用就像一个指针,声明迭代器为const与声明指针为const的含义一样,如果希望迭代器所指的东西为const则需要定义const_iterator,这里非常容易混淆!
关于const关键词有一个很重要的概念,即const成员函数,并引出另外两个流行概念:bitwise const和logical constness.,其中bitwise const流派主张const成员函数不可以更改对象内任何non-static成员变量,考虑下述代码:
class CTextBlock
{
public:
std::size_t length() const; //(1)式
private:
char* pText; //(2)式
std::size_t textLength; //(3)式
bool lengthIsValid; //(4)式
};
std::size_t CTextBlock::length() const
{
if (!lengthIsValid)
{
textLength = std::strlen(pText);
lengthIsValid = true;
}
char* temp= "Hello";
pText =temp; //(5)式
*pText = *temp; //(6)式
return textLength;
}
其中(1)式的length()被声明为const成员函数,但是在函数的定义中对象的成员变量都被进行了重新赋值,所以上述代码无法通过编译。值得注意的是,(6)式改变了对象的某些bits,因为修改了指针指向的字符串,但是不幸的是它却能通过bitwise const的测试,事实上这就是所谓的logical constness。在有些时候,即使我们声明了const成员函数,我们也希望某些变量可以被重新赋值,可以通过关键字mutable释放掉bitwise constness约束。
class CTextBlock
{
public:
std::size_t length() const; //(1)式
private:
char* pText; //(2)式
mutable std::size_t textLength; //(3)式
mutable bool lengthIsValid; //(4)式
};
std::size_t CTextBlock::length() const
{
if (!lengthIsValid)
{
textLength = std::strlen(pText);
lengthIsValid = true;
}
return textLength;
}
上述代码就可以正常通过编译。
在某些时候,我们可能在定义了一个const成员函数的同时也需要定义一个对应的non-const的成员函数,两者的功能代码可能会有大部分是重复的。为了避免重复代码,可以通过转型动作使得non-const成员函数调用const成员函数,注意不能反过来操作,因为会破坏const成员函数的bitwise constness约束。
class CTextBlock
{
public:
const char& operator[](std::size_t position) const
{
...
...
...
return text[position];
}
char& operator[](std::size_t position)
{
return
const_cast<char&>(static_cast<const CTextBlock&>(*this))[position]; //(1)式
}
private:
char* text;
};
其中(1)式经过了两次强制转型动作。
确定对象被使用前已先被初始化
前面我们说过在c++的c语言部分和非c语言部分的初始化规则并不是相同的,在C part of C++中,如果初始化可能招致运行期成本那么就不保证发生初始化,一旦进入non-C parts of C++,规则就有些变化。这就是为什么array不保证其内容被初始化,而vector确有此保证。针对这种情况,一个保险的情况是:永远在使用对象之前先将它初始化。对于内置类型之外的任何其他东西,初始化责任落在构造函数身上,这里需要注意的是别混淆赋值和初始化的区别。
class ABEntry
{
public:
ABEntry(const std::string& name);
private:
std::string theName;
};
ABEntry::ABEntry(const std::string& name) :theName(name) //(成员初值列,初始化)
{
//theName = name; //(赋值操作)
}
上述代码展示了赋值操作与初始化的区别,c++规定,对象的成员变量的初始化动作发生在进入构造函数本体之前,所以在本例中,如果成员变量theName是以赋值的方式进行“初始化”,那么他实际上进行的是:先调用theName自身的默认构造函数为其设初值,再用name给它赋予新值。而如果直接使用成员初值列的方式对其进行初始化执行的操作是:利用name对theName进行拷贝构造。后一种方法比前一种方法高效很多,所以建议尽可能使用成员初值列的方式进行初始化,值得注意的是:成员变量的初始化顺序只与其声明顺序有关,而与其成员初值列的顺序无关。
所谓编译单元是指产出单一目标文件的那些源码,基本上它是单一源码文件加上其所含入的头文件。如果涉及至少两个源码文件,每一个内含至少一个non-local static对象,且某个non-local static对象的初始化使用了另一个编译单元的某个non-local static对象,它所用的这个对象可能尚未被初始化,因为c++对“定义于不同编译单元内的non-local static对象”的初始化次序并无明确定义。
/*第一个源码文件*/
class FileSystem
{
public:
std::size_t numDisks() const;
};
extern FileSystem tfs;
/*第二个源码文件*/
class Directory
{
public:
Directory();
};
Directory::Directory()
{
std::size_t disks = tfs.numDisks();
}
/*创建一个Directory对象*/
Directory tempDir(); //(1)式
上述代码中的(1)式,除非tfs在tempDir之前先被初始化,否则tempDir的构造函数会用到尚未初始化的tfs,但实际上这是无法保证的。解决这个问题的办法是:将每个non-local static对象 搬到自己的专属函数内,这些函数返回一个reference指向它所含的对象,然后用户调用这些函数。
/*第一个源码文件*/
class FileSystem
{
public:
std::size_t numDisks() const;
};
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}
/*第二个源码文件*/
class Directory
{
public:
Directory();
};
Directory::Directory()
{
std::size_t disks = tfs().numDisks();
}
Directory& tempDir()
{
static Directory td;
return td;
}
如上进行修改,调用的方式由直接使用tfs和tempDir改为tfs()和tempDir()。
网友评论