1.final 与 override
这两个关键字解决的是多态继承中的两类错误:想重写却没重写、不想让重写却被重写。
override:显式声明"我要重写基类虚函数"
派生类函数签名(连 const 都不能差)只要和基类虚函数有一点点不匹配,编译器就静默地当成新函数------这种 bug 很难发现。override 让编译器帮你检查。
c
class Base {
public:
virtual void speak() const; // 注意 const
virtual ~Base() = default;
};
// 没有 override 时代:写错签名不报错,是多态失效的隐藏 bug
class Derived1 : public Base {
public:
void speak() {}; // 拼错/少写 const → 这是新函数,不是重写!静默出错
};
// 有了 override:写错直接编译错误
class Derived2 : public Base {
public:
void speak() const override {} // 正确
// void speak() override {} // 报错:基类没有可重写的 void speak()
// void sppek() const override {} // 报错:没有这个虚函数
};
final:禁止重写 / 禁止继承
c
class Base {
public:
virtual void speak() final; // 禁止派生类重写
};
class Animal final { // 禁止被继承(如 std::string 的设计)
};
// class Dog : public Animal {}; // 编译错误:Animal 是 final
// class X : public Base {
// void speak() override {} // 编译错误:Base::speak 是 final
// };
适用场景:接口设计时防止误用。override 应当默认加上(纯新函数除外);final 用于性能优化提示(帮助编译器去虚化 devirtualization)或设计上的封闭类。
2.using 的三种新用法
C++11 起 using 不只是命名空间引入了。
类型别名(替代 typedef,可读性更好,尤其是函数指针和模板)
c
typedef unsigned int uint_t;
using uint_t = unsigned int; // 等价,更直观:从左到右读
typedef void (*FuncPtr)(int, double);
using FuncPtr = void(*)(int, double); // 别名放在等号左边,清晰
// typedef 做不到的事:模板别名
template <typename T>
using Vec = std::vector<T>; // 类型参数化别名
Vec<int> v;
template <typename K, typename V>
using StrMap = std::map<K, V, std::less<>>;
StrMap<std::string, int> scores;
// typedef 写模板别名必须套一层 struct,很丑
template <typename T>
struct VecOld { typedef std::vector<T> type; };
VecOld<int>::type v2;
using 声明改变继承成员的访问级别
c
class Base {
public:
void hello() {}
protected:
void prot() {}
};
class Derived : private Base {
public:
using Base::hello; // 把私有继承来的 public 成员重新公开
};
Derived d;
d.hello(); // 可以
// d.prot(); // 仍然不行
继承构造函数(C++11)
c
class Base {
public:
Base(int) {}
Base(int, double) {}
};
class Derived : public Base {
public:
using Base::Base; // 继承 Base 的全部构造函数
// 等价于把 Base(int)、Base(int,double) 都转发一遍
Derived(const char* s) : Base(0) {} // 自己新增的构造函数
};
Derived d1(42); // 继承来的
Derived d2(1, 3.14); // 继承来的
Derived d3("hi"); // 自己的
派生类新增数据成员需要初始化时,继承的构造函数会用成员的默认初始化,要小心。
3.auto、decltype、尾置返回类型
auto:让编译器推导类型
c
auto i = 42; // int
auto d = 3.14; // double
auto s = std::string("hi"); // std::string
// 真正的用武之地:类型又长又臭的场合
std::map<std::string, std::vector<int>> m;
for (auto it = m.begin(); it != m.end(); ++it) { /* ... */ }
// 对比:std::map<std::string, std::vector<int>>::iterator it = ...
auto p = std::make_unique<int>(42); // std::unique_ptr<int>
auto 的几个坑:
c
const int x = 10;
auto a = x; // int!auto 会丢弃顶层 const
const auto b = x; // const int
auto& c = x; // const int& ------ 加引用后保留 const(x 本身 const)
std::vector<int> v{1,2,3};
auto v2 = v; // 拷贝整个 vector
auto& v3 = v; // 引用,不拷贝
auto i = 0; // int ------ 初始化为 0 时小心,auto 不会帮你变成 double
// auto 不能用于函数参数(C++20 的简写函数模板除外)、非静态成员变量
void f(auto x); // C++20 起可以(等价于模板)
decltype:查询表达式的类型,不丢弃 const/引用
c
int x = 0;
const int& r = x;
decltype(x) a; // int
decltype(r) b = x; // const int&
decltype(x+1) c; // int(表达式类型)
decltype((x)) d = x; // int&!多加一层括号变成表达式 → 左值 → 引用
// 主要用途:decltype(auto) 和复杂返回类型推导
尾置返回类型(trailing return type):把返回类型写在参数列表之后、-> 之前。
c
// 为什么需要它?看一个经典例子:返回类型依赖参数类型
template <typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
return t + u;
}
// 参数名 t、u 在返回类型处不可见,只有尾置形式能用到它们
// C++14 起可进一步简化为 auto(自动推导返回类型)
template <typename T, typename U>
auto add2(T t, U u) { return t + u; }
// decltype(auto):返回类型与 return 表达式完全一致
auto&& f() { static int x = 0; return (x); } // auto&& 推导为 int
decltype(auto) g() { static int x = 0; return (x); } // 精确返回 int&
4.强枚举类型(scoped enum,enum class)
传统 enum 的三大问题:名字泄漏到外层作用域、可隐式转 int、底层类型不确定。
c
enum Color { Red, Green, Blue };
// Red 污染了外层作用域:不能再定义其他 Red
Color c = Red;
int n = c; // 隐式转 int,类型不安全
enum class 全部解决:
c
enum class Color { Red, Green, Blue }; // 名字被限定在 Color 作用域内
enum class Traffic { Red, Yellow }; // Red 不再冲突!
Color c = Color::Red; // 必须带限定名
// int n = c; // 编译错误:禁止隐式转 int
int n = static_cast<int>(c); // 必须显式转换
Color d = static_cast<Color>(1); // 反向也要显式
// 可指定底层类型(默认 int),便于前向声明和内存控制
enum class Byte : unsigned char { A = 1, B = 2 };
enum class Status : uint8_t; // 前向声明(传统 enum 底层类型不固定,不能前向声明)
// 想输出怎么办?
std::ostream& operator<<(std::ostream& os, Color c) {
switch (c) {
case Color::Red: return os << "Red";
case Color::Green: return os << "Green";
case Color::Blue: return os << "Blue";
}
return os;
}
原则:新代码一律用 enum class,除非有兼容旧接口的强制理由。
5.常量表达式(constexpr)
constexpr 对象:
编译期常量,必须初始化,且初始化值必须是常量表达式
c
constexpr int N = 10;
int arr[N]; // OK,N 是编译期常量
const int M = getValue(); // 不是 constexpr:运行时初始化
// constexpr int K = getValue(); // 报错(除非 getValue 是 constexpr)
constexpr double pi = 3.14159;
constexpr 函数:
满足条件时可在编译期求值;不满足时自动退化为普通运行时函数(这是它与模板元编程的本质区别------一鱼两吃)。
c
constexpr int square(int x) { return x * x; }
constexpr int a = square(5); // 编译期求值
int n = 10;
int b = square(n); // 运行期求值,合法!
// C++14 起 constexpr 函数体内允许更复杂的语句
constexpr int fib(int n) {
if (n <= 1) return n; // C++11 只允许一个 return
return fib(n - 1) + fib(n - 2);
}
constexpr int f10 = fib(10); // 编译期算出 55
// constexpr 构造函数 → 编译期构造对象
class Point {
public:
constexpr Point(double x, double y) : x_(x), y_(y) {}
constexpr double norm2() const { return x_*x_ + y_*y_; }
private:
double x_, y_;
};
constexpr Point p(3.0, 4.0);
static_assert(p.norm2() == 25.0, "编译期检查失败"); // static_assert 也是编译期利器
const vs constexpr:
const |
constexpr |
|
|---|---|---|
| 含义 | 运行期不可修改 | 编译期就能确定值 |
| 初始化 | 运行期也可以 | 必须编译期可求值 |
| 用途 | 只读变量、只读成员函数 | 编译期常量、编译期计算、数组大小、模板参数 |
constexpr 成员函数隐含 const(C++14 之前;C++23 起规则有变,但传统认知是 constexpr 成员函数不能修改成员)。
C++17/20 补充:
c
constexpr std::array<int, 5> a = {1,2,3,4,5};
// C++17:if constexpr ------ 编译期分支,不满足的分支不参与实例化
template <typename T>
auto getValue(T t) {
if constexpr (std::is_pointer_v<T>) {
return *t; // T 不是指针时,这句根本不编译
} else {
return t;
}
}
// C++20:consteval(必须编译期求值,否则报错)和 constinit
consteval int mustBeCompileTime(int x) { return x * 2; }
constinit int g = 42; // 静态初始化,避免静态初始化顺序问题
6.内存对齐设置与查询
alignof:查询类型的对齐要求;alignas:指定对齐。
c
// 查询
std::cout << alignof(char); // 1
std::cout << alignof(int); // 4(常见平台)
std::cout << alignof(double); // 8
struct S { char c; double d; };
std::cout << alignof(S); // 8(S 的对齐 = 成员中最大的对齐)
// 用 alignas 指定对齐
struct alignas(16) Vec { // Vec 按 16 字节对齐(SIMD 常用)
float data[4];
};
static_assert(alignof(Vec) == 16);
alignas(64) char cacheLine[64]; // 单独变量指定对齐
应用:缓存行对齐避免伪共享(false sharing)
c
struct PaddedCounter {
alignas(64) // 独占一条缓存行
std::atomic<int> value{0};
};
// 多线程各自更新自己的 counter,互不干扰
// 无 alignas 时两个 counter 可能落在同一缓存行:
// 线程 A 修改 → 缓存行失效 → 线程 B 重新加载 → 性能雪崩
手动分配对齐内存(C++17 aligned new)
c
// C++17 之前:posix_memalign / _aligned_malloc,且不兼容 delete
// C++17 起:new 表达式直接支持对齐
struct alignas(128) Big { char data[128]; };
Big* p = new Big; // 自动按 128 对齐分配
delete p; // 配对释放
// 或者显式指定对齐的 new
void* raw = ::operator new(256, std::align_val_t(64));
::operator delete(raw, std::align_val_t(64));
7.Lambda 表达式与捕获
基本形式:
c
auto lam = [捕获列表](参数列表) -> 返回类型 { 函数体 };
// 大部分可省略:
[capture](params) { body } // 返回类型自动推导
[]{ std::cout << "hi\n"; }(); // 立即调用
捕获方式:
c
int x = 10, y = 20;
// ① 值捕获 [x]:拷贝一份进闭包,外部修改不影响 lambda
auto byValue = [x] { return x * 2; }; // x 拷贝了一份
x = 100;
byValue(); // 仍是 20(捕获时拷贝的是 10)
// ② 引用捕获 [&x]:捕获引用,看到的是当前值
auto byRef = [&x] { return x * 2; };
x = 100;
byRef(); // 200
// ③ 隐式捕获:[=] 全部值捕获;[&] 全部引用捕获
auto a = [=] { return x + y; }; // x、y 都拷贝
auto b = [&] { return x + y; }; // x、y 都按引用
auto c = [=, &y] { return x + y; }; // x 值捕获,y 引用捕获(混合语法)
// ④ this 捕获:[this] 或 [=]/[&] 隐式捕获(C++20 起隐式 this 被弃用)
struct Counter {
int n = 0;
auto makeIncrementer() {
// n是成员变量,需要捕获this下,编译器借助this访问其n成员
return [this] { ++n; }; // 捕获 this 指针(注意悬空风险!)
// C++20 推荐:[*this] 按值捕获整个对象副本,更安全
}
};
"可捕获"与"可直接使用"的区别------这是理解捕获的关键:
- 可以直接使用:静态变量、全局变量、constexpr、不依赖 this 的名字------它们在 lambda 所在作用域直接可见,不需要捕获。
c
int g = 1;
static int s = 2;
constexpr int C = 3;
auto lam = [] {
return g + s + C; // 全局、静态、constexpr 直接用,无需捕获
// 但局部变量 x 不捕获就报错
};
- 必须捕获:lambda 所在函数的局部变量、成员变量(经 this)。原因:lambda 本质是一个编译器生成的匿名类(闭包),捕获就是给这个类的成员赋值;局部变量出了作用域就销毁,不捕获就没有合法途径访问。
默认捕获的陷阱:
c
std::vector<std::function<int()>> fs;
for (int i = 0; i < 3; ++i) {
fs.push_back([i] { return i; }); // 正确:显式捕获 i 的当前值
// fs.push_back([&] { return i; }); // 错误!引用捕获循环变量 → 全部变成 3
}
for (auto& f : fs) std::cout << f() << " "; // 0 1 2
// [=] 同样危险:
std::function<int()> bad;
{
int local = 42;
bad = [=] { return local; }; // 值捕获 OK:拷贝了一份
// bad = [&] { return local; }; // 悬空引用!local 离开作用域后调用是 UB
}
bad(); // 值捕获:安全返回 42
mutable 与泛型 lambda:
c
int x = 0;
auto inc = [x]() mutable { ++x; }; // 值捕获默认是 const 成员,mutable 允许修改副本
inc(); inc(); // x 本身还是 0
// C++14 泛型 lambda
auto print = [](const auto& v) { std::cout << v << "\n"; };
print(42); print("hello"); print(3.14);
8.智能指针:shared_ptr / unique_ptr / weak_ptr / shared_from_this
先记住一条铁律:裸指针 new 之后交给智能指针,之后只通过智能指针操作,绝不手动 delete。
unique_ptr:独占所有权
c
#include <memory>
std::unique_ptr<int> p1(new int(42));
auto p2 = std::make_unique<int>(42); // 推荐:异常安全且高效(C++14)
std::unique_ptr<int> p3 = std::move(p1); // 所有权转移,p1 变空
// std::unique_ptr<int> p4 = p1; // 错误:禁止拷贝
if (p1 == nullptr) { /* p1 已空 */ }
*p3 = 100;
p3.reset(); // 释放
int* raw = p3.release(); // 交出所有权(返回裸指针,不再负责)
delete raw;
// 自定义删除器(RAII 封装任意资源)
std::unique_ptr<FILE, decltype(&fclose)> fp(fopen("a.txt", "r"), &fclose);
// 数组版本
auto arr = std::make_unique<int[]>(100); // unique_ptr 支持数组,shared_ptr 默认不支持
shared_ptr:共享所有权(引用计数)
c
auto p1 = std::make_shared<int>(42); // 引用计数 = 1(推荐 make_shared,一次分配控制块+对象)
{
auto p2 = p1; // 计数 = 2,共享同一块内存
auto p3 = p1; // 计数 = 3
} // p2、p3 离开作用域,计数回到 1
std::cout << p1.use_count(); // 1
// 自定义删除器
std::shared_ptr<int> p(new int(42), [](int* q) {
std::cout << "自定义释放\n";
delete q;
});
weak_ptr:观察不拥有(解决循环引用)
shared_ptr 的经典死穴------循环引用导致内存泄漏:
c
struct Node {
std::shared_ptr<Node> next; // 问题所在
~Node() { std::cout << "Node 析构\n"; }
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a; // 循环引用!计数永远是 1,永不释放 → 泄漏
解法:把其中一边换成 weak_ptr:
c
struct Node {
std::weak_ptr<Node> next; // 弱引用,不影响计数
};
auto a = std::make_shared<Node>();
auto b = std::make_shared<Node>();
a->next = b;
b->next = a; // OK,a 先析构 → b 计数归零 → b 析构 → 无泄漏
// weak_ptr 的使用方法
std::weak_ptr<Node> w = a;
if (auto locked = w.lock()) { // 使用前必须 lock() 提升为 shared_ptr
// 使用 locked,在此期间对象保证存活
} // locked 释放,计数恢复
std::cout << w.expired(); // 对象是否已销毁
w.reset(); // 清空
典型应用:缓存、观察者模式的 subject 对 observer 的引用、打破父子对象的双向引用。
shared_from_this:在对象内部安全获取 shared_ptr
直接的问题:不能在对象内部对 this 直接构造 shared_ptr,否则会形成两个独立控制块,导致二次释放。
c
struct Bad {
std::shared_ptr<Bad> getSelf() {
return std::shared_ptr<Bad>(this); // 危险!
}
};
void demo() {
auto p1 = std::make_shared<Bad>();
auto p2 = p1->getSelf();
// p1 和 p2 各自有控制块!对象会被 delete 两次 → 崩溃
}
正确姿势:继承 std::enable_shared_from_this:
c
struct Good : std::enable_shared_from_this<Good> {
std::shared_ptr<Good> getSelf() {
return shared_from_this(); // 返回与外部共享同一控制块的 shared_ptr
}
void registerSelf() {
registry.push_back(weak_from_this()); // C++17 起也可获取 weak_ptr,更安全
}
};
void demo() {
auto p1 = std::make_shared<Good>();
auto p2 = p1->getSelf(); // 同一控制块,计数 = 2,安全
std::weak_ptr<Good> w = p1->getSelf();
}
// 注意:shared_from_this() 只能在对象已被 shared_ptr 管理后调用
// 栈对象或裸 new 的对象调用会抛 std::bad_weak_ptr(或 UB,视实现)
Good g;
// g.getSelf(); // 错误:g 没有被 shared_ptr 管理
选型速查表:
| 场景 | 选择 |
|---|---|
| 独占资源、工厂转移所有权 | unique_ptr(默认首选,开销为零) |
| 多处共享所有权 | shared_ptr |
| 可能形成循环、观察缓存对象 | weak_ptr |
| 对象内部需要把自己的指针交出去 | enable_shared_from_this |
| 函数参数只是观察不接管 | 传裸指针或引用(Widget* / Widget&) |
| 函数接管所有权 | 按值传 unique_ptr(接收方移动),或传 unique_ptr&& |