c_note

一、变量

1、变量的声明

​ 变量的声明是给编译器看的,告诉编译器变量的类型以及名字等相关的信息。

格式:数据类型 变量名;

变量的声明显然是一个编译时概念,所以它和程序的运行没有太大的关系。

一个变量具有声明,并不意味着该变量会在运行时期分配内存空间。

2、变量的定义

​ 格式:数据类型 变量名;

变量的定义=变量的声明+确定该变量在运行时期分配内存空间

变量的定义一定是变量的声明,但反过来,并不是所有的变量声明都是变量定义

注意:某些变量的声明,并不是变量的定义,意味着某些声明语法仅是告诉编译器存在某个变量,但运行时期不会分配内存空间。

3、变量的初始化

​ 初始化是变量的第一次赋值。

4、变量的赋值

​ 初始化一定是赋值,但赋值不是初始化。C语言中的赋值专指在变量已经初始化赋值以后,再次赋值。

二、未定义行为

1、定义

​ 在标准C语言(ISO-C)当中,对于使用未初始化局部变量的后果,并没有明确规定,而是由具体的编译器实现自由的决定在C语言当中,把这种语法称之为“未定义行为”。编译器在实现时,可以自由的决定:编译错误,运行崩溃,给出一个错误的莫名其妙的结果,或者给出一个看起来正确的结果..

​ 在C语言中,局部变量的初始值是最特殊的。若一个局部变量只有定义,没有手动初始化赋值那么它的取值是一个"随机值”

2、随机值是怎么来的?为什么值不确定?

​ 在内存分配时,分配的内存可能被使用过,此时内存会残留一些垃圾数据,未被清理这些残留的垃圾数据就是随机值,随机值显然不知道具体的取值。具有随机值的局部变量显然是不可用的。

3、常见未定义行为

​ 1)同一个变量在同一个条语句之间只能被修改一次,如果多次修改会引发未定义行为。 如:a = a++;

​ 2)左移时如果发生了数据溢出将产生未定义行为。

​ 3)局部变量仅声明。

​ 4)通用指针转错类型。

三、程序的执行过程

​ C语言程序从源码到可执行文件的过程总的来说分为两大步:一、编译 二、链接

1、编译(广义上):预处理、编译、汇编

a、预处理阶段

​ 由预处理器来主要完成两件事情: 1.执行预处理指令 2.去掉代码中的注释。

预处理指令:在c语言中以#开头的指令就是预处理指令。

​ 有两种最常见的形式:1. #include:用于包含头文件。 2. #define:用于定义宏或者常量。

​ 1) #include :找到该头文件,把其中内容处理后复制到指令所在位置。

​ 2) #define : 在C语言中,用于定义宏,主要包括定义宏函数,以及宏常量。被#define定义的常量,也被称为符号常量。在预处理阶段体现为“文本替换”。

宏函数表达式中,每个参数都必须用小括号括起来,每一个想要优先计算的表达式都要用小括号括起来。

---》预处理后的文件image-20250101121415706

​ 3)预处理得到的.i和.c文件一样,同样都是C语言的源代码文件。预处理后得到的.i文件是一个无预处理指令,无注释以及无宏定义的源代码文件。这个文件的内容人肉眼是完全可以看懂的。

b、编译(狭义)

​ 在预处理完成后,编译器(Compiler)就开始处理预处理生成的.i文件了,这个过程就被称为(狭义上的)编译

​ 编译器在这个过程,主要完成以下工作:

​ 1)进行词法分析、语法分析、类型检查等操作。

​ 2)编译器还在此阶段对代码进行各种优化,以提高效率,减少最终生成代码的大小。

​ 3)最终,将预处理后的源代码转换成汇编语言。也就是将.i文件转换成.s文件

c、汇编

​ 编译器完成编译后,汇编器(Assembler)会将这些汇编指令转换为目标代码(机器代码),生成了一个.o(或者.obj)文件。从这一步生成的文件开始,文件中的内容就不是程序员能够肉眼看懂的文本代码了,而是二进制代码指令。

所以汇编过程的主要作用就是将汇编语言代码转换成机器语言,即转换成机器可以执行的指令,也就是生成.o文件。

.o文件还不是一个能直接执行的文件,因为它可能依赖于其他外部代码,比如在代码中调用printf函数。

预处理阶段不是已经包含头文件,为什么还说要依赖外部代码?

​ 头文件中往往只有函数的声明,包含头文件大概只意味着预处理器告诉编译器:“这里有一个函数叫做printf),它大概是这个鬼样子,后续的代码中会用到这个函数。你先知道有这么个玩意,别给我报错,至于这个函数到底是干啥的,做什么的,你先别管。”
​ 所以汇编后的代码,还需要经历一个链接的步骤,来依赖外部代码才能够真正的运行。

2、链接

​ 在链接阶段,链接器(Linker)会把项目中,经过汇编过程生成的多个(至少有1个).o文件和程序所需要的其它附加代码整合在一起,生成最终的可执行程序。

比如你在代码中调用了标准库函数,那么链接器会将库中的代码包含到最终的可执行文件中。

四、编译和链接错误

VS的debug模式用于调试运行时的错误,而不是编译或链接错误。

1、编译(广义)错误(C compile)

大多数编译错误,实际上是疏忽导致的语法错误。

​ VS中的C语言代码编译是由内嵌的编译器MSVC完成的,编译错误的报错信息也是这个编译器给出的,然后通过VS的图形界面显示。(红色)

​ 出现在代码的链接阶段,表示链接失败。链接错误大多和函数调用有关。

​ 链接的错误信息由链接器给出的。

为什么由链接器发出链接错误呢?而不是编译错误?

​ 用通俗的话来说,C语言为了能够让程序员调用外部的函数(比如库函数),所以在编译阶段不检查函数的定义,最多检查声明。检查函数是否有定义这个事情,是链接器做的。

经典链接错误----------->

链接器会将源代码中所有可能存在的外部引用进行解析,此时如果:

​ a、调用了一个完全不存在的函数或者写错了函数名

​ b、忘记包含头文件

​ c、...

链接器就无法真正将函数的定义(实现)合并到目标文件中,此时链接器就会报错。(不报错不行了,再不报错程序就执行了,但此时代码显然无法执行)

五、缓冲区

​ 无论哪种类型的缓冲区,当缓冲区满了时都会触发自动刷新。

1、全缓冲区

​ 全缓冲区,也叫满缓冲区。顾名思义,仅当缓冲区达到容量上限时,缓冲区才会自动刷新,并开始处理数据。否则,数据会持续积累在缓冲区中直到缓冲区满触发自动刷新。 文件操作的输出缓冲区便是这种类型的经典例子。

2、行缓冲区

​ 缓冲区一旦遇到换行符,缓冲区就会自动刷新,所有数据都会被传输。 stdout缓冲区就是典型的行缓冲区。

3、无缓冲区

​ 无缓冲区,不缓冲。在此模式下,数据不经过中间的缓冲步骤,每次的输入或输出操作都会直接执行。这种方法适用于需要快速、实时响应的场合。例如,stderr(标准错误输出)就是这种方式,它经常被用来即时上报错误信息。

注意:输出缓冲区中的数据需要刷新才能输出到目的地,但输入缓冲区通常不需要刷新,强制刷新输入缓冲区往往会引发未定义行为。

六、输入输出函数

1、printf

​ printf函数的核心作用是将各种类型的数据转换为字符形式并输出到stdout缓冲区中。为了增加输出的实时性和可预测性,一个常见策略是在输出字符串的末尾添加换行符‘\n’。

​ 在不影响程序的逻辑的前提下,调用printf函数的格式字符串应当总是以换行符"\n"结尾。


公式( [ ]为可选内容 ):

%[标志][字段宽度][.精度][长度]说明符

解释:

​ a、"%"是转换说明的开始,必不可省略。

​ b、[标志]用于决定一些特殊的格式,常见的标志有:

​ 1)-:左对齐输出。如果没有该标志,输出默认是右对齐的。

​ 2)+:输出正负号。对于正数,会输出+,对于负数,会输出-。

​ 3)0:当输出宽度大于实际数字的字符数量时,使用0而不是空格来填充。

​ 4)空格:当数值为正时,在数值前面添加一个空格,而负数则添加-。如果同时使用了+标志,+标志会覆盖空格标志。

​ c、[字段宽度]用于指定输出的最小字符宽度,但不会导致截断数据:

​ 1)如果输出的字符,宽度小于指定的宽度,那么输出的值将会按照指定的[标志]来进行填充。若标志位没有0,则会填充空格。

​ 2)如果输出的字符,宽度大于指定的宽度,那么printf函数并不会截断,而是完全输出所有字符。

​ d、[.精度]定义打印的精度:

​ 1)对于整数,表示要输出的最小位数,若位数不足则左侧填充0。

​ 2)对于浮点数,表示要在小数点后面打印的位数。

​ -当有效数字不足时,会自行在后面补0
​ -当有效位数超出时,会截断保留指定的有效位数。这个过程一般会遵守"四舍五入"的原则。
-但由于浮点数存储的固有精度问题,某些数值可能不能完美表示,导致结果中的数字稍有偏差。
-注意在不指定[.精度]的情况下,浮点数默认显示6位小数,多的部分舍弃,不够的话,会在后面补0。

​ e、[长度]主要描述参数的数据类型或大小。常见的长度修饰符有:

​ 1)h : 与整数说明符一起使用,表示short类型。

​ 2)l (小写的L): 通常与整数或浮点数说明符一起使用,表示long(对于整数)或double(对于浮点数)。

​ 3)ll (两个小写的L): 与整数说明符一起使用,表示long long类型的整数。

​ 4)L (大写的L): 与浮点数说明符一起使用,表示long double。

​ f、说明符,必不可省略。描述如何格式化和显示该参数。常见的说明符有:

​ 1)di : 表示有符号的十进制整数。

​ 2)u:表示无符号的十进制整数。

​ 3)o:表示无符号的八进制整数。

​ 4)x:表示无符号的十六进制整数,使用小写字母(例如:a-f)。

​ 5)X:表示无符号的十六进制整数,使用大写字母(例如:A-F)。

​ 6)f, eE : 浮点数。
​ -e:强制用科学计数法显示此浮点数,使用小写的“e”表示10的幂次。
​ -E: 强制用科学计数法显示此浮点数,使用大写的“E”表示10的幂次。
​ -printf中 %f 和% lf 输出的数据是一样的,等价的。不管参数是float还是double都会自动提升到double处理。但是要注意,这个等价仅限于printf函数,scanf函数没有这样的特点! scanf函数的%f和%lf转换说明,是完全不同的。

​ 7) gG : 选择最合适的表示方式,浮点数或科学记数法。
​ -g,当选择使用科学计数法显示此浮点数时,使用小写的“e”表示10的幂次。
​ -G,当选择使用科学计数法显示此浮点数时,使用大写的“E”表示10的幂次。
​ 8) c : 字符。

​ 9) s : 字符串。纯粹打印字符串一般不需要用转换说明,直接使用普通字符输出即可。

​ 10)p : 指针。

运行中的程序自动确定输出的宽度和小数点位数:

image-20250101131635680

printf :函数的返回值是一个 int 类型:

​ 1)在输出成功时,返回值表示函数实际输出的字符总数(转义字符也算。不包括末尾的空字符)。即输出成功时,返回值是一个非负数,最小也是0。

​ 2)如果输出由于某些原因失败,那么返回值将会是一个负数。


2、scanf

scanf函数本质上是一个"模式匹配"函数,试图把"stdin缓冲区"中的字符与格式字符串匹配。

scanf函数的转换说明符大都默认忽略前置的空白字符(空白字符要紧挨着转换说明符)。注意:不忽略后置空白字符。

如:%d 和 %f

但录入字符数据特殊,%c能识别空格,所以要自己在scanf中手动加空白字符。就是在%c前加个空格。

它的作用是能吸收掉任意数量的空格、制表符(tab)或换行。

注意:在一个scanf 后,紧接着又来了个scanf而且是要录入字符型,想要录入后面scanf数据要先清空缓冲区,因为你在第一个scanf时录入了回车。清空缓冲区的两种办法:①是在第一个scanf后写以下语句。while ( getchar() != '\n' ); //用getchar()吸掉'\n' ②是在第二个scanf双引号内第一个字符写成空格。

5 c 或 5c都可以正常录入。

原因:scanf从sdtin读数据,读完整数后当碰到空格时,scanf中的空白字符会吸收掉它,当读完整数后没有碰见空格,而是碰到了字符c,那么此时空白字符就是可有可无状态,不影响字符c的读取。


公式( [ ]为可选内容 )

%[*][宽度][长度]说明符

解释:

​ a、 [*]也称之为赋值抑制:当使用该符号时,对应的输入会被读取,但不会存储到任何变量中。例如,使用"%*d"意味着会读取一次输入,但此输入完全无效不会赋值到对应变量。

​ b、[宽度]:表示要读取的最大字符数量。例如,"%5d"意味着读取最多5个字符来解析为一个整数。

​ c、[长度]:描述参数的数据类型或大小。常见的长度修饰符有(和printf函数一致):

​ e、说明符:这是必不可少的部分,描述如何解析输入数据。

​ 大部分和printf一样,只有一个不同:‘ i ’

​ -printf函数的i和d都是等价的,都表示输出有符号的十进制整数。

​ -但scanf的i会自动判断输入的整数的进制,从而进行不同的录入。支持十进制、八进制、十六进制整数。

特殊:

1、%[字符集]:这告诉scanf只接受和存储来自指定字符集的字符。字符集是直接写在 [ 和 ] 之间的。例如,%[abc]将只读取'a', 'b', 或 'c'字符,其他的字符将导致读取停止。

2、%[^字符集]:这是扫描集的否定形式,告诉scanf接受和存储除了指定字符集之外的所有字符。^字符放在 [ 之后立即表示否定。例如,%[^abc]将读取除了'a', 'b', 和 'c'之外的所有字符,直到遇到这三个字符中的任何一个为止。


七、隐式转换

​ 1、当给变量赋值时,如果赋值表达式右边值的类型和左边变量的类型不匹配,则赋值表达式右边值的类型会被隐式转换为左边变量的类型。

​ 2、当函数调用时实参类型与形参类型不匹配,传递的实参会被隐式转换为形参类型。

​ 3、在使用return返回时,如果return后面的值的类型和返回值类型不匹配,也会隐式转换为返回值类型。

​ 4、当不同类型的参数共同组成一个表达式时,为确保表达式最终结果的类型是唯一的,编译器会自动进行隐式类型转换。两个不同类型的数发生运算,类型小的数转换为大类型,结果也为大类型。

注意:同一转换等级的有符号整数和无符号整数一起参与运算时,有符号整数会转换成对应的无符号整数。

八、size_t 和 ssize_t 类型

1、size_t

​ size_t 在任何平台下,都代表一个无符号的整数类型。在大多数情况下,它被设计为和平台位数一致的存储大小,比如32位平台下,它就是一个32位的无符号整数。

2、ssize_t

​ 在Linux环境,ssize_t 是一个与平台相关的有符号整数类型。它的大小取决于平台的位数:

​ a)在32位平台上,ssize_t 通常是一个32位的有符号整数。

​ b)在64位平台上,ssize_t 通常是一个64位的有符号整数。

可以把ssize_t视为size_t的有符号版本。但需要注意的是:ssize_t类型仅在Linux环境下使用,size_t则是C语言标准的一部分,是通用的。

九、运算符

注意:位运算符只能用于整数。

优先级 运算符 描述 结合性
0 小括号() 最高优先级 从左到右
1 ++ -- 后缀自增与自减,即a++这种 从左到右
小括号 () 函数调用的小括号
[] 数组下标
. 结构体与联合体成员访问
-> 结构体与联合体成员通过指针访问
2 ++ -- 前缀自增与自减,即--a这种 从右到左
+ - 一元加与减,表示操作数的正负
! ~ 逻辑非与按位非
(type_name) 强制类型转换语法
* 解引用运算符
& 取地址运算符
sizeof 取大小运算符
3 * / % 乘法、除法及取余运算符 从左到右
4 + - 二元运算符的加法及减法
5 << >> 左移及右移位运算符
6 < <= 分别为 < 与 ≤ 的关系运算符
> >= 分别为 > 与 ≥ 的关系运算符
7 == != 分别为 = 与 ≠ 的关系运算符
8 & 按位与
9 ^ 按位异或
10 ` ` 按位或
11 && 逻辑与
12 ` `
13 ? : 三目运算符 从右到左
14 = 简单赋值
+= -= 和及差复合赋值
*= /= %= 积、商及余数复合赋值
<<= >>= 左移及右移复合赋值
&= ^= ` =` 按位与、异或及或复合赋值
15 , 逗号 从左到右

十、ASCII码表

ASCII值 控制字符 ASCII值 控制字符 ASCII值 控制字符 ASCII值 控制字符
0 NUT 32 (space) 64 @ 96
1 SOH 33 ! 65 A 97 a
2 STX 34 66 B 98 b
3 ETX 35 # 67 C 99 c
4 EOT 36 $ 68 D 100 d
5 ENQ 37 % 69 E 101 e
6 ACK 38 & 70 F 102 f
7 BEL 39 , 71 G 103 g
8 BS 40 ( 72 H 104 h
9 HT 41 ) 73 I 105 i
10 LF 42 * 74 J 106 j
11 VT 43 + 75 K 107 k
12 FF 44 , 76 L 108 l
13 CR 45 - 77 M 109 m
14 SO 46 . 78 N 110 n
15 SI 47 / 79 O 111 o
16 DLE 48 0 80 P 112 p
17 DCI 49 1 81 Q 113 q
18 DC2 50 2 82 R 114 r
19 DC3 51 3 83 S 115 s
20 DC4 52 4 84 T 116 t
21 NAK 53 5 85 U 117 u
22 SYN 54 6 86 V 118 v
23 TB 55 7 87 W 119 w
24 CAN 56 8 88 X 120 x
25 EM 57 9 89 Y 121 y
26 SUB 58 (冒号) 90 Z 122 z
27 ESC 59 ; 91 [ 123 {
28 FS 60 < 92 / 124 |
29 GS 61 = 93 ] 125 }
30 RS 62 > 94 ^ 126 `
31 US 63 ? 95 _ 127 DEL

十一、一维数组

1、初始化

​ a)完整初始化:为数组的所有元素指定值。

int arr[5] = {1, 2, 3, 4, 5};

​ b)自动推断数组长度:在声明时省略数组大小,让编译器根据初始化的元素数量自动确定数组的大小。

int arr[] = {1, 2, 3, 4, 5}; // 数组长度自动设置为5

​ c)部分初始化:为数组的部分元素指定值,剩余的元素会自动初始化为0值。

int arr[5] = {1, 2, 3}; // arr的元素为 {1, 2, 3, 0, 0}

​ d)全部元素为0的初始化:可以使用{0},来初始化数组的所有元素为0。

int arr[5] = {0};

2、一些错误的初始化

​ a)那些可能在程序运行时期确定大小的变量,const修饰的只读变量(不是编译时常量),都是不能作为数组长度的:

int len = 10;
const int len2 = 20;
int arr[len]; // ERROR
int arr2[len2]; // ERROR

​ b)C语言也不允许数组的长度是0,以下声明是错误的:

int arr[0]; // ERROR

​ c)初始化包含了比数组长度更多的元素:

int arr[2] = { 1,2,3 }; // ERROR

​ d)初始化时,{}内不给出任何元素。这是不对的,C语言不允许数组长度是0:

int arr[3] = {}; // ERROR

3、获取数组长度的惯用法

#define ARR_LEN(arr) (sizeof(arr) / sizeof(arr[0]))
...
for (i = 0; i < ARR_LEN(arr); i++){
printf("%d", arr[i]);
}

4、局部变量数组

1.任何声明定义在函数体内部的局部变量数组,它们的作用域仅限于 { } 内部

⒉任何仅声明不手动初始化的局部变量数组,都是不可以直接使用,因为此时数组元素都是随机未定义的值。

3.局部变量的数组,它的内存空间,申请在栈上,也就是存储在虚拟内存空间的栈中

栈的优点是:性能强大,自动管理内存

缺点是:栈普遍非常小,占用极小的内存空间

编译器是可以调整栈的大小的,在默认情况下:

a.MSVC平台下,栈的大小是1Mb

b.gcc编译器平台下,栈的大小是8Mb

基于这样的特点,我们很容易就得出结论:栈上不能分配大的数据结构,比如不能分配大数组一旦分配过大的空间,导致栈区域不够用,这就产生了栈溢出(stack overflow)错误

十二、二维数组

1、初始化

​ a)完整初始化:

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

​ b)省略(矩阵)行数:

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

​ c)部分初始化:

如果只初始化部分元素,其他未指定的元素会默认初始化为0(对于基本数据类型)。

int arr[3][4] = {
{1, 2},
{5, 6}
};

这里,只有arr[0][0]、arr[0][1]、arr[1][0]、arr[1][1]被初始化,其他元素的值都是0。(arr[2]的所有元素都是0)

​ d)初始化为0值:

如果希望所有元素都初始化为0值,则按照下列方式:

int arr[3][4] = {
{ 0 },
};

2、要注意的问题

​ 与一维数组类似,二维数组中的元素,如果没有经过初始化,它们的值是不确定的。直接访问这些未经初始化的元素可能会导致程序行为异常。因此,一旦声明了二维数组,建议尽快对其所有元素进行初始化操作。

十三、const关键字

1、定义常量

​ const 定义常量

const int a = 10;
a = 20; // ERROR 修改const常量,导致编译错误

被const修饰的变量不能直接用变量名进行修改,使得它看起来无法被"修改"

2、常量数组

const int array[5] = {1, 2, 3, 4, 5};
array[0] = 10; // 编译错误

​ 该数组被初始化,其元素就不能再被修改了

​ 用const修饰的常量也不能初始化数组。

用const修饰的常量可以通过scanf修改。

十四、变量分类

image-20250105101031692

1、局部变量

a) 存储在栈帧中。

b) 局部变量没有默认0值,必须手动初始化。

c) 在C语言中,把局部变量依赖函数栈帧存在和销毁,这样的生命周期,称之为"自动存储期限"。意为:依赖函数调用,自动管理的生命周期,无需手动管理。

d) C语言函数栈帧的大小并不是运行时计算的,为了保证最好的运行时性能,栈帧大小都是编译时期确定和计算的.

这样设计的直接后果是:

栈区的数据都必须在编译时期确定大小和长度,没有任何动态性比如:

a.数组长度不能是变量

b.动态伸缩的数据结构不能在栈上申请,比如链表
...

总之,在C语言中,局部变量不能是动态的数据,不能是运行时期确定长度大小的数据。

2、全局变量

a) 存储在"数据段"内存区域当中。

b) 若未初始化,则设为默认值 0 。

c) 全局变量与程序的生命周期同步:它们在程序开始时被创建并初始化,并在程序结束时被销毁。

在C语言中,这种持续存在于程序整个执行周期的生命周期特性被称为“静态存储期限”

所有的存储在数据段当中的数据变量/常量,都具有静态存储期限

d) 全局变量的初始化有且仅有程序启动时的一次。

e) 在其他源文件中使用本文件全局变量,可以通过extern关键字来引用它

main.c 中定义全局变量:int global_num = 10 ;

demo.c 中引用全局变量:extern int global_num ; 这样在demo.c中就可以访问和修改main.c中的全局变量global_num 了。当然,前提是先得把main.c 和 demo.c “连” 起来。

注意:在demo.c文件中,使用extern关键字只是告诉编译器global_num变量在其余文件中定义,它并不会创建了一个新的变量。

f) 关于跨文件调用函数,一般步骤如下:

  1. 在一个头文件中声明你想跨文件调用的函数。
  2. 在一个源文件中 #include 头文件,然后定义这个函数。(包含自定义头文件,使用#include "xx.h"
  3. 在另一个源文件中,包含头文件并直接调用该函数

g) 程序应当尽可能的不使用全局变量。

3、静态全局变量

static修饰的全局变量的主要效果是将其作用域限制在声明它的文件中

其它的特性,如生命周期(静态存储期限)、初始化方式和存储位置,与普通的全局变量是相同的。

4、静态局部变量

a) 存储在“数据段”区域当中。

b) 生命周期是从程序启动到程序结束。"静态存储期限"。

静态局部变量不会随着函数的返回而销毁,它会始终保留到程序执行完毕。

c) 有默认初始值 0 。(普通局部变量没有)

d) 只初始化一次。后续无论再调用多少次所在函数,它都不会再初始化了。

实际上跟调不调用所在函数没关系。静态局部变量在程序启动时就进行了初始化,也就是说在函数调用之前,静态局部变量的初始值就已经确定了。

e) 作用域:只能在定义它的函数内部被访问

5、静态存储期限变量的初始化方式

a) 静态存储期限变量包括:1. 全局变量 2. 静态局部变量 3. static 修饰的全局变量。

b) 一旦要手动初始化,= 号右侧必须是一个常量表达式

十五、函数的调用

exit 退出函数

1、需要声明<stdlib.h>

2、传递给exit函数的参数和main函数的return返回值具有相同的含义,传递0表示程序正常结束,非0表示程序异常终止。

3、可以这样写:exit(0);

​ 也可以这样写:

exit(EXIT_SUCCESS);
exit(EXIT_FAILURE);

总:不管哪个函数调用exit函数都会导致程序终止,return语句仅当在main函数中执行时才会导致程序的终止。

十六、指针

1、局部指针未初始化,使用时会出现未定义行为。

2、如果实在不知道指针初始值,则设为NULL。不要操作NULL指针。

3、数组指针:

int a[10] = {0};
int (*p)[10] = &a; // &a 代表整个数组
// p解引用一次得到整个数组,再解一次得到数组首地址。
int matrix[5][5] = { {1,2,3,4,5},{6,7,8,9,10},{11,12,13,14,15},{16,17,18,19,20},{21,22,23,24,25} };
// *(*(matrix + 2) + 3) 代表什么
//首先要知道:martix是什么东西?是一个数组指针,它指向了一个一个的数组。matrix:是数组指针。指向了第一行的数据。
//matrix + 2:是数组指针,指向11这一行。
//*(matrix + 2):是数组。数组名和指针是等价的,所以我们可以当指针用。
//*(matrix + 2)+ 3:是指针,指向14这个数据
//*(*(matrix + 2)+ 3):是数据。是14这个数据

4、指针数组

int *p[10] ; //里面存int* 。一个元素一个int*。

十七、字符串

1、初始化

char* chr1 = "hello world! word Excel space blank";
char chr2[] = "hello world! word Excel space blank"; //自动补了个\0
char chr3[] = { 'h','e','l','l','o',' ','w','o','r','l','d','!',' ','w','o','r','d',' ','E','x','c','e','l',' ','s','p','a','c','e',' ','b','l','a','n','k','\0'}; //必须手动写个\0
char chr4[40] = "hello world! word Excel space blank"; //多出部分自动补\0
char chr5[40] = { 'h','e','l','l','o',' ','w','o','r','l','d','!',' ','w','o','r','d',' ','E','x','c','e','l',' ','s','p','a','c','e',' ','b','l','a','n','k'}; //多出部分自动补\0

2、输入输出方式

输入:

1.scanf :

​ a) scanf函数在表示录入字符串时,会在录入结束后,在字符信息的后面自动存储一个空字符。当录入字符数正好等于字符数组大小时,程序出现越界错误。

​ b) 利用scanf录入的字符串,永远不可能包含空白字符(空格,制表等),因为遇到空白字符它就会结束录入。

​ c) 利用%9s 限制读取字符。

char str[10];
printf("请键盘录入一个字符串: \n");
scanf("%9s", str); // 限制最多读取9个字符。注意:str不带&符

2.gets:

char* gets ( char * str );

使用:

gets(str); // str是一个存储数据的字符数组

​ a) gets不会跳过前面的空白字符,也不会遇到空白字符停止录入。gets在碰到换行符时才会停止录入!总之就是完整的录入一整行键盘输入!

​ b) gets函数使用方便且高效,但gets函数完全不能对录入数据的长度做出限制,相对于scanf,它更加的不安全

3.fgets:

fgets(str, sizeof(str), stdin);

​ a) 同样的录入一整行字符信息,不会跳过前面的空白字符,遇到空白字符也不会停止录入。碰到换行符才会停止录入!

​ b) 若str数组的长度是10,此函数调用最多会将9个字符存储到字符数组中,因为会留一个位置存空字符'\0'。所以此函数是安全的,不会导致数组越界,访问非法数据。

​ c) fgets函数和gets有一个非常大的不同是:

​ 1、fgets函数在容量充足的情况下,会将换行符'\n'存入字符数组,再存一个空字符。比如[ 'h', 'e', 'l', 'l', 'o', '\n', '0']

​ 2、而gets函数则不会存储换行符,只会存储一个空字符。比如[ 'h', 'e', 'l', 'l', 'o', '0']

​ d) 去掉fgets中录入的换行符:

// str是fgets函数录入数据的一个字符串,空字符前可能存在一个换行符
int len = strlen(str); // strlen获取当前字符串的长度,也就是空字符前面字符的个数
// len作为str数组的下标的话,空字符的下标
// 换行符如果有,那肯定是字符串最后一个有效字符
if (len > 0 && str[len - 1] == '\n') {
str[len - 1] = '\0';
}

输出:

1.printf

​ a) 循环输出:

for (int i = 0; str[i] != '\0'; i++) {
printf("%c\n", str[i]);
}

​ b) 常规方式输出字符串

printf("%s\n", str);
printf("%.5s\n", str); //此时printf函数会打印最大5个字符长度

2.puts

int puts(const char *s);

​ a) 函数执行成功时返回一个非负数,失败时一般会返回-1。

puts(str); // 等价于 printf("%s\n", str);

​ b) 几点需要注意:

​ 1、puts() 只能用于输出字符串,而 printf() 可用于输出各种数据类型。

​ 2、puts() 函数需要传入一个字符数组作为参数,若传入的字符数组无法表示字符串,会引发未定义行为。

3、puts() 在字符串后自动添加换行符,而 printf() 则不会,除非明确添加了 \n。

​ 4、puts() 通常比 printf() 快,因为它不需要解析格式字符串。

​ 5、printf() 显然更加灵活,因为它支持格式化输出。

3、几个C语言字符串库

前提是一定要先引用 <string.h> 库。

strlen

size_t strlen (const char *s);

作用是:返回当前字符串的长度,也就是字符数组中空字符'\0'前面的字符数量。

strlen 参数必须是字符串,否则报错。

strcpy

char *strcpy(char *dest, const char *src);

作用:把src字符串复制到dest,并在字符串末尾补个'\0'。

问题:不安全。没有考虑dest放不放得下。

strncpy

char *strncpy(char *dest, const char *src, size_t n);
char *mystrncpy(char *dest, const char *src, size_t n) {
int flag = 1;
for (size_t i = 0; i < n; i++) {
//*(dest + i) = *(src + i);
if (flag == 1 && *(src + i) != '\0') {
*(dest + i) = *(src + i);
}
else {
*(dest + i) = '\0';
flag = 0;
}
}
}

​ a) src 很长,dest比较小时。

​ 当 n == sizeof(dest) 时,dest不再是字符串,变成简单字符数组。

​ 当 n> sizeof(dest) 时,确实会复制过去,但此时出现越界异常。

​ 当 n< sizeof(dest) 时,会复制过去n个字符,但不会在这n个字符后补'\0'。

​ b) dest充足时,

​ 当 n> strlen(src) 时,会把src复制过去,n - strlen(src) 部分变'\0'。

​ 当 n<= strlen(src) 时,会复制过n个字符,但末尾不会有'\0'。

更安全的写法:

strncpy(dest, src, sizeof(dest) - 1); //(防止出现越界错误)
dest[sizeof(dest) - 1] = '\0'; //手动补个'\0',防止因str太长没完全复制过去末位非'\0'

总结:strncpy中,n是高度控盘的。n是几,就复制几个字符进dest,n多出部分补'\0'。不多不少正正好好(n == strlen(str)),则不会补'\0'。

strcat

char *strcat(char *dest, const char *src);

​ a) 首先找到dest字符串的结尾,也就是找到空字符。

​ b) 然后将src字符串的内容包含空字符在内,从dest的空字符开始,复制到dest的结尾。

strncat

char *strncat(char *dest, const char *src, size_t n);
char *my_strncat(char *dest, const char *src, size_t n) {
char *ret_result = dest;
// dest [20] "abc" "abc1234\0"
// src [20] "123456"
// 第一步,先找到 dest的 \0。 是从这个位置开始拼接的。
while (*dest != '\0'){
dest++;
}
for (size_t i = 0; i < n; i++){
if (*(src + i) != '\0') {
*dest++ = *(src + i);
}
else {
break;
}
}
// n这时候有两种情况。
*dest = '\0';
return dest;
}

总结:n >= strlen( src ) 才会把src完全连过去。n无论为何值,整个字符数组只有一个'\0',必是一个字符串。

​ n >= strlen (src) 实际需要 strlen(src) +1 个空间。

​ n < strlen (src) 实际需要 n + 1 空间。

安全写法:

int n = sizeof(dest) - strlen(dest) - 1;
strncat(dest, src, n); //防止src太过大,只截src一部分,同时留一位给strncat补0

十八、结构体

1、结构体类型变量可以当做普通数据类型变量那样来操作。

2、结构体变量不初始化,里面就是随机值。

3、结构体变量只要初始化一部分,剩余部分默认初始化为0。(数组全都存成0)

​ 结构体数组也是一样,只要稍微初始化一点,其余所有元素初始化为0。

复合字面量:

typedef struct {
int num;
} Num;
Num b;
// 初始化没有紧跟声明,不可以直接使用"{}"完成初始化
// b = { 1 };
// 此时初始化b需要加强转标志才能够初始化赋值成功
b = (Num){ 1 }; //符合字面量

十九、指针的高级应用

1、通用指针类型

通用指针类型指的是void类型的指针。

特点: a)可以将任意类型的地址赋值给void指针。

​ b)无法直接操作。直接操作通用指针类型,会产生编译错误。必须把它转换成具体的指针类型。

int a = 42;
void* void_ptr = &a;
// void指针转换成其它类型指针,在C++语法中必须加上显式类型转换说明
// float* float_ptr = (float*)void_ptr;
// 但C语言支持void指针隐式类型转换成各种其它指针类型,所以这个强转语法可加可不加
// 错误的类型转换
float* float_ptr = void_ptr;
// 解引用产生未定义行为
printf("%f\n", *float_ptr);
//正确的类型转换
int *int_ptr = void_ptr; // 或者: int * int_ptr ;
// int_ptr = void_ptr;

2、malloc

a) 声明:

void* malloc(size_t size); // 参数为“多少”个字节

b) 此函数会在堆空间上分配一片连续的,size个字节大小的内存块

c) 此函数不会对内存块中的数据进行初始化,内存块中的数据是随机未定义的。

d) 如果分配成功,此函数会返回指向该内存块地址(首字节地址)的指针。注意返回的指针类型是void指针,在操作之前需要进行转换。

e) 如果分配失败,此函数会返回一个空指针(NULL)

f) 使用示范:

int* arr_p = malloc(ARR_LEN * sizeof(int)); // 一定要用sizeof(int),而不是直接写4,因为不同平台int大小可能不同
// 检查malloc是否成功,即检查arr_p是否是一个空指针
if (arr_p == NULL) {
printf("memory allocation failed!\n");
return -1; // 也可以exit(-1);结束程序
}

3、calloc

a) 声明:

void* calloc(size_t num, size_t size); // 参数为申请多少个 和 每个多大(单位字节)

b) 返回值在分配成功和失败时,和malloc是一致的。

c) 初始化为零 char 类型数组,每个元素都是'\0'。

4、内存泄漏

内存泄漏是指程序在运行过程中,未能适时释放不再使用的内存区域,导致这部分内存在程序的生命周期内始终无法被重用。

5、free函数

a) 声明:

void free(void *ptr);

b) 参数必须是堆上申请内存块的地址(首字节地址),不能传递别的指针,否则会引发未定义行为。

c) free 函数的行为是:

​ 1、free函数并不会修改它所释放的内存区域中存储的任何数据。free 的作用仅仅是告诉操作系统这块内存不再被使用了,可以将其标记为可用状态,以供将来的内存分配请求使用。

​ 2、释放后的内存区域中的数据一般仍然会继续存在,直到被下一次的内存分配操作覆盖。当然即便free前的原始数据一直存在未被覆盖,这片内存区域也不再可用了,因为你不知道什么时候数据就会被覆盖掉了。

d) free 使用的注意事项:

​ 1、正确传参free函数。传入指向动态分配内存块首字节的指针。

​ 2、在free内存块后,建议立刻将指针设置为NULL。 ①是可以避免“double free”,对于空指针调用free 函数是安全的,不会有任何效果。② 减少悬空指针出现的风险。解引用空指针导致程序崩溃,比悬空指针带来的未定义行为要更容易检测和修正。

​ 3、慎重改变堆区指针的指向。改变之前应当考虑指向的内存块是否需要free。

​ 4、多函数共同管理同一块内存区域时,应严格遵循单一原则。尤其是,哪个函数用于分配内存,哪个函数用于free释放内存,这两个函数一定要明确单一的职责。

6、realloc

a) 声明:

void* realloc(void* ptr, size_t new_size);
// ptr:指向原来已分配内存的内存块。
// new_size:新的内存块大小。注意这里单位是字节。

如果ptr指针是一个空指针,那么该函数的行为和malloc一致 :分配new_size字节的内存空间,并且返回该内存块的指针

b) realloc用于重新调整已分配内存块的大小(也就是ptr指针指向的已分配内存块的大小):

  1. 当new_size的取值和已分配的内存块大小一致时,此函数不会做任何操作。
  2. 当new_size的取值比已分配的内存块小时(新内存块比旧内存块小时),会在旧内存块的尾部(高地址)截断,被截断抛弃的内存块会被自动释放。
  3. 当new_size的取值比已分配的内存块大时(新内存块比旧内存块大时),会尽可能地尝试原地扩大旧内存块(这样效率高);
  4. 如果无法原地进行扩大,则会在别处申请空间分配new_size大小的新内存块,并将旧内存块中的数据全部复制进去后,将旧内存块自动释放。
  5. 不管采用哪种方式扩展旧内存块,新扩展部分的内存区域都不会初始化,仍只具有随机值。

c) 如果realloc函数分配内存空间成功,它会返回指向新内存块的指针,若失败,仍会返回空指针,且不会改变旧内存块。

总之,realloc函数适用于调整已分配内存块的大小,特别是在动态数组或数据结构的大小需要在程序运行时增加或减少时使用。

7、函数指针

用于存储函数地址。最常见的用途就是——"将函数作为参数传递"。 这个作为参数的函数称为:“回调函数”。

a) 函数指针的声明(注意:“声明“就意味着去掉;号加上赋值号 = 就是进行初始化了):

函数返回值类型 (*函数指针名)(函数形参列表);

​ 示例:(注意:指针名随意起,形参只需照抄类型)

// 声明一个指向"返回值类型是void、不接受任何参数的函数"的指针
void (*fun_ptr)(void);
// 声明一个指向"返回值类型是int、接受两个int参数的函数"的指针
int (*fun_ptr2)(int, int);
// 声明一个指向"返回值类型是char指针、接受一个const char指针参数的函数"的指针
char* (*fun_ptr3)(const char *);

b) 给函数指针起别名:

typedef 函数返回值类型 (*函数指针别名)(函数形参列表);

​ 一般写在头文件中,或者源文件的上面。

c) 初始化方式

void test1(void){
.........
}
void test2(int a,int b){
printf("a = %d , b = %d \n",a,b);
}
int test03(int a){
return a + 1;
}
typedef void (*FuncVoidNoReturnPtr)(void);
typedef void (*FuncTwoIntNoReturnPtr)(int,int);
int main(void){
FuncVoidNoReturnPtr ptr1 = NULL;
FuncVoidNoReturnPtr ptr2 = test1;
FuncVoidNoReturnPtr ptr3 = &test1; //加不加&都可以。
ptr2(); // 调用了test1 函数
FuncTwoIntNoReturnPtr ptr4 = test2;
ptr4(1,2); //调用test2 函数
int (*test03_func_ptr)(int) = test03;
int res = test03_func_ptr(15); //调用test03 函数
printf("%d \n",res);
return 0;
}

d) 函数指针作为形参:

// test2函数需要传入一个"返回值类型是int、接受两个int参数的函数"的指针
void test2(int fun_ptr(int, int)){
}
void test2(int (*fun_ptr)(int, int)){
printf( "%d \n",fun_ptr(10,20));
}
int aaa(int a,int b){
return a + b;
}
int main(){
test2(aaa);
return 0;
}
//--------------------------------------------
void test1(void){ //放在test7777上边下边都行,但必须放在main上边。
printf("test1\n");
}
typedef void (*FuncVoidNoReturnPtr)(void);
void test7777(FuncVoidNoReturnPtr ptr){
ptr()
}
int main(){
test777(test1);
return 0;
}
posted @   向天呼喊保时捷!~  阅读(5)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示