一、数组名的理解
前面我们在使用指针访问数组内容的时候,有这样的代码:
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int* p = &arr[0];
在这里我们使用 &arr[0] 的方式拿到了数组第⼀个元素的地址,但是其实数组名本来就是地址,而且是数组首元素的地址。
我们来进行测试:
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int main() { int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; printf("&arr[0] = %p\n", &arr[0]); printf("arr = %p\n", arr); return 0; }
运行结果如下:
我们发现数组名和数组首元素的地址打印出的结果一模一样,数组名其实就是数组首元素(第⼀个元素)的地址。
1.1 size of与数组名
可能会有小伙伴会有疑问:如果数组名是数组首元素的地址,那下面的代码该怎么解释呢?
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int main() { int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; printf("%d\n", sizeof(arr)); return 0; }
运行结果如下:
按照我们刚才的结论,如果arr是数组首元素地址的话,那输出的结果应该是4/8才对。这里怎么会打印40呢?
其实数组名就是数组首元素(第⼀个元素)的地址是对的,但是有两个例外:
1.sizeof(数组名),这里的数组名表示的是整个数组,sizeof(数组名)计算的是整个数组的大小,单位是字节。
2.&数组名,这里的数组名表示整个数组,&数组名:取出的是整个数组的地址。
出来以上两种情况,其余遇到的数组名都是首元素的地址。
1.2 &arr[0],arr,&arr的区别
接下来,我们再来看一段代码:
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int main() { int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; printf("&arr[0] = %p\n", &arr[0]); printf("arr = %p\n", arr); printf("&arr = %p\n", &arr); return 0; }
运行结果如下:
三个打印结果⼀模⼀样,这时候可能又有小伙伴纳闷了:那他们之间有什么区别呢?
让我们再看看下面这段代码:
#include <stdio.h> int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; printf("&arr[0] = %p\n", &arr[0]); printf("&arr[0]+1 = %p\n", &arr[0] + 1); printf("arr = %p\n", arr); printf("arr+1 = %p\n", arr + 1); printf("&arr = %p\n", &arr); printf("&arr+1 = %p\n", &arr + 1); return 0; }
输出结果如下:
我们可以得出结论:
- &arr[0]与arr+1都是跳过4个字节,相当于跳过1个整型元素。
- &arr+1跳过40个字节,相当于10个整型,也就是整个数组。
总结:arr与&arr[0]都是首元素地址,指向数组第一个元素。&arr以首元素地址表示,但是指向的是整个数组。
二、二级指针
指针变量也是变量,是变量就需要在内存中划分一块区域来存放,那指针变量的地址存放在哪里呢?
答案是二级指针。
可能听起来有点拗口,我们可以通过以下代码理解一下:
#define _CRT_SECURE_NO_ARNINGS #include <stdio.h> int main() { int a = 10; int * p = &a;//p是一级指针 //a是整形变量,占用4个字节的空间,&a拿到的就是a所占4个字节的第一个字节的地址 //p是指针变量,占用4/8个字节的空间,p也有自己的地址,&p就拿到了p的地址 int* * pp = &p; **pp--> a; //pp也是指针变量,pp是二级指针变量 int** * ppp = &pp;//ppp是三级指针 //... return 0; }
对于二级指针的运算有:
(1)对pp解引用,找到p,也就是说*pp==p
(2)对pa解引用,找到a,也就是说**pp==a
1. int a = 10; 2. *pp = &a;
**pp = 10; //等价于*pp = &a; //等价于**pp = a; //等价于a = 30;
依次内推我们可以衍生出三级指针,四级指针。
三、指针与数组的关系
3.1 使用指针访问数组
假设有一个一维数组和二维数组:
1. int arr[5]={1,2,3,4,5}; 2. int arr[3][3]={{1,2,3},{4,5,6},{7,8,9}}
我们要访问他的每个元素有哪些方法呢?
3.1.1 数组访问
int arr1[5] = { 1,2,3,4,5 }; for (int i = 0; i < 5; i++) { printf("%d ", arr1[i]); }
int arr2[3][3] = { {1,2,3},{4,5,6},{7,8,9} }; for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { printf("%d ", arr2[i][j]); } printf("\n"); }
3.1.2 指针访问
1. for (int i = 0; i < 5; i++) 2. { 3. printf("%d ", *(arr1+i)); 4. }
for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { printf("%d ", *(*(arr2 + i) + j)); } }
通过对上面代码的观察,我们可以总结如下规律:
- arr[i]与*(arr+i)等价。
- arr[i][j]与*(*(arr+i)+j)等价。
3.2 指针数组
3.2.1 指针数组的概念
首先,我们得思考一个问题:指针数组是指针还是数组?
我们类比一下:
整形数组—存放整形的数组 int arr[10]
字符数组—存放字符的数组 char ch[5]
那么顾名思义指针数组就应该是存放指针的数组,指针数组的每个元素都是指针,用来存放地址的。
代码示例如下:
int main() { int arr1[] = { 1,2,3 }; int arr2[] = { 4,5,6 }; int arr3[] = { 7,8,9 }; int* parr[3] = { arr1,arr2,arr3 }; printf("%p\n", parr);//打印指针数组首元素地址,也就是打印存放arr1空间的地址 printf("%p\n", parr[0]);//arr1数组首元素地址 printf("%p\n", *parr);//arr1首元素地址 printf("%d\n", **parr);//相当于对arr1首元素地址解引用,指的的是1 printf("%d\n", *parr[0]);//也相当于对arr1首元素地址解引用,为1 printf("%d\n", *parr[1]);//相当于对arr2首元素地址解引用,为4 return 0; }
运行结果如下:
012FFE30
012FFE6C
012FFE6C
1
1
4
上面的代码是正确的,但是我们在实际使用的时候很少会这样用。事实上,我们更多时候用指针数组来模拟二维数组。
3.2.2 用指针数组来模拟二维数组
指针数组模拟二维数组是什么意思呢?
按照惯例,还是为大家先介绍一段代码:
#define _CRT_SECURE_NO_ARNINGS #include <stdio.h> int main() { int arr1[] = { 1, 2, 3, 4, 5 }; int arr2[] = { 1, 2, 3, 4, 5 }; int arr3[] = { 1, 2, 3, 4, 5 }; 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]); //parr[i] == *(parr+i) //parr[i][j] == *(*(parr+i)+j) } printf("\n"); } return 0; }
运行结果如下:
1 2 3 4 5
1 2 3 4 5
1 2 3 4 5
parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数组中的元素。
我们用上述代码模拟实现了⼆维数组的效果,但事实上并不是真正的⼆维数组,因为二维数组在内存中是连续存储的,而模拟出来的数组内存存储并不连续。
3.3 数组指针
之前我们学习了指针数组,指针数组是⼀种数组,数组中存放的是地址(指针)。那数组指针变量是指针,还是数组呢?
答案是指针。
我们已经熟悉:
1、整形指针变量: int* pint;存放的是整形变量的地址,是指向整形数据的指针。int n = 100; int* p = &n;
2、浮点型指针变量: float* pf;存放浮点型变量的地址,是指向浮点型数据的指针。float ch = ‘w’; float* pc = &w;
那么数组指针变量应该是:存放的是数组的地址,是指向数组的指针变量。
那么数组指针该怎么表示呢?
一些小伙伴心想:数组的一般形式为int arr[10],那么数组指针就应该表示为int [10]* p,这就是“经典的错误,标准的零分”!
事实上,数组指针的正确是写法应该是:int (*p)[10]。
参考如下代码:
int main() { int arr[5] = { 1,2,3,4,5 }; int(*parr)[5] = &arr; //对数组名取地址代表整个数组的地址 printf("%p\n", parr);//整个数组的地址一般用数组首元素地址表示 printf("%p\n", parr[0]);//相当于*(parr+0)==arr,首元素地址 printf("%p\n", *parr);//首元素地址 printf("%d\n", **parr);//相当于对首元素地址解引用,指的的是1 printf("%d\n", *parr[0]);//也相当于对首元素地址解引用,为1 printf("%d\n", *parr[1]);//等价于*(*(parr+1)),parr+1跳过一个数组大小的地址,越界访问 return 0; }
运行结果如下:
012FF6F0
012FF6F0
012FF6F0
1
1
-858993460(越界访问,随机数)
3.4 指针数组与数组指针的区别
那为何指针数组与数组指针是这么表示的呢,可能有许多小伙伴区别不清楚指针数组与数组指针,但是如果写成指针的数组,数组的指针,可能更好理解。接下来让我们具体分析一下吧?
我们要首先明确一个优先级的顺序:()>[]>*
在int*parr[]中,parr先与[]结合(数组),而parr前面声明的变量类型是int*。所以这是一个数组,数组中每个元素的类型是int*的指针,这一类我们统称为指针数组。
在int(*parr)[]中,parr先与*结合(指针),而后除开(*parr)是一个int []的数组类型。所以这是一个指针,这个指针指向的是一个数组,这一类我们称为数组指针。
3.5 字符型指针
在指针类型中有⼀种类型为字符指针,用符号char*来表示。
它的一般的使用方法如下:
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int main() { char ch = 'w'; char *pc = &ch; *pc = 'w'; return 0; }
另外,他还有另一种使用方式,:
//const可以省略 const char* p1 = "im betty"; const char* p2 = "abc";
我们知道const修饰在*前,不能改变指针变量所指向的值,所以这个字符串是不能改变的,这种字符串我们称为常量字符串
那以下的输出结果会是什么呢?
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> int main() { char* p = "abcdefghi";//这里是将abcdefgi\0字符串存放到p中了吗? printf("%c\n", *p); return 0; }
我们惊奇的发现,输出的结果是a
这里特别容易让小伙伴们以为是把字符串 “abcdefghi” 放到字符指针 p 里了,但本质上是把字符串 “abcdefghi” 的首字符 “a” 的地址放到了p中。
在《剑指offer》中有这么一道题:
#include <stdio.h> int main() { char str1[] = "hello bit."; char str2[] = "hello bit."; const char* str3 = "hello bit."; const char* str4 = "hello bit."; 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指向的是⼀个同⼀个常量字符串。
C/C++会把常量字符串存储到单独的⼀个内存区域(常量区),当⼏个指针指向同⼀个字符串的时候,他们实际会指向同⼀块内存。但是⽤相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块,每个数组地址就会不同。所以str1和str2不同,str3和str4相同。
3.6 数组的传参
3.6.1 一维数组的传参
首先先问小伙伴们一个问题:我们之前都是在函数外部计算数组的元素个数,那我们可以把数组传给⼀个函数后,在函数内部求数组的元素个数吗?
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> void test(int arr[]) { int sz2 = sizeof(arr) / sizeof(arr[0]); printf("sz2 = %d\n", sz2); } int main() { int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int sz1 = sizeof(arr) / sizeof(arr[0]); printf("sz1 = %d\n", sz1); test(arr); return 0; }
运行结果如下:
我们发现在函数内部是没有正确获得数组的元素个数。
这是为什么呢?
这就要用到我们刚才所讲的知识:数组名是数组首元素的地址。数组在传参的时候,传递的其实是数组名,也就是说数组传参本质上传递的是数组首元素的地址。
所以函数形参部分理论上应该使用指针变量来接收首元素的地址。那么在函数内部我们写size of(arr) /size of(arr[0])来计算的其实是⼀个地址的大小(单位字节)而不是数组的大小(单位字节)。正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。
所以我们有了另一种写法,就是指针传参:
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> void print1(int arr[])//参数写成数组形式,本质上还是指针 { printf("%d\n", sizeof(arr)); } void print2(int* parr)//参数写成指针形式 { printf("%d\n", sizeof(*(parr+i));//计算⼀个指针变量的⼤⼩ } int main() { int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; print1(arr); print2(arr); return 0; }
两个结果都是4或者8!
总结:⼀维数组传参,形参的部分可以写成数组的形式,也可以写成指针的形式
3.6.2 经典排序算法之冒泡排序
冒泡排序的核心思想就是:两两相邻的元素进行比较。
下面举一个例子为大家介绍冒泡排序:
#define _CRT_SECURE_NO_WARNINGS #include <stdio.h> void sort(int* arr, int sz) { //确定冒泡排序的趟数 int i = 0; for (i = 0; i < sz; i++) { int j = 0; for (j = 0; j < sz - 1 - i; j++) { if (*(arr + j) > arr[j + 1])//这里两边可以改用一样的写法 { //交换 int tmp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = tmp; } } } } void print(int* arr, int sz) { int i = 0; for (i = 0; i < sz; i++) { printf("%d ", *(arr + i)); } } int main() { int arr[] = { 9, 3, 2, 5, 4, 7, 8, 6, 1 }; //我们需要排序,排为升序 int sz = sizeof(arr) / sizeof(arr[0]); sort(arr, sz); print(arr, sz); return 0; }
3.6.3 二维数组的传参
过去我们有⼀个⼆维数组的需要传参给⼀个函数的时候,我们这样写:
void print(int arr[][5])//行可以省略,列不可以 { int i = 0; for (i = 0; i < 3; i++) { int j = 0; for (j = 0; j < 5; j++) { printf("%d ", 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}}; print(arr);//将数组传递给print函数 return 0; }
运行结果为:
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7
这里实参是⼆维数组,形参也写成⼆维数组的形式,那还有什么其他的写法吗?
首先我们再次理解⼀下⼆维数组,二维数组的每一行是一个一维数组,这个一维数组可以看作是二维数组的一个元素,所以二维数组也可以认为是一维数组的数组。
arr数组
下标 | 0 | 1 | 2 | 3 | 4 |
0 | 1 | 2 | 3 | 4 | 5 |
1 | 2 | 3 | 4 | 5 | 6 |
2 | 3 | 4 | 5 | 6 | 7 |
根据数组名是数组首元素的地址这个规则,⼆维数组的数组名表示的就是第一行的地址,即是一维数组的地址。根据上面的实例,第一行的⼀维数组的类型就是 int [5] ,所以第一行的地址类型就是数组指针类型 int(*)[5] 。
这就意味着⼆维数组传参本质上也是传递了地址,传递的是第一行这个一维数组的地址,那么形参也是可以写成指针形式的。
void test(int (*arr)[5], int r, int c) { int i = 0; for (i = 0; i < r; i++) { int j = 0; for (j = 0; j < c; j++) { //printf("%d ", arr[i][j]); //printf("%d ", *(*(arr + i) + j)); printf("%d ", (*(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 } }; //二维数组传参,传递的是首元素的地址,也就是第一行的地址 //形参的部分就可以写成指向第一行的地址 test(arr, 3, 5); return 0; }