基本

函数定义

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

函数声明

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

函数类型

std::string encode(const XmlFieldValue& field)
{
    ...
}

返回类型          参数列表
   ↓                 ↓
std::string (const XmlFieldValue&)
using EncodeFunc = std::string(const XmlFieldValue&); 相当于给这个函数类型起了个名字

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;//读取普通非 const 变量的值,也会 odr-use x。
    int* p = &x;//需要x 的地址,x 必须存在内存中,x 被 ODR-use
}

例如

//c++11 .h
struct State
{
    static constexpr std::uint32_t kDiagFrames = 20; // 需包含 <cstdint>
};
//.cpp
constexpr std::uint32_t State::kDiagFrames; // C++11/14:被 odr-use 时需提供类外定义
  • static 表示这个变量属于类,不属于对象
  • constexpr 表示编译期常量
auto p = &State::kDiagFrames;//必须存在一个真正的对象,也就是State::kDiagFrames需要内存地址

注意:

C++11/14 中,类内 static constexpr 成员可用于常量表达式;若被 odr-use(如取地址),仍需在一个源文件中提供上面的类外定义,且不再写初始化器。C++17 起 constexpr 静态数据成员隐含 inline,不再需要额外定义。

三五准则

  • 禁止拷贝构造
EventLogManager(const EventLogManager&) = delete;
  • 禁止拷贝赋值
EventLogManager& operator=(const EventLogManager&) = delete;

关键字

explicit

explicit 的作用是禁止隐式类型转换;而“有没有默认构造函数”取决于类里是否存在一个可以无参调用的构造函数,以及编译器是否还会自动生成默认构造函数。

class A
{
public:
    explicit A(int x) {}
};
A a;        // ❌ 编译失败
A a(10);    // ✅

左值

左值是标识对象或函数的一类表达式,例如变量名 a、解引用表达式 *p、返回 T& 的函数调用。左值不一定有名字,也不一定可修改(如 const 对象);位域左值还不能取地址。

int a = 10;(a是左值)

右值

右值包括纯右值(prvalue)和将亡值(xvalue),不等于“不可修改”。

纯右值:例如整数常量 42、a + b 和按值返回的函数调用;字符串字面量是左值。

将亡值:例如 std::move(a) 或返回 T&& 的函数调用。std::move 只做类型转换,不会自行移动或释放资源;有名字的右值引用变量在表达式中仍是左值。

std::move(a)
42, a + b, func()

右值引用

智能指针

unique_ptr

介绍

unique_ptr 表示独占所有权,不能复制,但可以移动。可以通过值传递把所有权移入函数,例如 consume(std::move(ptr));也可用于支持移动语义的容器和算法。

创建

std::unique_ptr<int> intprt {};
std::unique_ptr<int> intPtrB{ new int{ 15 } };
//C++ 14 及之后的初始化方式:std::make_unique <类型>();
std::unique_ptr<int> ptrA{ std::make_unique <int>(5)};  //把指针指向的那块内存初始化为 5;
std::unique_ptr<int[]> ptrB{ std::make_unique<int[]>(5) }; // 数组初始化为 有 5个元素;

用法

  • reset() 调用删除器释放原管理对象,并保存新指针(默认变为 nullptr);unique_ptr 对象本身仍存在。
  • get() 返回所管理的原始指针,不转移所有权;不要据此重复释放对象。

自定义删除器

std::unique_ptr<T, Deleter>

enable_shared_from_this

类型转换

dynamic_cast

将一个基类对象指针(或引用cast到继承类指针,dynamic_cast会根据基类指针是否真正指向继承类指针来做相应处理。

class Animal {
public:
    virtual ~Animal() = default;
    virtual void speak() {
        std::cout << "Animal" << std::endl;
    }
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Dog" << std::endl;
    }

    void run() {
        std::cout << "Dog running" << std::endl;
    }
};

Dog dog;
Animal* ptr = &dog;

虽然 ptr 的静态类型是 Animal*,但它实际指向的是一个 Dog 对象:

  • 编译时类型:Animal*
  • 运行时真实类型:Dog
  • ptr->speak():调用 Dog::speak(),因为它是虚函数。
  • ptr->run():编译失败,因为 Animal 没有声明 run()。

如果我们需要调用Dog::run(),应该怎么办?

基本用法

基类指针转换为派生类指针

Dog dog;
Animal* ptr = &dog;

Dog* dogPtr = dynamic_cast<Dog*>(ptr);

if (dogPtr != nullptr) {
    dogPtr->run();
}
//输出 "Dog running"

对于这种需要运行时检查的向下转换,源类型必须是多态类型(至少有一个虚函数)。失败时返回nullptr

dynamic_pointer_cast

std::dynamic_pointer_cast 是 C++11 提供的智能指针类型转换函数,专门用于 std::shared_ptr 在继承体系中的安全类型转换。std::dynamic_pointer_cast会正确处理 shared_ptr的引用计数和所有权。

class Cat : public Animal {};

std::shared_ptr<Animal> animal =
    std::make_shared<Cat>();

std::shared_ptr<Dog> dog =
    std::dynamic_pointer_cast<Dog>(animal);
// 为实际对象是 Cat,并不是 Dog,所以返回一个空的 shared_ptr<Dog>
if (!dog) {
    std::cout << "转换失败" << std::endl;
}

引用计数怎么变化

std::shared_ptr<Animal> animal =
    std::make_shared<Dog>();

std::cout << animal.use_count() << std::endl;  // 1

std::shared_ptr<Dog> dog =
    std::dynamic_pointer_cast<Dog>(animal);

std::cout << animal.use_count() << std::endl;  // 2
std::cout << dog.use_count() << std::endl;     // 2

static_cast

静态类型转换,用于编译期可确定的、相对安全的类型转换,包括:

  • 基本类型之间的转换(如 int ↔ double)。
  • 指针/引用在 有继承关系的类之间 向上或向下转换(但不安全!)。
  • void*与其他指针类型之间的转换。
  • 显式调用构造函数或转换函数 (如 const char* →std::string,可直接将 char * 、const char * 类型的对象赋值给 string 对象
  • enum 与整型之间。
  • void*与其他指针类型之间(但不能用于函数指针)。

注意

  1. static_cast 不进行运行时类型检查,向下转型(基类 → 派生类)可能不安全!

写文件

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

常用的标志位:

  • std::ios::out:为输出打开文件(ofstream 默认自带)。
  • std::ios::app:追加模式(所有写入都追加到文件末尾)。
  • std::ios::trunc:截断已有文件。ofstream 默认以 out 模式打开时会截断;显式使用 app 或 in | out 等组合时,行为不同。
  • std::ios::binary:二进制形式打开。

安全的写文件

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

这个流程用于提高整文件替换的崩溃一致性:临时文件应位于目标文件的同一目录,完整写入并检查每次返回值,再 fsync 文件、rename、fsync 目录。rename 的原子可见性不等于掉电持久性;最终保证还取决于文件系统、驱动及存储设备是否正确实现同步语义。

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。

*/

读文件

std::ifstream

explicit 

访问语义

指针、引用、const 限定和值类别共同影响表达式的使用方式。引用是对象或函数的别名,不能把“常见实现使用地址”当作语言规定的“引用就是指针”。

例如:

写法语言语义
T a直接对象
T* p指向 T 的指针
T& r绑定到 T 对象的左值引用,初始化后不能重新绑定
const T* p指针可改,但指向内容不可改
T* const p指针不可改,但内容可改
const T& r通过该引用把对象视为 const T
const 成员函数this 表达式的类型为 const T*,它是纯右值

螺旋法则

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

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

    p,从这里开始

  2. 先向右看(顺时针)

    如果右边是:

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

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

const 成员函数

void f() const;

在 A::f() const 内,this 是类型为 const A* 的纯右值表达式。编译器可能通过隐含参数实现成员调用,但下面只是理解用的近似示意,不是合法的成员函数改写:

// 概念示意:void f_impl(const A* self);

注意:

  • this 指向 const A
  • 不能通过 this 修改普通非 mutable 成员;mutable 成员仍可修改,指针成员所指对象也不会自动变成 const
this->x = 1;  // ❌

虚函数

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

多态

未显式限定类名的虚函数调用,语义上由对象的动态类型选择最终重写函数;直接对对象调用也遵守此规则。编译器能确定类型时可以去虚拟化优化。显式限定 Base::show() 会抑制虚派发。

Derived d;
Base b = d;       // 对象切片
b.show();         // b 已切片,动态类型就是 Base,因此调用 Base::show

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

p->Base::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,因此 b.show() 调用 Base::show。vptr/vtable 是常见实现方式,不是 C++ 标准强制规定的对象布局。

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()。在这里讨论的 C++11 常规 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 派生对象,应提供 public virtual 析构;若禁止这种删除,可采用 protected non-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、值类型返回不能协变。

虚函数表

常见 C++ ABI 使用虚函数表(vtable)和虚表指针(vptr)实现虚派发,但标准不规定必须采用这种布局;一个类也可能涉及多张虚表。

  • 虚表通常放在运行时只读的数据区域;例如 ELF 动态重定位可能使用 .data.rel.ro。数量和布局由编译器、ABI 及继承结构决定。
  • 采用这种 ABI 时,多态对象通常含一个或多个 vptr;相同动态类型、相同子对象位置通常共享相应的虚表。
  • 构造/析构时编译器会按当前阶段设置相应 vptr;按值复制产生的 Base 对象有自己的状态,不会改变原 Derived 对象的 vptr。
  • 调用虚函数:经 vptr 查表 + 间接跳转,有额外开销;极热路径慎用多态。

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

多继承(简述)

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

异步编程

参考:odr-use;值类别;this;delete。