c语言相关总结之指针总结
由于嵌入式设备的硬件资源一般都比较紧缺,内存小,处理器频率低等特点,c语言作为运行效率仅次于汇编的常用编程语言,自然作为嵌入式设备的首选编程语言,c语言的基础中重点和难点大部分集中在指针相关,本篇文章将对c指针进行一个小总结,有不足之处还希望大家指出。
1.指针的正确理解
要对指针有个正确的理解,首先要理解计算机的内存地址,计算机的内存是以字节为一个单位,被分为了若干个单位,而每个单位(字节)都有自己的编号,编号以十六进制数表示,一个字节一个字节增加如下图:
而这些编号就是地址,即0x00000000代表第一个字节的地址(即十进制编号0),0x00000001代表第二个字节的地址(即十进制编号1),以此类推一直到最后一个字节的地址0xFFFFFFFF(针对32位操作系统),例如定义一个整型变量a(int a),系统就给该变量分配四个字节的内存空间,假如占用的空间的地址(编号)为0x200000000 0x200000001 0x20000003 0x00000004这四个字节(一般变量的内存地址都是连续的),那么a的起始地址就是0x20000000(小端序机器),即变量a的地址是0x20000000。
现在再来理解下指针变量,c语言中指针通常是指指针变量,指针变量中存放的是地址,即上图中的那些字节的编号(对于32位操作系统来说是地址为四个字节,64位操作系统地址为八个字节),例如定义一个整型指针变量p(int* p),p中存放变量a的地址(p = &a),p中存放的数据即为a的起始地址(0x20000000),但是就会出现一个问题,只告诉了a的起始地址,没告诉a的结束地址,如何正确的通过指针访问到a所占的所有的内存数据?
系统会根据指针的类型来决定从起始地址向后访问几个字节,定义p时 int* p中的int就决定了从p向后访问四个字节的大小的数据,同理,如果是char* p,就代表从p中存放的地址向后访问一个字节的数据。
即指针类型决定了从指针变量中存放的地址向后访问几个字节的数据。
2.指针的相关用法
- 一维数组 二维数组
数组和指针密不可分,对于一维数组来说,如int a[3] = {1,2,3};数组a的三个元素在内存中也是连续的,假设a[0]的地址为:0x20000000,则a[1]的地址为0x20000004,a[2]的地址为0x20000008:
这和指针有点类似,都是使用基地址和偏移量去访问数据的,实际上数组名就是一个常量指针,存放的是首元素的地址,即&a[0] 与数组名a的值是相等的,这点可将&a[0]与a的值进行输出来验证,而且a[0]就是*(a+0)的简写,a[1]就是*(a+1)的简写。
对于二维数组,二维数组也是如此,不过二维数组中的元素为一维数组,即二维数组是由若干一维数组组成的,且这些一维数组的元素个数都相同,例如定义一个二维数组int a[2][3],
数组a中有两个元素,每个元素均为有三个整型数据的一维数组:
二维数组也可以使用指针访问,如a[1][1]就是*((a+1)+1)的简写,这里解释下,数组名a代表的是首元素的地址,也就是第一个一维数组起始地址,但是这是一个由3个元素的一维数组的地址,也就是说通过a可以访问到12个字节的数据(3个整型数据),a+1就是第二个一维数组的首地址,(a+1)就拿到了第二个一维数组首元素的地址,这一点可能有点绕,但是如果对a+1与*(a+1)进行输出,会发现他们两个的值是相同的:
2. 输出型参数
输出型参数,在函数返回时只能返回一个数据,利用输出型参数就可以返回多个数据,例如写一个函数用来计算两个数的平均数、和、差
int Cal(int a,int b,float* aver,int* sum,int* diff)
{
*aver = (a+b)/2.0f;
*sum = a+b;
*diff = a>b?(a-b):(b-a);
return 0;
}
int main(void)
{
Int a = 2;
Int b = 3;
float aver;
int sum;
int diff;
Cal(a,b,&float,&sum,&diff);
return 0;
}
在许多系统提供的函数中,特别是获取状态的函数,往往是利用输出型参数来输出状态的,例如:
int getStat(int arg,struct stat* stat);
函数自动对传进来的结构体的地址进行取目标,然后对结构体的成员进行赋值,这样就可以在主调函数中不通过返回值返回,以节省内存开销。
但要注意的是,若以指针作为函数的返回值,一定要注意该指针所指向的数据的生命周期有没有到:
运行结果为段错误
3. 函数指针
函数也有对应的指针,函数的指针的意义就是该函数的入口地址,且函数名本身就为该函数的入口地址,函数指针的定义方法为
返回值类型 (*指针名)(参数类型1,参数类型2…)
例如:
int open(char* path,int flg,);若定义一个该类型的函数指针p则为:
int (*p)(char,int),
对该函数指针进行赋值:p = open;
此时对open函数进行调用时就可以使用函数指针进行调用:
p(“/mnt/hgfs/1.txt”,O_RDONLY);
等同于:open(“/mnt/hgfs/1.txt”,O_RDONLY);
回调函数就是使用函数指针进行回调的。
- 字符指针
通过字符指针可以引用常量字符串,这样可以节省内存开销,但要注意的是字符指针引用字符串常量后,不能像字符数组那样对字符串进行更改:
5. 指针妙用(如寄存器映射)
通过指针可以有很多的妙用,举个例子,对于STM32微控制器的寄存器的使用中,如下图
要对该32位寄存器的bit 4和bit 5都置1,可以直接操作寄存器,通常要先找到该寄存器的起始地址(假设为0x40020000),再使用位运算进行操作:
((unsigned int)(0x40020000)) |= (0x03<<4);
可能对一个寄存器操作还可以,但是往往要操作很多个不同的的寄存器,每一次都要查找寄存器的地址再操作,效率往往较低,但是由于这些要操作的寄存器的地址都是连续的,因此就可以定义一个结构体,结构体成员为这些寄存器的位数所对应的数据类型,如32位寄存器可对应unsigned int类型,再将第一个寄存器的起始地址类型转换位该结构体类型的指针如下:
typedef struct
{
uint32_t MODER;
uint32_t OTYPER;
uint32_t OSPEEDR;
uint32_t PUPDR;
uint32_t IDR;
uint32_t ODR;
uint16_t BSSRL;//低16位
uint16_t BSSRH;//高16位
uint32_t LCKR;
uint32_t AFRL;
uint32_t AFRH;
}GPIO_TypeDef;
第一个寄存器为32位的MODER寄存器,假设起始地址为0x40020000,将该地址转换为GPIO_TypeDef的指针类型:(GPIO_TypeDef*)(0x40020000),那么将第二个32位的寄存器OTYPER的5位置1就可以:
typedef (GPIO_TypeDef*)(0x40020000) GPIO
GPIO->OTYPER | = (1<<5);
相对于直接找寄存器地址后再访问就方便的多,其实这就是STM32标准固件库中对寄存器映射的方法。
以上就是对c语言中指针的一个小总结,指针是把双刃剑,用的好了可以大幅度节省开发劳动量,但若用不好,很可能造成系统奔溃等问题,文中如有不正确之处还希望各位指出!