5.10 前置自增和自减 Preincrement and Predecrement
Tip 对于迭代器iterator和其他模板对象template object使用前缀形式(++i)的自增, 自减运算符;
定义:
对于变量在自增(++i 或 i++)或自减(--i 或 i--)后, 表达式的值没有被用到的情况下, 需要确定到底是使用前置还是后置的自增(自减);
优点:
不考虑返回值的话, 前置pre自增(++i)通常要比后置post自增(i++)效率更高; 因为后置自增(自减)需要对表达式的值 i 进行一次拷贝; 如果i是迭代器或其他非数值 non-scalar类型, 拷贝的代价是比较大的; 既然两种自增方式实现的功能一样, 为什么不总是使用前置自增呢?
缺点:
在C开发中, 当表达式的值未被使用时, 传统的做法还是使用后置自增, 特别是在 for循环中; 有些人觉得后置自增更加易懂, 因为这很像自然语言, 主语subject(i)在谓语动词precede(++)前;
[C语言中没有class类型, 基本上POD不必在意前置或后置的效率区别]
结论:
对简单数值scalar(非对象non-object), 两种都无所谓; 对迭代器和模板类型, 使用前置自增(自减);
5.11 const的使用 Use of const
Tip 强烈建议在任何可能的情况下都要使用 const; [Add] c++11中, constexpr对于某些const使用情况是更好的选择; [http://en.cppreference.com/w/cpp/language/constexpr]
<<<
定义:
在声明的变量或参数前preceded加上关键字 const用于指明变量值不可被篡改(如 const int foo); 为类中的函数加上 const限定符qualifier表明该函数不会修改类成员变量的状态(如 class Foo { int Bar(char c) cosnt; }; )
优点:
大家更容易理解如何使用变量; 编译器可以更好地进行类型检测; 相应地, 也能生成更好的代码; 人们对编写正确的代码更加自信, 因为他们知道所调用的函数被限定了能或不能修改变量值; 即使是在无锁的多线程编程中without locks in multi-threaded, 人们也知道什么样的函数是安全的;
缺点:
const是入侵性viral的: 如果你向一个函数传入 const变量, 函数原型声明中也必须对应 const参数(否则变量需要 const_cast类型转换), 在调用库函数是显得尤其麻烦;
结论:
const变量, 数据成员, 函数和参数为编译时类型检测增加了一层保障: 便于尽早发现错误; 因此, 我们强烈建议在任何可能的情况下使用 const;
- 如果函数不会修改传入的引用或指针类型参数, 该参数应声明为const;
- 尽可能将函数声明为const; 访问函数几乎总是const; 其他不会修改任何数据成员, 没有调用非const函数, 不会返回数据成员非const指针或引用的函数也应声明成const;
- 如果数据成员在对象构造之后不再发生变化, 可将其定义为const;
[Remove] 然而, 也不要发疯似的使用const; 像 const int* const* const x; 就有些过了, 虽然它非常精确地描述了常量 x; 关注真正有帮助一样的信息: 前面的例子写成 const int** x就够了; [内容不可变] <<<
关键字 mutable可以使用, 但是在多线程中是不安全的, 使用时首先要考虑线程安全性;
const的位置 Where to put the const:
有人喜欢 int const *foo形式, 不喜欢 const int* foo; 他们认为前者更一致因此可读性也更好: 遵循了const总位于其描述的对象之后的原则; 但是一致性原则不适用于此, 由于多数const表达式只有一个const, 而且应用的是一个值, 很少有深层嵌套的指针表达式few deeply-nested pointer expressions; "不要过度使用"的声明可以取消大部分你原本想保持的一致性;
将const放在前面才更易读, 因为在自然语言中形容词adjective(const)是在名词noun(int)之前;
这是说, 我们提倡但不强制const在前; 但要保持代码的一致性; (译注, 就是不要在一些地方把const写在类型前面, 在其他地方又写在后面, 要确定一种写法, 然后保持一致);
[Add]
使用constexpr Use of constexpr
C++11中, 使用constexpr来定义true的常量或确保常量初始化constant initialization;
定义:
一些变量可以被声明为constexpr, 表明变量是true的常量, e.g. 在编译/链接时是固定的; 一些函数和cotr可以被声明为constexpr, 让它们可以在定义一个constexpr变量时被使用;
优点:
使用constexpr定义浮点数floating-point表达式常量, 而不是字面量literal定义, 用户定义的类型的定义和函数调用function call的常量的定义;
缺点:
把某些东西定义为constexpr可能会在之后导致一些迁移migration问题, 或许不得不回退回去downgraded; 目前的对于constexpr函数和ctor的限制规定restriction可能会在这些定义中产生些隐晦的替代方案workaround;
结论:
constexpr定义可以在一些接口的const部分给出健壮的规格robust specification; 使用constexpr来指定真实常量true constants以及支持函数的定义; 使用constexpr来防止复杂的函数定义; 不要使用constexpr来强制内联 inline; [http://stackoverflow.com/questions/14391272/does-constexpr-imply-inline ]
<<<
5.12 整型 Integer Types
Tip C++内建整型中, 仅使用 int; 如果程序中需要不同大小的变量, 可以使用 <stdint.h>中长度精确precise-width的整型, 如 int16_t; [http://www.cplusplus.com/reference/cstdint/ ]
[Add] 如果你的变量表示了一个可以变大或者等于2^31(2GiB)的值, 使用一个64-bit类型, 比如int64_t; 记住即使你的值对于int来说不会过大, 它还是可能在一些中间计算中需要一个更大的类型; 如果不确定, 就选用更大的类型; <<<
定义:
C++没有指定整型的大小; 通常人们假定 short是16位, int是32位, long是32位, long long是64位; (bits)
优点:
保持声明统一性Uniformity;
缺点:
C++中整型大小因编译器和体系结构architecture的不同而不同;
结论:
<stdint.h>定义了 int16_t, uint32_t, int64_t等整型, 在需要确保guarantee 整型大小时可以优先preference使用它们代替 short, unsigned long long等; 在C整型中, 只使用 int; 在合适的情况下, 推荐使用标准类型如 size_t和ptrdiff_t;
如果已知整数不会太大, 我们常常会使用 int, 如循环计数loop counter; 在类似的情况下使用原生类型plain old int; 你可以认为int至少为32位, 但不要认为它会多于32位; 如果需要64位整型, 用 int64_t或 uint64_t;
对于大整数, 使用 int64_t;
不要使用 uint32_t等无符号整型, 除非有正当valid 理由, 比如在表示一个位组bit pattern而不是一个数值, 或是需要定义二进制补码溢出overflow modulo 2^N; 尤其是不要为了指出数值永远不会为负, 而使用无符号类型; 相反, 你应该用断言来保护数据;
<<<
[Add] 如果你的代码是一个返回大小的容器, 确保使用一个可适应accommodate任何使用的可能性的数据类型; 不确定的时候使用一个更大的类型而不是较小的类型;
当转化整型的时候要小心; 整型转换和提升promotion会引起一些反直觉non-intuitive的行为;
<<<
关于无符号整数 On Unsigned Integers:
有些人, 包括一些教科书作者, 推荐使用无符号整型表示非负数; 这种做法试图达到自我文档化self-documentation; 但是在C语言中, 这一优点被由其导致的bug所淹没outweighed; 看看下面的例子:
1 |
|
上述循环永远不会退出! 有时gcc会发现该bug并报警, 但大部分情况下都不会; 类似的bug还会出现在比较有符号变量和无符号变量时; 主要是C的类型提升机制会导致无符号类型的行为出乎你的意料;
因此, 使用断言来指出变量为非负数, 而不要使用无符号类型;
5.13 64位下的可移植性 64-bit Portability
Tip 代码应该对64位和32位系统友好; 处理打印printing, 比较comparison, 结构体对齐structure alignment时应该切记;
- 对于某些类型, printf()的指示符在32位和64位系统上可移植性不是很好; C99标准定义了一些可移植的格式化指示符; 不幸的是, MSVC7.1并非全部支持; 而且标准中也有所遗漏, 所以有时我们不得不自己定义一个丑陋的版本(头文件inttype.h仿标准风格):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
check table:
Type | DO NOT use | DO use | Notes |
---|---|---|---|
void * (or any pointer) |
%lx |
%p |
|
int64_t |
%qd , %lld |
%"PRId64" |
|
uint64_t |
%qu , %llu , %llx |
%"PRIu64" , %"PRIx64" |
|
size_t |
%u |
%"PRIuS" , %"PRIxS" |
C99 specifies %zu |
ptrdiff_t |
%d |
%"PRIdS" |
C99 specifies %td |
Note PRI*宏会被编译器扩展concatenated为独立字符串; 因此如果使用非常量的格式化字符串, 需要将宏的值而不是宏名插入格式中; 使用PRI*宏同样可以在 %后包含长度指示符,etc; 例如: printf("x = %30"PRIuS"\n", x); 在32位Linux将被展开为: printf("x = %30" "u" "\n", x); 编译器当成: printf("x = %30u\n",
x);处理; (译注: 这在MSVC6.0上不行, VC6编译器不会自动把引号间隔的多个字符串连接成一个长字符串);
- 记住 sizeof(void *) != sizeof(int); 如果需要一个指针大小的整数要用 intptr_t; [不同编译器, 系统不一样]
- 要非常小心地对待结构体对齐, 尤其是要持久化存储到磁盘上的结构体; (译注: 持久化--将数据按字节流顺序保存在磁盘文件或数据库中); 在64位系统中, 任何含有 int64_t/uint64_t成员的类/结构体, 缺省都以8字节在结尾对齐; 如果32位和64位代码要共用持久化在磁盘上的结构体; 需要确保两种体系结构下的结构体对齐一致;(packed) 大多数编译器都允许调整结构体对齐;
gcc中可使用 __attribute__((packed)); MSVC则提供了 #pragma pack()和 __deslspec(align());
(译注: 解决方案的项目属性栏里也可以直接设置) [VS的项目--solution]
[http://en.wikipedia.org/wiki/Data_structure_alignment ]
- 创建64位常量时使用 LL或 ULL作为后缀suffixes, 如:
1 2 |
|
- 如果你确实需要32位和64位系统具有不同代码, 可以使用 #ifdef _LP64指令在代码变量中区分 32/64位代码; (尽量不要这么做, 如果非用不可, 尽量使修改局部化);
1 2 3 |
|
]
5.14 预处理宏 Preprocessor Macros
Tip 使用宏时要非常谨慎, 尽量以内联函数, 枚举和常量替代之;
宏意味着你和编译器看到的代码是不同的; 这可能会导致异常行为, 尤其因为宏具有全局作用域;
值得庆幸的是, C++中, 宏不像在C中那么必不可少; 以往用宏展开性能关键performance-critical的代码, 现在可以用内联函数替代; 用宏表示的常量可以被 const变量代替; 用宏"缩写"长变量的别名abbreviate可被引用代替reference; [以及typedef]; 用宏进行条件编译...这个, 千万别这么做, (#define防止头文件重复包含当然是个特例) 会令测试更加痛苦;
宏可以做一些其他技术无法实现的事情, 在一些代码库(尤其是底层库中)可以看到宏的某些特性(如用#字符串化stringifying, 用##连接concatenation等); 但在使用前, 仔细考虑一下能不能不使用宏达到同样的目的; [Hack: private和public]
下面给出的用法模式可以避免使用宏带来的问题; 如果要用宏, 尽可能遵守:
- 不要在 .h文件中定义宏;
- 在马上要使用时才进行 #define, 使用完要立即 #undefine;
- 不要只是对已经存在的宏使用 #undef, 选择一个不会冲突的独特名称;
- 不要试图使用展开后会导致C++构造不稳定的宏, 否则至少要附上文档说明其行为; [不要在构造相关代码使用宏?]
- 最好不要使用 ##来产生 function/class/variable的名字;
5.15 0 and nullptr/NULL
Tip 整数用 0, 实数用 0.0, 指针用 NULL或nullptr, 字符chars(串)用 ‘\0‘;
整数用0, 实数用0.0, 这一点毫无争议controversial;
对于指针(地址值), 到底是使用0还是NULL/nullptr,
[Remove]Bjarne Stroustrup建议使用最原始的0; 我们建议使用看上去像是指针的 NULL; [C++11: nullptr]<<<
[Add] 对允许C++11的项目, 使用nullptr, C++03项目使用NULL, 看起来比较像个指针; <<<
事实上一些C++编译器(如gcc4.1.0)对 NULL进行了特殊的定义, 可以给出有用的警告信息, 尤其是 sizeof(NULL)和 sizeof(0)不相等的情况;
字符(串)用 ‘\0‘, 不仅类型正确而且可读性好; [http://bbs.csdn.net/topics/390615761]
5.16 sizeof
Tip 尽可能用 sizeof(varname)代替 sizeof(type);
使用 sizeof(varname)是因为当代码中变量类型改变时会自动更新; 某些特定情况下 sizeof(type)或许有意义, 比如管理external或internal数据类型变量的代码, 而没有方便的C++类型; 但还是要尽量避免, 因为它会导致变量类型改变后不能同步;
1 2 |
|
WARNING
1 |
|
[Add]
Other
1 2 3 4 |
|
<<<
[Add]
auto
使用auto来避免类型名字杂乱clutter; 当有助于可读性的时候继续使用明显的manifest类型声明, 除了本地local变量之外不要使用auto;
定义:
C++11中, 一个由auto指定类型的变量会给出符合初始化它的表达式的类型; 可以使用auto来用copy初始化initialize它, 或者绑定bind一个引用;
[http://en.cppreference.com/w/cpp/language/auto]
1 2 3 4 |
|
优点:
C++类型名称有时候很长而且笨重cumbersome, 特别是在模板或名字空间中:
1 |
|
返回值很难读懂, 蒙蔽obscure了语句的原本意图; 改为:
1 |
|
更易读懂;
没有auto的话我们有时不得不将一个类型名字在一个表达式内写两遍, 对于阅读者来说没有意义:
1 |
|
使用auto让中间intermediate变量的使用更合理, 减少了显式书写类型的负担;
缺点:
有时候变量是manifest的会更清晰, 特别是变量初始化依赖于我们之前声明的东西; 像这样的表达式:
1 |
|
如果x是几百行之前声明的, i的类型可能不够明显;
程序员不得不去理解auto和const auto&之间的区别, 有时候他们会在没有意识到的时候拿到一份copy;
auto和C++11 brace-initialization之间的交互interaction可能会令人混淆; 声明:
1 2 |
|
它们表示不同的东西, x是个int, 但y是一个std::initializer_list<int>; 相同情况会发生在其他普通隐式代理normally-invisible proxy类型上;
如果一个auto变量被用作接口的一部分, e.g. 在头文件中作为一个const, 程序员可能只是为了改变它的值而改变它的类型, 导致没有想到的一系列API的彻底radical改变;
结论:
auto只对本地变量开放; 不要在文件范围file-scopye或名字空间范围namespace-scope中对变量, 或类成员使用auto; 永远不要用大括号初始化列表braced initializer list初始化一个auto类型auto-typed变量;
auto关键字也用在C++feature无关的地方: 它作为一种新的函数声明的语法的一部分, 尾随返回值类型 trailing return type; trailing return type只在lambda表达式中被允许使用;
Braced Initializer List
braced initializer lists; [http://en.cppreference.com/w/cpp/language/list_initialization]
在C++03, 聚合类型aggregate type(没有ctor的数组和结构体)可以用braced initializer list初始化;
1 2 |
|
C++11中, 这个语法被普遍化generalized了, 任何一个对象类型都可以用braced initializer list来初始化, 作为一个braced-init-list的C++语法grammar; 这里有几个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
一个用户定义的类型也可以使用std::initializer_list<T> [http://en.cppreference.com/w/cpp/utility/initializer_list] 定义一个ctor或assignment operator, 会从braced-init-list自动创建;
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
最后 brace initialization也能调用普通的数据类型的ctor, 即使没有std::initializer_list<T>构造函数;
1 2 3 4 5 6 7 8 9 10 11 |
|
Note 永远不要把一个braced-init-list分配给一个auto的本地变量; 在单个元素的case中, 其意义可能会混淆:
Bad:
1 |
|
Good:
1 |
|
参见 Braced Initializer List Format;
<<<
---TBC---YCR