C语言:指针与内存

简介: C语言:指针与内存



内存与地址

计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,那这些内存空间如何⾼效的管理呢?

其实计算机会把内存划分为⼀个个的内存单元,以字节为一个基础的内存单元来进行管理。

每个内存单元也都有⼀个编号,有了这个内存单元的编号,CPU就可以快速找到⼀个内存空间。⽣活中我们把⻔牌号也叫地址,在计算机中我们把内存单元的编号也称为地址。C语⾔中给地址起了新的名字叫:指针

计算机内是有很多的硬件单元,而硬件单元是要互相协同⼯作的。所谓的协同,⾄少相互之间要能够进⾏数据传递。但是硬件与硬件之间是互相独⽴的,那么如何通信呢?

CPU与内存是计算机中分别独立的硬件,如果想要让不同的硬件实现数据交互,那么就需要用电线连起来。而当CPU要访问内存的数据,就需要通过地址来确定访问的内存单元,这个传输地址的数据线就叫做地址总线。

每根电线有两种形态:有电/没电,此时32根地址线就可以表示 2 ^ 32种地址,64根地址线就可以表示2 ^ 64种地址。


指针变量

取地址

理解了内存和地址的关系,我们再回到C语⾔,在C语⾔中创建变量其实就是向内存申请空间,⽐如:

int a = 10;

上述的代码就是创建了整型变量a,内存中申请4个字节,⽤于存放整数10,其中每个字节都有地址。

那我们如何能得到a的地址呢?

这⾥就得学习⼀个操作符:& 取地址操作符

int a = 10;
&a;//取出a的地址

&a取出的是a所占4个字节中编址最小的字节的地址。


指针变量

那我们通过取地址操作符拿到的地址是⼀个数值,⽐如:0x006FFD70,这个数值有时候也是需要存储起来,⽅便后期再使⽤的,那我们把这样的地址值存放在哪⾥呢?

C语言提供了指针变量,用于存储地址。

int a = 10;
int * pa = &a;

这里的pa就是一个指针变量,其内部存储了a的地址。

这⾥pa左边写的是 int** 是在说明pa是指针变量,⽽前⾯的 int 是在说明pa指向的是整型int类型的对象。

当一个指针指向类型 xxx,这个指针的类型就是 xxx*

比如一个指向char类型变量的指针:

char ch = 'w';
char* pc = &ch;

由于指针pc指向的类型是char,所以pc的类型就是char*


解引用

我们将地址保存起来,未来是要使⽤的,那怎么使⽤呢?

在现实⽣活中,我们使⽤地址要找到⼀个房间,在房间⾥可以拿去或者存放物品。

C语⾔中其实也是⼀样的,我们只要拿到了地址(指针),就可以通过地址(指针)找到地址(指针)指向的对象。

获取指针指向对象的值的操作符叫解引⽤操作符*

int a = 100;
int* pa = &a;
*pa = 0;

上⾯代码中第3⾏就使⽤了解引⽤操作符, *pa 的意思就是通过pa中存放的地址,找到指向的空间,*pa其实就是a变量了;所以*pa = 0这个操作相当于a = 0


指针的大小

地址是通过地址线传输的,对于32位机器,其有32根地址总线,那么就需要32个比特位来标识每根地址线的状态,需要4字节来存储指针;对于64位的机器,那么就需要64个比特位来标识每根地址线的状态,需要8字节来存储指针

32位计算机指针大小为4字节

64位计算机指针大小为8字节

注意指针内部只存储地址,与指针的类型无关,只要是指针,大小就是4/8字节


指针运算

指针也是可以进行运算的,指针有以下运算:

指针 + - 整数

指针对整数的 + - 用于到达下一个指针的位置。

示例:

int n = 10;
int* pi = &n;
printf("%p\n", pi);
printf("%p\n", pi + 1);

输出结果:

001EF868
001EF86C

通过16进制运算,可以发现两个地址之间相差4,而int类型刚好占用4字节。

再试试char*指针:

char c = 'w';
char* pc = &c;
printf("%p\n", pc);
printf("%p\n", pc + 1);

输出结果:

00FFFE77
00FFFE78

通过16进制运算,可以发现两个地址之间相差1,而char类型刚好占用1字节。

指针指向的类型占用多少个字节,那么指针+ -整数时就以多少个字节为单位

指针 - 指针

指针 - 指针得到两个指针之间的距离。

示例:

int arr[10] = { 0 };
int* p1 = &arr[0];
int* p2 = &arr[5];
int x = p2 - p1;
printf("%d", x);

以上示例中,p1指向了数组的第1个元素,p2指向了数组的6个元素,此时两个指针相减得到多少?是指针之间的元素个数5,还是指针之间的字节数4 * 5 = 20

输出结果:

5

可以看到,指针之间的减法也与类型是有关的,指针相减得到的是两个指针有几个指向的元素,而不是单纯的地址相减

再看一个案例:

int arr[10] = { 0 };
int* p1 = &arr[0];
int* p2 = &arr[5];
int x = (char*)p2 - (char*)p1;
printf("%d", x);

与刚才代码的唯一区别就是,我们在(char*)p2 - (char*)p1做指针减法的时候,将两个指针转化为了char*类型,此时输出结果是多少?

输出结果:

20

因为char*的指针指向的类型占用1字节,所以5个int类型的空间可以存储20个char,而我们将指针从int*转化为了char*,计算规则就从原来计算可以存放几个int,变成了可以存放几个char了。

指针关系运算

指针关系运算

< 和 > 比两个指针的地址大小

== 和 != 判断两个指针是否相等


const修饰指针

当一个变量被const修饰,那么这个变量就不能被修改。指针也可以被const修饰,但是它的修饰规则不太一样。

怎么样才算修改指针呢?

当我们获得一个指针变量,我们可以修改指针指向内容的值,比如这样:

int a = 10;
int* pa = &a;
*pa = 5;

我们通过指针把变量a的值修改了。

我们也可以修改指针的指向:

int a = 10;
int* pa = &a;
pa = &b;

此时pa这个指针从指向变量a,变成了指向变量b

那么const修饰时,到底时禁止哪一项不能修改?

这就需要讲解指针的特殊的const修饰规则了:

const放在 * 左边,指针指向的内容不能被指针修改

示例1:

const int* pa = &a;
pa = &b;//允许修改
*pa = 5;//不允许修改

示例2:

int const* pa = &a;
pa = &b;//允许修改
*pa = 5;//不允许修改

对指针来说,const可以放在int左边,也可以放在int右边,效果是一样的。

当const放在 * 右边,指针的指向不能改

示例:

int a = 10;
int b = 5;
int* const pa = &a;
pa = &b;//不允许修改
*pa = 5;//允许修改

字符指针

看到一串代码:

const char* p = "hello";

char*类型的指针用于存放char类型的数据,但是以上代码却可以把一个字符串存进char*类型的指针中,这是为什么?

所有常量字符串做表达式时,本质都是首个字符的地址

因此我们也可以把字符串当作指针来输出:

printf("%p", "hello");

输出结果:

00C57BD8

可以看出, "hello"这个字符串整体,代表了一个指针。

所以const char* p = "hello";的本质其实是把字符串“hello”的第一个元素‘h’的地址交给了p指针。

为什么要加const修饰指针?

用双引号引起来的字符串叫做常量字符串,存储在静态区不可以修改,所以在用指针接收时,需要const修饰常量字符串做表达式时,本质都是首个字符的地址。


野指针

野指针的概念

内存中的一块空间在使用前,是需要申请的。而指针作为直接访问内存的一种手段,当指针指向到、没有申请的空间,那么这就是一个野指针。

野指针的成因

指针没有初始化

int *p;//局部变量指针未初始化,默认为随机值
*p = 20;
return 0;

此处的p指针,没有设置初始值,就直接解引用了。此时指针的值就是一个随机值,而随机的地址,很有可能就访问到了没有向内存申请的空间,故p是一个野指针。

指针越界访问

int arr[10] = {0};
int *p = &arr[0];
for(int i = 0; i <= 11; i++)
{
  *(p++) = i;
}

以上代码中,我们用指针对数组进行了遍历,但是数组的下标是从0-9的,而我们访问到了10与11的位置,此时超过10的空间没被数组申请,访问到了没有申请的空间,p就是野指针了。

指针指向的空间被释放

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

此时的指针看似指向了&n,即n的地址。但是函数test调用时,会创建独立的栈帧,当函数调用结束,函数内部的变量n也会一起销毁,所以此时的n已经被释放了。p指向了被释放的空间,变成一个野指针。


如果我们的指针一开始没有想好赋什么值,那我们就得到了一个野指针,我们有没有办法让一个没有想好值的指针不是野指针呢?此时就需要空指针了。

NULL是C语言中的空指针,它本质上是数值为0的地址。

当我们没想好一个指针给什么值的时候,就可以给一个空指针:

int* ptr = NULL:

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就⾏,这样在debug版本写有利于程序员排查问题,在release版本不影响⽤⼾使⽤时程序的效率。


传址调用

写⼀个函数,交换两个整型变量的值:

思考后,你可能会给出这样的答案。

void Swap(int x, int y)
{
  int tmp = x;
  x = y;
  y = tmp;
}

在main函数中调用试试:

int main()
{
  int a = 0;
  int b = 0;
  scanf("%d %d", &a, &b);
  printf("交换前:a=%d b=%d\n", a, b);
  Swap(a, b);
  printf("交换后:a=%d b=%d\n", a, b);
  return 0;
}

输出结果:

10 20
交换前:a=10 b=20
交换后:a=10 b=20

我们发现其实没产⽣交换的效果,这是为什么呢?

main函数内部,创建了ab,在调⽤Swap函数时,将ab传递给了Swap函数,在Swap函数内部创建了形参xy接收ab的值。xy确实接收到了ab的值,不过x的地址和a的地址不⼀样,y的地址和b的地址不⼀样,相当于xy是独⽴的空间,它们只得到了实参的值

那么在Swap函数内部交换xy的值,⾃然不会影响ab。当Swap函数调⽤结束后回到main函数,ab其实没交换。

Swap函数在调用的时候,是把变量本⾝的值传递给了函数,这种叫传值调⽤。

结论:实参传递给形参的时候,形参会单独创建⼀份临时空间来接收实参,对形参的修改不影响实参。

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

修改后代码:

void Swap2(int*px, int*py)
{
  int tmp = *px;
  *px = *py;
  *py = tmp;
}

输出结果:

10 20
交换前:a=10 b=20
交换后:a=20 b=10

我们可以看到实现成传递地址的的⽅式,顺利完成了任务,这⾥调⽤Swap函数的时候是将变量的地址传递给了函数,这种函数调⽤⽅式叫:传址调⽤


相关文章
|
1月前
|
存储 人工智能 Java
一文轻松拿捏C语言的指针的基础使用
本文介绍了C语言中的指针概念,包括直接访问和间接访问内存的方式、指针变量的定义与使用、取址运算符`&`和取值运算符`*`的应用,帮助读者深入理解指针这一C语言的核心概念。君志所向,一往无前!
27 0
|
3月前
|
存储 编译器 程序员
【C语言】内存布局大揭秘 ! -《堆、栈和你从未听说过的内存角落》
在C语言中,内存布局是程序运行时非常重要的概念。内存布局直接影响程序的性能、稳定性和安全性。理解C程序的内存布局,有助于编写更高效和可靠的代码。本文将详细介绍C程序的内存布局,包括代码段、数据段、堆、栈等部分,并提供相关的示例和应用。
91 5
【C语言】内存布局大揭秘 ! -《堆、栈和你从未听说过的内存角落》
|
3月前
|
存储 NoSQL 编译器
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
指针是一个变量,它存储另一个变量的内存地址。换句话说,指针“指向”存储在内存中的某个数据。
141 3
【C语言】指针的神秘探险:从入门到精通的奇幻之旅 !
|
3月前
|
存储 编译器 C语言
【C语言】指针大小知多少 ?一场探寻C语言深处的冒险 !
在C语言中,指针的大小(即指针变量占用的内存大小)是由计算机的体系结构(例如32位还是64位)和编译器决定的。
463 9
|
3月前
|
安全 程序员 C语言
【C语言】指针的爱恨纠葛:常量指针vs指向常量的指针
在C语言中,“常量指针”和“指向常量的指针”是两个重要的指针概念。它们在控制指针的行为和数据的可修改性方面发挥着关键作用。理解这两个概念有助于编写更安全、有效的代码。本文将深入探讨这两个概念,包括定义、语法、实际应用、复杂示例、最佳实践以及常见问题。
82 7
|
3月前
|
存储 缓存 算法
【C语言】内存管理函数详细讲解
在C语言编程中,内存管理是至关重要的。动态内存分配函数允许程序在运行时请求和释放内存,这对于处理不确定大小的数据结构至关重要。以下是C语言内存管理函数的详细讲解,包括每个函数的功能、标准格式、示例代码、代码解释及其输出。
138 6
|
4月前
|
传感器 人工智能 物联网
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发
C 语言在计算机科学中尤其在硬件交互方面占据重要地位。本文探讨了 C 语言与硬件交互的主要方法,包括直接访问硬件寄存器、中断处理、I/O 端口操作、内存映射 I/O 和设备驱动程序开发,以及面临的挑战和未来趋势,旨在帮助读者深入了解并掌握这些关键技术。
109 6
|
4月前
|
存储 算法 程序员
C 语言指针详解 —— 内存操控的魔法棒
《C 语言指针详解》深入浅出地讲解了指针的概念、使用方法及其在内存操作中的重要作用,被誉为程序员手中的“内存操控魔法棒”。本书适合C语言初学者及希望深化理解指针机制的开发者阅读。
|
4月前
|
存储 程序员 编译器
C 语言数组与指针的深度剖析与应用
在C语言中,数组与指针是核心概念,二者既独立又紧密相连。数组是在连续内存中存储相同类型数据的结构,而指针则存储内存地址,二者结合可在数据处理、函数传参等方面发挥巨大作用。掌握它们的特性和关系,对于优化程序性能、灵活处理数据结构至关重要。
|
4月前
|
算法 C语言
C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项
本文深入讲解了C语言中的文件操作技巧,涵盖文件的打开与关闭、读取与写入、文件指针移动及注意事项,通过实例演示了文件操作的基本流程,帮助读者掌握这一重要技能,提升程序开发能力。
315 3