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

Contents

语法特性

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

操作符

取地址 &
引用 &
  • 函数传参的三种方式:按值传递、按指针传递、按引用传递
  • 引用并非对象,没有单独的地址,这与指针 * 有所区别。
    • 对于某个对象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

C++ 03

ISO/IEC 14882:2003

C++ 11

ISO/IEC 14882:2011

关键字

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

模板

parameter pack 模板参数包

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

C++ 14

ISO/IEC 14882:2014

C++ 17

ISO/IEC 14882:2017

statement

if constexpr

用于指示编译期进行求值判断,类似于宏(#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

关键字

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

STL

std::format

字符串格式化

内存管理

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

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

new/delete 与 malloc/free

内存顺序

Memory Order

Memory Barrier

C/C++混合编程

C中调用C++

wrapper c++ functions/class
/ 用法 /
  1. cpp文件中调用c++函数,使用兼容C的接口风格
  2. 头文件中使用#ifdef __cplusplus,声明接口为C风格
/ 示例 /
/**
 * Copyright (C) 2018 ZHICHEAUTO Technology (Shanghai) Co. Ltd.
 * All rights reserved.
 * @file    fusion_caller_wrapper.h
 * @brief   C++ code to C wrapper used by Simulink::C Caller .
 * @author  [Tang zhiyong](https://yirami.xyz)
 * @date    2018-12-31
 * @version 1.0
 * @note
 * @history
 */

#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
    3. 编译
      cd dir_unzip
      ./bootstrap
      make -j8
      sudo make install
    4. 验证
      cmake --version

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

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

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

程序占用

存储时

  • 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 平台

方法

  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等含有复杂内部信息类型的结构体等

参考

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