【C语言基础】:深入理解指针(三)

简介: 【C语言基础】:深入理解指针(三)

深入理解指针

指针系列回顾:

【C语言基础】:深入理解指针(一)

【C语言基础】:深入理解指针(二)


一、冒泡排序

冒泡排序的核心思想就是:两两相邻的元素进行比较。

#include<stdio.h>

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;
}

07d7f0c0618ab47f3023528fdb3fc1b8_8af93d11f5944847a12a03392656eee6.png

可以看到,这段代码对arr数组进行了排序,但这个代码还有一些缺陷,那就是无论数组内部的元素是否有序,他都会循环9次,这样肯定是不合理的,要进行优化一下。


我们在bubble_sort函数的第一层循环里面定义一个变量flag,进入第二层循环就修改flag的值,第二层循环结束时给变量flag来个判断,如果变量flag没有发生改变,说明没有进入第二层循环,也就是说这时数组里的元素是有序的,就会直接跳出第一层循环。另外,我们还可以在最外面定义一个全局变量用来计数,每进行一次元素交换就会自增1。


#include<stdio.h>
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("%d\n", count);
  return 0;
}

bea793bb4c70744e57990b4f2b63123c_b6a9356782a54eeeb1ead36a95a4ec93.png

运行之后可以发现,代码的运行效率明显提升了,理论来说上述数组元素在未优化的代码下运行是要进行45次交换的,现在明显次数减少了。


二、二级指针

通过前面的知识我们知道,指针变量也是一个变量,只要是一个变量就会有地址,那么,指针变量的地址放在哪里呢?

我们把存放地址的指针称为二级指针,也就是指针的指针


*ppa通过对ppa进行解引用,就可以找到pa,*ppa其实访问的就是pa。
int b = 20;
*ppa = &b;  // 等价于pa = &b

而三级指针就是存放二级指针变量的地址,四级指针、五级指针以此类推…


对于二级指针的运算有:


  • *ppa通过对ppa进行解引用,就可以找到pa,*ppa其实访问的就是pa。
int b = 20;
*ppa = &b;  // 等价于pa = &b
  • **pa先通过*ppa找到pa,然后对pa进行解引用操作:*pa,找到的就是a。
**ppa = 30;
//等价于*pa = 30;
//等价于a = 30;

三、指针数组

首先我们要弄懂一个问题:指针数组到底是一个指针还是一个数组?

这里我们可以类比一下:


整数数组:就是一个存放整型的数组。

字符数组:就是一个存放字符的数组。


那指针数组呢?没错,就是存放指针的数组。

916d2801e89391b9fa5e3dc27619d591_9203f8cbc7c64e468dee902aeaf9c25b.png

指针数组里的每个元素都是用来存放地址(指针)的。


如下图:

81775912431f988412aefb9b7c0579bf_865be9acf75749f1bb4a76f77a4f1e96.png

通过这张图可以看到,指针数组里的每一个元素都是一个指针,而这里面的每一个指针又可以指向一块区域。


3.1 指针数组模拟二维数组

#include<stdio.h>
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;
}


0ec0d3cf8b0337f84e7ded7d2601318d_6d471efaa394493c9fac800c521fd930.png ed1ca511047349fa9fbc26e4069905aa_c3a5bf9360cf4c10a16c7d47f63afef5.png

parr[i]是访问parr数组的元素,parr[i]找到的数组元素指向了整型⼀维数组,parr[i][j]就是整型⼀维数组中的元素。

注意:这只是利用指针数组模拟的二维数组,并非是真的二维数组,因为每一行的地址并非连续的。


四、字符指针变量

在指针的类型中我们知道有一种指针类型为字辅指针char*

一般使用:


#include<stdio.h>
int main()
{
  char ch = 'w';
  char* pc = &ch;
  *pc = 'w';
  return 0;
}

还有一种使用方式如下:

#include<stdio.h>
int main()
{
  const char* pc = "hello world";
  printf("%s\n", pc);//这里是把一个字符串放到pc指针变量里了吗?
  return 0;
}

399b96e99ca3c4690abd4fe6bce1e339_ab7964fd79a14df989b0a2efb3d9495b.png

注意:这里const char* pc = “hello world”; 特别容易让人认为是把字符串hello world放到字符指针pc里了,但其实本质上是字符串hello world 首字符的地址放到了pc中。

24041008879a6cf61000cc39794b688d_a68b892e94914f5a8c1c2cf9f1361e47.png

上面的代码的意思是把一个常量字符串的首字符h的地址存放到指针变量pc中。


下面是一道和字符串相关的题,我们可以学一下:

#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");  // 序号1
  else
    printf("str1 and str2 are not same\n");  // 序号2
  if (str3 == str4)
    printf("str3 and str4 are same\n");  // 序号3
  else
    printf("str3 and str4 are not same\n");  // 序号4
  return 0;
}

7320f30648209b7e80e401c2481c02b8_5d1fd362ae54444f8b8f176d7fcdfdff.png

运行之后我们会发现,打印的是序号2和序号3的语句,这是为什么呢?

这里的str3和str4指向的是同一个常量字符串。C/C++会把常量字符串存储到一个单独的内存区域,当几个指针指向同一个字符串的时候,它们实际会指向同一块内存。但是用相同的常量字符串去初始化不同的数组的时候就会开辟出不同的内存块。所以str1和str2不同,str3和str4相同。


五、数组指针变量

5.1 数组指针变量是什么?

上面在学指针数组的时候我们知道,指针数组是数组元素为指针的数组。那么数组指针是什么呢?

上面学习指针数组的时候类比了一下,那么这里不妨也来类比一下:


  • 整型指针变量: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;
}

a7be27a07c2a811188e2ca89c9a78d7b_673208acf1aa4f8fb5ab4b51c5d6e138.png

在调试的过程中我们也可以看到 &arr 和pa的类型是完全一致的。


数组指针类型解析:


int (*p) [10] = &arr;
 |    |    |
 |    |    |
 |    |    | p指向数组的元素个数
 |    p是数组指针变量名
 p指向的数组的元素类型

六、二维数组传参的本质

有了数组指针的理解,我们就能够讲⼀下二维数组传参的本质了。

过去我们有一个二维数组的需要传参给⼀个函数的时候,我们是这样写的:


#include<stdio.h>

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;
}

98fb358ee6090ca7daf0cf5db1ff6cf0_b163508f5552482ba62966b82a32fc5e.png

这里实参是二维数组,形参也写成⼆维数组的形式,那还有什么其他的写法吗?

首先我们再次理解⼀下二维数组,二维数组其实可以看做是每个元素是⼀维数组的数组,也就是⼆维数组的每个元素是⼀个⼀维数组。那么⼆维数组的首元素就是第一行,是个⼀维数组。

如下图:

f03e1fb5937df7b8a157a8f7f79bf876_14f1f730291b44c1be859ac9f317f1f7.png

所以,根据数组名是数组首元素的地址这个规则,⼆维数组的数组名表示的就是第一行的地址,是⼀维数组的地址。根据上面的例子,第一行的⼀维数组的类型就是 int [5] ,所以第一行的地址的类型就是数组指针类型 int(*)[5] 。那就意味着⼆维数组传参本质上也是传递了地址,传递的是第一行这个⼀维数组的地址,那么形参也是可以写成指针形式的。如下:

#include<stdio.h>

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;
}

8fae80417ce42043779b57a80e48e729_a17838533955498d97265e7929bb5461.png

总结:二维数组传参,形参的部分可以写成数组,也可以写成指针形式。

相关文章
|
1月前
|
存储 C语言
【C语言篇】深入理解指针3(附转移表源码)
【C语言篇】深入理解指针3(附转移表源码)
36 1
|
24天前
|
C语言
【c语言】指针就该这么学(1)
本文详细介绍了C语言中的指针概念及其基本操作。首先通过生活中的例子解释了指针的概念,即内存地址。接着,文章逐步讲解了指针变量的定义、取地址操作符`&`、解引用操作符`*`、指针变量的大小以及不同类型的指针变量的意义。此外,还介绍了`const`修饰符在指针中的应用,指针的运算(包括指针加减整数、指针相减和指针的大小比较),以及野指针的概念和如何规避野指针。最后,通过具体的代码示例帮助读者更好地理解和掌握指针的使用方法。
45 0
|
23天前
|
C语言
【c语言】指针就该这么学(3)
本文介绍了C语言中的函数指针、typedef关键字及函数指针数组的概念与应用。首先讲解了函数指针的创建与使用,接着通过typedef简化复杂类型定义,最后探讨了函数指针数组及其在转移表中的应用,通过实例展示了如何利用这些特性实现更简洁高效的代码。
15 2
|
23天前
|
C语言
如何避免 C 语言中的野指针问题?
在C语言中,野指针是指向未知内存地址的指针,可能引发程序崩溃或数据损坏。避免野指针的方法包括:初始化指针为NULL、使用完毕后将指针置为NULL、检查指针是否为空以及合理管理动态分配的内存。
|
23天前
|
C语言
C语言:哪些情况下会出现野指针
C语言中,野指针是指指向未知地址的指针,通常由以下情况产生:1) 指针被声明但未初始化;2) 指针指向的内存已被释放或重新分配;3) 指针指向局部变量,而该变量已超出作用域。使用野指针可能导致程序崩溃或不可预测的行为。
|
30天前
|
存储 C语言
C语言32位或64位平台下指针的大小
在32位平台上,C语言中指针的大小通常为4字节;而在64位平台上,指针的大小通常为8字节。这反映了不同平台对内存地址空间的不同处理方式。
|
29天前
|
存储 算法 C语言
C语言:什么是指针数组,它有什么用
指针数组是C语言中一种特殊的数据结构,每个元素都是一个指针。它用于存储多个内存地址,方便对多个变量或数组进行操作,常用于字符串处理、动态内存分配等场景。
|
30天前
|
存储 C语言
C语言指针与指针变量的区别指针
指针是C语言中的重要概念,用于存储内存地址。指针变量是一种特殊的变量,用于存放其他变量的内存地址,通过指针可以间接访问和修改该变量的值。指针与指针变量的主要区别在于:指针是一个泛指的概念,而指针变量是具体的实现形式。
|
30天前
|
C语言
C语言指针(3)
C语言指针(3)
11 1
|
30天前
|
C语言
C语言指针(2)
C语言指针(2)
13 1