当前位置: 首页 > article >正文

C语言:深入了解指针2(超详细)

1. 指针跟数组相关的知识

在 C 语言里,数组名在大多数表达式中会隐式转换为指向数组首元素的指针。不过,数组名和指针并不完全等同。

1.数组就是数组,是一块连续的空间,是可以存放一个或者多个数组的。
2.指针变量是一个变量,是可以存放地址的变量。
   数组和指针不是一回事。
   但是可以是一种指针来访问数组。

#include <stdio.h>

int main() 
{
    int arr[5] = {1, 2, 3, 4, 5};
    // 数组名arr隐式转换为指向数组首元素的指针
    int *p = arr;//数组名字就是数组的地址

    printf("arr[0]的值: %d\n", arr[0]);
    printf("通过指针访问arr[0]的值: %d\n", *p);

    return 0;
}

在上述代码中,arr 是一个整型数组,ptr 是一个整型指针。arr 在赋值给 p 时隐式转换为指向 arr[0] 的指针,因此可以通过 *p 访问 arr[0] 的值。 

                                           其实数组名就是数组首元素的地址(但有列外)

  有2个列外: 

1. sizeof(数组名):这里数组名并不表示首元素的地址,其实这里数组名表示整个数组,计算的是则是整个数组大小单位是字节

 2. &数组名: 这里的数组名也表示整个数组,取出的是整个数组地址。 

                                          除此之外,所有的数组名都是首元素的地址

                                   

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

                           

              虽然说 &arr 取出来的跟&arr[0]arr取出来的一样,但随着加1就可以看出,&arr取出的是整个数组的地址

2. 使⽤指针访问数组

   指针运算

1.数组在内存中是连续存放的。
2.指针的元素很方便的可以遍历数组,取出数组的内容

我们这里用代码展示:

int main()
{
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = arr;  //arr = &arr[0]
	int i = 0;
	//输入
	for (i = 0; i < sz; i++)
	{
		scanf("%d", p+i); //    p + i = arr + i = &arr[i]   这里都表示下标元素的地址
		//scanf("%d", arr + i);
	}
	//输出
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(p+i)); // *(p+i) = arr[i] =*(arr +i)这些都表示对相应的元素取值
		//printf("%d ", *(arr+ i));
	}

	return 0;

}

这里我们来详细讲解一下:p + i = arr + i = &arr [ i ]  这里都表示下标元素的地址,是完全等价

详细解释

1. arr + i 与 &arr[i] 相等
  • 数组名的本质:在 C 语言中,数组名在大多数表达式中会隐式转换为指向数组首元素的指针。也就是说,arr 本身代表了数组首元素 arr[0] 的地址。
  • 指针算术运算:当对指针进行加法运算时,如 arr + i,它会根据指针所指向的数据类型的大小进行偏移。由于 arr 是 int 类型的数组,arr + i 会将指针从数组首元素地址向后偏移 i * sizeof(int) 个字节,从而指向数组中第 i 个元素的地址。
  • &arr[i] 的含义arr[i] 是对数组中第 i 个元素的引用,& 是取地址运算符,所以 &arr[i] 表示获取数组中第 i 个元素的地址。
  • 等价性:由于 arr + i 和 &arr[i] 都表示数组中第 i 个元素的地址,因此它们是相等的。
2. p + i 与 arr + i 相等
  • 指针 p 的初始化:在前面的代码中,p 被初始化为 p = arr,这意味着 p 存储的是数组 arr 首元素的地址,即 p 和 arr 指向同一个位置。
  • 指针算术运算的一致性:当对 p 进行 p + i 的运算时,和 arr + i 一样,它也会根据 p 所指向的数据类型(这里是 int 类型)进行偏移,向后偏移 i * sizeof(int) 个字节,从而指向数组中第 i 个元素的地址。
  • 等价性:因为 p 和 arr 初始时指向同一个地址,且它们进行相同的指针算术运算(加上 i),所以 p + i 和 arr + i 也指向同一个地址,即它们是相等的。

我们结合上面的再详细讲解一下:*(p+i) = arr[ i ] =*(arr + i )这些都表示对相应的元素取值,都是等价

详细解释

1. arr + i 与指针算术运算
  • 在 C 语言中,数组名 arr 在大多数表达式里会隐式转换为指向数组首元素的指针,也就是说 arr 代表了 arr[0] 的地址。
  • 当进行 arr + i 这样的操作时,这属于指针算术运算。因为 arr 指向的是 int 类型的数据,所以 arr + i 会使指针从 arr 所指向的位置向后偏移 i * sizeof(int) 个字节,最终得到数组中第 i 个元素的地址。
2. *(arr + i):对地址解引用
  • * 是解引用运算符,它的作用是获取指针所指向地址处存储的值。
  • *(arr + i) 就是先通过 arr + i 得到数组中第 i 个元素的地址,然后对这个地址进行解引用操作,从而获取该地址处存储的元素值,也就是数组中第 i 个元素的值。
3. arr[i] 与 *(arr + i) 的等价性
  • 在 C 语言的语法设计中,arr[i] 实际上是 *(arr + i) 的一种语法糖。这是为了方便程序员更直观地访问数组元素而设计的。所以从本质上来说,arr[i] 和 *(arr + i) 是完全等价的,它们都表示数组 arr 中第 i 个元素的值。
4. p + i 与 arr + i 的关系
  • 由于 p 被初始化为指向数组 arr 的首元素,所以 p 和 arr 指向的是同一个地址。
  • 那么进行 p + i 操作时,和 arr + i 一样,也是将指针从当前位置向后偏移 i * sizeof(int) 个字节,最终得到数组中第 i 个元素的地址。
5. *(p + i):对 p + i 解引用
  • 同样,*(p + i) 是对 p + i 这个地址进行解引用操作,获取该地址处存储的值,也就是数组中第 i 个元素的值。所以 *(p + i) 与 *(arr + i) 以及 arr[i] 是等价的。

重点:

其实总结上面的我们可以理解:arr[ i ] == *( arr + i )  ,为什么arr恒等于&arr[0],其实&arr[0]可以写成&*(arr + 0),在这里说一下&和*这二个只要相遇就可以可以抵消(本质上是因为它们分别是取地址和解引用这一对相反操作,也就是说:

                                             &arr[ 0 ] = &*(arr + 0) = arr + 0 = arr

他们也是支持加法结合律如:arr[ 1 ] = *( arr + 1 )= 1[ arr ] =*( 1 + arr )

3. ⼀维数组传参的本质

1.在函数调用时,一维数组作为参数传递给函数有以下几种常见形式,但本质都是相同的。

形式一:以数组形式声明参数
#include <stdio.h>

// 函数以数组形式声明参数
void printArray1(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int arr[] = {1, 2, 3, 4, 5};
    int size = sizeof(arr) / sizeof(arr[0]);//计算元素个数
    printArray1(arr, size);//传递 数组地址 和 求的数组数量
    return 0;
}

在这个例子中,printArray1 函数的参数 arr 看起来是一个数组,但实际上在函数内部它会被当作指针来处理。

形式二:以指针形式声明参数
#include <stdio.h>

// 函数以指针形式声明参数
void printArray2(int *arr, int size) 
{
    for (int i = 0; i < size; i++) 
    {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() 
{
    int arr[] = {1, 2, 3, 4, 5};
    int size = sizeof(arr) / sizeof(arr[0]);
    printArray2(arr, size);
    return 0;
}

这里 printArray2 函数的参数 arr 直接声明为指针,它接收的也是数组首元素的地址。

2. 传参本质是传递数组首元素地址

  • 数组名的隐式转换:在 C 语言里,数组名在大多数表达式中会隐式转换为指向数组首元素的指针。当把数组作为参数传递给函数时,传递的不是整个数组,而是数组首元素的地址。例如在上面的代码中,arr 作为参数传递给 printArray1 或 printArray2 函数时,实际传递的是 &arr[0]
  • 函数内部处理:在函数内部,无论是以 int arr[] 还是 int *arr 形式声明参数,编译器都会将其当作指针来处理。因此,在函数中对 arr 进行的操作实际上是对指针进行的操作。例如 arr[i] 等价于 *(arr + i),都是通过指针偏移来访问数组元素。

3. 不能在函数内部获取数组长度

由于传递的只是数组首元素的地址,函数并不知道数组的长度。所以在函数参数中通常需要额外传递一个表示数组长度的参数,像上面例子中的 size。例如以下错误示例:

#include <stdio.h>

// 错误示例:无法在函数内部正确获取数组长度
void wrongPrintArray(int arr[]) 
//  这里int arr[] == int* arr, []里面其实放不放都没有影响,放了编译器也不会读,放多少都是一样
{
    int size = sizeof(arr) / sizeof(arr[0]);
//        1        4       /      4
    
   
        printf("%d", size);//这里我们打印出来就是1
//        得出的结果跟环境是有很大的关系的,
//        这里演示的是vs2022 x86 环境下运行的
    
}

int main() 
{
    int arr[] = {1, 2, 3, 4, 5};
    wrongPrintArray(arr);
    return 0;
}
详细讲解:
1. 数组作为参数传递时的本质

在 C 语言中,当数组作为参数传递给函数时,数组会 “退化” 为指向其首元素的指针。也就是说,在wrongPrintArray函数中,int arr[]实际上等同于int *arr

2. sizeof运算符在函数内部的作用
  • sizeof(arr[0])arr[0]是一个int类型的元素,在常见的系统中,int类型通常占用 4 个字节,所以sizeof(arr[0])的值为 4。
  • sizeof(arr)由于arr已经退化为指针,sizeof(arr)计算的是指针的大小。在 32 位系统中,指针大小为 4 个字节;在 64 位系统中,指针大小为 8 个字节。这里假设你使用的是 64 位系统,所以sizeof(arr)的值为 8。
  • int size = sizeof(arr) / sizeof(arr[0]);根据前面的分析,sizeof(arr)为 8,sizeof(arr[0])为 4,所以size的值为8 / 4 = 2。这与预期的数组长度 5 不符,因为arr已经退化为指针,无法通过sizeof运算符正确获取数组的长度。

4. ⼆级指针

定义和基本概念

指针是一个变量,它存储的是另一个变量的内存地址。而二级指针则是存储指针变量的内存地址,其他的三级指针,四级指针......等,都是一样的。

  • 普通指针的定义形式:类型 *指针变量名;
  • 二级指针的定义形式:类型 **二级指针变量名;

int a = 10;

int *pa = &a; // 一级指针,指向int类型变量a

int **ppa  = &pa; // 二级指针,指向一级指针pa

 

在上述代码中,pa是一个一级指针,它存储了a的地址;ppa是一个二级指针,它存储了pa的地址。

使用场景

  • 动态二维数组:在动态分配二维数组时,通常会使用二级指针。
  • 函数参数传递:当需要在函数内部修改一级指针的值时,需要使用二级指针作为函数参数。

示例代码

1. 访问变量的值
#include <stdio.h>

int main() {
    int num = 10;
    int *pa = &a;
    int **ppa = &pa;//存储的是一级指针的地址

    printf("a的值: %d\n", a);
    printf("通过一级指针访问a的值: %d\n", *pa);
    printf("通过二级指针访问a的值: %d\n", **ppa);

    return 0;
}

 

代码解释

  • *pa 是通过一级指针访问 a 的值。
  • **ppa 是先通过二级指针 ppa 找到一级指针 pa,再通过 pa 找到 a 的值。
2. 动态分配二维数组(这里看不懂可以跳过,等学习了动态内存分配再来看哦)
#include <stdio.h>
#include <stdlib.h>

int main() 
{
    int rows = 3;
    int cols = 4;

    // 动态分配二维数组
    int **matrix = (int **)malloc(rows * sizeof(int *));
    for (int i = 0; i < rows; i++) 
    {
        matrix[i] = (int *)malloc(cols * sizeof(int));//申请动态内存
    }

    // 初始化二维数组
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) 
        {
            matrix[i][j] = i * cols + j;
        }
    }

    // 打印二维数组
    for (int i = 0; i < rows; i++)  
    {
        for (int j = 0; j < cols; j++) 
        {
            printf("%d ", matrix[i][j]);
        }
        printf("\n");
    }

    // 释放内存
    for (int i = 0; i < rows; i++)  
    {
        free(matrix[i]);
    }
    free(matrix);//释放内存

    return 0;
}

代码解释

  • 首先使用 malloc 函数为二级指针 matrix 分配 rows 个 int * 类型的内存空间。
  • 然后为每一行分配 cols 个 int 类型的内存空间。
  • 初始化并打印二维数组后,需要释放分配的内存,避免内存泄漏。
3. 函数参数传递(动态内存申请看不懂可以先跳过,主要目的让你们知道有这些就已经足够了)
#include <stdio.h>
#include <stdlib.h>

// 函数用于动态分配内存并修改一级指针的值
void allocateMemory(int **ptr) //必须得用二维数组接收
{
    *ptr = (int *)malloc(sizeof(int));//申请动态内存
    **ptr = 100;
}

int main() 
{
    int *ptr = NULL;
    allocateMemory(&ptr);//一级指针传地址

    if (ptr != NULL) 
    {
        printf("分配的内存中的值: %d\n", *ptr);
        free(ptr);//释放内存
    }

    return 0;
}

代码解释

  • allocateMemory 函数接受一个二级指针 ptr 作为参数,在函数内部为一级指针分配内存并赋值。
  • 在 main 函数中,将一级指针 ptr 的地址传递给 allocateMemory 函数,从而在函数内部修改了 ptr 的值。

注意事项

  • 内存管理:使用二级指针进行动态内存分配时,要确保在使用完后及时释放内存,避免内存泄漏。
  • 空指针检查:在使用指针之前,要确保指针不为空,避免空指针引用错误。

5. 指针数组

 指针数组
 是指针? 还是数组?
 
指针数组 - 存放指针的数组,数组的每个元素其实是指针类型
char* arr[10];存放字符指针的数组
int* arr[5];存放整型指针的数组

                                   char arr[ 5 ]; 字符数组 - 存放字符的数组
                                     int arr[ 5 ];  整型数组 - 存放
整型的数组

                                   int* arr[5];存放整型指针的数组 

 

使用场景

  • 处理多个字符串:在 C 语言中,字符串通常用字符指针表示,指针数组可以方便地存储多个字符串。
  • 动态内存管理:当需要管理多个动态分配的内存块时,可以使用指针数组来存储这些内存块的地址。
初始化

指针数组可以在定义时进行初始化,也可以在后续的代码中逐个赋值。

定义时初始化
int main()
{
    int a = 10;
    int b = 20;
    int c = 30;

//如果我们按照以下操作非常繁琐麻烦
    /*   int* pa = &a;
         int* pb = &b;
         int* pc = &c;     */

 int* arr[] = { &a ,&b,&c };//所以我们可以创建一个指针数组(必须类型都是一样的才可以存放在一起)
      int i = 0;
      for (i = 0; i < 3; i++)
    {
        printf("%d\n", *(arr[i]));         //  *(*arr+i)
    }
    return 0;
}

也可以后续逐个赋值 
#include <stdio.h>

int main() 
{
    int a = 10, b = 20, c = 30;
    int *arr[3];

    arr[0] = &a;
    arr[1] = &b;
    arr[2] = &c;

    for (int i = 0; i < 3; i++) 
   {
        printf("%d ", *arr[i]);
    }
    printf("\n");

    return 0;
}
1. 处理多个字符串
#include <stdio.h>

int main() 
{
    // 定义一个指针数组来存储多个字符串
    char *arr[] = {"Hello","World","C Programming"};

    int size = sizeof(arr) / sizeof(arr[0]);//

    for (int i = 0; i < size; i++) 
    {
        printf("%s\n", arr[i]);
    }

    return 0;
}

代码解释

  • char *arr[] 定义了一个指针数组,每个元素都是一个指向字符的指针,也就是一个字符串。
  • 通过 sizeof(arr) / sizeof(arr[0]) 计算数组的大小。
  • 使用 for 循环遍历指针数组,并打印每个字符串。
2. 动态内存管理(这里看不懂可以跳过,等学习了动态内存分配再来看哦)
#include <stdio.h>
#include <stdlib.h>

int main() 
{
    int *arr[3];

    // 为每个指针分配内存
    for (int i = 0; i < 3; i++) 
    {
        arr[i] = (int *)malloc(sizeof(int));//申请动态内存

        if (arr[i] == NULL) //判断
        {
            printf("内存分配失败\n");
            return 1;
        }
        *arr[i] = i * 10;
    }

    // 打印每个指针指向的值
    for (int i = 0; i < 3; i++) 
    {
        printf("%d ", *arr[i]);
    }
    printf("\n");

    // 释放内存
    for (int i = 0; i < 3; i++) 
    {
        free(arr[i]);
    }

    return 0;
}

代码解释

  • 定义了一个包含 3 个元素的指针数组 arr
  • 使用 malloc 函数为每个指针分配内存,并赋值。
  • 打印每个指针指向的值。
  • 使用 free 函数释放分配的内存,避免内存泄漏。

注意事项

  • 内存管理:如果指针数组中的指针指向动态分配的内存,在使用完后要及时释放内存,避免内存泄漏。
  • 空指针检查:在使用指针之前,要确保指针不为空,避免空指针引用错误。

6. 指针数组模拟⼆维数组

 

#include <stdio.h>
int main()
{
 int arr1[] = {1,2,3,4,5};
 int arr2[] = {2,3,4,5,6};
 int arr3[] = {3,4,5,6,7};
 //数组名是数组⾸元素的地址,类型是int*的,就可以存放在arr数组中
 int* arr[3] = {arr1, arr2, arr3};
//我们可以把
 int i = 0;
 int j = 0;
 for(i=0; i<3; i++)
 {
 for(j=0; j<5; j++)
 {
 printf("%d ", arr[i][j]);      // *(arr[i]+j) == *(*(arr+i)+j)
//我们可以把*(arr[0]+0)当作是arr1,*(arr[1]+0)当作是arr2,*(arr[2]+0)当作是arr3
 }
 printf("\n");
 }
return 0;
} 

总结

通过使用指针数组 arr 存储多个一维数组的首地址,代码实现了对多个一维数组元素的统一管理和遍历输出,模拟了二维数组的访问方式。这种方式在处理多个相关的一维数组时非常方便,但需要注意的是,这里的指针数组所模拟的二维数组在内存中并不是连续存储的,与真正的二维数组有所区别。

7. 字符指针变量

定义和初始化

定义

字符指针变量用于存储字符或字符串的地址。其定义形式为:

char  *指针变量名;

 例如:

char *arr;

这里定义了一个名为arr的字符指针变量。 

初始化
  • 指向单个字符

char ch = 'A';

char *arr = &ch;

在这个例子中,arr指针变量存储了字符ch的地址。 

  • 指向字符串常量

char *arr = "Hello";

这里arr指向了字符串常量"Hello"的首字符地址。需要注意的是,字符串常量存储在只读内存区域不能通过该指针修改字符串的内容。 

使用场景

1. 字符串处理

字符指针常用于处理字符串,例如字符串的复制、比较、查找等操作。

#include <stdio.h>

int main() 
{
    char *arr = "Hello, World!";
    printf("%s\n", arr);  // 输出字符串

    // 遍历字符串
    while (*arr != '\0') 
    {
        printf("%c ", *arr);
        arr++;
    }
    printf("\n");

    return 0;
}

在上述代码中,arr指针指向字符串常量"Hello, World!",可以直接使用printf函数输出该字符串。同时,通过while循环遍历字符串的每个字符,每次访问完一个字符后,指针arr向后移动一位。

2. 函数参数传递

在函数调用时,可以使用字符指针作为参数传递字符串,避免了整个字符串的复制,提高了效率。

#include <stdio.h>

// 函数用于计算字符串的长度
int stringLength(char *arr) 
{
    int len = 0;
    while (*arr != '\0') 
   {
        len++;
        arr++;
    }
    return len;
}

int main() 
{
    char *arr = "Hello";
    int length = stringLength(arr);
    printf("字符串的长度是: %d\n", length);
    return 0;
}

 在这个例子中,stringLength函数接受一个字符指针作为参数,通过遍历指针所指向的字符串,计算字符串的长度。

注意事项

1. 字符串常量的只读性

当字符指针指向字符串常量时,不能修改该字符串的内容。例如:

char *arr = "Hello";

// str[0] = 'J';  // 错误,不能修改字符串常量

如果需要修改字符串的内容,应该使用字符数组来存储字符串。 

2. 空指针检查

在使用字符指针之前,应该确保指针不为空,避免空指针引用错误。例如:

char *str = NULL;

if (str != NULL) {

// 进行操作

}

3. 内存管理

如果字符指针指向动态分配的内存,使用完后要及时释放内存,避免内存泄漏。例如:

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

int main() 
{
    char *arr = (char *)malloc(10 * sizeof(char));//申请动态内存
    if (arr != NULL) 
   {
        strcpy(arr, "Hello");
        printf("%s\n", arr);
        free(arr);  // 释放内存
    }
    return 0;
}

在这个例子中,使用malloc函数动态分配了一块内存,使用完后使用free函数释放该内存。

这里有一个例子:
int main()
{
	char str1[] = "hello World";
	char str2[] = "hello World";
	const char* str3 = "hello World";
	const char* str4 = "hello World";
	                          
	if (str1 == str2)
		printf("str1 and str2 are same\n");//1
	else
		printf("str1 and str2 are not same\n");//2

	if (str3 == str4)
		printf("str3 and str4 are same\n");//3
	else
		printf("str3 and str4 are not same\n");//4

	return 0;
}

大家觉得会打印 1 2 3 4 那几个呐?

这里我们来分析一下把

1. str1 和 str2 的比较

char str1[] = "hello World";

char str2[] = "hello World";

  • 内存分配str1 和 str2 都是字符数组。当你使用字符数组来存储字符串时,编译器会为每个数组在栈上分配独立的内存空间,然后将字符串常量的内容复制到这些内存空间中。也就是说,str1 和 str2 是两个不同的数组,它们在内存中有各自独立的存储区域。
  • 比较过程:在 if(str1 == str2) 中,str1 和 str2 作为数组名,在这里会隐式转换为指向数组首元素的指针。由于 str1 和 str2 是不同的数组,它们的首元素地址是不同的,所以 str1 == str2 的比较结果为 false,因此输出 str1 and str2 are not same

2. str3 和 str4 的比较

const char *str3 = "hello World";

const char *str4 = "hello World";

  • 内存分配str3 和 str4 是指向字符串常量的指针。字符串常量 "hello World" 存储在内存的只读数据段(常量区)。为了节省内存,编译器通常会将相同的字符串常量合并,即只存储一份字符串常量,多个指向相同字符串常量的指针实际上指向的是内存中同一个地址。
  • 比较过程:在 if(str3 == str4) 中,str3 和 str4 是指针,它们存储的是字符串常量 "hello World" 的首字符地址。由于编译器对相同字符串常量进行了合并,str3 和 str4 指向的是同一个内存地址,所以 str3 == str4 的比较结果为 true,因此输出 str3 and str4 are same

总结

  • 字符数组在栈上有独立的内存空间,即使内容相同,它们的地址也不同。
  • 字符串常量存储在只读数据段,相同的字符串常量通常只存储一份,多个指向相同字符串常量的指针指向同一个地址。
  • 在 C 语言中,使用 == 比较指针时,比较的是指针存储的地址,而不是指针所指向的内容。如果需要比较字符串的内容,可以使用 strcmp 函数。

8. 数组指针变量

1. 数组指针变量的定义

数组指针变量的定义格式为:数据类型 (*指针变量名)[数组元素个数]; 。其中,数据类型 表示数组元素的类型,指针变量名 是自定义的变量名,数组元素个数 明确了该指针所指向数组的元素数量。

示例代码如下:

#include <stdio.h>

int main() {

// 定义一个指向包含 5 个整型元素的数组的指针

int ( *p )[ 5 ];

return 0;

}

在上述代码中,p 就是一个数组指针变量,它指向的数组包含 5 个 int 类型的元素。

 这里我们也可以类比一下


     字符指针 - char* - 指向字符的指针 - 字符指针变量中存放字符变量的地址
                             char ch = ‘w';
                             char* pc = &ch;
    整型指针 - int* - 指向整型的指针  - 整型指针变量中存放整型变量的地址
                              int a = 10;
                              int* p = &a;
          数组指针 - 指向数组的指针 - 数组指针变量中存放数组的地址

  int main()
{
    int  arr[ 10]  = { 1,2,3,4,5 };
    int ( *p )[10 ] = &arr;   //  p就是数组指针,p中存放的是数组的地址
    //   int( * )[ 10 ] = int( * )[ 10 ]

    //    arr           --   int*                  arr+1     跳过4个字节
    //    &arr[ 10 ] --   int*                  &arr[0]+1 跳过4个字节
    //     &arr        --   int( * )[ 10 ]      &arr+1    跳过40个字节

    int* ptr;
    return 0;
}

2. 数组指针变量的初始化

数组指针变量可以指向一个已存在的数组。示例如下:

#include <stdio.h>

int main() 
{
    int arr[5] = {1, 2, 3, 4, 5};
    // 定义数组指针变量并初始化为指向数组 arr
    int (*p)[5] = &arr;

    return 0;
}

 这里需要注意的是,要使用 &arr 来获取数组 arr 的地址并赋值给数组指针 p,而不是直接用 arr。因为 arr 通常会隐式转换为指向数组首元素的指针,而 &arr 才是整个数组的地址。

3. 使用数组指针变量访问数组元素

通过数组指针变量可以访问其所指向数组的元素。示例代码如下:

#include <stdio.h>

int main() 
{
    int arr[5] = {1, 2, 3, 4, 5};
    int (*p)[5] = &arr;

    // 访问数组元素
    for (int i = 0; i < 5; i++) 
    {
        // 先对数组指针解引用得到数组,再通过下标访问元素
        printf("%d ", (*p)[i]); 
    }
    printf("\n");

    return 0;
}

在上述代码中,(*p) 表示对数组指针 p 进行解引用操作,得到其所指向的数组,然后通过 [i] 来访问数组中的第 i 个元素。

4. 数组指针变量在二维数组中的应用

数组指针变量常用于处理二维数组。因为二维数组在内存中是按行存储的,每一行可以看作一个一维数组,数组指针可以方便地按行访问二维数组。示例代码如下:

#include <stdio.h>

int main() 
{
    int arr[3][4] = {
  
  {1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}};
    // 定义数组指针变量,指向包含 4 个整型元素的数组
    int (*p)[4] = arr; 

    for (int i = 0; i < 3; i++) 
   {
        for (int j = 0; j < 4; j++) 
        {
            // 通过数组指针访问二维数组元素
            printf("%d ", *(*(p + i) + j)); 
        }
        printf("\n");
    }

    return 0;
}

在这个例子中,p 是一个指向包含 4 个 int 类型元素的数组的指针。p + i 表示指向第 i 行的数组,*(p + i) 得到第 i 行数组的首地址,*(p + i) + j 指向第 i 行第 j 列元素的地址,*(*(p + i) + j) 则获取该元素的值。

5. 注意事项

  • 数组指针变量指向的数组元素个数必须与定义时指定的元素个数一致,否则可能会导致访问越界等问题。
  • 区分数组指针和指针数组。指针数组是一个数组,其元素是指针;而数组指针是一个指针,它指向一个数组。例如:

// 指针数组:包含 3 个整型指针的数组

int *ptrArr[3];

// 数组指针:指向包含 3 个整型元素的数组

int (*arrPtr)[3];

如果大家还是对于数组指针和指针数组不知道有什么区别可以看我的博客另外一篇:C语言:数组指针 和 指针数组 的对比-CSDN博客

9. ⼆维数组传参的本质 

1. 二维数组在内存中的存储

二维数组在内存中是按行优先顺序连续存储的。例如,对于一个 int arr[3][4] 的二维数组,它在内存中会依次存储第一行的 4 个元素,接着是第二行的 4 个元素,最后是第三行的 4 个元素,形成一个线性的存储序列。

2. 二维数组传参的常见形式及本质

形式一:指定列数的二维数组形式
#include <stdio.h>

// 函数以指定列数的二维数组形式接收参数
void printArray(int arr[][4], int rows) 
{
    for (int i = 0; i < rows; i++) 
    {
        for (int j = 0; j < 4; j++) 
       {
            printf("%d ", arr[i][j]);
        }
        printf("\n");
    }
}

int main() 
    {
    int arr[3][4] = {
  
  {1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}};
    printArray(arr, 3);
    return 0;
}
  • 本质:在这种形式中,虽然函数参数看起来是二维数组,但实际上 arr 会被当作指针来处理。这里 arr 是一个指向包含 4 个 int 类型元素的数组的指针,也就是数组指针。它接收的是二维数组首行的地址,即 &arr[0]。在函数内部,arr[i][j] 会被转化为 *(*(arr + i) + j) 这样的指针运算来访问元素。
形式二:数组指针形式
#include <stdio.h>

// 函数以数组指针形式接收参数
void printArray(int (*arr)[4], int rows) 
{
    for (int i = 0; i < rows; i++) 
   {
        for (int j = 0; j < 4; j++) 
        {
            printf("%d ", *(*(arr + i) + j));
        }
        printf("\n");
    }
}

int main() 
{
    int arr[3][4] = {
  
  {1, 2, 3, 4},{5, 6, 7, 8},{9, 10, 11, 12}};
    printArray(arr, 3);
    return 0;
}
  • 本质:这种形式与第一种本质相同,int (*arr)[4] 明确表明 arr 是一个数组指针,指向包含 4 个 int 类型元素的数组。在调用 printArray 函数时,传递的 arr 会隐式转换为二维数组首行的地址,即 &arr[0]。函数内部通过指针运算 *(arr + i) 得到第 i 行的首地址,再通过 *(arr + i) + j 得到第 i 行第 j 列元素的地址,最后通过 *(*(arr + i) + j) 访问该元素的值。

3. 传参时列数必须指定的原因

在二维数组传参时,必须指定列数。这是因为在进行指针运算时,需要知道每一行元素的个数,才能正确地定位到相应的元素。例如,arr + i 这个运算,编译器要根据列数来计算偏移量,偏移的字节数是 i * 列数 * sizeof(元素类型)。如果不指定列数,编译器就无法确定偏移量,从而无法正确访问元素。

4. 不能传递整个二维数组

和一维数组一样,在 C 语言中不能直接将整个二维数组传递给函数。传递的只是二维数组首行的地址,函数并不知道整个二维数组的行数和列数(除了指定的列数),所以通常需要额外传递行数作为参数。

综上所述,C 语言二维数组传参的本质是传递二维数组首行的地址,函数将其当作数组指针来处理,并且传参时必须指定列数以保证正确的指针运算。

10. 函数指针变量

函数指针变量的创建步骤与语法

步骤分析
  1. 明确函数类型:确定你要指向的函数的返回值类型和参数列表。
  2. 定义函数指针类型:根据函数的返回值类型和参数列表来定义函数指针的类型。
  3. 创建函数指针变量:使用定义好的函数指针类型来创建具体的函数指针变量。
语法形式

函数指针变量的一般定义语法如下:

返回值类型 (*指针变量名)(参数类型列表);

其中:

  • 返回值类型:与要指向的函数的返回值类型相同。
  • 指针变量名:自定义的函数指针变量名称。
  • 参数类型列表:与要指向的函数的参数类型及顺序一致。

                                                  函数知否有地址?

int Add(int x, int y)
{
    return x + y;
}
int main()
{
    //虽然arr和&arr是有区别
    // 但‘&函数名’和‘函数名’都是函数的地址,没有区别

    printf("%p\n", &Add);
    printf("%p\n", Add);
    return 0;
}

int Add(int x, int y)                       函数指针有几种写法?
{
    return x + y;
}
int main()
{

// int (*pf)(int, int) = Add;


    int (*pf)(int, int) = &Add;    //pf 函数指针变量  int (*pf)(int x, int y) = &Add;名字可以不写


//  int (*)(int, int) 去掉函数名就是函数指针类型


    int ret = (*pf)(4, 5);//int ret = pf(4, 5); *其实不写也是可以的,因为没有实际的用途
    printf("%d\n", ret);

    int ret2 = Add(4, 5);
    printf("%d", ret2);
    return 0;
}

 

示例代码及详细解释

示例 1:指向无参数、无返回值的函数
#include <stdio.h>

// 定义一个无参数、无返回值的函数
void hello() 
{
    printf("Hello, World!\n");
}

int main() 
{
    // 创建一个函数指针变量,指向返回值为void、无参数的函数
    void (*funcPtr)(); //

    // 让函数指针变量指向hello函数
    funcPtr = hello;

    // 通过函数指针调用函数
    funcPtr();

    return 0;
}

解释

  • void (*funcPtr)();:定义了一个函数指针变量 funcPtr,它可以指向返回值为 void 且无参数的函数。
  • funcPtr = hello;:将函数指针变量 funcPtr 指向 hello 函数。注意,函数名在这种情况下会隐式转换为函数的地址。
  • funcPtr();:通过函数指针变量 funcPtr 调用其所指向的函数,这里实际上调用的就是 hello 函数。
示例 2:指向有参数、有返回值的函数
#include <stdio.h>

// 定义一个有参数、有返回值的函数
int add(int a, int b) 
{
    return a + b;
}

int main() 
{
    // 创建一个函数指针变量,指向返回值为int、有两个int型参数的函数
    int (*addPtr)(int, int);

    // 让函数指针变量指向add函数
    addPtr = add;

    // 通过函数指针调用函数并接收返回值
    int result = addPtr(3, 5);

    printf("3 + 5 = %d\n", result);

    return 0;
}

解释

  • int (*addPtr)(int, int);:定义了一个函数指针变量 addPtr,它可以指向返回值为 int 且有两个 int 型参数的函数。
  • addPtr = add;:将函数指针变量 addPtr 指向 add 函数。
  • int result = addPtr(3, 5);:通过函数指针变量 addPtr 调用 add 函数,并将返回值存储在 result 变量中。

注意事项

  • 类型匹配:函数指针变量的返回值类型和参数列表必须与要指向的函数完全匹配,否则会导致编译错误。
  • 括号的使用:函数指针定义中的括号 (*指针变量名) 不能省略,否则会变成返回指针的函数的声明。例如,int *funcPtr(int, int); 声明的是一个返回 int 型指针的函数,而不是函数指针变量。

11. typedef 关键字

基本语法

typedef 的基本语法格式如下:

typedef 原数据类型 新类型名;

其中,原数据类型 可以是 C 语言中任何合法的数据类型,包括基本数据类型(如 intchar 等)、自定义数据类型(如结构体、联合体等)以及指针类型等;新类型名 是你为该数据类型定义的别名。

具体应用场景及示例

1. 为基本数据类型定义别名
#include <stdio.h>

// 为int类型定义别名Integer

typedef int Integer;

int main() 
{
    // 使用别名定义变量
    Integer num = 10;
    printf("num的值: %d\n", num);
    return 0;
}

在这个例子中,typedef int Integer; 为 int 类型定义了一个新的名称 Integer,之后就可以使用 Integer 来定义 int 类型的变量。

2. 为结构体类型定义别名
#include <stdio.h>

// 定义结构体并使用typedef为其定义别名
typedef struct 
{
    char name[20];
    int age;
} Person;

int main() 
{
    // 使用别名定义结构体变量
    Person p = {"John", 25};
    printf("姓名: %s, 年龄: %d\n", p.name, p.age);
    return 0;
}

这里 typedef struct {...} Person; 为一个匿名结构体定义了别名 Person,后续就可以直接使用 Person 来定义该结构体类型的变量,而不需要每次都写完整的 struct 定义。

3. 为指针类型定义别名
#include <stdio.h>

// 为int*类型定义别名IntPtr

typedef int* IntPtr;

int main() 
{
    int num = 10;
    // 使用别名定义指针变量
    IntPtr ptr = &num;
    printf("ptr指向的值: %d\n", *ptr);
    return 0;
}

typedef int* IntPtr; 为 int* 类型定义了别名 IntPtr,这样就可以用 IntPtr 来定义 int 类型的指针变量。

4. 为函数指针类型定义别名
#include <stdio.h>

// 定义一个函数
int add(int a, int b) 
{
    return a + b;
}

// 为函数指针类型定义别名
typedef int (*AddFunction)(int, int);

int main() 
{
    // 使用别名定义函数指针变量
    AddFunction func = add;
    int result = func(3, 5);
    printf("3 + 5 = %d\n", result);
    return 0;
}

typedef int (*AddFunction)(int, int); 为返回值为 int 且有两个 int 类型参数的函数指针类型定义了别名 AddFunction,之后就可以用 AddFunction 来定义函数指针变量。

注意事项

  • typedef 与 #define 的区别:虽然 #define 也可以用于定义别名,但 typedef 是由编译器处理的,它创建的是真正的类型别名,而 #define 是预处理器指令,只是简单的文本替换。例如:
    #define PINT int*
    typedef int* IntPtr;
    
    PINT a, b;  // 这里a是int*类型,b是int类型
    IntPtr c, d;  // 这里c和d都是int*类型

  • 作用域typedef 定义的别名的作用域遵循 C 语言的作用域规则。如果在函数内部定义,其作用域仅限于该函数;如果在文件全局作用域定义,则在整个文件中都可以使用

12. 函数指针数组

1. 函数指针数组的定义

函数指针数组的定义需要结合函数指针和数组的定义规则。其一般形式为:

返回值类型 (*数组名[数组大小])(参数类型列表);

其中:

  • 返回值类型:是数组中每个函数指针所指向函数的返回值类型。
  • 数组名:是自定义的数组名称。
  • 数组大小:指定数组中元素的个数。
  • 参数类型列表:是数组中每个函数指针所指向函数的参数类型及顺序。

2. 函数指针数组的初始化与使用示例

示例 1:简单的函数指针数组
#include <stdio.h>

// 定义几个简单的函数
int add(int a, int b) 
{
    return a + b;
}

int subtract(int a, int b) 
{
    return a - b;
}

int multiply(int a, int b) 
{
    return a * b;
}

int main() 
{
    // 定义一个函数指针数组,包含3个元素,每个元素指向返回值为int、有两个int型参数的函数
    int (*funcArray[3])(int, int) = {add, subtract, multiply};

    int num1 = 5, num2 = 3;

    // 通过函数指针数组调用不同的函数
    for (int i = 0; i < 3; i++) 
  {
        int result = funcArray[i](num1, num2);
        switch (i) 
       {
            case 0:
                printf("%d + %d = %d\n", num1, num2, result);
                break;
            case 1:
                printf("%d - %d = %d\n", num1, num2, result);
                break;
            case 2:
                printf("%d * %d = %d\n", num1, num2, result);
                break;
        }
    }

    return 0;
}

代码解释

  • int (*funcArray[3])(int, int) = {add, subtract, multiply};:定义了一个名为 funcArray 的函数指针数组,它包含 3 个元素,每个元素都是一个指向返回值为 int 且有两个 int 型参数的函数的指针。数组初始化时,分别将 addsubtract 和 multiply 函数的地址赋给数组元素。
  • funcArray[i](num1, num2):通过循环遍历函数指针数组,调用不同的函数并传入参数 num1 和 num2,将结果存储在 result 变量中,最后根据 i 的值输出相应的运算结果。
示例 2:使用 typedef 简化函数指针数组的定义
#include <stdio.h>

// 定义几个简单的函数
int add(int a, int b) 
{
    return a + b;
}

int subtract(int a, int b) 
{
    return a - b;
}

// 使用typedef为函数指针类型定义别名
typedef int (*Operation)(int, int);

int main() 
{
    // 定义一个函数指针数组,使用typedef定义的别名
    Operation funcArray[2] = {add, subtract};

    int num1 = 10, num2 = 5;

    // 通过函数指针数组调用不同的函数
    for (int i = 0; i < 2; i++) 
   {
        int result = funcArray[i](num1, num2);
        switch (i) 
       {
            case 0:
                printf("%d + %d = %d\n", num1, num2, result);
                break;
            case 1:
                printf("%d - %d = %d\n", num1, num2, result);
                break;
        }
    }

    return 0;
}

代码解释

  • typedef int (*Operation)(int, int);:使用 typedef 为返回值为 int 且有两个 int 型参数的函数指针类型定义了别名 Operation
  • Operation funcArray[2] = {add, subtract};:使用 Operation 别名定义了一个函数指针数组 funcArray,使代码更加简洁易读。

3. 函数指针数组的应用场景

函数指针数组常用于实现菜单系统、状态机等。例如,在一个简单的计算器程序中,可以使用函数指针数组根据用户输入的运算符选择不同的运算函数进行计算,避免使用大量的 if-else 或 switch 语句,提高代码的可扩展性。

13.转移表

在 C 语言中,转移表(Jump Table)是一种利用函数指针数组实现的技术,它允许程序根据某个变量的值来快速地跳转到不同的代码段执行,从而避免大量使用 if-else 或 switch 语句,提高代码的执行效率和可维护性。下面详细介绍转移表的原理、实现和应用场景。

原理

转移表的核心思想是使用一个函数指针数组,数组中的每个元素都是一个函数指针,分别指向不同的处理函数。程序通过一个索引值来访问数组中的特定元素,进而调用对应的处理函数。这样,根据不同的输入值,可以快速定位并执行相应的代码,而不需要进行逐个条件的判断。

实现步骤及示例代码

步骤分析
  1. 定义处理函数:编写一系列处理函数,这些函数具有相同的返回值类型和参数列表。
  2. 创建函数指针数组:定义一个函数指针数组,数组的每个元素指向一个处理函数。
  3. 使用索引调用函数:根据输入的索引值,从函数指针数组中选择对应的函数并调用。
#include <stdio.h>
int add(int a, int b)
{
 return a + b;
}
int sub(int a, int b)
{
 return a - b;
}
int mul(int a, int b)
{
 return a*b;
}
int div(int a, int b)
{
 return a / b;
}
int main()
{
    int x, y;
    int input = 1;
    int ret = 0;
    int(*p[5])(int x, int y) = { 0, add, sub, mul, div }; //转移表  
    //                           0    1    2    3   4
    do
    {
       printf("*************************\n");
       printf(" 1:add 2:sub \n");
       printf(" 3:mul 4:div \n");
       printf(" 0:exit \n");
       printf("*************************\n");
       printf( "请选择:" );
       scanf("%d", &input);
    if ((input <= 4 && input >= 1)) //输入数字的判断
    {
     printf( "输⼊操作数:" );
     scanf( "%d %d", &x, &y);
     ret = (*p[input])(x, y);//根据相应的数字调用相同的计算
     printf( "ret = %d\n", ret);
    }
     else if(input == 0)
     {
     printf("退出计算器\n");
     }
     else
     {
     printf( "输⼊有误\n" ); 
     }
      }while (input);
  return 0;
}
代码解释

转移表的优势

  • 显示菜单:使用 printf 函数显示操作菜单,提示用户选择运算操作或退出程序。
  • 获取用户输入:使用 scanf 函数读取用户输入的选项 input
  • 处理用户选择
    • 如果用户输入的 input 在 1 到 4 之间,表示用户选择了某种运算操作。程序会提示用户输入两个操作数 x 和 y,然后通过 (*p[input])(x, y) 调用对应的运算函数,将结果存储在 ret 中并输出。
    • 如果用户输入的 input 为 0,表示用户选择退出程序,程序会输出相应提示信息。
    • 如果用户输入的 input 不在有效范围内,程序会输出 “输入有误” 的提示信息。
  • 循环条件do - while 循环的条件是 input,只要 input 不为 0,循环就会继续执行,直到用户选择退出。
  • 代码简洁:避免了使用大量的 if - else 或 switch 语句来进行函数选择,使代码结构更加清晰。
  • 易于扩展:如果需要添加新的运算操作,只需要定义新的运算函数,并将其函数地址添加到转移表中即可,不需要修改大量的条件判断代码。

应用场景

  • 菜单系统:在具有多个选项的菜单系统中,使用转移表可以根据用户的选择快速调用相应的处理函数,避免使用大量的 if-else 或 switch 语句,使代码更加简洁和易于维护。
  • 状态机:在状态机的实现中,根据当前状态和输入事件,可以使用转移表快速跳转到下一个状态的处理函数,提高状态转换的效率。

注意事项

  • 边界检查:在使用转移表时,必须对输入的索引值进行边界检查,确保其在函数指针数组的有效范围内,否则可能会导致未定义行为。
  • 函数签名一致性:转移表中的所有函数必须具有相同的返回值类型和参数列表,以确保函数指针数组的正确性。


http://www.kler.cn/a/532968.html

相关文章:

  • 生成式AI安全最佳实践 - 抵御OWASP Top 10攻击 (上)
  • java-(Oracle)-Oracle,plsqldev,Sql语法,Oracle函数
  • 【C++】继承(下)
  • Med-R2:基于循证医学的检索推理框架:提升大语言模型医疗问答能力的新方法
  • Vue3的el-table-column下拉输入实时查询API数据选择的实现方法
  • AI智慧社区--Excel表的导入导出
  • LLMs瞬间获得视觉与听觉感知,无需专门训练:Meta的创新——在图像、音频和视频任务上实现最优性能。
  • 基于 Java 开发的 MongoDB 企业级应用全解析
  • ZOMI - AISystem AI Infra 分享
  • 【Rust自学】20.1. 最后的项目:单线程Web服务器
  • 基于python热门歌曲采集分析系统
  • 【力扣】53.最大子数组和
  • open-webui启动报错:OSError: [WinError 1314] 客户端没有所需的特权。
  • AI Block Blast Solver:提升游戏体验的智能助手
  • Innodb为何能干掉MyISAM
  • 编程AI深度实战:大模型哪个好? Mistral vs Qwen vs Deepseek vs Llama
  • Leetcode - 周赛434
  • 《深度洞察ICA:人工智能信号处理降维的独特利器》
  • DeepSeek-R1:通过强化学习提升大型语言模型推理能力的探索
  • 猫眼前端开发面试题及参考答案
  • Redis真的是单线程的吗?
  • Spring Bean 的生命周期介绍
  • SQL注入漏洞之绕过[前端 服务端 waf]限制 以及 防御手法 一篇文章给你搞定
  • 从Transformer到世界模型:AGI核心架构演进
  • 51单片机 06 定时器
  • Effective Objective-C 2.0 读书笔记—— 接口与API设计