上海建网站的公司,打字赚钱平台 学生一单一结,惠州建设工程造价管理协会网站,加油站网站大全C 语言在嵌入式学习中是必备的知识#xff0c;甚至大部分操作系统都要围绕 C 语言进行#xff0c;而其中有三块技术难点#xff0c;几乎是公认级别的“难啃的硬骨头”。
今天就来带你将这三块硬骨头细细拆解开来#xff0c;一定让你看明白了。 0x01 指针
指针是公认最难理…C 语言在嵌入式学习中是必备的知识甚至大部分操作系统都要围绕 C 语言进行而其中有三块技术难点几乎是公认级别的“难啃的硬骨头”。
今天就来带你将这三块硬骨头细细拆解开来一定让你看明白了。 0x01 指针
指针是公认最难理解的概念也是让很多初学者选择放弃的直接原因。
指针之所以难理解因为指针本身就是一个变量是一个非常特殊的变量专门存放地址的变量这个地址需要给申请空间才能装东西而且因为是个变量可以中间赋值这么一倒腾很多人就开始犯晕了绕不开弯了。
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是一个参数为一个整数据且返回一个指向由整型指针变量组成的数组的指针变量的函数。
说到这里也就差不多了。理解了这几个类型其它的类型对我们来说也是小菜了。不过一般不会用太复杂的类型那样会大大减小程序的可读性请慎用。这上面的几种类型已经足够我们用了。
细说指针
指针是一个特殊的变量它里面存储的数值被解释成为内存里的一个地址。
要搞清一个指针需要搞清指针的四方面的内容指针的类型、指针所指向的类型、指针的值或者叫指针所指向的内存区、指针本身所占据的内存区。让我们分别说明。
先声明几个指针放着做例子
1int *ptr;2char *ptr;3int **ptr;4int (*ptr)[3];5int *(*ptr)[4];指针的类型
从语法的角度看小伙伴们只要把指针声明语句里的指针名字去掉剩下的部分就是这个指针的类型。这是指针本身所具有的类型。
让我们看看上述例子中各个指针的类型
1int ptr;//指针的类型是int2char ptr;//指针的类型是char3int ptr;//指针的类型是int4int (ptr)[3];//指针的类型是int()[3]5int *(ptr)[4];//指针的类型是int(*)[4]怎么样找出指针的类型的方法是不是很简单
指针所指向的类型
当通过指针来访问指针所指向的内存区时指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。
从语法上看小伙伴们只需把指针声明语句中的指针名字和名字左边的指针声明符*去掉剩下的就是指针所指向的类型。
上述例子中各个指针所指向的类型
1int ptr; //指针所指向的类型是int2char *ptr; //指针所指向的的类型是char*3int *ptr; //指针所指向的的类型是int*4int (*ptr)[3]; //指针所指向的的类型是int(*)[3]5int *(*ptr)[4]; //指针所指向的的类型是int*(*)[4]在指针的算术运算中指针所指向的类型有很大的作用。
指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。当小伙伴们对 C 越来越熟悉时就会发现把与指针搅和在一起的类型这个概念分成指针的类型和指针所指向的类型两个概念是精通指针的关键点之一。
笔者看了不少书发现有些写得差的书中就把指针的这两个概念搅在一起了所以看起书来前后矛盾越看越糊涂。
指针的值
即指针所指向的内存区或地址。
指针的值是指针本身存储的数值这个值将被编译器当作一个地址而不是一个一般的数值。
在32位程序里所有类型的指针的值都是一个32位整数因为32位程序里内存地址全都是32位长。指针所指向的内存区就是从指针的值所代表的那个内存地址开始长度为si zeof(指针所指向的类型)的一片内存区。
以后我们说一个指针的值是XX就相当于说该指针指向了以XX为首地址的一片内存区域我们说一个指针指向了某块内存区域就相当于说该指针的值是这块内存区域的首地址。
指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在例一中指针所指向的类型已经有了但由于指针还未初始化所以它所指向的内存区是不存在的或者说是无意义的。
以后每遇到一个指针都应该问问这个指针的类型是什么指针指的类型是什么该指针指向了哪里
指针本身所占据的内存区
指针本身占了多大的内存只要用函数sizeof(指针的类型)测一下就知道了。在32位平台里指针本身占据4个字节的长度。指针本身占据的内存这个概念在判断一个指针表达式是否是左值时很有用。
0x02 函数
面向过程对象模块的基本单位以及对应各种组合函数指针指针函数。
一个函数就是一个业务逻辑块是面向过程单元模块的最小单元而且在函数的执行过程中形参、实参如何交换数据如何将数据传递出去如何设计一个合理的函数不单单是解决一个功能还要看是不是能够复用避免重复造轮子。
函数指针和指针函数表面是两个字面意思的互换实际上含义截然不同。指针函数比较好理解就是返回指针的一个函数函数指针这个主要用在回调函数很多人觉得函数都还没搞明白回调函数更晕菜了。其实可以通俗的理解指向函数的指针本身是一个指针变量只不过在初始化的时候指向了函数这又回到了指针层面。没搞明白指针再次深入的向前走特别难。 C语言的开发者们为后来的开发者做了一些省力气的事情他们编写了大量代码将常见的基本功能都完成了可以让别人直接拿来使用。但是那么多代码如何从中找到自己需要的呢将所有代码都拿来显然是不太现实。
但是这些代码早已被早期的开发者们分门别类地放在了不同的文件中并且每一段代码都有唯一的名字。所以其实学习C语言并没有那么难尤其是可以在动手锻炼做项目中进行。使用代码时只要在对应的名字后面加上( )就可以。这样的一段代码就是函数函数能够独立地完成某个功能一次编写完成后可以多次使用。
很多初学者可能都会把C语言中的函数和数学中的函数概念搞混淆。其实真相并没有那么复杂C语言中的函数是有规律可循的只要搞清楚了概念你会发现还挺有意思的。函数的英文名称是 Function对应翻译过来的中文还有“功能”的意思。C语言中的函数也跟功能有着密切的关系。
我们来看一小段C语言代码
#includestdio.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语言的函数说不定就能当场引来无数膜拜的目光。
0x03 结构体、递归
很多同学是在大学学习C语言的很多课程都没学完结构体都没学到因为从章节的安排来看好像结构体学习放在教材的后半部分了弄得很多学生觉得结构体不重要如果只是应付学校的考试或者就是为了混个毕业证的确学的意义不大。
如果想从事编程这个行业对这个概念还不了解基本上无法构造数据模型没有一个业务是完全使用原生数据类型来完成的很多高手在设计数据模型的时候一般先把头文件中的结构体数据整理出来。然后设计好功能函数的参数以及名字然后才真正开始写c源码。
如果从节省空间考虑结构体里面的数据放的顺序不一样在内存中占用的空间也不一样结构体与结构体之间赋值结构体存在指针那么赋值要特别注意需要进行深度的赋值。
递归一般用于从头到尾统计或者罗列一些数据在使用的时候很多初学者都觉得别扭怎么还能自己调用自己而且在使用的时候一定设置好跳出的条件不然无休止地进行下去真就成无限死循环了。
对于结构体方面的知识具体也可以参见大佬的经验
相信大家对于结构体都不陌生。在此分享出本人对C语言结构体的研究和学习的总结。如果你发现这个总结中有你以前所未掌握的那本文也算是有点价值了。当然水平有限若发现不足之处恳请指出。代码文件test.c我放在下面。在此我会围绕以下2个问题来分析和应用C语言结构体 C语言中的结构体有何作用 结构体成员变量内存对齐有何讲究(重点)
对于一些概念的说明我就不把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 yearvu8 monthvu8 datevu8 hourvu8 minvu8 sec)显然这样的形参很不可观数据结构管理起来也很繁琐。如果某个函数的返回值是一个表示日期和时间的数据那就更复杂了。这只是一方面。
另一方面如果用户需要表示日期和时间的数据中还要包含星期(周)这个时候如果之前没有用结构体那么应该在DsipDateTime()函数中再增加一个形参vu8 week
void DsipDateTime( vu16 yearvu8 monthvu8 datevu8 weekvu8 hourvu8 minvu8 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部分结构体成员变量内存对齐中详细说明。
#includestdio.hint 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 bb里有charintdouble等元素时那么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.c0x0012FF2C1244972char_long_short.l0x0012FF301244976char_long_short.s0x0012FF341244980
可见其内存分布图如下共12 bytes 首先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)。
至此一个结构体的内存布局完成了。
下面我们按照上述原则来验证这样的分析是不是正确。按上面的分析地址单元1244973、1244974、1244975以及1244982、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、微软、百度、华为的笔试和面试中也是常见的。
这三大块硬骨头是学习C语言的绊脚石下功夫拿掉基本上C语言的大动脉就打通了那么再去学习别的内容就相对比较简单了。
编程学习过程中越是痛苦的时候学到的东西就会越多克服过去就会自己的技能放弃了前面的付出的时间都将清零。越是难学的语言在入门之后在入门之后越觉得过瘾而且还容易上瘾。你上瘾了没还是放弃了