Skip to content

伪规则和错误的看法

请不要盲目的相信所谓的“经验”、“通俗的智慧”,在很多时候这些东西显得非常的可笑,他们可能来自于四十年前,或者其他的编程语言;信奉教条主义是愚蠢的行为。

不要吹毛求疵,本人非常的清楚某些非常愚蠢的规则在某些时候和场景会使用,毕竟这些规则不会凭空产生,这很合理,不要认为本人对此不清楚。

伪规则概览:

1:请勿坚持认为声明都应当放在函数的最上面

理由

“所有声明都在开头”的规则,来自于上古时代 C89 的规定,或者其他老编程语言。

在 C89 中,任何复合语句[1](块作用域)中的声明必须出现在块的开头,在任何语句之前。

这样做会导致更长的程序,以及更多由于未初始化的或者错误初始化的变量所导致的错误。

示例,不好

cpp
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 语句

理由

单返回规则会导致不必要地复杂的代码,并引入多余的状态变量。特别是,单返回规则导致更难在函数开头集中进行错误检查。

示例

cpp
std::string sign(int x){
    if (x < 0)
        return "小于0";
    if (x > 0)
        return "大于0";
    return "等于0";
}

为仅使用一个返回语句,我们得做类似这样的事:

cpp
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。

txt
int CheckPasswordCorrect(int password){
    if (!password) return -1;
    // todo...
    return i;
}

如果我们采纳这条规则的话,得做类似这样的事:

txt
int CheckPasswordCorrect(int password){
    int i;
    if (!password) 
        i = -1;     // 错误指示
    else{
        // ...
    }
    return i;
}

注意我们(故意地)违反了禁止未初始化变量的规则,因为这种风格通常都会导致这样。

应当遵守

  • 保持函数短小简单。
  • 随意使用多个 return 语句(以及抛出异常)。

3:请勿坚持把每个类定义放在其自己的源文件中

这个规则其实应该来自于隔壁 java,一个文件必须有一个公开的类,很多时候都是一个文件一个类。

将每个类都放进其自己的文件所导致的文件数量难于管理,并会拖慢编译过程。 单个的类很少是一种良好的维护和发布的逻辑单位。

4:请勿采用两阶段初始化

将初始化拆分为两步会导致不变式的弱化,更复杂的代码(必须处理半构造对象),以及错误(当未能一致地正确处理半构造对象时)。

同时,类不应有 init(初始化)成员函数,不然就是自找麻烦。

示例,不好

cpp
class DiskFile{
    FILE* f;
    // ...
public:
    DiskFile() = default;
    void init();        // 初始化 f
    void read();        // 从 f 读取
    // ...
};

int main(){
    DiskFile file;
    file.read();     // 崩溃,或错误读取!
    file.init();     // 太晚了
    //
}

更多时候 init 函数可能是一个全局函数,都同理。

示例,好

cpp
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 源码。

示例,不好

txt
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,是公共继承的。

cpp
private:
    element_type* _Ptr{nullptr};
    _Ref_count_base* _Rep{nullptr};

如你所见,在 _Ptr_base 父类中将这两个成员声明为了 private,而不是 protected,那么子类 std::shared_ptr 是如何使用这两个成员的呢?

很简单,用 friend

cpp
friend shared_ptr<_Ty>;

应当遵守

  • 使用 public 或 private。

  1. 复合语句,或称块,是花括号所包围的语句与声明的序列。{ 语句 | 声明...(可选) },函数体,或任意 {} 都是复合语句。 ↩︎