字符串是C语言最重要最有用的数据类型之一。
说到字符串,一定和指针分不开关系。毕竟连字符串本身都是指针。字符串数组也可以用指针数组来创建。实际上,字符串的大多数操作都是指针完成的。
本文主要会讲很多C库定义的字符串函数
C字符串是以空字符结尾的字符数组。
puts()
puts()函数也是stdio.h中的输入输出函数,但它只能显示字符串,并且自动在字符串末尾加换行符
下面的简单示例展示了三种定义字符串的方法
#include <stdio.h>
#define MSG "I am a symbolic string constant."//用字符串常量定义字符串
#define MAXLENGTH 81
int main()
{
char words[MAXLENGTH] = "I am a string in an array.";//用char类型数组定义字符串,未被使用的数组元素会被初始化为空字符
const char * pt1 = "Something is pointing at me.";//用指向char的指针定义字符串,const表明不会更改这个字符串
puts("Here are some strings:");
puts(MSG);
puts(words);
puts(pt1);
words[8] = 'p';
puts(words);
return 0;
}
注意用char数组定义字符串时,不需要用标准的数组初始化方式:
而且这样初始化务必注意自己加个空字符,不然这就不是字符串,而是字符数组啦
用上面程序的双引号初始化方式,编译器会自动加空字符,但是两种方式都需要自己注意数组的大小至少比字符数多1,以容纳空字符,所以最好还是像以前的数组那样,方括号空着,让编译器自己确定数组大小。即char a[] = “apple pen”; 这样很合理,也很安全。
但是只有在初始化数组的时候才可以让编译器自动确定大小,如果只是先声明一个数组,后面才去填充内容,那就必须写数组的大小了。
char words[20] = {
'I', 'a', 'm', 'a', 'b', 'e', 'e', '\0'};
Here are some strings:
I am a symbolic string constant.
I am a string in an array.
Something is pointing at me.
I am a spring in an array.
字符串字面量的串联(使用双引号)
两句等效
char words[MAXLENGTH] = "I ""am a string"" in an array.";
char words[MAXLENGTH] = "I am a string in an array.";
所以想在字符串内部使用双引号,则需要用转义符号,"
字符串常量属于静态存储类别
static storage class
即函数中的字符串常量只会被存储一次,他在程序的整个生命期内都存在,不管函数被调用几次。
字符串是指针!!!
字符串被视为指向该字符串存储位置的指针,类似于数组名是指向这个数组存储位置的指针
表示超级震惊,还有一点兴奋
#include <stdio.h>
int main()
{
printf("%s, %p, %c\n", "We", "are", *"space farers");//我们是空间旅行者
return 0;
}
我猜对了输出,由于“are”字符串是个指针,即他自己的首元素a的地址,所以%p输出字符a的内存位置;“space farers”是指针,指向首字符s的内存地址,所以对其解引用,得到了字符s,用%c输出。(我好奇用%s能不能输出整个字符串,尝试了,无法,只可以用%c输出首字符)
We, 0040b044, s
除了C99增加的变长数组以外,数组的大小必须是整型常量(包括整型常量的表达式)。
用指针表示法创建指针
这两句声明几乎一样的(不一样的地方后面说),pt1和ar都是字符串 的地址,且两种方式均由字符串决定数组的大小
const char * pt1 = "Something is pointing at me."; //指针法
const char ar[] = "Something is pointing at me.";//数组法
初始化字符数组以存储字符串 VS 初始化指针指向字符串的首字符
这两种创建字符串的方式的区别其实没那么重要,但是要看你用来干嘛了,如果你不想这个字符串字面量被修改,就用数组法;反之,用指针法。
下面较真地仔细分析一下咯
数组形式创建字符串实际会做什么
程序中用数组形式声明的字符串在编译(自动加上结尾空字符)和链接后,被存储在可执行文件(exe, 机器代码)中的数据段中。
当程序被载入到内存中时,字符串常量也就进入了内存,被存在静态存储区static memory中。
当程序开始运行时,才会为这个字符串数组分配内存,然后才把静态存储区中的字符串常量拷贝到数组中。所以此时这个字符串常量有2个副本,一个在静态内存中,一个在数组中(数组是在动态内存中)。
编译时就分配内存的是存在静态内存中,比如常量们;运行时才分配内存的存在动态内存中,比如数组
这时候,编译器才会把数组名ar识别为数组首元素ar[0]的地址&ar[0]。(问题:程序运行时编译器还会干活????看来是这样了)
重点:前面强调过,数组形式中的数组名ar是个**地址常量**!!!不可以对他递增递减,但可以有ar+1这样的运算。(因为==递增递减只能作用于可修改的左值,即变量==,不可用于常量)
指针形式创建字符串又会做什么
程序中用指针形式声明的字符串在编译(自动加上结尾空字符)和链接后,也被存储在可执行文件(exe, 机器代码)中的数据段中。
当程序被载入到内存中时,字符串常量进入内存,也被存在静态存储区static memory中。
至此,和数组形式没有区别。
当程序开始运行时,编译器会为指针变量pt分配一个内存地址,把pt存在那里,pt最初指向字符串首字符,但是他是可以递增递减的,于是可以指向字符串的任意字符。
需要注意的是:字符串字面量在C中被默认视为const数据,是受保护的。所以==指向字符串的指针一定要声明为const指针!!(墙裂推荐)==这样可以保证不能通过这个指针去修改字符串,但是指针本身的值可以修改。在保护字符串字面量这一点上,数组形式就相形见绌了,因为字符串字面量拷贝给数组后可以通过修改数组去修改拷贝来的字符串,但是你把数组也声明为const数组的话,就可以避免通过修改数组去修改数组中的字符串啦。但是不管怎样,数组表示法由于操作的是副本,所以绝对不会修改原来的原件,这一点很安全。
总结:
- 数组初始化法会把静态内存的字符串字面量拷贝给数组;而指针初始化只是把静态内存中字符串字面量的地址拷贝给指针而已。
数组获得的只是副本,指针拿到的是原件,然而这不是说指针厉害,而是说明指针危险!如果不想修改字符串的值,就不要用指针指向它啦 - 数组名是常量,指针名是变量,只有指针名可以递增。但是数组元素是变量,是可以修改的左值哦
示例
#include <stdio.h>
#define MSG "I'm special"
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;
}
这个结果还挺惊讶的,我以为直接写"I’m special"会创建一个新的字符串字面量呢。原来同一个程序中,同一个字符串字面量,不管你直接写它,还是写它的符号名,都只有一个它。(有点说了等于没说的感觉,毕竟MSG本身就只是复制替换)
书上的说明是:不同编译器的处理不同,有的编译器对于多次出现同一个字符串字面量会存在同一个位置;有的编译器存在多个位置。
只有ar的地址不一样,说明指针法确实是被赋给了字符串的地址,而数组则自己在一个新地址,它里面的内容只是一个拷贝副本。
第一句输出代码很棒,字符串就是指针,高级
address of "I'm special": 0040b044
address ar: 0061ff20
address pt: 0040b044
address of MSG: 0040b044
address of "I'm special": 0040b044
创建字符串数组的两种方法
1. 指向字符串的 指针数组
一般情况创建字符串数组,请使用这种方式,指针数组。它的效率比char数组的数组高,(主要是说内存使用率高)。
但指针数组的缺点:它指向的这些字符串字面量的内容是不可以修改的。(所以一定用const指针哦)
所以如果你想修改字符串的内容,或者需要为字符串输入预留空间的话,就只能用第二种方式了。
2. char数组(字符串) 的 数组
示例
#include <stdio.h>
#define SLEN 40
#define LIM 5
int main()
{
//[]优先级高于*,所以mytalents先和[]结合,所以一个const指针数组
const char *mytalents[LIM] = {
"Adding numbers swiftly",
"Multiplying accurately",
"Stashing data",
"Following instructions to the letter",
"Understanding the C language"};
//相当于二维数组,子数组是字符串(字符串是char数组)
char yourtalents[LIM][SLEN] = {
"Walking in a straight line",
"Sleeping", "Watching television",
"Mailing letters", "Reading email"};
int i;
puts("Let's compare talents.");
printf("%-36s %-25s\n", "My Talents", "Your Talents");
for(i=0;i<LIM;i++)
printf("%-36s %-25s\n", mytalents[i], yourtalents[i]);
printf("\nsize of mytalents:%zd, sizeof yourtalents: %zd\n", sizeof(mytalents), sizeof(yourtalents));
return 0;
}
可以看到,mytalents只占了20字节,一共5个字符串,所以一个字符串占了4字节,刚好就是一个地址(我的32位软件),所以mytalents实际上是一个指针数组,存了5个指针,分别指向每一个字符串。
yourtalents占200字节,每个字符串40字节,即每个字符串(字符数组)的长度是40,可以存储40个字符(包括空字符)。
二者相同和不同
相同之处:
- mytalents[1][2]指mytalents的第2个指针指向的字符串的第3个字符;yourtalents[1][2]指yourtalents的第2个字符串中的第3个字符。是一样的。
- 二者的初始化方式也一样。
不同之处:
- 声明不同。看代码
- mytalents中的指针指向的5个字符串存储在静态内存中,而yourtalents的字符串数组中的只是5个原始字符串的副本,存在动态内存中,
- 数组法的内存使用率更低,因为yourtalents的每个子数组的大小必须一样且必须大于最长的字符串。而指针法则有点像是不贵规则长度的数组(只是比喻长度的区别哈,二者存储位置都不一样)
- 指针法的每个字符串并不需要存储在连续的内存中,数组就必须。
Let's compare talents.
My Talents Your Talents
Adding numbers swiftly Walking in a straight line
Multiplying accurately Sleeping
Stashing data Watching television
Following instructions to the letter Mailing letters
Understanding the C language Reading email
size of mytalents:20, sizeof yourtalents: 200
拷贝指针(效率更高) VS 拷贝字符串
#include <stdio.h>
int main()
{
//mesg, copy都是指向char的指针
const char *mesg = "Don't be a fool!";
const char *copy;
copy = mesg;//这样只是创建了一个新的指针,也让他指向字符串,原来的指着还在,字符串也在
printf("%s\n", copy);
//&mesg是mesg指针被存储的位置,mesg是字符串被存储的位置(指针存储的地址)
printf("mesg = %s, &mesg = %p, value = %p\n", mesg, &mesg, mesg);
printf("copy = %s, © = %p, value = %p\n", copy, ©, copy);
return 0;
}
可以看出,只有© 和 &mesg不一样,所以字符串并没有被拷贝,只是让新指针也指向字符串,明显这样做比拷贝整个字符串的效率高很多,字符串越长对比优势越明显,所以指针是提供了一种捷径,一种更简单偷懒的方法
Don't be a fool!
mesg = Don't be a fool!, &mesg = 0061ff2c, value = 0040b044
copy = Don't be a fool!, © = 0061ff28, value = 0040b044
从键盘输入字符串
不要觉得这一节很简单,,没啥好说的,我们已经输入过很多次字符串了,但是我们不知内部原理,一丁点都不知道。
这一节主要两点,一是输入字符串之前给它分配内存,二是输入字符串的函数,比gets, fgets, scanf。
必须在程序中给字符串分配足够的内存才可以读入
计算机不会在读取字符串的时候顺便计算其长度再分配空间的,不过以后可以自己试试写一个这样的函数。
之前刚学指针时说过不要在指针未初始化时就解引用它,否则会导致不可预知的结果,因为指针可能指向任何地方, 那个地方存储的值也就不可预知。
但这里说的是,指向char的指针,未初始化时不可以用作字符串输入函数的参数。
通过上面的示例我们已经知道,%s实际上是对指向char字符的指针指向的内容的转换说明,它对应的参数是指针,之前我们用字符串作为他对应的参数,但现在我们知道,字符串就是指针。
最常用的 gets函数
scanf搭配%s只可以读取一个单词
scanf搭配%s只可以读取一个单词,因为scanf要跳过空白,换行符,制表符,它主要的工作并不是专门读取字符串的
#include <stdio.h>
int main()
{
char a[10];
//scanf("%s", &a);//警告%s要和char*类型匹配,即指向char的指针,但是&a的类型是char(*)[10],即指向一个长度为10的char数组的指针
scanf("%s", a);//a是char*类型,即指向char的指针
printf("%s", a);
return 0;
}
虽然有警告,但scanf("%s", &a);和scanf("%s", a);的输出结果一样,都只能读取一个单词
we are friends
we
重要,三遍
%s要和char类型匹配,即指向char的指针
%s要和char类型匹配,即指向char的指针
%s要和char*类型匹配,即指向char的指针
此外,如果指定字段宽度,则scanf要么读取字段宽度个字符(就算没遇到空白字符),要么遇到空白字符就停下了
#include <stdio.h>
int main()
{
char name1[11], name2[11];
int count;
printf("Please enter 2 names.\n");
count = scanf("%5s %10s", name1, name2);
printf("I read the %d names %s and %s", count, name1, name2);
return 0;
}
由于第一个名字字段只有5,所以littl读取到就结束了,读取第二个名字时emary还在缓冲区,于是就被读到了,后面又是空白字符,所以就结束读取,导致错误。
可以看到丢弃多余的字符是很有必要的,否则滞留在缓冲区会影响后续输入。
Please enter 2 names.
littlemary greenapple
I read the 2 names littl and emary
最常用却不安全的gets函数(已被C11标准废除,不要使用它了!!!)
gets函数会带来安全隐患。
为了读取一行输入, 于是gets诞生,用于读取一整行输入ÿ