指针
要想理解指针,就必须先理解不同的数据类型在内存中的分布,内存具有容量大小,比如 2G、4G、8G 等等,其中每一个段或区都代表一个字节,那么作为一个典型计算机内存系统,其中每个字节都有一个地址,地址是按序增长的。当定义一个变量时,计算机就会在内存中为这个变量分配一些内存空间,具体的空间大小取决于变量的数据类型,还取决于编译器。比如int
类型的变量系统会为它分配 4 个字节的空间大小,那么地址就可能是201
到204
,201
就是该变量的起始地址,当对该变量进行操作时就会去查找变量在内存中的起始地址,然后在这个地址中做相关操作
只要拥有了某个变量的地址,就可以直接进行操作。指针就是这样的东西,它是一种特殊的变量,和普通变量不同的是,指针变量存储的是内存地址,而普通变量存储的只是一个实际的值。站在内存的角度来看,通过地址就能访问数据,那么指针也就能直接通过地址操作数据
声明指针变量和普通变量一样,但前面要加上*
来表示它是一个指针变量,当然指针也是具有类型的,存储不同类型的变量地址也要用对应的指针类型
使用指针之前必须先了解两个运算符:
&
- 用于返回变量的地址*
- 声明指针变量或者解析地址得到对应的值,这也叫解引用
// 声明指针变量
int *p;
// 声明普通变量
int a = 3;
// 取出 a 的地址交给指针变量 p
p = &a;
// 解引用
printf("%d", *p); // 3
// 它和指针访问是等效的
printf("%d", a); // 3
对指针变量的赋值必须是一个同数据类型变量的地址,所以数组名、带地址运算符&
的变量名、另一个指针都可以进行赋值
提示
&
运算符的操作数必须是变量,取出来的地址大小取决于编译器和系统架构。在 32 位编译器中,指针的大小永远都是 4 字节,而 64 位编译器则永远都是 8 字节。因为 32 位处理器有 232 个字节,如果想要指针存储这 232 个地址,只需要 4 个字节就正好把内存中所有的地址表示完,表示的内存大小为 4GB,其它位数的处理器以此类推。内存地址属于无符号的整型,是 4 个字节的数据,通常使用十六进制来表示
指针的运算
指针可以参与有限的计算:
- 递增
- 递减
- 增加一个整数
- 减少一个整数
指针的运算实际上是地址的运算,整数会与指针类型占用的字节大小相乘,把结果和初始值相加减,就得到了下一个地址:<int>(p1 ± p2)/sizeof(int)
。这些运算除非在数组上进行,否则没什么意义,因为可能是一个垃圾值
int arr[3] = {1, 2, 3};
int *p = arr;
printf("%d", p++); // 1
printf("%d", p++); // 2
printf("%d", p++); // 3
偏移量
一个指针减去一个指针会得到两个地址之间的偏移量
简单来说指针的值以所执行对象的数据类型大小为单位进行改变
指针的指针
指针变量本身也会占用内存空间,它自己也会有内存地址,因此可以声明一个特殊类型的指针变量来指向另一个指针
int foo = 123;
int *p = &foo;
// 指向指针
int **q = &p;
printf("%d", foo); // 123
printf("%d", *p); // 123
// 在解引用的时候,同样需要一个额外的`*`,否则得到的是另一个指针的地址
printf("%d", **q); // 123
上一个示例是一个二级指针,它本身也具有地址,因此可以使用更加特殊的指针变量来指向它,只需要在声明时额外的增加*
,在进行解引用时也是同样如此
数组与指针
声明数组时,会按照元素类型大小分配一块连续的内存空间,每个元素都对应一个地址。数组名本身就表达地址,并且返回的是第一个元素的地址,也叫基地址。那么对于一个叫arr
的数组,arr
和&arr[0]
是等效的,但数组的元素表达的是变量,需要使用取地址符
int arr[3] = {1, 2, 3};
// int *p = &arr[0]; 和下面这句是一样的
int *p = arr;
printf("%d", p++); // 1
printf("%d", p++); // 2
printf("%d", p++); // 3
[]
运算符会自动计算数组元素的地址并解引用,但不仅仅可以对数组做,也可以对指针变量做
int arr[3] = {1, 2, 3};
int *p = arr;
printf("%d", p[0]); // 1
printf("%d", p[1]); // 2
printf("%d", p[2]); // 3
如果把一个数组当作参数传递给一个函数,这个函数接收的也是一个指针
void foo(int arr[])
{
printf("%d", arr[0]); // 1
printf("%d", arr[1]); // 2
printf("%d", arr[2]); // 3
}
int main(void)
{
int arr[] = {1, 2, 3};
int *p = arr;
foo(arr);
return 0;
}
在大多数情况下,指针和数组访问被视为相同的,但是有一些例外:
- sizeof
sizeof(array)
会返回数组中所有的元素字节大小sizeof(pointer)
会返回指针类型大小
- &
&array
是&array[0]
的别名,返回第一个元素的地址&pointer
返回指针的地址
- 指针变量可以赋值,数组不能
字符串
因为字符串本质上是一个字符数组,所以字符串传递的都是首字符的地址
函数与指针
如果有一个变量在main
中初始化,而需要传给另一个函数进行改写,同时这个函数不具备任何返回值,使用指针可以轻松做到
#include <stdio.h>
// 声明指针变量参数
void increment(int *a) {
// 解引用改写
*a = *a + 1;
}
int main()
{
int a = 0;
// 取出 a 的地址传给函数
increment(&a);
printf("%d", a); // 1
return 0;
}
如果这个函数定义的普通变量,只是一个值传递,而不是引用传递,自然就无法实现
指针也可以指向一个函数,函数在内存中也占用部分存储空间,所以它也有一个起始地址,那指针能够指向函数就不是那么令人意外了
void foo (){
printf("Hello, world");
}
int main(void) {
// 声明指向函数的指针
void (*p)();
// 函数名即代表地址
p = foo;
// 解引用函数地址并调用
(*p)();
}
为什么声明成(*p)()
这种形式,它原本是这种*p()
样子,只是运算符优先级会在这里起作用,所以只能通过(*p)()
来修改优先级
指针变量的类型代表函数的返回值类型,(*p)
后面的()
表示它是一个函数类型的指针,如果有参数,则指定参数类型,仅此而已
指针和函数的这种应用方式可以实现回调函数
#include "stdio.h"
#include "stdlib.h"
void callback() {
printf("The callback function is executed");
}
void foo(void (*callback)()) {
callback();
}
int main(void)
{
foo(callback);
return 0;
}
结构体与指针
指针也能指向结构体变量
struct s {
int x, y;
};
struct s foo = { 1, 2 };
struct s *p
p = &foo;
但是通过指针访问成员就要注意,如果写成*p.x
那就指定无法编译通过,因为.
的优先级比*
高,所以要加上括号保证优先级(*p).x
,为了解决这种问题,C 也引进了箭头写法->
来帮助指针访问成员
(*p).x;
p->x; // 为了解决上面的写法问题,C 提供了 -> 运算符来帮助简写,这和上面是等价的
提示
结构体只是一个创建变量的模板,并不占用内存空间,而结构体中的变量才是真正存放数据的地方,需要空间来存储
空指针和野指针
如果指针没有初始化任何内容(甚至不是NULL
)的指针成为野指针,它会指向乱七八糟的非空垃圾值。如果不想为指针分配垃圾,可以赋值为NULL
,它就是一个不指向任何内容的指针
// 现在是一个野指针
int *p;
// 不指向任何内容
int *p = NULL;
void 指针
void
指针是一种特殊的指针,它不和任何数据类型进行关联,因为它可以转换为任意类型的指针,所以它能够指向任何类型。它不能被解引用,但可以对它进行类型转换来实现,同时它不能够进行任何算术,因为没有值和大小
int foo = 123;
void *p;
p = &foo;
printf("%d", *((int*)p));
它也有很多优点,允许malloc()
和calloc()
分配任意数据类型的内存,还能实现通用的函数