深入理解指针
一、冒泡排序
冒泡排序的核心思想就是:两两相邻的元素进行比较。
#include
void bubble_sort(int arr[], int sz)
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int j = 0;
for (j = 0; j < sz - i - 1; j++)
{
if (arr[j] > arr[j + 1])
{
int tmp = 0;
tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
}
}
int main()
{
int arr[] = { 10,9,8,7,6,5,4,3,2,1 };
int sz = sizeof(arr) / sizeof(arr[0]);
bubble_sort(arr, sz);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
可以看到,这段代码对arr数组进行了排序,但这个代码还有一些缺陷,那就是无论数组内部的元素是否有序,他都会循环9次,这样肯定是不合理的,要进行优化一下。
我们在bubble_sort函数的第一层循环里面定义一个变量flag,进入第二层循环就修改flag的值,第二层循环结束时给变量flag来个判断,如果变量flag没有发生改变,说明没有进入第二层循环,也就是说这时数组里的元素是有序的,就会直接跳出第一层循环。另外,我们还可以在最外面定义一个全局变量用来计数,每进行一次元素交换就会自增1。
#include
int count = 0;
bubble_sort(int arr[], int sz) //参数接收数组元素个数
{
int i = 0;
for (i = 0; i < sz - 1; i++)
{
int flag = 1; //假设这趟已经有序了
int j = 0;
for (j = 0; j < sz - i - 1; j++)
{
count++;
if (arr[j] > arr[j + 1])
{
flag = 0; //发交换就说明,序
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
if (flag == 1) //这趟没交换就说明已经有序,后续序排序了
break;
}
}
int main()
{
int arr[] = { 0,9,8,6,5,3,1,2,4,7 };
int sz = sizeof(arr) / sizeof(arr[0]); // 求出数组中的元素个数
bubble_sort(arr, sz);
int i = 0;
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
printf("n");
printf("%dn", count);
return 0;
}
运行之后可以发现,代码的运行效率明显提升了,理论来说上述数组元素在未优化的代码下运行是要进行45次交换的,现在明显次数减少了。
二、二级指针
通过前面的知识我们知道,指针变量也是一个变量,只要是一个变量就会有地址,那么,指针变量的地址放在哪里呢?我们把存放地址的指针称为二级指针,也就是指针的指针
#include
int main()
{
int a = 10;
int* pa = &a;
int** ppa = &pa;
return 0;
}
而三级指针就是存放二级指针变量的地址,四级指针、五级指针以此类推...
对于二级指针的运算有:
- *ppa通过对ppa进行解引用,就可以找到pa,*ppa其实访问的就是pa。
int b = 20;
*ppa = &b; // 等价于pa = &b
- **pa先通过*ppa找到pa,然后对pa进行解引用操作:*pa,找到的就是a。
**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;
三、指针数组
首先我们要弄懂一个问题:指针数组到底是一个指针还是一个数组?这里我们可以类比一下:
整数数组:就是一个存放整型的数组。字符数组:就是一个存放字符的数组。
那指针数组呢?没错,就是存放指针的数组。指针数组里的每个元素都是用来存放地址(指针)的。
3.1 指针数组模拟二维数组
通过上图可以清晰地看到,指针数组中的每一个元素本质上都是一个指针变量。而关键在于,这些指针变量中的每一个都可以独立地指向内存中的一块特定区域。这种结构允许我们构建类似二维数组的逻辑模型,尽管其底层存储机制与真正的二维数组存在显著差异。
在具体的访问操作中,parr[i] 用于访问指针数组 parr 的第 i 个元素。当我们找到这个元素时,发现它指向了一个整型一维数组。因此,parr[i][j] 实际上就是访问该整型一维数组中的第 j 个元素。需要特别注意的一点是,这仅仅是利用指针数组对二维数组进行的模拟,它并非真正的二维数组。其核心区别在于每一行的地址在内存中并非连续排列,这与传统二维数组连续分配的内存空间有着本质的不同。
四、字符指针变量
在探讨指针的类型时,我们知道存在一种特殊的指针类型,即字符指针 char*。在常规使用中,我们通常直接定义一个字符指针变量来指向字符串常量或字符数组。
此外,还有一种常见的初始化方式如下所示:
这里有一个极易引起误解的地方:const char* pc = "hello world"; 这一语句特别容易让人误以为是将整个字符串 "hello world" 完整地拷贝并放置到了字符指针 pc 所指向的空间里。然而,其本质并非如此。实际上,是将字符串 "hello world" 的首字符 'h' 的地址赋值并存放到了指针变量 pc 中。
换句话说,上述代码的含义是将一个常量字符串的首字符地址存储到指针变量 pc 中,而非字符串内容本身。
为了深入理解这一概念,下面通过一道经典的字符串相关题目来进行分析:
运行程序后,我们会发现打印输出的是序号 2 和序号 3 的语句。这背后的原因是什么呢?关键在于 str3 和 str4 指向的是同一个常量字符串。在 C/C++ 语言中,编译器会将常量字符串存储在一个单独的、特定的内存区域(通常称为只读数据段)。当多个指针指向同一个字符串常量时,它们实际指向的是内存中的同一块区域。然而,如果使用相同的常量字符串去初始化不同的字符数组时,编译器会为每个数组开辟出独立的内存块来存储副本。因此,str1 和 str2 指向不同的内存地址,而 str3 和 str4 则指向相同的地址。
五、数组指针变量
5.1 数组指针变量是什么?
在前文学习指针数组时,我们明确了一个概念:指针数组是数组元素为指针的数组。那么,与之相对的“数组指针”究竟是什么呢?为了厘清这一概念,我们可以借鉴之前的类比方法,通过对比来深入理解数组指针的定义与特性。
3/4
- 整型指针变量:int * pint; 存放的是整型变量的地址,能够指向整形数据的指针。
- 浮点型指针变量:float * pf; 存放浮点型变量的地址,能够指向浮点型数据的指针。
那么数组指针就是存放数组的地址,能够指向数组数据的指针变量。
我们判断一下下面那个是数组指针
int *p1[10];
int (*p2)[10];
int * p1[10]; 是一个指针数组,是 10 个整型指针的数组。 int ( * p2)[10]; 是数组指针,表示一个指向包含 10 个整型数据的数组的指针。因为 p2 是一个指针,所以 p2 是一个指针类型。
数组指针变量
int (*p2)[10];
这里p会先和 * 号结合,说明p是一个指针变量,然后指向的是一个大小为10个整型数据大小的数组,所以p是一个指针,指向一个数组,叫数组指针。 注意:[]的优先级是高于 * 号的,所以必须要加上()来保证 p会先和 * 结合。
5.2 数组指针变量的初始化
数组指针变量是用来存放数组地址的,那怎么获得数组的地址呢?其实就是我们之前学的 &数组名。
int main()
{
int arr[10] = { 0 };
&arr; //得到的就是数组的地址
return 0;
}
如果要存放个数组的地址,就得存放在数组指针变量中,如下:
int main()
{
int arr[10] = { 0 };
int(*pa)[10] = &arr;
return 0;
}
在调试的过程中我们也可以看到 &arr 和pa的类型是完全一致的。
数组指针类型解析:
int (*p) [10] = &arr;
| | |
| | |
| | | p指向数组的元素个数
| p是数组指针变量名
p指向的数组的元素类型
六、二维数组传参的本质
有了数组指针的理解,我们就能够讲下二维数组传参的本质了。 过去我们有一个二维数组的需要传参给个函数的时候,我们是这样写的:
#include
void test(int a[3][5], int r, int c)
{
int i = 0;
int j = 0;
for (i = 0; i < r; i++)
{
for (j = 0; j < c; j++)
{
printf("%d ", a[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;
}
二维数组传参的指针形式
这里实参是二维数组,形参也写成二维数组的形式,那还有什么其他的写法吗?首先我们再次理解一下二维数组,二维数组其实可以看做是每个元素是一维数组的数组,也就是二维数组的每个元素是一个一维数组。那么二维数组的首元素就是第一行,是一个一维数组。
如下图:
#include
void test(int(*p)[5], int r, int c)
{
int i = 0;
int j = 0;
for (i = 0; i < r; i++)
{
for (j = 0; j < c; j++)
{
printf("%d ", *((*p + 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;
}
所以,根据数组名是数组首元素的地址这个规则,二维数组的数组名表示的就是第一行的地址,是一维数组的地址。根据上面的例子,第一行的一维数组的类型就是 int [5] ,所以第一行的地址的类型就是数组指针类型 int(*)[5] 。那就意味着二维数组传参本质上也是传递了地址,传递的是第一行这个一维数组的地址,那么形参也是可以写成指针形式的。如下:
总结:二维数组传参,形参的部分可以写成数组,也可以写成指针形式。
#include
int main()
{
int arr1[] = { 1,2,3,4,5 };
int arr2[] = { 2,3,4,5,6 };
int arr3[] = { 3,4,5,6,7 };
//数组名是数组首元素的地址,类型是int*的,就可以存放在parr数组中
int* parr[] = { arr1, arr2, arr3 };
int i = 0;
int j = 0;
for (i = 0; i < 3; i++)
{
for (j = 0; j < 5; j++)
{
printf("%d ", parr[i][j]);
}
printf("n");
}
return 0;
}
#include
int main()
{
char ch = 'w';
char* pc = &ch;
*pc = 'w';
return 0;
}
#include
int main()
{
const char* pc = "hello world";
printf("%sn", pc);//这里是把一个字符串放到pc指针变量里了吗?
return 0;
}
#include
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 samen"); // 序号1
else
printf("str1 and str2 are not samen"); // 序号2
if (str3 == str4)
printf("str3 and str4 are samen"); // 序号3
else
printf("str3 and str4 are not samen"); // 序号4
return 0;
}










