推荐文档:Google C++ 编程风格
头文件依赖与前向声明
当我们在头文件中需要类的定义时,只需要声明 class CLASSNAME 就可以了,无需使用#include “CLASSNAME.h”
。
优势:
- 减少文件依赖,节约编译时间;
- 更加明确的类的依赖关系。
函数的参数顺序
参数的声明顺序为,输入参数在前,输出参数在后。
- 输入参数一般为传值和常数引用;
- 输出参数或输入/输出参数一般为非常数指针。
优势:
- 通过位置明确参数的作用;
- 利于代码的合并(用合并软件)。
示例:
class Foo;
void Command(const int nInputParam1,
const float nInputParam2,
const Foo& nInputClass1,
const Foo* nInputClass2,
void* pOutputParam1);
头文件包含的顺序
顺序:
- 先包含类对应头文件
- C 系统头文件
- C++ 系统头文件
- 其他库头文件
- 本项目内头文件
之间通过空行区分。
优势:
- 增加可读性
- 利于代码合并
局部变量的初始化
在尽可能小的作用域中声明变量,离第一次使用的位置越近越好。
好处:
- 使得代码易读,避免一些错误。
对于结构体或数组,声明以后,也应立刻进行初始化操作:
TOOLTIP tooltip = {0};
char buff[1024] = {0};
memset(&tooltip, 0, sizeof(tooltip));
memset(buff, 0, 1024);
局部类的初始化
一个类的局部变量会默认做一次构造和析构,应注意避免多次的初始化与析构。
示例:
1 2 3 4 | for(int a = 0; a < 1000; a++){ Foo f; f.doFun(); } |
在循环内初始化的 f 会构造和析构 1000 次。应改为:
1 2 3 4 | Foo f; for(int a = 0; a < 1000; a++){ f.doFun(); } |
类的初始化(构造函数)
- 注意类成员的初始化,养成良好的类成员声明习惯(声明后立刻在构造函数中进行初始化操作);
- 类的初始化虽然很简单,但很多程序员忘记做了,导致不可预知的问题(因为导致变量的初始值不可预料)。
类的拷贝构造函数
编译器会默认为我们的类提供一个赋值操作符函数和拷贝构造函数,但同时也会带来问题。
注意:
- 禁用不必要的默认拷贝构造和赋值函数。
- 函数的参数,尽量使用引用或指针,避免拷贝或赋值构造。
深拷贝与浅拷贝:
默认的浅拷贝,两个对象的指针指向相同一块内存。
深拷贝示例:
// 重载赋值操作符
void operator=(const Foo& srcFoo){
if(this == &srcFoo) // 防止自复制
return;
if(srcFoo.m_pszName){
size_t nSize = _tcslen(srcFoo.m_pszName);
if(nSize > 0){
m_pszName = new wchar_t[nSize + 1];
_tcscpy_s(m_pszName, nSize + 1, srcFoo.m_pszName);
}
}
}
可以通过宏禁止不提供的拷贝函数,也就是将这些方法声明为私有函数:
// 禁止使用拷贝构造函数和赋值操作的宏
// 应在类的 private: 中使用
#define DISALLOW_COPY_AND_ASSIGN(TypeName) \
TypeName(const TypeName&); \
void operator=(const TypeName&)
class Foo {
private:
DISALLOW_COPY_AND_ASSIGN(Foo);
...
结构体和类
区别:在 C++ 中,struct 和 class 可以起到同样的作用,唯一的区别是 struct 默认方法和成员是 public 的,class 默认是 private 的。
(struct 也可以和 class 一样使用构造和析构函数对数据进行初始化和释放。)
我们约定仅当只有数据的时候使用struct,其他一般使用class。
操作符重载
操作符重载可以为我们的代码编写提供便利,但是也会使我们的代码变得不够直观。
例如增加一个 Equals
方法比直接对 ==
操作符重载要来的直观得多。
因此我们一般不要对操作符进行重载。
将类的成员私有化
面向对象编程的一个原则是隐藏内部的数据。有时候我们为了方便直接将数据声明为 public,应避免这种做法,虽然一时间节约了不少编码时间,但有时也会代码极大的不便。
优势:
- 代码封装性好;
- 有利于调试(因为所有对成员变量的调试场景都在 get 和 set 方法中)。
类中的声明顺序
顺序:
- public
- protected
- private
没有的块可以忽略。
块内顺序:
- typedef 和 enums
- 常量
- 构造函数
- 析构函数
- 成员函数,含静态成员函数
- 数据成员,含静态数据成员
优势:
- 代码封装性好;
- 有利于调试。
编写短小的函数
如果没有特殊的需求,函数的长度控制在40行左右。如果过长,不影响程序的运行的情况将长函数进行分割。
优势:
- 提取重复的代码;
- 便于他人阅读和修改;
- 便于发现和定位bug。
引用参数与 const
如果传入引用型参数,请务必添加上 const。
优势:
- 约束调用行为;
- 自解释了参数的作用。
尽量使用 const
尽量使用 const 定义参数类型。
使用的条件:
- 函数不会修改的引用或指针类型的参数;
- 不修改数据的函数,指定为 const;
- 如果类的成员变量在构造后不会改变,声明为const。
优势:
- 约束变量的操作行为。
示例:
bool Equal(const Point& p1, const Point* p2) const {
}
// 函数的参数,尽量使用引用或指针,避免拷贝或赋值构造。
其他
- 没有特殊要求,尽量不进行函数重载,通过函数名称进行区分。过多的重载函数有时也会是调用者无从选择。
- 禁用缺省参数。避免调用者理解错误。
- 禁用 RTTI(不使用 dynamic_cast)。运行时类型识别本身就说明设计存在问题。利用类型号或者 virtual 方法都可以做到同样的功能。
- 前置自增或自减的效率要好于后置,特别是迭代器,区别非常大。
- 尽可能用 sizeof(varname) 而不是 sizeof(type),防止变量类型在运行时被改变了。
变量命名
- 不要定义无法自解释的名称;
- 不要拼写错误或者拼写不完整;
- 类名称全部以大写开头;
- 结构体,枚举全部大写开头;
- 变量命名一律小写开头;
- 类成员变量以 m_ 开头;
- 所有的全局变量使用 g_ 开头;
- 常量使用小写 k 开头。如
const int m_kIndex
; - 函数名称以大写开头(我之前都是小写的..),单词之前首字母大写;
- 类的存取函数,取函数以Get开头,设置函数以Set开头;
- 枚举类型中的值全部大写表示;
- 宏名称全部大写,单词之间通过下划线进行分割。
0、NULL 与初始化
- 整数用0
- 实数用0.0
- 指针用NULL
- 字符串用“\0”
优势:通过初始化的值就可以判断变量的类型。
匈牙利命名规则
类型 + 作用名称
如 bVisible, pVecBooks, nIndexDay
类型 | 字符 | 示例 |
---|---|---|
指针 | p | pWnd pData |
函数 | fn | fnCalc |
无效 | v | vData |
句柄 | h | hBitmap |
长整型 | l | lDays |
布尔 | b | bVisible |
浮点型(有时也指文件) | f | fAngle |
双字 | dw | dwStyle |
字符串 | str | strName |
短整型 | n | nIndex |
双精度浮点 | d | dRotate |
计数 | c(通常用cnt) | cIndex |
字符 | ch(通常用c) | cByte |
整型 | i(我们通常用n) | iIndex |
字节 | by | byData |
字 | w | wChar |
实型 | r | rPi |
无符号 | u | uSize |
格式要求
- 一行代码不要过长;
- 空格作为制表符,两个空格代表一个制表符(VS 设置 - 所有语言 - 制表符,制表符大小和缩进大小都设置为2);
- 函数声明中返回类型和函数名在同一行中,如果过长对行数的参数进行分行处理,并进行对齐;
- 函数调用和返回放在同一行中,如果代码过长,对传入的参数进行分行处理;
- 条件判断的 if 要加括号,左/右大括号要另起一行,else 也是一样;
- 加减等操作符前后添加空格。