1 视C++为一个语言联邦
View C++ as a federation of languages
众所周知,C++是由C语言发展而来的。因此,C++语言需要兼容C语言,而C语言是面向过程语言,相比其它面向对象语言C++还继承了C语言的缺陷。
C++语言虽然存在缺陷,但是它仍然是面向对象语言,受到其它面向对象语言的“激励”,C++语言也在不断变化,C++语言已经不是C with class那个时代了。《Effective C++》一书中说道,C++是一门“联邦式的语言”。现今强大的C++语言大体分以下几个部分:
- C:区块、语句、预处理、内置数据类型等,缺陷是C语言没有模板,没有重载,没有异常。
- Object-Oriented C++:这部分是C with classes 的诉求,继承、多态、封装、virutal函数(动态绑定)
- Template C++:泛型编程部分,高效的程序是适用于多种场景,多种参数类型的程序。template功能很强大,它带来了崭新的编程范型,也就是template metaprogramming(TMP模板元编程)。
- STL:作为C++程序员必须对STL有一定掌握,STL对容器、算法、迭代器以及函数对象的规约有极佳的配合和协调。
- C++高效编程守则视情况而定,需要考虑所写程序对应的部分。
2 尽量以const、enum、inline替换#define
Prefer consts,enum,and inline to #defines
对于宏,我们知道,宏不是函数,它只是简单的替换,表达式替换过程可能因为运算符优先级问题造成不可预料的错误,宏替换发生在预处理阶段。
# define PI 3.14
遇到数学问题的圆周率,C语言时候我们经常会使用上面的宏定义。从这句代码开始,我们都是PI来代替3.14进行计算,后续代码中就会出现多份3.14。而如果我们使用const定义一个常变量,使用常变量来计算,就不会存在冗余的数据。
const float PI=3.14;
在class内部的成员,我们也需要const来定义一个常变量,#define没有作用域限制,一旦宏被定义,他就在其后的编译过程有效。#define不仅不能用来定义class专属常量,也不能提供封装性,const就不会存在这些问题。
class A{
private:
static const float PI;
};
static const float A::PI=3.14;
假如,我们的class专属常量还需要保持一份,是的,我们需要加static,static成员必须在类外进行初始化,这时候采用enum hack就可以实现在类内对常量成员进行初始化。
class A{
private:
enum{PI=3.14};
};
这种状况就不能对enum对象取地址,取地址就会出错。
实际上,使用宏还需要注意是否会存在副作用导致错误。这里是关于宏的总结
- 对于单纯常量,最好以const对象或者enums替换#define
- 对于形似函数的宏,最好改用inline函数替换#define
3 尽可能使用const
Use const whenever possible
- 常指针、常变量、常指针常变量
#include<iostream>
int main()
{
int a = 10;
const int* p1 = &a;//指向常变量的指针p1,不能通过p1改变a的值,p1可以指向其它地方
int* const p2 = &a;//常指针p2,只能指向b
const int* const p3 = &a;//指向常变量的常指针,p3只能指向b,不能通过p3改变b的值
}
const_iteraor
给出const T*
类型的指针,不能通过const_iterator
修改元素内容。声明常量迭代器,T* const
指针,迭代器不能移动(不能++或者–)。
#include<iostream>
#include<string>
using namespace std;
int main()
{
string str({"I love C plus plus..."});
const string::iterator it1 = str.end();//it1只能指向str的尾元素位置,不能移动
string::const_iterator it2 = str.begin();//it2可以指向其它位置,it2具有只读权限
while (it2 != it1)
{
cout << *it2;
it2++;
}
cout << endl;
return 0;
}
- 当函数返回值不希望被赋值时候,可声明为const类型。这样可以避免一些外界意外发生。
#include<iostream>
using namespace std;
class Rational{
public:
const Rational operator*(const Rational& r){}
};
int main()
{
Rational a, b, c;
//(a*b) = c;这种暴行就会报错
return 0;
}
- 两个成员函数,如果只是常量属性不同,可以被重载。
#include<iostream>
#include<string>
using namespace std;
class Text{
public:
char& operator[](size_t pos)//non-const对象
{
return str[pos];
}
const char& operator[](size_t pos)const//const对象专属
{
return str[pos];
}
private:
string str;
};
int main()
{
Text text;
const char c1 = text[0];
char c2 = text[0];
c2 = text[2];
return 0;
}
- bitwise constness和logical constness
- bitwise constness又称physical constness,认为成员函数只有在不更改对象之任何成员变量时才可以说是const,也就是一个bit位都不会被改变。const成员函数不可以更改对象内任何non-static成员变量。所以const成员函数中不能存在
=
,否则就会报错。 - 实例中,在const成员函数中,我们知道
=
不会改变对象内容时候,我们需要将被赋值的变量,给与它mutable属性。这个时候我们所遵循的就是logical constness,即就是逻辑上的常量性。
- non-const调用const版本,减少代码冗余
class Text{
public:
const char& operator[](size_t pos) const //const对象专属,不会改变
{
return str[pos];
}
char& operator[](size_t pos)
{
return const_cast<char&>(static_cast<const Text&>(*this)[pos]);
}
private:
string str;
};
(static_cast<const Text&>(*this)
将this指针转换成const类型,调用[ ]
会调用const成员函数,类型为const char&
,使用const_cast去除const属性。
为何不使用const成员函数调用non-const成员函数? const成员函数承诺自己不会改变对象内容,但是它却使用了non-const成员函数,如果non-const成员函数改变对象内容,那么const成员函数就犯错了。
- 编译器强制实施bitwise constness,编写程序时候应该使用“概念上的常量性”。
- const和non-const成员函数有着实质等价时,令non-const调用const版本。
4 使用对象前确定其已经被初始化
Make sure that objects are initialized before they’re used
C part of C++(STL的array)不保证初始化,non-C of C++(STL的vector)会保证初始化。
因此为避免错误,最好在对象定义时候直接初始化。
- 编译器初始化顺序是按照对象声明顺序初始化的。 栗子:array初始化前需要先初始化size。
- 构造函数最好将所有对象按照声明顺序在初始化列表初始化,防止遗漏。不在构造函数内赋值的原因是,构造函数在进行函数体之前,会执行初始化列表进行初始化,为了提高效率,避免多余操作。
class First_name{};
class Last_name{};
class Person
{
public:
Person()
:first_()
, last_()
, age_(20)
{}
private:
First_name first_;
Last_name last_;
int age_;
};
- 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local对象。
函数内的static对象称为local static对象,作用域仅在函数体内;其它static对象称之为non-local static对象,它们会在main函数结束时候被销毁。
当编译单元存在两个以上函数non-local static对象时候,其中一个non-local static对象初始化依赖另一个non-local static对象时候,由于C++对“定义于不同的编译单元内的non-local static 对象”的初始化相对次序并无明确定义。就可以能存在错误,避免错误的方法就是使用local static代替non-local static对象,典型例子:Singleton模式。
- 对内置对象进行手动初始化,因为C++不保证初始化它们。
- 构造函数最好使用初始化列表按照声明顺序初始化,而不是构造函数内赋值。
- 为免除“跨编译单元之初始化次序”问题,请以local static对象替换non-local对象。