当前位置:嗨网首页>书籍在线阅读

39-指针和字符串

  
选择背景色: 黄橙 洋红 淡粉 水蓝 草绿 白色 选择字体: 宋体 黑体 微软雅黑 楷体 选择字体大小: 恢复默认

4.8.3 指针和字符串

数组和指针的特殊关系可以扩展到C-风格字符串。请看下面的代码:

char flower[10] = "rose";
cout << flower << "s are red\n";

数组名是第一个元素的地址,因此cout语句中的flower是包含字符r的char元素的地址。cout对象认为char的地址是字符串的地址,因此它打印该地址处的字符,然后继续打印后面的字符,直到遇到空字符(\0)为止。总之,如果给cout提供一个字符的地址,则它将从该字符开始打印,直到遇到空字符为止。

这里的关键不在于flower是数组名,而在于flower是一个char的地址。这意味着可以将指向char的指针变量作为cout的参数,因为它也是char的地址。当然,该指针指向字符串的开头,稍后将核实这一点。

前面的cout语句中最后一部分的情况如何呢?如果flower是字符串第一个字符的地址,则表达式“s are red\n”是什么呢?为了与cout对字符串输出的处理保持一致,这个用引号括起的字符串也应当是一个地址。在C++中,用引号括起的字符串像数组名一样,也是第1个元素的地址。上述代码不会将整个字符串发送给cout,而只是发送该字符串的地址。这意味着对于数组中的字符串、用引号括起的字符串常量以及指针所描述的字符串,处理的方式是一样的,都将传递它们的地址。与逐个传递字符串中的所有字符相比,这样做的工作量确实要少。

注意: 在cout和多数C++表达式中,char数组名、char指针以及用引号括起的字符串常量都被解释为字符串第一个字符的地址。

程序清单4.20演示了如何使用不同形式的字符串。它使用了两个字符串库中的函数。函数strlen()我们以前用过,它返回字符串的长度。函数strcpy()将字符串从一个位置复制到另一个位置。这两个函数的原型都位于头文件cstring(在不太新的实现中,为string.h)中。该程序还通过注释指出了应尽量避免的错误使用指针的方式。

程序清单4.20 ptrstr.cpp

// ptrstr.cpp -- using pointers to strings
#include <iostream>
#include <cstring>                 // declare strlen(), strcpy()
int main()
{
    using namespace std;
    char animal[20] = "bear";      // animal holds bear
    const char * bird = "wren";    // bird holds address of string
    char * ps;                     // uninitialized
    cout << animal << " and ";     // display bear
    cout << bird << "\n";          // display wren
    // cout << ps << "\n";         //may display garbage, may cause a crash
    cout << "Enter a kind of animal: ";
    cin >> animal;                 // ok if input < 20 chars
    // cin >> ps; Too horrible a blunder to try; ps doesn't
    // point to allocated space
    ps = animal;                   // set ps to point to string
    cout << ps << "!\n";           // ok, same as using animal
    cout << "Before using strcpy():\n";
    cout << animal << " at " << (int *) animal << endl;
    cout << ps << " at " << (int *) ps << endl;
    ps = new char[strlen(animal) + 1];  // get new storage
    strcpy(ps, animal);            // copy string to new storage
    cout << "After using strcpy():\n";
    cout << animal << " at " << (int *) animal << endl;
    cout << ps << " at " << (int *) ps << endl;
    delete [] ps;
    return 0;
}

下面是该程序的运行情况:

bear and wren
Enter a kind of animal: fox
fox!
Before using strcpy():
fox at 0x0065fd30
fox at 0x0065fd30
After using strcpy():
fox at 0x0065fd30
fox at 0x004301c8

程序说明

程序清单4.20中的程序创建了一个char数组(animal)和两个指向char的指针变量(bird和ps)。该程序首先将animal数组初始化为字符串“bear”,就像初始化数组一样。然后,程序执行了一些新的操作,将char指针初始化为指向一个字符串:

const char * bird = "wren"; // bird holds address of string

记住,“wren”实际表示的是字符串的地址,因此这条语句将“wren”的地址赋给了bird指针。(一般来说,编译器在内存留出一些空间,以存储程序源代码中所有用引号括起的字符串,并将每个被存储的字符串与其地址关联起来。)这意味着可以像使用字符串“wren”那样使用指针bird,如下面的示例所示:

cout << "A concerned " << bird << " speaks\n";

字符串字面值是常量,这就是为什么代码在声明中使用关键字const的原因。以这种方式使用const意味着可以用bird来访问字符串,但不能修改它。第7章将详细介绍const指针。最后,指针ps未被初始化,因此不指向任何字符串(正如您知道的,这通常是个坏主意,这里也不例外)。

接下来,程序说明了这样一点,即对于cout来说,使用数组名animal和指针bird是一样的。毕竟,它们都是字符串的地址,cout将显示存储在这两个地址上的两个字符串(“bear”和“wren”)。如果激活错误地显示ps的代码,则将可能显示一个空行、一堆乱码,或者程序将崩溃。创建未初始化的指针有点像签发空头支票:无法控制它将被如何使用。

对于输入,情况有点不同。只要输入比较短,能够被存储在数组中,则使用数组animal进行输入将是安全的。然而,使用bird来进行输入并不合适:

  • 有些编译器将字符串字面值视为只读常量,如果试图修改它们,将导致运行阶段错误。在C++中,字符串字面值都将被视为常量,但并不是所有的编译器都对以前的行为做了这样的修改。
  • 有些编译器只使用字符串字面值的一个副本来表示程序中所有的该字面值。

下面讨论一下第二点。C++不能保证字符串字面值被唯一地存储。也就是说,如果在程序中多次使用了字符串字面值“wren”,则编译器将可能存储该字符串的多个副本,也可能只存储一个副本。如果是后面一种情况,则将bird设置为指向一个“wren”,将使它只是指向该字符串的唯一一个副本。将值读入一个字符串可能会影响被认为是独立的、位于其他地方的字符串。无论如何,由于bird指针被声明为const,因此编译器将禁止改变bird指向的位置中的内容。

试图将信息读入ps指向的位置将更糟。由于ps没有被初始化,因此并不知道信息将被存储在哪里,这甚至可能改写内存中的信息。幸运的是,要避免这种问题很容易——只要使用足够大的char数组来接收输入即可。请不要使用字符串常量或未被初始化的指针来接收输入。为避免这些问题,也可以使用std::string对象,而不是数组。

警告: 在将字符串读入程序时,应使用已分配的内存地址。该地址可以是数组名,也可以是使用new初始化过的指针。

接下来,请注意下述代码完成的工作:

ps = animal; // set ps to point to string
...
cout << animal << " at " << (int *) animal << endl;
cout << ps << " at " << (int *) ps << endl;

它将生成下面的输出:

fox at 0x0065fd30
fox at 0x0065fd30

一般来说,如果给cout提供一个指针,它将打印地址。但如果指针的类型为char ,则cout将显示指向的字符串。如果要显示的是字符串的地址,则必须将这种指针强制转换为另一种指针类型,如int (上面的代码就是这样做的)。因此,ps显示为字符串“fox”,而(int *)ps显示为该字符串的地址。注意,将animal赋给ps并不会复制字符串,而只是复制地址。这样,这两个指针将指向相同的内存单元和字符串。

要获得字符串的副本,还需要做其他工作。首先,需要分配内存来存储该字符串,这可以通过声明另一个数组或使用new来完成。后一种方法使得能够根据字符串的长度来指定所需的空间:

ps = new char[strlen(animal) + 1]; // get new storage

字符串“fox”不能填满整个animal数组,因此这样做浪费了空间。上述代码使用strlen()来确定字符串的长度,并将它加1来获得包含空字符时该字符串的长度。随后,程序使用new来分配刚好足够存储该字符串的空间。

接下来,需要将animal数组中的字符串复制到新分配的空间中。将animal赋给ps是不可行的,因为这样只能修改存储在ps中的地址,从而失去程序访问新分配内存的唯一途径。需要使用库函数strcpy():

strcpy(ps, animal); // copy string to new storage

strcpy()函数接受2个参数。第一个是目标地址,第二个是要复制的字符串的地址。您应确定,分配了目标空间,并有足够的空间来存储副本。在这里,我们用strlen()来确定所需的空间,并使用new获得可用的内存。

通过使用strcpy()和new,将获得“fox”的两个独立副本:

fox at 0x0065fd30
fox at 0x004301c8

另外,new在离animal数组很远的地方找到了所需的内存空间。

经常需要将字符串放到数组中。初始化数组时,请使用=运算符;否则应使用strcpy()或strncpy()。strcpy()在前面已经介绍过,其工作原理如下:

char food[20] = "carrots"; // initialization
strcpy(food, "flan");      // otherwise

注意,类似下面这样的代码可能导致问题,因为food数组比字符串小:

strcpy(food, "a picnic basket filled with many goodies");

在这种情况下,函数将字符串中剩余的部分复制到数组后面的内存字节中,这可能会覆盖程序正在使用的其他内存。要避免这种问题,请使用strncpy()。该函数还接受第3个参数——要复制的最大字符数。然而,要注意的是,如果该函数在到达字符串结尾之前,目标内存已经用完,则它将不会添加空字符。因此,应该这样使用该函数:

strncpy(food, "a picnic basket filled with many goodies", 19);
food[19] = '\0';

这样最多将19个字符复制到数组中,然后将最后一个元素设置成空字符。如果该字符串少于19个字符,则strncpy()将在复制完该字符串之后加上空字符,以标记该字符串的结尾。

警告: 应使用strcpy()或strncpy(),而不是赋值运算符来将字符串赋给数组。

您对使用C-风格字符串和cstring库的一些方面有了了解后,便可以理解为何使用C++ string类型更为简单了:您不用担心字符串会导致数组越界,并可以使用赋值运算符而不是函数strcpy()和strncpy()。