1. 内存

关于内存我们都耳熟能详,对于程序员而言,可以从两个维度去理解这个概念 — 物理存储器和存储地址空间

  • 内存是计算机系统中用于存储数据和指令的地方。它是计算机的关键组件之一,用于临时存储和处理正在运行的程序所需的数据。

    • 主板上装插的内存条
    • 显示卡上的显示RAM芯片
    • 各种适配卡上的RAM芯片和ROM芯片
  • 计算机的内存通常被划分为不同的存储单元,每个存储单元都有一个唯一的地址,用于标识其在内存中的位置。每个存储单元都可以存储一定数量的数据。

    • 编码:对每个物理存储单元(一个字节)分配一个号码
    • 寻址:可以根据分配的号码找到相应的存储单元,完成数据的读写

我们可以将内存抽象成一个很大的一维字符数组,编码就是对内存的每一个字节分配一个唯一的32位或64位的编号(与32位或者64位处理器相关),这个内存编号我们称之为内存地址,例如:一个内存地址可能是0x00007FF6D9A5C000。

内存中的每一个数据根据类型的不同,分配的内存大小也不尽相同:

  • char:占1个字节分配1个地址

  • int、float:占4个字节分配4个地址

  • double、long long:占8个字节分配8个地址

image-20231002093320773

2. 指针

指针是计算机编程中一个重要的概念,它是一种特殊的数据类型,用于存储变量的内存地址。简单来说,指针指向了一个变量在计算机内存中的存储位置

每个变量在内存中都有一个地址,在编程中,通过定义指针变量,我们可以存储一个变量的地址,这就使得我们可以通过间接的方式操作和修改变量,而不需要访问原始的变量名。

image-20231002155813474

2.1 指针变量的定义和使用

指针也是一种数据类型,指针变量也是一种变量,指针变量指向谁,就把谁的地址赋值给指针变量。

首先,要声明一个指针变量,需要使用星号"*"来表示该变量是一个指针。例如,以下示例声明了一个指向整数的指针变量 ptr:

1
2
3
4
5
// 关于 * 的位置没有要求,根据个人喜好书写即可
int *ptr;
int* ptr;
int * ptr;
int*ptr;

可以将指针初始化为某个特定变量的地址,也可以在后续的代码中将指针指向某个变量的地址。要将指针指向一个变量的内存地址,需要使用取地址符号"&",例如:

1
2
int num = 10;
ptr = # // 将ptr指向num的内存地址

要访问指针指向的变量的值,需要使用星号"*"来解引用指针。解引用操作符告诉编译器去访问指针所指向的内存地址,并获取那个地址处存储的值。例如:

1
printf("%d", *ptr); // 输出指针所指向的变量的值

还可以通过指针来修改变量的值。通过解引用指针并使用赋值操作符,可以将新的值存储到指针指向的内存地址处。例如:

1
*ptr = 20; // 将指针所指向的变量的值修改为20

下面是关于指针定义和使用的一段完整示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

int main()
{
int a = 9;
char b = 97;
printf("%p, %p\n", &a, &b); //打印a, b的地址

// int *代表是一种数据类型,int*指针类型,p才是变量名
// 定义了一个指针类型的变量,可以指向一个int类型变量的地址
int* p;
//将a的地址赋值给变量p,p也是一个变量,值是一个内存地址编号
p = &a;
printf("%d\n", *p);//p指向了a的地址,*p就是a的值

char* p1 = &b;
printf("%c\n", *p1);//*p1指向了b的地址,*p1就是b的值

return 0;
}

程序输出的结果如下:

1
2
3
4
0000008E46D9F914, 0000008E46D9F934
9
a
a = 100, b = 66

注意事项:&可以取得一个变量在内存中的地址。但是,不能取寄存器变量地址,因为寄存器变量不在内存里,而在CPU里面,所以是没有地址的。

2.2 指针大小

指针是一个变量,它存储了内存地址的值。在C语言中,使用运算符sizeof()可以计算指针的大小,指针的大小指的是指针变量指向的存储地址的大小,得到的值为 4 或 8:

  • 在32位平台,所有的指针(地址)都是32位(4字节)
  • 在64位平台,所有的指针(地址)都是64位(8字节)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int main()
{
int* p1;
int** p2;
char* p3;
char** p4;
printf("sizeof(p1) = %zd\n", sizeof(p1));
printf("sizeof(p2) = %zd\n", sizeof(p2));
printf("sizeof(p3) = %zd\n", sizeof(p3));
printf("sizeof(p4) = %zd\n", sizeof(p4));
printf("sizeof(double *) = %zd\n", sizeof(double*));

return 0;
}

在32为平台测试的结果如下:

1
2
3
4
5
sizeof(p1) = 4
sizeof(p2) = 4
sizeof(p3) = 4
sizeof(p4) = 4
sizeof(double *) = 4

在64为平台测试的结果如下:

1
2
3
4
5
sizeof(p1) = 8
sizeof(p2) = 8
sizeof(p3) = 8
sizeof(p4) = 8
sizeof(double *) = 8

2.3 野指针和空指针

指针变量也是变量,是变量就可以任意赋值,不要越界即可(32位为4字节,64位为8字节),但是,任意数值赋值给指针变量没有意义,因为这样的指针就成了野指针,此指针指向的区域是未知(操作系统不允许操作此指针指向的内存区域)。所以,野指针不会直接引发错误,操作野指针指向的内存区域才会出问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main()
{
int a = 100;
int* p;
// 给指针变量p赋值,p为野指针
p = a;
// 给指针变量p赋值,p为野指针
p = 0x12345678;
// 对野指针指向的未知区域进行写操作,内存出问题,error
*p = 1000;
return 0;
}

在上面的代码中演示的野指针有两种存在形式:

  • 第6行定义了指针变量但是没有初始化,其指向一块随机的内存地址
  • 第8行和第10行都是让指针指向了一块内存地址,这块地址属于哪个进程(程序)以及现在有没有被使用都是未知的

野指针和有效指针变量保存的都是数值,为了标志此指针变量没有指向任何变量(空闲可用),C语言中,可以把NULL赋值给此指针,这样就标志此指针为空指针,没有任何指针。

1
int* ptr = NULL;

NULL是一个值为0的宏常量:

1
#define NULL ((void *)0)

2.4 万能指针 void*

void* 是一个特殊的指针类型,用来表示一个指向未知类型的指针。它可以存储任何类型的地址,但无法直接解引用或操作其指向的数据。

它可以用于在没有明确类型信息的情况下表示指针。例如,当你需要在函数中传递一个指针,但不确定指针所指向的数据类型时,可以使用 void* 作为参数类型。

使用 void* 类型时,需要注意的是,在使用 void* 指针进行操作之前,必须将其转换为适当的指针类型,以便进行正确的解引用和操作。这是因为  void* 指针在不确定指向的具体类型时无法进行类型推断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main()
{
void* p = NULL;
int a = 10;
// 指向变量时,最好转换为void *
p = (void*)&a;

//使用指针变量指向的内存时,转换为int *
*((int*)p) = 11;
printf("a = %d\n", a);

return 0;
}

总而言之,void* 是一种特殊的指针类型,可以用于表示指向未知类型的指针。它在某些情况下非常有用,但需要小心使用,以避免类型不匹配或错误的操作。

2.5 const 与指针

在定义指针的时候可以添加const关键字, 根据const关键字的位置可以用其修饰指针本身也可以用来修饰指针指向的值:

  1. 常量指针:const 关键字在 * 左边,常量指针的本质是指针,表示指针所指向的地址可变,但是地址中的数据不能被修改。例如:

    1
    2
    const int* ptr;  // ptr 是一个指向 int 类型常量的指针
    int const *ptr; // 等价于 const int* ptr;

    在这个例子中,ptr 是一个指向 int 类型常量的指针。这意味着不能通过 ptr 修改它所指向的整数值,但该指针指向的地址是可以改变的

  2. 指针常量: const 关键字在 * 右边,表示指针指向的地址不能被修改,但是地址中的值可以被修改。例如:

    1
    2
    3
    int value = 10;
    // ptr 是一个常量指针,指向 int 类型的变量 value, 不能被重新赋值
    int* const ptr = &value;

    在这个例子中,ptr 是一个指针常量,指向了变量 value。这意味着不能通过 ptr 修改指针的值,即不能将 ptr 指向其它地址,但可以通过 *ptr 来修改所指向的变量的值。

    注意:指针常量在定义时要赋初值。

下面有几句口诀,方便大家记忆常量指针和指针常量:

  • const (*号)左边放,我是指针变量指向常量 - 常量指针
  • const (*号)右边放,我是指针常量指向变量 - 指针常量
  • const (*号)两边放,我是指针常量指向常量 - 常量指针常量

指针变量能改指向,指针常量不能转向,要是全部变成常量,锁死了,我不能转向,你也甭想变样!

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 main()
{
// 常量指针
int value1 = 100;
int value2 = 200;
const int* ptr = &value1; // 初始化
ptr = &value2; // ok, 可以修改指针指向的内存地址
*ptr = 300; // error, 不能修改指针指向的地址中的值

// 指针常量
int value = 10;
int* const ptr1 = &value; // 初始化
*ptr1 = 20; // ok, 可以修改指针指向的地址中的值
ptr1 = &value1; // error, 不能修改指针指向的内存地址

// 指向常量的指针常量
const int* const ptr2 = &value2;
*ptr2 = 99; // error, 不能修改指针指向的地址中的值
ptr2 = &value; // error, 不能修改指针指向的内存地址

return 0;
}

const 修饰的指针变量可以帮助确保数据的不可变性和程序的安全性,特别是在函数参数传递、返回值和常量数据的处理方面有广泛的应用。在使用 const 修饰的指针时,需要注意遵守 const 修饰符的规则,以便正确使用和理解指针的行为。