深入了解指针(2)
1.数组名的理解
先前我们说过数组名其实就是数组的首元素地址
我们现在就来验证一下 这个说法到底有多少可信度
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr[0];
printf("%p\n", arr);
printf("%p\n", p);
printf("%p\n", &arr[0]);
// 运行后 我们发现地址都是一样的
return 0;
}
从这个结果看 数组名其实就是数字的首元素地址的说法是正确的
那我们再来看一种情况
printf("%d\n",sizeof(arr)); // 40
如果数组名其实就是数字的首元素地址的话 这里应该是4或者8 但是是40说明了这个说法还是有一点问题的
还要第二种情况
&数组名 这里的地址是整个数组的地址
printf("%p\n", arr);
printf("%p\n", &arr);
printf("%p\n", &arr[0]);
这三个打印的其实都是数组首元素的地址
// 在这种情况下 区别就出来了
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
printf("arr + 1 = %p\n", &arr + 1);
printf("&arr[0] + 1 = %p\n", &arr[0] + 1);
printf("arr + 1 = %p\n", arr + 1);
&arr + 1 这个实际上会跳过40个字节
来一张图片更加清晰直观
总结:(数组名的两个例外)
只有两种情况下的数组名不代表数组的首元素地址
- sizeof(数组名) 这里放的数组名代表的是整个数组,计算的是整个数字的大小,单位是字节
- &数组名 这里的数组名也是代表整个数组,取出的是 整个数组的地址
注意!
我们之前说过 每个指针都有类型 代表着它们的步长 也就是一次+1跳过多少个字节
&数组名 + 1 一次跳过了40个字节 那这是什么类型呢
这其实是一个特殊类型———— 数组指针类型
2.使用指针访问数组
在之前的学习中,我们已经知道了指针访问数组的方法
现在再来重温加深一下
int main()
{
// 通过指针来对数组进行输入和输出
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[1]);
int* p = arr; // 指针类型要和数组类型是一个类型
输入
//for (int i = 0; i < sz; i++)
//{
// scanf("%d", p + i); // p + i 等价于 &arr[i]
//}
输出
//for (int i = 0; i < sz; i++)
//{
// printf("%d ", *p);
// p++; // 要注意这里的p执行完之后已经越界访问了
//}
// 还有一种写法
for (int i = 0; i < sz; i++)
{
/*printf("%d ", *(p+i));*/ // 这里的p执行完后还是指向首元素地址
// 我们可以思考一下 arr就是首元素地址 也就是说p代表的地址和arr是一样的
// 那么*(p+i)和 *(arr+i) 其实就是等价的
// 而*(arr+i) 和 arr[i] 又是等价的
// 那么有没有一种写法是 p[i]呢
printf("%d ", p[i]); // 我们发现是可以的
}
return 0;
}
**实际上在编译器当中 (p+i)和 (arr+i) 和 arr[i] 和 p[i]的写法是完全等价的
*编译器会将arr[i] 转化成这种方式 (arr+i) 去计算
其实还有更加奇葩的写法
*(arr + i) == *(i + arr) == i[arr] // 这种写法并不建议
其实就是说 [] ----- 操作符
x[y]的x和y对于这个[]操作符来说其实就是两个操作数 顺序调换一下并不会有什么影响
3.一维数组传参的本质
void test(int arr[]) // 本质上接受的是指针
{
int sz2 = sizeof(arr) / sizeof(arr[0]); // 2
printf("sz2 = %d\n", sz2);
}
int main()
{
int arr[10] = { 0 };
int sz1 = sizeof(arr) / sizeof(arr[0]); // 10
printf("sz1 = %d\n", sz1);
test(arr);
return 0;
}
由于 传过去的是arr 本质上是首元素的地址 在64位的环境下地址的大小是8个字节 因此8/4 = 2
而sz1 的arr代表的就是整个数组的大小 就是40 / 4 = 10
4.冒泡排序
排序算法其实很多
- 冒泡排序
- 选择排序
- 出入排序
- 希尔排序
- 快速排序
- …
这里我们学习一个冒泡排序
// 冒泡排序
void bubble_sort(int arr[],int sz)
{
int flag = 0;
for (int i = 0; i < sz - 1; i++)
{
for (int j = 0; j < sz - 1; j++)
{
//if (arr[j] > arr[j + 1])
//{
// int tmp = arr[j];
// arr[j] = arr[j + 1];
// arr[j + 1] = tmp;
// flag = 1;
//}
// 也可以改成指针的形式
if (*(arr + j) > *(arr + j + 1))
{
int tmp = *(arr+j);
*(arr + j) = *(arr + j + 1);
*(arr + j + 1) = tmp;
flag = 1;
}
}
if (flag == 0)
break;
}
}
void Print(int* p, int sz)
{
for (int i = 0; i < sz; i++)
{
printf("%d ", *(p + i));
}
}
int main()
{
int arr[] = { 1,3,2,4,6,7,5,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
Print(arr, sz);
return 0;
}
5.二级指针
我们都知道指针可以指向一个地址 并把这个地址存起来 但是指针变量存下一个地址的同时也需要开辟自己的内存空间 ,那么这个指针变量也有自己的地址 那么当另一个指针指向指针变量的地址时这个指针就叫二级指针
int a = 10;
int* p = &a;
int* * pp = &p;
pp就是2级指针 放着p这个指针变量的地址
int * p 的第一个 * 代表p是指针变量 int 代表指针指向的是一个int类型的变量 代表着p这个指针变量的步长
int** pp 最右边的 * 代表了pp是一个指针变量 int*代表了 指向的对象是一个int *类型的
来看一个实际应用
int main()
{
int a = 10;
int* p = &a;
int** pp = &p;
**pp = 100;
printf("%d\n", a); // 100
return 0;
}
*pp代表解引用pp这个指针变量 找到的是p 在解引用一次 就找到了a
6.指针数组
我们知道数组有各种类型
- 有整形数组 int arr[10] 存放整形
- 有字符数组 char arr 存放字符
那么指针数组就是专门用来存放指针的数组
int* arr[5]
arr 是数组名称 [5]是数组元素个数 int * 表示数组内部存放的是指向整形的指针的类型
7.指针数组模拟二维数组
我们直接来看一个案例
//指针数组模拟二维数组
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
// 通过指针数组来储存一维数组 模拟实现二维数组
int* arr[3] = { arr1,arr2,arr3 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
printf("%d", arr[i][j]);
}
printf("\n");
}
return 0;
}
因为每个数组名都是各自首元素的地址 因此我们就可以实现上面这个代码
,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
// 通过指针数组来储存一维数组 模拟实现二维数组
int* arr[3] = { arr1,arr2,arr3 };
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 5; j++)
{
printf(“%d”, arr[i][j]);
}
printf(“\n”);
}
return 0;
}
因为每个数组名都是各自首元素的地址 因此我们就可以实现上面这个代码