C++ 补遗

C++ 补遗

1. header guard

  1. 头文件只有预处理阶段 (preprocess) 才有用,preprocessor 在解析文件时,如果遇到 #include <xxx.h> 语句,就会将需要的头文件拷贝到被解析的文件头部。然后编译器进行编译

  2. 如果我们在 头文件中写了函数实现 (definition),并多次 include 该头文件,那么 编译器就会报错,因为预处理后的文件中会出现同一函数的多次实现,这是不符合语法的。

  3. 如何避免多次 include 的问题,就是 header guards 方法了,就是下面的方法。在同一个文件,多次 include 某个头文件,compile 在编译时只有第一次没有遇到 SOME_UNIQUE_NAME_HERE 宏,在定义好了后,则不会通过 #ifndef 检测

  4. 但是,根据前面预处理的原理,header guards 可以阻止 cpp 文件接收 guarded header 的多次拷贝,但是,它不能阻止 header file 被拷贝 (一次) 到不同的 cpp 文件。header 文件的函数实现被拷贝到多个 cpp 文件,这种情况会导致一个问题: 链接器报错,因为我们的程序有多个地方存在同一个函数的实现。
    就算是 const 变量,也推荐在 cpp 文件写实现 (definition),header 文件写声明 (declaration),可以大大减少编译时间,因为如果是 header 文件写实现,那么 preprocessor 会拷贝到每个引用了这个头文件 cpp 文件,然后重新编译该 cpp 文件,编译时间会很长。

2. 类型

2.1 类型后缀 (Type suffix)

Data type Suffix Meaning
int u or U unsigned int
int l or L long
int ul, uL, Ul, UL, lu, lU, Lu, LU unsigned long
int ll or LL long long
int ull, uLL, Ull, ULL, llu, llU, LLu, LLU unsigned long long
double f or F float
double l or L long double

2.2 引号的使用

C++14 引入了 单引号quotation mark (’) 作为 digit separator,便于阅读

2.3 float type comparison

这种方法最大的好处是根据相对于输入数值的大小判断两个浮点型数据是否相等。

但有一个小 Bug, 如果 a, b 均为 0 的话,反而会出错,因为 epsilon 乘以 0 还是为 0

2.4 隐式转换 (implicit type conversion)

  • Char, unsigned char, and short is promoted to an int.
  • unsigned short can be promoted to int or unsigned int, depending on the size of an int
  • Float is promoted to double
  • Enum is promoted to int

函数重载如何避免 ambiguous match ?

上面的例子中, ‘a’ 是 char 型,既可以转换为 unsinged int ,也可以转换为 float;0 是 int 型, 3,14159 没有 f 后缀,所以是 double 型,所有两个函数都可以使用。

  1. 定义新的重载函数
  2. 显式类型转换,static_cast
  3. 如果是字面量 (literal),可以在 literao 后面加类型后缀,比如, print(0u)

2.5 using 关键词和 std::function

C++ 有如下两种类型别名的表示方法:

typedef double distance_t;
using distance_t = double;

强烈推荐第二种,更直观。

同样地,函数指针也可以使用别名,同样用 using 的方法更自然

C++11 引入 std::function 定义函数指针

2.6 trailing return type

C++11 引入的函数声明的新方法

int add(int x, int y);

auto add(int x, int y) -> int;

3. const vs constexpr

  1. compile time: the process of compiling the program. During compile time, the compiler ensures the code is syntactically correct, and convert the code into object files.
  2. runtime: the process of running the program. During runtime, the code is executed line by line.

C++ actually has two different kinds of constants

  1. Runtime constants: the variables whose initialization values can only be resolved at runtime (when your program is running),这种情况我们用 const
  2. Compile-time constants: the variables whose initialization values can be resolved at compile-time (when your program is compiling),这种情况我们用 constexpr

constexpr 代替传统的 #define 方法定义 magic number

4. variable

变量的 3 大属性: scope, duration, linkage

C++ 有两种 scope: file scope, block scope

C++ 变量有 3 种 linkage (在链接器的角度来看):

  • no linkage,普通的局部变量都属于这种,
  • internal linkage,使用 static 修饰的变量为内部链接 (internal linkage),只能在文件内部使用
  • external linkage,使用 extern 修饰的变量为 external linkage

Note:

  1. non-const variable declared outside of a function are assumed to be external, const variable declared outside of a function are assumed to be internal.

  2. In order to use an external global variable that has been declared in another file, you must use a variable forward declaration via the extern keyword (with no initialization value).
    So the extern keyword has different meaning in different contexts. In some contexts, extern means “give this variable external linkage”. In other contexts, extern means “this is a forward declaration for an external variable that is defined somewhere else”

  3. function always default to external linkage, but can be set to internal linkage via the static keyword

总结如下:

Type Example Scope Duration Linkage Notes
Local variable int x; Block scope Automatic duration No linkage
Static local variable static int s_x; Block scope Static duration No linkage
Dynamic variable int *x = new int; Block scope Dynamic duration No linkage
Function parameter void foo(int x) Block scope Automatic duration No linkage
External non-const global variable int g_x; File scope Static duration External linkage Initialized or uninitialized
Internal non-const global variable static int g_x; File scope Static duration Internal linkage Initialized or uninitialized
Internal const global variable const int g_x(1); File scope Static duration Internal linkage Must be initialized
External const global variable extern const int g_x(1); File scope Static duration External linkage Must be initialized

Forward declaration:

Type Example Notes
Function forward declaration void foo(int x); Prototype only, no function body
Non-const global variable forward declaration extern int g_x; Must be uninitialized
Const global variable forward declaration extern const int g_x; Must be uninitialized

4.1 使用 全局变量的 3 点建议

  1. 尽量少使用 全局变量,或者尽可能将全局变量视为只读变量

  2. 如果需要使用,建议变量名前加上 g_ 前缀,避免和局部变量名冲突,同时也可以有效识别全局变量

  3. 使用 static 修饰变量,从而避免变量的全局暴露,通过函数接口 set/get 变量

  4. 函数中不直接使用全局变量,而是通过参数传递,从而减少对该变量的依赖

5. enum class

使用 enum class, 所有的 枚举值 (enumerator) 都视为 enum class 的一部分,必须使用 scope qualifier (::) 才能接触到枚举值,比如 Color::RED ,这样可以避免命名污染和潜在的命名冲突。

如果非得使用枚举值对应的整型值,建议使用 static_cast<int> 进行类型转换

6. struct

list initialization

uniform initialization

C++11 支持默认初始化:

7. Pseudo-random number generator

伪随机序列的每一个数都是由数组的前一个数确定的。如果我们数组的第一个数不变,那么整个数值是完全不变的。

这也是为什么需要需要修改 seed 的原因

std::srand(seed)
std::rand()

什么是一个好的随机序列产生器 ?

  1. 每个数出现的概率近似相等
  2. 随机产生的前后两个数没有必然的联系,或者说明显的联系
  3. 对所有的 seed number 都有足够长的重复周期

由于 std::rand()RAND_MAX 为 32767 (15 bits),如果我们想要质量更高的 PRNG,推荐 Mersenne Twister

Debug 的时候,为了保证重复性,推荐将 seed 设为 0

8. 指针

  • 声明指针变量时,将指针的星号 (*)紧挨变量名
  • 声明函数时,将返回指针的星号(*)紧挨类型名

8.1 nullptr

所谓空指针,就是不指向具体地址的指针,地址为 nullptr

C++ 推荐使用 nullptr 表示空指针,初始化时,如果不打算赋值,建议赋值为空指针

double *ptr{nullptr};

C++ 引入了 std::nullptr_t 类型 (头文件 <cstddef>),当函数的参数的值只能为 nullptr 时,我们使用 std::nullptr_t 定义该参数

8.2 数组指针退化 ( array pointer decay)

打印结果为:

32
4

数组指针传递时,会退化为数组类型的指针。

8.3 如何避免野指针 (dangling pointer)

  1. 尽量避免多个指针指向同一片动态内存地址。申请这块内存的指针负责释放这块内存
  2. 释放指针指向的内存后,设置指针为 nullptr,这一点非常有效

在极其罕见的情况下,机器没有多余的内存分配,会抛出 bad_alloc 异常,同时程序死机 (crash) ,避免这类问题的有效方法是:

使用 std::nothrow ,如果没有内存,则返回 nullptr ,同时使用指针前检查是否为空指针。因此,动态内存分配上下文中,空指针一般被视为这个指针没有被分配内存。

8.4 const 和 pointer 的组合

总结:

  • non-const pointer 可以重新指向其他地址
  • const pointer 永远指向同一片地址
  • 指向 non-const value 的 pointer 可以改变它指向的变量的值
  • 指向 const value 的 pointer 将它指向的变量视为 const (尽管它可能并不是),因此它指向的变量的值不能通过该指针改变

9. reference

左值和右值的区别:

  1. 左值 (l-value) 也称为 local value,一个占据内存地址的有符号标识的值。C++ 引入 const 关键词词后,左值又分为 modifiable l-values 以及 non-modifiable l-values
  2. 右值 (r-value) :除了左值以外的值,比如字面量(literal),临时表达式 (x+1),匿名对象

C++ 分 左值引用 (l-value reference) 和右值引用 (r-value reference)

请注意,引用本身是左值,因为它就有分配的内存,又有名称

9.1 non-const l-value reference

non-const l-value reference 就是传统的 C++ 引用,它只能引用 modifiable l-values,同时可以修改引用的值

Type Can be initialized with Can modify
Modifiable l-values Yes Yes
Non-modifiable l-values No No
R-values No No

9.2 const l-value reference

const l-value reference 可以引用 const l-value, non-const l-value,以及 r-value

Type Can be initialized with Can modify
Modifiable l-values Yes No
Non-modifiable l-values Yes No
R-values Yes No

由于 const l-value reference 可以实现各种值的传递,优先考虑函数参数使用 const l-value reference. Pass non-pointer, non-fundamental data type (such as structs) by const reference

9.3 r-value reference

C++11 加入右值引用 (&&) 符号,实现右值引用

本来右值在离开表达式后就会被销毁,但右值引用延长了右值的存在时间

Type Can be initialized with Can modify
Modifiable l-values Yes No
Non-modifiable l-values Yes No
R-values Yes No

r-value reference 的两个优点:

  1. extend the lifespan of the object they are initialized with to the lifespan of the r-value reference (l-value references to const object can do this too).
  2. non-const r-value reference allow you to modify the r-value.

9.4 r-value reference to const

Type Can be initialized with Can modify
Modifiable l-values Yes No
Non-modifiable l-values Yes No
R-values Yes No

9.5 move constructor, move assignment, std::move

当拷贝构造函数或者拷贝赋值的参数是右值时,会调用 move constructor 或者 move assignment,当然前提是这些函数定义了,默认的 move constructor 和 move assignment 实际上和默认拷贝构造以及拷贝赋值相同,并没有使用 move 语义。

class Auto_ptr 为例,格式如下:

Note:

  1. move function 传入的参数必须是右值

  2. move function 总是应该将传入的参数设置为确定的状态,推荐设置为 “null state”,就算这个参数是右值,也要设置

  3. automatic l-values returned by value may be moved instead of copied.

  4. 推荐 delete 拷贝构造和拷贝赋值

此外,C++ 引入了新的函数 std::move (头文件: utility),可以将左值转为右值 (r-value)。然后配合 move function,就可以实现左值的非拷贝赋值,这也是为什么要将传入参数设置为确定状态的原因,因为尽管它是右值,但是它也有可能是左值转过来的,左值后面可能还会有其他的用途

让我们看下 标准库是怎么干的。

输出结果:

C++ 的 class string 会将 std::move 返回的右值清空,这也解释了上面的 str 的内容被清空了。

9.6 智能指针

  1. std::unique_ptr 类型用于管理动态分配资源,当它离开 scope 时,会自动释放管理的资源,同时确保该资源不会被多个 std::unique_ptr 共享。 That is, std::unique_ptr should completely own the object it manages, not share that ownership with other classes. std::unique_ptr lives in the <memory> header.
    正是因为 std::unique_ptr 只支持资源被单一所有,因此不能通过 左值 拷贝赋值 (std 标准库已经 delete 这种方法),而应该使用右值赋值,即 std::move ,这样资源就从一个对象转移到另一个对象。

    误区:

    1. 使用同一个资源初始化两个 std::unique_ptr ,它们都会管理这个资源,于是当它们 out of scope 时,会分别释放资源时,冲突出错
    2. 手动释放资源,和 std::unique_ptr 的资源释放冲突
  2. std::share_ptr 实现多个对象对资源的共同管理,最后一个存在于内存的 share_ptr 在销毁时负责释放资源。
    注意:
    std::share_ptr 同样不能使用同一个资源分别独立初始化,而是应该拷贝初始化已经存在的 std::share_ptr

    类似于 std::make_unique ,使用 std::make_shared 模板函数同样可以返回对应结构的 shared_ptr

  3. 为了避免循环引用,C++ 引入了 std::weak_ptr ,用来和 std::shared_ptr 配合使用。std::weak_ptr is an observer – it can observe and access the same object as a std::shared_ptr but it is not considered an owner. Remember, when a std::shared pointer goes out of scope, it only considers whether other std::shared_ptr are co-owning the object. std::weak_ptr does not count !
    注意: std::weak_ptr 不支持前面的 -> 操作符,使用之前,可以通过 lock() 成员函数将 std::weak_ptr 转为 std::shared_ptr

10. for each loop

eg:

请注意:

  • for-each loop 需要知道 array 的大小,因此不能和指向 array 的 pointer 配合使用
  • for-each loop 不会提供元素的索引值,因为有的数据结构并没有索引值

11. 堆和栈 (stack and heap)

一个程序使用的内存通常分为如下几部分:

  • 代码段 (code segment / text segment) 用于程序代码在内存中的位置,通常是只读的
  • bss segment (uninitialized data segment) 用于存放零初始化的全局变量和静态变量
  • 堆 (heap) 用于分配动态内存,所谓 delete 指针,就是将指针对应的内存返还给堆 (heap)
  • 调用栈 (call stack) 用于函数参数,局部变量,和函数相关信息的存放, 它是内存中的栈结构区域

11.1 call stack

call stack 工作机制:

  1. 程序遇到一个函数调用
  2. 构建一个 stack frame,并将其入栈,stack frame 包括:
    • 函数调用的地址,CPU 执行函数结束后的跳转返回地址
    • 传递给函数的参数
    • local variable 内存
    • 所有函数中需要使用的寄存器的值的预先拷贝,执行函数结束后重新恢复寄存器的值
  3. CPU 跳转到函数的起点
  4. 执行函数的指令

当函数执行结束后,如下操作执行:

  1. 从 call stack 恢复寄存器的值
  2. 将 stack 顶部的 stack frame 出栈,这会释放所有 local variable 和 函数参数的内存
  3. 处理函数返回值
  4. CPU 从跳转到函数调用的返回地址继续执行

栈溢出 (stack overflow) 是指栈的内存全部被分配,一般函数嵌套过多会导致这个问题 (尤其是递归函数)。因此通过值拷贝的方式传递大的结构体或者 class 不是好的做法。

11.2 内联函数 (inline function)

C++ offers a way to combine the advantages of functions with the speed of code written in-place: inline functions. The inline keyword is used to request that the compiler treat your function as an inline function. When the compiler compiles your code, all inline functions are expanded in-place – that is, the function call is replaced with a copy of the contents of the function itself, which removes the function call overhead!

Because of the potential for code bloat, inlining a function is best suited to short functions (e.g. no more than a few lines) that are typically called inside loops and do not branch.

12. Class

12.1 const, reference member

使用 constructor member initializer list 实现,这是唯一的方式

Favor uniform initialization over direct initialization.

12.2 实现 chaining member function

函数返回 *this ,学会这个技巧吧

12.3 friend function, friend class

A friend function is a function that can access the private members of a class though it were a member of that class. A friend function may be either a normal function, or a member function of another class. To declare a friend function, simply use the friend keyword in front of the prototype of the function you wish to be a friend of the class.

利用友元函数实现操作符重载,我们优先考虑普通函数实现操作符重载,但前提是不引入多余的函数来获得类的私有变量,否则用友元函数更加方便

但是在实现操作符重载时,请注意:

  1. Not everything can be overloaded as a friend function
    赋值 (=),subscript ([]),函数调用 (()),成员选择 (->) 操作符只能通过成员函数重载

  2. Not everything can be overloaded as a member function
    由于操作符的左操作数 (left operand) 必须是对应的 class 类型,因此对于
    friend std::ostream& operator<< (std::ostream &out, const Point &point); 操作符,它的左操作数必须是 ostream ,无法通过成员函数实现
    通常,如果 left operand 不是 class 类型 (比如 : int),或者是我们不能改变的 class 类型 (比如 std 标准库自带的 ostream 类型),我们都不能通过 成员函数重载实现

  3. 如果是单目操作符 (unary operator),请通过 成员函数 实现

  4. 如果双目操作符 (binary operator) 不改变 left operand,建议通过 友元函数或者普通函数实现,因为它将左右操作符均视为参数,可以对任何类型操作。

  5. 如果双目操作符 (binary operator) 改变 left operand,建议通过 成员函数 实现,因为如果左操作符可以修改,则它一定是 class 类型,通过 *this 操作,很自然。

12.4 typecasts 重载

比如实现 int() 成员函数可以实现该类型向 int 类型的转换

此时隐式类型转换,可以将 Cents 类型转换为 int 类型,或者 static_cast<int> 也是可以的

12.5 explicit 和 delete

构造函数添加 explicit 关键词会避免参数的 implicit conversion

使用 delete 可以彻底避免 implicit conversion

12.6 copy constructor 和 assignment operator

两者的不同:

  1. 如果新的实例对象在拷贝之前创建,那么就使用 copy constructor
  2. 如果没有新的实例创建,那么就会使用 assignment operator

13. 如何写测试代码

  1. write your program into small, well defined units (functions), and compile often along the way
  2. Code coverage
    • 100% statement coverage (ensure the test hits every statement in the function)
    • 100% branch coverage (test each of your branch such that they are true at least once and false at least once)
    • 100% loop coverage (use the 0, 1, 2 test to ensure your loops work correctly with different number of iterations )
  3. Ensure you’re testing different categories of input
    • For integers, make sure you’ve considered how your function handles negative values, zero, and positive values. For input, you should also check for overflow if that’s relevant
    • For floating point numbers, make sure you’ve considered how your function handles values that have precision issues (values that are slightly larger or smaller than expected). Good test values are 0.1 and -0.1 (to test numbers that are slightly larger than expected) and 0.6 and -0.6 (to test numbers that are slightly smaller than expected).
    • For strings, make sure you’ve considered how your function handles an empty string (just a null terminator), normal valid strings, strings that have whitespace, and strings that are all whitespace. If your function takes a pointer to a char array, don’t forget to test nullptr as well (don’t worry if this doesn’t make sense, we haven’t covered it yet).

13.1 defensive programming

检测猜想错误:

  1. 函数调用时,传入的参数是否正确,是否语义正确
    因此在函数头部,检查参数是否有正确的值

  2. 函数的返回值会提示是否有错误发生,注:传统的 C 编程会把错误码作为返回值,0 表示没有错误,负数表示各种类型的错误
    函数返回时,检查返回值,是否有错误发生

  3. 当程序接收输入 (用户或者文件),必须满足预期的格式或者范围标准
    检查用户输入的格式

如何处理错误:

  1. 跳过依赖正确猜想的代码
  2. 如果是函数,返回错误码
  3. 结束程序 (exit())
  4. 如果用户输入格式错误,重新输入
  5. 使用 std::cerr 输出错误日志
  6. 如果使用图形界面框架,可以弹出错误对话框

13.2 断言 (assert)

An assert statement is a preprocessor macro that evaluates a conditional expression at runtime. If the conditional expression is true, the assert statement does nothing. If the conditional expression evaluates to false, an error message is displayed and the program is terminated.

下面是一个小技巧,可以输出额外的信息

在实际的产品代码中,添加 #define NDEBUG 宏,可以忽略 assert 检查

C++11 加入了 static_assert 静态断言,用于 compile-time 检查,如果检测条件不为真,编译器会报错

static_assert(sizeof(long) == 8, "long must be 8 bytes");
static_assert(sizeof(int) == 4, "int must be 4 bytes");
 
int main()
{
    return 0;
} 

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d 博主赞过: