string本质使一个类,虽然std::string
内部确实使用了字符数组来存储字符串数据,但它远不止是一个简单的char*
封装。它提供了一个完整、安全和方便的字符串操作接口,是C++中处理字符串的首选方式。
底层这块的实现:
C++ 的 std::string 类在底层通常是通过一个字符数组来存储字符串的字符序列,以及一个表示字符串长度的成员变量(有些实现可能还会有一个表示分配容量的成员变量)。这种实现方式允许 std::string 以连续的内存块来存储字符,从而实现高效的随机访问和修改操作。
std::string类是一个模板类特化std::basic_string <T>的具体化,通常定义为std::basic_string<char>。
容器类 basic_string的模板声明如下:
template<class _Elem,
class _Traits,
class _Ax>
class basic_string
在该模板定义中,最重要的参数是第一个: _Elem,它指定了 basic_string 对象将存储的数据类型。
因此, std::string 使用_Elem=char 具体化模板 basic_string 的结果,而 wstring 使用_Elem= wchar 具体化
模板 basic_string 的结果。
typedef basic_string<char, char_traits<char>, allocator<char> >
string;
而 STL wstring 类的定义如下:
typedef basic_string<wchar_t, char_traits<wchar_t>, allocator<wchar_t> >
string;
实现的时候需要考虑关键部分:
- 字符数组:用于存储实际的字符数据。这个数组的大小通常会在运行时动态调整,以容纳更多的字符或节省内存空间。
- 长度信息:一个整数,表示当前字符串中的字符数。这个信息用于快速获取字符串的长度,而不需要遍历整个字符数组。
- 容量信息(可选):
std::string
内部跟踪它当前存储的字符数量(大小)以及分配的总容量(容量)。这允许它在需要时重新分配内存,同时优化性能。 - 异常安全性:与直接使用
char*
相比,std::string
是异常安全的。这意味着在执行字符串操作时,即使发生异常,std::string
也能保证内存的正确管理,避免内存泄漏或悬挂指针。 - 兼容性:
std::string
与C风格的字符串兼容,可以通过c_str()
成员函数获取一个指向内部字符数组的常量指针,这在需要与C风格的API交互时非常有用。
内存管理
std::string 使用了一种称为“小对象优化”(Small Object Optimization, SOO)或“短字符串优化”(Short String Optimization, SSO)的技术来提高效率。这种技术的基本思想是,对于较短的字符串,直接在 std::string 对象内部存储字符数据,而不是动态分配内存。这样可以减少内存分配和释放的开销,提高短字符串处理的效率。
当字符串的长度超过某个阈值时(这个阈值通常是实现定义的),std::string 会动态分配一个更大的内存块来存储字符数据,并将长度和容量信息相应地更新为新的值。当字符串被修改或操作时,如果需要的内存空间超过了当前分配的容量,std::string 会重新分配一个更大的内存块,并将原有的字符数据复制到新的内存块中。
复制和赋值
当复制或赋值一个 std::string 对象时,通常会涉及到深拷贝(deep copy)的操作。也就是说,会复制字符数组中的数据,而不是仅仅复制指针或引用。这样做是为了保证每个 std::string 对象都有自己独立的字符数据存储空间,互不影响。
然而,现代 C++ 标准库的实现可能会使用一种称为“写时复制”(Copy-On-Write, COW)的技术来优化复制和赋值操作。这种技术的基本思想是,当复制或赋值一个 std::string 对象时,并不立即复制字符数据,而是让多个对象共享同一个字符数组。只有当其中一个对象被修改时,才会真正复制字符数据。这种优化可以减少不必要的内存分配和复制操作,提高性能。但需要注意的是,在多线程环境下使用 COW 技术可能会导致竞态条件和数据不一致的问题,因此现代 C++ 标准库的实现可能会避免使用这种技术,或者在多线程环境下禁用它。
注意事项
虽然 std::string 的底层实现原理对于大多数应用程序来说是透明的,但了解这些原理可以帮助你更好地理解和优化使用 std::string 的代码。例如,避免频繁地重新分配内存空间可以提高性能;使用 reserve() 函数预先分配足够的内存空间可以减少内存分配的次数;在需要频繁修改字符串的情况下,使用 std::stringbuilder(C++20 中引入)可以更有效地构建字符串。
#include <iostream>
#include <cstring>
#include <algorithm>
class String {
private:
char* str;
public:
// 构造函数
String(const char* s = "") {
str = new char[std::strlen(s) + 1];
std::strcpy(str, s);
}
// 析构函数
~String() {
delete[] str;
}
// 拷贝构造函数
String(const String& other) {
str = new char[std::strlen(other.str) + 1];
std::strcpy(str, other.str);
}
// 赋值运算符重载
String& operator=(const String& other) {
if (this != &other) {
delete[] str;
str = new char[std::strlen(other.str) + 1];
std::strcpy(str, other.str);
}
return *this;
}
// 获取字符串
const char* c_str() const {
return str;
}
};
int main() {
String s1("Hello");
String s2 = s1; // 拷贝构造
String s3;
s3 = s1; // 赋值操作
std::cout << "s1: " << s1.c_str() << std::endl;
std::cout << "s2: " << s2.c_str() << std::endl;
std::cout << "s3: " << s3.c_str() << std::endl;
return 0;
}
这段代码展示了如何用C++模拟实现一个简单的String
类,包括构造函数、析构函数、拷贝构造函数和赋值运算符重载。它演示了如何分配和释放内存以管理字符串,并且如何实现浅拷贝和深拷贝。这对于理解C++中对象的创建和销毁过程,以及内存管理非常有帮助。
```python
class BertPooler(nn.Module):
def __init__(self, config):
super().__init__()
self.dense = nn.Linear(config.hidden_size, config.hidden_size)
self.activation = nn.Tanh()
def forward(self, hidden_states):
# We "pool" the model by simply taking the hidden state corresponding
# to the first token.
first_token_tensor = hidden_states[:, 0]
pooled_output = self.dense(first_token_tensor)
pooled_output = self.activation(pooled_output)
return pooled_output
from transformers.models.bert.configuration_bert import *
import torch
config = BertConfig.from_pretrained("bert-base-uncased")
bert_pooler = BertPooler(config=config)
print("input to bert pooler size: {}".format(config.hidden_size))
batch_size = 1
seq_len = 2
hidden_size = 768
x = torch.rand(batch_size, seq_len, hidden_size)
y = bert_pooler(x)
print(y.size())
```