0%

C++ 编程风格建议

安全

  1. 引用和指针

    • 引用不会为空
    • 引用不会改变指向
    • 引用不能进行+、-、++、–运算
  2. 宏定义多条语句

    • 使用do…while(false)来包含
  3. 逻辑符号||的执行
    假设我们要同时自增a和b,如果任意一个函数失败了,则执行某些操作
    Bad
    if(!IncreaseA() || !IncreaseB())
    假设自增A函数失败了,则不会自增B
    Good
    分开写

  4. 尽可能缩短变量的存活时间

    • 短的变量存活时间减少了初始化错误的可能
    • 阅读者同一时间需要阅读的代码变少,便于理解
    • 当需要把一个大的函数分拆,短的存活时间方便拆分
  5. 避免浮点数的数量级相差巨大的数字之间的四则运算
    double d = 100000000.0 + 0.1;

  6. 避免浮点数的等量比较

  7. if/循环语句必须使用大括号

  8. 禁止通过声明的方式引用外部函数接口、变量
    通过extern声明的方式使用外部函数接口、变量,容易在外部接口改变时可能导致声明和定义不一致。

  9. 类的成员变量必须显式初始化

  10. 基类的析构函数必须声明为virtual

  11. 禁止虚函数使用缺省参数值

  12. 禁止重新定义继承而来的非虚函数

  13. 不允许使用宏来表示常量

    • 宏是简单的文本替换,在预处理阶段时完成,运行报错时直接报相应的值
    • 跟踪调试时也是显示值,而不是宏名
    • 宏没有类型检查,不安全
    • 宏没有作用域
  14. 禁止用memcpy_s、memset_s初始化非POD对象

    • POD类型主要包括int, char, float, double, enumeration, void, pointer等原始类ing以及聚合类型,不能使用封装和面向对象特性(如用户定义的构造/赋值/析构函数、基类、虚函数)
    • 由于非POD类型比如非聚合类型的class对象,可能存在虚函数,内存布局不确定,跟编译器有关,滥用内存拷贝可能会导致严重的问题。
    • 即使对聚合类型的class,使用直接的内存拷贝和比较,破坏了信息隐蔽和数据保护的作用,也不提倡使用memcpy_s、memset_s
  15. 含有变量自增或自减运算的表达式中禁止再次引用该变量
    x = b[i] + i++;

  16. 不要保存std::string的c_str()返回的指针

  17. 对于指针和引用类型的形参,如果是不需要修改的,请使用const

  18. 使用强类型参数,避免使用void*

  19. 使用std::string代替char*

    • 不用考虑结尾的’\0’
    • 可以直接使用+, =, ==等运算符以及其他字符串操作函数
    • 不需要考虑内存分配操作,避免了显式的new、delete, 以及由此导致的错误

效率

  1. 循环嵌套, 把大循环写在里面

  2. strcmp的判断
    Bad
    if(!strcmp(str1, str2))
    Good
    if(strcmp(str1, str2) == 0)

  3. 肯定语句比双重否定容易理解
    Good
    if(SomethingDone)
    Bad
    if(!NotDone)

  4. 条件判断语句
    常量在右,变量在左
    让编译器去检查误赋值的情况

  5. 使用小括号,避免优先级问题
    Bad
    if(a < b == c == d)
    Good
    if((a < b) == (c == d))

  6. 为变量指定唯一用途。避免采用不同取值区间来区分不同内容, 如,Account小于5000时表示老用户ID,大于5000时表示新用户ID

  7. 避免采用硬编码

  8. 布尔变量的命名

    • 避免采用status、sourcefile等模糊的布尔变量名,采用statusOK、sourcefileFound
    • 避免采用否定形式的布尔变量
      if(!NotSuccess)
  9. 避免在变量名中使用数字

    • 考虑使用数组代替
    • 如果数组不适合,那么数字更不适合
  10. 定义变量的作用域

    • 开始采用最严格的可见性,然后根据需求扩展变量的作用域
    • 循环内的变量挪动到循环外,比反过来简单
    • 把private变量变为public, 比反过来简单
  11. 不用的代码段直接删除,不要注释掉

    • 被注释掉的代码,无法被正常维护;当企图恢复使用这段代码时,极有可能引入容易被忽略的缺陷
    • 使用版本控制来,记录代码。
  12. 避免在变量名中使用容易混淆的字符

    • 数字1和小写的l
    • 数字1和大写的L
    • 数字0和大写的O
    • 数字2和小写的z
    • 数字6和大写的G
  13. 为空语句创建一个DoNothing()预处理宏或者内联函数

  14. 避免函数过长,函数不超过50行(非空非注释)

    • 函数应该可以一屏显示完(50行以内), 只做一件事情,而且把它做好。
    • 过长的函数往往意味着函数功能不单一,过于复杂,或过分呈现细节,未进行进一步抽象。
    • 即使一个长函数现在工作的很好,一旦有人对其修改,有可能出现新的问题,甚至导致难以发现的BUG。建议将其拆分为更加简短并易于管理的若干函数,以便于他人阅读和修改代码。

C++ 构造函数与析构函数

类的基本组合元素。
构造函数、析构函数、拷贝构造函数和拷贝复制符

构造函数在对象被创建的时候调用,如:

1
2
3
4
5
6
7
8
9
class A
{
};

int main()
{
A obj; ///< 调用构造函数
A* p = new A; ///< 调用构造函数
}

析构函数在对象被销毁时刻调用,如:

1
2
3
4
5
6
7
8
9
int main()
{
{
A obj;
} ///< 临时变量超出作用域,调用析构函数

A* p = new A;
delete p; ///< 调用析构函数
}

构造与析构函数的具体讲解可见 类内默认成员函数

面对对象的简介

对象有三个特点: 封装、继承和多态。

封装

  • 封装方法
  • 聚合数据
  • 隐藏细节

封装方法

当我们在使用对象时,自然而然可以把一系列方法放在一个类内,就比如我们想要定义一系列读取 Json 字符串的方法。

下面是 C 语言的封装方法:

1
2
3
4
GJSON*  gos_json_init(void);
void gos_json_free(GJSON* Json);
bool gos_json_parse(GJSON* Json, char *szJson);
char* gos_json_get_string (GJSON* Json, char *szKey);

我们推断使用顺序是:

  1. 使用 gos_json_init 来获取一个可用的 Json 解析用的结构体, 其中存储了一些信息。
  2. 使用 gos_json_parse 来读取一系列 Json 字符串中的键值。
  3. 使用 gos_json_get_string 来通过键来获取值。
  4. 最后使用 gos_json_free 来释放资源。

那么调用过程如以下代码:

1
2
3
4
5
6
7
8
int main()
{
GJSON* pJson = gos_json_init();
gos_json_parse(pJson, "Json string");
char* szValue = gos_json_get_string(pJson, "Key");
...
gos_json_free(pJson);
}

对比面对对象接口:

1
2
3
4
5
6
7
8
9
class Json
{
public:
bool parse(char *szJson);
char* get_string (char *szKey);

private:
GJSON* m_Json;
};

如上面所示: C++的接口

由于类内保存了一个 GJSON 的指针 m_Json, 所以接口函数不需要 GJSON* 的入参.
由于可以被调用的函数只有两个,那我们可以推测调用方法:

  1. 使用 parser 接口函数解析 json 字符串
  2. 使用 get_string 接口函数来获取对应键的值

调用如下:

1
2
3
4
5
6
int main()
{
Json obj;
obj.parser("Json string");
char* szValue = obj.get_string("Key");
}

由上面的调用可以看到少了调用 initfree 两个函数的过程,因为 class 可以使用构造函数中初始化自己,在析构函数中做相反动作,我们下面补充构造函数和析构函数的定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Json
{
public:
/// 构造函数
Json()
{
m_Json = init();
}

/// 析构函数
~Json()
{
free(m_Json);
}

bool parse(char *szJson);
char* get_string (char*szKey);

private:
GJSON* m_Json;
GJSON* init();
void free(GJSON* Json);
};

聚合数据

class 带来的好处是,类内不仅可以定义函数,也可以聚合成员,定义在一起方便查看与传递。

例如我们有一堆配置项数据需要保存。

C 语言可以这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
unsigned g_ulLogLevel;
bool g_bLogToStdout;
bool g_bLogToFile;

void GetLogCfg()
{
/// 给变量赋值
g_ulLogLevel = 1;
g_bLogToStdout = TRUE;
g_bLogToFile = TRUE;
}

/// 在其他 cpp 文件中访问这些配置项
extern unsigned g_ulLogLevel;
extern bool g_bLogToStdout;
extern bool g_bLogToFile;

C++ 可以这样写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class LocalCfg
{
public:
LocalCfg() { GetLogCfg(); }

unsigned m_ulLogLevel;
bool m_bLogToStdout;
bool m_bLogToFile;

private:
void GetLogCfg();
};

/// 在其他地方访问这些配置项
int main()
{
LogCfg obj;

obj.m_ulLogLevel;
obj.m_bLogToStdout;
obj.m_bLogToFile;
}

另外我们经常在类中见到函数 GetSet

1
2
3
4
5
6
7
8
class LocalCfg
{
public:
void Set(int i) { value = i; }
int Get() { return value; }
private:
int value;
};

我们为什么要把一个简单的赋值操作封装成函数呢?

如果我们想要把变量的赋值与其他业务联动,见下面的例子:

  1. 追踪赋值,添加打印
1
2
3
4
5
6
7
8
9
10
11
12
class LocalCfg
{
public:
void Set(int i)
{
GosLog(LOG_DETAIL, "value: %d -> %d", value, i);
value = i;
}
int Get() { return value; }
private:
int value;
};
  1. 添加锁来支持多线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class LocalCfg
{
public:
void Set(int i)
{
mutex.lock();
value = i;
mutex.unlock();
}

int Get()
{
mutex.lock();
int value_temp = value;
mutex.unlock();
return value_temp;
}

private:
int value;
GMutex mutex;
};
  1. 业务联动绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class LocalCfg
{
public:
void Set(int i)
{
value = i;
IsSet = true;
}

int Get()
{
if (IsSet)
{
return value;
}
else
{
/// 返回无效值
return -1;
}
}

private:
int value;
// 用于记录 value 是否有效
bool IsSet = false;
};

隐藏细节

见下面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class A
{
public:
/// 把大象放进冰箱里
bool PutElephantInFreezer()
{
/// 打开冰箱门
OpenFreezerDoor();
/// 把大象放进去
LetElephantIn();
/// 关上冰箱门
CloseFreezerDoor();
}

private:
void OpenFreezerDoor();
void LetElephantIn();
void CloseFreezerDoor();
};

int main()
{
A obj;
// 调用 public 函数来把大象放进冰箱里
obj.PutElephantInFreezer();
}

相关语法介绍

关于 publicprivate

关于关键字 publicprivate, public 类型的类内成员变量和函数,可以被类的实例调用而 private 不能。

实例化简单来说就是,把一个就是在代码中定义该对象。(如果把类的定义比作蛋糕模子,那么类的实例就是蛋糕)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class OBJECT
{
public:
int i_public;
private:
int i_private;
};

int main()
{
// 对象实例化
OBJECT obj;

// 对象实例访问 public 类成员
obj.i_public = 1;

// 对象实例无法访问 private 成员
// obj.i_private = 1;
}
友元函数介绍 (friend function)

对于私有变量和私有成员函数, 友元函数可以打破访问权限限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A
{
friend void Set(A& obj, int num);
private:
int i_private;
};

/// 友元函数不属于某个类,所以定义时
/// 不需要这样写:
/// void count::Set(counter& obj, int num)
void Set(A& obj, int num)
{
/// 友元函数内,对象实例访问对象私有成员
obj.i_private = num;
}
  1. 友元函数本质是普通函数,友元只是描述的是对类的友元。

  2. 友元函数不属于类,是独立的函数,所以不受作用域描述符的限制。

  3. 友元函数本身可以同时成为多个类的友元函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class B;

class A
{
friend void Set(A& obja, B& objb, int num);
private:
int i_private;
};

class B
{
friend void Set(A& obja, B& objb, int num);
private:
int i_private;
};

void Set(A& obja, B& objb, int num)
{
obja.i_private = num;
objb.i_private = num;
}

int main()
{
A obj_a;
B obj_b;
Set(obj_a, obj_b, 1);
return 0;
}
类内 static 与对象之间的关系

在对象内的 static 变量和函数,与对象的生命周期无关,每一个对象的所有实例都共享同一个 static 变量和函数。

类内 static 函数对类内的静态成员函数、构造函数、析构函数和静态成员变量享有访问权限。

类内静态变量
1
2
3
4
5
6
7
8
9
10
11
12
class A
{
public:
static int counter = 0; ///< 记录对象被实例化了多少次
A(){count++;}
};

int main()
{
A obj;
std::cout << A::counter << std::endl;
}
类内静态成员函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class A
{
public:
static int counter;
static int GetCounter()
{
return counter;
}

public:
int i_non_static = 0;
int fun_non_static()
{
printf("Hello World!");
}
};

int A::counter = 0;

int main()
{
A obj0;
A obj1;
printf("%d", A::GetCounter());
printf("%d", obj0.GetCounter());
printf("%d", obj1.GetCounter());
return 0;
}

如上面所示 类内静态成员函数是可以直接访问类内静态成员变量也可以调用类内静态成员函数
但不能调用类内非静态成员变量和函数, 如 i_non_staticfun_non_static

静态成员函数的使用限制,不能调用非 static 的类内成员函数和成员变量。

类内静态函数在单例模式中的应用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Singleton
{
public:
static Singleton& GetInstance()
{
static Singleon* pInstance = NULL;
if(pInstance == NULL)
{
pInstance = new Singleon;
}
return *pInstance;
}

private:
Singleon() {}
};

继承

继承用来从基类中继承来函数或成员变量, 省却重复定义。

假如我们有很多呼叫相关的业务,都需要一个唯一的业务标识号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class base
{
public:
base() : strBusinessID(gos::GetUUID()) {}
public:
std::string strBusinessID;
};

/// 点呼从基类中继承出来了一个业务 ID
class P2PCall : public base
{};

int main()
{
P2PCall p2p_call; ///< 自动生成了一个唯一的业务号

std::cout << p2p_call.strBusinessID << std::endl;
}

相关语法介绍

虚析构函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class base
{
virtual ~base(){ std::cout << "~base" << std::endl; }
};

class derive: public base
{
virtual ~derive() { std::cout << "~derive" << std::endl; } ///< 不定义虚析构函数会导致内存泄漏
};

int main()
{
base* p = new derive;
delete p;
}
protected 关键字
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class base
{
private:
int i_private;
protected:
int i_protected;
};

class derive : public base
{
public:
int get_protected()
{
// 可访问基类中的 protected 成员
return i_protected;
}

// int get_private()
// {
// 基类中的 private 成员不能被派生类访问
// return i_private;
// }
};

int main()
{
base obj0;
// 在基类实例中表现为私有成员, 不可访问
// obj0.i_protected = 0;
derive obj;
// 在派生类实例中表现为私有成员, 不可访问
// obj.i_protected = 0;
return 0;
}
多重继承

假设一个程序员又想拥有使用 VSCode 的能力 又想拥有使用 source insight 能力,UML 图如下

multiple_inheritance

写成代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class VSCode
{
public:
void UseVSCode();
};

class SourceInsight
{
public:
void UseSourceInsight();
};

// class Programmer 拥有了 class VSCode 和
// class SourceInsight 中的方法函数
class Programmer : public VSCode, public SourceInsight
{
};

int main()
{
Programmer lijiancong;
lijiancong.UseVSCode();
lijiancong.UserSourceInsight();
}

如果两个基类中拥有同名成员变量或函数,则派生类使用时应标注该成员变量或函数的作用域,避免产生编译错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class A
{
protected:
int i_protected;
};

class B
{
protected:
int i_protected;
};

class Derive : public A, public B
{
public:
int get()
{
/// 如果两个基类中拥有同名成员变量或函数,
/// 派生类使用时应该标注哪个类的成员变量或函数, 否则编译错误
return A::i_protected;
}
};
使用 virtual 阻隔菱形继承

我们在使用多重继承时,可能会出现如下的情况。

可能出现如下情况:

multiple_inheritance2

菱形继承不仅会出现二义性成员变量名或函数名,而且在虚函数的继承中,中间类每一个类都会保存一个继承的副本,导致未知问题。使用 virtual 关键字避免菱形继承导致的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class Tool
{
public:
Tool()
{
std::cout << "Tool::i: " << &i
<< std::endl;
}
protected:
int i = 1;
};

class VSCode : public Tool
{
public:
VSCode()
{
std::cout << "VSCode::i: " << &(VSCode::i)
<< std::endl;
}
};

class SourceInsight : public Tool
{
public:
SourceInsight()
{
std::cout << "SourceInsight::i: "
<< &(SourceInsight::i)
<< std::endl;
}
};

class Programmer : public VSCode, public SourceInsight
{
};

int main()
{
Programmer lijiancong;
return 0;
}

Snipaste_2021-10-23_14-41-51

如上图, VSCodeSourceInsight 两个类都保存了一份基类 Tool::i 的副本, 造成了二义性。使用 virtual 来避免菱形继承带来的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Tool
{
protected:
int i;
};

/// 使用 `virtual` 关键字来避免菱形继承
class VSCode : virtual public Tool
{
};

/// 使用 `virtual` 关键字来避免菱形继承
class SourceInsight : virtual public Tool
{
};

class Programmer : public VSCode, public SourceInsight
{
public:
int get()
{
return i; ///< 正常使用 Tool 类中的函数
}
};

int main()
{
Programmer lijiancong;
lijiancong.get();
}

如果上述对象 VSCodeSourceInsight 没有使用关键字 virtual 来标注继承方式,那么 Programmer 类中正常使用 Tool::i

继承的方式与访问权限

publicprivateprotected 三种继承方式

见基类定义:

1
2
3
4
5
6
7
8
9
class base
{
public:
int i_public;
protected:
int i_protected;
private:
int i_private;
};

public 继承:

  • 基类中 public 成员, 在派生类中表现为 public
  • 基类中 protected 成员,在派生类中表现为 protected
  • 基类中 private 成员,在派生类中不可访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// public 继承
class derive : public base
{
public:
void get()
{
i_public = 0; ///< 基类中 `public` 成员, 在派生类中表现为 `public`
i_protected = 0; ///< 基类中 `protected` 成员,在派生类中表现为 `protected`
/// i_private = 0; ///< 基类中 `private` 成员,在派生类中不可访问
}
};

int main()
{
derive obj;
obj.i_public = 0;
/// obj.i_protected = 0; 不可访问
/// obj.i_private = 0; 不可访问
return 0;
}

protected 继承:

  • 基类中 public 成员, 在派生类中表现为 protected
  • 基类中 protected 成员,在派生类中表现为 protected
  • 基类中 private 成员,在派生类中不可访问

protected 继承与 public 继承相比, 区别在于 基类中 public 成员在 protected 继承后的派生类中降级为 protected

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class derive : protected base
{
public:
void get()
{
i_public = 0; ///< 基类中 `public` 成员, 在派生类中表现为 `protected`
i_protected = 0; ///< 基类中 `protected` 成员,在派生类中表现为 `protected`
/// i_private = 0; ///< 基类中 `private` 成员,在派生类中不可访问
}
};

class derive0 : public derive
{
void get0()
{
i_public = 0;
i_protected = 0;
/// i_private = 0;
}
};

int main()
{
derive0 obj;
obj.get();
/// obj.i_public = 0; 不可访问
/// obj.i_protected = 0; 不可访问
/// obj.i_private = 0; 不可访问
return 0;
}

private 继承:

  • 基类中 public 成员,在派生类中表现为 protected
  • 基类中 protected 成员,在派生类中表现为 protected
  • 基类中 private 成员,在派生类中不可访问

private 继承与 public 继承相比,区别在于基类中 public 成员和 protected 成员在 private 继承后的派生类中都降级为 private

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class derive : private base
{
public:
void get()
{
i_public = 0; ///< 基类中 `public` 成员, 在派生类中表现为 `private`
i_protected = 0; ///< 基类中 `protected` 成员,在派生类中表现为 `private`
/// i_private = 0; ///< 基类中 `private` 成员,在派生类中不可访问
}
};

class derive0 : public derive
{
void get0()
{
/// i_public = 0;
/// i_protected = 0;
/// i_private = 0;
}
};

int main()
{
derive0 obj;
obj.get();
/// obj.i_public = 0; 不可访问
/// obj.i_protected = 0; 不可访问
/// obj.i_private = 0; 不可访问
return 0;
}

总而言之,什么类型的继承,在派生类中最高的类成员访问权限就降级为什么类型。

多态

多态用于接口与多态实现的分离

下面代码示例为多态在工厂模式中的应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class Interface
{
public:
virtual void Insert() = 0;
virtual void Query() = 0;
};

class MySqlImpl : public Interface
{
public:
virtual void Insert()
{
MySql_Insert();
}

virtual void Query()
{
MySql_Query();
}
};

class RedisImpl : public Interface
{
public:
virtual void Insert()
{
Redis_Insert();
}

virtual void Query()
{
Redis_Query();
}
};

class Factory
{
public:
static Interface* getInterface(bool bIsUseMySQL)
{
if(bIsUseMySQL)
{
p = new MySqlImpl();
}
else
{
p = new RedisImpl();
}
}
};

int main()
{
Factory factory;
Interface* p = factory.GetInterface(true);
// 通过基类指针指向派生类
// 调用基类中的虚函数,会通过编译器自动识别
// 是使用 MySQL 的实现还是 Redis 的实现
p->Insert();
p->Query();
}

上面代码使用多态进行了函数覆盖(override), 但是在基类和派生类中出现了同名但不同入参的函数名,则会发生函数隐藏(hide)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Interface
{
public:
virtual void Insert(int i) { std::cout << "Interface::Insert" << std::endl; }
};

class MySqlImpl : public Interface
{
public:
/// 这里没有使用多态对基类中的 `Insert` 函数进行覆盖(override), 而是单独创建了一个新的虚函数
virtual void Insert(std::string s) { std::cout << "MySQL::Insert" << std::endl; }
};

class RedisImpl : public Interface
{
public:
virtual void Insert(int d) { std::cout << "RedisImpl::Insert" << std::endl; }
};

int main()
{
Interface* pRedis = new RedisImpl;
pRedis->Insert(1); ///< 正常使用多态,访问派生类的 `Insert` 函数的实现

Interface* pMySQL = new MySqlImpl;
/// pMySQL->Insert("Hello World!"); 无法使用基类指针访问多态函数 `Insert`
pMySQL->Insert(1); ///< 只能访问基类 `Insert` 函数的实现
return 0;
}

使用关键字 override 关键字避免因输入错误而导致函数覆盖不正确的现象。

1
2
3
4
5
6
7
8
9
10
11
12
class Interface
{
public:
virtual void Insert(int i) { std::cout << "Interface::Insert" << std::endl; }
};

class MySqlImpl : public Interface
{
public:
/// 由于 `override` 要求必须该函数对基类函数进行覆盖,这里由于入参不一致, 会出现编译错误
virtual void Insert(std::string s) override { std::cout << "MySQL::Insert" << std::endl; }
};

虚函数表

一个基类的虚函数,在不同派生类中实现,会产生多个虚函数表。

派生类的多个实例都会保存一个指针,该指针指向对应虚函数的实现(即对应的虚函数表)。

虚函数表中放入特定实现的函数指针,被调用时,通过函数指针来调用对应的汇编。

类成员变量初始化顺序

成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。因为成员变量的初始化次序是根据变量在内存中次序有关,而内存中的排列顺序早在编译期就根据变量的定义次序决定了.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class a
{
public:
a(): y(0), x(y+1) {}
int x;
int y;
};

int main()
{
a obj;
std::cout << "x: " << obj.x << ", y: " << obj.y;
return 0;
}

输出:
x: -858993459, y: 0

查询

最简单的查询语句: 查询 dcuser 表中所有字段。

简单查询

1
SELECT * FROM dcuser;

以上语句在代码中禁止使用,因为在数据库扩展时,SELECT * 语句对应的结构体如果没有增加字段,则会出现未知错误。

指定字段查询

1
SELECT UserID, Name, DCType, DepotID FROM dcuser;

输出:

UserID Name DCType DepotID
3000 dis01 65535 1
3001 dis02 65535 1
3002 dis03 65535 1
3003 dis04 65535 1
9999 test 2 1

查询添加过滤条件

查询 dcuser 表中具有行车调度权限(DCType = 1)的记录。

1
SELECT UserID, Name, DCType, DepotID FROM dcuser WHERE DCType = 1;

查询 groupcallinfo 表中 DCUserID1300313004

1
SELECT DCUserID, GroupID, Time, CallType FROM groupcallinfo WHERE DCUserID IN (13003, 13004);

BETWEEN AND

查询 groupcallinfo 表中 DCUserID 介于 1300313005

1
2
SELECT DCUserID, GroupID, Time, CallType FROM groupcallinfo WHERE DCUserID BETWEEN 13003 AND 13005;
SELECT DCUserID, GroupID, Time, CallType FROM groupcallinfo WHERE DCUserID >= 13003 AND DCUserID <= 13005;

模糊查询

查询 dcuser 表中所有以 Name 字段以 dis 开头的内容。 % 代替任意数量字符。

1
SELECT UserID, Name, DCType, DepotID FROM dcuser WHERE Name LIKE 'dis%'

查询 dcuser 表中所有以 Name 字段以 dis0 + 任意一个字符的内容。 _ 代替一个任意字符。

1
SELECT UserID, Name, DCType, DepotID FROM dcuser WHERE Name LIKE 'dis0_'

查询结果排序

按时间降序查询 groupcallinfo 表中的数据。

升序(ASC): 数值小的记录在前。
降序(DESC): 数值大的记录在前。

如果不写关键字, 则默认使用 升序ASC

1
SELECT SeqID, DCUserID, GroupID, Time FROM groupcallinfo ORDER BY Time DESC;

也可以使用多字段排序。 按照 Time 字段降序, ASC 字段升序排列。

1
SELECT SeqID, DCUserID, GroupID, Time FROM groupcallinfo ORDER BY Time DESC, SeqID ASC;

查询总数量

查询 dcuser 表中有几个全功能调度员的账号。

1
SELECT COUNT(*) AS '记录数' FROM dcuser WHERE DCType = 65535;

输出:

记录数
2

限制查询记录条数

查询组呼记录,只显示100条。

1
SELECT DCUserID, GroupID, Time, CallType FROM groupcallinfo LIMIT 100;

子查询

查询组呼记录表中所有具有行车调度权限调度台处理的记录。

翻译为 SQL 语句:

查询 groupcallinfo 表中, DCUserID 等于 dcuser 表中 DCType 等于 1 记录的 UserID 字段的值

三句话等价:

1
2
3
SELECT DCUserID, GroupID, Time, CallType FROM groupcallinfo WHERE DCUserID IN (SELECT UserID FROM dcuser WHERE DCType = 1);
SELECT DCUserID, GroupID, Time, CallType FROM groupcallinfo WHERE DCUserID = ANY(SELECT UserID FROM dcuser WHERE DCType = 1);
SELECT A.DCUserID, A.GroupID, A.Time, A.CallType FROM groupcallinfo A WHERE A.DCUserID IN (SELECT B.UserID FROM dcuser B WHERE B.DCType = 1);
1
SELECT * FROM contacts WHERE (surname, firstname) IN (SELECT surname, firstname FROM customer);

ALL 与 ANY 关键字:

找出 class1 中比 class2 所有 source 都高的学生信息。

1
SELECT * FROM class1 WHERE source > ALL(SELECT source FROM class2);

找出 class1 中 second_name 与 class2 中的重名的学生信息。

1
SELECT * FROM class1 WHERE second_name = ANY(SELECT second_name FROM class2);

查询最大、最小、平均值

1
SELECT MAX(EndTime-StartTime) AS '最大通话时长', MIN(EndTime-StartTime) AS '最小通话时长', AVG(EndTime-StartTime) AS '平均通话时长' FROM trainposcallinfo;

输出:

最大通话时长 最小通话时长 平均通话时长
48 2 14.667

查询数据分组

统计不同的 DCUserID 都有多少条组呼记录。

1
SELECT DCUserID, COUNT(*) AS "总数" FROM groupcallinfo GROUP BY DCUserID;

输出:

DCUserID 总数
13001 13
13003 662
13005 53
13006 131

WITH ROLLUP

WITH ROLLUP 用来在 GROUP BY 统计的基础上再加一行总数的统计行。

1
SELECT DCUserID, COUNT(*) AS "总数" FROM groupcallinfo GROUP BY DCUserID WITH ROLLUP;

输出:

DCUserID 总数
13001 13
13003 662
13005 53
13006 131
859

HAVING 与 WHERE 区别

having子句与where都是设定条件筛选的语句,有相似之处也有区别。

having与where的区别:
having是在分组后对数据进行过滤
where是在分组前对数据进行过滤
having后面可以使用聚合函数
where后面不可以使用聚合

在查询过程中执行顺序:from>where>group(含聚合)>having>order>select。

所以聚合语句(sum,min,max,avg,count)要比having子句优先执行,而where子句在查询过程中执行优先级别优先于聚合语句(sum,min,max,avg,count)。
where子句:
select sum(num) as rmb from order where id>10
//只有先查询出id大于10的记录才能进行聚合语句

Mysql中having和where的区别

查询过滤重复数据

1
SELECT DISTINCT DCType FROM dcuser;

输出:

DCType
65535
2

说明: DISTINCT 关键词修饰的是语句整体,不能对单独字段修饰,并查询其他字段内容。
如:

1
SELECT DISTINCT DCType, UserID FROM dcuser;

如上语句意义为查询 DCType 且 UserID 同时不重复的列。

输出:

DCType UserID
65535 3000
65535 3001
65535 3002
65535 3003
2 9999

替换特定字段查询结果

查询 dcuser 表中所有数据, DCType 字段等于 65535 的显示全功能调度员, 等于 1 的显示行车调度员, 其他取值显示原本的值, 该字段结果显示为调度员类型

1
SELECT CASE DCType WHEN 65535 THEN "全功能调度员" WHEN 1 THEN "行车调度员" ELSE UserID END AS "调度员类型" FROM dcuser;

分段看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT

CASE DCType
WHEN 65535 THEN
"全功能调度员"
WHEN 1 THEN
"行车调度员"
ELSE
UserID
END
AS "调度员类型"

FROM
dcuser;

输出:

调度员类型
全功能调度员
14004
行车调度员

关于 CASE... WHEN... 更多用法见下面链接

SQL之CASE WHEN用法详解
SQL 查询:SELECT CASE 条件赋值
关于case when复杂sql语句查询

自连接

SQL SELECT(复杂查询)之 自连接 & 子查询 解析
sql中自连接的使用
010-MySQL:自连接查询
算法工程师-SQL进阶:神奇的自连接与子查询

重命名表

改变 dcuser 表名称到 dcuser_new

1
RENAME TABLE dcuser TO dcuser_new;

删除表

删除整个 dcuser 表。

1
DROP TABLE dcuser;

清空表

删除表信息的方式有两种 :

1
2
TRUNCATE TABLE dcuser;
DELETE FROM dcuser;

注 : truncate操作中的table可以省略,delete操作中的*可以省略

truncate、delete 清空表数据的区别 :
1> truncate 是整体删除 (速度较快),delete是逐条删除 (速度较慢)
2> truncate 不写服务器 log,delete 写服务器 log,也就是 truncate 效率比 delete高的原因
3> truncate 不激活trigger (触发器),但是会重置Identity (标识列、自增字段),相当于自增列会被置为初始值,又重新从1开始记录,而不是接着原来的 ID数。而 delete 删除以后,identity 依旧是接着被删除的最近的那一条记录ID加1后进行记录。如果只需删除表中的部分记录,只能使用 DELETE语句配合 where条件

参考资料:
MySQL 清空表(truncate)与删除表中数据(delete) 详解

Update

1
UPDATE runoob_tbl SET runoob_title='学习 C++' WHERE runoob_id=3;

过程、函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
DELIMITER //

DROP PROCEDURE IF EXISTS Create10K//

CREATE PROCEDURE `Create10K`(IN `for_time` INT, IN `type_int` INT, IN `info_text` VARCHAR(255))
BEGIN
DECLARE i INT DEFAULT 0;
WHILE i < for_time * 10000 DO
INSERT INTO test_table(Type, Info) VALUES(type_int, info_text);
SET i = i + 1;
END WHILE;
END//

DELIMITER ;

CALL Create10K(500, 1, "1");

参考资料

常用SQL查询语句

插入主键重复的数据

插入 dcuser 表中一条数据,如果主键重复则更新原数据的 StationList 字段。

1
INSERT INTO dcuser(UserID, Name, DCType, DepotID, StationList) VALUES(14005, '14005', 1, 1, "1,2,3,4") ON DUPLICATE KEY UPDATE StationList = "1,2,3,4";

按天查询数量

1
SELECT UNIX_TIMESTAMP(date_format(FROM_UNIXTIME(SendTime),'%y-%m-%d 0:0:0')), count(*) FROM sds_info GROUP BY date_format(FROM_UNIXTIME(SendTime),'%y-%m-%d');

参考资料

MySQL 8.0 Reference Manual
Chapter 7 Examples of Common Queries
13.2.9 REPLACE Statement

TODO: 合并查询、分页查询、空值判断、Contact 拼接查询结果、REPLACE INTO、多条插入、事务、存储过程、视图、批量插入、my.ini 的配置项的研究、中间表查询
SQL中的循环、for循环、游标

C++ 类型大小 (32bit 与 64bit)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>

int main()
{
std::cout << "---标准类型大小---" << std::endl;
std::cout << "short size: " << sizeof(short) << std::endl;
std::cout << "int size: " << sizeof(int) << std::endl;
std::cout << "unsigned int size: " << sizeof(unsigned int) << std::endl;
std::cout << "long size: " << sizeof(long) << std::endl;
std::cout << "unsigned long size: " << sizeof(unsigned long) << std::endl;
std::cout << "long long size: " << sizeof(long long) << std::endl;
std::cout << "unsigned long long size: " << sizeof(unsigned long long) << std::endl;
std::cout << "size_t size: " << sizeof(size_t) << std::endl;
std::cout << "double size: " << sizeof(double) << std::endl;
std::cout << "float size: " << sizeof(float) << std::endl;
std::cout << "char size: " << sizeof(char) << std::endl;
std::cout << "unsigned char size: " << sizeof(unsigned char) << std::endl;
std::cout << "signed char size: " << sizeof(signed char) << std::endl;
std::cout << "pointer size: " << sizeof(void*) << std::endl;

std::cout << std::endl;
std::cout << "---自定义类型大小---" << std::endl;
std::cout << "SHORT(short) size: " << sizeof(short) << std::endl;
std::cout << "INT(int) size: " << sizeof(int) << std::endl;
std::cout << "LONG(long) size: " << sizeof(long) << std::endl;
std::cout << "UINT8(unsigned char) size: " << sizeof(unsigned char) << std::endl;
std::cout << "UINT16(unsigned short) size: " << sizeof(unsigned short) << std::endl;
std::cout << "UINT32(unsigned int) size: " << sizeof(unsigned int) << std::endl;
std::cout << "UINT64(unsigned long long) size: " << sizeof(unsigned long long) << std::endl;
std::cout << "INT8(char) size: " << sizeof(char) << std::endl;
std::cout << "INT16(short) size: " << sizeof(short) << std::endl;
std::cout << "INT32(int) size: " << sizeof(int) << std::endl;
std::cout << "INT64(long long) size: " << sizeof(long long) << std::endl;
std::cout << "FLOAT(float) size: " << sizeof(float) << std::endl;
std::cout << "DOUBLE(double) size: " << sizeof(double) << std::endl;
std::cout << "CHAR(char) size: " << sizeof(char) << std::endl;
std::cout << "BOOL(int) size: " << sizeof(int) << std::endl;
std::cout << "BYTE(unsigned char) size: " << sizeof(unsigned char) << std::endl;
std::cout << "HANDLE(void*) size: " << sizeof(void*) << std::endl;

return 0;
}
类型 VS2010 32bit VS2010 64bit VS2019 32bit VS2019 64bit Linux 32bit Linux 64bit 备注
short 2 2 2 2 2
int 4 4 4 4 4
long 4 4 4 4 8 不同
long long 8 8 8 8 8
unsigned short 2 2 2 2 2
unsigned int 4 4 4 4 4
unsigned long 4 4 4 4 8 不同
unsigned long long 8 8 8 8 8
size_t 4 8 4 8 8 不同
char 1 1 1 1 1
signed char 1 1 1 1 1
unsigned char 1 1 1 1 1
float 4 4 4 4 4
double 8 8 8 8 8
pointer 4 8 4 8 8 不同
SHORT(short) 2 2 2 2 2
INT(int) 4 4 4 4 4
LONG(long) 4 4 4 4 8 不同
UINT8(unsigned char) 1 1 1 1 1
UINT16(unsigned short) 2 2 2 2 2
UINT32(unsigned int) 4 4 4 4 4
UINT64(unsigned long long) 8 8 8 8 8
INT8(char) 1 1 1 1 1
INT16(short) 2 2 2 2 2
INT32(int) 4 4 4 4 4
INT64(long long) 8 8 8 8 8
FLOAT(float) 4 4 4 4 4
DOUBLE(double) 8 8 8 8 8
CHAR(char) 1 1 1 1 1
BOOL(int) 4 4 4 4 4
BYTE(unsigned char) 1 1 1 1 1
HANDLE(void*) 4 8 4 8 8 不同

结论:

  1. size_tpointerlongunsigned longHANDLE这三种类型在32bit64bit的大小有差别。

Git 使用场景

场景:仓库中的临时文件

我们编译出来了大量临时文件或很大的二进制文件,如 .o, .lib文件,这些文件不想上传。

提出问题: 想要在本文件夹中做版本控制,但需要忽略某些特定的文件

解决方案: 使用.gitignore文件来标记不想要进行版本控制的临时文件。

.gitignore 文件的用法:

.gitignore文件是由我们自己创建, 并默认放置在仓库的根目录。
Git 默认会忽略.gitignore中的文件名的大小写, 不过我们可以通过git config core.ignorecase false,来设置为不忽略大小写。

  1. 文件内容格式
1
2
3
4
5
6
7
8
9
10
11
12
13
vim .gitignore

# 忽略.lib为后缀的文件
*.lib

# libmysql.lib 这个文件不忽略
!libmysql.lib

# 忽略所有的bin文件夹
bin/

# 忽略根目录下的bin文件夹
/bin/
  1. 已经被忽略的文件如何添加到暂存区: git add -f <filename>
  2. 已经添加到暂存区中的文件如何忽略: git rm --cached <filename>

场景: 不小心提交了一个临时文件

我们对这个临时文件不想做版本跟踪,但是在.gitignore文件中添加该文件,这个文件仍然会被追踪。

提出问题: 如何忽略一个已经被追踪的文件?

解决方案:

  1. git rm filename直接从仓库中删除该文件,并把该改动commit后,随后在.gitignore中添加该文件为忽略。
  2. git update-index --assume-unchanged <filename>, 这个操作不会删除该文件,也不用提交,但命令太长

场景: 需要标记一个特定的版本

当我们的代码进入到比较稳定,或者开发出了一个功能,需要标记一个commit来作为稳定版本的基准。

提出问题: 如何为commit添加标记和备注信息

解决方案: 使用git tag为版本打标签

1
2
git tag -a vx.x.x -m "message"
git push origin tags ///< 推送到远端

场景: 修改远端标签名称

修改tag名 v1.0重命名v2.0

1
2
3
4
git tag 新tag名称 旧tag名称
git tag -d 旧tag名称
git push origin :refs/tags/旧tag名称
git push --tags

场景: 开发一个功能

某项功能可能开发时间较久,但又想把未完成的代码上传到远端版本库,来实现多台电脑同步。

例如:在开发随车通信需求时,在linux上编译dis,而我开发的环境在windows上, 当我在本地window开发的临时代码,想要放到linux机器上,这时我们需要分支来对代码的同步。

提出问题: 怎样才能在不影响远端仓库的代码的情况下,在远端备份开发过程代码?

解决方法: 使用 git branch
brunch 介绍:
brunch意味着你可以从主分支中,分叉出来一个分支来提交代码而不影响主分支的代码。

1
2
3
4
5
6
7
8
9
10
11
12
/// 创建并切换到分支
git checkout -b <branch_name>

/// 做相应的提交,修改
git add .
git commit -m"some comment"

/// 把本地分支上传到远端
git push origin <branch_name>

/// 切换到另一台电脑上,拉去自己的分支
git pull origin <branch_name>

合并分支:

1
2
3
4
5
git checkout develop    ///< 当前处于develop分支下
git merge master ///< 把master的东西合入到当前分支,方便在自己开发的分支上处理冲突
git checkout master ///< 切换到master分支
git merge develop ///< 把develop合并到当前分支
git branch -d future ///< 把合并过的分支删除

场景:临时切换分支

我们会遇到临时切换回主分支的情况。
例如: 当我在开发随车通信功能开发一半时,雷总让我在仓库中提交一个文档。如果我在自己的开发分支上上传该文档,那么在master分支上会没有这个文档,其他人也获取不到,所以只能切换回master分支上进行上传。

提出问题: 快速切换分支,做完提交,切换回开发分支时,工作区应跟切换分支前一样。

解决方案:
那么现在分为两种情况:

  1. 我们工作区没有未被commit的文件,那么我们直接git checkout <branch_name>, 即可切换到相应的分支。
  2. 我们工作区有很多未被commit的代码,这时我们可以选择,把工作区内代码全部commit或者选择使用git stash来临时把未被commit的代码给存储起来, 在我们切换回开发分支时,再把临时存储的代码拿出来。

stash介绍:

1
2
3
4
5
6
7
8
9
10
11
12
13
/// 把所有未commit的文件(工作区、暂存区里的文件)都放入一个临时的分支,使工作区可以切换分支
git stash

git stash save "some comment"

/// 切换到其他分支,并做一些提交, 并切换回自己的开发分支
git checkout master
git add .
git commit -m"some comment"
git checkout develop

/// 把临时存储的代码给拿出来,放入工作区(之前暂存区的文件在pop后的状态是到工作区)
git stash pop

git stash --include-untrackedgit stash -u 来存储未被跟踪的文件

场景: 某个commit,提交错分支了

开发过程中,突然出现了一个BUG需要立即修复,我们急着修复,把修复的代码放入了正在大改开发分支上。
提出问题: 我们需要怎样,把主分支上的BUG给修正过来
解决方案:

  1. 切换到主分支,再次把刚才修改的文件,同样在主分支上进行修改,再次进行提交。
  2. 切换到主分支,使用git cherry-pick <SHA>把特定提交给放到主分支中。

方案一存在修改的不一致,当后面需要合并分支时,需要处理冲突。
方案二快速提交,不用再次使用手动修改文件。

git cherry-pick <SHA>用法示例:
该操作会把特定的commit给,放入当前所在的分支,并产生一个新的提交

之前分支的情况:

1
2
3
a - b - c - d - f   Master
\
e - f - g Feature

cherry-pick操作

1
2
3
4
/// 切换到
git checkout master
/// 把提交f给提交到本分支
git cherry-pick f

操作后的分支情况:

1
2
3
a - b - c - d - f   Master
\
e - f - g Feature

场景: commit的信息输入错了

提出问题: 怎么修改提交的commit信息

解决方案:

  1. 修改最近一次提交的commit
1
2
3
4
git commit --amend

/// 进入到提交的文件里面,默认使用vim打开
/// 修改好提交信息,保存后退出
  1. 如果想要修改之前的commit
1
2
3
4
5
6
git rebase -i HEAD~3    ///< 回退到HEAD前面第三个commit处

/// 想要修改哪一个提交就把pick换成你想要的操作,edit
git commit --amend
/// 然后执行
git rebase --continue
  1. 如果该 commit 已经 push 到远端
1
2
git commit --amend
git push --force-with-lease origin <分支名称>

场景: 开发到一半,发现修改的思路有误

我们从远端仓库拉去最新代码,修改过程中,发现修改错误了,想再从已经提交的代码上重新开始。

提出问题: 如何回退版本

解决方案:

  1. 没有commit想要回退, 只是清除工作区修改的代码, 如何让当前已经修改过的代码恢复到HEAD的最新提交代码一致, 即清除工作区修改的代码
1
2
3
4
5
/// 清除所有没有被暂存的改动
git checkout .

/// 清除该文件没有被暂存的改动
git checkout filename
  1. 想要撤销上一个commit
1
2
3
4
5
/// 删除工作区改动的代码,撤销最近一次的commit,撤销git add .
/// 注意完成这个操作后,就恢复到了上一次的commit状态。
git reset --hard HEAD^
/// HEAD 指向 commit_id 指向的提交
git reset --hard <commit_id>
  • --hard换成--soft, 则会保留已经暂存和修改的文件
  • HEAD^换成HEAD~2则可以回退两个commit

清除工作区的修改

1
2
3
4
5
6
7
8
9
git reset --hard <commit_id>    /// 返回到某个节点,不保留修改,已有的改动会丢失
git reset --soft <commit_id> /// 返回到某个节点,保留修改,已有的改动会保留,在未提交中, `git status` 或 `git diff` 查看

git clean -df /// 返回到某个节点(未跟踪文件的删除)
git clean -n /// 不实际删除,展示即将哪些文件要被删除
git clean -f /// 不实际删除,展示即将哪些文件要被删除
git clean -i /// 显示将要删除的文件
git clean -d /// 递归删除目录及文件(未跟踪的文件)
git clean -q /// 仅显示错误,成功删除的文件不显示

示例:

1
2
git clean -nxdf /// 查看要删除的文件及目录,确认无误后再使用下面的命令进行删除
git checkout . && git clean -xdf

revert 和 reset

  1. revert
    首先肯定的是 revertgit revert commit_id 能产生一个 与 commit_id 完全相反的提交,即 commit_id 里是添加, revert 提交里就是删除。
    revert 会生成一个新的提交记录,但不适合回退多个提交。

  2. reset
    reset 的原理是把 HEAD 的指向,并删除回退后的版本之后的提交(被删除的提交可以通过 git reflog 查看)。git reset --hard <commit_id>
    但是由于是本地回退版本,所以在推送至远端时,需要使用 git push -f origin master 的命令象只覆盖远端分支。由于我们的远端仓库大部分都是对 master 分支进行保护不允许使用 -f 强制覆盖。我们可以先回退 develop 分支, 在 develop 分支上在创建一次提交(该提交已经领先于远端master分支), 再提交至远端 develop 分支后 merge

场景: 想要找到某个特定业务的所有提交

假设我们的commit的信息都是采用模板来填写的,且已经有大量的commit时候,需要过滤检索一些特定提交信息的commit

提出问题: 如何使用关键字搜索提交信息

解决方案:
使用git自带的文字搜索功能git log --all --grep='TrainPosCall', 搜索提交信息中带有TrainPosCallcommit

场景: 想要确认代码的改动

想要分步提交修改库函数的文件和修改业务逻辑的文件,需要确认每个文件的改动。

提出问题: 怎么查看已修改的代码对比之前的版本

解决方案:

  1. 查看尚未缓存的改动:git diff
  2. 查看已缓存的改动: git diff --cached, git diff --staged
  3. 查看已缓存的与未缓存的所有改动:git diff HEAD
  4. 显示摘要而非整个 diff: git diff --stat
  5. 版本号与版本号之间的差别: git diff <SHA> <SHA>

场景误删除分支

在误删除分支后,可以使用 git reflog 来查看分支的commit id并使用该commit id来创建一个新的分支
git branch recover-branch [commit id]

场景: git 账户修改密码

操作git时, 出现错误

1
remote: HTTP Basic: Access denied

管理员权限输入以下命令后在命令行中操作git,重新输入用户名,密码。

1
git config --system --unset credential.helper

参考资料

git Reference

Pro Git

Git新手教程-添加忽略文件(十)

Git Cheat Sheet – 50 Git Commands You Should Know

git diff 命令

Git diff

git clean 删除忽略文件 和 未被跟踪文件及文件夹

git-branch - List, create, or delete branches

How to search a Git repository by commit message?

强制类型转换的应用

C语言中void* 可以转换为任意指针

size_t 到 unsigned

变量初始化

  • 从未对变量赋值。它的值只是程序启动时变量所处内存区域的值
  • 变量值已经过期。变量在某个地方曾经被赋值,但该值已经不再有效
  • 变量的一部分被赋值,而另一部分没有

在声明变量的时候初始化

理想情况下,在靠近第一次使用变量的位置声明和定义该变量

  • 在有可能的情况下使用const, 定义常量,入参。
  • 特别注意计数器和累加器,在下一次使用时忘记重置其值。
  • 在类的构造函数中,初始化该类的数据成员
  • 检查是否需要重新初始化
1
2
3
4
5
int index = 0;
for(int i = 0; i < 10; ++i)
{
/// do something with index
}
1
2
3
4
5
for(int i = 0; i < 10; ++i)
{
int index = 0;
/// do something with index
}

尽可能缩短变量存活时间

短的变量存活时间减少了初始化错误的可能。

变量存活时间短还会使代码具有可读性。阅读者同一时间内需要阅读的代码越少,越容易理解代码。

当需要把一个大的函数,拆分成几个小程序,短的存活时间方便拆分。

在循环开始之前再去初始化该循环里使用的变量,而不是在该循环所属的子程序的开始处初始化这些变量。

直到变量即将被使用时再为其赋值。

把相关语句放在一起。把相关语句提取成单独的子程序。

开始时采用最严格的可见性,然后根据扩展变量的作用域。比如,把一个循环内的变量挪到循环外的难度要比反过来难度低,或把一个private转变为public的难度远比反过来难度低。

避免采用硬编码,宏定义总是好于硬编码。

  • TITLE_BAR_COLOR0xFFFFFF 更能反应出所代表的信息
  • 同时,也方便修改宏定义时,同时改变所有的颜色的RGB值

为变量指定单一用途

1
2
3
4
5
6
7
8
temp = Sqrt(b*b - 4*a*c);
root[0] = (-b + temp) / (2*a);
root[1] = (-b - temp) / (2*a);

// swap the roots
temp = root[0];
root[0] = root[1];
root[1] = temp;
1
2
3
4
5
6
7
8
discriminant = Sqrt(b*b - 4*a*c);
root[0] = (-b + discriminant) / (2*a);
root[1] = (-b - discriminant) / (2*a);

// swap the roots
oldRoot = root[0];
root[0] = root[1];
root[1] = oldRoot;

避免让代码具有隐含含义,把同一变量用于多个多个用途的另外一种方式是当变量代表不同事务时让其具有不同的取值集合。

  • 变量count的取值可能表示某个计数,除非他等于-1,在这种情况下表明有错误发生
  • 变量customerId可能代表某个客户账号,除非他的取值大于50000,在这种情况下,你通过减去50000来得到过期账号。
  • 变量bytesWritten可能表示写入输出文件的字节数,除非它的取值为负,在这种情况下他表示的是用于输出磁盘驱动器的号码。

变量名的注意事项

糟糕的变量名

1
2
3
4
x = x - xx;
xxx = fido + SalesTax(fido);
x = x + LateFee(x1, x) + xxx;
x = x + Interest(x1, x);

良好的变量名

1
2
3
4
balance = balance - lastPayment;
monthlyTotal = newPurchases + SalesTax(newPurchases);
balance = balance + LateFee(customerID, balance) + monthlyTotal;
balance = balance + Interest(customerID, balance);

为变量命名时最重要的考虑事项时,改名字要完全、准确地描述该变量所代表的事物
不包含晦涩的缩写,同时也没有歧义。
对于一个表示中国奥林匹克代表团成员数量的变量,你可能会使用NumberOfPeopleOnTheChineseOlympicTeam
表示当前利率的变量最好为rate而不是r.

变量名太长: numberOfPeopleOnTheChineseOlympicTeam,numberOfSeatsInTheStadium, maximumNumberOfPointsInModernOlympics
变量名太短: n, np, ntm, ms, nsisd, m, max, min
变量名正好: numTeamMembers, teamMemberCount, numSeatsInStadium, seatCount, teamPointsMax, pointSRecord

很多程序有表示计算机结果的变量:总额、平均值、最大值,等等。如果你要用类似于TotalSumAverageMaxMinRecord这样的限定词,那么请一定记住把限定词加到名字最后。
变量名中最重要的部分应该被放置在最前面,限定词在最后。
这样做会避免,totalRevenuerevenueTotal异议词语

为状态变量起一个比flag更好的名字。最好把标记flag看作状态变量,标记的名字中不应该含有flag,因为你从中丝毫看不出该标记是做什么的。
含义模糊的标记

1
2
3
4
if (flag) ...
if (statusFlag & 0xF) ...
if (printFlag == 16) ...
if (computeFlag == 0) ...

更好的状态变量命名

1
2
3
4
if (dataReady) ...
if (characterType & PRINTABLE_CHAR) ...
if (reportType == ReportType_Annual) ...
if (recalcNeeded == false) ...

为布尔变量命名

  • donedone表示某件事情已经发生之前把变量值设为false, 在错误已经发生时把它设为true
  • errorerror表示有错误发生。在错误发生之前把变量值设为false, 在错误已经发生时把它设为true
  • foundfound来表明某个值已经找到了。在没有找到设为false, 找到后设为true.
  • successok, 操作失败时设为false, 操作成功后设为true

给布尔变量赋予隐含“真、假”含义的名字: statussourceFile是很糟糕的布尔变量名。
应该把status替换为类似error或者statusOK这样的名称,把sourceFile替换为sourceFileAvailablesourceFileFound

使用肯定的布尔变量名,否定的布尔名如notFoundnotDone以及notSuccessful比较难阅读。使用肯定的语义避免双重否定带来的阅读难度。

1
2
3
4
5
6
7
8
9
10
11
AnsiString strTmp;
strTmp = edtAccount->Text;
if(strTmp.IsInvalid())
{
st.Account = strTmp;
}
strTmp = edtPassword->Text;
if(strTmp.IsInvalid())
{
st.Password = strTmp;
}

缩写的一般指导原则:

  • 使用标准的缩写(列在字典中的那些常见缩写)
  • 去掉虚词and, or, the
  • 去掉无用的后缀ing, end
  • 确保不要改变变量的含义
  • 反复使用上述技术,知道你把每个变量名的长度缩减到了8到20个字符,或者达到你所用的编程语言对变量名的限制字符数。

不要用每个单词中删除一个字符的方式来缩写

键入一个字符算不上是什么额外工作,而节省一个字符带来的便利却很难抵消由此而造成的可读性的损失。

缩写要一致

应该一直使用相同的缩写。要么全用Num,要么全用No,也不要有些地方使用全写Number, 同时在其他地方使用缩写Num

创建你能读出来的名字

使用xPos而不是xPstn, 用needsCompu而不用ndsCmptg。这里可以使用电话沟通,如果你无法向他人读出你的代码,就请重新给变量起一个更清晰的名字。

名字对于代码的读者的意义要比对作者更重要

避免使用令人误解的名字或缩写

要确保名字的含义是明确的

避免使用具有相似含义的名字

如果你能够交换两个变量的名字而不会妨碍对程序的理解,那么你就需要为这两个变量重新命名了。

避免在名字中使用数字

如果名字中的数字真的非常重要,就请使用数组来代替一组单个的变量。如果数组不合适,那么数字就更不合适。

避免在名字中拼错单词

避免在名字中使用容易混淆的字符

  • 数字1和小写的l
  • 数字1和大写的L
  • 数字0和大写的O
  • 数字2和小写的z
  • 数字6和大写的G

避免浮点数的数量级相差巨大的数字之间的四则运算

1
2
3
4
5
6
7
8
#include <iostream>

int main()
{
double d = 100000000.0 + 0.1;
std::cout << d << std::endl;
return 0;
}

避免浮点数的等量比较

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

int main()
{
double tmp = 0.0;
for(int i = 0; i < 10; ++i)
{
tmp += 0.1;
std::cout << tmp << std::endl;
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int a = 0;
int b = 0;
bool IncreaseA()
{
a++;
return false;
}
bool IncreaseB()
{
b++;
return true;
}

int main(int argc, char* argv[])
{
if(!IncreaseA() || !IncreaseB())
{
}
}

为空语句创建一个DoNothing()预处理宏或者内联函数

1
2
3
4
5
6
7
8
9
10
while(recordArray.Read(index++) != recordArray.EmptyRecord())
{
;
}

#define DoNothing()
while(...)
{
DoNothing();
}

通常我们使用对象内的拷贝构造函数和拷贝构造符来进行初始化和拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class A
{
public:
/// 拷贝构造函数
A(const A& other)
{
i = other.i;
vec = other.vec;
}

/// 拷贝赋值符
A& operator=(const A& other)
{
if(this != &other)
{
i = other.i;
vec = other.vec;
}
return *this;
}
private:
int i;
std::vector<int> vec;
};

A foo;
A bar(foo); ///< 在这里调用拷贝构造函数
A bar2;
bar2 = foo; ///< 这里调用拷贝赋值符

关于memset

首先说结论,不推荐使用memset对某个对象进行擦写内存。因为可能导致未定义行为。
具体可以查看stackflow上的这个问题 memset for initialization in C++Use memset or a struct constructor? What’s the fastest?

你可以使用构造函数进行初始化,也可以定义成员函数clear(), 或是使用std::fill, std::fill_n
在使用函数memset时,有部分限定条件,只有目标对象为POD类型才可以使用。

简单来说就是,该对象如果没有继承,都是基础类型(如: intchar或其他POD类型), 没有包含如std::array, std::vector等STL容器, 该对象可以称为POD类型。如下面示例

1
2
3
4
5
6
7
class pod
{
char ac[12];
int i;
float f;
long l;
};

关于POD具体查看C++ named requirements: PODType

关于memcpy

结论是,不推荐使用,同样除了你能确保该对象为POD类型,否则则会导致未定义现象。
可以使用拷贝构造函数或拷贝赋值符,或是std::copystd::copy_n来代替memcpy;

华为C++语言编程规范

规则

3.7.1 if/循环语句必须使用大括号

1
2
3
4
5
6
7
8
9
if (...)
{
return false; ///< Good
}

while(true)
{
DoSomething(); ///< Good
}

4.4.3 不用的代码段直接删除,不要注释掉

被注释掉的代码,无法被正常维护;当企图恢复使用这段代码时,极有可能引入容易被忽略的缺陷。正确的做法是,不需要的代码直接删除掉。若再需要时,考虑移植或重写这段代码。使用版本控制来,记录代码。

5.2.3 禁止通过声明的方式引用外部函数接口、变量

只能通过包含头文件的方式使用其他模块或文件提供的接口。通过extern声明的方式使用外部函数接口、变量,容易在外部接口改变时可能导致声明和定义不一致。同时这种隐式依赖,容易导致架构腐化。

5.2.4 禁止在extern "C"中包含头文件

extern "C" 中包含头文件,有可能导致extern "C"嵌套,部分编译器对extern "C"嵌套层次有限制,嵌套层次太多会编译错误。

7.1.1 类的成员变量必须显式初始化

如果类有成员变量,没有定义构造函数,有没有定义默认构造函数,编译器将自动生成一个构造函数,但编译器生成的构造函数并不会对成员变量进行初始化,对象状态处于一种不确定性。
如果类的成员变量具有默认构造函数,那么可以不需要显式初始化

7.1.3 如果不需要拷贝构造函数、赋值操作符,请明确禁止。

可以将拷贝构造函数或者赋值操作符设置为private,并且不实现。
C++11以后可以使用关键字delete, 来删除该成员函数。

7.1.4 拷贝构造和拷贝赋值操作符应该是成对出现或者禁止

7.1.6 禁止在构造函数和析构函数中调用虚函数

在构造函数和析构函数中调用当前对象的虚函数,会导致未定义的行为。在C++中,一个基类一次只构造一个完整的对象。

7.2.1 基类的析构函数应该声明为virtual

虚析构函数

7.2.2 禁止虚函数使用缺省参数值

在C++中,虚函数是动态绑定的,但函数的缺省参数却是在编译时就静态绑定的。这意味着你最终执行的函数是一个定义在派生类,但使用了基类中的缺省参数值的虚函数。为了避免虚函数重载时,因参数声明不一致给使用者带来的困惑和由此导致的问题,规定所有虚函数均不允许声明缺省参数值。

7.2.3 禁止重新定义继承而来的非虚函数

因为非虚函数无法实现动态绑定,只有虚函数才能实现动态绑定:只要操作基类的指针,即可获得正确的结果。

8.1.1 避免函数过长,函数不超过50行(非空非注释)

函数应该可以一屏显示完(50行以内), 只做一件事情,而且把它做好。
过长的函数往往意味着函数功能不单一,过于复杂,或过分呈现细节,未进行进一步抽象。
例外:某些实现算法的函数,由于算法的聚合性与功能的全面性,可能会超过50行。
即使一个长函数现在工作的很好,一旦有人对其修改,有可能出现新的问题,甚至导致难以发现的BUG。建议将其拆分为更加简短并易于管理的若干函数,以便于他人阅读和修改代码。

9.1.1 不允许使用宏来表示常量

宏是简单的文本替换,在预处理阶段时完成,运行报错时直接报相应的值;跟踪调试时也是显示值,而不是宏名;宏没有类型检查,不安全;宏没有作用域。

1
2
3
4
5
#define MAX_MSI_SDN_LEN 20  ///< Bad

const int MAX_MSI_SDN_LEN = 20; ///< Good
/// C++11以上的版本,可以使用`constexpr`
constexpr int MAX_MSI_SDN_LEN = 20;

9.1.2不允许使用魔鬼数字

魔鬼数字是看不懂、难以理解的数字。
例如type = 12,看不懂,但改成mouthsCount = yearsCount * 12, 就容易理解。
数字0有时候也难以理解。status = 0并不能表达是什么状态。
解决途径: 对于局部使用的数字,可以增加注释说明,对于多处使用的数字,必须定义const常量,并通过符号命名自注释。
禁止出现下列情况: 没有通过符号来解释数字含义,如const int ZERO = 0,符号命名限制了其取值,如const int XXX_TIMER_INTERVAL_300MS = 300
直接使用XX_TIMER_INTERVAL_MS来表示该常量是定时器的时间间隔。

9.1.3 常量应该保证单一职责

一个常量只用来标识一个特定功能,即一个常量不能有多种用途。

好的例子。

1
2
3
4
5
6
7
8
9
10
11
const int A_MAX_MSI_SDN_LEN = 20;
const int B_MAX_MSI_SDN_LEN = 20;

namespace Namespace1
{
const int MAX_MSI_SDN_LEN = 20;
}
namespace Namespace2
{
const int MAX_MSI_SDN_LEN = 20;
}

9.1.4 禁止用memcpy_smemset_s初始化非POD对象

POD类型主要包括int, char, float, double, enumeration, void, pointer等原始类ing以及聚合类型,不能使用封装和面向对象特性(如用户定义的构造/赋值/析构函数、基类、虚函数)
由于非POD类型比如非聚合类型的class对象,可能存在虚函数,内存布局不确定,跟编译器有关,滥用内存拷贝可能会导致严重的问题。
即使对聚合类型的class,使用直接的内存拷贝和比较,破坏了信息隐蔽和数据保护的作用,也不提倡使用memcpy_smemset_s

9.2.1 含有变量自增或自减运算的表达式中禁止再次引用该变量

含有变量自增或自减的表达式中,如果再引用该变量,其结果在C++标准中未明确定义。会产生未定义的结果。
注意,运算次序的问题不能使用括号来解决,因为之不是优先级的问题。

1
x = b[i] + i++; ///< Bad: b[i]运算跟i++, 先后顺序并不明确。

正确的写法是将自增或自减运算单独放一行:

1
2
x = b[i] + i;
i++; ///< Good, 单独一行

函数参数

1
2
3
4
Func(i++, i)    ///< Bad

i++; ///< Good, 自增运算单独放一行
Func(i, i)

9.3.1 如果确定要使用类型转换,请使用由C++提供的类型转换,而不是C风格的类型转换

C++提供的类型转换操作比C风格更具有针对性,更易读,也更安全,C++提供的转换有:

  1. dynamic_cast: 主要用于继承体系下行转换,dynamic_cast具有类型检查的功能,请做好基类和派生类的设计,避免使用dynamec_cast来进行转换。
  2. static_cast: 和C风格相似可做值的强制转换,或上行转换(把派生类的指着或引用转换成基类的指针或引用)。该转换经常用于消除多重继承带来的类型歧义,是相对安全的。
  3. reinterpret_cast: 用于转换不相关的类型。reinterpret_cast强制编译器将某个类型对象的内存重新解释成另一种类型,这是一种不安全的转换,建议尽可能少用reinterpret_cast
  4. const_cast: 用于移除对象的const属性,使对象变得可修改,这样会破坏数据的不变性,建议尽可能少用。

9.5.1 不要保存std::stringc_str()返回的指针

C++标准中并未规定c_str()返回的指针持久有效,所以不要保存。

9.6.1 对于指针和引用类型的形参,如果是不需要修改的,请使用const

不变的值更易于理解/跟踪和分析, 把const作为默认悬念,在编译时会对其进行检查,使代码更安全。

1
void PrintInt(const int& i);

9.6.2 对于不会修改成员变量的成员函数请使用const修饰

尽可能将成员函数声明为const。访问函数应该总是const。只要不修改数据成员的成员函数,都声明为const。对于虚函数,应当从设计意图上考虑继承链上的所有类是否需要在此虚函数中修改数据成员,而不是仅关注单个类的实现。

1
2
3
4
5
6
7
8
9
10
class Foo
{
public:
int PrintValue() const ///< 修饰成员函数, 不会修改成员变量
{
return value_;
}
private:
int value_;
}

建议

2.4.1 避免滥用typedef或者#define对基本类型起别名

除有明确的必要性,否则不要用typedef#define对基本数据类型进行重定义。优先使用<cstdint>头文件中的基本。

有符号类型 无符号类型 描述
int8_t uint8_t 宽度恰为8的有、无符号整数类型
int16_t uint16_t 宽度恰为16的有、无符号整数类型
int32_t uint32_t 宽度恰为32的有、无符号整数类型
int64_t uint64_t 宽度恰为64的有、无符号整数类型
intptr_t uintptr_t 足以保存指针的有、无符号整数类型

3.1.1 行宽不超过120个字符

3.9.1 表达式换行要保持换行的一致性,运算符放行末

1
2
3
if(IsCorrect() &&
IsValid() &&
IsSomething()) {}

3.14.1 合理安排空行,保持代码紧凑

减少不必要的空行,可以显示更多的代码,方便代码阅读。

  • 根据上下内容的相关程度,合理安排空行
  • 函数内部、类型定义内部、宏内部、初始化表达式内部,不使用连续空行
  • 不适用连续三个空行,或更多
  • 大括号内的代码块行首之前和行尾之后不要加空行,但namespace的大括号内不做要求

5.2.1 尽量避免使用前置声明

6.1.1 对于cpp文件中不需要导出的变量,常量或者函数,请使用匿名namespace封装或者用static修饰

更加推荐使用匿名namespace

  1. static在C++中已经赋予了太多的含义,静态函数成员变量,静态成员函数, 静态全局变量,静态函数局部变量,每一种都有特殊的处理。
  2. static只能保证变量,常量和函数的文件作用域,但是namespace还可以封装类型等。
  3. 统一namespace来处理C++的作用域,而不需要同时使用staticnamespace来管理
  4. static修饰的函数不能用来实例化模板,而匿名namespace可以

注意: 不要.h中使用匿名namespace或者static

6.2.1 优先使用命名空间来管理全局函数,如果和某个class有直接关系的,可以使用静态成员函数

非成员函数放在名字控件内可避免污染全局作用域,也不要用类+静态成员方法来简单管理全局函数。如果某个全局函数和某个类有紧密联系,那么可以作为类的静态成员函数。
如果你需要定义一些全局函数,给某个cpp文件使用,那么请使用匿名namespace来管理。

6.4.1 尽量避免使用全局变量,考虑使用单例模式

全局变量是可以修改和读取的,那么这样会导致业务代码和这个全局变量产生数据耦合。

8.3.1 函数参数使用引用代替指针

引用比指针更安全,因为它一定非空,且一定不会再指向其他目标;引用不需要检查非法的NULL指针。

8.3.2 使用强类型参数,避免使用void*

一般认为C/C++是强类型语言,既然我们使用的是强类型语言,就应该保持这样的风格。好处是尽量让编译器在编译阶段就检查出类型不匹配的问题。

8.3.3 函数的参数个数不超过5个

函数的参数过多,会使得该函数易于受外部变化的影响,从而影响维护工作。函数的参数过多同时也会增大测试的工作量。
如果超过可以考虑

  • 看能否拆分函数
  • 看能否将相关参数合在一起,定义结构体

9.1.1 一组相关的整型变量应定义未枚举。

枚举比#defineconst int更安全。编译器会检查参数值是否位于枚举取值范围内,避免错误发生。

9.1.2 变量使用时才声明并初始化

变量在使用前未赋初值,是常见的低级编程错误。使用前才声明变量并初始化,非常方便地避免了此类低级错误。
在函数开始位置声明所有变量,后面才使用变量,作用域覆盖整个函数实现,容易导致如下问题:

  • 程序难以理解与维护: 变量定义与使用分离
  • 变量难以合理初始化:在函数开始时,经常没有足够的信息进行变量初始化,往往用某个默认的空值(0)来初始化。如果变量在被赋予有效值以前使用,还会导致错误。

遵循变量作用域最小化原则与就近声明原则,使得代码更容易阅读,方便了解变量的类型和初始值。特别是,应使用初始化的方式替代声明再赋值。

1
2
3
4
std::string name;   ///< Bad
name = "zhangsan";

std::string name("zhangsan"); ///< Good

9.2.1 表达式的比较,应当遵循左侧倾向于变化,右侧倾向于不变的原则

当变量与常量进行比较时,如果常量放左边, 如if(MAX == v)不符合阅读习惯,而if(MAX > v)更难以理解,应当按人的正常阅读、表达习惯,将常量放右边。

1
2
3
4
5
6
if(value == MAX)
{
}
if(value < MAX)
{
}

也有特殊情况, 如: if(MIN < value && value < MAX)用来描述区间时,前半段是常量在左的。
不用担心将==误写成=, 因为if(value = MAX)会有编译告警,其他静态检查工具也会报错。让工具去解决笔误问题,代码要符合可读性第一。

9.2.2 使用括号明确操作符的优先级

使用括号明确操作符的优先级,防止因默认的优先级与设计思想不符而导致程序出错;同时使得代码更为清晰可读,然而过多的括号会分散代码使其降低了可读性。

1
2
3
x = a + b + ; ///< 操作符相同,可以不加括号
x = 1 << (2 + 3); ///< 操作符不同, 需要括号
x = (a == b) ? a : (a - b); ///< 操作符不同, 需要括号

9.3.1 避免使用dynamic_cast

  1. dynamic_cast依赖于C++的RTTI, 让程序员在运行时识别C++类对象的类型
  2. dynamic_cast的出现一般说明我们的基类和派生类设计出现了问题,派生类破坏了基类的七月,不得不通过
  3. dynamic_cast转换到子类进行特殊处理,这个时候更希望来改善类的设计,而不是通过dynamic_cast来解决问题

9.3.2 避免使用reinterpret_cast

reinterpret_cast用于转换不相关类型。尝试用reinterpret_cast将一种类型强制转换另一种类型,这破坏了类型的安全性与可靠性,是一种不安全的转换。不同类型之间尽量避免转换。

9.3.3 避免使用const_cast

cosnt_cast用于移除对象的constvolatile性质。
使用const_cast转换后的指针或者引用来修改const对象,行为是未定义的。

1
2
3
4
/// 不好的例子
const int i = 1024;
int* p = const_cast<int*>(&i);
*p = 2048; ///< 未定义行为

9.4.1 使用RAII特性来帮助跟踪动态分配

RAII是”资源获取就是初始化”的简写(Resource Acquisition Is Initialization), 是一种利用对象生命周期来控制程序资源(如内存、文件句柄、网络连接、互斥量等等)的简单技术。
RAII的一般做法是这样的: 在对象构造时获取资源,接着控制对资源的访问使之在对象的生命周期内之中有效,最后在对象析构的时候释放资源,这种做法有两大好处:

  • 我们不需要显式地释放资源
  • 对象所需的资源在其生命期内始终有效。这样,就不必检查资源有效性问题,可以简化逻辑、提高效率。

9.5.1 使用std::string代替char*

说明: 使用string代替char*有很多优势,比如:

  1. 不用考虑结尾的'\0'
  2. 可以直接使用+, =, ==等运算符以及其他字符串操作函数
  3. 不需要考虑内存分配操作,避免了显式的newdelete, 以及由此导致的错误

需要注意的是

9.5.2 使用新的标准头文件

使用C++的标准头文件时,请使用<cstdlib>这样的,而不是<stdlib.h>

9.6.1 初始化后不会再修改的成员变量定义为const

1
2
3
4
5
6
7
class Foo
{
public:
Foo(int length): dataLength_(length){}
private:
const int dataLength_;
}

现代C++(since C++11)

规则

10.1.1 在重写虚函数时请使用overridefinal关键字

overridefinal关键字都能保证函数是虚函数,且重写了基类的虚函数。如果子类函数与基类函数圆形不一致,则产生编译错误。final还保证u函数不会再被子类重写。
使用overridefinal还保证虚函数不会再被子类重写。
使用overridefinal关键字后,如果修改了基类函数原型,但忘记修改子类重写的虚函数,在编译期就可以发现,也可以避免有多个子类时,重写虚函数的修改遗漏。

10.1.2 使用delete关键字删除函数

相比于将类成员函数声明为private但不实现, delete关键字更明确,且适用范围更广。

1
2
3
4
5
6
7
8
9
10
11
12
class Foo
{
private:
Foo(const Foo&); ///< 只看头文件不知道拷贝构造函数是否被删除
};

class Foo
{
public:
/// 明确删除拷贝赋值符
Foo& operator=(const Foo&) = delete;
};

另外,delete关键字还支持删除非成员函数。

1
2
3
4
5
template<typename T>
void Process(T value)

template<>
void Process<void> = delete;

10.1.3 使用nullptr, 而不是NULL0

1
2
3
4
5
6
#define NULL ((void*)0)

char* str = NULL; ///< 错误: void* 不能自动转为 char*

void(C::pmf)() = &C::Func;
if(pmf == NULL) {} ///< 错误: void* 不能自动转换为指向成员函数的指针

如果把NULL定义为0, 或者在需要空指针的地方直接使用0。这样引入了另外的问题

1
2
3
4
5
auto result = Find(id);
if(result == 0)
{
/// 无法判断返回的是整数还是空指针
}

重载也会出现重载的问题。

1
2
3
void F(int);
void F(int*`);
F(NULL); ///< 调用F(int), 而不是F(int*)

另外sizeof(NULL) == sizeof(void*)并不总是成立的。

nullptr的又是不仅仅是在字面上代表了空指针,使代码清晰,而且它不再是一个整数类型。
nullptrstd::nullptr_t类型, 而std::nullptr_t可以隐式的转换为所有的原始指针类型,这使得nullptr可以表现成指向任意类型的空指针。

1
2
3
4
5
6
7
8
9
void F(int);
void F(int*`);
F(nullptr); ///< 调用F(int*)

auto result = Find(id);
if(result == nullptr)
{
/// 正确的判断
}

建议

10.1.1 合理使用auto

  • auto可以避免编写冗长、重复的类型名,也可以保证定义变量时初始化
  • auto类型推导规则复杂,需要仔细理解
  • 如果能够使代码更清晰,继续使用明确的类型,且旨在局部变量使用auto
1
2
3
4
5
6
7
/// 避免冗长的类型名
std::map<std::string, std::pair<std::vector<int>, int>> iter = m.find(val);
auto iter = m.find(val);

/// 保证初始化
int x; ///< 编译正确,没有初始化
auto x; ///< 编译失败,没有初始化

泛化关系(generalization)

类的继承结构表现在UML中为:泛化(generalize)与实现(realize):

继承关系为 is-a的关系;两个对象之间如果可以用 is-a 来表示,就是继承关系:(..是..)

eg:自行车是车、猫是动物

泛化关系用一条带空心箭头的直接表示;如下图表示(A继承自B);

generalize_example

eg:猫是一种动物;猫与动物之间为泛化关系。

generalize_example(2)

实现关系(realize)

实现关系用一条带空心箭头的虚线表示;

eg:”猫”和”鸟”运动方式不同,它们的运动方式一个为走一个为飞,必须要在派生类”动物”中提供具体实现,那么”猫”和”鸟”对于基类动物来说为实现关系。

realize_example

聚合关系(aggregation)

聚合关系用一条带空心菱形箭头的直线表示,如下图表示A聚合到B上,或者说B由A组成;

aggregation_example

聚合关系用于表示实体对象之间的关系,表示整体由部分构成的语义;例如一个部门由多个员工组成;

与组合关系不同的是,整体和部分不是强依赖的,即使整体不存在了,部分仍然存在;例如, 部门撤销了,人员不会消失,他们依然存在;

组合关系(composition)

组合关系用一条带实心菱形箭头直线表示,如下图表示A组成B,或者B由A组成;

composition_example

与聚合关系一样,组合关系同样表示整体由部分构成的语义;比如公司由多个部门组成;

但组合关系是一种强依赖的特殊聚合关系,如果整体不存在了,则部分也不存在了;例如, 公司不存在了,部门也将不存在了;

关联关系(association)

关联关系是用一条直线表示的;它描述不同类的对象之间的结构关系;它是一种静态关系, 通常与运行状态无关,一般由常识等因素决定的;它一般用来定义对象之间静态的、天然的结构; 所以,关联关系是一种“强关联”的关系;

比如,乘车人和车票之间就是一种关联关系;学生和学校就是一种关联关系;

关联关系默认不强调方向,表示对象间相互知道;如果特别强调方向,如下图,表示A知道B,但 B不知道A;

association_example

注:在最终代码中,关联对象通常是以成员变量的形式实现的;

依赖关系(dependency)

依赖关系是用一套带箭头的虚线表示的;如下图表示A依赖于B;他描述一个对象在运行期间会用到另一个对象的关系;

dependency_example

与关联关系不同的是,它是一种临时性的关系,通常在运行期间产生,并且随着运行时的变化; 依赖关系也可能发生变化;

显然,依赖也有方向,双向依赖是一种非常糟糕的结构,我们总是应该保持单向依赖,杜绝双向依赖的产生;

注:在最终代码中,依赖关系体现为类构造方法及类方法的传入参数,箭头的指向为调用关系;依赖关系除了临时知道对方外,还是“使用”对方的方法和属性;

POD(plain old data)介绍

简旧类型(plain old data)

  • 一个标量类型(scalar type)
  • 简旧类型(POD)数组
  • 一个符合以下要求的class类型(class or struct or union)
    • C++11以前:
      • 是一个聚合类型(aggregate type)
      • 所有非静态成员都是简旧类型(POD)
      • 没有成员是引用类型
      • 没有用户定义的拷贝构造函数
      • 没有用户定义的析构函数
    • C++11以后
      • 是一个平凡类型(trivial type)
      • 是一个标准布局类型
      • 所有非静态成员是简旧类型(POD)

POD类型特别在哪里?

What are Aggregates and PODs and how/why are they special?

POD-classesPD-unions, scalar type数组这样的类型被统一的叫做POD-typesPODs在很多地方都非常特别。下面一些例子。

  • POD-classes最接近C语言形式的结构体。不同的是,PODs可以有成员函数和任意静态成员,但他们两者都不能改变对象的内存排布。所以假如你想要写一个或多或少可移植型的可以被C语言甚至.NET使用的动态库,你应该尝试你所有导出的函数和返回值都是POD-types.
  • 一个non-POD类类型对象的生存周期开始于当构造函数结束,结束于当析构函数结束。对于POD类型类,生命周期开始于内存空间被对象占用,结束于内存空间被释放或者被重用后。
  • 对于POD类型的对象, 标准保证它当你使用memcpy对你对象中内容转化为charunsigned数组时,然后memcpy这个内容回到你的对象内,这个对象将持有原始的值。请注意:对于non-POD类型对象没有这样的保证。下面的例子假设类型TPOD类型。
1
2
3
4
5
6
#define N sizeof(T)
char buf[N];
T obj; ///< obj initialized to its original value
memcpy(buf, &obj, N);
memcpy(&obj, buf, N);
/// 保持它的原始值
  • goto语句. 你可能知道,通过goto从一个一些变量还没有在这个作用域中定义的点跳转到一个已经定义的点是非法的(编译器会报错)。这个限制应用在只有当这个变量是一个non-POD类型。看下面例子中f()是语义错误, g()则符合语义。注意,微软编译器在这条规则上特别松散,它在这两个情况下只是抛出一个警告。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int f()
{
struct NonPOD {NonPOD(){}};
goto label;
NonPOD x;
label:
return 0;
}

int g()
{
struct POD{int i; char c;};
goto label;
POD x;
label:
return;
}
  • 它保证了在POD对象的开始处没有内存填充位。其他情况下,假如一个POD-class: A使一个类型T的第一个成员,你可以安全的使用reinterpret_castA*T*然后获取指向第一个成员的指针,反之亦然。

补充定义

标量类型(scalar type)

scalar type是一个不是数组类型或class类型的(可能constvolatile限定的[^2])object类型.
英文原文[^1]

scalar types are (possibly cv-qualified) object types that are not array types or class types

聚合类型(aggregate type)

首先介绍一下聚合类型:
聚合类型是以下类型的其中一种[^3]:

  • 数组类型
  • class类型(典型的例子, struct, union):
    • 没有privateprotected非静态数据成员(到C++11)
    • 没有用户定义的构造函数(显式的默认或删除的构造函数) (C++11起, 到C++17)
    • 没有用户提供的继承的或显式的构造函数(显式的默认或删除的构造函数)(C++17起,到C++20)
    • 没有用户定义的或继承的构造函数(C++20起)
    • 没有基类(C++17之前), 没有virtual,private,protected基类(C++17起)
    • 没有虚成员函数
    • 没有默认成员的初始化器(从C++11到C++14)

平凡类型 (TrivialType)

要求[^4]:

  • 可平凡复制(TrivialCopyable)
  • 若该类型是类类型或其数组,则该类拥有一个或多个合格的默认构造函数,均为平凡的

可平凡可复制(Trivially Copyable)

下面列举的类型称作平凡可复制类型[^5]:

  • 标量类型
  • 平凡可复制的类
    • 至少有一个拷贝构造函数,移动构造函数,拷贝赋值符是符合要求的
    • 每个合格的拷贝构造函数(假如有的话)是平凡的
    • 每个合格的移动构造函数(假如有的话)是平凡的
    • 每个合格的拷贝赋值符(假如有的话)是平凡的
    • 每个合格的移动赋值符(假如有的话)是平凡的
    • 有一个平凡的没有被删除的析构函数
  • 可平凡复制的数组类型
    这意味着一个平凡可拷贝的class没有虚函数和虚基类函数。

参考文献和扩展阅读

[^1]:What is a scalar Object in C++?

[^2]:What does “cv-unqualified” mean in C++?, cv (const and volatile) type qualifiers

[^3]:C++ standard: aggregate type

[^4]:C++ standard: C++ named requirements: TrivialType

[^5]: C++ standard: C++ named requirements: TriviallyCopyable