基本

函数定义

返回值类型 函数名(参数列表)      //函数头
{
	函数体                      
}

函数声明

返回值类型 函数名(参数列表);

ODR规则

ODR = One Definition Rule(单一定义规则),C++ 程序有一个核心规则:

一个变量、函数、类型,在整个程序中必须满足定义规则

禁止:

// a.cpp
int g_value = 10;

// b.cpp
int g_value = 20;

在链接的时候会出现multiple definition of g_value,违反了ODR

ODR-use

编译器必须真的需要这个对象的地址或者存储空间时,就叫 ODR-use。

例如:

int x = 10;

int main()
{
    int y = x;//只是读取值。
    int* p = &x;//需要x 的地址,x 必须存在内存中,x 被 ODR-use
}

例如

//c++11 .h
struct State
{
    static constexpr uint32_t kDiagFrames = 20;
};
//.cpp
constexpr int State::value;
  • static 表示这个变量属于类,不属于对象
  • constexpr 表示编译期常量
auto p = &State::kDiagFrames;//必须存在一个真正的对象,也就是State::kDiagFrames需要内存地址

注意:

static constexpr 初始化,只满足“编译期使用”,并不一定提供一个真正的内存对象。

  • 产生编译期常量,可以直接替换20 ,但是内存对象不存在

文件

写文件

std::ofstream (Output File Stream,输出文件流)专门用于向文件中写入数据。它包含在 <fstream> 头文件中。

常用的标志位:

  • std::ios::out:为输出打开文件(ofstream 默认自带)。
  • std::ios::app:追加模式(所有写入都追加到文件末尾)。
  • std::ios::trunc:截断模式(如果文件存在,先清空内容,ofstream 默认自带)。
  • std::ios::binary:二进制形式打开。

安全的写文件

在嵌入式系统中, jffs2/ubifs/ext2,掉电是常态,配置文件损坏就意味着设备变砖。

这个流程是安全替换整个文件的套路:要求断电或进程崩溃后,文件要么是旧版本,要么是完整的新版本

path.tmp -> fsync(tmp) -> rename(tmp, path) -> fsync(dir)
    
旧文件 path 仍然保留
        ↓
把新内容写入 path.tmp
        ↓
确认 path.tmp 内容落盘
        ↓
rename(path.tmp, path)
        ↓
旧 path 被原子替换成新 path

rename(“path.tmp”, “path”)具有原子可见性。其他进程看到的通常只有两种状态:

  • rename 前:完整旧文件
  • rename 后:完整新文件
//config.json      -> 旧 inode
//config.json.tmp  -> 新 inode
rename("config.json.tmp", "config.json"); //config.json -> 新 inode 

/*
如果某个进程之前已经打开旧文件:
int fd = open("config.json", O_RDONLY);
即使随后发生 rename(),这个 fd 仍然可以继续读取旧 inode。
而之后重新 open("config.json") 的进程会打开新 inode。

*/

访问语义

c++里所有“访问语义”本质:指针类型 + 是否允许写解引用目标

在编译器层面:

类型 = [是否可写] + [间接层级(指针/引用)] + [对象类型]

例如:

写法编译器真实语义
T a直接对象
T* p指向 T 的指针
T& r“不可改指针”的 T*(语法糖)
const T* p指针可改,但指向内容不可改
T* const p指针不可改,但内容可改
const T& r指向 const T 的“引用”(本质也是指针)
const 成员函数this 变成 const T* const this(核心)

螺旋法则

从变量名开始,按顺时针方向“绕一圈”,遇到谁就读谁

const int* const p;
  1. 找到变量名

    p,从这里开始

  2. 先向右看(顺时针)

    如果右边是:

    • [] → 数组
    • () → 函数
    • 没东西 → 往左走
  3. 再向左看

    • * → pointer
    • const → 只读
    • 类型名 → 基类型
1.左边:const  p 不可改
2.再左:*  pointer
3.再左:const int  指向内容不可改
 指针不可改 + 指向内容不可改

const 成员函数

void f() const;

变成:

void f(const A* const this);

注意:

  • this 指向 const A
  • 所以不能改成员
this->x = 1;  // ❌

虚函数

virtual 声明的成员函数,允许在派生类中重写(override),通过基类指针/引用实现运行时多态

多态

通过基类的指针引用调用虚函数,触发动态绑定;通过对象直接调用,或显式限定 Base::show(),则为静态绑定。

Derived d;
Base b = d;       // 对象切片
b.show();         // 静态绑定:Base::show

Base* p = &d;
p->show();        // 动态绑定:Derived::show

Base::show();     // 显式限定,静态绑定

对象切片

派生类对象按值赋给/复制成基类对象时,发生对象切片(Object Slicing),派生类部分被丢掉。

class Base {
public:
    virtual void show() const {
        std::cout << "Base::show\n";
    }

    int baseValue = 10;
};

class Derived : public Base {
public:
    void show() const override {
        std::cout << "Derived::show\n";
    }

    int derivedValue = 20;
};

int main()
{
    Derived d;
    Base b = d;   // 对象切片
    b.show();     // Base::show
}

调用 Base 的复制构造函数,得到真正的 Base 对象;其 vptr 指向 Base 的 vtable,故 b.show() 必走 Base::show

override 与 final(C++11)

  • override:标记派生类函数意图重写基类虚函数;签名不匹配时编译失败,避免「以为重写了其实没重写」。
  • final:禁止进一步重写;可修饰虚函数,也可修饰类(该类不可被继承)。

派生类重写时不必再写 virtual;写了 override 即表示重写。static 成员函数不能是 virtual

class Base {
public:
    virtual void foo() {}
    virtual void bar() {}
};

class Derived final : public Base {
public:
    void foo() override {}           // 重写
    void bar() final override {}     // 重写,且禁止子类再重写 bar
};

纯虚函数与抽象类

纯虚函数:virtual void f() = 0;,无默认实现(也可在类外提供实现,但类仍保持抽象)。

  • 含纯虚函数的类是抽象类,不能实例化,可作接口。
  • 派生类须实现全部纯虚函数后才能实例化。
class IDevice {
public:
    virtual ~IDevice() = default;
    virtual void start() = 0;
    virtual void stop() = 0;
};

class Camera : public IDevice {
public:
    void start() override { /* ... */ }
    void stop() override { /* ... */ }
};

虚析构

多态基类几乎总要 virtual ~Base()。通过基类指针 delete 派生对象时,无虚析构则只析构基类部分,派生资源泄漏。

class Base {
public:
    virtual ~Base() { std::cout << "~Base\n"; }
};

class Derived : public Base {
public:
    ~Derived() override { std::cout << "~Derived\n"; }
};

int main()
{
    Base* p = new Derived;
    delete p;   // 有虚析构:~Derived → ~Base
}

规则:类可能被 delete 基类指针、或放入多态容器时,基类析构必须是 virtual

重写与隐藏(name hiding)

派生类声明与基类同名但签名不同的函数时,隐藏基类同名函数,不会自动形成重写关系。

class Base {
public:
    virtual void f(int) { std::cout << "Base::f(int)\n"; }
};

class Derived : public Base {
public:
    void f(double) { std::cout << "Derived::f(double)\n"; }  // 隐藏 Base::f,非重写
};

Derived d;
d.f(1);           // Derived::f(double)
d.f(1.0);         // Derived::f(double)
Base* p = &d;
p->f(1);          // Base::f(int),动态绑定仍走基类版本

若需同时暴露基类版本:using Base::f;,或对每个要重写的签名显式 override

构造与析构期间的虚函数

构造/析构过程中,this 的类型随阶段变化,虚函数调用通常不按完整派生类型绑定:

  • 构造基类时:对象视为基类,调虚函数走基类实现。
  • 析构派生类已完成后:对象视为基类,调虚函数走基类实现。

因此不要在基类构造/析构里依赖「派生类尚未构造/已销毁」的虚函数行为。

默认实参与虚函数

默认实参在编译期静态类型绑定,与动态绑定的函数体无关。

class Base {
public:
    virtual void f(int x = 1) { std::cout << "Base " << x << "\n"; }
};

class Derived : public Base {
public:
    void f(int x = 2) override { std::cout << "Derived " << x << "\n"; }
};

Derived d;
Base* p = &d;
p->f();           // 输出 Derived 1(默认实参来自 Base,函数体来自 Derived)

避免在派生类重写时修改默认实参;若两侧默认值不同,易出歧义。

协变返回类型

重写时返回类型可以是基类返回类型的派生类指针/引用(协变)。

class Base {
public:
    virtual Base* clone() { return new Base(*this); }
};

class Derived : public Base {
public:
    Derived* clone() override { return new Derived(*this); }
};

仅适用于指针/引用返回;void、值类型返回不能协变。

虚函数表

每个含虚函数的类有一份虚函数表(vtable),存放该类虚函数入口地址。

  • vtable 通常在只读段(如 .rodata),全进程一份;不同类各有一份。
  • 每个对象含编译器生成的 vptr(虚表指针),指向所属类的 vtable;同类实例共享同一 vtable。
  • vptr 在构造时写入;对象切片后 vptr 变为基类 vtable。
  • 调用虚函数:经 vptr 查表 + 间接跳转,有额外开销;极热路径慎用多态。

vptr 的类型与布局是实现定义的,不要手写 void** 去碰。

多继承(简述)

多继承且多个基类含虚函数时,对象可能有多张 vtable / 多个 vptr;菱形继承需虚继承避免重复基类子对象。细节见 OOP/继承专题。

异步编程