三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

指针劝退指南??

指针劝退指南??

文章目录

  • 内存和地址
    • 内存
    • 理解编址
  • 指针变量和地址
    • 取地址操作符(&)
  • 指针变量和解引用操作符(*)
    • 指针变量
    • 如何拆解指针类型
    • 解引用操作符(*)
  • 指针变量的大小
  • 指针变量类型的意义
    • 指针的解引用
    • 指针+-整数
    • void*指针
  • 指针运算
    • 指针+-整数
    • 指针-指针
    • 指针的关系运算
  • const修饰指针
    • const修饰变量
  • 野指针
    • 野指针成因
  • 如何避免野指针
    • 指针变量初始化
    • 注意不要让指针越界
    • 指针变量不再使用时,及时置NULL,指针使用之前检查有效性
    • 避免返回局部变量的地址
  • assert断言
  • 指针的使用和传址调用
    • 传值调用和传址调用
  • 总结
    • 1. 内存与地址基础
    • 2. 指针变量与操作符
    • 3. 指针类型的重要性
    • 4. 指针运算
    • 5. const修饰指针
    • 6. 野指针与安全使用
    • 7. 实用工具与技巧
    • 关键要点

内存和地址

内存

在我们了解内存之前,可以用一个生活案例来帮助我们理解,在日常生活中我们网上购物后都要填写一个地址,如果我们不去填写地址那么我们购买的东西就不知道发到哪里去,有了地址商家就能快速的找到你并把货物给你.
如果把上面例子应用到计算机中呢?
我们直到计算机上的中央处理器(CPU)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回到内存中,我们的电脑内存都是8G/16G/32G等,如何来高效的管理这些内存呢?
其实是把这些内存划分为了一个个的内存单元.每个内存单元的大小为1个字节(8比特位),一个比特位可以存储一个2进制的位1或者0,一个内存单元就是一个字节,每个内存单元也有一个编号,有了这个内存单元的编号,cpu就可以根据编号快速找到所需要的内存空间

在计算机中我们把内存单元的编号也称为地址,C语言中给地址起了个新名字:指针
内存单元的编号 == 地址 == 指针

理解编址

首先,计算机内是有很多硬件单元的,硬件单元是要互相协同工作的,所谓的协同工作至少相互之间可以进行数据传递
但是硬件与硬件之间是相互独立的如何进行数据传递呢?其实他们之间是通过"线"连起来进而进行数据传递
当我们想要往内存中写入一些数据的时候控制总线会发出写入命令,然后地址总线会传输地址告诉我们位置,然后通过数据总线传入我们要写入的数据

在计算机中的编址,并不是把每个字节的地址都记录下来存贮,而是通过硬件设计完成的
我们可以简单理解32位的计算机有32根地址总线,每个线只有两种状态表示0或1脉冲有无),那么32根地址总线就能表示2^32种含义每一种含义都代表一个地址
地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传入cpu内寄存器

指针变量和地址

取地址操作符(&)

理解了内存和地址的关系,在C语言中创建变量就是向内存中申请空间

例如上述代码就是创建了整型变量a,向内存中申请了四个字节用来存放整数10,其中每个字节都有地址

那我们如何取得a的地址呢?这里就得学习一个操作符&–取地址操作符

intmain(){inta=10;printf("%p",&a);return0;}

上面截图可以得知这个10中四个字节每个字节都有地址那么打印出来的到底是谁的地址呢?

&a取出的是a所占4个字节中地址较小的字节的地址
虽然整型变量占用4个字节,我们只要知道了第一个字节地址,顺藤摸瓜就知道后面的地址

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

指针变量

如果我们想要把地址存放起来呢?应该存放在哪里?
答案是:指针变量中

intmain(){inta=10;int*p=&a;//取出a的地址并存放在指针变量p中//指针变量也是变量,是存放指针的变量return0;}

如何拆解指针类型

我们上面看到的int *是指针变量p的类型,我们该如何理解指针的类型呢?

inta=10;int*p=&a;

这里p的左边写的是int *, *是在说明p是指针变量而前面的int是在说明p执行的是整型(int)类型的对象

chara=10;char*p=&a;

如果指向的是字符类型那么指针变量p的类型就是char *

解引用操作符(*)

我们把地址保存起来的目的是要使用的,那么我们如何使用呢?
我们只要拿到了地址(指针)就可以通过地址(指针)找到地址指向的对象这里我们要学习一个操作符叫解引用操作符(*)

intmain(){inta=10;int*p=&a;*p=20;printf("%d",a);return0;}


上面代码中就使用了解引用操作符,*p的意思就是通过p中存放的地址找到了指向的空间,*p其实就是a这个变量,所以上述代码就是把变量a改成了20

指针变量的大小

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

指针变量类型的意义

指针的解引用

intmain(){inta=0x11223344;int*p=&a;*p=0;return0;}


intmain(){inta=0x11223344;char*p=&a;*p=0;return0;}


对比上面两段代码我们可以看到第一段代码把四个字节都改为了0但是代码2只把第一个字节改为了0
结论:指针的类型决定了对指针解引用的时候有多大的权限(一次能操作几个字节)
比如char的指针解引用就只能访问一个字节,int的指针的解引用就只能访问四个字节

指针±整数

intmain(){inta=0x11223344;char*p=&a;int*pa=&a;printf("p =%p\n",p);printf("p+1 =%p\n",p+1);printf("pa =%p\n",pa);printf("pa+1=%p\n",pa+1);return0;}


我们可以看出 char类型的指针变量+1跳过一个字节,int类型的指针变量+1跳过了4个字节,这就是指针变量的类型差异带来的变化,指针+1跳过一个指针指向的元素
结论:指针的类型决定了指针向前或向后走多大字节

void*指针

在指针类型中有一种特殊的类型是void *类型,可以理解为无具体类型的指针(或者泛型指针),这种类型的指针可以用来接收任意类型地址,但是也有局限性,void *类型的指针不能直接进行指针的±整数和解引用的运算

intmain(){inta=0x11223344;charo='w';char*p=&a;void*b=&a;b=&o;return0;}


o
我们可以看到void *类型的指针可以接收不同类型的地址,但是无法直接进行指针运算
那么void * 类型的指针到底能干啥呢?
一般void *类型的指针是使用在函数参数部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果,使得一个函数来处理多种类型的数据

指针运算

指针的运算就是让我们去访问内存中的数据
指针的基本运算有三种分别是:

  • 指针± 整数
  • 指针-指针
  • 指针的关系运算

指针±整数

因为数组在内存中是连续存在的,之要知道第一个元素的地址后面所有元素就都可以知道

intmain(){inta[10]={1,2,3,4,5,6,7,8,9,0};intnum=sizeof(a)/sizeof(a[0]);inti=0;int*p=&a[0];for(i=0;i<num;i++){printf("%d ",*(p+i));}}

intmain(){chara[]="hello world";char*p=&a[0];while(*p)// /0的ascll码值为0{printf("%c ",*p);p++;}return0;}

指针-指针

指针-指针的绝对值:得到的是两个指针之间的元素
前提:两个指针指向了同一块空间,否则不能相减

intmain(){inta[10]={0};printf("%lld",&a[9]-&a[0]);printf("%lls",&a[0]-&a[9]);return0;}


数组随着下标的增长,地址是由低到高变化的

指针的关系运算

例如我们要打印一个数组

intmain(){inta[10]={1,2,3,4,5,6,7,8,9,0};intu=sizeof(a)/sizeof(a[0]);int*p=a;while(p<a+u)//使用指针的关系运算比较大小{printf("%d ",*p);p++;}}

const修饰指针

const修饰变量

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



const 常属性的意思就是不能改变的意思
这里a本质上还是变量
只是在语法层面上限制了a的修改

如果我们通过指针去修改它的值也是可以修改的,但是我们并不想让它去修改,那么依旧是加上const


const在修饰变量的时候,const可以放在的左边,const限制的是pa指向的对象也就是pa不能修改但是pa不受限制也就是指针变量可以改变指向

intmain(){inta=10;intb=20;constint*pa=&a;//此时限制的是*pa,但是没有限制指针变量p,我们可以去改变指针变量的指向pa=&b;*pa=30;//err}

const 也可以放在的右边,此时const限制的是pa,也就是pa的指向不能改变了但是pa不受限制,即pa指向的内容

intmain(){inta=10;intb=20;int*constpa=&a;//此时限制的是pa,但是没有限制*p,我们可以去改变a的值pa=&b;//err*pa=30;}

野指针

野指针就是指针指向的位置是不可知的(随机的,不正确的,没有明确限制的)即指针指向的空间是不属于当前程序的

野指针成因

  • 指针未初始化
intmain(){int*p;//指针没有进行初始化,p是局部变量,局部变量没有进行初始化,它里面存放的是一个随机值*p=10;}
  • 指针越界访问
intmain(){inta[10]={0};int*p=a;inti=0;into=sizeof(a)/sizeof(a[0]);for(i=0;i<=o;i++)//循环到第11次的时候p就是野指针{p+i;printf("%d",*p);}return0;}
  • 指针指向的空间释放
inttest(){inti=100;//i为局部变量当test函数执行完毕后i就被销毁了return&i;}intmain(){int*a=test();printf("%d",*a);//这里*a就是野指针return0;}

如何避免野指针

指针变量初始化

明确知道指针变量要初始化为什么值,直接初始化

intmain(){inta=10;int*pa=&a;}

不知道指针变量应该指向哪里可以给指针赋值NULL
NULL是C语言中定义的一个标识符常量,值为0,0也是个地址

int*pa=NULL;//此处null是空指针,意思是p没有指向有效的空间暂时不要使用它

注意不要让指针越界

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

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

当指针变量指向一块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把该指针置为NULL.因为有一个约定俗成的一个规则:只要是NULL指针就不去访问,同时使用指针之前可以判断指针是否为NULL

intmain(){inta[10]={1,2,3,4,5,6,7};//如果我们想要把数组的前三个元素改为4inti=0;int*pa=a;for(i=0;i<3;i++){*pa=4;pa++;}pa=NULL;//这里已经不使用指针变量pa了给它置为NULL//如果我们现在又想使用pa呢?我们再给它指向我们想要使用的pa=a;if(pa==NULL){printf("pa是空指针");}else{.....;}return0;}

避免返回局部变量的地址

当我们创建一个局部变量的时候要注意它的生命周期,如果在它的生命周期外此时这个数据已经被销毁了,我们再去指向这个地址就已经不是我们想要的数据了

这里我们要分清返回局部变量和返回局部变量的地址的区别

返回局部变量的地址int*test(){intn=100;return&n;}intmain(){inta=test();pritnf("%d",*a);return0;}
返回局部变量inttest(){into=10;returno;}intmain(){inta=test();printf("%d",a);return0;}

assert断言

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

assert(p!=NULL);

上面代码在程序运行到这一行语句时,验证变量p是否等于NULL,如果不等于程序将继续运行,否则就会终止运行,并且给出报错信息提示
assert()宏接受一个表达式作为参数,如果该表达式为真(返回值非0),assert()不会产生任何作用,程序继续运行,如果该表达式为假(返回值为0),assert()就会报错,在标准错误流stderr中写入一条错误信息,显示没有通过的表达式以及包含这个表达式的文件名和行号

assert()的使用对于我们是非常友好的,使用assert()可以自动表示文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert()的机制,如果程序已经没有问题,不需要断言就在#include <assert.h>语句前定义一个宏NDEBUG

#includeNDEBUG#include<assert.h>

然后重新编译程序编译器会禁用文件中所有的assert()语句,如果程序又出现问题可以移除这条#include NDEBUG指令再次编译,就重新启用了assert()语句
assert()的缺点是因为引入了额外的检查增加了程序的运行时间

指针的使用和传址调用

传值调用和传址调用

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

voidswit(intc,intd){intz=0;z=c;c=d;d=z;}intmain(){inta=10;intb=20;printf("交换前a=%d b=%d\n",a,b);swit(a,b);printf("交换后a=%d b=%d\n",a,b);}


这就是传值调用,对于我们传过去的仅仅是数值
我们通过监视可以看到实参的地址和传到函数中后的地址是独立的因此我们无法通过这个来实现我们的需求,那如何能实现呢?这个时候就需要指针来实现

voidswit(int*pa,int*pb){intz=0;z=*pa;*pa=*pb;*pb=z;}intmain(){inta=10;intb=20;printf("交换前a=%d b=%d\n",a,b);swit(&a,&b);printf("交换后a=%d b=%d\n",a,b);}

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

传址调用,可以让函数和主调函数之间建立真正的联系,在函数内部可以修改主调函数中的变量,所以在未来函数中只需要主调函数中的变量值来实现计算,就可以使用传值调用,如果函数内部要修改主调函数中的变量的值,就需要传址调用.

总结

本文全面介绍了C语言中指针的核心概念和使用方法,主要内容总结如下:

1. 内存与地址基础

  • 内存单元:计算机内存被划分为一个个字节大小的内存单元,每个单元都有唯一编号(地址)
  • 指针本质:内存单元的编号就是地址,在C语言中称为指针

2. 指针变量与操作符

  • 取地址操作符(&):获取变量的内存地址
  • 指针变量:专门用于存储地址的变量,声明格式为类型 *变量名
  • 解引用操作符(*):通过指针访问或修改其指向的内存内容

3. 指针类型的重要性

  • 解引用权限:指针类型决定了通过解引用能操作的内存字节数
  • 指针运算步长:指针类型决定了指针加减整数时的步长大小
  • void*指针:通用指针类型,可接收任意类型地址,但不能直接进行算术运算

4. 指针运算

  • 指针±整数:在连续内存空间(如数组)中移动指针
  • 指针-指针:计算两个指针之间的元素个数
  • 指针关系运算:比较指针的大小关系

5. const修饰指针

  • const在*左边:限制指针指向的内容不可修改
  • const在*右边:限制指针本身不可修改指向
  • const在*两边:指针指向和内容都不可修改

6. 野指针与安全使用

  • 野指针成因:未初始化、越界访问、指向已释放空间
  • 避免方法:初始化指针、及时置NULL、使用前检查有效性、避免返回局部变量地址

7. 实用工具与技巧

  • assert断言:运行时检查程序条件,辅助调试
  • 传址调用:通过指针实现函数内修改主调函数变量,建立真正的数据联系

关键要点

  1. 指针是C语言的精髓,提供了直接操作内存的能力
  2. 理解指针类型对内存操作的影响至关重要
  3. 正确管理指针生命周期,避免野指针问题
  4. const修饰符提供了额外的类型安全保护
  5. 传址调用是实现函数间数据交互的有效方式

掌握指针需要理论与实践相结合,建议通过实际编程练习加深理解,从简单的指针操作开始,逐步掌握复杂的数据结构和算法实现。

← 返回列表