【C语言进阶】 带你玩转指针(二)

简介: 【C语言进阶】 带你玩转指针(二)

三、数组指针

数组指针的后缀是指针,那就说明数组指针本质上是指针,比如整型指针(int*):表示这个指针变量里面存放的是一个整型变量的地址,它指向一个整型变量。字符指针(char*):表示这个指针变量里面存放的是一个字符型变量的地址,它指向一个字符变量。同理:那数组指针一定是一个指向数组的指针,换言之,这个指针变量里面存放的是数组的地址。

//数组指针
int main()
{
  char ch = 'w';
  char* pc = &ch;//pc是字符指针,类型是:char*
  //这里的*交代pc是一个指针变量,char表示它指向指针
  int num = 10;
  int* pi = #//pi是整型指针,类型是int*
  //这里的*交代pi是一个指针变量,int表示它指向指针
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };//arr的类型是int [10],如果有一个指针变量指向它,那指针变量的类型应该也是这样
  int(*pa)[10]=&arr;//&arr取到的是整个数组的地址,pa是数组指针,类型是:int(*)[10]
  //这里的*交代pa是一个指针变量,int [10]表示它指向一个类型为int [10]的数组
  //int(*pa)[10] = arr;//这种写法是错误的,arr是数组首元素的地址,也就是1的地址,1是int型,int型的地址需要用int*来接收才对
  return 0;
}

3.1:数组指针的使用

在一维数组中的使用:

//数组指针的使用
int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  常规打印数组的方式
  //int* p = arr;//拿到首元素地址
  //int i = 0;
  //for (i = 0; i < 10; i++)
  //{
  //  //printf("%d ", *(p + i));//(p + i)就是偏移过i个元素,第i个元素的地址,然后解引用
  //  printf("%d ", p[i]);//通过首元素地址+下标的方式来打印
  //}
  //利用数组指针来打印
  int(*p)[10] = &arr;//拿到整个数组的地址
  printf("%p\n", p);
  printf("%p\n", (*p));
  printf("%p\n", &arr[0]);
  //三者打印出来的数据完全相同,都是首元素的地址
  printf("\n");
  printf("%p\n", p+1);//p+1跳过了整个数组,也就是40个字节
  printf("%p\n", (*p)+1);//(*p)+1只跳过了首元素,也就是4个字节
  printf("%p\n", &arr[0]+1);//&arr[0]+1也只跳过了首元素,也就是4个字节
  //这说明对数组指针解引用得到的是该指针所指向数组的首元素的地址
  //这一点就和以前有所不同
  //整型指针解引用得到的是一个整型变量
  //字符指针解引用得到的是一个字符变量
  //这里的数组指针解引用得到的是一个地址
  //有了上面的分析,我们就可以利用数组指针来打印数组了
  int i = 0;
  for (i = 0; i < 10; i++)
  {
    printf("%d ", *(*p + i));//*p得到的是首元素的地址,+i表示第i个元素的地址,然后再解引用
    printf("%d ", (*p)[i]);//既然(*p)得到的是数组首元素的地址,那就还可以通过:数组首元素地址+下标 的方式去访问数组中的每一个元素
  }
  return 0;
}

通过上面的代码不难发现:数组指针解引用得到的该指针所指向数组的首元素的地址,这一点和其他类型的指针有所不同。通过:*p = *(&arr) = arr 也不难发现,*p最终的结果是数组名,数组名又表示首元素地址,所以数组指针解引用得到的是首元素地址

在二维数组中的使用:

 这里主要得知道:二维数组的数组名也表示首元素地址,二维数组arr的首元素是二维数组中第一行那个一维数组arr[0],所以二维数组名就表示&arr[0],得到一个数组的地址

//数组指针在二维数组中的使用
void print1(int arr[3][4], int r, int l)
{
  int i = 0;
  for (i = 0; i < r; i++)
  {
    int j = 0;
    for (j = 0; j < l; j++)
    {
      printf("%d ", arr[i][j]);//普通打印方式
    }
    printf("\n");
  }
}
void print2(int(*p)[4],int r,int l)
{
  int i = 0;
  for (i = 0; i < r; i++)
  {
    int j = 0;
    for (j = 0; j < l; j++)
    {
      //printf("%d ",(*(p+i))[j]);//这里的p存放的第一行这个一位数组的地址,是一个数组指针,所以:p+i 会跳过i个数组得到第i个数组的地址,然后再对数组地址解引用得到数组首元素的地址,然后通过 数组首元素的地址+下标的方式进行访问
      printf("%d ", p[i][j]);//既然p是二维数组首元素第一行一维数组的地址,那就可以通过 首元素地址+下标i 的方式去访问二维数组第i个元素,也就是第i行一位数组的首地址
    }
    printf("\n");
  }
}
int main()
{
  int arr[3][4] = { {1,2,3,4},{5,4,3,2},{8,9,6,7} };//创建一个二维数组
  //print1(arr, 3, 4);
  print2(arr, 3, 4);
  //printf("%p\n", arr);
  //printf("%p\n", &arr[0][0]);
  //printf("%p\n", &(arr[0]));//arr[0]是二维数组中第一行那个一维数组的数组名,&(arr[0])就是取那个一维数组的地址
  三者打印结果一样,都是arr[0][0]的地址
  //printf("\n");
  //printf("%p\n", arr+1);//跳过16个字节,也就是二维数组的一行元素
  //printf("%p\n", &arr[0][0]+1);//跳过4个字节,也就是一个整型
  //printf("%p\n", &(arr[0])+1);//跳过16个字节,也就是二维数组的一行元素
  结果表明二维数组的数组名确实表示首元素的地址,但是二维数组中的首元素是第一行那个一维数组
  return 0;
}


d47e62de21e64e33aa7b420747d46154.png

看看下面代码分别是什么意思:

int arr[5];//arr是一个整型数组,可以存储5个元素
int *parr1[10];//首先parr1先和[10]结合,说明parr1是一个数组,可以存储10个元素,数组里面存储的元素类型是int*,所以parr1是一个整型指针数组
int (*parr2)[10];//首先parr2先和*结合,说明parr2是一个指针,指向的数据类型是 int [10],这是一个可以存储10个元素的整型数组,所以parr2是一个数组指针
int (*parr3[10])[5];//首先parr3先和[10]结合,说明parr3是一个可以存储10个元素的数组,这个数组里面存的元素类型是:int(*)[5],也就是数组指针类型的数据,所以parr3是一个数组指针数组

int (*parr3[10])[5];详解:

6a5b7b87af0d467287ee4320d99ea4f2.png

四、数组参数、指针参数

4.1:一维数组传参

普通一维数组传参:

//普通一维数组传参
void test(int arr[])//形参用数组接收,可以不写数组大小
{}
void test(int arr[10])//形参用数组接收,理所当然
{}
void test(int* arr)//数组名是首元素地址,首元素是一个整型,所以用一个整型指针接收也可以
{}
int main()
{
  int arr[10] = {0};
  text(arr);//参数是一维数组名,表示首元素地址
  return 0;
}

一维指针数组:

//一维指针数组
void test2(int* arr[20])//形参用对应类型的数组来接收理所应当
{}
void test2(int* arr[])//形参用对应的数组接收,可以省略数组元素个数
{}
void test2(int** arr)//数组名表示首元素地址,整型指针数组的首元素是一个整型指针,那首元素的地址就是一个指针的地址,指针的地址就是二级指针,所以形参用一个二级指针来接收
{}
int main()
{
  int *arr[20] = { 0 };
  text(arr);//参数是一维数组名,表示首元素地址
  return 0;
}

4.2:二维数组传参

//二维数组传参
void text(int arr[3][5])//形参用对应类型的二维数组来接收,可行
{}
void text(int arr[][])//形参用对应类型的二维数组来接收,行数可以省略但是列数不可以省略
{}
void text(int arr[][5])形参用对应类型的二维数组来接收,行数可以省略但是列数不可以省略
{}
void text(int(*arr)[5])//数组名是首元素地址,二维数组的首元素是第一行那个一维数组arr[0],所以首元素的地址就是一个一维数组的地址,所以形参应该用一个数组指针来接收,这个数组类型是 int [5]
{}
//一些错误写法
void text(int *arr)//这里的形参本质上是一个一级整型指针,指向一个整型,而我们需要的是一个可以存储一维数组地址的数组指针,所以这样写肯定不行
{}
void text(int *arr[5])//这里的形参本质上是一个可以存放5个元素的一维数组,形参如果是数组的话应该是对应的二维数组,这里肯能也不对
{}
void text(int **arr)//这里的形参是一个二级指针,也就是用来存储指针的地址(一级指针的地址),而我们需要的形参是一个可以存放数组地址的变量,仅仅是一个一维指针,所以这里肯定也是错误的
{}
int main()
{
  int arr[3][5] = { 0 };
  test(arr);
  return 0;
}

4.3:一级指针传参:

//一级指针传参
void print(int* p, int sz)//一级指针作为函数的实参,形参也用一级指针来接收
{
  int i = 0;
  for(i = 0; i < sz; i++)
  {
    printf("%d ", *(p + i));//指针变量加上一个偏移量,因为这是一个整形指针,加i就跳过4*i个字节,这里也就是取第i个元素的地址,然后解引用来访问第i个元素
    printf("%d ", p[i]);//也可以通过首元素地址+下标的形式来访问数组元素
  }
}
int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  int* p = arr;//数组名是首元素的地址
  int sz = sizeof(arr) / sizeof(arr[0]);
  print(p, sz);//一级指针p作为函数参数,传给函数
  return 0;
}

思考: 当一个函数的参数部分为一级指针的时候,函数能接收什么参数?

  • 一级指针变量
  • 一维数组的数组名
  • 变量的地址

4.4:二级指针传参:

//二级指针传参
void test(int **p)//形参直接用一个二级指针来接收
{
  printf("num=%d\n", **p);
}
int main()
{
  int n = 10;
  int* p = &n;
  int** pp = &p;
  test(pp);
  test(&p);
  return 0;
}

思考: 当一个函数的参数部分为二级指针的时候,函数能接收什么参数?

  • 二级指针变量
  • 一级指针的数组
  • 一维指针数组的数组名

五:函数指针

 函数指针:就是指向函数的指针

int Add(int x, int y)
{
  return x + y;
}
int main()
{
  printf("%p\n", &Add);
  //打印出来一个地址,说明&函数名,确实可以取出函数的地址
  //既然是地址那一定可以被存起来
  int(*p)(int, int) = &Add;//首先,&ADD得到的是一个地址,说明需要一个指针变量来存储,所以p先和*结合,说明p是指针变量,接着需要知道p这个指针所指向内容的类型,也就是这个函数的类型,函数的类型可以通过函数的声明看出来,去掉函数名剩下的就是函数类型,这里Add的函数类型为:int (int,int)
  int(*p)(int, int) = Add;
  //&函数名和函数名都是函数的地址
  //p 是一个存放函数地址的指针变量 - 函数指针
  //函数指针的使用
  int ret = (*p)(3, 2);//对p指针解引用得到对应的函数,然后调用它,进行传参
  int ret = p(3, 2);//这样写也是可以的,因为可以直接把Add传给p,说明Add和p是等价的,一般的函数调用就是直接通过Add函数名来调用,所以这里可以不用对p解引用,无论p前面有多少个*都无济于事
  printf("%d\n", ret);
  return 0;
}

注意:&函数名和函数名都是函数的地址

经典例题:

//练习1
int main()
{
  ( *(void (*)())0 )();//这里:void (*)()是一个函数指针类型,指向一个没有参数,返回值为void类型的函数,这个类型被放在一个括号里,说明要进行强制类型转换,这里把0强制类型转换成(void (*)())型,此时0就成了一个地址,指向一个void ()型函数,然后再通过解引用找到这个函数,进行传参,当然这个函数没有参数,所以最后一个括号是空的
  //该代码是一次函数调用
  return 0;
}
//练习2
int main()
{
  void (*signal(int, void(*)(int))) (int);
  //signal是函数名,他有两个参数,一个是int型,一个是void(*)(int)函数指针类型,该函数指针指向一个参数为int,返回类型为void的函数
  //函数名和参数都有了,去掉函数名和参数部分剩下的就是函数的返回值类型了
  //这里signal函数的返回值类型是void(*)(int)
  //该代码是一次函数声明
  return 0;
}
//简化
typedef void(*pf_t)(int);//把void(*)(int)型重命名成pf_t,注意pf_t的位置
int main()
{
  pf_t signal(int, pf_t);
  return 0;
}


目录
相关文章
|
2月前
|
C语言
【c语言】指针就该这么学(1)
本文详细介绍了C语言中的指针概念及其基本操作。首先通过生活中的例子解释了指针的概念,即内存地址。接着,文章逐步讲解了指针变量的定义、取地址操作符`&`、解引用操作符`*`、指针变量的大小以及不同类型的指针变量的意义。此外,还介绍了`const`修饰符在指针中的应用,指针的运算(包括指针加减整数、指针相减和指针的大小比较),以及野指针的概念和如何规避野指针。最后,通过具体的代码示例帮助读者更好地理解和掌握指针的使用方法。
54 0
|
19天前
|
存储 NoSQL 编译器
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
指针是一个变量,它存储另一个变量的内存地址。换句话说,指针“指向”存储在内存中的某个数据。
72 3
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
|
19天前
|
存储 编译器 C语言
【C语言】指针大小知多少 ?一场探寻C语言深处的冒险 !
在C语言中,指针的大小(即指针变量占用的内存大小)是由计算机的体系结构(例如32位还是64位)和编译器决定的。
44 9
|
19天前
|
安全 程序员 C语言
【C语言】指针的爱恨纠葛:常量指针vs指向常量的指针
在C语言中,“常量指针”和“指向常量的指针”是两个重要的指针概念。它们在控制指针的行为和数据的可修改性方面发挥着关键作用。理解这两个概念有助于编写更安全、有效的代码。本文将深入探讨这两个概念,包括定义、语法、实际应用、复杂示例、最佳实践以及常见问题。
40 7
|
29天前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
101 13
|
22天前
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
23天前
|
算法 C语言
C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项
本文深入讲解了C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项,通过实例演示了文件操作的基本流程,帮助读者掌握这一重要技能,提升程序开发能力。
70 3
|
23天前
|
存储 算法 程序员
C 语言指针详解 —— 内存操控的魔法棒
《C 语言指针详解》深入浅出地讲解了指针的概念、使用方法及其在内存操作中的重要作用,被誉为程序员手中的“内存操控魔法棒”。本书适合C语言初学者及希望深化理解指针机制的开发者阅读。
|
29天前
|
存储 C语言 开发者
C 语言指针与内存管理
C语言中的指针与内存管理是编程的核心概念。指针用于存储变量的内存地址,实现数据的间接访问和操作;内存管理涉及动态分配(如malloc、free函数)和释放内存,确保程序高效运行并避免内存泄漏。掌握这两者对于编写高质量的C语言程序至关重要。
52 11
|
23天前
|
程序员 C语言
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门。本文深入探讨了指针的基本概念、声明方式、动态内存分配、函数参数传递、指针运算及与数组和函数的关系,强调了正确使用指针的重要性,并鼓励读者通过实践掌握这一关键技能。
34 1