c语言从入门到实战——基于指针的数组与指针数组

本文涉及的产品
云原生网关 MSE Higress,422元/月
可观测可视化 Grafana 版,10个用户账号 1个月
应用实时监控服务-应用监控,每月50GB免费额度
简介: 指针的数组是指数组中的元素都是指针类型,它们指向某种数据类型的变量。基于指针的数组(Array of Pointers)和指针数组(Pointer to Array)是两种常见的C/C++语言中的数据结构,它们在内存布局和使用方式上有所不同。基于指针的数组是一个数组,其元素是指针,每个指针可以指向不同地址的数据。这种结构常用于存储指向不同对象的指针,例如字符串或动态分配的内存块。指针数组则是一个指针,它指向一个数组。这个指针固定指向数组的首地址,通过数组索引可以访问数组中的元素。这种结构常用于操作整个数组,例如作为函数参数传递数组。

基于指针的数组与指针数组


前言

指针的数组是指数组中的元素都是指针类型,它们指向某种数据类型的变量。

基于指针的数组(Array of Pointers)和指针数组(Pointer to Array)是两种常见的C/C++语言中的数据结构,它们在内存布局和使用方式上有所不同。

基于指针的数组是一个数组,其元素是指针,每个指针可以指向不同地址的数据。这种结构常用于存储指向不同对象的指针,例如字符串或动态分配的内存块。

指针数组则是一个指针,它指向一个数组。这个指针固定指向数组的首地址,通过数组索引可以访问数组中的元素。这种结构常用于操作整个数组,例如作为函数参数传递数组。

两种结构的选择取决于具体的应用场景,需要根据实际需求和数据结构的特点来决定使用哪种方式。


1. 数组名的理解

我们在使用指针访问数组的内容时,有这样的代码:

int arr[10] = {1,2,3,4,5,6,7,8,9,10};
int *p = &arr[0];

这里我们使用 &arr[0] 的方式拿到了数组第一个元素的地址,但是其实数组名本来就是地址,而且是数组首元素的地址,我们来做个测试。

#include <stdio.h>
int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  printf("&arr[0] = %p\n", &arr[0]);
  printf("arr = %p\n", arr);
  return 0;
}

输出结果:

我们发现数组名和数组首元素的地址打印出的结果一模一样,数组名就是数组首元素(第一个元素)的地址。

这时候有读者可能会有疑问?数组名如果是数组首元素的地址,那下面的代码怎么理解呢?

#include <stdio.h>
int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  printf("%d\n", sizeof(arr));
  return 0;
}

输出的结果是:40,如果arr是数组首元素的地址,那输出应该的应该是4/8才对。

其实数组名就是数组首元素(第一个元素)的地址是对的,但是有两个例外:

  • sizeof(数组名)sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节
  • &数组名,这里的数组名表示整个数组,取出的是整个数组的地址(整个数组的地址和数组首元素的地址是有区别的)

除此之外,任何地方使用数组名,数组名都表示首元素的地址。

可以再试一下这个代码:

#include <stdio.h>
int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  printf("&arr[0] = %p\n", &arr[0]);
  printf("arr = %p\n", arr);
  printf("&arr = %p\n", &arr);
  return 0;
}

三个打印结果一模一样,这时候又纳闷了,那arr&arr有啥区别呢?

#include <stdio.h>
int main()
{
  int arr[10] = { 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[0] = 0077F820
&arr[0]+1 = 0077F824
arr = 0077F820
arr+1 = 0077F824
&arr = 0077F820
&arr+1 = 0077F848

这里我们发现&arr[0]&arr[0]+1相差4个字节,arrarr+1相差4个字节,是因为&arr[0]arr都是首元素的地址,+1就是跳过一个元素。

但是&arr&arr+1相差40个字节,这就是因为&arr是数组的地址,+1操作是跳过整个数组的。

到这里大家应该搞清楚数组名的意义了吧。

数组名是数组首元素的地址,但是有2个例外。

2. 使用指针访问数组

有了前面知识的支持,再结合数组的特点,我们就可以很方便的使用指针访问数组了。

#include <stdio.h>
int main()
{
  int arr[10] = {0};
//输入
  int i = 0;
  int sz = sizeof(arr)/sizeof(arr[0]);
//输入
  int* p = arr;
  for(i=0; i<sz; i++)
  {
  scanf("%d", p+i);
//scanf("%d", arr+i);//也可以这样写
  }
//输出
  for(i=0; i<sz; i++)
  {
  printf("%d ", *(p+i));
  }
  return 0;
}

这个代码搞明白后,我们再试一下,如果我们再分析一下,数组名arr是数组首元素的地址,可以赋值给p,其实数组名arrp在这里是等价的。那我们可以使用arr[i]可以访问数组的元素,那p[i]是否也可以访问数组呢?

#include <stdio.h>
int main()
{
  int arr[10] = {0};
//输入
  int i = 0;
  int sz = sizeof(arr)/sizeof(arr[0]);
//输入
  int* p = arr;
  for(i=0; i<sz; i++)
  {
  scanf("%d", p+i);
//scanf("%d", arr+i);//也可以这样写
  }
//输出
  for(i=0; i<sz; i++)
  {
  printf("%d ", p[i]);//我们也可以使用i[p] ,编译器会编译成*(i + P) 和* (p + i )同理
  }
  return 0;
}

在第18行的地方,将*(p+i)换成p[i]也是能够正常打印的,所以本质上p[i]是等价于*(p+i)

同理arr[i]应该等价于*(arr+i),数组元素的访问在编译器处理的时候,也是转换成首元素的地址+偏移量求出元素的地址,然后解引用来访问的。

3. 一维数组传参的本质

数组是可以传递给函数的。

首先从一个问题开始,我们之前都是在函数外部计算数组的元素个数,那我们可以把函数传给一个函数后,函数内部求数组的元素个数吗?

#include <stdio.h>
void test(int arr[])
{
  int sz2 = sizeof(arr)/sizeof(arr[0]);
  printf("sz2 = %d\n", sz2);
}
int main()
{
  int arr[10] = {1,2,3,4,5,6,7,8,9,10};
  int sz1 = sizeof(arr)/sizeof(arr[0]);
  printf("sz1 = %d\n", sz1);
  test(arr);
  return 0;
}

输出的结果:

我们发现在函数内部是没有正确获得数组的元素个数。

这就要学习数组传参的本质了,上篇文章我讲了:数组名是数组首元素的地址;那么在数组传参的时候,传递的是数组名,也就是说本质上数组传参本质上传递的是数组首元素的地址。

所以函数形参的部分理论上应该使用指针变量来接收首元素的地址。那么在函数内部我们写

sizeof(arr) 计算的是一个地址的大小(单位字节)而不是数组的大小(单位字节)。

正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。

void test(int arr[]) //参数写成数组形式,本质上还是指针
{
  printf("%d\n", sizeof(arr));
}
void test(int* arr) //参数写成指针形式
{
  printf("%d\n", sizeof(arr)); //计算⼀个指针变量的大小
}
int main()
{
  int arr[10] = {1,2,3,4,5,6,7,8,9,10};
  test(arr);
  return 0;
}

总结:一维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式。

4. 冒泡排序

冒泡排序的核心思想就是:两两相邻的元素进行行比较。

//方法1
void bubble_sort(int arr[], int sz) //参数接收数组元素个数
{
  int i = 0;
  for(i=0; i<sz-1; i++)
  {
  int j = 0;
  for(j=0; j<sz-i-1; j++)
  {
  if(arr[j] > arr[j+1])
  {
  int tmp = arr[j];
  arr[j] = arr[j+1];
  arr[j+1] = tmp;
  }
  }
  }
}
int main()
{
  int arr[] = {3,1,7,5,8,9,0,2,4,6};
  int sz = sizeof(arr)/sizeof(arr[0]);
  bubble_sort(arr, sz);
  for(i=0; i<sz; i++)
  {
  printf("%d ", arr[i]);
  }
  return 0;
}
//方法2 - 优化
void bubble_sort(int arr[], int sz) //参数接收数组元素个数
{
  int i = 0;
  for(i=0; i<sz-1; i++)
  {
  int flag = 1; //假设这一趟已经有序了
  int j = 0;
  for(j=0; j<sz-i-1; j++)
  {
  if(arr[j] > arr[j+1])
  {
  flag = 0; //发生交换就说明,无序
  int tmp = arr[j];
  arr[j] = arr[j+1];
  arr[j+1] = tmp;
  }
  }
  if(flag == 1) //这一趟没交换就说明已经有序,后续无序排序了
  break;
  }
}
int main()
{
  int arr[] = {3,1,7,5,8,9,0,2,4,6};
  int sz = sizeof(arr)/sizeof(arr[0]);
  bubble_sort(arr, sz);
  for(i=0; i<sz; i++)
  {
  printf("%d ", arr[i]);
  }
return 0;
}

5. 二级指针

指针变量也是变量,是变量就有地址,那指针变量的地址存放在哪里——二级指针

对于二级指针的运算有:

  • *ppa 通过对ppa中的地址进行解引用,这样找到的是 pa*ppa 其实访问的就是 pa .
int b = 20;
*ppa = &b; //等价于 pa = &b;

**ppa 先通过 *ppa 找到 pa ,然后对 pa 进行解引用操作: *pa ,那找到的是 a

**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;

6. 指针数组

指针数组是指针还是数组

我们类比一下,整型数组,是存放整型的数组,字符数组是存放字符的数组。

那指针数组呢?是存放指针的数组。

指针数组的每个元素都是用来存放地址(指针)的。

如下图:

指针数组的每个元素是地址,又可以指向一块区域。

7. 指针数组模拟二维数组

#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*的,就可以存放在parr数组中
  int* parr[3] = {arr1, arr2, arr3};
  int i = 0;
  int j = 0;
  for(i=0; i<3; i++)
  {
    for(j=0; j<5; j++)
    {
    printf("%d ", parr[i][j]);
    }
  printf("\n");
  }
  return 0;
}

parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型一维数组,parr[i][j]就是整型一维数组中的元素。

上述的代码模拟出二维数组的效果,实际上并非完全是二维数组,因为每一行并非是连续的。

相关文章
|
2月前
|
存储 NoSQL 编译器
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
指针是一个变量,它存储另一个变量的内存地址。换句话说,指针“指向”存储在内存中的某个数据。
113 3
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
|
2月前
|
存储 编译器 C语言
【C语言】指针大小知多少 ?一场探寻C语言深处的冒险 !
在C语言中,指针的大小(即指针变量占用的内存大小)是由计算机的体系结构(例如32位还是64位)和编译器决定的。
112 9
|
2月前
|
安全 程序员 C语言
【C语言】指针的爱恨纠葛:常量指针vs指向常量的指针
在C语言中,“常量指针”和“指向常量的指针”是两个重要的指针概念。它们在控制指针的行为和数据的可修改性方面发挥着关键作用。理解这两个概念有助于编写更安全、有效的代码。本文将深入探讨这两个概念,包括定义、语法、实际应用、复杂示例、最佳实践以及常见问题。
57 7
|
2月前
|
传感器 算法 安全
【C语言】两个数组比较详解
比较两个数组在C语言中有多种实现方法,选择合适的方法取决于具体的应用场景和性能要求。从逐元素比较到使用`memcmp`函数,再到指针优化,每种方法都有其优点和适用范围。在嵌入式系统中,考虑性能和资源限制尤为重要。通过合理选择和优化,可以有效提高程序的运行效率和可靠性。
166 6
|
3月前
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
23天前
|
存储 算法 C语言
【C语言程序设计——函数】素数判定(头歌实践教学平台习题)【合集】
本内容介绍了编写一个判断素数的子函数的任务,涵盖循环控制与跳转语句、算术运算符(%)、以及素数的概念。任务要求在主函数中输入整数并输出是否为素数的信息。相关知识包括 `for` 和 `while` 循环、`break` 和 `continue` 语句、取余运算符 `%` 的使用及素数定义、分布规律和应用场景。编程要求根据提示补充代码,测试说明提供了输入输出示例,最后给出通关代码和测试结果。 任务核心:编写判断素数的子函数并在主函数中调用,涉及循环结构和条件判断。
55 23
|
23天前
|
算法 C语言
【C语言程序设计——函数】利用函数求解最大公约数和最小公倍数(头歌实践教学平台习题)【合集】
本文档介绍了如何编写两个子函数,分别求任意两个整数的最大公约数和最小公倍数。内容涵盖循环控制与跳转语句的使用、最大公约数的求法(包括辗转相除法和更相减损术),以及基于最大公约数求最小公倍数的方法。通过示例代码和测试说明,帮助读者理解和实现相关算法。最终提供了完整的通关代码及测试结果,确保编程任务的成功完成。
52 15
|
23天前
|
C语言
【C语言程序设计——函数】亲密数判定(头歌实践教学平台习题)【合集】
本文介绍了通过编程实现打印3000以内的全部亲密数的任务。主要内容包括: 1. **任务描述**:实现函数打印3000以内的全部亲密数。 2. **相关知识**: - 循环控制和跳转语句(for、while循环,break、continue语句)的使用。 - 亲密数的概念及历史背景。 - 判断亲密数的方法:计算数A的因子和存于B,再计算B的因子和存于sum,最后比较sum与A是否相等。 3. **编程要求**:根据提示在指定区域内补充代码。 4. **测试说明**:平台对代码进行测试,预期输出如220和284是一组亲密数。 5. **通关代码**:提供了完整的C语言代码实现
56 24
|
19天前
|
存储 C语言
【C语言程序设计——函数】递归求斐波那契数列的前n项(头歌实践教学平台习题)【合集】
本关任务是编写递归函数求斐波那契数列的前n项。主要内容包括: 1. **递归的概念**:递归是一种函数直接或间接调用自身的编程技巧,通过“俄罗斯套娃”的方式解决问题。 2. **边界条件的确定**:边界条件是递归停止的条件,确保递归不会无限进行。例如,计算阶乘时,当n为0或1时返回1。 3. **循环控制与跳转语句**:介绍`for`、`while`循环及`break`、`continue`语句的使用方法。 编程要求是在右侧编辑器Begin--End之间补充代码,测试输入分别为3和5,预期输出为斐波那契数列的前几项。通关代码已给出,需确保正确实现递归逻辑并处理好边界条件,以避免栈溢出或结果
52 16
|
18天前
|
存储 编译器 C语言
【C语言程序设计——函数】分数数列求和2(头歌实践教学平台习题)【合集】
函数首部:按照 C 语言语法,函数的定义首部表明这是一个自定义函数,函数名为fun,它接收一个整型参数n,用于指定要求阶乘的那个数,并且函数的返回值类型为float(在实际中如果阶乘结果数值较大,用float可能会有精度损失,也可以考虑使用double等更合适的数据类型,这里以float为例)。例如:// 函数体代码将放在这里函数体内部变量定义:在函数体中,首先需要定义一些变量来辅助完成阶乘的计算。比如需要定义一个变量(通常为float或double类型,这里假设用float。
28 3