许多程序都大量应用到字符串。C++ 为处理字符串提供了两种不同数据类型:C 字符串和 string 类。
string 类库有许多处理字符串的函数,这些函数可以执行许多实用的和字符串相关的功能,并且提供了编程上的安全防护,而这正是 C 字符串处理函数所缺乏的。出于以上理由,你应该会更喜欢使用 string 类而不是 C 字符串。
尽管如此,每个 C++ 程序员都应该对C字符串有足够的了解。string 类构建于 C 字符串之上,所以,了解 C 字符串有助于理解 string 类。此外,还有很多程序是在 string 类加入到 C++ 标准之前编写的,这样的程序需要能理解 C 字符串的程序员来维护它们。最后,程序员如果需要编写和维护底层代码,例如 string 类库或操作系统的一部分,则必须使用 C 字符串来表示字符串数据。
C 字符串是存储在连续内存位置中的字符序列,并以 null 字符结尾。回想一下,null 字符是 ASCII 码为 0 的字符。在程序中,null 字符通常写成 '\0'。程序中通常使用整数 0 或常量 NULL 来表示 null 字符。因此,以下所有语句都会将 null 字符存储到字符变量中:
char ch1, ch2, ch3;
ch1 = '\0';
ch2 = 0;
ch3 = NULL;
由于数组是一系列连续的存储位置,它们存储相同类型的值,所以 C 字符串实际上是一个以 NULL 结尾的字符数组。C 字符串可以按以下 3 种形式之一出现在程序中:
无论 C 字符串以 3 种形式中的哪一种出现在程序中,它始终是以 null 字符结尾的字符数组,并由指向数组中第一个字符的指针表示。换句话说,C 字符串的类型就是以下形式的:
也就是说,C 字符串的类型是指向 char 的指针。
字符串常数作为用双引号括起来的字符序列直接写入程序中。例如:
以上都是字符串常数。
当编译器遇到诸如 "Bailey" 这样的字符串常数时,它分配一个由 7 个字符组成的数组,在数组的前 6 个条目中存储 "Bailey" 的 6 个字符,然后将 null 字符存储在最后一个条目中,如图 1 所示。然后,编译器将数组的第一个字符的地址(char * 类型)作为字符串常数的值。
下面的程序说明了一个事实,即字符串常数被编译器认为是一个类型为 const char * 的值。关键字 const 表示编译器不希望程序员改变字符串常数的内容。
//This program demonstrates that string literals are pointers to char.
#include <iostream>
using namespace std;
int main()
{
//Define variables that are pointers to char
const char *p, *q;
// Assign string literals to the pointers to char
p = "Hello "; q = "Bailey";
// Print the pointers as C-strings!
cout << p << q << endl;
// Print the pointers as C-strings and as addresses
cout << p << " is stored at " << int (p) << endl;
cout << q << " is stored at " << int(q) << endl;
// A string literal can be treated as a pointer!
cout << "string literal stored at " << int ("literal");
return 0;
}
程序输出结果:
该程序的前两个赋值显示字符串常数是指向 char 类型的变量的 char 指针。指针 p 和 q 然后保存两个字符串常数的地址。通过将指针转换为 int,可以看到内存中的字符串常数存储在哪里。
请注意,在这种情况下,编译器已将所有字符串常数存储在连续内存位置的程序中。
字符串常数只能保存硬编码到程序中的 C 字符串。要使用从键盘或文件中读取字符的 C 字符串,必须明确定义一个数组来保存 C 字符串的字符。在这样做时,还应该确保在数组中为终止的 null 字符分配一个附加条目。
例如,如果 C 字符串长度最多为 19 个字符,则需要分配一个包含 20 个字符的数组,代码如下:
就常数而言,编译器将通过字符串的第一个字符(在这种情况下为数组标识符)的地址来表示 C 字符串。前面讲过,不带方括号的数组标识符被编译器解释为数组第一个条目的地址。
定义为数组的 C 字符串可以通过 3 种方式赋予其值:使用字符串常数进行初始化,从键盘或文件中读取字符,或者一次一个字符地将字符复制到数组中来。以下是一些初始化的例子:
const int SIZE = 20;
char company[SIZE] = "Robotic Systems, inc";
char corporation [] = "C. K. Graphics";
釆用通过字符串常数初始化一个数组的方式时,数组定义中数组的大小是可选的。如果没有指定,则编译器会将其大小设置为比初始化字符串中的字符数多一个(因为要为 null 终止符留出空间)。
我们知道,可以使用输入和输出流类的各种对象、运算符和成员函数来读取和写入定义为数组的 C 字符串。存储为程序员定义数组的 C 字符串可以使用标准下标符号进行处理。
下面的程序就是一个例子。它一次输出一个字符串,当它找到 null 终止符时停止。它使用了 getline 成员函数来读取要输出的字符串。
// This program cycles through a character arrayA displaying each element until a null terminator is encountered.
#include <iostream>
using namespace std;
int main()
{
const int LENGTH = 80; // Maximum length for string
char line[LENGTH]; // Array of char
// Read a string into the character array
cout << "Enter a sentence of no more than " << LENGTH-1 << " characters : \n";
cin.getline(line, LENGTH);
cout << "The sentence you entered is:\n";
// Loop through the array printing each character
for(int index = 0; line[index] != '\0'; index++)
{
cout << line[index];
}
return 0;
}
程序输出结果:
正如前面所看到的,C 字符串可以表示为字符串常数或字符数组。这两个方法都将分配一个数组,然后使用该数组的地址作为指向 char 的指针来实际表示字符串。
这两者之间的区别在于,在第一种情况下,用于存储字符串的数组由编译器隐式分配,而在第二种情况下,数组由程序员明确分配。
表示 C 字符串的第 3 种方法是使用指向 char 的指针指向一个 C 字符串,该字符串的存储已经通过其他两种方法之一分配了。以下是以这种方式使用 C 字符串的一些示例:
char name[] = "John Q. Public";
char *p;
p = name; //指向已有的C字符串
cout << p << endl; // 打印
p = " Jane Doe"; //指向其他C字符串
cout << p << endl; // 打印
使用指针变量来表示 C 字符串的一个主要优点是能够使指针指向不同的 C 字符串。使用指向 Char 的指针作为 C 字符串的另一种方法是定义该指针,然后将其设置为指向由 new 运算符返回的动态分配的存储。下面的程序对此进行了演示:
// This program illustrates dynamic allocation of storage for C-strings.
#include <iostream>
using namespace std;
int main()
{
const int NAME_LENGTH = 50; // Maximum length
char *pname = nullptr; // Address of array
// Allocate the array
pname = new char[NAME_LENGTH];
// Read a string
cout << "Enter your name: ";
cin.getline(pname, NAME_LENGTH);
//Display the string
cout << "Hello " << pname;
//Release the memory
delete[ ] pname;
return 0;
}
程序输出结果:
当使用指向 char 的指针作为 C 字符串时,一个常见的错误是使用了一个未指向正确分配的 C 字符串的指针。
例如,来看以下代码:
char *pname;
cout << "Enter your name: ";
cin >> pname;
以上代码是错误的,因为程序试图将一个字符串读入 pname 指向的内存位置,而 pname 则并未正确初始化。