| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 嵌入式 -> C语言学习 -> 正文阅读 |
|
[嵌入式]C语言学习 |
冯诺依曼结构和哈佛结构 冯诺依曼结构是:数据和代码放在一起。 哈佛结构是:数据和代码分开存在。 什么是代码:函数 什么是数据:全局变量、局部变量 在S5PV210中运行的linux系统上,运行应用程序时:这时候所有的应用程序的代码和数据都在DRAM,所以这种结构就是冯诺依曼结构;在单片机中,我们把程序代码烧写到Flash(NorFlash)中,然后程序在Flash中原地运行,程序中所涉及到的数据(全局变量、局部变量)不能放在Flash中,必须放在RAM(SRAM)中。这种就叫哈佛结构。 单片机并不用把程序加载到RAM中,ARM采用哈弗结构,SRAM取数据,FLASH取指令,两者同时进行. DRAM是动态内存,SRAM是静态内存。
C语言中,函数就是一段代码的封装。函数名的实质就是这一段代码的首地址。所以说函数名的本质也是一个内存地址。 struct s {
}; 使用这样的结构体就可以实现面向对象。 这样包含了函数指针的结构体就类似于面向对象中的class,结构体中的变量类似于class中的成员变量,结构体中的函数指针类似于class中的成员方法。 C语言操作堆内存的接口(malloc free) 堆内存释放时最简单,直接调用free释放即可。 void free(void *ptr); 堆内存申请时,有3个可选择的类似功能的函数:malloc, calloc, realloc void *malloc(size_t size); void *calloc(size_t nmemb, size_t size); // nmemb个单元,每个单元size字节 void *realloc(void *ptr, size_t size); // 改变原来申请的空间的大小的 譬如要申请10个int元素的内存: malloc(40); malloc(10*sizeof(int)); calloc(10, 4); calloc(10, sizeof(int)); 数组定义时必须同时给出数组元素个数(数组大小),而且一旦定义再无法更改。在Java等高级语言中,有一些语法技巧可以更改数组大小,但其实这只是一种障眼法。它的工作原理是:先重新创建一个新的数组大小为要更改后的数组,然后将原数组的所有元素复制进新的数组,然后释放掉原数组,最后返回新的数组给用户; 堆内存申请时必须给定大小,然后一旦申请完成大小不变,如果要变只能通过realloc接口。realloc的实现原理类似于上面说的Java中的可变大小的数组的方式。 位操作时,特定位清零用&,特定位置1用|,特定位取反用^ 位与、位或结合特定二进制数即可完成寄存器位操作需求 (1)如果你要的这个数比较少位为1,大部分位为0,则可以通过连续很多个1左移n位得到。 (2)如果你想要的数是比较少位为0,大部分位为1,则可以通过先构建其位反数,然后再位取反来得到。 (3)如果你想要的数中连续1(连续0)的部分不止1个,那么可以通过多段分别构造,然后再彼此位或即可。这时候因为参与位或运算的各个数为1的位是不重复的,所以这时候的位或其实相当于几个数的叠加。 用宏定义来完成位运算
截取变量的部分连续位。例如:变量0x88, 也就是10001000b,若截取第2~4位,则值为:100b = 4
(1)指针的出现是为了实现间接访问。在汇编中都有间接访问,其实就是CPU的寻址方式中的间接寻址。 (2)间接访问(CPU的间接寻址)是CPU设计时决定的,这个决定了汇编语言必须能够实现间接寻址,又决定了汇编之上的C语言也必须实现简介寻址。 (3)高级语言如Java、C#等没有指针,那他们怎么实现间接访问?答案是语言本身帮我们封装了。 const修饰指针有4种形式:
关于指针变量的理解,主要涉及到2个变量:第一个是指针变量p本身,第二个是p指向的那个变量(*p)。一个const关键字只能修饰一个变量,所以弄清楚这4个表达式的关键就是搞清楚const放在某个位置是修饰谁的 对数组名进行&操作,并不是取其地址,而是得到了指向整个数组的指针。也就是说,arr与&arr指向的是同一个地址,但是他们的类型不一样。arr相当于&arr[0],类型是int ,而&arr是指向整个数组的指针,类型是int ()[5]。 int、char、short等属于整形,他们的存储方式(数转换成二进制往内存中放的方式)是相同的,只是内存格子大小不同(所以这几种整形就彼此叫二进制兼容格式);而float和double的存储方式彼此不同,和整形更不同 sizeof是C语言的一个运算符 sizeof(数组名)实际返回的是整个数组所占用内存空间(以字节为单位的) void fun(int b[100]) {
} (1)函数传参,形参是可以用数组的 (2)函数形参是数组时,实际传递是不是整个数组,而是数组的首元素首地址(?我觉得是传递了数组名,它代表数组地址,只是值刚好与首元素首地址相同)。也就是说函数传参用数组来传,实际相当于传递的是指针(指针指向数组的首元素首地址)。
typedef char *tpChar;
#include <stdio.h> 运行结果 func sizeof(a) = 8, $a + 1 = 0x5487e4b0, $a = 0x5487e4a8, a = 0x5487e4c0 上面说明: 结构体变量作为函数形参的时候,实际上和普通变量(类似于int之类的)传参时表现是一模一样的,也是传了一份值的拷贝。 函数名是一个符号,表示整个函数代码段的首地址,实质是一个指针常量,所以在程序中使用到函数名时都是当地址用的,用来调用这个函数的。 int *p[5]; int (*p)[5]; 核心是p,p是一个指针,指针指向一个数组,数组有5个元素,数组中存的元素是int类型;整个符号的意义就是数组指针。 // 这句重命名了一种类型,这个新类型名字叫pType,类型是:char* (*)(char *, const char ); 使用typedef一次定义2个类型,分别是结构体变量类型,和结构体变量指针类型。 (1)typedef int *PINT; const PINT p2; 相当于是int *const p2; (2)typedef int *PINT; PINT const p2; 相当于是int *const p2; (3)如果确实想得到const int *p;这种效果,只能typedef const int *CPINT; CPINT p1; 二维数组的下标式访问和指针式访问 (1)回顾:一维数组的两种访问方式。以int b[10]为例, int *p = b;。 b[0] 等同于 *(p+0); b[9] 等同于 *(p+9); b[i] 等同于 *(p+i) (2)二维数组的两种访问方式:以int a[2][5]为例,(合适类型的)p = a; a[0][0]等同于*(*(p+0)+0); a[i][j]等同于 ((p+i)+j) 在一个C语言程序中,能够获取的内存就是三种情况:栈(stack)、堆(heap)、数据段(.data) malloc(0) malloc申请0字节内存本身就是一件无厘头事情,一般不会碰到这个需要。 如果真的malloc(0)返回的是NULL还是一个有效指针?答案是:实际分配了16Byte的一段内存并且返回了这段内存的地址。这个答案不是确定的,因为C语言并没有明确规定malloc(0)时的表现,由各malloc函数库的实现者来定义。
gcc中的malloc默认最小是以16B为分配单位的。如果malloc小于16B的大小时都会返回一个16字节的大小的内存。malloc实现时没有实现任意自己的分配而是允许一些大小的块内存的分配。 bss段(又叫ZI(zero initial)段):bss段的特点就是被初始化为0,bss段本质上也是属于数据段,bss段就是被初始化为0的数据段。 注意区分:数据段(.data)和bss段的区别和联系:二者本来没有本质区别,都是用来存放C程序中的全局变量的。区别在于把显示初始化为非零的全局变量存在.data段中,而把显式初始化为0或者并未显式初始化(C语言规定未显式初始化的全局变量值默认为0)的全局变量存在bss段。 有些特殊数据会被放到代码段 (1)C语言中使用char *p = “linux”;定义字符串时,字符串"linux"实际被分配在代码段,也就是说这个"linux"字符串实际上是一个常量字符串而不是变量字符串。 (2)const型常量:C语言中const关键字用来定义常量,常量就是不能被改变的量。const的实现方法至少有2种:第一种就是编译将const修饰的变量放在代码段去以实现不能修改(普遍见于各种单片机的编译器);第二种就是由编译器来检查以确保const型的常量不会被修改,实际上const型的常量还是和普通变量一样放在数据段的(gcc中就是这样实现的)。 显式初始化为非零的全局变量和静态局部变量放在数据段 (1)放在.data段的变量有2种:第一种是显式初始化为非零的全局变量。第二种是静态局部变量,也就是static修饰的局部变量。(普通局部变量分配在栈上,静态局部变量分配在.data段) 未初始化或显式初始化为0的全局变量放在bss段 (1)bss段和.data段并没有本质区别,几乎可以不用明确去区分这两种。 字符数组与字符串的本质差异(内存分配角度) (1)字符数组char a[] = “linux”;来说,定义了一个数组a,数组a占6字节,右值"linux"本身只存在于编译器中,编译器将它用来初始化字符数组a后丢弃掉(也就是说内存中是没有"linux"这个字符串的);这句就相当于是:char a[] = {‘l’, ‘i’, ‘n’, ‘u’, ‘x’, ‘\0’}; (2)字符串char *p = “linux”;定义了一个字符指针p,p占4字节,分配在栈上;同时还定义了一个字符串"linux",分配在代码段;然后把代码段中的字符串(一共占6字节)的首地址(也就是’l’的地址)赋值给p。 总结对比:字符数组和字符串有本质差别。字符数组本身是数组,数组自身自带内存空间,可以用来存东西(所以数组类似于容器);而字符串本身是指针,本身永远只占4字节,而且这4个字节还不能用来存有效数据,所以只能把有效数据存到别的地方,然后把地址存在p中。 也就是说字符数组自己存那些字符;字符串一定需要额外的内存来存那些字符,字符串本身只存真正的那些字符所在的内存空间的首地址。 gcc支持但不推荐的对齐指令:#pragma pack() #pragma pack(n) (n=1/2/4/8) (1)#pragma是用来指挥编译器,或者说设置编译器的对齐方式的。编译器的默认对齐方式是4,但是有时候我不希望对齐方式是4,而希望是别的(譬如希望1字节对齐,也可能希望是8,甚至可能希望128字节对齐)。 (2)常用的设置编译器编译器对齐命令有2种:第一种是#pragma pack(),这种就是设置编译器1字节对齐(有些人喜欢讲:设置编译器不对齐访问,还有些讲:取消编译器对齐访问);第二种是#pragma pack(4),这个括号中的数字就表示我们希望多少字节对齐。 (3)我们需要#prgama pack(n)开头,以#pragma pack()结尾,定义一个区间,这个区间内的对齐参数就是n。 (4)#prgma pack的方式在很多C环境下都是支持的,但是gcc虽然也可以不过不建议使用。 gcc推荐的对齐指令__attribute__((packed)) attribute((aligned(n))) (1)attribute((packed))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。packed的作用就是取消对齐访问。 (2)attribute((aligned(n)))使用时直接放在要进行内存对齐的类型定义的后面,然后它起作用的范围只有加了这个东西的这一个类型。它的作用是让整个结构体变量整体进行n字节对齐(注意是结构体变量整体n字节对齐,而不是结构体内各元素也要n字节对齐) // TYPE是结构体类型,MEMBER是结构体中一个元素的元素名 (1)offsetof宏的作用是:用宏来计算结构体中某个元素和结构体首地址的偏移量(其实质是通过编译器来帮我们计算)。 (2)offsetof宏的原理:我们虚拟一个type类型结构体变量,然后用type.member的方式来访问那个member元素,继而得到member相对于整个变量首地址的偏移量。 // ptr是指向结构体元素member的指针,type是结构体类型,member是结构体中一个元素的元素名 (1)作用:知道一个结构体中某个元素的指针,反推这个结构体变量的指针。有了container_of宏,我们可以从一个元素的指针得到整个结构体变量的指针,继而得到结构体中其他元素的指针。 (2)typeof关键字的作用是:typeof(a)时由变量a得到a的类型,typeof就是由变量名得到变量数据类型的。 (3)这个宏的工作原理:先用typeof得到member元素的类型定义成一个指针,然后用这个指针减去该元素相对于整个结构体变量的偏移量(偏移量用offsetof宏得到的),减去之后得到的就是整个结构体变量的首地址了,再把这个地址强制类型转换为type *即可。 #include <stdio.h>
}
} 看似可行实则不行的测试大小端方式:位与、移位、强制类型转化 (1)位与运算。 结论:位与的方式无法测试机器的大小端模式。(表现就是大端机器和小端机器的&运算后的值相同的) 理论分析:位与运算是编译器提供的运算,这个运算是高于内存层次的(或者说&运算在二进制层次具有可移植性,也就是说&的时候一定是高字节&高字节,低字节&低字节,和二进制存储无关)。 (2)移位 结论:移位的方式也不能测试机器大小端。 理论分析:原因和&运算符不能测试一样,因为C语言对运算符的级别是高于二进制层次的。右移运算永远是将低字节移除,而和二进制存储时这个低字节在高位还是低位无关的。 (3)强制类型转换 同上 源码.c->(预处理)->预处理过的.i源文件->(编译)->汇编文件.S->(汇编)->目标文件.o->(链接)->elf可执行程序 编程中常见的预处理 (1)#include(#include <>和#include ""的区别) (2)注释 (3)#if #elif #endif #ifdef (4)宏定义 gcc中只预处理不编译的方法 (1)gcc编译时可以给一些参数来做一些设置,譬如gcc xx.c -o xx可以指定可执行程序的名称;譬如gcc xx.c -c -o xx.o可以指定只编译不连接,也可以生成.o的目标文件。 (2)gcc -E xx.c -o xx.i可以实现只预处理不编译。一般情况下没必要只预处理不编译,但有时候这种技巧可以用来帮助我们研究预处理过程,帮助debug程序。 总结:宏定义被预处理时的现象有:第一,宏定义语句本身不见了(可见编译器根本就不认识#define,编译器根本不知道还有个宏定义);第二,typedef重命名语言还在,说明它和宏定义是有本质区别的(说明typedef是由编译器来处理而不是预处理器处理的); 宏定义示例1:MAX宏,求2个数中较大的一个 #define MAX(a, b) (((a)>(b)) ? (a) : (b)) 关键:
宏定义示例2:SEC_PER_YEAR,用宏定义表示一年中有多少秒 #define SEC_PER_YEAR (3652460*60UL) 关键:
宏定义和函数的差别: (1)内联函数通过在函数定义前加inline关键字实现。 (2)内联函数本质上是函数,所以有函数的优点(内联函数是编译器负责处理的,编译器可以帮我们做参数的静态类型检查);但是他同时也有带参宏的优点(不用调用开销,而是原地展开)。所以几乎可以这样认为:内联函数就是带了参数静态类型检查的宏。 (3)当我们的函数内函数体很短(譬如只有一两句话)的时候,我们又希望利用编译器的参数类型检查来排错,我还希望没有调用开销时,最适合使用内联函数。 实际上递归函数是在栈内存上递归执行的,每次递归执行一次就需要耗费一些栈内存。 栈内存的大小是限制递归深度的重要因素。 自己制作静态链接库并使用 (1)第一步:自己制作静态链接库
注意:制作出来了静态库之后,发布时需要发布.a文件和.h文件。 (2)第二步:使用静态链接库
all: 库函数的使用需要注意3点:第一,包含相应的头文件;第二,调用库函数时注意函数原型;第三,有些库函数链接时需要额外用-lxxx来指定链接;第四,如果是动态库,要注意-L指定动态库的地址。 除了ar命令外,还有个nm命令也很有用,它可以用来查看一个.a文件中都有哪些符号 自己制作动态链接库并使用 (1)动态链接库的后缀名是.so(对应windows系统中的dll),静态库的扩展名是.a (2)第一步:创建一个动态链接库。
注意:做库的人给用库的人发布库时,发布libxxx.so和xxx.h即可。 但是运行出错,报错信息: error while loading shared libraries: libaston.so: cannot open shared object file: No such file or directory 错误原因:动态链接库运行时需要被加载(运行时环境在执行test程序的时候发现他动态链接了libaston.so,于是乎会去固定目录尝试加载libaston.so,如果加载失败则会打印以上错误信息。) 从这里能看出静态库和动态库的区别 解决方法一: 将libaston.so放到固定目录下就可以了,这个固定目录一般是/usr/lib目录。 cp libaston.so /usr/lib即可 解决方法二:使用环境变量LD_LIBRARY_PATH。操作系统在加载固定目录/usr/lib之前,会先去LD_LIBRARY_PATH这个环境变量所指定的目录下去寻找,如果找到就不用去/usr/lib下面找了,如果没找到再去/usr/lib下面找。所以解决方案就是将libaston.so所在的目录导出到环境变量LD_LIBRARY_PATH中即可。 export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/mnt/hgfs/Winshare/s5pv210/AdvancedC/4.6.PreprocessFunction/4.6.12.sharedobject.c/sotest 在ubuntu中还有个解决方案三,用ldconfig (4)ldd命令:作用是可以在一个使用了共享库的程序执行之前解析出这个程序使用了哪些共享库,并且查看这些共享库是否能被找到,能被解析(决定这个程序是否能正确执行)。 register修饰的变量。编译器会尽量将它分配在寄存器中。(平时分配的一般的变量都是在内存中的)。分配在寄存器中一样的用,但是读写效率会高很多。所以register修饰的变量用在那种变量被反复高频率的使用,通过改善这个变量的访问效率可以极大的提升程序运行效率时。所以register是一种极致提升程序运行效率的手段。 中断isr中引用的变量,多线程中共用的变量,硬件会更改的变量,都是编译器在编译时无法预知的更改,此时应用使用volatile告诉编译器这个变量属于这种(可变的、易变的)情况。编译器在遇到volatile修饰的变量时就不会对改变量的访问进行优化,就不会出现错误。 关键字restrict只用于限定指针;该关键字用于告知编译器,所有修改该指针所指向内容的操作全部都是基于(base on)该指针的,即不存在其它进行修改操作的途径;这样的后果是帮助编译器进行更好的代码优化,生成更有效率的汇编代码 (1)普通(自动)局部变量分配在栈上,作用域为代码块作用域,生命周期是临时,连接属性为无连接。定义时如果未显式初始化则其值随机,变量地址由运行时在栈上分配得到,多次执行时地址不一定相同,函数不能返回该类变量的地址(指针)作为返回值。 (2)静态局部变量分配在数据段/bss段(显式初始化为非0则在数据段,显式初始化为0或未显示初始化则在bss段),作用域为代码块作用域(人为规定的),生命周期为永久(天然的),链接属性为无连接(天然的)。定义时如果未显式初始化则其值为0(天然的),变量地址由运行时环境在加载程序时确定,整个程序运行过程中唯一不变;静态局部变量其实就是作用域为代码块作用域(同时链接属性为无连接)的全局变量。静态局部变量可以改为用全局变量实现(程序中尽量避免用全局变量,因为会破坏结构性)。 (3)静态全局变量/静态函数和普通全局变量/普通函数的唯一差别是:static使全局变量/函数的链接属性由外部链接(整个程序所有文件范围)转为内部链接(当前c文件内)。这是为了解决全局变量/函数的重名问题(C语言没有命名空间namespace的概念,因此在程序中文件变多之后全局变量/函数的重名问题非常严重,将不必要被其他文件引用的全局变量/函数声明为static可以很大程度上改善重名问题,但是仍未彻底解决)。 (4)写程序尽量避免使用全局变量,尤其是非static类型的全局变量。能确定不会被其他文件引用的全局变量一定要static修饰。 (5)注意区分全局变量的定义和声明。一般规律如下:如果定义的同时有初始化则一定会被认为是定义;如果只是定义而没有初始化则有可能被编译器认为是定义,也可能被认为是声明,要具体分析;如果使用extern则肯定会被认为是声明(实际上使用extern也可以有定义,实际上加extern就是明确声明这个变量为外部链接属性)。 (6)全局变量应该定义在c文件中并且在头文件中声明,而不要定义在头文件中(因为如果定义在头文件中,则该头文件被多个c文件包含时该全局变量会重复定义)。 (7)在b.c中引用a.c中定义的全局变量/函数有2种方法:一是在a.h中声明该函数/全局变量,然后在b.c中#include <a.h>;二是在b.c中使用extern显式声明要引用的函数/全局变量。其中第一种方法比较正式。 (8)存储类决定生命周期,作用域决定链接属性 (9)宏和inline函数的链接属性为无连接。 C++的编译环境中,编译器预先定义了一个宏_cplusplus,程序中可以用条件编译来判断当前的编译环境是C++的还是C的。 链表还有另外一种用法,就是把头指针指向的第一个节点作为头节点使用。头节点的特点是:第一,它紧跟在头指针后面。第二,头节点的数据部分是空的(有时候不是空的,而是存储整个链表的节点数),指针部分指向下一个节点,也就是第一个节点。 linux内核链表中实现了一个纯链表(纯链表就是没有数据区域,只有前后向指针)的封装,以及纯链表的各种操作函数(节点创建、插入、删除、遍历······)。这个纯链表本身自己没有任何用处,它的用法是给我们具体链表作为核心来调用。 list.h文件简介 (1)内核中核心纯链表的实现在include/linux/list.h文件中 (2)list.h中就是一个纯链表的完整封装,包含节点定义和各种链表操作方法。 |
|
嵌入式 最新文章 |
基于高精度单片机开发红外测温仪方案 |
89C51单片机与DAC0832 |
基于51单片机宠物自动投料喂食器控制系统仿 |
《痞子衡嵌入式半月刊》 第 68 期 |
多思计组实验实验七 简单模型机实验 |
CSC7720 |
启明智显分享| ESP32学习笔记参考--PWM(脉冲 |
STM32初探 |
STM32 总结 |
【STM32】CubeMX例程四---定时器中断(附工 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/26 2:48:06- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |