C:PartⅢ
Zane Lv4

前言

承接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
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#define STLEN 81
int main(void)
{
char words[STLEN];

puts("Enter a string, please.");
gets(words);
printf("%s",&words);
puts(words);
}

即便这是gets()的典型用法,但往往上面这段代码会使得编译器出现警告,原因是gets()函数无法检查数组是否装的下输入行。gets()函数仅仅知道数组的开始处,但是无从定位结束的元素是哪个。

如果输入的字符过长,就会导致缓冲区溢出,多余的字符超过指定的目标空间。倘若这些字符修改了程序中的其他数据,就会导致程序出错!

因此早在C11标准中,委员会直接废除了gets()函数,尽管目前大部分编译器依旧支持。

替代品

C11标准新增的gets_s()函数可代替gets()函数。该函数与gets()非常接近,几乎可以替换现有代码中的所有gets()。

fgets()与fputs()函数

char *fgets(char *str, int num, FILE *stream);

fgets()函数通过增加两个参数限制读入字符来解决溢出的问题。其与gets()的区别如下:

  1. 安全性问题

gets()函数存在缓冲区溢出问题,如果读取的字符串长度超出了缓冲区长度,会导致缓冲区溢出。这样就会导致严重的安全问题,例如可以利用缓冲区溢出来执行恶意代码。而fgets()函数会自动检查读取的字符串长度是否超出缓冲区长度,避免了这种安全问题。

  1. 参数不同

gets()函数只有一个参数,即指向读取字符串的字符数组的指针。而fgets()函数有三个参数,分别是指向读取字符串的字符数组的指针、最大读取字符数和文件指针。

常用的stdin(标准输入)作为第三个参数,表示从键盘中读入的数据。

  1. 特殊字符处理

fgets()函数会读取换行符(\n)并把它保存在读取的字符串中,而gets()函数不会读取换行符。

同时,fputs()也有了多个参数,其第二个参数指明它要写入的文件,如果要显示在计算机显示器上,应使用stdout(标准输出)作为该参数。

int fputs(const char *str, FILE *stream);

gets_s()与puts_s()

gets_s()和puts_s()是C11标准新增的安全函数,主要用于输入输出字符串,其参数如下:

  1. gets_s()函数:

char * gets_s(char * str, rsize_t n);

  • str:指向目标字符数组的指针,用于存储输入的字符串。

  • n:目标字符数组的大小。

  1. 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
2
3
char str[] = "Hello World!";
size_t len = strlen(str);
printf("字符串的长度为:%zu\n", len);
strcpy()

函数原型:char *strcpy(char *dest, const char *src); 函数作用:将源字符串src复制到目标字符串dest中。
使用示例:

1
2
3
4
char src[] = "Hello World!";
char dest[20];
strcpy(dest, src);
printf("复制后的字符串为:%s\n", dest);
strcat()

函数原型:char *strcat(char *dest, const char *src); 函数作用:将源字符串src拼接到目标字符串dest的末尾。
使用示例:

1
2
3
4
char str1[20] = "Hello";
char str2[] = "World!";
strcat(str1, str2);
printf("拼接后的字符串为:%s\n", str1);
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
2
3
4
5
6
7
8
9
char str1[] = "Hello World!";
char str2[] = "hello world!";
int result = strcmp(str1, str2);
if (result > 0)
printf("字符串%s大于字符串%s\n", str1, str2);
else if (result < 0)
printf("字符串%s小于字符串%s\n", str1, str2);
else
printf("字符串%s等于字符串%s\n", str1, str2);

需要注意的是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
2
3
4
5
6
char str[] = "Hello World!";
char *p = strchr(str, 'W');
if (p != NULL)
printf("字符W在字符串%s中的位置为:%ld\n", str, p - str);
else
printf("字符串%s中没有字符W\n", str);
strstr()

函数原型:char *strstr(const char *haystack, const char *needle); 函数作用:查找字符串haystack中首次出现字符串needle的位置,并返回该位置的指针。
使用示例:

1
2
3
4
5
6
char str[] = "Hello World!";
char *p = strstr(str, "World");
if (p != NULL)
printf("字符串World在字符串%s中的位置为:%ld\n", str, p - str);
else
printf("字符串%s中没有子串World\n", str);

以上是C语言中常用的字符串函数及其用法。除了上述函数外,还有许多其他的字符串函数,如strtok()sprintf()sscanf()等。

6.4 常用ctypes.h字符函数

ctype.h库提供了一些用于字符处理的函数,这些函数可以检查和转换字符。下面介绍一些常用的函数:

  1. **isalnum(int c)**:判断c是否为字母或数字。

    • 返回值:若c是字母或数字,返回非零值,否则返回0。
  2. isalpha(int c):判断c是否为字母。

    • 返回值:若c是字母,返回非零值,否则返回0。
  3. **isdigit(int c)**:判断c是否为数字。

    • 返回值:若c是数字,返回非零值,否则返回0。
  4. **islower(int c)**:判断c是否为小写字母。

    • 返回值:若c是小写字母,返回非零值,否则返回0。
  5. **isupper(int c)**:判断c是否为大写字母。

    • 返回值:若c是大写字母,返回非零值,否则返回0。
  6. **tolower(int c)**:将c转换为小写字母。

    • 返回值:返回转换后的小写字母。
  7. **toupper(int c)**:将c转换为大写字母。

    • 返回值:返回转换后的大写字母。

这些函数的参数都是int类型,可以传入一个字符的ASCII码,也可以传入一个字符变量。这些函数都是可重入函数,线程安全的。

7. 存储类型、链接和内存管理

7.1 概念简介

存储类型

被存储的每一个值都占用一定的物理内存,C语言把这样的一块内存成为对象(object)。

链接

C语言中变量有3种链接属性:外部链接、内部链接或无链接。

具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量。

具有文件作用域的变量可以是外部链接或内部链接。

而外部链接可以在多文件的程序中使用,内部链接变量只能在一个翻译单元中使用。

存储期

作用域和链接描述了标识符的可见性。

存储期则描述了通过这些标识符访问的对象的生存期。

具体来说就是表示一个变量从创建到被摧毁的这段时间。

C对象有4种存储期:静态存储器、线程存储期、自动存储器、动态分配存储期。

静态存储期:静态存储期的变量在程序运行期间一直存在于内存中,直到程序结束时才被销毁。静态存储期的变量可以在函数内部使用,但它们的作用域是整个程序。静态存储期的变量可以使用关键字static来声明,如下所示:

1
static int count = 0;

自动存储期:自动存储期的变量在函数被调用时被创建,在函数执行结束时被销毁。自动存储期的变量通常是在函数内部定义的,如下所示:

1
2
3
4
5
void foo()
{
int i = 0;
// do something with i
}

动态存储期:动态存储期的变量可以在程序运行期间通过动态内存分配函数(如malloc())来创建,在不需要时通过动态内存释放函数(如free())来销毁。动态存储期的变量的生命周期由程序员手动管理,如下所示:

1
2
3
int* arr = malloc(sizeof(int) * 10);
// do something with arr
free(arr);

线程存储期:是指变量在特定线程中存在的时间,只有在多线程编程中才会涉及到。线程存储期的变量在每个线程中都有一份独立的拷贝,这样不同的线程之间就可以同时访问同一个变量而不会相互干扰。线程存储期的变量可以使用关键字_Thread_local或者thread_local来声明,具体使用方式如下:

1
2
_Thread_local int x;  // C11标准
thread_local int y; // C++11标准

需要注意的是,线程存储期只有在支持C11或者C++11标准的编译器中才能使用。如果您使用的编译器不支持这些标准,那么您可能需要使用平台特定的方式来实现线程存储期。

7.2 五种常见的存储类型

存储类型是指变量在内存中的存储位置与访问方式

自动变量

默认情况下,声明在块或函数头中的任何变量都属于自动存储类型。同样也可以显式的使用关键字auto,但需要注意的是编写C/C++的程序时,由于C++中的用法不同,最好不要用auto作为存储类型说明符。

1
2
3
4
int main(void)
{
auto int plox;
}
寄存器变量

寄存器存储类告诉编译器把变量存储在寄存器中,以便快速访问。但是,寄存器存储类不能保证变量一定被存储在寄存器中,它只是对编译器提出的请求。因此,使用寄存器存储类时需要注意,如下所示:

1
register int quick; //寄存器变量

通俗的讲,因为该变量时在寄存器中,而不是内存中,因此我们无从得知这个变量的地址,但是”幸运的话“它至少在最快存储单元中。

静态变量

其中包括块作用域的静态变量、外部链接的静态变量以及内部链接的静态变量。

静态变量并不是指变量不可变,而是值变量的地址在内存中原地不动

块作用域的静态变量

相比较自动变量,静态变量不会自动消失。也就是说这些变量具有块作用域、无连接,但是具有静态存储期。

计算机在多次函数调用之间会记录它们的值。在块中以存储类别说明符static声明这种变量。

1
static int count;  // 静态全局变量
外部链接的静态变量

外部链接的静态变量具有文件作用域、外部链接和静态存储期。把变量的定义性声明放在所有函数的外面便创建了外部变量。

为了指出该函数中使用了外部变量,可以在函数中用关键字extern再次声明。假设一个源文件使用的外部变量文件在另一个源文件中,则必须使用extern声明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int Errupt;   //外部定义的变量
double Up[100]; //外部定义的数组
extern char Coal; //如果Coal被定义在另一文件,必须这么声明

void next(void);
int main(void)
{
extern int Errupt; //可选的声明
extern double Up[100]; //可选的声明
}

void next(void)
{
//...
}

main()中的两条extern声明可以去掉,因为外部变量具有文件作用域。

需要注意的是,未初始化的外部变量,会被自动初始化为0。

1
2
3
4
5
int tern = 1;  //定义式声明
main()
{
extern int tern; //引用式声明
}
内部链接的静态变量

该存储类型的变量具有静态存储期、文件作用域和内部链接。在所有函数外部(同外部变量相同),用存储类型说明符static定义的变量。

1
2
3
4
5
static int svil = 1;  //静态变量,内部链接
int main(void)
{
//...
}

这种变量过去叫外部静态变量,但这个属于本身有点自相矛盾,因为它们具有内部链接,所以称为内部链接的静态变量

普通的外部变量可以用于同一程序中任意文件中的函数,但是内部链接的静态变量只能用于同一个文件中的函数。可以使用存在类别说明符extern,在函数中重复声明任何具有文件领域的变量。

1
2
3
4
5
6
7
int traveler = 1; //外部链接
static int stayhome = 1; //内部链接
int main()
{
extern int traveler; //使用定义在别处的变量
extern int stayhome;
}

当程序拥有多个翻译单元组成,才能体现内部链接与外部链接的重要性。

C通过在一个文件中进行定义式声明,然后再其他文件中进行引用式声明来实现共享。

除了定义声明外,其他声明都要使用exgern关键字。而且定义式声明才能初始化变量。

7.3 内存分配

malloc()和free()

原型在stdlib.g头文件中。

通常这两个函数是配套使用的,free()函数的参数是之前malloc()返回的地址,该函数释放之前分配的内存。

malloc()的返回类型通常被定义为指向char的指针,而在ANSI C标准开始,C开始一个新的类型:指向void的指针。该类型相当于一个”通用指针“。

这也是为什么使用malloc()应该使用强制类型转换,提高代码可读性。

calloc()函数

分配内存还可以使用calloc(),用法:

1
2
long *newmem;
newmem = (long *)calloc(100, sizeof(long));

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
2
if ((flags & MASK) == MASK)
puts("OK!");

需要注意的是掩码和信息的值宽度相同。

移位运算符

左移:<<

(10001010) << 2

(00101000)

每一位向左移动两位

8.5 位字段

通常,对于一个方框的属性有如下:

  • 方框透明不透明

  • 颜色

  • 等等

对于透明,我们只需要1位,对于颜色8种颜色只需要3个单元。

声明如下

1
2
3
4
struct box_props{
bool opaque :1;
unsigned int fill_color :3;
};

因此该结构只占了4位,就完成了对两个属性的描述。

位操作本身从来没用过,后面单独再详细介绍;

同时还有预处理部分同样用的很少单独再总结;

参考

C Primer Plus
ChatGpt

挖坑To-Do

  • 位操作

  • 预处理器和C库

由 Hexo 驱动 & 主题 Keep