指针:一种特殊的变量
指针是 C 语言中的变量
- 因为是变量,所以用于保存具体值
- 特殊之处,指针保存的值是内存中的地址
- 内存地址是什么?
- 内存是计算机中的存储部件,每个存储单元都有固定唯一的编号
- 内存中存储单元的编号即内存地址
内存示例
获取地址
- C 语言中通过 & 操作符获取程序元素的地址
- & 可获取变量,数组,函数的起始地址
- 内存地址的本质是一个无符号整数(4 / 8 个字节)
注意:只有通过 内存地址 + 长度 才能确定一个变量中保存的值
语法
指针定义语法:type * pointer;
- type-数据类型,决定访问内存时的长度
- *-标志,意味着定义一个指针变量
- point-变量名,遵循 C 语言命名规则
指针内存访问: pointer*
- 指针访问操作符(*)作用于指针变量即可访问内存数据(解引用)
- 指针的类型决定通过地址访问内存时的长度范围
- 指针的类型统一占用 4 字节或 8 字节
- sizeof(type *) == 4 or sizeof(type *) == 8
指针定义的规定
- Type * 类型的指针只能保存 Type 类型变量的地址
- 禁止不同类型的指针相互赋值
- 禁止将普通数值当作地址赋值给指针
指针保存的地址必须是有效地址
1 |
|
进行指针赋值时,一定要注意类型的区别,不如会造成各种奇快的错误,这些错误编译器可能并不会进行提示或者警告;
指针与数组
- 数组名可以看作一个指针,代表数组中 0 元素的地址
- 当指针指向数组元素时,可进行指针运算(指针移动)
1 | int a[] = {1, 2, 3, 4}; |
深入理解指针数组
- &a 与 a 在数值上是相同的,但是意义上不同(int a[] = {1, 2, 3, 4, 5};)
- &a 代表数组地址,类型为:int (*) [5]
- a 代表数组 0 号元素地址,类型为:int *
- 指向数组的指针:int (*pName) [5] = &a;
1 |
|
数组名不是指针,只是代表了 0 号元素的地址,因此,可以当作指针使用
int* p = a; 是一个声明,其中 p 是一个指向整数的指针。由于数组名 a 在这个上下文中会衰减为指向其首元素(即 a[0])的指针,因此这个赋值是合法的,并且 p 现在存储了 `a[0] 的地址。
字符串常量
- C 语言中的字符串常量是 char * 类型,一种指针类型
1 |
|
指针移动
*int v = p++
- 指针访问操作符(*)和自增运算操作符(++)优先级相同
- 所以,先从 p 指向内存中取值,然后 p 在进行移动
- int v = *p; p++;
1 |
|
指针与函数
- 函数的本质是一段内存中的代码(占用一片连续内存)
- 函数拥有类型,函数类型由返回类型和参数类型列表组成
函数声明 | 类型 |
---|---|
int sum(int n); | int(int) |
void swap(int* pa, int* pb); | void(int*, int*) |
void g(void); | void(v0id) |
- 函数名就是函数体代码的起始地址(函数入口地址)
- 通过函数名调用函数,本质为指定具体地址的跳转执行(跳转到指定地址处执行)
- 因此,可定义指针,保存函数入口地址
函数指针
(Type func(Type1 a, Type2 b))
- 函数名即函数入口地址,类型为 Type (*) (type1, type2)
- 对于 func 函数,&func 与 func 数值相同,意义相同
- 指向函数的指针:Type (*pFunc) (Type1, Type2) = func
- 对于函数来说,函数名就是地址,所以加不加 & 都是一样的,可以直接写 func
1 |
|
将函数指针作为参数
- 函数指针本质还是指针(变量,保存内存地址)
- 可定义函数指针参数,使用相同的代码实现不同的功能
1 | int calculate(int a[], int len, int(*cal) (int, int)) { |
注意
- 函数指针只是单纯的保存函数的入口地址
- 因此,
- 只能通过函数指针调用目标函数
- 不能进行指针移动(指针运算)
为什么数组作为参数时,无法拿到长度信息
当数组作为参数时,函数的数组形参退化为指针!,因此,不包含数组实参的长度信息;
使用数组名调用时,传递的是 0 号元素的地址;
void func (int a[]) <–> void func(int* a)
void func (int a[1]) <–>
void func (int a[10]) <–>
void func (int a[100]) <–>
1 |
|
为什么 int v = *a++; 而 demo 函数中却可以呢,因为当数组作为参数时,函数的数组形参退化为指针!
让我们详细分析一下:
- 数组名的性质:数组名在 C 语言中是一个常量表达式,表示数组的首元素地址。尽管它经常被当作指针来使用,但它本身并不是指针变量。你不能改变一个数组名来指向数组的下一个元素或另一个数组。
- ++ 操作符的用途:
++
操作符用于将变量的值增加 1。对于整数类型,它简单地增加变量的值;对于指针,它增加指针所指向地址的偏移量(通常是增加指针指向类型的大小)。然而,这两种情况都要求操作数是一个可修改的左值。- 数组名不是左值:在 C 语言中,左值是指可以出现在赋值语句左边的表达式,意味着它可以被赋值。数组名虽然在某种程度上可以表示地址,但它并不是一个可修改的左值。你不能将一个新的值赋给数组名来改变它的地址。
- 类型不匹配:即使我们忽略了数组名不是左值的事实,
a++
在类型上也是不合法的。因为a
(在表达式中)被视为指向其首元素的指针,但a++
的结果(即递增后的值)将不再是一个数组类型,而是一个指向下一个元素的指针。然而,由于a
本身不是左值,这种递增操作在语法上就是不允许的。综上所述,
int
类型的数组a
不能执行a++
这样的操作,因为数组名不是一个可修改的左值,而且++
操作符不适用于数组名。如果你想要遍历数组中的元素,你应该使用指针或数组索引来访问每个元素。例如:
指针与堆空间
堆空间的本质
- 备用的”内存仓库”,以字节为单位预留的可用内存
- 程序可在需要时,从”仓库“中申请使用内存(动态的借用)
- 当不需要再使用申请的内存时,需要及时归还(动态的归还)
void*
- void 类型是基础类型,对应的指针类型为 void*
- void* 是指针类型,其指针变量能够保存地址(可以保存任意类型的内存地址,也可以转化为其他任意类型的指针)
- 通过 void* 的指针无法获取内存长中的数据(无长度信息)
1 |
|
堆空间的使用
- 工具箱:stdlib.h
- 申请:void* malloc(unsigned bytes)
- 归还:void free(void* p)
1 |
|
多级指针
用于保存指向指针的指针
1 | Type v; |
因此: