1. 字符串
#include <stdio.h>
#define MSG "I am a symbolic string constant."
#define MAXLEN 81
int main(int argc, char const *argv[])
{
char words[MAXLEN] = "I am a string in an array.";
char *ptr1 = "Something is pointing at me.";
puts("Here are some strings");
puts(MSG);
puts(words);
puts(ptr1);
printf("\n");
return 0;
}
输出结果:
Here are some strings
I am a symbolic string constant.
I am a string in an array.
Something is pointing at me.
注意:puts() 函数和 printf() 不同的是,puts() 函数只显示字符串,而且自动在显示的字符串末尾加上换行符
2.定义字符串
a. 定义字符串常量
用双引号括起来的内容称为字符串常量,双引号中的字符和编译器自动加入末尾的 \0 字符,都作为字符串存储在内存中。
如果字符串常量之间没有间隔,或者用空白字符分隔,c会将其视为串联起来的字符串常量*
下面两种声明是等价的char greetinf[50] = "hello ""and" " how are y""ou!" char greetinf[50] = "hello and how are you!"
如果要在字符串内部使用双引号,则必须要在双引号前面加上一个反斜杠 ( \ )
char greetinf[50] = "\"hello\" and \"how are you!\""
输出结果:
"hello" and "how are you!"
字符串属于静态存储类别,这说明 如果函数中使用字符串常量,该字符只会被存储一次,在整个程序的生命周期内存在,即使函数被调用多次。用双引号括起来的内容 被视为 指向该字符串存储位置 的指针,这类似于把数组名作为指向该数组的指针。
printf("%s, %p, %c\n", "We", "are", *"hello word");
输出结果:
We, 0x55fa55b659c1, h
%s 打印字符串 We ,
%p 打印字符串 “are” 所在的地址(用双引号括起来的内容 被视为 指向该字符串存储位置 的指针),
%c 打印 ‘h’ 这个字符,“hello word” 表示该字符串所在的地址,* “hello word” 则表示该字符串所指向地址上存储的值,%c 只读取一个字符 h (字符串的地址 也就是 首字符所在的地址:这类似于把数组名作为指向该数组的指针)
b. 定义字符串数组和初始化
定义字符串数组时,必须要让编译器知道你需要多少空间,有两种方式存储字符串。
创建足够空间的数组存储字符串,用指定字符串初始化数组buf[ ]
char buf[40] = "I am a string in an array.";
这样初始化,比标准数组初始化简单,以下是用数组标准初始化
char buf2[40] = { 'I', ' ', 'a', ········, '\0'}
注意:
用标准数组初始化方法,最后一个字符是 ‘\0’ ,如果没有 ‘\0’ 这就不是一个字符串,而是一个字符数组。所以在指定数组大小时,要确保数组元素个数至少比字符串长度多1(容纳空字符)。所有未使用元素初始化的元素都被自动初始化为0(这里0是char形式的空字符 \0 ,不是数字字符0)数组初始化声明中的大小是可以省略的,这样编译器自动计算大小。让编译器确定初始化字符数组的大小是合理的,因为处理字符串的函数通常都不知道数组的大小,这些函数是通过查找字符串末尾的空字符确定字符串在何时结束。
注意:让编译器确定数组大小的只能用在初始化数组时,如果想先创建再填充数组,就必须要在声明的时候指定数组大小,这里又要注意数组大小必须是 整型常量或者整型常量的组合。
字符数组名和其他数组名一样,是该数组首元素的地址。声明以下字符数组:
char name[10] = "xiaoming";
则有以下关系对应:
name = &name[0] *name = name[0] = 'x' *(name + 1) = name[1] = 'i'
使用指针方法创建字符串
char *ptr1 = "Something is pointing at me."; char ptr2[] = "Something is pointing at me.";
以上两种声明几乎相同,ptr1 ptr2都是该字符串的地址,其中带双引号的字符串本身决定了预留给字符串的存储空间
c. 数组和指针
数组形式 ( ptr1[ ] ) 在计算机内存中分配一个内含29个元素的数组(每个元素对应一个字符,末尾还有一个空白字符 ‘\0’),每个元素被初始化为字符串常量对应的字符。通常,字符串都作为可执行程序的一部分存储在数据段中,当把程序载入内存时,也载入了程序中的字符串,字符串存储在静态存储区,但是程序在开始运行时才会为该数组分配内存,此时才将字符串拷贝到数组。
注意:此时字符串有两个副本:一个是静态内存中的字符串常量,另一个是存储在ptr1数组中的字符串。此后 编译器便把数组名 ptr1 识别为该数组首元素地址( &ptr1[0] )的别名,这里要关键理解:在数组形式中, ptr1 为地址常量,不可更改 ptr1 ,如果改变了 ptr1 ,那就意味着改变了数组的存储位置,可以进行 ptr1+1这样的操作 用于标识数组的下一个元素,但是不允许 ++ptr1 这种操作,递增运算符只用于变量名前,只能用于修改左值,不能用于常量。
指针形式 ( *ptr2 )也使得编译器为字符串在静态存储区预留了29个元素 的空间,程序开始执行后,会为指针变量 ptr2 开辟一块空间,并把字符串的地址赋值给指针变量,该变量最初指向字符串的首字符,它是指针变量,因此可以改变,可以使用自增运算符,即++ptr2 指向第二个字符。
字符串常量被视为const数据,由于 ptr2 指向这个const数据,所以应该把 ptr2 声明为const类型的指针,这意味着 不能用ptr2改变它指向的数据,但是ptr2本身是可以改变的(也就是 *ptr2 不可重新赋值,但是ptr2本身是可以改变的) ,如果把字符串拷贝给数组,那就可以随便改变数据,除非数组时const。
总结:初始化数据是把静态存储区的字符串拷贝到数组中,初始化指针只把字符串的地址拷贝给指针
看下面例子:#include <stdio.h> #define MSG "aaa" int main(int argc, char const *argv[]) { char ptr1[] = MSG; char *ptr2 = MSG; printf("\"aaa\" %p\n", "aaa"); printf("MSG %p\n", MSG); printf("ptr1 %p\n", ptr1); printf("ptr2 %p\n", ptr2); printf("\"aaa\" %p\n", "aaa"); return 0; }
运行结果
"aaa" 0x5654ae0ec7f4 MSG 0x5654ae0ec7f4 ptr1 0x7ffd56d0a934 ptr2 0x5654ae0ec7f4 "aaa" 0x5654ae0ec7f4
从上面结果可以看出:
第一:ptr2 和 MSG的地址相同,而ptr1的地址不同,这与总结的一致
第二:字符串常量 “aaa” 在程序中两个 printf() 函数中出现了2次,但是编译器只使用了一个位置来存储,并且与MSG的地址相同。编译器可以把多次使用的相同字符串常量存储在一处或者多处,另一个编译可能在不同位置存储”aaa”
第三:静态数据使用的内存和ptr1使用的动态内存不同,不仅值不同,不同的编译甚至使用不同的位数表示两种内存。
d.数组和指针的区别
两者主要的区别是:数组名是常量,而指针名是变量
看下面例子
char heart[] = "i love Tom"; char *head = "i love Tim";
都可以使用数组表示法:
for (i = 0; i < 5; ++i) { putchar(heart[i]); } putchar('\n'); for (i = 0; i < 5; ++i) { putchar(head[i); }
输出结果:
i lov i lov
都能进行指针加法操作
for (i = 0; i < 5; ++i) { putchar(*(heart+i)); } putchar('\n'); for (i = 0; i < 5; ++i) { putchar(*(head+i)); } putchar('\n');
输出结果:
i lov i lov
只有指针表示法可以递增
while(*(head)) putchar(*(head++)); putchar('\n');
输出结果:
i love Tim
数组的元素是变量(除非数组被声明为const),数组名是常量,不可改变,不可做左值使用
heart[0] = 'I'; puts(heart);
输出结果:
I love Tom
指针变量不可使用数组表示法修改字符串内容
head[0] = 'I'; //不允许,非法操作 puts(head);
输出结果:
Segmentation fault (core dumped)
注意:
1.指针初始化字符串变量时 最好使用const限定符const char * ptr1 = MSG;
2. 如果不修改字符串内容,尽量不要使用指针指向字符串常量。
e. 字符串数组
指针数组,可以通过下标访问多个不同字符串。
#include <stdio.h>
#define LEN 4
#define SIZE 10
int main(int argc, char const *argv[])
{
const char *buf1[LEN] = { "hello", "world", "char", "array"};
const char buf2[LEN][SIZE] = { "name", "age", "sex", "number"};
int i;
for (i = 0; i < LEN; ++i)
{
printf("%s\t", buf1[i]);
}
putchar('\n');
for (int i = 0; i < LEN; ++i)
{
printf("%s\t", buf2[i]);
}
putchar('\n');
printf("buf1 size : %ld\n", sizeof(buf1));
printf("buf2 size : %ld\n", sizeof(buf2));
return 0;
}
输出结果:
hello world char array
name age sex number
buf1 size : 32
buf2 size : 40
对于指针表示法的字符串数组buf1,内含4个指针的数组,共占用32个字节
对于数组表示法的字符串数组buf2,内含4个数组的数组,每个数组占用10个字节,共占用40个字节。
buf1[0] 和 buf2[0] 都表示第一个字符串,但是它们的类型并不相同,buf1中的指针指向初始化时所用的字符串自常量的位置,这些字符串被储存在静态内存中;buf2中的数组则存着字符串常量的副本,所以每个字符串都被存储了两次。此外 为字符串数组分配内存的使用率低,buf2中的每个元素大小相同,且必须是能存储最长字符串的大小。
可以把buf2理解为矩形二维数组,每行长度都是10,空白元素都是用 ‘\0’ 补全,而buf1则为不规则数组,每行的长度不同,
综上:
如果只是显示一系列字符串常量,使用指针数组,比二维字符数组效率更高,但是指针数组指向的字符串常量不可以修改
如果要改变字符串或者为字符串输入预留空间,则使用二维字符数组,不要使用指针数组。
f.指针和字符串
#include <stdio.h>
int main(int argc, char const *argv[])
{
const char * buf = "it is string.";
const char * copy;
copy = buf;
printf("buf = %s\t &buf = %p\t value = %p\n", buf, &buf, buf);
printf("copy = %s\t © = %p\t value = %p\n", copy, ©, copy);
return 0;
}
输出结果:
buf = it is string. &buf = 0x7ffffde2ab08 value = 0x561ae50237c8
copy = it is string. © = 0x7ffffde2ab10 value = 0x561ae50237c8
第一项打印字符串,都是 it is string.
第二项 是两个指针的地址
第三项 是指针的值,也就是它存储的地址,buf 好 copy都是 0x561ae50237c8 ,说明都指向同一个位置,也说明程序并没有拷贝字符串,而是让copy的指针指向buf所指向的字符串。