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

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

一、内存和地址

1.1 概念

我们都知道计算机的数据必须存储在内存里,为了正确地访问这些数据,必须为每个数据都编上号码,就像门牌号、身份证号一样,每个编号是唯一的,根据编号可以准确地找到某个数据。


生活中我们把门牌号叫地址,而在计算机中我们把内存单元的编号也称为地址。但是在C语⾔中给地址起了一个新的名字:指针。

所以我们可以理解为:

内存单元的编号 == 地址 == 指针


1.2 取地址操作符(&)

理解了内存和地址的关系,我们再回到C语⾔,在C语⾔中创建变量其实就是向内存申请空间!

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
  
int main()
{
  int a = 10;//变量创建的本质:在内存上开辟空间
  //要向内存申请个字节的空间,存放数据10
  //10 ---> a
  //0000 0000 0000 0000 0000 0000 0000 1010
  //0x   00   00   00   0a
  //
  //%a  &---取地址操作符
  printf("%p\n", &a);
  
  return 0;
}


整型变量a,在内存中申请4个字节,用于存放整数10,每个字节都有地址

&a取出的是a所占4个字节中第一个字节的地址(地址较小的那个字节的地址)来打印,例如:0x006FFC0C 。

虽然整型变量占用4个字节,但我们只要知道了第⼀个字节地址,顺藤摸瓜访问到4个字节的数据也是可行的。


1.3 指针变量和解引用操作符(*)

数据在内存中的地址称为指针,如果一个变量存储了一份数据的指针(地址),我们就称它为指针变量

那我们如何使用指针变量呢?


在酒店中,我们可以通过门牌号准确找到每个客户。同理,我们也可以通过每个地址准确找到每个变量。C语言中也是⼀样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象,而在这之前我们必须要学习⼀个操作符叫解引用操作符(*)。

例如:

int* p1;//指向一个整型的指针
char* p2;//指向一个字符的指针
float* p3;//指向一个单精度浮点数的指针
double* p4;//指向一个双精度浮点数的指针


并且我们可以通过指针变量进行赋值。

1. *p1 = 4;
2. *p2 = 'a';
3. *p3 = 5.0;


1.4 void指针和空指针

(1)void*是一种特殊的指针类型,它可以指向任意类型的数据,就是说可以用任意类型的指针对 void 指针赋值。

void*p1;
int*p2;
p1=p2;

但是却不能把void*指针赋值给任意指针类型,也不能直接对其解引用

例如:

void*p1;
int *p2;
//这是错误的赋值方式与解引用方式
p2=p1;
*p1

(2)NULL 是C语⾔中定义的⼀个标识符常量,值是0,地址也是0,这个地址是⽆法使⽤的。

int*p=NULL;//初始化指针


1.5 指针变量的大小

我们先运行以下代码

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
int main()
{
 
  int num = 10;
  int* p = &num;
 
  char ch = 'w';
  char* pc = &ch;
 
  printf("%u\n", sizeof(p));
  printf("%u\n", sizeof(pc));
  return 0;
}

结果为如下所示:

我们发现两种类型的指针变量大小都是8个字节,这是为什么呢?

首先我们要明白指针是用来干什么的?指针是为了存放地址,而地址的大小取决于存储一个地址需要多大的空间。


我们知道,现在常见的计算机分为32位机器和64位机器。地址是由地址总线产生的,32位的机器有32根地址线,地址上传输过来的电信号转换成数字信号后,得到的32个0/1组成的序列就是地址。地址都是32个0/1组成的二进制序列的话,那么存放这个地址所需要的空间大小是4个字节。所以指针变量的大小都是4个字节。

同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变的大小就是8个字节。


输出结果:


32位机器:4 4


64位机器:8 8


二、指针变量的运算

2.1 指针+/-整数

我们先观察一下如下代码的地址变化

#include <stdio.h>
int main()
{
  int n = 10;
  char* p1 = (char*)&n;//将int*强转为char*
  int* p2 = &n;
  printf("%p\n", &n);
  printf("%p\n", p1);
  printf("%p\n", p1 + 1);//p1向后移动一位
  printf("%p\n", p2);
  printf("%p\n", p2 + 1);//p2向后移动一位
  return 0;
}


输出结果如下:

我们可以看出, char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。由此我们得出结论:指针的类型决定了指针向前或者向后⾛⼀步有多⼤(距离)。


因为每次代码运行时,系统都会重新分配内存,所以输出结果每次都不会一样,但是规律是一样的。

我们知道数组在内存中是连续存储的(地址由低到高),所以我们只需要首元素的地址就能顺藤摸瓜就能找到后面的所有元素。

代码如下:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
int main()
{
  int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
  int *p = &arr[0];
  int i = 0;
  int sz = sizeof(arr) / sizeof(arr[0]);
  for (i = 0; i<sz; i++)
  {
    printf("%d ", *(p + i));//p+i 这⾥就是指针+整数
  }
  return 0;
}


编译结果如下:

2.2 指针-指针

前提条件:两个指针指向同一块空间。


指针 - 指针得到的是两个指针之间元素的个数。

例如:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
int my_strlen(char* str)
{
  char* start = str;
  while (*str != '\0')
  {
    str++;
  }
  return str - start;
}
 
int main()
{
  char arr[] = "abcdef";
  int len = my_strlen(arr);
  printf("%d\n", len);
  return 0;
}


输出结果为6


2.3 指针的关系运算

我们知道了指针变量本质是存放的地址,而地址本质就是十六进制的整数,所以指针变量也是可以比较大小的


前面我们通过循环的方式实现对数组的访问,而通过比较指针的大小,我们也可以实现对数组的访问,例如

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
int main()
{
  int arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
  int sz = sizeof(arr) / sizeof(arr[0]);
  //int* p=&arr[0];
  int* p = arr;//数组名就是首元素的地址
  while (p < arr + sz)//指针大小的比较
  {
    printf("%d ", *p);
    p++;
  }
  return 0;
}


编译结果如下:

三、const修饰指针

我们知道变量是可以改变的,但是在有些场景下,我们不希望变量改变,那我们该怎么办呢?这就是我们接下来要讲的const的作用啦。


3.1 指针修饰变量

简单来说,经过const修饰的变量,可以当做一个常量,而常量是不能改变的

1.  int a = 1;//a可修改的
2.  const int b = 2;
3.  b=3;//b不可修改的

但我们可以使用指针来修改:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
int main()
{
  const int n = 100;
  int* p = &n;
  *p = 10;
  printf("%d\n", n);
  return 0;
}


运行结果为10


通过代码我们发现,虽然const修饰变量n后,我们无法直接修改,但是当我们把变量的地址取出来,通过解引用操作*p=10,我们依然能够修改n的值。


这就好比我们发现门关了,就从窗户爬进去,这种爬窗户的行为显然是我们所不能接受的。


这显然也不合理,我们应该限制p也不能修改。要做到这种效果,需要我们用const来修饰指针。


3.2 指针修饰指针

我们知道const的作用后,就可以看看下面几段代码。

1.  int a = 10;
2.  const int* p = &a;
3.  *p = 20;//是否可以
4.  p = p + 1;//是否可以

通过测试我们发现,*p无法改变成20,但是p可以改变成p+1.

那如果把const调换一下位置,又会出现什么情况呢~

1.  int a = 10;
2. int* const p = &a;
3.  *p = 20;//是否可以
4.  p = p + 1;//是否可以

再次测试之后我们发现,*p可以被赋值为20,但是p不能赋值为p+1了


通过上述测试,我们大致可以总结出两个结论。


const如果放在int*的左边,修饰的是指针指向的内容,保证指针指向的内容不能通过指针来改变。但是指针变量本⾝的内容可变。


const如果放在int*的右边,修饰的是指针变量本⾝,保证了指针变量的内容不能修改,但是指针指向的内容,可以通过指针改变。


四、assert断言

assert是一个宏,它的头文件为<assert.h>,⽤于在运⾏时确保程序符合指定条件,如果不符合,就报错终⽌运⾏。这个宏常常被称为“断⾔”。

举一个简单的例子:

assert(a>0);


  1. 如果a的确大于0,assert判断为真,就会通过。
  2. 如果a不大于0,assert判断为假,就会报错。

所以assert常常用于检查空指针问题,以防止程序因为空指针的问题而出错。

1. int *p=NULL;
2. assert(p);//空指针是0,0为假,就会报错

但是assert() 也是有缺点的,因为引入了额外的检查,增加了程序的运行时间。


⼀般我们可以在debug中使用,在release版本中选择禁用assert就行,在VS这样的集成开发环境中,在release版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在release版本不影响用户使用时程序的效率。


五、传值调用与传址调用


5.1 传值调用

学习指针的目的是使用指针解决问题,那什么问题,非指针不可呢?

例如:写⼀个函数,交换两个整型变量的值

⼀番思考后,我们可能写出这样的代码:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
void Swap1(int x, int y)
{
  int tmp = x;
  x = y;
  y = tmp;
}
 
int main()
{
  int a = 0;
  int b = 0;
  scanf("%d %d", &a, &b);
  printf("交换前:a=%d b=%d\n", a, b);
  Swap1(a, b);
  printf("交换后:a=%d b=%d\n", a, b);
  return 0;
}


运行结果如下:

我们发现其实没有产生交换的效果,这是为什么呢?

我们将代码调试起来:


我们发现在main函数内部,创建了a和b,地址分别如下所示:

名称 类型
&a 0x0000003c41d9fb34 {3} int *
&b 0x0000003c41d9fb54 {4} int *
&x 0x0000003c41d9fb10 {3} int *
&y 0x0000003c41d9fb18 {4} int *


在调用Swap1函数时,将a和b传递给了Swap1函数,在Swap1函数内部创建了形参x和y接收a和b的值,x和y确实接收到了a和b的值,不过x的地址和a的地址不⼀样,y的地址和b的地址不⼀样,相当于x和y是独立的空间,那么在Swap1函数内部交换x和y的值,自然不会影响a和b。


当Swap1函数用结束后回到main函数,a和b的没法交换。Swap1函数在使用的时候,是把变量本身直接传递给了函数,这种调用函数的方式我们之前在函数的时候就知道了,这种叫传值调用。

总结:

因为形参只是实参的一份临时拷贝,对形参改变,根本不会改变实参


5.2 传址调用


那怎么办呢?

我们现在要解决的就是当调用Swap1函数的时候,Swap函数内部操作的就是main函数中的a和b,直接将a和b的值交换了。那么就可以使用指针,在main函数中将a和b的地址传递给Swap1函数,Swap函数里边通过地址间接的操作main函数中的a和b就好了。


修改后的代码:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
void Swap2(int*px, int*py)
{
  int tmp = 0;
  tmp = *px;
  *px = *py;
  *py = tmp;
}
 
int main()
{
  int a = 0;
  int b = 0;
  scanf("%d %d", &a, &b);
  printf("交换前:a=%d b=%d\n", a, b);
  Swap2(&a, &b);
  printf("交换后:a=%d b=%d\n", a, b);
  return 0;
}


运行结果如下:

我们可以看到实现成Swap2的方式,顺利完成了任务,这⾥调用Swap2函数的时候是将变量的地址传递给了函数,这种函数调用方式叫:传址调用


六、野指针


6.1 野指针的成因

那野指针是怎么产生的呢?

一般来说,产生野指针原因有3种:1. 指针未初始化;2. 指针越界访问;3. 指针指向的空间释放


6.1.1 指针未初始化

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
int main()
{
  int *p;//局部变量指针未初始化,默认为随机值
  *p = 20;
  return 0;
}


编译器会发生报错:

6.1.2 指针越界访问

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
int main()
{
  int arr[10] = { 0 };
  int *p = &arr[0];
  int i = 0;
  for (i = 0; i <= 11; i++)
  {
    //当指针指向的范围超出数组arr的范围时,p就是野指针
    *(p++) = i;
  }
  return 0;
}


运行结果如下:、

6.1.3 指针指向的空间释放

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
int* test()
{
  int n = 100;
  return &n;
}
 
int main()
{
  int* p = test();
  printf("%d\n", *p);
  return 0;


运行结果如下:

 

est函数返回之后,确实把n的地址带回来放到p里边去了。但是有一个现象需要我们注意,这个n是个局部变量,进入函数创建,出函数就销毁了。也即是进入函数后存放n的4个字节拿到了,出函数就还给操作系统了,但是p中还存放着这个地址。如果通过解引用操作给它赋值20,将20这个值放到n里面去,这样就非常危险。


6.2 如何规避野指针的出现

6.2.1 初始化

如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL。


NULL 是C语⾔中定义的⼀个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。

初始化如下:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
int main()
{
  int num = 10;
  int*p1 = &num;
  int*p2 = NULL;
 
  return 0;
}


6.2.2 小心越界访问

⼀个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。要解决越界访问,只能小伙伴们自己去解决,多敲代码。


6.2.3 不访问临时变量的地址

临时变量出了作用域就会销毁,系统会回收该空间,所以我们要尽量避免指针指向已经销毁的空间,尤其在函数中,不能返回临时变量的地址。

相关文章
|
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 “华为杯”第二十一届中国研究生数学建模竞赛 选题分析