基本
函数定义
返回值类型 函数名(参数列表) //函数头
{
函数体
}
函数声明
返回值类型 函数名(参数列表);
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;
找到变量名
p,从这里开始
先向右看(顺时针)
如果右边是:
[]→ 数组()→ 函数- 没东西 → 往左走
再向左看
*→ pointerconst→ 只读- 类型名 → 基类型
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/继承专题。