指针

指针:一种特殊的变量

指针是 C 语言中的变量

  • 因为是变量,所以用于保存具体值
  • 特殊之处,指针保存的值是内存中的地址
  • 内存地址是什么?
    • 内存是计算机中的存储部件,每个存储单元都有固定唯一的编号
    • 内存中存储单元的编号即内存地址

内存示例

neicunshili

获取地址

  • C 语言中通过 & 操作符获取程序元素的地址
  • & 可获取变量,数组,函数的起始地址
  • 内存地址的本质是一个无符号整数(4 / 8 个字节)

注意:只有通过 内存地址 + 长度 才能确定一个变量中保存的值

语法

指针定义语法:type * pointer;

  • type-数据类型,决定访问内存时的长度
  • *-标志,意味着定义一个指针变量
  • point-变量名,遵循 C 语言命名规则

指针内存访问: pointer*

  • 指针访问操作符(*)作用于指针变量即可访问内存数据(解引用)
  • 指针的类型决定通过地址访问内存时的长度范围
  • 指针的类型统一占用 4 字节或 8 字节
    • sizeof(type *) == 4 or sizeof(type *) == 8

指针定义的规定

  • Type * 类型的指针只能保存 Type 类型变量的地址
  • 禁止不同类型的指针相互赋值
  • 禁止将普通数值当作地址赋值给指针

指针保存的地址必须是有效地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include<stdio.h>

int main() {
int i = 10;
float f = 10;

int* pi = &f; // warning
float* pf = &f;

printf("pi = %p, pf = %p\n", pi, pf);
printf("*pi = %d, *pf = %f\n", *pi, *pf);

pi = i; // warning

*pi = 1000; // OOPS 程序会崩溃掉

printf("pi = %p, *pi = %d\n", pi, *pi);

return 0 ;

}

/*
pi = 00000026B29FF7E4, pf = 00000026B29FF7E4
*pi = 1092616192, *pf = 10.000000
崩溃了
*/

进行指针赋值时,一定要注意类型的区别,不如会造成各种奇快的错误,这些错误编译器可能并不会进行提示或者警告;

指针与数组

  • 数组名可以看作一个指针,代表数组中 0 元素的地址
  • 当指针指向数组元素时,可进行指针运算(指针移动)
1
2
3
int a[] = {1, 2, 3, 4};
int* p = a;
p = p + 1 //指向数组中第二个元素

深入理解指针数组

  • &a 与 a 在数值上是相同的,但是意义上不同(int a[] = {1, 2, 3, 4, 5};)
  • &a 代表数组地址,类型为:int (*) [5]
  • a 代表数组 0 号元素地址,类型为:int *
  • 指向数组的指针:int (*pName) [5] = &a;
1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>

int main() {
int a[] = {1, 2, 3, 4, 5};
int (*pa) [5] = &a; // int (*pa) [] = &a;
int* p = a;

printf("%p, %p\n", pa, p); //00000061F43FFDA0, 00000061F43FFDA0

pa = p // WARNING 类型不一致不能相互赋值
}

数组名不是指针,只是代表了 0 号元素的地址,因此,可以当作指针使用

int* p = a; 是一个声明,其中 p 是一个指向整数的指针。由于数组名 a 在这个上下文中会衰减为指向其首元素(即 a[0])的指针,因此这个赋值是合法的,并且 p 现在存储了 `a[0] 的地址。

字符串常量

  • C 语言中的字符串常量是 char * 类型,一种指针类型
1
2
3
4
5
6
7
8
9
10
11
#include<stdio.h>

int main() {
printf("%p\n", "kay.wang");
printf("%p\n", "kay.wang");
/*
00007FF67FE74000
00007FF67FE74000
*/

}

指针移动

*int v = p++

  • 指针访问操作符(*)和自增运算操作符(++)优先级相同
  • 所以,先从 p 指向内存中取值,然后 p 在进行移动
    • int v = *p; p++;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>

int main() {
char* s = NULL;
printf("first = %c\n", *"D.T.software");
s = "D.T.software";
while (*s) // 字符串转为 char 数组后最有一个元素为 \0 也就是 0 元素
{
printf("%c", *s++);
}
printf("\n");
return 0;
/*
first = D
D.T.software
*/
}

指针与函数

  • 函数的本质是一段内存中的代码(占用一片连续内存)
  • 函数拥有类型,函数类型由返回类型参数类型列表组成
函数声明 类型
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<stdio.h>

int add(int a, int b) { return a + b;}

int mul(int a, int b) { return a * b;}

int main() {
int (*pFunc)(int, int) = add;

printf("%d\n", pFunc(1, 2)); // 3
printf("%d\n", (*pFunc)(3, 4)); // 7

pFunc = &mul;

printf("%d\n", pFunc(1, 2)); // 2
printf("%d\n", (*pFunc)(3, 4)); // 12
}

将函数指针作为参数

  • 函数指针本质还是指针(变量,保存内存地址)
  • 可定义函数指针参数,使用相同的代码实现不同的功能
1
2
3
4
5
6
7
8
9
int calculate(int a[], int len, int(*cal) (int, int)) {
int ret = a[0];
int i = 0;

for(i = 1; i < len; i++) {
ret = cal(ret, a[i]);
}
return ret;
}

注意

  • 函数指针只是单纯的保存函数的入口地址
  • 因此,
    • 只能通过函数指针调用目标函数
    • 不能进行指针移动(指针运算)

为什么数组作为参数时,无法拿到长度信息

当数组作为参数时,函数的数组形参退化为指针!,因此,不包含数组实参的长度信息;

使用数组名调用时,传递的是 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<stdio.h>

int demo(int arr[], int len) {
int ret = 0;
int i = 0;

printf("demo: sizeof(arr) = %d\n", sizeof(arr)); // 32位系统为 4 64位系统为 8

while (i < len)
{
ret += *arr++;
i ++;
}

}

int main() {
int a[] = {1, 2, 3, 4, 5};

// int v = *a++; // Error

demo(a, 5);
return 0;
}

为什么 int v = *a++; 而 demo 函数中却可以呢,因为当数组作为参数时,函数的数组形参退化为指针!

让我们详细分析一下:

  1. 数组名的性质:数组名在 C 语言中是一个常量表达式,表示数组的首元素地址。尽管它经常被当作指针来使用,但它本身并不是指针变量。你不能改变一个数组名来指向数组的下一个元素或另一个数组。
  2. ++ 操作符的用途++ 操作符用于将变量的值增加 1。对于整数类型,它简单地增加变量的值;对于指针,它增加指针所指向地址的偏移量(通常是增加指针指向类型的大小)。然而,这两种情况都要求操作数是一个可修改的左值。
  3. 数组名不是左值:在 C 语言中,左值是指可以出现在赋值语句左边的表达式,意味着它可以被赋值。数组名虽然在某种程度上可以表示地址,但它并不是一个可修改的左值。你不能将一个新的值赋给数组名来改变它的地址。
  4. 类型不匹配:即使我们忽略了数组名不是左值的事实,a++ 在类型上也是不合法的。因为 a(在表达式中)被视为指向其首元素的指针,但 a++ 的结果(即递增后的值)将不再是一个数组类型,而是一个指向下一个元素的指针。然而,由于 a 本身不是左值,这种递增操作在语法上就是不允许的。

综上所述,int 类型的数组 a 不能执行 a++ 这样的操作,因为数组名不是一个可修改的左值,而且 ++ 操作符不适用于数组名。如果你想要遍历数组中的元素,你应该使用指针或数组索引来访问每个元素。例如:

指针与堆空间

堆空间的本质

  • 备用的”内存仓库”,以字节为单位预留的可用内存
  • 程序可在需要时,从”仓库“中申请使用内存(动态的借用)
  • 当不需要再使用申请的内存时,需要及时归还(动态的归还)

void*

  • void 类型是基础类型,对应的指针类型为 void*
  • void* 是指针类型,其指针变量能够保存地址(可以保存任意类型的内存地址,也可以转化为其他任意类型的指针)
  • 通过 void* 的指针无法获取内存长中的数据(无长度信息)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>

int main() {
char c = 0;
int i = 1;
float f= 2.0f;
double d = 3.0;

void* p = NULL;

p = &c;
p = &i;
p = &f;
p = &d;

// printf("%f\n", *p); // Error

return 0 ;
}

堆空间的使用

  • 工具箱:stdlib.h
  • 申请:void* malloc(unsigned bytes)
  • 归还:void free(void* p)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<stdio.h>
#include<stdlib.h>

int main() {

// 从堆空间中申请 4 个字节用来当作 int 类型的变量使用
int* p = malloc(4);
// 判断是否申请到了堆空间
if(p != NULL) { // 如果申请失败 p 为 0; 即:空值
*p = 100;
printf("%d\n", *p); // 100
free(p);
}
return 0 ;
}

多级指针

用于保存指向指针的指针

1
2
3
4
5
6
7
8
9
Type v;
Type* pv = &v;
Type** ppv = &pv;
Type*** pppv = &ppv;
==========================
/*
多级数组的指针该如何表示
C 语言中没有多级数组,所谓二级数组,就是一维数组的数组,即:数组中的元素是一维数组!!;以此类推
/*

因此:

erweishuzuzhizhen