深入C语言指针,使代码更加灵活(二)

简介: 深入C语言指针,使代码更加灵活(二)

一、数组名的理解

前面我们在使用指针访问数组内容的时候,有这样的代码:

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

输出结果如下:

我们可以得出结论:

  1. &arr[0]与arr+1都是跳过4个字节,相当于跳过1个整型元素。
  2. &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));
  }
}


通过对上面代码的观察,我们可以总结如下规律:

  1. arr[i]与*(arr+i)等价。
  2. 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;
}


相关文章
|
2天前
|
存储 缓存 关系型数据库
MySQL事务日志-Redo Log工作原理分析
事务的隔离性和原子性分别通过锁和事务日志实现,而持久性则依赖于事务日志中的`Redo Log`。在MySQL中,`Redo Log`确保已提交事务的数据能持久保存,即使系统崩溃也能通过重做日志恢复数据。其工作原理是记录数据在内存中的更改,待事务提交时写入磁盘。此外,`Redo Log`采用简单的物理日志格式和高效的顺序IO,确保快速提交。通过不同的落盘策略,可在性能和安全性之间做出权衡。
1519 4
|
29天前
|
弹性计算 人工智能 架构师
阿里云携手Altair共拓云上工业仿真新机遇
2024年9月12日,「2024 Altair 技术大会杭州站」成功召开,阿里云弹性计算产品运营与生态负责人何川,与Altair中国技术总监赵阳在会上联合发布了最新的“云上CAE一体机”。
阿里云携手Altair共拓云上工业仿真新机遇
|
5天前
|
人工智能 Rust Java
10月更文挑战赛火热启动,坚持热爱坚持创作!
开发者社区10月更文挑战,寻找热爱技术内容创作的你,欢迎来创作!
503 19
|
2天前
|
存储 SQL 关系型数据库
彻底搞懂InnoDB的MVCC多版本并发控制
本文详细介绍了InnoDB存储引擎中的两种并发控制方法:MVCC(多版本并发控制)和LBCC(基于锁的并发控制)。MVCC通过记录版本信息和使用快照读取机制,实现了高并发下的读写操作,而LBCC则通过加锁机制控制并发访问。文章深入探讨了MVCC的工作原理,包括插入、删除、修改流程及查询过程中的快照读取机制。通过多个案例演示了不同隔离级别下MVCC的具体表现,并解释了事务ID的分配和管理方式。最后,对比了四种隔离级别的性能特点,帮助读者理解如何根据具体需求选择合适的隔离级别以优化数据库性能。
179 1
|
8天前
|
JSON 自然语言处理 数据管理
阿里云百炼产品月刊【2024年9月】
阿里云百炼产品月刊【2024年9月】,涵盖本月产品和功能发布、活动,应用实践等内容,帮助您快速了解阿里云百炼产品的最新动态。
阿里云百炼产品月刊【2024年9月】
|
21天前
|
存储 关系型数据库 分布式数据库
GraphRAG:基于PolarDB+通义千问+LangChain的知识图谱+大模型最佳实践
本文介绍了如何使用PolarDB、通义千问和LangChain搭建GraphRAG系统,结合知识图谱和向量检索提升问答质量。通过实例展示了单独使用向量检索和图检索的局限性,并通过图+向量联合搜索增强了问答准确性。PolarDB支持AGE图引擎和pgvector插件,实现图数据和向量数据的统一存储与检索,提升了RAG系统的性能和效果。
|
9天前
|
Linux 虚拟化 开发者
一键将CentOs的yum源更换为国内阿里yum源
一键将CentOs的yum源更换为国内阿里yum源
457 5
|
7天前
|
存储 人工智能 搜索推荐
数据治理,是时候打破刻板印象了
瓴羊智能数据建设与治理产品Datapin全面升级,可演进扩展的数据架构体系为企业数据治理预留发展空间,推出敏捷版用以解决企业数据量不大但需构建数据的场景问题,基于大模型打造的DataAgent更是为企业用好数据资产提供了便利。
314 2
|
23天前
|
人工智能 IDE 程序员
期盼已久!通义灵码 AI 程序员开启邀测,全流程开发仅用几分钟
在云栖大会上,阿里云云原生应用平台负责人丁宇宣布,「通义灵码」完成全面升级,并正式发布 AI 程序员。
|
25天前
|
机器学习/深度学习 算法 大数据
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析
2024“华为杯”数学建模竞赛,对ABCDEF每个题进行详细的分析,涵盖风电场功率优化、WLAN网络吞吐量、磁性元件损耗建模、地理环境问题、高速公路应急车道启用和X射线脉冲星建模等多领域问题,解析了问题类型、专业和技能的需要。
2608 22
【BetterBench博士】2024 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析