条款01:视C++为一个语言联邦
现代C++已经完全区别于传统的C with Classes(带类的C);
我们如何理解C++这一个语言?
将C++视作一个由相关语言组成的联邦;
如何理解所谓联邦?
——C++是由四个次语言组成的“联邦政府”。
四个次语言
C语言
C++仍然以C为基础,例如区块、语句、预处理器、内置数据类型、数组、指针;
Object-Oriented C++(C++面向对象方法)
这也是C with Classes所诉求的,例如类与对象、封装、继承、多态;
Template C++(C++泛型编程)
C++泛型编程的部分,是大部分C++程序员经验最少的部分,但这个设计已经弥漫整个C++,泛型编程的威力很大,为我们带来了TMP(模板元编程)。
STL(C++标准模板库)
STL是一个template程序库,对容器、迭代器、算法、函数对象的规约有极佳的紧密配合与协调,总之,STL提供了丰富的工具,但我们使用时也要遵守它的规约。
条款02:尽量以const、enum、inline替换#define
"用编译器换预处理器"是其本质
使用const替换#define的实例
#define ASPECT_RATIO 1.653
我们可以这样理解将会发生什么:编译器开始处理源码之前ASPECT_RATIO就被预处理器移走了
#include <iostream>
#define ASPECT_RATIO 1.653
int main(int argc,char* argv[]){
std::cout << ASPECT_RATIO << std::endl;
return 0;
}
#include <iostream>
int main(int argc,char* argv[]){
std::cout << 1.653 << std::endl;
return 0;
}
你可以理解为,预处理器是这样作用的,即把上面的源代码替换为下面的源代码
#include <iostream>
const double AspectRatio = 1.653;
int main(int argc,char* argv[]){
std::cout << AspectRatio << std::endl;
return 0;
}
如果我们使用常量定义代替这种声明,那么AspectRatio是可以被我们跟踪到的,比如我们进行程序断点时,我们可以对这个量进行跟踪。
利用常量代替#defines的两种特殊情况
第一种情况
如果我们想要声明一个变量表示字符串,我们常常这么做
const char* str = "Hello";
但是,str真的是变量吗?当你看到前面有const修饰时,你会下意识的认为str是一个常量,实则不然,请运行下列代码
#include <iostream>
int main(int argc, char* argv[]){
const char* str = "Hello";
str = "World";
std::cout << str << std::endl;
return 0;
}
输出结果为:World
原因是这样的:const char* str表示的是声明了一个指向常量字符的指针,意思就是,const并非str的const,而是"Hello","World"等常量字符串的const,如果你想要使得str也为const,你应该这么做
#include <iostream>
int main(int argc, char* argv[]){
const char* const str = "Hello";
str = "World";
std::cout << str << std::endl;
return 0;
}
这段代码无法运行,因为此时str为常量,其类型是常量字符指针,这也是我们想要阐述的。
当然,在C++语言中,我们常使用string类型来表示一个字符串,当我们想要使某个string对象变为常量时,使用一个const即可。
第二种情况
如果要设定一个类的专属常量,即这个常量的作用域限制于类内,如果我们有这样的需求:我们最多有一份这样的常量(在内存中只出现一次),我们应该在这个常量声明前加上static,例如
class MyClass{
public:
static const int NUM = 100;
};
这样,对于每一个MyClass的NUM都是同一个NUM。
值得注意的是,我们并不会像上述代码实现,对于大部分C++库,我们都是先给出其声明,后给出其实现,例如,上面的代码应该改成
// 这段代码放在头文件MyClass.h当中
class MyClass{
public:
static const int NUM;
};
// 这段代码放在源文件MyClass.cpp中
#include "MyClass.h"
const int MyClass::NUM = 100;
当然,这个做法也有例外
class MyClass{
public:
static const int NUM;
int arr[NUM];
};
这样声明是违法的,因为编译器想要确定下arr的长度,但是它找不到NUM的值,此时,为了不违背在源文件实现其内容的原则,我们可以使用枚举类型进行补偿
class MyClass{
public:
enum {
NUM = 100
};
int arr[NUM];
};
此时可以通过编译
使用enum的好处是,别人无法通过某个指针或者引用指向你的常量,因为NUM可以看作是某个记号(但不同于#define,其仍有作用域约束)。
使用#define定义伪函数的弊端与替代法(内联函数)
#define所谓伪函数的弊端
我们可以为#define定义参数,实现类似函数的功能,但这并非函数,故作者称其为伪函数,但这种伪函数不一定正确,其由#define所定义,必然符合#define的特征——替换,即编译时期替换源码。
#include <iostream>
#define MAX(A,B) ((A > B) ? A : B)
int main(int argc, char* argv[]){
int a = 1;
int b = 0;
std::cout << MAX(++a, b) << std::endl;
std::cout << a << std::endl;
return 0;
}
输出结果:3,3
如果我们利用数学语言分析,我们并非想要得到这个结果,我们希望的是:++a得到的结果是2,请注意,前置++会返回自增后的结果,所以,我们希望这样子调用:MAX(2,0),结果应该是2,但是输出结果并非如此。我们可以将上述代码展开,用++a和b替换A,B
#include <iostream>
int main(int argc, char* argv[]){
int a = 1;
int b = 0;
std::cout << ((++a > b) ? ++a :b) << std::endl;
std::cout << a << std::endl;
return 0;
}
两段代码是等效的,分析这个三元运算符,首先,进行++a与b的大小,++a>b也即2>1(这时a的值为2)所以返回++a,此时++a也即返回3
使用内联函数解决这一问题
在计算机科学中,内联函数(有时称作在线函数或编译时期展开函数)是一种编程语言结构,用来建议编译器对一些特殊函数进行内联扩展(有时称作在线扩展);也就是说建议编译器将指定的函数体插入并取代每一处调用该函数的地方(上下文),从而节省了每次调用函数带来的额外时间开支。但在选择使用内联函数时,必须在程序占用空间和程序执行效率之间进行权衡,因为过多的比较复杂的函数进行内联扩展将带来很大的存储资源开支。另外还需要特别注意的是对递归函数的内联扩展可能引起部分编译器的无穷编译。 [1]
——摘录自百度百科
C++ 内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。对内联函数进行任何修改,都需要重新编译函数的所有客户端,因为编译器需要重新更换一次所有的代码,否则将会继续使用旧的函数。
如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字
inline,在调用函数之前需要对函数进行定义。如果已定义的函数多于一行,编译器会忽略 inline 限定符。
——摘自菜鸟教程
通过上述资料,可以简单了解到,编译器会将调用内联函数的地方进行展开函数体的操作,你可以理解为替换,但是它符合函数调用的模式(你可以把它视作普通函数),例如检查参数的正确性(例如类型),而非#define那样简单的“文本替换”。
#include <iostream>
inline int max(int a, int b){
return (a > b) ? a : b;
}
int main(int argc, char* argv[]){
int a = 1;
int b = 0;
std::cout << max(++a, b) << std::endl;
std::cout << a << std::endl;
return 0;
}
输出结果:2,2