作者:寒小阳
时间:2013年8月。
出处:http://blog.csdn.net/han_xiaoyang/article/details/10473845。
声明:版权所有,转载请注明出处,谢谢。
眼看着又要到一年一度的找工作季了,遥想去年这会儿,学校同学们也都是一副要上战场似得枕戈待旦整装待发的情景。想来自己当时笔试面试也确实参加了不少,无奈技术和智商平平,最后结果一般,但也确实经历了不少,外加一直有总结和整理资料的小习惯,手头上确实攒下了一些笔试面试的资源。又最近频繁有师弟师妹问我要相关的资料,想来还不如自己整理整理发到博客里,大家都能看看,希望能有一些帮助吧。
这部分内容大部分来自林锐博士01年写的高质量C++/C编程指南,距今已经十多年了,但是我依旧清晰地记得师兄当初介绍这本几十页的书给我,看后的惊艳,确切的说来,这本书让我的编程风格编程习惯以及对C++/C的认识有了大的提升(这个说来尴尬了,本人属于木有任何天赋的程序员,和大牛们差几百个档,之前写的程序只能用惨不忍睹来形容,即使现在可能也只属于勉强能看)。直至后来参加大大小小的笔试面试,发现里面考察略深一点点的C++/C基础知识在书里面都能看到;而后来参加某互联网公司面试,当场写算法程序时,被面试官夸过编程习惯和编程风格非常好(介个,其实也有可能是刚好对上面试官胃口罢了...),我再一次意识到,这份资料确实是不错的,所以这里打算整理整理,发到博客上共享一下,已经看过和熟知的童鞋或者大牛们自行绕开吧。
今天写的这部分,大体上都是关于编程风格和编程习惯的,C/C++基础和注意点下次再说吧。
我看过一些写的比较详细正统的头文件和定义文件的开头都有这部分。具体包括以下内容:
1) 版权信息。
2) 文件名称,标识符,摘要。
3) 当前版本号,作者/ 修改者,完成日期。
4 )版本历史信息。
1)为了防止头文件被重复引用,应当用ifndef /define/ endif 结构产生预处理块。
2)用#include <filename.h> 格式来引用标准库的头文件,用#include “filename .h”格式来引用非标准库的头文件。
3)头文件中只存放“声明”而不存放“定义”。
示例:
#ifndef GRAPHICS_H // 防止graphics.h 被重复引用
#define GRAPHICS_H
#include <math.h> // 引用标准库的头文件
…
#include “ m yheader.h” // 引用非标准库的头文件
…
void Function1( …); // 全局函数声明
…
class Box // 类结构声明
{
…
};
#endif
1) 通过头文件来调用库功能。
2) 头文件能加强类型安全检查。
建议:
1)在每个类声明之后、每个函数定义结束之后都要加空行。
2)在一个函数体内,逻揖上密切相关的语句之间不加空行,其它地方应加空行分隔。
实力如下图左右所示:
建议:
1)一行代码只做一件事情,如只定义一个变量,或只写一条语句。这样的代码容易阅读,并且方便于写注释。
2)if、for、while、do等语句自占一行,执行语句不得紧跟其后。不论执行语句有多少都要加{}。这样可以防止书写失误。
例下图中左侧为风格良好的代码,右侧为风格糟糕的代码。
3)尽可能在定义变量的同时初始化该变量,以减少安全隐患。
1)关键字(const、virtual、inline、case、if、for、while)之后要留空格。而函数名之后不要留空格,紧跟左括号‘(’,以与关键字区别。
2)‘(’向后紧跟,不留空格;‘)’、‘,’、‘; ’向前紧跟,不留空格。‘,’、‘; ’后接空格。
3)二元操作符(“= ”、“+=” “>=”、“<=”、“+ ”、“* ”、“% ”、“&&”、“|| ”、“<<”, “^ ”)前后应当加空格。 一元操作符(“! ”、“~ ”、“++”、“-- ”、“& ”)前后不加空格。
4)对于表达式比较长的for 语句和if 语句,为了紧凑起见可以适当地去掉一些空格,如for (i=0; i<10; i++) 和if ((a<=b) && (c<=d))
示例如下:
1)程序的分界符‘{ ’和‘} ’应独占一行并且位于同一列,同时与引用它们的语句左对齐。
2){ }之内的代码块在‘{ ’右边数格处左对齐。
这里的修饰符主要指 * 和 &,事实上关于它们应该靠近数据类型还是变量名,一直都有争议。将修饰符 * 靠近数据类型,例如:i nt* x; 比较直观,但同时在某些情况下也容易引起误解(例i nt* x, y中的y容易被误解为指针变量)。所以这里:
建议大家在写程序时将修饰符 * 和 & 紧靠变量名。
所以:
int *x, y; // 此处y不会被误解为指针
C 语言的注释符为“/*… * / ”。C++ 语言中,程序块的注释常采用“/*… * / ”,行注释一般采用“//… ”。注释的主要作用是:
1)版本、版权声明;
2)函数接口说明;
3)重要的代码行或段落提示
一个示例如下图所示:
关于变量和函数等的命名,程序员会有自己的认识和习惯,没有一种命名规则可以让所有的程序员赞同,重要的是要保持一致的风格。这个地方,比较著名的一种写法是Microsoft公司的“匈牙利”法。
主要命名规则总结如下:
1)类名和函数名用大写字母开头的单词组合而成。
例如:
class Node; // 类名
class LeafNode; // 类名
void Draw(void); // 函数名
void SetValue(int value); // 函数名
2)变量和参数用小写字母开头的单词组合而成。
例如:
BOOL flag;
int drawMode;
3)常量全用大写的字母,用下划线分割单词。
例如:
const int MAX = 100;
const int MAX_LENGTH = 100;
4)静态变量加前缀s_ (表示static)。
例如:
void Init(…)
{
static int s_initValue; // 静态变量
…
}
5)如果不得已需要全局变量,则使全局变量加前缀g_(表示global )。
例如:
int g_howMany People; // 全局变量
int g_howMuchMoney ; // 全局变量
6)类的数据成员加前缀m_(表示membe r ),这样可以避免数据成员与成员函数的参数同名。
例如:
void Object::SetValue(int width, int height)
{
m_width = width;
m_height = height;
}
有以下共同规则:
1. 标识符应当直观且可以拼读,可望文知意,不必进行“解码”。用词要恰当,莫把CurrentValue 写成NowValue 。
2. 标识符的长度应当符合“min-length && max-information”原则。即最短的程度表达最多的信息。
3. 命名规则尽量与所采用的操作系统或开发工具的风格保持一致。例如Windows中用AddChild。而Unix 中用add_child 。
4. 程序中不要出现仅靠大小写区分的相似的标识符。例int x, X; void foo(int x)和void FOO(float x)是不好的命名方式。
5. 变量的名字应当使用“名词”或者“形容词+名词”。例float oldValue;和float newV alue;
6. 全局函数的名字应当使用“动词”或者“动词+名词”(动宾词组)。例DrawBox(); box->Draw();
7. 用正确的反义词组命名具有互斥意义的变量或相反动作的函数等。例 int minValue; int maxValue; int SetValue(…); int GetValue(…);
首先非常重要的一个点是C/C++运算符的优先级问题,下图为总结的一张表,结合律特殊的运算符已经用黑体加粗标明出来了。
说实话,上表中的运算符优先级和结合律要熟记是非常困难的。虽说有表在,但是也不能每次都查表,所以我们在写程序的时候尽量要遵循后续规则:
如果代码行中的运算符比较多,用括号确定表达式的操作顺序,避免使用默认的优先级。
例:word = (high << 8) | low和if ((a | b) && (a & c))
对于复合表达式的一些建议:
1)不要编写太复杂的复合表达式。例如i = a >= b && c < d && c + f <= g + h ;就太复杂
2)不要有多用途的复合表达式。例如d = (a = b + c) + r ;应该拆成两个语句
3)不要把程序中的复合表达式与“真正的数学表达式”混淆。
if语句看似C ++/C语言中最简单、最常用的语句,然而很多程序员用隐含错误的方式写if语句。同时笔试面试的时候有些公司还是非常热衷于考察这个点,作为对应聘者基础知识扎实度评判的标准。
考察最多的是if语句各种与零值比较的语句写法:
不要将布尔变量直接与TRUE 、FALSE 或者1、0 进行比较。假设布尔变量名字为flag,它与零值比较的标准if 语句如下:
if (flag) // 表示flag为真
其它的用法都属于不良风格,例如:
if (flag == TRUE)
if (flag == 1 )
if (flag == FALSE)
if (flag == 0)
应当将整型变量用“==”或“!= ”直接与0 比较。假设整型变量的名字为value,它与零值比较的标准if 语句如下:
if (value == 0)
if (value != 0)
不可模仿布尔变量的风格而写成
if (value) // 会让人误解 value是布尔变量
if (!value)
不可将浮点变量用“==”或“!= ”与任何数字比较。千万要留意,无论是f loat 还是double类型的变量,都有精度限制。所以一定要避免将浮点变量用“==”或“!= ”与数字比较,应该设法转化成“>=”或“<=”形式。
假设浮点变量的名字为x ,应当将
if (x == 0.0) // 隐含错误的比较
转化为
if ((x>=-EPSINON) && (x<=EPSINON))
其中EPSINON 是允许的误差(即精度)
应当将指针变量用“==”或“!= ”与NULL比较。指针变量的零值是“空”(记为NULL)。尽管NUL L 的值与0 相同,但是两者意义不同。假设指针变量的名字为p ,它与零值比较的标准if 语句如下:
if (p == NULL) // p与NULL显式比较,强调p 是指针变量
if (p != NULL)
不要写成
if (p == 0) // 容易让人误解p 是整型变量
if (p != 0)
或者
if (p) // 容易让人误解p 是布尔变量
if (!p)
关于if语句的补充点:if (NULL == p) 这样古怪的格式,并不是程序员写错了,是为了防止将if (p == NULL)误写成if (p = NULL) ,而有意把p和NULL颠倒。编译器认为 if (p = NULL) 是合法的,但是会指出 if (N ULL = p)是错误的,因为NULL不能被赋值。
C++/C循环语句中,for 语句使用频率最高,w hile语句其次,do语句很少用。这里从for循环出发,提出几点需要注意的点:
1)在多重循环中,如果有可能,应当将最长的循环放在最内层,最短的循环放在最外层,以减少CPU 跨切循环层的次数。
例如以下图右比左执行效率高:
2)如果循环体内存在逻辑判断,并且循环次数很大,宜将逻辑判断移到循环体的外面。如下图右框内的程序,简洁度略有下降,但是执行效率却大幅度提高。
3)不可在for 循环体内修改循环变量,防止for 循环失去控制。
4)建议for 语句的循环控制变量的取值采用“半开半闭区间”写法。如下左所示:
Switch语句格式如下所示:
注意点:
1)每个cas e 语句的结尾不要忘了加break,否则将导致多个分支重叠(除非有意使多个分支重叠)。
2)不要忘记最后那个default 分支。即使程序真的不需要default 处理,也应该保留语句 default : break; 这样做并非多此一举,而是为了防止别人误以为你忘了default 处理。
C 语言用 #define来定义常量(称为宏常量)。C++ 语言除了 #define外还可以用const来定义常量(称为const常量),常量是一种标识符,它的值在运行期间恒定不变。
这个在笔试面试的时候也考过几次啊,属于比较基础比较细节的东西。总结下来主要是不使用常量的话有以下三个缺点:
1 ) 程序的可读性(可理解性)变差。程序员自己会忘记那些数字或字符串是什么意思,用户则更加不知它们从何处来、表示什么。
2 ) 在程序的很多地方输入同样的数字或字符串,难保不发生书写错误。
3 ) 如果要修改数字或字符串,则会在很多地方改动,既麻烦又容易出错。
但注意,我们在定义常量时要尽量使用含义直观的常量来表示那些将在程序中多次出现的数字或字符串。
例如:
#define MAX 100 /* C 语言的宏常量 */
const int MAX = 100; // C++ 语言的const 常量
const float PI = 3.14159; // C++ 语言的const 常量
C++ 语言可以用const和#define两种定义常量的方式,但是建议使用前者,因为:
1)const常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误(边际效应)。
2)有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调试。
简单说来,定义常量的时候,最好遵循以下规则:
1)需要对外公开的常量放在头文件中,不需要对外公开的常量放在定义文件的头部。为便于管理,可以把不同模块的常量集中存放在一个公共的头文件中。
2)如果某一常量与其它常量密切相关,应在定义中包含这种关系,而不应给出一些孤立的值。
例如: const float RADIUS = 100;
const float DIAMETER = RADIUS * 2;
非常重要的一点,也是常犯的错误,有时我们希望某些常量只在类中有效,所以想当然地觉得应该用const修饰数据成员来实现。但是const 数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的,因为类可以创建多个对象,不同的对象其const 数据成员的值可以不同。
所以:不能在类声明中初始化const 数据成员。因为类的对象未被创建时,编译器不知道SIZE的值是什么。const 数据成员的初始化只能在类构造函数的初始化表中进行。
如果想建立在整个类中都恒定的常量。const数据成员是完成不了滴,应该用类中的枚举常量来实现。
class A
{…
enum { SIZE1 = 100, SIZE2 = 200}; // 枚举常量
int array1[SIZE1];
int array2[SIZE2];
};
枚举常量不会占用对象的存储空间,它们在编译时被全部求值。枚举常量的缺点是:它的隐含数据类型是整数,其最大值有限,且不能表示浮点数(如PI=3.14159)。