还在因为指针头大吗,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;
}
对于str3
和str4
:
它们是内容相同的常量字符串,其实它们指向的是同一个字符串的首地址,所以
str3
==str4
。与const
无关,编译器可能会警告要求加const
。
对于str1
和str2
:
它们是两个内容相同的数组,但不是同一个内存空间。指向的都是各自的首元素地址,所以
str1
≠str2
。
从内存视角:
常量字符串存放在常量区,四个指针变量存放在栈区。指向常量字符串的指针变量存放的地址是同一个。
从初始化视角:
- 在定义字符指针
str3
和str4
的时候,我直接将它们指向同一个常量字符串,就相当于两个指针指向同一个地址;- 在定义数组时,是分别定义了两个数组,开辟了两个不同的内存空间,只是它们的内容相同。后面将数组名作比较,从形式上看起来跟
str3
和str4
一样好像是指针变量,其实数组名在这里仅理解为(换个名字)数组首地址为好。所以对数组加上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
数组名的地址- 首先它是一个指针,所以指针变量名要先跟*结合,
*p = &arr
- 其次它是指向一个数组的,所以要有
[ ]
,但不可以是这样:*p[ ] = &arr
,因为p
会先跟[ ]
结合,变成指针数组了;所以加上括号:(*p)[ ] = &arr
(*p)[ ] = &arr
表示的是指针p
指向数组,因为有10个元素,所以有(*p)[10] = &arr
- 数组的每个元素的类型是
int
型,所以有int (*p)[10] = &arr
- 去掉指针变量名即为指针的类型:
int (*)[10]
- 首先它是一个指针,所以指针变量名要先跟*结合,
小小结:
p
是一个指针,指向数组,所以p
叫数组指针,专门存放一个数组的地址。
注意:
- 以上提到的
int*
型等,计算的时候不用管*
,它只是表示变量是指针int (*p)[10]
,这个p
是4/8个字节,存放一个地址。这个地址指向的数组能存放10个元素。而不是p
存放数组中的十个元素的地址int (*p)[10]
这种写法是固定的,且10
不能省略。这里是符合在定义二维数组时不能省略列的规则的。
//练习
char* arr[5];
p = &arr;
//如何定义p?p的类型是?
//以上述思路复述
//char* (*p)[5]
//char* (*)[5]
小结
如何理解数组名?
- 首元素地址
- 整个数组:
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]
的理解
- 首先需要用一个指针变量接收数组名地址,所以有
(*p)
,要加括号- 其次一维数组的数组名地址指向的是整个数组,而数组由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;
}
注意:
这里数组名是第一行的地址,即一行五个整型元素组成 的一维数组。因此形参可以写成指针的形式。
怎么理解数组名是第一行的地址?
- 二维数组是“升维”后的一维数组,就像两条线组成了xoy坐标系一样,二维数组可以形象地认为是由若干一维数组组成的。
- 一维数组的数组名表示首元素的地址,用它来表示整个数组。
- 那么一维数组“升维”后,数组名就是第一行首元素地址,用它代表它所在的行的一维数组。
函数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]
的理解
- 首先需要用一个指针变量接收数组名,所以有
(*p)
,要加括号- 其次二维数组的数组名指向的是第一行的首元素,代表着第一行。而第一行(每一行)由5个
int
型的元素组成,所以由int (*p)[5]
对二维数组没有传数组名的地址&arr
,但仍然用int(*p)[5]
这种形式的思考
上面说到二维数组是一维数组的升维,一维数组的数组名的地址代表整个数组,二维数组仅数组名就代表第一行首个元素地址,这个地址代表了第一行,所以从指向/代表的对象来看,两者的意思是相同的,都是代表某一“行”,所以二维数组不用取地址。
因为下面要对第一行首个元素以外的元素进行操作,所以我必须得到每“行”的地址,这样对“行”进行操作。
对*(*(p + i) + j)
的理解(由内往外看)
- 这里数组名是第一行的地址,即一行五个整型元素组成 的一维数组。
p
接收了数组名arr
,指向数组的第一行- 首先知道数组是一块连续存放的内存,
p
指向第一行。而p
的类型是int(*) [5]
*,*所以+1后p
跳过一行(5个int
型元素),然后指向下一行。所以p+i
表示p
指向第i
行;*(p+i)
表示得到第i
行的数组名(参考上面的一维数组,这里的数组名指的是每行组成二维数组的一维数组的数组名),数组名又是首元素地址,所以*(p+i)
就是第i
行第一个元素的地址*(p + i) + j
表示在i
行第j
个元素的地址,*(*(p + i) + j)
表示得到第i
行第j
个元素的值。等价于p[i][j]
的写法。
注意:
虽
*(*(p + i) + j)
等价于p[i][j]
,后者固然简单易用,但要掌握前者的写法。实际上,编译器在编译时会将后者转化为前者。