伪规则和错误的看法
请不要盲目的相信所谓的“经验”、“通俗的智慧”,在很多时候这些东西显得非常的可笑,他们可能来自于四十年前,或者其他的编程语言;信奉教条主义是愚蠢的行为。
不要吹毛求疵,本人非常的清楚某些非常愚蠢的规则在某些时候和场景会使用,毕竟这些规则不会凭空产生,这很合理,不要认为本人对此不清楚。
伪规则概览:
- 1:请勿坚持认为声明都应当放在函数的最上面
- 2:请勿坚持使函数中只保留一个 return 语句
- 3:请勿坚持把每个类定义放在其自己的源文件中
- 4:请勿采用两阶段初始化
- 5:请勿把所有清理操作放在函数末尾并使用 goto exit
- 6:请勿使所有数据成员 protected
1:请勿坚持认为声明都应当放在函数的最上面
理由
“所有声明都在开头”的规则,来自于上古时代 C89 的规定,或者其他老编程语言。
在 C89 中,任何复合语句[1](块作用域)中的声明必须出现在块的开头,在任何语句之前。
这样做会导致更长的程序,以及更多由于未初始化的或者错误初始化的变量所导致的错误。
示例,不好
int use(int x)
{
int i;
char c;
double d;
// ... 做一些事 ...
// 应该在这里声明和初始化 i 和 d
if (x < i) {
// ...
i = f(x, d);
}
// 应该在这里声明和初始化 c
if (i < x) {
// ...
i = g(x, c);
}
return i;
}
未初始化变量和其使用点的距离越长,出现 BUG 的机会就越大,我们应该到确定需要在这里使用一个变量的时候再创建并初始化。
应当遵守
- 坚持为对象进行初始化。
- 不要在确实需要使用变量(或常量)之前就引入它。
2:请勿坚持使函数中只保留一个 return 语句
理由
单返回规则会导致不必要地复杂的代码,并引入多余的状态变量。特别是,单返回规则导致更难在函数开头集中进行错误检查。
示例
std::string sign(int x){
if (x < 0)
return "小于0";
if (x > 0)
return "大于0";
return "等于0";
}
为仅使用一个返回语句,我们得做类似这样的事:
std::string sign(int x){
std::string res;
if (x < 0)
res = "小于0";
else if (x > 0)
res = "大于0";
else
res = "等于0";
return res;
}
这不仅更长,而且很可能效率更差。越长越复杂的函数,对其进行变通就越是痛苦。 当然许多简单的函数因为它们本来就简单的逻辑都天然就只有一个 return。
int CheckPasswordCorrect(int password){
if (!password) return -1;
// todo...
return i;
}
如果我们采纳这条规则的话,得做类似这样的事:
int CheckPasswordCorrect(int password){
int i;
if (!password)
i = -1; // 错误指示
else{
// ...
}
return i;
}
注意我们(故意地)违反了禁止未初始化变量的规则,因为这种风格通常都会导致这样。
应当遵守
- 保持函数短小简单。
- 随意使用多个 return 语句(以及抛出异常)。
3:请勿坚持把每个类定义放在其自己的源文件中
这个规则其实应该来自于隔壁 java,一个文件必须有一个公开的类,很多时候都是一个文件一个类。
将每个类都放进其自己的文件所导致的文件数量难于管理,并会拖慢编译过程。 单个的类很少是一种良好的维护和发布的逻辑单位。
4:请勿采用两阶段初始化
将初始化拆分为两步会导致不变式的弱化,更复杂的代码(必须处理半构造对象),以及错误(当未能一致地正确处理半构造对象时)。
同时,类不应有 init(初始化)成员函数,不然就是自找麻烦。
示例,不好
class DiskFile{
FILE* f;
// ...
public:
DiskFile() = default;
void init(); // 初始化 f
void read(); // 从 f 读取
// ...
};
int main(){
DiskFile file;
file.read(); // 崩溃,或错误读取!
file.init(); // 太晚了
//
}
更多时候 init 函数可能是一个全局函数,都同理。
示例,好
class Date{
public:
Date();
//校验 {yy,mm,dd}是不是合法的日期并进行初始化
Date(int yy, Month mm, char dd);
int day()const;
Month month()const;
// ...
private:
// ... 具体的内部表示
};
在构造函数进行初始化,有参构造检查参数合法性,保证类的不变式。
应当遵守
- 构造函数应当创建完全初始化的对象。
- 始终在构造函数中建立类不变式。
5:请勿把所有清理操作放在函数末尾并使用 goto exit
理由
goto 是易错的。这种技巧是进行 RAII 式的资源和错误处理的前异常时代的技巧。
常见于 Linux 源码。
示例,不好
void do_something(int n)
{
if (n < 100) goto exit;
// ...
int* p = (int*) malloc(n);
// ...
if (some_error) goto exit;
// ...
exit:
free(p);
}
goto exit
跳过了“p”的初始化。
应当遵守
- 使用 RAII
6:请勿使所有数据成员 protected
理由
protected
数据是一种错误来源。protected
数据可以被各种地方的无界限数量的代码所操纵。protected
数据是在类层次中等价于全局对象的东西。
这可能让各位觉得违反常识,因为如果你子类要用父类的成员,通常需要它是 protected 权限的。 但是,不声明为 protected,其实就是不想让类能被继承后直接使用它的成员。
如果有子类需要使用父类成员的情况,通常是特殊处理的,我们将它在父类声明为
friend
,以此来控制。
示例
这里以 msvc STL 的 std::shared_ptr 的实现为例:
std::shared_ptr 只保有两个数据成员,一个指向资源的指针,一个指向控制块的指针,这两个成员来自它的父类 _Ptr_base,是公共继承的。
private:
element_type* _Ptr{nullptr};
_Ref_count_base* _Rep{nullptr};
如你所见,在 _Ptr_base 父类中将这两个成员声明为了 private,而不是 protected,那么子类 std::shared_ptr 是如何使用这两个成员的呢?
很简单,用 friend。
friend shared_ptr<_Ty>;
应当遵守
- 使用 public 或 private。
复合语句,或称块,是花括号所包围的语句与声明的序列。
{ 语句 | 声明...(可选) }
,函数体,或任意{}
都是复合语句。 ↩︎