ARM架构与C语言(韦东山)学习笔记(5)-指针

众所周知,指针是C语言的灵魂,那指针到底是什么?指针有什么应用?指针对于ARM架构和嵌入式开发有什么意义?

一、指针是什么?

C语言的指针是一个变量,它存储了一个内存地址。通过指针,可以直接访问或修改该内存地址中存储的数据。指针在C语言中非常重要,它可以用于动态分配内存、传递参数、访问硬件寄存器等。指针的基本操作包括指针声明、指针赋值、指针解引用、指针算术运算等。
怎么理解呢?

1.变量在内存中的存储结构

通常,我们认为变量是这样的:

int val;
float num;//浮点型
uint8_t time;//8位无符号int型变量
char ch='A';//单引号用于表示字符常量,字符常量是一个单独的字符,用于表示一个ASCII码
char str[]="hello world"//双引号用于表示字符串常量,字符串常量是一个字符数组,由多个字符组成,以空字符'\0'结尾

比如一个int val=1003,那么:在C语言中,int类型的变量a在内存中的存储结构通常是由四个字节(32位)的连续空间组成,这些空间分别对应于该变量的不同位数。这四个字节的存储顺序通常是从低位到高位,也就是说,最低有效位存储在最低地址处,最高有效位存储在最高地址处。
在这里插入图片描述
变量a的值为1003,它被表示为二进制数0000001100001000,所以只要两个字节16位即可表示1003,因此高位的另外两个字节全为0。
同理,当执行语句char ch=‘A’;时,编译器会在内存中为该变量分配一个字节的存储空间,并将字符’A’的ASCII码值(65)存储在该字节中。当声明一个uint8_t类型的变量time时,它会在内存中分配一个字节的存储空间。uint8_t是一个无符号8位整数类型。
在这里插入图片描述
如果我定义:uint8_t a=330;这是错误的,因为8位无符号整形,最大只能到0~2^8=255,如果超出范围,编译器将只截取低8八位,导致实际的值错误。

因此,可以知道:对于常规的变量,值被转化为2进制存储在特定地址的内容。地址就是柜子号,内容就是柜子里装的东西,而指针,就是指向这个指定柜子的变量,它代表的是这个柜子号,柜子号是不同的,因此不是常量,所以指针就是一个变量

2.指针是一种变量,那它会被分配内存空间吗?内存空间里的内容是什么?

(1)会被分配内存空间

指针变量本身是一个变量,它存储了一个内存地址。在定义指针变量时,编译器会为其分配内存空间,以存储指向的内存地址。
具体来说,不同类型的指针变量在内存中所占用的空间大小是不同的。在32位的系统中,不管指向的数据类型是什么,指针变量本身都占用4个字节的内存空间;在64位的系统中,指针变量占用8个字节的内存空间。
举个例子,如果你定义了一个int*类型的指针变量p,那么编译器会在内存中分配4个字节的空间,用来存储p指向的内存地址。如果你将p初始化为NULL,则p所指向的地址为0,即空指针。如果你将p指向一个int类型的变量,那么p所存储的值将是该变量的内存地址

(2)内容是一个内存地址,即指向某个内存位置的指针值

在这里插入图片描述
这个图很明显,int a=11;a的值被存储到内存中,这个变量的地址是0x20000001~0x20000004,定义一个指针变量int p;让p指向的就是a的首地址,那么这个指针p就不是一个空指针了,它的值被存储在另一块内存空间里,指针p的地址是0x30000001*。

(3)那这样岂不是可以无限套娃?指向指针变量的指针?指针的指针的指针?

是这样的。
指针是一个变量,它存储了一个内存地址。指针变量的类型决定了指针所指向的内存块的类型。例如,int类型的指针变量指向一个int类型的内存块,char类型的指针变量指向一个char类型的内存块。
指向指针变量的指针也称为二级指针。它是一个指针,它存储了一个指向指针变量的内存地址。二级指针的类型决定了它所指向的指针变量的类型。例如,int类型的二级指针指向一个int*类型的指针变量,char类型的二级指针指向一个char*类型的指针变量。

实际上,指向指针变量的指针可以无限嵌套,但在实际应用中,很少需要用到多级指针。
多级指针的主要应用场景是动态内存分配和指针数组。在动态内存分配中,我们通常使用malloc函数来分配一块内存,并将其地址存储在一个指针变量中。如果我们需要动态分配一个指针变量的数组,就需要使用多级指针来存储这个指针数组。

3.指针的C语言写法

(1)例一:使用指针来进行函数参数传递,实现两个变量的值交换

#include "stdio.h"
#include "stdlib.h"

void swap(int*a,int*b){
    
    //形参是两个指向int类型的指针
  int tmp=*a;//指针a指向局部int型变量tmp
  *a=*b;//将b指针所指向的变量的值赋值给a指针所指向的变量
  *b=tmp;//将tmp的值赋值给b指针所指向的变量
}

int main() {
    
    
    int x = 10, y = 20;
    printf("x = %d, y = %d\n", x, y);
    printf("x_addr=%d ,y_addr=%d\n",&x,&y);
    swap(&x, &y);//将x和y的地址作为参数传递给swap函数
    printf("x = %d, y = %d\n", x, y);
    printf("x_addr=%d ,y_addr=%d\n",&x,&y);
    return 0;
}

运行结果如下:
在这里插入图片描述
定义一个交换函数swap,它接受两个指向int类型变量的指针作为参数。函数内部定义了一个临时变量tmp,用来存储a指针所指向的变量的值。然后我们将b指针所指向的变量的值赋值给a指针所指向的变量,再将tmp的值赋值给b指针所指向的变量,从而实现了两个变量的值交换。

在main函数中,我们首先定义了两个int类型的量x和y,并打印出它们的值。然后我们调用swap函数,将x和y的地址作为参数传递给swap函数。**这里注意,&x、&y是取x和y变量的地址作为参数,就意味着a和b这两个指针的地址是x和y的地址,*a代表a指针指向的变量的值。如果&a,代表指向指针变量a的指针的地址。**函数执行完毕后,x和y的值发生了交换,我们再次打印出它们的值。

通俗地说:

int a=10;
int *p;
p=&a;//指针p指向了a

*p=20;//等效于使a=20

printf("%p\n", &p);   // 输出指针变量p的地址

(2)例二:利用指针实现字符串反转

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void reverse(char* str) {
    
    
    int len = strlen(str);
    char* p = str;//让指针p指向str字符串
    char tmp;
    for (int i = 0; i < len / 2; i++) {
    
    
        tmp = *(p + i);//开头第i个字符
        *(p + i) = *(p + len - i - 1);//把倒数第i+1个字符赋给开头第i个
        *(p + len - i - 1) = tmp;//把开头第i个字符赋给倒数第i+1个字符
    }
}

int main() {
    
    
    char str[] = "Hello, world!";//定义一个字符串
    printf("Before reverse: %s\n", str);
    reverse(str);
    printf("After reverse: %s\n", str);
    return 0;
}

在这里插入图片描述
这里我主要说明这个东西:*(p+1)

(p+1)表示指针p所指向的内存地址加上1,然后取得该地址中存储的值(即指针所指向的数据的值)。这里的是指针解引用运算符,用于访问指针所指向的数据。

例如,如果p是一个指向整型变量的指针,那么*(p+1)就表示指针p所指向的整型变量的下一个地址中存储的整数值。如果p指向的是一个字符数组,那么*(p+1)就表示指针p所指向的字符数组中的下一个字符。

需要注意的是,指针运算符+的优先级高于*,因此在进行指针运算时,需要使用圆括号来明确运算顺序。例如,(p+1)等价于(p+sizeof(int)),表示指针p向后移动一个整型变量的长度(通常是4个字节),然后取得新地址中存储的整数值。

(3)例三:利用指针实现动态数组

#include <stdio.h>
#include <stdlib.h>

int* create_array(int size) {
    
    
    int* arr = (int*)malloc(size * sizeof(int));
    for (int i = 0; i < size; i++) {
    
    
        *(arr + i) = i + 1;
    }
    return arr;
}

int main() {
    
    
    int size = 5;
    int* arr = create_array(size);
    for (int i = 0; i < size; i++) {
    
    
        printf("%d ", *(arr + i));
    }
    free(arr);
    return 0;
}

难点:
int* arr = (int*)malloc(size * sizeof(int));
这段代码使用了动态内存分配函数malloc来分配一段大小为size个整型变量大小的内存空间(堆),并将该内存空间的首地址赋值给指针变量arr。
具体地,sizeof(int)表示整型变量的大小,size * sizeof(int)表示要分配的内存空间的总大小(以字节为单位)。malloc函数返回一个指向分配内存空间首地址的指针,需要进行类型转换(将void类型转换为int类型)后才能将其赋值给指针变量arr。
结果:
arr指针指向的5个整形的数组的值为:1 2 3 4 5

4.指针在C语言中有什么好处呢?

指针是C语言中的一个重要特性,具有以下几个好处:

动态内存分配:指针可以通过动态内存分配函数(如malloc)来动态地分配内存空间,从而满足程序在运行时根据需要分配内存空间的需求。

函数参数传递:指针可以作为函数参数传递,使得函数可以直接修改指针所指向的变量的值,从而实现在函数内部修改实参的值的目的。

数组操作:指针可以用于数组操作,可以通过指针访问数组中的元素,实现对数组的遍历、排序等操作。

数据结构:指针可以用于实现数据结构,如链表、树等,可以灵活地管理数据,实现对数据的高效访问和操作。

函数指针:指针可以指向函数,实现函数的动态调用和回调,从而实现程序的灵活性和可扩展性。

二、指针的类型

1.void指针

void指针:void *类型的指针可以指向任何类型的数据,也就是通用指针。由于void指针没有具体的数据类型,因此在使用时需要进行类型转换,例如将void指针转换为int指针才能访问和操作int类型的数据。

2.整型指针

int *类型的指针可以指向int类型的数据,通过指针可以访问和修改int类型的数据。

3.字符型指针

char *类型的指针可以指向char类型的数据,通过指针可以访问和修改char类型的数据,也可以实现字符串的操作。

4.浮点型指针

float *类型的指针可以指向float类型的数据,通过指针可以访问和修改float类型的数据。

5.双精度型指针

double *类型的指针可以指向double类型的数据,通过指针可以访问和修改double类型的数据。

6.结构体指针

struct *类型的指针可以指向结构体类型的数据,通过指针可以访问和修改结构体类型的数据。结构体指针在嵌入式开发中十分重要,等下一节写结构体时,我会详细说明结构体指针的作用

7.函数指针

是一种特殊的指针,它指向函数的入口地址,可以通过函数指针来调用函数。

(1)函数的入口地址是什么

函数的入口地址是指函数在内存中的起始地址,也就是函数代码的第一条指令的地址。当程序调用一个函数时,实际上是通过函数的入口地址来执行该函数的代码。

在程序中可以通过取函数名的地址来获取函数的入口地址,例如:

#include <stdio.h>

int add(int a, int b) {
    
    
    return a + b;
}

int main() {
    
    
    int (*p_func)(int, int);  // 声明一个指向函数的指针变量 p_func
    p_func = add;             // 将函数 add 的地址赋给指针变量 p_func
    printf("add function address is %p\n", add);   // 打印函数 add 的地址
    printf("add function address is %p\n", p_func); // 打印指针变量 p_func 存储的地址,即函数 add 的地址
    return 0;
}

在这里插入图片描述
在上述示例中,使用%p格式化字符来打印函数的地址,可以通过取函数名的地址或者将函数的地址赋给函数指针来获取函数的入口地址。

(2)函数指针有什么用?

函数指针是一种特殊的指针类型,它指向函数的入口地址,可以通过函数指针来调用函数。函数指针的使用可以带来以下几个好处:

动态调用函数:函数指针可以在运行时动态地指向不同的函数,从而实现动态调用的功能。这在某些需要根据不同的情况来调用不同的函数的场景下非常有用。

回调函数:回调函数是一种常用的编程技巧,在回调函数中,程序会将一个函数指针作为参数传递给另外一个函数,使得该函数在执行时可以调用传入的函数指针。这种技巧经常用于事件处理、消息传递、接口实现等场景中。

函数指针数组:函数指针可以存储在数组中,从而实现一组函数的动态调用。这在某些需要根据不同的输入来执行不同的操作的场景下非常有用。

实现函数式编程:函数指针可以使用函数作为参数或返回值,使得函数可以像其他类型的变量一样被传递和使用。这种技巧可以用于实现函数式编程,从而让程序更加简洁和灵活。

三、指针中一些复杂的概念

1.指针数组和数组指针

指针数组是一个数组,数组的每个元素都是指针类型。例如,int *p[10]就是一个指针数组,它包含了10个指向int类型变量的指针。使用指针数组可以方便地对多个指针进行处理,如动态分配内存、传递指针参数等。

数组指针是一个指针,指向一个数组。例如,int (*p)[10]就是一个数组指针,它指向一个包含10个int类型变量的数组。使用数组指针可以方便地对整个数组进行处理,如排序、遍历等操作。

2.错误使用指针会造成的问题

下面是一些常见的错误使用指针会造成的问题:

空指针引用:如果指针没有被初始化或者指向的地址已经被释放,就会出现空指针引用的问题。这会导致程序崩溃。

悬垂指针:如果指针指向的地址已经被释放,但是指针没有被置为NULL或重新分配,就会出现悬垂指针的问题。这会导致程序崩溃或者未定义的行为。

内存泄漏:如果动态分配内存后,没有释放内存,就会出现内存泄漏的问题。这会导致程序占用的内存越来越多,最终导致系统崩溃。

数组越界:如果使用指针访问数组时,没有正确计算数组的长度或者越界访问数组,就会出现数组越界的问题。这会导致程序崩溃或者未定义的行为。

指针类型不匹配:如果使用不匹配的指针类型进行指针运算或者赋值,就会出现指针类型不匹配的问题。这会导致程序崩溃或者未定义的行为。

四、指针对于嵌入式开发有什么作用?

以下是指针在嵌入式开发中的一些常见用途:

访问外设寄存器:在嵌入式开发中,通常需要直接访问外设寄存器,以控制硬件设备。指针可以用来指向这些寄存器的地址,从而实现对这些寄存器的读写操作。

动态内存分配:在嵌入式开发中,通常需要动态地分配内存,以存储数据或者缓存区等。指针可以用来指向动态分配的内存地址,以便进行读写操作。

使用指针数组:在嵌入式开发中,通常需要使用指针数组来管理多个对象或者多个数据结构。指针数组可以用来存储一组指针,每个指针指向一个对象或者一个数据结构。

使用函数指针:在嵌入式开发中,通常需要使用函数指针来实现事件处理、中断处理等功能。函数指针可以用来指向事件处理函数或者中断处理函数的地址,从而实现对这些函数的调用。
具体的应用,下一节我结合结构体来进行说明。

猜你喜欢

转载自blog.csdn.net/qq_53092944/article/details/131156647