C语言:指针(2)
1. 数组名的理解
在前面我们知道,当数组名单独使用的时候代表的数组中首元素的地址,也就是说,下面的两句代码是等效的:
int arr[10] = { 0 };
int *p1 = arr;
int *p2 = &arr[0];
我们可以尝试将地址打印出来检测是否正确:
int main()
{int arr[10] = { 0 };int *p1 = arr;int *p2 = &arr[0];printf("p1 = %p\np2 = %p\n",p1,p2);return 0;
}
p1 = 000000381DDFFC5C
p2 = 000000381DDFFC5C
我们发现,两组打印结果相同,证明上面的两句代码的效果是一样的。
但是,我们又会想到,之前在计算数组的大小以及数组中元素个数时,我们会将代码写成如下形式:
int main()
{int arr[10] = { 0 };int a = sizeof(arr);int b = sizeof(arr)/ sizeof(arr[0]);printf("a = %d b = %d\n",a,b);
}
我们发现,a 输出值是40,可是如果 arr 代表的是首元素的地址的话,这里的 a 值结果应该是4或8才对,那这又是什么原因呢?
其实,这是因为,大部分情况下,数组名都是代表了数组中首元素的地址,但是有两种情况例外:
1. sizeof(数组名):当在sizeof()单独放入数组名的话,这个时候,数组名表示整个数组,计算出的是整个数组的大小,单位是字节。
2.&数组名:当取地址操作符与数组名一起使用时,数组名代表的是整个数组,取出的地址计算整个数组的地址(整个数组的地址与首元素的地址是有区别的)。
除此之外,任何地方使用数组名,数组名都表示首元素的地址。
为了体现不同,我们可以使用下面的代码尝试一下:
int main()
{int arr[10] = { 0 };printf("&arr[0] = %p\n",&arr[0]);printf("&arr = %p\n",&arr);printf("arr = %p\n",arr);printf("&arr[0] + 1 = %p\n",&arr[0] + 1);printf("&arr + 1 = %p\n",&arr + 1);printf("arr + 1 = %p\n",arr + 1);return 0;
}
&arr[0] = 00000039D51FF650
&arr = 00000039D51FF650
arr = 00000039D51FF650
&arr[0] + 1 = 00000039D51FF654
&arr + 1 = 00000039D51FF678
arr + 1 = 00000039D51FF654
我们可以发现,上面三行代码的打印的结果完全相同,证明arr,&arr[0],&arr所指向的都是一个地址,但是当我们让指针与整数进行加法运算后,我们发现,arr和&arr[0]加上1后都只跳过了4个字节,而&arr则跳过了40个字节。
这是因为,arr与arr[0]都是首元素的地址,所以+1只能跳过一个元素,即4个字节。而&arr是数组的地址,所以+1后就是跳过一个数组,在这里就跳过了40个字节。
2. 使用指针访问数组
了解了上面的知识后,我们就可以根据数组的特点,用指针来访问元素:
int main()
{int i;int arr[10] = { 0 };int *p = arr;for(i = 0;i < 10;i++)*(p + i) = i;for(i = 0;i < 10;i++)printf("%d ",*(p + i));return 0;
}
0 1 2 3 4 5 6 7 8 9
搞清楚这里代码后,我们知道,arr其实就是指针,并且还可以赋值给p,我们可以用p来访问数组中的元素,那p与arr就是等效的。我们可以用arr[i]来访问数组元素,那是不是也可以用p[i]来访问数组元素呢?
int main()
{int i;int arr[10] = { 0 };int *p = arr;for(i = 0;i < 10;i++)p[i] = i;for(i = 0;i < 10;i++)printf("%d ",p[i]);return 0;
}
0 1 2 3 4 5 6 7 8 9
我们发现,将*(p + i)换为p[i]后,代码也能正常运行,说明p[i] 与 *(p+i)是等效的。那么arr[i]也等效于 *(arr+i),编译器在访问数组元素的时候也是将其转换为首元素地址+偏移量的形式来获得地址,然后解引用来访问的。
3. 一维数组传参的本质
我们知道,数组也是可以作为参数传递给函数的。在之前,我们在计算数组元素个数的时候都是在函数外部进行的,那如果我们将数组作为参数传递给函数,能否在函数内部正确计算元素个数?
void test(int arr[])
{int sz2 = sizeof(arr)/ sizeof(arr[0]);printf("sz2 = %d\n",sz2);
}
int main()
{int arr[10] = { 0 };int sz1 = sizeof(arr)/ sizeof(arr[0]);printf("sz1 = %d\n",sz1);test(arr);return 0;
}
sz1 = 10
sz2 = 2
我们发现,函数并未得到正确的结果,这是因为数组名是首元素的地址,所以函数在传参时会将数组名作为指针处理,在计算时,将arr作为首元素的地址进行计算,因此得出结果为2(64位环境下指针大小为8字节)。
所以说,数组传参本质上传递的是首元素的地址。
4. 冒泡排序
冒泡排序的核心思想就是:两两相邻的元素进行比较
如果我们要将数组排序为降序的形式,当靠前的元素小于靠后的元素时,我们就交换两个元素的值。而很明显一轮只能保证最小的数字被移到最后,所以我们要进行多轮。当数组中一共有n个元素时,我们就只需要进行n - 1轮就可以完成排序。而我们每一轮结束就可以确定一个数字的位置,下一轮所需要遍历的元素个数就减少一个,所以每一轮需要遍历的元素个数是与轮次有关的,因此,第i轮只需要遍历n-1-i个元素。另外,我们可以创建一个变量flag,用来确定排序是否已经完成,避免多余的运算。
因此,我们可以得到如下的代码:
void Bubble(int* arr,int sz)
{int i,j,count = 0,temp;for(i = 0;i < sz - 1;i++){for(j = 0;j < sz - 1 - i;j++){if(*(arr + j) < *(arr + j + 1)){temp = *(arr + j);*(arr + j) = *(arr + j + 1);*(arr + j + 1) = temp;count++;}if(count == 0)break;}}
}
我们可以进行测试:
int main()
{int i;int arr[10] = {4,2,6,7,8,9,3,1,0,5};Bubble(arr,10);for( i = 0;i < 10;i++){printf("%d ",arr[i]);}return 0;
}
9 8 7 6 5 4 3 2 1 0
结果符合预期,代码正常运行。
5. 二级指针
我们知道,指针变量也是变量,而变量就需要地址来存储,那么指针变量存储在哪里呢?
这里我们就需要学习二级指针。
用代码可以表示为:
int a = 0;
int *pa = &a;
int **ppa = &p1;
那么,根据指针的知识来类推,对pa进行解引用操作得到的是a,那对ppa进行解引用操作得到的就是pa。所以,如果我们希望通过ppa来得到a的话就需要进行两次解引用操作,即:
**ppa = a;
//*(*ppa) = a;
//*pa = a
6. 指针数组
类比一下整型数组中存放整型数据,那么指针数组中存放的就是指针。
而指针又有类型的区别,所以类比一下整型数组的定义语法,我们定义整型指针数组时,可以写成这个样子:
int* arr[10];
其中,arr是数组名,10是数组大小,而int* 是数组中元素的类型。
7. 指针数组模拟二维数组
上面我们知道,指针数组中存放的都是指针,而指针又可以指向一个地址。那么我们是不是可以根据这个特点来用指针数组模拟二维数组呢?
int main()
{int arr1[4] = {0,1,2,3};int arr2[4] = {1,2,3,4};int arr3[4] = {2,3,4,5};int* p[3] = {arr1,arr2,arr3};int i,j;for(i = 0;i < 3;i++){for(j = 0;j < 4;j++){printf("%d ",p[i][j]);}printf("\n");}return 0;
}
0 1 2 3
1 2 3 4
2 3 4 5
数组名就是首元素地址,我们将三个数组的数组名放入指针数组p中,通过p[i]来访问数组的地址,再加上[j]来访问数组中的元素,通过解引用操作得到对应的元素的值。这样,我们就通过指针数组模拟了二维数组的效果。
但是实际上,这并不等同于二维数组。因为二位数组在内存中是连续的,而在这里,我们只是将不同的数组的地址放在了一起,进行访问,而在内存中,这些数组并没有连续储存在一起。