风格与技巧

杂项风格问题

int* p; 对还是 int *p; 对?

两者都“对”,因为它们都是有效的 C 和 C++ 代码,并且具有完全相同的含义。就语言定义和编译器而言,我们也可以写 int*p;int * p;

选择 int* p;int *p; 不是关于对错,而是关于风格和强调。C 强调表达式;声明通常被认为是不得已而为之的必要之恶。而 C++ 则非常强调类型。

“典型的 C 程序员”会写 int *p; 并解释为“*pint 类型”,强调语法,并可能引用 C(和 C++)的声明语法来论证这种风格的正确性。事实上,在语法中,* 是与名称 p 绑定的。

“典型的 C++ 程序员”会写 int* p; 并解释为“p 是一个指向 int 的指针”,强调类型。实际上,p 的类型是 int*。C++ 的思维方式更倾向于这种强调,并认为这对于更好地使用 C++ 更高级的部分非常重要。

关键的混淆(只)发生在人们尝试在一个声明中声明多个指针时

    int* p, p1; // probable error: p1 is not an int*

* 靠近名称并不会显著降低这类错误的发生率。

    int *p, p1; // probable error?

每个声明只声明一个名称能最大程度地减少问题——尤其是在我们初始化变量时。人们不太可能写出

    int* p = &i;
    int p1 = p; // error: int initialized by int*

如果他们这样做了,编译器会报错。

每当一件事可以用两种方式完成时,就会有人感到困惑。每当一件事是品味问题时,讨论就会无休止地拖延下去。坚持每个声明只定义一个指针,并始终初始化变量,这样混淆的根源就会消失。有关 C 声明语法的更详细讨论,请参阅《C++ 的设计与演化》

哪种布局风格最适合我的代码?

此类风格问题属于个人喜好。通常,关于代码布局的看法根深蒂固,但可能一致性比任何特定风格都更重要。和大多数人一样,你很难为自己的偏好构建一个坚实的逻辑论证。

设计问题,例如将抽象类用于主要接口,使用模板提供灵活的类型安全抽象,以及正确使用异常来表示错误,远比布局风格的选择重要。

你如何命名变量?你推荐匈牙利命名法吗?

存在两种“匈牙利命名法”——“系统匈牙利命名法”,即在变量名中编码类型的那种,被广泛认为是反模式。本常见问题解答讨论的就是这种匈牙利命名法。

在变量名中编码类型这种技术在非类型语言中可能有用,但完全不适用于支持泛型编程和面向对象编程的语言——这两种编程都强调根据参数的类型(由语言或运行时支持可知)选择操作。在这种情况下,“将对象的类型构建到名称中”只会使抽象复杂化并最小化。在不同程度上,你会遇到所有将关于语言技术细节(例如,作用域、存储类、语法类别)的信息嵌入到名称中的方案的类似问题。是的,在某些情况下,将类型提示构建到变量名中可能有所帮助,但通常,特别是随着软件的演进,这会成为维护隐患,严重损害良好代码。避之如瘟疫。

如果经过所有这些之后,您仍然想冒险在变量名中编码类型,悉听尊便:如果您决定玩“匈牙利游戏”,愿胜算永远对您有利。

继续前进……

那么,如果你不喜欢根据类型来命名变量,你应该喜欢和推荐什么呢?根据变量(函数、类型等)的用途或作用来命名。选择有意义的名称;也就是说,选择有助于人们理解你的程序的名称。如果你用 x1x2s3p7 这样容易输入的变量名来填充你的程序,即使是你自己也会很难理解你的程序应该做什么。缩写和首字母缩略词可能会让人困惑,所以要谨慎使用。首字母缩略词应该谨慎使用。考虑一下 mtbfTLAmywRTFMNBV。它们很明显,但过几个月你就会忘记至少一个。

xi 这样的短名称,在习惯用法下是有意义的;也就是说,x 应该是一个局部变量或参数,而 i 应该是一个循环索引。这没什么不对的。

不要使用过长的名称;它们很难输入,使行太长以至于屏幕放不下,并且难以快速阅读。这些可能还可以

    partial_sum    element_count    stable_partition

这些可能太长了

    the_number_of_elements    remaining_free_slots_in_symbol_table

ISO 标准倾向于在标识符中使用下划线来分隔单词(例如,`element_count`),而不是其他替代方案,例如 `elementCount` 和 `ElementCount`。切勿使用全大写字母的名称(例如,`BEGIN_TRANSACTION`),因为这通常是为宏保留的。即使您不使用宏,也可能有人在您的头文件中使用了它们。一种常见的做法是,类型使用首字母大写(例如,`Square` 和 `Graph`)。C++ 语言和标准库不使用大写字母,因此是 `int` 而不是 `Int`,`string` 而不是 `String`。这样,您就可以识别标准类型。

避免容易打错、读错或混淆的名称。例如

    name    names    nameS
    foo     f00
    fl      f1       fI       fi

字符 0, o, O, 1, lI 特别容易引起麻烦。

通常,你对命名约定的选择受到本地风格规则的限制。请记住,保持一致的风格通常比以你认为最好的方式处理每一个小细节更重要。

我应该把 `const` 放在类型之前还是之后?

许多作者将其放在前面,但这只是个人喜好问题。`const T` 和 `T const` 过去和现在都允许且等效。例如

    const int a = 1;    // ok
    int const b = 2;    // also ok

使用第一种写法可能会减少程序员的困惑(“更符合惯用法”)。

请注意,在 const 指针中,const 始终位于 * 之后。例如

    int *const p1 = q;  // constant pointer to int variable
    int const* p2 = q;  // pointer to constant int
    const int* p3 = q;  // pointer to constant int

static_cast 有什么用?

通常最好避免使用强制类型转换。除了 dynamic_cast 之外,它们的使用意味着可能存在类型错误或数值截断。即使是看起来无害的强制类型转换,如果在开发或维护过程中,所涉及的类型之一发生变化,也可能成为严重问题。例如,这表示什么?

    x = (T)y;

我们不知道。它取决于类型 `T` 以及 `x` 和 `y` 的类型。`T` 可能是类的名称、类型别名,或者可能是模板参数。也许 `x` 和 `y` 是标量变量,`(T)` 代表值转换。也许 `x` 是 `y` 的派生类,而 `(T)` 是向下转型。也许 `x` 和 `y` 是不相关的指针类型。由于 C 风格的强制类型转换 `(T)` 可以用来表达许多逻辑上不同的操作,编译器只有极小的机会捕获误用。出于同样的原因,程序员可能不确切知道强制类型转换的作用。这有时被新手程序员视为一个优点,但当新手猜测错误时,它也是细微错误的来源。

引入“新式类型转换”是为了让程序员有机会更清晰地表达他们的意图,并让编译器捕获更多错误。例如

    int a = 7;
    double* p1 = (double*) &a;          // ok (but a is not a double)
    double* p2 = static_cast<double*>(&a);  // error
    double* p2 = reinterpret_cast<double*>(&a); // ok: I really mean it

    const int c = 7;
    int* q1 = &c;           // error
    int* q2 = (int*)&c;     // ok (but *q2=2; is still invalid code and may fail)
    int* q3 = static_cast<int*>(&c);    // error: static_cast doesn't cast away const
    int* q4 = const_cast<int*>(&c); // I really mean it

其理念是,`static_cast` 允许的转换比需要 `reinterpret_cast` 的转换导致错误的可能性要小一些。原则上,可以使用 `static_cast` 的结果而无需将其强制转换回其原始类型,而 `reinterpret_cast` 的结果在使用之前应始终将其强制转换回其原始类型,以确保可移植性。

引入新式类型转换的次要原因是,C 风格的类型转换在程序中很难发现。例如,您无法方便地使用普通编辑器或文字处理器搜索类型转换。C 风格类型转换的这种近乎不可见性尤其不幸,因为它们潜在的破坏性很大。一个丑陋的操作应该有一个丑陋的语法形式。这一观察结果是选择新式类型转换语法的部分原因。另一个原因是新式类型转换与模板符号相匹配,这样程序员就可以编写自己的类型转换,尤其是运行时检查的类型转换。

也许,因为 static_cast 如此丑陋且相对难以输入,你更有可能在使用它之前三思?那会是好事,因为在现代 C++ 中,类型转换大部分是可以避免的。

那么,使用宏有什么问题呢?

宏不遵守 C++ 的作用域和类型规则。这通常是细微和不那么细微问题的原因。因此,C++ 提供了更符合 C++ 其余部分的替代方案,例如内联函数、模板和命名空间。

考虑

    #include "someheader.h"

    struct S {
        int alpha;
        int beta;
    };

如果有人(不明智地)编写了一个名为 `alpha` 或 `beta` 的宏,这可能无法编译,或者(更糟)编译成意想不到的东西。例如,`someheader.h` 可能包含

    #define alpha 'a'
    #define beta b[2]

诸如将宏(且仅将宏)全部大写的约定有所帮助,但没有语言级别的宏保护。例如,成员名称在结构体作用域内这一事实并没有帮助:宏在编译器真正看到程序之前,将其作为一个字符流进行操作。顺便说一句,这是 C 和 C++ 程序开发环境和工具一直不完善的主要原因:人类和编译器看到的是不同的东西。

不幸的是,你不能指望其他程序员始终避免你认为“真正愚蠢”的做法。例如,程序员报告了包含 `goto` 的宏的存在,并且听到了一些论点——在脆弱的时刻——似乎有道理。例如

    #define prefix get_ready(); int ret__
    #define Return(i) ret__=i; do_something(); goto exit
    #define suffix exit: cleanup(); return ret__

    int f()
    {
        prefix;
        // ...
        Return(10);
        // ...
        Return(x++);
        //...
        suffix;
    }

想象一下,作为一名维护程序员,你被要求处理这种情况;将宏“隐藏”在头文件中——这并不少见——使得这种“魔法”更难发现。

一个最常见的微妙问题是,函数式宏不遵守函数参数传递规则。例如

    #define square(x) (x*x)

    void f(double d, int i)
    {
        square(d);  // fine
        square(i++);    // ouch: means (i++*i++)
        square(d+1);    // ouch: means (d+1*d+1); that is, (d+d+1)
        // ...
    }

d+1”问题可以通过在“调用”或宏定义中添加括号来解决

    #define square(x) ((x)*(x)) /* better */

然而,`i++` 的(可能是无意的)双重求值问题依然存在。

是的,众所周知,有一些被称为宏的东西,它们没有 C/C++ 预处理器宏的问题。然而,C++ 社区通常没有改进 C++ 宏的雄心。相反,我们建议使用 C++ 语言本身的设施,例如 `inline` 函数、模板、构造函数(用于初始化)、析构函数(用于清理)、异常(用于退出上下文)等。

cout 怎么发音?

包括 Stroustrup 在内的许多人都将 cout 发音为“see-out”(类似“西奥特”)。“c”代表“character”(字符),因为 iostreams 将值映射为字节(char)表示,并从字节表示映射回值。

很多人将它发音为与“gout”(痛风)和“spout”(喷口)押韵。

char 怎么发音?

许多人将 `char` 发音为与英语动词“char”(如,在火中烧焦木头)相同。另一些人则将其发音为英语单词“care”(关心)的音,与“character”(字符)的第一个音节相同。

但你想怎么发音就怎么发音,我们不关心(`char`)。