C语言总结
文章目录前言1.数据类型及其存储1.1 基本数据类型基本数据类型两大类-常量与变量基本数据类型1 - 整型基本数据类型2 - 实型基本数据类型3 - 字符型字符串的输入获取基本数据类型4 - 枚举类型数据类型易错点隐式类型转换1.2 构造数据类型数组结构体联合体(略)指针类型大小端模式类型转换2.运算符3. 函数函数的调用方式常用库函数弱函数4.控制语句for, while, if数据的输入获取输入的一行字符串包括空格获取输入的一个数字以回车作为结尾前言C语言是一种可以直接操作内存单元的语言在嵌入式开发中经常使用得到。C语言的基础知识可以分为关键字数据类型运算符程序控制结构函数输入输出这及部分关键字。关于C语言的关键字要懂得各个关键字的用途。数据类型要知道其数据类型种类及其存储方式。运算符要懂得各种运算符的使用方法重点是其优先级和结合性。程序控制结构要了解语言的顺序结构条件结构循环结构。函数要了解函数的型参和实参函数的嵌套调用函数的递归调用函数指针钩子函数回调函数等概念。输入输出要了解格式输入输出字符输入输出字符串输入输出文件输入输出。参考资料《C语言谭浩强板》1.数据类型及其存储一门语言会有它固有的数据类型如python的字符字符串列表元组等C语言其也有其固有的数据类型。C夜有中数据类型细分为基本数据类型构造数据类型指针类型空类型。基本数据类型具体有整型实型字符型枚举型。构造数据类型复合类型有数组结构体联合体。指针类型内容包括指针的概念数组指针函数指针。1.1 基本数据类型基本数据类型两大类-常量与变量基本数据类型中有常量和变量之分。常量是在程序运行过程中不变的量其表示的方式有两种分别是字面常量符号常量。字面常量就是直接写出来的数组字符。符号常量即是通过宏定义定义出来的常量常量是在程序运行过程中会变的量变量要用变量名和变量值。基本数据类型1 - 整型整性常量的表示方法十进制八进制十六进制后缀有长整型后缀无符号后缀整型变量的表示方法int a0;存储格式是补码类型细分有基本型Int, 长整型long int, 无符号整性unsigned int等等。基本数据类型2 - 实型存储格式是IEEE754浮点数格式细分有单精度浮点数float双精度浮点数double,长双精度浮点数long double。实型常量表示方法十进制小数点法指数法基本数据类型3 - 字符型存储格式是ASCII码可以把字符型理解为只有8bit的整型。ASCII码中记住十进制的65表示字符’A’一些常用的转移字符字符串的输入获取#include stdio.h #include stdlib.h #include string.h int main() { char input[1024]; // 假设输入不会超过1023个字符最后一个字符为\0 printf(请输入一行字符串: ); if (fgets(input, sizeof(input), stdin) ! NULL) { // fgets() 会包含换行符如果有的话所以我们可以选择去掉它 input[strcspn(input, \n)] 0; // 使用strcspn移除换行符 printf(你输入的是: %s\n, input); } else { // 读取错误或EOF perror(fgets failed); } return 0; }基本数据类型4 - 枚举类型有些变量的取值被限定在一个有限的范围内使用枚举类型例如一周只有七天这时可以使用枚举类型定义星期一星期二…枚举类型的定义同构造类型的结构体。注意点枚举值是常量枚举元素序号从0开始。数据类型易错点隐式类型转换隐式类型转换(Implicit Conversion)是C语言中编译器自动进行的数据类型转换过程无需程序员显式指定。转换规则一低字节数据类型向高字节数据类型转换bool→char→short int→int→unsigned int→long→unsigned long→long long→float→double→long double注意有符号int类型 会被 转换成 无符号int类型小试牛刀根据如下代码猜测输出的c的值是多少#include stdio.h unsigned long Sum(long x, long y) { return (xy); } int main(void) { unsigned long a 10; long b -15; long c 0; c Sum(((ab0)?a:b), b); printf(ab0:%d\n, ab 0);//输出ab0:1 printf(c:%d\n, c);//输出c:-5 }二试牛刀下面代码会输出什么#includestdio.h#includestdint.h#defineVALUE_DETECTOR_READ_INITIAL_VALUE(0X8888)#defineVALUE_DETECTOR_READ_FAILD(0X5555)intmain(void){int16_taddrVALUE_DETECTOR_READ_INITIAL_VALUE;if(addrVALUE_DETECTOR_READ_INITIAL_VALUE){printf(\n);}else{printf(!\n);}return0;}答案输出!解析问题在于数据类型不匹配导致的比较结果不符合预期VALUE_DETECTOR_READ_INITIAL_VALUE定义为0X8888这是一个 16 位的十六进制值十进制为34952变量addr被声明为int16_t类型有符号 16 位整数在有符号 16 位整数的表示范围中最大值为0x7FFF327670x8888超出了有符号 16 位整数的表示范围会被解释为负数根据补码规则0x8888表示-30576因此实际比较的是if (-30576 34952)条件为假输出!1.2 构造数据类型数组按序排列的同类数据元素的集合称为数组。数组注意点数组声明元素个数时不可以使用变量来声明。按维度数组有一维数组二维数组多维数组按元素数据类型分分为整型数组字符数组字符串等。一维数组初始化注意点①部分元素初始化剩余的元素初始化为0二维数组用的不多略字符串字符串是一种特殊的一维数组数组的元素是字符以\0作为字符串结束符。字符串的处理函数数组与指针数组的步进单位char arr[3][2] {a,b, c,d,e,f}; printf(arr:%p\n, arr);//arr表示数组首地址 061FE1A printf(arr1:%p\n, arr1);//arr步进单位为整个数组1即加上数组的大小6061FE20 printf(arr:%p\n, arr);//arr表示数组首地址 061FE1A printf(arr1:%p\n, arr1);//arr步进步进单位为一个数组行1即加上数组行的大小2061FE1C printf(*arr:%p\n, *arr);//061FE1A*arr等同于arr[0]步进单位为数组元素大小 printf((*arr)1:%p\n, *arr1);//061FE1B1即加上数组元素char的大小1 printf(arr[0]:%p\n, arr[0]);//061FE1A表示一维数组arr[0]的首地址步进单位为数组元素大小 printf(arr[0]1:%p\n, arr[0]1);//061FE1B1即加上数组元素char的大小1 printf(arr[0]:%p\n, arr[0]);//arr[0]表示数组第0行首地址 061FE1A printf(arr[0]1:%p\n, arr[0]1);//arr[0]步进单位为数组行1即加上数组行的大小2061FE1C printf(arr[0][0]:%p\n, arr[0][0]);//arr[0]表示数组第0行第一列首地址 061FE1A printf(arr[0][0]1:%p\n, arr[0][0]1);//arr[0]步进单位为一个数组元素1即加上数组元素char的大小1061FE1B char arr[3][2] {a,b, c,d,e,f}; char (*p)[2] arr;//指向元素个数为2的一维数组的指针指针步进单位为一维数组大小 printf(p[2]:%p\n, p);//061FE1A printf(p[2]1:%p\n, p1);//061FE1C结构体结构体的初始化结构体的字节对齐结构体的字节对齐遵循以下三个规则规则一结构体中元素按照定义顺序依次置于内存中但并不是紧密排列默认情况下。从结构体首地址开始依次将元素放入内存时元素会被放置在其自身对齐大小的整数倍地址上整数倍地址是指该成员相对于结构体的首地址的 offset偏移必须是 其自身对齐大小 的倍数。各个数据类型的对齐大小如下。数组的对齐大小 数组元素类型的自身对齐大小注意规则二如果结构体大小不是所有元素中最大对齐大小的整数倍则结构体对齐到最大元素对齐大小的整数倍“末尾填充” 放在结构体末尾而 “内部填充” 必须插在不对齐的成员之间。规则三基本数据类型的对齐大小为其自身的大小结构体数据类型的对齐大小为其元素中最大对齐大小元素的对齐大小嵌套结构体的对齐大小取嵌套结构体的最大元素对齐大小。#include stdint.h // 验证规则一规则三元素对齐到自身大小整数倍 struct Test1 { uint8_t a; // uint8_t 对齐大小1 → 地址01的0倍占 1 字节0 uint32_t b; // uint32_t 对齐大小4 → 地址44的1倍a后填充3字节占 4 字节4‑7 uint16_t c; // uint16_t 对齐大小2 → 地址82的4倍占 2 字节8‑9 }; // 规则二总大小13填充4210 → 不是最大对齐大小4的倍数 → 末尾填充2字节 → 最终大小12 // 验证嵌套结构体规则三 struct Test2 { struct Test1 s; // 对齐大小Test1的最大元素对齐大小4 → 地址0 uint8_t d; // 对齐大小1 → 地址12 }; // 规则二总大小12113 → 不是4的倍数 → 末尾填充3字节 → 最终大小16 // 验证__packed打破规则嵌入式重点 struct __packed Test3 { uint8_t a; // 地址0无填充 uint32_t b; // 地址1非4字节对齐打破规则一 uint16_t c; // 地址5非2字节对齐 }; // __packed下规则二/三失效 → 总大小1427无任何填充联合体(略)指针类型指针类型的理解如下图所示为什么指针类型是属于构造类型指针类型是基于其他数据类型可以是基本类型、构造类型或其他指针类型衍生出来的用于表示内存地址因此被归类为构造类型。例如int*指向整数的指针依赖于int类型char**指向字符指针的指针依赖于char*类型变量指针在 C 语言中“变量指针” 通常指的是指向变量的指针即一个存储了某个变量内存地址的指针变量。inta10;// 定义一个int类型变量a值为10int*pa;// 定义指针p存储变量a的地址是取地址符这里的p就是变量 a的指针因为p中存放的是a在内存中的地址。通过*p解引用操作可以访问或修改a的值例如*p 20;会将a的值改为20。函数指针函数指针的定义示例#includestdio.h// 普通函数计算两数之和intadd(inta,intb){returnab;}// 普通函数计算两数之积intmultiply(inta,intb){returna*b;}intmain(){// 1. 定义函数指针指向“返回int、接收两个int参数”的函数int(*func_ptr)(int,int);// 2. 赋值指向add函数函数名即函数的入口地址func_ptradd;// 3. 调用两种等价方式intresult1func_ptr(3,4);// 方式1通过指针调用intresult2(*func_ptr)(5,6);// 方式2显式解引用兼容早期C语法printf(add结果%d, %d\n,result1,result2);// 输出7, 11// 4. 切换指向multiply函数func_ptrmultiply;intresult3func_ptr(3,4);printf(multiply结果%d\n,result3);// 输出12return0;}复杂的函数指针可以用 typedef 简化提高可读性// 定义一个函数指针类型 FuncPtr指向“返回int、接收两个int参数”的函数typedefint(*FuncPtr)(int,int);// 使用该类型定义指针变量FuncPtr ptr;ptradd;// 等价于之前的定义printf(%d\n,ptr(2,3));// 输出5数组名与指针uint8_t data[]数组声明含义声明了一个固定大小的数组data 是数组名。内存分配数组内存在声明处分配栈/全局区。大小在编译时确定显式指定或通过初始化推断。关键特性sizeof(data)返回 整个数组的字节大小元素数 × sizeof(uint8_t)data 是常量指针不可重新赋值如 data other; 非法。内存生命周期由作用域决定自动或静态。uint8_tdata[5]{1,2,3,4,5};// sizeof(data) 5 * 1 5uint8_t* data指针声明含义声明了一个指向 uint8_t 的指针。内存分配指针变量本身占用固定大小4/8字节不包含数据内存。数据内存需额外分配动态/静态/栈数组地址。关键特性sizeof(data) 返回 指针本身的大小通常 4 或 8 字节。可重新赋值指向其他内存如 data other_buffer;。可通过 malloc/new 动态管理内存需手动释放。函数形参中的uint8_t data[]和uint8_t* data的含义在函数参数中两者等价数组退化为指针voidfunc1(uint8_tdata[]);// 实际视为 uint8_t* datavoidfunc2(uint8_t*data);// 与 func1 完全相同此时数组语法仅为提示“期望数组”实际传递指针。sizeof(data) 在函数内返回指针大小非数组大小。数组名与指针的关系步进单位指向多维数组的指针int (*p)[4]指针的应用字符串复制函数cprstr (char *pss,char *pds) {while (*pdss*pss);}//与*的优先级一样单目运算符号右结合,*pss相当于*(pss)指针的运算思考有三种类型的指针uint32 *,uint16 *,uint8 *把这三种类型的指针都1它们的值是什么 #includestdio.h#includestdint.hintmain(void){uint32_tnum_uint320;uint32_t*p_uint320;uint16_t*p_uint160;uint8_t*p_uint80;printf(num_uint32 1 %p\n,num_uint321);printf(p_int 1 %p\n,p_uint321);printf(p_int 1 %p\n,p_uint161);printf(p_int 1 %p\n,p_uint81);return0;}结论指针1是 加上 该指针的数据类型的大小的值大小端模式参考文章大小端BOOL IsBigEndian() { int a 0x1234; char b *(char *)a; //通过将int强制类型转换成char单字节通过判断起始存储位置。即等于 取b等于a的低地址部分 if( b 0x12) { return TRUE; } return FALSE; }联合体union的存放顺序是所有成员都从低地址开始存放利用该特性可以轻松地获得了CPU对内存采用Little-endian还是Big-endian模式读写BOOL IsBigEndian() { union NUM { int a; char b; }num; num.a 0x1234; if( num.b 0x12 ) { return TRUE; } return FALSE; }注意C语言中对数字的 位与位或| 运算不需要考虑大小端问题。因为编译器在从内存读取到数据时已经根据其大小端存储方式把数据转换成了对应的数值egshort num 0x1234;char num2 (num 0xff00) 8此时printf(%#x, num2)会打印0x12只是在写内存时需要注意大小端的问题若是大端存储方式则将数值为0x1234的变量写入内存0x20000000后该内存值就是12 34因为数值为0x1234的变量在内存的存储形式是12 34若是小端存储方式则将数值为0x1234的变量写入内存0x20000000后该内存值就是34 12因为数值为0x1234的变量在内存的存储形式是34 12。STM32是小端模式。类型转换1.强转类型uint16_ta0xfeff;uint8_tb(uint8_t)a8;//b 0x00 why????ba8;//b 0xfe why????//第二条语句因为会先执行(uint8_t)a强转成了0xff然后再右移8bit故b0//第三条语句正常逻辑2.运算符在嵌入式中会经常使用到位运算符要知晓其优先级高低关系运算符, , , , !, 的优先级大于 逻辑运算符, ||注意和运算符的优先级这里很容易会写出错误的代码#includestdint.h#includestdio.hintmain(void){uint16_tnum0xf501;if(num0xff000xf500){printf(num 0xff00 0xf500\n);}else{printf(num 0xff00 ! 0xf500\n);}if((num0xff00)0xf500){printf((num 0xff00) 0xf500\n);}else{printf((num 0xff00) ! 0xf500\n);}return0;}3. 函数函数的调用方式常用库函数字符串操作函数 memcpy 注意点在以前的库函数中新版的库函数没这个问题了memcpy在处理特定内存重叠情况时确实会产生错误结果下面是一个典型的出错示例当目标地址在源地址之后且存在内存重叠时memcpy的正向拷贝会导致数据被错误覆盖。考虑将字符串abcd向右移动一位的情况#includestdio.h#includestring.hintmain(){charstr[10]abcd;// 使用memcpy将str向右移动一位memcpy(str1,str,3);printf(结果: %s\n,str);return0;}预期结果应该是aabcd但实际运行得到的结果却是aaaad错误原因分析第一次拷贝str[1] str[0] → 得到a正确第二次拷贝str[2] str[1] → 此时str[1]已经是’a’得到aa错误开始第三次拷贝str[3] str[2] → 此时str[2]也是’a’得到aaa完全错误这是因为memcpy采用从前往后的正向拷贝顺序在拷贝过程中源数据区域已经被修改导致后续拷贝使用了错误的数据正确解决方案使用memmove函数可以正确处理这种情况因为memmove会检测到目标地址在源地址之后自动采用从后往前的拷贝顺序。注意新版的库函数中memcpy已经不会出现这个问题了断言debug函数assert()assert宏的原型定义在assert.h中其作用是如果它的条件返回错误则终止程序执行原型定义#include assert.hvoid assert( int expression );assert的作用是先计算表达式expression如果其值为假即为0那么它先向标准错误流stderr打印一条出错信息然后通过调用abort来终止程序运行否则assert()无任何作用。宏assert一般用于确认程序的正常操作其中表达式构造无错时才为真值。完成调试后不必从源代码中删除assert()语句因为宏NDEBUG有定义时宏assert()的定义为空。注意#define NDEBUG必须在include assert.h前定义。弱函数注意兼容性__attribute__((weak))是GCC/Clang扩展非标准C语法实现如下在函数名称前面添加__attribute__((weak))关键字即表示该函数是弱函数//weak_func.c#includestdio.h// 声明弱函数默认实现__attribute__((weak))voidweak_func(void){printf(Default weak function\n);}voidtest_weak_func(void){weak_func();}//main.c#includestdio.h// void weak_func(void)// {// printf(weak func!!!!!!!!\n);// }intmain(void){externvoidtest_weak_func(void);test_weak_func();//通过注释 或 取消注释 上方的 weak_func() 来测试使用弱函数return0;}注意可以同时存在多个同名的弱函数最终调用哪个是根据链接的先后顺序来决定的4.控制语句for, while, if循环while循环与for循环的等价while(condition){loopbody();}//等价于:for(;condition;){loopbody();}for(initial();condition;operation()){loopbody();}//等价于initial();while(condition){loopbody();operation();}注意虽然while循环和for循环有如上等价关系但是两者的continue是不同的。for循环在执行continue时会隐藏执行operation()如下例子所示inti;//下面循环会打印0 1 2 3 4 6 7 8 9for(i0;i10;i){if(i5)continue;printf(%d ,i);}//下面循环只会打印0 1 2 3 4//并且卡住在循环中i0;while(i10){if(i5)continue;printf(%d ,i);i}所以我们可以使用for来说操作链表typdefstructm_node{intdata;structm_node*next;}m_node_s;intmain(){m_node_s header;m_node_s*pheader.next;//下面的for语句会先判断 p ! NULL如果满足则进入到循环中执行 do something...for(;p!NULL;pp-next){//do something...}return0;}数据的输入获取输入的一行字符串包括空格#includestdio.h#includestdlib.h#includestring.hintmain(){charinput[1024];// 假设输入不会超过1023个字符最后一个字符为\0printf(请输入一行字符串: );if(fgets(input,sizeof(input),stdin)!NULL){// fgets() 会包含换行符如果有的话所以我们可以选择去掉它input[strcspn(input,\n)]0;// 使用strcspn移除换行符printf(你输入的是: %s\n,input);}else{// 读取错误或EOFperror(fgets failed);}return0;}获取输入的一个数字以回车作为结尾#include stdio.h int main() { int num; int array[500] {0}; scanf(%d, num); printf(num:%d\n, num); int i 0; while(i num) { scanf(%d, arrayi); i; } for (i 0; i num; i) { printf(%d , array[i]); } }