C与C++风格的字符串辨析
C++中实际上存在两类字符串,一类是常说的标准库中的string,另一类是C风格的字符串。
正如C++ primer中所述:
尽管C++支持C风格的字符串,但是在C++程序中最好还是不要使用它们。这是因为C风格字符串不仅使用起来不太方便,而且极易引发程序漏洞,是诸多安全问题的根本原因。
确实C风格的字符串及其难用,而且非常容易发生问题。
但是,我们不能完全不了解C风格的字符串,因为很多程序和相关的API在标准库出现前就已经完成。而且其中很多涉及重要的系统调用,例如网络编程中就有大量的C风格字符串的使用。我们可以嫌弃它,但是掌握它也是必须的。
下面就C与C++风格的字符串中一些使用上极易混淆的地方做一些辨析。
1. 关于头文件
-
C风格的字符串是
string.h
。 -
cstring
是C风格的string.h在C++下的对应头文件。 -
C++风格的字符串是
string
(STL标准库)。
实际是C++是兼容上述三个头文件的。
-
string.h和cstring都提供了对于C风格字符串的相关操作。
-
而string是STL标准库提供的模板类容器。
他们是完全不一样的,如需详细了解,可以查阅cppreference.com文档。
2. 关于C风格字符串的初始化与赋值操作
2.1 不同方式的初始化与其存储位置
C风格的字符串有两大类初始化方式:
// 第一类:字符数组形式
// 指定大小
char c[6] = "abcde";
// 由编译器计算大小
char c[] = "abcde";
// 第二类:字符指针形式
char *c = "abcde";
第一类方法用来指定大小时,必须至少比初始化的内容多1个空间,用来存放null-terminated,即'\0'。否则会有undefined错误发生。
必须要明确知道,这两类初始化完全不同。使用字符数组所创建的字符串是可读可写的,存储位置为栈区。而用字符指针所得到的字符串是只读的,因为存储位置位于常量区。这是使用前必须明确的。
2.2 字符数组的名字和字符指针的关系
由于上述两种关系,我们常常会觉得,字符数组的名字和字符指针是一样的。实际上在很多使用到数组名字的地方,确实编译器会帮我们自动将其替换成「一个指向数组首元素的指针」。这句话的另一层意思就是,二者其实是不一样的,只是“好像在一些操作中一样”。有两个点需要注意:
- 使用auto变量推断一个C风格字符串时得到的是字符指针,而非字符数组。
- 使用decltype推断时,得到的是一个字符数组而不是字符指针(也就是上述的转换没有发生)
即:
char c[] = "abcde";
auto auto_c = c;
decltype(c) decl_c = "abc";
观察其类型:
注意:decltype连大小都继承下来了,因此不注意的话非常容易出现错误(不过大部分IDE能检测到)。
2.3 赋值问题
在知道了字符数组和字符指针的区别后,必须要指出,不建议使用2.1中所述的字符指针方法构建字符串(即第二类)。这并不是一种标准的用法,而且十分危险。它的内存存放在文本常量区,可读不可写。如果试图写入,程序会直接崩溃,甚至编译器不会警告(GCC应该会)。因此,一个常规的做法是用第一类方法初始化,而字符指针可以用来指向已经初始化好的字符串。接下来讨论赋值的时候,都是存储在栈中的字符串。
当我们以为辨清了存储问题就可以随意赋值,那就大错特错了,因为在C风格的字符串中,不能将数组的内容拷贝给其他数组作为其初始值。
也就是说,不能进行赋值操作。具体来说是不能使用「等号」进行赋值。我们应该是用cstring或string.h中提供的字符串拷贝函数来进行操作:
char* strcpy( char* dest, const char* src );
char *strncpy( char *dest, const char *src, std::size_t count );
这里通常就会出现及其危险的操作了,必须足够注意:
- 使用strcpy的时候必须保证dest是由足够空间的,否则结果为undefined。
- 使用strncpy时,不会保证拷贝 null-terminated,即'\0'。
一个常用的方法时把dest开得大一些,然后用0作为初始值,这样能以一种相对优雅的方式防止大多数问题,但是使用起来仍需注意。
char src[] = "abcde";
char dest[100] = {0};
// 拷贝下标2开始的总共2个字符
strncpy(dest, src + 2, 2);
cout << dest << endl;
// 结果输出:cd
3. 关于混用
前面也提到了,我们无法抛弃C风格的字符串。当我们使用STL的string时,难免遇到C风格与STL容器类混用的问题。幸运的是STL已经为我们考虑了足够多,二者的转换可以非常方便。
3.1 C风格字符串转STL string
STL对于C风格是非常宽容的,我们可以直接使用构造函数将C风格转换成容器类。
char cs[] = "unix";
string s(cs);
cout << s << endl;
//输出:unix
也可以直接用C风格给STL string赋值。
string s2;
s2 = cs;
cout << s2 << endl;
//输出:unix
进行STL string的加法运算时,C风格的字符串可以作为其中一个对象。
string scs = s + cs;
cout << scs << endl;
//输出:unixunix
需要再次警告:我们必须保证C风格的字符串是正确的,如果它没有争取地以'\0'结尾,那结果也是undefined。
3.2 STL string转C风格
当我们的一些API只能接收C风格的字符串时,就必须考虑STL string向C风格转换。这里STL再一次为我们考虑了,提供了对应的转换函数:
它的使用方法非常简单,如下:
string s = "unix";
// 以C格式的字符指针传进去
func(s.c_str());
参考
- 《C++ Primer 中文第五版》
- cppreference.com
- https://blog.csdn.net/u012611878/article/details/78291036