9 27
C语言字符指针与数组

基本概念

字符串是C语言中最有用、最重要的数据类型之一,在C语言里面,字符串常量是一个字符数组,例如:

"I am a string"

在字符串的内部表示中,字符数组以空字符 '\0' 结尾的char类型数组,所以程序可以通过检查空字符找到字符数组的结尾。字符串常量占据的存储单元数也因此比双引号内的字符数大1

字符串常量最常见的用法也许是作为函数参数,例如:

printf("hello world\n")

当类似于这样的一个字符串出现在程序中时,实际上是通过字符指针访问该字符串的。在上述语句中,printf接受的是一个指向字符数组第一个字符的指针。也就是说,字符串常量可通过一个指向其第一个元素的指针访问。

除了作为函数参数外,字符串常量还有其它用法。假定指针pmessage的声明如下:

char *pmessage;

那么,语句

pmesage = "now is the time";

将把一个指向该字符数组的指针赋值给pmessage。该过程并没有进行字符串的复制,而只是涉及到指针的操作。C语言没有提供将整个 字符串最为一个整体进行处理的运算符。

下面两个定义之间没有很大的差别:

char amessage[] = "now is the time"; /* 定义一个数组 */
char *pmessage = "now is the time";  /* 定义一个指针 */

上述声明中,amessage是一个仅仅足以存放初始化字符串以及空字符'\0'的一维数组。数组中的单个字符可以进行修改。但amessage始终指向同一个存储位置。另一当面,pmessage是一个指针,其初始指向一个字符串常量,之后它可以被修改以指向其他地址,但如果试图修改字符串的内容,结果是没有定义的。

因为字符常量属于静态存储类别(static storage class),这说明如果在函数中使用字符串常量,该字符串只会被存储一次(存于静态存储区),在整个程序的生命周期内存在,即使函数被调用多次。用双引号括起来的内容被视为指向该字符串存储位置的指针。这类似于把数组名作为指向该数组位置的指针。

看一个例子:

const char * pt1 = "Something is pointing at me.";
const char ar1[] = "Something is pointing at me.";

以上两个声明表明,pt1和ar1都是该字符串的地址。在这两种情况下,带双引号的字符串本身决定了预留给字符串的存储空间。尽管如此,这两种形式并不完全相同。

数组和指针

数组(字符串就是字符数组的表现形式)形式和指针形式有何不同?以上面的声明为例,数组形式(ar1[])在计算机的内存中分配为一个内含29个元素的数组(每个元素对应一个字符,还加上一个末尾的空字符‘\0’),每个元素被初始化为字符串字面量对应的字符。通常,字符串都作为可执行文件的一部分存储在数据段中。当把程序载入内存时,也载入了程序中的字符串。字符串存储在静态存储区(static memory)中。但是,程序开始运行时才会为该数组分配内存。此时,才将字符串拷贝到数组中。注意:此时字符串有两个副本。一个是在静态内存中的字符串字面量,另外一个是存储在ar1数组中的字符串。

此后,编译器便把数组名ar1识别为该数组首元素地址(&ar1[0])的别名。这里关键要理解,在数组形式中,ar1是地址常量,不能更改ar1,如果ar1被改变了,则意味了改变了数组的存储位置(即地址)。可以进行类似ar1+1这样的操作,识别数组的下一个元素。但是不允许进行++ar1这样的操作。递增运算只能用于变量名前,不能用于常量。

指针形式(*pt1)也使得编译器为字符串在静态存储区预留29个元素的空间。另外,一旦开始执行程序,它会为指针变量pt1留出一个存储位置,并把字符串的地址存储在指针变量中。该变量最初指向该字符串的首字符,但是它的值可以改变。因此。可以使用递增运算符。例如++pt1将指向第2个字符(o)

字符串字面量被视为 const 数据,由于pt1指向这个const数据,所以应该把pt1声明为指向const数据的指针。这意味着不能用pt1改变它指向的数据,但是扔可以改变pt1的值(即,pt1指向的位置)。如果一个字符串字面量拷贝给一个数组,就可以随便改变数据,除非把数组声明为const。

总之,初始化数组把静态存储区的字符串拷贝到数组中,而初始化指针只把字符串的地址拷贝给指针。

字符串操作

为了更进一步地讨论指针和数组其他方面的问题,下面以标准库中两个有用的函数为例来研究它们的不同实现版本。第一个函数strcpy(s,t)把指针t指向的字符串复制到指针s指向的位置。如果使用语句 s=t 实现该功能,其实实质上只是拷贝了指针,而并没有复制字符。为了进行字符的复制,这里使用了一个循环语句。strcpy函数的第1个版本是通过数组方法实现的,如下所示:

/* strcpy函数:将指针t指向的字符串复制到指针s指向的位置;使用数组下标实现的版本 */
void strcpy(char *s, char *t)
{
    int i;
    i = 0;
    while((s[i] = t[i]) != '\0')
        i++;
}

为了进行比较,下面是用指针方法实现的strcpy函数:

/* strcpy函数:将指针t指向的字符串复制到指针s指向的位置;使用指针方式实现的版本1 */
void strcpy(char *s, char *t)
{
    while((*s = *t) != '\0')
        s++;
        t++;
}

因为参数是通过值传递的,所以在strcpy函数中可以以任何方式使用参数s和t。在此,s和t是方便地进行了初始化的指针,循环每执行一次,它们就沿着相应的数组前进一个字符,直到将t的结束符'\0'复制到s为止。

实际上,strcpy函数并不会按照上面的这些方式编写。经验丰富的程序员更喜欢将它编写成下列形式:

/* strcpy函数:将指针t指向的字符串复制到指针s指向的位置;使用指针方式实现的版本2 */
void strcpy(char *s, char *t)
{
    while((*s++ = *t++) != '\0')
        ;
}

在该版本中,s和t的自增运算放到了循环的测试部分中。表达式*t++的值是执行自增运算之前t所指向的字符。后缀运算符++表示在读取该字符之后才改变t的值。同样的道理,在s执行自增运算之前,字符就被存储到指针s指向的旧位置。该字符值同时也可以用来和空字符'\0'进行比较运算,以控制循环的执行。最后的结果是依次将t指向的字符复制到s指向的位置,直到遇到结束符'\0'为止(同时也复制该结束符)。

为了更进一步精炼程序,我们注意到,表达式'\0'的比较是多余的,因为只需要判断表达式的值是否0即可。因此,该函数可进一步写成下列形式:

/* strcpy函数:将指针t指向的字符串复制到指针s指向的位置;使用指针方式实现的版本2 */
void strcpy(char *s, char *t)
{
    while(*s++ = *t++)
        ;
}

该函数初看起来不太容易理解,但这种表示方法是很有好处的,我们应该掌握这种方法,C语句程序中经常会参与这种写法。

标准库 <string.h> 中提供的函数 strcpy把目标字符串作为函数值返回。

/* strcmp函数:根据s按照字典顺序小于、等于或大于t的结果分别返回负整数、0或正整数 */
int strcmp(char *s, char *t)
{
    int i;
    for (i = 0; s[i] == t[i]; i++)
        if (s[i] == '\0')
            return 0;
    return s[i] - t[i];
}

下面是用指针方式实现的strcmp函数

/* strcmp函数:根据s按照字典顺序小于、等于或大于t的结果分别返回负整数、0或正整数 */
int strcmp(char *s, char *t)
{
    for ( ; *s == *t; s++,t++)
        if (*s == '\0')
            return 0;
    return *s - *t;
}

第二个函数是字符串的比较函数strcmp(s,t)。该函数比较字符串s和t,并且根据s按照字典顺序小于、等于或大于t的结果分别返回负整数、0或正整数。该返回值是s和t由前向后逐字符比较时遇到的第一个不相等字符处的字符的差值。

指针(数组)的分配释放问题

上面针对字符指针的示例中,我们演示了如何使用指针定义字符串(字符数组)。

如果通过指针实现数组的情景下,其实我们还可以使用malloc和free的配套使用,通过动态分配的方式初始化我们的指针,这比变长数组要灵活,举个例子:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    double * ptd;
    int max;
    int i = 0;
    int number;

    puts("What is the maximim number of type double entries ?");
    if (scanf("%d", &max)!=1 )
    {
        puts("Number not correctly entered -- byte");
        exit(EXIT_FAILURE);
    }

    ptd = (double *) malloc(max * sizeof(double));
    if (ptd == NULL) 
    {
        puts("Memory allocation failed, Goodbye .");
        exit(EXIT_FAILURE);
    }

    puts("Enter the values (q to quit):");
    while(i < max && scanf("%lf", &ptd[i]) == 1)
        ++i;
    printf("Here are your %d entries: \n", number = i);
    for (i = 0; i < number; i++)
    {
        printf("%7.2f ", ptd[i]);
        if (i % 7 == 6)
            putchar('\n');
    }

    if (i % 7 !=0 )
        putchar('\n');

    puts("Done.");
    free(ptd);
    return 0;
}

这种情况下,可以使用一个动态数组调整程序以适应不同的情况。

需要注意的是指针和数组很像,但指针不能等同于数组。

指针: - 指针保存的数据的地址,间接访问数据。 - 通常用于动态数据结构 - 相关函数为malloc()、free() - 通常指向匿名数据

数组: - 单单保存数据,直接访问数据 - 通常用于存储固定数且数据类型相同的元素 - 隐式分配和删除 - 自身即为数据名

数组和指针都可以在它们的定义中用字符串常量进行初始化,尽管看上去一样,但底层机制有很大的不同:

定义指针时,编译器并不为指针指向的对象分配空间,它只是分配指针自身的空间,除非在定义时赋给指针一个字符串常量进行初始化,例如:

char *p = "I am a gopher"

注意,只有字符串常量才是如此,不要指望浮点型数之类的常量会分配空间

float *p = 3.14 //编译无法通过