std::string 是 C++ 标准库提供的用于处理字符串的类。它在 头文件中定义。std::string 提供了一种灵活、高效的字符串表示方式,相比于 C 语言中的字符串表示(使用字符数组或指针),std::string 更易于使用,更安全,并且提供了许多便捷的操作。

一、底层实现

std::string 类的底层实现通常是一个动态分配的字符数组(即堆上的内存),并且该数组的长度可以动态地增长和收缩以适应字符串的长度变化。

1.1字符数组

std::string对象通常包含了一个指向堆上分配的内存的指针,用于存储较长的字符串。较短的字符串可能会直接存在对象本身的内部缓冲区。有些实现还会使用一个独立的缓冲区来存储字符串内容。

std::strig str = "123";
const char* cstr = str.c_str();
// 输出 C 风格字符串
std::cout << "C-style string: " << cstr << std::endl;

无论std::string底层是怎么优化的,或者string本身是存在堆区还是栈区。c_str总是可以拿到内部存储的C风格字符串,即字符串的字符数组的首地址。
使用 c_str() 函数时需要注意以下几点:

1、 生命周期问题

c_str() 返回的 C 风格字符串指针指向 std::string 内部的字符数组,这个指针的有效性与 std::string 对象的生命周期相关联。如果在 std::string 对象被销毁之后仍然使用 c_str() 返回的指针,将导致未定义的行为。因此,在使用 c_str() 返回的指针时,要确保对应的 std::string 对象仍然有效。

2、 只读属性

c_str() 返回的指针指向的是一个只读的 C 风格字符串,这意味着不能通过这个指针来修改 std::string 对象中字符串的内容。如果尝试修改 c_str() 返回的指针指向的字符数组内容,将导致未定义的行为。

3、 空指针检查

在某些情况下,c_str() 可能会返回一个空指针(nullptr),例如当 std::string 对象为空时。在使用 c_str() 返回的指针之前,应该进行空指针检查,以避免出现空指针解引用错误。

4、 数据不稳定

由于 c_str() 返回的指针指向的字符数组可能随着 std::string 对象的操作而被修改或重新分配,因此在调用 c_str() 之后,如果对 std::string 进行了可能导致重新分配内存的操作(如添加字符、删除字符、修改字符串内容等),那么之前返回的指针就可能会失效。

如果需要用到c风格的字符串,建议拷贝出来使用。

#include <iostream>
#include <string>
#include <cstring>
int main() {
    std::string str = "Hello";
    const char* cstr = str.c_str();

    // 拷贝到另一个字符数组
    char buffer[20]; // 假设足够大
    strcpy_s(buffer, sizeof(buffer), cstr);

    // 使用拷贝后的字符数组
    std::cout << buffer << std::endl;

    return 0;
}

1.2长度和容量管理

除了存储字符数组外,std::string 还会记录字符串的当前长度和缓冲区的容量。使用length()size() 方法,获得字符串中字符的数量。

长度表示字符串当前实际包含的字符数,而容量表示分配给字符数组的内存空间大小。当字符串的长度超过容量时,std::string 会重新分配更大的内存空间,并将原有的字符复制到新的内存位置。

大多数C++标准库会采用指数级增长,每次扩展缓冲区大小都是前一次大小的倍数,可以保障操作的时间复杂度为常数级,有效避免频繁地调整缓冲区大小带来的性能损耗,减少内存碎片化的问题。另外还有线性增长策略,每次扩展缓冲区大小都是固定值,这样扩展速度比较慢,但避免了大量的内存浪费情况。

#include <iostream>
#include <string>

using namespace std;
int main()
{	// 使用的gcc version 13.2.0 (GCC)默认的capacity是15,线性增长。
    string str = "";
    cout << str.size() << endl; // 0
    cout << str.capacity() << endl; // 15
    str += "1234654646416456544645";
    cout << str.size() << endl; // 22
    cout << str.capacity() << endl; // 30
    return 0;
}

1.3 动态内存分配

std::string 类内部会自动使用 newdelete,或者动态内存分配和释放函数(如 malloc()free()),来管理字符串的字符数组,封装了内存管理的细节,不用担心内存管理的问题。

std::string 对象赋值一个 C 风格字符串或者另一个 std::string 对象时,std::string 类会负责分配足够大的内存来存储字符串的内容,并将字符串复制到这块内存中。这个内存通常是在堆上动态分配的,因此你不需要担心栈空间的限制。

std::string 对象的生命周期结束时,它会自动释放所分配的内存。不需要手动调用 deletefree() 来释放字符串的内存,因为 std::string 类会在对象销毁时自动调用相应的析构函数来释放内存。

1.4 空终止符

与 C 语言中的字符串类似,std::string 的底层实现也会在字符数组的末尾添加一个空终止字符(‘\0’),以表示字符串的结束。这样可以使得 std::string 类的接口与标准 C 字符串函数兼容。但是std::stringsize()length()不会统计('\0')

#include <iostream>
#include <string>

using namespace std;
int main()
{
    string str = "123";
    cout << str.size() << endl; // 3
    cout << str.length() << endl; // 3
    return 0;
}

二、成员函数

std::string 类提供了公开的成员函数来访问内部的数据。直接使用str.调用

  1. append():在字符串末尾添加字符或字符串。
    std::string str = "1";
    std::cout<<str<<std::endl; // 1
    str.append("2");
    std::cout<<str<<std::endl; // 12    
  1. assign():用新的字符串替换原有内容。
    std::string str = "1";
    std::cout<<str<<std::endl; // 1
    str.assign("2");
    std::cout<<str<<std::endl; // 2
  1. at():返回指定位置的字符。
    std::string str = "123";
    std::cout<<str.at(1)<<std::endl; // 2
  1. back():返回最后一个字符。
    std::string str = "123";
    std::cout<<str.back()<<std::endl; // 3
  1. begin():返回指向字符串第一个字符的迭代器。
  2. end():::返回自字符串最后一个字符后一个位置的迭代器
  3. cbegin():返回指向字符串第一个字符的常量迭代器。
  4. cend():返回指向字符串最后一个字符之后的位置的常量迭代器。
  5. crbegin():返回指向字符串最后一个字符的常量逆向迭代器。
  6. crend():返回指向字符串第一个字符之前的位置的常量逆向迭代器。
  7. . rbegin():返回指向字符串最后一个字符的逆向迭代器。
  8. . rend():返回指向字符串第一个字符之前的位置的逆向迭代器。
    std::string str = "123";
    for (auto it = str.begin(); it != str.end(); it++) {
        std::cout<<*it<<std::endl; // 1 2 3
    }
    for (auto it = str.cbegin(); it != str.cend(); it++) {
        std::cout<<*it<<std::endl; // 1 2 3
    }
    for (auto it = str.rbegin(); it != str.rend(); it++) {
        std::cout<<*it<<std::endl; // 3 2 1
    }
  1. capacity():返回字符串当前的容量。
    std::string str = "123";
    std::cout<<str.capacity()<<std::endl; // 15
  1. . clear():清空字符串。
str.clear();
  1. compare():比较两个字符串。(ASCII字典顺序)
    std::string str1 = "1";
    std::string str2 = "2";
    std::string str3 = "3";
    std::cout<<str2.compare(str1)<<std::endl; // > 1
    std::cout<<str1.compare(str2)<<std::endl; // < -1
    std::cout<<str2.compare(str2)<<std::endl; // = 0
  1. copy():复制字符到字符数组中。
    std::string str = "123";
    char c[20];
    str.copy(c,3,0);
    c[3] = '\0';
    std::cout<<str; // 123
  1. data():返回指向字符串第一个字符的指针。
  2. c_str():返回指向字符串第一个字符的指针。
    std::string str = "123";
    char * c1 = str.data();
    const char * c2 = str.c_str();
    std::cout<<c1<<std::endl; // 123
    std::cout<<c2<<std::endl; // 123
  1. empty():检查字符串是否为空。
    std::string str = "123";
    std::cout<<str.empty()<<std::endl; // 0
    str.assign("");
    std::cout<<str.empty()<<std::endl; // 1
  1. erase():删除字符串中的指定位置或指定范围的字符
    # test erase :
    string str = "0123456789";
    cout << str << endl; // 0123456789
    str.erase(2); // 删除索引2及以后的字符
    cout << str << endl; // 01
    str = "0123456789";
    str.erase(2,3); // 删除从索引2起的3个字符
    cout << str << endl; // 0156789
  1. find():查找子字符串。
    std::string str1 = "123";
    if (str1.find('2') != std::string::npos) {
        std::cout<<"find 2"<<std::endl; // find 2
    } 
    if (str1.find("4") != std::string::npos) {
        std::cout<<"find 4"<<std::endl;
    }
  1. front():返回第一个字符。
    std::string str = "123";
    std::cout<<str.front(); // 1
  1. insert():在指定位置插入字符或字符串。
    std::string str = "0123456";
    str.insert(0,"a");
    std::cout<<str<<std::endl; // a0123456
    const char* str1 = "bcd";
    str.insert(2,str1);
    std::cout<<str<<std::endl; // a0bcd123456
    str.insert(2,str1,2);
    std::cout<<str<<std::endl; // a0bcbcd123456
    str.insert(4,2,'Q');
    std::cout<<str<<std::endl; // a0bcQQbcd123456
  1. length()size():返回字符串中字符的数量。
    std::string str = "0123456";
    std::cout<<str.size()<<std::endl; // 7 
    std::cout<<str.length()<<std::endl; // 7
  1. pop_back():删除最后一个字符。
    std::string str = "0123";
    std::cout<<str<<std::endl; // 0123
    str.pop_back();
    std::cout<<str<<std::endl; // 012
  1. push_back():在字符串末尾添加一个字符。
    std::string str = "0123";
    std::cout<<str<<std::endl; // 0123
    str.push_back('4');
    std::cout<<str<<std::endl; // 01234
  1. replace():替换字符串中的部分内容。
    std::string str = "012345";
    std::cout<<str<<std::endl; // 012345
    str.replace(2,3,"ABC");
    std::cout<<str<<std::endl; // 01ABC5
  1. reserve():为字符串预留空间。
    std::string str = "012345";
    std::cout<<str.capacity()<<std::endl; // 15
    str.reserve(50);
    std::cout<<str.capacity()<<std::endl; // 50
  1. resize():更改字符串的大小。
    std::string str = "012345";
    str.resize(3);
    std::cout<<str.size()<<std::endl; // 3
    std::cout<<str<<std::endl; // 012
  1. shrink_to_fit():释放多余的内存空间。
    std::string str = "012345";
    std::cout<<str.capacity()<<std::endl; // 15
    str.shrink_to_fit();
    std::cout<<str.capacity()<<std::endl; // 15
    str.reserve(50);
    std::cout<<str.capacity()<<std::endl; // 50
    str.shrink_to_fit();
    std::cout<<str.capacity()<<std::endl; // 15
  1. substr():提取子字符串。
    std::string str = "012345";
    std::cout<<str.substr(2,3)<<std::endl; // 234
  1. swap():交换字符串内容
    std::string str1 = "123";
    std::string str2 = "456";
    std::cout<<str1<<std::endl; // 123
    std::cout<<str2<<std::endl; // 456
    str1.swap(str2);
    std::cout<<str1<<std::endl; // 456
    std::cout<<str2<<std::endl; // 123

三、构造函数与析构函数

在 C++ 中,std::string 类的构造函数和析构函数负责创建和销毁字符串对象,并管理其内存资源。

构造函数:

std::string 类具有多个构造函数,可以用不同的方式创建字符串对象:

1、 默认构造函数:创建一个空字符串对象。

std::string str; // 创建一个空字符串对象

2、 使用 C 风格字符串构造:使用以 null 结尾的字符数组来初始化字符串对象。

const char* cstr = "Hello";
std::string str(cstr); // 使用 C 风格字符串来构造字符串对象

3、 使用字符和长度构造:使用字符数组和长度来初始化字符串对象。

const char chars[] = {'H', 'e', 'l', 'l', 'o'};
std::string str(chars, 5); // 使用字符数组和长度来构造字符串对象

4、使用重复字符构造:使用指定的字符重复指定次数来构造字符串对象。

std::string str(5, 'H'); // 使用指定的字符重复指定次数来构造字符串对象

5、 使用另一个字符串构造:使用另一个字符串对象来初始化当前字符串对象。

std::string str1 = "Hello";
std::string str2(str1); // 使用另一个字符串对象来构造当前字符串对象

析构函数:

std::string 类的析构函数负责在对象销毁时释放其内存资源。通常情况下,析构函数会自动调用,但如果需要手动释放内存,也可以使用 std::string 对象的 clear() 函数来清空字符串,并释放其内存资源。

std::string str = "Hello";
str.clear(); // 清空字符串并释放内存资源

总的来说,std::string 类的构造函数用于创建字符串对象,而析构函数则用于销毁对象并释放内存资源,从而确保在对象生命周期结束时资源能够得到正确地释放。

四、运算符重载

在C++中,std::string 类重载了一系列运算符,使得对字符串的操作更加方便和直观。以下是一些常用的字符串运算符重载:

1、 赋值运算符 (=):用于将一个字符串赋值给另一个字符串。

std::string str1 = "Hello";
std::string str2;
str2 = str1; // 将 str1 赋值给 str2

2、 加法运算符 (+):用于将两个字符串连接起来。

std::string str1 = "Hello";
std::string str2 = "World";
std::string result = str1 + str2; // 将 str1 和 str2 连接起来赋值给 result

3、 比较运算符 (==, !=, <, >, <=, >=):用于比较两个字符串的大小关系。(ASCII字典顺序)

std::string str1 = "Hello";
std::string str2 = "World";
if (str1 == str2) {
    // 字符串相等
} else if (str1 < str2) {
    // str1 小于 str2
} else {
    // str1 大于 str2
}

4、 下标运算符 ([]):用于访问字符串中的单个字符。

std::string str = "Hello";
char ch = str[0]; // 访问字符串的第一个字符

5、 复合赋值运算符 (+=):用于将一个字符串连接到另一个字符串的末尾。

std::string str = "Hello";
str += " World"; // 在字符串末尾添加 " World"

6、 输入输出运算符 (<<, >>):用于将字符串输入到流中或从流中输出字符串。

std::string str;
std::cin >> str; // 从标准输入流中读取字符串
std::cout << str; // 输出字符串到标准输出流

这些运算符重载使得对字符串的操作更加直观和方便,使得字符串的处理更类似于基本数据类型的操作。

五、字符串的性能问题

在处理大量字符串时,可能会面临一些性能问题,主要涉及以下几个方面:

1、 内存分配和释放开销:字符串的动态内存分配和释放可能会导致性能下降。频繁地创建、销毁和修改字符串对象可能会引起内存碎片问题,增加内存分配和释放的开销。为了减少这种开销,可以考虑使用预留空间的方式避免频繁的内存重新分配。

#include <iostream>
#include <string>

int main() {
    std::string str;

    // 预留10个字符的空间
    str.reserve(10);

    // 向字符串中添加字符
    str += "Hello";

    // 打印字符串的长度和容量
    std::cout << "Length: " << str.length() << std::endl;  // 输出:5
    std::cout << "Capacity: " << str.capacity() << std::endl;  // 输出:10

    return 0;
}

2、 拷贝和赋值开销:字符串的拷贝和赋值操作可能会涉及大量的内存复制,特别是对于较长的字符串。频繁地执行这些操作会影响程序的性能。可以使用引用或指针来避免不必要的拷贝,以及使用移动语义(move semantics)来避免不必要的内存复制。

// 使用指针传递字符串参数
void processString1(const std::string* strPtr) {
    // 处理字符串
}

// 使用引用传递字符串参数
void processString2(const std::string& strRef) {
    // 处理字符串
}
std::string str1 = "Hello";
std::string str2 = std::move(str1); // 使用移动语义将 str1 移动到 str2

// 函数接受一个字符串参数,使用移动语义
void processString3(std::string&& str) {
    // 处理字符串
    std::cout << "Processing string: " << str << std::endl;
}

int main() {
	processString1("Hello");
	processString2("Hello");
    processString3("Hello");
    return 0;
}

3、 字符串连接的性能问题:当需要将多个字符串连接成一个较长的字符串时,多次执行字符串连接操作可能会导致性能下降。这是因为+运算符在每次连接都需要重新分配内存,并复制原始字符串的内容。为了提高性能,可以考虑使用 std::stringappend() 函数或 += 运算符,以及使用 std::stringstream 类型来代替直接连接字符串。

// 直接连接字符串指的是使用 `+` 运算符将两个字符串连接起来。
std::string str1 = "Hello";
std::string str2 = "World";
std::string result = str1 + str2;

append() 函数和 += 运算符都不会引起额外的内存分配,因为它们在连接字符串时可以利用已有的内存空间。

也可以使用std::stringstream类,它在内存中创建一个字符串流(string stream)

#include <iostream>
#include <sstream>

int main() {
    // 格式化输出
    std::stringstream ss;
    int num = 123;
    ss << "The number is: " << num;
    std::string result = ss.str(); // 获取格式化后的字符串
    std::cout << result << std::endl; // 输出 "The number is: 123"

    // 解析字符串
    std::string input = "3.14 42 Hello";
    std::stringstream ss2(input);
    double pi;
    int answer;
    std::string greeting;
    ss2 >> pi >> answer >> greeting; // 解析字符串中的数据
    std::cout << "Pi: " << pi << ", Answer: " << answer << ", Greeting: " << greeting << std::endl;

    return 0;
}
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐