Skip to content

介绍

  1. 程序中的变量都是保存在内存中的
  2. 所有地变量都有一个真实的内存地址(独一无二的, 如: 0x00fafafa)
  3. 所谓指针就是用来保存内存地址的特殊变量

如何获取一个变量的内存地址?

c
#include <stdio.h>

int main() {
  int num = 10;

  // &num 表示获取 num 变量的内存地址
  // 每次输出的都不一样, 说明每次运行
  // 程序,内存地址都是随机分配的
  // addr: 0x7ff7b37c202c
  printf("addr: %p \n", &num);
}

指针变量与解引用

用于保存其他变量的内存地址的变量就称之为指针变量(简称指针)

c
#include <stdio.h>

int main() {
  int num = 10;

  // 变量名前面加 * 就表示这是个指针变量
  // 但我用这个变量时, 前面加 * 表示解引用
  int *addr = &num;

  printf("addr: %p \n", addr);   // addr: 0x7ff7b37c202c
  printf("value: %d \n", *addr); // value: 10

  // 注意了:
  // 当我输出指针变量的时候, 用的占位符是 %p
  // 而我解引用之后, 此时用的是 %d, 说明此时
  // 输出的是值不是内存地址
}

通过指针操作内存中的数据

c
#include <stdio.h>

int main() {
  // 普通变量: 存储的是数据
  int num1 = 10;
  int num2 = 20;
  printf("num1 value is: %d\n", num1); // 10
  printf("num2 value is: %d\n", num2); // 20

  // 指针变量: 存储的是内存地址
  int *p1 = &num1;
  int *p2 = &num2;
  printf("p1 addr is: %p\n", p1); // 0x00fafaf1
  printf("p2 addr is: %p\n", p2); // 0x00fafaf2

  int tmp = *p1; // *p1 解引用, 此时 tmp 的值是 10

  // *p1 解引用并赋值, 那么此时就是直接拿
  // 到 *p1 对应内存地址的位置重新放置数据
  // 那么 *p1 的值就是 20, 因为 p2 解引用的值是20
  *p1 = *p2;

  // *p2 解引用并赋值, 此时 p2 对应内存地址的值就是 10
  *p2 = tmp;

  printf("-----\n");
  printf("num1 value is: %d\n", num1); // 20
  printf("num2 value is: %d\n", num2); // 10
}

解引用可以看做直接根据内存地址操作对应的内存空间:

  • 如果是 get 操作: 那就直接获取内存对应地址中的数据
  • 如果是 set 操作: 那就直接找到对应内存地址重新放置数据

txt
tmp = *p1;
获取 *p1 中目标地址(0x00fafaf1)中的数据(10), 并且赋值给 tmp

*p1 = *p2;
找到 *p2 中目标地址(0x00fafaf2)中的数据(20),
并且放到 *p1 目标地址(0x00fafaf1)对应的内存空间中

*p2 = tmp;
获取 tmp 的值并且放到 *p2 目标地址(0x00fafaf2)对应的内存空间中

此时就通过指针操作了内存中的数据
完成了两个变量值的交换

获取指针变量的地址

指针变量自身的地址, 而不是目标地址

c
#include <stdio.h>

int main() {
  int x = 10;
  int *ptr = &x;

  printf("目标地址: %p\n", ptr);
  printf("自身地址: %p\n", &ptr);
  // output:
  // 目标地址: 0x7ff7bea9e02c
  // 自身地址: 0x7ff7bea9e020
  // 虽然说, 内存是每次都是随机分配的
  // 但是, 可以看出每次输出两个值都是不同的
}

多级指针

既然指针变量也可以获取内存地址, 那就意味着可以用一个指针变量去指向另外一个指针变量

c
#include <stdio.h>

int main() {
  int x = 10;
  int *ptr = &x;
  int **other_ptr = &ptr;          // 二级指针
  int ***another_ptr = &other_ptr; // 三级指针

  **other_ptr = 20;
  printf("x is %d \n", x); // 20

  ***another_ptr = 30;
  printf("x is %d \n", x); // 30
  // 虽然说可以这样操作,
  // 但是极度不推荐这样操作
  // 这样的代码有点绕, 不够直观
}

指针与常量

指针也无法修改常量

c
#include <stdio.h>

int main() {
  const int x = 1;

  // 编译器警告:
  // warning: initializing 'int *' with an expression of type 'const int *' discards qualifiers
  int *ptr = &x;

  *ptr = 2;
  printf("x is %d\n", x); // 1
}

const 修饰指针

  1. const 在类型前则无法修改目标地址对应内存空间的值
  2. const 在类型后则无法修改保存的目标地址
c
#include <stdio.h>

int main() {
  int a = 1;
  int b = 2;

  const int * ptr = &a;

  // ptr 变量是可以修改的
  // 也就是说 ptr 指针存储的目标地址是可以修改的
  ptr = &b;

  // *ptr = 20;
  // 但是目标地址对应的内存空间的值是无法修改的
  // 报错: error: read-only variable is not assignable
  printf("a = %d\n", a);
}
c
#include <stdio.h>

int main() {
  int a = 1;
  int b = 2;
  int* const ptr = &a;

  // 指针目标地址对应的内存空间的值是可以修改的
  *ptr = 20;

  // ptr = &b;
  // 但是指针本身是只读的, 也就是是无法修改保存
  // 的目标地址, 报错:
  // error: cannot assign to variable 'ptr' with const-qualified type 'int *const'

  printf("a = %d\n", a);
}
c
#include <stdio.h>

int main() {
  int a = 1;
  int b = 2;
  const int* const ptr = &a;

  // 当通过指针来操作时, 无论是保存的指针还是
  // 目标地址对应空间的值都不允许修改

  // error: read-only variable is not assignable
  // *ptr = 20;

  // error: cannot assign to variable 'ptr' with const-qualified type 'int *const'
  // ptr = &b;
}

数组指针

数组的本质就是内存中一块连续的空间(并非随机的了), 所以在声明数组的时候需要指定类型和大小, 因为这些内存已经被确定了(多少个连续的空间, 每个内存空间的大小) 而数组变量存储的是这些连续空间的第一个空间的内存地址

array-ptr

c
#include <stdio.h>

int main() {
  char str[] = "hello world";

  // 可以看到, 每次 &str 和 &str[0] 的地址都是一样的
  printf("str addr: %p \n", &str);
  printf("str[0] addr: %p \n", &str[0]);
  printf("str[1] addr: %p \n", &str[1]);
  printf("str[2] addr: %p \n", &str[2]);
}

为什么数组可以遍历?

由于数组的内存空间是连续的, 所以, 只需要知道第一个内存空间的地址, 后续的地址在第一内存空间的地址的基础上+1就可以了

c
#include <stdio.h>

int main() {
  int nums[3] = {1, 3, 5};

  // 假设: nums[0] 内存空间的位置是: 0xfafafac1
  // 那么就可以直接推断出, nums 其他元素的位置了
  // nums[0](1): 0xfafafac1
  // nums[1](3): 0xfafafac2
  // nums[2](5): 0xfafafac3

  // 也就是说, 后续的内存地址都是基于第一个位置推导出来的
  // 所以, 你甚至可以这样操作:

  int three = *(nums + 1);
  // *(nums + 1): 找到数组的第一个内存空间的地址+1, 然后解引用
  // 也就是获取: 第一个内存空间的地址+1位置的地址(0xfafafac2)对
  // 应的空间的值, 赋值给 three

  int *five = nums + 2;
  // nums 保存的是数组第1个内存空间的位置, 那么 +2
  // 就可以推导出, 第3个内存空间的位置的地址
  // 由于获取的是个内存地址, 所以可以赋值给指针变量

  printf("tree value is: %d \n", three); // 3
  printf("five value is: %d \n", *five);  // 5
}

指针数组

数组是一堆数据的有序集合, 那么专门用于存储指针的数组就是 指针数组

c
#include <stdio.h>

int main() {
  int a = 1;
  int b = 2;
  int c = 3;

  int * ptrs[3] = {&a, &b, &c};

  *ptrs[0] = 99;
  // 1. 通过 ptrs[0] 获取 a 的地址
  // 2. 解引用并赋值
  printf("a = %d \n", a); // a = 99

  // 通过二级指针去指向 ptrs 这个指针数组
  int **pp = ptrs;

  // *pp 解引用获取到 ptrs(指针数组)
  // [1] 给数组第一个元素(内存地址对应的空间)赋值
  *pp[1] = 88;
  printf("b = %d \n", b); // b = 88

  // 那么也就是说, 我可以这样操作:
  // 1. ptrs 是一个数组, 所以 ptrs 变量的目标地址是数组第一个元素的内存地址
  // 2. pp 的目标地址保存的是 ptrs 的内存地址
  // 3. pp + 2 也就是 ptrs数组第一个元素的内存地址 + 2, 也就是第3个位置的地址
  // 4. **(pp+2) 解引用, 获取到 ptrs 第3个位置的目标地址(对应内存空间)的值
  printf("c = %d \n", **(pp+2));
}

多次一举, 用二级指针的目的是为了什么?

  1. ptrs 的目标地址保存的是数组的第一个元素的内存地址
  2. pp 的目标地址是 ptrs 的内存地址

也就是说: ptrs 代表的是数组的第一个元素, 而 pp 代表的是整个数组, 由于这种多级指针比较变态, 所以更多还是用解引用+数组索引的访问方式 *ptrs[0] 或者 *pp[0]

pptr

函数指针

  1. 函数的本质也是存在内存中的一些数据(这些数据可能记录了应该怎样执行代码)
  2. 所谓的函数指针就是指向目标地址指向一个函数内存地址的指针
  3. 可以利用函数指针实现 回调函数 的效果
c
#include <stdio.h>

void println(int lines) {
  int times = lines < 0 ? 1 : lines;
  for (int i = 0; i < lines; i++) {
    printf("\n");
  }
}

int main() {
  println(3);
  printf("println memory addr: %p \n", &println);

  // 创建一个指针变量newlines, 目标地址不
  // 是一个普通的数据而是一个函数, 所以就叫函数指针
  // 函数返回值类型 (*变量名)(参数类型) = 函数名
  void (*newlines)(int) = println;

  newlines(3);
}
c
#include <stdio.h>

void each(int arr[], int len, void (*callback)(int)) {
  for (int i = 0; i < len; i++) {
    // 自动调用传入的函数
    callback(arr[i]);
  }
}

void print_newline(int n) {
  printf("%d \n", n);
}

int main() {
  int nums[] = {1, 3, 5};
  each(nums, 3, &print_newline);
}

指针函数

所谓的指针函数就是指 返回值是内存地址(或者指针变量)的函数

c
#include <stdio.h>

int num;

int * sum(int a, int b) {
  num = a + b;
  return &num;
}

int main() {
  // 由于返回的是一个内存地址
  // 所以需要用指针变量去接收返回值
  int * res = sum(10, 20);

  printf("res = %d \n", *res);
}


/***********************************
指针函数不能返回一个局部变量的地址,
因为当 test_fn 执行完之后, x 变量
对应的内存地址被释放了, 此时就无法
正确的获取到 x 的内存地址

int * test_fn() {
  int x = 1;
  return &x;
}
***********************************/

空指针与野指针

  • 迷途指针(Dangling pointer)也叫 野指针悬空指针
  • 空指针: 表示不指向任何内存位置, 用于初始化指针, 使用关键字 NULL 来表示
c
#include <stdio.h>
int main() {
  int x = 10;
  int *p1 = &x;

  // 正常输出指针内存地址和目标地址的值
  printf("p1 addr is: %p \n", p1); // 0x7ff7b50791bc
  printf("x =  %d \n", *p1);       // 10

  // 明明没有赋值, 却能够输出地址和值
  // 这不是我们想要的结果, 为什么能够获取到数据? 这和 "内存初始化" 有关
  // 没有初始化的指针就可以称之为野指针
  // 因为谁也不知道这个指针指到哪里去了
  int *p2;
  printf("p2 addr is: %p \n", p2); // 0x1126b57b3
  printf("p2 = %d \n", *p2);       // 1484491592

  // 在声明指针变量的时候, 可能不确定现在要赋值为
  // 哪个变量的内存地址时就可以使用 空指针来初始化
  // 避免出现野指针
  int *p3 = NULL;
  printf("p3 addr is: %p \n", p3); // 0x00
  if (p3 == NULL) {
    printf("p3 is NULL\n");
  }
}

内存初始化

其实应该叫内存数据初始化

c
#include <stdio.h>

int *p = NULL;

void test() {
  int num = 123;
  p = &num;
}
int main() {
  test();
  // 此时test执行完, num对应的内存已经释放掉了
  // 此时再输出 *p 的值, 可以正常输出(123)
  // 那么就证明: 内存空间中的数据还是存在的, 并没有丢失
  // 释放: 只是告诉操作系统现在我不需要这块内存空间了
  // 你可以用这块内存空间去做其他事情了, 但是并没有将数据一并删除掉
  printf("num = %d \n", *p);
}

Released under the MIT License.