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

深入理解指针 (1)

1.内存和地址

1.1内存

1.1.1内存的使用和管理 

(1)内存划分为一个个的内存单元,每个内存单元的大小是1个字节,一个内存单元可以存放8个bit。

(2)每个内存单元有一个编号,内存单元的编号在计算机中也叫地址,C语言中称为指针

内存单元的编号 == 地址 == 指针

1.2编址 

CPU访问内存中的某个字节空间,必须要知道内存单元的位置(给内存单元编号),地址信息被下达给内存,在内存上就可以找到该地址的对应数据,将数据通过数据总线传入CPU内寄存器

地址总线:传递信息,通过该信息定位内存单元

2.指针变量和地址

2.1变量创建的本质

int a = 20;
//变量创建的本质是在内存中申请空间
//向内存申请4个字节的空间,用来存放20这个数值
//这4个字节每个字节都有编号(地址)
//变量的名字是给程序员看的,编译器是通过地址找内存单元

2.2取地址操作符(&)

	int a = 20;//&a, &--取地址操作符,拿到变量a的地址printf("%p\n", &a);int* pa = &a;//pa是一个变量,用来存放地址(指针),pa叫指针变量

 理解int* pa = &a;

pa的类型是int*

pa是指针变量的名字

* 表示pa是指针变量

int表示pa指向的变量a的类型是int

 

2.3解引用操作符(*)

	int a = 20;int* pa = &a;*pa = 300;//*--解引用操作符(间接访问操作符)printf("%d\n", a);

2.4指针变量大小

指针变量用来存放地址

        一个32位机器,有32根地址总线,每根地址总线传递一个信号,32根地址总线产生32bit的地址,需要4字节存放。

        一个64位机器,有64根地址总线,每根地址总线传递一个信号,64根地址总线产生64bit的地址,需要8字节存放。

指针变量的大小只与机器的位长有关,与本身类型无关。

理解:一个char类型的地址是地址,同时一个int类型的地址同样也是地址,char*, int* 取地址时取的都是地址,所以与类型无关(通俗理解:苹果是水果,香蕉也是水果,当不论是拿苹果还是香蕉时都是拿的水果)

3.指针变量类型的意义

3.1指针的解引用

指针的类型决定了对指针解引用权限有多大。

如:char* 解引用能访问1个字节,int* 解引用能访问4个字节

3.2指针+-整数

指针类型决定了指针向前或向后一步走多远(距离)

如:一个char* 跳过1个字节,一个int* 跳过4个字节

理解:int* pa;        pa + 1——>  + 1 * sizeof(int)

                              pa + n——>  + n * sizeof(int) 

	int a = 10;int* pa = &a;char* pb = &a;printf("a = %p\n", &a);printf("pa = %p\n", pa);printf("pb = %p\n", pb);printf("a + 1 = %p\n", &a + 1);printf("pa + 1 = %p\n", pa + 1);printf("pb + 1 = %p\n", pb + 1);

3.3void* 指针

 无具体类型的指针(泛型指针),可以用来接收任何类型的地址,但是不能直接进行解引用操作和指针的加减整数操作。即:可接收不同类型地址但不能进行指针运算

void* 一般在函数参数的部分使用,用来接收不同数据类型的地址。使得一个函数能够处理多种类型数据。

4.const修饰指针

4.1const修饰变量

const修饰变量的时候叫:常变量。这个被修改的变量本质上还是变量,只是不能被修改

当使用指针变量可以对其const修饰的变量进行修改(门被关起来了,从窗户跳进去)

const int n = 20;int* p = &n;*p = 200;

4.2const修饰指针变量

4.2.1.const位于*左边

const放在*左边,限制的是指针指向的内容,即,不能通过指针变量来修改它所指向的内容

但是指针变量本身可以被修改

4.2.2.const位于*右边

const放在*右边,限制的是指针变量本身,即,指针不能改变它的指向

但是可以通过指针变量修改它所指向的内容

4.2.3.const位于*左右两侧

此时指针指向的内容和指针变量本身都不能被修改

:关于指针p有3个相关的值

1.p,p里面存放着一个地址

2.*p,p指向的那个对象

3.&p,表示的是p变量的地址

5.指针运算

5.1指针+-整数

1.指针类型决定了指针+1的步长

2.数组在内存中是连续存放

//顺序打印数组	
int a[10] = { 1,2,3,4,5,6,7,8,9,10 };int sz = sizeof(a) / sizeof(a[0]);int* p = &a[0];for (int i = 0; i < sz; i++){printf("%d ", *p);p++;//printf("%d ", *(p + i));}
//逆序打印数组int a[10] = { 1,2,3,4,5,6,7,8,9,10 };int sz = sizeof(a) / sizeof(a[0]);int* p = &a[sz - 1];for (int i = 0; i < sz; i++){//printf("%d ", *p);//p--;printf("%d ", *(p - i));}

5.2指针-指针

指针-指针的绝对值指针之间元素的个数

指针-指针,计算的前提条件两个指针指向的是同一个空间

	int a[10] = { 1,2,3,4,5,6,7,8,9,10 };printf("%d\n", &a[9] - &a[0]);printf("%d\n", &a[0] - &a[9]);

实例:写一个函数,求字符串长度

1.通过strlen函数求字符串长度

#include <string.h>char ch[] = "abcdef";size_t len = strlen(ch);printf("%zd\n", len);

2.写一个函数,模拟strlen函数实现求字符串长度

方法1:(指针+-整数操作)

size_t my_strlen(char* p)
{int count = 0;while (*p != '\0')    //while(*p){count++;p++;}return count;
}int main()
{char ch[] = "abcdef";size_t len = my_strlen(ch);	//数组名其实是数组首元素的地址 ch==&ch[0]printf("%zd\n", len);return 0;
}

方法2:(指针-指针操作)

size_t my_strlen(char* p)
{char* start = p;char* end = p;while (*end != '\0')    //while(*end){end++;}return end - start;
}int main()
{char ch[] = "abcdef";size_t len = my_strlen(ch);	//数组名其实是数组首元素的地址 ch==&ch[0]printf("%zd\n", len);return 0;
}

5.3指针的关系运算 

当一个指针指向一个数组时,指针向后移动,指针指向的地址由小变大

	int a[10] = { 1,2,3,4,5,6,7,8,9,10 };int sz = sizeof(a) / sizeof(a[0]);int* p = a;//while(p < a + sz)while (p < &a[sz]) //指针的大小比较{printf("%d ", *p);p++;}

6.野指针

指针指向的位置不可知(随机、不正确、无明确限制)

6.1野指针的成因

(1)指针未初始化

一个局部变量的值不初始化,值是随机的

如果将p中存放的值当作地址,解引用操作符会形成非法访问

int* p;    //指针未初始化
*p = 10;

(2)指针越界

注:数组下标是从0开始的,不要越界

(3)将指针指向的空间释放

局部变量进函数创建,出函数销毁,当它出函数时,内存空间归还给操作系统

int* test()
{int n = 10;return &n;
}int main()
{int* p = test();printf("%d\n", *p);return 0;
}

6.2如何规避野指针

(1)指针初始化

 如果明确知道指向哪里就直接赋值地址,不知道指向哪里赋值NULL,NULL是C语言中的标识符常量,值为0,0也是地址,这个地址无法使用,读写该地址会报错。

int a = 10;
int* p = &a;    //直接复制地址
int* s = NULL;    //赋值NULL
*s = 100;    //error

(2)指针越界

一个程序申请了多少内存空间,指针也只能访问这些空间,超出范围访问就是指针越界。

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

只要指针是NULL就不进行访问,指针在使用之前先进行判空再进行解引用操作。

int a = 20;
int* p = &a;
if(*p != NULL)    //判空
{*p = 200;
}

(4)避免返回局部变量的地址 

不要返回局部变量的地址

7.assert断言

assert.h头文件定义了宏assert(),用于在运行确保程序符合指定条件,如果不符合就会报错终止运行,这个宏叫做断言。

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

优点:(1)自动识别文件和出错的行号

           (2)无需更改代码就能开启或关闭assert()机制

           (3)如果不需要使用时,在头文件定义宏NDEBUG

#define NDEBUG
#include <assert.h>assert(*p != NULL);

缺点:增加了运行时间 

注:在VS的Release版本中会优化掉assert(),即不会起作用

8.指针的调用

8.1传值调用和传址调用

8.1.1传值调用

传值调用就是在函数使用时,把变量本身的值直接传递给函数。实参传递给形参的时候,形参会单独创建一份临时空间来接收实参,此时对形参的修改不会影响对实参的改变

//传值调用(不能实现变量数值的交换)
void swap1(int x, int y)
{int z = 0;z = x;x = y;y = z;
}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;
}

8.1.2传址调用

能建立函数和主调函数之间的联系,可以在函数内部修改主调函数的变量,从而使主调函数中的变量发生改变。

void swap1(int* pa, int* pb)
{int z = 0;z = *pa;*pa = *pb;*pb = z;
}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;
}

注:如果只是需要主调函数的变量值来实现计算,就可以采用传值调用。如果函数内部要修改主调函数中的变量的值,就需要传址调用

未完待续……

相关文章:

  • Unity 打包后 无阴影 阴影不显示
  • Hi3516CV608 超高清智慧视觉 SoC 芯片 可提供开发资料
  • 论分布式事务及其解决方案 架构师论文范文(考试笔记)
  • 电力系统惯性与惯量关系解析
  • SCI论文结构笔记
  • ctfshow-web-新春欢乐杯
  • 【使用层次序列构建二叉树(数据结构C)】
  • 脚本分享:快速作图对比wannier拟合能带python脚本
  • 深入理解C语言变量:从基础到实践
  • 《深入浅出ProtoBuf:从环境搭建到高效数据序列化》​
  • 三串口进行试验
  • 如何将 sNp 文件导入并绘制到 AEDT (HFSS)
  • Vue多地址代理端口调用
  • version `GLIBCXX_3.4.32‘ not found 解决方法
  • Coding Practice,48天强训(23)
  • BIOS主板(非UEFI)安装fedora42的方法
  • How to haggle over salary with HR
  • 从LLM到AI Agent的技术演进路径:架构解析与实现逻辑
  • 软考【网络工程师】2023年5月上午题答案解析
  • 截至2025年4月,AI硬件已深度融入各自场景!!
  • 毕节两兄弟摘马蜂窝致路人被蜇去世,涉嫌过失致人死亡罪被公诉
  • 中国体育报:中国乒协新周期新起点再出发
  • 更好发挥汽车产业在扩投资促消费方面的带动作用!陈吉宁调研上海车展
  • 杨国荣丨《儒耶对话与中国现代思想的生成和发展》序
  • 成都两宗宅地成功出让,民企四川联投溢价33%竞得郫都区宅地
  • 机器人马拉松背后的五大启示:未来社会与机器人的深度融合