C++泛型编程(二):现代C++特性
1.核心语言特性
1.auto类型推导
auto 关键字允许编译器自动推导变量的类型,简化了复杂类型的声明
// 基础类型推导
auto i = 42; // 自动推导为 int
auto str = "hello"; // 自动推导为 const char*
auto pi = 3.14159; // 自动推导为 double// 容器类型推导
std::vector<int> numbers{1, 2, 3};
auto iter = numbers.begin(); // 自动推导为 vector<int>::iterator// 在复杂类型中的应用
std::map<std::string, std::vector<int>> complex_map;
for (const auto& [key, value] : complex_map) { // 结构化绑定配合 auto// 使用 key 和 value
}// 当类型名称冗长或复杂时使用
// 当类型显而易见时,可以显式声明
// 迭代器和 lambda 表达式时推荐使用
2.decltype类型推导
用于推导表达式的类型,常用于泛型编程和后置返回类型
// 基础用法
int x = 42;
decltype(x) y = 100; // y 的类型与 x 相同// 在模板中的应用
template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) { // 后置返回类型return t + u;
}// 与 auto 配合使用
template<typename Container>
auto getValue(Container& c) -> decltype(c.front()) {return c.front();
}// 用于需要依赖表达式类型的场景
// 在模板编程中推导返回类型
// 需要保持类型一致性时使用
3.lambda表达式
lambda 表达式提供了定义匿名函数对象的方式,使代码更加简洁和灵活
// 基本语法:[捕获列表](参数列表) -> 返回类型 { 函数体 }// 简单示例
auto add = [](int a, int b) { return a + b; };
std::cout << add(3, 4) << std::endl; // 输出:7// 捕获外部变量
int multiplier = 10;
auto multiply = [multiplier](int x) { return x * multiplier; }; // 值捕获
auto multiply_ref = [&multiplier](int x) { return x * multiplier; }; // 引用捕获// 在算法中的应用
std::vector<int> numbers = {3, 1, 4, 1, 5, 9};
std::sort(numbers.begin(), numbers.end(), [](int a, int b) { return a > b; }); // 自定义排序规则// 泛型 lambda(C++14)
auto generic_lambda = [](auto x, auto y) { return x + y; };// 短小的函数逻辑适合使用 lambda
// 注意捕获变量的生命周期
// 复杂逻辑建议使用命名函数
4.范围for循环
// 基础容器遍历
std::vector<int> nums = {1, 2, 3, 4, 5};
for (const auto& num : nums) {std::cout << num << " ";
}// 自定义类型遍历
class NumberRange {
public:NumberRange(int start, int end) : start_(start), end_(end) {}class iterator {// 迭代器实现};iterator begin() { return iterator(start_); }iterator end() { return iterator(end_); }
private:int start_, end_;
};// 使用
for (auto num : NumberRange(1, 5)) {std::cout << num << " ";
}
5.nullptr关键字
nullptr 是类型安全的指针空值常量,用来替代传统的 NULL 宏
// 更安全的指针空值表示
void* ptr = nullptr; // 明确表示空指针
std::shared_ptr<int> smart_ptr = nullptr; // 用于智能指针初始化// 函数重载场景
void process(int i) { std::cout << "整数版本" << std::endl; }
void process(int* p) { std::cout << "指针版本" << std::endl; }process(nullptr); // 明确调用指针版本
// process(NULL); // 可能导致编译错误,因为 NULL 可能被解释为 0// 完全替代 NULL 的使用
// 指针初始化时使用
// 在条件判断中使用 if (ptr != nullptr)
6.统一初始化语法
为解决初始化方式不统一、隐式类型转换意外出错等问题,C++使用花括号 {}
提供了统一的初始化语法
// 以前的基本数据类型初始化
int a = 10; // 赋值初始化
int a(10); // 构造函数初始化// 以前的类类型初始化
std::string str("hello"); // 构造函数初始化
std::string str = "hello"; // 赋值初始化// 上述容易让开发者产生混淆// 统一后
// 基本类型初始化
int n{42}; // 直接初始化
std::string str{"hello"}; // 类对象初始化// 容器初始化
std::vector<int> vec{1, 2, 3, 4, 5}; // 列表初始化
std::map<std::string, int> map{{"one", 1},{"two", 2}
};// 自定义类的初始化
class Point {
public:Point(int x, int y) : x_{x}, y_{y} {}
private:int x_{0}; // 成员默认初始化int y_{0};
};Point p{10, 20}; // 统一初始化语法// 优先使用花括号初始化
// 注意防止窄化转换
// 类成员使用默认初始化
7.委托构造函数
允许构造函数调用同一个类的其他构造函数
class Person {
public:// 主构造函数Person(const std::string& name, int age, const std::string& address): name_(name), age_(age), address_(address) {}// 委托构造函数Person(const std::string& name, int age): Person(name, age, "未知") {}Person(const std::string& name): Person(name, 0) {}private:std::string name_;int age_;std::string address_;
};// 减少构造函数代码重复
// 保持初始化逻辑一致性
// 避免构造函数循环委托
8.右值引用和移动语义
两者提供了资源转移而非拷贝的能力
// 移动构造函数
class Buffer {
public:Buffer(size_t size) : data_(new char[size]), size_(size) {}// 移动构造函数Buffer(Buffer&& other) noexcept: data_(other.data_), size_(other.size_) {other.data_ = nullptr;other.size_ = 0;}// 移动赋值运算符Buffer& operator=(Buffer&& other) noexcept {if (this != &other) {delete[] data_;data_ = other.data_;size_ = other.size_;other.data_ = nullptr;other.size_ = 0;}return *this;}private:char* data_;size_t size_;
};// 使用示例
Buffer createBuffer(size_t size) {return Buffer(size); // 返回时触发移动构造
}// 大对象传递时考虑使用移动语义
// 实现移动语义时注意处理原对象状态
// 移动构造函数和移动赋值运算符应声明为 noexcept
2.智能指针
1.定义与分类
C++中的一个类模板,它能够自动管理动态分配的内存,确保在适当的时候自动释放资源。它是RAII(资源获取即初始化)技术的一个具体实现
主要分类为:shared_ptr(共享式智能指针)、unique_ptr(独占式智能指针)、weak_ptr(弱引用智能指针)
2.shared_ptr
允许多个指针指向同一个对象。通过引用计数机制来管理对象的生命周期,当最后一个指向对象的shared_ptr被销毁时,对象会被自动删除
// 基本使用
shared_ptr<int> sp1 = make_shared<int>(10);
shared_ptr<int> sp2 = sp1; // 引用计数+1// 引用计数
cout << sp1.use_count(); // 输出2// 自定义删除器
shared_ptr<int> sp3(new int[10], [](int* p) { delete[] p; });// 循环引用问题
class Node {shared_ptr<Node> next; // 可能造成循环引用
};
3.unique_ptr
同一时刻只能有一个指针指向给定的对象。它对于被管理的对象具有唯一的所有权,不允许复制,但可以移动(std::move())
// 基本使用
unique_ptr<int> ptr1(new int(10));
unique_ptr<int> ptr2 = make_unique<int>(20); // 推荐方式// 所有权转移
unique_ptr<int> ptr3 = std::move(ptr1); // ptr1变为nullptr// 不允许复制
// unique_ptr<int> ptr4 = ptr2; // 编译错误// 自定义删除器
unique_ptr<FILE, decltype(&fclose)> fp(fopen("test.txt", "r"), fclose);
4.weak_ptr
是shared_ptr的助手,它指向shared_ptr管理的对象但不增加引用计数。主要用于解决shared_ptr循环引用的问题
void explainCircularReference() {// 1. 初始创建shared_ptr<Node> node1 = make_shared<Node>(1); // node1引用计数=1shared_ptr<Node> node2 = make_shared<Node>(2); // node2引用计数=1// 2. 建立相互引用node1->next = node2; // node2引用计数+1=2// 因为现在有两个shared_ptr指向node2:// a. 原始的node2// b. node1->nextnode2->prev = node1; // node1引用计数+1=2// 因为现在有两个shared_ptr指向node1:// a. 原始的node1// b. node2->prev// 3. 函数结束时// node1和node2的局部变量被销毁,各自引用计数-1// 但由于相互引用,引用计数仍然为1// node1被node2->prev引用// node2被node1->next引用
} // 引用计数永远不为0,无法释放内存// 解决方案:使用weak_ptr
class Node {
public:shared_ptr<Node> next;weak_ptr<Node> prev; // 使用weak_ptrint data;Node(int val) : data(val) {}~Node() {cout << "Node " << data << " destroyed" << endl;}
};void demonstrateWeakPtr() {// 1. 创建节点shared_ptr<Node> node1 = make_shared<Node>(1); // 引用计数=1shared_ptr<Node> node2 = make_shared<Node>(2); // 引用计数=1// 2. 建立连接node1->next = node2; // node2引用计数+1=2node2->prev = node1; // 使用weak_ptr,node1引用计数不变=1// 3. 访问weak_ptr指向的对象if (auto ptr = node2->prev.lock()) { // 检查weak_ptr是否有效cout << "Node2's prev points to: " << ptr->data << endl;}// 4. 函数结束时// node1的局部变量销毁,引用计数-1=0,node1被删除// node2的局部变量销毁,引用计数-1=1// node1被删除后,node2->prev自动失效// node2的引用计数最终变为0,也被删除
} // 两个节点都能正确释放
5.常用用法
// 容器使用
vector<shared_ptr<int>> vec;
vec.push_back(make_shared<int>(10));// 类成员
class Resource {unique_ptr<int> data;
public:Resource() : data(make_unique<int>(0)) {}
};// 函数返回
unique_ptr<int> createInt() {return make_unique<int>(42);
}
6.注意事项
// 避免原始指针转换
int* raw = new int(10);
shared_ptr<int> sp(raw);
// shared_ptr<int> sp2(raw); // 危险:双重释放// 使用make_函数
auto sp = make_shared<int>(10); // 比 shared_ptr<int>(new int(10)) 更安全// 及时释放
{shared_ptr<int> sp = make_shared<int>(42);
} // sp离开作用域自动释放
3.并发编程
1.std::thread
提供了创建和管理线程的标准方式
#include <thread>
#include <iostream>// 基本线程创建
void worker(int id) {std::cout << "线程 " << id << " 正在执行\n";
}int main() {std::thread t1(worker, 1); // 创建线程std::thread t2([](){ std::cout << "Lambda线程正在执行\n"; });t1.join(); // 等待线程完成t2.join();return 0;
}
2.std::mutex
提供互斥锁机制,保护共享资源
#include <mutex>class ThreadSafeCounter {
public:void increment() {std::lock_guard<std::mutex> lock(mutex_); // RAII方式加锁++count_;}int get() const {std::lock_guard<std::mutex> lock(mutex_);return count_;}private:mutable std::mutex mutex_;int count_{0};
};
3.std::condition_variable
std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
bool done = false;void producer() {// 生产10个数据for (int i = 0; i < 10; ++i) {{ // 作用域锁std::lock_guard<std::mutex> lock(mtx); // 自动加锁解锁data_queue.push(i); // 将数据放入队列} // 离开作用域,自动解锁cv.notify_one(); // 通知一个等待的消费者std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产耗时}// 生产结束,标记完成{std::lock_guard<std::mutex> lock(mtx);done = true; // 设置结束标志}cv.notify_all(); // 通知所有消费者,生产已结束
}// 消费者
void consumer() {while (true) {std::unique_lock<std::mutex> lock(mtx); // 可以手动解锁的锁// 等待条件:队列非空或生产结束cv.wait(lock, [] { return !data_queue.empty() || done; });// 如果生产结束且队列为空,退出循环if (done && data_queue.empty()) {break;}// 获取并处理数据int value = data_queue.front();data_queue.pop();lock.unlock(); // 提前解锁,让其他消费者能够访问队列std::cout << "消费: " << value << std::endl;}
}
4.std::future/promise
异步编程中的数据传递
std::promise<int> promise;
std::future<int> future = promise.get_future();// 异步任务
std::thread worker([&promise] {// 模拟耗时计算std::this_thread::sleep_for(std::chrono::seconds(2));promise.set_value(42);
});// 主线程等待结果
std::cout << "等待结果...\n";
std::cout << "结果: " << future.get() << std::endl;
worker.join();// 使用 async
auto future2 = std::async(std::launch::async, [] {std::this_thread::sleep_for(std::chrono::seconds(2));return 42;
});
4.标准库增加
1.std::tuple
处理异构数据集合
// 创建元组
auto student = std::make_tuple("张三", 20, 3.8);// 获取元素
std::cout << std::get<0>(student) << std::endl; // 姓名
std::cout << std::get<1>(student) << std::endl; // 年龄// 结构化绑定(C++17)
auto [name, age, gpa] = student;// 合并元组
auto info1 = std::make_tuple("张三", 20);
auto info2 = std::make_tuple(3.8, "计算机系");
auto full_info = std::tuple_cat(info1, info2);
2.std::optional
处理可能不存在的值
#include <optional>std::optional<std::string> getUserName(bool hasName) {if (hasName) {return "张三";}return std::nullopt;
}// 使用示例
auto name = getUserName(true);
if (name) {std::cout << "名字: " << *name << std::endl;
}// 提供默认值
std::cout << name.value_or("未知用户") << std::endl;
5.模板元编程
模板元编程是在编译期进行的计算和类型操作,主要用于:
- 编译期计算
- 类型转换和判断
- 代码生成
1.计算示例
// 1.编译期计算阶乘
template<unsigned N>
struct Factorial {static constexpr unsigned value = N * Factorial<N-1>::value;
};
// 递归终止
template<>
struct Factorial<0> {static constexpr unsigned value = 1;
};
// 使用
constexpr auto result = Factorial<5>::value; // 5! = 120// 2.斐波那契数列
template<unsigned N>
struct Fibonacci {static constexpr unsigned value = Fibonacci<N-1>::value + Fibonacci<N-2>::value;
};
template<>
struct Fibonacci<0> {static constexpr unsigned value = 0;
};
template<>
struct Fibonacci<1> {static constexpr unsigned value = 1;
};
2.类型操作
// 1.判断类型是否为指针
template<typename T>
struct IsPointer {static constexpr bool value = false;
};// 偏特化
template<typename T>
struct IsPointer<T*> {static constexpr bool value = true;
};// 2.移除引用
template<typename T>
struct RemoveReference {using type = T;
};template<typename T>
struct RemoveReference<T&> {using type = T;
};template<typename T>
struct RemoveReference<T&&> {using type = T;
};
3.注意事项
- 编译时间:复杂的模板元编程会增加编译时间、应适度使用,避免过度复杂化
- 可读性:模板元编程代码往往难以理解、需要良好的文档和注释
- 调试难度:编译期错误信息复杂、调试工具支持有限
- 维护成本:代码复杂度高、修改需要谨慎
6.性能优化
1. 移动语义
- 通过移动而非拷贝来转移资源所有权
- 避免不必要的深拷贝操作
- 使用右值引用(&&)实现
class String {
public:// 移动构造函数String(String&& other) noexcept : data_(other.data_) {other.data_ = nullptr; // 源对象置空}// 移动赋值运算符String& operator=(String&& other) noexcept {if (this != &other) {delete[] data_;data_ = other.data_;other.data_ = nullptr;}return *this;}private:char* data_;
};// 使用场景
String createString() {return String("hello"); // 自动触发移动 -- 使用右值版本:String(String&& other) noexcept
}
std::vector<String> vec;
vec.push_back(String("hello")); // 移动而非拷贝
2.完美转发
- 保持参数的值类别(左值/右值)
- 通过 std::forward 实现
- 用于泛型编程
template<typename T>
void wrapper(T&& param) { // 完美转发参数process(std::forward<T>(param));
}// 多参数版本
template<typename... Args>
void wrapper(Args&&... args) {process(std::forward<Args>(args)...);
}// 实现原理void wrapper(T&& param) // T&& 这里是通用引用,不是右值引用
// 使得当传入左值时,T 被推导为左值引用类型;当传入右值时,T 被推导为普通类型// 假设有个 int 类型变量 x
int x = 42;
wrapper(x); // T 被推导为 int&,arg 类型为 int&
wrapper(42); // T 被推导为 int,arg 类型为 int&&// 当 T 为 int& 时(传入左值):
// T&& => int& && => int& // 折叠为左值引用
// 当 T 为 int 时(传入右值):
// T&& => int&& => int&& // 保持为右值引用
3.CRTP
CRTP (Curiously Recurring Template Pattern,奇异递归模板模式) 主要用于实现静态多态
// 代码示例
// CRTP实现静态多态
template<typename Derived>
class Shape {
public:double area() {return static_cast<Derived*>(this)->computeArea();}
};class Circle : public Shape<Circle> {
public:Circle(double r) : radius(r) {}double computeArea() { return 3.14 * radius * radius; }
private:double radius;
};
通俗来讲,想象你在设计一个游戏,里面有不同类型的角色(战士、法师、弓箭手),他们都需要一个"攻击"的功能
// 传统方式 -- 虚函数
class 角色基类 {
public:virtual void 攻击() = 0; // 每次调用都要查表,比较慢
};class 战士 : public 角色基类 {void 攻击() override { /* 挥剑 */ }
};// CRTP方式
template<typename 具体角色>
class 角色基类 {
public:void 攻击() {// 直接知道要调用哪个具体角色的攻击方法,不需要查表static_cast<具体角色*>(this)->具体攻击();}
};class 战士 : public 角色基类<战士> {
public:void 具体攻击() { /* 挥剑 */ }
};
// CRTP方式就像是提前做好了"攻击招式说明书",不用临时翻书查找,但是一旦写好就不能临时(动态)改变了
7.其他
本号文章仅为个人收集总结,强烈欢迎大佬与同好指误或讨论 ^^