条款01 视C++为一个语言联邦
- C++是多元的,使用时以方便为主。
C++最早只是简单的C与对象的结合(C with classes),随着语言的发展,C++已经是一个多重范型语言,一个支持:
- 过程(procedural)
- 面向对象(object-oriented)
- 函数式(functional)
- 泛型(generic)
- 元编程(metaprogramming)
C++的语言风格:
- C
- Object-oriented C++
- Template C++
- STL
多重范式语言(multiparadigm programming language)是一个编程术语,指的是能够支持多种语言编程范式的编程语言。编程范式是一种编程的风格或范式。
根据使用最为方便原则,风格之间会混合使用。如在往函数传递一个对象,若该对象是一个内置类型,使用C风格传值即可;若参数是对象,那么Object-oriented C++传递引用的特性就比较合适;有时候我们甚至不确定处理对象的类型,那么则使用Template模板抽象化参数类型;STL在使用过程中,迭代器、函数对象也应用到了C传值风格。总而言之,C++写程序往往不会是单一的范式,一切以方便为主。
条款02 尽量以const,enum,inline替换#define
- 对于单纯的常量最好以const或enums代替#define
- 形似函数的宏,最好改用inline函数
- 相对于#define,const更好。原因如下:
- 浮点数而言,目标码可能更小[1]
- 更容易发现错误(进入记号表)
- 有作用域范围
- 为什么要用enums?
作者在写这个条款的时候,编译器不都完全支持类内的static
变量,如果不支持,
作者是想说其实就是类的静态变量,现在的C++基本不会有不支持static成员变量的了。
- 为什么用inline函数代替#define定义的函数?
为了防止降低#define容易忘记小括号导致的错误,除此inline是个函数,遵循访问规则和作用域约束。
条款03 尽可能使用const
const这个东西在C++中很多地方都有用到,作用非常广泛:
- classes外部修饰global或者namespace作用域的常量
- 文件、函数和块作用声明static的对象
- 指针常量、常量指针和引用常量
3.1 const和指向性类型讨论
const出现的位置决定了指向性对象的top-level和low-level属性,top-level表示本身是常量,low-level则指向的对象是常量。指针可选择top-level和low-level中的一个或全选,引用因为初始化绑定后就不能改变,因此可选属性只有一个low-level(top-level已经默认设置)。
在声明中,const可以和函数返回值、参数和成员函数本身起作用,避免一些无意义的情况。const作为返回值目的是防止类似以下的情况:
class Ratioanl{...}
const Rational operator*(const Rational &lhs,const Rational &rhs);
int main()
{
Rational a,b,c;
(a*b)=c;//a*b返回值是一个局部变量,进行赋值,整个式子没有意义,用const可以避免,
if(a*b=c){...}//a*b返回值赋值运算始终为真,可以通过const避免
}
3.2 const成员函数讨论
const成员函数是必要的,接口更加清晰,是提升C++效率的重要手段;const成员函数是保证不更改数据成员的声明;const成员函数分为两大阵营bitwise和logic,后者通过关键字mutable实现bitwise检查豁免,最后作者介绍了常量性移除用于优化、简化代码。
const成员函数很重要,基于两个理由:
- 接口更加清晰
- 使操作const对象成为可能
void A::fun(int i)const;
尽管const A a
实例是一个常量,但是我们仍能对其进行操作,在保证不修改常量实例下进行操作,作者说,改善C++程序效率的根本办法是pass by reference-to-const,如果不能操作const对象,改善无从谈起。作者还提到,const函数和非const函数是一对重载声明:
void fun(int i)const ;
void fun(int i);
const 成员函数的两个阵营:bitwise constness(physical constness)和logic constness,这两个阵营其实争论点在于如何去定义一个const对象,是一个数据成员改变就是nont-const(bitwise constness),还是根据用于具体逻辑,改变一个两个都可以视作const(logic constness)。
默认情况C++将使用bitwise constness进行常量属性检查,但不是所有情况都工作良好,看下面这个例子:
class CTextBlock{
public:
char &operator[](std::size_t position) const;
{
return pText[position];
}
private:
char * pText;
}
bitwise constness失效情况。编译器根据bitwise constness默认规则检查后发现下标运算符实现中没有改变数据成员pText的值,编译顺利通过,但是其返回了一个左值char &,如果用户做如下操作:
const CTextBlock cctb("hello");
char *pc=&cctb[0];
*pc="j";
虽然用户本意是创建一个常量的hello,但是暴露出的引用却改变了他的常量属性。
在有些应用,我们希望打破这种bitwise检查:
class A
{
public:
void doSomething()const
{
m_i=555;//error,bitwise检查失败。
m_j=564;//ok,logic检查,特别通行证
}
private:
int m_i=4396;
mutable int m_j=567;
}
有时候我们既要const成员函数,也要non-const成员函数,但是其中内容相差不多,代码较为冗余。作者提供了一种解决方法:常量性移除(casting away constness)。
class TextBlock
{
public:
const char& operator[](std::size_t position) const{
return text[postion];
}
char &operator[](std::size_t postion)
{
return
const_cast<char &> //强制转换成const char&
(static_cast<const TextBlock&>(*this) //强制转换成const,以便复用const char &operator
[position];
)
}
}
这里利用了两次强制转换:
- 第一次,为*this添加const属性以便调用const成员函数
- 第二次,为const operator[ ]移除const,还原none-const属性
注意,是在一般函数调用const函数,不是const函数调用一般函数!后者破坏了const的语义。
条款04 确定对象被使用前已先被初始化
4.1 区分初始化和赋值
一个对象要么执行默认初始化,要么执行值初始化。内置类型有初值则执行值初始化;没有初值执行默认初始化,默认初始化行为与作用域相关,要么初始化为0,要么不执行初始化。
默认初始化结果 | 作用域 |
---|---|
不初始化 | 非static和全局作用域 |
初始化为0 | static和全局作用域 |
推荐使用类内初值或初始化列表进行初始化,而不是先构造再进行赋值;有些情况不得不采用前者,这是因为以下情况:
- const成员
- 引用对象
- 数据成员未提供默认构造
列表初始化或者类内初值发生时间早于构造函数。另外,列表初始化在某个情况比类内初值更加灵活,例如一个类中定义了多个构造函数,每个构造函数的初值都有所不同,类内初值都是相同的初值,显然不合适。
4.2 注意初始化顺序
初始化列表只说明数据成员的值,并不保证初始化顺序。看看下面这个例子:
class X
{
int i;
int j;
public:
X(int val):j(val),i(j){}
}
正如一开始说到的,列表初始化表示的是j的值是val,i的值是j,谁先谁后并不保证,因此可能出现j尚未被val初始化就被用于i赋值,这样的i值是没有任何意义的。C++ primer建议,如果可能尽量避免使用某些数据成员来初始化其他成员。
C++对于定义于不同编译单元内的non-local-static对象的初始化顺序是不确定的。static对象其生存周期是从构造到程序结束,它可以分为local-static和non-local-static两种,local-static只有一种情况,那就是位于函数作用域中的;其他的,如global、namespace、class和file作用域都是属于non-local-static,static对象只在main()程序结束后调用。当一个non-static对象(以global为例),在一个文件中被初始化,在另一个文件中被使用,C++无法为我们保证static在使用之时被初始化,所以可能出现一些意想不到的现象。为了解决non-local-static对象这种不确定性,我们可以通过定义一个函数返回一个local-static对象的指针或者引用,因为local-static会在第一次调用这个函数进行初始化,而不是在运行前就进行初始化,从而避免了因为调用顺序而导致的问题,这其实是单例模式的一种常见实现,但是在多线程中可能会出现竞争问题,如何解决?通过在单线程启动阶段手动调用所有的reference-returning函数。
[1] https://www.zhihu.com/question/424561003