行业资讯
📅 2026/8/25 18:05:33
C 语言笔记
笔记会不定期补充更新数据类型基本类型数据整型整型 -- int 4字节短整型 -- short int 2字节长整型 -- long int 8字节浮点数【实数】单精度浮点数 -- float 4字节双精度浮点数 -- double 8字节字符char 1字节复合类型数据结构体、 枚举、共用体变量# includestdio.h int main(void) { int i; i 3; printf(i %d\n,i);//3最终是存放在内存中程序终止后3所占的空间被释放 return 0; }让内存条中的一块空闲单元与所定义变量产生关联。变量的本质就是内存中的一段存储空间程序终止后内存空间被释放。变量必须初始化就是赋值的原因替代内存中原来的垃圾值。单个字符变量要用单引号括起来字符串要用双引号括起来。字符串不能赋给单个字符变量char。单引号也不能括字符串。printf用法将变量的内容输出到显示器上。1.printf字符串;2.printf (输出控制符输出参数);3.printf输出控制符1 输出控制符2 ......,输出参数1,输出参数2.........);4.printf(输出控制符 非输出控制符,输出参数);输出控制符%d %ld %c %f %lf %x %s输出控制符可以清楚的表明所输出的内容所表达的具体含义。scanf用法通过键盘将数据输入到变量中。1.scanf(输入控制符,输入变量);将输入的数据放到输入变量地址下存放。2.含有非输出控制符例如逗号、字母等等。非输入控制符必须原样输入。#includestdio.h int main(void) { int i; scanf(m%d,i); //m是非输出控制符 printf(i %d\n,i); return 0; } /* 输入123 输出垃圾值 输入m123 输出123 输入m123n 输出123 先输入m时计算机认为合法输入123也合法输入n不合法。 */3.应该编写代码对用户的非法输入做适当处理while ((ch getchar()) ! \n)continue;这段代码的功能是从标准输入读取字符并且忽略这些字符直到遇到换行符为止。流程控制什么是流程控制 程序代码的执行顺序。顺序、选择、循环。顺序1. if只能控制一条语句的执行else也只能控制一条语句。所以在执行多条语句时要加{ } 。if...else...为顺序执行语句若出现多个条件同时成立则执行最靠前的一条。2. 一个分号代表一条语句的结束。循环for循环# includestdio.h int main(void) { int i; int sum sum i; for(i1;i4;i) //执行完条件判断语句后执行条件执行语句然后才会执行i sum sum i; printf(sum %d\n,sum); //在for循环结束后才会执行printf()指令 return 0; }1.for循环同样只能控制一条语句控制多条语句加括号。在for循环结束后才会执行printf()指令。2. 执行完条件判断语句后执行条件执行语句然后才会执行i。3.强制类型转换格式(数据类型)(表达式)功能把表达式的值强制转化为前面所执行的数据类型。4.浮点数存储带来的问题float和double都不能保证可以精确的存储一个小数。举例有一个浮点型变量x如何判断x的值是否为零if(x 0) //不一定能判断正确判断条件可以写成if|x-0.000001| 0.000001是else不是while循环1.格式while表达式执行语句2.while同样只能控制一条语句的执行。若执行语句含有多条语句要注意语句的排列顺序其是顺序执行的。do.....while:格式do{...........}while(表达式);1.do......while主要用于人机交互,至少执行一遍while和for可能一遍都不执行。选择switch格式#includestdio.h int main(void) { int val; printf(请输入楼层); scanf(%d,val); switch (val) { case 1: printf(1层开); break; case 2: printf(1层开); break; default: printf(还没有); break; } return 0; }1. 在switch中case、default是程序的入口然后顺序执行break是程序的出口 。2.所有case后面的常量表达式只能是枚举常量、数值常量、字符常量、常变量或宏名中的一种注意普通变量和枚举变量是不能作为case后面的常量表达式使用的。3.switch后边括号里的表达式可以使任何类型。4.break的用法用在循环中则是用来终止循环。用于switch则是用来终止switch终止距离最近的switch。不能直接用于if除非if属于循环内部的一个子句。在循环中if后面的break是用来终止循环的不是用来终止if的。在多层循环中break只能终止距离他最近的循环。5.continue的用法#includestdio.h int main(void) { int i; char ch; scanf(%d,i); printf(i %d\n,i); while ((chgetchar()) ! \n) continue; //只有检测到输入为换行符时才执行continue之后的指令 int j; //功能可以检查非法输入 scanf(%d,j); //例输入1 da dasd\n 输出1 da dasd printf(j %d\n,j); //只有在输入\n后才会输出正确的输入字符 return 0; }主要用在循环语句中当执行到continue时跳过在本次循环中continue后面的程序直接去判断下一次循环条件是否满足。数组数组的定义int i[5] {1,2,3,4,5}; //定义长度为5的数组完全初始化 int i[5] {1,2,3,}; //未完全初始化未初始化的位为0int i[5] ; //未初始化所有位是垃圾值int i[5] {0}; //清零数组的作用为n个变量连续分配存储空间所有变量数据类型必须相同。为了解决大量同类型数据的存储和使用问题 为了模拟现实世界。一维数组一维数组的名称是变量名其保存数组第一位数据i[0]的地址。只有在定义数组时才能整体赋值其他情况下整体赋值都是错误的。二维数组 定义int a[2][3]; //二行三列a[0][0]、a[0][1]....... 二维数组将其内容输出通过两个for循环。#includestdio.h int main(void) { int a[3][4] { //初始化数组 {1,2,3,4}, {5,6,7,8}, {9,10,11,12} }; int i,j; /* int a[3][4] {1,2,3,4,5,6,7,8,9,10,11,12}; //初始化数组 */ //输出数组内容 for(i0;i3;i) { for(j0;j4;j) printf(%-5d\n,a[i][j]); //5表示每个字符占五个位置-号表示左对齐 printf(\n); } return 0; }多维数组多维数组不存在多维数组因为内存是线性一维的n维数组可以当作每个元素是n-1维数组的一维数组。注把数组a中的值全部赋给数组b正确写法;for(i0;i5;i)b[i] a[i];补充:sizeof(arr)arr虽然数组名保存的是数组首元素的地址但上述两种操作判断的是整个数组的大小、取的是整个数组的地址而不是首元素的大小、首元素的地址对于数组前对于数组类型的定义定义的是数组中每个元素的类型同样定义了每个元素的大小int (*p)[10] arrp是数组指针也叫 “指向数组的指针”p保存的是数组的地址写[10]是为了明确指针指向的是有10个元素的数组由于arr是数组地址如果写 int *p arr 会报 类型不匹配函数指针数组/* Add,Mul,Sub,Div是四个函数 */int (*pf)(int,int) Add; //pf是函数指针int (*arr[4])(int,int) {Add,Mul,Sub,Div}; //arr是函数指针数组函数指针数组的使用ret arr[input](x,y); //对于功能繁多的情况使用case语句繁杂其不易维护就可以使用函数指针数组掉用不同的功能柔性数组在结构体的最后一个成员时未明确大小的数组struct S { int n; int arr[]; } int main() { /* 柔性数组的使用 */ struct S* ps (struct S*)malloc(sizeof(struct S) 40); //结构体的大小只有4字节 ps-n 100; int i 0; for(i 0; i 10; i) { ps-arr[i] i; } for(i 0; i 10; i) { printf(%d,ps-arr[i]); } struct S* ptr (struct S)relloc(ps,sizeof(struct S) 80); if(ptr ! NULL) ( ps ptr; //此时ps与ptr都指向relloc分配的空间地址 ptr NULL; //赋值为空防止误操作ptr使得ps变成野指针 ) //释放 free(ps); ps NULL; return 0; }函数函数定义void max(int i, int j)}函数的执行体 {void表示函数没有返回值max是函数名称括号()内是形参列表 。int f(void) //void表示函数f不能接收数据int是函数返回值的类型。 { return 10; //向主调函数返回10 } void g(void) //函数名前面的void表示函数没有返回值 { //return 10; //如果写了返回值就会报错 }注在使用函数时会为形参变量分配空间当函数执行完后该空间会被释放在下一次使用该函数时会为形参变量分配新的存储空间且分配的不一定是同一个空间。但程序是从main函数进入的也是从main函数退出的。函数返回值的类型也称为函数的类型因为如果函数名前的返回值类型与函数执行体中的return 表达式中表达式的类型不同的话则最终函数返回值的类型以函数名前的返回值类型为准。实参与形参形参是函数定义时的参数用于接收实参的值在函数内部使用。实参是函数调用时传递的实际值用于初始化形参需要确切的数据。函数的形参函数的形参在函数执行结束后就被销毁了return使用格式return 表达式;作用函数中的return是用来终止被调函数并向主调函数返回表达式的值如果表达式为空则只终止函数不向被调函数返回任何值。与break是用来终止循环不同。需要在main函数中调用的函数要写在main函数的前面顺序执行。main函数下可以调用其他函数但其他函数不能调用main函数可在main函数前添加函数声明就能将函数写在main函数的后面。函数声明格式void f(void); //函数声明是一个语句要加分号函数的功能设计要单一才能提高复用率而不是设计的十分复杂。函数中传递一维数组参数需要传递数组名称和元素个数。但字符数组不需要传元素个数。//数组传参 //函数传递数组求和 #include stdio.h int array_sum(int * data,int n); int main(int argc,char *argv[]) { int a[] {5,9,10,3,10}; int sum 0; sum array_sum(a,sizeof(a)/sizeof(int));//传递数组就必须要传起始地址a和元素个数n reyurn 0; } int array_sum(int * data,int n)//int * data a;就是一个指针接收了数组名 { int ret 0; int i; for(i0;in;i) ret data[i]; return ret; }//字符数组传参 //函数字符数组传参不需要传元素个数因为字符数组有\0判断结束 includestido.h int main(int argc,char *argv[]) { char s[] h a sdg k; puts(s); del_space(s); puts(s); return 0; } void del_space(char * s1) { char * s2; s2 s1; //一个指针用来处理一个指针用来赋值 while (*s1){ if(*s1 ){ s1; } else{ *s2 *s1; s1; s2; } *s2 \0;//补充\0标志字符串结束 } }指针函数指针函数是指一个函数的返回值为地址量的函数。结构数据类型*函数名称(参数说明){语句序列;}错误示例#includestdio.h #includestring.h char * mystring(){ char str[20]; strcpy(str,hello); return str; //返回值是指针 } int main(void) { printf(%s\n,mystring()); //打印函数返回值 return 0; }由于函数执行完成后对应内存已经释放所以编译错误。只有当返回值为全局变量的地址、static变量地址、字符串常量的地址或使用malloc函数申请存储空间。递归递归函数指一个函数体中直接或间接调用了该函数本身。//阶乘计算 #includestdio.h int fac(int n); int main(int argc,char *argv[]) { int n; printf(input); scanf(%d,n); return 0; } int fac(int n) { if(n 0 || n 1) return 1; return n * fac(n-1);//在函数中调用了该函数 }栈一个端口既是出口也是入口。补充对于函数参数的设计函数参数的设计取决于要传入的是什么就是说形参与实参的类型要匹配假设要传入一个一级指针那就需要将参数定义为一级指针类型函数指针指向函数的指针数组指针指向数组的指针指针数组数组成员是指针的数组回调函数通过函数指针在适当是调用他所指向的函数变量函数内定义的变量是局部变量所有函数外定义的变量是全局变量。在一个函数内部如果定义的局部变量的名字和全局变量名一样时局部变量会屏蔽掉全局变量。static变量1. 存储位置固定静态变量存放在程序的数据段已初始化的静态变量或者 BSS 段未初始化的静态变量并非栈或堆。这使得它的内存地址在程序运行期间保持不变。2. 延长生命周期静态变量的生命周期和程序的运行周期一样长。就算定义它的函数执行完毕其值也不会被销毁下次调用该函数时静态变量会使用上次保存的值。3. 仅初始化一次静态变量在程序加载时就完成初始化而非每次函数调用时都初始化。如果没有显式初始化它会被自动初始化为 0对于数值类型或者 NULL对于指针类型。指针1指针的含义#include stdio.h int main(void) { int *p; int i; p i; /* p保存了i的地址因此p指向i p不是ii也不是p更准确的说修改p的值不会影响i修改i的值也不会影响p */ return 0; }指针的本质是操作受限的非负整数不能进行加、减、乘、除等操作。p是变量的名字int *表示p变量存放的是int类型变量的地址。也就是说只有整形变量的地址才能存入变量p。p是变量名p变量的数据类型是int*类型而*p是int类型也就是指针所指向的内容的数据类型是整型。*p 就是以p的内容为地址的变量*p和取地址是逆运算。指针就是地址地址就是指针指针变量是存放地址的变量。如果一个指针变量指向了某个普通变量则*指针变量就完全等同于普通变量。地址就是内存单元的编号。p指的是所指变量的地址*p指的是该地址下的内容。汇编中的的概念给地址加括号表示该地址的内容。例DPTR 1000H,DPTR 1230H 变量p存放的是地址*p存放的是该地址下的数据。在 C 语言里声明指针时不会自动为其分配内存地址。要是直接使用未初始化的指针它的值是随机的这就会让指针指向内存中一个不确定的位置。在其他函数中定义的二级指针能跨函数修改变量的值是因为二级指针存放的是地址而不是变量内容 。/* 数字交换程序 */ includestdio.h void exchange(int *p,int * q) { int t; //注意t必须定义为int类型 t *p; *p *q; *q t; } int main(void) { int a 3; int b 4; exchange(a,b); //exchang*p*q是错误的exchangeab也是错误的 printf(a %d, b %d/n,a,b); reeturn 0; }void类型指针未定义指针类型在使用时需要强制类型转换。const修饰指针情况一const 数据类型*指针变量名称[指针运算表达式];常量化指针目标是限制通过指针改变其目标的数值但指针变量存储的地址值可以修改。情况二数据类型*const指针变量[指针运算表达式];使得指针变量存储的地址值不能修改。但是可以通过*指针变量名称可以修改指针所指向的变量的数值。#includestdio.h int main(int argc,const char argv[]) { int m 10; const int * p; //情况一 int * const q m; //情况二 const int * const r m; p m; //right //(*p); //error //q m; //error *q 20; //right r; //error (*r); //error return 0; }*号的用法乘法、定义指针、指针运算符a *p*p是以p的内容为地址的变量。通过指针在被调函数中修改主调函数中的值例 *形参名 ........2指针常见的错误# include stdio.h int main(void) { int i 5; int *p; int *q; p i; *q p; //错误1类型不匹配 错误2没有对q初始化 *q *p; //错误q未初始化 p q; //q是垃圾值q赋给pp也变成了垃圾值 /* 在 C 语言里声明指针时不会自动为其分配内存地址。要是直接使用未初始化的指 针它的值是随机的这就会让指针指向内存中一个不确定的位置。 本程序无法读取*q的内容得到原因指针q没有被初始化它指向的是一个随机的内 存地址直接对其进行解引用会导致未定义行为。 */ printf(%d\n,*q); return 0; }3指针和数组指针和一维数组一维数组名是一个指针常量不能做赋值操作存放的是第一个元素的地址。确定一个一维数组只需要数组第一个元素的地址和数组长度。#include stdio.h void f(int * pArr, int len) { pArr[3] 88; //改变的是数组第四个元素的值 } int main(void) { int a[6] {1,2,3,4,5,6,}; printf(%d\n,a[3]) f(a,6) //将数组a的首地址传递给函数f确定一个数 //组或对数组进行操作只需要数组的首地址和数组长度 printf(%d/n,a[3]) return 0; } 输出结果 --------------------------------- 4 88 ---------------------------------下标和指针的关系如果p是一个指针变量则 p[i] 永远等价于 *p i。*p i的含义取出pi地址中的内容。指针变量运算指针变量不能相加、相乘、相除。只能相减 如果两个指针变量指向的是同一块连续空间中不同的存储单元则这两个指针变量才能相减。 例如一个数组中不同元素的地址。一个变量的地址使用该变量首字节的地址表示。无论他指向的变量占几个字节每个指针变量只占用四个字节。因为无论指针变量保存的地址有多大都需要32位地址线来表示(324*8)。对于硬件地址每一个字节一个编号。例如double类型的变量i占8个字节那么i保存的是第一个字节的地址。一个变量的地址使用该变量首字节的地址表示4动态内存分配静态数组的缺点1.数组的长度必须提前制定且定义的长度只能是常数。int a[5]; //okint len 5; int a[len]; //error2.传统形式定义的数组该数组的内存程序员无法手动释放。函数中定义的数组只有在函数运行完之后系统分配个数组的内存空间才会被释放。3.数组的长度不能在函数运行的过程中动态的扩充或缩小。4.A函数定义的数组在A函数运行期间可以被其他函数使用但A函数运行结束之后由于数组A的内存已经释放了所有A函数中的数组就不能被其他函数使用了。为什么需要动态分配内存原因就是解决了上面的四个缺陷。//动态分配函数malloc的用法1 #include stdio.h #include malloc.h int main(void) { int i 5; //i是静态分配的 int * p (int*)malloc(4); //a行需对函数返回值强制类型转换才能确定有几个变量由于 //int类型变量占4个字节所有该行动态分配了1个变量 /* 1.要使用malloc这个函数要添加对应头文件 2.malloc函数只有一个形参并且形参是整数 3.4表示请求系统为本程序分配4个字节 4.malloc函数只能返回第一个字节的地址 5.a行分配了8个字节p变量占4个字节p所指向的内存也占4个字节 6.p本身所占的内存是静态分配的p所指向的内存是动态分配的 */ free(p);//由程序员手动释放掉p所指向的动态内存 //而p所占用的内存只能在所在函数运行终止时由系统释放 return 0; }/* malloc函数用法2:在一个函数中动态分配的内存在另一个函数中进行处理 */ # include stdio.h # include malloc.h void f(int * q) { *q 200; } int main(void) { int * p (int*)malloc(sizeof(int)); *p 10; printf(%d\n,*p); f(p); printf(%d,*p); return 0; }动态数组的构造#include stdio.h #include malloc.h int main(void) { int len; int *pArr; int i; //动态构造一维数组 printf(请输入你要存放的元素的个数:) scanf%dlen pArr int*malloc4 * len //数组的每个元素都是int类型。 //也可以写成pArr[i]的形式使用 //对一维数组进行操作 for(i0;ilen;i) scanf(%d\n,pArr[i]); //对一维数组进行输出 printf(一维数组输出的内容时\n) for(i0;ilen;i) printf(%d\n,pArr[i]) free(pArr);//释放掉动态分配的数组 return 0; }静态内存与动态内存的区别补充malloc和freevoid* malloc(size_t size);1.如果开辟成功则返回一个指向开辟空间的指针2.如果开辟失败则返回一个NULL指针因此返回值一定要检查; if(p NULL){}3.返回值的类型是void*所以malloc函数并不知道开辟空间的类型具体在使用的时候使用者自己决定强制类型转换4.如果size为0malloc的行为是标准的是未定义的取决于编译器内存开辟int* p malloc(40); //在堆区开辟40字节大小的内存内存释放free(p);p NULL; //释放内存后p仍然保存了原地址为防止内存泄露为p赋值NULLcalloc函数void* calloc(size_t num, size_t size);1.num是创建元素个数 size每个元素的大小2.相比于malloccalloc会将开辟的每个字节都初始化为0relloc函数void* relloc(void* ptr, size_t size);1.ptr是要调整的内存地址size是调整后的新大小2.返回值为调整之后的内存的起始地址3.调整之后的内存空间可能是在原来内存空间的基础上也可能是开辟了一块新的空间扩充空间的两种情况1原空间后有足够大的空间满足扩充条件则由原空间加扩充的部分构成2原空间后空间不足就会开辟一块新的空间能够容纳扩充后的大小动态内存分配中的常见错误1.对于创建的动态内存要检验其返回值是否为空再对其进行解引用操作2.不能对动态分配的空间越界访问3.对非动态开辟的空间使用free释放4.由于程序将堆区地址跑丢了使用free释放一块动态开辟内存的一部分可以定义中间变量保存动态内存的起始地址在释放时传中间变量给free()或者写成 p[i]*(pi) p是动态内存分配函数的返回值5.对同一块动态内存多次free因为在第一次free后函数的返回值依然记着所释放内存的地址所以要在ferr后对返回值赋值NULL6.动态开辟的内存空间忘记释放内存泄露开辟了空间但是不使用导致该进程中该地址空间不能被使用只有进程结束后才会被系统回收相当于内存泄露了比如1在开辟内存命令和释放内存命令之间存在return命令可能出现没有机会执行free命令的情况2内存开辟在一个函数中进行但释放命令交给了其他函数忘记写释放命令/* 程序崩溃 */ void GetMemory(char* p) { p (char*)malloc(100); //存在内存泄露p是形参函数结束后就被销毁了所以p指向的动态分配空间在该函数执行完成后就找不到了 } void Test(void) { char* str NULL; GetMemory(str); strcpy(str, hello world); //str依然是NULL解引用时崩溃 printf(str); //且该程序没有free过程也存在问题 } int main() { Test(); return 0; }(5)跨函数使用内存问题静态内存不能跨函数使用//静态变量不能跨函数使用 #include stdio.h void f(int ** q) { int i 5; *q i; } int main(void) { int *p; f(p); printf(%d\n,*p);//该行语法没问题但是逻辑有问题p可以保存i的地址但不能访问i的空间 //因为在f函数执行完后i的空间就被释放了不能读也不能写 return 0; }动态内存可以跨函数使用/* 动态内存分配 静态内存在栈里面分配动态内存在堆里面分配 */ #include stdio.h #include malloc.h void f(int * q) { *q (int *)malloc(sizeof(int)); **q 5; //等价于pint*mallocsizeofint } int main(void) { int * p; f(p); printf(%d\n,*p);//由于在f函数中没有手动释放内存命令 //所有该内存在执行完f函数后任然存在所有该行语句正确 return 0; }定义的静态变量在其内存被释放之后就不能再在其他函数中被调用了。不直接写数字是为了在不同设备上定义正确的字节数。函数sizeof返回变量类型的字节数。动态分配是在堆中分配地址。而静态分配是在栈中分配地址运行结束后就会出栈就释放了所有就不能跨函数调用。malloc函数就是通过定义二级指针的方式实现跨函数修改变量的目的。二级指针的核心作用是允许函数修改一级指针的值。如果只是用一级指针指向另一个一级指针类型不匹配而且无法实现修改实参指针这样的功能。…………………………………………………………………………………………第二段代码里不是“普通变量能在main里赋值”——普通变量 p 、 q 只是“持地址的工具人”真正被跨函数赋值、修改的是 malloc 申请的堆内存它不受函数生命周期限制只要有有效地址就能操作。普通变量栈区本身依然不能跨函数赋值比如 f 里改 q 的值 main 里的 p 不会变。指针指向的内存“无效”要么被释放、要么根本没分配过此时通过指针操作内存读/写就是非法的野指针操作。核心答案一句话说透内存是否释放和“哪个函数分配”无关只看“内存存放在栈区还是堆区”——栈区内存随函数结束自动释放堆区内存必须手动free才释放。一、先明确两个关键前提嵌入式开发必记1. C语言中局部变量的“存放区域”由 定义方式 决定不是“哪个函数定义”决定- 普通局部变量比如 int i 5; 默认存 栈区函数专属内存轻量、自动管理。- malloc 申请的内存比如 int *p (int*)malloc(...) 存 堆区全局共享内存重量级、手动管理。2. 函数调用的本质只是“执行代码逻辑”不会改变内存的“区域属性”——栈区内存不会因为在其他函数中被访问就变成堆区堆区内存也不会因为函数结束就自动变成栈区。二、两段代码的内存差异拆解对应你的疑问我们分别看“f函数中的i”和“malloc申请的内存”的生命周期1. 第一段代码f函数里的 int i 5; - i 的定义 f 函数内的 普通局部变量 → 存栈区。- 内存生命周期- 进入 f 函数时系统在栈区给 i 分配空间存5- 退出 f 函数时栈区的核心规则是“函数结束自动回收内部所有局部变量的内存”——不管 main 函数的指针 p 是否还存着 i 的地址 i 的栈内存已经被系统标记为“可复用”后续访问就是非法的“野指针操作”。- 关键 i 是栈区变量所以 f 函数结束就释放和“有没有被其他函数的指针指向”无关。2. 第二段代码main里 malloc 的内存- 内存的定义 main 函数中用 malloc 申请 → 存堆区。- 内存生命周期- malloc 执行时系统在堆区分配一块 int 大小的空间返回地址给 p - 调用 f(p) 时只是把“堆区地址”传给 f 的 q q 和 p 指向 同一块堆内存—— f 函数里的 *q 200 本质是修改“堆区内存的值”不是修改“内存的存放区域”- 退出 f 函数时 f 的局部变量 q 栈区指针会被回收但它指向的 堆内存没动堆区内存的回收只认 free 不认函数结束- 回到 main 后 p 依然指向这块堆内存所以能正常访问修改后的值200直到手动 free(p) 才释放。- 关键 malloc 的内存是堆区变量所以 f 函数结束不会释放和“在main里分配、f里访问”无关。三、一句话总结核心区别简单说栈区内存“跟函数走”函数没了内存就没堆区内存“跟程序员走”你不释放它就一直在——这也是嵌入式开发中处理缓冲区、数据结构时优先用 malloc 需注意内存泄漏的原因。…………………………………………………………………………………………也就是说无论是在哪里定义的什么类型的变量操作逻辑都是针对内存空间来说的只需要看内存空间的地址和地址下所存储的内容。变量只是内存空间的“别名”或“标识”所有对变量的操作本质都是通过“变量关联的内存地址”去读写这块地址里的内容变量是给人看的“标识”内存地址和内容才是给计算机执行的“本质”——所有代码逻辑最终都会落地到“哪个地址、存什么内容”的操作上。6函数指针定义数据类型*函数指针名称[大小](说明列表)其中大小是指函数指针数组元素的个数#includestdio.h //生成函数 int add(int a,int b) { return ab; } int main(int argc, char *argv[]) { int m 10,n 20; int (*p)(int,int); //函数声明 p[0] add; //指针p指向函数add printf(%d\n,(*p)(m,n)); //函数调用 //p[1] sub; return 0; }补充定义指针的类型是指指针所指向的内容的类型比如char *p指的是p指向的内容是char类型函数指针指向函数的指针数组指针指向数组的指针指针数组数组成员是指针的数组函数指针在代码重复的部分其中只有一个函数不一样就可以采取封装一个函数这个函数的形参是函数指针类型指向冗余代码中的不同函数无具体类型的指针void* pa a;/* void*表示无具体类型的指针可以接受任意类型的地址由于其是无具体类型 的指针所有不能解引用通过指针变量访问其指向的内存地址中存储的值操作也不能-整数需要强制类型转换后在操作 */指针的初始化对于未初始化的指针定义该指针可能指向任何位置可能是程序的其他数据、代码区甚至是受保护的内存此时解引用会导致程序崩溃结构体定义赋值结构体#include stdio.h struct student //这只是定义了一个数据类型并没有定义变量 { int age; float score; char name; } //结构体定义赋值 int main(void) { //方式一 struct student st {15, 84, JIM}; //st是变量名定义的同时赋初值。 struct student st2; st2.age 15; //赋值 st2.score 84; st2.name JIM; //方式二 struct student *pst st; //将普通变量st的地址保存到pst pst-age 15; //赋值 return 0; }pst - age 88.8f; //方式二/指针变量名- 成员名 在计算机内部会被转化成(*pst).age也等价于st.age。88.8默认为double类型在末尾加f可转换成float类型且浮点数不能准确存储。通过函数读结构体进行输入和输出int main(void) { struct Student st; //st为struct Student类型变量名用来保存该结构体的数据 InputStudent(st); //对结构体变量输入必须发送st的地址 OutputStudent(st); //对结构体变量输出可以发送st的地址也可以发送st的内容 // OutputStudent(st); return 0; } //输出函数参数名为ss void OutputStudent(struct Student ss) { printf(%d %c %s\n , ss.age, ss.sex, ss.name); } //输入函数参数名为pstu void InputStudent(struct Student * pstu) //pstu只占四个字节,*前面的是指针的 //类型用来确定该指针占多少字节 { (*pstu).age 10; //等于st.age strcpy(pstu-name,张三); pstu-sex F; }虽然发送地址可能会造成内容误改写但是如果发送的是内容占用的内存太大而如果发送地址占用内存少执行速度快。所以推荐发送地址。结构体变量的运算:结构体变量不能相加不能相减也不能相乘除只能相互赋值。补充结构体对齐:对齐规则1.第一个结构体成员在与结构体变量偏移为0的地址处2.其他成员要对齐到该成员数据类型大小的整数倍地址处对齐数 编译器默认对齐数与该成员数据类型大小相比中的较小值3.结构体的总大小为最大对齐数的整数倍4.如果嵌套了结构体嵌套的结构体对齐到自己最大对齐数的整数倍外层结构体的大小依然是最大对齐数的整数倍含嵌套结构体的对齐数原因1.方便不同硬件平台访问数据读/写2.提高访问数据的速度用空间换时间对齐只需要访问一次但是不对齐可能出现一次访问没有访问到全部的数据对策让占用小空间的成员经量集中在一块结构体传参要传结构体地址减小在传值时压栈的开销导致的性能下降枚举把一个事物的所有取值的列举出来。//这只是定义了一个数据类型而不是一个变量,数据类型的名字是enum WeekDay enum WeekDay { monday,tuesday,wednesday,........,sunday }; int main(void) { enum WeekDay day wednesday; //day是变量等号后面只能写类型 //中的元素不能直接写数字 printf(%d\n,day); //输出结果为2 return 0; } //本函数的目的只是期望接受0-6之间的数字将形参定义为枚举类型 void f(enum weekday i) { switch (i) { case 0:printf(monday\n); break; case 1:printf(tuesday\n); break; case 2:printf(wednesday\n); break; case 3:printf(thursday\n); break; .... } }定义为枚举类型可以保证传输的都是有效信息使代码更安全。联合体成员共用同一段内存空间那么联合体的大小至少是最大成员的大小定义union un{char b,int c};//该联合体中成员b、c的存储起始地址是相同的所以也会出现数据覆盖的情况补码计算机内的数据以补码的形式存储。二进制转十进制先确定依据最高位确定正负若为负数将所有位取反再加1最后添加负号。十进制转二进制正数的补码:为其二进制本身空余的位数补0。负数的补码:其绝对值 的二进制取反加1空余的位数全部补1。要补够空余位数因为系统默认补0。例-3的补码是0xFFFFFFFDint类型变量所存储的最大正数用十六进制表示0x7FFF_FFFFint类型变量所存储的绝对值最大负数用十六进制表示0x8000_0000缺少符号位发生溢出ch中只保存了1个字节的数据 char ch 0x80;//字符型变量占一个字节 ch 129; //129占4个字节 printf(%d\n,ch); //输出结果为-127 0000_0000 0 0000_0001 1 ......... ... 0111_1111 127 1000_0000 -128 1000_0001 -127 ......... ... 1111_1111 -1链表链表的建立用结构体和函数写链表先构造一个头结点再依次写所需的节点个数通过malloc函数构造节点再将所构造的节点插入到头结点的后边返回了头结点的地址 。一个节点包括数据域和指针域两部分。#includestdio.h #includemalloc.h #inncludestdlib.h //定义了一个链表节点的数据类型 struct Node { int data;//数据域 struct Node * pNext;//指针域递归 }; //函数声明 struct Node * create_list(void); void traverse_list(struct Node *); int main(void) { struct Node * pHead NULL;//创建了一个struct Node类型的指针用来存放头节点的地址 pHead createlist();//CreateList()功能创建一个非循环单链表并将头节点地址作为函数返回值 traverse_list(pHead);//遍历链表元素 return 0; } //非循环单链表函数 struct Node * creat_list(void) { int len;//用来存放有效节点个数 int i; int val;//用来临时存放用户输入的节点的值 //分配一个不存放有效数据的头节点 struct Node * pHead (struct Node *)malloc(sizeof(struct Node)); if(NULL pHead) { printf(分配失败程序终止\n); exit(-1); } struct Node * pTail pHead; pTail-pHead NULL; printf(请输入您要生成链表节点的个数len ); scanf(%d,len); for(i0;ilen;i) { printf(请输入第%d个节点的值i1); scanf(%d,val); struct Node * pNew (struct Node *)malloc(sizeof(struct Node)); if(pNew NULL) { printf(分配失败程序终止\n); exit(-1); } pNew-data val; pTail-pNext pNew; pNew-pNext NULL; pTail pNew; } return pHead;//返回值为头节点的地址 //遍历函数 void traverse_list(struct Node * pHead) { struct Node * p pHead-pNext; while(NULL ! p) //链表不为空则执行循环 { printf(d\n,p-data); p p-pNext; } return; } }首节点存放第一个有效数据的节点。尾节点存放最后一个有效数据的节点。头结点头结点和首节点的数据类型一模一样头结点是首节点前面那个节点头结点并不存放有效数据。设置头结点的目的是为了方便对链表进行操作。头指针存放头结点地址的指针变量。确定一个链表只需要一个参数头指针。算法通俗定义解题的方法和步骤。狭义定义对存储数据的操作。对不同存储结构要完成一个功能所执行的操作是不一样的。例如输出数组中所有的元素的操作与输出链表中所有元素的操作肯定是不一样的。这说明算法是依附于存储结构的不同的存储结构所执行的算法是不一样的。广义定义也叫泛型。无论数据是如何存储的对该数据的操作都是一样的。数组优点存取速度快缺点需要一个连续的很大的内存空间插入和删除元素的效率很低。链表查找某个元素的效率低而链表不需要连续的空间来存储数据每一个元素的空间存储数据和下一个元素的地址。靠指针找到下一个元素。NULL的含义1.数值0。2.字符串结束标记符。3.空指针NULL,表示的是编号位零的地址。 至于其到底是什么含义取决于解码方式。计算机规定了以零为编号的存储单元内容不可读也不可写。free (p);p NULL;*p 某值;对于被释放的变量p要没有给p写入NULL就可能出现任然可以对*p赋值的现象。但如果写如NULL计算机不允许对编号为零的存储单元进行读写操作就避免了前面问题的发生。宏定义含参宏定义#define ADD(a,b) ((a)(b)) int result ADD(3,4);1.括号的使用要给每个参数和整个表达式都加上括号以此避免宏展开后出现运算优先级方面的问题。2.参数为表达式时可能出现副作用当参数是带有副作用的表达式像自增、自减运算时宏展开后可能会产生多次求值的情况。#define MAX(a,b) ((a) (b) ? (a) : (b)) int x 5; int y MAX(x,10)// 宏展开后是 ((x) (10) ? (x) : (10))x 会被递增两次3.字符串化操作符#借助#操作符可以把参数转换为字符串字面量。#define PRINT(x) printf(#x %d,x) PRINT(10); // 会被展开成 printf(10 %d\n, 10); 也就是 printf(10 %d\n, 10);4.标记粘贴操作符##使用##操作符能够把两个参数连接成一个标识符。#define CONCAT(a, b) a##b int xy 10; int result CONCAT(x, y); // 会被展开成 int result xy;补充assert的作用assert断言是编程中用于调试阶段验证关键条件 / 假设的工具核心作用是「自我检查」—— 确保程序运行到某一位置时预期的条件必须成立若不成立程序会主动触发错误断言失败帮助开发者快速定位问题而非等到错误扩散后才暴露。const的作用语义是只读基础类型的值直接存在内存栈中const 会锁定这个值的内存地址无法通过变量重新赋值修改会直接报错