1. C++模板编程深度解析
1.1 为什么需要模板
在C++开发中,我们经常遇到需要为不同类型实现相同逻辑的情况。比如实现一个交换函数,对于int类型需要写一个版本,对于double类型又需要重写一个几乎相同的版本。这不仅造成代码冗余,更增加了维护成本。
模板的出现完美解决了这个问题。它允许我们编写与类型无关的通用代码,编译器会在使用时根据具体类型自动生成对应的代码。这种技术称为泛型编程,是C++最强大的特性之一。
1.2 函数模板详解
1.2.1 基本语法与实例
函数模板的基本语法如下:
cpp复制template<typename T>
void swap(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
这里typename T声明了一个类型参数,可以用class替代,两者在函数模板中没有区别。
实际使用时:
cpp复制int a = 1, b = 2;
swap(a, b); // 编译器生成swap<int>版本
double x = 1.1, y = 2.2;
swap(x, y); // 编译器生成swap<double>版本
1.2.2 模板参数推导规则
编译器会根据传入参数自动推导模板参数类型。但有几个重要规则需要注意:
- 推导必须一致:
cpp复制int a = 1;
double b = 2.0;
swap(a, b); // 错误:T无法同时为int和double
- 处理不同类型参数的三种方法:
方法一:强制类型转换
cpp复制cout << add(a, (int)b) << endl;
方法二:显式实例化
cpp复制add<int>(a, b);
方法三:多模板参数
cpp复制template<class T1, class T2>
T1 add(const T1& a, const T2& b) {
return a + b;
}
1.2.3 模板与普通函数的优先级
当模板函数和普通函数都匹配时,编译器优先选择普通函数:
cpp复制template<class T1, class T2>
T1 add(const T1& a, const T2& b) { /*...*/ }
int add(int a, int b) { /*...*/ }
add(1, 2); // 调用普通函数版本
1.3 类模板深度剖析
1.3.1 类模板基本实现
以栈为例展示类模板的实现:
cpp复制template<class T>
class Stack {
public:
Stack(int capacity = 4)
: _capacity(capacity), _size(0), _data(new T[capacity]) {}
~Stack() {
delete[] _data;
_data = nullptr;
}
void Push(const T& val) {
if (_size == _capacity) {
ExpandCapacity();
}
_data[_size++] = val;
}
private:
void ExpandCapacity() {
T* newData = new T[_capacity * 2];
for(size_t i = 0; i < _size; ++i) {
newData[i] = _data[i];
}
delete[] _data;
_data = newData;
_capacity *= 2;
}
T* _data;
size_t _size;
size_t _capacity;
};
使用示例:
cpp复制Stack<int> intStack;
Stack<double> doubleStack;
1.3.2 类模板的声明与定义分离
类模板的成员函数在类外定义时需要特殊语法:
cpp复制// 声明
template<class T>
class Stack {
public:
void Push(const T& val);
};
// 定义
template<class T>
void Stack<T>::Push(const T& val) {
if (_size == _capacity) {
ExpandCapacity();
}
_data[_size++] = val;
}
1.3.3 模板参数默认值
类模板支持为模板参数提供默认值:
cpp复制template<class T = int>
class Array {
// ...
};
Array<> arr; // 使用默认类型int
1.4 模板高级特性
1.4.1 非类型模板参数
模板参数不仅可以是类型,还可以是整型常量:
cpp复制template<class T, size_t N>
class FixedArray {
T _data[N];
};
FixedArray<int, 100> arr;
1.4.2 模板特化
可以为特定类型提供特殊实现:
cpp复制template<>
class Stack<bool> {
// 针对bool类型的特殊实现
};
1.4.3 可变参数模板
C++11引入了可变参数模板:
cpp复制template<class... Args>
void print(Args... args) {
// 处理可变参数
}
2. C++ string类全面解析
2.1 string类基础
2.1.1 为什么需要string类
C风格字符串(char*)存在诸多问题:
- 需要手动管理内存
- 容易发生缓冲区溢出
- 缺乏常用字符串操作
string类封装了字符串的存储和操作,提供了安全、便捷的字符串处理方式。
2.1.2 基本使用
cpp复制#include <string>
using namespace std;
string s1; // 空字符串
string s2 = "hello"; // 初始化
string s3(s2); // 拷贝构造
string s4(10, 'a'); // 10个'a'
2.2 string常用操作
2.2.1 容量操作
cpp复制s.empty(); // 是否为空
s.size(); // 字符数
s.capacity(); // 当前容量
s.reserve(100); // 预留空间
2.2.2 元素访问
cpp复制s[0]; // 不检查越界
s.at(0); // 检查越界,抛出异常
s.front(); // 首字符
s.back(); // 末字符
2.2.3 修改操作
cpp复制s += " world"; // 追加
s.append("!");
s.push_back('!');
s.insert(5, " dear");
s.erase(5, 5); // 从位置5删除5个字符
s.clear(); // 清空
2.3 string高级特性
2.3.1 字符串查找
cpp复制s.find("ll"); // 返回首次出现位置
s.rfind("ll"); // 反向查找
s.find_first_of("aeiou"); // 查找任意匹配字符
s.find_last_not_of(" \t"); // 查找最后不匹配字符
2.3.2 子字符串操作
cpp复制string sub = s.substr(6, 5); // 从6开始取5个字符
2.3.3 数值转换
cpp复制string s = to_string(123); // 数字转字符串
int n = stoi("456"); // 字符串转整数
double d = stod("3.14"); // 字符串转浮点数
3. 模板与string实战技巧
3.1 模板编程最佳实践
- 尽量将模板定义放在头文件中,因为模板需要在编译时实例化
- 使用typename和class声明模板参数时没有区别,但typename更直观
- 对于复杂模板,使用static_assert进行编译时检查
- 模板元编程可以用于编译期计算,但要谨慎使用
3.2 string使用注意事项
-
避免频繁的字符串拼接,可以使用ostringstream
cpp复制ostringstream oss; oss << "Hello" << " " << "World"; string s = oss.str(); -
大字符串操作时考虑预先reserve足够空间
-
需要C风格字符串时使用c_str(),但注意生命周期
-
多线程环境下注意string的非原子操作
3.3 常见问题排查
- 模板编译错误:通常信息冗长,重点看第一个报错
- 链接错误:确保模板定义可见
- string越界访问:使用at()而非[]进行安全访问
- 内存问题:string管理的内存不需要手动释放
4. 进阶话题
4.1 移动语义与string
C++11引入的移动语义大大提高了string的性能:
cpp复制string createString() {
string s(1000000, 'a');
return s; // 触发移动构造而非拷贝
}
string s = createString(); // 高效
4.2 SSO优化
大多数实现使用SSO(Small String Optimization)优化短字符串:
- 短字符串直接存储在对象内部
- 长字符串才使用堆内存
- 通常临界点在15-22个字符左右
4.3 自定义分配器
string允许指定自定义内存分配器:
cpp复制template<class charT,
class traits = char_traits<charT>,
class Allocator = allocator<charT>>
class basic_string;
using string = basic_string<char>;
5. 性能优化建议
- 避免不必要的字符串拷贝,使用引用或移动语义
- 预先分配足够空间减少重新分配
- 考虑使用string_view(C++17)代替只读字符串参数
- 高频操作考虑直接操作底层字符数组(data())
重要提示:虽然可以直接操作string内部的字符数组(data()),但任何导致字符串长度变化的操作都可能使之前的指针失效。