本系列文章为浙江大学翁恺C语言程序设计学习笔记,前面的系列文章链接如下:
C语言程序设计学习笔记:P1-程序设计与C语言
C语言程序设计学习笔记:P2-计算
C语言程序设计学习笔记:P3-判断
C语言程序设计学习笔记:P4-循环
C语言程序设计学习笔记:P5-循环控制
C语言程序设计学习笔记:P6-数据类型
C语言程序设计学习笔记:P7-函数
C语言程序设计学习笔记:P8-数组
C语言程序设计学习笔记:P9-指针
定义字符数组:
char word[] = {‘H’, ‘e’, ‘l’, ‘l’, ‘o’,‘!’};
这个数组中若干字符组合在一起,并且它们组合起来是有意义的。但是这不是C语言的字符串,因为不能用字符串的方式做计算。
定义字符串:
char word[] = {‘H’, ‘e’, ‘l’, ‘l’, ‘o’,‘!’,’\0’};
C语言中字符串的性质如下:
• 以0(整数0)结尾的一串字符
• 0或’\0’是一样的,但是和’0’不同
• 0标志字符串的结束,但它不是字符串的一部分
• 计算字符串长度的时候不包含这个0
• 字符串以数组的形式存在,以数组或指针的形式访问
• 更多的是以指针的形式
• string.h 里有很多处理字符串的函数
定义字符串变量的方式:
char *str = “Hello”;
char word[] = “Hello”;
char line[10] = “Hello”; //占据6个字节,因为后面还有个0
字符串常量:
• “Hello”
• ″Hello″ 会被编译器变成一个字符数组放在某处,这个数组的长度是6,结尾还有表示结束的0
• 两个相邻的字符串常量会被自动连接起来
• 比如之前写的printf("请输入身高的英尺和英寸," "如输入\"5 7\"表示5英尺7英寸:");
• 这两个字符串会被连接起来成为1个大的字符串。
• 行末的\表示下一行还是这个字符串常量
• printf("请输入身高的英尺和英寸, \
如输入\"5 7\"表示5英尺7英寸:");
注:如果用\来连接两个字符串,\和后面字符之间的一些空格、Tab也会进入到新的字符串中。因此,一般第二行的字符串我们都贴到最左侧。
总结:
• C语言的字符串是以字符数组的形态存在的
• 不能用运算符对字符串做运算
• 通过数组的方式可以遍历字符串
• 唯一特殊的地方是字符串字面量可以用来初始化字符数组
• 以及标准库提供了一系列字符串函数
上面我们写了三种定义字符串的方式,既然字符串也是一个特殊的字符数组,那我们试着对其内容进行修改,看看会发生什么。
#include
#include
int main(void)
{
char *s = "Hello World";
s[0] = 'B';
printf("Here!s[0]=%c\n", s[0]);
return 0;
}
在上面程序中我们设置修改字符串的首个字母。运行,可以发现直接异常。
为什么会出异常呢?我们先来看看s的地址是多少,同时定义一个内容相同的字符串变量s2和一个int类型的变量i,看看这三者的地址有什么差异。
#include
#include
int main(void)
{
int i = 0;
char *s = "Hello World";
char *s2 = "Hello World";
printf("&i=%p\n", &i);
printf("s =%p\n", s);
printf("s2=%p\n", s2);
printf("Here!s[0]=%c\n", s[0]);
return 0;
}
运行,可以发现s2与s的地址竟然相同,同时s与s2的地址明显比int类型变量i的地址小得多。
实际上,造成这种结果的原因是:i这个变量是个本地变量,它与s所指的那个字符串在内存中不在同一块区域,它们离得很远。s所指的那个字符串在内存中的代码段,且代码段中的内容是只读的,不可修改。
因此,对于字符串常量,总结如下:
char* s = "Hello, world!";
• s是一个指针,初始化为指向一个字符串常量
• 由于这个常量所在的地方,所以实际上s是 const char* s ,但是由于历史的原因,编译器接受不带const的写法
• 但是试图对s所指的字符串做写入会导致严重的后果
• 如果需要修改字符串,应该用数组:
char s[] = "Hello, world!";
我们来看下char* s = "Hello, world!"和char s[] = "Hello, world!"的区别和联系。我们使用数组定义一个内容相同的字符串变量s3,然后看看其地址且修改其首字母。
#include
#include
int main(void)
{
int i = 0;
char *s = "Hello World";
char *s2 = "Hello World";
char s3[] = "Hello Wordl";
printf("&i=%p\n", &i);
printf("s =%p\n", s);
printf("s2=%p\n", s2);
printf("s3=%p\n", s3);
s3[0] = 'B';
printf("Here!s3[0]=%c\n", s3[0]);
return 0;
}
用指针和数组构建字符串的区别和用处:
• char *str = “Hello”;
• char word[] = “Hello”;
• 数组:这个字符串在这里
• 作为本地变量空间自动被回收
• 指针:这个字符串不知道在哪里
• 处理参数
• 动态分配空间
char*是字符串?
• 字符串可以表达为char*的形式
• char*不一定是字符串
• 本意是指向字符的指针,可能指向的是字符的数组(就像int*一样)
• 只有它所指的字符数组有结尾的0,才能说它所指的是字符串
相较于C语言后面的语言,它对字符串处理的能力还是不足。比如说现在有两个字符串要做赋值
char *t = “title”;
char *s;
s = t;
并没有产生新的字符串,只是让指针s指向了t所指的字符串,对s的任何操作就是对t做的
对于C语言的基础类型,我们有办法做输入输出。对于字符串,printf和scanf也有特殊的手段对其做输入和输出,那就是%s。
char string[8];
scanf(“%s”, string);
printf(“%s”, string);
我们来写代码来看看printf和scanf对字符串做操作的具体细节。
#include
int main(void)
{
char string[8];
scanf("%s", string);
printf("%s##\n", string);
return 0;
}
运行,输入Hello World,可以发现只读到了Hello。
我再来测试一下,看看连续输入两个字符串会是什么结果。
#include
int main(void)
{
char word[8];
char word2[8];
scanf_s("%s", word);
scanf_s("%s", word2);
printf("%s##%s##\n", word, word2);
return 0;
}
我们输入Hello World和Hello 回车 World。可以发现分别被读入word和word2。
#define _CRT_SECURE_NO_WARNINGS
#include
int main(void)
{
char word[8];
char word2[8];
scanf("%s", word);
scanf("%s", word2);
printf("%s##%s##\n", word, word2);
return 0;
}
运行,分别测试两种输入方式,可以看出成功读入。
因此,scanf读取字符串总结如下:
• scanf读入一个单词(到空格、tab或回车为止)
• scanf是不安全的,因为不知道要读入的内容的长度
• 比如我这里的word是8个字节,而scanf并不知道
如果我们输入超过8个字节,如我输入12345678 回车 12345678,直接报错,数组越界。
因此,安全的方式是:在%和s之间插入一个数字,代表输入多少个字符,如%7s。
#define _CRT_SECURE_NO_WARNINGS
#include
int main(void)
{
char word[8];
char word2[8];
scanf("%7s", word);
scanf("%7s", word2);
printf("%s##%s##\n", word, word2);
return 0;
}
我分别输入123 回车 12345678。可以看出,123不到7个被直接读入,12345678只被读取了前7个。
如果我直接输入一个超过7位的数,如12345678,可以看出1234567给了第一个字符串,8自动给了第二个字符串。
因此,对于scanf安全地输入字符串总结如下:
• char string[8];
• scanf(“%7s”, string);
• 在%和s之间的数字表示最多允许读入的字符的数量,这个数字应该比数组的大小小一
常见的错误:
• char *string;
• scanf(“%s”, string);
• 以为char*是字符串类型,定义了一个字符串类型的变量string就可以直接使用了
• 由于没有对string初始化为0,所以不一定每次运行都出错
空字符串
• char buffer[100]=””;
• 这是一个空的字符串,buffer[0] == ‘\0’
• char buffer[] = “”;
• 这个数组的长度只有1,即buffer[0]才有内容,是个'\0'。
如果想写一个字符串数组表达多个字符串,该怎么写呢?我们试着来写几种看看是不是字符串数组。
1、char **a
a是一个指针,指向另一个指针,那个指针指向一个字符(串)
2、char a[][]
a是一个二维数组,第二个维度的大小不知道,不能编译
3、char a[][10]
a是一个二维数组,a[x]是一个char[10]
4、char *a[]
a是一个一维数组,a[x]是一个char*
对于第三种和第四种可以用来当作字符串数组,但是第三种有一定局限性,即由于二维数组必须确定列数,导致所有的字符串必须小于10个字符。第三种与第四种方法的区别如下图所示:
字符串数组还有个有趣的应用的地方,那就是main函数的参数。
• int main(int argc, char const *argv[])
• argv[0]是命令本身,argc代表后面字符串数组argc有多少个字符串
• 当使用Unix的符号链接时,反映符号链接的名字
我们写代码来看看这个argv字符串数组里面有什么东西。
#define _CRT_SECURE_NO_WARNINGS
#include
int main(int argc, char const *argv[])
{
int i;
for (i = 0; i < argc; i++)
{
printf("%d:%s\n",i,argv[i]);
}
return 0;
}
运行,可以看出输出了可执行文件位置和名称。
如果我们在后面再接一些东西,也会打印出来。
putchar函数
• int putchar(int c);
• 向标准输出写一个字符
• 返回写了几个字符,EOF(-1)表示写失败
getchar函数
• int getchar(void);
• 从标准输入读入一个字符
• 返回类型是int是为了返回EOF(-1)
• Windows—>Ctrl-Z
• Unix—>Ctrl-D
我们写个程序来详细探究putchar和getchar的功能。
#define _CRT_SECURE_NO_WARNINGS
#include
int main(int argc, char const *argv[])
{
int ch;
while ((ch=getchar()) != EOF)
{
putchar(ch);
}
printf("EOF\n");
return 0;
}
我们运行,并输入一些东西来测试,发现都原封不动地打印了出来。直到我们输入了Ctrl+C,getchar停止读取。当我们按下Ctrl+Z,并继续按下回车,也会产生相同的效果。(我这里和视频中的Dev C++不同,视频里面按下Ctrl+C程序直接结束,不会打印EOF。视频里面输入Ctrl+Z效果和我这里相同)
思考:getchar()只读1个字符,但是我这里输入了很多东西它都没有回答我,直到我按下回车。同时,按下Ctrl+C程序直接结束了,按下Ctrl+D,getchar就停止读取了,这是为什么呢?
这是因为前面我们讲过,我们的程序和显示器、键盘之间通过Shell相连。键盘输入的东西,Shell会形成一个行编辑,在这个缓冲区内存放了我们输入的所有东西。只要在缓冲区内遇到回车,便会将前面的东西打印出来(getchar一个一个取出来打印,scanf将多个字符看成一个大的东西输出打印)。当我们输入Ctrl+D,就会在缓冲区内加上一个类似于EOF的东西,getchar读到它后就不再读取。如果我们输入Ctrl+C(Windows是Ctrl+Z),直接杀死我们的程序。
C语言提供了很多函数来帮助我们处理字符串,这些函数的原型都在string.h中,因此使用这些函数需要#include
• size_t strlen(const char *s);
• 返回s的字符串长度(不包括结尾的0)
我们写一些代码来看看strlen的用法。我们定义了一个字符串,然后分别使用strlen和sizeof求其长度与大小。
#define _CRT_SECURE_NO_WARNINGS
#include
#include
int main(int argc, char const *argv[])
{
char line[] = "Hello";
printf("strlen=%lu\n", strlen(line));
printf("sizeof=%lu\n", sizeof(line));
return 0;
}
可以看出,字符串长度为5,大小为6(含有最后的’\0’)。
我们试着自己来写这个strlen函数看看。我们可以从字符串首个字符开始遍历,直到遇到’\0’。
#define _CRT_SECURE_NO_WARNINGS
#include
#include
size_t mylen(const char* s)
{
int idx = 0;
while (s[idx] != '\0') {
idx++;
}
return idx;
}
int main(int argc, char const *argv[])
{
char line[] = "Hello";
printf("strlen=%lu\n", mylen(line));
printf("sizeof=%lu\n", sizeof(line));
return 0;
}
运行,使用自己写的strlen函数来查看字符串的长度,结果正确。
strcmp用来比较两个字符串,最后的结果有三种:相对、大于、小于。
• int strcmp(const char *s1, const char *s2);
• 比较两个字符串,返回:
• 0:s1==s2
• >0:s1>s2
• <0:s1
那么如何来定义两个字符串谁大谁小呢?我们先来测试下直接使用==来比较两个字符串和使用strcmp来比较两个字符串。
#define _CRT_SECURE_NO_WARNINGS
#include
#include
int main(int argc, char const *argv[])
{
char s1[] = "abc";
char s2[] = "abc";
printf("%d\n", s1==s2);
printf("%d\n", strcmp(s1,s2));
return 0;
}
数组1==数组2这种写法,结果恒为0(即不相等)。注意:两个字符串相等,strcmp返回0.
我们再来测试下不相等的情况。
#define _CRT_SECURE_NO_WARNINGS
#include
#include
int main(int argc, char const *argv[])
{
char s1[] = "abc";
char s2[] = "bbc";
printf("%d\n", strcmp(s1,s2));
char s3[] = "abc";
char s4[] = "Abc";
printf("%d\n", strcmp(s3, s4));
printf("%d\n", 'a' - 'A');
return 0;
}
运行,可以发现’a’比’b’小,所以返回-1。'a’比’A’大,所以返回1。(在视频中Dev C++中strcmp返回不相等字符对应的差值,如代码中strcmp(s3,s4)会返回32。这是由于我们的编译器不同所导致)
我们再看一种特殊情况,两个字符串前面一行,其中一个字符串只比另外一个字符串多一个空格,看看会发生什么。
#define _CRT_SECURE_NO_WARNINGS
#include
#include
int main(int argc, char const *argv[])
{
char s1[] = "abc";
char s2[] = "abc ";
printf("%d\n", strcmp(s1,s2));
printf("%d\n", '\0' - ' ');
return 0;
}
可以看出,带空格的更大。这是由于s1[3]-s2[3]= ‘\0’ - ’ ’ = 0 - 32 = -32。
我们自己写一个strcmp看看,其中我们使用了两种方式:数组和指针的方式。
#define _CRT_SECURE_NO_WARNINGS
#include
#include
//使用数组的方式
int mycmp1(const char* s1, const char* s2)
{
int idx = 0;
while (s1[idx] == s2[idx] && s1[idx] != '\0') {
idx++;
}
return s1[idx] - s2[idx];
}
//使用指针的方式
int mycmp2(const char* s1, const char* s2)
{
while (*s1 == *s2 && *s1 != '\0') {
s1++;
s2++;
}
return *s1 - *s2;
}
int main(int argc, char const *argv[])
{
char s1[] = "abc";
char s2[] = "Abc ";
printf("%d\n", mycmp1(s1,s2));
printf("%d\n", mycmp2(s1,s2));
return 0;
}
测试,可以发现两种方法结果都正确。这也是我们常用的处理字符串的两种方法:把其当作数组、把其当作指针。
字符串拷贝函数strcpy定义如下:
• char * strcpy(char *restrict dst, const char *restrict src);
• 把src的字符串拷贝到dst
• restrict表明src和dst不重叠(C99)
• 返回dst
• 为了能链起代码来
其中,restict表明src和dst不重叠,重叠的情况是什么样的,如下图所示:
为什么不能重叠呢。因为字符串拷贝操作经常使用,现在的计算机都是多核多线程,这些拷贝操作通常由多个核一起完成,如果出现重叠就会导致错误。
此外,函数返回就是dst,这是因为我们希望strcpy的结果能够再参与其它运算。我们拿strcpy经常做的一件事就是复制一个字符串。
//注意要+1,因为有个结尾的'\0'
char *dst = (char*)malloc(strlen(src)+1);
strcpy(dst, src);
我们自己来写一个strcpy函数。
#define _CRT_SECURE_NO_WARNINGS
#include
#include
//使用数组的方式
char* mycpy1(char* dst, const char* src)
{
int idx = 0;
while (src[idx]) {
dst[idx] = src[idx];
idx++;
}
dst[idx] = '\0';
return dst;
}
//使用指针的方式
char* mycpy2(char* dst, const char* src)
{
char* ret = dst;
while (*dst++ = *src++)
;//while循环就是个空循环
*dst = '\0';
return ret;
}
int main(int argc, char const *argv[])
{
char s1[] = "abc";
char s2[] = "def";
char s3[] = "ghi";
char s4[] = "jkl";
mycpy1(s1,s2);
int i = 0;
while (s1[i] != '\0') {
printf("%c ",s1[i]);
i++;
}
printf("\n");
mycpy2(s3,s4);
i=0;
while (s3[i] != '\0') {
printf("%c ", s3[i]);
i++;
}
printf("\n");
return 0;
}
strcat函数主要做字符串拼接,定义如下:
• char * strcat(char *restrict s1, const char *restrict s2);
• 把s2拷贝到s1的后面,接成一个长的字符串
• 返回s1
• s1必须具有足够的空间
其实就是从dst[strlen(dst)]开始将src的内容拷贝过去,如下图所示。
安全问题
• strcpy和strcat都可能出现安全问题
• 如果目的地没有足够的空间?
• 建议:尽可能不用它们,使用安全版本
安全版本
安全版本的参数表中多了个n,且函数名称也多了个n。这个n就代表最多可以拷贝过去多少个字符,如果多了就掐掉。对于strcmp的安全版本,我们只是用来判断前n个是否相等。
char * strncpy(char *restrict dst, const char *restrict src, size_t n);
char * strncat(char *restrict s1, const char *restrict s2, size_t n);
int strncmp(const char *s1, const char *s2, size_t n);
字符串中找字符
• char * strchr(const char *s, int c);
• 从左往右搜索,返回c第一次出现的位置
• char * strrchr(const char *s, int c);
• 从右往左搜索,返回c第一次出现的位置
• 返回NULL表示没有找到
我们写段代码来测试下这些函数功能。
#define _CRT_SECURE_NO_WARNINGS
#include
#include
int main(int argc, char const *argv[])
{
char s[] = "hello";
char *p = strchr(s, 'l');
printf("%s\n", p);
return 0;
}
运行,可以发现成功将l从左往右第一次出现的地方及后面的字符以字符串形式打印出来了。
现在我有个问题,如何寻找第2次出现的地方呢?我们只需从第一次找到’l’的地方后面一位开始继续寻找’l’出现的地方。
#define _CRT_SECURE_NO_WARNINGS
#include
#include
int main(int argc, char const *argv[])
{
char s[] = "hello";
char *p = strchr(s, 'l');
p = strchr(p+1, 'l');
printf("%s\n", p);
return 0;
}
运行,结果正确。
现在我想把’l’后面的东西复制到另外一个地方
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
int main(int argc, char const *argv[])
{
char s[] = "hello";
char *p = strchr(s, 'l');
char *t = (char*)malloc(strlen(p)+1);
strcpy(t,p);
printf("%s\n", t);
free(t);
return 0;
}
运行,结果正确。
如果我想把’l’左边的东西放到另外个地方,怎么办呢?
#define _CRT_SECURE_NO_WARNINGS
#include
#include
#include
int main(int argc, char const *argv[])
{
char s[] = "hello";
char *p = strchr(s, 'l');
char c = *p;
*p = '\0';
char *t = (char*)malloc(strlen(s)+1);
strcpy(t,s);
printf("%s\n", t);
free(t);
return 0;
}
char * strstr(const char *s1, const char *s2);
char * strcasestr(const char *s1, const char *s2); //寻找过程中忽略大小写