条款01:View C++ as a federation of languages
作者将C++分为四个部分
- C:C++以C为基础,C++完全兼容C,所以在C++的编程过程中部分的写法本质上依然是C,但是C语言具有的局限有没有模板(template)、异常(exceptions)、重载(overloading)
- Object-Oriented C++:包括classes、封装(encapsulation)、继承(inheritance)、多态(polymorphism)、virtual函数等
- Template C++:C++的泛型,使得代码的复用性大大提高
- STL:template程序库,写算法刷题神器。
条款02-03:使用const,enum以及inline
在C语言中,我们一般使用宏定义#define语句来声明某些常量或是函数,但是C++中推荐使用const、enum、inline等来替代#define的使用。事实上,#define语句在使用中有诸多不足,从原理上讲,在程序编译的预处理阶段,编译器会把#define声明的代码直接插入程序中,加入编写如下代码’
#include <iostream>
#define PLUS(x) x+x
using namespace std;
int main() {
int x = 3;
int ans = PLUS(x) * PLUS(x);
cout << ans;
return 0;
}
按照期望的结果,ans = (3 + 3) * (3 + 3)应为36,然而事实的答案是15,为什么会得到这个结果?因为预处理时仅仅是将x+x简单地把PLUS(x)替换掉所以该语句实际上是
int ans = x+x * x+x;
解决方法是将每个变量两边都加上括号
#include <iostream>
#define PLUS(x) ((x)+(x))
using namespace std;
int main() {
int x = 3;
int ans = PLUS(x) * PLUS(x);
cout << ans;
return 0;
}
这样的执行结果就是36
可是,这样就高枕无忧了吗,并没有
#include <iostream>
#define CALL_WITH_MAX(a, b) ((a) > (b) ? (a) : (b))
using namespace std;
int main() {
int a = 5, b = 0;
cout << CALL_WITH_MAX(++a, b) << endl; //a被累加2次
cout << CALL_WITH_MAX(++a, b + 10) << endl; //a被累加1次
return 0;
}
输出结果分别为7、10,其中第一次调用时a被累加两次,这不是期望出现的结果,这可以看出#define所具有的局限性,在c++中可以用const和inline关键字来解决这个问题
const int a = 5;
#include <iostream>
using namespace std;
inline int callWithMax(const int& a, const int& b) {
return a > b ? a : b;
}
int main() {
int a = 5, b = 0;
cout << callWithMax(++a, b) << endl;
cout << callWithMax(++a, b + 10) << endl;
return 0;
}
//输出结果为6, 10
对于const和enum还有如下使用方法
class GamePlayer {
private:
static const int NumTurns = 5; //常量声明,static关键字使得多个对象分享一个该常量的实体
int scores[NumTurns]; //使用该常量
};
class GamePlayer {
private:
enum {NumTurns = 5};
int scores[NumTurns];
};
当然也可以这样
class GamePlayer {
private:
static const int NumTurns; //常量声明,static关键字使得多个对象分享一个该常量的实体
int scores[NumTurns]; //使用该常量
};
const int GamePlayer::NumTurns = 6; //cpp文件中
关于const char*、char* const以及const char* const
int main() {
const char* name = "Rong-Yan Xu";
char* name2 = "Mike";
name[0] = 'h'; //不允许,name为const char*型,不允许被改变(const data)
name = name2; //允许,char*为non-const pointer
return 0;
}
int main() {
char* const name = "Rong-Yan Xu";
char* name2 = "Mike";
name[0] = 'h'; //允许,name为char*型,允许被改变(non-const data)
name = name2; //不允许,char*为const pointer
return 0;
}
int main() {
const char* const name = "Rong-Yan Xu";
char* name2 = "Mike";
name[0] = 'h'; //不允许,name为const char*型,不允许被改变(const data)
name = name2; //不允许,char*为const pointer
return 0;
}
const成员函数
const char& operator[](size_t pos) { //传回的char引用可读不可写
return text[pos];
}
char& operator[](size_t pos) { //传回的char引用可读可写
return text[pos];
}
值得一提的是,若制定函数返回类型为基本类型(非引用也非指针),就没有必要再返回值类型前加上const,因为即使没有加上const,你在改变返回值时,也只是再改变一个副本,这种改动没有意义。
另外,声明了non-const的返回值,就要拿一个non-const去接,声明了const的返回值就要拿一个const接
const修饰成员函数时,代表该函数内不允许修改成员函数的任何属性,即使该属性并没有被声明为const型,而mutable关键字可以破除这个限制
class GamePlayer {
private:
char* pText;
size_t slength;
bool valid;
public:
size_t length() const;
};
size_t GamePlayer::length() const {
if(!valid) {
slength = strlen(pText); //不合法,const成员函数不允许改变对象内的任何值
valid = true; //同上
}
return slength;
}
tips:
- C++程序中应尽可能使用const来强制约束某些不该被改变的量,以免出现意料之外的错误
条款04:Make sure that object are initialized before they are used
确定一个对象被使用前已经先被初始化了
默认初始化的情况:
class Point {
int x, y; //在某些编译器下会默认初始化为0,某些情况下不会
};
struct Node{
int x, y; //在某些编译器下会默认初始化为0,某些情况下不会
};
int main() {
int x; //在某些编译器下会默认初始化为0,某些情况下不会
return 0;
}
可见是否被默认初始化的情况并不被确定,而对某些未初始化的指针或者对象进行调用可能会发生爆炸,故需要学习一些初始化的方法。
对于C++的对象而言,其初始化的任务落在了构造函数上(constructor)上,一般的情况下,我可能会这么写一个构造函数(java出身。。)
GamePlayer::GamePlayer(char *pText, size_t slength, bool valid) { //是赋值而不是初始化
this->pText = pText;
this->slength = slength;
this->valid = valid;
}
这样的写法是赋值而不是初始化,区别就是赋值是已经构造出这个对象之后在对里面的成员进行赋值,而初始化是在构造对象时,根据传进来的参数对成员进行初始化。初始化代码如下。
GamePlayer::GamePlayer(char *pText, size_t slength, bool valid) : pText(pText), slength(slength), valid(valid) {
//do nothing
}
C++有十分固定的成员初始化顺序(父类先于子类,类的内部以成员声明次序初始化)
不同编译单元定义非本地的对象时初始化的行为
如下代码:
class GamePlayer {
private:
char* pText;
mutable size_t slength;
mutable bool valid;
public:
size_t length() const;
};
extern GamePlayer gp; //预备给别的客户的对象,客户在别的编译单元内
class Client { //另一个编译单元的客户
public:
Client();
};
Client::Client() {
size_t x = gp.length(); //调用时无法确定gp是否被初始化
}
C++没有定义不同单元内非本地对象的初始化顺序,所以client在调用gp的函数时无法确定gp是否被初始化,解决方法如下
class GamePlayer {
private:
char* pText;
mutable size_t slength;
mutable bool valid;
static GamePlayer g;
public:
size_t length() const;
static GamePlayer& gp() {
return g;
}
};
class Client { //另一个编译单元的客户
public:
Client();
};
Client::Client() {
size_t x = GamePlayer::gp().length(); //调用时无法确定gp是否被初始化
}
类似于单例模式的做法
tips:
- 总是在初值列中列出所有成员变量,以免还得记住哪些成员变量可以无需初值
- 内置型对象进行手工初始化,C++不能保证初始化它们
- 构造函数最好使用成员初值列,而不使用赋值,初值列的排列次序应该和它们在class中的声明次序相同
- 为了免除“跨编译单元的初始化次序”问题,用本地的static对象来替代非本地(non-local)的static对象
参考文献
Effective C++
C++ Primer