第一部分 C++语言基础知识
1、1 尽量用const 和inline 而不用#define。
这个条款最好称为:“尽量用编译器而不用预处理”,因为#define 经常被认为好象不是语言本身的一部分。如果出现错误很难追溯出结果。如:#define ASPECT_RATIO 1.653。编译器会永远也看不到ASPECT_RATIO 这个符号名,因为在源码进入编译器之前,它会被预处理程序去掉,于是ASPECT_RATIO 不会加入到符号列表中。如果涉及到这个常量的代码在编译时报错,就会很令人费解,因为报错信息指的是1.653,而不是ASPECT_RATIO。首先,定义指针常量时会有点不同。因为常量定义一般是放在头文件中(许多源文件会包含它),除了指针所指的类型要定义成const 外,重要的是指针也经常要定义成const。例如,要在头文件中定义一个基于char*的字符串常量,你要写两次const:
const char * const authorName = "Scott Meyers";
定义某个类(class)的常量一般也很方便,只有一点点不同。要把常量限制在类中,首先要使它成为类的成员;为了保证常量最多只有一份拷贝,还要把它定义为静态成员:
class GamePlayer {
private:
static const int NUM_TURNS = 5; // constant declaration
int scores[NUM_TURNS]; // use of constant
...
};
还有一点,正如你看到的,上面的语句是NUM_TURNS 的声明,而不是定义,所以你还必须在类的实现代码文件中定义类的静态成员:
const int GamePlayer::NUM_TURNS; // mandatory definition;
// goes in class impl. file
1、2 尽量用<iostream>而不用<stdio.h>。
从技术上说,其实没有<iostream.h>这样的东西——标准化委员会在简化非C 标准头文件时用<iostream>取代了它。如果编译器同时支持 <iostream>和<iostream.h> ,那头文件名的使用会很微妙。例如,如果使用了#include <iostream>, 得到的是置于名字空间std(见条款28)下的iostream 库的元素;如果使用#include <iostream.h>,得到的是置于全局空间的同样的元素。在全局空间获取元素会导致名字冲突,而设计名字空间的初衷正是用来避免这种名字冲突的发生。
1、3 条款3:尽量用new 和delete 而不用malloc 和free。
把new 和delete 与malloc 和free 混在一起用也是个坏想法。对一个用new获取来的指针调用free,或者对一个用malloc 获取来的指针调用delete,其后果是不可预测的。大家都知道“不可预测”的意思:它可能在开发阶段工作良好,在测试阶段工作良好,但也可能会最后在你最重要的客户的脸上爆炸。
用C++形式的句法来注释掉这个程序块时,嵌在里面的最初的注释不受影响,但如果选择C 风格的注释就会发生严重的错误:
if ( a > b ) {
/* int temp = a; /* swap a and b */
a = b;
b = temp;
*/
}
请注意嵌在代码块里的注释是怎么无意间使本来想注释掉整个代码块的注
释提前结束的。值得指出的是,有些老的专门为C 写的预处理程序不知道处理C+风格的注释,所以象下面这种情形时,事情就不会象预想的那样:
#define LIGHT_SPEED 3e8 // m/sec (in a vacuum)
对于不熟悉C++的预处理程序来说,行尾的注释竟然成为了宏的一部分!
1、4 对应的new 和delete 要采用相同的形式
下面的语句有什么错?
string *stringArray = new string[100];
...
delete stringArray;
一切好象都井然有序——一个new 对应着一个delete— — 然而却隐藏着很大的错误:程序的运行情况将是不可预测的。至少,stringArray 指向的100 个string 对象中的99 个不会被正确地摧毁,因为他们的析构函数永远不会被调用。这个问题简单来说就是:要被删除的指针指向的是单个对象呢,还是对象数组?这只有你来告诉delete。如果你在用delete 时没用括号,delete 就会认为指向的是单个对象,否则,它就会认为指向的是一个数组:
string *stringPtr1 = new string;
string *stringPtr2 = new string[100];
...
delete stringPtr1; // 删除一个对象
delete [] stringPtr2; // 删除对象数组
如果你在stringPtr1 前加了"[]"会怎样呢?答案是:那将是不可预测的;如果你没在stringPtr2 前没加上"[]"又会怎样呢?答案也是:不可预测。而且对于象int 这样的固定类型来说,结果也是不可预测的,即使这样的类型没有析构函数。所以,解决这类问题的规则很简单:如果你调用new时用了[],调用delete时也要用[]。如果调用new 时没有用[],那调用delete 时也不要用[]。
1、5 析构函数里对指针成员调用delete。
删除空指针是安全的(因为它什么也没做)。所以,在写构造函数,赋值操作符,或其他成员函数时,类的每个指针成员要么指向有效的内存,要么就指向空,那在你的析构函数里你就可以只用简单地delete 掉他们,而不用担心他们是不是被new 过。
1、6 预先准备好内存不够的情况。
用预处理。例如,C 的一种常用的做法是,定义一个类型无关的宏来分配内存并检查分配是否成功。对于C++来说,这个宏看起来可能象这样:
#define NEW(PTR, TYPE) /
try { (PTR) = new TYPE; } /
catch (std::bad_alloc&) { assert(0); }
NEW 宏不但有着上面所说的通病,即用assert 去检查可能发生在已发布程序里的状态(然而任何时候都可能发生内存不够的情况),同时,它还在C++里有另外一个缺陷:它没有考虑到new 有各种各样的使用方式。例如,想创建类型T 对象,一般有三种常见的语法形式,你必须对每种形式可能产生的异常都要进行处理:
new T;
new T(constructor arguments);
new T[size];
这里对问题大大进行了简化,因为有人还会自定义(重载)operator new,所以程序里会包含任意个使用new 的语法形式。
如果想用一个很简单的出错处理方法,可以这么做:当内存分配请求不能满足时,调用你预先指定的一个出错处理函数。这个方法基于一个常规,即当operator new 不能满足请求时,会在抛出异常之前调用客户指定的一个出错处理函数——一般称为new-handler 函数。
指定出错处理函数时要用到set_new_handler 函数,它在头文件<new>里大致是象下面这样定义的:
typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();
可以看到,new_handler 是一个自定义的函数指针类型,它指向一个没有输入参数也没有返回值的函数。set_new_handler 则是一个输入并返回new_handler 类型的函数。set_new_handler 的输入参数是operator new 分配内存失败时要调用的出错处理函数的指针,返回值是set_new_handler 没调用之前就已经在起作用的旧的出错处理函数的指针。可以象下面这样使用set_new_handler:
// function to call if operator new can't allocate enough memory
void noMoreMemory()
{
cerr << "Unable to satisfy request for memory/n";
abort();
}
int main()
{
set_new_handler(noMoreMemory);
int *pBigDataArray = new int[100000000];
...
}
1.7 在使用NEW和DELETE的时候应注意的问题
如果你调用new时用了[],调用delete时也要用[]。如果调用new 时没有用[],那调用delete 时也不要用[],否则会出现不可预测的结果。
第二部分 嵌入式开发学习
2.1 用预处理指令#define 声明一个常数,用以表明1 年中有多少秒(忽略闰年问题)
#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL
我在这想看到几件事情:
• #define 语法的基本知识(例如:不能以分号结束,括号的使用,等等)
• 懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的。
• 意识到这个表达式将使一个16 位机的整型数溢出-因此要用到长整型符号L,告诉编译器这个常数是的长整型数。
• 如果你在你的表达式中用到UL(表示无符号长整型),那么你有了一个好的起点。记住,第一印象很重要。
2.2 写一个"标准"宏MIN ,这个宏输入两个参数并返回较小的一个。
#define MIN(A,B) ((A) <= (B) ? (A) : (B))
这个测试是为下面的目的而设的:
• 标识#define 在宏中应用的基本知识。这是很重要的,因为直到嵌入(inline)操作符变为标准C 的一部分,宏是方便产生嵌入代码的唯一方法,对于嵌入式系统来说,为了能达到要求的性能,嵌入代码经常是必须的方法。
• 三重条件操作符的知识。这个操作符存在C 语言中的原因是它使得编译器能产生比if-then-else 更优化的代码,了解这个用法是很重要的。
• 懂得在宏中小心地把参数用括号括起来
• 我也用这个问题开始讨论宏的副作用,例如:当你写下面的代码时会发生什么事?
least = MIN(*p++, b);
2.3 用变量a 给出下面的定义
a) 一个整型数(An integer)[int a;]
b)一个指向整型数的指针( A pointer to an integer)[int *a;]
c)一个指向指针的的指针,它指向的指针是指向一个整型数( A pointer to a pointer to an intege)[int **a;]
d)一个有10 个整型数的数组( An array of 10 integers)[int a[10];]
e) 一个有10 个指针的数组,该指针是指向一个整型数的。(An array of 10 pointers to integers)[int *a[10];]
f) 一个指向有10 个整型数数组的指针( A pointer to an array of 10 integers)[int (*a)[10];]
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument and returns an integer)[int (*a)(int);]
h)一个有10 个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数( An array of ten pointers to functions that take an integer argument and return an integer )[int (*a[10])(int);]
2.4 关键字static 的作用是什么?
这个简单的问题很少有人能回答完全。在C 语言中,关键字static 有三个明显的作用:
• 在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
• 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。
• 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。大多数应试者能正确回答第一部分,一部分能正确回答第二部分,同是很少的人能懂得第三部分。这是一个应试者的严重的缺点,因为他显然不懂得本地化数据和代码范围的好处和重要性。
2.5 关键字const 有什么含意?
我只要一听到被面试者说:"const 意味着常数",我就知道我正在和一个业余者打交道。去年Dan Saks 已经在他的文章里完全概括了const的所有用法,因此ESP(译者:Embedded Systems Programming)的每一位读者应该非常熟悉const 能做什么和不能做什么.如果你从没有读到那篇文章,只要能说出const 意味着"只读"就可以了。尽管这个答案不是完全的答案,但我接受它作为一个正确的答案。(如果你想知道更详细的答案,仔细读一下Saks 的文章吧。)如果应试者能正确回答这个问题,我将问他一个附加的问题:
下面的声明都是什么意思?
const int a;
int const a;
const int *a;
int * const a;
int const * a const;
/******/
前两个的作用是一样,a 是一个常整型数。第三个意味着a 是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个意思a 是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修的,但指针是不可修改的)。最后一个意味着a 是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。顺带提一句,也许你可能会问,即使不用关键字const,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const 呢?我也如下的几下理由:
• 关键字const 的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const 的程序员很少会留下的垃圾让别人来清理的。)
• 通过给优化器一些附加的信息,使用关键字const 也许能产生更紧凑的代码。
• 合理地使用关键字const 可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。
2.6 关键字volatile 有什么含意?并给出三个不同的例子。
一个定义为volatile 的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile 变量的几个例子:
• 并行设备的硬件寄存器(如:状态寄存器)
• 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
• 多线程应用中被几个任务共享的变量