c++新特性--实用新特性

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&&
相关推荐
Soneswy1 小时前
C++基础篇——类与对象(中)
开发语言·c++
kiracrimson2 小时前
STL:List的使用和模拟实现
开发语言·c++·list
二十七日秋2 小时前
算法刷题笔记「Day 1|四道 OJ 入门题里的深坑」
c语言·汇编·数据结构·c++·笔记·python·算法
流年如夢3 小时前
继承与多态_继承篇
c++·继承·面向对象·c++进阶
沫璃染墨3 小时前
从零入门计算机网络系列(十):HTTP协议进阶——从资源路径解析到状态码与请求头详解
linux·运维·服务器·网络·c++·http
xiaoye-duck3 小时前
《C++ 工具库实战》高性能日志 spdlog:从基础使用到工程化封装
c++·第三方库
努力努力再努力wz4 小时前
【Lua入门系列】从 C++ 迭代器到 Lua 迭代器:一文深入理解 Lua 有状态以及无状态迭代器与泛型for、闭包、ipairs、pairs 和 next
开发语言·c++·lua
努力实干4 小时前
第二阶段 · 第 7 课:C++ 多线程与线程安全
开发语言·c++·安全
小菜要扣篮5 小时前
Ubuntu 下使用 vscode 打开 C++ 项目后,无法通过 F12 定位到定义问题解决
c++·vscode·ubuntu