【C语言】指针详解(一)

个人主页 : zxctscl
如有转载请先通知

文章目录

  • 1.内存与地址
  • 2.指针变量与地址
    • 2.1 取地址操作符&
    • 2.2 指针变量
    • 2.3 指针类型
    • 2.4 解引用操作符
    • 2.5 指针变量的大小
  • 3. 指针变量类型的意义
    • 3.1 指针的解引用
  • 4. const修饰指针
    • 4.1 const修饰变量
    • 4.2 const修饰指针变量
  • 5. 指针运算
    • 5.1 指针+-整数
    • 5.2 指针-指针
    • 5.3 指针的运算关系
  • 6. 野指针
    • 6.1 野指针成因
    • 6.2 如何规避野指针
      • 6.2.1 指针初始化
      • 6.2.2 小心指针越界
      • 6.2.3 指针变量不再使用时,及时置NULL,指针使用之前检查有效性
  • 7. assert断言
  • 8. 指针的使用和传址调用

1.内存与地址

大家对地址都不陌生,就像在生活中住酒店如何找到房间?那不就通过房卡上的房间号先确定楼层在确定房间。而这些房间号我们也叫地址。
把内存划分为一个个内存单元,一个单元为一个字节,而计算机中都是以一个比特位存储一个2进制位,一个字节也就是8个比特位。
这使得每个内存单元都有一个编号,通过这个编号,就能迅速找到这个内存空间。
在C语言中给地址起了新名叫:指针
内存编号

所以我们理解的:内存单元的编号 == 地址 == 指针

2.指针变量与地址

2.1 取地址操作符&

在C语言中创建变量其实就是在向内存申请空间。就像这样:

#include <stdio.h>
int main()
{int a = 5;return 0;
}

创建了整型变量a,内存中就申请了4个字节,用来存放整数5。每个字节都有地址,而上面申请的4个字节的地址分别在下面划红线四个。
在这里插入图片描述
如果我们想得到a的地址,就需要用到取地址操作符&
像下面这样就可,但值得注意的是我们打印的是地址用到的是**%p**。

#include <stdio.h>
int main()
{int a = 5;&a;printf("%p\n", &a);return 0;
}

x86

2.2 指针变量

我们通过取地址操作符&,得到的仅仅是地址,它就只是一个数值,有时候为了方便使用,我们把它用指针变量存储。

#include <stdio.h>
int main()
{int a = 5;int* p = &a;return 0;
}

指针变量也是一种变量,不过是用来存放地址,存放在指针变量中的值被理解为指针。

2.3 指针类型

指针也是有类型的。
就像前面所写的:

int a = 5;
int* p = &a;

在p的左边就是int*,*说明的是p为指针变量,而前面的int就是说明p指向的是整型(int)类型的对象。
同理指向char型的指针变量就是char*

char b = 'a';
char* p = &b;

2.4 解引用操作符

在C语言中,我们找到地址,就可以对地址所指向的对象,而此时所要用到的就是解引用操作符(*)。

#include <stdio.h>
int main()
{int a = 5;int* p = &a;*p=0;return 0;
}

而在上面的代码中,原来a为5,我们通过指针拿到了a的地址,然后通过解引用操作*p=0将原来a的5改为0。

2.5 指针变量的大小

32位机器假设有32根地址总线,每根地址线出来的电信号转换成数字信号后是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。
如果指针变量是⽤来存放地址的,那么指针变的⼤⼩就得是4个字节的空间才可以。
同理64位机器,假设有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要8个字节的空间,指针变的⼤⼩就是8个字节。

#include <stdio.h>
int main()
{printf("%zd\n", sizeof(char *));printf("%zd\n", sizeof(short *));printf("%zd\n", sizeof(int *));printf("%zd\n", sizeof(double *));printf("%zd\n", sizeof(float *));return 0;
}

32位平台下地址,指针变量大小是4个字节

x86
64位平台下地址,指针变量大小是8个字节
x64
结论:

  1. 32位平台下地址,指针变量大小是4个字节
  2. 64位平台下地址,指针变量大小是8个字节
  3. 注意指针变量的大小和类型是无关的,只要指针类型的变量,在相同的平台下,大小都是相同的。

3. 指针变量类型的意义

指针变量的大小和类型无关,只要是指针变量,在同一个平台下,大小都是一样的,为什么还要有各种各样的指针类型呢

3.1 指针的解引用

对比,下面2段代码,主要在调试时观察内存的变化。

//代码1
#include <stdio.h>
int main()
{int n = 0x11223344;int* pi = &n;*pi = 0;return 0;
}
//代码2
#include <stdio.h>
int main()
{int n = 0x11223344;char* pc = (char*)&n;*pc = 0;return 0;
}

调试我们可以看到,代码1会将n的4个字节全部改为0,
在这里插入图片描述

但是代码2只是将n的第一个字节改为0。
在这里插入图片描述

结论:指针的类型决定了,对指针解引用的时候有多大的权限(一次能操作几个字节)。比如: char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。

4. const修饰指针

4.1 const修饰变量

变量是可以修改的,如果把变量的地址交给一个指针变量,通过指针变量的也可以修改这个变量。
但是如果我们希望一个变量加上一些限制,不能被修改,怎么做呢?这就是const的作用。

#include <stdio.h>
int main()
{int m = 0;m = 20;//m是可以修改的const int n = 0;n = 20;//n是不能被修改的return 0;
}

在这里插入图片描述
代码中n是不能被修改的,其实n本质是变量,只不过被const修饰后,在语法上加了限制,只要我们在代码中对n就行修改,就不符合语法规则,就报错,致使没法直接修改n。

但是如果我们绕过n,使用n的地址,去修改n就能做到了,虽然这样做是在打破语法规则。

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

在这里插入图片描述
可以看到这里一个确实修改了,但是我们还是要思考一下,为什么n要被const修饰呢?就是为了不能被修改,如果p拿到n的地址就能修改n,这样就打破了const的限制,这是不合理的,所以应该让p拿到n的地址也不能修改n,那接下来怎么做呢?

4.2 const修饰指针变量

测试无const修饰的情况:

#include <stdio.h>
//代码1
void test1()
{int n = 10;int m = 20;int* p = &n;*p = 20;//ok?p = &m; //ok?
}

测试const放在*的左边情况:

void test2()
{//代码2int n = 10;int m = 20;const int* p = &n;*p = 20;//ok?p = &m; //ok?
}

在这里插入图片描述

测试const放在*的右边情况

void test3()
{int n = 10;int m = 20;int* const p = &n;*p = 20; //ok?p = &m; //ok?
}

在这里插入图片描述

测试*的左右两边都有const

void test4()
{int n = 10;int m = 20;int const* const p = &n;*p = 20; //ok?p = &m; //ok?
}

在这里插入图片描述
const修饰指针变量的时候:

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

5. 指针运算

指针的基本运算有三种,分别是:
• 指针± 整数
• 指针-指针
• 指针的关系运算

5.1 指针±整数

数组在内存中是连续存储的,只要知道第一个元素的地址,后面的元素依次就能找到。

int arr[]={1,2,3,4,5};

而所对应的下标为0,1,2,3,4。

在对不同类型指针变量加减时结果不同,
举个例子:

#include <stdio.h>
int main()
{int n = 10;int* p1 = &n;char* p2 = &n;printf("p1=%p\n", p1);printf("p1+1=%p\n", p1+1);printf("p2=%p\n", p2);printf("p2+1=%p\n", p2+1);return 0;
}

在下面为结果
int类型的就跳过了4个字节,
char类型就跳过1个字节
指针+-
结论:
指针的类型决定了,指针加减整数时,一次性跳过多少个字节。

5.2 指针-指针

在指针变量相同类型时,计算出的是中间间隔的个数。
举个例子:

#include <stdio.h>
int main()
{int arr[10] = { 0 };int* p1 = &arr[9];int* p2 = &arr[0];int ret = p1-p2 ;printf("%d\n", ret);return 0;
}

结果为
间隔9
指针类型不同时不能进行指针的加减运算。

5.3 指针的运算关系

计算数组的元素个数时,我们使用了sizeof(数组名),而sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。
sizeof(arr[0])计算的是首元素的大小,单位也是字节。

#include <stdio.h>
int main()
{int arr[] = { 1,2,3,4,5,6,7,8,9,10 };int sz = sizeof(arr) / sizeof(arr[0]);printf("%d\n", sz);return 0;
}

数组大小
数组名就是数组首元素(第一个元素)的地址是对的,但是有两个例外:
1.sizeof(数组名),sizeof中单独放数组名,这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。
2.&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
除此之外,任何地方使用数组名,数组名都表示首元素的地址。

6. 野指针

概念: 野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

6.1 野指针成因

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

在这里插入图片描述

  1. 指针越界访问
#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;
}

在这里插入图片描述

  1. 指针指向的空间释放
#include <stdio.h>
int* test()
{int n = 100;return &n;
}
int main()
{int* p = test();printf("%d\n", *p);return 0;
}

在这里插入图片描述

6.2 如何规避野指针

6.2.1 指针初始化

如果明确知道指针指向哪⾥就直接赋值地址,如果不知道指针应该指向哪⾥,可以给指针赋值NULL.NULL 是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址会报错。

#ifdef __cplusplus#define NULL 0
#else#define NULL ((void *)0)
#endif

初始化如下:

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

6.2.2 小心指针越界

一个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。

6.2.3 指针变量不再使用时,及时置NULL,指针使用之前检查有效性

当指针变量指向一块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的一个规则就是:只要是NULL指针就不去访问,同时使用指针之前可以判断指针是否为NULL。
我们可以把野指针想象成野狗,野狗放任不管是⾮常危险的,所以我们可以找一棵树把野狗拴起来,就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗栓前来,就是把野指针暂时管理起来。
不过野狗即使拴起来我们也要绕着走,不能去挑逗野狗,有点危险;对于指针也是,在使用之前,我们也要判断是否为NULL,看看是不是被拴起来起来的野狗,如果是不能直接使用,如果不是我们再去使用。

#include <stdio.h>
int main()
{int arr[10] = { 1,2,3,4,5,67,7,8,9,10 };int* p = &arr[0];for (i = 0; i < 10; i++){*(p++) = i;}//此时p已经越界了,可以把p置为NULLp = NULL;//下次使⽤的时候,判断p不为NULL的时候再使⽤//...p = &arr[0];//重新让p获得地址if (p != NULL) //判断{//...}return 0;
}

7. assert断言

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

assert(p != NULL);

代码在程序运行到这一行语句时,验证变量 p 是否等于 NULL 。如果确实不等于 NULL ,程序继续运行,否则就会终止运行,并且给出报错信息提示。
assert() 宏接受一个表达式作为参数。如果该表达式为真(返回值非零), assert() 不会产生任何作用,程序继续运行。如果该表达式为假(返回值为零), assert() 就会报错,在标准错误流 stderr 中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。
assert() 的使用对程序员是非常友好的,使用assert() 有几个好处:它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭 assert() 的机制。如果已经确认程序没有问题,不需要再做断言,就在 #include<assert.h> 语句的前面,定义一个宏 NDEBUG 。

#define NDEBUG
#include <assert.h>

然后,重新编译程序,编译器就会禁⽤⽂件中所有的 assert() 语句。如果程序⼜出现问题,可以移除这条 #define NDBUG 指令(或者把它注释掉),再次编译,这样就重新启⽤了 assert() 语句。

assert() 的缺点是,因为引入了额外的检查,增加了程序的运行时间。
⼀般我们可以在debug中使用,在release版本中选择禁⽤assert就行,在VS这样的集成开发环境中,在release版本中,直接就是优化掉了。这样在debug版本写有利于程序员排查问题,在release版本不影响用户使用时程序的效率。

8. 指针的使用和传址调用

学习指针的目的是使用指针解决问题,那什么问题,非指针不可呢?
例如:写一个函数,交换两个整型变量的值
一番思考后,我们可能写出这样的代码

#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的地址是0x00cffdd0,b的地址是0x00cffdc4,在调用Swap1函数时,将a和b传递给了Swap1函数,在Swap1函数内部创建了形参x和y接收a和b的值,但是x的地址是0x00cffcec,y的地址是0x00cffcf0,x和y确实接收到了a和b的值,不过x的地址和a的地址不一样,y的地址和b的地址不一样,相当于x和y是独立的空间,那么在Swap1函数内部交换x和y的值,自然不会影响a和b,当Swap1函数调用结束后回到main函数,a和b的没法交换。Swap1函数在使用的时候,是把变量本身直接传递给了函数,这种调用函数的方式我们之前在函数的时候就知道了,这种叫传值调用。

结论:实参传递给形参的时候,形参会单独创建一份临时空间来接收实参,对形参的修改不影响实参。所以Swap是失败的了。

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

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

在这里插入图片描述
有问题请指出,大家一起进步!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/436408.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

矿石运输船数据集、散货船数据集、普通货船数据集、集装箱船数据集、渔船数据集以及客船数据集

海船&#xff1a;用于船只检测的大规模精准标注数据集 我们很高兴地介绍一个新的大规模数据集——海船&#xff0c;该数据集专为训练和评估船只目标检测算法而设计。目前&#xff0c;这个数据集包含31,455张图像&#xff0c;并涵盖了六种常见的船只类型&#xff0c;包括矿石运…

如何使用ssm实现科技银行业务管理系统+vue

TOC ssm743科技银行业务管理系统vue 第一章 绪论 1.1 研究背景 在现在社会&#xff0c;对于信息处理方面&#xff0c;是有很高的要求的&#xff0c;因为信息的产生是无时无刻的&#xff0c;并且信息产生的数量是呈几何形式的增加&#xff0c;而增加的信息如何存储以及短时间…

网络通信——动态路由协议RIP

目录 一.动态路由协议分类 二.距离矢量路由协议 &#xff08;理解&#xff09; 三. 链路状态路由协议&#xff08;理解&#xff09; 四.RIP的工作原理 五.路由表的形成过程 六. RIP的度量值&#xff08;条数&#xff09;cost 七.RIP的版本&#xff08;v1和v2&#xff0…

springboot整合seata

一、准备 docker部署seata-server 1.5.2参考&#xff1a;docker安装各个组件的命令 二、springboot集成seata 2.1 引入依赖 <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId>&…

数据清洗第1篇章 - 处理缺失值和重复值

数据清洗是数据分析过程中至关重要的一步&#xff0c;它确保数据的准确性、一致性和完整性。这不仅有助于提高分析结果的可靠性和有效性&#xff0c;还能为算法建模决策提供高质量的数据基础。在进行数据分析和建模的过程中&#xff0c;大量的时间花在数据准备上&#xff1a;加…

【Linux服务器】git和github交互使用

前言&#xff1a;有时候pycharm连接不上github&#xff0c;还是得命令行操作 目录 1. 准备git2. 配置github账户3. 上传项目3.1 创建本地仓库3.2 提交本地代码3.3 上传到github 4. 注意 1. 准备git 下载链接&#xff1a;官网 下载后直接运行安装&#xff0c;cmd输入git --vers…

Redis篇(缓存机制 - 多级缓存)(持续更新迭代)

目录 一、传统缓存的问题 二、JVM进程缓存 1. 导入案例 2. 初识Caffeine 3. 实现JVM进程缓存 3.1. 需求 3.2. 实现 三、Lua语法入门 1. 初识Lua 2. HelloWorld 3. 变量和循环 3.1. Lua的数据类型 3.2. 声明变量 3.3. 循环 4. 条件控制、函数 4.1. 函数 4.2. 条…

set和map结构的使用

个人主页&#xff1a;敲上瘾-CSDN博客 个人专栏&#xff1a;游戏、数据结构、c语言基础、c学习、算法 目录 一、序列式容器和关联式容器 二、set和multiset 1.insert 2.erase 3.find 4.count 三、map和mapmulti 1.pair 2.insert 3.find 4.operator[ ] 5.erase 6.lo…

UE虚幻引擎云渲染汽车动画的优势!

在汽车广告和动画制作领域&#xff0c;虚幻引擎&#xff08;UE&#xff09;结合云渲染技术正掀起一场技术革命。这项技术以其高性能、成本效益和灵活性&#xff0c;为创作者提供了强大的工具&#xff0c;以实现更加逼真和高效的汽车动画制作。 一、为什么选择UE虚幻引擎制作汽车…

MATLAB案例 | Copula的密度函数和分布函数图

本文介绍各种类型&#xff08;Gaussian、t、Gumbel、Clayton、Frank&#xff09;Copula的密度函数和分布函数图的绘制 完整代码 clc close all clear%% ********************计算Copula的密度函数和分布函数图************************ [Udata,Vdata] meshgrid(linspace(0,1…

armbian安装docker

最近又搞了台瑞莎Radxa 3E &#xff0c;从零开始部署unbuntu环境&#xff0c;发现是真曲折啊&#xff0c;虽然有点前车之鉴了 在Armbian上安装Docker&#xff0c;可以按照以下步骤操作&#xff1a; 1、更新软件包列表&#xff1a; sudo apt-get update 2、安装必要的软件包…

Web和UE5像素流送、通信教程

一、web端配置 首先打开Github地址&#xff1a;https://github.com/EpicGamesExt/PixelStreamingInfrastructure 找到自己虚幻引擎对应版本的项目并下载下来&#xff0c;我这里用的是5.3。 打开项目找到PixelStreamingInfrastructure-master > Frontend > implementat…

算法训练营打卡Day19

目录 1.二叉搜索树的最近公共祖先 2.二叉树中的插入操作 3.删除二叉搜索树中的节点 题目1、二叉搜索树的最近公共祖先 力扣题目链接(opens new window) 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。 百度百科中最近公共祖先的定义为&#xff1a;“对于有…

【数据结构与算法】算法和算法分析

文章目录 一.算法1.定义2.描述 二.算法与程序三.算法特性四.算法效率的度量4.1算法时间事前分析法算法时间复杂度的渐进表示法分析算法时间复杂度的基本方法 4.2算法空间 数据的逻辑结构映像到内存就是数据的存储结构&#xff0c;针对数据的逻辑结构可以选择多种存储结构。数据…

python --qt5(webview)/防多开/套壳网页/多次点击激活旧窗口

pyqtwebengine5.12 PyQt55.12class MyWindow(QMainWindow):def __init__(self):super(MyWindow, self).__init__()self.browser QWebEngineView(self) # 如果不写self则新生成一个窗口self.browser.setWindowTitle(技术领域占比分析)self.browser.setWindowIcon(QIcon(LOGO_P…

C0007.Clion中添加ui文件及运行的完整步骤

1.创建ui文件 选择Ui文件目录,右击,打开Qt Designer; 创建完成后,保存ui界面,并且命名为test.ui; 2.新建头文件test.h 在include目录中,新建头文件,文件名为test.h 3.新建test.cpp源文件

基于SpringBoot的休闲娱乐代理售票系统设计与实现

1.1研究背景 21世纪&#xff0c;我国早在上世纪就已普及互联网信息&#xff0c;互联网对人们生活中带来了无限的便利。像大部分的企事业单位都有自己的系统&#xff0c;由从今传统的管理模式向互联网发展&#xff0c;如今开发自己的系统是理所当然的。那么开发休闲娱乐代理售票…

Leetcode面试经典150题-322.零钱兑换

给你一个整数数组 coins &#xff0c;表示不同面额的硬币&#xff1b;以及一个整数 amount &#xff0c;表示总金额。 计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额&#xff0c;返回 -1 。 你可以认为每种硬币的数量是无限的。 示…

Java项目实战II基于Java+Spring Boot+MySQL的大创管理系统(源码+数据库+文档)

目录 一、前言 二、技术介绍 三、系统实现 四、文档参考 五、核心代码 六、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者 一、前言 在当前创新创业氛围浓厚的背景下&#xff0c;大学生创新创业项目&#xff08;简称“大创”&#xff0…

【MySQL】-- 数据库基础

文章目录 1. 数据库简介1.1 什么是数据库1.2 什么是关系型数据库 2. 客户端与服务器的通讯方式2.1 CS架构 3. MySQL架构 1. 数据库简介 1.1 什么是数据库 什么是数据库&#xff1f; 组织和保存数据的应用程序。数据库和之前学的数据结构有什么关系&#xff1f; 数据结构是组织数…