C字符串简介

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第1天,点击查看活动详情

前言

       本文对C字符串进行简单的介绍,为的是给后面将要写的字符串函数和内存函数的文章做铺垫。

       新手一个,水平比较低,还请包涵。

字符串

        C语言中对字符和字符串的处理很是频繁,但是C语言本身是没有字符串类型的,字符串通常放在常量字符串中或者字符数组中。

        字符串常量适用于那些对它不做修改的字符串函数。

字符串字面量

        用双引号括起来的内容称为字符串字面量, 也叫作字符串常量。双引号中的字符和编译器自动加入末尾的\0字符,都作为字符串储存在内存中,所以"I am a symbolic stringconstant."、 "I am a string in an array."都是字符串字面量。

        从ANSI C标准起,如果字符串字面量之间没有间隔,或者用空白字符分隔,C会将其视为串联起来的字符串字面量。

例如:

char greeting[50] = "Hello, and"" how are" " you"" today!";

        与下面的代码等价:

char greeting[50] = "Hello, and how are you today!";

        字符串常量属于静态存储类别 ,这说明如果在函数中使用字符串常量,该字符串只会被储存一次,在整个程序的生命期内存在,即使函数被调用多次。

        用双引号括起来的内容被视为指向该字符串储存位置的指针。这类似于把数组名作为指向该数组位置的指针。 

例如:

#include <stdio.h>
int main()
{
    printf("%s, %p, %c\n", "We", "are", *"space farers");
    return 0;
}

        printf()根据%s 转换说明打印 We,根据%p 转换说明打印一个地址。因此,如果"are"代表一个地址,printf()将打印该字符串首字符的地址。

image.png

         字符串"space farers"被视为字符串地址,而字符串地址与首元素地址数值上相同,解引用就相当于解引用第一个字符,所以得到的就是s。

字符串数组和初始化

        定义字符串数组时,必须让编译器知道需要多少空间。一种方法是用足够空间的数组储存字符串。 在下面的声明中,用指定的字符串初始化数组m1:

 char m1[40] = "Limit yourself to one line's worth.";

        还可以这样: 

 char m1[40] = { 'L','i', 'm', 'i', 't', ' ', 'y', 'o', 'u', 'r', 's', 'e', 'l',
'f', ' ', 't', 'o', ' ', 'o', 'n', 'e', ' ','l', 'i', 'n', 'e',
'", 's', ' ', 'w', 'o', 'r','t', 'h', '.', '\0’
};

        不过太麻烦了,一般直接用字符串字面量来初始化字符数组。

        在指定数组大小时, 要确保数组的元素个数至少比字符串长度多1( 为了容纳空字符)。所有未被使用的元素都被自动初始化为0(这里的0指的是char形式的空字符,不是数字字符0)

image.png

        通常,让编译器确定数组的大小很方便。省略数组初始化声明中的大小,编译器会自动计算数组的大小。

例如:

 char m2[ ] = "If you can't think of anything, fake it.";

        字符数组名和其他数组名一样,是该数组首元素的地址。

        还可以使用指针表示法创建字符串。

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

        该声明和下面的声明几乎相同:

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

        但其实又有所区别,且听我慢慢道来。

字符数组和指针

        数组形式和指针形式有何不同? 

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

        数组形式(ar1[ ]) 在计算机的内存中分配为一个内含29个元素的数组(每个元素对应一个字符,还加上一个末尾的空字符'\0'),每个元素被初始化为字符串字面量对应的字符。

        通常,字符串都作为可执行文件的一部分储存在数据段中。当把程序载入内存时,也载入了程序中的字符串。字符串储存在静态存储区中。

        但是,程序在开始运行时才会为该数组分配内存。此时,才将字符串拷贝到数组中。注意,此时字符串实际上有两个副本。 一个是在静态内存中的字符串字面量,另一个是储存在ar1数组中的字符串。

image.png

         指针形式(*pt1)也使得编译器为字符串在静态存储区预留29个元素的空间。另外,一旦开始执行程序,它会为指针变量pt1留出一个储存位置,并把字符串的地址储存在指针变量中。

        字符串字面量被视为const数据,也就是不可被修改。由于pt1指向这个const数据,所以应该把pt1声明为指向const数据的指针,不然有可能会出问题。何出此言?

        你想啊,指针既灵活又危险,当pt1指向该字符串字面量时,谁也说不准会不会出现*pt1这样的代码,一旦解引用试图修改该字符串就会出问题——C标准未定义该行为,也就是说不同编译器反应不同,有可能导致内存访问错误等等。原来字符串字面量就是被限制了不能被修改的,使用指针指向它就让使用者获得了修改的权限,尽管是不应该被修改的。为了限制指针的权限,使用const修饰指针让它不能修改字符串字面量就能避免一系列潜在的危险和错误。

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

示例:

#define MSG "I'm special"
#include<stdio.h>
int main()
{
    char ar[] = MSG;
    const char *pt = MSG;
    printf("address of "I'm special": %p \n", "I'm special");
    printf(" address ar: %p\n", ar);
    printf(" address pt: %p\n", pt);
    printf(" address of MSG: %p\n", MSG);
    printf("address of "I'm special": %p \n", "I'm special");
    return 0;
}

image.png

说明什么?

         第一,pt和MSG的地址相同,而ar的地址不同,这与我们前面讨论的内容一致。

         第二,虽然字符串字面量"I'm special"在程序的两个printf()函数中出现了两次,但是编译器只使用了一个存储位置,而且与MSG的地址相同。编译器可以把多次使用的相同字面量储存在一处或多处,只不过最初是存储在静态区的,后面有可能被别的字符数组拷贝。

        第三,静态数据使用的内存与ar使用的栈区内存不同。