【C语言】—— 指针一 : 初识指针(上)
- 一、内存和地址
- 1.1、如何理解内存和地址
- 1.2、 如何理解编址
- 二、指针变量和地址
- 2.1、取地址操作符 &
- 2.2、 指针变量
- 2.3、 解引用操作符 ∗ * ∗
- 2.4、指针变量的大小
- 三、指针变量类型的意义
- 3.1、 指针的解引用
- 3.2、 指针 +- 整数
- 3.3、 v o i d void void * 指针
- 四、指针运算
- 5.1、指针 +- 整数
- 5.2、指针 -- 指针
- 5.3、指针关系的运算
一、内存和地址
1.1、如何理解内存和地址
在讲内存和地址之前,我想给大家讲一个生活中的案例。
假设我们要找好朋友,他只告诉你在哪栋楼,但那栋楼没有门牌号。这时,你想找到他,只能挨家挨户地去寻找,这样效率无疑会很低,而且其他住户也会有意见。
因此为了方便查找与管理,楼栋都会给每个房间上一个门牌号,有了门牌号,就能快速定位朋友所在的房间。
在生活中,我们给每个房间一个房间号,提高效率,那如果对照计算器中又会是怎样的呢?
CPU(中央处理器)在处理数据的时候,需要的数据是从内存中读取的,处理后的数据也会放在内存中。我们知道,买电脑时,电脑内存有 8GB / 16GB / 32GB,那么电脑是如何管理这些内存的呢?
其实计算机也是把内存分为一个个内存单元,每个内存单元大小取一个字节。每个内存单元有一个编号 (这个编号相当于房间的门牌号),有了这个内存单元的编号,CPU就可以快速访问一个内存空间。
生活中,我们把门牌号叫做地址,在计算机中,我们把内存单元的编号也称为地址
。
C语言中给地址起了个新名字:指针
我们可以这样理解:
内存单元的编号 == 地址 == 指针
1.2、 如何理解编址
CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,⽽因为内存中字节很多,所以需要给内存进⾏编址。
计算机中的编址,并不是把计算机每个字节的地址记录下来,而是通过硬件设计来完成。
首先,我们要知道计算机内有许多硬件单元,硬件单元的相互协作,需要不断进行数据传递。
那么问题来了,硬件与硬件之间是相互独立的,那么他们是如何交流信息的呢?答案其实并不神秘:那就是用线相连。
CPU与内存之间也有大量的数据交互,他两也必须用线连起来。
今天,我们关心的一组线,叫地址总线。
我们可以简单理解,32位机器有32根地址总线,每根线只有两种状态:0 或 1 【电脉冲的有无】。所以,一根线只有两种状态,只能表示两种含义,而 32 根线就能表示 2 32 2^{32} 232 中含义,每一种含义都代表着一个地址。
地址信息被下达给内存,在内存上,就可以找到该地址所对应的数据,而数据通过数据总线传入CPU内寄存器。
二、指针变量和地址
2.1、取地址操作符 &
在C语言中,创建一个变量本质上就是向内存申请空间
例如:
上述代码就是为 a a a 创建了一个 4 个字节大小的内存空间,里面存放的是10(16进制表示)
四个地址分别是:
- 0x00AFF830
- 0x00AFF831
- 0x00AFF832
- 0x00AFF833
而我们想要得到 a a a 的地址,就需要用到取地址操作符(&)
学习前面的知识,我们知道,每个字节都有一个地址,而整型变量 a a a 一共申请了 4 个字节,那&操作符
取出的地址是哪一个呢?
#include<stdio.h>int main()
{int a = 10;//为a申请4个字节的空间&a;//取出a的地址printf("%p\n", &a);return 0;
}
由于编译器每一次运行给变量分配的地址都不同,这里我就不打印了
& a a a 取出的是 a a a 所占 4 个字节中最小的地址
按上面的例子,即:0x00AFF830
2.2、 指针变量
我们用取地址操作符取出变量的地址,把他放在哪呢?不存储起来以后想要使用怎么办呢?地址其实也只是一个数值而已,如地址:0x0012FF40,总不能让他无家可归吧。
就像上述例子中的 10,创建了整型变量 a a a 来存放,C语言中也提供了专门的变量类型来存放指针,那就是:指针变量
#include<stdio.h>int main()
{int a = 10;//为a申请4个字节的空间int* pa = &a;//取出a的地址放在指针变量p中printf("%p\n", pa);return 0;
}
上述代码中, p a pa pa 就是一个指针变量,他的类型是 int*
我们该如何理解
int*
呢?
- ∗ * ∗ 表示他是指针变量
- i n t int int 表示他所存放的地址所指向的类型为 i n t int int 类型
而同理,指针变量还有其他类型:
- l o n g ∗ long* long∗ 指向 l o n g long long 类型
- c h a r ∗ char* char∗ 指向 c h a r char char 类型
- s h o r t ∗ short* short∗ 指向 s h o r t short short 类型
- … … … … … …
2.3、 解引用操作符 ∗ * ∗
我们用取地址操作符(&)
把变量的地址取出来有什么用呢?那自然是为了方便未来要找到他。
我们只要拿到了地址(指针),就能通过地址(指针)找到他所指向的对象,而这个过程就需要用到解引用操作符(*)
。
#include<stdio.h>int mian()
{int a = 0;int* pa = &a;*pa = 100;return 0;
}
上述代码中
*pa = 100;
就运用了解引用操作符,* pa
的意思是:通过pa存放的地址,找到其指向的空间,那么*pa
其实就是变量a
了;使用*pa = 100;
就是把 a a a 变成了 100.
2.4、指针变量的大小
前面的学习中,我们了解到,32位的机器设有32根地址总线,每根线只有两种状态:即有电流通过和无电流通过,逻辑上表示 0 和 1 ,这与二进制序列的表示相符。我们把32位的地址总线产生的二进制序列看成一个地址,那么这个地址一共有32个 b i t bit bit 位,即 4 个字节才能存储。
同理,64位的机器,存储地址需要 8 个字节的空间。
- 32位平台下地址是32个 b i t bit bit 位,指针变量大小是 4 个字节
- 64位平台下地址是64个 b i t bit bit 位,指针变量大小是 8 个字节
注意:指针变量的大小与类型无关,只要他是指针变量,他的大小就是 4 字节或 8 字节
。
三、指针变量类型的意义
3.1、 指针的解引用
我们得知,指针变量的大小和类型无关,那么为什么还要有各种各样的指针类型呢?
C语言当然不可能无缘无故创造这么多类型,他们都是有意义的,让我们一起往下看。
下面,我们来看看两段代码:
#include<stdio.h>int main()
{int n = 0x11223344;int* pi = &n;*pi = 0;return 0;
}
#include<stdio.h>int main()
{int n = 0x11223344;char* pc = (char*)&n;*pc = 0;return 0;
}
从调试我们可以看到,代码 1 会将 n n n 的 4 个字节全部改为 0 ,而代码 2 只是将 n n n 的第一个字节改为 0。
指针的类型决定了对指针解引用时,指针有多大的访问权限(一次能操作几个字节)
例如: c h a r char char* 只能访问 1 个字节, i n t int int* 能访问 4 个字节
3.2、 指针 ± 整数
让我们先来看一段代码,观察他们的地址变化
#include<stdio.h>int main()
{int n = 10;char* pc = (char*)&n;int* pi = &n;printf("&n =%p\n", &n);printf("pc =%p\n", pc);printf("pc+1 =%p\n", pc + 1);printf("pi =%p\n", pi);printf("pi+1 =%p\n", pi + 1);return 0;
}
运行结果:
我们可以看到, c h a r char char* 类型 +1 跳过了 1 个字节, i n t int int* 类型 +1 跳过了 4 个字节。
这是因为 c h a r char char * 指针类型所指向的 c h a r char char 类型大小是 1 个字节 ,同理, i n t int int * 所指向的 i n t int int 类型大小是 4 个字节
指针的类型决定了指针向前或向后走一步有多大
3.3、 v o i d void void * 指针
在所有指针类型中,还有一种特殊的指针: v o i d void void * 指针。我们知道 v o i d void void 用在函数的返回类型和参数中时,表示无返回值、无参数,那么 v o i d void void * 指针又表示什么呢?
void * 可以理解为无具体类型的指针(或者叫泛型指针),它可以接收任意类型的地址
但是它也有局限性:void *类型指针不能完成上面的解引用和指针 +- 整数的运算
,因为 void *类型指针不知道访问权限(一次应操作几个字节),想完成上述运算,只能先强制类型转换
为其他指针类型。
举例:
#include<stdio,h>int main()
{int a = 10;int* pi = &a;char* pc = &a;return 0;
}
我们可以看到,将一个 i n t int int 类型的变量地址赋值给一个 c h a r char char * 类型的指针变量。编译器给出了警告,因为类型不兼容,要想消除这个警告,除了强制类型转换成 c h a r char char * 类型,还可以用 v o i d void void *类型接收。
如下:
#include<stdio.h>int main()
{int a = 10;void* pi = &a;void* pc = &a;*pi = 10;*pc = 0;return 0;
}
VS编译器运行结果:
我们可以看到,虽然用 v o i d void void * 类型接收编译器并没有报警告,但是却无法解引用,编译器直接报错,那么 v o i d void void * 类型有什么用呢?
一般 v o i d void void * 类型用在函数参数部分
,用来接收未知类型数据的地址
的,这样设计可以达到泛型编程的效果。使得一个函数可以处理多种类型的数据。
四、指针运算
指针的基本运算有三种,分别是:
- 指针 ± 整数
- 指针 - 指针
- 指针的关系运算
5.1、指针 ± 整数
在 C语言——详解数组 一文中,我曾经提到过,数组在内存中是连续存放的。因此,对于数组来说,我们只要找到了他的首元素地址,那他剩下元素的地址也能顺藤摸瓜找到。
#include<stdio.h>
//指针 +- 整数
int main()
{int arr[9] = { 1,2,3,4,5,6,7,8,9 };int* p = &arr[0];// 取出数组首元素的地址int i = 0;int sz = sizeof(arr) / sizeof(arr[0]);for (i = 0; i < sz; i++){printf("%d ", *(p + i));// p + i 这里就是指针 + 整数}return 0;
}
- 注:用指针访问数组的前提条件:
数组在内存中是连续存放的
指针 ± 整数公式:
- p + n p + n p+n − > -> −> 跳过 n + s i z e o f ( t y p e ) n + sizeof(type) n+sizeof(type) 个字节
例如:
int a = 10; int* p = &a;
则 p + 1 p + 1 p+1 跳过4 个字节 ( 1 ∗ s i z e o f ( t y p e )) (1 * sizeof(type)) (1∗sizeof(type))
5.2、指针 – 指针
指针 – 指针是什么意思呢?我们可以类比生活中的例子:
上面的例子:指针 ± 正数可以类比日期 ± 天数
同理,指针 – 指针也可类比成日期 – 日期
看下面这段代码:
#include<stdio.h>int my_strlen(char* s)
{char* p = s;while (*p != '\0')p++;return p - s;
}int main()
{printf("%d\n", my_strlen("abc"));return 0;
}
这里,我们模拟实现了一个简陋的 s t r l e n strlen strlen 函数
函数来求字符串长度,代码逻辑是:传入字符串首元素的地址(指针),然后找到字符串最后一个元素的地址(指针),两个指针相减,得到的是中间的元素格式,即整个字符串的长度啦。
但需要注意的是:
- 指针能减指针,但
指针不能+指针
,你听说法日期加日期的吗?指针也是如此。 - 可以低地址 – 高地址,只不过得到的是负数
- 指针 – 指针的前提条件一定是:
两个指针指向的是同一块空间
例如:
#include<stdio.h>int main()
{int arr[10] = { 0 };char ch[5] = { 0 };printf("%d\n", &ch[4] - &arr[6]);return 0;
}
上述代码是错误的代码,因为两个指针并没有指向同一块空间。
5.3、指针关系的运算
指针关系运算即地址(指针)与地址(指针)比较大小
请看代码:
#include<stdio.h>int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p = &arr[0];int i = 0;int sz = sizeof(arr) / sizeof(arr[0]);while (p < arr + sz)// 指针大小比较{printf("%d ", *p);p++;}
}
该代码中,循环条件判断中就运用了指针关系的运算,以确保对数组的访问不越界。
好啦,本期关于指针就介绍到这里啦,希望本期博客能对你有所帮助,同时,如果有错误的地方请多多指正,让我们在C语言的学习路上一起进步!