MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C++ 字符串函数要注意的关键点

2024-08-141.7k 阅读

字符串函数基础介绍

在 C++ 中,处理字符串是一项常见的任务。C++ 提供了丰富的字符串函数来满足各种需求,从简单的字符串拼接、查找,到复杂的格式化和转换。这些函数分布在不同的头文件中,主要涉及 <cstring><string> 头文件。<cstring> 头文件主要包含传统 C 风格字符串(以 null 结尾的字符数组)的函数,而 <string> 头文件则提供了 C++ 风格字符串类 std::string 的成员函数和相关的非成员函数。

<cstring> 头文件中的字符串函数

strlen 函数

strlen 函数用于计算 C 风格字符串的长度,不包括字符串末尾的 null 字符。其函数原型为:

size_t strlen(const char* str);

示例代码如下:

#include <iostream>
#include <cstring>

int main() {
    const char* str = "Hello, World!";
    size_t length = strlen(str);
    std::cout << "Length of the string: " << length << std::endl;
    return 0;
}

在这个例子中,strlen 函数准确地返回了字符串 "Hello, World!" 的字符数,不包括末尾的 null 字符。需要注意的是,如果传入的指针不是指向以 null 结尾的字符串,strlen 函数会继续读取内存,直到遇到 null 字符,这会导致未定义行为。

strcpy 函数

strcpy 函数用于将一个 C 风格字符串复制到另一个字符数组中。其函数原型为:

char* strcpy(char* destination, const char* source);

示例代码:

#include <iostream>
#include <cstring>

int main() {
    char source[] = "Hello";
    char destination[10];
    strcpy(destination, source);
    std::cout << "Copied string: " << destination << std::endl;
    return 0;
}

这里需要特别注意的是,目标数组 destination 必须足够大,以容纳源字符串及其末尾的 null 字符。如果目标数组大小不足,会导致缓冲区溢出,这是一个严重的安全漏洞,可能被攻击者利用来执行恶意代码。

strcat 函数

strcat 函数用于将一个 C 风格字符串追加到另一个字符串的末尾。其函数原型为:

char* strcat(char* destination, const char* source);

示例代码:

#include <iostream>
#include <cstring>

int main() {
    char str1[20] = "Hello, ";
    char str2[] = "World!";
    strcat(str1, str2);
    std::cout << "Concatenated string: " << str1 << std::endl;
    return 0;
}

同样,目标数组 str1 必须有足够的空间来容纳追加后的字符串。否则,会发生缓冲区溢出。在实际应用中,要谨慎使用 strcat,特别是在处理用户输入的字符串时,因为缓冲区溢出可能导致程序崩溃或安全问题。

strcmp 函数

strcmp 函数用于比较两个 C 风格字符串。它返回一个整数值,根据比较结果来判断两个字符串的大小关系。其函数原型为:

int strcmp(const char* str1, const char* str2);

返回值为:

  • 如果 str1 小于 str2,返回一个负整数。
  • 如果 str1 等于 str2,返回 0。
  • 如果 str1 大于 str2,返回一个正整数。 示例代码:
#include <iostream>
#include <cstring>

int main() {
    const char* str1 = "apple";
    const char* str2 = "banana";
    int result = strcmp(str1, str2);
    if (result < 0) {
        std::cout << str1 << " is less than " << str2 << std::endl;
    } else if (result == 0) {
        std::cout << str1 << " is equal to " << str2 << std::endl;
    } else {
        std::cout << str1 << " is greater than " << str2 << std::endl;
    }
    return 0;
}

strcmp 函数是按字符的 ASCII 码值进行比较的。如果需要不区分大小写的比较,可以使用 stricmp 函数(在 Windows 平台的 <cstring> 中有,在 POSIX 系统中可以通过 strcasecmp 实现类似功能)。

<string> 头文件中的 std::string 类函数

构造函数

std::string 类有多个构造函数,提供了灵活的方式来初始化字符串对象。

  • 无参构造函数:
std::string s1; // 创建一个空字符串
  • 以 C 风格字符串初始化:
const char* cstr = "Hello";
std::string s2(cstr);
  • 以指定字符重复一定次数初始化:
std::string s3(5, 'a'); // 创建字符串 "aaaaa"
  • 从另一个 std::string 对象初始化:
std::string s4(s2);

字符串拼接

std::string 类重载了 + 运算符和 += 运算符,用于字符串拼接。 示例代码:

#include <iostream>
#include <string>

int main() {
    std::string s1 = "Hello";
    std::string s2 = ", World!";
    std::string s3 = s1 + s2;
    std::cout << "Concatenated string: " << s3 << std::endl;

    s1 += s2;
    std::cout << "Concatenated string with +=: " << s1 << std::endl;
    return 0;
}

std::string 类在拼接时会自动管理内存,避免了 C 风格字符串拼接时可能出现的缓冲区溢出问题。同时,append 成员函数也可以用于字符串拼接,它提供了更多的拼接方式,例如可以从指定位置开始拼接另一个字符串的一部分。

std::string s4 = "Hello";
std::string s5 = ", World!";
s4.append(s5, 2, 5); // 从 s5 的第 2 个字符开始,拼接 5 个字符
std::cout << "Appended string: " << s4 << std::endl;

字符串查找

std::string 类提供了丰富的查找函数,如 findrfindfind_first_offind_last_offind_first_not_offind_last_not_of 等。

  • find 函数用于查找子字符串首次出现的位置:
std::string s = "Hello, World!";
size_t pos = s.find("World");
if (pos != std::string::npos) {
    std::cout << "Substring found at position: " << pos << std::endl;
} else {
    std::cout << "Substring not found" << std::endl;
}

find 函数返回子字符串首次出现的位置,如果未找到则返回 std::string::nposrfind 函数则是查找子字符串最后一次出现的位置。

  • find_first_of 函数用于查找字符串中首次出现指定字符集合中的任意一个字符的位置:
std::string s6 = "Hello, World!";
std::string chars = "aeiou";
size_t pos2 = s6.find_first_of(chars);
if (pos2 != std::string::npos) {
    std::cout << "First vowel found at position: " << pos2 << std::endl;
} else {
    std::cout << "No vowel found" << std::endl;
}

find_last_offind_first_not_offind_last_not_of 函数的功能与之类似,但查找逻辑有所不同。在使用这些查找函数时,要注意 std::string::npos 的返回值处理,以确保程序的健壮性。

字符串替换

std::string 类的 replace 函数可以用于替换字符串中的部分内容。 示例代码:

std::string s7 = "Hello, World!";
s7.replace(7, 5, "Universe");
std::cout << "Replaced string: " << s7 << std::endl;

这里 replace 函数从位置 7 开始,替换 5 个字符为 "Universe"replace 函数还有其他重载形式,可以根据不同的需求进行灵活的字符串替换操作,例如替换指定子字符串为另一个字符串,或者替换指定字符范围为另一个字符串等。

字符串截取

substr 函数用于截取字符串的一部分。其函数原型为:

std::string substr(size_t pos = 0, size_t len = std::string::npos) const;

示例代码:

std::string s8 = "Hello, World!";
std::string sub = s8.substr(7, 5);
std::cout << "Substring: " << sub << std::endl;

在这个例子中,从位置 7 开始截取 5 个字符,得到 "World"。如果不指定 len,则截取从 pos 开始到字符串末尾的所有字符。

字符串格式化与转换

sprintf 与 snprintf

在 C 风格中,sprintf 函数用于将格式化的数据写入字符串。其函数原型为:

int sprintf(char* str, const char* format, ...);

示例代码:

#include <iostream>
#include <cstdio>

int main() {
    int num = 42;
    char str[50];
    sprintf(str, "The number is %d", num);
    std::cout << "Formatted string: " << str << std::endl;
    return 0;
}

然而,sprintf 存在缓冲区溢出的风险,因为它不会检查目标数组是否足够大。为了避免这种情况,应使用 snprintf 函数,它会限制写入的字符数,防止缓冲区溢出。其函数原型为:

int snprintf(char* str, size_t size, const char* format, ...);

示例代码:

#include <iostream>
#include <cstdio>

int main() {
    int num = 42;
    char str[10];
    int result = snprintf(str, sizeof(str), "The number is %d", num);
    if (result >= sizeof(str)) {
        std::cout << "Truncated. String was too long." << std::endl;
    } else {
        std::cout << "Formatted string: " << str << std::endl;
    }
    return 0;
}

std::stringstream

在 C++ 中,std::stringstream 类提供了一种更安全、更灵活的字符串格式化和转换方式。它位于 <sstream> 头文件中。 示例代码:

#include <iostream>
#include <sstream>

int main() {
    int num = 42;
    std::stringstream ss;
    ss << "The number is " << num;
    std::string result = ss.str();
    std::cout << "Formatted string: " << result << std::endl;

    // 从字符串转换回数字
    std::string str = "123";
    int num2;
    std::stringstream ss2(str);
    ss2 >> num2;
    std::cout << "Converted number: " << num2 << std::endl;
    return 0;
}

std::stringstream 可以方便地将各种数据类型转换为字符串,也可以将字符串转换为其他数据类型。它自动管理内存,并且不会像 sprintf 那样存在缓冲区溢出的风险。

字符串编码与国际化

在处理字符串时,特别是在国际化应用中,需要考虑字符串的编码问题。常见的编码方式有 ASCII、UTF - 8、UTF - 16 和 UTF - 32 等。

ASCII 编码

ASCII 编码使用 7 位二进制数表示 128 个字符,主要用于表示英文字母、数字和一些常用符号。它的优点是简单、占用空间小,但只能表示基本的拉丁字符集,无法满足多语言支持的需求。

UTF - 8 编码

UTF - 8 是一种变长编码方式,它可以表示世界上几乎所有的字符。UTF - 8 编码中,英文字母等 ASCII 字符用 1 个字节表示,其他字符根据需要用 2 到 4 个字节表示。在 C++ 中,处理 UTF - 8 编码的字符串时,要注意字符长度的计算和处理。例如,在 std::string 中,一个 UTF - 8 编码的字符可能占用多个字节,不能简单地用 strlenstd::string::size 来获取字符个数。可以使用一些第三方库,如 ICU(International Components for Unicode)来正确处理 UTF - 8 编码的字符串,包括字符计数、字符串查找、排序等操作。 示例代码(使用 ICU 库计算 UTF - 8 字符串的字符个数):

#include <iostream>
#include <unicode/ustring.h>
#include <unicode/utf8.h>

int main() {
    const char* utf8Str = "你好,世界";
    icu::UnicodeString ustr;
    icu::UTF8::toUnicodeString(utf8Str, ustr);
    std::cout << "Character count: " << ustr.length() << std::endl;
    return 0;
}

UTF - 16 编码

UTF - 16 也是一种变长编码,它使用 16 位代码单元来表示字符。对于基本多文种平面(BMP)内的字符,使用一个 16 位代码单元表示;对于辅助平面内的字符,使用两个 16 位代码单元(代理对)表示。在 Windows 平台上,std::wstring 通常用于处理 UTF - 16 编码的字符串。std::wstring 类提供了类似 std::string 的功能,但针对宽字符。例如,可以使用 std::wcout 来输出 UTF - 16 编码的字符串。 示例代码:

#include <iostream>
#include <string>

int main() {
    std::wstring ws = L"你好,世界";
    std::wcout << ws << std::endl;
    return 0;
}

UTF - 32 编码

UTF - 32 是一种定长编码,每个字符都用 32 位(4 个字节)表示。这种编码方式简单直观,字符索引方便,但占用空间较大。在 C++ 中,没有标准库直接支持 UTF - 32 编码的字符串类型,但可以通过自定义结构体或使用第三方库来处理。

性能考虑

在处理字符串时,性能是一个重要的考虑因素。不同的字符串操作和函数在性能上可能有很大差异。

C 风格字符串与 std::string 的性能比较

C 风格字符串函数通常比较底层,直接操作字符数组,在一些简单场景下可能具有较好的性能,因为它们没有额外的对象管理开销。例如,strlen 函数在计算字符串长度时,直接遍历字符数组直到遇到 null 字符,效率较高。然而,C 风格字符串在处理复杂操作,如字符串拼接、动态内存管理时,需要手动处理内存,容易出错且性能可能不佳。

std::string 类提供了更高级的抽象和自动内存管理,使用起来更安全和方便。在字符串拼接等操作中,std::string 会自动调整内存大小,但这也带来了一定的性能开销,特别是在频繁拼接大量字符串的场景下。为了优化性能,可以预先分配足够的空间,避免频繁的内存重新分配。例如,在使用 += 运算符拼接字符串之前,可以使用 reserve 函数预先分配足够的空间:

std::string s9;
s9.reserve(100); // 预先分配 100 个字符的空间
s9 += "Hello";
s9 += ", World!";

字符串查找与替换的性能优化

在进行字符串查找和替换操作时,不同的算法和函数选择会影响性能。例如,std::string::find 函数使用的是简单的线性查找算法,在查找长字符串中的子字符串时,时间复杂度为 O(n * m),其中 n 是主字符串的长度,m 是子字符串的长度。对于大规模数据的查找,可以考虑使用更高效的算法,如 Boyer - Moore 算法或 Rabin - Karp 算法,一些第三方库提供了这些算法的实现。

在字符串替换操作中,如果需要替换多个子字符串,可以考虑一次性处理,而不是多次调用 replace 函数,以减少字符串重新分配内存的次数,提高性能。

多线程环境下的字符串处理

在多线程编程中,处理字符串需要特别小心,因为字符串操作可能不是线程安全的。

C 风格字符串函数的线程安全性

<cstring> 头文件中的大多数函数都不是线程安全的。例如,strtok 函数使用静态内部变量来保存字符串解析的状态,在多线程环境下同时调用 strtok 会导致数据竞争和未定义行为。如果在多线程环境中需要使用类似 strtok 的功能,可以使用 strtok_r 函数(在 POSIX 系统中),它通过传入一个额外的上下文指针来避免静态变量带来的问题。

std::string 的线程安全性

std::string 类的大多数成员函数是线程安全的,但有一些需要注意的地方。例如,std::string 的非 const 成员函数在多线程环境下可能会修改字符串的内部状态,因此如果多个线程同时调用这些非 const 成员函数,需要进行同步。可以使用互斥锁(如 std::mutex)来保护对 std::string 对象的访问。 示例代码:

#include <iostream>
#include <string>
#include <thread>
#include <mutex>

std::string sharedStr;
std::mutex strMutex;

void modifyString() {
    std::lock_guard<std::mutex> lock(strMutex);
    sharedStr += " World!";
}

int main() {
    sharedStr = "Hello";
    std::thread t1(modifyString);
    std::thread t2(modifyString);

    t1.join();
    t2.join();

    std::cout << "Shared string: " << sharedStr << std::endl;
    return 0;
}

在这个例子中,使用 std::lock_guard 来自动管理互斥锁,确保在多个线程同时修改 sharedStr 时不会发生数据竞争。

字符串函数的安全使用

在使用字符串函数时,安全是至关重要的。除了前面提到的缓冲区溢出问题外,还有其他一些安全隐患需要注意。

防止注入攻击

在处理用户输入的字符串并将其用于格式化字符串、SQL 查询等场景时,要防止注入攻击。例如,在使用 sprintfstd::stringstream 进行格式化时,如果直接将用户输入作为格式化字符串的一部分,可能会导致格式化字符串注入攻击。 示例代码(存在风险的代码):

#include <iostream>
#include <cstdio>

int main() {
    char userInput[50];
    std::cout << "Enter some text: ";
    std::cin.getline(userInput, sizeof(userInput));
    char result[100];
    sprintf(result, "You entered: %s", userInput);
    std::cout << result << std::endl;
    return 0;
}

如果用户输入恶意字符串,如 %s %s %s,可能会导致程序崩溃或泄露敏感信息。为了防止这种情况,应避免将用户输入直接作为格式化字符串的一部分,或者对用户输入进行严格的验证和过滤。

内存管理与资源泄漏

在使用 C 风格字符串函数时,要注意正确的内存管理。例如,使用 strdup 函数(它返回一个新分配的字符串副本)后,需要及时释放分配的内存,否则会导致内存泄漏。在使用 std::string 类时,虽然它自动管理内存,但在某些情况下,如将 std::string 对象转换为 C 风格字符串(通过 c_str 函数获取指针)并传递给需要释放内存的函数时,也要注意内存的释放。 示例代码(正确释放 strdup 分配的内存):

#include <iostream>
#include <cstring>
#include <cstdlib>

int main() {
    const char* original = "Hello";
    char* copy = strdup(original);
    if (copy != nullptr) {
        std::cout << "Copied string: " << copy << std::endl;
        free(copy);
    }
    return 0;
}

通过深入理解和注意这些关键点,能够在 C++ 编程中更安全、高效地使用字符串函数,避免常见的错误和安全隐患,提高程序的质量和稳定性。无论是处理简单的文本操作,还是复杂的国际化应用,正确运用字符串函数都是非常重要的。在实际开发中,要根据具体的需求和场景,选择合适的字符串处理方式和函数,同时注重性能优化和安全问题。