还在因为指针头大吗,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],后者固然简单易用,但要掌握前者的写法。

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

目录
相关文章
|
3月前
使用指针访问数组元素
【10月更文挑战第30天】使用指针访问数组元素。
53 3
|
2月前
|
IDE 编译器 开发工具
【C语言】全面系统讲解 `#pragma` 指令:从基本用法到高级应用
在本文中,我们系统地讲解了常见的 `#pragma` 指令,包括其基本用法、编译器支持情况、示例代码以及与传统方法的对比。`#pragma` 指令是一个强大的工具,可以帮助开发者精细控制编译器的行为,优化代码性能,避免错误,并确保跨平台兼容性。然而,使用这些指令时需要特别注意编译器的支持情况,因为并非所有的 `#pragma` 指令都能在所有编译器中得到支持。
177 41
【C语言】全面系统讲解 `#pragma` 指令:从基本用法到高级应用
|
2月前
|
存储 算法 C语言
【C语言】字符常量详解
字符常量是C语言中处理字符数据的重要工具。通过单引号括起一个字符,我们可以方便地使用字符常量进行字符判断、字符运算和字符串处理等操作。理解字符常量的表示方法、使用场景和ASCII码对应关系,对于编写高效的C语言程序至关重要。
201 11
|
2月前
|
存储 C语言 开发者
【C语言】格式化输出占位符及其标志字符详解(基于ISO/IEC 9899:2024)
在C语言中,格式化输出通过 `printf` 函数等格式化输出函数来实现。格式说明符(占位符)定义了数据的输出方式,标准ISO/IEC 9899:2024(C23)对这些格式说明符进行了详细规定。本文将详细讲解格式说明符的组成部分,包括标志字符、宽度、精度、长度修饰符和类型字符,并适当增加表格说明。
64 6
|
3月前
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
3月前
|
存储 C语言 计算机视觉
在C语言中指针数组和数组指针在动态内存分配中的应用
在C语言中,指针数组和数组指针均可用于动态内存分配。指针数组是数组的每个元素都是指针,可用于指向多个动态分配的内存块;数组指针则指向一个数组,可动态分配和管理大型数据结构。两者结合使用,灵活高效地管理内存。
|
3月前
|
容器
在使用指针数组进行动态内存分配时,如何避免内存泄漏
在使用指针数组进行动态内存分配时,避免内存泄漏的关键在于确保每个分配的内存块都能被正确释放。具体做法包括:1. 分配后立即检查是否成功;2. 使用完成后及时释放内存;3. 避免重复释放同一内存地址;4. 尽量使用智能指针或容器类管理内存。
|
3月前
|
存储 NoSQL 编译器
C 语言中指针数组与数组指针的辨析与应用
在C语言中,指针数组和数组指针是两个容易混淆但用途不同的概念。指针数组是一个数组,其元素是指针类型;而数组指针是指向数组的指针。两者在声明、使用及内存布局上各有特点,正确理解它们有助于更高效地编程。
|
3月前
|
存储 人工智能 算法
数据结构实验之C 语言的函数数组指针结构体知识
本实验旨在复习C语言中的函数、数组、指针、结构体与共用体等核心概念,并通过具体编程任务加深理解。任务包括输出100以内所有素数、逆序排列一维数组、查找二维数组中的鞍点、利用指针输出二维数组元素,以及使用结构体和共用体处理教师与学生信息。每个任务不仅强化了基本语法的应用,还涉及到了算法逻辑的设计与优化。实验结果显示,学生能够有效掌握并运用这些知识完成指定任务。
81 4
|
3月前
使用指针访问数组元素
【10月更文挑战第31天】使用指针访问数组元素。
63 2