五、C++ 新特性、关键字与编译原理(进阶)(一)

五、C++ 新特性、关键字与编译原理(第 1~13 项)

0. 知识点之间的联系

C/C++ 程序构建链路
源代码 .cpp/.h
  │
  ├─ 预处理阶段
  │   ├─ 1. #define 宏
  │   └─ 8. #include <> 与 #include ""
  │
  ├─ 声明、类型与作用域阶段
  │   ├─ 2. typedef:给类型起别名
  │   ├─ 3. register:旧式寄存器存储提示
  │   ├─ 4. const:只读约束
  │   ├─ 5. extern:外部链接声明
  │   ├─ 6. volatile:禁止特定内存访问优化
  │   └─ 10. static:生命周期、作用域与类级共享
  │
  ├─ 编译与运行时类型阶段
  │   └─ 7. dynamic_cast / RTTI
  │
  ├─ C 风格接口与内存操作
  │   └─ 11. sprintf / strcpy / memcpy
  │
  ├─ 现代 C++ 替代与增强
  │   ├─ 9. const 替代 #define
  │   ├─ 12. C++11 新特性
  │   └─ 13. sizeof 与 strlen
  │
  └─ 汇编、链接、可执行文件

这一部分的主线是:预处理指令在正式编译前做文本替换;关键字决定变量的类型、作用域、生命周期、链接属性和编译器优化方式;C++11 用更安全、类型化、面向对象的方式替代大量 C 风格写法。


1. C/C++ 中宏定义的相关知识

核心:宏由预处理器处理,发生在正式语法编译之前,本质是文本替换。宏不具备类型系统,也不遵循普通作用域规则,使用方便但容易产生隐蔽错误。

宏使用 #define 定义,使用 #undef 取消:

#define PI 3.14159
#define MAX_BUFFER_SIZE 1024

#undef PI

1.1 对象式宏

#define BUFFER_SIZE 1024

int buffer[BUFFER_SIZE];

预处理后,代码中的 BUFFER_SIZE 会被直接替换成 1024。注意这是文本替换,不是变量,也不占用运行时对象空间。

1.2 函数式宏

#define MAX(a, b) ((a) > (b) ? (a) : (b))
#define SQUARE(x) ((x) * (x))

宏参数必须充分加括号,否则会因运算符优先级出错:

#define BAD_ADD(a, b) a + b

int x = BAD_ADD(1, 2) * 10;
// 文本展开:1 + 2 * 10 = 21,而不是 30

函数式宏还可能多次求值参数:

#include <iostream>

#define SQUARE(x) ((x) * (x))

int main() {
    int n = 3;
    int y = SQUARE(n++);

    // 展开为 ((n++) * (n++))
    // n 被自增两次,结果和求值顺序都非常危险
}

普通内联函数不会这样:

template <class T>
inline T square(T x) {
    return x * x;
}

1.3 字符串化与标记拼接

# 把宏参数变成字符串,## 拼接标记:

#define TO_STRING(x) #x
#define CONCAT(a, b) a##b

TO_STRING(hello);      // "hello"
int CONCAT(value, 1);  // int value1;

1.4 条件编译

#ifdef DEBUG
    std::cout << "debug mode\n";
#else
    std::cout << "release mode\n";
#endif

#ifndef HEADER_FILE_H
#define HEADER_FILE_H

// 头文件内容

#endif

#ifndef/#define/#endif 是传统头文件保护,防止头文件被重复包含。C++ 也可以使用:

#pragma once

1.5 常见预定义宏

#include <iostream>

int main() {
    std::cout << __FILE__ << '\n'; // 当前文件名
    std::cout << __LINE__ << '\n'; // 当前行号
    std::cout << __DATE__ << '\n'; // 编译日期
    std::cout << __TIME__ << '\n'; // 编译时间
}

宏适合做编译期开关、平台判断、通用调试输出、头文件保护;但常量优先使用 constexpr,短函数优先使用 inline 函数或模板,类型集合优先使用 enum class。宏没有类型检查、没有命名空间、不会进入正常调试符号,调试难度也更高。现代 C++ 的原则是:能用语言特性解决,就不要滥用宏。


2. C/C++ 中 typedef 关键字的相关知识

核心:typedef 用来给已有类型起别名,可以提高可读性、屏蔽平台差异、简化复杂类型声明。它不是创建全新类型,而是给原类型增加一个同义名称。

2.1 基本用法

typedef unsigned long size_type;
typedef int Integer;

size_type len = 100;
Integer x = 10;

预处理后类型仍然是原来的类型,Integer 只是 int 的别名。

2.2 结构体别名

C 语言中定义结构体变量时经常需要写 struct,因此常用 typedef 简化:

typedef struct Point {
    int x;
    int y;
} Point;

Point p;

C++ 中结构体标签本身可以直接作为类型名,但这种写法仍常见于跨 C/C++ 头文件。

2.3 函数指针别名

#include <iostream>

typedef int (*CompareFunc)(int, int);

int ascending(int a, int b) {
    return a - b;
}

void sort_array(int* arr, int n, CompareFunc cmp) {
    for (int i = 0; i < n; ++i) {
        for (int j = i + 1; j < n; ++j) {
            if (cmp(arr[i], arr[j]) > 0) {
                int tmp = arr[i];
                arr[i] = arr[j];
                arr[j] = tmp;
            }
        }
    }
}

如果没有 typedef,函数指针参数会非常难读。

2.4 跨平台类型抽象

#ifdef _WIN32
typedef unsigned __int64 uint64;
#else
typedef unsigned long long uint64;
#endif

标准库中的 std::size_tstd::uint32_t 等类型本质上也承担了平台抽象作用。

2.5 typedef 与 using

C++11 推荐使用 using 写类型别名:

using IntPtr = int*;
using CompareFunc = int (*)(int, int);

普通类型别名中二者等价,但 using 更直观,并且支持别名模板:

template <class T>
using Vec = std::vector<T>;

Vec<int> nums;      // std::vector<int>
Vec<double> scores; // std::vector<double>

typedef 很难直接实现别名模板。

2.6 typedef 与 #define 的区别

typedef int* IntPtr;
#define PTR_INT int*

IntPtr a, b; // a、b 都是 int*
PTR_INT c, d; // 展开为 int* c, d; c 是 int*,d 是 int

区别如下:

对比项typedef#define
处理阶段编译阶段预处理阶段
是否有类型检查没有
是否遵循作用域否,直到 #undef 或文件结束
复杂类型更安全容易被文本展开坑害
模板别名不直接支持不支持
推荐程度C 风格兼容现代 C++ 优先 using

需要注意,typedef 并不创建强类型。例如:

typedef int UserId;
typedef int OrderId;

UserId 和 OrderId 仍可互相赋值,因为它们本质都是 int。如果需要类型安全的强类型 ID,应封装结构体或使用强类型别名库。现代 C++ 代码中,普通别名优先写 using,阅读顺序是“新名字 = 原类型”,比 typedef 更清晰。


3. 介绍一下 register 关键字

核心:register 是 C/C++ 早期的存储类说明符,用来“建议”编译器把频繁使用的变量放入 CPU 寄存器,以减少内存访问。现代编译器的寄存器分配算法远比人工提示强大,因此该关键字基本失去实际意义,现代 C++ 中不再推荐使用。

旧式写法:

#include <stdio.h>

int sum(int n) {
    register int i;
    register int result = 0;

    for (i = 1; i <= n; ++i) {
        result += i;
    }

    return result;
}

在早期编译器优化能力较弱时,程序员可能通过 register 提示循环变量适合放在寄存器中。但现代编译器会基于数据流分析、循环频率、指令调度和目标平台寄存器数量自动决定变量分配,程序员的提示反而可能干扰优化。

3.1 存储位置并非由程序员保证

register 只是建议,不是命令。编译器完全可以忽略它:

程序员:希望 i 在寄存器中
编译器:根据实际情况决定
        ├─ 寄存器足够 → 可能放寄存器
        └─ 寄存器紧张 → 仍放栈上

3.2 早期限制:不能取地址

在 C 的传统规则中,被 register 修饰的变量不能使用 & 取地址,因为寄存器没有内存地址:

register int x = 10;
// int* p = &x; // 传统 C 中错误

C++ 的历史规则略有差异,但总体思想一致:既然你暗示它可能不在内存中,就不应依赖它的地址。

3.3 与 auto、static 的区别

关键字作用生命周期存储位置
auto(C++ 旧含义)自动局部变量作用域内通常栈上
register建议使用寄存器作用域内编译器决定
static 局部变量延长生命周期程序运行期静态存储区
global全局变量程序运行期静态存储区

3.4 现代替代方式

现代代码不需要写 register,直接让编译器优化即可:

long long sum(int n) {
    long long result = 0;

    for (int i = 1; i <= n; ++i) {
        result += i;
    }

    return result;
}

编译时开启优化:

g++ -O2 main.cpp -o main

在 -O2-O3 下,编译器会自动进行循环优化、寄存器分配、内联和向量化。

C++17 起,register 不再作为存储类说明符使用;保留它主要是为了兼容旧代码。现代面试中回答此题应强调三点:第一,它是历史遗留关键字;第二,它只是建议而不是强制;第三,现代编译器优化比人工指定寄存器更可靠,因此不要在新代码中使用。真正影响性能时,应使用性能分析工具找到热点,再优化算法、数据布局、缓存命中和编译选项,而不是盲目添加 register


4. 介绍一下 const 关键字

核心:const 表示“只读约束”,用于保护变量、指针、引用、参数、返回值和成员函数不被意外修改。合理使用 const 可以让编译器帮助检查逻辑错误,提高代码可读性和安全性。

4.1 const 普通变量

const int max_count = 100;

// max_count = 200; // 错误:const 变量初始化后不能修改

const 变量必须初始化,因为之后没有机会再赋值。

4.2 const 与指针

这是最容易混淆的部分:

int x = 10;
int y = 20;

const int* p1 = &x;
// *p1 = 30;  // 错误:不能通过 p1 修改指向的值
p1 = &y;       // 正确:指针本身可以改指向

int* const p2 = &x;
*p2 = 30;      // 正确:可以通过 p2 修改值
// p2 = &y;   // 错误:指针本身不能改指向

const int* const p3 = &x;
// *p3 = 30;   // 错误
// p3 = &y;    // 错误

记忆方法:看 const 位于 * 左边还是右边。

const int* p       → 指向常量的指针,不能改“值”
int* const p       → 常量指针,不能改“指向”
const int* const p → 值和指向都不能改

4.3 const 引用

void print_value(const std::string& s) {
    std::cout << s << '\n';
    // s = "new"; // 错误
}

const T& 可以避免拷贝,同时承诺函数不会修改参数,还能绑定临时对象。

4.4 const 成员函数

#include <iostream>

class Account {
private:
    int balance;

public:
    explicit Account(int b) : balance(b) {}

    int get_balance() const {
        // this 类型近似为 const Account*
        // balance = 0; // 错误
        return balance;
    }

    void deposit(int money) {
        balance += money;
    }
};

const 对象只能调用 const 成员函数:

const Account account(100);
account.get_balance(); // 正确
// account.deposit(10); // 错误

4.5 const 与 constexpr

const 表示运行期或编译期只读;constexpr 强制要求值可在编译期计算:

const int a = get_runtime_value();       // 运行期初始化也可以
constexpr int b = 10 * 10;               // 必须编译期可得
constexpr int square(int x) {
    return x * x;
}

4.6 const 正确性

如果一个函数不会修改对象,就应该尽量使用 const

std::size_t length(const std::vector<int>& v) {
    return v.size();
}

这样既能接收 const 对象,也能防止函数内部意外修改。mutable 是极少数例外,用于锁、缓存等“不影响逻辑状态”的成员。

4.7 顶层 const 与底层 const

const int* const p = &x;

左侧 const int 是底层 const,限制指向值;右侧 const p 是顶层 const,限制指针本身。拷贝时顶层 const 不影响类型匹配,底层 const 不能随意丢掉。

int* q = nullptr;
const int* cp = q; // 正确:权限收窄
// q = cp;         // 错误:不能把只读权限重新放大

总结:const 不是“变量一定放在只读内存”,而是语言层面的修改约束。它的价值在于让接口意图明确、让非法修改提前在编译期暴露,是现代 C++ const correctness 的基础。


5. 介绍一下 external(extern)关键字

核心:C++ 中没有 external 关键字,正确名称是 extern。它用于声明一个变量或函数定义在其他翻译单元中,并告诉链接器在链接阶段解析符号。

一个 C++ 程序通常由多个 .cpp 文件分别编译成目标文件,最后由链接器合并。extern 的作用就是“这里只声明,不在这里定义”。

5.1 变量声明与定义

// global.cpp
int g_count = 10;
// main.cpp
#include <iostream>

extern int g_count; // 声明:g_count 在别处定义

int main() {
    std::cout << g_count << '\n';
}

区别如下:

extern int x; // 声明,不分配内存
int x;        // 定义,分配内存
extern int y = 10; // 带初始化时是定义

如果多个文件都写 int g_count;,可能产生重复定义;如果都只写 extern int g_count; 但没有任何地方定义,链接阶段会报未定义符号错误。

5.2 函数的 extern

函数默认具有外部链接属性,因此:

extern void foo();
void foo();

在大多数情况下含义接近。实际代码中更常见的是把函数声明放在头文件:

// math_utils.h
#pragma once

int add(int a, int b);
// math_utils.cpp
int add(int a, int b) {
    return a + b;
}

5.3 extern const

C++ 中命名空间作用域下的 const 变量默认是内部链接,类似 static。如果想跨文件访问,需要在定义处加 extern

// config.cpp
extern const int model_size = 128;
// main.cpp
extern const int model_size;

更推荐的工程做法是把 inline constexpr 变量放进头文件:

// config.hpp
#pragma once
#include <cstddef>

inline constexpr std::size_t model_size = 128;

5.4 extern “C”

C++ 支持函数重载,编译后会对函数名进行 name mangling,例如 foo(int) 和 foo(double) 生成不同符号。C 编译器不做这种修饰。调用 C 库时需要:

extern "C" {
    int c_library_function(int);
}

如果头文件要同时兼容 C 和 C++:

#ifdef __cplusplus
extern "C" {
#endif

int c_library_function(int);

#ifdef __cplusplus
}
#endif

编译链接流程如下:

a.cpp ──编译──> a.o ┐
                    ├──链接──> 可执行文件
b.cpp ──编译──> b.o ┘
        extern 告诉链接器:符号可能在另一个 .o 中

5.5 常见错误

  • 只有 extern int x;,没有任何 .cpp 中定义 int x;:链接错误 undefined reference。
  • 头文件中定义变量并被多个 .cpp 包含:重复定义。
  • 混淆声明和定义,带初始化的 extern int x = 1; 是定义。
  • C++ 调用 C 库忘记 extern "C":找不到被 C++ 修饰后的符号。

现代工程中,跨文件共享变量应谨慎,因为全局变量会增加耦合。更好的方式是通过接口传参、单例对象、配置类或头文件中的 inline constexpr 常量管理。


6. 介绍一下 volatile 关键字

核心:volatile 告诉编译器:被修饰的内存可能在程序当前控制流之外被改变,因此每次访问都必须真实读取或写入,不能把它缓存到寄存器或直接优化掉。它主要用于内存映射硬件寄存器、信号处理中的特殊变量等底层场景。

先看编译器优化可能做什么:

int flag = 0;

void wait() {
    while (flag == 0) {
        // 如果编译器认为当前线程不会修改 flag,
        // 可能把 flag 读取提到循环外,导致死循环
    }
}

volatile 要求每次循环都重新从内存读取:

volatile int flag = 0;

void wait() {
    while (flag == 0) {
        // 每次都从 flag 对应内存重新加载
    }
}

6.1 内存映射 IO

嵌入式开发中,硬件寄存器可能由硬件改变:

// 示意:某状态寄存器地址
#define UART_STATUS (*(volatile unsigned int*)0x40001000)

void wait_ready() {
    while ((UART_STATUS & 0x01) == 0) {
        // 每次都必须真实读取硬件寄存器
    }
}

如果不加 volatile,编译器可能发现第一次读取后程序没有修改它,就不再读取后续硬件状态。

6.2 volatile 指针写法

volatile int* p;             // 指向 volatile int 的指针
int* volatile p2;            // 指针本身 volatile,较少见
volatile int* volatile p3;   // 值和指针都带 volatile

判断方式与 const 类似。

6.3 volatile 成员函数

class HardwareRegister {
private:
    volatile unsigned int reg;

public:
    unsigned int read() const volatile {
        return reg;
    }
};

6.4 常见误区:volatile 不等于线程同步

很多初学者误以为 volatile 能保证多线程可见性,这是错误的。在 C++ 内存模型中,volatile 不保证原子性,不保证内存顺序,也不建立 happens-before 关系。多线程应该使用 std::atomic

#include <atomic>
#include <thread>

std::atomic<bool> stop_flag{false};

void worker() {
    while (!stop_flag.load()) {
        // 正确:原子变量保证线程间可见性和内存序
    }
}

对比:

场景应使用
硬件寄存器volatile
信号处理中可被异步修改的标志volatile sig_atomic_t
多线程共享标志std::atomic
互斥访问复杂临界区std::mutex
防止普通变量被优化先确认是否真的需要

6.5 volatile 与 const 可以同时存在

只读硬件状态寄存器可能对程序只读,但硬件自身会改变:

const volatile unsigned int* status;
  • const:程序不能写入。
  • volatile:编译器不能假设值不变,必须每次读取。

总结:volatile 的核心不是“线程可见”,而是“禁止编译器省略或重排对这块内存的访问”。在普通业务代码中几乎不需要它;在嵌入式、驱动、内核和信号处理中才常见。现代多线程并发应使用 std::atomic 和互斥锁,而不是用 volatile 模拟线程同步。


7. 介绍一下 dynamic 在什么时候使用

核心:C++ 没有名为 dynamic 的普通关键字,面试通常问的是 dynamic_cast。它用于在继承层次中进行运行时类型转换,最典型场景是把基类指针或引用安全转换为派生类指针或引用,即安全向下转型。

7.1 为什么需要 dynamic_cast

多态中通常通过基类接口操作对象:

class Animal {
public:
    virtual void speak() const = 0;
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void speak() const override;
    void guard() const;
};

如果当前只有 Animal*,但想调用 Dog 独有方法 guard(),就需要判断它是否真的是 Dog

#include <iostream>

void process(const Animal* animal) {
    const Dog* dog = dynamic_cast<const Dog*>(animal);

    if (dog) {
        dog->guard();
    }
}

7.2 指针和引用的失败行为

指针转换失败返回 nullptr

Base* base = new Derived;

auto* derived = dynamic_cast<Derived*>(base);
if (derived) {
    // 类型正确
}

引用没有空引用,转换失败抛出 std::bad_cast

#include <stdexcept>

void process(const Base& base) {
    try {
        const Derived& d = dynamic_cast<const Derived&>(base);
    } catch (const std::bad_cast& e) {
        // 类型不匹配
    }
}

7.3 必须有多态条件

dynamic_cast 依赖运行时类型信息 RTTI,因此基类必须至少有一个虚函数,通常是虚析构函数:

class Base {
public:
    virtual ~Base() = default;
};

class Derived : public Base {};

如果类没有虚函数,向下转型会编译失败,因为运行时没有类型信息可用。

7.4 向上、向下与横向转换

Animal
  ├─ Dog
  └─ Cat
  • 向上转型:Dog* → Animal*,天然安全,可隐式转换,不需要 dynamic_cast
  • 向下转型:Animal* → Dog*,可能失败,需要运行时检查。
  • 横向转换:多重继承中,从一个兄弟基类子对象转到另一个基类子对象,也可借助 dynamic_cast

7.5 与 static_cast 的区别

class Base {
public:
    virtual ~Base() = default;
};

class Derived : public Base {};

Base* p = new Base;

auto* p1 = dynamic_cast<Derived*>(p); // 安全,返回 nullptr
auto* p2 = static_cast<Derived*>(p); // 编译通过,但对象实际不是 Derived,使用后是未定义行为
转换方式检查时机安全性开销
static_cast编译期不检查真实类型几乎无额外开销
dynamic_cast运行期类型不符可发现有 RTTI 查询开销
reinterpret_cast不做类型安全转换危险底层位模式重解释

7.6 什么时候不该用

如果代码中大量出现 dynamic_cast,往往说明基类接口设计不足。更好的做法是把通用行为抽象成虚函数:

class Animal {
public:
    virtual void action() const = 0;
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void action() const override {
        guard();
    }

private:
    void guard() const;
};

调用方只写 animal->action(),不需要判断具体类型,这更符合多态思想。

因此,dynamic_cast 适合少量、确实需要恢复具体类型的场景,例如反序列化、事件对象分发、调试检查、插件接口查询;不适合把它当成弥补抽象设计缺陷的常规手段。使用时还要注意,部分嵌入式或高性能项目会关闭 RTTI,此时 dynamic_cast 和 typeid 可能不可用。


8. #include <file.h> 与 #include "file.h" 的区别

核心:二者都用于把头文件内容在预处理阶段插入当前位置,主要区别是搜索路径和搜索顺序不同。尖括号通常用于系统或标准库头文件,双引号通常用于项目内部头文件。

预处理过程:

main.cpp
  │
  │ #include "student.h"
  ▼
把 student.h 的内容原样插入
  │
  ▼
得到一个更大的翻译单元,再交给编译器

8.1 尖括号形式

#include <iostream>
#include <vector>
#include <string>
#include <cstdint>

尖括号通常告诉预处理器:不要优先在当前源文件目录寻找,而是在系统头文件路径、编译器标准库路径和通过编译选项指定的外部 include 路径中查找。

例如:

g++ main.cpp -I/third_party/include

-I 指定的路径通常也可用于查找尖括号头文件。

8.2 双引号形式

#include "student.h"
#include "../common/config.h"
#include "network/socket_client.h"

双引号通常先在当前文件所在目录或项目本地路径查找;如果找不到,再按尖括号的路径继续查找。标准表述是:双引号形式使用“由实现定义的本地文件搜索规则”,找不到后回退到尖括号形式。

8.3 对比表

写法常见用途搜索特点
#include <file.h>标准库、第三方库、系统头文件优先搜索系统/外部 include 路径
#include "file.h"当前项目自己的头文件优先搜索当前项目本地目录,失败后再走系统路径

8.4 头文件保护

无论哪种形式,都应防止重复包含:

// student.h
#pragma once

class Student {
};

或传统写法:

#ifndef STUDENT_H
#define STUDENT_H

class Student {
};

#endif

如果没有保护,一个头文件被间接包含多次,可能出现类重复定义:

a.h 定义 class A
b.h 包含 a.h
main.cpp 同时包含 a.h 和 b.h
→ class A 出现两次

8.5 C++ 标准头为什么没有 .h

#include <iostream>   // C++ 标准库
#include <stdio.h>    // C 旧头文件
#include <cstdio>     // C++ 对 C 头文件的封装,名称在 std 命名空间

推荐 C++ 代码使用 <cstdio><cstring><cstdint> 这类 C++ 版本。

8.6 路径组织建议

project/
├─ include/
│  └─ mylib/
│     ├─ parser.h
│     └─ token.h
├─ src/
│  ├─ parser.cpp
│  └─ main.cpp

项目内部可以写:

#include "mylib/parser.h"

并在构建系统中配置:

-Iinclude

不要写大量脆弱的相对路径:

#include "../../include/mylib/parser.h" // 可维护性差

总结:二者不是“一个只能包含系统文件、一个只能包含本地文件”的绝对区别,而是搜索顺序和工程约定不同。规范写法是:标准库和外部库用尖括号,项目内部头文件用双引号,并统一 include 根目录,避免混乱的相对路径。


9. const 和 #define 有什么区别?

核心:const 是语言层面的只读变量,有类型、作用域和调试信息;#define 是预处理阶段的文本替换,没有类型检查和作用域概念。现代 C++ 中普通常量优先使用 constexpr,其次使用 const,尽量不用宏定义常量。

9.1 基本对比

#define MAX_SIZE 100
const int max_size = 100;

预处理后,MAX_SIZE 会被直接替换为 100;而 max_size 是一个有类型的只读符号。

9.2 类型检查

#define VALUE 123

const int value = 123;

void print_double(double x);

宏没有类型,编译器只看到替换后的文本;const int 明确知道类型,函数重载、模板推导和类型转换都更可控。

9.3 作用域

class Config {
public:
    static constexpr int timeout = 30;
};

void f() {
    // Config::timeout 有明确作用域
}

宏没有类作用域和命名空间:

#define MAX 100

namespace A {
    // MAX 仍然是全局文本替换
}

宏一旦定义,在后续文本中都会生效,直到 #undef 或文件结束。

9.4 调试体验

#define DEBUG_LEVEL 3
const int debug_level = 3;

宏在预处理阶段被替换,调试器可能看不到 DEBUG_LEVEL 这个符号;const 常量通常能进入符号表,调试时更容易观察。

9.5 宏的副作用

宏不会求值一次,也不遵循函数调用规则:

#define DOUBLE(x) ((x) + (x))

int n = 1;
int y = DOUBLE(n++); // n++ 执行两次

const 常量不存在这个问题:

const int n = 1;
int y = n + n; // n 不会变化

9.6 指针和字符串

const char* name1 = "Alice";
#define NAME2 "Bob"

const char* 可以进一步使用 const std::string 或 std::string_view

#include <string_view>

constexpr std::string_view model_name = "resnet50";

9.7 对比表

维度const / constexpr#define
处理阶段编译阶段预处理阶段
类型检查
作用域遵循命名空间、类、块作用域文本全局生效
调试更容易替换后原名字可能消失
内存const 可能占存储,constexpr 可编译期折叠纯文本替换
安全性容易产生优先级、重复求值问题
现代推荐推荐常量不推荐

9.8 宏仍有价值的场景

宏并非完全无用,以下场景仍常见:

  • 条件编译:#ifdef _WIN32
  • 头文件保护:#ifndef
  • 日志中自动记录 __FILE____LINE__
  • 跨编译器属性封装
  • 确实需要文本拼接或字符串化
#ifdef NDEBUG
#define LOG(x)
#else
#define LOG(x) std::cout << __FILE__ << ":" << __LINE__ << " " << x << '\n'
#endif

但普通数值常量、字符串常量、简单函数,应优先使用 constexprinline 函数和枚举。总结一句:const 是类型系统保护下的只读实体,#define 是绕过类型系统的文本替换;现代 C++ 应把宏限制在条件编译和编译器扩展等真正需要预处理的场景。


10. 介绍一下 static 关键字的作用

核心:static 在 C/C++ 中有五类典型作用:控制局部变量生命周期、限制全局变量或函数的链接范围、声明类共享静态成员变量、声明类静态成员函数,以及 C++11 后保证局部静态变量线程安全初始化。

10.1 静态局部变量:延长生命周期

普通局部变量每次进入函数都会创建,离开作用域销毁:

int next_id() {
    int id = 0;
    return ++id; // 每次都从 1 开始
}

加 static 后只初始化一次,生命周期持续到程序结束:

#include <iostream>

int next_id() {
    static int id = 0;
    return ++id;
}

int main() {
    std::cout << next_id() << '\n'; // 1
    std::cout << next_id() << '\n'; // 2
    std::cout << next_id() << '\n'; // 3
}

10.2 静态全局变量/函数:内部链接

// file1.cpp
static void helper() {} // 只能在当前 .cpp 中使用
static int cache = 0;   // 只能在当前翻译单元访问

这可以避免不同 .cpp 文件中的同名函数或全局变量冲突。现代 C++ 更推荐使用匿名命名空间:

namespace {
    void helper() {}
}

10.3 静态成员变量:所有对象共享

#include <iostream>

class Student {
private:
    inline static int total = 0; // C++17

public:
    Student() { ++total; }
    ~Student() { --total; }

    static int count() {
        return total;
    }
};

所有 Student 对象共享同一份 total,它属于类而不是某个对象。

10.4 静态成员函数

class MathUtil {
public:
    static int square(int x) {
        return x * x;
    }
};

int main() {
    int x = MathUtil::square(5);
}

静态成员函数没有 this 指针,因此:

  • 不能直接访问普通非静态成员。
  • 不能声明为 const
  • 不能是虚函数。
  • 可以直接访问静态成员变量。
class Counter {
private:
    int x = 1;
    static int y;

public:
    static void f() {
        // ++x; // 错误:没有 this,不知道是哪个对象的 x
        ++y;    // 正确
    }
};

10.5 C++11 局部 static 线程安全初始化

class Config {
public:
    static Config& instance() {
        static Config config;
        return config;
    }

private:
    Config() = default;
};

C++11 保证函数内局部静态对象只被初始化一次,并且并发首次进入时初始化过程是线程安全的,这叫 magic statics。

10.6 五种作用总结

场景作用
函数内 static 变量只初始化一次,生命周期延续到程序结束
文件内 static 变量内部链接,只在当前翻译单元可见
static 函数内部链接,只在当前翻译单元可见
类 static 成员变量所有对象共享
类 static 成员函数属于类,没有 this

10.7 生命周期与作用域区别

普通局部变量:
进入作用域创建,离开作用域销毁

静态局部变量:
第一次进入时初始化,程序结束时销毁,只初始化一次

普通全局变量:
外部链接,其他文件可 extern 声明后访问

static 全局变量:
内部链接,只在当前编译文件可见

使用 static 时要注意,静态共享状态在多线程下可能产生数据竞争,需要 std::mutex 或 std::atomic 保护;静态对象之间也不应依赖复杂初始化顺序。合理使用 static 可以实现对象计数、单例、私有辅助函数和持久缓存,但滥用会让函数带有隐式状态,降低可测试性。


11. sprintf、strcpy 及 memcpy 函数有什么区别

核心:sprintf 负责格式化输出到字符串;strcpy 负责复制以 \0 结尾的 C 字符串;memcpy 负责按字节复制指定长度内存。三者处理对象、结束条件、重叠规则和安全性都不同。

函数原型:

int sprintf(char* buffer, const char* format, ...);

char* strcpy(char* dest, const char* src);

void* memcpy(void* dest, const void* src, std::size_t count);

11.1 sprintf:格式化拼接

#include <cstdio>

int main() {
    char buf[64];
    int age = 20;

    sprintf(buf, "age = %d", age);
    // buf 中为 "age = 20\0"
}

sprintf 会按照格式字符串把整数、浮点数、字符串等格式化到目标缓冲区,并自动追加字符串结束符 \0。返回值是写入的字符数,不包括末尾 \0

它的最大问题是无法限制输出长度:

char small[8];
sprintf(small, "this is a very long string"); // 缓冲区溢出

应使用 snprintf

char small[8];
std::snprintf(small, sizeof(small), "%s", "this is a very long string");

snprintf 最多写入 size - 1 个有效字符,并保留 \0

11.2 strcpy:复制 C 字符串

#include <cstring>

int main() {
    const char* src = "hello";
    char dest[16];

    strcpy(dest, src);
}

strcpy 从 src 逐个字符复制,直到遇到 \0,并把 \0 一起复制。它不要求传入长度,因此调用者必须保证目标缓冲区足够大。

src:  h e l l o \0
复制:h e l l o \0

危险示例:

char dest[4];
strcpy(dest, "hello"); // 目标空间不足,溢出

strncpy 可以限制复制数量,但它有反直觉行为:如果源字符串长度达到指定数量,它不会自动补 \0,必须手动保证:

char dest[8];
std::strncpy(dest, "hello", sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';

11.3 memcpy:按字节复制内存

#include <cstring>

struct Data {
    int id;
    double score;
};

int main() {
    int a[5] = {1, 2, 3, 4, 5};
    int b[5];

    std::memcpy(b, a, sizeof(a));

    Data x{1, 90.5};
    Data y;
    std::memcpy(&y, &x, sizeof(Data));
}

memcpy 不关心内容是不是字符串,也不会遇到 \0 停止,而是严格复制第三个参数指定的字节数。

11.4 memcpy 与内存重叠

memcpy 要求源内存和目标内存不能重叠,否则行为未定义。重叠场景使用 memmove

int arr[5] = {1, 2, 3, 4, 5};

// 同一块内存内移动,使用 memmove
std::memmove(arr + 1, arr, 3 * sizeof(int));

11.5 对比表

函数处理对象停止/长度依据是否补 \0是否允许重叠安全建议
sprintf格式化文本格式字符串和参数不建议重叠用 snprintf
strcpyC 字符串遇到源串 \0不允许确保容量或用更安全接口
memcpy任意内存字节显式传入字节数不允许重叠用 memmove

11.6 C++ 推荐方式

普通字符串优先使用 std::string

#include <format> // C++20
#include <string>

std::string s = "age = " + std::to_string(20);

原始内存复制应明确对象生命周期和可平凡复制要求。对于非平凡 C++ 对象,不能简单 memcpy,例如含 std::string、虚函数、智能指针的类,应使用拷贝构造、拷贝赋值或容器算法,否则会绕过构造函数和资源管理,造成严重错误。


12. C++11 的新特性都有哪些?

核心:C++11 是现代 C++ 的分水岭,引入了自动类型推导、移动语义、lambda、智能指针、统一初始化、并发库、范围 for、枚举类、空指针常量等大量特性,使 C++ 从“带类的 C”显著转向更安全、更高层、更具表达力的现代语言。

12.1 auto 类型推导

std::map<std::string, std::vector<int>>::iterator it1 = data.begin();

auto it2 = data.begin(); // 简洁且不易写错类型

auto 不代表动态类型,编译期仍然确定静态类型。

12.2 范围 for 循环

std::vector<int> nums = {1, 2, 3};

for (int x : nums) {
    std::cout << x << ' ';
}

for (auto& x : nums) {
    x *= 2; // 修改元素使用引用
}

12.3 nullptr

void f(int);
void f(int*);

f(0);       // 可能调用 int 版本
f(nullptr); // 明确是空指针

NULL 可能只是整数 0 的宏,nullptr 类型是 std::nullptr_t,重载解析更安全。

12.4 统一初始化与 initializer_list

int x{10};

std::vector<int> nums{1, 2, 3, 4};

class Point {
public:
    int x, y;
};

Point p{1, 2};

花括号初始化还能防止窄化转换:

int value{3.14}; // 编译错误或警告,避免精度丢失

12.5 lambda 表达式

#include <algorithm>
#include <vector>

int main() {
    std::vector<int> nums = {3, 1, 4, 2};

    std::sort(nums.begin(), nums.end(), [](int a, int b) {
        return a > b;
    });

    int base = 10;
    auto add = [base](int x) {
        return base + x;
    };
}

lambda 本质上会生成一个匿名函数对象,捕获方式包括值捕获、引用捕获和隐式捕获。

12.6 右值引用与移动语义

#include <iostream>
#include <string>

class Buffer {
public:
    Buffer() = default;

    Buffer(Buffer&& other) noexcept {
        // 转移资源,而不是深拷贝
    }
};

int main() {
    std::string s1 = "hello";
    std::string s2 = std::move(s1); // s1 资源移动给 s2
}

移动语义减少临时对象深拷贝,是高性能 C++ 的基础。

12.7 智能指针

#include <memory>

void f() {
    auto p = std::make_unique<int>(10); // 独占所有权
    auto q = std::make_shared<int>(20); // 共享所有权
} // 自动释放,避免 delete 泄漏
  • unique_ptr:独占、轻量、不能拷贝但可移动。
  • shared_ptr:引用计数共享。
  • weak_ptr:打破循环引用。

12.8 constexpr

constexpr int square(int x) {
    return x * x;
}

constexpr int v = square(10); // 编译期计算

12.9 override、final、noexcept、default、delete

class Base {
public:
    virtual void f() {}
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    void f() override {} // 编译器检查是否真的覆盖
};

class NonCopyable {
public:
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
};

void safe_func() noexcept {
    // 承诺不抛异常
}

12.10 enum class

enum class Color { Red, Green, Blue };

Color c = Color::Red;

// if (c == 1) {} // 错误:不会隐式转换成整数

它避免传统枚举的名字污染和隐式整数转换。

12.11 并发与标准库增强

#include <thread>
#include <mutex>
#include <future>
#include <chrono>
#include <array>
#include <unordered_map>
#include <tuple>
#include <type_traits>

C++11 第一次在语言标准层面引入线程、互斥锁、条件变量、future、异步任务和统一内存模型;标准库增加 arrayunordered_map/settuplefunctionbind、正则、随机数、chrono 时间库等。

12.12 特性分组图

C++11
├─ 类型表达:auto、decltype、尾置返回类型
├─ 初始化:{}、initializer_list、类成员默认初始化
├─ 函数增强:lambda、constexpr、noexcept、变参模板
├─ 面向对象:override/final/default/delete、委托构造、继承构造
├─ 资源管理:unique_ptr/shared_ptr/weak_ptr、移动语义
├─ 类型安全:nullptr、enum class、static_assert
└─ 并发与标准库:thread、mutex、future、chrono、unordered 容器

面试回答时不必只罗列名词,应说明每类特性解决什么问题:auto 简化复杂类型;移动语义减少不必要拷贝;智能指针解决裸指针资源释放;lambda 让算法回调更自然;并发库让多线程从平台 API 上升到标准层面。


13. sizeof 与 strlen 的区别

核心:sizeof 是编译期运算符,用来求类型或对象占用的字节数;strlen 是 C 标准库函数,用来在运行时统计 C 字符串中第一个 \0 之前的字符个数。前者看“内存大小”,后者看“字符串长度”。

13.1 基本示例

#include <cstring>
#include <iostream>

int main() {
    char s[] = "hello";

    std::cout << sizeof(s) << '\n'; // 6:h e l l o \0
    std::cout << std::strlen(s) << '\n'; // 5:不包含 \0
}

内存结构:

下标:  0 1 2 3 4 5
内容:  h e l l o \0

strlen 数到 \0 前停止:5
sizeof 统计整个数组字节数:6

13.2 对比表

维度sizeofstrlen
本质运算符库函数
求值时间通常编译期运行时
是否需要 \0不需要必须依赖 \0
统计内容类型/对象占用字节数字符串结束前字符数
参数类型或对象const char*
是否包含 \0字符数组包含不包含
对指针得到指针大小从指针地址开始找 \0

13.3 指针陷阱

char s1[] = "hello";
const char* s2 = "hello";

sizeof(s1); // 数组大小,6
sizeof(s2); // 指针大小,64 位平台通常是 8

strlen(s1); // 5
strlen(s2); // 5

数组作为函数参数会退化为指针:

void bad(char s[100]) {
    sizeof(s); // 不是 100,而是 char* 的大小
}

正确做法是额外传入长度:

void good(const char* s, std::size_t size) {
    // size 由调用者传入
}

13.4 没有结束符时 strlen 很危险

char buf[5] = {'h', 'e', 'l', 'l', 'o'}; // 没有 \0
std::strlen(buf); // 继续向后越界寻找 \0,未定义行为

而 sizeof(buf) 不关心 \0,结果固定为 5。

13.5 不同类型上的 sizeof

int arr[10];
sizeof(arr);              // 10 × sizeof(int)
sizeof(arr[0]);           // sizeof(int)
sizeof(arr) / sizeof(arr[0]); // 元素个数:10

struct Node {
    char c;
    int x;
};
sizeof(Node); // 可能大于 5,因为结构体内存对齐

C++ 中推荐更类型安全的数组大小写法:

#include <array>

std::array<int, 10> arr;
arr.size(); // 10,不会发生指针退化

13.6 sizeof 与动态数组

int* p = new int[10];

sizeof(p); // 只是指针大小,不是 40
delete[] p;

new[] 分配后的内存大小不能通过 sizeof 获得,因为 p 已经是指针。使用 std::vector 可以同时保存元素数量和容量:

std::vector<int> v(10);
v.size();     // 10
v.capacity(); // 当前容量

总结:sizeof 问的是“这个类型或对象占多少字节”,编译期基本确定;strlen 问的是“这个以 \0 结尾的字符串有多少个有效字符”,运行时逐个扫描。 对字符数组而言,sizeof 通常包含末尾 \0strlen 一定不包含;对指针而言,sizeof 只会得到指针大小,不能得到它指向内存块的大小。


本部分总体总结

模块核心结论
#define预处理文本替换,灵活但无类型检查,常量优先用 constexpr
typedef类型别名,现代 C++ 优先用 using
register旧式寄存器提示,现代编译器自动优化,基本废弃
const只读约束,是 const correctness 的核心
extern外部链接声明,extern "C" 用于 C/C++ 混合编程
volatile禁止省略内存访问,主要用于硬件寄存器,不等于线程同步
dynamic_cast运行时安全向下转型,依赖虚函数和 RTTI
#include尖括号偏系统/外部库,双引号偏项目本地头文件
const vs defineconst 有类型、作用域和调试信息,宏只是文本替换
static控制生命周期、内部链接和类级共享
字符串/内存函数sprintf 格式化、strcpy 复制字符串、memcpy 复制字节
C++11现代 C++ 分水岭,核心是自动推导、移动语义、lambda、智能指针和并发
sizeof/strlensizeof 看字节大小,strlen 看 \0 前字符数

记忆主线:

预处理:#include / #define
类型声明:typedef / const / static / extern / volatile
运行时识别:dynamic_cast / RTTI
旧式 C 接口:sprintf / strcpy / memcpy / strlen
现代 C++:constexpr、using、lambda、智能指针、移动语义、auto、nullptr、enum class
本数据集收录了1961–2024年西太平洋区域所有台风的完整轨迹气象特征信息,每条台风路径均以经纬度时间序列点形式呈现,伴有对应的中心气压、台风级别(热带低压、热带风暴、强热带风暴、台风、强台风、超级台风)、UTC时间节点以及2分钟平均风速等核心气象参数。原始数据来源于热带气旋资料中心。每年的台风数据放置于个文件夹中,每个Excel表格以台风名称命名。 Excel 数据表结构清晰,由多个字段组成,包括台风编号、名称、时间戳(年-月-日 时:分),位置坐标(经度、纬度),分类等级、中心气压(百帕)、平均风速等,支持按台风名称或编号进行筛选归档,适合海温、气压、风场等大气环境数据进行耦合分析。配套数据格式简洁,使用 Excel 提供标准表格,可无缝导入 Python、R、Matlab、GIS 时序分析系统等工具中进行气候变化趋势研究、台风路径聚类、极端风灾风险评估灾害损失模拟。研究者可基于该数据开展台风强度演化规律分析、登陆频率受影响区域时空变化对比、热带气旋海洋热含量关系研究。同时,该数据亦适用于气象部门、灾害管理机构科研高校进行数值模拟驱动、灾害应对演练、台风灾害地图制作及减灾方案编制,是西太平洋台风灾害风险分析应对策略研究的重要信息资源。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值