0. 内存
指针会涉及到内存地址,因此在指针之前先了解下内存的相关知识。
内存和外存
外存就是我们常用的硬盘(C盘、D盘等)、U 盘、光盘等。
外存存储的数据量大,但是处理数据的速度慢,而 CPU 的运行速度是非常快的,为了提高效率,就出现了内存,内存存储的数据量小(目前常见的是 8 G),但是处理数据的速度快,CPU 处理的数据都在内存中。
内存和外存的区别:
- 内存存储的数据量远小于外存。目前个人电脑内存比较常见的是 8G 和 16G,而硬盘常见的应该是 300G 左右吧,U 盘基本上也是 32G 左右的比较常用。
- 内存速度比外存的速度快。这也是使用内存的原因,外存处理数据速度太慢,而内存的速度则比外存快的太多了,因此使用内存来和 CPU 进行数据的交互。
- 内存的成本大于外存。要是内存成本 <= 外存成本,那么直接就是全用内存作为存储设备了,还分啥内外存呢。
- 内存存储数据的时间小于外存。内存存储的数据在断电之后会消失,而外存会一直存储。
为了进一步提高效率,在内存和 CPU 之间还会有缓存。缓存的存储量更小,速度更快。
程序的存储
我们编写的程序(代码)在不执行的时候,以文件的形式存储在外存中。而当我们执行程序时,程序就会被调入到内存中(程序在执行之后称为进程)。
在编写 C 语言程序时,我们写的变量、函数等等,都是会在程序执行之后放入到内存中,以供 CPU 使用。而 CPU 只能通过地址来获取内存中的代码和数据,程序在执行过程中会告知 CPU 要执行的代码以及要读写的数据的地址。
CPU 访问内存时需要的是地址,而不是变量名和函数名!
变量名和函数名只是地址的一种助记符,是为我们提供方便的,让我们在编写代码的过程中可以使用易于阅读和理解的英文字符串,不用直接面对二进制地址。
当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。
1. 什么是指针?
从根本上看,指针是一个值为内存地址的变量(或数据对象)。正如 char 类型变量的值是字符,int 类型变量的值是整数,指针变量的值是地址。
事实上,我们的程序中的变量名、函数名、指针在本质上是一样的,它们都是地址助记符。但在编写代码的过程中,我们认为变量名表示的是数据本身,而函数名、指针表示的是代码块或数据块的首地址。
2. 怎么声明一个指针变量?
定义指针变量与定义普通变量非常类似,不过要在变量名前面加星号*,格式为:
// 声明指针变量
变量类型* 变量名;
// 声明并初始化指针变量
变量类型* 变量名 = 地址值;
示例:
// 声明指针变量
double* p; // p 是指向 double 类型的指针
// 声明并初始化指针变量
int a = 10;
int* q = &a; // q 是指向 int 类型的指针
运算符* 在这里的含义是指针,运算符& 在这里的含义是取地址符。
声明指针变量时必须指定指针所指向变量的类型。首先,指针只保存数据的首地址,而不同的变量类型占用不同的存储空间,一些指针操作要求知道操作对象的大小。其次,程序必须知道储存在指定地址上的数据类型,long 和 float 可能占用相同的存储空间,但是它们储存数字却大相径庭。
类型说明符表明了指针所指向对象的类型,星号(*)表明声明的变量是一个指针。
3. 指针初始化和使用
普通变量可以初始化,type name=value;
。同样的,指针变量也可以初始化,初始化的值是一个地址值。
3.1. 地址运算符
在指针中常用的两个地址运算符 —— & 和 。
& 运算符,也称取址运算符,后面跟变量名,给出该变量的地址。
** 运算符**,也称解引用运算符,后面跟指针名(或地址),给出存储在指针指向的地址上的值。
3.2. 初始化
可以借助 & 运算符将某个变量的地址赋值给指针。
int a = 5;
int* ptr = &a; // ptr指针指向a
也可以将指针变量赋值给另一个指针变量。
int a = 5;
int* ptr1 = &a; // ptr1指针指向a
int* ptr2 = ptr1;
注意,将普通变量赋值给指针时,需要在普通变量前面加 & 运算符,因为指针的值是地址,所以用 & 获取普通变量的地址,并赋值给指针变量。而指针变量给指针变量赋值时,直接赋值即可,不需要用 & 运算符。
将普通变量的地址赋值给指针变量的过程也称作指针指向变量。
对于二维指针来说,一维指针相当于普通变量:
int a = 5;
int* ptr1 = &a;
int** ptr2 = &ptr1;
3.3. 指针的使用
指针变量的值是地址,但我们往往需要使用的是指针指向的数据,这是就需要用到 * 运算符。
int a = 5;
int* ptr = &a;
// *ptr 是 5
3.4. 数组的指针表示法
在 C 语言中 ar[i] 和 *(ar+i) 是等价的,无论 ar 是数组还是指针变量。
但只有当 ar 是指针变量时,才能对 arr 值进行修改,如 ar++。
指针表示法(尤其与递增运算符一起使用时)更接近机器语言,因此一些编译器在编译时能生成效率更高的代码。
还记得我们在学习数组时提到的指针表示法吗?指针表示法就是将数组当成指针变量来访问数组元素。
int arr[6] = {1, 4, 3, 6, 1, 8};
// arr[0] 等价于 *(arr + 0) 或 *(arr)
// arr[1] 等价于 *(arr + 1)
// arr[2] 等价于 *(arr + 2)
对于多维数组也可以使用指针表示法:
int arr[3][4] = {{1,3,4},{1,2,3,3},{4,5}};
// arr[0][3] 等价于 *(*(arr)+3)
// arr[1][2] 等价于 *(*(arr+1)+2)
4. 指针运算
指针可以的运算有六种,分别是赋值、解引用、取址、加法、减法、比较。
PS:指针变量没有乘除法运算,且两个指针相加是没有意义的。
4.1. 赋值
可以将地址赋值给指针。 例如,用数组名、带地址运算符(&)的 变量名、另一个指针进行赋值。
4.2. 解引用
4.3. 取址
和所有变量一样,指针变量也有自己的地址和值。 对指针而言, &运算符给出指针本身的地址。
4.4. 加法
- 指针和整数相加;
-
4.5. 减法
指针和整数相减;
- 指针递减;
- 指针和指针相减。通过计算求出两元素之间的距离,通常用于求同一个数组中两个元素的距离。
4.6. 比较
使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。千万不要解引用未初始化的指针!!!可能会导致极其严重的错误 —— 可能会擦写数据或代码,或者导致程序崩溃
5. 为什么要学习指针?
- 指针提供一种以符号形式使用地址的方法。因为计算机的硬件指令非常依赖地址,指针在某种程度上把程序员想要传达的指令以更接近机器的方式表达。因此,使用指针的程序更有效率。
- 指针的大小是固定的,在 32 位系统中占 4 字节,在 64 位系统中占 8 字节。在面对普通的数据类型时,我们可能不觉得有什么,但是在学习过结构体之后,函数间传递结构体一般都是用函数体指针,而不是传递结构体的值,因为这样可以节省很大的内存开支。