文章目录
- 前言
- 1、类中定义的属性和方法默认都是私有的
- 2、只有main函数可以不返回任何值(默认自动返回int类型0)
- 3、函数分文件编写一般有4个步骤
- 4、内联函数(inline)
- 5、include后面接“ ” 还是《》?
- 6、引用在声明的时候必须马上赋值
- 7、类中的成员默认都是私有的,结构体中的成员默认都是公开的
- 8、c++中的全局静态,类中静态,静态局部区别
- 9、extern 的核心用途
- 10、c++枚举用法
- 11、C++继承
- 12、虚函数表
- 13、C++ 虚函数 vs 纯虚函数(简要区别)
- 14、字符串字面量
- 15、const的所有用法
- 16、C++ 中返回引用类型的注意事项
- 17、成员初始化列表
- 18、隐式转化与explicit关键字
- 19、智能指针
- 20、vector中push_back和emplace_back的区别
- 21、静态链接和动态链接库
- 22、tuple元组
- 23、模版template
- 24、宏 #define
- 25、auto用法
- 26、静态数组array
- 27、函数指针
- 28、Lambda表达式
- 29、using用法
- 30、匿名空间namespace
- 31、线程Thread
- 32、`<chrono>` 库简介
- 33、联合体union
- 34、虚析构函数
- 35、类型转换
- 36、预编译头文件
- 37、std::optional
- 38、std::variant
- 39、std::any
- 40、如何让字符串更快
- 41、std::async
- 42、单例模式
- 43、内存分配跟踪实现
- 44、左值和右值以及左值引用右值引用
- 45、参数计算顺序
- 46、移动语义
- 47、花括号 {} 初始化对象与传统初始化的区别
- 48、引用限定符(Reference Qualifiers)
- 49、decltype关键字
- 50、`delete`详解
- 51、类中非静态成员变量初始化注意事项
前言
本文为了方便自学使用,欢迎大家补充
1、类中定义的属性和方法默认都是私有的
2、只有main函数可以不返回任何值(默认自动返回int类型0)
3、函数分文件编写一般有4个步骤
-
创建后缀名为.h的头文件
-
创建后缀名为.cpp的源文件
-
在头文件中写函数的声明
-
在源文件中写函数的定义
//swap.h文件
#include<iostream>
using namespace std;
//实现两个数字交换的函数声明
void swap(int a, int b);
//头文件中一般不写函数定义,因为函数定义会导致链接失败(与编译原理有关),如果一定要写函数定义,需要在函数前面加上static
//swap.cpp文件
#include "swap.h"
void swap(int a, int b)
{
int temp = a;
a = b;
b = temp;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
}
//main函数文件
#include "swap.h"
int main() {
int a = 100;
int b = 200;
swap(a, b);
system("pause");
return 0;
}
4、内联函数(inline)
内联函数 必须在定义的时候 使用关键字inline修饰, 不能在声明的时候使用inline
内联函数:在编译阶段 将内联函数中的函数体 替换函数调用处。避免函数调用时的开销。
宏函数和内联函数的区别
宏函数和内联函数 都会在适当的位置 进行展开 避免函数调用开销。
宏函数在预处理阶段展开
内联函数在编译阶段展开
宏函数的参数没有类型,不能保证参数的完整性。
内联函数的参数有类型 能保证参数的完整性。
宏函数没有作用域的限制,不能作为命名空间、结构体、类的成员
内联函数有作用域的限制,能作为命名空间、结构体、类的成员
5、include后面接“ ” 还是《》?
如果是同一目录就用“ ”,不同目录就用《》
6、引用在声明的时候必须马上赋值
7、类中的成员默认都是私有的,结构体中的成员默认都是公开的
| 特性 | C语言结构体 | C++结构体 |
|---|---|---|
| 默认访问权限 | 无(全部公开) | public |
| 成员函数 | ❌ 不支持 | ✅ 支持 |
| 继承 | ❌ 不支持 | ✅ 支持 |
| 构造函数/析构函数 | ❌ 不支持 | ✅ 支持 |
| 静态成员 | ❌ 不支持 | ✅ 支持 |
与 class 的区别 | 无 class | 仅默认访问权限不同 |
| 模板 | ❌ 不支持 | ✅ 支持 |
| 运算符重载 | ❌ 不支持 | ✅ 支持 |
C++的结构体几乎可以看作是一个默认访问权限为 public 的 class,而C的结构体仅是一个数据集合。
8、c++中的全局静态,类中静态,静态局部区别
| 特性 | 全局静态变量 | 类中静态成员 | 局部静态变量 |
|---|---|---|---|
| 作用域 | 文件内可见 | 类内共享 | 函数/代码块内可见 |
| 生命周期 | 程序生命周期 | 程序生命周期 | 程序生命周期 |
| 内存位置 | 全局数据段 | 全局数据段 | 全局数据段 |
| 初始化 | 程序启动时(默认0) | 类外显式初始化 | 首次调用时初始化 |
| 访问权限 | 文件内自由访问 | 类名或对象访问 | 仅函数内访问 |
| 典型应用 | 文件内共享配置 | 实例计数器、工具函数 | 跨调用状态保持 |
总结
- 全局静态变量:用于文件内数据隔离,避免全局命名冲突。
- 类中静态成员:实现类级别的共享数据和方法,提升封装性。静态方法没有类实例,因此无法访问非静态成员。
- 局部静态变量:延长局部变量的生命周期,实现函数间状态持久化。
9、extern 的核心用途
-
跨文件共享全局变量
• 声明外部变量:在头文件中用extern声明全局变量(如extern int x;),表示该变量在其他文件中已定义,当前文件仅引用。真正的定义需在某个.cpp文件中完成(如int x = 5;)。• 作用扩展:若全局变量定义在文件末尾,需通过
extern提前声明才能在前面的函数中使用。 -
跨文件共享函数
• 声明外部函数:在头文件中用extern声明函数(如extern void func();),函数定义在其他文件实现。适用于多文件协作开发时分离接口与实现。 -
混合 C/C++ 编程(
extern "C")
• 兼容 C 函数:C++ 支持函数重载,编译时会进行名称修饰(Name Mangling),而 C 不会。通过extern "C"包裹 C 函数声明(或包含 C 头文件),强制 C++ 以 C 风格编译函数,避免链接错误。// C++ 调用 C 函数 extern "C" { #include "c_lib.h" void c_function(int); }
10、c++枚举用法
一、传统枚举(C++98/03)
- 基本定义与初始化
• 语法:
enum 枚举名 { 枚举常量1, 枚举常量2, ... };
例如:
enum Weekday { Mon, Tue, Wed, Thu, Fri, Sat, Sun }; // 默认从 0 开始递增。
• 显式赋值:
可以手动指定某些枚举常量的值,后续未赋值的常量按前一个值递增:
enum Color { Red = 1, Green, Blue = 5 }; // Red=1, Green=2, Blue=5。
- 枚举变量声明与赋值
• 声明变量:
Weekday today = Wed; // 正确,只能赋枚举常量。
• 错误示例:
today = 2; // 错误,不能直接赋整数值。
today = (Weekday)2; // 正确,强制类型转换后可赋值。
- 作用域与限制
• 作用域污染:传统枚举的常量直接暴露在外围作用域,可能导致命名冲突:
enum Fruit { Apple, Orange };
enum Car { BMW, Apple }; // 错误,Apple 重复定义。
• 底层类型固定:传统枚举的底层类型由编译器决定,无法手动指定。
二、强类型枚举(C++11 引入的 enum class)
- 定义与访问
• 语法:
enum class 枚举名 { 枚举常量1, 枚举常量2, ... };
例如:
enum class Day { Mon, Tue, Wed }; // 必须通过作用域访问:Day::Mon。
• 显式作用域:常量需通过 枚举名::常量 访问,避免命名冲突:
Day today = Day::Mon; // 正确,作用域隔离。
- 类型安全与底层类型控制
• 禁止隐式转换:强类型枚举的值不能隐式转换为整数,需显式转换:
int value = static_cast<int>(Day::Mon); // 正确。
• 指定底层类型:可手动设置枚举的底层类型(如 int, unsigned char 等):
enum class Color : unsigned char { Red = 1, Green, Blue }; // 底层类型为 unsigned char。
三、枚举的常见操作
- 比较与运算
• 枚举变量支持比较运算(如==,<),但算术运算需谨慎:
if (today == Day::Mon) { ... } // 正确。
today = static_cast<Day>(static_cast<int>(today) + 1); // 递增需转换。
- 输入输出处理
• 输出示例:通过switch将枚举转换为字符串:
void printDay(Day d) {
switch(d) {
case Day::Mon: cout << "Monday"; break;
// ...
}
}。
• 输入限制:无法直接通过 cin 输入枚举变量,需手动映射字符串或数值。
四、典型应用场景
- 状态码与模式标识:
如表示程序状态(Success、Error)、网络请求结果等,提升可读性。 - 配置选项分组:
例如日志级别(Debug、Warning、Error),限制变量的合法取值范围。 - 单例模式与线程安全:
结合局部静态变量实现延迟初始化单例。
五、注意事项
-
枚举常量不可重复赋值:
enum Color { Red = 1, Green = 1 }; // 允许但易引发逻辑错误。 -
禁止非整型常量:
枚举值只能是整数,不能是字符串或浮点数:enum Invalid { Val1 = "A" }; // 错误。 -
优先使用
enum class:
强类型枚举提供更好的类型安全性和作用域隔离,推荐在新代码中使用。
示例代码(传统枚举与强类型枚举对比)
// 传统枚举
enum Fruit { Apple, Orange };
Fruit f = Apple; // 直接访问常量
// 强类型枚举
enum class Car { BMW, Tesla };
Car c = Car::BMW; // 必须通过作用域访问
// 类型转换示例
int fruitCode = Apple; // 正确,传统枚举隐式转换
int carCode = static_cast<int>(c); // 必须显式转换。
通过合理使用枚举,可以显著提升代码的可读性和健壮性。在复杂项目中,优先选择 enum class 以避免潜在问题。
11、C++继承
1. 基本语法
class 派生类 : 继承方式 基类 {
// 派生类新增成员
};
• 继承方式:public(常用)、protected、private(默认 class 是 private,struct 是 public)。
2. 访问控制
| 基类成员访问权限 | public 继承 | protected 继承 | private 继承 |
|---|---|---|---|
public | public | protected | private |
protected | protected | protected | private |
private | 不可访问 | 不可访问 | 不可访问 |
3. 构造函数与析构函数
• 构造顺序:基类 → 派生类成员 → 派生类构造函数。
• 析构顺序:派生类析构 → 派生类成员析构 → 基类析构。
4. 多继承(慎用)
class Derived : public Base1, public Base2 { ... };
• 问题:菱形继承(重复基类成员)→ 用 虚继承 解决:
class Base {};
class Derived1 : virtual public Base {};
class Derived2 : virtual public Base {};
class Final : public Derived1, public Derived2 {}; // 仅一份 Base 成员
5. 关键特性
• 成员隐藏:派生类同名成员会隐藏基类成员(即使参数不同)。
• 对象切片:派生类对象赋值给基类对象时,只保留基类部分。
6. 最佳实践
• 优先用组合(has-a)而非继承(is-a)。
• 避免多继承,改用接口(纯虚类)。
示例:
class Animal {
public:
void eat() { cout << "Eating..." << endl; }
};
class Dog : public Animal {
public:
void bark() { cout << "Barking!" << endl; }
};
int main() {
Dog d;
d.eat(); // 继承自 Animal
d.bark(); // 自己的方法
return 0;
}
12、虚函数表
1. 作用
• 实现运行时多态(动态绑定),允许通过基类指针/引用调用正确的派生类函数。
• 每个含虚函数的类都有一个虚函数表(vtable),存储该类所有虚函数的地址。
• 每个对象内部有一个虚指针(vptr),指向所属类的虚函数表。
2. 核心规则
| 场景 | 说明 |
|---|---|
| 类声明虚函数 | 编译器自动生成虚函数表(vtable)和对象的虚指针(vptr)。 |
| 派生类重写虚函数 | 派生类的虚函数表中,被重写的函数地址更新为派生类版本。 |
| 多继承 | 派生类会包含多个虚表(每个基类一个),对象内部有多个 vptr。 |
| 虚继承 | 虚基类的虚表单独存储,解决菱形继承的重复数据问题。 |
3. 底层原理(示例)
class Base {
public:
virtual void foo() {} // 虚函数
};
class Derived : public Base {
public:
void foo() override {} // 重写虚函数
};
内存布局:
Base 对象: Derived 对象:
+--------+ +--------+
| vptr | ---→ | vptr | ---→
+--------+ +--------+ Derived的vtable:
Base成员变量 Base成员变量 [0]: &Derived::foo
Derived成员变量
• 调用 obj->foo() 时,通过 vptr 找到虚表,再跳转到正确的函数地址。
4. 关键点
- 虚函数调用开销:比普通函数多一次间接寻址(查虚表)。
- 构造函数/析构函数中调用虚函数:不会多态,因为此时
vptr可能未初始化或已销毁。 - 纯虚函数:虚表对应位置为
0(或纯虚函数占位符),使类成为抽象类。
5. 代码验证(GCC/Clang)
#include <iostream>
class Base {
public:
virtual void foo() { std::cout << "Base::foo\n"; }
};
class Derived : public Base {
public:
void foo() override { std::cout << "Derived::foo\n"; }
};
int main() {
Base* obj = new Derived();
obj->foo(); // 输出 "Derived::foo"(通过虚表动态绑定)
delete obj;
return 0;
}
6. 总结
• 虚表(vtable) = 虚函数地址数组,虚指针(vptr) = 指向虚表的指针。
• 多态的本质:通过 vptr 在运行时查虚表,决定调用哪个函数。
• 性能注意:虚函数调用比普通函数稍慢(多一次间接寻址),但灵活性更高。
13、C++ 虚函数 vs 纯虚函数(简要区别)
| 特性 | 虚函数(virtual) | 纯虚函数(= 0) |
|---|---|---|
| 定义方式 | virtual void func(); | virtual void func() = 0; |
| 是否必须实现 | 基类可以提供默认实现,派生类可选择是否重写 | 基类不提供实现,派生类必须重写 |
| 用途 | 支持多态,允许派生类覆盖基类行为 | 定义接口规范,强制派生类实现特定功能 |
| 能否实例化 | 可以创建基类对象 | 不能实例化,基类变为抽象类 |
| 典型场景 | 基类提供通用逻辑,派生类微调行为 | 设计模式(如策略模式)、接口类(如 Shape) |
14、字符串字面量
在C++中,字符串字面量(string literal)是由双引号 "" 包围的字符序列,用于表示常量字符串。以下是关于C++字符串字面量的关键点:
-
基本语法
const char* str = "Hello, World!"; // 字符串字面量• 类型为
const char[N](N为字符数+1,含结尾的空字符\0)。• 实际存储于程序的只读内存段(如
.rodata),不可修改。 -
特性
• 自动添加空字符:编译器会在末尾添加\0,因此长度比显式字符数多1。"ABC" // 实际存储为 'A','B','C','\0'(占4字节)• 支持转义字符:如
\n(换行)、\t(制表符)、\"(嵌入双引号)等。"Line1\nLine2" // 包含换行的字符串 -
原始字符串字面量(C++11起)
用R"(...)"避免转义,保留原始格式:const char* path = R"(C:\Files\test.txt)"; // 无需转义反斜杠 const char* multiline = R"( Line 1 Line 2 )"; // 保留换行和缩进 -
编码前缀(C++11起)
•u8:UTF-8(如u8"中文")。•
L:宽字符(wchar_t,如L"宽字符")。•
u16/u32:UTF-16/UTF-32(char16_t/char32_t)。 -
与字符字面量的区别
• 字符字面量用单引号'',类型为char(如'A')。• 字符串字面量是字符数组,含结尾的
\0。
示例代码
#include <iostream>
int main() {
const char* s1 = "Hello"; // 传统字符串
const wchar_t* s2 = L"宽字符"; // 宽字符串
const char* s3 = u8"UTF-8"; // UTF-8编码
const char* raw = R"(Raw \n String)"; // 原始字符串(不转义\n)
std::cout << s1 << std::endl; // 输出: Hello
std::cout << raw << std::endl; // 输出: Raw \n String
return 0;
}
注意事项
• 修改字符串字面量是未定义行为(可能导致崩溃)。
• 需要可修改的字符串时,应使用std::string或字符数组。
总结:C++字符串字面量是常量、不可修改的字符数组,支持多种编码和原始字符串格式,是处理文本的基础工具。
15、const的所有用法
在C++中,const 是一个关键字,用于定义常量或限制变量、指针、引用、成员函数等的修改行为。以下是 const 的主要用法及其含义:
1. 定义常量变量
const 可以用于定义不可修改的变量(常量):
const int MAX_SIZE = 100; // MAX_SIZE 是常量,不能修改
// MAX_SIZE = 200; // 错误!不能修改 const 变量
2. 指针与 const
const 可以修饰指针本身或指针指向的数据,形成不同的组合:
(1) const int *p(指向常量的指针)
• 指针可以改变指向,但 不能通过 p 修改指向的数据:
const int *p;
int x = 10;
p = &x; // 合法
// *p = 20; // 错误!不能修改 x
(2) int *const p(常量指针)
• 指针不能改变指向,但 可以修改指向的数据:
int y = 30;
int *const p = &y; // 必须初始化
*p = 40; // 合法
// p = &x; // 错误!不能改变 p 的指向
(3) const int *const p(指向常量的常量指针)
• 指针不能改变指向,且 不能修改指向的数据:
const int z = 50;
const int *const p = &z; // 必须初始化
// *p = 60; // 错误!
// p = &x; // 错误!
3. 引用与 const
const 可以用于修饰引用,使其成为 常量引用(不能通过引用修改原变量):
int a = 10;
const int &ref = a; // ref 是 a 的常量引用
// ref = 20; // 错误!不能通过 ref 修改 a
a = 20; // 合法,原变量仍可修改
用途:
• 防止函数修改传入的参数:
void print(const std::string &s); // 保证不修改 s
4. const 成员函数
在类中,const 可以修饰成员函数,表示 该函数不会修改类的成员变量:
class MyClass {
public:
int value;
void modify() { value = 10; } // 可以修改成员
void read() const { /* value = 20; */ } // 错误!不能修改成员
};
规则:
• const 成员函数只能调用其他 const 成员函数。
• 非 const 对象可以调用 const 和非 const 成员函数。
• const 对象只能调用 const 成员函数。
5. const 与函数返回值
const 可以用于修饰函数返回值,防止返回值被修改:
const int getValue() { return 42; }
// getValue() = 100; // 错误!不能修改返回值
用途:
• 防止返回的引用或指针被意外修改:
const int& getRef() { return someVar; }
6. constexpr(C++11 起)
constexpr 表示 编译时常量,比 const 更严格:
constexpr int SIZE = 100; // 编译时确定
constexpr int square(int x) { return x * x; } // 编译时计算
int arr[square(5)]; // 合法,数组大小在编译时确定
与 const 的区别:
• const 变量可以在运行时初始化。
• constexpr 必须在编译时确定值。
7. mutable 突破 const 限制
在类中,mutable 修饰的成员变量可以在 const 成员函数中被修改:
class Cache {
mutable int cachedValue; // 即使 const 函数也能修改
public:
int getValue() const {
cachedValue = compute(); // 合法
return cachedValue;
}
};
总结
| 用法 | 含义 | 示例 |
|---|---|---|
const 变量 | 定义不可修改的变量 | const int x = 10; |
const 指针 | 控制指针或数据的修改 | const int *p; / int *const p; |
const 引用 | 防止通过引用修改原变量 | const int &ref = x; |
const 成员函数 | 保证不修改类成员 | void func() const; |
const 返回值 | 防止返回值被修改 | const int getValue(); |
constexpr | 编译时常量 | constexpr int SIZE = 100; |
mutable | 允许 const 函数修改特定成员 | mutable int cache; |
const 的正确使用可以提高代码的安全性、可读性和优化潜力。
16、C++ 中返回引用类型的注意事项
以下是关键注意事项:
- 生命周期管理(最重要)
绝对不要返回局部变量的引用:
int& badFunction() {
int x = 10; // 局部变量
return x; // 错误!x 将在函数结束时销毁
} // 返回的是悬垂引用(dangling reference)
安全做法:
• 返回成员变量的引用(确保对象生命周期足够长)
• 返回静态变量的引用
• 返回参数传入的引用
- 避免意外修改
当不希望调用者修改返回值时,返回const引用:
const std::string& getReadOnlyName() const {
return name_; // 调用者只能读取不能修改
}
- 类设计注意事项
• 成员函数返回成员变量的引用时,应考虑是否破坏封装性
• 对于容器的访问函数,应同时提供 const 和非 const 版本:
const T& get() const { return data_; } // const 版本
T& get() { return data_; } // 非 const 版本
在 C++ 中,当同时提供 const 和非 const 版本的重载成员函数时,编译器会根据调用对象的 const 性质 自动选择最匹配的版本:
调用规则:
- 当通过 const 对象调用时 → 调用
const T& get() const版本 - 当通过非 const 对象调用时 → 调用
T& get()版本 - 当通过 const 引用/指针调用时 → 调用 const 版本
- 当通过非 const 引用/指针调用时 → 调用非 const 版本
示例代码:
class Container {
int data_ = 42;
public:
// 两个重载版本
const int& get() const {
std::cout << "const version\n";
return data_;
}
int& get() {
std::cout << "non-const version\n";
return data_;
}
};
int main() {
Container c; // 非 const 对象
const Container& cr = c; // const 引用
c.get(); // 输出 "non-const version" (调用非 const 版本)
cr.get(); // 输出 "const version" (调用 const 版本)
// 链式调用示例
c.get() = 100; // 合法,因为返回的是非 const 引用
// cr.get() = 200; // 错误!返回的是 const 引用,不能修改
}
关键点:
-
const 正确性:const 对象只能调用 const 成员函数
-
自动选择机制:编译器根据对象的 const 性质自动选择
-
典型应用场景:
• 允许 const 对象安全读取数据• 允许非 const 对象修改数据
-
STL 中的范例:标准容器(如
std::vector)的operator[]也采用这种模式
这种设计模式既保证了 const 对象的安全性,又为非 const 对象提供了修改数据的灵活性,是 C++ const 正确性的经典实现方式。
- 运算符重载的特殊情况
• 赋值运算符 (=) 通常返回 *this 的引用:
MyClass& operator=(const MyClass& other) {
// 实现赋值
return *this;
}
• 下标运算符 ([]) 通常返回元素的引用
17、成员初始化列表
✅ 优点
-
效率更高
• 直接初始化成员变量,避免先默认构造再赋值的额外开销(尤其对类类型成员)。• 例如:
class A { std::string name; public: A(const std::string &n) : name(n) {} // 直接构造(高效) // 对比:A(const std::string &n) { name = n; } // 先默认构造,再赋值(低效) }; -
必须用于某些情况
• const 成员:必须在初始化列表赋值,不能后续修改。• 引用成员:必须在初始化列表绑定。
• 无默认构造函数的类成员:必须显式初始化。
• 基类构造:派生类必须在初始化列表中调用基类构造函数。
-
初始化顺序可控
• 成员变量的初始化顺序由类定义中的声明顺序决定(与初始化列表顺序无关),避免依赖问题。 -
避免未定义行为
• 确保所有成员在构造函数体执行前已被正确初始化。
❌ 缺点
-
语法稍复杂
• 初始化列表的语法可能对新手不够直观,尤其是涉及多个成员或继承时。 -
调试不便
• 初始化列表中的操作在构造函数体执行前完成,若初始化出错(如抛出异常),调试信息可能较少。 -
不能使用运行时计算的值(直接)
• 初始化列表中的表达式必须是编译时可确定的(或构造函数参数),无法直接使用构造函数体内的计算结果。• 例如:
class B { int x, y; public: B(int a) { y = a * 2; // 需要先计算 x = y + 1; // 无法直接用 y 初始化 x } }; -
可能隐藏初始化顺序问题
• 若初始化列表顺序与成员声明顺序不一致,可能导致依赖错误(编译器可能警告)。
总结
• 优先使用初始化列表:提高效率,支持 const/引用成员,符合 C++ 最佳实践。
• 构造函数体内赋值:仅当需要依赖运行时计算或逻辑控制时使用。
例如:
class Example {
const int id;
std::string name;
int &ref;
public:
Example(int i, std::string n, int &r)
: id(i), name(std::move(n)), ref(r) {} // 必须用初始化列表
};
18、隐式转化与explicit关键字
1. 隐式转换(Implicit Conversion)
在 C++ 中,如果某个类定义了单参数构造函数(或可以通过默认参数变成单参数的构造函数),编译器可能会在需要时自动调用该构造函数进行隐式转换。
示例:
class MyInt {
public:
MyInt(int x) : value(x) {} // 单参数构造函数
int getValue() const { return value; }
private:
int value;
};
void printInt(const MyInt& num) {
std::cout << num.getValue() << std::endl;
}
int main() {
printInt(42); // 隐式调用 MyInt(42)
return 0;
}
这里 printInt(42) 会自动调用 MyInt(int) 构造函数,将 42 转换为 MyInt 类型。
2. explicit 关键字的作用
为了避免隐式转换带来的潜在歧义,C++ 提供了 explicit 关键字,用于禁止隐式转换,要求必须显式调用构造函数。
修改后的示例:
class MyInt {
public:
explicit MyInt(int x) : value(x) {} // 禁止隐式转换
int getValue() const { return value; }
private:
int value;
};
void printInt(const MyInt& num) {
std::cout << num.getValue() << std::endl;
}
int main() {
// printInt(42); // 错误!不能隐式转换
printInt(MyInt(42)); // 必须显式构造
return 0;
}
此时 printInt(42) 会编译失败,必须显式调用 MyInt(42)。
3. 适用场景
• 推荐使用 explicit:
• 单参数构造函数(尤其是涉及资源管理或可能引起歧义的类,如 std::string(const char*))。
• 避免意外的类型转换导致逻辑错误。
• 允许隐式转换:
• 设计上确实需要自动转换(如 std::string 从 const char* 构造)。
4. 总结
| 特性 | 隐式转换 | explicit |
|---|---|---|
| 行为 | 自动调用单参数构造函数 | 禁止隐式转换,必须显式构造 |
| 适用场景 | 需要自动类型转换 | 避免意外转换,提高代码安全性 |
| 示例 | printInt(42) | printInt(MyInt(42)) |
最佳实践:除非有明确需求,否则建议将单参数构造函数声明为 explicit,以避免潜在的隐式转换问题。
19、智能指针
在C++中,智能指针是用于自动管理动态内存的模板类,通过RAII(资源获取即初始化)机制帮助避免内存泄漏。以下是常见的智能指针及其核心特性:
1.std::unique_ptr
• 所有权:独占资源所有权,不可复制(移动语义)。
• 用途:替代裸指针,明确表达唯一所有权。
• 特点:
- 轻量级,几乎无额外开销。
- 可通过
std::move转移所有权。 - 支持自定义删除器(如
unique_ptr<FILE, decltype(&fclose)>)。
• 示例:
auto ptr = std::make_unique<int>(42); // C++14推荐
2.std::shared_ptr
• 所有权:共享所有权,基于引用计数。
• 用途:多个对象共享同一资源时使用。
• 特点:
- 引用计数为零时自动释放内存。
- 支持自定义删除器。
- 注意循环引用问题(需结合
weak_ptr解决)。
• 示例:
auto ptr = std::make_shared<int>(42); // 引用计数为1
auto ptr2 = ptr; // 引用计数+1
3.std::weak_ptr
• 所有权:不增加引用计数,解决循环引用。
• 用途:观察 shared_ptr 管理的资源,避免强引用。
• 特点:
- 需通过
lock()获取临时shared_ptr访问资源。
• 示例:
std::weak_ptr<int> weak = ptr;
if (auto temp = weak.lock()) { /* 使用temp */ }
4.std::auto_ptr(已废弃)
• 问题:C++98中引入,因所有权转移语义不明确(复制时隐式转移)被弃用,C++17中移除。
关键点总结
• 优先使用 make_unique/make_shared:避免显式 new,提升异常安全性。
• 所有权选择:
- 唯一所有权 →
unique_ptr。 - 共享所有权 →
shared_ptr+weak_ptr。
• 性能:unique_ptr ≈ 裸指针,shared_ptr 有计数开销。
智能指针显著简化了内存管理,是现代C++避免手动 delete 的核心工具。
20、vector中push_back和emplace_back的区别
在C++中,std::vector 的 push_back 和 emplace_back 都用于向容器尾部添加元素,但它们在实现方式和性能上有重要区别:
1. 基本区别
| 方法 | 参数传递方式 | 底层行为 | 适用场景 |
|---|---|---|---|
push_back | 接受已构造的对象(拷贝或移动) | 调用拷贝/移动构造函数插入容器 | 已有对象需要插入时 |
emplace_back | 接受构造参数(直接构造) | 在容器内存中原地构造对象 | 直接传递构造参数效率更高时 |
2. 关键差异
(1) 构造方式
• push_back:
• 需要先构造一个临时对象,再拷贝或移动到容器中。
• 可能引发额外的拷贝/移动开销(尤其在对象不可移动时)。
std::vector<std::string> vec;
vec.push_back("Hello"); // 先构造临时string,再移动到容器
• emplace_back:
• 直接在容器内存中构造对象,避免临时对象的创建和拷贝。
• 通过完美转发(perfect forwarding) 传递参数给构造函数。
vec.emplace_back("Hello"); // 直接调用string(const char*)构造
(2) 性能差异
• 对于复杂对象(如std::string、自定义类),emplace_back 通常更高效(省去临时对象开销)。
• 对于简单类型(如int、double),两者性能几乎无差别。
(3) 语法灵活性
• emplace_back 可以直接匹配对象的构造函数参数:
std::vector<std::pair<int, std::string>> vec;
vec.emplace_back(1, "Alice"); // 直接构造pair(1, "Alice")
// 等效于 vec.push_back(std::make_pair(1, "Alice"));
3. 使用建议
• 优先使用 emplace_back:
当直接传递构造参数时(尤其是复杂对象),默认选择 emplace_back 以提升性能。
• 使用 push_back:
当已有对象需要插入时(如从另一个容器迁移数据),或代码可读性更重要时。
4. 示例代码
#include <vector>
#include <string>
int main() {
std::vector<std::string> vec;
// push_back: 先构造临时string,再移动
vec.push_back("Hello"); // 可能触发移动构造函数
// emplace_back: 直接构造
vec.emplace_back("World"); // 无临时对象,直接调用string(const char*)
// 复杂对象示例
std::vector<std::pair<int, std::string>> data;
data.emplace_back(1, "Alice"); // 直接构造pair
data.push_back({2, "Bob"}); // 构造临时pair再移动
return 0;
}
5. 注意事项
• 对象构造顺序:
emplace_back 的参数需严格匹配构造函数,否则可能编译失败或引发歧义。
• 异常安全:
如果构造过程中抛出异常,emplace_back 可能比 push_back 更难保证容器状态(因原地构造的中间状态)。
在C++11及以后的版本中,emplace_back 是更现代的插入方式,合理使用可减少不必要的拷贝/移动操作。
21、静态链接和动态链接库
1. 静态链接库(Static Library)
特点
• 编译时链接:库代码直接嵌入到最终的可执行文件(.exe 或二进制文件)。
• 文件扩展名:
• Windows: .lib(静态库)
• Linux/macOS: .a(Archive)
• 优点:
• 独立运行:无需额外依赖库文件。
• 性能略高(无运行时加载开销)。
• 缺点:
• 体积较大(每个程序都包含库的副本)。
• 更新困难(修改库后需重新编译程序)。
使用方式(示例)
// 静态库 libmath.lib(Windows)或 libmath.a(Linux)
int add(int a, int b); // 声明
// 编译时链接静态库(g++)
g++ main.cpp -o app -L. -lmath # Linux
cl main.cpp /link math.lib # Windows (MSVC)
2. 动态链接库(Dynamic Library)
特点
• 运行时链接:程序运行时才加载库(.dll / .so)。
• 文件扩展名:
• Windows: .dll(Dynamic Link Library)
• Linux: .so(Shared Object)
• macOS: .dylib
• 优点:
• 节省内存(多个程序共享同一库)。
• 更新方便(替换 .dll / .so 即可升级)。
• 缺点:
• 依赖管理(需确保目标机器有正确的库版本)。
• 轻微性能开销(运行时加载)。
使用方式(示例)
// 动态库 math.dll(Windows)或 libmath.so(Linux)
#ifdef _WIN32
#define EXPORT __declspec(dllexport) // Windows 导出符号
#else
#define EXPORT __attribute__((visibility("default"))) // Linux/macOS
#endif
EXPORT int add(int a, int b); // 导出函数
// 编译动态库(g++)
g++ -shared -fPIC math.cpp -o libmath.so # Linux
cl /LD math.cpp /link /OUT:math.dll # Windows (MSVC)
// 运行时加载(Linux)
#include <dlfcn.h>
void* handle = dlopen("./libmath.so", RTLD_LAZY);
auto add = (int(*)(int,int)) dlsym(handle, "add");
3. 关键对比
| 特性 | 静态链接库(.lib / .a) | 动态链接库(.dll / .so) |
|---|---|---|
| 链接时机 | 编译时 | 运行时 |
| 文件大小 | 较大(库代码嵌入) | 较小(仅引用) |
| 内存占用 | 高(多份副本) | 低(共享库) |
| 更新方式 | 需重新编译 | 替换库文件即可 |
| 依赖管理 | 无依赖 | 需确保库存在 |
4. 如何选择?
• 静态链接:适合小型工具、嵌入式系统或需要独立部署的场景。
• 动态链接:适合大型软件、插件系统或需要热更新的场景(如游戏模组)。
在 C++ 开发中,通常结合使用两者,例如:
• 核心逻辑用静态库保证性能。
• 插件或可扩展模块用动态库方便更新。
22、tuple元组
C++中的tuple(元组)是标准模板库(STL)中的一种容器,用于将多个不同类型的值组合成一个固定大小的有序集合。以下是其核心特性与用法的总结:
1. 基本概念
• 异构性:tuple可以存储任意数量、不同类型的元素,例如int、double、string等。
• 有序性:元素按声明顺序排列,通过索引(从0开始)访问。
• 替代结构体:无需预定义结构体即可打包多类型数据,适用于需要临时组合数据的场景。
2. 创建与初始化
• 直接初始化:显式指定元素类型和值,如std::tuple<int, string> t(10, "Hello");。
• make_tuple:自动推导类型,简化创建,如auto t = std::make_tuple(1, 3.14, "world");。
• 结构化绑定(C++17):直接解包到变量,如auto [a, b, c] = t;。
3. 元素访问与修改
• std::get<index>:通过索引获取元素,如int x = std::get<0>(t);。
• 引用修改:可通过std::get修改元素值,如std::get<0>(t) = 20;。
• std::tie解包:将元素绑定到变量,如std::tie(a, b, c) = t;。
4. 常用操作
• 比较运算符:支持==、<等操作符,按元素顺序逐项比较。
• 合并与拆分:
• 使用std::tuple_cat合并多个tuple。
• 通过遍历索引将元素存入其他容器(如vector)。
• 类型与大小查询:
• std::tuple_size获取元素数量。
• std::tuple_element获取指定位置的类型。
5. 应用场景
• 函数返回多值:替代结构体,直接返回多个不同类型的计算结果。
• 泛型编程:用于模板元编程,处理可变参数和类型推导。
• 临时数据组合:存储如坐标、配置参数等异构数据,避免定义冗余结构。
6. 注意事项
• 固定大小:元素数量在编译时确定,无法动态增删。
• 不可直接迭代:需通过索引或递归模板访问元素。
• 性能优化:适用于轻量级数据组合,复杂场景可能需要自定义结构体。
示例代码
#include <tuple>
#include <iostream>
int main() {
// 创建并初始化
auto t = std::make_tuple(42, "Hello", 3.14);
// 访问元素
std::cout << std::get<0>(t) << std::endl; // 输出42
// 结构化绑定(C++17)
auto [num, str, dbl] = t;
std::cout << str << std::endl; // 输出Hello
return 0;
}
通过灵活使用tuple,开发者可以简化代码逻辑,尤其在需要临时组合多类型数据或减少结构体定义时,其优势显著。
23、模版template
1. 基本概念
• 泛型编程:通过模板编写通用代码,适用于多种数据类型,避免针对每种类型重复实现相同逻辑。
• 分类:
• 函数模板:定义通用函数,如 max、add,根据传入参数类型自动生成实例。
• 类模板:定义通用类,如 vector、Box,通过显式指定类型实例化。
• 核心关键字:template 声明模板,typename 或 class 标识类型参数(两者等价)。
2. 函数模板
• 语法:
template <typename T>
T add(T a, T b) { return a + b; }
编译器根据调用时的实参类型自动推导 T(如 add(3, 5) 生成 int 版本)。
• 特化与重载:
• 完全特化:为特定类型提供定制实现(如字符串处理)。
• 多参数模板:支持多个类型参数(如 template <typename T1, typename T2>)。
• 隐式 vs 显式实例化:可通过 add<int>(3, 5) 显式指定类型。
3. 类模板
• 语法:
template <typename T>
class Box {
private:
T value;
public:
Box(T val) : value(val) {}
};
使用时需显式指定类型(如 Box<int> intBox(10))。
• 非类型模板参数:允许传递常量值(如数组大小 int N)。
template <typename T, int N>
class Array { T data[N]; };
• 特化:支持完全特化(如针对 int 的优化实现)和偏特化(部分参数特化)。
4. 其他特性
• 默认模板参数:类模板可设置默认类型(如 template <typename T = int>)。
• 类型安全与性能:编译器在实例化时进行类型检查,生成针对类型的优化代码,无运行时开销。
• 限制:
• 模板定义需在全局或命名空间作用域,不可在函数内局部定义。
• 非类型参数仅支持整型、指针、引用等。
5. 应用场景
• 通用算法:如排序、查找(如 std::sort)。
• 容器类:如 vector、map,支持任意元素类型。
• 数学运算库:处理多类型数值计算(如矩阵运算)。
• 元编程:编译时计算与类型推导(如 std::tuple)。
6. 示例与注意事项
• 示例代码:
// 函数模板
auto max_val = max(3, 5); // T推导为int
auto concat = add("Hello", "World"); // 特化版本处理字符串
// 类模板
Array<double, 5> arr; // 非类型参数N=5
• 注意事项:
• 避免过度使用导致代码膨胀(多个实例化版本增加二进制大小)。
• 优先使用标准库模板(如STL容器)而非重复造轮子。
通过合理使用模板,开发者可显著提升代码复用性和灵活性,同时保持类型安全和高性能。
24、宏 #define
1. 基本概念
• 定义方式:使用 #define 指令,将标识符(宏名)与替换文本绑定,例如 #define PI 3.14。
• 工作原理:预处理器在编译前将代码中所有宏名替换为对应的文本或表达式,例如 AREA(r) 替换为 PI * r * r。
• 分类:
• 无参宏:定义常量或简单表达式,如 #define MAX_SIZE 100。
• 带参宏:类似函数,支持参数化替换,如 #define SQUARE(x) ((x)*(x))。
• 条件编译宏:通过 #ifdef、#ifndef 控制代码是否编译,常用于跨平台或调试。
2. 创建与使用
• 定义常量:
#define PI 3.14159 // 替换代码中的PI为数值
• 带参数宏:
#define MAX(a, b) ((a) > (b) ? (a) : (b)) // 条件判断宏
• 宏函数:
#define SWAP(a, b, type) { type tmp = a; a = b; b = tmp; } // 交换变量
• 条件编译:
#ifdef DEBUG
std::cout << "调试信息" << std::endl; // 仅在DEBUG定义时编译
#endif
| 指令 | 功能描述 | 应用场景示例 |
|---|---|---|
#ifdef / #if defined | 若指定宏已定义,则编译后续代码块 | 检查调试模式:#ifdef DEBUG 输出调试信息。 |
#ifndef / #if !defined | 若指定宏未定义,则编译后续代码块 | 防止头文件重复包含:#ifndef HEADER_H → #define HEADER_H → 头文件内容。 |
#if | 根据常量表达式结果(非零为真)编译代码 | 跨平台适配:#if defined(_WIN32) 编译Windows专用代码。 |
#elif | 若前序条件不满足且当前表达式为真,则编译当前代码块 | 多条件分支选择:#elif (VERSION > 2) 处理高版本逻辑。 |
#else | 若所有前序条件均不满足,则编译该代码块 | 默认逻辑处理:如未定义平台宏则报错或启用通用代码。 |
#endif | 结束当前条件编译块 | 所有条件编译指令必须以此结尾,否则会引发编译错误。 |
3. 优缺点分析
• 优点:
• 代码复用:简化重复逻辑,如数学运算或调试输出。
• 性能优化:避免函数调用开销(如内联展开)。
• 跨平台支持:通过条件编译适配不同环境。
• 缺点:
• 类型不安全:无类型检查,易导致隐式错误,如 SQUARE(1+2) 展开为 (1+2 * 1+2)。
• 调试困难:宏展开后代码不可见,增加调试复杂度。
• 命名冲突:全局作用域易引发宏名污染。
4. 替代方案
• const常量:类型安全,替代无参宏,如 constexpr double PI = 3.14;。
• 内联函数(inline):提供类型检查,替代带参宏,如 inline int max(int a, int b)。
• 模板:支持泛型编程,避免宏的副作用,如 template<typename T> T square(T x)。
5. 注意事项
• 括号必要性:带参宏的参数需用括号包裹,避免运算符优先级问题,如 #define MUL(a, b) ((a)*(b))。
• 避免副作用:宏参数避免多次求值,如 #define INC(x) (x++) 可能导致多次自增。
• 作用域管理:通过 #undef 及时取消宏定义,减少命名冲突。
示例代码
#include <iostream>
#define DEBUG // 启用调试模式
#define AREA(r) (3.14 * (r) * (r)) // 计算圆面积
int main() {
int radius = 5;
#ifdef DEBUG
std::cout << "半径:" << radius << std::endl; // 条件编译输出
#endif
std::cout << "面积:" << AREA(radius) << std::endl; // 宏展开为3.14 * 5 * 5
return 0;
}
总结
C++宏是强大的预处理工具,但需谨慎使用。优先考虑类型安全的替代方案(如 const、模板),仅在需要条件编译或性能优化时合理使用宏。理解其原理与局限,可避免潜在错误并提升代码质量。
25、auto用法
一、基本用法
-
变量声明
auto可直接用于变量声明,通过初始化表达式推导类型。例如:auto x = 42; // 推导为int auto y = 3.14; // 推导为double auto s = "Hello"; // 推导为const char*需注意:必须显式初始化变量,否则编译报错。
-
容器迭代
简化迭代器声明,避免冗长的类型书写:std::vector<int> vec = {1, 2, 3}; for (auto it = vec.begin(); it != vec.end(); ++it) { /*...*/ } // 推导为std::vector<int>::iterator
二、高级用法
3.复杂类型推导
在处理模板、嵌套容器或映射时,auto能自动推导复杂类型:
std::map<std::string, int> myMap = {{"apple", 5}};
for (auto&& pair : myMap) { // 推导为std::pair<const std::string, int>
std::cout << pair.first << ": " << pair.second;
}
4.函数返回类型推导(C++14+)
允许函数返回类型通过auto自动推断:
auto add(int a, int b) { return a + b; } // 返回类型为int
5.结构化绑定(C++17+)
结合结构化绑定解构元组或容器元素:
std::tuple<int, double> t(42, 3.14);
auto [x, y] = t; // x推导为int,y推导为double
三、注意事项
类型推导规则
• auto会忽略引用和cv限定符(如const、volatile)。例如:
const int a = 10;
auto b = a; // b类型为int,而非const int
• 需显式指定引用或常量性时,应结合const/&使用:
const auto& c = a; // 保持常量性和引用
限制场景
• 不能用于函数参数、非静态成员变量或数组类型声明。
• 在接口或公共代码中,建议优先使用显式类型以提高可读性。
26、静态数组array
一、基本定义与特性
-
模板类封装
std::array是C++11引入的STL容器,封装了C风格静态数组,提供更安全、现代化的接口。
定义格式:#include <array> std::array<类型, 长度> 变量名; // 示例:std::array<int, 5> arr;• 固定大小:长度在编译时确定,不可动态调整。
• 内存分配:元素存储在栈上(与
vector不同),内存连续,访问高效。 -
初始化方式
• 列表初始化:std::array<int, 4> arr = {10, 20, 30, 40}; // 显式初始化所有元素 std::array<int, 3> arr = {1}; // 剩余元素默认初始化为0• 填充值:
使用
fill()统一赋值:arr.fill(0); // 所有元素设为0
二、常用操作与成员函数
-
元素访问
• 下标操作:arr[索引](无边界检查)或arr.at(索引)(有边界检查,越界抛异常)。• 首尾元素:
front()和back()分别返回首元素和末元素的引用。• 数据指针:
data()返回指向底层数组的指针,兼容C风格接口。 -
迭代器支持
• 支持正向迭代器(begin(),end())和反向迭代器(rbegin(),rend()),可直接用于STL算法:for (auto it = arr.begin(); it != arr.end(); ++it) { std::cout << *it << " "; } -
其他成员函数
•size():返回数组长度(编译时常量)。•
empty():检查是否为空(仅当长度为0时返回true)。•
swap():交换两个同类型数组的内容。
三、优势与局限性
-
优点
• 安全性:支持边界检查(通过at()),避免缓冲区溢出。• 兼容性:可与STL算法(如
std::sort)无缝配合。• 性能:与C风格数组性能相当,无额外内存开销。
-
局限性
• 固定大小:无法动态调整容量,需预先确定长度。• 栈空间限制:过大数组可能导致栈溢出(需改用
vector)。
四、与其他数组类型的对比
| 特性 | std::array | C风格数组 | std::vector |
|---|---|---|---|
| 大小固定 | ✔️(编译时确定) | ✔️ | ❌(动态调整) |
| 内存位置 | 栈 | 栈 | 堆 |
| 边界检查 | 支持(at()) | 不支持 | 支持(at()) |
| STL兼容性 | 完全支持 | 不支持 | 完全支持 |
五、应用场景
• 固定长度数据:如矩阵运算、缓冲区管理。
• 性能敏感场景:需避免动态内存分配的开销时。
• 安全需求:需边界检查或与其他STL组件配合时。
通过合理使用 std::array,可在保证性能的同时提升代码安全性和可维护性。
27、函数指针
一、基本概念与声明
-
定义与作用
函数指针是指向函数入口地址的指针变量,允许通过指针间接调用函数,常用于回调、动态函数选择和策略模式等场景。 -
声明语法
返回类型 (*指针名)(参数类型列表);• 示例:
int (*funcPtr)(int, int); // 指向返回int且接受两个int参数的函数 -
初始化与调用
• 赋值时直接使用函数名(无需取地址符&):int add(int a, int b) { return a + b; } funcPtr = add; // 或 funcPtr = &add;• 调用方式与普通函数一致:
int result = funcPtr(3, 5); // 等价于 add(3,5)
二、高级用法
4.作为函数参数(回调函数)
可将函数指针传递给其他函数,实现动态行为控制:
void execute(int a, int b, int (*operation)(int, int)) {
cout << operation(a, b); // 动态调用不同函数
}
execute(5, 3, add); // 输出8
5.函数指针数组(函数表)
定义多个函数指针组成的数组,用于状态机或命令模式:
int (*funcTable[])(int, int) = {add, subtract};
cout << funcTable[0](5, 3); // 调用add
6.与typedef/using简化
使用类型别名提升可读性:
typedef int (*MathFunc)(int, int); // C风格
using MathFunc = int(*)(int, int); // C++11风格
MathFunc func = add; // 声明更简洁
三、注意事项与扩展
7.类型严格匹配
函数指针的返回类型和参数列表必须与目标函数完全一致,否则编译错误。
8.成员函数指针
指向类成员函数时需指定作用域,并使用特殊语法:
class MyClass {
public:
void print() { cout << "Hello"; }
};
void (MyClass::*memFuncPtr)() = &MyClass::print;
MyClass obj;
(obj.*memFuncPtr)(); // 输出Hello
9.现代替代方案
• auto关键字(C++11+):自动推导函数指针类型:
auto funcPtr = add; // 类型自动推断为int(*)(int, int)
• std::function模板:更灵活地包装函数、仿函数或Lambda表达式:
#include <functional>
std::function<int(int, int)> func = add; // 支持多种可调用对象
四、典型应用场景
• 事件驱动编程:GUI框架中传递事件处理器。
• 插件系统:动态加载外部库的函数。
• 策略模式:运行时切换算法实现(如排序策略)。
• STL算法:结合std::sort自定义比较逻辑。
28、Lambda表达式
一、基本概念与语法结构
-
定义
Lambda表达式是C++11引入的匿名函数对象,允许在代码中直接定义轻量级的函数逻辑,常用于替代函数指针或仿函数。其基本语法为:[捕获列表](参数列表) mutable(可选) -> 返回类型 { 函数体 }• 捕获列表:决定如何访问外部变量(详见下文)。
• 参数列表:与普通函数参数一致,支持C++14的泛型参数(如
auto)。• mutable:允许修改按值捕获的变量(默认按值捕获的变量为只读)。
• 返回类型:可省略,编译器自动推导;复杂逻辑需显式声明(如
-> int)。
二、捕获列表详解
2.捕获方式
• 空捕获 []:不访问外部变量,仅使用参数和局部变量。
• 按值捕获 [=]:复制外部变量到Lambda内部,副本不可修改(除非添加mutable)。
示例:`[x] { return x + 1; }`。
• 按引用捕获 [&]:直接引用外部变量,修改会影响原值。
示例:`[&total] { total += x; }`。
• 混合捕获:如[x, &y](x按值,y按引用)或[=, &z](默认按值,z按引用)。
• 初始化捕获(C++14):直接初始化变量,如[z = x + 1],避免悬垂引用。
3.特殊捕获
• this指针:访问类成员时需捕获this,例如[this] { member++; }。
• 生命周期安全:按引用捕获需确保外部变量在Lambda执行期间有效,否则可能导致未定义行为。
三、核心特性与用法
4.应用场景
• STL算法:简化排序、遍历等操作。
std::sort(vec.begin(), vec.end(), [](int a, int b) { return a > b; });
• 回调函数:用于事件处理、异步编程(如多线程任务)。
• 闭包封装:捕获上下文变量,实现灵活逻辑。例如:
int x = 10;
auto lambda = [x]() mutable { x++; return x; }; // 内部修改不影响外部x
5.类型与存储
• 类型唯一性:Lambda的类型是编译器生成的闭包类型,需用auto或std::function存储。
• 转换为函数指针:仅当Lambda不捕获变量时,可隐式转换为函数指针。
四、高级特性(C++14+)
6.泛型Lambda
支持参数使用auto,实现模板化的简洁写法:
auto sum = [](auto a, auto b) { return a + b; };
等效于模板函数,适用于多种类型。
7.初始化捕获扩展
允许在捕获列表中直接初始化变量,避免复杂逻辑:
auto ptr = std::make_unique<int>(10);
auto lambda = [ptr = std::move(ptr)] { return *ptr; }; // 移动语义捕获资源。
五、注意事项与最佳实践
• 避免隐式捕获:优先显式列出变量(如[x, &y]),而非[=]或[&],减少意外行为。
• 性能优化:按值捕获小对象,按引用捕获大对象;避免在长生命周期Lambda中捕获局部变量的引用。
• mutable的使用:仅在需要修改按值捕获变量时添加,且会改变Lambda的常量性。
示例代码
// 按值捕获与mutable
int x = 5;
auto func = [x]() mutable {
x *= 2;
return x;
};
cout << func(); // 输出10(内部副本修改)
cout << x; // 输出5(外部x未变)
// 按引用捕获与生命周期
std::function<void()> task;
{
int y = 10;
task = [&y] { cout << y; }; // y被销毁后调用会导致未定义行为
}
// task(); // 危险!
通过合理使用Lambda表达式,可以显著提升代码的简洁性和可维护性,尤其在泛型编程和函数式编程场景中表现突出。
29、using用法
- 类型别名(替代
typedef)
using IntPtr = int*; // 等价于 typedef int* IntPtr;
using Vec = std::vector<int>; // 定义模板别名更直观
- 命名空间引入
using std::cout; // 引入单个符号
using namespace std; // 引入整个命名空间(慎用,易引发命名冲突)
- 继承中的成员重定向
解决基类成员被派生类同名成员隐藏的问题:
class Base {
public:
void foo() {}
};
class Derived : public Base {
public:
using Base::foo; // 使基类的 foo 在派生类中可见
void foo(int) {} // 重载
};
- 模板别名(C++11 特有)
template<typename T>
using MyAllocVector = std::vector<T, MyAllocator<T>>; // 模板别名
5.在函数中引入基类成员
用于派生类中恢复基类被覆盖的成员函数:
class Parent {
public:
virtual void func() {}
};
class Child : public Parent {
public:
using Parent::func; // 显式引入基类版本
void func(int) {} // 重载
};
关键区别:
usingvstypedef
using更直观,尤其在模板别名中(typedef无法直接实现模板别名)。- 作用域控制
using可精确控制引入的符号(如命名空间中的特定名称),而typedef仅用于类型别名
30、匿名空间namespace
| 用法 | 说明 | 示例 |
|---|---|---|
namespace Name { ... } | 定义命名空间 | namespace MyNS { int x; } |
Name::member | 访问命名空间成员 | MyNS::x |
using Name::member | 引入单个成员 | using MyNS::x |
using namespace Name | 引入整个命名空间(慎用) | using namespace std |
namespace A::B::C | 嵌套命名空间(C++17) | namespace A::B { int y; } |
namespace { ... } | 匿名命名空间(文件作用域) | namespace { int local; } |
namespace Alias = LongName | 命名空间别名 | namespace Lib = VeryLongLib |
31、线程Thread
1. 基本用法
(1) 创建线程
#include <iostream>
#include <thread>
void hello() {
std::cout << "Hello from thread!" << std::endl;
}
int main() {
std::thread t(hello); // 创建线程并执行 hello()
t.join(); // 等待线程结束
return 0;
}
• std::thread t(func):创建线程并执行 func。
• t.join():主线程等待 t 执行完毕。
(2) 传递参数
void print(int x, const std::string& s) {
std::cout << x << ", " << s << std::endl;
}
int main() {
std::thread t(print, 42, "C++"); // 传递参数
t.join();
return 0;
}
• 参数按值传递(若需引用,用 std::ref)。
2. 线程管理
(1) join() vs detach()
| 方法 | 说明 | 示例 |
|---|---|---|
t.join() | 主线程等待 t 结束 | t.join(); |
t.detach() | 线程分离,独立运行 | t.detach(); |
⚠️ 注意:
• 线程对象销毁前必须调用 join() 或 detach(),否则程序终止(std::terminate)。
• detach() 后线程无法再控制,可能导致资源泄漏。
(2) 检查线程是否可 join()
if (t.joinable()) {
t.join(); // 仅在可 join 时调用
}
3. 线程同步(避免数据竞争)
(1) std::mutex(互斥锁)
#include <mutex>
std::mutex mtx;
int shared_data = 0;
void increment() {
mtx.lock(); // 加锁
++shared_data; // 临界区
mtx.unlock(); // 解锁
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << shared_data << std::endl; // 2
return 0;
}
• 问题:lock() 后忘记 unlock() 会导致死锁。
• 改进:使用 std::lock_guard(RAII 风格)。
(2) std::lock_guard(自动加锁/解锁)
void increment() {
std::lock_guard<std::mutex> lock(mtx); // 构造时加锁,析构时解锁
++shared_data;
}
(3) std::unique_lock(更灵活的锁)
void increment() {
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 延迟加锁
lock.lock(); // 手动加锁
++shared_data;
lock.unlock(); // 可手动解锁
}
• 适用于需要手动控制锁的场景(如条件变量)。
4. 线程间通信
(1) std::condition_variable(条件变量)
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void worker() {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] { return ready; }); // 等待 ready == true
std::cout << "Worker done!" << std::endl;
}
int main() {
std::thread t(worker);
{
std::lock_guard<std::mutex> lock(mtx);
ready = true; // 修改条件
}
cv.notify_one(); // 通知等待的线程
t.join();
return 0;
}
• cv.wait(lock, predicate):等待条件成立。
• cv.notify_one():唤醒一个等待线程。
(2) std::future 和 std::async(异步任务)
#include <future>
int compute() {
return 42;
}
int main() {
std::future<int> fut = std::async(compute); // 异步执行 compute()
std::cout << fut.get() << std::endl; // 获取结果(阻塞)
return 0;
}
• std::async 可返回 std::future,用于获取异步结果。
5. 线程局部存储(TLS)
thread_local int tls_var = 0; // 每个线程独立拷贝
void increment() {
++tls_var;
std::cout << tls_var << std::endl;
}
int main() {
std::thread t1(increment); // 输出 1
std::thread t2(increment); // 输出 1(独立于 t1)
t1.join();
t2.join();
return 0;
}
• thread_local 变量在每个线程中独立存在。
6. 线程池(C++17 无标准实现,可用第三方库)
• 例如,使用 std::async 模拟简单线程池:
std::vector<std::future<void>> tasks;
for (int i = 0; i < 10; ++i) {
tasks.push_back(std::async([] { /* 任务代码 */ }));
}
for (auto& task : tasks) {
task.wait(); // 等待所有任务完成
}
总结
| 功能 | 方法 | 说明 |
|---|---|---|
| 创建线程 | std::thread t(func) | 启动新线程 |
| 等待线程 | t.join() | 阻塞直到线程结束 |
| 分离线程 | t.detach() | 让线程独立运行 |
| 互斥锁 | std::mutex | 保护共享数据 |
| RAII 锁 | std::lock_guard | 自动加锁/解锁 |
| 条件变量 | std::condition_variable | 线程间同步 |
| 异步任务 | std::async + std::future | 获取异步结果 |
| 线程局部变量 | thread_local | 每个线程独立拷贝 |
最佳实践:
✅ 优先使用 std::lock_guard 或 std::unique_lock 避免死锁。
✅ 用 std::async 替代手动管理线程(简化代码)。
✅ 避免全局变量,改用线程局部存储(thread_local)或消息传递。
32、<chrono> 库简介
C++11 引入了 <chrono> 标准库,用于处理时间相关的操作,提供高精度的时间测量、时间点和时间间隔计算等功能。
1. 核心组件
<chrono> 主要包含以下三个核心概念:
| 组件 | 说明 | 示例 |
|---|---|---|
std::chrono::duration | 表示时间间隔(如秒、毫秒) | 5s(5秒)、100ms(100毫秒) |
std::chrono::time_point | 表示某个时间点(如当前时间) | system_clock::now() |
std::chrono::clock | 提供时间基准(如系统时钟、高精度时钟) | system_clock, steady_clock |
2. 时间间隔(duration)
duration 表示一段时间,由 数值 + 时间单位 组成。
(1) 预定义的时间单位
| 类型 | 说明 | 示例 |
|---|---|---|
std::chrono::nanoseconds | 纳秒 | 5ns |
std::chrono::microseconds | 微秒 | 100us |
std::chrono::milliseconds | 毫秒 | 500ms |
std::chrono::seconds | 秒 | 2s |
std::chrono::minutes | 分钟 | 10min |
std::chrono::hours | 小时 | 3h |
(2) 使用示例
#include <chrono>
using namespace std::chrono_literals; // 启用字面量(如 5s)
int main() {
auto d1 = 5s; // 5 秒
auto d2 = 100ms; // 100 毫秒
auto d3 = d1 + d2; // 5100 毫秒(自动转换)
std::cout << d3.count() << "ms\n"; // 输出:5100ms
return 0;
}
• count():获取时间间隔的数值。
3. 时间点(time_point)
time_point 表示某个具体的时间点(如当前时间)。
(1) 获取当前时间
auto now = std::chrono::system_clock::now(); // 当前系统时间
(2) 计算时间差
auto start = std::chrono::steady_clock::now();
// ... 执行某些操作 ...
auto end = std::chrono::steady_clock::now();
auto elapsed = end - start; // 返回 duration 类型
std::cout << "耗时: " << elapsed.count() << "ns\n";
4. 时钟(clock)
C++ 提供三种主要时钟:
| 时钟 | 说明 | 用途 |
|---|---|---|
std::chrono::system_clock | 系统时间(可调整) | 获取当前日期/时间 |
std::chrono::steady_clock | 稳定递增的时钟(不受系统时间影响) | 测量代码执行时间 |
std::chrono::high_resolution_clock | 最高精度的时钟(可能是 steady_clock 的别名) | 高精度计时 |
示例:测量代码执行时间
#include <chrono>
#include <iostream>
int main() {
auto start = std::chrono::steady_clock::now();
// 模拟耗时操作
for (int i = 0; i < 1000000; ++i) {}
auto end = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
std::cout << "耗时: " << elapsed.count() << "us\n";
return 0;
}
• duration_cast:转换时间单位(如微秒 us)。
5. 时间点与时间转换
(1) 时间点转 time_t(用于 C 风格函数)
auto now = std::chrono::system_clock::now();
std::time_t now_c = std::chrono::system_clock::to_time_t(now);
std::cout << "当前时间: " << std::ctime(&now_c);
(2) 自定义时间点
using namespace std::chrono;
auto tp = system_clock::time_point(1h + 30min); // 1小时30分钟后
6. C++20 新增功能
C++20 扩展了 <chrono>,支持日历和时区操作:
#include <chrono>
using namespace std::chrono;
int main() {
auto today = floor<days>(system_clock::now()); // 获取当前日期
year_month_day ymd{today}; // 转换为年-月-日
std::cout << "Today is: " << ymd << "\n"; // 输出:2023-08-20
return 0;
}
总结
| 功能 | 方法 | 说明 |
|---|---|---|
| 时间间隔 | std::chrono::duration | 表示时间段(如 5s) |
| 时间点 | std::chrono::time_point | 表示某个时刻(如 now()) |
| 时钟 | system_clock, steady_clock | 提供时间基准 |
| 单位转换 | duration_cast | 转换时间单位(如 ms→s) |
| 时间测量 | start = steady_clock::now() | 计算代码执行时间 |
| C++20 日历 | year_month_day | 处理日期 |
最佳实践:
✅ 测量代码性能用 steady_clock(不受系统时间调整影响)。
✅ 需要日期/时间时用 system_clock。
✅ 使用 duration_cast 进行时间单位转换。
33、联合体union
在C++中,联合体(union)是一种特殊的数据结构,允许在同一内存位置存储不同的数据类型,但同一时间只能使用其中一个成员。以下是联合体的核心用法和特性:
1. 基本定义
union MyUnion {
int i;
float f;
char c;
};
- 所有成员共享同一块内存空间,大小为最大成员的大小。
- 修改一个成员会影响其他成员的值(因为内存重叠)。
2. 使用场景
- 节省内存:多个数据不会同时使用时(如协议解析、类型转换)。
- 类型转换:通过不同成员解释同一段内存(如将
float的二进制表示转换为int)。 - 低级编程:直接操作硬件或二进制数据时。
3. 匿名联合体(C++11起)
可直接访问成员,无需通过联合体名称:
struct Data {
enum { INT, FLOAT } type;
union {
int i;
float f;
}; // 匿名联合体
};
Data d;
d.i = 42; // 直接访问
4. 联合体与类(C++11扩展)
- 可以包含成员函数、构造/析构函数。
- 但不能继承或作为基类。
union ComplexUnion {
int x;
std::string s; // 注意:包含非平凡类型需手动管理内存
~ComplexUnion() {} // 需自定义析构
};
5. 注意事项
- 内存共享:修改一个成员会覆盖其他成员的值。
- 类型安全:需自行确保当前使用的成员是正确的。
- 非平凡类型:C++11后支持,但需手动管理构造/析构(如
std::string)。
示例:类型转换
union Converter {
int i;
float f;
};
Converter u;
u.f = 3.14f;
std::cout << u.i; // 输出float的二进制表示(整数形式)
联合体在特定场景下非常高效,但需谨慎使用以避免未定义行为。
34、虚析构函数
在C++中,虚析构函数(virtual destructor) 用于确保在多态(继承)场景下正确调用派生类的析构函数,避免内存泄漏。以下是其核心要点:
1. 基本作用
• 多态析构:当通过基类指针删除派生类对象时,若基类析构函数是虚函数,会先调用派生类析构函数,再调用基类析构函数。
• 避免资源泄漏:若基类析构函数非虚,则只会调用基类析构函数,导致派生类资源未释放。
2. 语法示例
class Base {
public:
virtual ~Base() {
std::cout << "Base destructor\n";
}
};
class Derived : public Base {
public:
~Derived() override {
std::cout << "Derived destructor\n";
}
};
int main() {
Base* obj = new Derived();
delete obj; // 正确调用Derived和Base的析构函数
return 0;
}
输出:
Derived destructor
Base destructor
3. 关键规则
• 必须为虚的情况:若类可能被继承,且会通过基类指针操作派生类对象,则基类析构函数必须为虚。
• 非虚析构的风险:若基类析构非虚,delete基类指针时行为未定义(通常仅析构基类部分)。
• 虚函数开销:虚析构函数会引入虚表指针(vptr)的存储和间接调用开销,但通常可忽略。
4. 其他注意事项
• 抽象类的析构函数:即使纯虚类(含纯虚函数),也应提供虚析构函数的实现(可为空):
class AbstractBase {
public:
virtual ~AbstractBase() = default; // C++11:显式默认实现
};
• C++11后的override:建议用override标记派生类析构函数,增强可读性。
• 析构顺序:派生类析构 → 成员对象析构 → 基类析构(与构造顺序相反)。
5. 典型应用场景
• 多态基类:如图形类(Shape)的派生类(Circle、Rectangle)。
• 工厂模式:返回基类指针的工厂函数。
• STL容器存储指针:如vector<Base*>。
总结
• 用虚析构函数:当类可能作为多态基类时。
• 无需虚析构:若类不会被继承,或仅通过具体类型(非指针/引用)使用。
35、类型转换
在C++中,类型转换(Type Casting) 用于将一种数据类型转换为另一种数据类型。C++提供了多种类型转换方式,包括C风格转换和C++特有的四种显式类型转换运算符(更安全、更明确)。以下是它们的核心用法和区别:
1. C风格类型转换(不推荐)
语法:(目标类型)表达式
特点:
• 功能强大但不安全,可能引发未定义行为(UB)。
• 编译器不进行严格检查,容易隐藏错误。
示例:
int a = 10;
double b = (double)a; // C风格转换
2. C++四种显式类型转换(推荐)
C++引入四种类型转换运算符,提供更安全的转换方式:
(1) static_cast(静态转换)
用途:
• 用于相关类型之间的转换(如数值类型、基类与派生类指针/引用)。
• 编译时检查,不保证运行时安全。
示例:
int i = 42;
double d = static_cast<double>(i); // 数值转换
Base* base = new Derived();
Derived* derived = static_cast<Derived*>(base); // 向下转型(不安全,需确保类型正确)
适用场景:
• 基本数据类型转换(int → double)。
• 类层次转换(向上转型安全,向下转型需谨慎)。
(2) dynamic_cast(动态转换)
用途:
• 主要用于多态类型(含虚函数的类)的向下转型(Base* → Derived*)。
• 运行时检查,失败返回nullptr(指针)或抛出std::bad_cast(引用)。
示例:
Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base); // 安全向下转型
if (derived) {
// 转换成功
} else {
// 转换失败
}
适用场景:
• 多态类型的安全转换。
• 需检查转换是否成功时。
(3) const_cast(常量性转换)
用途:
• 移除或添加const/volatile修饰符(不改变底层数据)。
• 主要用于兼容旧代码或API。
示例:
const int a = 10;
int* b = const_cast<int*>(&a); // 移除const(危险!修改可能导致UB)
*b = 20; // 未定义行为(UB),因为a原本是const
适用场景:
• 调用接受非const参数的函数,但数据本身不应被修改时。
(4) reinterpret_cast(重新解释转换)
用途:
• 低级别位模式转换(如指针→整数、不同类型指针转换)。
• 极度危险,仅用于特定场景(如硬件操作、序列化)。
示例:
int* p = new int(65);
char* c = reinterpret_cast<char*>(p); // 将int*解释为char*
std::cout << *c; // 输出 'A'(ASCII 65)
适用场景:
• 底层二进制数据处理(如网络协议、内存映射)。
• 需谨慎使用,易引发未定义行为。
3. 总结对比
| 转换方式 | 用途 | 检查时机 | 安全性 |
|---|---|---|---|
static_cast | 相关类型转换(数值、类层次) | 编译时 | 较安全 |
dynamic_cast | 多态类型向下转型 | 运行时 | 最安全 |
const_cast | 修改const/volatile | 编译时 | 危险 |
reinterpret_cast | 低级别二进制转换 | 无 | 极度危险 |
| C风格转换 | 任意转换 | 无 | 最不安全 |
4. 最佳实践
- 优先使用C++风格转换(避免C风格转换)。
- 多态类型转换用
dynamic_cast(确保安全)。 - 避免
reinterpret_cast和const_cast,除非必要。 static_cast用于明确安全的转换(如数值计算)。
正确使用类型转换能提高代码安全性和可维护性!
36、预编译头文件
C++ 预编译头文件(PCH)通过将常用且稳定的头文件预先编译为二进制格式,显著减少重复解析时间,从而提升大型项目的编译效率。以下是其核心使用要点:
-
创建预编译头文件
• 命名与内容:通常命名为stdafx.h或pch.h,包含高频使用且不常修改的头文件,例如标准库(<iostream>、<vector>等)、第三方库或项目通用头文件。• 示例:
// pch.h #pragma once #include <iostream> #include <vector> #include "CommonProjectHeaders.h" -
配置编译器生成预编译文件
• GCC/Clang:g++ -xc++-header pch.h -o pch.h.gch生成
.gch文件后,源文件只需包含pch.h,编译器会自动优先使用预编译版本。
• MSVC (Visual Studio): ◦创建
pch.cpp文件仅包含#include "pch.h"。 ◦项目属性中设置
/Yc(生成)和/Yu(使用)选项,指定pch.h为预编译头。• CMake:使用
target_precompile_headers命令自动集成。 -
在源文件中使用
• 包含顺序:必须在源文件首行显式包含预编译头文件,否则可能失效。例如:// main.cpp #include "pch.h" // 其他代码... -
注意事项
• 内容稳定性:避免在预编译头中包含频繁修改的头文件(如项目内业务逻辑头文件),否则需频繁重新生成,抵消优化效果。• 跨平台差异:
◦ GCC 的
.gch文件需与源文件同名且在同一目录。 ◦ MSVC 需通过
.pch文件配合项目配置。• 依赖管理:确保所有源文件统一使用预编译头,避免直接包含已预编译的头文件,防止重复定义错误。
-
替代方案
• C++20 模块:通过import语句直接引入编译后的模块,可替代预编译头文件,实现更高效的编译。
适用场景
预编译头文件适合大型项目(如依赖 STL、Boost 等),对小型项目优化效果有限。合理使用可缩短 30%-50% 的编译时间。
37、std::optional
std::optional 是 C++17 引入的标准库模板类,用于表示一个可能包含值也可能为空(无值)的对象。它提供了一种类型安全且直观的方式来处理可选值,避免了传统指针或特殊标记(如 nullptr 或 -1)的潜在问题。
核心特性
-
基本用法
-
头文件:
#include <optional> -
声明与初始化:
std::optional<int> opt; // 默认无值 std::optional<int> opt_val = 42; // 直接赋值 auto opt_made = std::make_optional(3.14); // 工厂函数
-
-
状态检查与访问
- 检查是否有值:
has_value()或直接转换为布尔值(if (opt))。 - 访问值:
operator*或operator->(需先检查,否则未定义行为)。value():无值时抛出std::bad_optional_access。value_or(default):无值时返回默认值。
- 检查是否有值:
-
重置与修改
- 重置为无值:
opt.reset()或opt = std::nullopt。 - 赋值或原地构造:
opt.emplace(100)。
- 重置为无值:
典型应用场景
函数返回值:替代可能失败的函数返回(如查找操作)。
std::optional<int> find(const std::vector<int>& vec, int target) {
auto it = std::find(vec.begin(), vec.end(), target);
return it != vec.end() ? std::optional(*it) : std::nullopt;
}
- 配置或参数:表示可选参数,避免重载或默认参数。
- 替代指针:明确表达“值可能不存在”,避免空指针异常。
优势
- 类型安全:避免裸指针或特殊值的误用。
- 代码清晰:显式表达“可选”语义,提升可读性。
- 性能优化:相比动态分配(如指针),
std::optional通常通过栈存储实现,开销更低。
注意事项
- 访问前必须检查值是否存在,否则可能引发未定义行为或异常。
- 适用于值类型,对大型对象可能需配合移动语义优化。
38、std::variant
std::variant 是 C++17 引入的一种类型安全的联合体(tagged union),用于存储一组预定义类型中的某一个值,同时保证类型安全。以下是其核心特性和用法:
1. 基本概念
- 定义:
std::variant<T1, T2, ...>可存储T1, T2等类型中的任意一种,但同一时间只能存储一个值。 - 与
union的区别:传统union需要手动管理当前活跃类型,而variant自动跟踪类型,避免未定义行为。
2. 核心操作
(1) 初始化与赋值
std::variant<int, double, std::string> v;
v = 42; // 存储 int
v = 3.14; // 改为 double
v = "hello"; // 改为 string
(2) 类型检查
std::holds_alternative<T>(v):检查是否存储了类型T。v.index():返回当前存储类型的索引(从 0 开始)。
(3) 值访问
-
std::get<T>(v):直接获取类型T的值,类型不匹配时抛出std::bad_variant_access。 -
std::get_if<T>(&v):安全获取指针,失败返回nullptr。 -
std::visit:通过访问者模式处理所有可能的类型:std::visit([](auto&& arg) { std::cout << arg; }, v);
3. 特点与优势
- 类型安全:编译时检查类型有效性,避免运行时错误。
- 自动析构:销毁时自动调用当前存储值的析构函数。
- 模式匹配:结合
std::visit实现类似多态的行为。 - 替代方案:比
union更安全,比继承更轻量。
4. 示例代码
#include <variant>
#include <iostream>
int main() {
std::variant<int, std::string> v = "hello";
if (std::holds_alternative<std::string>(v)) {
std::cout << std::get<std::string>(v); // 输出 "hello"
}
return 0;
}
5. 注意事项
- 性能:相比原生类型可能有额外开销(如类型标签)。
- 默认构造:若首类型不可默认构造,需用
std::monostate占位。 - 异常安全:
std::get失败会抛出异常。
通过 std::variant,C++ 提供了一种灵活且安全的多类型存储方案,适用于配置管理、错误处理等场景。
39、std::any
C++ 中的 std::any 是 C++17 引入的通用类型容器,属于 <any> 头文件。它允许存储任意类型的值,并在运行时安全地访问。以下是其核心特性:
1. 基本功能
- 类型安全存储:可保存任意可复制构造的类型(如
int、string、自定义类等)。 - 类型擦除:内部通过类型擦除技术隐藏具体类型,用户只需通过接口操作。
2. 核心操作
-
赋值:直接存储值(自动推导类型)。
std::any a = 42; // 存储 int a = std::string("hi"); // 替换为 string -
访问值:需通过 std::any_cast,若类型不匹配会抛出 std::bad_any_cast。
try { int val = std::any_cast<int>(a); // 提取 int } catch (const std::bad_any_cast& e) { std::cerr << "类型错误!"; } -
检查类型:用 type() 获取类型信息(返回 std::type_info)。
if (a.type() == typeid(int)) { std::cout << "存储的是 int"; } -
清空/判空:
a.reset(); // 清空 bool empty = a.has_value(); // 是否为空
3. 特点与限制
- 优点:比
void*更安全(类型检查),比模板更灵活(无需编译时确定类型)。 - 限制:
- 存储的值必须可复制构造。
- 性能略低于具体类型(涉及动态内存分配和类型检查)。
4. 典型用途
- 需要动态处理未知类型的场景(如插件系统、配置项存储)。
- 替代联合体(
union)或基类多态,简化代码。
示例代码
#include <any>
#include <iostream>
int main() {
std::any data;
data = 3.14; // 存储 double
data = "Hello"; // 替换为 const char*
if (data.type() == typeid(const char*)) {
std::cout << std::any_cast<const char*>(data); // 输出: Hello
}
}
std::any 适合需要灵活类型的场景,但需注意类型安全和性能权衡。
40、如何让字符串更快
在 C++ 中优化字符串性能需要结合具体场景(如构造、拼接、查找等),以下是关键优化策略:
1. 避免不必要的拷贝
-
使用
string_view(C++17):对只读操作(如子串、函数参数),用 std::string_view 避免拷贝:
void process(std::string_view sv) {} // 无需拷贝原字符串 std::string s = "data"; process(s); // 隐式转换 -
移动语义:
传递或返回大字符串时,使用 std::move 转移所有权:
std::string createString() { std::string s(1000, 'a'); return s; // 编译器自动优化为移动(RVO/NRVO) } auto s = createString();
2. 预分配内存
-
reserve()提前分配:已知大小时预先分配内存,避免多次扩容:
std::string s; s.reserve(1000); // 预分配 for (int i = 0; i < 1000; ++i) s += 'x'; // 无重分配 -
构造时初始化:
直接指定大小和填充字符,减少后续操作:
std::string s(1000, 'x'); // 一步构造
3. 高效拼接
-
避免
+运算符链式拼接:多次 + 会生成临时对象,改用 += 或 append():
// 低效 std::string s = a + b + c + d; // 高效 std::string s; s += a; s += b; s += c; s += d; -
使用
ostringstream批量拼接:适用于复杂拼接场景(如混合类型):
std::ostringstream oss; oss << "Value: " << 42 << ", Text: " << str; std::string s = oss.str();
4. 减少动态分配
- 短字符串优化(SSO):
多数实现(如 MSVC、GCC)对小字符串(通常 ≤15字节)直接存储在栈上,无需堆分配。
建议:优先使用短字符串或分段处理大字符串。
5. 算法优化
-
避免
std::string::find频繁调用:
多次查找时,将字符串转为string_view或使用 Boyer-Moore 等高效算法(需第三方库如Boost)。 -
就地修改:
使用 replace() 或迭代器直接操作,而非生成新字符串:
std::string s = "hello"; s.replace(1, 3, "123"); // 直接修改
6. 其他技巧
-
自定义分配器:
针对特定场景(如高频小字符串),使用内存池分配器(如boost::pool_allocator)。 -
避免 C 风格转换:
c_str() 返回的指针仅在字符串生命周期内有效,如需长期保存,应拷贝数据:
const char* unsafe = s.c_str(); // 危险! std::vector<char> safe(s.begin(), s.end()); // 安全拷贝
性能对比示例
| 操作 | 低效实现 | 优化实现 |
|---|---|---|
| 拼接字符串 | s = s1 + s2 + s3; | s = s1; s += s2; s += s3; |
| 遍历字符 | for (int i=0; i<s.size(); ++i) s[i] | for (char c : s) (范围for循环) |
工具支持
- 性能分析:
使用perf(Linux) 或 VTune (Intel) 分析热点。 - 编译器优化:
开启-O2/-O3和链接时优化(-flto)。
通过结合预分配、减少拷贝、利用现代 C++ 特性(如 string_view),可显著提升字符串处理速度。需根据实际场景权衡可读性与性能。
41、std::async
std::async 是 C++11 引入的异步任务执行工具(位于 <future> 头文件),用于简化多线程编程。它允许以同步或异步方式执行函数,并返回一个 std::future 对象来获取结果。
1. 核心功能
- 异步执行:在后台线程中运行任务,不阻塞主线程。
- 结果获取:通过
future.get()阻塞等待结果,或future.wait()仅等待完成。 - 策略控制:支持选择立即启动(异步)或延迟启动(惰性)。
2. 基本用法
语法
#include <future>
#include <iostream>
int task() { return 42; }
int main() {
// 启动异步任务
std::future<int> fut = std::async(std::launch::async, task);
// 阻塞获取结果
std::cout << fut.get(); // 输出: 42
}
参数说明
- 启动策略(可选):
std::launch::async:强制异步执行(新线程)。std::launch::deferred:延迟执行(调用get()或wait()时在当前线程运行)。- 默认(不指定):由实现决定(通常为
async)。
3. 关键特性
异步 vs 同步
auto fut1 = std::async(std::launch::async, []{
std::this_thread::sleep_for(1s);
return 1;
}); // 后台执行
auto fut2 = std::async(std::launch::deferred, []{
return 2;
}); // 调用 get() 时执行
std::cout << fut2.get(); // 此时才执行任务
异常处理
若任务抛出异常,future.get() 会重新抛出:
auto fut = std::async([] {
throw std::runtime_error("error");
});
try { fut.get(); }
catch (const std::exception& e) {
std::cerr << e.what();
}
线程资源管理
- 异步任务由标准库管理的线程池或新线程执行(依赖实现)。
- 析构时若未调用
get()/wait(),可能阻塞等待任务完成(类似std::thread的join())。
4. 适用场景
- 并行计算:分解耗时任务(如数值计算、IO操作)。
- 惰性求值:延迟执行直到需要结果时。
- 替代
std::thread:简化线程创建和结果传递。
5. 注意事项
-
生命周期问题:确保 std::async返回的 future 被持有,否则任务可能被析构。
// 错误!临时 future 析构可能导致任务终止 std::async(std::launch::async, []{ while(true); }); -
性能权衡:
频繁创建小任务可能导致线程开销,考虑用线程池替代。 -
不可复制参数:
传递的参数需支持拷贝或移动(或使用std::ref包装引用)。
6. 示例:并行求和
#include <future>
#include <vector>
int sum(const std::vector<int>& v, int start, int end) {
int res = 0;
for (int i = start; i < end; ++i) res += v[i];
return res;
}
int main() {
std::vector<int> data = {1, 2, 3, 4, 5, 6};
auto fut1 = std::async(std::launch::async, sum, std::ref(data), 0, 3);
auto fut2 = std::async(std::launch::async, sum, std::ref(data), 3, 6);
std::cout << fut1.get() + fut2.get(); // 输出: 21
}
总结
| 特性 | 说明 |
|---|---|
| 异步执行 | 通过 std::launch::async 启动 |
| 结果延迟获取 | 使用 future.get() 阻塞等待 |
| 异常传播 | 通过 future.get() 捕获 |
| 资源自动释放 | future 析构时同步任务状态 |
std::async 是轻量级异步编程工具,适合简单任务并行化,但复杂场景需结合 std::thread 或线程库(如 TBB)。
42、单例模式
单例模式(Singleton Pattern)是一种创建型设计模式,确保一个类只有一个实例,并提供全局访问点。其核心特点包括:
- 私有构造函数:防止外部直接实例化。
- 静态实例指针:保存唯一的实例。
- 静态访问方法(如
getInstance()):提供全局访问入口。 - 线程安全(可选):多线程环境下需避免重复创建。
单例类实现示例(懒汉式,线程安全)
#include <iostream>
#include <mutex>
class Singleton {
private:
// 1. 私有构造函数
Singleton() {
std::cout << "Singleton instance created." << std::endl;
}
// 2. 静态实例指针
static Singleton* instance;
static std::mutex mtx; // 用于线程安全
public:
// 3. 删除拷贝构造和赋值操作
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
// 4. 静态访问方法
static Singleton* getInstance() {
std::lock_guard<std::mutex> lock(mtx); // 加锁
if (instance == nullptr) {
instance = new Singleton();
}
return instance;
}
void showMessage() {
std::cout << "Hello from Singleton!" << std::endl;
}
};
// 初始化静态成员
Singleton* Singleton::instance = nullptr;
std::mutex Singleton::mtx;
// 使用示例
int main() {
Singleton* obj1 = Singleton::getInstance();
Singleton* obj2 = Singleton::getInstance();
obj1->showMessage();
std::cout << "Addresses: " << obj1 << " vs " << obj2 << std::endl; // 地址相同
return 0;
}
关键点说明
- 懒汉式:实例在第一次调用
getInstance()时创建(节省资源)。 - 线程安全:通过
std::mutex确保多线程下仅创建一次。 - 防拷贝:禁用拷贝构造和赋值操作,避免通过复制创建新实例。
- 全局访问:通过静态方法
getInstance()获取唯一实例。
其他实现方式
-
饿汉式:在程序启动时直接初始化实例(线程安全但可能浪费资源):
// 在类定义中直接初始化 static Singleton* instance = new Singleton(); -
C++11 后的更简洁写法(利用局部静态变量):
static Singleton& getInstance() { static Singleton instance; // 线程安全(C++11起) return instance; }
选择取决于具体需求(如资源敏感度、C++标准支持等)。
43、内存分配跟踪实现
重载全局 new/delete
这是最全面的方法,可以跟踪所有动态内存分配。
#include <iostream>
#include <cstdlib>
#include <map>
#include <mutex>
// 内存分配信息结构
struct AllocationInfo {
size_t size;
const char* file;
int line;
};
// 全局分配跟踪变量
static std::map<void*, AllocationInfo> allocationMap;
static std::mutex allocationMutex;
static size_t totalAllocated = 0;
// 重载全局 new 操作符
void* operator new(size_t size, const char* file, int line) {
void* ptr = malloc(size);
if (!ptr) throw std::bad_alloc();
{
std::lock_guard<std::mutex> lock(allocationMutex);
allocationMap[ptr] = {size, file, line};
totalAllocated += size;
}
return ptr;
}
// 重载全局 delete 操作符
void operator delete(void* ptr) noexcept {
{
std::lock_guard<std::mutex> lock(allocationMutex);
auto it = allocationMap.find(ptr);
if (it != allocationMap.end()) {
totalAllocated -= it->second.size;
allocationMap.erase(it);
}
}
free(ptr);
}
// 定义宏以便自动获取文件名和行号
#define new new(__FILE__, __LINE__)
// 打印内存分配报告
void PrintMemoryReport() {
std::lock_guard<std::mutex> lock(allocationMutex);
std::cout << "\n=== Memory Allocation Report ===\n";
std::cout << "Total allocated: " << totalAllocated << " bytes\n";
std::cout << "Current allocations: " << allocationMap.size() << "\n";
for (const auto& entry : allocationMap) {
std::cout << " " << entry.first << ": " << entry.second.size
<< " bytes allocated at " << entry.second.file
<< ":" << entry.second.line << "\n";
}
}
// 示例使用
int main() {
int* p1 = new int(42);
double* p2 = new double(3.14);
char* str = new char[100];
delete p1;
PrintMemoryReport();
delete p2;
delete[] str;
return 0;
}
44、左值和右值以及左值引用右值引用
在C++中,左值(lvalue)、右值(rvalue)、**左值引用(lvalue reference)和右值引用(rvalue reference)**是核心概念,尤其在C++11引入移动语义后变得尤为重要。
1. 左值(lvalue)
-
定义:有持久内存地址的表达式(可被取地址)。
-
特点:
- 通常有变量名(如
int x中的x)。 - 能出现在赋值语句的左侧或右侧。
- 生命周期由其作用域决定。
- 通常有变量名(如
-
示例:
int a = 10; // `a`是左值 int* p = &a; // 可对左值取地址 a = 20; // 左值在赋值左侧 int b = a; // 左值在赋值右侧
2. 右值(rvalue)
-
定义:临时对象或字面量,无持久内存地址(不可取地址)。
-
特点:
- 通常是临时值(如表达式结果、函数返回的临时对象)。
- 只能出现在赋值语句的右侧。
- C++11起支持移动语义(可被“窃取”资源)。
-
示例:
int c = 10 + 5; // `10+5`是右值(临时结果) std::string s = "hello"; // 字符串字面量是右值 int&& r = 42; // 42是右值(字面量)
3. 左值引用(lvalue reference)
-
语法:
T&(如int&)。 -
作用:绑定到左值,用于修改或延长左值的生命周期。
-
限制:不能绑定到右值(除非使用
const T&)。 -
示例:
int x = 10; int& ref = x; // 左值引用绑定到左值 ref = 20; // 修改x的值 // int& bad = 42; // 错误:不能绑定到右值 const int& cref = 42; // 允许:const左值引用可绑定右值
4. 右值引用(rvalue reference)
-
语法:
T&&(如int&&)。 -
作用:
- 绑定到右值,用于实现移动语义(高效转移资源)。
- 延长临时对象的生命周期。
-
示例:
int&& rref = 42; // 右值引用绑定到右值 std::string s1 = "hello"; std::string s2 = std::move(s1); // std::move将左值转为右值引用 // 触发移动构造函数(避免拷贝)
关键对比
| 特性 | 左值 | 右值 | 左值引用(T&) | 右值引用(T&&) |
|---|---|---|---|---|
| 是否可寻址 | 是 | 否 | 是 | 否(绑定右值时) |
| 赋值位置 | 左侧或右侧 | 仅右侧 | 绑定左值 | 绑定右值 |
| 典型用途 | 变量、持久对象 | 临时值、字面量 | 修改左值 | 移动语义、完美转发 |
| 生命周期扩展 | 作用域内 | 临时(通常短) | 可延长左值 | 可延长右值 |
应用场景
- 左值引用:函数参数传递(避免拷贝)、修改传入对象。
- 右值引用:
- 移动构造函数/赋值运算符(如
std::vector的移动操作)。 - 完美转发(
std::forward结合模板)。
- 移动构造函数/赋值运算符(如
总结
- 左值:有地址,可重复使用。
- 右值:临时值,可被移动。
- 左值引用:绑定左值,用于修改。
- 右值引用:绑定右值,用于高效资源转移。
理解这些概念是掌握现代C++(如移动语义、智能指针)的基础!
45、参数计算顺序
在C++中,函数参数的求值顺序是一个重要但容易引发误解的主题,具体规则如下:
1.C++17之前的未定义顺序
在C++17标准之前,函数参数的求值顺序是未定义的,这意味着编译器可以自由选择从左到右、从右到左或其他任意顺序计算参数。例如:
int i = 0;
func(i++, i++);
此时参数的值可能是(0, 0)或(1, 0),具体取决于编译器实现。这种不确定性容易导致代码逻辑错误,尤其是在参数中存在副作用(如自增、函数调用)时。
2.C++17后的明确规则
C++17引入了更严格的求值顺序规定:
-
函数参数:从左到右依次求值。例如:
func(a(), b(), c()); // 保证a() → b() → c()的顺序 -
赋值运算符:右侧表达式先于左侧被求值。例如:
x = y + z; // 先计算y+z,再赋值给x -
初始化列表:按声明顺序从左到右求值。
3.例外与注意事项
- 逻辑运算符短路:
&&和||的左操作数先求值,右操作数可能被跳过(如左操作数已确定结果时)。 - 逗号运算符:按从左到右顺序求值并返回最后一个结果。
- 函数重载与运算符重载:可能引入隐式函数调用,需注意求值顺序是否被改变。
4.编程建议
-
避免依赖未定义行为:即使C++17明确了部分规则,仍建议通过显式拆分表达式或使用临时变量来消除歧义。例如:
int tmp1 = a++; int tmp2 = a; func(tmp1, tmp2); // 替代func(a++, a); -
使用括号明确优先级:在复杂表达式中用括号强制指定计算顺序。
46、移动语义
C++的移动语义是C++11引入的核心特性,旨在通过资源所有权转移(而非拷贝)提升程序性能。其核心要点如下:
1. 核心思想与价值
- 避免深拷贝:通过直接“窃取”临时对象(右值)的资源,将原本需要O(n)时间复杂度的拷贝操作降为O(1)的指针转移。
- 性能优化:适用于管理动态资源(如内存、文件句柄)的类,显著减少内存分配和数据复制的开销。
- 支持高效资源转移:特别适合临时对象、容器操作和大型对象传递场景,提升程序整体效率。
2. 实现机制
右值引用(Rvalue Reference)
- 通过
T&&语法声明,用于绑定右值(如临时对象、字面量或通过std::move转换的左值)。 - 右值引用变量本身是左值(因其有名字),需通过
std::move再次转换为右值以触发移动语义。 std::move本质是静态类型转换(static_cast<T&&>),将左值强制转为右值引用。它本身不执行任何资源移动操作,仅标记对象可被移动。
移动构造函数与移动赋值运算符
-
移动构造函数:接受右值引用参数,接管源对象资源并置空其指针,避免重复释放。
class MyString { public: MyString(MyString&& other) noexcept : data_(other.data_), size_(other.size_) { other.data_ = nullptr; // 原对象置空 } }; -
移动赋值运算符:释放当前资源后接管新资源,需检查自赋值。
MyString& operator=(MyString&& other) noexcept { if (this != &other) { delete[] data_; // 释放当前资源 data_ = other.data_; // 接管资源 other.data_ = nullptr; // 原对象置空 } return *this; }
3. 触发场景
- 临时对象初始化:用右值初始化新对象时自动调用移动构造函数(如
MyString s2 = std::move(s1);)。 - 函数返回值:返回局部对象时,编译器优先使用移动而非拷贝(若移动构造函数存在)。
- 容器操作:如
std::vector::push_back、std::swap等标准库操作内部自动使用移动语义优化性能。 - 显式调用:通过
std::move将左值强制转换为右值引用,触发移动语义。
4. 关键工具
std::move:将左值转换为右值引用,允许显式触发移动语义,但本身不执行任何移动操作。noexcept:标记移动操作不抛异常,确保标准库操作(如vector扩容)的安全性。
5. 注意事项
- 移动后对象状态:被移动的对象应处于有效但未定义状态(如指针置空),可安全析构但不可依赖其数据。
- 避免重复移动:对已移动的对象再次使用可能导致未定义行为。
- 兼容拷贝语义:若未定义移动操作,编译器会回退到拷贝操作。
总结
移动语义通过右值引用和资源所有权转移,解决了传统拷贝操作在性能上的瓶颈,是现代C++高效编程的基石。正确使用移动语义需结合std::move、noexcept及合理的类设计,以实现资源的高效管理。
47、花括号 {} 初始化对象与传统初始化的区别
在 C++11 中引入的花括号初始化 {}(也称为列表初始化)相较于传统初始化方式(如圆括号 () 或赋值式 =)有以下几个关键区别:
- 防止窄化转换(Narrowing Conversion)
- 花括号初始化:禁止隐式的窄化转换(精度损失或范围溢出)。
- 传统初始化:允许(可能带警告)。
int a = 3.14; // ✅ 传统初始化(警告:可能丢失精度)
int b(3.14); // ✅ 同上
int c{3.14}; // ❌ 编译错误:窄化转换(double→int)
- 解决“最令人烦恼的解析”(Most Vexing Parse)
- 花括号初始化:避开歧义(不会被解析为函数声明)。
- 圆括号初始化:可能导致歧义。
std::vector<int> v1(10); // ✅ 初始化含 10 个元素的 vector
std::vector<int> v2(); // ❌ 被解析为函数声明(无参函数)
std::vector<int> v3{}; // ✅ 初始化空 vector(无歧义)
- 统一初始化语法
- 花括号初始化:适用于所有类型(基本类型、数组、容器、类等)。
- 传统初始化:语法不统一。
int arr1[] = {1, 2, 3}; // ✅ 传统数组初始化
std::array<int, 3> arr2{1, 2, 3}; // ✅ 统一初始化
- 优先调用
std::initializer_list构造函数
- 花括号初始化:优先匹配
initializer_list重载的构造函数。 - 圆括号初始化:直接匹配普通构造函数。
std::vector<int> v1(3, 5); // 含3个元素,值均为5
std::vector<int> v2{3, 5}; // 含2个元素:3和5(调用 initializer_list)
- 默认初始化行为
- 花括号初始化:空花括号执行值初始化(
T{}→ 零值)。 - 传统初始化:空圆括号可能被解析为函数声明或默认初始化。
int x{}; // ✅ 值初始化为0
int y(); // ❌ 函数声明(返回int的函数)
int z = int(); // ✅ 值初始化为0(但语法冗长)
- 聚合类型的初始化
- 花括号初始化:支持直接初始化聚合类型(如结构体)。
- 传统初始化:不支持。
struct Point { int x, y; };
Point p1{1, 2}; // ✅ 直接初始化成员(C++11起)
Point p2(1, 2); // ❌ 编译错误(除非定义构造函数)
- 避免 C 风格转换的歧义
- 花括号减少与函数声明、类型转换的歧义。
何时使用哪种初始化?
-
推荐花括号初始化:
- 默认初始化(如
int x{}代替int x = 0;) - 避免窄化转换
- 初始化容器(如
std::vector) - 解决解析歧义
- 默认初始化(如
-
推荐圆括号初始化:
-
显式调用构造函数(避开
initializer_list优先规则)
std::vector<int> v(3, 5); // 需要3个元素=5,而非{3,5}
-
示例:initializer_list 优先规则
struct Widget {
Widget(int) {} // 普通构造函数
Widget(std::initializer_list<double>) {} // initializer_list
};
Widget w1(10); // 调用普通构造函数(int版本)
Widget w2{10}; // 优先调用 initializer_list(即使需要 int→double 转换)
总结
| 特性 | 花括号初始化 {} | 传统初始化 () 或 = |
|---|---|---|
| 窄化转换 | ❌ 禁止 | ✅ 允许(带警告) |
| 解决解析歧义 | ✅ 安全 | ❌ 可能被解析为函数声明 |
| 统一语法 | ✅ 适用于所有类型 | ❌ 语法不统一 |
initializer_list | ✅ 优先匹配 | ❌ 忽略 |
| 默认初始化 | ✅ 值初始化(零值) | ❌ 局部变量未初始化(危险) |
| 聚合类型初始化 | ✅ 直接初始化成员 | ❌ 不支持 |
花括号初始化在安全和一致性上表现更优,但在需显式调用非-initializer_list构造函数时,仍需使用圆括号。
48、引用限定符(Reference Qualifiers)
核心作用
- 区分左值对象和右值对象上的调用:
- 你可以指定一个成员函数只能在左值对象(如具名变量)上调用。
- 或者指定一个成员函数只能在右值对象(如临时对象、
std::move的结果)上调用。 - 或者为左值对象和右值对象提供不同实现的重载版本。
- 语法:
&:表示该成员函数只能被左值对象调用。&&:表示该成员函数只能被右值对象调用。- 引用限定符必须添加在成员函数声明的参数列表之后,
const限定符(如果存在)之后。
为什么需要引用限定符?****
-
利用右值的临时性进行优化:
-
最经典的例子是实现高效的赋值操作operator=。当你将一个右值赋值给一个对象时,你知道被赋值的对象是临时的、即将销毁的。因此,你可以“窃取”(move)它的资源(如动态内存、文件句柄等),而不是进行昂贵的深度复制。operator=可以这样重载:
class MyClass { public: // 标准赋值,接受左值参数(进行拷贝) MyClass& operator=(const MyClass& other); // 拷贝赋值 // 接受右值参数(进行移动,更高效!) MyClass& operator=(MyClass&& other) noexcept; // 移动赋值 }; -
移动赋值运算符
operator=(MyClass&&)使用了&&,表示它被右值对象调用是没有意义的。我们关心的是参数的左右值性,而不是调用者的。 -
这时候引用限定符就有了用武之地:限定调用者对象的左右值性。
-
-
防止对临时对象进行修改或链式调用:
-
考虑一个返回*this的成员函数:
class Builder { public: Builder& buildStepA() { ...; return *this; } // 返回左值引用 }; -
你可以这样链式调用:
builder.buildStepA().buildStepB().buildStepC();因为返回的是Builder&(左值引用),所以后面的.buildStepB()调用在左值上是合法的。 -
但如果buildStepA()返回的是值(比如实现了移动构造):
class Builder { public: Builder buildStepA() { ...; return std::move(*this); } // 返回一个新对象(右值) }; -
调用
builder.buildStepA().buildStepB();是危险的!因为buildStepA()返回的是一个临时对象(右值)。 -
如果我们定义的.buildStepB()是这样的:
class Builder { public: Builder& buildStepB() & { ...; return *this; } // 只能被左值调用 }; -
那么
builder.buildStepA().buildStepB();会导致编译错误!因为试图在一个右值临时对象上调用一个限定只能在左值上调用的成员函数。这防止了你无意中对一个即将消亡的临时对象进行修改或继续操作。
-
关键例子
#include <iostream>
class MyClass {
public:
// 只能在左值对象上调用
void printName() & {
std::cout << "Called on an lvalue (probably persistent)\n";
}
// 只能在右值对象上调用
void printName() && {
std::cout << "Called on an rvalue (temporary, about to die)\n";
}
// 带const的重载
void printName() const & {
std::cout << "Called on a const lvalue\n";
}
// 注意顺序:引用限定符在 const 之后
};
int main() {
MyClass obj; // 左值
obj.printName(); // 调用 void printName() &
std::move(obj).printName(); // 调用 void printName() &&
const MyClass constObj; // const左值
constObj.printName(); // 调用 void printName() const &
// MyClass().printName(); // 临时对象是右值,会调用 void printName() &&
// 但上面注释行在当前类中会和 void printName() const & 构成重载冲突?
// 更清晰的例子通常仅提供非const的&&版和const &版以避免歧义。
return 0;
}
重要规则与使用场景
- 与
const结合使用: 引用限定符必须放在const限定符之后。可以组合出const &和const &&限定符。 - 重载决议: 编译器根据调用成员函数的对象的左值/右值性质以及
const性质,选择最匹配的限定版本。 - 主要应用场景:
- 强制资源移动(Move-From)状态的后置条件: 如果对象被移动了(通过
operator=(&&)或其它接收右值引用的成员函数),限制对该对象(现在通常处于有效但未指定状态)后续操作的调用。 - 防止对临时对象进行修改: 比如上文的
Builder例子,限制只能在非临时对象(左值)上调用修改状态的方法。 - 实现
std::ref的行为: 其成员函数get()带有&限定,确保你只能从原始左值引用对象获取引用,不能从临时的reference_wrapper对象获取。 - 提升接口安全性和表达性: 更清晰地表达成员函数的使用意图和约束。
- 强制资源移动(Move-From)状态的后置条件: 如果对象被移动了(通过
总结
C++引用限定符&和&&提供了一种强大机制,让类设计者能够:
- 区分处理左值和右值调用者对象。
- 限制某些成员函数只能在特定类型的对象(持久存在的左值或临时右值)上调用。
- 利用临时对象的特性进行优化(如禁用操作)。
- 编写出更安全、意图更明确、效率更高的代码。
理解它们对于编写现代、高效、资源管理清晰的C++代码至关重要,尤其在涉及移动语义和完美转发的场景中。
49、decltype关键字
核心功能
- 获取表达式的声明类型:返回表达式在代码中声明的完整类型(保留所有修饰符)
- 不计算表达式的值:仅分析类型,不会执行表达式
- 编译时操作:结果在编译阶段确定
基本语法
decltype(expression) var; // 声明var为expression的类型
核心推导规则(重要!)
-
变量标识符(无括号)
返回变量的原始声明类型(包含所有 CV 限定符和引用)int x = 10; int& rx = x; const int cx = 20; decltype(x) a = x; // int decltype(rx) b = x; // int& decltype(cx) c = cx; // const int -
函数调用
返回函数返回值的实际类型int func(); int& func_ref(); decltype(func()) d; // int decltype(func_ref()) e; // int& -
带括号的表达式
返回表达式结果的 值类别(value category):- 左值 →
T& - 亡值(xvalue) →
T&& - 纯右值(prvalue) →
T
int x = 0; decltype((x)) f = x; // int& (括号使表达式成为左值) decltype(x++) g; // int (后置++返回右值) decltype(++x) h = x; // int& (前置++返回左值) decltype(std::move(x)) i = std::move(x); // int&& - 左值 →
关键应用场景
-
模板返回类型推导
template<class T, class U> auto add(T t, U u) -> decltype(t + u) { return t + u; } -
声明复杂类型变量
std::map<std::string, std::vector<int>> data; decltype(data)::value_type elem; // std::pair<const string, vector<int>> -
元编程和类型萃取
template<typename T> struct IsPointer { static constexpr bool value = std::is_same_v<decltype(*std::declval<T>()), std::add_lvalue_reference_t<T>>; }; -
配合 auto 的精确控制(decltype(auto) - C++14)
int x = 42; const int& crx = x; auto a = crx; // int (丢弃const和引用) decltype(auto) b = crx; // const int& (保留原类型)
与 auto 的关键区别
| 特性 | auto | decltype |
|---|---|---|
| 推导依据 | 初始化表达式类型 | 给定表达式的声明类型 |
| 引用处理 | 默认丢弃引用 (除非用 auto&) | 保留引用 |
| const 限定 | 丢弃顶层 const | 保留所有 const |
| 数组推导 | 退化为指针 | 保留数组类型 (如 int[5]) |
| 表达式支持 | 仅支持简单表达式 | 支持任意表达式 |
典型用例说明
#include <iostream>
struct Point {
int x, y;
};
Point getPoint() { return {1, 2}; }
Point& getRefPoint() { static Point p{3,4}; return p; }
int main() {
Point p{0,0};
// 规则1: 标识符推导
decltype(p) a = p; // Point
decltype(p.x) b = 10; // int
// 规则2: 函数调用推导
decltype(getPoint()) c; // Point (临时对象)
decltype(getRefPoint()) d = p; // Point&
// 规则3: 表达式推导
decltype((p)) e = p; // Point& (左值)
decltype(Point{5,6}) f; // Point (右值)
// 实际应用:条件类型选择
using Type = decltype(p.x + p.y); // int
}
核心价值总结
- 解决模板编程中的类型依赖问题
- 精确保留表达式的类型特征(引用/const/值类别)
- 实现复杂的编译时类型计算
- 与完美转发配合实现泛型编程
decltype 是现代 C++ 泛型编程的基石之一,尤其在与 std::declval、std::result_of(已弃用)等工具配合使用时,能够构建强大的类型推导系统。它在模板元编程、库开发和复杂类型系统中起着不可替代的作用。
50、delete详解
delete是 C++ 中用于释放动态分配的内存的操作符,必须与 new配对使用。若使用不当,会导致内存泄漏、悬空指针或未定义行为。
核心用法
-
释放单个对象
int* p = new int(10); // 动态分配 delete p; // 释放内存 p = nullptr; // 避免悬空指针 -
释放对象数组
int* arr = new int[5]; // 分配数组 delete[] arr; // 必须用 delete[] arr = nullptr;
关键规则
-
配对使用
new→deletenew Type[N]→delete[]
-
空指针安全
int* p = nullptr; delete p; // 安全,无操作 -
不可重复删除
int* p = new int; delete p; delete p; // 错误!未定义行为(崩溃风险) -
禁止释放非堆内存
int x; int* p = &x; delete p; // 错误!非 new 分配的内存
底层行为
-
调用对象的析构函数(若存在)
class MyClass { public: ~MyClass() { std::cout << "析构\n"; } }; MyClass* obj = new MyClass; delete obj; // 先调用析构函数,再释放内存 -
对于数组:
MyClass* arr = new MyClass[3]; delete[] arr; // 对每个元素调用析构函数(逆序)
常见错误与后果
| 错误类型 | 后果 |
|---|---|
delete忘记调用 | 内存泄漏 |
deletevs delete[]混用 | 部分内存未释放/未定义行为 |
| 访问已释放的内存 | 悬空指针 → 数据损坏/崩溃 |
最佳实践
-
优先使用智能指针
#include <memory> std::unique_ptr<int> p = std::make_unique<int>(10); // 自动释放,无需手动 delete -
释放后置空指针
delete p; p = nullptr; // 避免后续误用 -
RAII 原则
- 资源在构造时分配,在析构时释放(如容器、锁等)。
结论:delete是手动内存管理的核心工具,但易出错。现代 C++ 应优先使用智能指针(unique_ptr, shared_ptr)和容器(vector, string)来自动管理内存,避免直接使用裸 new/delete。
51、类中非静态成员变量初始化注意事项
在C++中,非静态成员变量可以在类定义中直接初始化(C++11起)。以下是关键注意事项及示例:
1. 初始化方式优先级
- 类内初始值(直接初始化):在成员声明时初始化(
= value或{value}) - 构造函数初始化列表:优先级更高,会覆盖类内初始值
- 构造函数体内赋值:不属于初始化,是赋值操作(效率较低)
class Example {
public:
// 类内初始化
int a = 10; // = 初始化
int b{20}; // {} 初始化 (C++11)
// 构造函数
Example() = default; // 使用类内初始值: a=10, b=20
Example(int x) : a(x) {} // a使用参数x初始化,b使用类内值20
Example(int x, int y) : a(x), b(y) {} // 覆盖所有类内初始值
};
2. 初始化顺序
- 严格按声明顺序初始化(与初始化列表顺序无关)
class Danger {
int x = 10;
int y = x + 5; // 危险!x先初始化,y依赖x
public:
Danger() : y(100), x(50) {}
// 实际顺序: x先=10 → y=15 → 然后x被赋值50 → y被赋值100
};
重要:成员初始化顺序应与声明顺序一致,避免依赖未初始化的成员。
3. 特殊成员处理
const成员
class ConstDemo {
const int ID = 100; // ✅ 合法:类内初始化
const int serial;
public:
ConstDemo(int s) : serial(s) {} // 必须初始化
};
引用成员
class RefDemo {
int base;
int& ref = base; // ✅ 类内初始化到base的引用
public:
RefDemo(int x) : base(x) {}
};
类类型成员(无默认构造函数)
class NoDefaultCtor {
int val;
public:
NoDefaultCtor(int v) : val(v) {}
};
class Wrapper {
NoDefaultCtor obj{42}; // ✅ 直接初始化
// NoDefaultCtor obj; // ❌ 错误:无默认构造函数
};
4. 避免的陷阱
歧义初始化 (Most Vexing Parse)
class Problem {
// std::vector<int> v(10); // ❌ 错误:被解析为函数声明
std::vector<int> v1{10}; // ✅ 正确:含1个元素(值为10)
std::vector<int> v2 = std::vector<int>(10); // ✅ 显式调用构造函数
};
静态成员
class StaticDemo {
static int s1; // ❌ 类内初始化非法
static inline int s2 = 42; // ✅ C++17起允许(需inline)
};
int StaticDemo::s1 = 100; // 必须在类外定义
5. C++11前的老代码兼容
- C++11之前只允许静态常量整型成员在类内初始化
class Legacy {
static const int MAX = 100; // ✅ 合法(C++98起)
int count = 0; // ❌ C++03不合法(仅C++11+)
};
最佳实践总结
- 优先使用构造函数初始化列表:显式控制初始化逻辑
- 类内初始值用于默认值:简化代码,减少构造函数重载
- 声明顺序即初始化顺序:保持二者一致
- 特殊成员处理:引用/const成员必须初始化
- 避免歧义:对非基础类型使用
{}或=初始化 - C++17优化:静态成员使用
inline简化定义
正确示例:
class BestPractice { std::string name = "Unknown"; // 合理的默认值 int id{0}; const double PI = 3.14159; std::vector<int> data = {1,2,3}; // 使用初始化列表 public: BestPractice() = default; BestPractice(std::string n) : name(std::move(n)) {} // 仅覆盖name // 成员按声明顺序自动初始化:name→id→PI→data };
11万+

被折叠的 条评论
为什么被折叠?



