当前位置:   article > 正文

stm32基础入门——C语言篇(下)_stm32语言

stm32语言

目录

一.C语言数组

二.C语言结构体

 三.C语言联合体

四.C语言指针

五.C语言宏定义


一.C语言数组

1.数组的概念

由一系列类型相同的元素构成。

2.数组的声明

数组声明中包括数组元素的数目和元素的类型。编译器根据这些信息创建合适的数组,数组元素可以具有同变量一样的类型。如下例子

  1. int main()
  2. {
  3.     int arr[25];
  4.     char code[17];
  5.     float candy[55];
  6. }

3.数组的初始化

程序中经常会用数组来存储数据。例如,含有7个元素的数组可以用来存储一周的天数。这种情况下,程序在一开始就初始化数组比较方便。

int power[8] = {1, 2, 3, 4, 5, 6, 7, 8};

以上可以看出,可以使用花括号括起来的一系列数值初始化数组。数组元素的下标是从零开始的,数组的首元素(power[0])被初始化为1,依次类推,最后power[7]的值为8(如果你的编译器不支持这种初始化,提示这是一个语法错误,那么你使用的是ANSI以前的编译器。在数组的定义之前添加关键字static即可)。

     有时需要使用只读数组,也就是程序从数组中读取数值,但是程序不向数组中写数据。这种情况下声明并初始化数组时,建议使用关键字const。在声明const数组时对其初始化,因为在声明之后,不能在对它赋值。

数组初始化注意的两点:

 1.使用方括号对数组进行初始化时,编译器会根据方括号中的数值来确定数组大小。

 2.求数组大小时可以用(sizeof 数组名) / (sizeof 数组首元素的大小)的方法求取数组的大小。 此处补充一下,除了sizeof后直接跟数组名 和 &后直接跟数组名 这两种情况下数组名表示整个数组以外其他的情况数组名均不表示整个数组。
4.数组的赋值

 声明完数组后,可以借助数组的下标对数组的成员进行赋值。 注意:C不允许把数组作为一个整体来进行赋值,也不支持用花括号括起来的列表 形式进行赋值(初始化的时候除外)。

5.数组边界

使用数组的时候,需要注意的数组所有不能超过数组的边界。编译器不会检查索引的合法性,程序也许 能够运行,但是运行结果可能很奇怪,也可能会异常中断程序的执行。

        需要记住的一件事是数组的计数是从零开始的。避免这个问题可以才用的方法是在数组的声明中使用符号常量,然后在需要使用数组大小的地方都直接引用符号常量。
6.二维数组的初始化

二维数组的初始化是建立在对一维数组的的初始化上的。且二维数组在内存中也是连续存放的。

二.C语言结构体

1.什么是结构体

定义:结构体是一系列数据的集合这些数据可能描述了一个物体,也可能是一个问题的抽象。

简单说明:结构体是C语言中一种重要的数据类型,该数据类型由一组称为成员(或称为域,或称为元素)的不同数据组成,其中每个成员可以具有不同的类型。结构体通常用来表示类型不同但是又相关的若干数据。结构体类型不是由系统定义好的,而是需要程序设计者自己定义的。C语言提供了关键字struct来标识所定义的结构体类型。

2.结构体定义

  1. typedef struct Stu
  2. {
  3. char name[20];//名字
  4. int age;//年龄
  5. char sex[5];//性别
  6. char id[20];//学号
  7. }Stu;//分号不能丢

 3.结构体访问

为了访问结构的成员,我们使用成员访问运算符(.)

引用形式:<结构体类型变量名>.<成员名>

注意:结构体变量不能整体引用,只能引用变量成员

成员访问运算符是结构变量名称和我们要访问的结构成员之间的一个句号。可以使用 struct 关键字来定义结构类型的变量。

需要注意的是,结构体是一种自定义的数据类型,是创建变量的模板,不占用内存空间;结构体变量才包含了实实在在的数据,需要内存空间来存储。

4.结构体做函数参数

可以把结构作为函数参数,传参方式与其他类型的变量或指针类似。

 三.C语言联合体

1.联合体的定义

联合也叫共用体。

联合也是一种特殊的自定义类型 这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。比如:

2.联合体变量定义

创建联合和创建结构的方式相同,需要一个联合模板和联合变量。

(1)先创模板,后变量

  1. union perdata
  2. {
  3. int Class;
  4. char Office;
  5. };
  6. // 使用该联合体模板创建两个变量a, b
  7. union perdata a,b;

此处,perdata是联合体名,该名字是由我们任意定的,但是尽量起个有意义的名称。其相当于一个模板,可以使用这个模板去定义变量a、b。定义的时候不要忘了union

(2)同时创建模板和变量

  1. union perdata
  2. {
  3. int Class;
  4. char Office;
  5. }a,b;

3.初始化结构体

联合体的初始化与结构体不同,联合体只能存储一个值。

  1. perdata_U a;
  2. a.Class = 10;
  3. perdata_U b = a; /* 1、把一个联合初始化为另一个同类型的联合; */
  4. perdata_U c = {20}; /* 2、初始化联合的第一个成员; */
  5. perdata_U d = {.Office = 30}; /* 3、根据C99标准,使用指定初始化器。 */

 4.联合体相关注意事项

由于联合体中的所有成员是共享一段内存的,因此每个成员的存放首地址相对于于联合体变量的基地址的偏移量为0,即所有成员的首地址都是一样的。为了使得所有成员能够共享一段内存,因此该空间必须足够容纳这些成员中最宽的成员。对于这句“对齐方式要适合其中所有的成员”是指其必须符合所有成员的自身对齐方式。

下面举例说明:

例2:

union U

{undefined

char s[9];

int n;

double d;

};

s占9字节,n占4字节,d占8字节,因此其至少需9字节的空间。然而其实际大小并不是9,用运算符sizeof测试其大小为16.这是因为这里存在字节对齐的问题,9既不能被4整除,也不能被8整除。因此补充字节到16,这样就符合所有成员的自身对齐了。从这里可以看出联合体所占的空间不仅取决于最宽成员,还跟所有成员有关系,即其大小必须满足两个条件:1)大小足够容纳最宽的成员;2)大小能被其包含的所有基本数据类型的大小所整除。

四.C语言指针

1.指针的定义

我们指知道:C语言中的数组是指 一类 类型,数组具体区分为 int 类型数组,double类型数组,char数组 等等。同样指针 这个概念也泛指 一类 数据类型,int指针类型,double指针类型,char指针类型等等。

通常,我们用int类型保存一些整型的数据,如 int num = 97 , 我们也会用char来存储字符: char ch = ‘a’。

我们也必须知道:任何程序数据载入内存后,在内存都有他们的地址,这就是指针。而为了保存一个数据在内存中的地址,我们就需要指针变量。

因此:指针是程序数据在内存中的地址,而指针变量是用来保存这些地址的变量。

2.数据地址的概念

内存是一个很大的,线性的字节数组(平坦寻址)。每一个字节都是固定的大小,由8个二进制位组成。最关键的是,每一个字节都有一个唯一的编号,编号从0开始,一直到最后一个字节。如上图中,这是一个256M的内存,他一共有256x1024x1024 = 268435456个字节,那么它的地址范围就是 0 ~268435455 。

操作系统将RAM等硬件和软件结合起来,提供的抽象机制使得程序使用的是虚拟存储器,而不是直接使用真实存在的物理内存。所有的虚拟地址形成的集合就是虚拟地址空间。

由于内存中的每一个字节都有一个唯一的编号,因此,在程序中使用的变量,常量,甚至数函数等数据,当他们被载入到内存中后,都有自己唯一的一个编号,这个编号就是这个数据的地址。指针就是这样形成的。

下面用代码说明

#include <stdio.h>

int main(void)

{

    char ch = 'a';

    int  num = 97;

    printf("ch 的地址:%p\n",&ch);   //ch 的地址:0028FF47

    printf("num的地址:%p\n",&num);  //num的地址:0028FF40

    return 0;

}

指针的值实质是内存单元(即字节)的编号,所以指针 单独从数值上看,也是整数,他们一般用16进制表示。指针的值(虚拟地址值)使用一个机器字的大小来存储,也就是说,对于一个机器字为w位的电脑而言,它的虚拟地址空间是0~2∧w - 1 ,程序最多能能访问2∧w字节。这就是为什么xp这种32位系统最大支持4GB内存的原因了。

我们可以大致画出变量ch和num在内存模型中的存储。(假设 char占1个字节,int占4字节)。

3.指针变量与指针关系

用来保存 指针 的变量,就是指针变量。如果指针变量p1保存了变量 num的地址,则就说:p1指向了变量num,也可以说p1指向了num所在的内存块 ,这种指向关系,在图中一般用 箭头表示。

各种类型的指针

  1. int* p_int; //指向int类型变量的指针
  2. double* p_double; //指向idouble类型变量的指针
  3. struct Student *p_struct; //结构体类型的指针
  4. int(*p_func)(int,int); //指向返回类型为int,有2个int形参的函数的指针
  5. int(*p_arr)[3]; //指向含有3个int元素的数组的指针
  6. int** p_pointer; //指向 一个整形变量指针的指针
  7.  

 4.指针间的赋值

指针赋值和int变量赋值一样,就是将地址的值拷贝给另外一个。指针之间的赋值是一种浅拷贝,是在多个编程单元之间共享内存数据的高效的方法。

int* p1  = & num;

int* p3 = p1;

5.指针的重要属性

指针也是一种数据,指针变量也是一种变量,因此指针 这种数据也符合前面 变量和内存 主题中的特性。 这里我只想强调2个属性: 指针的类型,指针的值。

  1. int main(void)
  2. {
  3.     int num = 97;
  4.     int *p1  = #
  5.     char* p2 = (char*)(&num);
  6.     printf("%d\n",*p1);    //输出  97
  7.     putchar(*p2);          //输出  a
  8.     return 0;
  9. }

指针的值:很好理解,如上面的num 变量 ,其地址的值就是0028FF40 ,因此 p1的值就是0028FF40。数据的地址用于在内存中定位和标识这个数据,因为任何2个内存不重叠的不同数据的地址都是不同的。

指针的类型:指针的类型决定了这个指针指向的内存的字节数并如何解释这些字节信息。一般指针变量的类型要和它指向的数据的类型匹配。

由于num的地址是0028FF40,因此p1 和 p2的值都是0028FF40

*p1 : 将从地址0028FF40 开始解析,因为p1是int类型指针,int占4字节,因此向后连续取4个字节,并将这4个字节的二进制数据解析为一个整数 97。

*p2 : 将从地址0028FF40 开始解析,因为p2是char类型指针,char占1字节,因此向后连续取1个字节,并将这1个字节的二进制数据解析为一个字符,即’a’。

同样的地址,因为指针的类型不同,对它指向的内存的解释就不同,得到的就是不同的数据。

void*类型指针

由于void是空类型,因此void*类型的指针只保存了指针的值,而丢失了类型信息,我们不知道他指向的数据是什么类型的,只指定这个数据在内存中的起始地址,如果想要完整的提取指向的数据,程序员就必须对这个指针做出正确的类型转换,然后再解指针。因为,编译器不允许直接对void*类型的指针做解指针操作。

结构体和指针

结构体指针有特殊的语法: -> 符号

如果p是一个结构体指针,则可以使用 p ->【成员】 的方法访问结构体的成员

  1. typedef struct
  2. {
  3.     char name[31];
  4.     int age;
  5.     float score;
  6. }Student;
  7. int main(void)
  8. {
  9.     Student stu = { "Bob" , 19, 98.0};
  10.     Student*ps = &stu;
  11.     ps->age = 20;
  12.     ps->score = 99.0;
  13.     printf("name:%s age:%d\n",ps->name,ps->age);
  14.     return 0;
  15. }

数组和指针

1、数组名作为右值的时候,就是第一个元素的地址。

  1. int main(void)
  2. {
  3.     int arr[3] = { 1,2,3};
  4.     int*p_first = arr;
  5.     printf("%d\n",*p_first);  //1
  6.     return 0
  7. }

2、指向数组元素的指针 支持 递增 递减 运算。(实质上所有指针都支持递增递减 运算 ,但只有在数组中使用才是有意义的)

  1. int main(void)
  2. {
  3.     int arr[3] = { 1,2,3};
  4.     int*p = arr;
  5.     for(;p!=arr+3;p++)
  6. {
  7.         printf("%d\n",*p);
  8.     }
  9.     return 0;
  10. }

3、p= p+1 意思是,让p指向原来指向的内存块的下一个相邻的相同类型的内存块。

​ 同一个数组中,元素的指针之间可以做减法运算,此时,指针之差等于下标之差。

4、p[n] == *(p+n)

​ p[n][m] == (p+n)+ m )

5、当对数组名使用sizeof时,返回的是整个数组占用的内存字节数。当把数组名赋值给一个指针后,再对指针使用sizeof运算符,返回的是指针的大小。

这就是为什么我么将一个数组传递给一个函数时,需要另外用一个参数传递数组元素个数的原因了。

  1. int main(void)
  2. {
  3.     int arr[3] = { 1,2,3};
  4.     int*p = arr;
  5.     printf("sizeof(arr)=%d\n",sizeof(arr));  //sizeof(arr)=12
  6.     printf("sizeof(p)=%d\n",sizeof(p));   //sizeof(p)=4
  7.     return 0
  8. }

函数的参数和指针

C语言中,实参传递给形参,是按值传递的,也就是说,函数中的形参是实参的拷贝份,形参和实参只是在值上面一样,而不是同一个内存数据对象。这就意味着:这种数据传递是单向的,即从调用者传递给被调函数,而被调函数无法修改传递的参数达到回传的效果。

  1. void change(int a)
  2. {
  3.     a++;      //在函数中改变的只是这个函数的局部变量a,而随着函数执行结束,a被销毁。age还是原来的age,纹丝不动。
  4. }
  5. int main(void)
  6. {
  7.     int age = 19;
  8.     change(age);
  9.     printf("age = %d\n",age);   // age = 19
  10.     return 0;
  11. }

有时候我们可以使用函数的返回值来回传数据,在简单的情况下是可以的,但是如果返回值有其它用途(例如返回函数的执行状态量),或者要回传的数据不止一个,返回值就解决不了了。

传递变量的指针可以轻松解决上述问题。

五.C语言宏定义

1.宏定义的定义

宏定义在 C 语言源程序中允许用一个标识符来表示一个==字符串==,称为“==宏/宏体==” ,被定义为“宏”的==标识符==称为“==宏名==”。在编译预处理时,对程序中所有出现的宏名,都用宏定义中的字符串去代换,这称为“==宏替换==”或“==宏展开==”。 宏定义是由源程序中的宏定义命令完成的,宏代换是由预处理程序自动完成的。

         #define 标识符 字符串

在 C 语言中,宏分为 有参数和无参数两种。无参宏的宏名后不带参数,其定义的一般形式为:
#define 标识符 字符串
#表示这是一条预处理命令(在C语言中凡是以#开头的均为预处理命令)
==define #117411==为宏定义命令
==标识符 #800023==为所定义的宏名,
==字符串 #800019==可以是常数、表达式、格式串等。符号常量
2.宏定义的作用

使用简单宏定义可用宏代替一个在程序中经常使用的常量,这样在将该常量改变时,不用对整个程序进行修改,只修改宏定义的字符串即可,而且当常量比较长时, 我们可以用较短的有意义的标识符来写程序,这样更方便一些。
相对于==全局变量==两者的区别如下:
1. 宏定义在编译期间即会使用并替换,而全局变量要到运行时才可以。
2. 宏定义的只是一段字符,在编译的时候被替换到引用的位置。在运行中是没有宏定义的概念的。而变量在运行时要为其分配内存。
3. 宏定义不可以被赋值,即其值一旦定义不可修改,而变量在运行过程中可以被修改。
4. 宏定义只有在定义所在文件,或引用所在文件的其它文件中使用。 而全局变量可以在工程所有文件中使用,只要再使用前加一个声明就可以了。换句话说,宏定义不需要extern。


使用带参数的宏定义可完成函数调用的功能,又能减少系统开销,提高运行效率。正如C语言中所讲,函数的使用可以使程序更加模块化,便于组织,而且可重复利用,但在发生==函数调用 #800023==时,需要保留调用函数的现场,以便子 函数执行结束后能返回继续执行,同样在子函数执行完后要恢复调用函数的现场,这都需要一定的时间,如果子函数执行的操作比较多,这种转换时间开销可以忽 略,但如果子函数完成的功能比较少,甚至于只完成一点操作,如一个乘法语句的操作,则这部分转换开销就相对较大了,但使用带参数的宏定义就不会出现这个问 题,因为它是在预处理阶段即进行了宏展开,在执行时不需要转换,即在当地执行。宏定义可完成简单的操作,但复杂的操作还是要由函数调用来完成,而且宏定义所占用的目标代码空间相对较大。所以在使用时要依据具体情况来决定是否使用宏定义。
 

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/很楠不爱3/article/detail/280978
推荐阅读
相关标签
  

闽ICP备14008679号