栏目分类
热点资讯
你的位置:欧冠体育游戏手机登录注册 > 典型案例 > 嵌入式 C 言语中三块难啃的硬骨头

典型案例

嵌入式 C 言语中三块难啃的硬骨头

发布日期:2022-08-07 03:56    点击次数:90

 C言语在嵌入式深造中是必备的知识,查核大部份操作都要萦绕C言语举行,而个中有三块“难啃的硬骨头”几近是公认级另外。

01. 指针

指针公认最难理解的见解,也是让良多初学者抉择销毁的间接启事

指针之所以难理解,因为指针本身就是一个变量,是一个极度不凡的变量,专门寄放地点的变量,这个地点须要给请求空间材干装货物,并且由是以个变量可以或许中央赋值,这么一捣腾良多人就起头犯晕了,绕不开弯了。C言语之所以被良多好手所爱好,就是指针的魅力,中央可以或许灵巧的切换,执行效劳超高,这点也是让小白晕菜之处。

指针是深造绕不夙昔的知识点,并且学完C言语,下一步紧接着切换到数据布局和算法,指针是切换的重点,指针搞不定下一步举行起来就很难,会让良多人销毁延续深造的勇气。

指针间接对接内存布局,罕见的C言语内里的指针乱指,数组越界基本启事就是内存成就。在指针这个点有没无限无尽的发挥空间。良多编程的手艺都在此集结。

指针还奔忙及怎么请求释放内存,假定释放不实时就会出现内存泄露的环境,指针是高效好用,但不完整搞显明关于有些人来说几近就是噩梦。

在见解方面成便可以或许拜会此前推文《关于C言语指针最详确的解说》,那末在指针方面可以或许拜会一下大神的经历:

▎宏壮范例分化

要相识指针,多几几何会出现一些相比宏壮的范例。所以先介绍一下怎么齐全理解一个宏壮范例。

要理解宏壮范例着实很俭朴,一个范例里会出现良多运算符,他们也像通俗的剖明式同样,有优先级,其优先级和运算优先级同样。

所以笔者总结了一下其原则:从变量名处起,痛处运算符优先级联合,一步一步阐发。

上面让我们先从俭朴的范例起头逐步阐发吧。

 int p;

这是一个通俗的整型变量

 int p;

首先从P处起头,先与联合,所以分化P是一个指针。尔后再与int联合,分化指针所指向的内容的范例为int型,所以P是一个前去整型数据的指针

 int p[3];

首先从P处起头,先与[]联合,分化P是一个数组。尔后与int联合,分化数组里的元素是整型的,所以P是一个由整型数据形成的数组。

 int *p[3];

首先从P处起头,先与[]联合,因为其优先级比高,所以P是一个数组。尔后再与联合,分化数组里的元素是指针范例。当前再与int联合,分化指针所指向的内容的范例是整型的,所以P是一个由前去整型数据的指针所形成的数组。

 int (*p)[3];

首先从P处起头,先与联合,分化P是一个指针。尔后再与[]联合(与"()"这步可以或许轻忽,只是为了改变优先级),分化指针所指向的内容是一个数组。当前再与int联合,分化数组里的元素是整型的。所以P是一个指向由整型数据形成3个整数的指针。

 int **p;

首先从P起头,先与*联合,分化P是一个指针。尔后再与*联合,分化指针所指向的元素是指针。当前再与int联合,分化该指针所指向的元素是整型数据。因为二级指针以及更低档的指针极少用在宏壮的范例中,所今后面更宏壮的范例我们就不推敲多级指针了,至多只推敲一级指针。

 int p(int);

从P处起,先与()联合,分化P是一个函数。尔落后入()里阐发,分化该函数有一个整型变量的参数,当前再与外表的int联合,分化函数的前去值是一个整型数据。

 Int (*p)(int);

从P处起头,先与指针联合,分化P是一个指针。尔后与()联合,分化指针指向的是一个函数。当前再与()里的int联合,分化函数有一个int型的参数,再与最外层的int联合,分化函数的前去范例是整型,所以P是一个指向有一个整型参数且前去范例为整型的函数的指针。

  int (p(int))[3];

可以或许先跳过,不看这个范例,过于宏壮。从P起头,先与()联合,分化P是一个函数。尔落后入()内里,与int联合,分化函数有一个整型变量参数。尔后再与外表的联合,分化函数前去的是一个指针。当前到最外表一层,先与[]联合,分化前去的指针指向的是一个数组。接着再与联合,分化数组里的元素是指针,最后再与int联合,分化指针指向的内容是整型数据。所以P是一个参数为一个整数据且前去一个指向由整型指针变量形成的数组的指针变量的函数。

说到这里也就差不多了。懂患有这几个范例,另外的范例对我们来说也是小菜了。不过普通不会用太宏壮的范例,那样会大大减小顺序的可读性,请慎用。这上面的几种范例已经足够我们用了。

▎细说指针

指针是一个不凡的变量,它内里存储的数值被说明成为内存里的一个地点。

要搞清一个指针须要搞清指针的四方面的内容:指针的范例、指针所指向的范例、指针的值或许叫指针所指向的内存区、指针本身所盘踞的内存区。让我们划分分化。

先声名几个指针放着做例子:

(1)int*ptr;

(2)char*ptr;

(3)int**ptr;

(4)int(*ptr)[3];

(5)int*(*ptr)[4];

▎指针的范例

从语法的角度看,小搭档们只有把指针声名语句里的指针名字去掉,剩下的部份就是这个指针的范例。这是指针本身所具有的范例。

让我们看看上述例子中各个指针的范例:

(1)intptr;//指针的范例是int

(2)charptr;//指针的范例是char

(3)intptr;//指针的范例是int

(4)int(ptr)[3];//指针的范例是int()[3]

(5)int*(ptr)[4];//指针的范例是int(*)[4]

怎样?找出指针的范例的编制是不是很俭朴?

▎指针所指向的范例

当经由过程指针来拜访指针所指向的内存区时,指针所指向的范例抉择了编译器将把那片内存区里的内容当成什么来看待。

从语法上看,小搭档们只有把指针声名语句中的指针名字和名字右边的指针声名符*去掉,剩下的就是指针所指向的范例。

上述例子中各个指针所指向的范例:

(1)intptr; //指针所指向的范例是int

(2)char*ptr; //指针所指向的的范例是char*

(3)int*ptr; //指针所指向的的范例是int*

(4)int(*ptr)[3]; //指针所指向的的范例是int(*)[3]

(5)int*(*ptr)[4]; //指针所指向的的范例是int*(*)[4]

在指针的算术运算中,指针所指向的范例有很大的浸染。

指针的范例(即指针本身的范例)和指针所指向的范例是两个见解。当小搭档们对C 越来越意识时,就会缔造,把与指针搅和在一起的"范例"这个见解分成"指针的范例"和"指针所指向的范例"两个见解,是醒目指针的关键点之一。

笔者看了良多书,缔造有些写得差的书中,就把指针的这两个见解搅在一起了,所以看起书来先后抵牾,越看越懵懂。

▎指针的值

即指针所指向的内存区或地点。

指针的值是指针本身存储的数值,这个值将被编译器看成一个地点,而不是一个普通的数值。

在32位顺序里,全体范例的指针的值都是一个32位整数,因为32位顺序里内存地点全都是32位长。指针所指向的内存区就是从指针的值所代表的那个内存地点起头,长度为si zeof(指针所指向的范例)的一片内存区。

当前,我们说一个指针的值是XX,就相当于说该指针指向了以XX为首地点的一片内存地区;我们说一个指针指向了某块内存地区,就相当于说该指针的值是这块内存地区的首地点。

指针所指向的内存区和指针所指向的范例是两个齐全差别的见解。在例一中,指针所指向的范例已经有了,但因为指针还未初始化,所以它所指向的内存区是不存在的,或许说是无意义的。

当前,每遇到一个指针,都该当问问:这个指针的范例是什么?指针指的范例是什么?该指针指向了何处?

▎指针本身所盘踞的内存区

指针本身占了多大的内存?只有用函数sizeof(指针的范例)测一下就晓得了。在32位平台里,指针本身盘踞4个字节的长度。指针本身盘踞的内存这个见解在鉴定一个指针剖明式是不是左值时颇有效。

02. 函数见解

面向进程工具模块的根抵单元,以及对应种种组合,函数指针,指针函数

一个函数就是一个业务逻辑块,是面向进程,单元模块的最小单元,并且在函数的执进步程中,形参,实参怎么交换数据,怎么将数据通报进来,怎么策画一个公正的函数,岂但单是经管一个功用,还要看是不是兴许复用,防止重复造轮子。

函数指针和指针函数,详情是两个字面意思的更调事实上含义大相径庭,指针函数相比好理解,就是前去指针的一个函数,函数指针这个首要用在回调函数,良多人感应函数都没还搞显明,回调函数更晕菜了。着实可以或许艰深的理解指向函数的指针,本身是一个指针变量,只不过在初始化的时光指向了函数,这又回到了指针层面。没搞显明指针再次深入的向前走特殊难。

C言语的开发者们为其后的开发者做了一些省力量的工作,他们编写了大量代码,将罕见的根抵功用都实现了,可以或许让别人间接拿来应用。然则那末多代码,怎么从中找到本身须要的呢?将全体代码都拿来显明是不太事实。

然则这些代码,早已被晚期的开发者们分门别类地放在了差别的文件中,并且每一段代码都有仅有的名字。所以着实深造C言语并无那末难,尤为是可以或许在着手锻炼做名目及第行。应用代码时,只有在对应的名字后面加之( )就能。这样的一段代码就是函数,函数兴许独顿时实现某个功用,一次编写实现后可以或许屡次应用。

良多初学者兴许都市把C言语中的函数和数学中的函数见解搞混合。着实原形并无那末宏壮,C言语中的函数是有纪律可循迹的,只有搞清楚了见解你会缔造还挺有意思的。

函数的英文名称是 Function,对应翻译已往的中文另有“功用”的意思。C言语中的函数也跟功用有着亲昵的纠葛。

我们来看一小段C言语代码: 

#include<stdio.h>  int main()  {  puts("Hello World");  return 0;  } 

把眼光放在第4行代码上,这行代码会在体现器上输出“Hello World”。后面我们已经讲过,puts 后面要带( ),字符串也要放在( )中。

在C言语中,有的语句应历时不克不及带括号,有的语句必须带括号。带括号的就是函数(Function)。

C言语供应了良多功用,我们只有要一句俭朴的代码就兴许应用。然则这些功用的底层都相比宏壮,平日是软件和硬件的联合,还要要推敲良多细节和界限,假定将这些功用都交给顺序员去实现,那将极大添加顺序员的深造成本,升高编程效劳。

有了函数当前,C言语的编程效劳就彷佛有了神器同样,开发者们只有要随时调用就能了,像过程函数、操作函数、时光日期函数等均可以或许协助我们间接实现C言语本身的功用。

C言语函数是可以或许重复应用的。

函数的一个显明个性就是应历时必须带括号( ),须要的话,括号中还可以或许包孕待处理惩罚的数据。譬如puts("尚观科技")就应用了一段具有输出功用的代码,这段代码的名字是 puts,"尚观科技" 是要交给这段代码处理惩罚的数据。应用函数在编程中有业余的称说,叫做函数调用(Function Call)。

假定函数须要处理惩罚多个数据,那末它们之间应用逗号,分开,譬如:

pow(10, 2);

该函数用来求10的2次方。

好了,看到这里你有没有感应着实C言语函数照旧相比有意思的,并且并无那末宏壮费力。当前再遇到菜鸟小白的时光,典型案例你一口一个C言语的函数,说不定就能当场引来无数跪拜的眼光。

03. 布局体,递归

良多在大学深造C言语的,良多课程都没学完,布局体都没学到,因为从章节的安插来看彷佛,布局体深造放在课本的后半部份了,弄得良多门生感应布局体不首要,假定只是应付学校的查验,或许就是为了混个结业证,切实学的意思不大。

假定想从事编程这个行业,对这个见解还不相识,根抵上没法布局数据模型,没有一个业务体是齐全应用原生数据范例来实现的,良多好手在策画数据模型的时光,普通先把头文件中的布局体数据摒挡进去。尔后策画好功用函数的参数,以及名字,尔后才真正起头写c源码。

假定从减省空间推敲布局体内里的数据放的按次不一样在内存中占用的空间也不一样,布局体与布局体之间赋值,布局体存在指针那末赋值要特殊留心,须要举行深度的赋值。

递归普通用于重新到位统计或许枚举一些数据,在应用的时光良多初学者都感应别扭,怎么还能本身调用本身?并且在应用的时光,必定配置好跳出的条件,不然无截止的举行下去,真就成无线死循环了。

关于布局体方面的知识,可以或许拜会此前推送的文章《C言语布局体(struct)最全的解说(万字干货)》。详细也可以拜会大佬的经历:

信赖巨匠关于布局体都不目生。在此,分享出自己对C言语布局体的研究和深造的总结。假定你缔造这个总结中有你从前所未独霸的,那本文也算是有点价钱了。固然,水平无限,若缔造无余之处恳请指出。代码文件test.c我放在上面。

在此,我会萦绕下列2个成就来阐发和应用C言语布局体:

1. C言语中的布局体有何浸染

2. 布局体成员变量内存对齐有何讲求(重点)

关于一些见解的分化,我就不把C言语课本上的定义搬下去。我们坐上去逐步聊吧。

1. 布局体有何浸染

三个月前,教研室里一个学长在华为南京研究院的面试中就遇到这个成就。固然,这只是面试中最根抵的成就。假定问你你怎么回覆?

我的理解是这样的,C言语中布局体起码有下列三个浸染:

(1) 无机地构造了工具的属性。

比喻,在STM32的RTC开发中,我们须要数据来默示日期和时光,这些数据平日是年、月、日、时、分、秒。假定我们不消布局体,那末就须要定义6个变量来默示。这样的话顺序的数据布局是疏松的,我们的数据布局最佳是“高内聚,低耦合”的。所以,用一个布局体来默示更好,不管是从顺序的可读性照旧可移植性照旧可回护性皆是:

typedef struct //公历日期和时光布局体 

{  vu16 year;  vu8 month;  vu8 date;  vu8 hour;  vu8 min;  vu8 sec;  }_calendar_obj;  _calendar_obj calendar; //定义布局体变量 

(2) 以编削布局体成员变量的编制接替了函数(入口参数)的重新定义。

假定说布局体无机地构造了工具的属性默示布局体“中看”,那末以编削布局体成员变量的编制接替函数(入口参数)的重新定义就默示告终构体“中用”。延续以上面的布局体为例子,我们来阐发。假若今朝我有以下函数来体现日期和时光: 

void DsipDateTime( _calendar_obj DateTimeVal) 

那末我们只有将一个_calendar_obj这个布局体范例的变量作为实参调用DsipDateTime()即可,DsipDateTime()经由过程DateTimeVal的成变量来实现内容的体现。假定不消布局体,我们很兴许须要写这样的一个函数: 

void DsipDateTime( vu16 year,vu8 month,vu8 date,vu8 hour,vu8 min,vu8 sec) 

显明这样的形参很不成观,数据布局打点起来也很繁缛。假定某个函数的前去值得是一个默示日期和时光的数据,那就更宏壮了。这只是一方面。

另外一方面,假定用户须要默示日期和时光的数据中还要包孕星期(周),这个时光,假定从前没有效机构体,那末该当在DsipDateTime()函数中在添加一个形参vu8 week: 

void DsipDateTime( vu16 year,vu8 month,vu8 date,vu8 week,vu8 hour,vu8 min,vu8 sec) 

可见这类编制来通报参数极度繁缛。所以以布局体作为函数的入口参数的益处之一就是函数的声名void DsipDateTime( _calendar_obj DateTimeVal)不须要改变,只有要添加布局体的成员变量,尔后在函数的外部实现上对calendar.week作响应的处理惩罚即可。这样,在顺序的编削、回护方面浸染较着。 

typedef struct //公历日期和时光布局体  {  vu16 year;  vu8 month;  vu8 date;  vu8 week;  vu8 hour;  vu8 min;  vu8 sec;  }_calendar_obj;  _calendar_obj calendar; //定义布局体变量 

(3) 布局体的内存对齐原则可以或许进步CPU对内存的拜访速度(以空间替换时光)。

并且,布局体成员变量的地点可以或许痛处基地点(以偏移量offset)计算。我们先来看看上面的一段俭朴的顺序,关于此顺序的阐发会在第2部份布局体成员变量内存对齐中详细分化。 

#include<stdio.h>  int main()  {      struct    //声名布局体char_short_long      {          char  c;          short s;          long  l;      }char_short_long;     struct    //声名布局体long_short_char      {          long  l;          short s;          char  c;      }long_short_char;      struct    //声名布局体char_long_short      {         char  c;          long  l;          short s;      }char_long_short;  printf(" \n");  printf(" Size of char   = %d bytes\n",sizeof(char));  printf(" Size of shrot  = %d bytes\n",sizeof(short));  printf(" Size of long   = %d bytes\n",sizeof(long));  printf(" \n");  //char_short_long  printf(" Size of char_short_long       = %d bytes\n",sizeof(char_short_long));  printf("     Addr of char_short_long.c = 0x%p (10进制:%d)\n",&char_short_long.c,&char_short_long.c);  printf("     Addr of char_short_long.s = 0x%p (10进制:%d)\n",&char_short_long.s,&char_short_long.s);  printf("     Addr of char_short_long.l = 0x%p (10进制:%d)\n",&char_short_long.l,&char_short_long.l);  printf(" \n");  printf(" \n");  //long_short_char  printf(" Size of long_short_char       = %d bytes\n",sizeof(long_short_char));  printf("     Addr of long_short_char.l = 0x%p (10进制:%d)\n",&long_short_char.l,&long_short_char.l);  printf("     Addr of long_short_char.s = 0x%p (10进制:%d)\n",&long_short_char.s,&long_short_char.s);  printf("     Addr of long_short_char.c = 0x%p (10进制:%d)\n",&long_short_char.c,&long_short_char.c);  printf(" \n");  printf(" \n");  //char_long_short  printf(" Size of char_long_short       = %d bytes\n",sizeof(char_long_short));  printf("     Addr of char_long_short.c = 0x%p (10进制:%d)\n",&char_long_short.c,&char_long_short.c);  printf("     Addr of char_long_short.l = 0x%p (10进制:%d)\n",&char_long_short.l,&char_long_short.l);  printf("     Addr of char_long_short.s = 0x%p (10进制:%d)\n",&char_long_short.s,&char_long_short.s);  printf(" \n");  return 0;  } 

顺序的运行终局以下(留心:括号内的数据是成员变量的地点的十进制模式):

2. 布局体成员变量内存对齐

首先,我们来阐发一下上面顺序的运行终局。前三行分化在我的顺序中,char型占1个字节,short型占2个字节,long型占4个字节。char_short_long、long_short_char和char_long_short是三个布局体成员沟通然则成员变量的陈设按次差别。并且从顺序的运行终局来看,  

Size of char_short_long = 8 bytes  Size of long_short_char = 8 bytes  Size of char_long_short = 12 bytes //比前两种环境大4 byte !  

并且,还要留心到,1 byte (char)+ 2 byte (short)+ 4 byte (long) = 7 byte,而不是8 byte。

所以,布局体成员变量的搁置按次影响着布局体所占的内存空间的大小。一个布局体变量所占内存的大小不必定等于其成员变量所占空间之和。假定一个用户顺序或许操作体系(比喻uC/OS-II)中存在大量布局体变量时,这类内存占用必须要举行优化,也就是说,布局体外部成员变量的陈设序次是有讲求的。

布局体成员变量究竟是怎么寄放的呢?

在这里,我就不卖关子了,间接给出以下结论,在没有#pragma pack宏的环境下:

原则1 布局(struct或联合union)的数据成员,第一个数据成员放在offset为0之处,当前每个数据成员存储的肇端职位地方要从该成员大小的整数倍起头(比喻int在32位机为4字节,则要从4的整数倍地点起头存储)。

原则2 布局体的总大小,也就是sizeof的终局,必须是其外部最大成员的整数倍,无余的要补齐。

*原则3 布局体作为成员时,布局体成员要从其外部最大元素大小的整数倍地点起头存储。(struct a里存有struct b,b里有char,int,double等元素时,那末b该当从8的整数倍地点处起头存储,因为sizeof(double) = 8 bytes)

这里,我们联合上面的顺序来阐发(姑且不探究原则3)。

先看看char_short_long和long_short_char这两个布局体,从它们的成员变量的地点可以或许看进去,这两个布局体吻合原则1和原则2。留心,在 char_short_long的成员变量的地点中,char_short_long.s的地点是1244994,也就是说,1244993是“空的”,只是被“占位”了!

再看看char_long_short这个布局体,char_long_short的地点漫衍环境以下表:

成员变量 成员变量十六进制地点 成员变量十进制地点 char_long_short.c 0x0012FF2C 1244972 char_long_short.l 0x0012FF30 1244976 char_long_short.s 0x0012FF34 1244980

可见,其内存漫衍图以下,共12 bytes:

地点 1244972 1244973 1244974 1244975 1244976 1244977 1244978 1244979 1244980 1244981 1244982 1244983 成员 .c       .l .s    

首先,1244972能被1整除,所以char_long_short.c放在1244972处没有成就(着实,就char型成员变量本身来说,其放在任何地点单元处都没有成就),痛处原则1,在当前的1244973~1244975中都没有能被4(因为sizeof(long)=4bytes)整除的,1244976能被4整除,所以char_long_short.l该当放在1244976处,那末同理,最后一个.s(sizeof(short)=2 bytes)是该当放在1244980处。

是不是这样就终止了?不是,另有原则2。痛处原则2的哀告,char_long_short这个布局体所占的空间大小该当是其占内存空间最大的成员变量的大小的整数倍。假定我们到此就终止了,那末char_long_short所占的内存空间是1244972~1244981共计10bytes,不吻合原则2,所以,必须在最后补齐2个 bytes(1244982~1244983)。

至此,一个布局体的内存计划实现了。

上面我们根据上述原则,来验证这样的阐发是不是准确。按上面的阐发,地点单元124497三、124497四、1244975以及124498二、1244983都是空的(起码char_long_short未用到,只是“占位”了)。假定我们的阐发是准确的,那末,定义这样一个布局体,其所占内存也该当是12 bytes: 

struct //声名布局体char_long_short_new  {  char c;  char add1; //补齐空间  char add2; //补齐空间  char add3; //补齐空间  long l;  short s;  char add4; //补齐空间  char add5; //补齐空间  }char_long_short_new; 

运行终局以下:

可见,我们的阐发是准确的。至于原则3,巨匠可以或许本身编程验证,这里就再也不探究了。

所以,不管你是在VC6.0照旧Keil C51,照旧Keil MDK中,当你须要定义一个布局体时,只有你轻细留心布局体成员变量内存对齐这一景象,就能在很洪水平上勤俭MCU的RAM。这一点不只仅应用于事实编程,在良多大型公司,比喻IBM、微软、baidu、华为的面试和面试中,也是罕见的。

这三大块硬骨头是深造C言语的绊脚石,下功夫拿掉根抵上C言语的大动脉就打通了,那末再去深造另外内容就相对比拟俭朴了。编程深造进程中越是苦楚的时光,学到的货物就会越多,克遵夙昔就会本身的手艺,销毁了后面的支出的时光都将清零。越是难学的言语在入门当前,在入门当前越感应过瘾,并且还苟且上瘾。你上瘾了没?照旧销毁了?