你不知道的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

相关文章

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

sqlite3 相关知识

WAL 模式 VS 回滚模式 特性WAL 模式回滚模式(Rollback Journal)定义使用写前日志来记录变更。使用回滚日志来记录事务的所有修改。特点更高的并发性和性能;支持多读者和单写者。支持安全的事务回滚,但并发性较低。性能写入性能更好,尤其是读多写少的场景。写操作会造成较大的性能开销,尤其是在事务开始时。写入流程数据首先写入 WAL 文件,然后才从 WAL 刷新到主数据库。数据在开始

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl

透彻!驯服大型语言模型(LLMs)的五种方法,及具体方法选择思路

引言 随着时间的发展,大型语言模型不再停留在演示阶段而是逐步面向生产系统的应用,随着人们期望的不断增加,目标也发生了巨大的变化。在短短的几个月的时间里,人们对大模型的认识已经从对其zero-shot能力感到惊讶,转变为考虑改进模型质量、提高模型可用性。 「大语言模型(LLMs)其实就是利用高容量的模型架构(例如Transformer)对海量的、多种多样的数据分布进行建模得到,它包含了大量的先验

系统架构师考试学习笔记第三篇——架构设计高级知识(20)通信系统架构设计理论与实践

本章知识考点:         第20课时主要学习通信系统架构设计的理论和工作中的实践。根据新版考试大纲,本课时知识点会涉及案例分析题(25分),而在历年考试中,案例题对该部分内容的考查并不多,虽在综合知识选择题目中经常考查,但分值也不高。本课时内容侧重于对知识点的记忆和理解,按照以往的出题规律,通信系统架构设计基础知识点多来源于教材内的基础网络设备、网络架构和教材外最新时事热点技术。本课时知识

C语言 | Leetcode C语言题解之第393题UTF-8编码验证

题目: 题解: static const int MASK1 = 1 << 7;static const int MASK2 = (1 << 7) + (1 << 6);bool isValid(int num) {return (num & MASK2) == MASK1;}int getBytes(int num) {if ((num & MASK1) == 0) {return

MiniGPT-3D, 首个高效的3D点云大语言模型,仅需一张RTX3090显卡,训练一天时间,已开源

项目主页:https://tangyuan96.github.io/minigpt_3d_project_page/ 代码:https://github.com/TangYuan96/MiniGPT-3D 论文:https://arxiv.org/pdf/2405.01413 MiniGPT-3D在多个任务上取得了SoTA,被ACM MM2024接收,只拥有47.8M的可训练参数,在一张RTX

【C++学习笔记 20】C++中的智能指针

智能指针的功能 在上一篇笔记提到了在栈和堆上创建变量的区别,使用new关键字创建变量时,需要搭配delete关键字销毁变量。而智能指针的作用就是调用new分配内存时,不必自己去调用delete,甚至不用调用new。 智能指针实际上就是对原始指针的包装。 unique_ptr 最简单的智能指针,是一种作用域指针,意思是当指针超出该作用域时,会自动调用delete。它名为unique的原因是这个

如何确定 Go 语言中 HTTP 连接池的最佳参数?

确定 Go 语言中 HTTP 连接池的最佳参数可以通过以下几种方式: 一、分析应用场景和需求 并发请求量: 确定应用程序在特定时间段内可能同时发起的 HTTP 请求数量。如果并发请求量很高,需要设置较大的连接池参数以满足需求。例如,对于一个高并发的 Web 服务,可能同时有数百个请求在处理,此时需要较大的连接池大小。可以通过压力测试工具模拟高并发场景,观察系统在不同并发请求下的性能表现,从而

C语言:柔性数组

数组定义 柔性数组 err int arr[0] = {0}; // ERROR 柔性数组 // 常见struct Test{int len;char arr[1024];} // 柔性数组struct Test{int len;char arr[0];}struct Test *t;t = malloc(sizeof(Test) + 11);strcpy(t->arr,