【C语言进阶】指针进阶的详细讲解--(数组指针、指针数组、函数指针、函数指针数组、指向函数指针数组)

简介: 【C语言进阶】指针进阶的详细讲解--(数组指针、指针数组、函数指针、函数指针数组、指向函数指针数组)

1.字符指针

1.1字符指针的使用:

形式一:

int main()
{
  char ch = 'w';
  char* pc = &ch;
  *pc = 'w';
  printf("%c\n", *pc);
}

形式二:

int main()
{
  const char* pstr = "hello bit.";
  printf("%s\n", *pstr);
  return 0;
}

第二种用法的本质是把常量字符串 ’ hello bit. ’ 的首字符 ’ h ’ 的地址放到了pstr中,不是把一个字符串放到pstr指针变量里。

1.2.常量字符串:

注意,常量字符串作为常量,它们不可被修改

int main()
{
    char* p = "abcdef";
    //使指针指向常量区的常量字符串abcdef
    *p = 'A';
    //按照我们的理解,*p中存放的是首元素a的地址,我们尝试改变它
    //我们运行起来发现无法完成编译运行,程序会直接卡住
    //于是我们可以得出,当使用常量字符串时,常量字符串abcdef不可修改
    return 0;
}

这些常量字符串中的常量字符,是原本就储存在常量区(只读数据区)的一些常量,不同于需要我们输入的字符变量。所以当使用字符指针直接指向常量字符串时,可以直接进行调用但无法对其进行修改。


当出现这种错误时,程序往往是可以正常编译运行但会卡住而得不出结果,会给我们的代码书写造成很大的困扰。为了尽可能的避免这种情况的发生,我们通常在指向常量字符串时,使用 const 对指针变量进行修饰,进行这样的书写操作之后,当我们试图对常量字符串进行改动时,将会直接报错而无法进行编译运行, 便于我们找到问题的所在

1.3.相关面试题:

int main()
{
    char str1[] = "hello bit.";
    char str2[] = "hello bit.";
    //创建两个数组、两个字符型指针变量
    const char* str3 = "hello bit.";
    const char* str4 = "hello bit.";
//判断数组内容是否相同:
    if (str1 == str2)
    {
        printf("str1 and str2 are same\n");
    }
    else
    {
        printf("str1 and str2 are not same\n");
    }
//判断指针变量内容是否相同:
    if (str3 == str4)
    {
        printf("str3 and str4 are same\n");
    }
    else
    {
        printf("str3 and str4 are not same\n");
    }
 return 0;
}

这道面试题中定义了两个字符型数组和两个字符型指针变量,将两个字符型数组进行对比,又将两个字符型指针变量尽行了对比,目的是为了验证它们在存储时内存的开辟。


这里 str3 和 str4 指向的是一个同一个常量字符串。C/C++会把常量字符串存储到单独的一个内存区域(在我们的内存中存在着常量区用于存放一些常量),当几个指针指向同一个字符串的时候,他们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同

2.指针数组:

指针数组的本质是数组,是用于存放指针变量的数组:

int main()
{
    //创建变量:
    int a = 1;
    int b = 2;
    int c = 3;
 //创建指针数组,用于存放指针(即变量的地址):
    int* arr[3] = { &a,&b,&c }; 
//打印验证内容:
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        printf("%p\n", arr[i]);
    }
//也可以通过指针数组(存放的是各个变量的地址)来访问变量内容:
    for (i = 0; i < 3; i++)
    {
        printf("%d\n", *(arr[i]));
    }
    return 0;
}

更常用的却是另外一种情况:

int main()
{
 //创建数组:
    int arr1[3] = { 1,2,3 };
    int arr2[3] = { 4,5,6 };
    int arr3[3] = { 7,8,9 };
  int* parr[3] = { arr1,arr2,arr3 };
    //我们都知道,数组名即为数组内首元素地址
    //即此处的数组名为地址,故可以存放在指针变量内
//验证指针数组内存放数据:
    int i = 0;
    for (i = 0; i < 3; i++)
    {
        printf("整型数组arr%d[3]首元素地址为:%p\n", i + 1, parr[i]);
    }
    printf("\n");
 //通过指针数组访问整型数组内的数据:
    for (i = 0; i < 3; i++)
    {
        printf("arr%d[3]中存储的数据为:", i + 1);
        int j = 0;
        for (j = 0; j < 3; j++)
        {
            printf("%d ", *(parr[i] + j));
        }
        printf("\n");
    }
    return 0;
}

3.数组指针:

3.1 数组指针的定义:

数组指针的本质是指针。不同于其他指针,数组指针指向的不是地址而是数组

int main()
{
    int* p1[10];
    //p1是数组,指针数组
  int(*p2)[10];
    //p2是指针。数组指针
    return 0;
}

上面的示例中,p2先和*进行结合,说明p2是一个指针变量,再指向大小为10的整型数组。即p2是一个指向数组的指针,叫做数组指针。简单来说,数组指针就是专门用来存放一个数组的地址的指针。

3.2.&数组名VS数组名:

我们都知道,除一些特殊情况外,大多数的数组名都表示数组首元素的地址,那么既然数组名已经是地址了,&+数组名的组合又代表着什么呢

int main()
{
    int arr[5] = { 0 };
    printf("arr :%p\n", arr);
    printf("&arr:%p\n", &arr);
    return 0;
}

我们如果通过打印,会发现 arr 的打印结果与 &arr 相同:

那它们真的是一回事吗?

我们将上面这段代码进行改写:

int main()
{
    int arr[5] = { 0 };
  printf("arr :%p\n", arr);
    printf("&arr:%p\n", &arr);
  printf("\n");
  printf("arr+1 = %p\n", arr + 1);
    printf("&arr+1= %p\n", &arr + 1);
    return 0;
}

我们使 arr 与 &arr 同时向后走一步,这时我们看到了不一样的结果:

实际上,&arr 表示的是整个数组的地址,而 arr 仅表示其中首元素的地址,这就导致了我们在使它们在向后走的时候,它们向后走的步幅不同,&arr 跨过了整个数组的长度而 arr 仅仅只跳过了一个数据元素。

3.3.数组指针的使用:

在上面的介绍中,我们看到了数组指针的最基础用法:

int arr[10] = { 0 };
int (*p2)[10] = &arr;

但我们却很少会这样去使用它。更多的是在我们进行函数调用,进行数组传参时,可以通过数组指针来进行接收:

void print_arr1(int arr[3][5], int row, int col)
{
    int i = 0;
    for (i = 0; i < row; i++)
    {
        int j = 0;
        for (j = 0; j < col; j++)
        {
            printf("%02d ", arr[i][j]);
        }
        printf("\n");
    }
}
//使用数组指针来进行接收:
void print_arr2(int(*arr)[5], int row, int col)
{
    int i = 0;
    for (i = 0; i < row; i++)
    {
        int j = 0;
        for (j = 0; j < col; j++)
        {
            printf("%02d ", arr[i][j]);
        }
        printf("\n");
    }
}
int main()
{
    int arr[3][5] = { 1,2,3,4,5,6,7,8,9,10 };
    print_arr1(arr, 3, 5);
    //数组名arr,表示首元素的地址
    //但是二维数组的首元素是二维数组的第一行,所以这里传递的arr,其实相当于第一行的地址,是一维数组的地址
    printf("\n");
   //可以使用数组指针来进行接收:
    print_arr2(arr, 3, 5);
    return 0;
}

4.数组参数:

4.1一维数组传参:

我们都知道,在对数组进行传参时并不会真实的在内存中创建临时数组,数组名的意义是作为数组内首元素的地址,因此当函数参数为数组名时,实际上传递的是数组中首元素的地址,于是我们可以发现下面三种传参方式都是可行的

//方式1:标准传参方式
//完整的传递数组内容
void test1(int arr[3])
{
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    printf("%d ", arr[i]);
  }
  printf("\n");
}
//方式2:省略数组大小
//形参部分的数组大小可以省略
void test2(int arr[])
{
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    printf("%d ", arr[i]);
  }
  printf("\n");
}
//方式3:扩大形参数组大小
//实际仍为传递首元素地址,故可行,但不建议
void test3(int arr[100])
{
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    printf("%d ", arr[i]);
  }
  printf("\n");
}
int main()
{
  int arr[3] = { 1,2,3 };
 //调用函数:
  test1(arr);
  test2(arr);
  test3(arr);
  return 0;
}

这三种将形参写作数组形式的方式都是可行的,但是为了避免出现错误,推荐大家尽可能的使用第一种方式,其次是第二种方式。第三种方式虽然也可以运行,但有可能会出现难以预料的错误,不建议使用。

同时,以上三种将形式参数写成数组形式的写法,也可以改写为使用指针做形式参数的形

//使用指针作为形式参数:
//一级指针:
void test1(int* p)
{
  int i;
  for (i = 0; i < 3; i++)
  {
    printf("%d ", *(p + i));
  }
  printf("\n");
}
//二级指针_形式1:
void test2(int** p)
{
  int i;
  for (i = 0; i < 3; i++)
  {
    printf("%d ", *(p + i));
  }
  printf("\n");
}
//二级指针_形式2:
void test3(int** p)
{
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    printf("%d ", **(p + i));
  }
}
int main()
{
  int arr[3] = { 1,2,3 };
    int* parr = &arr;
   int arrA[3] = { 1,2,3 };
  int arrB[3] = { 4,5,6 };
  int arrC[3] = { 7,8,9 };
  int* arrD[3] = { &arrA,&arrA,&arrC };
  //调用函数:
  test1(arr);
    test2(parr);
  test3(arrD);
  return 0;
}

并且同理,使用指针作为形式参数的另外两种形式也是可以(但最后一种方式同样不建议)的

void test(int* p[3])
{
    ...
}
 void test(int* p[100])
//可以但不建议使用
{
    ...
}

4.2 二维数组传参:

我们在前面初阶的部分学习二维数组传参时就知道了,二维数组在进行传参时可以不知道有多好行,但必须知道有多少列,这样计算机才知道应该在何时进行换行。如此只要知道了什么时候进行换行,对于行数就不需要再进行强制要求了,所以在数组进行传参时,允许写成以下三种形式:

//二维数组传参:
//方式1:标准传参
void test1(int arr[3][3])
{
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    int j = 0;
    for (j = 0; j < 3; j++)
    {
      printf("arr[%d][%d] = %d ", i, j, arr[i][j]);
    }
    printf("\n");
  }
}
//方式2:行数可以省略
void test2(int arr[][3])
{
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    int j = 0;
    for (j = 0; j < 3; j++)
    {
      printf("arr[%d][%d] = %d ", i, j, arr[i][j]);
    }
    printf("\n");
  }
}
//方式3:行数可以超出原数组上限
void test3(int arr[100][3])
{
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    int j = 0;
    for (j = 0; j < 3; j++)
    {
      printf("arr[%d][%d] = %d ", i, j, arr[i][j]);
    }
    printf("\n");
  }
}
int main()
{
  int arr[3][3] = { {1,2,3},{4,5,6},{7,8,9} };
 //函数调用:
  test1(arr);
  test2(arr);
  test3(arr);
  return 0;
}

同样的,二维数组除了可以使用数组作为函数的参数以外,也可以使用指针作为函数的参数进行传参,区别于一维数组,二维数组在传参时传递的不是首元素的地址而是首行元素的地址

void test(int(*p)[3])
//此处的数组大小3为列数
{
  int i = 0;
  for (i = 0; i < 3; i++)
  {
    int j = 0;
    for (j = 0; j < 3; j++)
    {
      printf("%d ", (*p + i * 3)[j]);
            //这里注意p+(i*3)是因为i为行号,跳过行时需要跳过i*3个数据元素
    }
    printf("\n");
  }
}
int main()
{
  int arr[3][3] = { {1,2,3},{4,5,6},{7,8,9} };
 test(arr);
return 0;
}

但是要格外注意,二维数组与一维数组不同,不可以使用二级指针进行传参。其原理是,二级指针的作用是用于存储一级指针的的地址,而传递过来的参数是二维数组第一行(这里可以简单理解为一个一维数组,但本质上不是)的地址,无法使用二级指针进行存储。

相关文章
|
3月前
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
3月前
|
容器
在使用指针数组进行动态内存分配时,如何避免内存泄漏
在使用指针数组进行动态内存分配时,避免内存泄漏的关键在于确保每个分配的内存块都能被正确释放。具体做法包括:1. 分配后立即检查是否成功;2. 使用完成后及时释放内存;3. 避免重复释放同一内存地址;4. 尽量使用智能指针或容器类管理内存。
|
3月前
|
存储 C语言 计算机视觉
在C语言中指针数组和数组指针在动态内存分配中的应用
在C语言中,指针数组和数组指针均可用于动态内存分配。指针数组是数组的每个元素都是指针,可用于指向多个动态分配的内存块;数组指针则指向一个数组,可动态分配和管理大型数据结构。两者结合使用,灵活高效地管理内存。
|
3月前
|
存储 NoSQL 编译器
C 语言中指针数组与数组指针的辨析与应用
在C语言中,指针数组和数组指针是两个容易混淆但用途不同的概念。指针数组是一个数组,其元素是指针类型;而数组指针是指向数组的指针。两者在声明、使用及内存布局上各有特点,正确理解它们有助于更高效地编程。
|
3月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
276 13
|
3月前
|
存储 人工智能 算法
数据结构实验之C 语言的函数数组指针结构体知识
本实验旨在复习C语言中的函数、数组、指针、结构体与共用体等核心概念,并通过具体编程任务加深理解。任务包括输出100以内所有素数、逆序排列一维数组、查找二维数组中的鞍点、利用指针输出二维数组元素,以及使用结构体和共用体处理教师与学生信息。每个任务不仅强化了基本语法的应用,还涉及到了算法逻辑的设计与优化。实验结果显示,学生能够有效掌握并运用这些知识完成指定任务。
81 4
|
3月前
使用指针访问数组元素
【10月更文挑战第31天】使用指针访问数组元素。
63 2
|
3月前
使用指针访问数组元素
【10月更文挑战第30天】使用指针访问数组元素。
53 3
|
3月前
|
算法 索引
单链表题+数组题(快慢指针和左右指针)
单链表题+数组题(快慢指针和左右指针)
51 1
|
4月前
|
存储
如何使用指针数组来实现动态二维数组
指针数组可以用来实现动态二维数组。首先,定义一个指向指针的指针变量,并使用 `malloc` 为它分配内存,然后为每个子数组分配内存。通过这种方式,可以灵活地创建和管理不同大小的二维数组。

热门文章

最新文章