不管你的编程经验如何,C++的入门门槛都很低的。它是一个强大的语言,拥有大量的特性。但是在你能够驾驭并写出高效的代码之前,你必须要适应C++做事的方式。这正本书就是关于这些的。但是有些东西更基础,我们这一章就从这些基础的东西开始!
在开始的时候,C++只是在C的基础上加上了面向对象的特性而已。C++的初始的名字:C with Classes,就反应了这个简单的继承关系。
随着这个语言的慢慢成熟,它也变得越来越大胆,越来越有冒险精神了,开始采用新的观点,新的特性以及编程模式,这些都是和“C with classes”不同的:异常的处理需要对函数的构建采用不同的方式;模板的出现给了设计模式新的设计思路;STL定义了一种大多数人都不曾见过的增强扩展性的方式。
今天,C++是一种
多范式编程语言,是一个融合了面向过程、面向对象、函数的、范式的、元编程的特点的语言,该死,你又在罗列名词,没事,可以上网查资料。这种能力使得C++成为一种无出其右的语言(有失偏颇,每个语言都是一个trade-off),但同时也会引起一些困惑。所有的“正确的”用法好像都会产生异常,那么我们怎么能够用这门语言来产生生产力?
要想理解这个问题,最简单的方式就是不要将C++视做一个单一的语言,而是将其视为一个相关联的几个
子语言的联合。在一个特定的子语言中,规则可能简单,直接而且容易记忆,但是你切换到另外一个子语言时,规则就变了。要想搞懂C++,我认为你应该认清它的主要的子语言。幸运的是,只有四个子语言:
-
C 回到最低层,C++是基于C的语言。语言块、语句、预处理器、内置类型、数组、指针等等,这些都是来自C的东西。在很多情况下,你可以在C++中找到比用C解决问题更好的方式(详见条目2和条目3),但是当你用C++中的C这个子语言时,你会发现要想设计高效的代码,C会带来很多的限制:没有模板,没有异常处理,没有重载等等。
-
面向对象的C++。这一个子语言就是所谓的“C with Classes”:类(包括构造函数,析构函数),封装,继承,多态,虚函数(动态绑定)等等。这是面向对象的语言基本都包含的东西。
-
模板C++。这是C++中的范式编程部分,也是很多的程序员没有经验的部分。模板的使用可以在C++中到处可见,并且好的程序应该包含特殊的template-only 子句。实际上,模板是如此的强大,它可以给我们提供一个全新的编程模式,即模板元编程(template metaprogramming TMP)。在条目48中涉及了TMP,但是除非你是一个模板发烧友,你就不需要担心它。模板方面的规则基本上是不和C++的主流规则有什么相互影响的。
-
STL.STL就是指模板库,当然是一个特殊的模板库。它可以将容器、迭代器、算法以及函数对象非常巧妙地的融合在一起。但是模板以及库可以和其它的观点和想法结合在一起。STL的以自己的行事规则办事,当你使用STL时,你要注意遵循这些规则。
将这四类子语言记在心里,而且为了能够设计出高效的代码,要能够在这几个子语言中进行灵活的切换。例如,对于内置类型的变量来说,传值比传引用通常更有效,但是当你从C切换到面向对象的C++时,通过用户设计的构造函数和析构函数,通过传递常量应用通常更好。这种情况对于C++中的模板更是如此,因为在模板中你可能连自己要操作的变量的类型都不知道。当你跳跃到STL时,你知道迭代器和函数对象是C中的指针的变形,所以对于STL中的迭代器和函数对象,古老的传值规则又可以用了。(详见条目20)
这样说来,C++就不是一个只有单独的一个规则的统一的语言了,而是一个包含了四个子语言的联合。每个子语言都有自己的规则。你把这个观点记住,你就会发现C++变得容易理解许多。
需要记住的事
高效设计代码的规则是不同的,取决于你所使用的C++的那部分子语言。
个人觉得,这是作者的贡献之一,从一个更高的角度看待问题,从子语言的角度来看待一整个语言,可以解释我们平时面临的一些困惑。
条目二 尽量使用 const,enum以及inline,而少用#define
这一个条目应该叫做”与其用预处理不如用编译器“,意思就是说,尽量不要用预处理器,这是因为#define可能让人觉得它不属于你写的代码似的,因为它的值要被预处理器在预处理的时候就给替换掉了。当你写下如下代码:
-
#define ASPECT_RATIO 1.653
的时候,编译器永远看不到符号
ASPECT_RATIO,预处理可能将这个符号移除了,让后将预处理的结果交给编译器。结果就导致
ASPECT_RATIO不会进入符号表中。这样当你在编译中遇到和这个常量相关的错误时就会感觉无所适从,因为错误可能指向的是1.653,而不是
ASPECT_RATIO,如果
ASPECT_RATIO在一个不是你写的头文件中,你就不知道这个1.653是从哪里来的,你将要花费时间在追踪这个问题上。在符号调试器中也可能突然出现这个问题,因为你在编写的代码中涉及的符号都不在符号列表中。
解决方法就是将宏(#define就是宏的一种)变为constant:
-
const double AspectRatio = 1.653; /// 宏中的符号经常为大写,所以这里做了些许改变
作为一个语言的常量,编译器肯定可以看到
AspectRatio,并且要将其列入符号列表中。另外,如果使用的是浮点型的常量,使用const会比使用#define更节省空间,产生更少的代码。这是因为预处理器在遇到
ASPECT_RATIO时就要进行替换,而使用const则只会占用内存的地址空间来存放这个数据。但是作者不能一直黑宏吧,存在即合理吧?宏的好处是能给程序的速度带来一定的提升。
当使用const来代替#define时,有两个特殊的地方需要注意。第一就是定义常量指针时。因为常量的定义一般放在头文件里,因此我们需要将指针所指向的变量声明为const的同时,还需要将指针本身也声明为const类型的。例如当你在头文件中定义一个char*类型的字符串常量时,你必须写两次const:
-
const char *const authorName = " scott mayers";
如果你对这个代码有一定的困惑,我在这里解释一下。常量指针是比较难理解的东西。这里的两个const的作用是不同的,有过一定经验的朋友应该知道,我们在读到这样的代码时应该从右往左读。那么我们遇到的第一个const是用来修饰authorName的,也就是说authorName是一个常量,是一个什么常量呢?是一个指针常量(*告诉我们的),是一个指向什么类型的指针常量呢?是一个指向常量字符的常量指针。那么authorName里放的是什么呢?是"scott mayers"这个字符串的第一个字符的地址。那么这个字符串多长呢?13.为什么呢?因为末尾还有一个'\0'.英语里有一句话叫做:get your hand dirty。什么意思呢?就是要注重实践,如果你对一段代码感觉莫名其妙,一个一个的输入到文本中,慢慢的你就懂了。
在条目三中我们的对const的用法进行了讲解。但是,我们有必要在这里提醒一下,在c++的世界里,string比char数组更受欢迎,更好用,虽然char是string的基础和祖先,呵呵。所以这样定义authorName更好:
-
const std::string authorName("Scott mayers")
第二个特殊的用法涉及到类中的常量。要想将某个变量限定在类中,你必须将它定义为类的成员,并且如果你想让这个常量对这个类来说只有一份拷贝,你就必须将它定义为static:
-
class GmaePlayer
-
{
-
private:
-
statci const int NumTurns = 5; ///常量声明
-
int scores[NumTurns]; ///使用常量
-
........
-
};
你所看到的是NumTurns的声明,而非定义。通常C++都要求你对你要使用的任何东西都要进行定义,但是类中的静态常量整数是例外(所谓的整数是指int,char,和bool)。为什么呢?这里没说,应该是和存储有关。也就是说你可以在这里将NumTruns赋值为5,但是如果你另外声明一个double型的成员,然后在类中进行赋值,编译器会报错的。
对于静态常量整数,如果你不取它们的地址,你就可以声明之后就使用,而不必进行定义。如果你需要对这些变量进行地址操作或者你的编译器向你抱怨你的这种有点冒险的操作,那么你就必须先声明再定义了,而且定义是在类的定义之外:
-
const int GamePlayer:NumTurns; //////类中的常量的定义,看下文为什么没有给出初始值
注意你是将这个放在执行文件中,而不是头文件中。因为我们已经在声明的时候给出了值,所以在定义的时候就不能再给出值。这也是为什么能够在类中中能够使用这个值的原因,如果我们重新给它赋值,那么请问scores数组怎么知道自己多大?怎么分配空间呢?
注意到一点,我们不能在类中通过#define来新建常量,以为内#define是和作用域无关的。一旦你定义了一个宏,那么在接下来的编译中就都有效,除非在期间遇到了#undef来取消宏的定义。这也就意味着我们不仅不能用#define来定义类中的常量,也不能提供类似封装的概念,因为不能定义一个private #define.当然了,const可以定义为private来实现封装。作者的意思呢,就是说一旦#define了某个东西,就在接下来的语境中都有效,这也就不能将变量放在class的笼子中,所以不能实现封装等.
当你需要在定义类的时候就要对const变量进行引用的时候,你就需要给申明的静态常量整数赋值,这也是一个不常见的方法吧。但是如果你想换其它的方法呢?你可以尝试使用enum(枚举),它也被亲切的叫做”枚举黑客“??。这个技巧利用的就是枚举类型的变量中的数可以在需要整形数的语境中拿来用这样一个特性。所以我们可以将GamePlayer定义为如下:
-
class GamePlayer
-
{
-
private:
-
enum{NumTurns=5}; ////通过枚举,NumTurns就变成了5这个整数的符号名称了
-
int scores[NumTurns]; /////这样是可以的
-
};
赶紧get your hand dirty,看看GamePlayer的sizeof是多少。答案是20.也就是说enum是没有占内存的,我们只是给5这个整数取了一个符号名称叫做 NumTurns.
enum有几点值得注意的地方。第一点是enmu表现出来的更像#define而不像const,有时这是你所希望的。例如你可以合法的取得const类型的变量的地址,但是取enum的地址是非法的,同时要想取#define定义的名称的地址也是非法的。如果你不想让人对你的某个整数常量取地址或者是取引用,使用enum是不错的选择。另外,从节省空间的角度来考虑的话,一般的编译器不会对const常量申请变量空间,但是一些粗心的编译器是会的。如果你想不为这些常量设置空间,你可以使用enum,因为enum中的名称只是一个名称,没有占用空间,不信你可以试试的。
另外一个要了解enum的原因是从实际出发的,因为好多人都喜欢用这个,尤其是老牌的程序员,呵呵。所以你要想读一些老大牛们写的代码你还是需要了解enum的。实际上enum是模板元编程的基础技术。
回到刚才说的预处理器中,另外一个对#define的错误的用法是用它来定义一个宏来实现一个类似函数调用的功能,但是又没有考虑这样做会带来的恶果。下面定义了一个宏:
-
// 将a和b中较大的值传给f
-
#define CALL_WITH_MAX(a,b) f((a>b)?(a):(b))
像这样的宏要多恶心有多恶心,你就当它很恶心就是了。作者已经要疯了这是要。
任何时候当你写这样的宏的时候,你就要想到将参数用括号括站起来,要不然如果有人传递表达式给这个宏时就会产生错误,但是即使你这样做了,还是会出问题:
-
int a=5,b=0;
-
CALL_WITH_MAX(++a,b); ///// a被加了两次
-
CALL_WITH_MAX(++a,b+10); //// a 被加了一次
这里a被加一的次数竟然取决于它要和什么数进行比较!这是为什么呢?说实话,这里我不懂,为什么和b进行比较就进行了两次加一操作呢?
幸运的是你可以不用忍受这种没有意义的东西。你可以通过使用模板加上inline函数来实现上述宏想要做的工作,而且避免了模棱两可的错误:
-
template
-
inline void callWithMax(const T &a,const T &b) ///因为我们不知道T是什么,所以我们这里通过传递const类型的引用,见条目20
-
{
-
f(a>b?a:b);
-
}
这个模板实际上就定义了一系列的函数,每一个函数都包含两个相同类型的参数,然后将两个参数中较大的一个传递给f.我们这里不需要括号,不需要担心会对参数进行多次计算等等。另外,因为callWithMax是一个真正的函数,它就遵循
作用域与可进入规则。例如我们可以说某个inline函数是某个类的私有成员,但是对于宏就不能这么说了。
如果使用了const,enum和inline,你使用#define的机会就少了,但是我们还不能将#define去掉。#include这个宏依然很重要,#ifdef/ifndef 在编译时还是依然很重要的。我们还不能完全的去掉预处理器,你还要和它一起共事很久呢!
需要记住的事:
-
对于简单的常量,尽量使用const和enum,而不用#define
-
对于像函数的宏,使用inline函数而不是使用#define