你不知道的C语言知识(第四期:指针【1】)

2024-09-02 19:52

本文主要是介绍你不知道的C语言知识(第四期:指针【1】),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述
本期介绍🍖
主要介绍:C语言中一些大家熟知知识点中的盲区,这是第四期,主讲指针方面。


文章目录

  • 1. 内存和地址
    • 1.1 内存
    • 1.2 该如何理解编址
  • 2. 指针类型的意义
    • 2.1 指针的解引用
    • 2.2 指针加减整数
  • 3. const修饰
    • 3.1 const修饰变量
    • 3.2 const修饰指针变量
  • 4. 指针运算
  • 5. 野指针
    • 5.1 指针未初始化
    • 5.2 指针越界访问
    • 5.3 指针指向的空间被释放
  • 6. assert断言
  • 7. 传值调用和传址调用
    • 7.1 传值调用
    • 7.2 传址调用


1. 内存和地址

1.1 内存

  内存是计算机中一个非常重要的存储器,为系统、软件、程序的运行提供了足够的空间。值得注意的是,内存只用于暂时性的存放数据和程序,一旦关闭电源或发生断电,其中的程序和数据将会丢失。
  我们知道CPU在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中。当下买电脑的时候,内存的容量有8GB、16GB、32GB,面对这么大一块内存空间,该如何高效管理呢?
  面对这么大的一块内存空间,不可能就只存放一个数据的,首先就需要将它划分成一个个小的内存单元,每个内存单元占1个字节的空间用于存放数据,如下图所示。想要准确的找到所需的内存单元该如何是好呢?。在生活中,快递公司想将包裹送到你家,必然需要有你家的地址。同理,只需给每个内存单元编号,通过这个编号就能找到该内存单元。

在这里插入图片描述
  生活中我们把门牌叫做地址,同理在计算机所说的 “地址” 就是指中内存单元的编号,C语言中又给地址起了个新名字 “指针”。所以我们可以理解为:内存单元的编号 == 地址 == 指针


1.2 该如何理解编址

  计算机内有很多硬件单元,而硬件单元是要相互协同的(协同:指相互之间进行数据的交互)。但是硬件单元之间是相互独立的,故需要用 “线” 将它们连起来,CPU与内存之间就存在着3条线(地址总线数据总线控制总线),如下图所示。
在这里插入图片描述
  其中控制总线用于传输CPU是要读取数据、写入数据,地址总线传输需要写入/读取的内存地址,数据总线最后再将数据进行传输,要么读入CPU中要么写入内存空间中。今天主要讲地址总线,思考一下:内存地址的编址是如何通过硬件来实现的呢?
  地址总线具体有多少根线,可以从计算机处理器的位数上知晓,就譬如32位机器就有32根地址线,64位机器就有64根地址线。而每根地址线只有得电、失电两种,转换成数字量就是0、1。那么1根地址线能表示2种状态,2根地址线能表示4种状态,3根地址线能表示8种状态。以此类推,32根地址线就能够表示232种状态,每种状态都代表一个地址,那就是有232个地址,从0X000000000XFFFFFFFF(十六进制表示形式)。
  值得注意:32位的地址需要用32个bit位来存放,即4个byte。那么64位的地址,自然需要使用64个bit位来存放,即8个byte。


2. 指针类型的意义

  在相同的平台上,指针变量的大小与类型一点关系都没有。举例:在32位平台上char*int*double*指针的大小都是4个字节。问:那为什么还需要存在各种各样的指针类型呢?重定义一个专门用与指针的类型不好吗?

2.1 指针的解引用

  通过指针的*(解引用操作符),可以访问到指针所指向的那个对象,那指针的类型存在的意义会不会在此呢?如下所示案例。
在这里插入图片描述
  当在调试的时候观察内存窗口,会发现int*的指针“解引用”一次能够修改4个字节,而char*型的指针“解引用”一次只能够修改1个字节,如下图所示。
在这里插入图片描述
  结论: 指针的类型决定了,指针在解引用操作的时候一次能够访问空间的大小。


2.2 指针加减整数

  对指针进行+-整数操作,也就是对地址进行+-整数操作,是不是只增加/减少几个地址编号,也就是跳过一个内存单元(1个字节空间)?举例如下所示。

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

在这里插入图片描述
  可以看出,整型指针加1后跳过了4个字节,也就是一个整型变量,字符指针加1后跳过了1个字节,也就是一个字符变量。其实指针加1就是跳过了一个指针指向类型的元素。
  结论: 指针的类型决定了指针向前或向后走一步有多大距离。


3. const修饰

3.1 const修饰变量

  const作为C语言中的一个关键字,一般用于修饰某个变量。当变量被const修饰时,那么这个变量就拥有了恒定不变的属性。举例如下。

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

在这里插入图片描述
  当想更改已经被const修饰的变量,会发现编译器报错不让修改。这样变量a不就变得跟常量一样了嘛,但这是否就意味着被const修饰的变量就变为常量了?举例如下。

#include<stdio.h>
int main()
{int arr[10] = { 0 };const int n = 10;char str[n] = { 0 };return 0;
}

在这里插入图片描述
  在创建数组时,数组[]内必须是常量表达式,假设const修饰后的变量会转换为常量,那么一定会得到:使用const修饰的变量去创建数组是能够通过。但真正去这么做的时候,编译器就报错了,它不认为const修饰的变量是常量,从这可知const修饰的变量任然是变量。
  结论: const只是赋予变量常属性,本质上其还是变量,并没有变成常量,只是拥有了常量那不可变的性质。


3.2 const修饰指针变量

  值得注意的是,const修饰过变量虽然无法再通过变量名去访问修改该变量,但是可以另辟蹊径通过变量的地址去间接的访问,举例如下。

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

在这里插入图片描述
  发现指针可以间接的修改const修饰的变量,但问题来了,如果想让变量不被指针间接的修改掉,该这么做呢?答案是:使用const修饰指向变量的指针。一般来说const修饰指针变量,可以放在*的左边,也可以放在*的右边,意义是不一样的。

  1. const放在*的左边

  const限制的是*p,意思是不能通过p来改变p指向对象的内容,但是p本身是可以改变的,能指向其他对象。举例如下。

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

在这里插入图片描述

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

在这里插入图片描述

  1. const放在*的右边:

  const限制的是p,意思是p本身无法改变,不能指向其他对象,但是可以通过p来改变指向对象的内容。举例如下。

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

在这里插入图片描述

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

在这里插入图片描述
  当然const在指针变量的*的两边都有时时,例如:const char* const p = &num,这样既修饰的是*p也修饰了p。也就是说指针p既不能改变其所指向对象,也不能通过解引用的方法来改变内容。


4. 指针运算

  指针的基本运算有三种:指针加减整数指针减指针指针的关系运算。其中指针加减整数已经讲述过了,不再多赘述。指针的关系运算顾名思义就是:指针跟指针之间的比大小,也就是地址之间的比大小。比较难理解的是指针减指针运算,那该怎么理解呢?如下所示。
在这里插入图片描述
  首先,我们知道指针加减整数会得到一个新的指针,转换公式:指针减指针会得到一个正负整数,如上图所示。那么指针减指针得到的这个整数究竟代表什么?
  答案: 元素的个数,指针与指针之间元素的个数。举个例子:通过指针减指针实现求字符串长度。

#include<stdio.h>
int my_strlen(char* str)
{char* head = str;char* end = str;while (*end != '\0'){end++;}return end - head;
}
int main()
{char str[] = "hello world";int sz = my_strlen(str);printf("字符串长度 = %d\n", sz);return 0;
}

在这里插入图片描述

  指针减指针运算的前提是,两个指针的类型相同,并且指向同一块空间。指针减指针的绝对值,求的是两个指针之间的元素个数。


5. 野指针

  野指针就是:指针指向的位置是不可知的、随机的、没有明确限制的。最为常见的产生野指针的情况有三种:指针未初始化指针越界访问指针指向的空间被释放

5.1 指针未初始化

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

  为了避免这种情况出现,可以在创建一个暂时不需要使用的指针时,初始化该指针为NULL(空指针)。NULL是C语言中定义的一个标识符常量,值为0,地址为0x00000000。这个地址是无法访问的,读写该地址就会报错。
  当然只初始化为空指针是不够的,在使用指针之前还是需要对其进行是否有效的判断,即判断是否不为NULL,只有不为空才能进行读写操作。当后期不再使用某指针访问空间的时候,就应该把该指针置为NULL。举例如下。

#include<stdio.h>
int main()
{int* p = NULLif(p != NULL){...}return 0;
}

5.2 指针越界访问

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

  指针越界访问是指:访问了不属于你的空间,也就是那些没有申请的空间。指针越界访问是无法避免的,程序员只能说尽量避免去写越界访问的程序。


5.3 指针指向的空间被释放

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

  函数不能返回函数内部定义的局部变量的地址。


6. assert断言

  assert(表达式)是一个宏,常常被称为:断言,使用这个宏需要引用<assert.h>头文件。如果表达式为真,assert不会有任何动作;如果表达式为假,assert就会果断报错终止运行程序。报错的信息会包含没有通过的表达式,以及包含这个表达式的文件名和行号。举例如下。

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

在这里插入图片描述
  assert的使用对程序员是非常友好的,它不仅能自动标识文件地址和出问题的行号,还能做到无需更改代码,就能关闭assert的机制。如果已经确定程序没有问题,不需要再做断言,只需在#include<assert.h>语句前定义一个宏NDEBUG,就能关闭所有断言。举例如下。

#define NDEBUG
#include<assert.h>
#include<stdio.h>
int main()
{int a = 0;assert(a != 0);printf("%d\n", a);return 0;
}

在这里插入图片描述
  值得注意,在Release版本中assert直接就被优化掉了,不会执行。因为assert的缺点是引入了额外的检查,增加了程序的运行时间,影响用户的体验。但在Debugassert可以使用排查问题。


7. 传值调用和传址调用

7.1 传值调用

  首先思考一个问题:如果想让函数一次返回2个参数该怎么做?return能做到吗?做不到,因为return返回参数是通过eax寄存器返回的,只能做到返回一个参数。那该怎么办?能够通过函数的参数返回吗?如下所示。

#include<stdio.h>//交换两个参数里面的值
void exchange(int x, int y)
{int tmp = 0;tmp = x;x = y;y = tmp;
}int main()
{int a = 10;int b = 20;printf("a = %d, b = %d\n", a, b);exchange(a, b);printf("a = %d, b = %d\n", a, b);return 0;
}

在这里插入图片描述

  当交换函数内的两个形参,函数外实参会发生变化吗?如上图所示。不会发生任何变化,那为什么不会改变呢?那是因为调用函数时,传递过去的形参和实参不是同一块内存空间,改变形参实参自然不会发生变化。如下所示。
在这里插入图片描述
  结论: 函数传参时,形参只是实参的一份临时拷贝,形参发生变化,实参是不会跟着改变的,因为两者不在同一块空间。


7.2 传址调用

  传值调用无法做到传递两个参数出来,那如果将传递的参数变为变量的地址呢?这样函数内部不就可以间接的访问到函数外部的参数了嘛。如下所示。

#include<stdio.h>
void exchange(int* pa, int* pb)
{int tmp = 0;tmp = *pa;*pa = *pb;*pb = tmp;
}int main()
{int a = 10;int b = 20;printf("a = %d, b = %d\n", a, b);exchange(&a, &b);printf("a = %d, b = %d\n", a, b);return 0;
}

在这里插入图片描述

  可以看出,传址调用可以让主调函数和被调函数之间建立真正的联系,在被调函数内存可以修改主调函数中的变量。如下图所示。
在这里插入图片描述


在这里插入图片描述

这份博客👍如果对你有帮助,给博主一个免费的点赞以示鼓励欢迎各位🔎点赞👍评论收藏⭐️,谢谢!!!
如果有什么疑问或不同的见解,欢迎评论区留言欧👀。

这篇关于你不知道的C语言知识(第四期:指针【1】)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1130912

相关文章

C语言中的数据类型强制转换

《C语言中的数据类型强制转换》:本文主要介绍C语言中的数据类型强制转换方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录C语言数据类型强制转换自动转换强制转换类型总结C语言数据类型强制转换强制类型转换:是通过类型转换运算来实现的,主要的数据类型转换分为自动转换

利用Go语言开发文件操作工具轻松处理所有文件

《利用Go语言开发文件操作工具轻松处理所有文件》在后端开发中,文件操作是一个非常常见但又容易出错的场景,本文小编要向大家介绍一个强大的Go语言文件操作工具库,它能帮你轻松处理各种文件操作场景... 目录为什么需要这个工具?核心功能详解1. 文件/目录存javascript在性检查2. 批量创建目录3. 文件

C语言实现两个变量值交换的三种方式

《C语言实现两个变量值交换的三种方式》两个变量值的交换是编程中最常见的问题之一,以下将介绍三种变量的交换方式,其中第一种方式是最常用也是最实用的,后两种方式一般只在特殊限制下使用,需要的朋友可以参考下... 目录1.使用临时变量(推荐)2.相加和相减的方式(值较大时可能丢失数据)3.按位异或运算1.使用临时

使用C语言实现交换整数的奇数位和偶数位

《使用C语言实现交换整数的奇数位和偶数位》在C语言中,要交换一个整数的二进制位中的奇数位和偶数位,重点需要理解位操作,当我们谈论二进制位的奇数位和偶数位时,我们是指从右到左数的位置,本文给大家介绍了使... 目录一、问题描述二、解决思路三、函数实现四、宏实现五、总结一、问题描述使用C语言代码实现:将一个整

C语言字符函数和字符串函数示例详解

《C语言字符函数和字符串函数示例详解》本文详细介绍了C语言中字符分类函数、字符转换函数及字符串操作函数的使用方法,并通过示例代码展示了如何实现这些功能,通过这些内容,读者可以深入理解并掌握C语言中的字... 目录一、字符分类函数二、字符转换函数三、strlen的使用和模拟实现3.1strlen函数3.2st

Go语言中最便捷的http请求包resty的使用详解

《Go语言中最便捷的http请求包resty的使用详解》go语言虽然自身就有net/http包,但是说实话用起来没那么好用,resty包是go语言中一个非常受欢迎的http请求处理包,下面我们一起来学... 目录安装一、一个简单的get二、带查询参数三、设置请求头、body四、设置表单数据五、处理响应六、超

C语言中的浮点数存储详解

《C语言中的浮点数存储详解》:本文主要介绍C语言中的浮点数存储详解,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、首先明确一个概念2、接下来,讲解C语言中浮点型数存储的规则2.1、可以将上述公式分为两部分来看2.2、问:十进制小数0.5该如何存储?2.3 浮点

国内环境搭建私有知识问答库踩坑记录(ollama+deepseek+ragflow)

《国内环境搭建私有知识问答库踩坑记录(ollama+deepseek+ragflow)》本文给大家利用deepseek模型搭建私有知识问答库的详细步骤和遇到的问题及解决办法,感兴趣的朋友一起看看吧... 目录1. 第1步大家在安装完ollama后,需要到系统环境变量中添加两个变量2. 第3步 “在cmd中

Java8需要知道的4个函数式接口简单教程

《Java8需要知道的4个函数式接口简单教程》:本文主要介绍Java8中引入的函数式接口,包括Consumer、Supplier、Predicate和Function,以及它们的用法和特点,文中... 目录什么是函数是接口?Consumer接口定义核心特点注意事项常见用法1.基本用法2.结合andThen链

基于Python实现多语言朗读与单词选择测验

《基于Python实现多语言朗读与单词选择测验》在数字化教育日益普及的今天,开发一款能够支持多语言朗读和单词选择测验的程序,对于语言学习者来说无疑是一个巨大的福音,下面我们就来用Python实现一个这... 目录一、项目概述二、环境准备三、实现朗读功能四、实现单词选择测验五、创建图形用户界面六、运行程序七、