介绍
- 程序中的变量都是保存在内存中的
- 所有地变量都有一个真实的内存地址(独一无二的, 如:
0x00fafafa
) - 所谓指针就是用来保存内存地址的特殊变量
如何获取一个变量的内存地址?
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 = #
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 修饰指针
const 在类型前
则无法修改目标地址对应内存空间的值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;
}
数组指针
数组的本质就是内存中一块连续的空间(并非随机的了), 所以在声明数组的时候需要指定类型和大小, 因为这些内存已经被确定了(多少个连续的空间, 每个内存空间的大小) 而数组变量存储的是这些连续空间的第一个空间的内存地址
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));
}
多次一举, 用二级指针的目的是为了什么?
- ptrs 的目标地址保存的是数组的第一个元素的内存地址
- pp 的目标地址是 ptrs 的内存地址
也就是说: ptrs 代表的是数组的第一个元素, 而 pp 代表的是整个数组, 由于这种多级指针比较变态, 所以更多还是用解引用+数组索引的访问方式 *ptrs[0]
或者 *pp[0]
函数指针
- 函数的本质也是存在内存中的一些数据(这些数据可能记录了应该怎样执行代码)
- 所谓的函数指针就是指向目标地址指向一个函数内存地址的指针
- 可以利用函数指针实现
回调函数
的效果
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 #
}
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 = #
}
int main() {
test();
// 此时test执行完, num对应的内存已经释放掉了
// 此时再输出 *p 的值, 可以正常输出(123)
// 那么就证明: 内存空间中的数据还是存在的, 并没有丢失
// 释放: 只是告诉操作系统现在我不需要这块内存空间了
// 你可以用这块内存空间去做其他事情了, 但是并没有将数据一并删除掉
printf("num = %d \n", *p);
}