01.accustoming-yourself-to-cpp
原书的第一部分,主要是对#define
的方法等比较基础的知识点。
item1 视C++ 为一个语言集合
不同于
- C:
C++ 是以C 语言为基础的,C++ 的语法规则、预处理器、内置数据类型、数组、指针等都是来自于C ,许多C++ 的语法就是较高级的C 。 - Object-Oriented C++:也就是很多时候提到的
C With Classes ,其中包含了C++ 面向对象的机制:类,以及封装、继承、多态、虚函数等类的支持机制。 - Template C++:泛型编程模块。
- STL:
STL 是一个封装了容器、迭代器、算法以及函数对象的template 库。
为什么要区分
- 在
C 语言中,一般使用值传递(pass-by-value) ; - 在
OOC 和模板C++ 中,使用常量引用(pass-by-reference-const)传递更加高效; - 对于
STL ,因为迭代器是基于指针构造而成,直接使用值传递即可。
item2 用const,enum,inline 代替#define
换言之,以编译器替换预处理器。预处理宏会在编译器之前被简单的替换成代码,经常会发生意料之外的错误,这种错误往往难以跟踪到。因此,除了一些特殊的指令外,如#include
,应该尽量避免使用预处理宏。解决之道是对#define
进行替换。
2.1 const 替换#define 定义的常量
- 数值常量:对于数值类型的常量,可以通过定义一个全局常量来对
#define
进行替换,如const double A=3.141592654;
替换#define A 3.141592654
。 - 常量指针:由于常量定义通常被放在头文件中以便被不同的源码包含,因此需要把指针以及指针指向的数据都定义位
const
,如const char* const author="Scotter Meyers";
。 - 类的常量:对于类中的常量成员,为了保证其能被类的对象访问到,又不会生成多个拷贝,需要将其声明为静态常量,即
class Player{
private:
static const int numTurns=5;//静态成员变量,属于类不会在实例化过程中产生拷贝;const成员,不能进行赋值操作
}
2.2 enum 使用技巧
当我们要在一个类中声明一个常量,这个常量不允许在在声明时进行初始化,而接下来某个语句明确要用到这个变量,比如说静态数组的声明,如:
class Player{
private:
static const int numTurns;//一些编译器不允许static成员在声明时赋值,需要在类外进行定义
int scores[numTurns]; // 编译无法通过,在编译过程中必须要指定数组大小
}
面对上面个的问题,我们可以选择使用
class Player{
private:
enum {NumTurns=5}; // 因为枚举类型的数值可以充当int使用,
int scores[NumTurns]; // 所以,编译通过。
}
2.3 inline 函数
对于一些简单又需要反复调用的程序语句,将其封装成函数是非常不划算的,因为调用函数的开销甚至超过了函数内代码运行的开销。#define
实现类似于函数的宏定义,好处是可以减少简单函数调用造成额外开销,但是其代码非常不雅观,而且会导致一些未知的错误。
Note:总结
- 对于单纯的常量,最好用
const
对象或enum
对象替换#define
- 对于形似函数的宏,最好用
inline
函数替换#define
item3 多用const
3.1 const 与指针
- 常量指针:指向常量的指针,
const 在*
左边 - 指针常量:指针类型的常量,
const 在*
右边
const char* p; // 数据是常量
char const* p; // 数据是常量
char* const p; // 指针是常量
const char* const p; // 指针和数据都是常量
3.2 const 与STL 迭代器
T*
。因此,如果想定义一个迭代器指向一个常数,需要使用
std::vector<int> vec;
std::vector<int>::const_iterator const_itr = vec.begin(); // const_itr类似于const T*,指向一个常量
++const_itr; // 迭代器可变
*const_itr = 10; // 错误,*const_itr不可变
const std::vector<int>::iterator itr = vec.begin(); // iter类似于T* const
++itr; // 错误,指针是一个常量
*itr = 10; // 改变指向的值
3.3 const 与函数
class Rational{...};
Rational operator*(const Rational& lhs, const Rational& rhs){...};
在某处使用乘法操作符时,误把比较操作符===
写成了赋值操作符=
,如
Rational a,b,c;
if((a*b)=c){...} // 编译器不会报错,很难追踪错误
解决办法是将操作符定义为返回
const Rational operator*(const Rational& lhs, const Rational& rhs){...};
3.4 const 与类的成员函数
用
- 一是可以直观的告诉用户函数是否会改变成员变量;
- 二是用
const 修饰的对象只能调用const 修饰的成员函数。
3.5 数据常量性和逻辑常量性
class CTextBlock
{
public:
char& operator[](std::size_t pos) const
{
return pText[pos];
}
private:
char* pText;
}
只有指针属于对象,指针所指向的数据不属于对象,operator[]
中并没有赋值操作符,编译器会通过编译,但是其存在着潜在风险。如:
const CTextBlock ctb("Hello"); // 声明一个常量对象
char* pc = &ctb[0]; // 调用operator[]取得一个指针,指向ctb的数据
*pc = "J"; // 数据被修改为"Jello"
数据常量性还存在着另一个局限性,如:
class CTextBlock
{
public:
std::size_t length() const
{
if(!lengthIsValid)
{
textLength = std::strlen(pText); // 有赋值操作,编译会发生错误,但事实上这种改变是允许且必要的
lengthIsValid = true;
}
return textLength;
}
private:
char* pText;
std::size_t textLength;
bool lengthIsValid;
}
解决办法是逻辑常量性,即使用mutable
关键字来修饰成员变量,允许数据被修改,但是这些修改不反映到类外。
class CTextBlock
{
public:
std::size_t length() const
{
if(!lengthIsValid)
{
textLength = std::strlen(pText); // 编译可以通过
lengthIsValid = true;
}
return textLength;
}
private:
char* pText;
mutable std::size_t textLength;
mutable bool lengthIsValid;
}
3.6 在const 和non-const 成员函数中避免重复
在
class TextBlock
{
public:
const char& operator[](std::size_t pos) const
{
// ....
return pText[ pos ];
}
char& operator[](std::size_t pos)
{
return
const_cast<char&>( // 将[]的const移除,转换成char&
static_cast<const TextBlock&>(*this) // 为*this加上const,从而可以调用const操作
[ position ]
);
}
private:
char* pText;
}
Note:总结
- 变量,指针,迭代器以及函数都可以通过
const 的修饰来实现只读的目的; - 编译器强制使用的是数据常量性,但是编写程序的时候应该采用逻辑常量性,对需要修改的成员变量加上
mutalbe
关键字修饰; const 和non-const 成员函数有着大量重复的实现,可以使用non-const 函数来调用const 函数来避免重复。
item4 确保对象在使用前已被初始化
4.1 内置数据类型的初始化
4.2 类的初始化
-
对于用户自定义的类,需要构造函数来完成类的初始化,需要保证构造函数将对象的每一个成员初始化;
-
C++ 规定,对象的成员变量的初始化动作发生在进入构造函数体之前。因此,在构造函数体内对成员变量进行的是赋值操作,其先调用默认构造函数对成员函数进行初始化,然后对它们赋予新值; -
较优的办法是使用初始化列表来对成员变量进行初始化,从而不再调用默认构造函数,直接进行赋值操作;
-
C++ 有着固定的初始化顺序,基类先于派生类被初始化,成员变量是按照其声明的顺序进行初始化的 -
类的初始化:构造函数与初始化列表
-
引用和常量的初始化
-
初始化顺序:继承关系中,先父类后子类;同一个类中,只与成员变量的声明顺序有关
-
非局部静态对象的初始化
4.3 non-local static 对象的初始化
在不同的源码文件中,分别包含了至少一个
Note:
/* demo1.cpp */
class FileSyste
{
public:
// ...
std::size_t numDisks() const;
};
extern FileSystem fs;// 在全局范围声明一个对象fs,供其他单元调用
/* demo2.cpp */
class Directory
{
public:
Directory(param)
{
std::size_t disks = fs.numDisks(); // 调用了fs对象
}
}
在上述代码中,如果我们要创建一个Directory
对象,构造函数就会调用fs
对象。但是两个对象是在不同的源文件不同的时间建立起来的,无法保证
/* demo1.cpp */
class FileSyste
{
public:
// ...
std::size_t numDisks() const;
}
FileSystem& fs()
{
static FileSystem fs; // 声明了一个局部静态变量
return fs;
}
/* demo2.cpp */
class Directory
{
public:
Directory(param)
{
std::size_t disks = fs().numDisks(); // 调用了fs对象
}
}
Directory& dir()
{
static Directory dir;
return dir;
}
Note:总结
- 对于内置的数据类型,要进行手动初始化;
- 构造函数对类进行初始化最好使用初始化列表 来替换在构造函数中使用赋值操作 。构造函数按照变量声明的顺序进行初始化;
- 对于静态对象,用局部静态对象来替换全局静态对象来保证使用前确定被初始化。