
前言
承接PartⅡ,本篇讲解了字符串、存储类型、链接和内存管理以及简单介绍了位操作的相关内容。
字符串
除了使用char类型数组(以空字符\0结尾),由于字符串十分常用,C语言提供了许多专门用于处理字符串的函数。
6.1 字符串字面量(字符串常量)
用双引号括起来的内容称为字符串字面量了,也叫字符串常量。双引号中的字符和编译器自动加入末尾的\0字符,都作为字符串储存在内存中。
从ANSI C标准后,字符串字面量之间没有间隔,或用空白字符分割,C会将其视为串联起来字符串常量。
char greeting[50] = "Hello, and" "how are";
等价于:char greeting[50] = "Hello, and how are";
倘如想在字符串中使用双引号,则必须在双引号前面加一个反斜杠””
printf("\"Run, Spot, run!\" exclaimed Dick.\n");
“Run, Spot, run!” exclaimed Dick.
6.2 gets()函数与它的替代品
gets()函数是非常直观好用的函数,读取整行的输入,直至遇到换行符。经常和puts()配对使用。例如
1 |
|
即便这是gets()的典型用法,但往往上面这段代码会使得编译器出现警告,原因是gets()函数无法检查数组是否装的下输入行。gets()函数仅仅知道数组的开始处,但是无从定位结束的元素是哪个。
如果输入的字符过长,就会导致缓冲区溢出,多余的字符超过指定的目标空间。倘若这些字符修改了程序中的其他数据,就会导致程序出错!
因此早在C11标准中,委员会直接废除了gets()函数,尽管目前大部分编译器依旧支持。
替代品
C11标准新增的gets_s()函数可代替gets()函数。该函数与gets()非常接近,几乎可以替换现有代码中的所有gets()。
fgets()与fputs()函数
char *fgets(char *str, int num, FILE *stream);
fgets()函数通过增加两个参数限制读入字符来解决溢出的问题。其与gets()的区别如下:
- 安全性问题
gets()函数存在缓冲区溢出问题,如果读取的字符串长度超出了缓冲区长度,会导致缓冲区溢出。这样就会导致严重的安全问题,例如可以利用缓冲区溢出来执行恶意代码。而fgets()函数会自动检查读取的字符串长度是否超出缓冲区长度,避免了这种安全问题。
- 参数不同
gets()函数只有一个参数,即指向读取字符串的字符数组的指针。而fgets()函数有三个参数,分别是指向读取字符串的字符数组的指针、最大读取字符数和文件指针。
常用的stdin(标准输入)作为第三个参数,表示从键盘中读入的数据。
- 特殊字符处理
fgets()函数会读取换行符(\n)并把它保存在读取的字符串中,而gets()函数不会读取换行符。
同时,fputs()也有了多个参数,其第二个参数指明它要写入的文件,如果要显示在计算机显示器上,应使用stdout(标准输出)作为该参数。
int fputs(const char *str, FILE *stream);
gets_s()与puts_s()
gets_s()和puts_s()是C11标准新增的安全函数,主要用于输入输出字符串,其参数如下:
- gets_s()函数:
char * gets_s(char * str, rsize_t n);
str:指向目标字符数组的指针,用于存储输入的字符串。
n:目标字符数组的大小。
- puts_s()函数:
int puts_s(const char * str);
- str:指向要输出的字符串的指针。
在这两个函数中,都使用了第二个参数来避免缓冲区溢出,这样可以提高程序的安全性。在使用gets_s()函数时,我们需要注意以下几点:
- 目标字符数组的大小必须足够存储输入的字符串,否则可能会导致缓冲区溢出。
- 使用gets_s()函数输入的字符串结尾会自动添加空字符’\0’,所以在定义目标字符数组时不必再额外分配空间存储空字符。
- 在使用gets_s()函数时,应该检查函数返回值是否为NULL,如果是则表示输入失败或已到达文件结尾。
在使用puts_s()函数时,我们需要注意以下几点:
- 输出的字符串中不应该包含任何控制字符(比如换行符等),否则可能导致意外的输出结果。
- 如果输出成功,则返回值为非负数,否则返回EOF(-1)。
- 如果在使用puts_s()函数时没有指定输出文件,则会将字符串输出到标准输出流stdout中。
scanf()函数
当使用scanf()来读取字符串,常常使用到%s。scanf()和gets()或fgets()的区别在于它们怎么确定字符串的末尾:scanf()
更像是获取单词函数,而不是获取字符串函数;如果预留的存储空间装的下输入行,gets()和fegets()会读取第一个换行符之前的所有字符。而在不指定字段宽度(%10s)的前提下,scanf()
将读取10个字符或在10个字符之前读取的第一个空白字符停止。
printf()函数
和puts()一样,printf()
把字符串的地址作为参数。与其不同的是,printf()
不会自动在每个字符串末尾加上一个换行符。因此更具有灵活性,可以格式化不同的数据类型。
除了以上这些,还可以通过使用putchar()以及getchar()自定义输入输出函数
6.3 字符串函数
strlen()
函数原型:size_t strlen(const char *s);
函数作用:计算字符串的长度,即不包括字符串末尾的空字符\0
的字符个数。
使用示例:
1 | char str[] = "Hello World!"; |
strcpy()
函数原型:char *strcpy(char *dest, const char *src);
函数作用:将源字符串src
复制到目标字符串dest
中。
使用示例:
1 | char src[] = "Hello World!"; |
strcat()
函数原型:char *strcat(char *dest, const char *src);
函数作用:将源字符串src
拼接到目标字符串dest
的末尾。
使用示例:
1 | char str1[20] = "Hello"; |
strncat()
strcat()无法检测第一个数据是否能容纳第二个字符串。如果分配给第一个数组的空间不够大,就会造成溢出的问题。使用该函数,将第3个参数指定了最大添加字符数。如:
strncat(bugs, addmon, 13)
将addon字符串的内容附加给bugs,在加到第13个字符或遇到空字符时停止。
strcmp()
函数原型:int strcmp(const char *s1, const char *s2);
函数作用:比较两个字符串的大小。若s1
大于s2
,返回正整数;若s1
小于s2
,返回负整数;若s1
等于s2
,返回0。
使用示例:
1 | char str1[] = "Hello World!"; |
需要注意的是strcmp()函数比较的是字符串,而不是字符,所以其参数应该是字符串。
strncmp()
同样的,strcmp()比较字符串的字符,直到发现不同的字符为止,这一过程持续到字符串的末尾。而strncmp()函数比较两个字符串时,可以比较字符不同的地方,也可以只比较第3个参数指定的字符数。
strncmp(list[i], "astro", 5);
strcpy()和strncpy()函数
strcpy()的第2个参数指向的字符串被拷贝至第1个参数指向的数组中。需要注意的是:
第一,strcpy()的返回值是char *,该函数返回的是第1个参数的值,即一个字符的地址。
第二,第1个参数不必指向数组的开始。这个属性可以用于拷贝数组的一部分。
但,strncpy
是使用字符串拷贝时更慎重的选择,原因同strncat()
,strcpy()
也有同样的问题,不能检查目标空间是否能容纳源字符串的副本。同样,第3个参数可以指定可拷贝的最大字符数。
sprintf()函数
首先,这个函数是声明在<stdio.h>而非<string.h>。和printf()
类似,但是它是把数据写入字符串,而不是打印在显示器上。
sprintf()
的第1个参数是目标字符串的地址,其余参数同printf()一样,即格式字符串和待写入项的列表。
strchr()
函数原型:char *strchr(const char *s, int c);
函数作用:查找字符串s
中首次出现字符c
的位置,并返回该位置的指针。
使用示例:
1 | char str[] = "Hello World!"; |
strstr()
函数原型:char *strstr(const char *haystack, const char *needle);
函数作用:查找字符串haystack
中首次出现字符串needle
的位置,并返回该位置的指针。
使用示例:
1 | char str[] = "Hello World!"; |
以上是C语言中常用的字符串函数及其用法。除了上述函数外,还有许多其他的字符串函数,如strtok()
、sprintf()
、sscanf()
等。
6.4 常用ctypes.h字符函数
ctype.h库提供了一些用于字符处理的函数,这些函数可以检查和转换字符。下面介绍一些常用的函数:
**isalnum(int c)**:判断c是否为字母或数字。
- 返回值:若c是字母或数字,返回非零值,否则返回0。
isalpha(int c):判断c是否为字母。
- 返回值:若c是字母,返回非零值,否则返回0。
**isdigit(int c)**:判断c是否为数字。
- 返回值:若c是数字,返回非零值,否则返回0。
**islower(int c)**:判断c是否为小写字母。
- 返回值:若c是小写字母,返回非零值,否则返回0。
**isupper(int c)**:判断c是否为大写字母。
- 返回值:若c是大写字母,返回非零值,否则返回0。
**tolower(int c)**:将c转换为小写字母。
- 返回值:返回转换后的小写字母。
**toupper(int c)**:将c转换为大写字母。
- 返回值:返回转换后的大写字母。
这些函数的参数都是int类型,可以传入一个字符的ASCII码,也可以传入一个字符变量。这些函数都是可重入函数,线程安全的。
7. 存储类型、链接和内存管理
7.1 概念简介
存储类型
被存储的每一个值都占用一定的物理内存,C语言把这样的一块内存成为对象(object)。
链接
C语言中变量有3种链接属性:外部链接、内部链接或无链接。
具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量。
具有文件作用域的变量可以是外部链接或内部链接。
而外部链接可以在多文件的程序中使用,内部链接变量只能在一个翻译单元中使用。
存储期
作用域和链接描述了标识符的可见性。
存储期则描述了通过这些标识符访问的对象的生存期。
具体来说就是表示一个变量从创建到被摧毁的这段时间。
C对象有4种存储期:静态存储器、线程存储期、自动存储器、动态分配存储期。
静态存储期:静态存储期的变量在程序运行期间一直存在于内存中,直到程序结束时才被销毁。静态存储期的变量可以在函数内部使用,但它们的作用域是整个程序。静态存储期的变量可以使用关键字static来声明,如下所示:
1 | static int count = 0; |
自动存储期:自动存储期的变量在函数被调用时被创建,在函数执行结束时被销毁。自动存储期的变量通常是在函数内部定义的,如下所示:
1 | void foo() |
动态存储期:动态存储期的变量可以在程序运行期间通过动态内存分配函数(如malloc())来创建,在不需要时通过动态内存释放函数(如free())来销毁。动态存储期的变量的生命周期由程序员手动管理,如下所示:
1 | int* arr = malloc(sizeof(int) * 10); |
线程存储期:是指变量在特定线程中存在的时间,只有在多线程编程中才会涉及到。线程存储期的变量在每个线程中都有一份独立的拷贝,这样不同的线程之间就可以同时访问同一个变量而不会相互干扰。线程存储期的变量可以使用关键字_Thread_local
或者thread_local
来声明,具体使用方式如下:
1 | _Thread_local int x; // C11标准 |
需要注意的是,线程存储期只有在支持C11或者C++11标准的编译器中才能使用。如果您使用的编译器不支持这些标准,那么您可能需要使用平台特定的方式来实现线程存储期。
7.2 五种常见的存储类型
存储类型是指变量在内存中的存储位置与访问方式

自动变量
默认情况下,声明在块或函数头中的任何变量都属于自动存储类型。同样也可以显式的使用关键字auto,但需要注意的是编写C/C++的程序时,由于C++中的用法不同,最好不要用auto作为存储类型说明符。
1 | int main(void) |
寄存器变量
寄存器存储类告诉编译器把变量存储在寄存器中,以便快速访问。但是,寄存器存储类不能保证变量一定被存储在寄存器中,它只是对编译器提出的请求。因此,使用寄存器存储类时需要注意,如下所示:
1 | register int quick; //寄存器变量 |
通俗的讲,因为该变量时在寄存器中,而不是内存中,因此我们无从得知这个变量的地址,但是”幸运的话“它至少在最快存储单元中。
静态变量
其中包括块作用域的静态变量、外部链接的静态变量以及内部链接的静态变量。
静态变量并不是指变量不可变,而是值变量的地址在内存中原地不动。
块作用域的静态变量
相比较自动变量,静态变量不会自动消失。也就是说这些变量具有块作用域、无连接,但是具有静态存储期。
计算机在多次函数调用之间会记录它们的值。在块中以存储类别说明符static
声明这种变量。
1 | static int count; // 静态全局变量 |
外部链接的静态变量
外部链接的静态变量具有文件作用域、外部链接和静态存储期。把变量的定义性声明放在所有函数的外面便创建了外部变量。
为了指出该函数中使用了外部变量,可以在函数中用关键字extern
再次声明。假设一个源文件使用的外部变量文件在另一个源文件中,则必须使用extern
声明。
1 | int Errupt; //外部定义的变量 |
main()
中的两条extern
声明可以去掉,因为外部变量具有文件作用域。
需要注意的是,未初始化的外部变量,会被自动初始化为0。
1 | int tern = 1; //定义式声明 |
内部链接的静态变量
该存储类型的变量具有静态存储期、文件作用域和内部链接。在所有函数外部(同外部变量相同),用存储类型说明符static
定义的变量。
1 | static int svil = 1; //静态变量,内部链接 |
这种变量过去叫外部静态变量,但这个属于本身有点自相矛盾,因为它们具有内部链接,所以称为内部链接的静态变量。
普通的外部变量可以用于同一程序中任意文件中的函数,但是内部链接的静态变量只能用于同一个文件中的函数。可以使用存在类别说明符extern,在函数中重复声明任何具有文件领域的变量。
1 | int traveler = 1; //外部链接 |
当程序拥有多个翻译单元组成,才能体现内部链接与外部链接的重要性。
C通过在一个文件中进行定义式声明,然后再其他文件中进行引用式声明来实现共享。
除了定义声明外,其他声明都要使用exgern关键字。而且定义式声明才能初始化变量。
7.3 内存分配
malloc()和free()
原型在stdlib.g
头文件中。
通常这两个函数是配套使用的,free()
函数的参数是之前malloc()
返回的地址,该函数释放之前分配的内存。
malloc()
的返回类型通常被定义为指向char的指针,而在ANSI C标准开始,C开始一个新的类型:指向void的指针。该类型相当于一个”通用指针“。
这也是为什么使用malloc()
应该使用强制类型转换,提高代码可读性。
calloc()函数
分配内存还可以使用calloc(),用法:
1 | long *newmem; |
和malloc()
一样,在ANSI之后,应当在使用时强制类型转换运算符。但,
calloc()
函数接受两个无符号整数作为参数(ANSI C规定是size_t类型)。
第1个参数是所需的存储单元数量,第2个参数是存储单元大小(以字节为单位)
8. 位操作
8.1 二进制整数
假设C字节就是8位(实际可能是8位、9位、16位或其他)
位编号从7~0,高阶位到低阶位。
对于有符号整数,最简单的方式就是用1位(如高阶位存储符号)。如10000001
表示-1,00000001
表示1。因此其表示的范围是-127~+127;
但是,首先容易混淆其次使用一位表示过于浪费因此引出了二进制补码;
以1字节为例,二进制补码用1字节后面的7位表示0~127,高阶位设置为0.另外,如果高阶位为1,则表示为负。
这两种方法的区别在于判断正负的方法。从一个9为组合100000000,减去一个负数的为组合,结果是该复制的量。例如,假设一个负值的位组合是10000000,作为一个无符号字节,该组合表示128;作为一个有符号值,该组合表示负值,而且100000000-10000000,即10000000(128)。因此该数位-128。类似的,10000001是-127,11111111是-1。
该方法表示-128~+127范围的数。
8.2 二进制浮点数
二进制小数
使用2的幂作为分母,所以二进制小数。101表示:
1/2 + 0/4 +1/8
许多小数不能精确的用二进制小数表示,如3/4和7/8能够可以但1/3和5/2就不行。
浮点数表示法
为了计算机能够表示一个浮点数,要留出若干位(因系统而异)储存二进制分数,其他位储存指数。一般而言,数字的实际值是由二进制小数乘以2的指定次幂组成。
8.3 C按位运算符
一元运算符取反:~
按位与:&
按位或:|
按位异或:^
用法:掩码
掩码中的0隐藏了flags
种对应的位。
使用&运算符。将需要掩盖的位全部设置0。
用法:打开位(设置位)
设置flags
的某一位位1,其他不变。
使用|运算符。任何为与0组合都是本身;任何为与1组合,都是1;
用法:关闭位(清空位)
和打开特定的位相似,又是需要关闭一些位。
使用&运算符与~一元运算符结合。
flags = flags & ~MASK;
用法:切换位
打开关闭的位,关闭打开的位。
使用^异或运算符。
flags = flags ^ MASK;
用法:检查位的值
1 | if ((flags & MASK) == MASK) |
需要注意的是掩码和信息的值宽度相同。
移位运算符
左移:<<
(10001010) << 2
(00101000)
每一位向左移动两位
8.5 位字段
通常,对于一个方框的属性有如下:
方框透明不透明
颜色
等等
对于透明,我们只需要1位,对于颜色8种颜色只需要3个单元。
声明如下
1 | struct box_props{ |
因此该结构只占了4位,就完成了对两个属性的描述。
位操作本身从来没用过,后面单独再详细介绍;
同时还有预处理部分同样用的很少单独再总结;
参考
C Primer Plus
ChatGpt
挖坑To-Do
位操作
预处理器和C库