回顾:
指针和指针变量
两者的区别以及不同类型存在的意义请看
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]
,后者固然简单易用,但要掌握前者的写法。实际上,编译器在编译时会将后者转化为前者。