还在因为指针头大吗,6000字的白话让你掌握字符指针/指针数组/数组指针的用法【C语言/指针/进阶/程序员内功修炼】【上】

简介: 还在因为指针头大吗,6000字的白话让你掌握字符指针/指针数组/数组指针的用法【C语言/指针/进阶/程序员内功修炼】【上】

回顾:

指针和指针变量

两者的区别以及不同类型存在的意义请看

指针基础必备知识【C语言/初阶】

1. 字符指针

1.1 例1

int main()
{
  const char* pstr = "hello bit.";//这里是把一个字符串的首字母地址放到pstr指针变量里了
  printf("%s\n", pstr);//没有解引用
  printf("%c\n",*(pstr));//打印首字母
  return 0;
}

如果不加const,后面再修改*pstr的值,程序会崩溃。

因为"hello bit."是一个常量字符串,不能修改。

字符串和数组类似,都是在内存中连续存放的地址,指向首位即指向整体

1.2 例2

#include <stdio.h>
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");
  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;
}

对于str3str4

它们是内容相同的常量字符串,其实它们指向的是同一个字符串的首地址,所以str3==str4。与const无关,编译器可能会警告要求加const

对于str1str2

它们是两个内容相同的数组,但不是同一个内存空间。指向的都是各自的首元素地址,所以str1str2

从内存视角:

常量字符串存放在常量区,四个指针变量存放在栈区。指向常量字符串的指针变量存放的地址是同一个。

从初始化视角:

  1. 在定义字符指针str3str4的时候,我直接将它们指向同一个常量字符串,就相当于两个指针指向同一个地址;
  2. 在定义数组时,是分别定义了两个数组,开辟了两个不同的内存空间,只是它们的内容相同。后面将数组名作比较,从形式上看起来跟str3str4一样好像是指针变量,其实数组名在这里仅理解为(换个名字)数组首地址为好。所以对数组加上const,并不能实现和常量字符串同样的效果。

2. 指针数组

类比:

整型数组:存放整型的数组;

字符数组:存放字符的数组;

指针数组:存放指针的数组。

例1

int main()
{
  //int* arr[10];//存放整型指针的数组
  //char* ch[5];//存放字符指针的数组
  int a = 10;
  int b = 20;
  int c = 30;
  int* p1 = &a;
  int* p2 = &b;
  int* p3 = &c;
  int* arr[3] = { &a, &b, &c };//arr就是一个指针数组
  int i = 0;
  for (i = 0; i < 3; i++)//打印 a b c
  {
    printf("%d ", *(arr[i]));
  }
  return 0;
}

例2

int main()
{
  int arr1[5] = { 1,2,3,4,5 };
  int arr2[5] = { 2,3,4,5,6 };
  int arr3[5] = { 3,4,5,6,7 };
  int* parr[3] = { arr1, arr2, arr3 };
  int i = 0;
//模拟二维数组
  for (i = 0; i < 3; i++)
  {
    int j = 0;
    for (j = 0; j < 5; j++)
    {
      //printf("%d ", parr[i][j]);//这里直接用二维数组的形式打印
      printf("%d ", *(parr[i]+j)); //数组的写法
    }
    printf("\n");
  }
  return 0;
}

*(parr[i]+j) 的理解:

先看括号:从左往右看,parr[i]是数组名即一维数组的首元素地址。假设i==0+j后表示*(arr1 + j),这其实就是在遍历一维数组。其实编译器在编译时会将*(parr[i]+j) 转化为*(arr1 + j),这两种写法等价,只不过在这里我要遍历所有元素,所以用前者的方式更方便。

3. 数组指针

3.1 定义

类比:

int main()
{
  int a = 10;
  int *p = &a;//整型指针 - 指向整型的指针, 存放整型变量的地址
  char ch = 'w';
  char* pc= &ch;//字符指针 - 指向字符的指针,存放的是字符变量的地址
  //数组指针 - 指向数组的指针
  int* p1[10];
  int(*p2)[10];
  return 0;
}

int* p1[10]的理解:

p1首先和[10]结合,表示p1是一个数组,int*表示p1的类型是指针数组

int(*p2)[10]的理解:

首先(*p2)表示p2是一个指针(变量),然后[10]表示它指向了10个元素,最后int表示它指向的10个元素的类型是int

3.2 数组名和数组名的地址(&数组名)

例子

int main()
{
  int a = 10;
  int* p = &a;
  int arr[10] = {0};
  //数组是首元素的地址
  printf("%p\n", arr);
  printf("%p\n", arr+1);
  printf("%p\n", &arr[0]);
  printf("%p\n", &arr[0]+1);
  printf("%p\n", &arr);
  printf("%p\n", &arr+1);
  return 0;
}

结果

理解

对于前两组:

因为数组名是首元素地址,以指针的视角看的话,它们的类型是int*型,对它+1得到的地址就相当于跳过一个int*型的地址,也就是+4个字节

对于最后一组:

数组名的地址(&arr),因为是一个地址,所以用一个指针变量p来存放它。以指针的视角看:取arr的地址出来,那么它的类型是int (*)[10] 。再对它+1,就相当于跳过一个有10个int型元素的数组的地址,也就是+40个字节

  • 这个指针的类型应该这样看:
    这里假设我要定义并初始化p为指针变量存下arr数组名的地址
  1. 首先它是一个指针,所以指针变量名要先跟*结合,*p = &arr
  2. 其次它是指向一个数组的,所以要有[ ],但不可以是这样:*p[ ] = &arr,因为p会先跟[ ]结合,变成指针数组了;所以加上括号:(*p)[ ] = &arr
  3. (*p)[ ] = &arr表示的是指针p指向数组,因为有10个元素,所以有(*p)[10] = &arr
  4. 数组的每个元素的类型是int型,所以有int (*p)[10] = &arr
  5. 去掉指针变量名即为指针的类型int (*)[10]

小小结:

  1. p是一个指针,指向数组,所以p数组指针,专门存放一个数组的地址。

注意:

  1. 以上提到的int*型等,计算的时候不用管*,它只是表示变量是指针
  2. int (*p)[10],这个p是4/8个字节,存放一个地址。这个地址指向的数组能存放10个元素。而不是p存放数组中的十个元素的地址
  3. int (*p)[10]这种写法是固定的,且10不能省略。这里是符合在定义二维数组时不能省略列的规则的。
//练习
char* arr[5];
p = &arr;
//如何定义p?p的类型是?
//以上述思路复述
//char* (*p)[5]
//char* (*)[5]

小结

如何理解数组名?

  1. 首元素地址
  2. 整个数组:sizeof(数组名)&数组名

3.3 数组指针的使用

3.3.1 一维数组

注意:

数组指针一般不用在一维数组

例子:

传首元素地址/数组名

//
//形参写出数组
//void print1(int arr[], int sz)
//{
//  int i = 0;
//  for (i = 0; i < sz; i++)
//  {
//    printf("%d ", arr[i]);
//  }
//  printf("\n");
//}
//形参写成指针的形式
void print1(int* arr, int sz)
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    printf("%d ", *(arr + i));
  }
  printf("\n");
}
int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  //写一个函数打印arr数组的内容
  int sz = sizeof(arr) / sizeof(arr[0]);
  print1(arr, sz);
  return 0;
}

传数组名地址(仅举例)

//这不是推荐的写法(仅举例理解)
void print1(int (*p)[10], int sz)
{
  int i = 0;
  for (i = 0; i < sz; i++)
  {
    **     //非常重要**
    **//*(&arr)=*p,相当于取出了整个数组,而代表整个数组的是数组名**
    **    //所以*p 相当于数组名,↓数组名又是首元素的地址**
    **   //所以*p就是&arr[0]**
    printf("%d ", *(*p + i));//可以但没必要
  }
  printf("\n");
}
int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  //写一个函数打印arr数组的内容
  int sz = sizeof(arr) / sizeof(arr[0]);
  //
  print1(&arr, sz);//**这里是&arr**
  return 0;
}

int (*p)[10]的理解

  1. 首先需要用一个指针变量接收数组名地址,所以有(*p),要加括号
  2. 其次一维数组的数组名地址指向的是整个数组,而数组由10个int型的元素组成,所以由int (*p)[10]

3.3.2 二维数组

函数1

void print2(int arr[3][5], int c, int r)
{
  int i = 0;
  for (i = 0; i < c; i++)
  {
    int j = 0;
    for (j = 0; j < r; j++)
    {
      printf("%d ", arr[i][j]);
    }
    printf("\n");
  }
}
int main()
{
  int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
  //写一个函数打印arr数组的内容
  int sz = sizeof(arr) / sizeof(arr[0]);
  //
  print1(&arr, sz);//**这里传的是&arr**
  return 0;
}

注意:

这里数组名是第一行的地址,即一行五个整型元素组成 的一维数组。因此形参可以写成指针的形式。

怎么理解数组名是第一行的地址?

  1. 二维数组是“升维”后的一维数组,就像两条线组成了xoy坐标系一样,二维数组可以形象地认为是由若干一维数组组成的。
  2. 一维数组的数组名表示首元素的地址,用它来表示整个数组。
  3. 那么一维数组“升维”后,数组名就是第一行首元素地址,用它代表它所在的行的一维数组。

函数1形参的指针写法

void print2(***int (*p)[5]***, int c, int r)
{
  int i = 0;
  for (i = 0; i < c; i++)
  {
    int j = 0;
    for (j = 0; j < r; j++)
    {
      //p+i是指向第i行的
      //*(p+i)相当于拿到了第i行,也相当于第i行的数组名
      //数组名表示首元素的地址,*(p+i) 就是第i行第一个元素的地址
      printf("%d ", *(*(p + i) + j));
      //printf("%d ", p[i][j]);
    }
    //arr[i]
    //*(arr+i)
    //    ↓
    //arr[i][j]
    //*(arr+i)[j]
    //*(*(arr+i)+j)
    //以上三者等价
    printf("\n");
  }
}
int main()
{
  int arr[3][5] = { {1,2,3,4,5},{2,3,4,5,6},{3,4,5,6,7} };
  //int (*ptr)[3][5] = &arr;
  //写一个函数,打印arr数组
  print2(arr, 3, 5);//**这里没有传&arr**
  return 0;
}

int (*p)[5]的理解

  1. 首先需要用一个指针变量接收数组名,所以有(*p),要加括号
  2. 其次二维数组的数组名指向的是第一行的首元素,代表着第一行。而第一行(每一行)由5个int型的元素组成,所以由int (*p)[5]

对二维数组没有传数组名的地址&arr,但仍然用int(*p)[5]这种形式的思考

  1. 上面说到二维数组是一维数组的升维,一维数组的数组名的地址代表整个数组,二维数组仅数组名就代表第一行首个元素地址,这个地址代表了第一行,所以从指向/代表的对象来看,两者的意思是相同的,都是代表某一“行”,所以二维数组不用取地址。
  2. 因为下面要对第一行首个元素以外的元素进行操作,所以我必须得到每“行”的地址,这样对“行”进行操作。

*(*(p + i) + j)的理解(由内往外看)

  1. 这里数组名是第一行的地址,即一行五个整型元素组成 的一维数组。p接收了数组名arr,指向数组的第一行
  2. 首先知道数组是一块连续存放的内存,p指向第一行。而p的类型是int(*) [5]*,*所以+1后p跳过一行(5个int型元素),然后指向下一行。所以p+i表示p指向第i行;*(p+i)表示得到第i行的数组名(参考上面的一维数组,这里的数组名指的是每行组成二维数组的一维数组的数组名),数组名又是首元素地址,所以*(p+i)就是第i行第一个元素的地址
  3. *(p + i) + j表示在i行第j个元素的地址,*(*(p + i) + j)表示得到第i行第j个元素的值。等价于p[i][j]的写法。

注意:

*(*(p + i) + j)等价于p[i][j],后者固然简单易用,但要掌握前者的写法。

实际上,编译器在编译时会将后者转化为前者。

目录
相关文章
|
21天前
|
存储 NoSQL 编译器
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
指针是一个变量,它存储另一个变量的内存地址。换句话说,指针“指向”存储在内存中的某个数据。
74 3
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
|
21天前
|
存储 编译器 C语言
【C语言】指针大小知多少 ?一场探寻C语言深处的冒险 !
在C语言中,指针的大小(即指针变量占用的内存大小)是由计算机的体系结构(例如32位还是64位)和编译器决定的。
46 9
|
21天前
|
安全 程序员 C语言
【C语言】指针的爱恨纠葛:常量指针vs指向常量的指针
在C语言中,“常量指针”和“指向常量的指针”是两个重要的指针概念。它们在控制指针的行为和数据的可修改性方面发挥着关键作用。理解这两个概念有助于编写更安全、有效的代码。本文将深入探讨这两个概念,包括定义、语法、实际应用、复杂示例、最佳实践以及常见问题。
40 7
|
24天前
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
24天前
|
算法 C语言
C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项
本文深入讲解了C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项,通过实例演示了文件操作的基本流程,帮助读者掌握这一重要技能,提升程序开发能力。
82 3
|
25天前
|
存储 算法 程序员
C 语言指针详解 —— 内存操控的魔法棒
《C 语言指针详解》深入浅出地讲解了指针的概念、使用方法及其在内存操作中的重要作用,被誉为程序员手中的“内存操控魔法棒”。本书适合C语言初学者及希望深化理解指针机制的开发者阅读。
|
24天前
|
程序员 C语言
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门
C语言中的指针既强大又具挑战性,它像一把钥匙,开启程序世界的隐秘之门。本文深入探讨了指针的基本概念、声明方式、动态内存分配、函数参数传递、指针运算及与数组和函数的关系,强调了正确使用指针的重要性,并鼓励读者通过实践掌握这一关键技能。
35 1
|
21天前
|
存储 C语言 开发者
【C语言】字符串操作函数详解
这些字符串操作函数在C语言中提供了强大的功能,帮助开发者有效地处理字符串数据。通过对每个函数的详细讲解、示例代码和表格说明,可以更好地理解如何使用这些函数进行各种字符串操作。如果在实际编程中遇到特定的字符串处理需求,可以参考这些函数和示例,灵活运用。
40 10
|
21天前
|
存储 程序员 C语言
【C语言】文件操作函数详解
C语言提供了一组标准库函数来处理文件操作,这些函数定义在 `<stdio.h>` 头文件中。文件操作包括文件的打开、读写、关闭以及文件属性的查询等。以下是常用文件操作函数的详细讲解,包括函数原型、参数说明、返回值说明、示例代码和表格汇总。
42 9
|
21天前
|
存储 Unix Serverless
【C语言】常用函数汇总表
本文总结了C语言中常用的函数,涵盖输入/输出、字符串操作、内存管理、数学运算、时间处理、文件操作及布尔类型等多个方面。每类函数均以表格形式列出其功能和使用示例,便于快速查阅和学习。通过综合示例代码,展示了这些函数的实际应用,帮助读者更好地理解和掌握C语言的基本功能和标准库函数的使用方法。感谢阅读,希望对你有所帮助!
32 8