C语言学习笔记

C语言学习笔记

                                                                                              创建日期:2023‎年‎5‎月‎17‎日,‏‎21:42:57
                                                                                                                               ——ByNUISTWF

基础概念

printf/fprintf均是标准io
\n换行,刷新缓冲区(标准io需要刷新,系统io是实时的)

C源文件-预处理->编译i->汇编s->链接o->可执行文件
程序的编译、链接和运行通常按照以下顺序进行:
1. 编写源代码:程序员使用文本编辑器或集成开发环境(IDE)等工具编写源代码。
2. 预处理(Preprocessing):源代码经过预处理器处理,执行诸如宏替换、头文件包含等操作。预处理器生成一个被编译的源代码文件。
   gcc -E source.c -o preprocessed_source.c
3. 编译(Compilation):预处理后的源代码由编译器转换为汇编代码。
   gcc -S preprocessed_source.c -o assembly_code.s
4. 汇编(Assembly):汇编器将汇编代码转换为目标机器代码,生成一个目标文件。
   gcc -c assembly_code.s -o object_file.o
5. 链接(Linking):将程序的目标文件与所需的库文件链接,生成可执行文件。
   gcc object_file.o -o executable_file
6. 运行(Execution):执行生成的可执行文件。
   ./executable_file
这是一个典型的编译和运行的流程。不同的编程语言和开发环境可能有一些差异,但基本的步骤通常是相似的。

#开头的全在预处理里面被解决掉(替换掉)[也就是#include头文件以及#define宏等替换文件]
/etc/vim$ sudo vim vimrc 里面可以修改,但是尽量不要修改总的,可以复制一份到当前目录下进行修改:cp /etc/vim/vimrc ~/.vimrc,然后编辑:vim ~/.vimrc
12 set tabstop=4                     //设置制表符宽度为 4
13 set shiftwidth=4                  //设置缩进的空格数为 4
14 set nu                                 //加上显示行数
15 set autoindent                    //设置换行自动缩进

在程序代码中shift+k直接可以看man手册里的函数原型,再敲qq回去

gcc test.c -Wall(all warning)调试:打印出所有的警告;经过实验,发现write包含头文件#include <unistd.h>,但不加也能运行成功打印出来,这是个隐含警告即隐含错误!还有malloc包含头文件stdlib.h这个头文件,不包含也能运行,但是是个隐含错误!void可以与任何类型互相赋值
fprintf(stderr, "fopen():%s\n", strerror(errno));没有包含头文件#include<string.h>,char*stderror返回默认均为int类型,所以产生int与%s不匹配问题

return 0;结束当前函数(给父进程看的(int main内一个函数就是给shell看)),exit(0);结束当前进程

echo $?指的是打印出来上一个语句执行结束的状态,若为return 0;则显示0.若为return 1;则显示为1,若函数里面没有return 0;返回的是最后一行输出的字符数包括换行,比如:int main(){printf("hello world!\n");}输出13

#if 0    //代表下面这一段在编译的时候就不参与编译了,gcc看不到这一段
func()
{
 
}
#endif    //用来注释这一段函数

防止写越界,防止内存泄漏,谁打开谁关闭,谁申请谁释放

10进制->2(8)进制,10进制一直除2(8),余数方向排列,如254(10)->11111110(2)=376(8)=FE(16)
2(8)进制->10进制,(0101)=1*2(8)的0次方+1*2(8)的2次方
B是二进制(B11111110),0是八进制(0376),0x是16进制0xFE()
unsigned int32位,254(11111110)二进制的补码就是其本身
-254->绝对值254->11111110 取反加1 = 所获得的结果就是-254的补码形式(补码就是取绝对值取反加1)

float(0.314 * 10^1)以这种类型存储,0-22,23位记录精度部分(小数部分314);23-30,8位记录指数部分,1次方;31为符号位用来判断正负即unsigned;例如:0.000789 -> (小数点后面直接跟精度)0.789 * 10^(-3)
double类型多出来的32位放在了精度控制上(小数部分),所以double的精度比float的高

空洞文件充斥着ascll码为0的字符,还包括字符串最后尾0作为结束标记也是ascll码为0的特殊字符
字符(0->0x30,48);(A->0x41,65);(a->0x61,97)

标准c中,char不一定是有符号类型,也有可能无符号,具体未定义(验证来看可能不能超过127)

(隐式转换)char+int默认往精度高或者所占字长高的那部分靠拢,即=int;float+double=double(精度较高);例如:(char+int)-(float-double)=int-double=double
(显式转换->也就是强制类型转换)

布尔型 bool,只输出俩个值0或者1,假为0,真为1:bool.c
float 类型数无法和一确切的数比较是否相等(float 类型本身并不精确);float就是一大概范围的表示,比如像刻度尺,1跟2,你咋知道1.5在哪,万一那个地方是1.4999呢,面试:float.c,float表示的是一个范围

在Linux和类Unix操作系统中, echo ¥?是一个特殊字符,可以用于表示上一个命令的退出状态码(errno)。这个状态码范围是从0到255,其中0表示命令成功执行,而非0则表示命令执行失败或异常退出。因此,echo ¥?可以用于查看上一个命令的退出状态码。这个状态码是一个非负整数,不能是负数。

不同形式的 0值 (0,'0',"0",'\0',NULL)
数据类型与后续代码中所使用的输入输出要相匹配,即精度大的数值不要当成精度低的来输出,会有损失导致错误:char a = 127;char b = 128;char c = 129;结果:a = 127, b = -128, c = -127


常量:在程序执行过程中值不会发生变化的量(数组名是个常量,分配完已经定死)
1、(int)整型常量(44)
2、(float/double)实型常量(3.14)
3、(char)字符常量(单引号引起来的单个字符或转义字符)('a','\n','\0','\015'代表三位8进制,像'\018'不是八进制数,因为八进制里面不会出现8,当作字符串的话就是\0 ,1,8,但是字符串又不是单引号要用双引号;'\x7F'两位16进制数)(%d十进制,%x十六进制,%o八进制)
4、(array[]/*p)字符串常量("hello",""(结束标志,尾0,空字符占一个字节),"abcd\n\021\018"(特殊) 占9个字节:a,b,c,d,\n,\021(8进制算一个),\0,1,8)
5、标识常量(#define MAX(a, b)  ((a) > (b) ? (a) : (b))用MAX(a, b)替换后面一大项,预处理的时候再换掉,处理在程序预处理阶段全部替换掉,优点是一改全改,缺点是不检查语法,只是单纯的宏名与宏值之间的替换,后面有const与define的不同):define.c,执行gcc -E define.c 可以看到预处理时候将PI全都替换成3.1415。

#define宏的替换,占用的时间是编译的时间,节省运行时间,替换了不会占用多余的时间;但是函数的话在当前位置进行一个压栈保存,跳到函数位置去执行再回来弹栈恢复的过程,占用的是运行时间。宏相对函数来讲是危险的,节省运行时间,内核中若少一点运行时间会给应用层减少时间就用宏来写;在写应用层的时候,多用函数,应用层更加要求的稳定,内核模块尽量效率高。若 int a; 则 typeof(a) == int,形如 int B = b;等价于 typeof(a) B=b;

函数和宏是编程中的两个不同概念,它们在使用方式和行为特性上存在一些区别,从而导致了在某些情况下宏可能比函数更容易引入错误或带来风险。

1. 作用域和命名空间:函数具有自己的作用域和命名空间,在函数内部定义的变量通常不会影响到外部。这种封闭性使得函数调用相对稳定,不容易受到外部环境的干扰。而宏展开发生在编译阶段,它可以直接操作源代码,并可能引入变量名冲突或命名空间污染的问题。

2. 运行时和编译时:函数在运行时被调用,因此可以根据实际参数动态执行逻辑。这种动态性提供了更大的灵活性和可扩展性。相比之下,宏在编译阶段被展开,其生成的代码在运行时执行。这意味着宏的行为在编译期就已固定,无法根据运行时的条件进行调整。

3. 调试和错误处理:函数相对独立,可以通过给函数传递参数和检查返回值来验证其正确性。在调试过程中,可以逐步跟踪函数的执行路径,更容易定位和解决问题。相比之下,宏展开后的代码与原始源代码耦合度较高,调试时难以直接追踪到具体的宏展开过程。

4. 可读性和维护性:函数通常具有更清晰的逻辑结构和语义,使得代码可读性更好,更易于理解和维护。相比之下,宏可以通过代码生成等方式实现更复杂的操作,但可能会引入难以理解和维护的宏展开结果。


变量:用来保存一些特定内容,并且在程序执行过程中值随时会发生变化的量:
定义:[存储类型] 数据类型 标识符 = 值 :int a = 1;

寄存器作为CPU内部的高速临时存储器,用于支持CPU的指令执行和数据处理;而存储器作为外部大容量存储设备,用于长期存储程序和数据。

存储类型: 总共有四种存储类型的变量,分别为自动变量(auto)、静态变量(static)、外部变量(extern)以及寄存器变量(register);非定义型关键字,属于说明型关键字
1、auto -> 函数中所有的非静态局部变量,默认auto存储类型,自动分配空间,自动回收空间(栈空间上):auto.c
2、register -> (建议型,也包括inline内联函数)一般经常被使用的的变量(如某一变量需要计算几千次)可以设置成寄存器变量,register 变量会被存储在寄存器中,计算速度远快于存在内存中的非 register 变量。register int i = 1; 申请建议,但是gcc不一定同意,由编译器决定是否存储在寄存器中,大小有限制,只能用来定义局部变量,32位机器只能定义32位大小的数据类型,如 double就不可以,寄存器没有地址,所以无法打印寄存器类型变量的地址进行查看或使用;条件苛刻,基本不行。寄存器是通过寄存器名直接进行访问的,它们具有独特的寄存器编号或寄存器名字。寄存器的读写速度非常快,可以在一个时钟周期内完成,几乎是立即响应。相比之下,存储器需要通过地址进行访问,需要提供存储器单元的地址以读取或写入数据。存储器的访问速度较慢,通常需要多个时钟周期才能完成。
3、static -> 在变量前加上 static 关键字的变量,静态型,自动初始化为 0值或空值,并且其变量的值具有继承性,用于修饰变量或函数,从定义开始到程序结束
4、extern -> 把全局变量在其他源文件中声明成 extern 变量,可以扩展该全局变量的作用域至声明的那个文件,其本质作用就是对全局变量作用域的扩展;说明型,不能改变被说明的变量的值或类型,如extern int a;来声明有一个外部整型变量a,不能写成extern int a = 1;来给外部整型变量赋值,编译会报错;不能给extern赋值

数据类型:基本数据类型(char,int等等) + 构造类型(数组,指针,结构体)
标识符:由字母,数字,下划线组成且不能以数字开头的一个标识序列,不能有空格;所定义的变量,编译器会自动为当前变量分配一个存储空间(房间号即标识符)

变量的生命周期和作用范围: 
全局变量:作用范围从定义位置开始直到程序结束:global_var.c
局部变量:作用范围从声明位置开始直到当前块作用域结束
errno本来是定义的全局变量,要及时输出错误,不然被其他错误修改,已经变成私有化过的数据,现在变成宏的形式来替代

static_extern_testproject文件目录下一个项目包含多个文件写法以及测试全局变量,vim * -p 就是编辑当前目录的所有文件;esc退出后gt切换;gcc *.c将当前目录下所有.c文件全部编译;俩个人俩个函数内都写全局变量i,会冲突,用static(只能只在当前模块内使用)或者extern;项目工程中如果没有共同一个变量的需求就用static定义全局变量,保证在其他函数不出错不重复冲突,该变量只能在当前文件内用,所以一般情况下使用全局变量都加上static(就是这个文件的头文件被包含在其他函数内,这个全局变量也会被延申过去,如果再定义一个就会重复);extern的话,如上介绍;static int k = 88;与extern int k;不可合用,不成立;static void func2()//当前这个函数的用法只希望在当前.c内使用不能被调用,编译报错,防止当前函数对外扩展(封装和隐藏,用C来完成面向对象的编程);如测试文件:main.c proj.c proj.h;可以间接调用static修饰的函数


运算符和表达式
表达式和语句的区别:(在大多数编程语言中,表达式可以作为一个整体被赋值给变量,或者作为函数的参数传递。表达式还可以用于条件判断和循环控制等地方;表达式是由操作数和运算符组成的计算式,它可以计算并返回一个值。而语句是程序中执行命令或指令的操作,它用于控制流程和执行各种任务,并且不一定返回一个值)
i = 1  表达式 
i = 1; 语句(多一个分号) 

运算符部分:operator.c(除余运算符两边均是整数,在C语言中,除余运算符要求操作数都是整数类型。如果其中一个或两个操作数是浮点数,则会引发错误或警告)
1、每个运算符所需要的参与运算的操作数个数,几目就是几次操作(三目运算符)
2、结合性(单目运算符,条件运算符以及赋值运算符这三种运算符右结合)
3、优先级 
4、运算符的特殊用法,如:%要求左右操作数必须为整型, == 和 = 分开注意,区别逻辑运算符(&&, ||)的短路特性 :&&逻辑与:左边表达式为真的时候才会判断右边的表达式,左边为假直接结束,右边不执行;逻辑或||,左边为假才会继续判断右边,左边为真则直接结束不执行右边的表达式
5、位运算的重要意义

除法分母不能为0;取余%要求两边均是整型数 5%2 == 1,5.0/2 == 2.5没问题,但5.0%2是错误的,求余的结果是个整数,因此左右两边的数也必须都是整数;不同类型的转换(要么显式(强制类型转换)要么隐式(默认int+char=int))
&&逻辑与,两边均为1真才为1真,&按位与,每一位相与(两者均为1则为1,有一个0则为0),|按位或,每一位相或(两者均为0则为0,有一个1则为1)
逗号运算符:for(i = 0, j = 1; ; ),逗号运算符,整个表达式的值就是最后一项的值,运算从左往右进行,只取最后的表达式值
sizeof不是一个关键字是一个运算符

在C和C++中,sizeof 是一个操作符(operator),而不是关键字(keyword)。
sizeof 操作符用于获取给定类型或变量的大小(即占用的字节数)。它是编译时的操作符,返回一个常量表达式,表示操作数所占的内存大小。
之所以 sizeof 不是关键字,是因为它可以被赋予任意合法的标识符作为变量名。例如,你可以创建一个名为 sizeof 的变量,但这样做并不常见,也容易引起混淆;关键字不能当作新变量来使用,操作符可以。
作为操作符,sizeof 后面可以跟随不同的操作数。例如:
sizeof(int):获取 int 类型的大小。
sizeof(float):获取 float 类型的大小。
sizeof(variable):获取变量 variable 的大小。
需要注意的是,sizeof 操作符返回的是一个 size_t 类型的值,表示以字节为单位的大小。因此,通常用 %zu 格式字符串进行打印。
总结来说,sizeof 是一个操作符,用于获取给定类型或变量的大小。它不是关键字,因为可以作为标识符用于变量名。编译时求值:sizeof 运算符在编译时求值,而不是在运行时。这意味着它返回的结果是在编译阶段根据类型或表达式计算出来的常量。


模式,用来检查每个ll/ls -l文件的权限:int stat(const char *path, struct stat *buf); struct stat {mode_t  st_mode;}  /* protection */;其中00001,others其他用户可执行 ,00002(次低位为1的时候,即最后两位为10)others用户有个写权限,00004(即100)others有个读的权限;所以当为111的时候代表即 7=4+2+1 -> 读(4)写(2)可执行(1),即[rwx];100代表只读[r--]

二进制右移1位相当于除2,左移1位,相当于乘2(左乘右除);
^表示异或,不同为1,相同为0,异或只能位运算(位运算即只能一位进行比对)即1和0直接对比,不能10跟1进行异或
将操作数中第n(右往左n)位置 1,其他位不变:num = num | (1 << n);//将1左移n位,移动到第n个位置,然后或上1,即将n位变成1了;00011001
将操作数中第n(右往左n)位清 0,其他位不变:num = num & ~(1 << n);//将1左移n位取反,则这一位变成0,再与上那一位就变成0了
测试第n位:if(num & (1 << n)),如果第n位为1,则表达式为真,第n位为0的话则为假 

输入输出

input & output -> I/O (标准 IO,文件/系统 IO) 

1、格式化输入输出函数:printf,scanf

int printf(const char *format, ...); format:("%d", i)
printf的返回值是能够正常输出的字符个数但不包括'\0'即不包括尾0;'\0' 表示空字符(Null Character),它的 ASCII 码值是 0(也就是十进制为0,即ascll码表第一个数)。虽然在字符常量中看起来像是字符 '0',但它实际上是一个整数值为 0 的字符。
下面是一些常用的格式字符:
%d: 输出带符号的十进制整数(int类型)
%u: 输出无符号的十进制整数(unsigned int类型)!!!初始定义为unsigned int类型就用%u来输出!!!
%f: 输出浮点数(double类型):float a==567.789;printf("%f", a); -> 567.789000(后面保存6位有效精度,多截少补)
%s: 输出字符串(char*类型)
%c: 输出字符(char类型)
%p: 输出指针的地址(void*类型)
%e: 用科学计数法输出浮点数(double类型):float a==567.789;printf("%e", a); -> 5.677890e02
%g: 根据浮点数的大小决定使用 %f 或 %e 进行输出,e和f中较短的一种(double类型):float a==567.789;printf("%f", a); -> 567.789
此外,还有一些用于指定输出宽度和精度的修饰符,如:
%5d:指定输出宽度为5个字符(包括符号和数字),若不足5字符,则左边用空格填充
%.2f: 指定输出精度为2,若不足2位,则用0进行填充
修饰符:如代码printf.c所示

long型并不是int型的子类型,而是一种与int型平级的数据类型,通常情况下,long型的整数范围在编译器中是两倍于int型,有时候long跟int均是32位,long long才是64位;func(12LL)->func(long long i),传参的时候,直接12可能报错,不知道12是什么类型的数据,每个机子long不一样,加个L就代表是long型,普通数据234LL也代表这是个long long类型数据;没单位的数值在计算机的世界里是一样的,没意义

判断是否变参实现的方法:多传参数,如果报语法错误则为定参,如果不报而在使用过程出问题则为变参 (直接报错定参,使用出错变参)!!!
建议每行加一个\n,不仅仅换行的问题;本来是全缓冲模式,涉及到了终端设备变成了行缓冲模式,printf遇到\n才去刷新缓冲区或者等缓冲区满了才自动刷新,不加\n则会放到输出缓冲区,要么等程序结束刷新缓冲区,要么遇到强制刷新函数fflush,要么等缓冲区满了一次性刷新!!!测试代码:buffer.c

int scanf(const char *format, ...);
scanf("%s", str);用%s来获取一个字符串,空格,tab键,回车键,都会作为当前输入的一个结束,即中间不能有任何分隔符出现;要用怎么办,我们有专门的字符串输入函数;用%s非常危险,因为不知道存储空间大小!当str[SIZE]中的size只有3,'\0'即尾0还占一个,只能输入俩个有效字符,当输入hello的时候仍然会显示hello且什么警告都没有,其实已经出现越界错误!!!
在循环中一定小心scanf的使用,放在循环中一定要校验返回值!!!scanf 放在 while()中非常危险,要注意能否接收到正常有效的内容,校验返回值!while(scanf("%d", &i))//规范化使用scanf,校验scanf返回值的正确性从而判断是否循环;返回值为,如果能正常接收一个字符的话就返回1,若2个%d%f的话就是返回2,没得到想要的程序也不会出错,不然就容易崩了,防止程序崩了:scanf.c
scanf("%d", &k);ch = getchar();实际上上面输入完k后紧接着那个回车算是字符被getchar捕获了,此时引入一个抑制符,如:scanf("%*c%c", &ch);相当于吃掉一个内容,scanf("%*c%c", &ch);//用抑制符吃掉中间这个char型的大小(回车,空格什么的)

在 scanf 函数中,%*c 是一个格式说明符,用于忽略并丢弃一个字符输入。它是抑制符(suppress),表示读取一个字符但不将其存储到指定的变量中。
具体而言,%*c 的作用如下:
%*:% 是格式说明符的开始标志,而 * 表示抑制赋值。它告诉 scanf 函数忽略将要读取的数据,并不进行任何赋值操作。
c:c 是格式说明符的一部分,表示读取一个字符。在这里,它指示 scanf 函数读取一个字符,但不将其存储到指定的变量中。
因此, scanf("%*c%c", &ch) 的意思是:
%*c:读取并忽略一个字符。
%c:读取下一个字符,并将其存储到变量 ch 中。
这样的语句可以用来跳过输入中的一个字符,然后读取接下来的字符。这种情况可能出现在需要处理特定格式的输入时,其中某些字符只用于分隔、标记或其他用途,而不需要存储到变量中。
需要注意的是,%*c 只能忽略单个字符。如果希望忽略多个字符,可以多次使用 %*c。同时,为了确保输入的正确匹配,建议在 scanf 函数调用后检查返回值来确保成功读取所需的字符。
举例如下:
scanf("%d", &k);
scanf("%*c%c", &ch);
printf("k = %d, ch = %c\n", k, ch);
如果不加*c,则输入完2回车就直接显示出来k = 2, ch = ,这里的ch就是回车符,所以加一个*c来吃掉这个回车符

2、字符输入输出函数:getchar,putchar (getchar返回值是以一个无符号字节读入的,也就是没有负数,要兼容到EOF出错情况,所以用int接受;而且字符包括0到255,char不够)
int getchar(void);getchar() 函数的返回值类型是 int,因为它需要返回一个值来表示输入字符的 ASCII 码值。数据类型 int 存储容量比 char 更大,可以用来存储比 char 更大的整数值或特殊值 EOF。因此,如果我们用 char 类型来接收 getchar() 的返回值,可能会导致信息的丢失或误解。将读取的字符作为无符号字符转换为int(方便打印返回错误),文件结束时出现EOF(end of file是type define出来的一个宏,宏值是-1,但还是用EOF)或出现错误。如果使用 char 类型来接收 EOF,它会被解释成字符而不是特殊值-1,因此程序无法正确处理 EOF,在读取输入时,最好使用int来接收getchar()函数的返回值,然后再进行必要的转换;其实char和int只是可以表示的大小不同,不一定只有char才能表示字符!!!

int putchar(int c);写进的以整型值(unsigned char转为int)形式来输出,出错返回EOF

3、字符串输入输出函数:gets(危险!),puts
char *gets(char *s);        //gets将stdin(标准输入即键盘)中的一行读取到s指向的缓冲区中,直到出现终止的换行'\n'或EOF,在尾部加个'\0';gets 函数十分危险!!!可以用 fgets,getline 替代 。gets不检查缓冲区(SIZE我所定义的小空间)的溢出!!!如:#define STRSIZE 5    char str[STRSIZE];其中size只能是5,即包括尾0在内在表面只能输入4个字符,但是输入多了也会正常显示,除非数组越界已经踩到了写保护权限的地址空间了才会报段错误

gets为什么危险gpt:gets()函数是一个不安全的函数,因为它没有提供任何缓冲区溢出的保护措施。它接收输入的数据,并将其存储在一个字符数组中,但不会检查输入数据的长度是否超出了数组的大小。因此,如果用户输入的数据长度超过了数组的大小,就会导致缓冲区溢出,可能会破坏程序的数据结构或内存。

char *fgets(char *s, int size, FILE *stream);        //可以从任何一个打开的流中来获取,不一定是stdin标准输入来(即键盘上的输入),读到size-1个留一个尾0;从stream中获取size个字节的串放在*s中获取得到,(保证你不出错,但不能保证能拿到所要的字符串)
为了解决这个问题,C11标准已经将 gets()函数从标准库中移除了,取而代之的是安全性更高的 fgets() 函数。fgets() 函数允许指定最大读取字符数,并将自动检测输入的字符数是否超出了允许的最大值。另外,与 gets() 不同的是,fgets() 可以读取字符以外的字符,如回车、空格等,所以它在输入少量定长字符串时也不失为一个好的选择。因此,在编写 C 语言程序时,请使用更为安全的 fgets() 函数替代 gets() 函数。

fgets缺点:fgets() 函数确保不会发生缓冲区溢出的问题,因为它会从标准输入或文件中最多读取 n-1 个字符,其中 n 是指定的缓冲区大小,保留一个字节用于存储字符串的结束符 '\0'。这意味着它可以安全地读取输入流中长度不超过指定大小的字符串,从而保证了程序的安全性。
但是,fgets() 函数不能保证一定能读取到需求的字符串,因为其行为可能会受到许多其他因素的影响。例如,如果输入流中的数据不符合要求,或者程序没有正确处理换行符或回车符等特殊字符,都可能会导致 fgets() 函数无法读取到所需的字符串。fgets() 无法保证能够获取到精确的所需字符串,这是因为当读取到指定的长度或者换行符时就会停止读取,如果所需字符串的长度比指定的长度更大,fgets() 读取到的数据可能不完整。此外,当遇到错误或者文件结束时,fgets() 也会返回一个空指针,因此它也无法保证总是能够读取到所需的字符串。

ssize_t getline(char **lineptr, size_t *n, FILE *stream);        //只存在于GNU库中,是个方言
动态内存的实现,能帮助你拿到完整的一个串,多大都行,每拿一行就扩展一行的内存

总结来说最好还是使用getline函数进行获取字符!!!

int puts(const char *s);//将s开头的串写到stdout标准输出中去,以一个换行符'\n'进行终止

流程控制

顺序:语句逐句执行 
选择:出现一种以上情况:if-else switch-case
循环:在某个条件成立情况下重复执行某个动作:while do-while for if-goto
辅助控制:continue;(继续下一次循环;跳出此次循环,继续判断条件是否成立继续循环); break;(跳出循环或者switch语句往下执行)

NS图,流程图,有限状态机(最复杂最重要)!!!

else 只与和它最近的 if 相匹配!!!
while: 最少执行 0 次;do-while: 最少执行 1 次; for: 最少执行 0 次 ;if-goto:(慎用:goto 实现的是无条件的跳转且不能跨函数跳转)
死循环:while(1); for(;;); 杀掉死循环:ctrl + c

这样写没有意外错误情况:
if(...)
            ;
    else if(...)
            ;
    else
            error;(杜绝错误)
如果直接:
if(...)
    ;
else
    ;
不属于if的判断情况就是else的情况;程序会有可能出现意外情况发生错误,越界溢出等等

switch不能少了break(跳出当前switch); 以及结束的时候default: _exit(0); //下划线的exit是直接结束当前进程不会做任何的清理动作(刷新io等等),程序出现意外了;或者不用_exit(0); 选择一个信号signal能够杀死当前进程,产生一个文件呈现当前出错的现象

switch.c:
case 常量或常量表达式 : (case后面只能常量表达式,不能比较式或者变量式);
case 后面想并列只能上下一起不能用或:
case 'a':
case 'A':
不能  case 'a'|'A':(错误的!)这不是常量表达式,这是逻辑或的表达式

循环while/do while;loop.c:
define的数值不让修改!!!#define LEFT  ->  LEFT = i;(错误的!)
if-goto: goto是无条件跳转且不能跨函数跳转(goto到另外一个函数,执行权过去但是线程没有过去;正常情况下调用函数,原本的函数是要入栈保存的,然后新的入口地址函数执行完后回到原来函数弹栈恢复,goto没有这样功能)goto就是跳转过去不管之前的函数了
流程控制语句练习test*.c:

数组

一维数组
数组里面不能变长,只能是#define定义的;数组的值在内存中地址是连续存放的(数组的最大特点);数组越界编译不出来,只能由自己发现

二维数组
只能省行号,不能缺少列号;二维数组里面每行都是一个一维数组;二维数组几行几列,int a[M][N]数组名a加1即跳过了列N个整型int距离,即 N*4 个地址大小,即下一行

字符数组:char.c
初始化: 可以单个字符初始化;也可以直接一整个字符串常量初始化;scanf()不能获取带有分隔符的输入,像空格tab等,但是同时输入多个串,可以空格分开分别输入,回车会结束当前输入;字符串常量多了个尾0;要注意gets不检查缓冲区溢出,只是添加尾0;不能str=“hello”来赋值,字符串是个常量

常用函数:(strlen以尾0结束来判断当前大小【运行时才知道】,sizeof计算该串在内存中占用的字节数大小【是个操作符不是关键字,编译时就知道;也可以用来命名变量,关键字不能用作变量名】)
1、strlen(不包含\0即尾0的大小) & sizeof ;sizeof和strlen都是C语言中常用的操作符,但它们有着不同的作用及适用范围。

sizeof是一个操作符,用于计算数据类型或变量占用的内存字节数的大小,包括数据类型、结构体、联合体、指针和数组等。sizeof在编译时计算,可以在程序的任何时候使用,不会改变原变量的值,因此它适用于静态的计算数组、结构体等数据类型的大小。例如:
int a[10];
printf("%d", sizeof(a)); // 输出 40,即整型数组 a 的大小为 40 字节

strlen是一个库函数,它用于计算以NULL字符('\0')结尾的C风格字符串长度,即在字符串中第一个NULL字符前的字符数目。strlen在运行时计算,需要扫描整个字符串,只适用于计算字符串长度。例如:
char str[50] = "Hello, World!";
printf("%d", strlen(str)); // 输出 13,即字符串 "Hello, World!" 的长度为 13 个字符

//sizeof可以计算数组大小是因为数组的大小在编译期是已知的;
//而strlen无法计算数组大小是因为字符串的长度只有在运行期才能确定。
因此,sizeof和strlen的差异主要在于两者的计算方式及适用范围不同。需要注意的是,在计算字符串长度时,如果字符串中没有NULL字符,则strlen会一直扫描内存,直到程序崩溃或者发现了NULL字符。因此,需要对输入的字符串进行合法性检查,以防止溢出及相关安全问题。

2、strcpy & strncpy 拷贝函数,(赋值)
char *strcpy(char *dest, const char *src);//后面拷贝到前面的串里
char *strncpy(char *dest, const char *src, size_t n);//拷贝n个字节到dest目标里;习惯设置 n = sizeof(str)/sizeof(str[0]),n值为dest的大小;或者:char str[STRSIZE]; strncpy(str, "abcde", STRSIZE - 1); n为strsize;//-1是预留一个尾0的位置,因为每次这个函数都会在结尾拷贝一份尾0过去

3、strcat & strncat 
连接函数
覆盖目标串最后的尾0,结束再添加新的尾0;strncat函数中的n取值不足n个有效字符时,取到尾0为止

4、strcmp & strncmp
比较函数
strcmp比较输出返回的值为首字母的ascll码相减数值;n是指比较前n个字符

对文本数一下有多少个单词(空格跟tab隔开):count_word.c

指针

编译器是根据变量的地址来找到这个变量的位置,int i这个i编译器不认识,只是我自己对于一块地址空间的抽象表示,变量名是方便用户,计算机通过变量找到这块空间
指针就是地址,把指针指向a,把指针指向b即就是把指针变量里的存放的地址值改变了;指针*p一定要赋值,不然就是野指针了!!!

为什么要int i; int *p=&i;为什么要int不是char,*p取p地址数值的时候要知道是去某一个地址取四个字节,不是取1个字节,这个取决于当前指针的类型

void *p空类型指针值,任何类型的指针值都能够把自己的值赋给void*,同样的void*也能将自己的值赋给任何类型的指针,void*百搭类型,void*和一个指针函数之间互相赋值,只有这种情况没有定义

int a[3]; int只是代表的是连续的三块存储空间里面每块都是整型内容;数组名加1即a+1代表是移动了一个int大小的字节
//a[i] = *(a+i) = *(p+i) = p[i]
//&a[i] = a+i = p+i = &p[i]
sizeof(a)/sizeof(a[0]) == sizeof(a)/sizeof(*(a+0)) == sizeof(a)/sizeof(*a) 更方便的用最后一个来表示数组的大小(整个数组的大小除以第一个元素的大小)
一维数组数组名和一维指针p区别:a是一个数组名是一个表示地址的常量,p是一个用来从存放地址的变量;a是常量(值不能发生变化)p是变量(值在运行过程中可以随时随地发生变化)!!!可以p++但不能a++!!!p+1和p++不一样,p++改变地址,p+1不改变地址还指向这里只是获取下一个元素的地址:test.c

可以创建匿名对象, 没有数组名,数组名不重要,数组名只是对某一块空间的抽象:int *p = (int [3]){1, 2, 3}; (三块整型空间连续存放)有数组但是数组没名字,用 p来引用所有元素

二维数组:a[2][3]就相当于有一个数组叫a[2],这个数组的type类型是 int [3],也就是意味着数组的类型是三个int一起存放的小一维数组
为什么取*就是二维数组降级到一维里面,因为二维相当于是大数组,里面存放的是右边划分出来三个数值即一维数组的地址,取一次*就是拿到了里面一维数组的首地址值
二维数组,行指针;二维数组里的int a[][]; int *p = a;这里的p+1是指下一列元素地址,a+1是指下一行元素的首地址(因为就上一行所说的,这个类型是int [3],也就是一次性加这么多大小的数据);一般来说二维数组a不能直接赋值给p,即p=a会有警告;p在列上移动,而a是行上移动,所以列指针不可以获得行指针;p=a不可以!p=*a(p = *(a+0))就可以了(相当于p+1 == *(a+0)+1也就是第一行第二列的内容),此时p获得列地址(取*相当于降级)!!!!!!二维数组指针这么定义:int a[2][3] = {1,2,3,4,5,6}; int *p = *a;  !!!!!!数组指针可以有效地化解p=a的问题

数组指针(指向数组的指针)  int  (*p)[3]:总而言之还是一个指针,type name == int[3] *p (一整个一维数组地址)    这个指针是个数组类型的,用这个指针指向这个数组,这个指针指向 int[3] 这个元素的起始位置;原来p+1是移动一个int类型的大小,现在p+1是一下移动三个int类型的大小;所以化解了p=a的问题,都是一次性移动三个类型大小的数据:int  (*p)[3] = a;如: double_array.c

long unsigned int (sizeof)是:lu 
size_t (strlen)是:zu
字符指针覆盖:char *str = "hello";  strcpy(str, "world"); 是错误的,这个是想将world覆盖到str所指向的字符串位置去,字符串是个常量,想用world来覆盖一个字符串常量(这个字符串在当前的存储位置是特殊的,使用上不允许被改变或者覆盖);指针str指向的是个常量(不是数组那样连续存放的一个一个的数值)不允许被覆盖改变!!!使用这样子书写:str = "world"; 代表放弃指向hello那个字符串转而指向world这个串,实质就是将指针指向的值改变就行了!!!而不是试图改变指针所指向的字符串,是改变指针指向不是改变指针的内容;指针指向的内存空间是常量,不能通过指针修改这个内存空间的值。
字符数组覆盖:char str[] = "hello";     strcpy(str, "world");    //用strcpy来一个一个覆盖掉数组里的内容
字符串可以直接覆盖:char *str = "hello"; str = "world"; 但不能char *str = "hello"; strcpy(str, "world"); 这是错误的

指针数组(全是指针的数组)  int *p[3]:归根结底是一个数组;存放指针的数组,type name == int*  p[3] (数组里每个元素都是一个指针,每个元素都存放的是地址或者指针变量)例如:char *name[5] = {"follow me",  "basic",  "wangfei",  "hello",  "hi"};数组当中有五个元素,每个元素都是一个指针变量即char*类型point_array.c

const与指针:const.c !!!
const是C++中的一个关键字,用于定义常量。常量是程序中不可修改的值,通过使用const关键字可以让编译器保证这个值不会被修改,从而增加程序的安全性和可维护性。在C++中,可以使用const修饰变量、函数和指针,其中:
- 用于修饰变量时,const可以将变量定义为常量,例如:const int i = 10;表示将i定义为一个常量,其值不能再被修改。
- 用于修饰函数时,const表示在函数内部不会修改任何变量的值,可以保证函数的安全性。
- 用于修饰指针时,const可以将指针指向的值定义为常量,或者将指针本身定义为常量。例如,const int *p = &i;表示p是一个指向常量的指针,指向的值不能被修改;而int * const p = &i;表示p是一个常量指针,不能指向其他变量,但可以通过p修改指针指向的值。
总之,const关键字可以为程序中的常量、变量、函数和指针添加额外的保护机制,提高程序的安全性和可维护性。

const来限制某块空间的值不能发生变化,只是限制不能通过这个名字去变化,并没有锁定这块空间不能发生变化!!!

/*
 *  const int a;
 *  int const a;
 *  
 *  const int *p;            //常量指针
 *  int const *p;            //指针的指向可以发生变化,指针所指向的当前空间内的内容不能发生变化
 * 
 *  int *const p;            //指针常量
 *  const int *const p;  //既是指针常量又是常量指针
 * 
 */
常量指针:(常量指针不能修改常量数值)(先看到const就先念常量)(看const后面跟的是什么;const后面跟的是*,p指针取*即*p可以认为是目标变量的值,也就是const保护这个目标变量*p不能变)!!!
const int *p; //指针的指向可以发生变化,指针所指向的当前空间内的内容不能发生变化,也就是指针保存的地址对应的那个内容不能发生变化
int const *p; 
const int *const p;

指针常量:(指针常量不能修改指针指向)(先看到的是*就先念指针)(看const后面跟的是什么;const后面跟的是p即一个指针变量,即指针不能变,也就是指向不能变)!!!
int *const p; //指针的指向永远不能变化,但是指针所指向的目标变量的值可以变化;也就是const p,不就是const修饰的p,就是p的值不能变,即p保存的地址也就是指向的地址不能变,可以改变这个地址上的值
指针常量需要初始化,不能直接定义不初始化赋值!!!类似于引用,指针常量需要初始化,因为它是一个常量。在定义时,所有的常量都必须进行初始化。

const int i = 1;
int k = 2;
const int *p = &i; 
*p = 3;             错误的,因为*p是改变内容,const int *p 是常量指针,限制了指针指向的内容不能改变,也就是指针所保存的地址上的数值不能发生变化,不能通过*p来改变指向的内容
p = &k;            可以,改变指针指向

寄存器与存储器的区别:
寄存器和存储器都是计算机中用于存储数据的组件,但它们在工作方式、使用方法和特征上有所不同。

首先,寄存器是计算机CPU内部直接访问的一组高速存储器,用于保存指令执行时需要的数据和中间结果。寄存器是非常快速的,因为它们嵌入在CPU内部,可以直接访问,而不需要通过任何其他组件进行访问。寄存器的数量通常非常有限,它们的大小也相对较小,通常只能存储几个字节的数据,同时寄存器的种类也不同,例如,常见的寄存器有通用寄存器、程序计数器、堆栈指针等。

其次,存储器通常是指计算机中的主存储器,它用于存储程序、数据和系统所需的其他信息。存储器通常比寄存器慢,因为它需要通过总线进行访问,同时存储器的容量也相对较大,常见的容量有几个GB或者TB。存储器的访问速度主要受到总线带宽、内存类型、发生不命中的次数等因素的影响。

const char *pathname 是指通过指针不能改变你传过来的内容,常量指针,即 *pathname = 是不成立的;放心传参,我对你传过来的文件名不会加以改变(*p代表首字符,所以不会通过*p来修改你传过来的字符串)

char *p = "adc";
printf("%c\n", *p);
这就代表了*p就是第一个字符,限制了不能通过*p来修改传过来的字符串

函数

echo $? 查看上一个进程的返回值(有return 0就返回0,没有就返回最后执行语句的printf的有效字符个数);一个进程的返回状态是给他的父进程看的,谁创建了当前进程就是他的父进程(int main里面的返回就是shell创建的)
int main(int argc, char *argv[]);argv是个指针数组(字符指针数组的首地址即起始位置),该数组里存的都是每一个字符指针:argc.c
shell会解析通配符,./argc  /etc/a*.conf 其实本来etc里面有2个(ls /etc/a*.conf -->  有2个文件),会解析通配符,执行结果是3;运行./argc /etc/a*.conf
argc = 3
./argc
/etc/adduser.conf
/etc/apg.conf
argv是个指针数组,数组名叫argv,argv这个数组有多少个元素取决于argc,每个元素类型为char*(指向一个字符串的指针),最后一个是空指针 NULL 来作为字符指针数组的结束

函数的传参 
1、值传递 
2、地址传递 :swap_func.c
3、全局变量 

函数的调用 
1、嵌套调用 :min_max.c
2、递归     :recursive.c
递归其实就是抽象出来一个解决问题的方法,直接或间接的调用自己,递归需要设置停止条件,函数反复被调用
__FUNCTION__用%s输出,可以显示当前的函数名字
执行到函数c的时候,有一个压栈保存,弹栈恢复,也就是main在最底下,然后a,然后b,然后c,一个一个压栈,执行完c后在返回b,返回a,返回main,上面的函数均被释放弹出去,即弹栈;递归不知道有多少次函数调用,有可能造成栈不够,导致栈破裂

factorial.c: 阶乘
n! = n*(n-1)!  //这是抽象出来的一个公式,factorial 函数就是用来算 n! 的,也就是 factorial(n) == n!,即 n*factorial(n-1); 此时 factorial(n-1) 等价于(n-1)!
fibnacci.c: 斐波那契数列
hanio.c: 汉诺塔游戏,递归完成;对于n层塔的移动步数:2^n-1;

函数与数组 
(二维数组a[1][2]==*(*(a+1)+2)==*(a[1]+2));a是指所有的数值的首地址,二维数组中a[1]是指第二行元素的首地址(在一维数组中就是单纯指数值)
array.c:
形参中!!!一个方括号等于一个*,p[]==*p,sizeof(p)=8;均等于8个字节;一维数组名跟一级指针的区别就是一个是常量一个是变量,形参就算写一个像数组的也不是定义的数组,是一个形参,形参就是一个指针的大小;p[]==*p,p[]在形参中本质就是一个指针,在定义中才是数组,不一样!数组符号就相当于一个*,所以在int main(int argc, char *argv[]);中这个数组符号就相当于**argv,这不是二级指针,单纯是*argv[]这个的另一种写法,本质才是*argv[],就是指针数组,存放多个字符串的!

double_array.c:
二维数组,行指针;二维数组里的int a[][]; int *p = a;这里的p+1是指下一列元素地址,a+1是指下一行元素的首地址;一般来说二维数组a不能直接赋值给p,即p=a会有警告;p在列上移动,而a是行上移动,所以列指针不可以获得行指针;p=a不可以!p=*a(p = *(a+0))就可以了(相当于p+1 == *(a+0)+1也就是第一行第二列的内容),此时p获得列地址(取*相当于降级)!!!!!!二维数组指针这么定义:int a[2][3] = {1,2,3,4,5,6}; int *p = *a; 数组指针可以有效地化解p=a的问题
就是想办法将行指针 a 转变为列指针 p;     &a[0][0] (第一个元素取地址)== *a (第一行数组的首地址)== *(a+0) (第一行数组的首地址)== a[0](第一行数组的首地址) == a(整个数组的首地址)
  (*a相当于进入了第一行里面的第一个元素的地址,降级;a相当于外面第一行的首地址)
a == int p[][N] == int (*p)[N] --> 数组指针,指向具有三个元素的一维数组;就第一个a来说,a+1就是代表下一行跳过了3列元素,第二个就是p来命名一样的二维数组,第三个就是数组指针,每次加一跳3个元素

/*
 *  int a[M][N] = {...};
 *  int *p = *a;            // p = *a        *a退级为列指针,就是一列一列跟这里的p一样
 *  int (*q)[N] = a;        // q == a        a就是行指针,每次加1都是下一行,所以搞个数组指针与之对应,每次也是加3个元素的大小
 * 
 *  实参:a[i][j]   *(a+i)+j   a[i]+j      p[i]   *p           a
 *        q[i][j]   *q         q           p+3    q+2          a
 *
 *  形参:int       int*       int*        int       int          int (*p)[N]
 *        int       int*       int (*p)[N] int*   int (*p)[N]  p[][N]
 * */
当不区分二维数组的时候,看成一个大的一维数组就传列指针过来即 p=*a,要区分的时候就传行指针过来!!!
像*a和a在二维数组里都是指首地址,但是意义不一样,*a是列指针,a是行指针!!!

char_array.c:
实现mystrcpy和mystrncpy函数,函数返回一个指向des目标字符串首地址的指针

函数与指针 
1、指针函数 ;返回值 *函数名(形参表) ;如: int *func(int) 总而言之是个函数,返回值是个指针;用这个指针来进行下一步操作,所以返回指针

2、函数指针 ;类型 (*指针名)(形参表) ;如: int (*p)(int)  总而言之是个指针,指针指向的是返回值类型为int,形参为int的一个函数(想传入函数名就用相同类型的函数指针来接受)
整型指针:指针指向的是整型数 int *;
数组指针:指针指向的是数组(指向数组的指针)  int (*p)[3]:总而言之还是一个指针,相当于行指针,每行3个元素,一次性加三个元素的地址
函数指针:定义一个指针指向函数,比如说我要定义一个指针指向a就要跟a类型一样(int a; int *p = &a;),比如说我想让指针指向函数 [int add(int a,int b)],即int (*p)(int, int); p = add;(函数名本身就是一个地址) 原来add(a, b) == p(a, b);里面的三个int都是一一对应,(函数名是一段代码所关联的入口地址)

3、函数指针数组 ;类型 (*数组名[下标])(形参表) ;如: int (*arr[N])(int) 
多个相同类型的数值可以放在数组中,指针数组(全是指针的数组)  int *p[3]:归根结底是一个数组;存放指针的数组
使用:int (*arr[2])(int, int) = {add, sub}; 中的  2代表数组里有 2个函数指针!!!也就是可以用数组来存放函数指针,这两个指针指向两个函数即可,就可以用arr[0]来表示第一个函数,也就是函数名
中文强调的都是后面俩个字,比如指针数组就是个数组里面放的是指针,函数指针数组就是一个数组里面都放的是指针,这些指针都指向函数!!!这个指针数组里的指针都指向函数

4、指针函数指针数组 ;类型 *(*数组名[下标])(形参表) ;如: int *(*p[N])(int) :指向指针函数的函数指针数组(相比于上一个就是所有的函数返回值都是指针)
在上面函数指针数组的前提下加上一个指针,这是一个数组里面都放的是指针,这些指针都指向函数,最后返回指针
***说白了就是:( 数组存放指针,指针指向函数,函数返回值是指针类型 )!!!!!!

从左往右先读指针括号外面的,比如说int *func(int)就是指针函数,int (*p)(int)就是函数指针,int (*arr[N])(int)就是函数指针数组,int *(*p[N])(int)就是指针函数指针数组:先*再最后面的int,再(*p[N])读*指针再数组

可以通过函数名直接调用,为什么还要指针来使用,我想传参怎么办;我传数组可以用指针接受,我传函数名用啥接受呢;函数名就是入口地址,依然要用指向函数的指针来接受,即函数指针(用来接受函数名)
例如:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
第三个参数 void *(*start_routine) (void *) 是一个指针函数指针;它是一个指针,指向的是参数为void*类型的返回值为指针的函数;
想调用这个函数,第三个参数给他一个指针函数的入口地址,也就是给予一个指针函数的函数名即可,所以他用一个指向指针函数的 函数指针来接受。
函数指针就是用来接受函数名的!!!
接受一个普通函数需要函数指针,接受一个指针函数(即这个函数的返回值是指针)需要指针函数指针;

构造类型

结构体 
1、产生原因及意义
数组只能存储相同类型的数据,需要存放一个东西的属性,属性的数据类型都不一样;需要结构体
提高代码的可读性,结构体可以使代码更加模块化和易于组织。
可以使用结构体来定义新的数据类型,增加代码的抽象层次。
可以在结构体中定义函数,形成数据与操作结合的类似面向对象的编程思想。
可以方便地传递结构体对象到函数中,并对其进行处理。

2、类型描述:struct.c 
struct 结构体名         //也可以定义无名的结构体,没有名字;结构体也是一种类型

    数据类型 成员 1;     //结构体定义就好比 int,其不占存储空间,仅是类型描述;其不能在结构体内赋值,不能直接等号,是没有空间的,不能存放数值
    数据类型 成员 2; 
    ... 
}; 要有分号结尾!
也就相当于这么一大串就是int在定义,还没赋值;定义结束【struct 结构体名】才当作是int这种TYPE,即类型(TYPE NAME = VALUE)

3、嵌套定义
struct birthday_st birth; 其中struct birthday_st是个结构体类型,birth是变量名

4、定义变量(常规变量, 数组, 指针),初始化及成员引用
成员引用:    变量名. 成员名; 
             指针->成员名;
             *(指针). 成员名;
struct student_st arr[2] = {
                                   {202212490653, "Wangfei", {2000, 9, 4}, 88, 97}, 
                                   {.maths = 99, .chinese = 89}                        // 局部赋值,不赋值若输出则为0
                           };
struct student_st *q = &arr[0];//arr[0]是元素
printf("%lu %s %d-%d-%d %d %d\n", arr[i].id, (q+i)->name, (q+i)->birth.year, arr[i].birth.month, arr[i].birth.day, (arr+i)->maths, (arr+i)->chinese);
三种表示方法:数组值表示,指针表示,数组名地址表示;结构体类型的数组,数组里面是俩个不同的成员数值,可以变量名加点,可以数组名地址指针加箭头

struct
{
    int i;
    float f;
    char ch;
}a = {1, 2.3, 'a'}, b = {.f = 123.456}, c, *p, *q;
无头结构体,没有名字,只能在声明的同时定义好变量


5、结构体内存占用空间大小
相同成员位置不一样大小也不一样:

struct simple_st
{
    int i;    //4
    char ch;  //1 
    float f;  //4 --> 12
}__attribute__((packed)); //加上这串代码就意味着告诉编译器不用对齐,不然通信传输的时候会出问题,对方收到编译器解释不一样

struct simple_st
{
    int i;    //4
    char ch;  //1 --> 8
    char ch1; //1 --> 8 --> 是因为ch占一个字节,当前有四个字节,ch1也存放进去了,不扩展内存
    float f;  //4 -->  12
}; 

struct simple_st
{
    int i;    //4
    char ch;  //1 --> 当前所占地址比int即4字节小,地址要进行偏移,到这里变成8了
    float f;  //4 -->  12
    char ch1; //1 --> 16
}; 

地址对齐的概念!!!不同平台对齐方式不一样
计算方法:
address地址 % sizeof = 0 存放,不等于0就跳过继续偏移
例如第一个结构体:0,1,2,3存放int;第四个内存即 4 % 1(sizeof(char))== 0 所以char放在第四个地址里面;第五个 5 % 4(sizeof(float)) !=0 不能整除所以继续偏移地址,直到 8 % 4 == 0  所以从起始地址号8开始存储四个字节空间大小存放float;此时5,6,7地址空闲出来。


6、函数传参(值,地址)
值传递相当于多值传递,地址相当于是传变量的首地址
传参的消耗非常大,结构体占的字节很大,接受的形参也需要开辟这么大字节来接受;在进行结构体函数传参的时候一般用指针;形参指针的大小永远是8,用指针间接引用来指向结构体进行操作

student.c:*p = *tmp; //结构体成员之间可以直接用等号赋值
自引用结构,用作链表基础,链表一定有一个自引用结构的指针存在


共用体(可以提供多个属性,但同一时刻只能存在 1 个属性生效,类比性别) 【结构体比如有什么兴趣爱好可以存在很多,共用体比如是性别只能是单选】
1、产生及意义
    共用体的每个成员都占用相同的内存空间,并且共用体的大小由它最大的成员来决定。而在某一时刻,只能使用一个成员,这意味着一次只能存储或读取其中的一个成员。
    共用体的产生,主要是为了节省内存空间和提高存储效率。在编写程序时,如果需要在不同的数据类型之间进行转换,而这些数据类型占用的内存空间大小相同,那么可以考虑使用共用体来减少内存的使用。
    另外,共用体还可以被用来实现类似于多态的特性,即在不同的情况下使用同一块内存来存储不同的数据类型,这对于某些特定的应用场景非常有用。
    需要注意的是,由于共用体的成员共用同一段内存,因此在使用共用体时需要格外小心,防止因为误操作或者类型转换失误导致数据的损坏。同时,在编写程序时,需要保证对共用体成员的访问是安全的,避免出现未定义行为和数据损坏的情况。

2、类型描述:union.c
union 共用体名 

    数据类型 成员名1; 
    数据类型 成员名2; 
    ... 
}; 
union test_un a = {1, 1.1, 2.3, 'a'};//错误的,同一时刻只有一个成员能够生效

3、嵌套定义
union1.c
硬件存储分为大端格式和小端格式,(arm)大端格式:数据的低位保存在高地址中,(x86)小端格式:数据的低位保存在低地址中
相同的数据类型,存储的空间是一样的,初始化好第一个数据,第二个数据也一样的保存在同一个地址内使用,如下:
union test_un
{
    int f;
    int d;
};
a.f = 345;
printf("a.f = %d\n", a.f); / printf("a.d = %d\n", a.d);
上面打印出来的a.f和a.d都一样的均是345

4、定义变量(变量,数组,指针),初始化及成员引用 
成员引用: 变量名.成员名;
        指针->成员名;
        *(指针).成员名;

5、占用内存大小
多个成员共用同一块空间,sizeof为最大值,大小由它最大的成员来决定

6、函数传参(值,地址)
用地址传参

7、位域
共用体里面有个位域!!!位域是共用体的一种形式,存放变量不以字节为单位,以位为单位
union2.c

一个字符型数字6,通常会使用ASCII编码来表示,其二进制形式为00110110。在存储这个字符型数字时,通常会使用一个字节(即8位)进行存储。
由于数字6只需要用到后面的4位(即0110),因此在存储时,可以使用位操作来将这个数字存储到一个字节中。比如可以先将这个数字左移两位,然后再与0xF0(即二进制形式11110000)按位或运算,将前面高四位都置为0,最后得到的结果是00110100,即 6 的 ASCII 码值。这样就可以将字符型数字6存储在一个字节中的低四位上,而高四位则为0。
unsigned char类型就直接二进制输出每一位,char类型则是以补码的形式存入的,有负数,具体见函数union.c

补码是计算机中用于表示负整数的一种方式
源码转为补码是加1取反,%d打印需要将补码(有符号位就是补码)转为源码显示即减1取反

位域(Bit-fields)是一种 C 语言的数据结构特性,用于将数据成员紧凑地存储在一个字节或多个字节中。通过位域,可以在一个字节(或其他存储单元)中存储多个不同大小的字段,而不需要使用整个字节的空间
在结构体中,位域的声明方式如下:
struct BitFieldStruct {
    type member1 : width1;
    type member2 : width2;
};
其中:
- type表示位域的基本数据类型,可以是 int、unsigned int 等。
- member是位域的成员名。
- width是该成员占用的位数。

例如:
struct RGB {
    unsigned int red : 3;      // 3 bits for red color
    unsigned int green : 4;  // 4 bits for green color
    unsigned int blue : 1;    // 1 bit for blue color
};

在这个例子中,`RGB` 结构体使用位域存储颜色信息,`red` 占用 3 位,`green` 占用 4 位,`blue` 占用 1 位。

使用位域的优点包括:
- 节省存储空间,特别是在对存储空间有限的嵌入式系统中。
- 可以更方便地处理硬件寄存器或网络协议中的位操作。

然而,位域的使用也有一些限制和注意事项,包括跨平台兼容性和可移植性的问题。
以下是一个简单的例子,演示如何使用位域:
union
{
    struct
    {
        char a:1;
        char b:2;
        char c:1;
    }x;
    char y;
}w;

int main()
{
    w.y = 5;              //    0000 0101

    printf("%d\n", w.x.a);    // -1
    printf("%d\n", w.x.b);    // -2
    printf("%d\n", w.x.c);    // 0

    exit(0);
}
补码是计算机中用于表示负整数的一种方式
负数都是补码形式,char是有符号的,说明第一位代表符号位,1为负数,所以是补码,所以需要将源码转为补码是加1取反,%d是打印有符号十进制的输出,需要将补码(有符号位就是补码)转为源码显示即减1取反(详情可看保存的图片24.1.11更新)


枚举:enum.c
枚举(Enumeration)是一种数据类型,用于定义具有一组预定义值的变量。枚举类型可以将一组有限的可能取值列举出来,在编写程序时可以使用这些值来做出决策或赋值操作。枚举通常用于表示有限的状态、对象或选项。
enum 标识符 

    成员1, 
    成员2,         //以逗号隔开
    成员3        //最后一个是没有逗号的
};
枚举值的取值默认是从0开始,每次自动加1,也可以手动指定枚举值的取值
定义的变量只能取值enum枚举类型中的值,也就是只能变量等于成员1这种

enum        
{
    STATE_RUNNING = 1,
    STATE_CANCELLED,
    STATE_OVER
};
当作宏来使用:用宏define的时候,预处理的时候不一样,预处理会替换宏值,enum不会替换还是原来的样子;enum是定义结果,宏是预处理期间就发生替换;有的时候需要查看预处理的原因问题等等代码看起更清楚;枚举常用于宏值定义!!!当然也不能替代宏,宏可以替换参数,传参等等

宏和枚举是两种不同的概念,虽然它们在某些情况下可以用来实现相似的功能,但在设计和使用上有一些关键区别:

1. 类型检查:
   - 枚举是一种具有类型的常量整数,编译器会对其进行类型检查。这意味着枚举提供了一定程度的类型安全。
   - 宏是一种简单的文本替换,没有类型信息,因此在宏替换时不进行类型检查。

2. 可读性和调试:
   - 枚举提供了一种符号名称,使得代码更易读、易理解。枚举常量的名字可以提供更多的上下文信息。
   - 宏是文本替换,其值在代码中可能是不透明的,可能会导致代码的可读性下降,特别是在宏被大量使用的情况下。

3. 作用域:
   - 枚举常量有作用域,它们的可见性受限于定义它们的作用域。
   - 宏是全局的,可以在文件的任何地方使用。

4. 编译时和运行时:
   - 枚举是在编译时解析的,它们的值在编译时确定。
   - 宏是在预处理阶段进行文本替换,它们的值在编译前被替换。

5. 错误检测:
   - 枚举常量的值是由编译器进行分配的,如果赋予了一个非法值,编译器通常会发出警告。
   - 宏在预处理阶段仅仅是文本替换,没有编译器进行值的检查,可能导致错误不容易被察觉。

在一些情况下,使用枚举更为安全和可读,而宏通常用于一些需要在编译前展开的复杂文本替换。选择使用枚举还是宏通常取决于具体的需求和编码风格。

char_test.c:用来测试当前机器环境char类型有无符号

动态内存

为什么需要内存分配
用到的时候才能确定要多少内存,不要提前定义,浪费空间,大小也不确定
动态内存分配允许程序在运行时根据需要分配内存,并在不需要时释放已分配的内存;它可以在堆(heap)中分配一定大小的内存空间,并返回一个指向分配内存首地址的指针

四个函数:malloc calloc realloc free

void *malloc(size_t size);                //指针函数:在堆上找连续size个空间,返回起始地址;void*可以赋值给任意类型的指针,任何类型的指针也可以赋值给void*
void free(void *ptr);                    //函数指针:free()函数的作用是释放程序已经分配的动态内存空间,并将该内存空间标记为可用;free代表我无法再通过p对malloc申请的空间进行引用,没有权限;此时p没有指向,要赋值为NULL
void *calloc(size_t nmemb, size_t size);        //指针函数:我想要memb个成员,一个成员size个大小;就是连续申请n块size大小的内存
void *realloc(void *ptr, size_t size);        //指针函数:重新为我分配一块内存,ptr必须是malloc或者calloc返回的指针,在此地址的基础上继续申请内存,需要size(300)个大小的内存空间,会在本来不足的基础上加到300个大小的空间;若向下扩展的时候地址不够300个,下面的内存空间被占用了,不能够顺次扩展,会在其他的地址上找一块连续的内存地址是300个字节,把本来那块释放掉,把内容拿过来,重新返回起始位置;原来要的内存太大了,想把它变小,填入size,相当于从尾巴开始截断,截完等于最终需要的size个大小的内存!!!
失败返回NULL

原则:谁申请谁释放,不可随意搭配,防止产生内存泄漏,尽量一个函数,最少一个模块内要释放!程序一直跑,一直要内存,不够了就容易出问题
申请内存关闭内存!
打开文件关闭文件!

void*和函数指针进行转换在 c99 标准中未定义
一个函数若没看到头文件,函数的返回值一般情况下当作int来对待,所以可以当作int来对待导致等号俩边不匹配(其实是没包含头文件,不是强转的问题)

面试题:test.c,我要让开辟的内存与主函数p有关系,要么返回p要么地址传参
free(p);
p = NULL;       //一旦释放立即置空,良好习惯
例子如下:

#include <stdio.h>
#include <stdlib.h>

void func(int **p, int n)              //不能传入int *p
{                                                //用二级指针间接赋值,一级指针是值传递
    *p = malloc(n);                      //*p == main中的p;也就是main中的p本来是空值,然后给这块地址申请空间,这样主函数p中的内容就是申请到新空间的起始地址
    if(*p == NULL)
    {
        exit(1);
    }
    (*p)[1] = 99;                          //*p就等于主函数中的p

    return;
}

int *func2(int *p, int n)              //指针函数,返回那100个字节起始地址号
{
    p = malloc(n);
    if(p == NULL)
    {
        exit(1);
    }
    p[1] = 99;

    return p;                       //我要让开辟的内存与主函数p有关系,要么返回p要么地址传参;带回开辟空间的起始位置
}

int main()
{
    int num = 100;
    int *p = NULL;

//  func(&p, num);              //把p的地址传过去拿回来所开辟的空间首地址

    p = func2(p, num);
    printf("p[1] = %d\n", p[1]); //验证p是那个开辟的空间的p

    free(p);
    exit(0);
}

#if 0          //是有问题的,func结束,p就没了,p是func函数的局部变量,func释放,p指向的起始地址就没了,在main中没有获取指向100个字节的首地址
void func(int *p, int n)
{
    p = malloc(n);
    if(p == NULL)
    {
        exit(1);
    }

    return;
}

int main()
{
    int num = 100;
    int *p = NULL;

    func(p, num);           // 就是相当于定义了一个数值为空的指针,指向的是空地址,然后将p的数值即空值传过去,这是值传递,函数那边接收到一个空,就对主函数没后续了

    free(p);                    //free空指针相当于什么都不做,但也不出错;这100个字节空间只有主程序正常结束了才会释放
    exit(0);
}                                  //已经产生了内存泄漏
#endif


student.c:数组太过于死板,要指定大小,换成指针可以进行内存动态分配;里面还有其他注意事项,比如说数值赋值正常,指针赋值不可以,俩个指针就指向同一个了


重定义typedefine:

typedef: 针对某一个已有的数据类型改名
形式:typedef  已有数据类型  新名字;  --->  typedef int INT; 结尾有分号!!!
typedef int INT;比如当我int类型不够用,换unsigned int可以直接 typedef unsigned int INT;可以直接换掉不需要全局一个一个替换
两个不同的设备,int类型可能一个四字节一个两字节,全局改很麻烦,用typedef改了

typedef.c:中区别各种typedef和#define

#define INT int        //新名字     旧名字      第一个代替第二个(define是直接一比一的替换,不做任何困难的操作)
typedef int INT;       //旧名字     新名字;    第二个代替第一个(typedef是用第二个换到初始地方去替换得到表达式,如typedef int FUNC(int);  而FUNC f; 等同于  int f(int); 就是将f代入到typedef定义中去,即f替换这typedef int FUNC(int)里面的FUNC

//这两写法不一样要注意区分!!!

#define   IP     int *       IP p, q;  --> int *p, q;          //define只是替换这一部分,define是单纯的一对一替换
typedef   int *     IP;     IP p, q;  --> int *p, *q;         //typedef相当于给int * 取了个别名叫IP,typedef是取别名
 
typedef int ARR[6];   -->  不是 int--> ARR[6];  而是 int [6] -> ARR  //在给int[6] 改名
ARR a;  -->  int a[6];    //定义一个ARR a;  相当于定义的 int a[6];

struct node_st
{
    int i;
    float f;
};
typedef struct node_st NODE;        NODE a;  -->  struct node_st a;      /     NODE *p;  -->  struct node_st *p;
typedef struct node_st *NODEP;        NODEP p;  -->  struct node_st *p;        // 相当于将p代入原式中的NODEP

typedef struct        //可以没有名字,我给他取别名了 --> 相当于typedef struct [无名] NODE; 将这个结构体取别名为NODE
{
    int i;
    float f;
}NODE, *NODEP;        //将struct这个结构体类型改成 NODE 来代替         可以用 NODE 来定义:NODE a;

typedef int FUNC(int);   -->   int (int) -> FUNC        FUNC f; 等同于  int f(int);
为返回值为int,参数为int的函数改的名,这个函数的名字叫FUNC

typedef int *FUNCP(int);   -->  int *(int) -> FUNCP        FUNCP p; 等同于  int *p(int);
指针函数;为返回值为指针int *,参数为int的函数改的名,这个函数的名字叫FUNCP

typedef int *(*FUNCP)(int);   -->   int *(int) -> (*FUNCP)        FUNCP p;  等同于  int *(*p)(int);
指针函数指针;定义了一个指针变量p,p指向返回值为指针int*,参数为int的这样一个函数
指向指针函数的函数指针(是一个函数指针(指针指向函数),指向的是指针函数(是一个返回值为指针的函数))

Makefile

Makefile 工程管理器
makefile / Makefile 源码包里面有这俩,大写M 代表程序员写的,make默认执行makefile里面的内容;Makefile程序员写的一些编译过程,我不需要可以自己写一个小写的makefile,优先调用小写的makefile
make执行的脚本就是makefile

一个大工程项目project包含很多小项目,小项目里面又包含很多模块,一个一个执行gcc太麻烦了;做这个脚本一次性编译

格式:
target : files
    cmd

对于5个文件:main.c  tool1.c  tool1.h  tool2.c  tool2.h 同时编译:gcc *.c  生成  a.out, a.out(相当于我取名为mytool的文件)依赖于-->  main.o(依赖于-->main.c); tool1.o(依赖于-->tool1.c);  tool2.o(依赖于-->tool2.c); 
用makefile://-c是编译,-g是调试gdb,-o是指定名字

clean:
    rm *.o mytool -rf
强制删除并且递归删除
命令行内执行make clean 就会执行rm *.o mytool -rf这条语句,删除这些文件,再make重新生成

能分得清时间戳改变(也就是mytool是最新的文件,其他时间比它晚,那肯定就是最新的文件不需要重新生成,如果其他文件时间比他新那他就需要重新生成),变了则重新生成,make也会有提示,这是新的文件;只会更新变了的文件

CFLAGS 表示用于 C 编译器的选项,
CXXFLAGS 表示用于 C++ 编译器的选项。
这两个变量实际上涵盖了编译和汇编两个步骤。
CFLAGS+= //是指在原来的基础上再加上一些命令功能

例子如下:
OBJS=main.o tool1.o tool2.o
CC=gcc
RM=rm -f
CFLAGS+=-c -Wall -g        // 我也不知道你本来有什么命令选项,我额外继续添加命令


mytool:$(OBJS)
    $(CC) $(OBJS) -o mytool
等价
mytool:main.o tool1.o tool2.o                   // mytool最终可执行文件依赖于这三个.o文件
    gcc main.o tool1.o tool2.o -o mytool    // 编译这三个.o文件合并为mytool文件(-o是指定名字)


main.o:main.c
    $(CC) main.c $(CFLAGS) -o main.o
等价
main.o:main.c                                        // main.o依赖于main.c文件
    gcc main.c -c -Wall -g -o main.o        // 编译main.c并指定名字即生成main.o(-c是编译,-g是调试,-o是命名,-Wall是显示警告)


tool1.o:tool1.c
    $(CC) tool1.c $(CFLAGS) -o tool1.o


tool2.o:tool2.c
    $(CC) tool2.c $(CFLAGS) -o tool2.o


clean:
    $(RM) *.o mytool -r                        // 删除所有的.o文件(RM默认为rm -f)
等价
clean:
    rm *.o mytool -rf                             // rf是强制删除并且递归删除


继续改进简洁一点:

mytool:$(OBJS)
    $(CC)   $^   -o   $@
^代表把上面的OBJS挪过来了,即上述依赖的文件;$@ == mytool即是上一句中的目标文件

%.o:%.c
%百分号相当于通配符,在同一句话当中%代表同一个名字

例子如下:
OBJS=main.o tool1.o tool2.o
CC=gcc
RM=rm -f
CFLAGS+=-c -Wall -g


mytool:$(OBJS)
    $(CC) $^ -o $@                // $^ == $(OBJS) == main.o tool1.o tool2.o 表示上一句依赖关系中被依赖的所有文件,$@ == mytool即上一句中的目标文件

%.o:%.c                                // % 百分号相当于通配符,在同一句话当中 % 代表同一个名字;这样就相当于等于三句main.o,tool1.o,tool2.o
    $(CC) $^ $(CFLAGS) -o $@        // 相当于每个类型的.o都执行这两行语句, %代表所有的该类型文件


clean:
    $(RM) *.o mytool -r

24.1.12复习

  • 21
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Linux再对我好一点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值