20-06-16-指针的各种骚操作

本文深入探讨C语言中的指针概念,包括基本定义、指针的指针、指针类型如字符型、整型、数组指针等,以及结构体、函数指针的使用方法。同时,介绍了指针数组和指针函数的概念。

看c语言也有一段时间了,想记录一下关于指针的各种用法。
参考书籍以及资源如下:
c语言中文教程
参考书籍:c语言入门经典(第5版)

1 指针?

什么是指针?指针就是地址。就这么理解,没错的。

指针(即地址)数据

图 1-1
图 1-1

那我们要指针干嘛呢?也就是它有什么用呢?对吧,你不能光有这个东西,没啥用怎么行。
指针就是指向数据的地址,关于数据是什么?只要合理都行。可以是整数、小数、函数、结构体、共用体等等,都行。因为指针就是一个地址啊,地址里面是什么都可以啊,对吧,挺简单的吧。

1.1 怎么表示指针?

我们不能光用文字表示指针吧,这多麻烦啊。所以需要定义指针的表示方式或者说符号。
⚠️注意:这里仅仅在c语言中,本文档没有设计c++。但是后续会重新写关于c++的部分。

【&】在变量中取地址

【*】在地址中取变量

可以参考链接01 一、初级篇——指针()、取地址(&)、解引用()与引用(&)的区别

#include<stdio.h>
void main() {
    int i = 10;//定义一个变量,并赋初始值为10
    int *p = &i;//定义一个指针变量,并赋初始值为i的地址
    *p = 199;
    printf("%d=%d", *p,i);//输出199=199
    printf("---%d---", p);//得到变量i的地址
}

简单讲就是:
--------->1 有某个数据(上面代码的例子是整数i=10)
--------->2 取这个数据的地址,也就是&i
--------->3 因为地址就是指针,所以呢,我们可以定义一个指针来等于这个地址。
--------->-- int *p =&i 在这里呢,*就是表示p是指针的符号;也就是p指向i的地址.如果这里还不能理解,请参考链接01 一、初级篇——指针()、取地址(&)、解引用()与引用(&)的区别

1.2 指针的指针

在这里插入图片描述
图1-2
我们用图来说明一下问题。
指针p1指向的是某数据的地址,指针p2指针的是p1的地址。这就是指针的指针啊。

上面是文字描述,接下来我们用符号表示。

#include<stdio.h>
int main()
{
	int i = 10;//定义一个变量,并赋初始值为10
    int *p1 = &i;//定义一个指针变量,并赋初始值为i的地址
    int *p2 = &p1; //定义一个指针变量,并赋予初始值为p1的地址
    printf("--打印p1的地址---%d", p1)
    printf("--打印p2的地址---%d", p2)
    printf("---------------------")
    printf("--打印p1的值---%d", *p1)
    printf("--打印p2的值---%d", *p2)
    printf("--打印i的值---%d", i)
}

结果:

--打印p1的地址----272632452
--打印p2的地址----272632464
---------------------
--打印p1的值---10
--打印p2的值----272632452
--打印i的值---10
Program ended with exit code: 0

我们可以发现,打印的p2的值是p1的地址哦,也就说明,p2指向的是p1的地址啊,对吧。这就是指针的指针。

1.2.1 指针的指向

上面代码中,指针指向了i的地址,i=10。这只是指针的初始值,指针是一个变量,它是可以变换的。

#include<stdio.h>
 
int main(void)
{
	int num = 7;
	int *p = &num;
	printf("数值%d所在的地址是 %p\n", num, p);
	printf("指针p所指向的地址为 %p , 该地址上所保存的值为%d\n", p, *p);
	*p = 100;
	printf("指针p所指向的地址为 %p , 该地址上所保存的值为%d\n", p, num);
	return 0;
}
----------------
数值7所在的地址是 0x7ffeefbff578
指针p所指向的地址为 0x7ffeefbff578 , 该地址上所保存的值为7
指针p所指向的地址为 0x7ffeefbff578 , 该地址上所保存的值为100
Program ended with exit code: 0

可以看到,当*p=100后,p指向的地址的数据就是100了。如果不理解,请参考链接01 一、初级篇——指针()、取地址(&)、解引用()与引用(&)的区别

1.3 指针类型

  • 字符型指针
    char *p; //定义了一个字符变量指针,只能存放字符型数据的地址编号
    -±---------------------± - -
    char ch;
    p = &ch;

  • 短整型指针
    short int *p; //定义了一个短整型的指针变量p,只能存放短整型变量的地址

  • 整型指针
    int *p;

  • 长整型指针
    long int *p;

  • float型指针
    float *p;

  • double 型指针
    double *p;

  • 数组指针

  • 什么是数组?
    数组(Array)是一系列具有相同类型的数据的集合,每一份数据叫做一个数组元素(Element)。数组中的所有元素在内存中是连续排列的,整个数组占用的是一块内存。以int arr[] = { 99, 15, 100, 888, 252 };为例,该数组在内存中的分布如下图所示:
    在这里插入图片描述
    定义数组时,要给出数组名和数组长度,数组名可以认为是一个指针,它指向数组的第 0 个元素。在C语言中,我们将第 0 个元素的地址称为数组的首地址。以上面的数组为例,下图是 arr 的指向:

在这里插入图片描述
下面的例子演示了如何以指针的方式遍历数组元素:

#include <stdio.h>
int main(){
    int arr[] = { 99, 15, 100, 888, 252 };
    int len = sizeof(arr) / sizeof(int);  //求数组长度
    int i;
    for(i=0; i<len; i++){
        printf("%d  ", *(arr+i) );  //*(arr+i)等价于arr[i]
    }
    printf("\n");
    printf("----len_arr---%d\n",sizeof(arr));
    printf("---len_int---%d\n",sizeof(int));
    printf("\n");
    return 0;
}
----------
99  15  100  888  252  
----len_arr---20
---len_int---4

可能需要解释一下,数组的长度,数组的长度是元素所占的字符数,而不是元素个数。如果要求元素个数需要除以类型所占字符。

代码中我们使用了*(arr+i)这个表达式,arr 是数组名,指向数组的第 0 个元素,表示数组首地址, arr+i 指向数组的第 i 个元素,*(arr+i) 表示取第 i 个元素的数据,它等价于 arr[i]。
±----------------------------------------------------+
如果一个指针指向了数组,我们就称它为数组指针(Array Pointer)。
datatype (*array_name) [length];
比如:int (*p2)[10];
p2 是一个指针,它指向一个包含10 个int 类型数据的数组,即数组指针。
在这里插入图片描述

  • 结构体指针

  • 什么是结构体?
    前面我们讲解了数组(Array),它是一组具有相同类型的数据的集合。但在实际的编程过程中,我们往往还需要一组类型不同的数据,例如对于学生信息登记表,姓名为字符串,学号为整数,年龄为整数,所在的学习小组为字符,成绩为小数,因为数据类型不同,显然不能用一个数组来存放。

    可以使用结构体(Struct)来存放一组不同类型的数据。结构体的定义形式为:

struct 结构体名{
    结构体所包含的变量或数组
};

结构体是一种集合,它里面包含了多个变量或数组,它们的类型可以相同,也可以不同,每个这样的变量或数组都称为结构体的成员(Member)。请看下面的一个例子:

struct stu{
    char *name;  //姓名
    int num;  //学号
    int age;  //年龄
    char group;  //所在学习小组
    float score;  //成绩
};

stu 为结构体名,它包含了 5 个成员,分别是 name、num、age、group、score。结构体成员的定义方式与变量和数组的定义方式相同,只是不能初始化。

  • 结构体指针
    其实也就是地址中的数据是结构体,就叫结构体指针。
    struct <结构体名>*<变量名>
    实例:
struct data
{
    int i;
    char c;
    int k;
};
struct data *var;//声明data结构体类型指针变量"var"

需要说明的是,结构体指针,指向的是结构体变量。

可以参考链接02结构体指针

  • 函数指针

  • 什么是函数?
    函数是一段可以重复使用的代码,用来独立地完成某个功能,它可以接收用户传递的数据,也可以不接收。接收用户数据的函数在定义时要指明参数,不接收用户数据的不需要指明,根据这一点可以将函数分为有参函数和无参函数。

    将代码段封装成函数的过程叫做函数定义
    如果函数不接收用户传递的数据,那么定义时可以不带参数。如下所示:

dataType  functionName(){
    //body
}

例如,定义一个函数,计算从 1 加到 100 的结果:

int sum(){
    int i, sum=0;
    for(i=1; i<=100; i++){
        sum+=i;
    }
    return sum;
}

如果函数需要接收用户传递的数据,那么定义时就要带上参数。如下所示:

dataType  functionName( dataType1 param1, dataType2 param2 ... ){
    //body
}

数据通过参数传递到函数内部进行处理,处理完成以后再通过返回值告知函数外部。

int sum(int m, int n){
    int i, sum=0;
    for(i=m; i<=n; i++){
        sum+=i;
    }
    return sum;
}
  • 函数指针就是一个指向函数的指针
    函数指针可以像一般函数一样,用于调用函数、传递参数。

    函数指针变量的声明:

typedef int (*fun_ptr)(int,int); // 声明一个指向同样参数、返回值的函数指针类型
#include <stdio.h>
 
int max(int x, int y)
{
    return x > y ? x : y;
}
 
int main(void)
{
    /* p 是函数指针 */
    int (* p)(int, int) = & max; // &可以省略
    int a, b, c, d;
 
    printf("请输入三个数字:");
    scanf("%d %d %d", & a, & b, & c);
 
    /* 与直接调用函数等价,d = max(max(a, b), c) */
    d = p(p(a, b), c); 
 
    printf("最大的数字是: %d\n", d);
 
    return 0;
}
  • 回调函数—就是通过指针调用的函数

    函数指针变量可以作为某个函数的参数来使用的,回调函数就是一个通过函数指针调用的函数。

实例:

#include <stdlib.h>  
#include <stdio.h>
 
// 回调函数
void populate_array(int *array, size_t arraySize, int (*getNextValue)(void))
{
    for (size_t i=0; i<arraySize; i++)
        array[i] = getNextValue();
}
 
// 获取随机值
int getNextRandomValue(void)
{
    return rand();
}
 
int main(void)
{
    int myarray[10];
    populate_array(myarray, 10, getNextRandomValue);
    for(int i = 0; i < 10; i++) {
        printf("%d ", myarray[i]);
    }
    printf("\n");
    return 0;
}

简单讲,就是array指向的是数组,我们用array就是希望调用这个数组。同理,(*getNextValue)(void)就是指向函数的指针,我们用它就是希望调用函数。
可以参考链接03菜鸟教程

1.4 指针数组

前面我们讲了数组指针,现在我们讲指针数组。
指针数组,本质上是数组。数组元素是指针。
dataType *arrayName[length];
例子:

#include <stdio.h>
int main(){
    int a = 16, b = 932, c = 100;
    //定义一个指针数组
    int *arr[3] = {&a, &b, &c};//也可以不指定长度,直接写作 int *arr[]
    //定义一个指向指针数组的指针
    int **parr = arr;
    printf("%d, %d, %d\n", *arr[0], *arr[1], *arr[2]);
    printf("%d, %d, %d\n", **(parr+0), **(parr+1), **(parr+2));
    printf("------------------\n");
    printf("---arr[0]d的地址--%d\n-",arr[0]);
    printf("---arr[1]d的地址--%d\n-",arr[1]);
    printf("---arr[2]d的地址--%d\n-",arr[2]);
    return 0;
}
-----------
16, 932, 100
16, 932, 100
------------------
---arr[0]d的地址---272632488
----arr[1]d的地址---272632492
----arr[2]d的地址---272632496
-Program ended with exit code: 0

arr[1]是地址,*arr[1]是地址的数据。

1.5 指针函数

前面我们讲了函数指针。
指针函数,本质是一个函数,函数返回类型是某一类型的指针。
类型标识符 *函数名(参数表) int *f(x,y);
当一个函数声明其返回值为一个指针时,实际上就是返回一个地址给调用函数,以用于需要指针或地址的表达式中。

char *cp(char *s, char *t) 
{ 
    t = s; return t; 
}
int main() 
{ 
    char s1[10] = "hello"; 
    char s2[10] = "world"; 
    printf("%s %sn", s1, s2); 
    printf("%sn",cp(s1,s2)); 
    return 0; 
}

这里返回值就是一个地址. 这里 char *cp(char *s, char t)里面的第一个我们可以看做是跟定义一个指针 char p,里面的是一个意思,就是只是表示这是一个指针而已.所以我们调用cp(s1,s2)返回的就是一个地址,printf("%s")的时候,传入的是一个地址或者字符串的名字.如果是一个 int *get(),那么获得返回值就是 printf("%dn",*get())就可以了.
关于指针函数写的不好,晚上改吧。

<think> 我们正在讨论STM32的GPIO操作函数GPIO_WriteBit,用户提供了几个引用,其中引用[1]列出了几个GPIO函数,包括GPIO_WriteBit;引用[3]展示了GPIO_WriteBit的使用示例;引用[4]提到使用GPIO_WriteBit函数使LED灯闪烁。 用户询问的是函数GPIO_WriteBit(led->Port, led->Pin, led->OffBit)的使用方法和参数含义,特别关注led->Port, led->Pin, led->OffBit这三个参数。 根据引用[1]中的函数原型: void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal); 参数解释: 1. GPIOx: 指定GPIO端口,例如GPIOA, GPIOB等,在STM32中,每个GPIO端口用一个结构体指针表示(如GPIO_TypeDef*类型)。 2. GPIO_Pin: 指定要操作的引脚,使用宏定义如GPIO_Pin_0, GPIO_Pin_1等,这些宏实际上是一个16位的数,每一位代表一个引脚。 3. BitVal: 要写入的值,可以是Bit_RESET(低电平)或Bit_SET(高电平)。在标准库中,BitAction枚举类型定义如下: typedef enum { Bit_RESET = 0, Bit_SET }BitAction; 在用户给出的函数调用中,使用了三个参数:led->Port, led->Pin, led->OffBit。这通常是在一个LED结构体(可能是用户自定义的)中定义的成员。 假设用户定义了一个LED结构体,例如: typedef struct { GPIO_TypeDef* Port; // 指向LED连接的GPIO端口,如GPIOA uint16_t Pin; // LED连接的引脚,如GPIO_Pin_4 BitAction OffBit; // LED熄灭时对应的电平(因为有的LED是低电平点亮,有的是高电平点亮) } LED_TypeDef; 那么: led->Port: 就是LED所连接的GPIO端口,比如GPIOA。 led->Pin: LED所连接的引脚,比如GPIO_Pin_4。 led->OffBit: 表示要使LED熄灭需要写入的电平。例如,如果LED是低电平点亮,那么熄灭就是高电平,所以OffBit应该是Bit_SET;反之,如果LED是高电平点亮,那么熄灭就是低电平,OffBit就是Bit_RESET。 因此,调用GPIO_WriteBit(led->Port, led->Pin, led->OffBit)的作用是将LED所连接的引脚设置为熄灭状态(写入OffBit对应的电平)。 使用方法示例: 假设我们有一个LED,连接在GPIOC的第13脚,且该LED是低电平点亮(即当引脚为低电平时LED亮,高电平时灭)。那么: Port = GPIOC Pin = GPIO_Pin_13 OffBit = Bit_SET (因为要使LED熄灭,需要将引脚置高) 则调用:GPIO_WriteBit(GPIOC, GPIO_Pin_13, Bit_SET); 即可熄灭LED。 注意:在引用[4]中,用户提到使用GPIO_WriteBit函数实现LED闪烁,其中有一个示例代码片段: GPIO_WriteBit(GPIOA,GPIO_Pin_4,Bit_RESET); // 第四种方法,Bit_RESET置低电平,Bit_SET置高电平 另外,引用[3]中有一个翻转引脚的操作: GPIO_WriteBit(GPIOA,GPIO_Pin_0,(BitAction)(! GPIO_ReadOutputDataBit(GPIOA,GPIO_Pin_0))); 这里读取当前引脚输出状态,然后取反写入,实现翻转。 因此,如果我们要实现LED的闪烁,可以交替写入熄灭和点亮的状态(注意点亮的状态应该是与OffBit相反的状态,即可以用一个亮的状态宏,比如OnBit,或者用1-OffBit?)。但在用户给出的调用中,直接写入led->OffBit就是熄灭。 总结: - 函数功能:设置指定GPIO端口的指定引脚为高电平或低电平。 - 参数led->Port:GPIO端口基地址指针- 参数led->Pin:要操作的引脚(16位无符号整数,使用库中的宏定义)。 - 参数led->OffBit:要使LED熄灭需要设置的电平(Bit_RESET或Bit_SET)。 注意:这个函数调用只会设置一个引脚,即使同一个端口有其他引脚,也不会影响。 参考标准外设库,这个函数的实现可能是: void GPIO_WriteBit(GPIO_TypeDef* GPIOx, uint16_t GPIO_Pin, BitAction BitVal) { /* Check the parameters */ assert_param(IS_GPIO_ALL_PERIPH(GPIOx)); assert_param(IS_GET_GPIO_PIN(GPIO_Pin)); assert_param(IS_GPIO_BIT_ACTION(BitVal)); if (BitVal != Bit_RESET) { GPIOx->BSRR = GPIO_Pin; // 置位引脚(高电平) } else { GPIOx->BRR = GPIO_Pin; // 复位引脚(低电平) } } 或者使用BSRR寄存器同时设置置位和复位(因为BSRR的高16位用于复位,低16位用于置位),所以也可以这样写: if (BitVal != Bit_RESET) { GPIOx->BSRR = GPIO_Pin; // 低16位置位 } else { GPIOx->BSRR = (uint32_t)GPIO_Pin << 16; // 高16位复位 } 但具体实现要看库的版本。 现在,我们可以组织回答: 1. 函数功能 2. 参数详细解释(结合用户给出的led结构体成员) 3. 使用示例(包括如何定义led结构体,如何调用) 4. 注意事项 同时,生成相关问题。 </think> ### `GPIO_WriteBit()`函数详解 #### 1. 函数功能与原型 `GPIO_WriteBit()`是STM32标准外设库中的关键函数,用于精确控制单个GPIO引脚的电平状态: ```c void GPIO_WriteBit( GPIO_TypeDef* GPIOx, // GPIO端口基地址 uint16_t GPIO_Pin, // 目标引脚编号 BitAction BitVal // 写入的电平值 ); ``` 功能:**设置指定GPIO端口的单个引脚为高电平(`Bit_SET`)或低电平(`Bit_RESET`)**[^1]。 #### 2. 参数解析(结合`led`结构体) 在代码`GPIO_WriteBit(led->Port, led->Pin, led->OffBit)`中: - **`led->Port`** 指向GPIO端口的指针(如`GPIOA`, `GPIOB`),决定了操作哪个物理端口[^1][^3] 示例:`GPIOA`表示操作A组GPIO - **`led->Pin`** 目标引脚编号(16位无符号整数),使用预定义宏指定: $$ \text{GPIO\_Pin\_0} = 0x0001,\ \text{GPIO\_Pin\_1} = 0x0002,\ ...,\ \text{GPIO\_Pin\_15} = 0x8000 $$ 示例:`GPIO_Pin_13`表示操作第13引脚[^3] - **`led->OffBit`** 引脚目标状态(枚举类型),取值范围: ```c typedef enum { Bit_RESET = 0, // 低电平(0V) Bit_SET // 高电平(3.3V) } BitAction; ``` 通常定义为结构体成员,表示LED熄灭所需电平[^4] #### 3. 典型应用场景 1. **控制LED亮灭** 实现LED周期性闪烁的核心逻辑: ```c // 点亮LED(假设低电平点亮) GPIO_WriteBit(led->Port, led->Pin, led->OffBit); // OffBit=Bit_RESET // 熄灭LED GPIO_WriteBit(led->Port, led->Pin, !led->OffBit); // 取反电平 ``` 2. **引脚电平翻转(引用[2]操作)** 实现单条指令翻转引脚状态: ```c // 读取当前状态并取反写入 GPIO_WriteBit(GPIOA, GPIO_Pin_0, (BitAction)(!GPIO_ReadOutputDataBit(GPIOA, GPIO_Pin_0))); ``` 等效于:$ \text{新电平} = \neg(\text{当前电平}) $[^2] 3. **多方法对比(引用[3])** | 方法 | 特点 | 示例代码 | |------------------------|-------------------------------|-----------------------------------| | GPIO_WriteBit() | 精确控制单引脚 | `GPIO_WriteBit(GPIOA, Pin_4, Bit_RESET)` | | GPIO_ResetBits() | 专用于置低电平 | `GPIO_ResetBits(GPIOC, Pin_13)` | | 直接写BSRR寄存器 | 最高效,需位操作 | `GPIOC->BSRR = 1<<(16+13)` | | GPIO_Write() | 同时控制端口所有引脚 | `GPIO_Write(GPIOA, 0xFFEF)` | #### 4. 使用注意事项 1. **电平逻辑匹配** 需根据硬件电路设计`led->OffBit`值: - 共阴极LED:`OffBit = Bit_RESET`(低电平熄灭) - 共阳极LED:`OffBit = Bit_SET`(高电平熄灭) 2. **动态状态切换** 结合`GPIO_ReadOutputDataBit()`实现状态翻转: ```c // 读取当前引脚状态并翻转(引用[3]) GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)(1 - GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_4))); ``` 3. **与延时函数配合** 实现稳定闪烁必须添加延时(引用[4]): ```c while(1) { GPIO_WriteBit(led->Port, led->Pin, led->OffBit); // 亮 delay_ms(500); // 500ms延时 GPIO_WriteBit(led->Port, led->Pin, !led->OffBit);// 灭 delay_ms(500); } ``` #### 5. 错误使用案例 ```c // 错误:未初始化led->OffBit,可能导致意外状态 GPIO_WriteBit(led->Port, led->Pin, led->OffBit); // 错误:混淆引脚编号(GPIO_Pin_13对应PC13引脚) GPIO_WriteBit(GPIOA, GPIO_Pin_13, Bit_RESET); // 目标应是GPIOC ``` > **结果**:操作无效引脚或电平逻辑错误,导致LED不响应 --- ### 相关问题 1. 如何通过`GPIO_WriteBit()`实现无延时函数的LED呼吸灯效果? 2. 在寄存器级别,`GPIO_WriteBit()`与直接操作`BSRR`寄存器的性能差异有多大? 3. 如何利用`GPIO_WriteBit()`实现多引脚同步电平切换? 4. 当`led->OffBit`值与实际硬件电路不匹配时,会产生什么风险?[^4]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值