当前位置: 首页 > news >正文

C语言——深入理解指针(一)

C语言——指针(一)

进入指针后,C语言就有了一定的难度,我们需要认真理解

指针(一)

1 .内存和地址

内存:程序运行起来后,要加载到内存中,数据的存储也是在内存中。
我们知道电脑上有CPU,内存(8G,16G,32G),硬盘(500G/1T),外部设备.我们常说的电脑内存几百G其实是不对的,那其实应该是硬盘的大小。

计算机常见的单位:1个比特位可以存储一个2进制的位1或者0
1个字节(Byte)=8个比特位(bit)
1kB=1024(2^10)Byte
1MB=1024kB,1GB=1024MB
1TB=1024GB
1PB=1024YB
1个字节就是1个内存单元,如下图,16进制数字如0xFFFFFFFF就是1个地址

在这里插入图片描述
内存单元的编号=地址=指针
编址:CPU与内存之间有大量数据交互,两者就必须要用各种“线”连接起来。CPU与内存之间的线有地址总线,数据总线,控制总线;我们需要了解其中一组线叫地址总线
CPU访问内存中的某个字节空间必须知道这个字节空间在内存的什么位置,而因为内存中的字节很多,所以需要给内存进行编址

2.指针变量和地址

1.取地址操作符&:变量创建的本质是内存申请自己的空间,用来存储数据。

#int main()
{int a=10;int* p=&a;//编号=地址=指针//p被称为指针变量,理解为:存放指针的变量//指针变量也是变量,是专门存放地址的
}

2.拆解指针类型

int a=10;
int * pa=&a;

这里pa左边是int*,*是在说明pa指向的是整型(int)类型的对象。(间接访问操作符)
我们在拿到地址(指针),就可以通过地址(指针)指向的对象,需用到解引用操作符

3.** 解引用操作符 **

int main()
{int a=100;int* p=&a;//*p的意思就是通过p中存放的地址找到指向的空间*p=0;//这里是用*取出变量a,把a的值改成了0return 0;
}

4.指针变量的大小
x64下地址是64个比特位,指针变量大小是8个字节,与类型无关;32位同理,地址是32个比特位,指针变量大小是4个字节。
以下是在64位下的指针变量大小
在这里插入图片描述
5.指针变量类型的意义
(1)指针的解引用
指针的类型决定了,对指针解引用的时候有多大的权限(一次能操作几个字节)。比如:char的指针解引用就只能访问一个字节,而int的指针的解引用就能够访问四个字节。
(2)指针加减整数
在这里插入图片描述
由上得出,指针的类型决定了向前或向后走一步走多大距离。
(3)void *指针
可理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型地址。但是也有局限性,该类型的指针不能直接进行指针的加减整数和解引用的运算。

3.指针运算

  • 指针+ -整数
int arr[10]={1,2,3,4,5,6,7,8,9,10};
int i=0;
int sz=sizeof(arr)/sizeof(arr[0]);
int* p=&arr[0];//拿到第一个元素的地址
for(i=0;i<sz;i++)
{
printf("%d ",*p);
p++;//p=p+1;
}
//第二种写法:
int arr[10]={1,2,3,4,5,6,7,8,9,10};
int i=0;
int sz=sizeof(arr)/sizeof(arr[0]);
int* p=&arr[0];
for(i=0;i<sz;i++)
{
printf("%d ",*(p+i));
}

在这里插入图片描述

  • 指针-指针(前提是两个指针指向了同一块空间,否则不能相减)
    指针减指针的绝对值是它们之间元素的个数
int arr[10]={0};
printf("%lld\n,&arr[9]-&arr[0]);//-->结果为9
printf("%lld\n,&arr[0]-&arr[9]);//-->结果为-9

在这里插入图片描述

数组名是数组首元素的地址,即

arr==&arr[0]

例:用指针减指针求字符串长度
在这里插入图片描述

  • 指针的关系运算
    在这里插入图片描述

指针(二)

4. const修饰指针

1.const修饰变量:用const修饰变量后,这个变量的值就不能再被更改

const int a=10;//const就是常属性的意思
printf("%d\n",a);//a就叫常变量,虽然a不能被修改了,但a的本质还是变量

2。** const修饰指针变量 **
但是我们可以利用指针进行更改1中变量的值

const int a=10;
int* pa=&a;
*pa-20;
printf("%d\n",a);
//此时a就被更改了,打印的结果就是20

要是不想让其被更改,可以让const修饰指针变量,即

const int a=10;
//这里有两种写法
const int* pa=&a;//const修饰*pa
int* const pa=&a;//const修饰pa
//此时a的值不能被更改
//*pa就是pa指向的对象

当const放在星左边,const限制的是pa指向的对象,也就是 星pa 不能给修改,但是pa不受限制,也就是指针变量可以改变指向
当const放在星的右边,const限制的是pa,也就是pa的指向不能改变了,但是*pa不受限制,也就是说pa指向的内容,是可以通过pa来改变的

举个例子:

1.
int a=10;
int b=100;
const int* p=&a;
*p-=10;//a=0;
p=&b;//ok
2.
int a=10;
int b=100;
int * const p=&a;
*p-=10;//right
p=&b;//error

5.野指针

  • 概念野指针(指针指向的空间,是不属于当前程序的)就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)
  • 野指针成因:(列举三个)
    (1)指针未初始化
int main()
{int* p;//p是局部变量,未初始化,它里面就存放了一个随机值*p=20;}

(2)指针越界访问

int main()
{int arr[10]={1,2,3,4,5,6,7,8,9,10};int* p=arr;int sz=sizeof(arr)/sizeof(arr[0]);int i=0;for(i=0;i<=sz;i++){printf("%d ",*p);//循环到第11次的时候,p就是野指针了p++;}return 0;
}

在这里插入图片描述
(3)指针指向的空间释放

int* test()
{int n=100;//n是局部变量,生命周期在出函数就结束了,那这个空间就不再属于n了return &n;
}
int main()
{int* p=test();//p里面是n的地址,但当回去再在这个地址找n时,就不一定能找到n了printf("haha");printf("%d\n",*p);//此时,p就成了野指针了return 0;
  • 如何规避野指针
    (1)指针变量要初始化

1.明确知道,指针变量要初始化为什么值
int a=10;
int* pa=&a;
2.不知道指针变量应该给什么值
int* p=NULL;//空指针

(2)小心指针越界。
(3)指针变量不再使用时,及时置NULL,指针使用之前检查有效性。
(4)避免返回局部变量(栈空间)的地址

下面这种叫返回局部变量是可以的
int* test()
{int n=100;return n;
}
int main()
{int r=test();printf("%d\n",r);return 0;
下面这种才叫返回局部变量的地址是不可以的
int* test()
{int n=100;return &n;
}
int main()
{int* p=test();printf("%d\n",*p);return 0;
}

6.assert断言

assert.h头文件定义了宏assert(),用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行,这个宏常常被称为“断言

assert(p!=NULL);

assert()宏接受一个表达式作为参数,如果该表达式为真,assert()不会产生任何作用,程序继续运行。如果该表达式为假,assert()就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。

在这里插入图片描述

它有几个好处:它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert()的机制,如果已经确认程序没有问题,不需要再做断言,就在#include<assert.h>语句前面,定义一个宏NDEBUG

#define NDEBUG
#include<assert.h>

然后,重新编译程序,编译器就会禁用文件中所有的assert()语句。如果程序又出现问题,可以移#define NDEBUG 指令(或者把它注释掉),再次编译,这样就重新启用了assert() 语句。
assert()的缺点是,因为引入了额外的检查,增加了程序的运行时间。
一般我们可以在Debug 中使用,在Release 版本中选择禁用assert 就行,在VS这样的集成开发环境中,在Release 版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在Ralease版本不影响用户使用时程序的效率

指针的使用和传值调用

  • 1.strlen的实现:库函数strlen的功能是求字符串长度,统计的是\0之前的字符的个数,函数原型如下:
size_t strlen(const char * str);

例:

#include<stdio.h>
#include<assert.h>
int my_strlen(const char * str)
{int count=0;assert(str);while(*str){count++;str++;}return count;
}
int main()
{int len=my_strlen("abcdef");printf("%d\n",len);return 0;
}
  • 2.传值调用和传址调用
    那些功能是非指针不可呢?首先我们先来写一下,交换两个整型变量的值
    传值调用:(行不通)
#include<stdio.h> 
void Swap1(int x, int y)
{int z = x;x = y;y = z;
}
int main()
{int a = 10;int b = 20;printf("交换前:a=%d,b=%d\n", a, b);Swap1(a, b);//传值调用//完成不了,因为形参是实参的临时拷贝,改变形参不影响实参printf("交换后:a=%d,b=%d\n", a, b);return 0;
}

所以这里不用指针是完成不了交换的,以下为一种使用指针交换变量的方式
传址调用:

#include<stdio.h>
void Swap2(int* pa,int * pb)
{int z = 0;z = *pa;//z=a*pa = *pb;//a=b*pb = z;
}
int main()
{int a = 10;int b = 20;printf("交换前:a=%d,b=%d\n", a, b);Swap2(&a, &b);//传址调用printf("交换后:a=%d,b=%d\n", a, b);return 0;
}

相关文章:

  • day30 python 模块、包与库的高效使用指南
  • 09、底层注解-@Import导入组件
  • Fastadmin表单分组显示
  • 【2025最新】Spring Boot + Spring AI 玩转智能应用开发
  • 1.1 Epson机器人常用指令1-Print函数、RobotInfo$
  • 实景VR展厅制作流程与众趣科技实景VR展厅应用
  • 将 Element UI 表格拖动功能提取为公共方法
  • Linux云计算训练营笔记day11(Linux CentOS7)
  • 智慧赋能光伏运维——无人机巡检+地面监控双链路覆盖,打造光伏电站管理新标杆
  • Ansible模块——主机名设置和用户/用户组管理
  • 牛客网NC209794:使徒袭来
  • 一周快讯 | 银发文娱旅游一周新鲜事
  • 【愚公系列】《Manus极简入门》048-自然探险之旅:“户外活动规划师”
  • 深入理解 SPI 通信中的时钟极性与相位(CPOL 与 CPHA)
  • ARP 原理总结
  • 全新的开源监控工具CheckCle
  • C++学习:六个月从基础到就业——C++20:范围(Ranges)进阶
  • Supermemory:让大模型拥有“长效记忆“
  • 开源AI大模型等“神秘组合”,如何颠覆零售业数字化转型?
  • 统计客户端使用情况,使用es存储数据,实现去重以及计数
  • 北美票房|华纳又赢了,《死神来了6》开画远超预期
  • 苏丹港持续遭无人机袭击,外交部:呼吁各方保护民用设施和平民安全
  • 中美博弈新阶段,这个“热带中国”火了
  • 世界高血压日|专家:高血压患者控制血压同时应注重心率管理
  • 朱雀二号改进型遥二运载火箭发射成功
  • 全国多家健身房女性月卡延长,补足因月经期耽误的健身时间