目录

「C&C++」从入门到放弃

目录

记录并整理C、C++理论及实践知识。

语法特性

C89(C90)

在标准化之前,即 1972~1989 年间,“C语言”最初叫 K&R C
1989 年,美国国家标准协会(American National Standards Institute, ANSI)发布了最初的C语言版本,即 C89ANSI-C
1990 年,国际标准化组织(International Organization for Standardization, ISO)采纳了 C89,并命名为 C90。从技术上讲,C89C90 是同一个标准。
1989 年以来,ANSI 除了作为 ISO 标准下的一个工作实例外,与 C 语言没有任何关系。如今美国处理 C 语言相关的事务由国际信息技术标准委员会(InterNational Committee for Information Technology Standards, INCITS)完成,它将该版本 C 语言正式称为 INCITS/ISO/IEC 9899。因此仍在使用 ANSI-C 称呼的程序员通常不明白它的含义,实际上 ISO 通过 ISO 9899 拥有 C 语言1

C95

ISO/IEC 9899:1990/Amd.1:1995
这个版本不是重大修订,主要做了些技术修订,包括引入了广泛的字符支持等。

C99

ISO 9899:1999

C11

ISO 9899:2011

C17(C18)

ISO 9899:2018
该版本在 2017 年修订完成,但在 2018 年才被 ISO 释放,因此 C17 和 C18 间有些容易混淆。相对上一版,它没有增加新特性,仅做了些调整。

C++ <11

概念

表达式分类
  • lvalue
  • rvalue
aggregate type / 聚合类型

简单但不准确的判断规则为:没有自定义构造、所有成员为 public 且没有虚函数,准确的定义可以参考

语义

位域 bit-field

位域 表示类成员仅使用结构体中若干个比特位存储,并由编译器打包。

struct Flags {
  unsigned int a : 1;  // 语法:1-bit 位域
  unsigned int b : 3;  // 语义:存储 3-bit 整数
};

注意:

A non-const reference shall not be bound to a bit-field.

C++ 11 (Section 9.6/3) 标准明确禁止对位域取 non-const reference。这是因为位域不一定从字节边界开始,它们可能只占一个字节的一部分,甚至横跨多个字节的中间几位。由于编译器无法获得明确的独立地址,因而不可能获得一个指向位域的指针或引用。但编译器允许从位域初始化一个 const reference,这是因为编译器构建了一个同类型的临时变量,并使用位域的值拷贝初始化了该临时变量,而常量引用绑定到了该临时变量上。

操作符

取地址 &
引用 &
  • 函数传参的三种方式:按值传递、按指针传递、按引用传递
  • 引用并非对象,没有单独的地址,这与指针 * 有所区别。
    • 对于某个对象A可以创建一个指针使其指向该对象,此时内存中会创建一个指针对象B,其值为对象A的地址。通过修改指针对象B的存储内容(即对象A的地址)可以使得其指向其它任意对象(当然得合法)。当函数传参时(即形参为对象A的指针),在对象A看来,传递的是其地址,是按指针传递,但在指针对象B看来,是按值传递,传递的是指针对象B的值,可以认为是复制了一份指针对象B指针对象BB传递进去。因此如果函数意图是通过指针修改对象A则无问题,但若是函数意图是修改指针对象B指向的内容(如与对象A同类型的对象AA)则存在问题。因为函数内部仅仅修改了指针对象BB的指向,它本身是从指针对象B复制出来的,函数退出将被销毁。针对这种本质是修改指针对象B本身的意图,可以通过另一个指针变量来指向它(一般体现为二级指针)实现,也可以通过引用实现。引用不是一个独立的变量,
    • 对某个对象A可以创建一个引用B,是指对该变量起了别名,底层实现依赖于对象A的地址,但是并未像指针一样,另外创建一个变量(对象)来存储它。引用B对象A具有相同的物理地址。
    • 因为引用并非对象,因此没有引用的数组指向引用的指针引用的引用等等
    • 关于引用和指针的区别,可参考What are the differences between a pointer variable and a reference variable?
    • 更详细的介绍,可参考Pointers, References and Dynamic Memory Allocation
  • 指针的引用 *&,如其字面含义,即创建指针对象的引用或者说是指针对象的别名,可参考What is a reference-to-pointer?。形参为 *& 要求传递的是具有实际地址的(具名的)某类型指针变量,不能是动态地(匿名地)创建的指针(典型如函数调用处使用取地址操作符&取某类型对象的地址),参见Passing references to pointers in C++。个人分析是取地址操作符&构造了一个临时的(匿名的)指针变量存储对象的地址,函数内修改其指向是无意义的,背离了*&的设计意义。
域解析 ::

关键字

access specifiers
public
protected
private
declaration specifiers
alignment specifiers
pure-specifier
= 0
function specifiers
explicit
storage class specifiers
static
extern
mutable

允许在被声明为 const 的类成员方法中修改被声明为 mutable 的变量。

内存申请与释放

new/delete VS malloc/free

C 的 malloc/free 在堆区申请和释放内存。

C++ 的 new/delete 在自由存储区(Free Store)申请和释放内存。其中自由存储区(取决于具体的实现)可能基于堆区(Heap)全局/静态区等分配。

Const Data
The const data area stores string literals and other data whose values are known at compile time. No objects of class type can exist in this area. All data in this area is available during the entire lifetime of the program.
Further, all of this data is read-only, and the results of trying to modify it are undefined. This is in part because even the underlying storage format is subject to arbitrary optimization by the implementation. For example, a particular compiler may store string literals in overlapping objects if it wants to.

Stack
The stack stores automatic variables. Typically allocation is much faster than for dynamic storage (heap or free store) because a memory allocation involves only pointer increment rather than more complex management. Objects are constructed immediately after memory is allocated and destroyed immediately before memory is deallocated, so there is no opportunity for programmers to directly manipulate allocated but uninitialized stack space (barring willful tampering using explicit dtors and placement new).

Free Store
The free store is one of the two dynamic memory areas, allocated/freed by new/delete. Object lifetime can be less than the time the storage is allocated; that is, free store objects can have memory allocated without being immediately initialized, and can be destroyed without the memory being immediately deallocated. During the period when the storage is allocated but outside the object’s lifetime, the storage may be accessed and manipulated through a void* but none of the proto-object’s nonstatic members or member functions may be accessed, have their addresses taken, or be otherwise manipulated.

Heap
The heap is the other dynamic memory area, allocated/freed by malloc/free and their variants. Note that while the default global new and delete might be implemented in terms of malloc and free by a particular compiler, the heap is not the same as free store and memory allocated in one area cannot be safely deallocated in the other. Memory allocated from the heap can be used for objects of class type by placement-new construction and explicit destruction. If so used, the notes about free store object lifetime apply similarly here.

Global/Static
Global or static variables and objects have their storage allocated at program startup, but may not be initialized until after the program has begun executing. For instance, a static variable in a function is initialized only the first time program execution passes through its definition. The order of initialization of global variables across translation units is not defined, and special care is needed to manage dependencies between global objects (including class statics). As always, uninitialized proto-objects’ storage may be accessed and manipulated through a void* but no nonstatic members or member functions may be used or referenced outside the object’s actual lifetime.

———— GotW #9

构造与析构

copy constructor
ExampleClass(const ExampleClass &src)
copy assignment operator
ExampleClass& operator=(const ExampleClass &src)

继承

成员变量
  • 子类成员变量会隐藏父类的同名成员变量。
  • 无法(像虚函数那样)通过基类指针访问子类成员变量。
成员函数
override
hide

模板

模板传参
// 模板函数得写在`.h`文件中
template <typename T, size_t len>
void _ParseOnePack(unsigned char *pack, std::array<T, len> &sigs_layout) {
... }
模板继承

C++ 98

ISO/IEC 14882:1998

语义

exception specification / 异常说明

主要用于约定函数“承诺”会抛出哪些类型的异常,属于 API 设计中的语义信息。但因为编译器不会在函数实现、异常说明和客户端代码之间提供一致性保证,因此函数实现改变可能会要求同步修改异常说明及客户端代码,这在多数开发者看来不值得。

因此,自 C++ 11 开始,该语义表达已经被标记为废弃,自 C++ 17 开始,异常说明 被移除。取而代之的是自 C++ 11 开始引入的 异常声明(noexcept)

Two-Phase Name Lookup / 模板两阶段查找规则

模板的编译分两步:

  1. 第一阶段(模板定义时):编译器不关心模板参数具体是什么类型,只分析语法结构(不替换类型参数),能解析就解析
  2. 第二阶段(模板实例化时):模板参数具体化,编译器才会替换类型参数、解析依赖名称等

因为第一阶段不知道类型参数代表什么,所以在一些写法中可能会产生语义模糊,编译器需要你手动告诉它“这是类型”还是“这是模板”。

  • typename 说明依赖类型名称

    template<typename T>
    void func() {
      typename T::value_type x;  // 告诉编译器:T::value_type 是一个类型名
    }

    如果不写 typename,编译器可能会误以为 T::value_type 是个静态成员变量而不是类型(因为第一阶段不知道 T 是什么)。

  • template 说明依赖模板名称

    template<typename T>
    void callNested() {
      T::template rebind<int>::other obj;  // 告诉编译器:rebind 是个模板
    }

    如果不写 template,编译器在第一阶段可能无法识别 rebind<int> 是模板调用,而以为 T::rebind < int 是某个表达式。

SFINAE (Substitution Failure Is Not An Error)

这是一个随着模板一起引入的模板语义,当模板实例化的过程中(在重载解析前),可以根据不同的模板实参进行条件编译,从而为不同的条件提供不同的重载实现。

extern template / 外部模板

外部模板(extern template)是一种用于显式禁止模板实例化的机制,可以减少重复实例化并加快编译、链接速度。

extern template declaration;

标准规定2,上述显式实例声明可以抑制翻译单元中指定类型的模板的隐式实例化。

An explicit instantiation declaration (using the extern keyword) prevents implicit instantiation of the template in that translation unit.

典型用法:

  • foo.h
// foo.h
#pragma once

#include <iostream>

template<typename T>
class Foo {
public:
    void bar();
};

// 声明:在别的地方有 Foo<int> 的显式实例化定义
extern template class Foo<int>;
  • foo.cpp
// foo.cpp
#include "foo.h"

template<typename T>
void Foo<T>::bar() {
    std::cout << "Foo<T>::bar()" << std::endl;
}

// 显式实例化定义:生成 Foo<int> 的代码
template class Foo<int>;
  • main.cpp
// main.cpp
#include "foo.h"

int main() {
    Foo<int> f;  // 不会实例化模板代码
    f.bar();     // 使用的是 foo.cpp 中已实例化好的版本
}

对于单头文件模板的情形,可以采用 分离实例化模块 的方式,即单独准备一对 .h/.cpp 文件,在 .h 文件中进行模板的外部声明,在 .cpp 文件中进行实例化定义。这样对于想要启用外部模板加速的工程可以引入该专用头文件并链接定义文件,而对于不使用的工程不包含不链接即可。可以做到:

  • 无侵入:不污染原始单头文件模板,可安全复用
  • 灵活配置:可以自由选择包含、链接外部模板声明来启用
  • 编译优化:显式实例化模板只会生成一次,避免重复实例化、缩短链接时间
  • 分模板管理:多个外部模板可集中放在一个 extern 模块组,统一维护
POD (Plain Old Data) 类型

POD 类型用于定义能与 C struct 兼容的类型。但语言后续的演进淡化了这个概念,而是细化为两个语义,即 trivial 类型和 standard-layout 类型。其中 trivial 类型指构造、拷贝、析构都可以是默认的、编译器提供的,没有用户定义版本,而 standard-layout 类型指内存布局规则简单、类的成员布局像 C struct,容易预测。

因此,可以说 POD = trivial + standard-layout

C++ 03

ISO/IEC 14882:2003

C++ 11

ISO/IEC 14882:2011

概念

表达式分类
标记名称定义备注
glvalue泛左值The expressions that have identity are called “glvalue expressions” (glvalue stands for “generalized lvalue”)“generalized” value
lvalue左值have identity and cannot be moved from
xvalue将亡值have identity and can be moved from“eXpiring” value
rvalue右值The expressions that can be moved from are called “rvalue expressions”
prvalue纯右值do not have identity and can be moved from“pure”

上述类别之间的关系可以用下图来表示:

https://store.yirami.xyz/review/c_cpp/value_categories.png

语义

exception declaration / 异常声明

异常声明(noexcept) 用于修饰函数是否可以抛出异常。

正如 Effective Modern C++ 建议的:如果函数不抛出异常,请尽量使用 noexcept 标记,可以提升性能。

list-initialization / 列表初始化

一种以花括号封闭的初始化列表,用法参见

T object { arg1, arg2, ... };
aggregate initialization / 聚合初始化

属于列表初始化的特殊情形,即仅作用于聚合类型的一种初始化方式。其与普通的列表初始化的主要区别在于作用对象、以及是否会调用构造函数。

对于聚合类型的判定,随着 C++ 标准的更新,有一定的演变,可以参考 cppreference 中的定义。

另有一个与聚合类型容易混淆的概念,即 POD (Plain Old Data) 类型。它们的语义关注点有所不同,聚合类型聚焦于初始化机制,而 POD 类型聚焦于类型布局与 C struct 的内存兼容性。

std::enable_if

是一种依赖于 SFINAE 机制的语法糖,其根据模板参数是否满足某个“类型约束条件”来决定这个模板是否参与重载或实例化。其简化了使用 SFINAE + decltype 的写法。

如,对于限制模板函数 foo 仅对整型进行实例化,有如下的写法:

// SFINAE + decltype
template <typename T>
auto foo(T t) -> decltype(std::enable_if_t<std::is_integral_v<T>>(), void()) {
  std::cout << "integral" << std::endl;
}

// std::enable_if
template <typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
foo(T t) {
  std::cout << "integral" << std::endl;
}

关键字

inline

inline 需放在函数定义处,对于函数声明使用毫无意义。它会建议编译器对该函数使用内联,减少压栈、出栈时间,从而提升效率。

inline 是以代码膨胀为代价,从可执行程序大小、缓存命中等角度看,不当使用可能会造成性能下降。

reference from: http://www.sunistudio.com/cppfaq/inline-functions.html

  • 可能会使代码速度更快:顺序集成可能会移除很多不必要的指令,这可能会加快速度
  • 可能会使代码速度更慢:过多的内联可能会使代码膨胀,在使用分页虚拟内存的系统上,这可能会导致性能下降。换句话说,如果可执行文件过大,系统可能会花费很多时间到磁盘上获取下一块代码
  • 可能会增加可执行文件尺寸:代码膨胀
  • 可能会减少可执行文件尺寸:如果不内联展开函数体,编译器可能会要产生更多代码来压入/弹出寄存器内容和参数。对于很小的函数来说会是这样。如果优化器能够通过顺序集成消除雕大量冗余代码的话,那么对大函数也会起作用(也就是说,优化器能够使大函数变小)
  • 可能会导致系统性能下降:内联可能会导致二进制可执行文件尺寸变大,由此导致系统性能下降
  • 可能会避免系统性能下降:即使可执行文件尺寸变大,当前正在使用的物理内存数量(即需要同时留在内存中的页面数量)却仍然可能降低。当f()调用g()时,代码经常分散在2个不同的页面上。当编译器将g()的代码顺序集成到f()后,代码通常会放在一个页面上
  • 可能会降低缓存的命中率:内联可能会导致内层循环跨越多行的内存缓存,这可能会导致内存和缓存频繁交换,从而性能下降
  • 可能会提高缓存的命中率:内联通常能够在二进制代码中就近安排所用到的内容,这可能会减少用来存放内层循环代码的缓存数量。最终这会使CPU密集型程序跑得更快
  • 可能与速度无关:大多数系统不是CPU密集型的,而使I/O密集型的、数据库密集型的或是网 络密集型的。这表明系统的瓶颈存在于文件系统、数据库或网络。除非你的“CPU速度表”指示是100%,否则内联函数可能不会使你的系统速度更快。(即使 是CPU密集型的系统,也只有在被用到瓶颈之处时,内联才会有帮助。而瓶颈通常只存在于很少一部分代码中。)
using
placeholder type specifiers
auto
decltype specifier

探测实例的声明类型,用于自动类型推断。其使用场景比 auto 更广泛。

std::nullptr_t 与 nullptr

nullptrstd::nullptr_t的字面值

constexpr
cv type qualifiers
const
volatile

指示变量随时可变,避免编译器优化或重排序。任何通过非易失性的泛左值(如指针、引用)访问易失性变量的行为都是未定义的。

const volatile

常见于共享内存案例。程序A中该内存地址不可变,但程序B中可能修改该内存地址内容,因此程序A中应该标记为易失性变量。

noexcept
// 1. 用作修饰符表明函数不会抛出异常
//   如果抛出,编译器可选择调用`std::terminate()`终结程序
//   功能相当于`C++11`之前的`throw()`,但更高效
ExampleClass(ExampleClass &&src) noexcept;
// 2. 用作操作符,常与模板结合以更好的支持泛型编程
//   `noexcept(T())`会判断`T()`是否会抛出异常
//   如果不会则返回`true`,则`ExampleFunc()`将被声明为`noexcept`
template <typename T> void ExampleFunc() noexcept(noexcept(T())) {}
virt-specifier
final

终结类

override

编译器会辅助检查是否真正被重写,而不是其它(如被隐藏)。

sizeof
default
delete
static_assert

静态断言,与运行时断言assert对应。

alignas 与 alignof

内存对齐状态:alignas指定类型的对齐字节数;alignof获取类型的对齐字节数;

storage class specifiers
thread_local

将全局或static变量声明为线程局部存储(TLS, thread local storage),即拥有线程生命周期及线程可见性的变量。

一旦申明一个变量为thread_local,其值的改变只对所在线程有效,其它线程不可见。

类型

String literal

参见cppreference

  • "" | Ordinary string literal
  • L"" | Wide string literal
  • u8"" | UTF-8 string literal
  • u"" | UTF-16 string literal
  • U"" | UTF-32 string literal
  • R"" | raw string literals
std::array
#include <array>
std::array<int, 3> a1{{1, 2, 3}};	// CWG 1270 重申前的 C++11 中要求双花括号,C++14 起不要求
std::array<int, 3> a2 = {1, 2, 3};	// = 后决不要求双花括号
enum class
  1. 底层默认int型,可指定底层类型

    enum class Color:unsigned long{...}; 
    
  2. 获取底层类型

    std::underlying_type<Color>::type
  3. 输出枚举类字面值

    Color a = Color::Yellow;
    std::cout<<static_cast<std::underlying_type<Color>::type>(a)<<std::endl;
  4. 待补充

rvalue reference

用于实现移动语义,只能作用于右值。

移动是用于避免多次拷贝浪费资源,主要用于转移临时变量的资源,满足一定条件时编译器会隐式生成。

std::move

可将左值转为右值,常将左值转为右值然后进行移动。

在函数返时使用 std::move 是没有必要的,甚至会影响到编译器的返回值优化((Named) Return Value Optimization, (N)RVO)降低性能。如return std::move(local_var) 是否有必要? 中提到在满足允许消除复制操作的条件时,无论原对象是左值还是右值,都会优先匹配移动构造函数。

PS:此处返回值优化包括 RVONRVO,期初编译器只对匿名返回值进行优化(RVO),后期发展到对具名返回值也进行优化(NRVO)。匿名指在返回处直接构造的匿名变量,如 return any_type(balabala);,具名则如 any_type balabala; return balabala;

move constructor
ExampleClass(ExampleClass &&src) noexcept;  // 移动构造通常不涉及内存分配,一般不会有异常,所以加`noexcept`修饰
move assignment operator
ExampleClass& operator=(ExampleClass &&src) {  // 不是构造函数,所以我没加`noexcept`
if (this == &src) return *this;  // 一定要检查是否自我移动赋值
  // move
  // release
  // return
  return *this;
}
smart pointers
std::unique_ptr

独有对象所有权。指针失效时,自动析构其管理对象

std::shared_ptr

共享对象所有权。最后一个拥有对象的指针失效时,自动析构其管理对象

std::weak_ptr

std::shared_ptr管理对象的弱引用

std::auto_ptr

c++ 17废弃

std::thread
  1. invoke
// simple case
std::thread thread_obj(func_ptr);
// simple case with parameters passing
std::thread thread_obj(func_ptr, para1, &para2);
// invoke static member func
std::thread thread_obj(&ClassName::static_mem_func, &para1);
// [invoke overloaded static member func](https://stackoverflow.com/questions/14276425/calling-overloaded-member-functions-using-stdthread)
  // 1. < C++ 11
void (ClassName::*static_mem_func)(para1_ptr_type) = &ClassName::static_mem_func;
std::thread thread_obj(static_mem_func, &para1);
  // 2. >= C++ 11
using static_mem_func_type = void (ClassName::*)(para1_ptr_type);
static_mem_func_type static_mem_func = &ClassName::static_mem_func;
std::thread thread_obj(static_mem_func, &para1);
  // 3. lambda
std::thread thread_obj([=]{instance.static_mem_func(&para1);});
// [invoke non-static member func](https://stackoverflow.com/questions/41476077/thread-error-invalid-use-of-non-static-member-function)
std::thread thread_obj(&ClassName::non_static_mem_func, &instance, &para1);
// invoke overloaded static member func

// invoke overloaded non-static member func
std::memory_order

内存顺序。参见cppreference

std::pair

容器模板

std::tuple

容器模板,是 std::pair 的推广。

函数

Lambda expressions

Lambda 表达式(通常称为 Lambda)是一种在被调用的位置或作为参数传递给函数的位置定义匿名函数对象(闭包)的简便方法。

对于Lambda,实际调用的是由编译器创建的匿名类的成员运算符 operator(),且被隐式定义为 inline。参见Are lambdas inlined like functions in C++?

相比普通函数,使用 lambda 可以更好的被编译器优化,但性能提升这点与 inline 类似,需要具体分析。参见https://stackoverflow.com/questions/13722426/why-can-lambdas-be-better-optimized-by-the-compiler-than-plain-functions?

std::decay

一个用于类型退化(去除对象的某些限定符)的模板函数

std::is_same

判断类型是否相同(考虑constvolatile限定符)

std::fmod

对浮点数取模。

std::iota

生成递增序列。参见cppreference

std::call_once

用于替代用户手写的双重检查锁以解决静态变量懒初始化中可能的线程竞态问题。

C++ 11 起,静态局部变量的初始化能够保证线程安全,因此如果不考虑失败重试及语义更明确的问题,使用静态局部变量更有效率。

模板

parameter pack 模板参数包

支持零个或多个参数,具备至少一个模板参数包的模板称为可变模板。

C++ 14

ISO/IEC 14882:2014

语义

std::enable_if

进一步简化写法。

如,对于限制模板函数 foo 仅对整型进行实例化,有如下的写法:

#include <type_traits>

// std::enable_if (C++ 11)
template <typename T>
typename std::enable_if<std::is_integral<T>::value, void>::type
foo(T t) {
  std::cout << "integral" << std::endl;
}

// std::enable_if (C++ 14)
template <typename T>
std::enable_if_t<std::is_integral_v<T>, void>
foo(T t) {
  std::cout << "integral" << std::endl;
}

C++ 17

ISO/IEC 14882:2017

概念

表达式分类
标记名称定义备注
glvalue泛左值The expressions that have identity are called “glvalue expressions” (glvalue stands for “generalized lvalue”)“generalized” value
lvalue左值have identity and cannot be moved from
xvalue将亡值have identity and can be moved from“eXpiring” value
rvalue右值The expressions that can be moved from are called “rvalue expressions”
prvalue纯右值do not have identity and (in contrast with the C++11 scheme,) no longer moved from“pure”

语义

inline specifier

C++ 17 开始,inline 允许修饰具有静态存储期(static storage duration)的变量,它允许在多个翻译单元中定义同一个变量,且不违反一个定义原则(One Definition Rule, ODR)。编译器和链接器会把这些多处定义视为同一个变量,避免重复定义错误。

因为 inline 的核心意义是“允许多重定义但视为同一实体”,这就要求:

  • 变量的生命周期是全局的
  • 变量必须唯一,而不是每次调用新生成

因此,局部变量、临时变量等不能使用。

此外,对于声明为 constexpr 的静态成员变量,在其第一次声明时具备 inline 属性。

C++ 17 之前,如果头文件中定义 static constexpr 的变量,虽然不会编译出错,但是可能会在多个翻译单元中存在多个副本,浪费内存资源。这是因为如果涉及到观察变量的地址引用身份,会打断编译器的常量折叠(Constant Folding),强制生成存储。

constexpr if

用于指示编译期进行求值判断,类似于宏(#if/#endif),替代宏进行条件编译,同时更好的支持泛型编程。

参考C++17 之 “constexpr if”中的一些用法。

类型

std::variant

变体是类型安全的union,即会做类型检查。

std::monostate

为变体提供一种经过良好定义的空单元类型。对于非默认可构造的变体,若将 std::monostate 作为第一类型,可使得变体默认可构造。

std::filesystem
std::filesystem::path
std::filesystem::remove
std::filesystem::remove_all

函数

std::holds_alternative

检查变体当前存储的数据类型是否为指定类型。

std::visit

用于访问变体的内容。通过重载对变体各种类型的访问操作,可以方便的使用 std::visit 遍历变体。

std::get

从变体中获取指定类型的内容,类型不匹配时会抛出异常。

std::get_if

从变体中获取指定类型的内容,类型不匹配时返回 false

std::clamp

斩波函数

C++ 20

ISO/IEC 14882:2020

语义

aggregate initialization / 聚合初始化

聚合初始化是列表初始化的一种形式,自 C++ 20 开始,引入了一种类似于 C99字段名指定初始化(Designated Initializers)方式,提升了代码的可读性以及初始化的明确性,无需按顺序初始化字段。

T object{.des1 = arg1 , .des2 { arg2 } ... };

注意,普通列表初始化还不支持这种字段名指定初始化的方式。

requires

一种更清晰、现代的 表达约束 的语法工具,是 std::enable_if 的升级版本。

如,对于限制模板函数 foo 仅对整型进行实例化,有如下的写法:

#include <type_traits>

// std::enable_if (C++ 14)
template <typename T>
std::enable_if_t<std::is_integral_v<T>, void>
foo(T t) {
  std::cout << "integral" << std::endl;
}

// requires
template <typename T>
requires std::is_integral_v<T>
void foo(T t) {
  std::cout << "integral" << std::endl;
}
concept

一种 可复用约束定义

如,想要约束类型具备 可加性,有如下的写法:

// requires
template <typename T>
void foo(T t) requires requires(T x) { x + 1; } {
  std::cout << "T can be added with 1\n";
}

// requires + concept
// 1. define concept
template <typename T>
concept Addable = requires(T x) {
  x + 1;
};

// 2.1 use concept with requires
template <typename T>
requires Addable<T>
void foo(T t) {
  std::cout << "T is addable\n";
}

// 2.2 or use like this
template <Addable T>
void foo(T t) {
  std::cout << "T is addable\n";
}

使用 concept 可以方便地定义更复杂、且可复用的约束,如约束类型支持相加,且相加后仍可隐式转换为该类型可以这么写:

template <typename T>
concept Addable = requires(T x, T y) {
  { x + y } -> std::convertible_to<T>;
};

操作符

三路比较 <=>

三路比较运算符,也叫太空船运算符,统一和简化了比较逻辑的编写,旨在解决比较操作运算符实现冗余、易错和维护困难的问题。虽然其只是语法层面的改进,但在自动生成比较、合并操作、泛型优化方面,为编译器和算法提供了更好的优化空间。

关键字

concept
requires

函数

std::same_as

参看参考中的参考实现,std::same_as 保证 std::same_as<T, U>std::same_as<U, T> 具有相同结果。这是其与 std::is_same 的区别,这里可参看Why does same_as concept check type equality twice?,大体上与原子操作有关。

mathematical constants

pi

C++ 23

ISO/IEC 14882:2024

C++ 26

STL

std::format

字符串格式化

内存管理

堆(heap) / 栈(stack)上分配内存

在堆上创建对象,还是在栈上?

new/delete 与 malloc/free

动态链接共享库中的全局/静态变量时的行为

参考回答

这是 WindowsUnix-like 系统之间的一个典型区别。

大的前提:

  • 每个进程都有自己的地址空间,除非使用一些进程间通信库或扩展,否则进程之间永远不会共享任何内存
  • 单一定义规则(ODR)仍然适用,即在链接时(静态或动态链接)只能有一个全局变量的定义可见

所以关键问题是 可见性

在任何时候,静态全局变量(或函数)都不会从模块(dll/soexecutable)外部可见。C++ 标准要求这些对象具有内部链接,这意味着它们在定义它们的翻译单元(将成为对象文件)之外不可见。所以,这解决了 静态变量 这个问题。

然而,当有 extern 的全局变量时,它会变得复杂。在这里,WindowsUnix-like 系统完全不同。

对于 Windows.exe.dll),extern 全局变量默认不会被导出到 符号表。换句话说,不同的模块根本不知道其他模块中定义的全局变量。这意味着,如果尝试创建要使用动态库中定义的全局变量的可执行文件,会出现链接器错误。想要绕过这个链接错误,可以将全局变量放到对象文件(.obj)或静态库(.lib)中,并在 .exe.dll 中都静态链接这个对象文件。不过,这会产生两个不同的全局变量(一个属于 .exe,一个属于 .dll)。

而要真正解决这个问题,需要像导出函数一样导出全局变量:

#ifdef BUILDING_MY_DLL
#define MY_DLL_EXPORT __declspec(dllexport)
#else
#define MY_DLL_EXPORT __declspec(dllimport)
#endif

MY_DLL_EXPORT int my_global;

当然,也可以将变量封装,并对外提供 getter/setter 方法。

而在 Unix-like 系统中,.so 库默认会导出所有 extern 的全局变量和函数符号。因此如果使用 加载时链接load-time linking,即在程序启动时由 dynamic linker 加载 .so),不同模块访问的是同一个全局变量实例。

最后,无论是 Windows 还是 Unix-like 系统,都可以选择 运行时链接runtime linking,即程序运行时手动加载动态库),此时无法通过编译器自动解析符号,而需要手动通过名字查找符号地址。而符号表导出的规则如前所述,对于 Windows 必须显示导出。

内存顺序you

Memory Order

Memory Barrier

C/C++混合编程

C中调用C++

wrapper c++ functions/class
/ 用法 /
  1. cpp文件中调用c++函数,使用兼容C的接口风格
  2. 头文件中使用#ifdef __cplusplus,声明接口为C风格
/ 示例 /
#ifndef FUSION_CALLER_WRAPPER_H_
#define FUSION_CALLER_WRAPPER_H_

#ifdef __cplusplus
#include <array>
#include <string.h>
// include c++ code headers here
#include "track_manage.hpp"
#include "fusion_types.h"

extern "C" {
#endif

// write wrapper functions invoked by c projects here
void CCall_BoschManage( const BoschFrontRadar *detect,
                        TrackRadarTarget *track,
                        float speed,
                        float cost_thr);

#ifdef __cplusplus
}
#endif

#endif

类继承明确使用this

对于模板类继承情形,如果子类想使用父类成员变量,需要加this->限定,或者使用using

重载、重写(覆盖)、隐藏

重载(Overload)

  1. 相同的范围(在同一个类中);
  2. 函数名字相同;
  3. 参数不同;
  4. virtual 关键字可有可无。

重写(Override)

  1. 不同的范围(分别位于派生类与基类);
  2. 函数名字相同;
  3. 参数相同;
  4. 基类函数必须有virtual 关键字。

隐藏(Hide)

  1. 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
  2. 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)

string 标准头文件

  1. string 可以定义string s;可以用到strcpy等函数
  2. string.h 不可以定义string s;可以用到strcpy等函数
  3. cstring 加c前缀,实际同上
  4. CString MFC头文件

建议规范头文件的使用

有时候不包含string,依旧可以使用,可能是间接包含,并不可靠,见案例

std::chrono 计时库

#include <chrono>
auto start = std::chrono::system_clock::now();
auto end = std::chrono::system_clock::now();
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
double time_s = double(duration.count()) * std::chrono::microseconds::period::num / std::chrono::microseconds::period::den;

Awesome C/C++

See also awesome and Awesome C++.

编译

Visual Studio

Error C2664

1. 无法将参数?xxx转为xxx &

传参时采用{}创建临时对象,函数对应参数需匹配为const

CMake+G++/Gcc

CMake

安装及配置

Ubuntu

  1. apt
# Ubuntu 16.04源的cmake版本并非最新,不建议
sudo apt install cmake
  1. source

    1. 源码下载,版本:cmake-xxx.tar.gz
    2. 解压
    cd dir_download
    tar zxvf cmake-xxx.tar.gz
    1. 编译
    cd dir_unzip
    ./bootstrap
    make -j8
    sudo make install
    1. 验证
    cmake --version

Windows,建议采用 TDM-64 编译套件

cmake .. -G "MinGW Makefiles"
mingw32-make.exe
CMakeLists.txt
  1. CMake 入门实战
  2. CMake之CMakeLists.txt编写入门
编译选项
  1. -fPIC / 生成位置无关代码

Linux 下编译生成库建议增加此配置

编译性能优化

参考C++服务编译耗时优化原理及实践

通用方案

并行编译

Linux 平台上一般使用 GNUMake/CMake 工具进行编译,在执行 make 命令时可以加上 -j 参数增加编译并行度,如 make -j4 将开启 $4$ 个任务。在实践中我们并不将该参数写死,而是通过 $(nproc) 方法动态获取编译机的 CPU 核数作为编译并发度,从而最大限度利用多核的性能优势。

分布式编译

使用分布式编译技术,比如利用 DistccDmucs 构建大规模、分布式 C++ 编译环境,Linux 平台利用网络集群进行分布式编译,需要考虑网络时延与网络稳定性。分布式编译适合规模较大的项目,比如单机编译需要数小时甚至数天。

预编译头文件

PCH(Precompiled Header),该方法预先将常用头文件的编译结果保存起来,这样编译器在处理对应的头文件引入时可以直接使用预先编译好的结果,从而加快整个编译流程。PCH是业内十分常用的加速编译的方法,且大家反馈效果非常不错。需要注意的是,Shared Library 相互之间无法共享 PCH。

CCache

‘CCache’ 是一个编译器缓存工具,它的主要作用是加速编译过程,特别是对于大型项目或需要频繁修改和构建的项目,效果非常明显。CCache 基于源码哈希、编译参数、宏等缓存结果,缓存编译结果,避免重复编译相同代码,从而节省编译时间。相比 CMake 的增量编译,其使用全局共享缓存(即,~/.ccache/), 即使多次 clean/build 都能命中缓存。

Module 编译

C++ 20 开始,引入 模块(Modules) 来替代传统的 #include 头文件机制,解决其带来的编译慢、重复解析、宏污染等问题,以提升 编译速度封装性依赖管理。其核心理念是:

  • 不再通过 #include 拷贝文本,而是通过 import 语义式引用
  • 编译器只需编译一次模块接口,生成 .pcm(预编译模块)文件,其它地方引用即可
  • 更强封装性、更快编译速度、无宏污染
自动依赖分析

自动依赖分析旨在分析找到不必要包含的头文件,以减少预处理、以及依赖传播范围。主流的分析工具有 IWYU

代码优化方案

前置类型声明

在编译器无需知道类型的完整定义的场景,使用 前置类型声明,仅告诉编译器有这个类型,不引入类型的完整定义,因此避免了解析整个头文件内容,从而减少了编译时间和依赖。

外部模板

使用外部模板声明,并仅在一处进行实例化,避免重复实例化、缩短编译、链接时间。

解决编译依赖,提高编译并行度

合理设置模块的编译依赖,如在 CMake 构建系统中,尽量使用最小依赖边界,避免不必要的头文件传播,并用 target_* 语义明确控制依赖流向。

常见的错误用法:

  1. 滥用 include_directories() + add_subdirectory()

    1. 使用 target_link_libraries() 精确表达依赖,如:

      target_link_libraries(app PRIVATE foo)

      其中:

      • PRIVATE: 当前目标使用,编译时也需要
      • PUBLIC: 当前和下游都使用
      • INTERFACE: 当前目标不使用,但依赖者需要
    2. 使用 target_include_directories() 精准控制头文件可见性,如:

      target_include_directories(foo
        PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include  # 下游依赖者需要
        PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src     # 仅自身编译使用
      )
    3. 把每个模块变成单独的 target,避免将大量 .cpp 合成一个巨型目标,可以

      • 多目标间改动不会互相影响
      • 并行构建粒度更细,速度更快

程序占用

存储时

  • Text Segment
  • Data Segment: RO_data + RW_data

运行时

  • Block Started by Symbol(BSS) Segment: ZI_data

    BSS段用于存储未经初始化的全局变量和静态变量;该段不会被打包到可执行文件,程序启动时由操作系统进行初始化

  • Text Segment

    代码段一般为只读(某些架构下可写);可能包含一些常量,如字符串常量等

  • Data Segment

    属于静态分配内存

  • Dynamic Memory
    • heap
    • stack

调试

本地调试

工具

Windows 平台
  1. 大杀器 —— Visual Studio
  2. VS Code
Linux 平台
  1. gdb

技巧

  1. 基于VS/VSCode调试时,可在Watch窗口通过var,h or var,b直接查看变量二进制、十六进制等。

测试

测试框架

GoogleTest
install
use in project
# embedding googletest must turn off install,
# ... see also: https://github.com/google/googletest/blob/main/CMakeLists.txt#L32
## override default option from parent cmake: 
## ... see: https://stackoverflow.com/a/3769269/19527989
SET(BUILD_GMOCK OFF CACHE BOOL "Do not use GMOCK." FORCE)
SET(INSTALL_GTEST OFF CACHE BOOL "Do not install GTEST." FORCE)
## set option by cmake command line:
## ... see issue: https://github.com/google/googletest/issues/2829
-DINSTALL_GTEST=OFF

性能提升

性能分析

工具

Windows 平台
  1. 大杀器 —— Visual Studio 性能探查器(Alt+F2)
Linux 平台
Perf
perf stat <task command>  # Run a command and gather performance counter statistics
perf record -F 999 <task command>  # Run a command and record its profile into perf.data
perf report  # Read perf.data (created by perf record) and display the profile
perf script -i perf.data &> perf.unfold  # can copy to local then use FlameGraph to generate flame graph
FlameGraph/stackcollapse-perf.pl perf.unfold &> perf.folded
FlameGraph/flamegraph.pl perf.folded > perf.svg
#!/bin/bash
RUN_MODE=false
GEN_MODE=false
TARGET=""
FLAME_GRAPH_PATH=""

while getopts ":rgt:p:f:" opt; do
  case $opt in
    r)
      RUN_MODE=true
      ;;
    g)
      GEN_MODE=true
      ;;
    t)
      TARGET="$OPTARG"
      ;;
    p)
      FLAME_GRAPH_PATH="$OPTARG"
      ;;
    f)
      FILTER="$OPTARG"
      ;;
    \?)
      echo "error: unknown paras -$OPTARG"
      exit 1
      ;;
    :)
      echo "error: option -$OPTARG need one para"
      exit 1
      ;;
  esac
done

if [ "$RUN_MODE" = true ]; then
  echo "enter run mode with paras:"

  if [ -z "$TARGET" ]; then
    TARGET="f360_resim"
  fi

  echo "TARGET: $TARGET"

  perf record -e cpu-clock -F 999 -g ./$TARGET ./test_logs/smart_proj/035_rdvA0105031601 -trkopt -start_det -endopt
  perf script -i perf.data &> perf.unfold

elif [ "$GEN_MODE" = true ]; then
  echo "enter generate mode with paras:"

  if [ -z "$FLAME_GRAPH_PATH" ]; then
    FLAME_GRAPH_PATH="/mnt/c/Utils/FlameGraph"
  fi
  
  echo "FLAME_GRAPH_PATH: $FLAME_GRAPH_PATH"
  
  ${FLAME_GRAPH_PATH}/stackcollapse-perf.pl perf.unfold &> perf.folded;
  ${FLAME_GRAPH_PATH}/flamegraph.pl perf.folded > perf.svg;

fi
GPerfTools
#!/bin/bash
RUN_MODE=false
GEN_MODE=false
TARGET=""
PERF_LIB=""
FILTER=""

while getopts ":rgt:p:f:" opt; do
  case $opt in
    r)
      RUN_MODE=true
      ;;
    g)
      GEN_MODE=true
      ;;
    t)
      TARGET="$OPTARG"
      ;;
    p)
      PERF_LIB="$OPTARG"
      ;;
    f)
      FILTER="$OPTARG"
      ;;
    \?)
      echo "error: unknown paras -$OPTARG"
      exit 1
      ;;
    :)
      echo "error: option -$OPTARG need one para"
      exit 1
      ;;
  esac
done

if [ "$RUN_MODE" = true ]; then
  echo "enter run mode with paras:"

  if [ -z "$TARGET" ]; then
    TARGET="f360_resim"
  fi

  if [ -z "$PERF_LIB" ]; then
    PERF_LIB="/mnt/dev/yirami/profile_tools_and_libs/perftools/lib/libprofiler.so"
  fi

  echo "TARGET: $TARGET"
  echo "PERF_LIB: $PERF_LIB"

  LD_PRELOAD=$PERF_LIB CPUPROFILE=./$TARGET.prof CPUPROFILE_FREQUENCY=999 ./$TARGET ./test_logs/smart_proj/035_rdvA0105031601 -trkopt -start_det -endopt

elif [ "$GEN_MODE" = true ]; then
  echo "enter generate mode with paras:"

  if [ -z "$TARGET" ]; then
    TARGET="f360_resim"
  fi

  echo "TARGET: $TARGET"
  echo "FILTER: $FILTER"

  #pprof --text ./$TARGET ./$TARGET.prof

  if [ -z "$FILTER" ]; then
    pprof --pdf ./$TARGET ./$TARGET.prof > ./$TARGET.pdf
  else
    pprof --pdf --focus=$FILTER ./$TARGET ./$TARGET.prof > ./${TARGET}_${FILTER}.pdf
  fi

fi

方法

  1. 使用 inline 内联关键字

    对于短小精悍的函数,采用内联可以减少函数调用参数压栈、出栈时间,提升效率

  2. 注意元素访问效率,优先使用方括号访问

    [] > for (auto var:obj) > for (auto iter=obj.begin();iter!=obj.end;iter++)
  3. 酌情使用memset

    如果内存会在使用时被重新赋值,且逻辑上并不需要这块内存“干净”,尽量避免使用memset以优化性能

  4. 待补充

踩坑热图

内存非法访问

  1. 2018.0718 指针强制类型转换,注意其指向空间的合法性
  2. 2020.0103 慎重(避免)使用memset初始化包含vector等含有复杂内部信息类型的结构体等

最佳实践

Warnings

Clang -Wrange-loop-bind-reference
std::vector<bool> vec;
for (const auto& elt : vec) {  // warning was throwed
  (void)elt;
}

PS: more analysis is posted on: -Wrange-loop-bind-reference and auto&&

// better choice
std::vector<bool> vec;
for (auto&& elt : vec) {
    (void)elt;
}

参考

  • C++风格指南 · Ref
  • main函数 · Ref · Ref
  • 类模板、函数模板、类外定义类成员函数 · Ref
  • 类成员变量初始化规则 · Ref
  • C++ 11 初始化列表 · Ref
  • C++ 11 初始化 · Ref
  • C++ 11 左值引用&和右值引用&&

  1. What is the difference between C, C99, ANSI C and GNU C? ↩︎

  2. 很多文献,包括 cppreference 皆认为该语法从 C++ 11 开始,可能是因为首次在标准文档中以示例形式列出,或者编译器从 C++ 11 才开始稳定支持等。 ↩︎