C++ Draft

1. C++ 基础语法及原理查漏补缺

命名规范

类别
命名方式

目录与文件名

全小写加下划线

namespace

全小写加下划线

类/结构体/typedef/模板参数/枚举类型名

大写字母开头的驼峰式命名,例如:MyString

函数名

小写字母开头的驼峰式命名,例如:printList

变量名

小写字母开头的驼峰式命名,例如:myString

枚举类型内部元素/宏定义

全大写加下划线

文件名后缀约定

  • .c: C 语言的源码文件

  • .h: C 语言的头文件

  • .cpp: C++ 语言的源码文件

  • .hpp: C++ 语言的源码文件

  • .cu: CUDA C/C++ 的源码文件

  • .cuh: CUDA C/C++ 的头文件

编译命令

通常来说, 应该先分别编译, 然后再进行链接为可执行文件/动态链接库:

分别编译然后链接为一个可执行文件的步骤通常如下:

分别编译然后链接为一个动态链接库, 以及如何使用动态链接库的做法:

TODO: libmylib.so 文件可以随意放置, 然后只要编译 main.cpputils.cpp 文件代码中包含 #include 相关的头文件, 以及链接时加上 -L-l 参数即可吗

(TODO 修改标题)编译命令 (编译单元与 include 规范)

先看一个基础版本

目录结构

代码内容

编译指令

在 C++ 中, 通常把一个 .cpp 文件和其依赖的头文件看作是一个编译单元, 先分别编译每一个编译单元为 .o 文件, 然后再将 .o 文件链接在一起.

首先, 编译过程主要处理的是定义(函数定义, 类定义), 一上面的例子为例, 我们最终都是需要把这些源文件转化为机器码:

  • a.cpp: foobar 函数的定义

  • b.cpp: fn 函数的定义

  • main.cpp: main 函数的定义

一步编译是指 g++ main.cpp a.cpp b.cpp -o app, 而分开编译使得并行编译成为了可能(例如每个单元使用一个独立的CPU核来编译), 并且在大项目中, 编译一次之后, 做修改后再编译则只需要编译修改了的部分即可. 注意: 这种一步编译的写法实际上也是将后面跟的输入文件的每一个都当作是一个编译单元, 然后链接在一起, 只是没有在磁盘上保留 .o 文件.

在上面的例子中我们更细致地分析一下 #include 编译预处理指令的用法应该是符合最佳实践 (TODO)

  • a.cpp: 无

  • a.h: 无

  • b.cpp: 无

  • b.h: #include "a.h"

  • main.cpp: #include "b.h", include <iostream>

模板一般只写在头文件中

编译命令为(只有一个编译单元因此无需分开编译): g++ m.cpp -o m, 在这个例子中, 如果将 t.h 中的 #include "t.tpp" 删除

  • 尝试 g++ -c t.tpp -o t.o 进行编译(实质上等同于模板的声明与定义全部写在了 t.tpp 文件内), 会发现得不到 t.o 文件, 这是因为模板只有在实例化时才会生成代码, 否则就不会生成代码 (TODO: 感觉解释地有些牵强)

  • 尝试一步编译 g++ m.cpp t.tpp -o m, 也会出现报错, 这是因为一步编译也会分别把 t.tpp 当作编译单元进行单独编译, 而它并不能生成 Foo<int> 的定义, 然后也将 main.cpp 作为单独的编译单元进行编译, 这个时候由于 #include "t.h" 的原因会生成 Foo<int> 的声明, 因此可以正常得到 m.o, 但在链接时会发现找不到 Foo<int> 的定义

在明白上面的原因后, 其实我们可以做一下修改: 首先删去 t.h 中的 #include "t.tpp", 而将 t.hpp 修改为 t.cpp, 且内容改为:

这样一来, 便可以使用分开编译或一次编译了:

一篇关于分开编译的博客: https://medium.com/@kunal-mod/c-best-practices-understanding-the-need-for-splitting-class-declaration-and-definitions-into-389d523695b9

template 怎么处理: https://stackoverflow.com/questions/495021/why-can-templates-only-be-implemented-in-the-header-file

lambda

上述 lambda 函数基本等价于

备注:

  • 如下两种写法均会报错

通过指针修改某块内存的值

下面的程序段演示了数组的诸多问题,这些都没有很好的解决方案,用C++有时就是这么麻烦,可能还需要查看以下别的包例如opencv,gmp加深体会

关于重载

注意: 似乎自动转为选择容量小的重载形式进行执行, 与定义或声明的顺序无关.

关于枚举类型与宏定义

指针与数组

两个主要的区别:

  • 数组名可以近似看作是常量指针

  • sizeof的运算结果不同

数组名只能作为右值而不能作为左值。

函数声明时展示抛出的异常类型

运算符优先级

C++运算优先级(cpp referrence)

简单记忆(按优先级从高到低, 大多数结合性都是从左到右)

其他>算术>移位>比较>按位逻辑>逻辑>三目, 赋值>逗号

其他

  • ::

  • a++ a-- type() a() a[] . ->

  • (从右到左)++a --a +a -a ! ~ (type) * & sizeof new delete

  • . ->

  • * / %

  • >> <<

  • <= < >= >

  • == !=

  • &

  • ^

  • |

  • &&

  • ||

  • (从右到左) a?b:c {运算符}=

  • ,

range based for

例子1:基础用法

备注: 到目前为止, for (auto i: collection)的写法不支持从容器的第2个元素开始遍历的写法

最佳实践:

  • 在可能的情况下, 使用auto+range based for loop, 省事并且显得专业, 但前提是知道它实际上是什么

  • 如果不能用上述方式, 优先使用迭代器

  • 最次, 对于可以使用下标进行索引的容器, 可以考虑下标遍历

详细介绍:

例子2:统计字符串中各个字符出现的次数, 重复两次

多级指针

类(模板)成员函数带有其他模板参数(member template)

模板特化

函数模板不能特化?https://www.fluentcpp.com/2017/08/15/function-templates-partial-specialization-cpp/

运算类型隐式转换

问题来源于在leetcode上刷题时, 如下一行代码报错:

原因在于右侧表达式参与运算的数都是int类型, 所以在计算1 << 32时会报错, 改正方法是

静态成员变量

typedef 与 using

两者都用于为类取别名,C++ 11 推荐使用 using

using 作用于命名空间,相当于对命名空间里的所有标识符取别名(待确认)

uniform initialization(待补充)

这是 C++ 11 新特性,之前的初始化的写法有:

C++ 11 引入了一种一致性的初始化写法:

其背后原理如下,编译器在编译阶段,将 {1, 2, 3} 自动转换为:

explicit

C++ 11 之前,只适用于单参数构造函数(或者是只有一个参数没有默认值的构造函数),用于防止编译器做隐式的类型转换:

C++ 11 后,此关键字也可使用于多个参数的构造函数。

const 与 extern

在多文件链接时,变量具有 external linkage,函数具有 external linkage,而常量具有 internal linkage 的特性。external linkage(外部链接)指的是在链接时,同一个标识符在多个文件中只能被定义一次,而 internal linkage (内部链接)指的是在链接时,该标识符在只存在于该编译单元中。

对于变量而言,声明/定义有如下形式

其中 (1) 与 (2) 是完全等价的,表示定义变量并赋初值。(3) 表示只定义变量。(4) 表示变量声明。

对于常量而言,声明/定义有如下形式

其中 (1) 表示定义常量,并且该常量的作用范围为全局。(2) 表示定义常量,该常量只在本编译单元中有效,多文件编译时其他编译单元也可以定义该标识符。(3) 表示声明该常量已在别处定义。

因此,为了使得编译通过,对于变量而言,只能有一个文件进行变量定义,其他文件不能重复使用这个变量名。即

而对于常量而言

  • 定义多个“局部”常量

  • 使用“共同”的常量

  • 特殊情况:覆盖

对于函数而言,定义与声明的方式分别为

因此,函数定义只能在多个文件出现一次,函数声明可以出现若干次。

例子:

同时定义多个“相似”类型变量

结论: 在同一条语句里, 非const与const不能混用, 唯一的例外是常量指针

最佳实践:

  • 避免const变量与非const用一条语句定义

  • 避免用一条语句中前面的变量来计算/初始化后面的变量

关于使用一条语句定义变量的问题, 目前为止, 这种语法是可以的(不确定用a和b对p与r初始化是否是C++标准)

这种定义方式后三个不行

整数与字符串的转换

C语言中提供函数atoiitoa分别进行字符串到整数以及整数到字符串的转换, C++中则使用std::stoistd::to_string, 这两个函数在string头文件中定义.

一些常用函数

__builtin_popcount(n): GCC内建函数, 统计n的二进制表示中有多少个1

C/C++整数存储/自动转换/溢出

计算机存储(整数)

一个数在机器中的表示形式称为"机器数", 其代表的值称为"真值". 所谓表示, 实际上就是制定一套真值与机器数的对应规则, 同时希望真值的运算跟机器数的某种计算方法对应上.

为方便讨论, 假设一个无符号int型的字节数为1(注意: 采用补码形式的时候, 此时它能表示的数据范围为: [-128, 127)共256个数字).首先讨论带符号数的存储:

十进制
原码表示
反码表示
补码表示

10

00001010

00001010

00001010

-10

10001010

11110101

11110110

00000000 10000000

00000000 11111111

00000000 1|00000000

原码表示易于理解: 即最高位用于存储符号(0表示整数, 1表示), 其余位置为二进制表示. 注意0有两种表示.

反码表示: 正数与原码相同, 负数在原码的基础上, 保留最高位不变, 其余位按位取反.

补码表示(计算机的实际存储方式): 整数与原码相同, 负数在补码的基础上加1.

为何要采取补码表示? 首先要从计算机的运算说起, 任何运算都必然是取模运算(例如上述的两个整型数相加实际上是模256=2^8进行的). 结果我们发现补码表示的10与-10相加刚好是256(也就是0). 所以20-10=00010100+11110110=1|00001010. (可以进行数学上的证明, 此处不提, 但注意如果是123+23会超过127得到的会是一个负数, 原因是我们实际上不能表示146这个数).

接下来, 无符号型整数实际上就是没有符号位的整数, 于是表示范围变成了[0, 255].

注: 实际上, 可以将负数赋值给无符号整型, 实际上机器数是不变的. 即unsigned a = -10, 即a = 11110110, 但输出a会变成一个正数. 另外注意输出函数实际上是将机器数翻译为10进制数, 其翻译准则由函数所定义.

特别说明: |,&,^,<<,>>都是直接对补码所有位(包括最高位)进行的. 特别说明右移的时候, 左边补的数字由原来数字的最高位决定.

隐式转换

C语言溢出判断

乘法溢出最简单的办法是

C语言有符号整数溢出判断

引用自stackoverflow

according to the C standard, signed integer overflow is undefined behavior. So you can't cause undefined behavior, and then try to detect the overflow after the fact.

C语言无符号整数溢出判断

一些疑惑:

自加运算符:++(待补充)

i++称为后置自加, ++i称为前置加加.

  • 前置加加效率更高

  • 后置加加优先级(由左到右)>前置加加优先级(由右到左)=解引用优先级(由右到左)

重载++

当操作数 i 为重载时

在运算符重载时, 以STL中list<T>::iterator为例:

运算符重载(待补充)

特殊情况:->的重载

https://blog.csdn.net/friendbkf/article/details/45949661

特殊情况:*的重载

虚函数与纯虚函数: hidden, override, overload

只带有非纯虚函数函数的类可以实例化, 并且非纯虚函数可以有实现, 子类继承时可以不覆盖 (override) 这个父类的实现. 而带有纯虚函数的类无法实例化, 且子类必须覆盖这个纯虚函数.

虚函数:

纯虚函数

隐藏 (hidden): 父类在不声明为虚函数时, 子类继承时实现了一个一模一样的函数时, 发生的是隐藏

强制类型转换

参考: https://stackoverflow.com/questions/332030/when-should-static-cast-dynamic-cast-const-cast-and-reinterpret-cast-be-used

C++ 有四种强制类型转换运算符

  • static_cast: 用于基本数据类型之间的转换, 例如将 float 转换为 int, 也用于类层次结构中将基类指针或引用转换为派生类指针或引用, 前提是它们的类型之间存在明确的转换关系。是最安全的一种转换方式, 而且性能也最好 (编译期就确定, 运行时无性能损耗)

  • dynamic_cast: 主要用于处理多态, 它在类层次结构中仅用于将基类指针或引用安全地转换为派生类指针或引用. 它在转换时会检查类型的安全性, 如果转换失败, 则会返回 nullptr (针对指针) 或抛出 std::bad_cast 异常 (针对引用). dynamic_cast运行期进行类型检查与转换, 所以它比 static_cast 慢.

  • const_cast 用来移除或添加 const 属性. 例如将 const 类型的引用或指针转换为非 const 类型的引用或指针. 类型转换发生在编译期

  • reinterpret_cast 是用于进行各种不同类型的指针之间的转换, 或者是指针与足够大的整数类型之间的转换, 它可以将任何指针转换为任何其他类型的指针. 这种转换是不安全的, 因为它不进行类型检查和格式转换,所以可能会导致程序错误, 因此应当慎用. 类型转换发生在编译期

最佳实践是尽可能使用 static_cast, 在无法使用 static_cast 时可以考虑使用 dynamic_cast 做涉及到多态时的安全类型转换. const_cast 一般是为了兼容已有的 API, 例如已有的某个函数的定义中接受的是普通指针, 但作为使用方, 手头上只有一个常指针时, 可以使用 const_cast 来做转换. reinterpret_cast 应当尽可能避免使用.

一些基本的例子:

以下是一个只能用 dynamic_cast 而不能用 static_cast 的例子:

在 C 语言中, 显式的类型转换是使用括号运算符来强制类型转换,例如 (int) myFloat. 此外, C 标准库提供了一系列的函数来转换字符串到基本数据类型, 例如 atoi, atof, atol

智能指针 (Unfinished)

裸指针:

unique_ptr 智能指针: 当 unique_ptr 被销毁时, 它指向的资源也会被销毁. 并且不能有多个 unique_ptr 指向同一份资源.

shared_ptr 智能指针: 启用引用计数功能, 指向同一个资源的 shared_ptr 全部被销毁时, 那么这个资源也会被销毁

移动赋值在 unique_ptr 下的作用:

TODO:

  • weak_ptr

  • 一些更实际的例子

std::is_same

一个简化的实现如下:

其中第一个是主模板, 第二个是模板偏特化, 会优先匹配模板偏特化, 而 value 是结构体 is_same 的静态成员变量, 能用 类名::静态变量名 或者 实例名.静态变量名 来访问, 但后者不推荐

std::enable_if

如果 enable_if<false, T>::type 的第一个参数是 false, 那么它将不包含 type 成员, 触发编译错误. 而 std::enable_if<true, int>::type 会在编译器最终确定为 int.

跳过模板特化

源码

作用是只有当 T 为 float, double 或者 __half 时, 才能用此模板实例化, 否则去寻找其他重载形式.

TODO: 还是不能理解 typename enable_if<true, bool> = true 为什么能通过编译并运行

模板 (Alpha)

总结

  • 模板参数: 可以是 typename T, int x, int

  • 函数模板:

    • 偏特化: 不允许

    • 全特化: 允许, 函数模板, 全特化, 重载的书写顺序是无所谓的, 但一般会按从一般到特殊的顺序写: 函数模板, 重载/全特化

    • 重载: 允许

  • 类模板

    • 对类模板进行偏特化: 允许, 偏特化与全特化的书写顺序无要求, 但一般会按从一般到特殊的顺序写: 类模板, 类模板偏特化, 类模板全特化, 对单个成员函数全特化

    • 对类模板进行全特化: 允许

    • 对类模板的单个成员函数进行偏特化: 不允许

    • 对类模板的单个成员函数进行全特化: 允许, 书写顺序上必须出现在对类模板的偏/全特化之后

函数模板(基础认知)

类模板(基础认知)

2. C++标准库

https://blog.csdn.net/lyh03601/column/info/geek-stl

vector,string,algorithm,iterator

迭代器是一种检查容器内元素并遍历元素的数据类型。C++更趋向于使用迭代器而不是下标操作,因为标准库为每一种标准容器(如vector)定义了一种迭代器类型,而只用少数容器(如vector)支持下标操作访问容器元素。(链接:C++迭代器

示例代码

以下是对链接中的部分注解:

size_t类型

size_t类型:建议下标类型为size_t。即:

那么实际上,size_t类型占多少内存呢?似乎与编译器生成的程序的位数有关,32或者64位,分别对应4字节与8字节。更多可参考:https://blog.csdn.net/Richard__Ting/article/details/79433814

2.1 容器与迭代器

vector,stack,queue

string

关于string使用方法的野博客

提示:string,cstring,string.h

可以简单认为cstring是C++里对string.h的一层包装

std::string的常见操作

哈希表,集合,字典

2.2 算法、仿函数、lambda

实现 numpyargsort 函数

2.3 常用函数

make_heap, priority_queue

make_heap vs priority_queue: stackoverflow

相关应用: leetcode 2558

来自 cppreference 的例子改编

输出

Last updated

Was this helpful?