1.8 字符指针和字符串处理

在C++中,字符串既可以通过字符数组来存取,也可以用字符指针来操作。但使用字符数组时,有时其数组大小与字符串的长度不一定相匹配,因此在许多场合中使用字符指针更为恰当。另外,字符串本身有拼接、截取及比较等操作,本节就来讨论这些内容。

1.8.1 字符指针

若定义一个指针时指定的类型是char*,则这样定义的指针为字符指针。与普通指针变量一样,在C++中定义一个字符指针变量(指针)的格式如下:

char*<指针名1>[,*<指针名2>, …];

例如:

char*str1,*str2;                 // 字符指针

则定义的str1和str2都是字符指针变量。对于字符指针变量的初始化,可以用字符串常量或一维字符数组进行。

由于一个字符串常量有一个地址,因而它可以赋给一个字符指针变量。例如:

char *p1="Hello";

char *p1;
p1 = "Hello";

都使得字符指针变量p1指向“Hello”字符串常量的内存空间。

由于一维字符数组可以存放字符串常量,此时的字符数组名就是一个指向字符串的指针常量,因此它也可用于字符指针变量的初始化或赋值操作。例如:

char *p1, str[] = "Hello";

p1 = str;

则使得字符指针变量p1指向字符数组str的内存空间,而str存放的内容是“Hello”字符串常量,因此这种赋值实际上是使p1间接指向“Hello”字符串常量。

要注意,用字符串常量和字符数组来初始化字符指针在本质上是不同的。主要体现在:

(1)在有的编译系统中,相同字符串常量的地址可能是相同的,但相同字符串内容的两个字符数组的地址一定不同。例如:

char *p1,*p2;
p1="Hello";   p2="Hello";

由于字符指针p1和p2指向的是同一个字符串变量,因此它们的地址值在有的编译系统(如Visual C++)中是相同的。正因为如此,许多操作系统允许用一个字符串常量来标识一个内存块。但如果:

char *p1,*p2;
char str1[]="Hello",  str2[]="Hello";
p1=str1; p2=str2;

则字符指针p1和p2间接指向的虽是同一个字符串变量,但它们的地址值一定是不同的。因为它们指向的字符数组空间的地址不一样。

(2)在大多数编译系统(如Visual C++)中,字符串常量所在的是常量区,其内存空间的内容在程序运行时是不可修改的,而字符数组的内存空间的内容是可修改的。例如,若:

char str[80],*p1=str;
cin>>str;                          /* 合法 */
cout<<p1;                          /* 合法 */

char *p1="Hello";
cin>>p1;                          /*A:合法,但Visual C++会使程序异常终止*/
cout<<p1;                         /* 合法 */

这里p1指向“Hello”所在的常量内存区。由于该内存空间的内容不可在程序运行中修改,因此A语句虽在编译时是合法的,但运行时它会修改常量区的内容,这是不允许的,因而会造成程序异常终止。

字符指针一旦初始化或赋初值后,就可在程序中使用它,并且以前讨论过的指针操作都可以用于字符指针。例如,下面的示例是将一个字符串逆序输出。

例Ex_StrInv】 字符串逆序输出

#include <iostream.h>
int  main()
{
    char *p1 = "ABCDEFG", *p2 = p1;
    while(*p1!='\0')p1++;           /* 将指针指向字符常量最后的结束符 */
    while (p2<=p1--)
        cout<<*p1;
    cout<<endl;
    return 0;
}

程序运行结果如下:

GFEDCBA

1.8.2 带参数的main函数

到目前为止,我们所接触到的main函数都是不带参数的。但在实际应用中,程序有时需要从命令行输入参数。例如:

c:\>copy file1 file2

这是一个常用的DOS命令。当它运行时,操作系统将命令行参数以字符串的形式传递给main()。为了能使程序处理这些参数,需要main带有参数,其最常用的格式是:

数据类型 main(int argc,char*argv[])

其中,第一个int型参数用来存放命令行参数的个数,实际上argc所存放的数值比命令行参数的个数多1,即将命令字(或称为可执行文件名,如copy)也计算在内。第二个参数argv是一个一维的指针数组,用来存放命令行中各个参数和命令字的字符串,且规定:

argv[0]存放命令字
argv[1]存放命令行中第一个参数
argv[2]存放命令行中第二个参数
argv[3]存放命令行中第三个参数
…

这里,argc的值和argv[]各个元素的值都是系统自动赋给的。

例Ex_Main】 处理命令行参数

#include <iostream.h>
int  main(int argc,char*argv[])
{
    cout<<"这个程序的程序名是:"<<argv[0]<<"\n";
    if (argc<=1)
        cout<<"没有参数!";
    else
    {
        int nCount = 1;
        while(nCount < argc)
        {
            cout<<"第"<<nCount<<"个参数是:"<<argv[nCount]<<"\n";
            nCount++;
        }
    }
    return 0;
}

程序编译连接后,将Ex_Main.exe复制到C盘,然后切换到DOS命令提示符进行测试。

程序运行结果如下:

这个程序的程序名是:Ex_Main

第1个参数是:ab

第2个参数是:cd

第3个参数是:E

第4个参数是:F

1.8.3 字符串处理函数

由于字符串使用广泛,几乎所有版本的C++都提供了若干个字符串处理函数,放在string.h头文件中,这里介绍几个常用的函数。

1.strcat和strncat

函数strcat是“string(字符串)catenate(连接)”的简写,其作用是将两个字符串连接起来,形成一个新的字符串。它的函数原型如下:

char *strcat(char*dest,const char*src);

其功能是将第2个参数src指定的字符串连接到由第1个参数dest指定的字符串的末尾,连接成新的字符串后由参数dest返回。函数成功调用后,返回指向dest内存空间的指针,否则返回空指针NULL。例如:

结果输出goodmorning。需要说明的是:

(1)dest指向的内存空间必须足够大,且是可写的,以便能存下连接的新字符串。这就是说, dest位置处的实参不能是字符串常量,也不能是const字符指针。

(2)尽管dest和scr指定的字符串都有'\0',但连接的时候,dest字符串后面的'\0'被清除,这样连接后的新字符串只有末尾仍保留'\0'结束符。

(3)在string.h头文件中,还有一个strncat函数,其作用也是用于两个字符串的连接,其函数原型如下:

char *strncat(char*dest,const char*src,size_t maxlen);

只不过,它还限定了连接到dest的字符串src的最大字符个数maxlen。若字符串src字符个数小于或等于maxlen,则等同于strcat。若字符串src字符个数大于maxlen,则只有字符串src的前maxlen个字符被连接到dest字符串末尾。例如:

则输出结果为goodmor。

2.strcpy和strncpy

函数strcpy是“string copy”(字符串复制)的简写,用于字符串的“赋值”。其函数原型如下:

char  *strcpy(char*dest,const char*src);

其功能是将第2个参数src指定的字符串复制到由第1个参数dest指定的内存空间中,包括结尾的字符串结束符'\0'。复制后的字符串由参数dest返回。函数成功调用后,返回指向dest内存空间的指针,否则返回空指针NULL。例如:

char s1[50];
char s2[]="word";
strcpy(s1,s2);
cout<<s1;

结果输出word,说明strcpy已经将s2的字符串复制到了s1中。需要说明的是:

(1)复制是内存空间的写入操作,因而需要dest所指向的内存空间足够大,且内存空间是可写入的,以便能容纳被复制的字符串src。要注意,dest所指向的内存空间的大小至少是scr字符个数+1,因为末尾还有一个结束符'\0'。例如,下面的错误代码比较隐蔽:

char s2[]="ABC";
char s1[3];
strcpy(s1,s2);
cout<<s1;

表面上看s2只有3个字符,s1定义长度3就够了。但strcpy执行过程会将字符串结束符也一起复制过去,因此s1的长度应该至少定义为4。

(2)不能试图通过指针的指向改变来复制字符串。例如,下面的代码都不是复制:

char s2[]="ABC";
char s1[10], *pstr;
s1=s2;                        // 错误:s1是指针常量,不能作为左值
pstr=s1;                      // pstr指向s1内存空间
pstr=s2;                      // pstr指向s2内存空间
cout<<s1;

虽然输出的结果也是ABC,看似复制成功,但事实上只是pstr指向s2内存空间,并非s1内存空间的内容是字符串“ABC”。

(3)可以使用strncpy函数来限制被复制的字符串src的字符个数。strncpy函数原型如下:

char *strncpy(char*dest,const char*src,size_t maxlen);

其中,maxlen用来指定被复制字符串src的最大字符个数(不含结束符'\0')。若字符串src字符个数小于或等于maxlen,则等同于strcpy。若字符串src字符个数大于maxlen,则只有字符串src的前maxlen个字符连同结束符'\0'被复制到dest指定的内存空间中。例如:

char s1[50];
char s2[]="word";
strncpy(s1,s2, 2);
cout<<s1;

结果输出wo。

3.strcmp和strncmp

string.h头文件中定义的函数strcmp是“string compare”(字符串比较)的简写,用于两个字符串的“比较”。其函数原型如下:

int  strcmp(const char*s1,const char*s2);

其功能是:如果字符串s1和字符串s2完全相等,则函数返回0;如果字符串s1大于字符串s2,则函数返回一个正整数;如果字符串s1小于字符串s2,则函数返回一个负整数。

在strcmp函数中,字符串比较的规则是:将两个字符串从左至右逐个字符按照ASCII码值的大小进行比较,直到出现ASCII码值不相等的字符或遇到'\0'为止。如果所有字符的ASCII码值都相等,则这两个字符串相等。如果出现了不相等的字符,以第一个不相等字符的ASCII码值比较结果为准。需要说明的是:

(1)在字符串比较操作中,不能直接通过使用“关系运算符”来比较两个字符数组名或字符串常量或字符指针来决定字符串本身是否相等、大于或小于等。例如:

char s1[100], s2[100];
cin>>s1>>s2;
if( s1 == s2 ) cout<<"same!"<<endl;

这种比较只是比较s1和s2所在的内存空间的首地址,并非是字符串内容的比较。

(2)可以使用strncmp函数来限制两个字符串比较的字符个数。strncmp函数原型如下:

int strncmp(const char *s1, const char *s2, size_t maxlen);

其中,maxlen用来指定两个字符串比较的最大字符个数。若字符串s1或s2中任一字符串的字符个数小于或等于maxlen,则等同于strcmp。若字符串s1和s2字符个数都大于maxlen,则参与比较的是前maxlen个字符。例如:

char s1[] = "these";
char s2[] = "that";
int i = strncmp(s1,s2, 2);
cout<<i<<endl;

结果输出为0,因为s1和s2字符串的前两个字符是相等的。

事实上,字符串操作还不止上述论及的库函数,string.h头文件中定义的还有许多,如strlen (求字符串长度、字符个数,不是字节数)、strlwr(转换成小写)、strupr(转换成大写)及strstr (查找子串)等。这些库函数的功能和原型可参见附录A。