std::string 介绍

一 介绍

std::string 是 C++ 标准库中提供的一个类,用于表示和操作可变长度的字符串。它封装了字符数组,提供了丰富的成员函数来执行字符串的各种操作,如插入、删除、查找、替换、比较等。std::string 使得处理字符串变得更加安全和方便,因为它自动管理内存,避免了传统 C 风格字符串中的缓冲区溢出等问题。

std::string 的特点

  1. 动态内存分配std::string 使用动态内存分配来存储字符序列,因此它可以存储任意长度的字符串(实际上受限于系统内存限制)。

  2. 自动内存管理std::string 自动管理其内部字符数组的内存分配和释放,无需用户手动操作。

  3. 成员函数丰富std::string 提供了大量的成员函数,用于字符串的创建、修改、查询等操作。

  4. 标准库兼容性std::string 完全兼容 C++ 标准库,可以与其他容器、算法等无缝协作。

std::string 的内存布局

虽然 std::string 的具体实现可能因编译器和库的不同而有所差异,但一般来说,它包含以下几个部分:

  • 指向字符数组的指针:通常是一个指向动态分配字符数组的指针,用于存储字符串的实际内容。
  • 字符串长度:一个整数,用于存储当前字符串的长度。
  • 容量(可选):一个整数,表示当前分配的内存能够存储的最大字符数(包括结尾的空字符)。这个信息在某些实现中可能不存在,或者作为其他形式存在(如通过指针运算来隐式地表示)。

sizeof(std::string) 的值

由于 std::string 是一个类,sizeof(std::string) 返回的是该类实例在内存中的大小,即类内部所有非静态成员变量所占用的空间大小,不包括动态分配的内存。因此,这个值通常是一个固定的值,但它取决于编译器和库的具体实现。

  • 在一些常见的编译器(如 GCC 和 Clang)中,sizeof(std::string) 的值可能在 24 到 32 字节之间,但这只是一个大概的范围,具体值可能会有所不同。
  • 需要注意的是,这个大小只包括 std::string 对象本身所需的内存(如指针和长度信息等),并不包括实际存储字符串字符所需的动态分配的内存。

因此,当你询问 sizeof(std::string) 的值时,重要的是要理解它只表示了 std::string 对象本身的内存占用,而不包括其存储的字符串内容的内存占用。

#include <iostream>
#include <string>

int main() {
    
    
    std::string myString = "01234567890123456789";
    std::cout << "sizeof of the string object:   " << sizeof(myString) << " bytes" << std::endl;
    std::cout << "capacity of the string object: " << myString.capacity() << " bytes" << std::endl;
    std::cout << "size of the string object:     " << myString.size() << " bytes" << std::endl;
    std::cout << "length of the string object:   " << myString.length() << " bytes" << std::endl;
    std::cout << "------------------------------------" << std::endl;
    
    std::string myString2 = "0123456789";
    std::cout << "sizeof of the string object:   " << sizeof(myString2) << " bytes" << std::endl;
    std::cout << "capacity of the string object: " << myString2.capacity() << " bytes" << std::endl;
    std::cout << "size of the string object:     " << myString2.size() << " bytes" << std::endl;
    std::cout << "length of the string object:   " << myString2.length() << " bytes" << std::endl;
    return 0;
}

输出:

sizeof of the string object:   12 bytes
capacity of the string object: 31 bytes
size of the string object:     20 bytes
length of the string object:   20 bytes
------------------------------------
sizeof of the string object:   12 bytes
capacity of the string object: 10 bytes
size of the string object:     10 bytes
length of the string object:   10 bytes

请注意,上述代码中 sizeof(myString) 返回的是 std::string 对象的静态大小,而不是实际分配给字符串内容的内存大小。要获取实际分配给字符串内容的内存大小,可以使用 myString.capacity() 方法。


二 支持多种构造函数

std::string 在 C++ 标准库中提供了多种构造函数,以支持不同的初始化方式。以下是 std::string 的一些常用构造函数示例:

1. 默认构造函数

创建一个空字符串。

std::string str1; // str1 为空字符串

2. 字符指针构造函数

从 C 风格字符串(以 null 结尾的字符数组)初始化。

const char* cstr = "Hello";
std::string str2(cstr); // str2 包含 "Hello"

3. 字符和计数构造函数

通过给定的字符和重复次数来初始化字符串。

char ch = 'a';
std::string str3(5, ch); // str3 为 "aaaaa"

4. 字符串拷贝构造函数

从另一个 std::string 对象初始化。

std::string str4("Copy me");
std::string str5(str4); // str5 是 str4 的一个拷贝,包含 "Copy me"

5. 子字符串构造函数

从另一个 std::string 对象的子字符串初始化。

std::string original("Hello, World!");
std::string sub(original, 7, 5); // 从位置 7 开始,长度为 5 的子字符串,即 "World"

6. 初始化列表构造函数

从字符的初始化列表初始化(C++11 及以后)。

std::string initList = {
    
    'H', 'e', 'l', 'l', 'o'}; // 注意:末尾不需要 '\0'
// 或者使用 C++11 的列表初始化语法
std::string initListCpp11 = {
    
    "H", 'e', 'l', 'l', 'o'};

7. 移动构造函数(C++11 及以后)

从另一个(临时或右值引用的)std::string 对象“窃取”资源,避免不必要的拷贝。

std::string moveSource = "Move me";
std::string moveTarget(std::move(moveSource)); // moveSource 变为未定义状态,moveTarget 包含 "Move me"
// 注意:在实际使用中,避免再次使用 moveSource,除非重新给它赋值

8. 字符串字面量(隐式转换)

虽然不是直接通过构造函数调用,但 std::string 可以隐式地从字符串字面量(即常量字符数组)转换。

std::string str6 = "Implicit conversion"; // 隐式地从 "Implicit conversion" 字符串字面量创建

这些构造函数提供了灵活的方式来创建和初始化 std::string 对象,以适应不同的编程需求。

特别说明

在 C++ 中,表达式 std::string str6 = "test"; 使用了所谓的拷贝初始化(Copy Initialization)来构造 std::string 对象 str6。尽管这里看起来像是赋值操作,但实际上它是通过调用 std::string 的构造函数来完成的,具体来说是调用了一个接受 const char* 参数的构造函数。

构造过程详解

  1. 字面量转换为 const char*:首先,字符串字面量 "test" 在编译时会被存储在程序的只读数据段中,并且在运行时,它的地址可以被视为一个指向该字面量的 const char* 指针。

  2. 调用构造函数:然后,编译器会生成代码来调用 std::string 的一个构造函数,该构造函数接受一个 const char* 类型的参数。这个构造函数会负责从 C 风格字符串(即 const char* 指向的以 null 结尾的字符数组)中复制字符到 std::string 对象内部管理的动态分配的内存中。

  3. 对象构造std::string 对象 str6 被构造出来,并包含了从 "test" 字面量中复制的所有字符(在这个例子中是 ‘t’, ‘e’, ‘s’, ‘t’),以及一个额外的 null 终止符(这个终止符在 std::string 的内部表示中是不需要的,因为 std::string 使用长度字段来管理字符串的长度,但它在从 C 风格字符串复制时可能会被暂时使用)。然而,重要的是要理解,虽然 C 风格字符串以 null 结尾,但 std::string 对象并不包含这个 null 终止符作为字符串的一部分。

  4. 返回值:在这个特定的表达式中,没有返回值的概念,因为我们是在直接构造一个对象。但是,如果我们将这个表达式放在需要 std::string 类型值的地方(比如函数返回或赋值给另一个 std::string 对象),那么新构造的 std::string 对象 str6 将被用作那个上下文中的值。

拷贝初始化与直接初始化

值得注意的是,虽然在这个 std::string str6 = "test"; 例子中使用了拷贝初始化(因为使用了 =),但 C++ 还允许使用直接初始化,它通常更高效(尽管对于 std::string 和许多其他类型来说,这种差异可能很小或不存在):

std::string str7("test"); // 直接初始化

在直接初始化中,编译器会尝试使用与初始化列表中的参数最匹配的构造函数来直接构造对象,而不是先构造一个临时对象然后再赋值。然而,对于接受单个 const char* 参数的 std::string 构造函数来说,这两种初始化方式在效果上通常是相同的。


三 拷贝初始化

在 C++ 中,std::string 的拷贝初始化(Copy Initialization)是通过使用等号 = 或构造函数语法(但不使用圆括号包围初始化器)但不涉及显式类型转换或移动语义的情况下,从另一个值(如另一个 std::string 对象、C 风格字符串、字符等)创建 std::string 对象的过程。然而,严格来说,当直接使用 = 时,特别是在声明变量的同时,我们实际上是在进行拷贝初始化,但这里有一个微妙之处:C++ 标准中的术语“拷贝初始化”通常指的是通过调用拷贝构造函数(或相应的转换构造函数)来初始化对象,即使实际上可能发生了复制洗脱优化(Copy Elision)或移动语义的应用。

对于 std::string,当你说“拷贝初始化”时,你可能是指以下几种情况之一:

  1. 使用 = 初始化变量(尽管这可能更接近于赋值操作的语法,但在声明时它实际上是初始化):

    std::string str = "hello"; // 拷贝初始化,但实际上可能发生了复制洗脱优化
    

    在这里,尽管使用了 =,但实际上 str 是通过调用 std::string 的接受 const char* 的构造函数来初始化的。在某些编译器和编译设置下,这个构造过程可能会因为复制洗脱优化而被省略,但概念上它仍然被视为拷贝初始化。

  2. 使用构造函数语法但不使用圆括号(这在 C++11 及以后版本的列表初始化上下文中更为常见,但对于 std::string 的拷贝初始化来说不是典型的用法):

    std::string str = std::string("hello"); // 仍然是拷贝初始化,但稍显冗余
    

    这种写法虽然技术上可行,但通常不是初始化 std::string 的推荐方式,因为它比直接使用字符串字面量更冗长且效率更低(因为涉及到了额外的 std::string 对象的构造和可能的销毁)。

  3. 通过拷贝构造函数(虽然这通常不是通过简单的 = 初始化语法来展示的):

    std::string str1 = "hello";
    std::string str2 = str1; // 拷贝初始化,通过调用拷贝构造函数
    

    在这个例子中,str2 是通过调用 std::string 的拷贝构造函数(接受另一个 std::string 对象的引用作为参数)来初始化的。这是拷贝初始化的一个更直接和典型的例子。

重要的是要注意,尽管我们谈论的是“拷贝初始化”,但在现代 C++ 编译器中,由于复制洗脱优化(在 C++11 及更高版本中得到了增强)和可能的移动语义的应用,实际的拷贝或移动操作可能会被省略。然而,从概念上讲,我们仍然可以将这些初始化过程归类为拷贝初始化,因为它们涉及到了拷贝构造函数或相应的转换构造函数的调用(尽管这些调用可能在优化过程中被省略)。


四 复制洗脱优化

复制洗脱优化(Copy Elision) 是C++编译器的一种优化技术,用于在创建和销毁临时对象时减少不必要的拷贝或移动操作,从而提高程序的运行效率。在C++11及以后的版本中,复制洗脱优化得到了显著增强,并在C++17中引入了保证复制洗脱(Mandatory Copy Elision)的规则。

复制洗脱优化的应用场景

  1. 返回值优化(Return Value Optimization, RVO)

    • 当函数返回一个局部对象时,编译器可以省略从函数返回局部对象到调用者的拷贝或移动操作,而是直接构造返回对象的副本于调用者的作用域内。
    • 示例:std::string func() { return std::string("Hello"); } 在这个例子中,如果编译器应用了RVO,那么就不会创建临时的 std::string 对象来存储 "Hello",而是直接在调用 func() 的地方构造 std::string 对象。
  2. 命名返回值优化(Named Return Value Optimization, NRVO)

    • 这是RVO的一个特例,它适用于函数内部有一个命名的返回值对象,并且该对象被返回给调用者的情况。
    • 示例:std::string func() { std::string result = "Hello"; return result; } 如果编译器支持NRVO,那么同样可以省略从 result 到调用者的拷贝或移动操作。
  3. 临时对象的拷贝或移动省略

    • 在某些情况下,即使不是直接从函数返回对象,编译器也可以省略临时对象的拷贝或移动操作。
    • 示例:std::string str = std::string("Hello") + " World"; 在这个例子中,std::string("Hello")" World" 相加时会产生一个临时对象,但编译器可以省略这个临时对象到 str 的拷贝或移动操作,而是直接在 str 的位置构造最终的结果。

C++17中的保证复制洗脱

  • 从C++17开始,对于某些特定情况,编译器必须执行复制洗脱优化,而不是仅仅可以选择性地执行。这包括从函数返回局部对象给调用者,以及通过特定的初始化列表构造对象时。
  • 这意味着,在C++17及更高版本的编译器中,上述示例中的拷贝或移动操作几乎总是会被省略。

总结

复制洗脱优化是C++编译器提高程序性能的重要手段之一。它通过减少不必要的拷贝或移动操作来降低程序的内存使用和提高运行效率。在C++11及以后的版本中,随着复制洗脱优化的增强和C++17中保证复制洗脱的引入,程序员可以更加依赖这种优化来编写高效、简洁的代码。然而,需要注意的是,虽然编译器会尽力执行复制洗脱优化,但在某些情况下(如涉及到复杂构造函数或析构函数的类),优化可能不会被执行。因此,在编写代码时仍然需要注意避免不必要的拷贝或移动操作。


五 赋值操作符(operator=

实际上,std::stringoperator= 并不是用于构造 std::string 对象的。它是用于给已经构造的 std::string 对象赋值。构造 std::string 对象通常是通过构造函数来完成的,而不是赋值操作符。

然而,理解 operator= 的行为在处理 std::string 对象时仍然非常重要,因为它定义了如何将新值赋给已经存在的 std::string 对象。

这里是一些关于 std::string 构造函数和 operator= 的区别和用法的澄清:

构造函数

构造函数用于在创建对象时初始化对象。std::string 提供了多种构造函数,允许你以不同的方式初始化字符串,例如:

std::string str1; // 默认构造函数,创建一个空字符串
std::string str2("Hello"); // 使用 C 风格字符串初始化
std::string str3(str2); // 使用另一个 std::string 对象初始化(复制构造函数)
std::string str4(5, 'a'); // 使用字符和重复次数初始化

赋值操作符(operator=)

赋值操作符 operator= 用于将新值赋给已经存在的对象。对于 std::string,这意味着你可以将一个字符串(无论是 std::string 对象、C 风格字符串、字符数组、或单个字符)的值赋给另一个 std::string 对象。

std::string str1 = "Initial";
std::string str2;
str2 = str1; // 使用 operator= 赋值另一个 std::string 对象
str2 = "New Value"; // 使用 C 风格字符串赋值
str2 = 'A'; // 赋值单个字符,实际上是创建了一个只包含一个字符的字符串

注意事项

  • 当使用 operator= 赋值时,如果赋值操作符左侧的 std::string 对象之前已经分配了内存(即它不是通过默认构造函数创建的空字符串),那么这个赋值操作可能会导致旧内容的释放和新内容的分配(这取决于实现,但通常是如此)。
  • 赋值操作不会改变赋值操作符右侧对象的值或内容。
  • 如果赋值操作符左侧和右侧是同一个对象(即发生了自赋值),则好的 operator= 实现应该能够安全地处理这种情况,通常是通过检查自赋值并直接返回对象本身(但不做任何更改)来实现。

结论

std::stringoperator= 用于给已经存在的对象赋值,而不是用于构造对象。构造对象应该使用构造函数。


六 拼接操作符 operator+

std::stringoperator+std::string 类的一个友元函数(或者在某些实现中,可能是成员函数和友元函数的组合,但通常表现为友元函数以支持更灵活的参数类型),它用于连接(拼接)两个或多个 std::string 对象或字符串字面量,并返回一个新的 std::string 对象,该对象包含了连接后的结果。

基本用法

operator+ 的基本用法是将两个 std::string 对象相加,或者将一个 std::string 对象与一个字符串字面量相加,生成一个新的 std::string 对象。这个操作符可以连续使用以实现多个字符串的连接。

#include <iostream>
#include <string>

int main() {
    
    
    std::string str1 = "Hello, ";
    std::string str2 = "World!";
    std::string result;

    // 使用 operator+ 连接 str1 和 str2
    result = str1 + str2;

    // 输出结果
    std::cout << "Result: " << result << std::endl; // 输出: Result: Hello, World!

    // 也可以与字符串字面量一起使用
    std::string greeting = "Hello, " + "World!" + " How are you?"; //compile error!

    // 输出 greeting
    std::cout << "Greeting: " << greeting << std::endl; // 输出: Greeting: Hello, World! How are you?

    return 0;
}

上述代码有一个编译错误,因为 operator+ 不支持两个 字符串字面量 相加。
例如下面:
请添加图片描述

注意事项

  • 当与字符串字面量一起使用时,由于字符串字面量的类型是 const char* 而不是 std::string,因此 operator+ 实际上是通过隐式转换将字符串字面量转换为 std::string 对象,然后再进行连接。这种转换是安全的,但可能会对性能产生一定影响,因为每次连接时都可能涉及到额外的内存分配和拷贝操作。
  • 由于 operator+ 返回的是一个新的 std::string 对象,因此它可能会导致不必要的内存分配和拷贝,特别是在进行多次连接时。为了提高性能,可以考虑使用 std::ostringstream 或 C++17 引入的字符串连接初始化列表(如果可用)来构建最终的字符串。
  • 在某些情况下,编译器可能会优化掉不必要的内存分配和拷贝,但这取决于编译器的具体实现和编译器的优化设置。

性能优化

为了优化多个字符串的连接操作,可以考虑以下替代方案:

  • 使用 std::ostringstreamstd::ostringstream 提供了一个流接口来构建字符串,它可以在内部进行高效的内存管理,减少不必要的内存分配和拷贝。

  • C++17 字符串连接初始化列表(如果可用):在 C++17 及更高版本中,可以使用初始化列表语法来初始化 std::string,这可以更高效地进行多个字符串的连接。但是,请注意,这实际上是构造了一个新的 std::string 对象,而不是使用 operator+ 进行连接。

  • 手动管理内存:对于极端性能敏感的场景,可以考虑使用原始字符数组和手动内存管理来构建字符串,但这通常不推荐,因为它增加了代码复杂性和出错的可能性。

猜你喜欢

转载自blog.csdn.net/lijian2017/article/details/140610586