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函数的时候是将变量的地址传递给了函数,这种函数调⽤⽅式叫:传址调⽤


相关文章
|
8天前
|
程序员 C语言
C语言库函数 — 内存函数(含模拟实现内存函数)
C语言库函数 — 内存函数(含模拟实现内存函数)
16 0
|
19天前
|
编译器 C语言 C++
【C语言】memset()函数(内存块初始化函数)
【C语言】memset()函数(内存块初始化函数)
23 0
|
19天前
|
编译器 C语言 C++
【C语言】memcpy()函数(内存块拷贝函数)
【C语言】memcpy()函数(内存块拷贝函数)
38 0
|
10天前
|
存储 编译器 C语言
深入探索C语言动态内存分配:释放你的程序潜力
深入探索C语言动态内存分配:释放你的程序潜力
26 0
|
8天前
|
存储 C语言
C语言 — 指针进阶篇(下)
C语言 — 指针进阶篇(下)
15 0
|
8天前
|
存储 C语言 C++
C语言 — 指针进阶篇(上)
C语言 — 指针进阶篇(上)
6 0
|
15天前
|
存储 C语言
【C语言】深入解开指针(三)2
【C语言】深入解开指针(三)
|
15天前
|
存储 程序员 C语言
【C语言】深入解开指针(二)2
【C语言】深入解开指针(二)
【C语言】深入解开指针(二)2
|
15天前
|
存储 C语言
【C语言】深入解开指针(一)1
【C语言】深入解开指针(一)
|
16天前
|
C语言 索引
基于C语言的函数指针应用-消息命令处理框架
基于C语言的函数指针应用-消息命令处理框架
11 0