尔游网
您的当前位置:首页C语言:指针、结构体、函数指针、链表

C语言:指针、结构体、函数指针、链表

来源:尔游网

1、指针

核心是变量、变量、变量,他既然能够变,肯定就在内存里,如下:

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

在上图里面我们定义一个int a,它是4个字节,

如果定义一个字符变量的话,它是一个字节,

如果定义一个结构体的话,结构体可能更大。

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

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

R0=123

那么我怎么通过变量P来操作变量a呢?

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

我们怎么操作寄存器呢?

unsigned int *p = 某个地址;

使用的时候怎么做呢?

*p = 某个值;
unsigned int *p = 某个地址;    

就是:

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

2、结构体

下面我们来讲结构体。

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

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

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

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

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

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

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

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

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

所以我们用的是结构体指针。
再看看这个图里面:

//1、先定义一个结构体
typedef struct
{
volatile unsigned int CRL;
volatile unsigned int CRH;
volatile unsigned int IDR;
volatile unsigned int ODR;
volatile unsigned int BSRR;
volatile unsigned int BRR;
volatile unsigned int LCKR;
}GPIO_TypeDef;
//2、再定义结构体指针
GPIO_TypeDef *gpioa;
//3、gpioa变量等于GPIOA的首地址
gpioa=(GPIO_TypeDef *)0x40010800;
//4、操作寄存器
gpioa->ODR=1;

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

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

“->”:指向的xxx。

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

3、函数指针

我们的函数保存在哪里?

  • 保存在flash上面。

保存在flash哪里?

  • 总得有个地址吧。

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

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

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

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

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

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

左边赋值 指针,右边赋值 函数指针;
如图:

记住我们的口诀,变量变量,可以变,就是可读可写。

可读可写,只能在内存里面。你定义一个变量的时候,在内存里面必定会分配一块空间.

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

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

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

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

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

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

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

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

4、链表

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

你看有三个特务,ABC。

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

我们来画图讲解一下

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

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

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

A.next_addr = &B;
B.next_addr = &C;
C.next_addr = NULL;


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

我们使用箭头来表示:


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

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

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

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

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

void main()
{
    int i;
    struct student myclass_students[10];
    for(i=0;i<10;i++);
    {
        scanf("%s",myclass_students[i].name);
        scanf("%d",&myclass_students[i].age);
    }

    for(i=0;i<0;i++)
    {
        printf("name:%s,age:%d\n",myclass_students[i].name,
        myclass_students[i].age);
    }
}

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

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

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

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

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

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

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

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

struct student{
    char name[8];
    int age;
    struct student *next;
}

void main()
{
    int i;
    struct student *student_head;
    struct student *new_student;
    
   while(1)
    {
        new_student malloc(sizeof(struct student));
        scanf("%s",new_student->name);
        scanf("%d",&new_student->age);
        //把new_student插入链表
        //判断是否退出
    }

    //打印链表所有成员
    {
        printf("name:%s,age:%d\n",myclass_students[i].name,
        myclass_students[i].age);
    }
}

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

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

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

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

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

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

5、Q&A

问:

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

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

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

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

char a;
int *p;
p=&a;//a的地址赋予p
*p='A';//ASCII-A的值赋给指针p所指向的内存位置

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

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

再举个例子:

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


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

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

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

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

//1、先定义一个结构体
typedef struct
{
    volatile unsigned int CRL;
    volatile unsigned int CRH;
    volatile unsigned int IDR;
    volatile unsigned int ODR:
    volatile unsigned int BSRR:
    volatile unsigned int BRR:
    volatile unsigned int LCKR;
}GPIO_TypeDef;/*声明结构体类型,就表示:结构体里面有啥,并不表示已经分配了空间。
你声明自己有钱,你就真的有钱吗?*/
//2、在定义结构体指针
GPIO_TypeDef *gpioa;

//3、gpioa变量等于GPIOA的首地址
gpioa=(gpio_TypeDef *)0x40010800;//这里设置地址

//4、操作寄存器
gpioa->ODR=1;

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

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

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

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

对于这个GPIO结构体:

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

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

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

问: 结构体定义,是保存在flash的吧?
**答:**对于这个问题,大家可以反过来想一想。

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

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

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

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

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

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

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

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

答案是 都可以!

这就引入了大小字节序。

我们举个例子:


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

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

问:

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

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

不管你的程序怎么写,不管你程序怎么定义变量,跟我GPIO寄存器有什么关系呢?

你写程序,程序是否要操作GPIO寄存器,GPIO寄存器肯定都有初始值。

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

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

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

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

**答:**函数指针用的非常非常多,你应该用的少是因为你们还没有接触到。

我来举个使用函数指针的例子,核心就在于让代码更加容易移植。

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

void LCD_3_5_draw_logo(void)
{
/*在3.5寸的LCD上面LOGO*/
}

void LCD_4_2_draw_logo(void)
{
/*在4.3寸的LCD上画LOGO*/
}

void main()
{
#ifdef LCD_3_5
    LCD_3_5_draw_logo();
#else
    LCD_4_2_draw_logo();
#endif
}

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

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

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

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

void LCD_3_5_draw_logo(void)
{
/*在3.5寸的LCD上面LOGO*/
}

void LCD_4_2_draw_logo(void)
{
/*在4.3寸的LCD上画LOGO*/
}

void main()
{
    int type;
    type=read_gpio();//通过硬件分辨LCD型号
    if(type==LCD_3_5)
        LCD_3_5_draw_logo();
     else if(type==LCD_4_2)
         LCD_4_2_draw_logo();
}

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

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

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

struct lcd_ops{
    int type;
    void(*draw_logo)(void);
};

void LCD_3_5_draw_logo(void)
{
//在3.5寸的LCD上面画LOGO
}

void LCD_4_2_draw_logo(void)
{
//在4.2寸的LCD上面画LOGO
}

struct lcd_ops lcds[]={
    {LCD_3_5,LCD_3_5_draw_logo},
    {LCD_4_2,LCD_4_2_draw_logo},
};

struct lcd *get_lcd()
{
    struct lcd_ops* lcd;

    lcd=get_lcd();//通过硬件分辨、得到LCD结构体
    lcd->draw_logo();
}

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

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

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

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

struct spy{
    int val;
    struct spy* next_addr;
};

int main()
{
    struct spy A;
    A.next_addr=&A;
}


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

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

struct spy* next_addr;怎么理解?

**问:**老师你们有没有出单片机裸机课的?
**答:**有,我们有HAL库的教程,也有arm架构课程,从0写代码,不用任何HAL库:这属于RTOS训练营中的一部分

因篇幅问题不能全部显示,请点此查看更多更全内容