C++ 字符串和格式化输入输出
C++ 字符串
字符串基础概念
在 C++ 中,字符串是一种表示文本的数据类型。从本质上来说,C++ 支持两种主要的字符串表示形式:C 风格字符串和 C++ 标准库的 std::string
。
C 风格字符串
C 风格字符串是以空字符 '\0'
结尾的字符数组。例如,我们可以这样定义一个 C 风格字符串:
char greeting[] = "Hello, World!";
这里,greeting
是一个字符数组,编译器会自动在字符串字面量 "Hello, World!"
的末尾添加空字符 '\0'
。数组的大小实际上是字符串的长度(不包括 '\0'
)加上 1。
需要注意的是,由于 C 风格字符串是基于数组的,所以它们在使用上有一些限制。例如,对 C 风格字符串进行复制、拼接等操作相对繁琐。下面是一个手动复制 C 风格字符串的示例:
#include <iostream>
#include <cstring>
int main() {
char source[] = "Hello";
char destination[6]; // 足够大以容纳源字符串和 '\0'
std::strcpy(destination, source);
std::cout << "Copied string: " << destination << std::endl;
return 0;
}
在这个例子中,我们使用了 <cstring>
头文件中的 std::strcpy
函数来复制字符串。如果目标数组的大小不足以容纳源字符串(包括 '\0'
),就会导致缓冲区溢出,这是一种严重的安全漏洞。
C++ std::string
std::string
是 C++ 标准库提供的字符串类,它在很大程度上简化了字符串的操作。std::string
会自动管理内存,并且提供了丰富的成员函数来进行字符串的各种操作,如复制、拼接、查找等。
要使用 std::string
,我们需要包含 <string>
头文件。以下是一个简单的示例:
#include <iostream>
#include <string>
int main() {
std::string str = "Hello, ";
str += "World!";
std::cout << str << std::endl;
return 0;
}
在这个例子中,我们首先定义了一个 std::string
对象 str
,并初始化为 "Hello, "
。然后使用 +=
运算符将 "World!"
拼接到 str
后面。std::string
类会自动管理内存,确保拼接操作的安全和便捷。
字符串操作
字符串的基本操作
- 赋值操作
对于 C 风格字符串,如前文所述,使用
std::strcpy
函数进行赋值。而对于std::string
,可以直接使用=
运算符。例如:
#include <iostream>
#include <string>
int main() {
std::string s1 = "Initial string";
std::string s2;
s2 = s1;
std::cout << "s2: " << s2 << std::endl;
return 0;
}
- 拼接操作
对于 C 风格字符串,拼接相对复杂,通常需要使用
std::strcat
函数,并且要确保目标数组有足够的空间。而std::string
提供了更简洁的方式,除了+=
运算符外,还可以使用append
成员函数。例如:
#include <iostream>
#include <string>
int main() {
std::string s1 = "Hello";
std::string s2 = " World";
s1.append(s2);
std::cout << "Concatenated string: " << s1 << std::endl;
return 0;
}
- 比较操作
C 风格字符串使用
std::strcmp
函数进行比较,而std::string
可以直接使用比较运算符(<
,>
,<=
,>=
,==
,!=
)。例如:
#include <iostream>
#include <string>
int main() {
std::string s1 = "apple";
std::string s2 = "banana";
if (s1 < s2) {
std::cout << s1 << " comes before " << s2 << " alphabetically." << std::endl;
}
return 0;
}
字符串查找与替换
- 查找操作
std::string
类提供了多种查找函数,如find
、rfind
(从后往前查找)、find_first_of
、find_last_of
等。例如,查找子字符串:
#include <iostream>
#include <string>
int main() {
std::string str = "Hello, World! Hello, C++!";
size_t pos = str.find("Hello");
if (pos != std::string::npos) {
std::cout << "Substring found at position: " << pos << std::endl;
} else {
std::cout << "Substring not found." << std::endl;
}
return 0;
}
- 替换操作
std::string
类的replace
函数可以用于替换字符串中的子字符串。例如:
#include <iostream>
#include <string>
int main() {
std::string str = "Hello, World!";
str.replace(0, 5, "Hi");
std::cout << "Replaced string: " << str << std::endl;
return 0;
}
在这个例子中,replace
函数将从位置 0 开始的 5 个字符(即 "Hello"
)替换为 "Hi"
。
字符串截取
std::string
类的 substr
函数可以用于截取字符串的一部分。例如:
#include <iostream>
#include <string>
int main() {
std::string str = "Hello, World!";
std::string sub = str.substr(7, 5); // 从位置 7 开始截取 5 个字符
std::cout << "Substring: " << sub << std::endl;
return 0;
}
这里,sub
将会是 "World"
。
C++ 格式化输入输出
输入输出流基础
在 C++ 中,输入输出操作是通过输入输出流(I/O Streams)来实现的。标准输入输出流主要有 std::cin
(标准输入)、std::cout
(标准输出)和 std::cerr
(标准错误输出),它们都定义在 <iostream>
头文件中。
基本输入输出
- 输出操作
std::cout
用于向标准输出设备(通常是控制台)输出数据。我们可以使用<<
运算符来输出各种数据类型,包括字符串、整数、浮点数等。例如:
#include <iostream>
int main() {
int num = 42;
std::cout << "The number is: " << num << std::endl;
return 0;
}
在这个例子中,我们首先定义了一个整数变量 num
,然后使用 std::cout
和 <<
运算符将字符串 "The number is: "
和变量 num
的值输出到控制台。std::endl
用于输出一个换行符并刷新缓冲区。
- 输入操作
std::cin
用于从标准输入设备(通常是键盘)读取数据。我们使用>>
运算符来读取数据到变量中。例如:
#include <iostream>
int main() {
int num;
std::cout << "Enter a number: ";
std::cin >> num;
std::cout << "You entered: " << num << std::endl;
return 0;
}
在这个例子中,程序首先提示用户输入一个数字,然后使用 std::cin
和 >>
运算符将用户输入的值读取到变量 num
中,并输出用户输入的数字。
格式化输出
使用 std::cout
进行格式化输出
- 控制整数输出格式
- 进制控制:我们可以使用
std::hex
、std::dec
和std::oct
操纵符来控制整数的输出进制。例如:
- 进制控制:我们可以使用
#include <iostream>
#include <iomanip>
int main() {
int num = 42;
std::cout << "Decimal: " << std::dec << num << std::endl;
std::cout << "Hexadecimal: " << std::hex << num << std::endl;
std::cout << "Octal: " << std::oct << num << std::endl;
return 0;
}
- **宽度和填充**:`std::setw` 操纵符用于设置输出宽度,`std::setfill` 操纵符用于设置填充字符。例如:
#include <iostream>
#include <iomanip>
int main() {
int num = 42;
std::cout << std::setw(5) << std::setfill('0') << num << std::endl;
return 0;
}
在这个例子中,std::setw(5)
设置输出宽度为 5,std::setfill('0')
设置填充字符为 '0'
,所以输出结果为 00042
。
- 控制浮点数输出格式
- 精度控制:
std::setprecision
操纵符用于控制浮点数的输出精度。例如:
- 精度控制:
#include <iostream>
#include <iomanip>
int main() {
double pi = 3.14159265358979323846;
std::cout << std::setprecision(4) << pi << std::endl;
return 0;
}
这里,std::setprecision(4)
设置输出精度为 4,所以输出结果为 3.142
。
- 科学计数法:可以使用 std::scientific
操纵符将浮点数以科学计数法形式输出。例如:
#include <iostream>
#include <iomanip>
int main() {
double num = 123456789.123456789;
std::cout << std::scientific << num << std::endl;
return 0;
}
输出结果可能为 1.234568e+08
。
使用 printf
进行格式化输出
printf
是 C 语言中常用的格式化输出函数,在 C++ 中仍然可以使用,需要包含 <cstdio>
头文件。printf
使用格式化字符串来指定输出格式。例如:
#include <cstdio>
int main() {
int num = 42;
double pi = 3.14159;
printf("The number is %d and pi is %.2f\n", num, pi);
return 0;
}
在这个例子中,%d
是整数的格式说明符,%.2f
是浮点数的格式说明符,.2
表示保留两位小数。
格式化输入
使用 std::cin
进行格式化输入
std::cin
在读取基本数据类型时,会自动根据变量的类型进行格式化输入。例如,读取整数和浮点数:
#include <iostream>
int main() {
int num;
double pi;
std::cout << "Enter an integer and a double: ";
std::cin >> num >> pi;
std::cout << "You entered: " << num << " and " << pi << std::endl;
return 0;
}
然而,当读取字符串时,std::cin
会在遇到空白字符(空格、制表符、换行符等)时停止读取。如果要读取包含空白字符的字符串,可以使用 std::getline
函数。例如:
#include <iostream>
#include <string>
int main() {
std::string str;
std::cout << "Enter a sentence: ";
std::getline(std::cin, str);
std::cout << "You entered: " << str << std::endl;
return 0;
}
使用 scanf
进行格式化输入
scanf
是 C 语言中的格式化输入函数,在 C++ 中也可使用,需包含 <cstdio>
头文件。scanf
使用格式化字符串来指定输入格式。例如:
#include <cstdio>
int main() {
int num;
double pi;
printf("Enter an integer and a double: ");
scanf("%d %lf", &num, &pi);
printf("You entered: %d and %lf\n", num, pi);
return 0;
}
在这个例子中,%d
用于读取整数,%lf
用于读取双精度浮点数。需要注意的是,scanf
在处理输入错误时相对复杂,使用不当容易导致程序错误,而 std::cin
在错误处理方面相对更友好。
字符串流与格式化
std::stringstream
std::stringstream
是 C++ 标准库中的一个类,定义在 <sstream>
头文件中。它允许我们将字符串当作流来处理,进行格式化输入输出操作。
- 从字符串读取数据 例如,我们有一个包含整数和浮点数的字符串,想从中提取这些数据:
#include <iostream>
#include <sstream>
#include <string>
int main() {
std::string str = "42 3.14";
std::stringstream ss(str);
int num;
double pi;
ss >> num >> pi;
std::cout << "Read: " << num << " and " << pi << std::endl;
return 0;
}
在这个例子中,我们首先创建了一个 std::stringstream
对象 ss
,并将字符串 str
作为参数传递给它。然后使用 >>
运算符从 ss
中读取整数和浮点数。
- 向字符串写入数据
我们也可以使用
std::stringstream
将数据格式化为字符串。例如:
#include <iostream>
#include <sstream>
#include <string>
int main() {
int num = 42;
double pi = 3.14;
std::stringstream ss;
ss << "The number is " << num << " and pi is " << pi;
std::string result = ss.str();
std::cout << result << std::endl;
return 0;
}
在这个例子中,我们使用 <<
运算符将数据写入 std::stringstream
对象 ss
,然后通过 str
成员函数获取格式化后的字符串。
std::istringstream
和 std::ostringstream
std::istringstream
专门用于从字符串读取数据,std::ostringstream
专门用于向字符串写入数据。它们的使用方法与 std::stringstream
类似,但功能更为单一。例如:
#include <iostream>
#include <sstream>
#include <string>
int main() {
// 使用 std::istringstream 读取字符串
std::string readStr = "10 20.5";
std::istringstream iss(readStr);
int intVal;
double doubleVal;
iss >> intVal >> doubleVal;
std::cout << "Read int: " << intVal << ", double: " << doubleVal << std::endl;
// 使用 std::ostringstream 写入字符串
std::ostringstream oss;
int num = 42;
double pi = 3.14;
oss << "The number is " << num << " and pi is " << pi;
std::string writeStr = oss.str();
std::cout << "Written string: " << writeStr << std::endl;
return 0;
}
通过这些字符串流类,我们可以更灵活地在字符串和其他数据类型之间进行转换和格式化操作,为 C++ 程序的字符串处理和输入输出提供了强大的支持。无论是处理复杂的文本解析,还是生成格式化的报告,字符串流都能发挥重要作用。同时,在实际编程中,根据具体需求选择合适的字符串表示形式(C 风格字符串或 std::string
)以及合适的输入输出方式(std::cout
、printf
等),对于编写高效、可靠的代码至关重要。
在处理较大规模的字符串数据或者对性能有较高要求的场景下,可能需要更深入地了解底层实现细节,例如 std::string
的内存管理策略。std::string
在现代 C++ 实现中通常采用写时复制(Copy - on - Write,COW)或者短字符串优化(Small String Optimization,SSO)等技术来提高性能。写时复制技术可以避免不必要的字符串复制操作,只有在字符串内容真正需要修改时才进行复制。而短字符串优化则针对较短的字符串,将字符串内容直接存储在 std::string
对象内部,避免了动态内存分配的开销。
在输入输出方面,了解缓冲区的工作原理也很重要。std::cout
等流对象都有缓冲区,数据会先写入缓冲区,当缓冲区满、遇到 std::endl
或者手动调用 flush
函数时,缓冲区的数据才会真正输出到目标设备。对于频繁的输出操作,如果不恰当处理缓冲区,可能会影响程序的性能。例如,在一些对实时性要求较高的程序中,可能需要及时刷新缓冲区以确保数据能够尽快显示在控制台或者写入文件。
此外,在处理国际化和本地化相关的字符串操作时,C++ 标准库也提供了一些支持。例如,std::locale
类可以用于设置不同的本地化环境,影响字符串的格式化、排序等操作。通过设置合适的本地化环境,我们可以确保程序在不同地区和语言环境下正确地处理日期、时间、货币等数据的显示和输入。
在实际项目开发中,还需要考虑到字符串操作和输入输出的安全性。例如,避免 C 风格字符串操作中的缓冲区溢出问题,以及在格式化输入输出中正确处理用户输入,防止格式化字符串漏洞(如在使用 printf
和 scanf
时确保格式化字符串与实际参数匹配)。同时,对于网络编程中的字符串处理,还需要注意字符编码的转换,确保不同编码格式之间的数据能够正确传输和处理。
总之,C++ 的字符串和格式化输入输出是一个丰富而复杂的领域,深入理解其原理和各种操作方式,能够帮助我们编写出更健壮、高效且功能丰富的程序。无论是开发小型的控制台应用,还是大型的企业级软件,掌握这些知识都是非常重要的。