HiHuo
首页
博客
手册
工具
关于
首页
博客
手册
工具
关于
  • C 语言入门教程

    • C语言系列教程
    • 第01章 基础概念与编译
    • 第02章 数据类型与变量
    • 第03章 运算符
    • 第04章 输入输出
    • 第05章 流程控制
    • 第06章 数组
    • 第07章 指针
    • 第08章 函数
    • 第09章 构造类型

第07章 指针

7.1 指针基础

7.1.1 什么是指针

指针: 存储内存地址的变量。

int a = 10;      // 普通变量,存储值
int *p = &a;     // 指针变量,存储地址

内存示意图:

变量a:  地址0x1000  →  值10
指针p:  地址0x2000  →  值0x1000 (a的地址)

7.1.2 指针的定义

语法:

数据类型 *指针名;

示例:

int *p1;        // 整型指针
float *p2;      // 浮点型指针
char *p3;       // 字符指针
double *p4;     // 双精度指针

⚠️ 注意: *属于变量名,不是类型的一部分

int* p1, p2;    // p1是指针,p2是int(容易误解!)
int *p1, *p2;   // 推荐:p1和p2都是指针

7.1.3 指针的基本操作

取地址运算符 &:

int a = 10;
int *p = &a;    // p指向a的地址

**解引用运算符 ***:

int a = 10;
int *p = &a;

printf("%d\n", *p);   // 通过指针访问a的值: 10
*p = 20;              // 通过指针修改a的值
printf("%d\n", a);    // a变成了20

完整示例:

#include <stdio.h>

int main(void)
{
    int a = 10;
    int *p = &a;

    printf("a的值: %d\n", a);           // 10
    printf("a的地址: %p\n", (void*)&a); // 0x7fff...
    printf("p的值(a的地址): %p\n", (void*)p);  // 0x7fff...
    printf("*p的值: %d\n", *p);         // 10

    *p = 20;  // 通过指针修改a

    printf("修改后a的值: %d\n", a);     // 20

    return 0;
}

7.1.4 指针的大小

指针的大小取决于系统架构:

#include <stdio.h>

int main(void)
{
    printf("指针大小:\n");
    printf("int*:    %lu 字节\n", sizeof(int*));
    printf("char*:   %lu 字节\n", sizeof(char*));
    printf("double*: %lu 字节\n", sizeof(double*));

    return 0;
}

输出(64位系统):

int*:    8 字节
char*:   8 字节
double*: 8 字节

⚠️ 重点: 所有类型的指针大小相同,取决于系统(32位→4字节,64位→8字节)

7.1.5 空指针

NULL指针: 不指向任何有效内存地址的指针。

#include <stdio.h>

int main(void)
{
    int *p = NULL;  // 初始化为空指针

    if (p == NULL)
    {
        printf("p是空指针\n");
    }

    // *p = 10;  // 错误!空指针不能解引用,会崩溃

    return 0;
}

⚠️ 重要: 使用指针前务必检查是否为NULL!

7.1.6 野指针

野指针: 未初始化或指向无效内存的指针。

int *p;         // 野指针!未初始化,指向随机地址
*p = 10;        // 危险!可能崩溃

int *q = NULL;  // 正确:初始化为NULL

避免野指针:

  1. 声明时初始化
  2. 释放后置为NULL
  3. 使用前检查是否为NULL
int *p = NULL;  // 初始化

if (p != NULL)  // 使用前检查
{
    *p = 10;
}

7.2 指针与数组

7.2.1 数组名与指针

数组名是指向数组首元素的常量指针:

#include <stdio.h>

int main(void)
{
    int arr[5] = {1, 2, 3, 4, 5};
    int *p = arr;  // 等价于 int *p = &arr[0]

    printf("arr:     %p\n", (void*)arr);
    printf("&arr[0]: %p\n", (void*)&arr[0]);
    printf("p:       %p\n", (void*)p);

    return 0;
}

7.2.2 指针访问数组

方式1: 数组下标

int arr[5] = {1, 2, 3, 4, 5};
printf("%d\n", arr[2]);  // 3

方式2: 指针解引用

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;

printf("%d\n", *p);       // 1 (第一个元素)
printf("%d\n", *(p+1));   // 2 (第二个元素)
printf("%d\n", *(p+2));   // 3 (第三个元素)

方式3: 指针下标

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;

printf("%d\n", p[0]);     // 1
printf("%d\n", p[1]);     // 2
printf("%d\n", p[2]);     // 3

7.2.3 指针算术运算

指针加法:

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;

p + 0  // 指向arr[0]
p + 1  // 指向arr[1]
p + 2  // 指向arr[2]

⚠️ 重要: 指针+1移动的字节数 = sizeof(指针类型)

#include <stdio.h>

int main(void)
{
    int arr[5] = {1, 2, 3, 4, 5};
    int *p = arr;

    printf("p:     %p\n", (void*)p);
    printf("p+1:   %p\n", (void*)(p+1));  // 地址增加4字节(int大小)
    printf("p+2:   %p\n", (void*)(p+2));  // 地址增加8字节

    return 0;
}

指针减法:

int arr[5] = {1, 2, 3, 4, 5};
int *p1 = &arr[4];
int *p2 = &arr[1];

printf("%ld\n", p1 - p2);  // 3 (相差3个int元素)

指针比较:

int arr[5] = {1, 2, 3, 4, 5};
int *p1 = &arr[1];
int *p2 = &arr[3];

if (p1 < p2)
{
    printf("p1在p2前面\n");
}

7.2.4 指针遍历数组

方式1: 指针递增

#include <stdio.h>

int main(void)
{
    int arr[5] = {1, 2, 3, 4, 5};
    int *p = arr;

    for (int i = 0; i < 5; i++)
    {
        printf("%d ", *p);
        p++;  // 指针移动到下一个元素
    }
    printf("\n");

    return 0;
}

方式2: 指针偏移

#include <stdio.h>

int main(void)
{
    int arr[5] = {1, 2, 3, 4, 5};
    int *p = arr;

    for (int i = 0; i < 5; i++)
    {
        printf("%d ", *(p + i));
    }
    printf("\n");

    return 0;
}

7.2.5 数组名与指针的区别

特性数组名指针变量
本质常量变量
可否赋值否是
sizeof整个数组大小指针大小
&运算整个数组地址指针变量地址

示例:

#include <stdio.h>

int main(void)
{
    int arr[5] = {1, 2, 3, 4, 5};
    int *p = arr;

    printf("sizeof(arr): %lu\n", sizeof(arr));  // 20 (5*4)
    printf("sizeof(p):   %lu\n", sizeof(p));    // 8 (指针大小)

    // arr = arr + 1;  // 错误!数组名不能赋值
    p = p + 1;         // 正确!指针可以修改

    return 0;
}

7.3 指针与字符串

7.3.1 字符串的两种表示

方式1: 字符数组

char str[] = "Hello";  // 可修改
str[0] = 'h';          // 正确

方式2: 字符指针

char *str = "Hello";   // 指向字符串常量,不可修改
// str[0] = 'h';       // 错误!字符串常量不可修改

⚠️ 区别:

char str1[] = "Hello";   // 栈上分配,可修改
char *str2 = "Hello";    // 常量区,不可修改

7.3.2 字符指针操作字符串

#include <stdio.h>

int main(void)
{
    char *str = "Hello World";
    char *p = str;

    // 遍历字符串
    while (*p != '\0')
    {
        printf("%c", *p);
        p++;
    }
    printf("\n");

    return 0;
}

7.3.3 字符串数组

#include <stdio.h>

int main(void)
{
    char *fruits[] = {
        "Apple",
        "Banana",
        "Orange",
        "Grape"
    };

    int n = sizeof(fruits) / sizeof(fruits[0]);

    for (int i = 0; i < n; i++)
    {
        printf("%s\n", fruits[i]);
    }

    return 0;
}

7.3.4 字符串处理示例

示例: 实现strlen

#include <stdio.h>

size_t myStrlen(const char *str)
{
    const char *p = str;
    while (*p != '\0')
    {
        p++;
    }
    return p - str;
}

int main(void)
{
    char *str = "Hello";
    printf("长度: %lu\n", myStrlen(str));

    return 0;
}

示例: 实现strcpy

#include <stdio.h>

char* myStrcpy(char *dest, const char *src)
{
    char *ret = dest;

    while ((*dest++ = *src++) != '\0')
    {
        // 循环体为空
    }

    return ret;
}

int main(void)
{
    char src[] = "Hello";
    char dest[20];

    myStrcpy(dest, src);
    printf("dest: %s\n", dest);

    return 0;
}

7.4 数组指针与指针数组

7.4.1 指针数组

定义: 数组的每个元素都是指针

语法:

数据类型 *数组名[元素个数];

示例:

#include <stdio.h>

int main(void)
{
    int a = 10, b = 20, c = 30;
    int *arr[3] = {&a, &b, &c};  // 指针数组

    for (int i = 0; i < 3; i++)
    {
        printf("arr[%d] = %d\n", i, *arr[i]);
    }

    return 0;
}

输出:

arr[0] = 10
arr[1] = 20
arr[2] = 30

7.4.2 数组指针

定义: 指向数组的指针

语法:

数据类型 (*指针名)[元素个数];

示例:

#include <stdio.h>

int main(void)
{
    int arr[5] = {1, 2, 3, 4, 5};
    int (*p)[5] = &arr;  // 数组指针,指向整个数组

    for (int i = 0; i < 5; i++)
    {
        printf("%d ", (*p)[i]);
    }
    printf("\n");

    return 0;
}

7.4.3 区分指针数组与数组指针

int *arr[5];     // 指针数组:5个int*元素的数组
int (*p)[5];     // 数组指针:指向包含5个int的数组

// 记忆方法:先看[]还是*
// arr[5]  → 数组
// (*p)    → 指针

优先级: [] > *

int *arr[5];     // arr先与[]结合 → 数组
                 // 再与*结合 → 元素是指针

int (*p)[5];     // ()改变优先级
                 // p先与*结合 → 指针
                 // 再与[]结合 → 指向数组

7.4.4 数组指针的应用

应用: 二维数组

#include <stdio.h>

int main(void)
{
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    int (*p)[4] = matrix;  // 指向每一行(每行4个int)

    for (int i = 0; i < 3; i++)
    {
        for (int j = 0; j < 4; j++)
        {
            printf("%3d ", p[i][j]);
            // 等价于: (*(p+i))[j]
            // 等价于: *(*(p+i)+j)
        }
        printf("\n");
    }

    return 0;
}

7.5 const与指针

7.5.1 四种组合

1. 指向常量的指针:

const int *p;        // 指针指向的内容不可修改
int const *p;        // 同上

int a = 10;
const int *p = &a;
// *p = 20;          // 错误!不能通过p修改a
a = 20;              // 正确!可以直接修改a
p = &b;              // 正确!指针本身可以修改

2. 常量指针:

int * const p = &a;  // 指针本身不可修改,内容可以修改

*p = 20;             // 正确!可以修改内容
// p = &b;           // 错误!指针不能修改

3. 指向常量的常量指针:

const int * const p = &a;  // 指针和内容都不可修改

// *p = 20;          // 错误!
// p = &b;           // 错误!

4. 普通指针:

int *p = &a;         // 指针和内容都可以修改

*p = 20;             // 正确!
p = &b;              // 正确!

7.5.2 记忆技巧

规则: const修饰的是其右边的内容

const int *p;        // const修饰int → *p不可修改
int * const p;       // const修饰p → p不可修改
const int * const p; // 两者都不可修改

7.5.3 const指针的应用

保护函数参数:

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

// 不会修改str的内容
size_t myStrlen(const char *str)
{
    const char *p = str;
    while (*p != '\0')
    {
        p++;
    }
    return p - str;
}

int main(void)
{
    char str[] = "Hello";
    printf("长度: %lu\n", myStrlen(str));

    return 0;
}

7.6 多级指针

7.6.1 二级指针

定义: 指向指针的指针

int a = 10;
int *p = &a;       // 一级指针
int **pp = &p;     // 二级指针

内存示意图:

a:   地址0x1000 → 值10
p:   地址0x2000 → 值0x1000 (a的地址)
pp:  地址0x3000 → 值0x2000 (p的地址)

访问:

#include <stdio.h>

int main(void)
{
    int a = 10;
    int *p = &a;
    int **pp = &p;

    printf("a   = %d\n", a);      // 10
    printf("*p  = %d\n", *p);     // 10
    printf("**pp = %d\n", **pp);  // 10

    **pp = 20;  // 通过二级指针修改a

    printf("a   = %d\n", a);      // 20

    return 0;
}

7.6.2 二级指针的应用

应用1: 指针数组

#include <stdio.h>

int main(void)
{
    char *fruits[] = {"Apple", "Banana", "Orange"};
    char **p = fruits;  // 二级指针指向指针数组

    for (int i = 0; i < 3; i++)
    {
        printf("%s\n", p[i]);
        // 等价于: *(p + i)
    }

    return 0;
}

应用2: main函数参数

#include <stdio.h>

int main(int argc, char **argv)  // 或 char *argv[]
{
    printf("参数个数: %d\n", argc);

    for (int i = 0; i < argc; i++)
    {
        printf("argv[%d] = %s\n", i, argv[i]);
    }

    return 0;
}

执行:

$ ./test hello world
参数个数: 3
argv[0] = ./test
argv[1] = hello
argv[2] = world

7.7 函数指针

7.7.1 函数指针的定义

语法:

返回类型 (*指针名)(参数类型列表);

示例:

#include <stdio.h>

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

int main(void)
{
    int (*func)(int, int) = add;  // 函数指针

    int result = func(3, 5);      // 通过函数指针调用
    printf("3 + 5 = %d\n", result);

    return 0;
}

7.7.2 函数指针数组

#include <stdio.h>

int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }
int divide(int a, int b) { return a / b; }

int main(void)
{
    int (*funcs[])(int, int) = {add, sub, mul, divide};
    char *ops[] = {"+", "-", "*", "/"};

    int a = 10, b = 5;

    for (int i = 0; i < 4; i++)
    {
        printf("%d %s %d = %d\n", a, ops[i], b, funcs[i](a, b));
    }

    return 0;
}

输出:

10 + 5 = 15
10 - 5 = 5
10 * 5 = 50
10 / 5 = 2

7.7.3 回调函数

#include <stdio.h>

void forEach(int arr[], int n, void (*callback)(int))
{
    for (int i = 0; i < n; i++)
    {
        callback(arr[i]);
    }
}

void printElement(int x)
{
    printf("%d ", x);
}

void printSquare(int x)
{
    printf("%d ", x * x);
}

int main(void)
{
    int arr[] = {1, 2, 3, 4, 5};
    int n = sizeof(arr) / sizeof(arr[0]);

    printf("元素: ");
    forEach(arr, n, printElement);
    printf("\n");

    printf("平方: ");
    forEach(arr, n, printSquare);
    printf("\n");

    return 0;
}

7.8 实践练习

练习1: 指针交换两个变量

#include <stdio.h>

void swap(int *a, int *b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main(void)
{
    int x = 10, y = 20;

    printf("交换前: x=%d, y=%d\n", x, y);

    swap(&x, &y);

    printf("交换后: x=%d, y=%d\n", x, y);

    return 0;
}

练习2: 使用指针反转字符串

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

void reverseString(char *str)
{
    char *left = str;
    char *right = str + strlen(str) - 1;

    while (left < right)
    {
        char temp = *left;
        *left = *right;
        *right = temp;

        left++;
        right--;
    }
}

int main(void)
{
    char str[] = "Hello";

    printf("原字符串: %s\n", str);

    reverseString(str);

    printf("反转后: %s\n", str);

    return 0;
}

练习3: 使用函数指针实现计算器

#include <stdio.h>

double add(double a, double b) { return a + b; }
double sub(double a, double b) { return a - b; }
double mul(double a, double b) { return a * b; }
double div(double a, double b) { return a / b; }

double calculate(double a, double b, double (*op)(double, double))
{
    return op(a, b);
}

int main(void)
{
    double a = 10.0, b = 5.0;

    printf("%.2f + %.2f = %.2f\n", a, b, calculate(a, b, add));
    printf("%.2f - %.2f = %.2f\n", a, b, calculate(a, b, sub));
    printf("%.2f * %.2f = %.2f\n", a, b, calculate(a, b, mul));
    printf("%.2f / %.2f = %.2f\n", a, b, calculate(a, b, div));

    return 0;
}

7.9 常见错误

错误1: 野指针

int *p;          // 未初始化
*p = 10;         // 崩溃!

// 正确
int *p = NULL;
if (p != NULL) {
    *p = 10;
}

错误2: 空指针解引用

int *p = NULL;
*p = 10;         // 崩溃!

错误3: 指针悬挂

int *p;
{
    int a = 10;
    p = &a;
}  // a的生命周期结束
*p = 20;  // 危险!p指向无效内存

错误4: 返回局部变量地址

int* func()
{
    int a = 10;
    return &a;  // 错误!返回局部变量地址
}

错误5: 忘记分配内存

char *str;
strcpy(str, "Hello");  // 错误!str未分配内存

// 正确
char str[20];
strcpy(str, "Hello");

7.10 小结

本章学习了:

指针的概念与基本操作(&、*) 指针与数组的关系 指针算术运算 指针与字符串 数组指针与指针数组 const与指针的四种组合 多级指针(二级指针) 函数指针与回调函数

重点提示:

  1. 指针使用前必须初始化
  2. 使用前检查是否为NULL
  3. 避免野指针和悬挂指针
  4. 理解指针与数组的关系
  5. 掌握const指针的各种形式
  6. 指针是C语言的精华,也是难点,需要大量练习

记忆口诀:

  • 指针数组: int *arr[5] → 数组,元素是指针
  • 数组指针: int (*p)[5] → 指针,指向数组
  • 函数指针: int (*func)(int, int) → 指针,指向函数

上一章: 第06章 数组下一章: 第08章 函数

Prev
第06章 数组
Next
第08章 函数