一. 程序设计基础知识

1.1 计算机语言

计算机语言是指用于人与计算机之间通信的语言,总体而言分为机器语言,汇编语言,高级语言三类。其中,C语言就是一种高级语言。

1.2 程序

沃斯提出一个公式: 数据结构 + 算法 = 程序

1.3 算法

算法有五个基本特征:有穷性、确定性、有效性、有零个或多个输入、有一个或多个输出。

1.3.1 流程图

1.3.2 伪代码

规范:使用缩进,自然语言加上某种常用语言的格式语句,忽略细节。(总而言之,是写给人读的)

1.4 结构化程序设计方法

1.4.1 三种基本结构

三种基本结构包含顺序控制结构,分支控制结构,循环控制结构。这三种结构按不同次序进行执行,从而在整体上实现结构模块化,从而遵循自顶而下的设计方法。

1.4.2 注释的要求

注释分为序言性注释,功能性注释

序言性注释包括:每个模块的用途功能;模块的调用方式,参数描述;重要数据的名称,用途等 ;开发的人员信息

功能性注释包括:说明程序段

二、C语言的数据类型与运算规则

2.0 标识符的规范

为常量或变量起名字时,规范有:

1、有字母,数字,下划线组成

2、第一个字符必须为字母或下划线(下划线慎用,容易与关键字重复)

3、C语言对大小写是很敏感的,A和a是不同的

2.1 数据与数据类型

在定义中不允许连续赋值!!!

2.1.1 整型数据

分为int, long int, unsigned int, unsigned long共四种类型。int一般到32000多,long一般到20亿多(简单记一下大小就行)。

计算机内部总是采用二进制补码的形式表示一个数值型数据。正数的补码与原码相同,负数的补码将该数的绝对值的二进制形式按位取反再加1.

整型数据与无符号整型数据都是用两个字节(16位二进制数)表示,整型数据最高位为符号位,“1”表示负数,“0”表示正数,其余15位表示数值。而无符号整数数据全部16位表示数值。

在C源程序中不能表示的进制是二进制,能表示的进制是八进制、十进制、十六进制。不同进制的表示方式不同,常见的八进制和十六进制分别在数前面加上“0”和“0x”【注意:题目中 ‘\18’ 很容易迷惑人,本来想表示八进制,但是八进制中是不含有8的】

整型数据很容易溢出!!!整数溢出后往往采用回绕的方式

2.1.2 实数型数据(浮点型)

分为float, double, long double共三种类型。其中float叫做单精度型,double叫做双精度型,long double叫做长双精度型。float一般到小数点后7位,double到小数点后15~16位,long double到小数点后18~19位。

科学计数法在C语言中用e或E来表示。指数必须是不超过数据范围的正负整数,并且在e或E两侧必须有数字,且右侧必须是整数,可以带上 + 或 -(e3是不合法的)

浮点型的数在运算中会出现误差,而且不能直接比较是否相等(一般用 (a-b) < 1 e-6来判断两数是否相等)

2.1.3 字符型数据

字符型数据(char)在计算机中存储的是字符的 ASCLL 码,一个字符的存储占用一个字节。在使用时字符可以与整型数据通用,因为 ASCLL 码代表的是0~255的整数。

转义字符时需要注意的,容易忘掉。比如:

字符常量用单引号括起来,字符串常量用双引号括起来。同时,一个字符占一个字节,一个一位字符串占两个字节

2.2 运算符与表达式

2.2.1 基本运算符

一、算术运算符:

算术运算符用于执行基本的数学运算,例如:加法、减法、乘法、除法等。以下是常用的算术运算符:

+:加法运算符,用于相加两个操作数。

-:减法运算符,用于从第一个操作数中减去第二个操作数。

*:乘法运算符,用于将两个操作数相乘。

/:除法运算符,用于将第一个操作数除以第二个操作数。(整数除法的小数部分会被丢弃,称为截断;混合整数和浮点数计算的结果是浮点数;对于负数的整数除法,C99规定趋零截断,例如-3.8转换为-3,而不是-4)

%:取模运算符,用于计算两个操作数相除后的余数。(这个要注意两侧的数据只能是整数,不能是浮点数)

二、赋值运算符:

赋值运算符用于将一个值赋给变量。以下是常用的赋值运算符:

=:简单赋值运算符,用于将右侧的值赋给左侧的变量(也就是说赋值运算符的左侧一定是可修改左值)。

三、比较运算符:

比较运算符用于比较两个值的关系,并返回一个 bool 值(0或1)。以下是常用的比较运算符:

==:等于运算符,用于检查两个操作数是否相等。

!=:不等于运算符,用于检查两个操作数是否不相等。

:大于运算符,用于检查第一个操作数是否大于第二个操作数。

:小于运算符,用于检查第一个操作数是否小于第二个操作数。

=:大于等于运算符,用于检查第一个操作数是否大于或等于第二个操作数。

=:小于等于运算符,用于检查第一个操作数是否小于或等于第二个操作数。

四、逻辑运算符:

逻辑运算符用于执行逻辑运算,并返回一个布尔值(0或1)。以下是常用的逻辑运算符:

&&:逻辑与运算符,用于同时判断两个条件是否为真。

||:逻辑或运算符,用于判断两个条件中至少一个是否为真。

!:逻辑非运算符,用于取反一个条件的值。

五、位运算符:

位运算符用于对二进制位进行操作。以下是常用的位运算符:

&:按位与运算符,用于对两个操作数的每个对应位执行与操作。

|:按位或运算符,用于对两个操作数的每个对应位执行或操作。

^:按位异或运算符,用于对两个操作数的每个对应位执行异或操作。

~:按位取反运算符,用于对操作数的每个位执行取反操作。

:左移运算符,将操作数的二进制表示向左移动指定的位数。

:右移运算符,将操作数的二进制表示向右移动指定的位数。

六、递增递减运算符:

递增递减运算主要有两种形式:++i 或是 i++ ,分别称为前缀形式和后缀形式。两者的区别在于递增行为发生的时间有所不同。

下面我们举例说明:

#include

int main (void)

{

int a = 1, b = 1;

int a_post = a++, b_post = --b;

return 0

}

在上述程序中若最终输出 a,b,a_post 和 b_post 的值的话,会发现 a = 2,b = 0,a_post = 1,b_post = 0。

由此我们看出:后缀的意思是使用后再递增或递减;前缀的意思是使用前先递增或递减。

2.2.2 运算符优先级

在C语言中,不同的运算符具有不同的优先级。当一个表达式中存在多个运算符时,优先级高的运算符先进行计算。如果两个运算符的优先级相同,那么根据结合性规则来确定计算顺序。以下是C语言中常见运算符的优先级从高到低的顺序:

2.2.3 表达式与语句

表达式由运算符和运算对象组成,每一个表达式都有一个值,这个值要按照运算符的优先级来运算得到。

语句是C程序的基本构建块,一条语句相当于一条完整的计算机指令,一般以;结尾。语句可以分为简单语句和复合语句,其中复合语句常用花括号括起来组成一个代码块。

2.2.4 类型转换

当我们在语句或表达式中使用混合类型时,C语言程序有一种固定的方式对其进行类型转换。尽管很方便,但如果不知道是怎么转换的,有时候会十分危险。

1、运算中所有 char 型数据都转换成 int 型,float 型转换成 double 型

2、类型的级别由低到高分别是:char -> int -> unsigned -> long -> float -> double

3、赋值中最终结果的类型,以赋值运算符左边变量的类型为准,

使用时的格式为:(数据类型)表达式

【注意】数据类型两侧的小括号不能省略

三、顺序结构程序设计

3.1 格式化屏幕输出函数 printf()

1)格式一:printf(“字符串”);将字符串内容原样输出,必须用一对双引号括起来。

2)格式二:printf(“格式控制串”,输出项列表);用于输出运算结果,且控制数据输出的格式。

一般在输出函数中含有三种符号:普通字符、转义字符和格式说明符

(1)普通字符:原样输出。

(2)转义字符:以 \ 开头,最常用的是 \n(换行)。

(3)格式说明符:以 % 开头,控制数据输出的格式。

以下是一些常用的格式说明符:

另外,还有一些辅助格式说明符:

h、l 表示数据长度——例如,%hd 是短整型,%lf 是双精度型。

%3d、%3f 表示数据宽度为3【注意:如果数据长度大于3,不会截断,而是完整地将数据输出】

%. 2f 表示小数精度为两位(四舍五入)

% #o 表示输出前导 0 的八进制整数

% d 表示左对齐输出

% d 表示右对齐输出

3.2 格式化键盘输入函数 scanf()

大部分格式说明符与 printf() 中的一致,但是不能含有转义字符。下面是一些注意事项:

(1)输入时,变量前面要加取地址运算符 &

(2)如果格式控制串中有普通字符,则普通字符必须原样输入,例如:

int x , y; scanf(“x=%d, y=%d”, &x, &y);

那么,在输入时一定要类似于:“x=12, y=34”

(3)此时整数输入依然可以用 %md 控制长度,但这时如果数据太长,会被截断,只读入前 m 位

(4)输入小数时,不能对其小数位数进行控制

(5)当字符和数值混合输入时,如果字符在前,数值在后,则字符和数值之间可以加空格,反之,不能添加空格

(6)如果输入地址项列表中是指针变量或者数组名,则前面不用加取地址运算符

(7)在输入时,一行末尾的换行符(回车)也是一个字符

3.3 字符输入输出函数

3.3.1 getchar() 函数

专门用于输入单个字符,不能用于输入整数或实数 。调用格式:ch = getchar();

【注意】 有时常用 getchar() 来读入一个字符,且不将其赋给任何一个变量,从而实现在读入时跳过部分内容。

3.3.2 putchar() 函数

专门用于输出单个字符,不能用于输出整数或实数。调用格式:putchar( ch );

四、选择结构程序设计

4.0 逻辑运算符的短路特性

如果逻辑与(&&)运算符左侧表达式的值为假(0),则其右侧的表达式不会被运算,将其比喻为短路。如果逻辑或(||)运算符左侧表达式为真(1),则其右侧表达式不会被运算,也称为被短路了。

4.1 if 语句

4.1.1 单分支 if 语句

1、if 表达式后面不能加分号

2、if 只能自动结合一条语句,如果有多条语句需要结合,要用花括号将其括住,这是一个常考点。

4.1.2 双分支 if 语句

1、if … else … 语句构成二选一结构,任何时候都只会执行其中一个。

2、else 有就近配对的原则,它不能单独存在,总是和前面距离它最近的且不带其他 else 子句的 if 语句配对使用,与书写格式无关。

4.1.3 多分支 if 语句

1、if … else if … else 语句构成多选一结构,任何时候都是从上往下进行判断【因此要注意设置条件的合理性】

2、if 子句可以单独存在,但是 else if 子句、else 子句都不能单独存在,必须与 if 配套使用。

4.2 switch 语句

4.2.1 语句形式

switch (表达式)

{

case 常量表达式1:<语句体1;>

case 常量表达式2:<语句体2; >

case 常量表达式n:<语句体n; >

default:<语句体 n+1;>

}

4.2.2 注意事项

(1)switch 后面的表达式的值必须是整型或者是字符型,不能实型。

(2)break 语句表示执行完该分支就跳出 switch 语句,缺省break 的话会继续执行下一分支。

(3)default 语句可以写在任何位置,并且也可以缺省。

(4)case 后面是常量表达式,不能是变量。

五、循环结构程序设计

5.1 while 语句

挺简单的,好像没什么注意点,唯一要注意的是不要忘了在while的表达式中的变量应该要实现变化的功能,否则就是恒为真或是恒为假。

5.2 do…while 语句

一般用于在判断真假前首先需要执行一次的程序类型。

一些注意事项如下:

(1)while 后面的 ; 不要忘记写了。

(2)一旦用 do…while 则程序至少运行了一次,这一点不要忘了。

5.3 for 语句

总体上也是简单的,几个注意事项如下:

(1)for (表达式1 ;表达式2 ;表达式3) 中间的三个表达式之间应该用分号隔开,而每个表达式可以使用多个逗号组成。

(2)根据具体情况,三个表达式均可以缺省,但两个分号不能缺省。

(3)for 语句一般用于已经知道要循环的次数,而 while 语句一般用于不清楚循环的次数

5.4 break 语句

break 语句用在循环结构中,可以提前结束本层的循环过程,即不论循环体有几层嵌套,break 语句的执行都只能结束他所在的那一层循环。

5.5 continue 语句

continue 语句只能用在循环体中,作用是提前结束本次循环,提前进入下一次循环。一旦执行 continue 语句,则循环体中,放在 continue 之后的语句都将被跳过。

要注意分辨 break 和 continue 的作用,选择合适的语句进行操作。

5.6 goto 语句

一般不用,甚至大部分老师会说禁止使用

六、数组

6.1 一维数组

6.1.1 一维数组的定义

(1)定义格式:

数据类型标识符 数据名 [整型常量表达式]

(2)注意事项:

1、数组定义时,有一些编译器要求方括号内的表达式只能包含整型常量,不能包含变量。

2、数组名实质上是一个指针,指向整个数组的首地址。

3、在赋值时,数组名是一个常量,不能被赋值;数组元素是变量,可以被赋值。

6.1.2 一维数组元素的引用

(1)引用格式:

数组名 [下标表达式]

(2)注意事项:

1、数组元素的下标从 0 开始。

2、数组定义时,[整型常量表达式] 决定数组的大小,表达式的值直接就是数组的存储单元的数量和数组引用时,[下标表达式] 代表的是数组元素在数组中的序号

3、要小心数组越界,这会导致程序运行出错,但系统不会提示错误

6.1.3 一维数组初始化

数组定义后如果没有初始化,每个数组元素的初值都是随机的,因此最好初始化。

(1)全部元素初始化。这时可以缺省数组的长度。例如:

int a[5] = {10,20,30,40,50}; 等价于 int a[] = {10,20,30,40,50}

(2)部分元素初始化,未被初始化的元素自动赋初值为 0。例如:

int a[5] = {10,20,30} 则剩下两个元素为 0 ,即事实上数组为 a[5] = {10,20,30,0,0}

6.2 二维数组

6.2.1 二维数组的定义

(1)定义格式:

数据类型标识符 数组名 [整型常量表达式1] [整型常量表达式2];

(2)注意事项:

1、表达式1 和表达式2 分别对应的是数组的行数与列数

2、二维数组在内存中是按行存放,及系统先放满第一行,再放第二行。

6.2.2 二维数组元素的引用

(1)引用格式:

数组名 [行下标] [列下标]

6.2.3 二维数组的初始化方式

(1)全部元素初始化,可以分行或是不分行。此时可以缺省行长度,但不能缺省列长度(在多维数组中,则是可以缺省第一个常量表达式,但之后的所有常量表达式都不能缺省)。例如:

int a[3][2] = {{10,20},{30,40},{50,60}}

int a[3][2] = {10,20,30,40,50,60}

int a[][2] = {{10,20},{30,40},{50,60}}

(2)部分元素初始化,可以分行或是不分行,与上面类似,不再赘诉。

七、指针

7.1 指针的基本概念

7.1.1 地址的概念

计算机的内存空间有许多存储单元构成,每个存储单元都有一个唯一的编号,称为“地址”。程序运行时,各种数值都存放在这些存储单元中。

7.1.2 指针与地址的关系

指针——就是地址。一个存储单元的地址就称为该存储单元的指针,通过指针就能够对该存储单元的内容进行间接访问。指针分为指针常量,指针变量。数组名就是一个指针常量,因此可以直接使用。但是指针变量必须先定义后使用。也就是说,如果想用一个指针来操作一个数组,也需要使这个指针先指向这个数组再进行操作。

7.1.3 指针变量

(1)指针变量的定义:

数据类型说明符 * 指针变量名;

例如:int *p;

(2)注意事项:

1、指针变量名前面的 * 是为了说明其是一个指针,而不是一个普通变量。

2、指针变量使用前,应为其赋地址值,让其指向某个存储单元,未赋地址值的指针变量称为“悬空指针”,使用悬空指针很危险,容易使程序出错。

(3)赋值语句:

1、指针变量 = 地址,例如:int a = 12; int *p = &a;

2、指针变量1 = 同类型的指针变量2,例如:int *p1 ,*p2 ,a; p1 = &a; p2 = p1;

7.2 指针的运算

7.2.1 两个运算符

(1)* (间接引用运算符):引用指针所存储单元的内容。

(2)&(取地址运算符):取出某个存储单元的地址。

7.2.2 指针的加减运算

指针只有在指向数组或者是字符串时,进行加减运算才有意义,因为数组和字符串的存储单元是连续的,加减运算时指针在连续的存储空间里进行移动。

指针的加减不是数值上的加加减减,而是单元的移动,即

(1)“指针 + 整数n” 表示指针向后移动了 n 个单元

(2)“指针 - 整数n” 表示指针向前移动了 n 个单元

(3)“指针 ++” 表示指针向后移动了一个单元

(4)“指针 --” 表示指针向前移动了一个单元

7.2.3 两个指针相减与比较

如果两个指针指向同一个数组,或是同一个字符串,则两个指针之间可以相减,从而得出两个指针之间的距离(通过存储单元的数量来度量)。

同样地,只有两个指针指向同一个数组,或是同一个字符串,则两个指针可以进行比较运算,从而体现两个指针在数组或是字符串中的相对位置。但任何指针都可以与 0 或是 NULL进行比较来判断是否为空指针。

7.2.4 无效指针

无效指针是我们在写程序时经常出现的一些失误,需要当心。以下是一些无效指针的类型:

(1)没有指向有效数据对象或函数的非空指针

(2)野指针:没有经过初始化或赋值的指针变量

(3)悬空指针:原本所指向的内存空间已经被释放的指针

(4)指向数组元素的指针移动到了数组范围之外【容易意识不到】

(5)把非0整数值强制转换成指针

7.3 指针与一维数组

7.3.1 指针指向一维数组的方法

(1)指针变量 = 数组名;例如:p = a;

(2)指针变量 = &数组元素;例如:p = &a[1];

(3)指针变量 = 数组名 + 整数n;例如:p = a + 2

7.3.2 指针引用数组元素值或者地址的方法

(1)下标法:**

例如:p[i] 为 p 所指向的数组的第 i + 1 个元素的值;而相应地,&p[i] 为 p 所指向的数组的第 i + 1个元素的地址。

(2)地址法:

例如:*(p + i) 为 p 所指向的数组的第 i + 1 个元素的值;而相应地,p + i 为 p 所指向的数组的第 i + 1个元素的地址。

7.3.3 易混表达式的判别方式

已知:&、*、++、 运算符的优先级同级,结合性是自右往左。因此在适当的时候需要通过添加括号是表达式成立。

例如:++(*p) 等价于 ++*p ;*p++ 等价于 *(p++)

7.4 指针与二维数组

7.4.1 指针与二维数组

二维数组名是一个行指针常量,行指针是指向一行元素的指针,该指针的基础类型取决于一行元素的存储容量。例如:int a[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}} ,则二维数组名 a 是行指针常量,它指向的一行元素是 4 个 int 型,其基础类型对应存储空间为16B。

7.4.2 二维数组的引用方法

(1)下标法,例如:a[i] [j] 或者是 * ( a [ i ] + j )

(2)地址法,例如:*( *( a + i )[ j ] )或者是 *( *(a + i) + j )

【注意】括号不能漏加,否则程序会有问题

(3)利用行指针变量:

由于二维数组名本身是一个行指针常量,因此可以定义一个行指针变量来指向二维数组,然后利用行指针变量引用二维数组元素。

1、行指针变量的定义:

数据类型说明符 (*行指针)[整型常量];

例如:int (*p)[4] 此时,首先由于有小括号,p与*结合,说明它是一个指针;然后与[4]结合,说明该指针的基础类型是4个int型,即指针p指向由4个int型构成的一行元素。

2、特别说明,行指针变量能够指向二维数组的前提条件是:行指针变量定义时方括号内的常量值必须与二维数组的列长度相同。

3、在定义了行指针变量后,其余的引用方式与上文(1)(2)类似。

7.4.3 指针数组与二维数组

(1)指针数组的定义:

指针数组是由若干个指针变量构成的数组。其定义格式为

数据类型说明符 *指针变量 [整型常量];

例如:int *a[3] 由于方括号的优先级高于*,所以 a 先结合 [],然后结合 *。结合 [] 说明 a 是一个数组,然后结合 int * 说明数组中的每一个元素都是一个整型指针。

(2)指针数组与二维数组

假设有定义:

int a[3][4] = {{1,2,3,4},{5,6,7,8},{9,10,11,12}},*q[3];

q[0] = a[0]; q[1] = a[1]; q[2] = a[2];

(3)引用二维数组元素

也和上文类似的有下标法与地址法。

7.5 指针的动态存储分配

7.5.1 动态分配

动态分配主要分为 malloc() 和 calloc(),其使用格式如下:

1)指针变量 = (数据类型 *)malloc (存储单元个数 * 每个存储单元的字节数);

假设有定义:

double *p;

p = (double *) malloc (10 * sizeof(double));

2)指针变量 = (数据类型 *)calloc (存储单元个数 * 每个存储单元的字节数);

假设有定义:

double *p;

p = (double *) calloc (10 , sizeof(double));

7.5.2 动态释放

当动态申请的存储单元不再使用时,应将这些存储单元释放掉,即将存储空间归还给操作系统。使用 free 函数,调用格式如下:

free(指针变量);

【注意】考试时一定要记得释放空间,否则会进行一定的扣分。

7.6 函数指针

7.6.1 函数指针的概念

函数和数据一样,在内存中都是占用存储空间,各种数据可以都可以通过指针来访问,同样,函数也可以通过指针来访问。指向函数的指针称为”函数指针“。

定义函数指针的格式为:

数据类型说明符(*指针变量名)();

7.6.2 关于函数指针的注意事项

1、定义函数指针变量时,必须用小括号将星号和指针变量括起来,后面还必须有一对小括号。

2、定义函数指针变量时,前面应给出其所指函数的返回类型。

3、为函数指针变量赋函数名时,函数名后面不能有小括号。

利用函数指针调用子函数的语句既可以写成类似于 z = (*pf)(x,y); ,也可以写成 z = pf(x,y); 。

八、函数

8.1 函数基础

8.1.1 函数的几种类别

函数是C程序的基本组成单位,一个C程序由一个 main() 函数和若干个子函数组成。函数分为主函数、标准库函数、用户自定义函数。

(1)主函数:一个C程序有且仅有一个 main() 函数,main() 函数是一个程序的入口和正常出口。

(2)标准库函数:调用标准库函数,别忘记了要包含相对应的头文件。

(3)用户自定义函数:用户自己编写的函数

8.1.2 函数定义

(1)函数定义的格式如下:

函数返回值类型 函数名 ([类型说明符 形参1,类型说明符 形参2,])

{

说明语句;

执行语句;

return 表达式;

}

(2)注意事项:

1、有返回值一定要写明返回值类型,如果没有返回值则必须写明标识符 void。

2、有返回值一定要有 return 语句;如果没有返回值,可以不写 return 语句。

3、函数名要遵守标识符的命名规则,且同一程序中不能有相同函数名。

4、写形参时不要忘了在参数前面加上类型标识符。

8.1.3 函数调用

调用格式:函数名 ([实参表]);

【注意事项】要区分实参和形参,函数定义是的参数称为形参,函数调用时的参数称为实参。函数调用时,只能是实参传给形参,反过来不行,这称为”传值的单向性“。形参是变量,因此每个形参前要有数据类型说明符。实参是表达式,因此前面不可以有数据类型表达式。

8.1.4 函数声明

要区分函数声明与函数的定义,在一个程序中,函数只能被定义一次,但是可以被声明无数次。

(1)声明格式:

函数返回值类型 函数名 ([类型说明符 形参1,类型说明符 形参2,])

(2)注意事项:

1、如果函数定义在前,调用在后,则可以缺省函数声明。但是如果函数调用在前,那么就不可以缺省函数声明。

2、如果函数声明放在所有函数定义的前面,那么说明所有函数都可以调用被声明的函数。如果函数声明放在某个函数定义的内部,那么就只有这个函数可以调用被声明的函数。

8.2 变量的作用范围与存储类别

8.2.1 变量的作用范围

变量按照作用范围可以分为全局变量和局部变量,以下是对两者进行比较:

8.2.2 变量的存储类别

(1)auto 型(动态变量)

一般我们用的都是 auto 型变量,不太需要单独注意,因此不在此赘述。

(2)static 型(静态变量)

静态变量的生存期是整个C程序,因此静态变量也称为永久存储变量。

静态变量分为”静态全局变量“和”静态局部变量“

1、静态全局变量定义在函数体外部。

2、静态局部变量定义在函数体内部,在函数执行结束后变量的存储空间依然存在,也就是说在下一次调用时,该变量的值会延续之前存储的数值。

【注意】静态变量定义后如果没有赋值,会直接默认为 0 ,这是与动态变量不同的一点。

(3)register 型(寄存器变量)

寄存器变量顾名思义是存储在寄存器中,不是存储在内存中。因此,由于寄存器的读写速度极快,可以将程序使用的频率较高的少数变量定义为 register 型,以提高程序的运行速度。

寄存器变量也属于动态变量,因此在使用完毕后会被释放。

【注意】寄存器变量只限整型、字符型、指针型,不能将实型定义为寄存器变量

(4)extern 型(外部变量)

这个东西很复杂,可以联系函数的声明与定义的关系来理解 extern 和 auto的关系。extern 型变量的使用是在函数体的外面,针对那些将要被用到,但还没定义的动态变量。这一点和函数的声明与定义颇为相似。

其次,在多个源程序进行协同操作时,也可以用 extern 型变量实现变量在多个文件中的传输。总之,我们要注意的是,使用 extern是在对变量进行声明,不是在进行定义。同样地,声明可以有很多次,但是定义只能有一次。

另外,实质上函数的声明其实就是使用了外部变量,但为什么不用 extern 呢?因为函数的定义有函数体,函数的声明没有函数体,编译器很容易区分定义和声明,所以对于函数声明来说,有没有extern 都是一样的。但是作用于变量名时 extern关键字就不是可有可无的了,全局变量在外部使用声明时,extern关键词是必须的,如果变量无 extern修饰且没有显式的初始化,就成为了变量的定义,因此此时必须加 extern。

8.3 函数的递归

递归有两种类型:若是函数自己调用自己,则称为直接递归调用;若是A函数调用B函数,B函数调用A函数,则称为间接递归调用。递归的规程大约是先逐层调用,在逐层返回,所以输出时的顺序是反过来的,这一点要当心。

【注意】递归过程中一定要有判断递归结束的条件,防止无限制的递归,这会导致栈爆炸,很危险。

九、字符串

9.1 字符串的概念

字符串使用双引号括起来的零个或者任意多个字符,字符串必须以 ”\0“ 作为结束标志,这是要注意的一点。

9.2 字符串的输入输出函数

这一块与上文的输入输出有一定相似之处。但要注意 gets() 和 puts() 函数的使用,使用的时候不要弄错了。

9.3 字符串处理函数

首先这些函数都在 string.h 中

9.3.1 字符串长度函数 strlen()

(1)调用形式:整型变量 = strlen(字符数组或是字符指针);

(2)功能: 求一个字符串的长度,并返回字符串中第一个 ”\0“ 之前的有效字符的个数,注意是不包括 ”\0“ 的。

9.3.2 字符串复制函数 strcpy()

(1)调用形式:strcpy(字符串1,字符串2)

(2)功能: 将字符串 2 复制到字符串 1 中。

9.3.3 字符串连接函数

(1)调用形式:strcat(字符串1,字符串2)

(2)功能: 将字符串 2 连接到字符串 1 的结尾中,形成一个新的字符串。

9.3.4 字符串比较函数

(1)调用形式:strcmp(字符串1,字符串2)

(2)功能: 从左到右依次比较字符串中的对应字符,比较字符 ASCLL 的值的大小。直到遇到不同字符为止,或是比较到其中一个字符串结束为止。根据大小比较关系,该函数的返回值有三种情况:

函数返回值当字符串大于字符串当两者相同时当字符串小于字符串

十、结构体与链表

10.1 结构体变量与结构体指针

10.1.1 声明结构体类型的格式

struct <结构体标识名>

{

类型名 1 结构体成员列表 1;

类型名 2 结构体成员列表 2;

类型名 n 结构体成员列表 n;

};

例如:

struct date

{

int year;

int month;

int day;

};

10.1.2 结构体变量的定义

按照上述例子,我们给出结构体变量的两种定义方式

(1)方法一

struct date

{

int year;

int month;

int day;

};

struct date x,y;

(2)方法二

struct date

{

int year;

int month;

int day;

} x,y;

10.1.3 结构体指针的定义和引用

(1)定义格式:

struct 结构体标识名 * 指针变量 ;

【注意】结构体指针定义后必须为其赋地址值,让其指向与之同类型的存储空间,然后才能通过指针引用该存储空间里的结构体成员。

(2)引用方法:

方法一:结构体变量. 结构体成员

方法二:结构体指针 结构体成员

方法三:( * 结构体指针 ). 结构体成员

10.2 结构体数组

我们通过举例来解释如果定义结构体数组:

struct date

`{

int year;

int month;

int day;

};

struct date x[5];

这样就定义了一个长度为 5 的结构体数组,该数组中每个元素都是一个结构体变量。

【注意】在利用结构体指针来操作结构体数组时,要注意符号的优先级结合顺序,即运算符 的优先级高于 和

10.3 链表

链表是一种常用的、重要的数据结构,链表由若干个离散的结点组成,每个结点是一个结构体类型的存储空间,各个结点之间通过指针联系在一起,因此称之为链表。

10.3.1 链表结点结构体类型的声明

我们通过举例来解释如果定义结构体数组:

struct date

{

int year;

int month;

struct date * next;

};

以上声明了一个结构体类型。成员 next 是一个结构体类型的指针,用于存放链表中的下一个结点的地址,将它归类为指针域。

10.3.2 链表的创建

(1)尾插法:

链表的建立是指从无到有地建立一个链表,对于每一个结点,首先为其分配存储空间,然后在结点的数据域存入数值,然后将该结点的地址赋值给前一个结点的指针域,建立起前后相邻结点的连接关系。

链表的建立过程通常需要 3 个指针:第 1 个指针始终指向链表的头结点,通过该指针可以找到链表;第 2 个指针总是指向新创建的结点;第 3 个指针用于将新结点连接到链表中,该指针需要不断后移,它总是指向当前链表的最后一个结点。

(2)头插法:

方法类似于尾插法,只不过那个移动的指针是指向 head 的指针,不断的往前移,从而不断在头部插入新的结点。

10.3.3 链表的输出

链表的输出是指从链表的第一个结点开始,输出每个结点的数据域。首先通过指向链表头结点的指针找到链表,每访问一个结点,便输出其数据域中的值,直到访问到链表的末尾结点。

链表的输出过程通常需要 2 个指针:第 1 个指针始终指向链表的头结点;第 2 个指针从第一个结点开始不断地后移,依次指向需要访问的后续结点。

10.3.4 链表的插入

链表的插入是指在一个链表的指定位置插入一个结点,并保持链表的连续性。对于每一个结点,首先为其分配存储空间,然后在结点的数据域存入数值,接着,为了保证后面的地址不会丢失,首先将新结点指向插入位置的后一个结点,接着再将插入位置的前一个结点指向新的结点,从而实现链表的插入。

链表的插入过程通常需要 4 个指针:第 1 个指针始终指向链表的头结点;第 2 个指针指向被插入位置的前一个结点;第 3 个指针指向被插入位置的后一个结点;第 4 个指针指向被插入的新结点。

10.3.5 链表的删除

链表的删除是指从一个链表中删除一个指定结点,将其从链表中分离出来,并保证链表不会断开。过程比较容易,与插入的过程类似,因此不再赘述。

链表的删除过程通常需要 3 个指针:第 1 个指针始终指向链表的头结点;第 2 个指针指向被删除结点的前一个结点;第 3 个指针指向被删除结点的后一个结点。

10.3.6 链表的排序

链表的排序过程可以参考三种排序方式,选择,冒泡,插入排序都可以类似的实现链表的排序。

由于之前没有讲述这件事情,在此先简要概括一下三种排序方式:

首先是选择排序:

接着是冒泡排序:

最后是插入排序:

接着我们来以选择排序为例讲述怎样实现链表的排序:如果链表为空,不需要排序。如果链表只有一个结点,不需要排序。如果有多个结点。先将第一个结点与后面所有的结点依次对比数据域,只要有比第一个结点数据域小的,则交换位置。交换之后,拿新的第一个结点的数据域与下一个结点再次对比,如果比他小,再次交换,依次类推。第一个结点确定完毕之后,接下来再将第二个结点与后面所有的结点对比,直到最后一个结点也对比完毕为止。其余两种算法类似,不再赘述。

以下是一些关系链表的操作的源代码,可以帮助大家更好地理解一下链表的操作:

#include``#include`` ``typedef struct Node {` `int data;` `struct Node* next;``} Node;`` ``Node* initList() {` `Node* head = (Node*) malloc (sizeof(Node));` `if (!head) {` `exit(1); // 内存分配失败,结束程序` `}` `head->next = NULL; // 初始化头节点指针域为空` `return head;``}`` ``void insertNodeInEnd(Node* head, int data) {` `Node* newNode = (Node*) malloc (sizeof(Node));` `if (!newNode) {` `exit(1); // 内存分配失败,结束程序` `}` `newNode->data = data;` `newNode->next = NULL;`` ` `// 找到链表的最后一个节点` `Node* curr = head;` `while (curr->next != NULL) {` `curr = curr->next;` `}`` ` `// 将新节点插入链表` `curr->next = newNode;``}`` ``void insertNodeInHead(Node* head, int data) {` `Node* newNode = (Node*) malloc (sizeof(Node));` `if (!newNode) {` `exit(1); // 内存分配失败,结束程序` `}` `newNode->data = data;` `newNode->next = head->next;` `// 将head前移一位` `head->next = newNode;``}`` ``void deleteNode(Node* head, int data) {` `if (head == NULL || head->next == NULL) { // 空链表或只有一个节点的链表` `return;` `}` `// 如果要删除的节点是头节点` `if (head->next->data == data) {` `Node* temp = head->next; // 暂存要删除的节点` `head->next = temp->next; // 修改头节点的指针域,跳过要删除的节点` `free(temp); // 释放temp的内存空间` `return;` `}` `// 如果要删除的节点不是头节点` `Node* curr = head;` `while (curr->next != NULL && curr->next->data != data) { // 找到要删除的节点的前一个节点` `curr = curr->next;` `}` `if (curr->next == NULL) { // 如果没有找到该节点` `return;` `}` `Node* temp = curr->next; // 暂存要删除的节点` `curr->next = temp->next; // 修改该节点的指针域,跳过要删除的节点` `free(temp); // 释放temp的内存空间``}`` ``void traverseList(Node* head) {` `Node* curr = head->next;` `while (curr != NULL) {` `printf("%d ", curr->data);` `curr = curr->next;` `}` `printf("\n");``}`` ``Node* reverseList(Node* head) {` `Node* prev = NULL;` `Node* curr = head->next;` `Node* next = NULL;`` ` `while (curr != NULL) {` `next = curr->next; // 暂存当前节点的下一个节点` `curr->next = prev; // 将当前节点指向前一个节点` `prev = curr; // 移动前一节点到当前节点` `curr = next; // 移动当前节点到下一节点` `}` `head->next = prev; // 反转后,新的第一个节点变为prev` `return head;``}`` ``int main (void) {` `Node* head = initList();` `insertNodeInEnd(head, 1);` `insertNodeInEnd(head, 2);` `insertNodeInEnd(head, 3);` `insertNodeInHead(head, 4);` `traverseList(head); // 输出: 4 1 2 3` ` ` `deleteNode(head, 3);` `traverseList(head); // 输出: 4 1 2`` ` `head = reverseList(head);` `traverseList(head); // 输出: 2 1 4` `return 0;``}

十一、文件

11.1 文件的基本概念

11.1.1 文件的分类

文件可以按照数据的存放形式,分为”文本文件“和”二进制文件“。其中,文本文件的每个字节存放一个 ASCLL 码,代表一个字符。二进制文件是把数据按照其在内存中的存储形式原封不动地存放到磁盘中,一个字节并不是对应一个字符,不能直接输出字符形式。

【注意】二进制文件的输入输出相对快一些。

11.1.2 文件指针

文件指针是一个指向结构体类型名为 FILE 的指针。对文件的打开,关闭,读,写等操作都必须通过文件指针来完成。例如: FILE * fp;

这就是定义了一个 FILE 结构体类型的文件指针。

11.2 文件操作库函数

首先文件操作库函数都在 stdio.h 中

11.2.1 文件的打开与关闭

(1)文件打开函数 fopen()

函数原型:

FILE *fopen(char * fp, char * type);

调用形式:

文件指针变量 = fopen(文件名,文件使用方式)

(2)注意事项:

1、fopen() 函数如果成功调用会返回一个 FILE 类型的指针,如果调用失败会返回 NULL。

2、fopen 的两个参数都是字符串(尤其是后一个要当心),所以一定要用双引号把括起来

3、以下是一些文件的使用方式:

[1] 文本文件

打开方式 含义 文件不存在时 文件存在时

[2] 二进制文件

打开方式 含义 文件不存在时 文件存在时

(3)文件关闭函数 fclose()

函数原型:

fclose(FILE * fp);

调用形式:

fclose(文件指针变量);

【注意】在程序结束之前,千千万万要记得将文件关闭,这是一个扣分点。

11.2.2 文件的读与写

首先我们要分清一些概念:(键盘输入) 内存(程序) (写文件) 硬盘(文件)

(显示屏) 内存(程序) (读文件) 硬盘(文件)

因此,键盘输入和写文件是不同的概念;屏幕输出与读文件也是不同的概念。下面我们详细来区分一些函数的用法:

(1)格式化输入输出函数 fscanf() 和 fprintf()

函数调用形式:

fscanf ( 文件指针,格式控制串,输入项列表 );

fprintf ( 文件指针,格式控制串,输出项列表 );

功能:fscanf() 函数是从文件读入格式化数据,fprintf() 函数是将格式化数据写入文件中,例如:

FILE *fp; int x; fscanf(fp,“%d”,&x); 相当于从文件中读取一个整形数据并将其赋给 x。

FILE *fp; int x = 5; fprintf(fp,“%d”,x); 相当于将 x 的值写入文件中。

这一步要和 scanf() 函数和 printf() 函数的功能相区分清楚。

(2)字符输入输出函数 fgetc() / getc()、fputc() / putc()

函数调用形式:

字符变量 = fgetc(文件指针);

fputc (ch, 文件指针);

功能:fgetc() 函数是从文件读入一个字符,fputc() 函数是将一个字符写入文件中,例如:

FILE *fp; char x; x = fgetc(fp);

FILE *fp; char x = ‘a’; fputc(x, fp);

这一步要和 getchar() 和 putchar() 分清楚

(3)字符串输入输出函数 fgets() 和 fputs()

函数调用形式:

fgets ( 字符数组或字符指针或字符串,读入长度n,文件指针 );

fputs ( 字符数组或字符指针或字符串,文件指针 );

功能:fgets() 函数是从文件读入一个字符串,fputs() 函数是将一个字符串写入文件中。

以下是几点说明:

1、fgets() 函数最多只能从文件中读入 n-1 个字符,读入结束后,系统自动添加 ‘\0’。

2、fputs() 函数向文件写字符串时,会将 ‘\0’ 写入文件。

这一步要和 gets() 和 puts() 分清楚。

(4)数据块读写函数 fread() 和 fwrite()

这两个函数比较复杂,用处也少。

函数调用形式:

fread ( buffer, size, count, 文件指针 );

fwrite ( buffer, size, count, 文件指针 );

功能:fread() 函数是从文件读入 count 个大小为 size 的数据块,存入 buffer 所指的存储空间中。fwrite() 函数是将 buffer 所指的存储空间中 count 个大小为 size 的数据块写入文件中。

参数:buffer 是指向数据块的指针,输入或准备输出的数据存放在此内存块中;size 表示每个数据块的字节数(常用sizeof 函数来完成);count 用来指定每次读写的数据块个数。

以下是几点说明:

1、这两个都是以数据块为单位的读写函数,一般用于二进制文件的读写。

2、这里的数据块是指一串固定长度的字节,比如一个 int 、一个结构体或者一个定长数组。

(5)判断二进制文件是否结束函数 feof()

函数调用形式:

feof ( 文件指针 );

功能:判断二进制文件是否结束,即判断二进制文件的指针是否位于文件的末尾,如果文件结束返回 1,未结束返回 0。

说明:文本文件是以 EOF (相当于 -1)作为文件结束的标志。二进制文件没有明显的结束标志,因此判断二进制文件是否结束必须调用 feof() 函数。

(6)文件位置指针定位函数 fseek()

函数调用形式:

fseek ( 文件指针,offset,base );

功能:移动文件位置指针到指定位置上,后续的读写操作就从这里开始。

参数:offset 代表位移量,是一个 long 型数据,它表示文件位置指针相对于起始点移动的字节数,如果 offset 是一个正数,表示从起始点向文件尾方向移动;如果是一个负数,表示从起始点向文件头方向移动。base 表示起始点,用以指定位移量是以哪个位置为基准,起始点不能任意设定。

(7)获取文件位置指针当前位置的函数 ftell()

函数调用形式:

长整形数据 = ftell ( 文件指针 );

功能:该函数返回一个长整型的数,表示当前文件位置指针相对于文件开头的字节数。【注意】是字节数

(8)“反绕函数” rewind()

函数调用形式:

rewind ( 文件指针 );

功能:使文件位置指针回到文件开头处。该函数等价于” fseek ( fp, 0L, SEEK_SET );“

黑客/网络安全学习路线

今天只要你给我的文章点赞,我私藏的网安学习资料一样免费共享给你们,来看看有哪些东西。

网络安全学习资源分享:

最后给大家分享我自己学习的一份全套的网络安全学习资料,希望对想学习 网络安全的小伙伴们有帮助!

零基础入门

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

读者福利 | CSDN大礼包:《网络安全入门&进阶学习资源包》免费分享 (安全链接,放心点击)

1.网络安全学习路线图

要学习一门新的技术,作为新手一定要先学习成长路线图,方向不对,努力白费。

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图&学习规划。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。

2.视频教程

网上虽然也有很多的学习资源,但基本上都残缺不全的,这是我自己录的网安视频教程,上面路线图的每一个知识点,我都有配套的视频讲解。

技术文档也是我自己整理的,包括我参加大型网安行动、CTF和挖SRC漏洞的经验和技术要点,电子书也有200多本【点击领取技术文档】

(都打包成一块的了,不能一一展开,总共300多集)

3.技术文档和电子书

技术文档也是我自己整理的,包括我参加大型网安行动、CTF和挖SRC漏洞的经验和技术要点,电子书也有200多本【点击领取书籍】

4.工具包、面试题和源码

“工欲善其事必先利其器”我为大家总结出了最受欢迎的几十款款黑客工具。涉及范围主要集中在 信息收集、Android黑客工具、自动化工具、网络钓鱼等,感兴趣的同学不容错过。

最后就是我这几年整理的网安方面的面试题,如果你是要找网安方面的工作,它们绝对能帮你大忙。

这些题目都是大家在面试深信服、奇安信、腾讯或者其它大厂面试时经常遇到的,如果大家有好的题目或者好的见解欢迎分享。

参考解析:深信服官网、奇安信官网、Freebuf、csdn等

内容特点:条理清晰,含图像化表示更加易懂。

内容概要:包括 内网、操作系统、协议、渗透测试、安服、漏洞、注入、XSS、CSRF、SSRF、文件上传、文件下载、文件包含、XXE、逻辑漏洞、工具、SQLmap、NMAP、BP、MSF…

读者福利 | CSDN大礼包:《网络安全入门&进阶学习资源包》免费分享 (安全链接,放心点击)

2025-05-09 22:54:51