
C语言知识点整理一、变量和指针二、sizeof和关键字三、struct四、变量赋值五、通过指针赋值六、上午答疑七、结构体指针_函数指针结构体中如何使用指针函数指针八、下午答疑_关于指针九、什么是链表十、链表的插入操作十一、链表的删除操作十二、ARM架构与汇编的初步体验十三、全局变量的初始化与栈的引入1 有初始值的全局变量如何进行初始化2 无初始值或者初始值为0的全局变量如何进行初始化3 引入栈的概念十四、局部变量的初始化韦东山C语言知识点学习。 B站链接。简单记。一、变量和指针主函数中定义变量但未使用。编译会提示“有变量 定义但未使用”查看map文件会找不到对应的标识符identity。即表明此变量被编译器优化掉。加上关键字volatile易变的则不会被优化掉。main也是一个函数在其中定义的变量是局部变量存在栈stack中。map文件中体现的是“全局变量”。map文件中会体现栈stack的起始位置和长度但是不会体现其中的变量。栈中的变量是随用随销的。CPU相当于内核RAM存变量Flash存程序代码。指针变量也是变量也存在内存中。指针操作的是地址指针占用的空间应该能包含全部的地址范围。即CPU寻址的最大范围。一般32位的CPU寻址范围值2^32 - 1即0x0000 0000 – 0xFFFF FFFF。所以指针变量的长度为4个字节。在物理层面32位的CPU地址线为32根即0-31。32位的CPU不管什么类型的指针变量占用空间的长度都是4个字节。指针变量操作的是地址所以指针变量的长度应该能把所有的地址包含进来。而变量占用的长度与变量类型有关。只读的量为常量不是变量。一些只读的“常量”例如define的宏定义switch中的case值常量都放在FLASH中。有时编译器为了加快读的速度也将常量放在RAM内存中DATA段。后半句话是对的。加const关键字用const定义变量其变量的值在运行过程中不能被修改。应该会放在FALSH中应该验证一下。二、sizeof和关键字类比chara;intb;要获取a b占用的字节直接将变量名输入到sizeofsizeof(a);sizeof(b);那么要获取指针的长度也是将指针变量名输入进入。char*p;//认为char*为指向char的指针类型 后面是该指针类型的变量名pint*p1;//即查找指针变量的长度应该用sizeof(p);sizeof(p1);//如果是sizeof(*p);sizeof(*p1);//含义为查找类型对应的长度 即char 1字节 int 4字节视频中工程为STM32的工程点击编译后没有下载直接点击debug使用view的uart1就可以查看串口printf输出的内容。工程中没有对STM32的配置就是简单的C前面头文件后面main函数。只加一个这个函数。在keil中使用C语言可以查一下。也可以看下老师的工程看看这个函数是什么作用。不清除是什么原理。应该是电脑连着仿真器直接点击debug就可以运行了这个函数可能是将所有的函数集中封装起来了。不下载程序直接点击debug应该也不行吧还没有理解老师视频中的操作。volatile从程序运行的角度i 1;没有必要所以编译器会将此语句给优化掉。但是代码作者想i 1;有特殊用处所以加volatile不让编译器优化i这个变量。另一个角度变量char i 0;肯定在内存RAM中CPU在使用变量的时候会执行读-修改-写先将变量读到CPU内部寄存器再修改这个值然后再写回到RAM内存。如果变量i用作for循环for(i0;i100;i){}不加volatile则CPU读取这个值之后后续执行i修改变量只会修改CPU寄存器中缓存这也是CPU几级缓存的概念 为什么叫缓存的i值并不会去实际修改内存RAM中的值。CPU只来利用缓存中的数据执行速度非常快。如果加volatile则CPU执行代码时会将执行完i;之后修改变量会将修改后的值写回到RAM中。这样执行速度相对降低。CPU缓存一般是SRAM静态内存。而RAM内存一般指DRAM动态内存。静态内存SRAM执行速度比动态内存DRAM要快。实际上DRAM的执行速度也挺快的寄存器也相当于一个变量实际含义应该是内存中一个具体的地址例如0x0000 0002芯片规定为GPIOA高低电平的寄存器。当GPIOA0电平为1时该寄存器的D0为1当GPIOA0电平为0时该寄存器的D0为0即硬件上将GPIO某个引脚的电平状态连接到此地址某一个位置上。如果定义一个变量在0x0000 0002这个地址那么变量的D0-D7分别代表GPIOA0-A7的电平状态。此部分地址一般在单片机的片内RAM中例如32为单片机地址范围是0x0000 0000 — 0xFFFF FFFF比如规定一块地址范围0x0000 0000 — 0x2000 0000作为特殊功能寄存器地址使用会将“变量”指定地址的方式定义在这块范围编程时操作某一个寄存器读或写就是操作这一块内存。其它地址空间供用户自定义变量使用。参考链接8位和32位单片机的区别当读GPIO的寄存器”变量“时如果不使用volitile则可能会实际读取CPU缓存的数据而不会实际读取内存RAM中的数据。会导致读寄存器而不能反应实际GPIO的状态。所以寄存器一般加volatile关键字每次读取GPIO寄存器从实际的RAM内存中读取而不是从CPU缓存寄存器中读取。const此关键字修饰的变量不能被修改可以放在flash中也可以放在ram中。static此关键字修饰的变量/函数作用范围只在c文件内。extern外部函数或变量。从汇编的角度理解比较好。下面图片中的方法一个是告诉编译器此变量在外部的c文件中被定义本c文件可以直接使用extern放在c中。用的多另一个是告诉编译器本c文件中的变量将被外部的c文件使用extern放在本c对应的h文件中外部的c文件需要包含此h文件。需要理解一点汇编和c的编译过程。__weak三、struct结构体是一种类型不是变量不会分配空间只有变量才占空间。sizeof(structperson);//查看结构体类型的大小(长度)不管指针指向的类型是什么指针类型的长度永远为4字节32位系统与系统的位数有关实际为8字节发生了对齐。char被扩充为4字节。不同平台是否对齐好像不一样F28377D用奇地址访问一个字节效率也很高但是用奇数地址访问4字节效率低有些硬件平台不支持具体可以查一下暂时先这么理解问题如果不是结构体定义char也是分配4个字节吗是的如果只定义一个char占用4个字节如果定义两个char则两个char占用4个字节浪费两个。只不过单独定义char变量编译器会将其它char变量放一块看起来像是每个char只分配一个地址。结构体成员一般在内存上是连续的所以可以比较明显的看到char被分配了四个字节。具体可以debug查看可能不同平台不一样。四、变量赋值都是4。a是一个类型。0x00010x0041int a给a赋值1 和赋值 2^32 - 1。都是完整的给4个字节赋值。上面c前面的三个地址没有使用所以这样写代码在此种情况下不会产生什么问题。如果定义两个char变量即c前面的一个地址是另一个char变量这样写代码便会出现问题。好问题如果用*p赋值d会影响age吗如果是四字节操作感觉会影响的。实际开发时发生了内存错误很难找到原因。五、通过指针赋值变量赋值操作和指针操作是同一个含义。都隐含对地址的操作前面的a 123;隐含着将123给到a对应的地址。注意理解指针与变量的“等级”关系。六、上午答疑之前写代码的时候用过*p 1;这种操作与上面图片中的代码一样。只不过常用的是 通过指针给普通类型的变量赋值而上面图片中为通过指针给结构体类型的变量赋值。两者是类似的。*p 1; 星号运算符为取值运算符含义为取指针指向地址中的内容将内容按指针类型解释。头文件中定义一个变量如果该头文件被include两次将会报错变量重复定义。Include头文件相当于将头文件中的内容原封不动的放在include处。为了避免在最终的hex/bin文件中重复包含所以一般加条件编译既能满足c文件编写又满足生成最后的hex/bin文件。int类型占4个字节变量a假设有一个结构体类型占8个字节变量b一个int类型的指针指向a的地址因为是int类型所以将后面4个字节(包含第一个地址)认为成此变量的占用范围。一个8字节的结构体类型指针指向b因为是8个字节所以将后面8个字节的范围认为是该结构体变量的占用范围。所以前面wei2结构体变量赋值的本质就是将这n个字节通过指针全部赋值给了另一个结构体变量wei。指针类型的作用就是对首地址往后的n个字节按指针的类型做出解释。所以串口发送float时可以将float做强制指针类型转换转换为32位的无符号类型然后再按位与提取为4个字节然后再通过串口逐个字节发送。联合体的作用与强制指针类型转换的作用相同本质都是对首地址后面的n个字节做不同的类型解释。不能通过指针将一个数组完全赋值给另一个数组数组为同一类型变量的集合而不是 整个数组为一个数组类型。七、结构体指针_函数指针结构体中如何使用指针结构体变量中包含结构体变量本身不行函数指针所以如果要定义函数指针应该将函数名与*星号括号起来。关键点是理解函数指针的定义形式。推广typedef void (*play_ball)(void);实际为为函数指针起别名可以用play_ball定义函数指针的变量。注意自定义函数指针类型 后面需要加分号给函数指针赋值时直接将函数名赋值给函数指针即可。函数名就是地址所以不用取地址符。用了也没错。变量i为变量i的内容。i为变量的地址。而函数名是否加符效果都是一样的都是地址。、函数指针初始化之后使用时直接 函数指针名形参就当做一个函数用。CPU会在STACK中为形参开辟空间然后跳转到真正的函数地址执行。函数指针通常用作某种模块库封装方便用户实现功能。目前用到比较多的场景1 使用函数指针类型 在函数A的形参中定义形参变量给函数A的函数指针变量传入不同的函数名 实现在函数A中执行不同的动作。即库的用户自定义功能接口2 使用函数指针类型 定义函数指针变量 初始化时给函数指针变量赋值。一般搭建一些库时这样写底层的接口使用函数指针实现适配不同的底层硬件接口。即库的底层接口注函数指针一般用来实现接口。有点晦涩多多体会。当需要通过判断决定执行哪一个函数时改为结构体的形式。相当于将if逐个判断索引改为了查表的形式。常见的查表是通过索引返回一个数值而这个是返回一个函数指针。实现了通过索引调用不同函数的功能。在内存中相当于索引0函数0地址索引1函数1地址索引2函数2地址…本质就是一个表格特殊的表格。使用这种表格的形式省去了if这样的判断语句从而会节约硬件资源代码占用会变小。函数名就是一个地址函数代码所在的入口地址。在程序中调用一个函数就相当于在汇编中添加call跳转到函数的地址执行代码执行完之后再跳转原来的地方。指针变量保存的就是一个地址。函数指针相当于通过指针预留了一个函数此函数需要用户定义。函数指针和函数的使用方法一样本质上是一段函数代码其首地址一个函数名一个是函数指针。函数指针就是以指针的方式使用函数。深刻理解这段话。函数指针的定义方式与定义普通变量的方式有点差别。见的多了习惯就好。注意函数指针是一个指针变量指向了一个函数。准确来说应该是一个函数指针变量。指向函数的指针变量简称函数指针。普通的指针比如指向char类型的指针变量最常用通常只称为指针。再次封装下面这部分不常用再封装一次有点理解不过来了。这个return有点高级返回数组成员的地址使用了取地址符。因为函数的返回值类型为指针所以可以返回地址。数组元素为结构体类型。不常见。八、下午答疑_关于指针问题1纠正该赋值方法不是GCC的扩展而是C99的标准。勾选C99之后编译成功。问题2type def类型定义用来创建类型的给类型起别名。问题3在C语言ESP32案例分析文章中单独写过这个问题。问题4问题5FLASH是只读的通过指针写FLASHCPU可以执行但是写失败。写FLASH之前需要先执行擦除等操作将FLASH恢复到默认状态然后再写FLASH。九、什么是链表重要。最后一个表的next指针为0可以新建一个指针记录当前的链表。十、链表的插入操作链表结构体中有一个指针指向下一个链表称为单向链表。若还有一个指针指向上一个链表称为双向链表。链表的实质就是指针。视频中讲的不是在已有的链表项A-B-C-D中添加新的链表而是新建插入函数通过插入函数创建A-B-C-D这个链表。比较简单。头部head设置为NULL然后先插入A形成链表然后查询链表中的最后一个链表逐次插入B C D。十一、链表的删除操作B的下线好找在B的结构体中就记录了。通过遍历寻找B的上线如果某一项的下线为B那么这一项就是B的上线。视频中以间谍为例所以是上线下线。代码比较简单写起来比较有意思。十二、ARM架构与汇编的初步体验王爽老师的《汇编语言》不错。如何理解例子代码执行时CPU的实际的操作步骤(与汇编有关)RAM架构使用精简指令集所有的计算都在CPU中实现。RAM可读可写FLASH只能读。C代码编译后产生汇编指令汇编指令操作码本质是一些十六进制(或者二进制)数据。下载后程序在FLASH中。当程序执行时CPU在FLASH中取指令当取到int a 1;时CPU在RAM内存中开辟4字节空间存放a的值。当执行a a b时CPU读出RAM内存中a的值放在CPU内部的寄存器或者称为缓存中然后读出b的值计算ab把计算结果再放回到a的RAM地址处。大体是这个过程CPU内部的寄存器R0 – R1532位用于缓存数据所以寄存器也可以称为缓存。CPU内部还有有计算能力的寄存器如ALU算术逻辑单元 执行加减 与或非等操作等。缓存用于暂存数据ALU用计算。十三、全局变量的初始化与栈的引入实际感受一个嵌入式程序运行的本质是怎样的全局变量是如何被设置的。局部变量是在哪里需要看一下C的反汇编代码。1 有初始值的全局变量如何进行初始化断电后程序数据都只在FLASH中。RAM CPU中都掉电消失了。程序运行时才会在RAM中产生变量。1 全局变量的初值来自FLASH。FLASH中存放代码和数据数据就是已初始化的全局变量的初始值。一个定义int a 0x12345678;对应两条汇编指令a 从flash的某个地址读取数据到CPU的某一个寄存器b 将寄存器的值赋值到RAM的某个地址。此时便完成全局变量的定义全局变量存在在RAM中。此方法效率低若有100个全局变量每一条全局变量都对应两条汇编指令。所以编译器实际的操作是将所有的全局变量初始值集中起来作为数据段。烧写程序时bin或着hex等二进制文件会被完整的复制到芯片内部的FLASH中。当程序运行时FLASH中的数据段会被完整的复制到RAM中。所以省去了将全局变量逐个复制到RAM中。数据作为一整段整段复制到RAM中效率更高。如果数据分散在FLASH的不同页上对变量寻址还需要修改页寄存器。数据集中在一起也省去了这个步骤。对这些内容有个笼统的概念即可。看一下汇编文件。__main的代码 应该在代码的安装包了 视频中没有继续找。将目的地址LDR到R0将源地址LDR到R1将长度LDR到R2然后跳转到 memcpy 标号处总结有初始值的全局变量初始值会保存在FLASH中初始化时实际的执行操作类似memcpy将falsh中的有初始值的全局变量段整段复制到RAM中。2 无初始值或者初始值为0的全局变量如何进行初始化这种变量在汇编中全部在ZI段。在ZI段定义。RAM上电时初始值不确定但一般是0在对ZI段初始化时执行类似memset的操作将RAM对应的ZI段全部设置为0.相当于完成对“没有初始值的全局变量的初始化”因为没有初始值或者初始值为0那么只要在初始化时将RAM的一段空间置为0就可以了。与有初始值的全局变量初始化对比理解有初始值的全局变量会在FLASH中保存初始值。无初始值的全局变量初始化时将ram置为0就可以了不需要再FLASH中存初始值。在进入main函数之前会先完成 “有初始值的全局变量初始化” 和 “无初始值的全局变量的初始化”。完成后再进入main函数。3 引入栈的概念局部变量在栈中如何理解CPU R14寄存器是LR寄存器用来保存返回地址。但是如果函数嵌套时有多个返回地址所以一个LR就不够用了所以需要将返回地址保存在栈空间中。也就是RAM的一段空间中。返回地址一定保存在LR中LR中的值再保存在栈空间中。十四、局部变量的初始化深入理解栈理解局部变量在栈空间需要看汇编文件。push {r2 - r4,lr} ;将r2 r3 r4 lr的值 push到栈空间中。因为是32位CPUCPU内部的寄存器为32位所以每push一个寄存器SP栈指针减4。栈空间是FIFO先入先出。栈的位置由SP指针记录。volatile int a 1; //初始化局部变量翻译为汇编对应下面两条汇编指令MOVS R0,#0x01 将0x01赋值给R0寄存器。加井号是将0x01认为是数值(一个立即数)而不是地址。RAM汇编和x86汇编立即数的方表示方式不同此处是RAM架构下的汇编。实际编程中需要查看所用芯片的架构架构不同汇编语法也有所不同STR R0,[ SP , #0x04] 将R0的值STR存储到SP 4的地址处。也就是下图中R3的位置。 [ SP , #0x04]为SP 0X04。加星号为立即数。可以看到有初值的全局变量在DATA段中初始化时将data段整段cpy到RAM内存中。而有初值/无初值的全局变量定义时不会连续cpy到一段内存中而是每一条局部变量定义都对应像上面的两条汇编语句。所以相对于全局变量初始化局部变量初始化的效率会低很多。push {r2 - r4,lr} ;将r2 r3 r4 lr的值 push到栈空间中。后面又STR R0,[ SP , #0x04]导致R3的位置又被占用意思是前面push r3到栈中后面又占用了R3保存在stack中的值没问题这涉及到push {r2 - r4,lr} 是想在栈中占个位置。有个大概的概念即可暂时不深究了。有初值的变量其初值都会保存在代码中也就是会占用一部分FALSH空间。断断续续 看了很久 完结撒花