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

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

目录
相关文章
|
1天前
|
C语言
C语言刷题(数组)
C语言刷题(数组)
|
1天前
|
C语言
指针进阶(C语言终)
指针进阶(C语言终)
|
1天前
|
C语言
指针进阶(回调函数)(C语言)
指针进阶(回调函数)(C语言)
|
1天前
|
存储 C语言 C++
指针进阶(函数指针)(C语言)
指针进阶(函数指针)(C语言)
|
1天前
|
编译器 C语言
指针进阶(数组指针 )(C语言)
指针进阶(数组指针 )(C语言)
|
1天前
|
机器学习/深度学习 C语言
详细解读C语言math.h中常用函数
详细解读C语言math.h中常用函数
|
1天前
|
C语言
C语言刷题(函数)
C语言刷题(函数)
|
1天前
|
编译器 程序员 Serverless