内存和地址
引入
想象这样一个生活场景:假设你住在一栋楼里面,楼上有许多房间,每个房间都没有编号,如果你点了一份外卖,指定外卖员将外卖送到你的家里,但此时如果外卖员想要找到你,就要挨个房间去找你,这样效率就十分低下,你可能还会因为这个给外卖员差评,但如果将每个房间按照楼层编上号,外卖员就能按照编号将外卖准时交到你的手上。
计算机中,CPU(中央处理器)在处理数据时会向内存中读取数据,处理后的数据也会放回内存中,电脑上内存有8GB/16GB/32GB不等,这些内存空间是如何进行有效管理的?
跟引入案例类似,也是把内存划分为一个个内存单元,每个内存单元的大小取一个字节。
计算机中的常见单位:
bit——比特位 1Byte=8bit
Byte——字节 1KB=1024Byte
KB 1MB=1024KB
MB 1GB=1024MB
GB 1TB=1024GB
TB 1PB=1024KB
PB
每个内存单元相当于一个学生宿舍,一个字节空间里面可以放8个比特位,就好比同学们住在8人间,每个人是一个比特位。
每个内存单元也有一个编号(类比于宿舍的门牌号),有了这个内存单元的编号,CPU可以迅速找到一个内存空间。
生活中我们把门牌号叫做地址,在计算机中把内存单元的编号也称为地址,C语言中给地址起了新的名字:指针。
可以这样理解:内存单元的编号=地址=指针
指针变量和地址
&——取地址操作符
我们之前说过,创建变量的本质就是像内存申请空间,那么,申请的内存空间一定是有它的编号的,我们应该如何拿到这个编号呢?
这里就要学习一个操作符——&(取地址操作符)。
指针变量和解引用操作符(*)
我们通过&操作符拿到了变量的地址,这个地址有的时候需要存储起来方便后期使用。那我们应该如何存储一个地址数据呢?
答案就是通过指针变量来存储地址数据。
比如:
#include<stdio.h>
int main()
{
int a = 10;
int* pa = &a;//将变量a的地址取出来并存储到pa变量中
return 0;
}
指针变来那个也是一种变量,指针变量就是用来存放地址的,存放在指针变量中的值都会被理解为地址。
指针变量的类型
如何理解指针变量的类型呢?
int a = 10;
int* pa = &a;
pa是变量名,int*就是pa的类型,我们可以这样理解:int*中的int表示pa指向的是整形类型的变量(pa中存储的是整型变量的地址),*表示pa是一个指针变量。
那假如有一个char类型的变量ch,我们应该如何存储ch的地址?
char ch ='w' ;
char* pc=&ch;
解引用操作符
我们通过编号找到房间,在房间里我们可以放东西或拿东西。
同样,在C语言中,我们拿到了地址(指针),就可以通过地址(指针)找到他所指向的变量,并对这个变量进行操作,那么我们如何通过指针对他所指向的对象进行操作?这里就要引入解引用操作符(*)。
#include<stdio.h>
int main()
{
int a = 0;
int* pa = &a;
*pa = 10;
printf("%d\n", a);
return 0;
}
上述代码第三行就是用了解引用操作符,*pa的意思就是通过pa中存放的地址找到它所指向的空间,*pa就找到了变量a(*pa等价于a),所以代码中对*pa进行修改就是对a进行修改,a的值最终是10。
指针变量的大小
- 指针变量的大小 只取决于系统架构(32位→4字节,64位→8字节),与指针类型无关,只要是指针变量,在相同的平台下,它们的大小是相同的
- 32位平台下地址是32个bit位,指针变量的大小是4个字节(32位机器上,地址是通过32根地址线传递的,那么一个地址就是32个0/1的二进制,需要32个比特位(4个字节)存储,指针变量的大小也就是4个字节)
- 64位平台下地址是64个bit位,指针变量的大小是8个字节
#include<stdio.h>
int main()
{
//VS debug X64环境下,结果都为8
//VS debug X86环境下,结果都为4
printf("%zu\n", sizeof(int*));
printf("%zu\n", sizeof(short*));
printf("%zu\n", sizeof(char*));
printf("%zu\n", sizeof(float*));
printf("%zu\n", sizeof(double*));
return 0;
}
指针变量类型的意义
指针变量的解引用
对比一下两段代码,观察调试过程中内存的变化:
//代码一:
#include<stdio.h>
int main()
{
int a = 0x11223344;//十六进制
int* pa = &a;
*pa = 0;
printf("%d\n", a);
return 0;
}
//代码二:
#include<stdio.h>
int main()
{
int a = 0x11223344;//十六进制
char* pa = &a;
//&a的类型应该是int*类型,试想,char*类型能放得下int*类型的地址吗,答案是可以的。
//因为任何类型的指针变量的大小都是一样的
*pa = 0;
printf("%d\n", a);
return 0;
}
调试过程中,我们可以看到,代码1中会将a的四个字节的内容全部改为0,但是代码2只将a的第一个字节改为0,而4字节恰好是整型变量的大小,1字节恰好是字符型变量的大小。
结论:指针类型决定了对指针解引用的权限有多大(即使用指针改变所指向的对象时,一次能访问多少个字节)
比如:char*类型的指针解引用就只能访问一个字节,int*的指针解引用能访问4个字节。
指针+-整数
看一下代码,观察地址的变化:
#include<stdio.h>
int main()
{
int a = 10;
int* p1 = &a;
char* p2 = &a;
printf("&a =%p\n", &a);
printf("p1 =%p\n", p1);
printf("p1+1 =%p\n", p1+1);
printf("p2 =%p\n", p2);
printf("p2+1 =%p\n", p2+1);
return 0;
}
我们可以看到,char*类型的指针+1会跳过一个字节,int*类型的指针+1会跳过4个字节,这就是指针变量类型的差异带来的变化。指针+1,就是跳过1个指针指向的元素(若指针指向的元素类型大小为x字节,指针+1就会跳过x字节)。指针可以+1,也可以-1.
结论: 指针类型决定了指针向前走一步或向后走一步的距离。
void*类型的指针
在指针类型中有一个特殊类型:void*。可以理解为无具体类型的指针(泛型指针),这种类型的指针可以用来接收任意类型的地址,但是也有局限性,void型指针不能直接进行指针+-整数和解引用的运算。
#include<stdio.h>
int main()
{
int a = 10;
char ch = 'w';
void* p = &a;
void* p1 = &ch;
return 0;
}
但是void型指针不能直接进行指针+-整数和解引用,因为:编译器不知道目标类型的大小,而:
-
解引用操作(如
*p
)需要知道指针指向的数据类型,以确定:-
读取多少字节(如
int
读 4 字节,double
读 8 字节)。 -
如何解释这些字节(如整型、浮点型、结构体等)。
-
-
void*
只是单纯的地址,没有关联的类型信息,所以编译器无法安全地解引用。
同时,指针+-整数的运算也需要确定目标类型的大小以确定跳过多少字节。
那么void*类型的指针究竟能干嘛?
一般void*类型的指针是使用在函数参数部分,用来接收不同类型数据的地址,这样的设计可以实现泛型编程的效果1,使得一个函数来处理多种类型的数据。
指针的基本运算
指针的基本运算有三种,分别是:
- 指针+-整数
- 指针-指针
- 指针的关系运算
指针+-整数
既然我们之前学过了数组,知道数组里面的元素是连续存放的,那么我们知道第一个数组元素的地址就可以顺藤摸瓜找到所有元素的地址,并通过地址(指针)访问数组元素。
如何得到数组中第一个元素的地址——&arr[0](实际上,数组名就是数组首元素的地址,这个我们后续会再讲解)
//利用指针访问数组中的元素
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* pa = &arr[0];
//不改变pa指向
for (int i = 0; i < sz; i++)
{
printf("%d ", *(pa + i));
}
printf("\n");
//改变pa指向
pa = &arr[0];
for (int i = 0; i < sz; i++)
{
printf("%d ", *pa);
pa++;
}
printf("\n");
//倒序打印数组元素
//不改变pa的指向
pa = &arr[0];
for (int i = sz - 1; i >= 0; i--)
{
printf("%d ", *(pa + i));
}
printf("\n");
pa = &arr[sz - 1];
//改变pa指向
for (int i = 0; i < sz; i++)
{
printf("%d ", *pa);
pa--;
}
printf("\n");
return 0;
}
指针-指针
指针-指针的绝对值:得到的是两个指针之间的元素个数。
注意前提哦:两个指针指向了同一块空间,否则不能相减 !!!
//以下为错误示范
#include<stdio.h>
int main()
{
int a, b;
int* pa = &a;
int* pb = &b;
printf("%d ", pa - pb);
return 0;
}
应用:求字符串中'\0'之前的字符个数(模拟库函数:strlen())
//写一个函数求解字符串'\0'前的长度
//数组名是数组首元素的地址
#include<stdio.h>
size_t my_strlen_1(char ch[])
{
char* p1 = &ch[0];
char* p2 = p1;
while (*p2)
{
p2++;
}
//跳出循环时p2指向的是‘\0’字符串中第一个字符到字符'\0'间的字符个数就是字符串中‘\0'之前的字符个数
return p2 - p1;
}
size_t my_strlen_2(char ch[])
{
char* p1 = &ch[0];
int count = 0;
while (*p1)
{
count++;
p1++;
}
return count;
}
int main()
{
char ch[] = "hello world";
size_t len = my_strlen_2(ch);
printf("字符串的长度是:%zu\n", len);
return 0;
}
指针的关系运算
//利用指针访问数组中的元素
#include<stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
int* pa = &arr[0];
//通过指针的关系运算来打印指针
while (pa < &arr[0] + sz)
{
printf("%d ", *pa);
pa++;
}
return 0;
}