当前位置: 首页 > news >正文

C++学习:六个月从基础到就业——模板编程:类模板

C++学习:六个月从基础到就业——模板编程:类模板

本文是我C++学习之旅系列的第三十三篇技术文章,也是第二阶段"C++进阶特性"的第十一篇,主要介绍C++中的类模板编程。查看完整系列目录了解更多内容。
在这里插入图片描述


在这里插入图片描述


目录

  • 引言
  • 类模板的基本语法
    • 类模板定义
    • 类模板实例化
  • 类模板的成员函数
    • 成员函数定义
    • 成员函数模板
  • 类模板的特化
    • 全特化
    • 偏特化
  • 默认模板参数
  • 模板的继承与组合
    • 模板类的继承
    • 模板与组合
  • 类模板的嵌套
  • 类型萃取和SFINAE
    • 类型萃取
    • SFINAE技术
  • 可变参数模板类
  • 类模板实例化控制
  • 实际应用案例
    • 通用智能指针
    • 泛型事件系统
  • 类模板的最佳实践
    • 接口设计
    • 模板约束
    • 避免常见问题
  • 总结
  • 参考资源

引言

在上一篇文章中,我们深入探讨了C++函数模板的概念和应用。本文将继续模板编程的旅程,重点讨论类模板,这是C++泛型编程的另一个强大工具。

类模板允许我们创建通用的类,这些类能够处理各种数据类型,而不需要为每种类型编写单独的类定义。C++标准库中的容器(如std::vectorstd::map)就是类模板的典型应用。通过类模板,我们可以编写一次代码,却能创建适用于不同数据类型的类,大大提高了代码的复用性和灵活性。

本文将详细介绍类模板的定义和使用方法、类模板的成员函数实现、模板特化、默认模板参数以及类模板的嵌套与继承等高级特性。我们还将通过实际案例展示类模板在C++编程中的应用。

类模板的基本语法

类模板定义

类模板的定义使用template关键字,后跟尖括号中的模板参数列表,然后是类的定义:

template <typename T>  // 或 template <class T>
class MyContainer {
private:T* elements;size_t count;size_t capacity;public:MyContainer(size_t initialCapacity = 10);~MyContainer();void push_back(const T& element);T& at(size_t index);const T& at(size_t index) const;size_t size() const { return count; }
};

这里定义了一个简单的容器类模板MyContainer,可以存储任意类型的元素。模板参数T在类定义中用作占位符,表示将来使用类模板时会被具体类型替换。

类模板实例化

要使用类模板,需要指定模板参数的具体类型:

int main() {// 创建存储int的容器MyContainer<int> intContainer;intContainer.push_back(42);intContainer.push_back(73);// 创建存储string的容器MyContainer<std::string> stringContainer;stringContainer.push_back("Hello");stringContainer.push_back("World");std::cout << "intContainer[0] = " << intContainer.at(0) << std::endl;std::cout << "stringContainer[0] = " << stringContainer.at(0) << std::endl;return 0;
}

当我们声明MyContainer<int>时,编译器会生成一个将T替换为int的类定义。同样,MyContainer<std::string>会生成另一个将T替换为std::string的类定义。这个过程称为类模板实例化

类模板的成员函数

成员函数定义

类模板的成员函数可以在类内定义,也可以在类外定义。在类外定义时,需要使用模板前缀和类限定符:

// 类内定义(如前面例子中的size()函数)
template <typename T>
class MyContainer {
public:size_t size() const { return count; }  // 直接在类内定义// ...其他声明
};// 类外定义
template <typename T>
MyContainer<T>::MyContainer(size_t initialCapacity) : elements(new T[initialCapacity]), count(0), capacity(initialCapacity) {
}template <typename T>
MyContainer<T>::~MyContainer() {delete[] elements;
}template <typename T>
void MyContainer<T>::push_back(const T& element) {if (count == capacity) {// 扩展容量capacity *= 2;T* newElements = new T[capacity];for (size_t i = 0; i < count; ++i) {newElements[i] = elements[i];}delete[] elements;elements = newElements;}elements[count++] = element;
}template <typename T>
T& MyContainer<T>::at(size_t index) {if (index >= count) {throw std::out_of_range("Index out of bounds");}return elements[index];
}template <typename T>
const T& MyContainer<T>::at(size_t index) const {if (index >= count) {throw std::out_of_range("Index out of bounds");}return elements[index];
}

在类外定义成员函数时,每个函数定义前都必须有完整的模板声明,并且在函数名前使用MyContainer<T>::限定符。

成员函数模板

类模板的成员函数本身也可以是模板,这称为成员函数模板

template <typename T>
class MyContainer {// ...其他成员public:// 成员函数模板template <typename Func>void forEach(Func func) {for (size_t i = 0; i < count; ++i) {func(elements[i]);}}// 另一个成员函数模板template <typename U>void appendFrom(const MyContainer<U>& other) {for (size_t i = 0; i < other.size(); ++i) {// 需要U能转换为Tpush_back(static_cast<T>(other.at(i)));}}
};

使用成员函数模板:

int main() {MyContainer<int> intContainer;intContainer.push_back(1);intContainer.push_back(2);intContainer.push_back(3);// 使用forEach和lambda表达式打印元素intContainer.forEach([](const int& value) {std::cout << value << " ";});std::cout << std::endl;// 从double容器向int容器添加元素MyContainer<double> doubleContainer;doubleContainer.push_back(1.1);doubleContainer.push_back(2.2);intContainer.appendFrom(doubleContainer);  // 将double转换为intreturn 0;
}

类模板的特化

类模板可以通过特化为特定类型提供专门的实现。这在类型有特殊需求或可以优化时非常有用。

全特化

类模板的全特化为特定类型提供完全不同的实现:

// 主模板
template <typename T>
class Storage {
private:T data;
public:Storage(const T& value) : data(value) {}void print() const {std::cout << "Generic Storage: " << data << std::endl;}T& getData() { return data; }
};// 为bool类型的全特化
template <>
class Storage<bool> {
private:unsigned char data;  // 使用一个字节存储多个bool值static const unsigned char mask = 1;  // 掩码
public:Storage(bool value) : data(value ? mask : 0) {}void print() const {std::cout << "Bool Storage: " << (data ? "true" : "false") << std::endl;}bool getData() { return data & mask; }void setData(bool value) { if (value)data |= mask;  // 设置位elsedata &= ~mask;  // 清除位}
};

使用特化的类模板:

int main() {Storage<int> intStorage(42);intStorage.print();  // 使用通用版本Storage<bool> boolStorage(true);boolStorage.print();  // 使用bool特化版本boolStorage.setData(false);std::cout << "Changed value: " << (boolStorage.getData() ? "true" : "false") << std::endl;return 0;
}

偏特化

类模板的偏特化允许为模板参数的某个子集提供特化:

// 主模板
template <typename T, typename U>
class Pair {
private:T first;U second;
public:Pair(const T& t, const U& u) : first(t), second(u) {}void print() const {std::cout << "Generic Pair: " << first << ", " << second << std::endl;}
};// 两个类型相同的偏特化
template <typename T>
class Pair<T, T> {
private:T first;T second;
public:Pair(const T& t1, const T& t2) : first(t1), second(t2) {}void print() const {std::cout << "Same-type Pair: " << first << ", " << second << std::endl;}bool areEqual() const { return first == second; }
};// 指针类型的偏特化
template <typename T, typename U>
class Pair<T*, U*> {
private:T* first;U* second;
public:Pair(T* t, U* u) : first(t), second(u) {}void print() const {std::cout << "Pointer Pair: " << *first << ", " << *second << std::endl;}
};

使用偏特化的类模板:

int main() {// 使用通用版本Pair<int, double> p1(42, 3.14);p1.print();// 使用相同类型的偏特化Pair<int, int> p2(10, 20);p2.print();std::cout << "Are equal: " << (p2.areEqual() ? "yes" : "no") << std::endl;// 使用指针类型的偏特化int x = 100, y = 200;Pair<int, int> p3(&x, &y);p3.print();return 0;
}

默认模板参数

类模板可以为模板参数指定默认值,简化模板的使用:

template <typename T, typename Container = std::vector<T>, typename Compare = std::less<T>>
class PriorityQueue {
private:Container data;Compare comp;public:PriorityQueue() = default;void push(const T& value) {data.push_back(value);std::push_heap(data.begin(), data.end(), comp);}T pop() {T top = data.front();std::pop_heap(data.begin(), data.end(), comp);data.pop_back();return top;}const T& top() const {return data.front();}bool empty() const {return data.empty();}size_t size() const {return data.size();}
};

使用默认模板参数:

int main() {// 使用默认参数:vector<int>容器和less<int>比较器PriorityQueue<int> minHeap;minHeap.push(3);minHeap.push(1);minHeap.push(4);while (!minHeap.empty()) {std::cout << minHeap.pop() << " "; // 输出:1 3 4}std::cout << std::endl;// 使用自定义比较器:最大堆PriorityQueue<int, std::vector<int>, std::greater<int>> maxHeap;maxHeap.push(3);maxHeap.push(1);maxHeap.push(4);while (!maxHeap.empty()) {std::cout << maxHeap.pop() << " "; // 输出:4 3 1}std::cout << std::endl;return 0;
}

模板的继承与组合

模板类的继承

模板类可以继承自非模板类、其他模板类或者自身的特化:

// 基类模板
template <typename T>
class Container {
protected:T* data;size_t size;public:Container(size_t s = 0) : size(s), data(s > 0 ? new T[s] : nullptr) {}virtual ~Container() { delete[] data; }size_t getSize() const { return size; }virtual void print() const = 0;
};// 派生类模板从基类模板继承
template <typename T>
class Array : public Container<T> {
public:Array(size_t s) : Container<T>(s) {}T& operator[](size_t index) {if (index >= this->size) throw std::out_of_range("Index out of bounds");return this->data[index];}void print() const override {std::cout << "Array: ";for (size_t i = 0; i < this->size; ++i) {std::cout << this->data[i] << " ";}std::cout << std::endl;}
};// 模板类派生自非模板类
class Shape {
protected:std::string name;
public:Shape(const std::string& n) : name(n) {}virtual ~Shape() = default;virtual double area() const = 0;const std::string& getName() const { return name; }
};template <typename T>
class Circle : public Shape {
private:T radius;
public:Circle(const std::string& name, T r) : Shape(name), radius(r) {}double area() const override {return 3.14159 * radius * radius;}
};

使用继承的模板类:

int main() {// Array继承自ContainerArray<int> intArray(5);for (size_t i = 0; i < intArray.getSize(); ++i) {intArray[i] = i * 10;}intArray.print();// Circle继承自ShapeCircle<double> circle("My Circle", 2.5);std::cout << circle.getName() << " has area: " << circle.area() << std::endl;return 0;
}

模板与组合

模板类也经常使用组合(而非继承)来实现代码复用:

// 使用组合的Stack模板
template <typename T, typename Container = std::vector<T>>
class Stack {
private:Container container;  // 组合一个容器public:void push(const T& value) {container.push_back(value);}void pop() {if (empty()) throw std::underflow_error("Stack is empty");container.pop_back();}const T& top() const {if (empty()) throw std::underflow_error("Stack is empty");return container.back();}bool empty() const {return container.empty();}size_t size() const {return container.size();}
};

使用组合的模板类:

int main() {// 默认使用vector作为底层容器Stack<int> stack1;stack1.push(1);stack1.push(2);stack1.push(3);std::cout << "Stack with vector: ";while (!stack1.empty()) {std::cout << stack1.top() << " ";stack1.pop();}std::cout << std::endl;// 使用deque作为底层容器Stack<int, std::deque<int>> stack2;stack2.push(4);stack2.push(5);stack2.push(6);std::cout << "Stack with deque: ";while (!stack2.empty()) {std::cout << stack2.top() << " ";stack2.pop();}std::cout << std::endl;return 0;
}

类模板的嵌套

类模板可以包含嵌套的类、结构体或者枚举,这些嵌套类型也可以依赖外部模板参数:

template <typename T>
class OuterTemplate {
public:// 嵌套的类模板template <typename U>class NestedTemplate {private:T outerValue;U innerValue;public:NestedTemplate(const T& t, const U& u) : outerValue(t), innerValue(u) {}void print() const {std::cout << "Outer value: " << outerValue << ", Inner value: " << innerValue << std::endl;}};// 依赖于T的嵌套类class NestedClass {private:T value;public:NestedClass(const T& v) : value(v) {}void print() const {std::cout << "Nested value: " << value << std::endl;}};// 嵌套枚举(C++11起可以指定底层类型)enum class Status : int {Success = 0,Failure = 1,Pending = 2};
};

使用嵌套类模板:

int main() {// 使用嵌套的类模板OuterTemplate<int>::NestedTemplate<std::string> nested(42, "Hello");nested.print();// 使用依赖于外部模板参数的嵌套类OuterTemplate<double>::NestedClass nestedObj(3.14);nestedObj.print();// 使用嵌套枚举auto status = OuterTemplate<int>::Status::Success;std::cout << "Status value: " << static_cast<int>(status) << std::endl;return 0;
}

类型萃取和SFINAE

类模板在实现通用代码时,常常需要对不同类型做特殊处理。类型萃取(Type Traits)和SFINAE(Substitution Failure Is Not An Error)是用于此目的的重要技术。

类型萃取

类型萃取允许我们在编译时检查和修改类型特性:

#include <type_traits>// 使用类型萃取实现通用容器
template <typename T>
class SafeContainer {
private:std::vector<T> data;// 使用SFINAE选择合适的初始化函数template <typename U = T>typename std::enable_if<std::is_default_constructible<U>::value>::typeinitialize(size_t size) {data.resize(size);  // T可以默认构造,直接调用resize}template <typename U = T>typename std::enable_if<!std::is_default_constructible<U>::value>::typeinitialize(size_t size) {// T不可默认构造,不调整大小,仅预留空间data.reserve(size);}public:SafeContainer(size_t size = 0) {initialize<T>(size);}void add(const T& value) {data.push_back(value);}size_t size() const {return data.size();}// 基于类型特性提供特定功能template <typename U = T>typename std::enable_if<std::is_arithmetic<U>::value, U>::typesum() const {U result = U();for (const auto& item : data) {result += item;}return result;}
};

使用结合类型萃取的容器:

class NonDefaultConstructible {
private:int value;
public:NonDefaultConstructible(int v) : value(v) {}// 没有默认构造函数
};int main() {// 使用可默认构造的类型SafeContainer<int> intContainer(5);intContainer.add(10);intContainer.add(20);std::cout << "Int container size: " << intContainer.size() << std::endl;std::cout << "Int container sum: " << intContainer.sum() << std::endl;// 使用不可默认构造的类型SafeContainer<NonDefaultConstructible> customContainer;customContainer.add(NonDefaultConstructible(5));customContainer.add(NonDefaultConstructible(10));std::cout << "Custom container size: " << customContainer.size() << std::endl;// customContainer.sum() 会导致编译错误,因为NonDefaultConstructible不是算术类型return 0;
}

SFINAE技术

SFINAE允许我们根据类型特性提供不同的模板特化:

#include <iostream>
#include <type_traits>
#include <vector>
#include <list>// 检测容器是否有随机访问迭代器
template <typename Container>
class ContainerTraits {
private:// 测试函数,只有当迭代器为随机访问迭代器时有效template <typename C>static constexpr auto test(int) -> decltype(typename C::iterator() += 1,  // 随机访问迭代器支持+=操作std::true_type{});// 匹配任意类型的后备函数template <typename>static constexpr std::false_type test(...);public:// value为true表示容器有随机访问迭代器static constexpr bool has_random_access = decltype(test<Container>(0))::value;
};// 根据容器特性选择最优的算法
template <typename Container>
void process(Container& container) {if constexpr (ContainerTraits<Container>::has_random_access) {std::cout << "Using fast algorithm for random access container" << std::endl;// 实现依赖随机访问的快速算法} else {std::cout << "Using general algorithm for sequential access container" << std::endl;// 实现适用于顺序访问的通用算法}
}

使用SFINAE的容器处理:

int main() {std::vector<int> vec = {1, 2, 3, 4, 5};std::list<int> lst = {1, 2, 3, 4, 5};std::cout << "Vector has random access: " << (ContainerTraits<std::vector<int>>::has_random_access ? "yes" : "no") << std::endl;std::cout << "List has random access: " << (ContainerTraits<std::list<int>>::has_random_access ? "yes" : "no") << std::endl;process(vec);  // 使用快速算法process(lst);  // 使用一般算法return 0;
}

可变参数模板类

C++11引入了可变参数模板,允许接受任意数量和类型的模板参数:

// 可变参数类模板
template <typename... Types>
class Tuple;// 基本情况:空元组
template <>
class Tuple<> {
public:static constexpr size_t size = 0;void print() const {std::cout << "()" << std::endl;}
};// 递归情况:首元素 + 剩余元素元组
template <typename Head, typename... Tail>
class Tuple<Head, Tail...> : private Tuple<Tail...> {
private:Head head;// 基类类型别名using Base = Tuple<Tail...>;public:static constexpr size_t size = 1 + Base::size;// 构造函数Tuple(const Head& h, const Tail&... tail): Base(tail...), head(h) {}// 获取首元素const Head& getHead() const {return head;}// 获取尾元组const Base& getTail() const {return *this;}// 递归打印元组内容void print() const {std::cout << "(" << head;printRest();std::cout << ")" << std::endl;}private:void printRest() const {const Base& tail = getTail();// 检查尾元组是否为空if constexpr (Base::size > 0) {std::cout << ", ";// 访问尾元组的getHead()方法std::cout << tail.getHead();// 递归处理剩余元素tail.printRest();}}
};

使用可变参数类模板:

int main() {// 创建不同类型的元组Tuple<int, double, std::string> t1(42, 3.14, "hello");std::cout << "Tuple t1: ";t1.print();std::cout << "Size of t1: " << decltype(t1)::size << std::endl;// 创建单元素元组Tuple<char> t2('A');std::cout << "Tuple t2: ";t2.print();// 创建空元组Tuple<> t3;std::cout << "Tuple t3: ";t3.print();return 0;
}

类模板实例化控制

类模板的每次实例化都会生成代码,这可能导致代码膨胀。通过显式实例化和外部模板声明,可以控制实例化的位置和次数:

// 在头文件中声明模板
// mycontainer.h
template <typename T>
class MyContainer {
private:std::vector<T> data;public:void add(const T& value);const T& get(size_t index) const;size_t size() const;
};// 实现模板
// mycontainer.cpp
template <typename T>
void MyContainer<T>::add(const T& value) {data.push_back(value);
}template <typename T>
const T& MyContainer<T>::get(size_t index) const {return data.at(index);
}template <typename T>
size_t MyContainer<T>::size() const {return data.size();
}// 显式实例化常用类型,减少代码重复
template class MyContainer<int>;
template class MyContainer<double>;
template class MyContainer<std::string>;// 在其他文件中使用外部模板声明
// main.cpp
extern template class MyContainer<int>;
extern template class MyContainer<double>;int main() {MyContainer<int> intContainer;intContainer.add(42);MyContainer<double> doubleContainer;doubleContainer.add(3.14);// 这将导致新的实例化,因为没有显式实例化MyContainer<char> charContainer;charContainer.add('A');return 0;
}

实际应用案例

让我们看一些类模板的实际应用案例:

通用智能指针

实现一个简单的智能指针模板类:

#include <iostream>
#include <cstddef>// 定义删除器类型
template <typename T>
struct DefaultDeleter {void operator()(T* ptr) const {delete ptr;}
};template <typename T>
struct ArrayDeleter {void operator()(T* ptr) const {delete[] ptr;}
};// 自定义智能指针模板
template <typename T, typename Deleter = DefaultDeleter<T>>
class UniquePtr {
private:T* ptr;Deleter deleter;// 禁止拷贝UniquePtr(const UniquePtr&) = delete;UniquePtr& operator=(const UniquePtr&) = delete;public:// 构造函数explicit UniquePtr(T* p = nullptr, Deleter d = Deleter()): ptr(p), deleter(std::move(d)) {}// 移动构造函数UniquePtr(UniquePtr&& other) noexcept: ptr(other.ptr), deleter(std::move(other.deleter)) {other.ptr = nullptr;}// 移动赋值运算符UniquePtr& operator=(UniquePtr&& other) noexcept {if (this != &other) {reset();ptr = other.ptr;deleter = std::move(other.deleter);other.ptr = nullptr;}return *this;}// 析构函数~UniquePtr() {reset();}// 释放资源void reset(T* p = nullptr) {if (ptr != nullptr) {deleter(ptr);}ptr = p;}// 放弃所有权T* release() {T* temp = ptr;ptr = nullptr;return temp;}// 访问指针T* get() const {return ptr;}// 解引用操作符T& operator*() const {return *ptr;}// 成员访问操作符T* operator->() const {return ptr;}// 布尔转换explicit operator bool() const {return ptr != nullptr;}// 交换void swap(UniquePtr& other) noexcept {std::swap(ptr, other.ptr);std::swap(deleter, other.deleter);}
};// 为数组类型特化make_unique
template <typename T, typename... Args>
UniquePtr<T> make_unique(Args&&... args) {return UniquePtr<T>(new T(std::forward<Args>(args)...));
}template <typename T>
UniquePtr<T, ArrayDeleter<T>> make_unique_array(size_t size) {return UniquePtr<T, ArrayDeleter<T>>(new T[size]);
}

使用自定义智能指针:

class Resource {
private:std::string name;
public:Resource(const std::string& n = "Unnamed") : name(n) {std::cout << "Resource " << name << " created" << std::endl;}~Resource() {std::cout << "Resource " << name << " destroyed" << std::endl;}void use() const {std::cout << "Using resource " << name << std::endl;}
};int main() {// 创建智能指针UniquePtr<Resource> res1(new Resource("First"));// 使用make_uniqueauto res2 = make_unique<Resource>("Second");// 使用资源res1->use();(*res2).use();// 移动所有权UniquePtr<Resource> res3 = std::move(res1);// res1现在为空if (!res1) {std::cout << "res1 is empty" << std::endl;}// res3持有原res1的资源if (res3) {res3->use();}// 数组版本auto numbers = make_unique_array<int>(5);// 函数结束时,所有智能指针销毁,释放资源return 0;
}

泛型事件系统

实现一个简单的类型安全事件系统:

#include <iostream>
#include <string>
#include <functional>
#include <unordered_map>
#include <vector>
#include <memory>
#include <any>// 事件基类
class EventBase {};// 泛型事件类
template <typename... Args>
class Event : public EventBase {
public:using HandlerFunc = std::function<void(Args...)>;using HandlerId = size_t;private:std::unordered_map<HandlerId, HandlerFunc> handlers;HandlerId nextId = 0;public:// 注册处理器HandlerId addHandler(HandlerFunc handler) {HandlerId id = nextId++;handlers[id] = std::move(handler);return id;}// 移除处理器void removeHandler(HandlerId id) {handlers.erase(id);}// 触发事件void trigger(Args... args) const {for (const auto& [id, handler] : handlers) {handler(args...);}}// 获取处理器数量size_t handlerCount() const {return handlers.size();}
};// 事件分发器
class EventDispatcher {
private:std::unordered_map<std::string, std::shared_ptr<EventBase>> events;public:// 获取事件(如果不存在则创建)template <typename... Args>Event<Args...>& getEvent(const std::string& name) {auto it = events.find(name);if (it == events.end()) {auto event = std::make_shared<Event<Args...>>();events[name] = event;return *event;}// 尝试将事件转换为正确的类型auto* typedEvent = dynamic_cast<Event<Args...>*>(it->second.get());if (!typedEvent) {throw std::runtime_error("Event type mismatch");}return *typedEvent;}// 触发事件template <typename... Args>void dispatchEvent(const std::string& name, Args... args) {auto& event = getEvent<Args...>(name);event.trigger(args...);}
};

使用泛型事件系统:

class Player {
private:std::string name;int health;public:Player(const std::string& n, int h) : name(n), health(h) {}void takeDamage(int amount) {health -= amount;std::cout << name << " takes " << amount << " damage. Health: " << health << std::endl;}void heal(int amount) {health += amount;std::cout << name << " heals for " << amount << ". Health: " << health << std::endl;}const std::string& getName() const { return name; }int getHealth() const { return health; }
};int main() {EventDispatcher dispatcher;// 定义并注册各种事件using DamageEvent = Event<Player&, int>;using HealEvent = Event<Player&, int>;using GameOverEvent = Event<const std::string&>;// 创建玩家Player player("Hero", 100);// 注册伤害事件处理器dispatcher.getEvent<Player&, int>("damage").addHandler([](Player& p, int amount) {p.takeDamage(amount);std::cout << "Damage event handled!" << std::endl;});// 注册治疗事件处理器dispatcher.getEvent<Player&, int>("heal").addHandler([](Player& p, int amount) {p.heal(amount);std::cout << "Heal event handled!" << std::endl;});// 注册游戏结束事件处理器dispatcher.getEvent<const std::string&>("gameOver").addHandler([](const std::string& message) {std::cout << "Game over: " << message << std::endl;});// 触发一些事件dispatcher.dispatchEvent("damage", player, 30);dispatcher.dispatchEvent("heal", player, 15);dispatcher.dispatchEvent("damage", player, 95);// 检查玩家状态if (player.getHealth() <= 0) {dispatcher.dispatchEvent("gameOver", "Player has been defeated!");}return 0;
}

类模板的最佳实践

接口设计

  1. 简洁明了的模板参数:只使用必要的模板参数,并为常用场景提供默认参数。
  2. 分离接口与实现:将模板的声明和实现分开,提高代码可读性。
  3. 提供清晰的文档:注明模板参数的预期类型和约束条件。
// 良好设计的模板类
template <typename T, typename Allocator = std::allocator<T>, typename Compare = std::less<T>>
class SortedContainer {
public:// 类型别名提高可读性using value_type = T;using allocator_type = Allocator;using compare_type = Compare;using size_type = std::size_t;// 构造函数explicit SortedContainer(const Compare& comp = Compare{});// 主要接口方法void insert(const T& value);bool contains(const T& value) const;size_type size() const;bool empty() const;// 迭代器using iterator = /* ... */;using const_iterator = /* ... */;iterator begin();iterator end();const_iterator begin() const;const_iterator end() const;const_iterator cbegin() const;const_iterator cend() const;// 对于每个必要的操作都提供文档/*** 插入值并保持容器排序* @param value 要插入的值* @complexity O(log n) for lookup, O(n) for insertion*/void insert(const T& value);// ... 其他实现 ...
};

模板约束

使用适当的技术确保模板参数满足必要的要求:

// 使用C++20概念
template <typename T>
concept Sortable = requires(T a, T b) {{ a < b } -> std::convertible_to<bool>;{ a = b } -> std::same_as<T&>;
};template <Sortable T>
class SortedVector {// 实现...
};// 在C++17及更早版本中使用SFINAE
template <typename T, typename = std::enable_if_t<std::is_copy_assignable_v<T> &&std::is_copy_constructible_v<T> &&std::is_default_constructible_v<T>>>
class SafeContainer {// 实现...
};

避免常见问题

  1. 过多的模板参数:限制模板参数的数量,使类易于使用。
  2. 不必要的依赖:避免在模板中包含不依赖于模板参数的代码。
  3. 编译时间膨胀:使用技术如显式实例化和外部模板声明减少代码重复。
// 不好的设计:过多的模板参数
template <typename T, typename U, typename V, typename W, typename Allocator = std::allocator<T>>
class OverTemplated { /* ... */ };// 更好的设计:使用嵌套或组合
template <typename Key, typename Value>
class Dictionary {
private:struct Entry {Key key;Value value;};std::vector<Entry> entries;public:// 更简洁的接口
};// 不好的设计:不依赖模板参数的代码
template <typename T>
class Logger {
public:void log(const std::string& message) {// 这个方法不使用Tstd::cout << message << std::endl;}void logValue(const T& value) {std::cout << value << std::endl;}
};// 更好的设计:分离不依赖模板参数的代码
class LoggerBase {
public:void log(const std::string& message) {std::cout << message << std::endl;}
};template <typename T>
class TypedLogger : public LoggerBase {
public:void logValue(const T& value) {std::cout << value << std::endl;}
};

总结

类模板是C++泛型编程的核心组件之一,它允许我们创建能够处理各种数据类型的通用类。通过类模板,我们可以实现代码复用、类型安全和高效的容器和算法。

本文中,我们探讨了:

  1. 类模板的基本语法:定义和实例化类模板的方法
  2. 类模板的成员函数:在类内和类外定义成员函数
  3. 类模板的特化:为特定类型提供专门实现的全特化和偏特化
  4. 默认模板参数:简化模板使用的方法
  5. 模板的继承与组合:通过继承和组合使用模板
  6. 类模板的嵌套:在模板中定义嵌套类型
  7. 类型萃取和SFINAE:根据类型特性提供不同实现
  8. 可变参数模板类:创建接受任意数量参数的模板
  9. 类模板实例化控制:管理模板代码膨胀的技术
  10. 实际应用案例:智能指针和事件系统实现
  11. 最佳实践:设计良好模板类的指导原则

掌握类模板是成为高级C++程序员的关键一步。随着我们继续探索模板编程,接下来我们将深入学习模板特化技术,这将使我们能够为特定类型提供更加高效或专门化的实现。

参考资源

  • C++ Reference
  • 《C++ Templates: The Complete Guide》by David Vandevoorde and Nicolai M. Josuttis
  • 《Modern C++ Design》by Andrei Alexandrescu
  • 《Effective Modern C++》by Scott Meyers
  • 《C++ Core Guidelines》by Bjarne Stroustrup and Herb Sutter

在这里插入图片描述


这是我C++学习之旅系列的第三十三篇技术文章。查看完整系列目录了解更多内容。

如有任何问题或建议,欢迎在评论区留言交流!

相关文章:

C++学习:六个月从基础到就业——模板编程:类模板

C学习&#xff1a;六个月从基础到就业——模板编程&#xff1a;类模板 本文是我C学习之旅系列的第三十三篇技术文章&#xff0c;也是第二阶段"C进阶特性"的第十一篇&#xff0c;主要介绍C中的类模板编程。查看完整系列目录了解更多内容。 目录 引言类模板的基本语法…...

Conda 虚拟环境复用

文章目录 一、导出环境配置二、克隆环境配置三、区别小结 一、导出环境配置 导出&#xff1a;将当前虚拟环境导出成一个yml配置文件。conda activate your_env conda env export > your_env.yml导入&#xff1a;基于yml文件创建新环境&#xff0c;会自动按照yml里的配置&am…...

Nacos简介—4.Nacos架构和原理三

大纲 1.Nacos的定位和优势 2.Nacos的整体架构 3.Nacos的配置模型 4.Nacos内核设计之一致性协议 5.Nacos内核设计之自研Distro协议 6.Nacos内核设计之通信通道 7.Nacos内核设计之寻址机制 8.服务注册发现模块的注册中心的设计原理 9.服务注册发现模块的注册中心的服务数…...

4月27日日记

现在想来&#xff0c;可以想到什么就记录下来&#xff0c;这也是网上写日记的一个好处&#xff0c;然后 今天英语课上看到一个有关迷信的视频&#xff0c;就是老师课件里的&#xff0c;感觉画风很不错&#xff0c;但是我贫瘠的语言形容不出来&#xff0c;就想到是不是世界上的…...

CentOS7.9安装OpenSSL 1.1.1t和OpenSSH9.9p1

一、临时开启telnet登录方式&#xff0c;避免升级失败无法登录系统 &#xff08;注意telnet登录方式存在安全隐患&#xff0c;升级openssh相关服务后要记得关闭&#xff09; 1.安装telnet服务 yum -y install xinetd telnet* 2.允许root用户通过telnet登陆&#xff0c;编辑…...

单例模式:全局唯一性在软件设计中的艺术实践

引言 在软件架构设计中&#xff0c;单例模式&#xff08;Singleton Pattern&#xff09;以其独特的实例控制能力&#xff0c;成为解决资源复用与全局访问矛盾的经典方案。该模式通过私有化构造方法、静态实例存储与全局访问接口三大核心机制&#xff0c;确保系统中特定类仅存在…...

Spring 与 ActiveMQ 的深度集成实践(三)

五、实战案例分析 5.1 案例背景与需求 假设我们正在开发一个电商系统&#xff0c;其中订单模块和库存模块是两个独立的子系统 。当用户下单后&#xff0c;订单模块需要通知库存模块进行库存扣减操作 。在传统的同步调用方式下&#xff0c;订单模块需要等待库存模块完成扣减操…...

30-算法打卡-字符串-重复的子字符串-leetcode(459)-第三十天

1 题目地址 459. 重复的子字符串 - 力扣&#xff08;LeetCode&#xff09;459. 重复的子字符串 - 给定一个非空的字符串 s &#xff0c;检查是否可以通过由它的一个子串重复多次构成。 示例 1:输入: s "abab"输出: true解释: 可由子串 "ab" 重复两次构成…...

rocketmq一些异常记录

rocketmq一些异常记录 Product 设置不重复发送 发送 一次失败&#xff0c;不会在被发送到mq消息队列中&#xff0c;相当于消息丢失。 2、 Consumer 消费失败 重试三次消费 都失败 则消息消费失败&#xff0c;失败后 会放入 死信队列&#xff0c;可以手动处理在mq面板 处理死信队…...

SQLMesh 测试自动化:提升数据工程效率

在现代数据工程中&#xff0c;确保数据模型的准确性和可靠性至关重要。SQLMesh 提供了一套强大的测试工具&#xff0c;用于验证数据模型的输出是否符合预期。本文将深入探讨 SQLMesh 的测试功能&#xff0c;包括如何创建测试、支持的数据格式以及如何运行和调试测试。 SQLMesh …...

WPF使用SQLite与JSON文本文件结合存储体侧平衡数据的设计与实现

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…...

关系型数据库PostgreSQL vs MySQL 深度对比:专业术语+白话解析+实战案例

PostgreSQL 与 MySQL 的详细对比 PostgreSQL 和 MySQL 是两种最流行的开源关系型数据库&#xff0c;它们在设计理念、功能特性和适用场景上有显著差异。以下是它们的详细对比&#xff1a; 一、基本架构与设计理念 PostgreSQL&#xff1a;多进程架构&#xff0c;使用共享内存通…...

利用 SSRF 和 Redis 渗透

环境搭建 在本次实验中&#xff0c;我们使用 Docker 环境进行测试。 解压实验包&#xff0c;搭建 docker 环境。 docker环境 web的dockerfile 主要利用代码 &#xff1a; redis服务器 通过 docker-compose up -d 启动相关容器&#xff0c;初次启动失败。 发现 docker 版本问…...

脏读、幻读、可重复读

脏读 定义&#xff1a;一个事务读取了另一个事务尚未提交的数据 。比如事务 A 修改了某条数据但还没提交&#xff0c;此时事务 B 读取了这条被修改但未提交的数据。若事务 A 后续回滚&#xff0c;事务 B 读到的数据就是无效的&#xff0c;相当于读到了 “脏数据”。危害&#…...

第1讲、#PyTorch教学环境搭建与Tensor基础操作详解

引言 PyTorch是当前深度学习领域最流行的框架之一&#xff0c;因其动态计算图和直观的API而备受开发者青睐。本文将从零开始介绍PyTorch的环境搭建与基础操作&#xff0c;适合各种平台的用户和深度学习初学者。 1. 安装和环境搭建 macOS (Apple Silicon) 对于Mac M1/M2/M3用…...

【创新实训个人博客】数据库搭建

1.原因 为了降低模型使用以前训练的数据或者幻觉知识&#xff0c;我们在对话时需要提供相关内容的数据&#xff0c;同时由于需要最新的广告实时数据&#xff0c;实时爬取和版权问题。数据由团队在网上爬取&#xff0c;为了广告内容的有效性&#xff0c;如果长期使用&#xff0…...

《代码整洁之道》第6章 对象和数据结构 - 笔记

数据抽象 (Data Abstraction) 这个小节主要讲的是**面向对象编程&#xff08;OOP&#xff09;**的一种核心思想&#xff1a;对象应该隐藏它的内部数据&#xff0c;只暴露可以操作这些数据的“行为”&#xff08;也就是方法/函数&#xff09;。 大白话&#xff1a; 你创建一个…...

Python判断字符串中是否包含特殊字符

在 Python 中&#xff0c;判断一个字符串是否包含特殊字符可以通过多种方法实现。常见的特殊字符包括空格、感叹号、单引号、括号、星号、加号、逗号、斜杠、冒号、分号、等号、问号、 符号、方括号、花括号和 & 符号等。 为了判断字符串中是否包含这些特殊字符&#xff0…...

disruptor-spring-boot-start版本优化升级

文章目录 1.前言2.升级内容3.依赖4.总结 1.前言 由于之前写了一篇《disruptor-spring-boot-start生产实践导致pod节点CPU爆表100%的问题解决说明》的文章&#xff0c;里面说本地启动没有啥问题&#xff0c;后面我启动之前写的那个测试的controller发现&#xff0c;本地电脑的CP…...

复杂背景下无人机影像小目标检测:MPE-YOLO抗遮挡与抗背景干扰设计

目录 一、引言 二、挑战和贡献 密集小目标和遮挡 实时性要求与精度权衡 复杂背景 三、MPE-YOLO模型细节 多级特征集成器&#xff08;MFI&#xff09; 感知增强卷积&#xff08;PEC&#xff09; 增强范围C2f模块&#xff08;ES-C2f&#xff09; 四、Coovally AI模型训…...

项目实战 -- 状态管理

redux基础 还记得好久好久之前就想要实现的一个功能吗&#xff1f; 收起侧边栏折叠菜单&#xff0c;没错&#xff0c;现在才实现 因为不是父子通信&#xff0c;所以处理起来相对麻烦一点 可以使用状态树或者中间人模式 这就需要会redux了 Redux工作流&#xff1a; 异步就…...

基于单片机的智能药盒系统

标题:基于单片机的智能药盒系统 内容:1.摘要 本文聚焦于基于单片机的智能药盒系统。背景方面&#xff0c;随着人口老龄化加剧&#xff0c;老年人按时准确服药问题愈发凸显&#xff0c;同时现代快节奏生活也使人们容易遗忘服药时间。目的是设计并实现一个能帮助人们按时、按量服…...

【PyCharm- Python- ArcGIS】:安装一个和 ArcGIS 不冲突的独立 Python让PyCharm 使用 (解决全过程记录)

之前电脑上安装了anaconda3&#xff0c;python3和arcgis10.2.其中anaconda3带有python3&#xff0c;arcgis10.2自带python2.7。arcgis不能正常使用&#xff0c;之前为了使用arcgis&#xff0c;因此卸载了anaconda3和python3&#xff0c;PyCharm不能正常使用了 之前安装的卸载后…...

【C语言干货】回调函数

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、回调函数 前言 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、回调函数 在 C 语言中&#xff0c;当你有一个函数并希望将其作…...

Redis使用总结

NoSQL 1.1为什么要用NoSQL 面对现在用户数据的急剧上升&#xff0c;我们需要对这些用户数据进行挖掘&#xff0c;传统的关系型数据库已经不适合这些 应用了.Nosql 的发展可以很了的处理这些大的数据. 1.2什么是NoSQL Not Only Sql->NoSQL(不仅仅是SQL) 非关系型数据库.随…...

现场问题排查-postgresql某表索引损坏导致指定数据无法更新影响卷宗材料上传

问题现象 今天突然被拉进一个群&#xff0c;说某地区友商推送编目结果报错&#xff0c;在我们自己的卷宗系统上传材料也一直转圈&#xff0c;也删除不了案件卷宗&#xff0c;重置模板也没用&#xff0c;只有个别案件有问题。虽然这事儿不属于我负责&#xff0c;但还是抽时间给…...

数字化转型的未来趋势:从工具到生态,聚焦生态合作、绿色转型与全球化布局

摘要 本文将深入探讨了数字化转型的演进路径&#xff0c;特别是从依赖单一数字化工具向构建和参与复杂商业生态系统的战略转变。分析表明&#xff0c;这一转变不仅是技术升级&#xff0c;更是商业模式、运营逻辑和价值创造方式的根本性变革。云计算、人工智能和大数据分析等 f…...

记录学习记录学习《手动学习深度学习》这本书的笔记(九)

马不停蹄地来到了第十二章&#xff1a;计算性能…… 感觉应该是讲并行计算方面的&#xff0c;比如GPU、CPU、CUDA那些。 第十二章&#xff1a;计算性能 12.1 编译器和解释器 这里先提出了命令式编程和符号式编程的概念。 命令式编程VS符号式编程 目前为止&#xff0c;本书…...

麒麟系统通过 Service 启动 JAR 包的完整指南

&#x1f9d1; 博主简介&#xff1a;CSDN博客专家、CSDN平台优质创作者&#xff0c;高级开发工程师&#xff0c;数学专业&#xff0c;10年以上C/C, C#, Java等多种编程语言开发经验&#xff0c;拥有高级工程师证书&#xff1b;擅长C/C、C#等开发语言&#xff0c;熟悉Java常用开…...

【记录maven依赖规则-dependencyManagement,dependencies】

记录maven依赖规则-dependencyManagement&#xff0c;dependencies 依赖方式 直接依赖 间接依赖 依赖关系 直接依赖&#xff1a; 父级管理定义的版本&#xff0c;并且在中进行引用了的版本。 优先使用dependencyManagement定义的版本。 间接依赖&#xff1a; 如果间接依赖…...

macos下mysql 5.7/8.0版本切换

1、首先安装好mysql 5.7/8.0&#xff0c;可以用brew进行安装 5.7 的原始配置文件路径&#xff1a; /usr/local/Cellar/mysql5.7/5.7.44_1/homebrew.mxcl.mysql5.7.plist 配置内容如下&#xff1a; 对应的.cnf配置文件内容如下&#xff1a; 8.0 的原始配置文件路径&#xff1…...

FPGA时钟设计

实现功能&#xff1a;基于Verilog的动态显示时钟设计&#xff0c;支持整点&#xff08;时:00:00&#xff09;闪烁功能。代码包含时钟计数、动态扫描、整点检测和闪烁控制模块&#xff1a; module dynamic_clock(input clk, // 主时钟&#xff08;假设50MHz&#xff0…...

【NVM】管理不同版本的node.js

目录 一、下载nvm 二、安装nvm 三、验证安装 四、配置下载镜像 五、使用NVM 前言&#xff1a;不同的node.js版本会让你在使用过程很费劲&#xff0c;nvm是一个node版本管理工具&#xff0c;通过它可以安装多种node版本并且可以快速、简单的切换node版本。 一、下载nvm htt…...

【今日三题】笨小猴(模拟) / 主持人调度(排序) / 分割等和子集(01背包)

⭐️个人主页&#xff1a;小羊 ⭐️所属专栏&#xff1a;每日两三题 很荣幸您能阅读我的文章&#xff0c;诚请评论指点&#xff0c;欢迎欢迎 ~ 目录 笨小猴(模拟)主持人调度(排序)分割等和子集(01背包) 笨小猴(模拟) 笨小猴 #include <iostream> #include <string…...

android10 卸载应用出现回退栈异常问题

打开设置&#xff0c;打开APP1&#xff0c;使用adb uninstall 卸载APP1/或者杀掉APP1进程&#xff0c;没有回到设置而是回到了桌面 抓取eventlog&#xff0c;查看ams/wms打印&#xff0c;发现“am_focused_stack: appDied leftTaskHistoryEmpty”源码中搜索“leftTaskHistoryE…...

位置差在坐标系间的相互转换

1 NED转经纬高 &#xff08;n 系下的北向、东向和垂向位置差异&#xff08;单位 m&#xff09;转化为纬度、经度和高程分量的差异&#xff09; 2 基站坐标转换 纬度、经度、高程 到 ECEF %纬度、经度、高程 到 ECEF clc; clear; glvs; addpath(genpath(E:\GNSSINS\ACES)…...

在线重定义——分区表改造

在数据库管理过程中&#xff0c;随着数据量的不断增长&#xff0c;普通表的查询、维护成本不断上升。为了提升查询性能和管理效率&#xff0c;通常需要将大表进行分区处理。 本文介绍如何使用 Oracle 在线重定义&#xff08;DBMS_REDEFINITION&#xff09; 的方式对现有大表进行…...

day51—二分法—x 的平方根(LeetCode-69)

题目描述 给你一个非负整数 x &#xff0c;计算并返回 x 的 算术平方根 。 由于返回类型是整数&#xff0c;结果只保留 整数部分 &#xff0c;小数部分将被 舍去 。 注意&#xff1a;不允许使用任何内置指数函数和算符&#xff0c;例如 pow(x, 0.5) 或者 x ** 0.5 。 示例 …...

网络安全漏洞现状与风险管理分析

在当今数字化时代&#xff0c;网络安全已成为企业和组织不可忽视的核心问题。网络环境的日益复杂和攻击手段的不断升级&#xff0c;使得漏洞管理成为网络安全战略中的关键环节。下面将详细分析当前网络安全领域的漏洞现状及有效的风险管理策略。 当前网络安全面临的挑战 高危漏…...

二、Web服务常用的I/O操作

一、单个或者批量上传文件 前端&#xff1a; <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>文件…...

Pinia——Vue的Store状态管理库

一、Store 是什么&#xff1f; Store (如 Pinia) 是一个保存状态和业务逻辑的实体&#xff0c;它并不与你的组件树绑定。换句话说&#xff0c;它承载着全局状态。它有点像一个永远存在的组件&#xff0c;每个组件都可以读取和写入它。它有三个概念&#xff0c;state、getter 和…...

生成式人工智能认证(GAI认证)适合那些人考?

在人工智能浪潮席卷全球的今天,你是否曾思考过:当机器开始创作诗歌、设计建筑、撰写代码,甚至模拟人类思维时,我们该如何与这个“新物种”共处?更关键的是,当生成式人工智能(Generative AI)从实验室走向千行百业,谁将成为驾驭这场技术革命的“领航者”?答案或许藏在一…...

使用cmd来创建数据库和数据库表-简洁步骤

创建数据库和表&#xff1a; 1. 按WinR打开“运行”&#xff0c;输入cmd&#xff0c;回车 2. 登录数据库&#xff1a;mysql -u root -p 然后输入密码 3. 创建数据库create database myblog; myblog为数据库名(自定义你的数据库名) &#xff01;注意分号不要漏了&#xff01; …...

微博安卓版话题热度推荐算法与内容真实性分析

微博是目前最受欢迎的社交平台之一&#xff0c;它的推荐算法在推动话题热度和内容传播方面发挥着重要作用。然而&#xff0c;这一算法也引发了对于内容真实性的担忧。本文将通过分析微博安卓版的推荐机制&#xff0c;探讨其对话题热度的影响以及内容真实性问题。 微博的推荐算法…...

助力产业升级 | BMC安全启动方案上新了!

近日&#xff0c;OurBMC 社区联合其理事成员单位中移&#xff08;苏州&#xff09;软件技术有限公司&#xff0c;在产业化落地SIG发布计算机系统安全可信创新解决方案——《 BMC 安全启动方案》。该方案为开发者提供了清晰、可实现的技术实施路径&#xff0c;可有效助力开发者提…...

Python中使用Redis的参数

Python中使用Redis通常是通过redis-py这个库来实现的。redis-py是一个Python客户端&#xff0c;它提供了对Redis数据库的完整操作接口。在使用redis-py时&#xff0c;你需要通过连接参数来配置与Redis服务器的连接。下面是一些常用的连接参数及其解释&#xff1a; host 描述&…...

tensorflow使用详解

一、TensorFlow基础环境搭建 安装与验证 # 安装CPU版本 pip install tensorflow# 安装GPU版本&#xff08;需CUDA 11.x和cuDNN 8.x&#xff09; pip install tensorflow-gpu# 验证安装 python -c "import tensorflow as tf; print(tf.__version__)"核心概念 Tensor…...

FreeMarker语法深度解析与Node.js集成实践指南

一、FreeMarker核心语法体系 1.1 基础模板结构 <#-- 注释语法 --> ${expression} <#-- 输出表达式 --> <#directive paramvalue> <#-- 指令语法 -->1.2 数据类型处理 标量类型深度处理&#xff1a; <#assign num 123.45?floor> <#--…...

如何实现一个可视化的文字编辑器(C语言版)?

一、软件安装 Visual Studio 2022 Visual Studio 2022 是微软提供的强大集成开发环境&#xff08;IDE&#xff09;&#xff0c;广泛用于C/C、C#、Python等多种编程语言的开发。它提供了许多强大的工具&#xff0c;帮助开发者编写、调试和优化代码。 1.下载 Visual Studio 202…...

学习海康VisionMaster之路径提取

一&#xff1a;进一步学习了 今天学习下VisionMaster中的路径提取&#xff1a;可在绘制的路径上等间隔取点或查找边缘点 二&#xff1a;开始学习 1&#xff1a;什么是路径提取&#xff1f; 相当于事先指定一段路径&#xff0c;然后在对应的路径上查找边缘&#xff0c;这个也是…...