C 指针

本章导览 指针是 C 语言最强大也最容易出错的特性。指针 = 地址,理解了这一点,其余都是推论。 本章涵盖:指针基本概念 → 指针变量 → 指针运算 → 数组指针 → 字符串指针 → 函数指针 → 指针数组与多级指针 → 动态内存分配 → 综合小结

Abstract: 本章导览 指针是 C 语言最强大也最容易出错的特性。指针 = 地址,理解了这一点,其余都是推论。

本章涵盖:指针基本概念 → 指针变量 → 指针运算 → 数组指针 → 字符串指针 → 函数指针 → 指针数组与多级指针 → 动态内存分配 → 综合小结


1 指针是什么

指针就是内存地址。

数据存储在内存中,每一块内存都有一个编号(地址)。如果一个变量存储了另一份数据的地址,我们就称它为指针变量

指针变量的值就是某份数据的地址,这份数据可以是普通变量、数组、字符串、函数,甚至是另一个指针变量。

Example: 直观理解 假设有一个 char 类型的变量 c,它存储了字符 'K'(ASCII 码 75),占用地址 0x11A 的内存。另有一个指针变量 p,它的值为 0x11A,正好等于变量 c 的地址——这种情况我们就称 p 指向了 c,或者说 p 是指向变量 c 的指针。

内存地址:  ... | 0x11A | 0x11B | ...
内存内容:  ... |  'K'  |  ...  | ...

           p ────┘  (p 的值 = 0x11A)

2 指针变量

定义指针变量

在变量名前加星号 *

datatype *name;
// 或者定义时初始化
datatype *name = value;
  • * 表示这是一个指针变量
  • datatype 表示该指针所指向的数据的类型
int a = 100;
int *p_a = &a;   // p_a 指向 a,p_a 的类型是 int*

Attention: 取地址符 & 指针需要的是地址,所以变量前必须加 & 取地址符。

// 定义普通变量
float a = 99.5, b = 10.6;
char  c = '@',  d = '#';

// 定义指针变量
float *p1 = &a;
char  *p2 = &c;

// 修改指针变量的值(注意:赋值时不带 *)
p1 = &b;
p2 = &d;

Caution: 关键规则

  • 定义指针变量时必须*
  • 给指针变量赋值时不能*
  • p1 的类型是 float*p2 的类型是 char*
  • 连续定义多个指针变量时,每个变量前面都需要带 *int *p1, *p2, *p3;

通过指针变量取得数据

通过指针获取其指向地址上的数据,使用指针运算符 *

*pointer   // 取得 pointer 所指向的数据
#include <stdio.h>

int main(){
    int a = 15;
    int *p = &a;
    printf("%d, %d\n", a, *p);  // 两种方式都可以输出 a 的值
    return 0;
}

运行结果:15, 15

  • 使用变量名直接获取数据
  • 使用指针间接获取数据(代价更高,但灵活性更强)

Note: * 在不同场景的含义

场景 含义 示例
定义指针变量 表明这是指针变量 int *p;
使用指针变量 获取指针指向的数据 printf("%d", *p);
赋值指针变量 不能加 * p = &a;*p = &a;

通过指针交换两个变量的值

这是理解指针作为函数参数的经典例子。

❌ 不能实现交换的写法

写法一:未初始化的指针 temp

void swap(int *p1, int *p2) {
    int *temp;
    *temp = *p1;    // temp 没有赋值,*temp 无法代表某个变量
    *p1 = *p2;
    *p2 = *temp;
}

temp 是野指针,没有指向合法内存,行为未定义。

写法二:值传递,无法影响外部变量

void swap(int x, int y) {
    int temp;
    temp = x;  x = y;  y = temp;
}
// 调用:swap(a, b);

abxy单向值传递,交换 xy 不能交换 ab

写法三:交换了指针本身,而非指向的数据

void swap(int *p1, int *p2) {
    int *p;
    p = p1;
    p1 = p2;
    p2 = p;
}

交换的是函数内部形参 p1p2 的值,不影响外部的实参。

✅ 正确的交换写法

void swap(int *p1, int *p2) {
    int temp;
    temp = *p1;     // 取出 p1 指向的值
    *p1 = *p2;      // 将 p2 指向的值赋给 p1 指向的地址
    *p2 = temp;     // 将原来的值赋给 p2 指向的地址
}

// 调用
int a = 10, b = 20;
swap(&a, &b);       // 传递地址
// 此时 a=20, b=10

Tip: 核心原则 要在函数内部修改外部变量,必须传递变量的地址(指针),然后通过 * 操作指针所指向的内容。


3 指针变量的运算

指针保存的是地址,地址本质上是整数,因此指针可以进行加法、减法和比较运算

int    a = 10,   *pa = &a, *paa = &a;
double b = 99.9, *pb = &b;
char   c = '@',  *pc = &c;

// 最初的值
printf("&a=%#X, &b=%#X, &c=%#X\n", &a, &b, &c);
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);

// 加法运算
pa++; pb++; pc++;
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);

// 减法运算
pa -= 2; pb -= 2; pc -= 2;
printf("pa=%#X, pb=%#X, pc=%#X\n", pa, pb, pc);

// 比较运算
if(pa <mark> paa){
    printf("%d\n", *paa);
} else {
    printf("%d\n", *pa);
}

运行结果:

&a=0X28FF44, &b=0X28FF30, &c=0X28FF2B
pa=0X28FF44, pb=0X28FF30, pc=0X28FF2B
pa=0X28FF48, pb=0X28FF38, pc=0X28FF2C
pa=0X28FF40, pb=0X28FF28, pc=0X28FF2A
2686784

Important: 指针加减的本质 指针每次 +1,地址增加的是所指向类型的大小(而不是 1 个字节):

类型 大小 p++ 地址变化 p-=2 地址变化
int 4 字节 +4 -8
double 8 字节 +8 -16
char 1 字节 +1 -2

这是为了保证指针总是指向完整的数据单元的起始位置。

两个指针相减:结果是两个指针之间相隔的元素个数(不是字节数)。仅当两个指针指向同一个数组时,相减才有意义。


4 数组与指针

数组在内存中是连续排列的。数组名可以认为是一个指针,它指向数组的第 0 个元素。第 0 个元素的地址称为数组的首地址

int arr[] = { 99, 15, 100, 888, 252 };
内存分布:
| arr[0] | arr[1] | arr[2] | arr[3] | arr[4] |
|   99   |   15   |  100   |  888   |  252   |

  arr(首地址)

4.1 引用数组元素

引用一个数组元素,可以用下标法指针法两种方式:

下标法 指针法
a[i] *(a+i)
p[i] *(p+i)

其中 a 是数组名(int a[10]),p 是指针变量(int *p = a)。

类似地,元素的地址也有两种表示:

下标法 指针法
&a[i] a+i
&p[i] p+i

因此,输入数组元素时:

for(i=0; i<N; i++) scanf("%d", &a[i]);
// 等价于
for(i=0; i<N; i++) scanf("%d", a+i);

4.2 三种遍历数组的方式

(1)下标法 — 最直观

#include <stdio.h>
int main() {
    int a[10];
    int i;
    printf("enter 10 integer numbers:\n");
    for(i=0; i<10; i++) scanf("%d", &a[i]);
    for(i=0; i<10; i++) printf("%d ", a[i]);
    printf("\n");
    return 0;
}

(2)通过数组名计算元素地址

#include <stdio.h>
int main() {
    int a[10];
    int i;
    printf("enter 10 integer numbers:\n");
    for(i=0; i<10; i++) scanf("%d", a+i);
    for(i=0; i<10; i++) printf("%d ", *(a+i));
    printf("\n");
    return 0;
}

(3)用指针变量遍历 — 执行速度最快

#include <stdio.h>
int main() {
    int a[10];
    int *p;
    printf("enter 10 integer numbers:\n");
    for(p=a; p<(a+10); p++) scanf("%d", p);
    for(p=a; p<(a+10); p++) printf("%d ", *p);
    printf("\n");
    return 0;
}

Warning: 指针遍历的常见错误

int *p = a;
for(i=0; i<10; i++) scanf("%d", p++);
// 此时 p 已经指向数组末尾之后!
for(i=0; i<10; i++, p++) printf("%d ", *p); // 错误!

第一个循环结束后,p 已经移动到了数组之外。必须在第二个循环前重置 p = a

4.3 *p++*++p(*p)++ 的区别

假设 p 指向数组第 n 个元素:

表达式 等价于 含义 口诀
*p++ *(p++) 先取第 n 个元素的值,再让 p 指向下一个元素 先用后加
*++p *(++p) 先让 p 指向下一个元素,再取该元素的值(即第 n+1 个) 先加后用
(*p)++ 先取第 n 个元素的值,再对该元素的值 +1 值自增

4.4 数组作为函数参数

函数一章中已知数组可以作为函数参数。C 语言规定形参中的数组实质上是指针变量

实参和形参可以有 4 种对应组合:

实参 形参
数组名 数组名
数组名 指针变量
指针变量 数组名
指针变量 指针变量

Example: 例:将数组逆序排列

// 实参:数组名,形参:指针变量
#include <stdio.h>

void inv(int *x, int n) {
    int temp, *y = x + n - 1;
    for ( ; x < y; x++, y--) {
        temp = *x;
        *x = *y;
        *y = temp;
    }
}

int main() {
    int i, a[10];
    for(i=0; i<10; i++) scanf("%d", a+i);
    inv(a, 10);
    for(i=0; i<10; i++) printf("%d ", a[i]);
    printf("\n");
    return 0;
}

Example: 例:选择排序(指针实现)

void sort(int *x, int n) {
    int i, j, k, t;
    for(i=0; i<n-1; i++) {
        k = i;
        for(j=i+1; j<n; j++)
            if(*(x+j) > *(x+k)) k = j;
        if(k != i) {
            t = *(x+i);
            *(x+i) = *(x+k);
            *(x+k) = t;
        }
    }
}

5 二维数组与指针

5.1 二维数组元素的地址

int a[3][4] = {1,3,5,7, 9,11,13,15, 17,19,21,23};

可以把 a 看成由一维数组构成的一维数组:a 有 3 个元素 a[0]a[1]a[2],每个元素又是一个含有 4 个 int 的一维数组。

“行的地址”与“元素的地址”:

  • a+i行的地址(第 i 行一维数组的地址),也可表示为 &a[i]
  • a[i]+j元素的地址(第 i 行第 j 列),也可表示为 *(a+i)+j&a[i][j]

二维数组元素的 4 种表示方式:

表示方式 说明
a[i][j] 下标法
*(*(a+i)+j) 纯指针法
*(a[i]+j) 混合法
(*(a+i))[j] 混合法

5.2 指向二维数组元素的指针变量

方式一:指向单个元素的指针

将二维数组看作一维连续内存,用 int *p 遍历:

#include <stdio.h>
int main() {
    int a[3][4] = {1,3,5,7, 9,11,13,15, 17,19,21,23};
    int *p;
    for(p = a[0]; p < a[0]+12; p++) {
        if((p - a[0]) % 4 </mark> 0) printf("\n");
        printf("%4d", *p);
    }
    printf("\n");
    return 0;
}

对于 int a[M][N],用 int *p = &a[0][0],则 p+k 指向 a[i][j],其中:

k = i × N + j, i = k / N, j = k % N

方式二:指向一维数组(行)的指针(行指针)

int (*p)[4];   // p 是指向含 4 个 int 的一维数组的指针
p = a;

此时 *(*(p+i)+j) 就是 a[i][j]

#include <stdio.h>
int main() {
    int a[3][4] = {1,3,5,7, 9,11,13,15, 17,19,21,23};
    int (*p)[4], i, j;
    p = a;
    printf("enter row and column: ");
    scanf("%d,%d", &i, &j);
    printf("a[%d][%d] = %d\n", i, j, *(*(p+i)+j));
    return 0;
}

Caution: int *p[4] vs int (*p)[4]

  • int *p[4]指针数组:p 是包含 4 个 int* 元素的数组
  • int (*p)[4]行指针:p 是指向含 4 个 int 的一维数组的指针

括号至关重要!

5.3 用指针作形参处理二维数组

// 形参为指向元素的指针——将二维数组"拉平"
void average(float *p, int n) {
    float *p_end = p + n - 1;
    float sum = 0;
    for( ; p <= p_end; p++)
        sum += (*p);
    printf("average = %5.2f\n", sum / n);
}

// 形参为行指针——保留二维结构
void search(float (*p)[4], int n) {
    int i;
    printf("The score of No.%d are:\n", n);
    for(i=0; i<4; i++)
        printf("%5.2f ", *(*(p+n)+i));
    printf("\n");
}

// 调用
float score[3][4] = {{65,67,70,60},{80,87,90,81},{90,99,100,98}};
average(*score, 12);    // *score 即 score[0],类型为 float*
search(score, 2);       // score 的类型为 float(*)[4]

6 字符串与指针

C 语言没有专门的字符串类型,字符串存放在字符数组中。数组章节中讲到的指针与数组的规则同样适用于字符数组。

6.1 字符串的两种引用方式

(1)用字符数组存放字符串

char string[] = "I love China!";
printf("%s\n", string);
printf("%c\n", string[7]);  // 输出 'C'

(2)用字符指针指向字符串

char *string = "I love China!";
printf("%s\n", string);

字符指针可以随时指向不同的字符串:

char *string;
string = "I love China!";
printf("%s\n", string);
string = "I am a student.";
printf("%s\n", string);

6.2 用指针复制字符串

#include <stdio.h>
int main() {
    char a[] = "I am a boy.", b[20], *p1, *p2;
    p1 = a;  p2 = b;
    for( ; *p1 != '\0'; p1++, p2++)
        *p2 = *p1;
    *p2 = '\0';
    printf("string a is: %s\n", a);
    printf("string b is: %s\n", b);
    return 0;
}

6.3 字符指针作函数参数

用函数实现字符串的复制,形参和实参可以分别是字符数组名或字符指针变量。

// 写法一:数组形参
void copy_string(char from[], char to[]) {
    int i = 0;
    while(from[i] != '\0') {
        to[i] = from[i];
        i++;
    }
    to[i] = '\0';
}

// 写法二:指针形参
void copy_string(char *from, char *to) {
    for( ; *from != '\0'; from++, to++)
        *to = *from;
    *to = '\0';
}

// 写法三:极简版(经典 C 风格)
void copy_string(char *from, char *to) {
    while(*to++ = *from++);
}

写法三利用了赋值表达式的值:当 *from'\0'(即 0)时,赋值表达式的值为 0,循环终止,同时 '\0' 已经被复制到了 to 中。

6.4 字符数组与字符指针的重要区别

特性 字符数组 char a[] 字符指针 char *p
存储内容 各个字符 字符串的首地址
赋值方式 只能在定义时整体赋值,之后逐字符赋值 可以随时赋值指向新字符串
数组名/指针值 是常量,不能修改 a = a+7 是变量,可以修改 p = p+7
修改字符 a[2] = 'r' p[2] = 'r' ❌(字符串常量不可修改)
存储区域 栈(可读写) 字符串常量区(只读)
char a[] = "House";   // a 在栈上,内容可修改
char *b  = "House";   // b 指向常量区,内容不可修改

a[2] = 'r';   // ✅ 合法,a 变为 "Horse"
b[2] = 'r';   // ❌ 未定义行为(段错误)

Tip: 指针指向格式字符串

char *format = "a=%d, b=%f\n";
printf(format, a, b);
// 等价于
printf("a=%d, b=%f\n", a, b);

7 函数指针

7.1 什么是函数指针

函数在编译时会被分配一段存储空间,这段空间的起始地址就是函数的指针。可以定义一个指针变量指向函数:

int (*p)(int, int);
  • p 是一个指向函数的指针变量
  • 该函数的参数为两个 int,返回值为 int
  • p 的类型用 int (*)(int, int) 表示

Caution: 区分函数指针和返回指针的函数

  • int (*p)(int, int);函数指针:p 指向一个函数
  • int *p(int, int);返回指针的函数:p 是一个函数,返回 int*

7.2 用函数指针调用函数

#include <stdio.h>

int max(int x, int y) {
    return (x > y) ? x : y;
}

int main() {
    int (*p)(int, int);   // 定义函数指针
    int a, b, c;
    p = max;              // 将函数 max 的地址赋给 p
    printf("please enter a and b: ");
    scanf("%d,%d", &a, &b);
    c = (*p)(a, b);       // 通过函数指针调用
    // 也可以写成 c = p(a, b);
    printf("%d, %d, max=%d\n", a, b, c);
    return 0;
}

Attention: 函数指针使用注意

  • p = max; ✅ — 将函数地址赋给指针
  • p = max(a,b); ❌ — 这是函数调用,不是赋地址
  • p + np++p-- 等运算无意义,不允许

7.3 函数指针的实际应用

根据用户选择调用不同函数:

#include <stdio.h>

int max(int x, int y) { return (x>y) ? x : y; }
int min(int x, int y) { return (x<y) ? x : y; }
int add(int x, int y) { return x + y; }

int main() {
    int (*p)(int, int);
    int a, b, n;
    scanf("%d,%d", &a, &b);
    scanf("%d", &n);
    if(n <mark> 1)      p = max;
    else if(n </mark> 2)  p = min;
    else             p = add;
    printf("result = %d\n", (*p)(a, b));
    return 0;
}

将函数指针作为函数参数(回调函数):

void fun(int x, int y, int (*p)(int, int)) {
    int result = (*p)(x, y);
    printf("%d\n", result);
}

// 调用
fun(34, -21, max);   // 求最大值
fun(34, -21, min);   // 求最小值
fun(34, -21, add);   // 求和

Tip: 回调函数 将函数指针作为参数传递给另一个函数,使得被调用的函数可以“回调”传入的函数。这是 C 语言实现多态和策略模式的基本手段。


8 返回指针值的函数

一个函数可以返回指针类型的数据(即地址)。定义格式:

类型名 *函数名(参数列表);
float *search(float (*pointer)[4], int n) {
    float *pt;
    pt = *(pointer + n);   // 获取第 n 行的首地址
    return pt;
}

// 调用
float score[3][4] = {{60,70,80,90},{56,89,67,88},{34,78,90,66}};
float *p;
int k;
scanf("%d", &k);
p = search(score, k);
for(int i=0; i<4; i++)
    printf("%5.2f\t", *(p+i));

Example: 查找不及格学生

float *search(float (*pointer)[4]) {
    int i;
    float *pt = NULL;
    for(i=0; i<4; i++)
        if(*(*pointer + i) < 60) pt = *pointer;
    return pt;   // 返回 NULL 表示该生全部及格
}

// 遍历所有学生
for(i=0; i<3; i++) {
    p = search(score + i);
    if(p <mark> *(score + i)) {
        printf("No.%d fails: ", i+1);
        for(j=0; j<4; j++)
            printf("%5.1f ", *(p+j));
        printf("\n");
    }
}

9 指针数组与多级指针

9.1 指针数组

一个数组,其所有元素都是指针类型,称为指针数组

类型名 *数组名[数组长度];
int *p[4];   // p 是包含 4 个 int* 元素的数组

指针数组适合管理多个字符串(各字符串长度不等时,比二维数组更省内存):

#include <stdio.h>
#include <string.h>

void sort(char *name[], int n) {
    char *temp;
    int i, j, k;
    for(i=0; i<n-1; i++) {
        k = i;
        for(j=i+1; j<n; j++)
            if(strcmp(name[k], name[j]) > 0) k = j;
        if(k != i) {
            temp = name[i];
            name[i] = name[k];
            name[k] = temp;
        }
    }
}

int main() {
    char *name[] = {"Follow", "Great", "FORTRAN", "Computer"};
    int n = 4;
    sort(name, n);
    for(int i=0; i<n; i++)
        printf("%s\n", name[i]);
    return 0;
}

注意:排序时交换的是指针(地址),而不是字符串内容本身,效率很高。

9.2 指向指针数据的指针(二级指针)

可以定义一个指针变量指向指针数组:

char *name[4] = {"Follow", "Great", "FORTRAN", "Computer"};
char **p = name;    // p 是二级指针

for(int i=0; i<4; i++) {
    p = name + i;
    printf("%s\n", *p);
}
  • p 的类型是 char**,它指向一个 char* 类型的数据
  • *p 得到的是 char*(某个字符串的首地址)
  • **p 得到的是 char(某个字符)

9.3 带参数的 main 函数

main 函数可以接收命令行参数:

int main(int argc, char *argv[])
  • argc:参数的个数(包含程序名本身)
  • argv:指针数组,每个元素指向一个命令行参数字符串

例如运行 file1 C program 时:

  • argc = 3
  • argv[0] = "file1"argv[1] = "C"argv[2] = "program"
#include <stdio.h>
int main(int argc, char *argv[]) {
    while(argc > 1) {
        ++argv;
        printf("%s\n", *argv);
        --argc;
    }
    return 0;
}

10 动态内存分配

10.1 什么是内存的动态分配

  • 栈(Stack):局部变量自动分配,函数结束自动释放
  • 堆(Heap):程序员手动申请和释放,大小可在运行时决定

10.2 四个动态内存函数

需要包含头文件 #include <stdlib.h>(或 #include <malloc.h>

malloc — 分配内存

void *malloc(unsigned int size);
  • 在堆中分配 size字节的连续空间
  • 返回该空间首字节的地址(void* 类型)
  • 分配失败返回 NULL
int *p = (int *)malloc(10 * sizeof(int));  // 分配 10 个 int 的空间

calloc — 分配并初始化

void *calloc(unsigned n, unsigned size);
  • 分配 n 个大小为 size 的连续空间,并全部初始化为 0
  • 相当于为一维数组动态分配空间
int *p = (int *)calloc(50, sizeof(int));  // 分配 50 个 int,全部初始化为 0

free — 释放内存

void free(void *p);
  • 释放 p 指向的动态空间(p 必须是 malloc/calloc 的返回值)
  • 释放后该空间可被重新使用
  • 无返回值
free(p);   // 释放 p 指向的空间
p = NULL;  // 好习惯:置空防止野指针

realloc — 重新分配

void *realloc(void *p, unsigned int size);
  • p 指向的已分配空间大小改为 size 字节
  • 返回新空间的首地址(可能与原地址不同)
  • 失败返回 NULL
p = (int *)realloc(p, 100 * sizeof(int));  // 扩大到 100 个 int

Example: 动态数组排序

#include <stdio.h>
#include <stdlib.h>

void sort(int a[], int n) {
    int i, j, t;
    for(j=0; j<n-1; j++)
        for(i=0; i<n-1-j; i++)
            if(a[i] > a[i+1]) {
                t = a[i];
                a[i] = a[i+1];
                a[i+1] = t;
            }
}

int main() {
    int *p, i, n;
    printf("请输入整数的个数:\n");
    scanf("%d", &n);
    p = (int *)calloc(n, sizeof(int));  // 动态分配
    printf("输入 %d 个整数:\n", n);
    for(i=0; i<n; i++) scanf("%d", &p[i]);
    sort(p, n);
    printf("排序结果:\n");
    for(i=0; i<n; i++) printf("  %d", p[i]);
    printf("\n");
    free(p);   // 释放内存
    return 0;
}

11 综合小结

核心概念回顾

  1. 指针就是地址。“指向”意味着通过地址可以找到目标数据。
  2. 只有基类型相同的数据的地址才能存入对应的指针变量。void* 是通用指针,可指向任何类型。
  3. 要通过指针修改函数外部的变量,必须传递变量的地址

有关指针类型的完整对比

定义 含义
int i; 普通整型变量
int *p; 指向 int 数据的指针变量
int a[N]; 整型数组,含 N 个元素
int *p[N]; 指针数组,含 N 个 int* 元素
int (*p)[N]; 行指针,指向含 N 个 int 的一维数组
int f(); 返回 int 的函数
int *f(); 返回指针的函数,返回 int*
int (*p)(); 函数指针,指向返回 int 的函数
int **p; 二级指针,指向一个 int* 指针变量

指针运算规则

运算 说明
p + n / p - n 前移/后移 n 个元素(不是 n 个字节)
p++ / p-- 移动到下一个/上一个元素
p1 - p2 两个指针之间的元素个数(须指向同一数组)
p1 </mark> p2 / p1 < p2 比较两个指针的位置
p = NULL 空指针,不指向任何有效数据

使用指针的黄金法则

Danger: 必须牢记

  1. 永远不要使用未初始化的指针(野指针)
  2. malloc/calloc 之后必须检查是否为 NULL
  3. free 之后立即将指针置为 NULL
  4. 指针的基类型必须与指向的数据类型匹配
  5. 数组名是常量,不能对其赋值(a = a+1 ❌)

Quote: 相关笔记

  • c-arrays — 数组基础、字符数组与字符串
  • c-functions — 函数定义、形参与实参、值传递
  • c-linked-list — 指针在动态数据结构中的典型应用
  • 历史版本保留在本地归档中,不随网站发布。