条款一:视C++为一个语言联邦
C++可以视为由相关次语言组成的联邦,每个次语言都有自己的规约。
大致分为四个:
C:C++仍然是以C为基础。区块,语句,预处理,内置数据类型,数组,指针都是来自C
Object-Oriented C++:面向对象设计。封装,继承,多态,虚函数,析构构造。
Template C++:泛型编程。templates威力强大,可以带来崭新的编程范型。
STL:标准模板程序库。他对容器,迭代器,算法以及函数对象的规约有极佳的紧密配合与协调。
请记住:C++高效编程守则视状况而改变,取决于你使用c++的哪一部分
条款二:尽量以const,enum,inline替换#define
因为#define不能被视为语言,可能会导致以下问题:
比如有#define PI 3.14
1.PI也许从未被编译器看见;也许在编译器开始处理源码之前它就被预处理器移走了。于是PI有可能没进入符号表(symbol table)
[symbol table:通常,C 或 C++ 编译器将单个源文件编译为扩展名为 .obj 或 .o 的目标文件。目标文件中有一个称为符号表的数据结构,它将目标文件中的不同项目映射到链接器可以理解的名称。]
2.用#dfine会产生大量的码,因为预处理器“盲目地将宏名称PI替换为3.14”导致目标码出现多份3.14. 可以通过使用常量解决这个问题[const double PI=3.14;]
3.#define 无法构建class的专属常量,也不提供任何封装性,因为#define不重视作用域(scope)。
[
你可以使用enum hack技术实现在类中定义匿名枚举,达到常量的作用。
如:
class GamePlayer
{
private:
enum { NUM = 5 };
int socres[NUM];
};
enum hack的行为比较像#define而不像const,因为不能对enum取地址。这样可以不让别人获得一个指针或引用指向你的某个整数常量。
]
4.使用#define实现函数,这样不会招致函数调用的额外开销,但是会有很多问题。
比如如下函数:
#include <iostream>
using namespace std;
template<class T>
void f(T x)
{
cout << "max is " << x << endl;
}
#define MAX(a,b) f((a)>(b)?(a):(b));
int main()
{
int a = 5, b = 0;
MAX(++a, b); //max is 7
a = 5, b = 0;
MAX(++a, b + 10);//max is 10
return 0;;
}
调用f之前,a的递增次数竟然取决于”它被拿来和谁比较”。
[
你可以使用inline关键字替代这种宏定义函数,(注意内联函数只是对编译器的建议,实际是否内联需要看编译器心情)
内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。
#include <iostream>
using namespace std;
template<class T>
void f(T x)
{
cout << "max is " << x << endl;
}
template<class T>
void MAX(const T &a,const T &b)
{
f(a > b ? a : b);
}
int main()
{
int a = 5, b = 0;
MAX(++a, b); //max is 6
a = 5, b = 0;
MAX(++a, b + 10);//max is 10
return 0;;
}
]
请记住:对于单纯的常量最好以const对象或enums替换#define
对于形似函数的宏,最好改用inline函数替换#define
条款三:尽可能使用const
const允许你指定语义约束,而编译器会强制实施这项约束。它允许你告诉编译器和其他程序员某值应该保持不变。
1.常量指针&指针常量&&常量指针常量
const出现在左边,表示指针自身是常量,即常量指针。int * const p;
如果出现在右边,表示被指物是常量,即指针常量。int const * p;
如果都出现,则被指物和指针都是常量,即常量指针常量。const int *const
#include <iostream>
using namespace std;
int main()
{
int a = 10;
int b = 20;
int* const p1 = &a;//指针是常量不能更改。指针常量。
*p1 = 10;//正确
p1 = &b;//错误
const int* p2 = &a;//被指物是常量不能更改。常量指针。
*p2 = 10;//错误
p2 = &b;//正确
int const* p3 = &a;//被指物是常量不能更改。常量指针。
*p3 = 10;//错误
p3 = &b;//正确
const int* const p4 = &a;//指针和被指物都是常量不能更改。常量指针常量。
*p4 = 10;//错误
p4 = &b;//错误
return 0;
}
[
个人记忆方法:哪个是哪个就顺着读就行,const * 常量指针/* const指针常量/const * const 常量指针常量
然后用具体哪个是常量的话,
指针常量:指针常量就是指针本身是常量
常量指针:指向常量的指针
]
2.const和函数产生的关联
一,令函数返回一个常量值,可以降低因客户错误而造成的意外。
#include <iostream>
using namespace std;
class A
{
public:
friend A operator*(const A& a, const A& b)
{
A res;
res.x = a.x * b.x;
return res;
}
int x;
};
int main()
{
A a, b, c;
a.x = 2;
b.x = 4;
c = a * b;
cout << c.x << endl;//8
(a * b) = c;//允许
return 0;
}
客户可以实现(a * b) = c;对两个乘积然后赋值的操作这种骚操作。
但如果写成friend const A operator*(const A& a, const A& b)
就能避免这种问题的出现。
二,const成员函数
将const实施于成员函数的目的是,为了确认该成员可以作用于const对象身上。
它可以使得接口比较容易理解。甚至可以使得”操作const对象”成为可能(等下说)
许多人漠视一件实事:两个成员函数只是常量性不同,可以被重载。
这是一个重要的C++特性。
#include <iostream>
using namespace std;
class A
{
public:
A(string res):text(res)
{
}
const char& operator[](int pos)const
{
cout << "call const operator[]" << endl;
return text[pos];
}
char& operator[](int pos)
{
cout << "call non-const operator[]" << endl;
return text[pos];
}
private:
string text;
};
int main()
{
A a("123");
cout << a[0] << endl;//call non-const operator[]
const A a1("456");
cout << a1[0] << endl;//call const operator[]
a[0] = 'c';
return 0;
}
真实程序const对象大多用于通过指针转常量或者通过引用转常量的传递结果。
插播一条关于bitwise const和logical constness的讨论。
bitwise constness:理论上 const成员函数 表明这个函数不会对这个类对象的数据成员(准确地说是非静态数据成员)作任何改变。但是bitwise constness机制产生的漏洞可以使我们改变成员 通过获得静态成员对象的指针,并修改指针内容做到。
logical constness:成员变量加上mutable关键字就能使其在const成员函数内被修改
三,使用non-const成员函数调用const成员函数可以避免代码重复。
假设operator[]不单只返回一个引用指向某字符,而且执行边界检验,日志访问信息,等等代码。这样的话你在两个函数都要写大量重复的代码。
const char& operator[](int pos)const
{
//执行边界检验,日志访问信息。。。
return text[pos];
}
char& operator[](int pos)
{
//执行边界检验,日志访问信息。。。
return text[pos];
}
所以避免重复,我们可以使用non-const调用const的
char& operator[](int pos)
{
return const_cast<char&>(static_cast<const A&>(*this)[pos]);
}
请记住:
将某些东西声明为const可帮助侦探出错误用法。const可被施加于任何作用域内的对象,函数参数,函数返回类型,成员函数本体。
编译器强制实施bitwise constness,但你编写程序时应该使用“概念上的常量性”(conceptual constness)
当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复
条款四:确定对象被使用前已先被初始化
int x;
1.读取未初始化的值会导致不明确的行为。在某些平台上,仅仅只是读取未初始化的值,就可能让你程序终止。所以最佳的处理办法就是:永远在使用对象之前先将它初始化
2.还有别混淆了赋值和初始化。
C++规定对象的成员变量的初始化动作发生在进入构造函数本体之前。所以,尽可能使用成员初值列(member initialization list)替换赋值操作。而且这样效率较高。
class A
{
public:
A(string res):text(res)//初始化
{
}
A(string res)
{
text = res;//赋值
}
private:
string text;
};
3.default构造一个成员变量
class A
{
public:
A():text()//初始化
{
}
private:
string text;
};
如果成员变量在”成员初值列”中没有被指定初值的话,那么编译器会为用户自定义类型的成员变量自动调用default构造函数。
4.如果成员变量是const或reference那么一定需要初值,而且不能被赋值。
5.static对象。
当我们想使用一个静态对象,它却还未构造完成就使用了,则会造成惨重的灾难。但是我们无法确定non-local static对象(全局)的初始化相对次序,因为它无明确定义,解决方案是使用每个non-local static对象搬到自己专属的函数内。这些函数返回一个引用指向它所含的对象。
换句话说non-local static对象变成了local-static对象,这其实就是单例的运用。
这个手法的基础在于:C++保证,函数内的local static对象会在“该函数调用期间””首次遇上该对象的定义式”时被初始化。
请记住:
为内置型对象进行手工初始化,因为C++不保证初始化它们
构造函数最好使用成员初值列,而不要在构造函数体内使用赋值操作。初值列列出的成员变量,其排列次序应该和它们在class中的声明次序相同。
为免除“跨编译单元的初始化次序”问题,请以local-static对象替换non-static对象。