diff --git a/C Crash Course/13 String/Prelesson/ten-common-used-string-func.md b/C Crash Course/13 String/Prelesson/ten-common-used-string-func.md index a7377ba..cb3dec6 100644 --- a/C Crash Course/13 String/Prelesson/ten-common-used-string-func.md +++ b/C Crash Course/13 String/Prelesson/ten-common-used-string-func.md @@ -1,637 +1,651 @@ -### 全局变量 -#### 认识 全局变量 -- 定义在函数外的变量就是全局变量 -- 全局变量具有全局的生存期和作用域 - - 它们与任何函数无关 - - 任何函数(定义在全局变量后的的函数)内部都可以使用它们 +>看下去一定对你有帮助!有帮助就点个赞吧 -例如: +字符串函数 指的是头文件 ` stdio.h ` 中的输入输出函数 和 头文件`string.h`里定义的我们平时直接使用的函数。 +一下是本节重点讲解的 10 个函数。对于生僻点的字符串函数我们以后再讲。 + +- putchar & getchar +- strlen & strnlen_s +- strcmp & strncmp +- strcpy & strncpy +- strcat & strncat + +这些函数我们到处在用,可你有没有想过,究竟这些函数是怎么声明和定义的?他们远没有你想的那么简单。 + +**以下被划掉的部分如果你理解,那是最好。不理解不可以不用纠结,~~慢慢来~~** +![](https://img-blog.csdnimg.cn/20200206205235611.jpg) +### (一)putchar & getchar +#### putchar +>`int putchar( int ch )` +>**头文件**:stdio.h +>**定义**:写字符 ch 到 stdout 。在内部,字符于写入前被转换到 unsigned char 。 +>*stdout:标准输出 我们后面会单独讲* +>意思就是:向标准输出写入一个字符 +~~等价于 putc(ch, stdout) 。~~ +>**参数**: `ch` 要被写入的字符串 +>**返回值**: +>成功时返回写入的字符。 +失败时返回 EOF ~~并设置 stdout 上的错误指示器~~ +*EOF(end of file)是一个宏,值为 -1* + +第一次看到这个函数的 返回类型 和 参数类型 我其实很懵: +嗯? +我输入的不是 char 类型的吗? 怎么参数类型是 int ? +我看到的不是 char 类型的 `A` 吗?怎么返回类型是 int? + +![](https://img-blog.csdnimg.cn/20200206220023221.jpg) +其实,输出是什么不代表返回就是什么。scanf还返回整数呢,照样可以输出汉字。 + +下面的程序帮助大家理解: ```c -int f(void); - -int gAll = 12; - -int main(void){ - - //__func__ 可以打印出当前函数的函数名,下划线一边是两个 - printf("in %s gAll = %d\n", __func__, gAll); - //全局变量可以直接使用,不需要再声明 - f(); - printf("again in %s gAll = %d\n", __func__, gAll); - //函数内对全局变量值的改变在 main函数中依然存在 - return 0; -} - -int f(void) { - - printf("in %s gAll = %d\n", __func__, gAll); - gAll += 2; - printf("again in %s gAll = %d\n", __func__, gAll); - - return gAll; -} -``` -输出: -```c -in main gAll = 12 -in f gAll = 12 -again in f gAll = 14 -again in main gAll = 14 -``` -###### 全局变量的初始化 -- 没有初始化的全局变量**默认值为 0** - - 指针默认为 **NULL** -- 只能用**编译时刻已知**[^1]的值来初始化全局变量 -- 全局变量的初始化发生在main函数之前 - -注释1: -```c -int gAll = 12; -int g = gAll;//报错 - int main(void) { - return 0; -} -``` -下面这段代码在某些编译器(dev c++)上是可以编译的,但是在 vs 上是不能编译的 -```c -const int gAll = 12; -int g = gAll; + char ch = 'A'; -int main(void) { - - return 0; -} -``` -**但是,这种方式是不推荐的** - -###### 被隐藏的全局变量 -- 如果函数内部存在与全局变量同名的变量,则全局变量被隐藏。 - -```c -int f(void); - -int gAll = 12; - -int main(void) { - - printf("in %s gAll = %d\n", __func__, gAll); - - f(); - - printf("again in %s gAll = %d\n", __func__, gAll); - - return 0; -} - -int f(void) { - - int gAll = 2;//仅在这个范围内适用 - printf("in %s gAll = %d\n", __func__, gAll); - gAll += 2; - printf("again in %s gAll = %d\n", __func__, gAll); - - return gAll; -} -``` -输出: -```c -in main gAll = 12 -in f gAll = 2 -again in f gAll = 4 -again in main gAll = 12 -``` -即使 gAll 在 main 函数中被覆盖,f 函数中的 gAll 也是不会被该改变的 - -为什么会这样?自己思考一下。 - -#### 静态本地变量 -- 在本地变量定义时加上 static 修饰符就成为静态本地变量 -- 当离开函数的生存期后,静态本地变量会继续存在并保持其值 -- 静态本地变量的初始化只会在第一次进入这个函数时进行,以后进入函数时会保持上次离开时的值。 - -例: -**不用static**的情况 -```c -int f(void); - -int main(void) { - - f(); - f(); - f(); - - return 0; -} - -int f(void) { - - int All = 1; - printf("in %s All = %d\n", __func__, All); - All += 2; - printf("again in %s All = %d\n", __func__, All); - - return All; -} -``` -输出: -```c -in f All = 1 -again in f All = 3 -in f All = 1 -again in f All = 3 -in f All = 1 -again in f All = 3 -``` - -**使用static**: -```c -int f(void); - -int main(void) { - - f(); - f(); - f(); - - return 0; -} - -int f(void) { - - static int All = 1;//只添加 static - printf("in %s All = %d\n", __func__, All); - All += 2; - printf("again in %s All = %d\n", __func__, All); - - return All; -} -``` -输出: -```c -in f All = 1 -again in f All = 3 -in f All = 3 -again in f All = 5 -in f All = 5 -again in f All = 7 -``` -**看看地址** -```c -int f(void); - -int gAll = 12; - -int main(void) { - - printf("1 st\n"); - f(); - printf("2 nd\n"); - f(); - - return 0; -} - -int f(void) { - int a = 0; - int b = 0; - static int All = 1; - printf("&All : %p\n", &All); - printf("&gAll: %p\n", &gAll); - printf("&a : %p\n", &a); - printf("&b : %p\n", &b); + a = putchar(ch); + + printf("\n%d", a); + + return 0; - return All; } ``` 输出: ```c -1 st -&All : 00007FF6A9ECC054 -&gAll: 00007FF6A9ECC050 -&a : 000000E8815CF8B4 -&b : 000000E8815CF8D4 -2 nd -&All : 00007FF6A9ECC054 -&gAll: 00007FF6A9ECC050 -&a : 000000E8815CF8B4 -&b : 000000E8815CF8D4 +A +65 ``` -全局变量 gAll 与 静态局部变量 All 在内存中相邻 -**总结** -- 静态本地变量实际上是特殊的全局变量 -- 它们位于相同的内存区域 -- 静态本地变量具有全局的生存期,函数内的局部作用域 - -#### 返回指针的函数 -请同学们先看一下下面这个程序: +上面我说慢慢来的时候也许有同学不屑:“这还用慢慢来?早会了!” +那好吧,`putchar` 的上面的定义中说它等价于 `putc` +要不我们再来看看 putc 是怎么定义的?与 putc 类似的还有个 `fputc` +要想真正理解它们还得看看 `ferror`,一个个来呗? +![](https://img-blog.csdnimg.cn/202002062210064.jpg) +A watched pot never boils —— 心急吃不了热豆腐 +#### getchar +>`int getchar(void)` +>**头文件**:stdio.h +>**定义**: +>从 stdin 读取下一个字符。 +~~等价于 getc(stdin) 。~~ +也就是 从标准输入读入一个字符 +**参数**:无 +**返回值**: +成功时为获得的字符 +失败时为 EOF 。 +#### getchar的返回值有什么用? +如何退出下面程序中的 while循环? +可以自己打出来先测试一下。 ```c -int* f(void); -void g(void); - int main(void) { - int* p = f(); - printf("*p = %d\n", *p); - g(); - printf("*p = %d\n", *p); - - return 0; -} - -int* f(void) { - - int i = 12; - - return &i; -} -void g(void) { - - int k = 24; + int ch; - printf("k = %d\n", k); + while ((ch = getchar()) != EOF) { + putchar(ch); + } - return k; + printf("EOF\n"); + //退出循环的方式可能有两种: + //1.程序被关闭。EOF不会输出 + //2.退出了循环,程序继续向下运行。EOF会被输出 + + return 0; +} +``` +后面还会继续详细讲解 这部分知识。可以自行思考一下,也可以查阅资料看看。 +我做了一个便于理解的图示,如果现在就想看,在公众号回复[0206]查看。 + +*为了减少冗余,下面的程序我只写 main 函数部分, +但是在你写程序到时候你要记得引用头文件 string.h* + +### (二)strlen & strnlen_s +帮你理解: +strlen: string lenth +#### strlen +>`size_t strlen( const char *str )` +>**头文件**:`string.h` +>**参数**:str - 指向要检测的空终止字符串的指针 +>**返回值**: 空终止字节字符串 str 的长度。 +>*** +>**定义**:返回给定空终止字符串的长度,即首元素为 str 所指,且不包含首个空字符的字符数组中的字符数。 + 若 str 不是指向空终止字节字符串的指针则行为未定义。 + +*什么是 空终止字节字符串? +空终止字节字符串( NTBS )是尾随零值字节(空终止字符)的非零字节序列。字节字符串中的每个字节都是一些字符集的编码。例如,字符数组 {'\x63','\x61','\x74','\0'} 是一个以 ASCII 编码表示字符串 "cat" 的 NTBS 。* + +#### strnlen_s +>`size_t strnlen_s( const char *str, size_t strsz )` +>**头文件**:`string.h` +>**参数**: +>str - 指向要检测的空终止字符串的指针 +strsz - 要检测的最大字符数量 +>**返回值**: +>成功时为空终止字节字符串 str 的长度,若 str 是空指针则为零,若找不到空字符则为 strsz 。 +>*** +>**定义**: +>除了若 str 为空指针则返回零,而若在 str 的首 strsz 个字节找不到空字符则返回 strsz 。 +> 若 str 指向缺少空字符的字符数组且该字符数组的大小 < strsz 则行为未定义;~~换言之, strsz 的错误值不会暴露行将来临的缓冲区溢出。~~ + +#### strlen 与 strnlen_s 的区别与用法 +**1.空指针** +```c +int main() { + + char* str = NULL; + + //str为空指针,行为未定义。程序会崩溃 + printf("%d\n", strlen(str)); + + //str为空指针,返回 0 + printf("%d\n", strnlen_s(str, 1)); + + return 0; +} +``` +**2.没有终止符的字符串数组当作函数参数** +```c +int main() { + + char str[] = { 'H', 'E', 'L', 'L', 'O', }; + + printf("%d\n", strlen(str)); + + printf("%d\n", strnlen_s(str, (size_t)sizeof(str))); + + return 0; +} +``` +猜一猜会输出什么? +```c +19 +5 +``` +**当我们不清楚字符串中有没有 '\0' 时,我们要小心使用 strlen** +strlen 只有遇到 '\0' 才会停止,这造成的潜在的数组越界风险。 + +**3. 当 strsz > str的大小 时** +1)若 str 有终止符 +```c +int main() { + + char str[] = { 'H', 'I', '!', 0 }; + + printf("%d\n", strnlen_s(str, 5)); + + return 0; +} +``` +```c +3 +``` +2) 若 str 无终止符, 行为未定义 +```c +int main() { + + char str[] = { 'H', 'I', '!'}; + + printf("%d\n", strnlen_s(str, 5)); + + return 0; +} +``` +```c +5 +``` +最后,对于 `strnlen_s `来说**如果 strsz < str数组大小**,直接返回 strsz + +#### strlen 详解 +###### const的作用 +`size_t strlen( const char *str )` + +const 的作用是什么? +简单来说,如果你不希望这个函数改变你传入的数组,const 具有保护作用,使得 strlen 函数内部无法改变 str 数组每个元素的值。 + +const详解可以参考这篇文章: +[点击查看](https://mp.weixin.qq.com/s/Fc-sAgpXmJ1eVKufZCvN8A) + +#### mystrlen +mystrlen 的写法有很多,如果你的编译器是 VS,你甚至可以直接看编译器是如何去写的。 +一下提供一种比较简洁的写法供大家参考: + +不难(你细品 + +```c +int mystrlen(const char* str) { + + char* end = str; + + while ( *end++ ); + + //退出while循环时,多加了一次 1 + return (end - start - 1); + +} + +int main() { + + char* str = "Hello World!"; + + printf("%d\n", mystrlen(str)); + + return 0; +} +``` +![](https://img-blog.csdnimg.cn/2020020814321837.jpg) +### (三) strcmp & strncmp +如何记忆? +strcmp:string compare +lhs:left-hand side +rhs:right-hand side +#### strcmp +>`int strcmp( const char *lhs, const char *rhs )` +>**头文件**:`string.h` +>**参数**: +lhs, rhs - 指向要比较的空终止字节字符串的指针 +>**返回值**: +>若字典序中 lhs 先出现于 rhs 则为负值。 +若 lhs 与 rhs 比较相等则为零。 +若字典序中 lhs 后出现于 rhs 则为正值。 +>*** +>*什么是字典序?* +>*简单理解就是在字母表中出现的顺序。* +>*记法小窍门:* +>*lhs ASCII码值大 就为正 否则为负* +>*解释:ASCII值大在字典序中肯定靠后,是后出现的* +>*** +>**定义**: +>以字典序比较二个空终止字节字符串。 +结果的符号是被比较的字符串中首对不同字符(都转译成 unsigned char )的值间的差的符号。 +若 lhs 或 rhs 不是指向空终止字节字符串的指针,则行为未定义。 + +### strncmp +>`int strncmp( const char *lhs, const char *rhs, size_t count )` +>**头文件**:`string.h` +>**参数**: +>lhs, rhs - 指向要比较的可能空终止的数组的指针 +count - 要比较的最大字符数 +>**返回值**: +>若字典序中 lhs 先出现于 rhs 则为负值。 +若 lhs 与 rhs 比较相等,或若 count 为零,则为零。 +若字典序中 lhs 后出现于 rhs 则为正值。 +>*** +>**定义**: +>比较二个可能空终止的数组的至多 count 个字符。按字典序进行比较。不比较后随空字符的字符。 +结果的符号是被比较的数组中首对字符(都转译成 unsigned char )的值间的差的符号。 +若出现越过 lhs 或 rhs 结尾的访问,则行为未定义。若 lhs 或 rhs 为空指针,则行为未定义。 +#### strcmp 与 strncmp 比较 +**1. lhs 或 rhs 为非空终止字符字符串** +```c +int main() { + + char str2[3] = { 'J', 'I', 'M' }; + //'M' 后的字符是不可预测的。但是肯定都比0大,所以输出 0 或 1 + //你可以试试将 str1 也改为 str2 的数组形式, 试试结果会不会为 -1 + char str1[3] = "JIM"; + + printf("%d\n", strcmp(str1, str2)); + + + return 0; +} +``` +**2. count 的作用** +```c +int main() { + + char* str1 = "Helloa"; + char* str2 = "HelloA"; + + printf("%d\n", strcmp(str1, str2)); + //输出 1, str1 > str2 + + printf("%d\n", strncmp(str1, str2, 5)); + //输出 0, str1 = str2 + return 0; +} +``` +**3. "Hello" 与 "Hello " 的区别?** +字符串 "Hello" 是小于字符串 "Hello " 的。(用strcmp函数检测) +因为最后一次字符比较是 '\0' 与 ' '比较, +'\0' ASCII码值为 0, ' ' ASCII码值为 32 +如图: +![](https://img-blog.csdnimg.cn/20200208155506396.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) +```c +int main() { + + char* str1 = "Hello"; + char* str2 = "Hello "; + + printf("%d\n", strcmp(str1, str2)); + printf("%d\n", strncmp(str1, str2, 10)); + + return 0; } ``` 输出: ```c -*p = 12 - k = 24 -*p = 24 +-1 +-1 ``` -**i 和 k 的内存其实是同一块空间** -**总结** +#### mystrcmp +先想后做,事半功倍: +1. 按字符比较,都相等返回0; + 出现不相等,返回 *lhs - *rhs 的差值 +2. *lhs 或 *rhs 遇到 '\0' 退出循环返回 差值 -- 返回 **本地变量** 的地址是危险的 -- 返回 **全局变量** 或 **静态局部变量** 的地址是安全的 -- 返回函数内 malloc 的内存是安全的,但是容易造成问题 -- 最好的做法是**返回传入的指针** - -说了这么多,总结一句话 -###### 尽量避免使用 全局变量 和 静态本地变量 -![???](https://img-blog.csdnimg.cn/20200211000341388.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) -为什么这里就不深讲了,有兴趣的朋友可以下来自己查查。 - -*** -### 编译预处理 与 宏 - -#### 编译预处理指令 -- `#` 开头的是编译预处理指令 -- 它们不是 C语言的一部分,但是 C语言离不开他们 -- `#define` 用来定义一个宏 - -#### define 关键字 -回想我们刚学 double 的时候,是不是计算过圆的面积。当时我们可能是这样写的: ```c -#include +int mystrcmp(char* str1, char* str2) { + + while (1) { + if (*str1 != *str2) + break; + else if (*str1 == 0) + break; + else + ++str1, ++str2; + } -const double PI = 3.14159; + return (*str1 - *str2); +} -int main(void) { +int main() { + + char* str1 = "Hello"; + char* str2 = "Hello"; + + printf("%d\n", mystrcmp(str1, str2)); - printf("%f\n", 2 * PI * 3.0); return 0; } ``` -现在我们用 宏 就不需要用 const 修饰的全局变量了,我们也说过,全局变量最好不用。 +上面的 mystrcmp 看着很笨,当然是可以改进的。 +自己思考一下。 +答案放在了我的GitHub上。[点击查看](https://github.com/hairrrrr/C-CrashCourse/tree/master/C%20Crash%20Course/13%20String/Prelesson/code/mystrcmp) +对你有帮助,麻烦给我点个小星星哦,方便下次查看。 +如果你有更好的解法,欢迎 pull request ! + +![胜利在望!](https://img-blog.csdnimg.cn/20200208174239803.jpg) +### (四) strcpy & strncpy +帮助理解: +strcpy:string copy +dest:destination +src:source +#### strcpy +>`char *strcpy( char *dest, const char *src )` +>**头文件**:`string.h` +>**参数**: +>dest - 指向要写入的字符数组的指针 +src - 指向要复制的空终止字节字符串的指针 +>**返回值**: +>返回 dest 的副本 +>*** +>**定义**: +>复制 src 所指向的空终止字节字符串,包含空终止符,到首元素为 dest 所指的字符数组。 + 若 dest 数组长度不足则行为未定义。 + 若字符串覆盖则行为未定义。 + 若 dest 不是指向字符数组的指针或 src 不是指向空终止字节字符串的指针则行为未定义。 + +#### strncpy +>`char *strncpy( char *dest, const char *src, size_t count )` +>**头文件**:`string.h` +>**参数**: +>dest - 指向要复制到的字符数组的指针 +src - 指向复制来源的字符数组的指针 +count - 要复制的最大字符数 +>**返回值**: +> 返回 dest 的副本 +> +>*** +>**定义**: +>复制 src 所指向的字符数组的至多 count 个字符(包含空终止字符,但不包含后随空字符的任何字符)到 dest 所指向的字符数组。 +**若在完全复制整个 src 数组前抵达 count ,则结果的字符数组不是空终止的。** + 若在复制来自 src 的空终止字符后未抵达 count ,则写入额外的空字符到 dest ,直至写入总共 count 个字符。 + 若字符数组重叠, + 若 dest 或 src 不是指向字符数组的指针(包含若 dest 或 src 为空指针), + 若 dest 所指向的数组大小小于 count , + 或若 src 所指向的数组大小小于 count 且它不含空字符, + 则行为未定义。 + +#### strcpy 与 strncpy 的未定义行为 +**1. dest 和 src 一定不能是空终止字节字符串, 且要指向字符串** +**2. dest 与 src 覆盖** +从 C99起 strcpy函数原型变成了这样: +`char *strcpy( char *restrict dest, const char *restrict src )` +`restrict` 表示两个字符串是不重叠的 +重叠并不是重复一样的意思。这一点我们目前不去深入。 + +**3. dest 长度小于 src** ```c -#include +int main() { -#define PI 3.14159 -//注意:不写分号 不写等于号 + char* str1 = "Hello"; + char* str2 = "Hello World"; -int main(void) { + strcpy(str1, str2); + strncpy(str1, str2, 12); + + puts(str1); - printf("%f\n", 2 * PI * 3.0); return 0; } ``` -现在,我们打开我们的虚拟机,进入 Linux 系统。 -![1.创建一个 c文件](https://img-blog.csdnimg.cn/20200211003555492.png) +这样写是可以通过编译的,但是你要知道这样做实际上已经越界了。 -![2.写一个简单的带宏的 c程序](https://img-blog.csdnimg.cn/20200211005048362.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) +如果用数组的形式定义字符串,编译器才会报错。 +可以看出,在这种情况下。编译器对数组更为敏感,数组的写法也更加安全。 +```c +int main() { -![3.这时后我们成功创建了一个 c文件](https://img-blog.csdnimg.cn/20200211003635685.png) + char str1[] = "hello"; + char str2[] = "hello world"; + + //两个函数都会报错 + strcpy(str1, str2); + //strncpy(str1, str2, 12); -![4.编译c文件,并保留中间文件](https://img-blog.csdnimg.cn/20200211003754529.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) -现在多出来了 4 个文件,蓝色的是文件夹,我们不去管它,绿色的是可执行文件,类似 windows 的 .exe 文件 -现在我们主要关注这 3 个中间文件 + puts(str1); -![文件详细情况](https://img-blog.csdnimg.cn/20200211003955771.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) -一个 c文件编译的过程文件变化是这样的: ->`.c `(处理编译预处理指令)-> `.i `(产生汇编代码)-> `.s`(汇编生成目标文件) -> `.o`(链接等) -> `a.out ` + return 0; +} +``` +**4. strncpy:dest 大小小于 count** +这点其实 3 也说明了。 +对于 strcpy 来说, dest 的大小不能小于 src +而 strncpy 只需要 dest 的大小不小于 count 即可 + +**5. src 大小小于 count 且 src 不含空字符** +```c +int main() { + + char str1[] = "Hello World"; + char str2[] = { 'H', 'e', 'l','l', 'o' }; + + strncpy(str1, str2, 10); + + puts(str1); + + return 0; +} +``` +输出: +```c +Hello烫烫蘢 +``` +其实这也不难理解,strncpy 需要 '\0' 来判断 src 是否写完。 +如果有 src 结尾有'\0' ,这时如果 count 还没有写满 +函数会向 dest 中写入 '\0' 直到写满 count +但是如果 src 没有 '\0' 那么函数不知道 src 已经结束,而继续写入后面的内容,结果可想而知,是不可预测的。 + +#### mystrcpy +```c +#include + +char* mystrcpy(char* str1, char* str2) { + + assert(str1 != NULL && str2 != NULL); + assert(strlen(str1) >= strlen(str2)); + + //核心代码从这里开始, 上面的不懂可以加QQ群问我(群在我公众号关注回复的消息里) + char* after = str1; + + while (*str1++ = *str2++); + + return after; +} + +int main() { + + char* str1 = "HI!!!"; + char* str2 = "Hello"; + + mystrcpy(str1, str2); + puts(str1); + + return 0; +} +``` +### (五)strcat & strncat +帮你理解: +strcat:string catenate 字符串连接 + +#### strcat +>`char *strcat( char *dest, const char *src )` +>**头文件**:`string.h` +>**参数**: +>dest - 指向要后附到的空终止字节字符串的指针 +src - 指向作为复制来源的空终止字节字符串的指针 +>**返回值**: +>返回 dest 的副本 +>*** +>**定义**: +>后附 src 所指向的空终止字节字符串的副本到 dest 所指向的空终止字节字符串的结尾。字符 src[0] 替换 dest 末尾的空终止符。产生的字节字符串是空终止的。 + 若目标数组对于 src 和 dest 的内容以及空终止符不够大,则行为未定义。 + 若字符串重叠,则行为未定义。 + 若 dest 或 src 不是指向空终止字节字符串的指针,则行为未定义。 + +#### strncat +>`char *strncat( char *dest, const char *src, size_t count )` +>**头文件**:`string.h` +>**参数**: +>dest - 指向要后附到的空终止字节字符串的指针 +src - 指向作为复制来源的字符数组的指针 +count - 要复制的最大字符数 +>**反回值**: +>返回 dest 的副本 +>*** +>**定义**: +>后附来自 src 所指向的字符数组的至多 count 个字符,到 dest 所指向的空终止字节字符串的末尾,若找到空字符则停止。字符 src[0] 替换位于 dest 末尾的空终止符。始终后附终止空字符到末尾(故函数可写入的最大字节数是 count+1 )。 + 若目标数组没有对于 dest 和 src 的首 count 个字符加上终止空字符的足够空间,则行为未定义。 + 若源与目标对象重叠,则行为未定义。 + 若 dest 不是指向空终止字节字符串的指针,或 src 不是指向字符数组的指针,则行为未定义。 > -可以看到 .i 文件时很大 -![我们看看 .i 文件的 结尾部分](https://img-blog.csdnimg.cn/20200211005244118.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) -![对比 .c 文件](https://img-blog.csdnimg.cn/20200211005330453.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) -**我们发现程序中的宏 PI 被换成了它所表示的 数字** - -这种替换是**简单的文本替换**,我们再试试其他的替换方式: - -![替换字符串](https://img-blog.csdnimg.cn/20200211010337202.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) -![FRORMAT 同样被替换了](https://img-blog.csdnimg.cn/20200211010437762.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) -我们再试试这样,定义宏的时候 不带双引号: -![编译器给了 warning](https://img-blog.csdnimg.cn/20200211010618834.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) -![FORMAT并没有被替换](https://img-blog.csdnimg.cn/20200211010833810.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) -因此可知,**被 `" "`扩起来的字符串 宏 是不会替换的** - -###### 总结 -- 格式: `#define <名字> <值>` -- 注意结尾没有分号,因为不是 C 的语句 -- 名字必须是一个单词,值可以是任何(注意字符串替换定义时需要带引号) -- 在 **C语言的编译器开始编译之前**,编译预处理程序(cpp)会把程序中的宏的名字替换为值 -- linux/unix - - 编译并保留中间文件指令:`gcc --save-temps` - - 查看文件结尾:`tail` - -###### 宏 -- 如果在一个宏的值中有其他宏的名字,这些宏也是会被替换的 -- 如果一个宏的值超过一行,最后一行之前的行末需要加 \ -- 宏的值后面出现的注释不会被当作宏的值的一部分 - -###### 没有值的宏 -- `#define _DEBUG` -- ` #define _CRT_SECURE_NO_WARNINGS` 用 VS 的应该都知道这个吧,加上这个你就可以直接用`scanf`而不是`scanf_s`了 ->这类宏是用来做条件编译的,后面有其他编译预处理指令来检查这个宏是否已经被定义过了。 ->比如有这个宏执行这部分代码,没有则执行另外一部分 - -###### 预定义的宏 -- `__LINE__` -- `__FILE__` -- `__DATE__` -- `__TIME__` -- `__STDC__` - -我们来试着用一下: +#### strcat 与 strncat 用法解读 +**1. strcat:dest >= dest + src + '\0'** ```c -int main(void) { +int main() { - printf("%s : %d\n", __FILE__, __LINE__); - printf("%s %s\n", __DATE__, __TIME__); - - return 0; -} -``` -输出: -```c -D:\vscode\练习\12-31\Project1\oj.c : 174 -Feb 11 2020 04:12:57 -``` -值得注意的是,`__LINE__`表示的是它自己所在的行数 - -你们在熟睡,而我还在给你们写教学,关注我/点个赞/转发 不过分吧~ - -![](https://img-blog.csdnimg.cn/2020021104154880.gif) -#### 带参数的宏 -- `#define cube(x) ( (x) * (x) * (x) )` - -例如: -```c -#define cube(x) ((x) * (x) * (x)) - -int main(void) { - - printf("%d\n", cube(5)); - return 0; -} -``` -输出: -```c -125 -``` -**容易犯的错误** - -一下这两种写法在程序中会不会有问题? -- `#define ERROR(1x) (x * 57)` -- `#define ERROR2(x) (x) * 57` - -思考一下这个程序会的到你想要的结果吗? -```c -#define ERROR1(x) (x * 57) -#define ERROR2(x) (x) * 57 - -int main(void) { + char str1[11] = "Hello"; + char* str2 = " World"; - printf("%d\n", ERROR(1 + 2)); - printf("%d\n", 300 / ERROR(1)); + //11刚好可以放下"Hello World",但是因为没有'\0' 的位置,程序崩溃。 + strcat(str1, str2); + puts(str1); return 0; } ``` -输出: +**2. strcpy:dest 或 src 不是 空终止字节字符串** ```c -115 -17100 -``` -为什么会这样呢?我们不妨来看一下,`.i`文件内部: +int main() { -![注意运算顺序](https://img-blog.csdnimg.cn/20200211043539513.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) -**定义带参数的宏的原则** -- 一切都要有括号 - - 整个值有括号 - - 每个参数都有括号 - -所以,上面错误的例子的正确的写法就是: -`#define ERROR ( (x) * 57 )` - -**带参数的宏的更多用法**: -- `#define MIN(a, b) ((a) > (b) ? (b) : (a))` - -**定义宏切记不要加分号** - -错误示范: -```c -#define PRETTY_PRINT(msg) printf(msg); - -int main(void) { - - int n = 0; - - printf("Input an number\n"); - scanf("%d", &n); - - if (n < 10) - PRETTY_PRINT("less than 10\n"); - else - PRETTY_PRINT("more than 10\n"); + char str1[11] = "Hello"; + char str2[3] = {'J', 'I', 'M'};//程序崩溃 + + strcat(str1, str2); + puts(str1); return 0; } ``` -VS 会报错 :没有匹配 if 的非法 else,为什么呢? - -因为如果你在宏后面加了 `;`,你又在 if 内的语句后加了`;` -这样在`.i`的阶段,if 后的语句有了两个 `;`,即: -`PRETTY_PRINT("less than 10\n");;` -第二个`;`表示 一个空语句,这样 else 前面就没有对象可以匹配了 - -###### 总结 -- **#开头的预处理指令并不是 C语言独有的内容** -- **宏的参数时没有类型的** -- 大型程序中宏的使用很常见 -- 宏可以很复杂,可以产生函数 - - 使用运算符 `#` 和 `##` -- 部分宏会被 `inline`函数取代 -- 中西方差异(国人少用) - -*** -Quiz: -请看下面的代码片段,判断这段程序会输出什么? +**3. strncpy:dest 不是空终止字节字符串?** ```c +int main() { -#define TOUPPER(c) ('a' <= (c) && (c) <= 'z' ? (c) - 'a' + 'A' : (c)) + char str1[11] = {'H', 'I'}; + char str2[3] = { 'J', 'I', 'M' }; - int i = 0; - char s[1000]; - - strcpy(s, "abcd"); - - putchar(TOUPPER(s[++i])); + strncat(str1, str2, 3); + puts(str1); + return 0; +} ``` -A: B -B: C -C: D -D: E - -*这道题是需要都脑子的呦!* -公众号后台回复:**0211 1** 查看答案和解析 -*** -### 大程序结构 -#### 多个源代码文件 -###### 多个源文件`.c` -**引入** -回想我们学习的过程,开始是 main()里的代码太长了,我们学习了函数,将其分开 -现在如果 一个源文件太长了,我们就可以将其分成几个源文件 - -**怎么让多个源文件联系起来?** -在编译器上创建一个项目,将你想操作的 .c 文件放到同一个项目中 - -#### 头文件 `.h` -###### **`" " ` 还是 `< >` ?** -- `#include`有两种形式来指出要插入的文件 - - `" "`要求编译器首先在当前目录(.c 文件所在目录)寻找这个文件;如果没有,再去编译器指定的目录寻找。**自己的头文件用** - - `< >`让编译器只在指定位置寻找 **。系统的头文件用** -- 编译器知道自己的标准库的头文件在哪里 -- 环境变量 和 编译器命令行参数也可以指定寻找头文件的目录 - -###### **`#include`的误区** -- `#include`不是用来引入库的 -- `stdio.h ` 中只有函数的声明,函数的定义在其他的地方 -- C语言编译器默认会引入所有标准库 -- `#include`的作用其实就是将 这个头文件的所有内容 插入到这个文件中来。目的是让编译器知道你使用的函数时所给的参数是否正确。(类似函数的声明) - -**为什么不引用 `stdlib.h` 依然可以使用 `malloc ` ?** -这时因为在你调用函数前没有声明函数(引入头文件),编译器回去猜测 参数 和 函数返回类型都为 `int`型 -恰好 `malloc` 的参数 `size_t` 是 `long int` ,返回值是个指针,也可以看作是 16进制的 整型。 - -*[为什么?可以参考我的另一篇文章,点击跳转](https://mp.weixin.qq.com/s/JEalmGOwNXp9IM0W7B7YJw -)* - -###### 头文件 -- 使用和定义函数的地方都应该包含这个头文件 -- 将 函数声明 全局变量 放入 `.h`文件 - -###### 不对外公开的 函数&变量 -函数&全局变量前加上 `static`就使得这个 函数/变量 只能在当前文件中被使用 - -### 声明 -###### extern -当一个c 文件想调用另一个 c文件中定义的全局变量时 -需要在头文件中加上 `extern <类型> <变量名>` 来声明这个变量 - -例如: +正确地输出了: ```c -<1.c> -int gAll = 12; - -<2.c> -printf("%d\n", gAll); - -<1.h> -extern int gAll; +HIJIM ``` -**声明不产生代码** +可以看出 strncpy 具有某种优化,即使 dest 没有空字符,它也能正确找到正确的后缀位置 +即使 src 没有空字符,也可以在新的 dest 后加上空字符 -###### 避免重复声明 -请看下例: + +#### mystrcat ```c -<1.h> -int a ; +char* mystrcat(char* str1, char* str2) { + //这一步请细品 + while (str1[strlen(str1)] = *str2++); -<2.h> -#include"1.h" + return str1; +} -<3.h> -#include"1.h" -//相当于 -//int a +int main() { -#include"2.h" -//相当于 -//#include"1.h" -//相当于 -//int a ; + char str1[8] = "Hi"; + char str2[4] = "YOU"; -//可以看到, int a 被重定义的 -``` -如何避免上述这种重定义情况? - -###### 条件编译和宏 -- 运用条件编译和宏,保证这个头文件在一个编译单元中只会被 include 一次 -- `#pragma once`也能起到相同作用,但不是所有的编译器都支持 -```c -#ifdef _MAIN_H_//先看 这个宏是否定义过,是:继续 不是:跳过这个结构。跳过的意思是编译时将不再向 .i 文件中插入这段代码 -#define _MAIN_H_//没有定义,则定义 - -#endif -``` -这就是我们前面说的预定义的宏的一种使用方法。 -应用这种方法我们再看上例 - -```c -<1.h> -#ifdef _FIRST_H_ -#define _FIRST_H_ - -int a ; - -#endif -<2.h> -#include"1.h" - -<3.h> -#include"1.h" -//相当于: -//#ifdef _FIRST_H -//#define _FIRST_H -//int a ; -//#endif -#include"2.h" -//相当于: -//#include"1.h" -//这时,_FIRST_H 已经被定义,则跳过 + mystrcat(str1, str2); + puts(str1); + return 0; +} ``` *** -以上就是本次的内容,欢迎各位指出我的错误,谢谢! -这篇教程的Github地址: +### 写在后面 +![](https://img-blog.csdnimg.cn/20200208222045318.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) +说了半天,其实就是想带大家过一遍我们常用的字符串函数。 +里面肯定有你忽略的知识点,希望大家**一定不要放过一些细节**,**一定要多想**,**一定要多写代码**。 +重要的事说三遍。 +再有就是,不要嫌麻烦。 +毕竟你看完可能只要 10 分钟,我写要写好几个小时。 -https://github.com/hairrrrr/C-CrashCourse +其实这也只是入门的基础概念,真的要学会还是要拿来用。 +具体使用有机会再总结吧。 -Github 大概在 微信公众号更新 1 ~ 2 天后更新,欢迎加入我,让我们一起完成全世界最全的 C 语言教学! +*** +**更多精彩内容:** -关注我的微信公众号 -获取第一时间更新 👇👇👇 +[指针入门](https://mp.weixin.qq.com/s/0DD10hQQ4411ycbKSpghTw) +[你不知道的素数判断方法](https://mp.weixin.qq.com/s/T-ovU-PIunKFrgH1ZVLOLw) + +*** + +如果你找到了文章的错误,请一定留言/评论/QQ群 告诉我,如果你给的建议好,我会在文中加上你的内容并且加上你的ID。 + +你也可以在 GitHub 上提交请求,这样也会让你上榜留名哦! +Github会在 **公众号上发布 1~2天后**发布。 +**喜欢就点个小星星吧**,下次方便查看 + +关注我的公众号,获取第一时间更新: +![扫码关注我!!](https://img-blog.csdnimg.cn/20200211141933317.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQ0OTU0MDEw,size_16,color_FFFFFF,t_70) +以上,感谢观看! + + +参考资料:cppreference.com