zhyDaDa的个人站点

目录
字符串
特殊的0
字符串变量
字符串常量
连接的两个字符字面量
小总结
字符串常量
采用数组
指针和数组的选择
字符串的有关操作
赋值
输入输出
读取的机制
安全的输入
常见错误
空字符串
字符串数组
一些表达方式
程序参数
字符函数
单个字符输入 putchar
语法
说明
单个字符输出 getchar
语法
说明
字符串函数
所用的库
strlen
strcmp
strcpy
复制一个字符串
可能的实现形式
strcat
安全问题
安全的版本
strchr
寻找第二个字符
得到第一个配对之前的字符串
strstr
关于输入输出 和 SHELL的一些知识
什么是SHELL?
为什么敲回车程序才继续?
如何停止输入?


字符串

char word1[]={'H', 'e', 'l', 'l', 'o', '!'};被称为字符数组, 有意义, 但不是一个字符串
在C语言中, 定义字符串是这样的:
char word2[]={'H', 'e', 'l', 'l', 'o', '!', '\0'};

注意: word2首先是个字符数组, 但由于其中一个单元存在'\0', 这使之称为了C语言的字符串

特殊的0

  1. 整数0和字符'\0'等价
  2. 字符'0'对应整数48
char a = '0';
char b = '\0';
printf("a=%d\n", a);
printf("b=%d\n", b);

结果是:

a=48
b=0
  1. '\0'标志着字符串的结束, 但ta不属于该字符串
  2. 计算字符串长度时, 不包括这个'\0'(当然, 作为数组时的长度, 以及在计算内存占用时, 仍旧会包含在内)

字符串变量

定义的语法有这样3种

char *str = "Hello";
char word[] = "Hello";
char line[10] = "Hello";

注意, 对于word来说, ta占据了6个字节, 最后的结束符'\0'会由编译器自动补上

char word[] = "Hello";
printf("sizeof(word)=%d\n", sizeof(word));

结果是:
sizeof(word)=6

字符串常量

形如"Hello"的带有双引号的字面量被称为 字符串常量 或是 字符串字面量
这样的字面量会被编译器编译为一个字符数组存放在别处

连接的两个字符字面量

如果两个字符串是相邻的, 且中间没有其他符号, 编译器会自动将两者连接起来
比如一下这个例子:

printf("The end is"
        " not the end.\n");

结果是:

The end is not the end.

此外, 还可以在字符串的中间换行断开, 并用反斜杠\承接
但要注意的是, 这样会把制表符一起混进去

int main()
{
    printf("The end is\
            not the end.\n");

    return 0;
}

结果是:

The end is            not the end.

小总结

  1. 字符串是一种数组
  2. 不能直接参与运算
  3. 一般用数组遍历的方式使用字符串
  4. 唯一特殊的是可以用字符串字面量直接对字符数组初始化

字符串常量

尝试对一个直接定义的字符串做修改

char *s="Hao";
s[0]='C';

结果是程序崩溃

实际上, 通过查询变量的地址, 不难发现
这些字符串字面量的实际地址很小(相较于其他变量的地址)
其实这个地址指向的是存放原始代码的地方, 收到系统保护, 显然是不允许修改的

这是因为 字符串常量 存储的位置与其他变量不同
其实质是一个只读变量
相当于const char *s

采用数组

char s[]="Hao";
s[0]='C';

这种方法就不会报错了

还是通过查看地址就能发现
此时的地址较大, 说明是一般的变量了

指针和数组的选择

选用数组:
+ 表示 “字符串就在这儿”
+ 作为本地变量, 空间被自动开辟/回收

选用指针:
+ 不知道在哪里, 只是去读取不做修改
+ 要动态分配空间的话(使用malloc), 必定需要使用指针

此外, 作为一个函数的参数时, 这两种效果一致

要构造字符串, 选用数组
要处理字符串, 选用指针

字符串的有关操作

赋值

char* s1 = "title";
char* s2;
s2=s1;

根据之前的内容我们可以知道, 第三行的赋值命令的实质, 只是让指针s2也指向了s1指向的字符串的地址
这个过程并不会开辟新的空间, 创造新的字符串

之后在字符串函数中, 有个strcpy函数可以实现创造一个新的一样的字符串的需求

输入输出

char string[8];
scanf("%s", string);
printf("%s", string);

字符串对应的 格式字符"%s"

读取的机制

对于scanf, 其实际动作是读取到 空格/换行符/Tab 为止
这个读取到的”单词”是不包含上述的三样的
而且scanf是不安全的, 因为ta不知道读取内容的长度

char word1[8];
char word2[8];
scanf("%s", word1);
scanf("%s", word2);
printf("%s|<=\n", word1);
printf("%s|<=\n", word2);

输入的内容是: hello world[回车]
输出的内容是:

hello|<=
world|<=

安全的输入

可以在格式字符中间加入一个限定, 指定读入的字符个数

char string[8];
scanf("%7s", string);

下一个scanf的读取这后面开始

注意数字应当等于字符串数组的大小减一

scanf非常不安全, 如果纵容ta随意读取的话, 会造成 数据溢出
其后果可能报错, 可能无大碍, 可能崩溃
更严重的是, 通过ta, 攻击者可以注入攻击性代码, 篡改内存, 甚至能实现操纵电脑

常见错误

char *string;
scanf("%s", string);

第一行代码的意义是: 定义一个字符串类型的指针, 但违背初始化, 是个 野指针

这种定义通俗来说就是:”string是一个将来要指向某个字符数组的指针”
未被初始化就意味着ta会指向任何地方, 有可能会指向不该指的地方

空字符串

char a1[100]="";
这行代码初始化了一个空字符串
+ 这是正确的操作, a1是合法的字符串
+ a1[0]=='\0'true

char a2[]="";
如果让电脑自动分配空间, 那么a2的长度就是1
并且唯一的元素a2[0]就是'\0', 没有意义, 无法存放字符串

字符串数组

一些表达方式

  • char **a;: a是一个指针, 指向另一个指针, 而那个指针指向一个字符(串)

    这样的指针通常称为 二级指针

  • char b[][]: 编译无法通过, 因为第二位的大小必须给定
  • char c[][10]: 表示若干个大小为10的字符数组, 但是如果用于储存字符串, 那么其长度必须确定有上界
  • char *d[]: 每一个元素都是一个字符指针, 能装下很多字符串. 因此, 一般用ta作为字符串数组

程序参数

来看看main函数的参数:
int main (int argc, char const *argv[])
+ argc: 表示字符串数组的个数
+ *argv[]: 字符串数组, 存有若干指令, 这些指令是在运行程序时一并输入的参数

做一个很简单的试验就能知道*argv[]存了什么

int main(int argc, char const *argv[])
{
    for (int i = 0; i < argc; i++)
    {
        printf("%d: %s\n", i, argv[i]);
    }

    return 0;
}

编译为exe文件后在命令行输入指令: .\test.exe 123 abc \n \t
会发现输出结果为:

0: [文件目录]\test.exe
1: 123
2: abc
3: \n
4: \t

第[0]条的意义是什么?
有时候可以说明是以何种方式来运行程序的(比如这种方式下就得到了文件的目录)

通过这种方式可以在外部给程序传参了

字符函数

单个字符输入 putchar

语法

原型
int putchar(int c);
使用
putchar('a')

说明

  • 作用是向 标准输出一个字符
  • 接受的参数是int类型, 但实际上只能接收一个字符的大小
  • 返回值是写了几个字符(通常是1)
  • 如果写入失败, 返回EOF (end of file)

EOF是C语言中定义的 “宏”, 其-1

单个字符输出 getchar

语法

原型
int getchar(void);
使用
char a = getchar()

说明

  • 之所以返回值是int, 是因为有可能要返回EOF

字符串函数

所用的库

常用的标准库是<string.h>
常用的字符串函数有:
+ strlen
+ strcmp
+ strcpy
+ strcat
+ strchr
+ strstr

strlen

原型: size_t strlen(const char *s);
作用: 返回s的字符串的长度(不包括末尾的0)
备注: lenlength 的缩写

可能的实现形式:

int mylen(const char* s){
    int index = 0;
    while (s[index++]!='\0')
        ;
    return --index;
}

strcmp

原型: int strcmp(const char *s1, const char *s2);
作用: 比较两个字符串是否相等
返回值:
+ 0: 表示两者相等
+ 大于零: 表示前者大于后者
+ 小于零: 表示前者小于后者

这里的大小是说字母的ASCII码值, 字母'a'就比'b'来的小
返回值其实就是: 第一个不相等的位置上的字符的差值

备注: cmpcompare 的缩写

可能的实现形式:
+ 数组形式

int mycmp(const char *s1, const char *s2)
{
    int i = 0;
    while (s1[i] == s2[i] && s1[i] != '\0')
    {
        i++;
    }
    return s1[i] - s2[i];
}
  • 指针形式
int mycmp(const char *s1, const char *s2)
{
    while (*s1 == *s2 && *s1 != '\0')
    {
        s1++;
        s2++;
    }
    return *s1 - *s2;
}

如果直接s1==s2会得到什么?
永远会是falsewarning!
因为这个操作实质上是在比较两者的地址, 而地址绝对不是同一个

有一些编译器的strcmp只会返回0或者±1
那这个时候返回值可以改写为三元表达式, 能达到相同效果

strcpy

原型: char* strcpy(char *restrict dst, const char *restrict src);

关键字: restrict表明srcdst不能重叠

“重叠” 就是说dst可能就在src的前方, “拷贝”的动作会覆盖原字符

作用: 把src的字符拷贝到dst
返回值: 就是dst
注意: 参数先dstsrc

复制一个字符串

char *dst = (char *)malloc(strlen(src) + 1);
strcpy(dst, src);

一个小套路, 先申请动态分配空间, 确保这里的dst不会涉及其他内存, 是独立的存在
注意: 别漏了+1

可能的实现形式

  • 数组
char* mycpy(char *dst, const char *src)
{
    int index = 0;
    while (src[index]!='\0')
    {
        dst[index] = src[index];
        index++;
    }
    dst[index] = '\0';
    return dst;
}
  • 指针
char* mycpy(char *dst, const char *src)
{
    char *result = dst;
    while (*dst++=*src++)
        ;
    return result;
}

事实上, 无论代码怎么精简, 编译之后效率是一样的
写的很精简只会令人难以理解(可以看做是一种另类的锻炼方式吧)

strcat

原型: char* strcat(char *restrict s1, const char *restrict s2);
作用: 吧s2拷贝到s1后面, 接成更长的字符串

实质还是一种拷贝
其实就是把s2的第一个字符拷贝到s1最后的\0

返回值: s1

安全问题

strcpystrcat都存在明显的安全隐患
即, 盲目的写入
这很可能会覆盖掉其他需要的数据

安全的版本

  • 对于cpy
    char* strncpy(char *restrict dst, const char *restrict src, size_t n);

  • 对于cat
    char* strncat(char *restrict s1, const char *restrict s2, size_t n);

上述两个函数多了一个参数n
n表示 目的地最多能容纳多少字符
如果超出了这个值, 多余的会被掐掉
因此不存在越界问题

  • 对于cmp
    int strncmp(const char *s1, const char *s2, size_t n);

这里其实并非是出于”安全”目的而设计这个函数
这个n表示仅仅比较两个字符串的前n位, 其余忽略

strchr

原型:
+ char * strchr(const char *s, int c);
+ char * strrchr(const char *s, int c);

作用: 在字符串s中寻找字符
后者只是在名字上多了一个’r’, 表示从右边开始找

返回值:
返回那个字符的指针
没有找到返回 null

寻找第二个字符

char str[] = "zhyDaDa";
char *p = strchr(str, 'D');
p = strchr(p + 1, 'D');
printf("%s\n", p);

这里当找到第一个’D’后得到了其指针p,
此时的p可以看做是一个字符串(因为后面有'\0')
p的后面一个字符找起, 即得到第二个’D’

得到第一个配对之前的字符串

char str[] = "zhyDaDa";
char *p = strchr(str, 'D');
char *t = (char *)malloc(strlen(str) + 1);
char c = *p;
*p = '\0';
strcpy(t, str);
*p = c;
printf("str: %s\n", str);
printf("p: %s\n", p);
printf("t: %s\n", t);
printf("c: %c\n", c);
free(t);

输出的结果是:

str: zhyDaDa
p: DaDa
t: zhy
c: D

这里用了一个非常巧妙的小技巧
即, 将扫描到的第一个匹配改写成'\0'
这样原来的字符串就缩减到了第一个匹配之前的字符串
当然这之后还要改回去, c就是临时储存的被改掉的字符

strstr

原型:
+ char * strstr(const caht *s1, const char *s2);
+ char * strcasestr(const caht *s1, const char *s2);

作用: 前者在一个字符串中寻找字符串
后者的区别是, 在名称上多了一个”case”
ta在寻找字符串的时候会 忽略大小写

关于输入输出 和 SHELL的一些知识

什么是SHELL?

SHELL就是人机交互的接口
用户敲键盘, 输入的字符, 输入快捷键等等行为, 会先送到SHELL去处理
SHELL会准备一个 缓存区 去接受用户的输入
在那之后SHELL把这个缓存区交给程序去处理
程序再告诉SHELL要显示什么, 藉由SHELL来打印到控制台

通俗来说, SHELL 是连接用户和程序之间的一个中介

为什么敲回车程序才继续?

既然getchar只接收一个字符, 为什么在用户输入了很多内容后仍旧处在”等待输入”的状态, 直到用户键入回车才有反应?

这是因为SHELL最基本的一个功能就是 “行编辑”
按下回车之前, 他们都没有被送到程序那里去

“行编辑”其实就是在输入前的一个调整, 让你写下来的东西处在编辑状态
否则如果没有这个功能, 你每次按下一个键就会立刻生效, 那么就无法输入大于一个字符的信息了

当用户编辑好要输入的内容后, 按下了回车
回车的动作被SHELL接收, ta就把用户输入的内容(连带着那个回车一起)并入缓存区
接着SHELL把缓存区的内容交给程序, 此时程序才从缓存区读入字符
程序一旦读完了缓存区的内容并且又需要用户输入了
那么程序告诉SHELL, SHELL就让用户输入(这就是等待输入的状态)

比方用户输入的是: abc123并按下回车
那么缓存区的状态就类似于: |a|b|c|1|2|3|\n|\0|

这里的两个|表示一个字符的单位
最后的\0是假想的, 实际中会用其他符号来代替, 用来表示缓存区的结尾

如果此时有形如scanf("%c%c%c%d%d")的输入命令, 缓存内容不够了
用户再输入: ‘789def并按下回车
那么缓存区就类似于:
|a|b|c|1|2|3|\n|7|8|9|d|e|f|\n|\0|`

如果上述的读取运行了, 那么缓存区就会变成:
|d|e|f|\n|\0|
注意, 此时的缓存区不是空的!
这就是为什么后面跟着的读取会不再要求用户输入!
例如后面还有getchar

这也就是为什么推荐在要求用户输入内容后
最好跟上一个fflush(stdin)来清空缓存

子函数要求输入后如果有残余的缓存, 那么主函数的输入很可能出问题

如何停止输入?

下面这段代码会不断接受并打印输入的内容
只是通过输入字符是无法跳出循环的

int c;
while ((c = getchar())!=EOF)
{
    putchar(c);
}
printf("读到了EOF, 程序正常结束");

此时就要告诉SHELL, 通过SHELL达成目的

如果键入Control+C, 那么相当于要求SHELL强制结束程序
此时由于程序非正常退出, 最后一行代码就不会输出

如果键入Contorl+Z(不同的SHELL如Linux的, 就是输入Contorl+D)
此时SHELL就会在缓存区的最后添上代表EOF的一个符号
此时 getchar 才读到了 EOF
最后的输出也证实了这一点

Avatar photo
我是 zhyDaDa

前端/UI/交互/独立游戏/JPOP/电吉他/游戏配乐/网球/纸牌魔术

发表回复