【RTOS训练营】课程学习方法和C语言知识(指针、结构体、函数指针、链表)和学员问题

一、课程学习方法

因为有些学员是刚进群,所以这里再把学习方法讲一下。

1. 预习

我们会在每一节晚课之后会通知要预习的章节,学员需要按如下操作观看相关视频。

1.1 打开百问网官网

​1.2 点击首页的"深入学习单片机双架构双系统项目实战线上班"

​1.3 再点击"单片机双架构双系统项目实战",观看视频


在电脑上、在手机浏览器里、微信或QQ里,都可以打开我们的官网,然后使用超清观看(目前微信小程序还不支持超清观看)。

2. 晚课

预习之后就是晚上上课,晚课注意事项如下:


我们大部分学员都是在上班,有可能没办法参加晚上的课程,因此我们第2天会在官网整理出晚课的图文笔记,在如下类似的位置:

这个图片上4-7、4-8是根据前次晚课录制的补充扩展内容。

并不是每次晚课的第2天都会录制视频,但是图文是肯定会有的。

图里面红框里最后一个文件是我们的一个AI课程、AI回放,基本上就是QQ聊天内容的复现。

3. 遇到问题

如果您在学习中遇到问题可以在我们论坛内对应版本进行提问,论坛使用方法:

​ 3.1 浏览器打开:RTOS在线培训课程 - 最新的 - 问题 - 百问网嵌入式问答社区
​ 3.2 登陆用户名:购买课程时的手机号
​ 初始密码: 100ask (建议修改自己密码)

​ (如果是最近报班的学员,系统可能没有录入账号,请使用报名登记的手机号注册,再联系班主任开通发帖权限)

​ 3.3 账号有问题的话,联系班主任。

今天我们的主题是指针链表

二、指针

前面我录了两节视频(前面图片的4-7、4-8),核心就是变量、变量、变量,它既然能够变,肯定就在内存里。

我来举一个例子:

在代码里面定义一个变量,这个变量必定保存在内存里。
定义一个变量的时候,在内存里必定会分配一块对应的空间。

在上图里面我们定义一个int a,它是4个字节,
如果定义一个字符变量的话,它是一个字节,
如果定义一个结构体的话,结构体可能更大。

但是不管怎样,变量,变变变,它能够变,肯定是能够读能够写,他肯定是在内存里。

我们再加一个指针变量,指针变量它是指针也是变量,既然是变量也是在内存里。

请添加图片描述

这里又定义了一个int指针,在内存里面也给他分配了一个空间,
对于所有的指针, 不管你是什么类型的指针,它保存的是地址,
在32位处理器里面,地址占用的空间大小必定是4个字节的。

我们说指针这个变量,它保存的是地址,
它是保存一个数值,这个数值是地址,这个数值是32位的。

我们再来看看下面这个图,让这个指针指向某个变量,
所谓指向某一个变量,就是这个指针的值等于那个变量的地址。

在上图里面,假设变量a在内存里面的地址是addr1
P等于&a,就是P的值等于a的地址,就是图中的红字。

现在给变量a赋值,让a等于123。

结合在前面发布的扩展视频,C语言里面,a=123,实际上等于几条汇编指令?

大家现在还不涉及汇编指令,没关系,
这是给大家讲讲这个a = 123,实际上也是要去访问内存中的某个地址

简单来说就等于三条指令:

  1. R0=123
  2. R1=变量a的地址
  3. 把R0的值写到R1所指示的地址去

我们再来看看指针变量P,不是指向了a的地址吗?
那么我怎么通过变量P来操作变量a呢?

int a;
int *p;
p = &a;  // p等于变量a的地址
*p = 123; // 操作变量a

我们怎么操作寄存器呢?

这几句指令精简起来就是一样,就变成这样:

unsigned int *p = 某个地址;

使用的时候怎么做呢?

*p = 某个值;

这只是最简单用法,以前的P等于变量a的地址,现在的P等于寄存器的地址。

unsigned int *p = 某个地址;
就是:

unsigned int *p; 
p = 某个地址;  // p = &a;

三、结构体

下面我们来讲结构体。

我们怎么描述一个人,我们可以用一个结构体:

struct person {
  int age;
  char name[8];
};

我定义一个结构体变量: struct person wei;

变量、变量、变量, 他能够变, 可读可写,他必定在内存里。

那么我们再定义一个结构体指针呢?
指针、指针、指针里面存放的地址是4字节(32位处理器)。

现在我们定义了两个变量,一个是结构体变量,另外一个是指针变量,在内存里面都有对应的空间。

指针里面存放的永远是地址。

现在我们怎么使用变量wei, 或者指针变量p, 来访问这个结构体呢?

wei.age = 40;
p->age = 40;

这两句指定的效果是完全一样的

  • “.” : 我的
  • “->”:我指向的

以上就是结构体的基本知识。

我们再来看看怎么去访问下图gpio外设 ?

我们有GPIO模块的地址,可以像这样做:

请添加图片描述

你看你有7 ~ 8个寄存器,你得定义7 ~ 8个指针,还容易写错。

我们应该使用结构体还是使用结构体指针来表示寄存器?

如果定一个结构体的话,结构体变量它还是变量呀,它就在内存里面分配一大块空间。

而我们要操作的是gpio啊,在内存里面分配一大块空间干嘛?

所以我们用的是结构体指针。

再看看这个图里面:

编号2的地方定义了一个结构体指针,这个指针它是一个变量,它在内存里面必定有一个空间。

这个指针,它的指是一个地址。

在编号3的地方,我们要这个指针,他的值等于GPIO模块的首地址。

在编号4的地方,我们使用这个指针,操作寄存器。

“->”:指向的xxx。

gpioa->ODR = 1; 翻译就是: gpioa指向 的 ODR,等于1。

四、函数指针

我们的函数保存在哪里?

  • 保存在flash上面。

保存在flash哪里?

  • 总得有个地址吧。

既然有地址,是不是可以设置某个指针?让这个指针的值,等于函数的位置呢?

这个指针,它是函数的指针,也就是函数的位置,在32位处理器里面,它仍然是4个字节。

我们可以使用类比的方法,记忆函数指针:

int a;      <===>   int add(int a, int b) {return a+b;}
int *p;     <===>   int (*pf)(int a, int b);
p = &a;    	<===>   pf = &add;

仔细看这个对比,左右两边对比一下

左边定义变量a,右边定义函数add;

左边定义 int指针,右边定义 函数指针;

左边赋值 指针,右边赋值 函数指针;

如图:

记住我们的口诀,
变量变量,可以变,就是可读可写。
可读可写,只能在内存里面。
你定义一个变量的时候,在内存里面必定会分配一块空间.

左边是int指针,右边是函数指针

int指针是变量,函数指针也是变量。

在图里面椭圆形的地方,就是这两个变量。

指针、指针,在32个处理器里面,指针必定是4字节。

不管你是字符指针,in的指针、函数指针,结构体指针通通都是四字节。

以前的int指针,等于某个int变量的地址。

现在的函数指针,它的值等于某个函数的地址。

怎么使用呢,还是用类比的方法:

1. 赋值: pf = add;  // add, &add是完全一样的
2. 使用: pf(1,2 );  //  和 (*pf) (1, 2) 是完全一样的

讲那么久的指针,就要用起来了。

在HAL的代码里面,就经常使用的结构体指针,让这个指针等于某一个模块的地址。

先来看看gpio的HAL库,工程文件在下面这个图这里:

看第1个参数,他定义了一个结构体指针。

这个指针他就是一个值,是什么值呢?是某个GPIO模块的首地址。

我们来看别人调用这个函数的时候,必定会传入一个指针,或者说必定会传入一个地址值。

接着看看这个结构体长什么样子:

请添加图片描述

在这个结构体里面,它定义了很多成员,这些成员跟寄存器一一对应。

现在我们的结构体指针跟HAL库就扯上了关系了。

五、链表

链表的操作实际上并不是很复杂,你只要把指针搞清楚就行。

你看有三个特务,ABC。

A要去找B, A手上得有B的地址
B要去找C, B手上得有C的地址

ABC串起来就变成了一个链。

我们来画图讲解一下

第一,定义了结构体变量A,B,C;

在内存里面就必定有这三个结构体变量所对应的空间

请添加图片描述

假设在内存里面分配了ABC三个结构体变量,

他们地址分别是addrA, addrB, addrC。

A要去找B, A的手上得有B的地址,执行下面这条语句(注意箭头处):
A.next_addr = &B;

B要去找C, B的手上得有C的地址,执行下条语句:
B.next_addr = &C;

C,他没有下线了,他手上的地址是无效的:
C.next_addr = NULL;

大家看到这个链表,实际上是非常枯燥的。

变量a里面保存有变量B的地址变量,B里面保存有变量C的地址。

我们使用用箭头来表示:

看看蓝色的箭头,只是为了让我们人类更加容易理解而已

它的实质仍然是:
a里面有一个成员,用来保存B的地址
B里面有一个成员,用来保存C的地址

列表的所有的复杂操作,都是从这些基础的知识里面扩展出来,比如双向链表、链表的插入和删除。

列表的本质我再强调一遍:我手里有你家的地址,我才能够找到你。

在C语言里面这个地址怎么表示呀?用指针来表示。

在一个特务组织里面,我有上线的地址,还有下线的地址,这叫做双向链表。

在一个特务组织里面,我只有下线的地址,其他人也都只有下线的地址这叫做单向链表。

现在举一些应用的例子,然后再讲一下插入和删除操作。

我们举一个日常的例子,你们班10个学生,老师说要打印10个学生的信息,你可以用一个数组来做。

你看这个程序,就是你可以输入这10个学生的名字年龄,然后再把它们打印出来。他使用数组来保存这些学生的信息。

如果你这个班级有100个学生怎么办呢?

你得把这个数组大小设置为100。

那如果这个学校还有一些超级班级,比如有1000个学生,也得把这个数组设置成1000项。

也就是说为了支持这些小班级、中班级、超大的班级,你这个程序里面你得把这个数据设置设置的超级大,缺点就是浪费空间。

再比如说,这100个学生里面中间有某一个人转学走了,你这个数组中间就会空出一项,那一项你就标为无效。

再比如说,本来你在班级里面有1000个学生再插班进来一个学生,你这个程序就没有办法处理,问题的根源在于这个数组的容量是定死的。

如果使用链表,就可以这样写:

它的诀窍在于对于每一个学生我都会临时分配一个结构体。

你有10个我就分配10个结构体,你有100个我就分配100个结构体,你有1000个我有1000个,我就分配1000个结构体。

我使用列表可以支持小班级、中班级,超大班级。

如果有人走的话,有人转走了,我可以把列表中那一位给删除掉。

如果有人插进来,我又可以重新分配一个结构体,把这个新的结构体放进链表。

这就是日常生活中的一个例子,在rtos里面,常使用链表来管理任务。后面讲rtos时再来讲具体的任务链表。

六、晚课学员问题

1. 问:

typedef struct
{
    int a;
    Char b;
    Char buffer[100]
} X_x;

X_x w;
int *p =&w;

p指向结构体w的a的地址吗?

w.a,w.b,w.buffer 地址连续吗?

w占用了多少个字节?4+1+100?

答: int *p = &w; 结果没问题,编译有警告。

这个警告是编译器为了让你的程序写得更加规范,为了让你避免各种莫名其妙的问题。

我们可以举一个例子:

char a;
int *p;
p = &a;
*p = 12; 

*p = 12; 写4字节,但是变量a只有1字节的空间

我们可以再扩展一下,这样写程序的时候,会出现莫名其妙的问题:

char a;
int *p;
p = &a;
*p = 'A';

这段代码会有警告,但运行起来不会有问题,为什么呢?
为了追求效率,编译器也给char a分配了是4字节的空间

*p = 'A', 这个指令会写4个字节,错有错招,没什么后果。

再举个例子:

char a;
char b;  // 多了这个变量
int *p;
p = &a;
*p = 'A';

如果a、b挨着存放,赋值变量a,就会符变变量b。

所以说大家写是C程序的时候,任何警告都要引起重视。

这里a、b也不一定是连续存放的,不同的编译器有不同的考虑,比如说优化等级不一样的时候它也不一样。

p指向结构体w的a的地址吗? //是的

w.a,w.b,w.buffer 地址连续吗?//不连续,韦老效率,char b也被分配4字节,只用1字节,浪费3字节。

w占用了多少个字节?4+1+100? //应该是4+4+100

2. 问: 和&在c语言中使用的区别是什么?

答: &: 取地址
* : 用地址,去操作值

3. 问: 结构体指针gpioa的地址是在GPIO_TypeDef *gpioa;分配的。

他内部指向的->CRL,-> CRH 这些在什么时候被分配空间?
如果一直没有赋值GPIO->CRH =1这个内容,是不是它的GPIO->CRH空间一直不存在?

答:

我们去定义一个结构体类型的时候,只是去创建一种数据类型,就像创建char int 这些基本的类型一样。

int a; 才分配空间,int b才分配空间,int 不分配空间。

因此,struct person wei 才分配空间,struct person 不分配空间。

你只要定义了一个变量,就肯定会分配它的空间。
你只要定义了一个指针变量,就肯定会分配他的空间。

对于这个GPIO结构体:

编号为2的地方,它定义了结构体的变量,在图里面内存中,就分配了那个结构体。

编号为3的地方,他定义了一个结构体的指针,在内存中就分配了那个指针。

不管你用不用,一旦定义了必定会分配。

你看我们想去访问gpio,你在内存里面定义一个结构体有什么意义呢?

我们应该定一个结构体的指针,让这个指针等于GPIO模块的地址。

4. 问: 结构体定义,是保存在flash的吧?

答: 对于这个问题,大家可以反过来想一想。

Flash里面会保存chat这个类型吗?会保存int这个类型吗?会保存各种结构体的类型吗?

这些数据类型只是给C语言用而已,C语言最终要转换成汇编。在汇编里面,根本就没有这些数据类型。

这些数据类型只是给编译器使用的,让编译器来给那些变量分配空间而已。

最终编出来的程序程序里面根本就不含有这些数据类型。

5. 问: 因为我学的没那么深,大小端那里也没听太懂,希望老师整理资料的时候多做一些解释。

答: 我们来插讲一下大小端。

我们说一个变量,它在内存里面必定有对应的空间

我希望你们把这个口诀记到脑子里面去,
变量变量,可以变化,
可以变化,就是可以读,可以写。
可读可写的话,只能在内存里面,
所以说一个变量在内存里面必定有空间。

你看对于这个变量,a它有4个字节,也就是有4个地址

那么我们说:个十百千万,个位保存在哪里?

个位,保存在低地址那里,还是保存在高地址那里?

答案是 都可以!

这就引入了大小字节序。

个位保存在低地址,就是小字节序,也叫做小端

个位保存在高地址,就是大字节序,也叫大端。

我们举个例子:

6. 问: 咱们课程会讲 oled的芯片手册里面的 各个指令吗?感觉看手册很吃力,但是感觉底层驱动非常重要。

答: 不会,本课程重点是RTOS。

7. 问:

请添加图片描述

答: 首先如果你问的是那些寄存器的话,那些寄存器是GPIO里的寄存器,他们是有初始值的。

上面这个图里面红色方框中就是那些寄存器,那些硬件寄存器当然有初始值了。

不管你的程序怎么写,不管你程序怎么定义变量,跟我GPIO寄存器有什么关系呢?
你写程序,程序是否要操作GPIO寄存器,GPIO寄存器肯定都有初始值。

这个问题的核心在于定义一个变量,这个变量是在内存里面,

而我的硬件寄存器在另外一个GPIO模块上面,他们两个之间没有什么关系。

你使用指针来读写硬件寄存器时,才会去影响到硬件寄存器的值。

8. 问: 函数指针有啥用?目前很少用到。

答: 函数指针用的非常非常多,你应该用的少是因为你们还没有接触到。我来举个使用函数指针的例子,核心就在于让代码更加容易移植。

假设你们公司有一款产品, 要用到两款LCD,你可以在main中这样写代码。

使用一个宏开关,来决定使用哪一个函数。

再看一下,如果我们把这个程序分为两部分,main函数是APP,上面两个函数是驱动。

我是不是换一款屏幕就得重新写一下main函数:重新定义宏,重新编译。

那么有我们有没有办法呢?可以改进一下:

按这种方法,添加了一个新的函数,你使用不同的LCD时,我可以去读取某些引脚来判断你使用哪一个LCD。

在这种情况下,你即使更换了一款LCD,我也可以让这个main自动的去适应它。比前面那个好一点了。

但是,你们的公司这款产品它支持100款LCD,你就得加100个 if 判断,有些祖传代码有几十个上百个这样的判断,我们再怎么改进呢?

来看看这段代码,main函数基本上就不用变

变的只是你上面的驱动程序,你添加100款LCD都没关系,你就在那个数据里面往里面添加LCD就好了。

9. 问: 老师,结构体的自引用,只能自引用指针吗?

答: 自引用,是指自己引用自己吗?我暂且认为你是这样问的。

请添加图片描述

经常用来表示:链表里只有它一个成员。

if (A.next_addr == &A)
	printf("只有一个");

struct spy* next_addr;怎么理解?

spy*它是一个指针,它是结构体指针,这个结构体指针就等于某个结构体的地址。

在下面的main函数中, A里面这个指针,等于A自己的地址。

10. 问: 老师你们有没有出单片机裸机课的?

答: 有,我们有HAL库的教程,也有arm架构课程,从0写代码,不用任何HAL库:这属于RTOS训练营中的一部分

1 个赞