飞翔飞翔
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
  • 数据库

    • SQL教程
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
主页
  • 计算机基础

    • TCP/IP协议
    • Linux命令
  • 数据库

    • SQL教程
  • 编程语言

    • C语言
    • Python2
    • Python3
  • 数据格式

    • JSON教程
  • 工具

    • Markdown指南
  • Git

    • GitFlow
  • Quartz

    • Quartz教程
  • Java

    • Java设计模式
  • 缓存

    • Redis教程
联系
阿里云
  • 学习路径
  • 第1章 编程基础概念

    • 冯·诺依曼体系结构
    • 数据在计算机中的表示
    • 编程语言的层次
    • C语言的起源与发展
    • C99标准的主要改进
    • 开发环境搭建
    • 第一个C程序
    • 编译与运行流程
    • 可移植性风险的三级体系
  • 第2章 数据类型与运算

    • 字符集与标识符
    • 关键字
    • 注释
    • char 类型
    • short 与 int
    • long 与 long long
    • 有符号与无符号
    • 取值范围与 limits.h
    • float 与 double
    • long double
    • _Bool 类型
    • 变量声明与定义
    • 常量
    • 转义序列
    • 算术运算符
    • 赋值运算符
    • 自增自减运算符
    • 关系与判等运算符
    • 逻辑运算符
    • 位运算符
    • 条件运算符
    • 逗号运算符
    • 运算符优先级
    • 隐式类型转换
    • 显式类型转换
  • 第3章 控制流

    • 表达式语句与空语句
    • 复合语句
    • if 语句
    • switch 语句
    • while 循环
    • do-while 循环
    • for 循环
    • break 与 continue
    • goto 语句
    • return 语句
  • 第4章 函数与模块化编程

    • 函数定义
    • 函数声明与原型
    • main 函数
    • 函数调用机制
    • 传值调用
    • 数组参数
    • 作用域
    • 存储期
    • 链接属性
    • static 与 extern
    • 递归
    • 头文件与源文件
    • 头文件保护
    • include 规则
  • 第5章 数组与字符串

    • 一维数组声明与初始化
    • 数组的存储模型
    • 数组访问与越界
    • 数组操作
    • 二维数组
    • 变长数组 VLA
    • 字符串基础
    • 字符串输入输出
    • 字符串处理函数
    • 字符串与数字转换
  • 第6章 指针

    • 指针的概念
    • 指针的声明与使用
    • 指针运算
    • const 与指针
    • 数组名与指针
    • 指针遍历数组
    • 指针与多维数组
    • 指针作为函数参数
    • 函数返回指针
    • 函数指针
    • 二级指针
    • 复杂声明解析
  • 第7章 结构体、联合体与枚举

    • 结构体定义与声明
    • 结构体初始化
    • 结构体成员访问
    • 结构体嵌套
    • 结构体指针
    • 结构体与函数
    • 联合体
    • 联合体与类型双关
    • 枚举类型
    • 位域
    • 内存对齐与填充
  • 第8章 动态内存管理

    • malloc 与 free
    • calloc 与 realloc
    • 内存泄漏
    • 悬垂指针
    • 内存分配策略
    • 自定义内存池
    • Valgrind 与内存检测
    • 内存碎片
    • 内存对齐分配
    • 常见内存错误
  • 第9章 文件输入输出

    • 文件打开与关闭
    • 文本读写
    • 格式化输入输出
    • 二进制读写
    • 文件定位
    • 错误处理
    • 标准流
    • 临时文件
    • 文件操作示例
  • 第10章 预处理器

    • 预处理器基础
    • 宏定义
    • 带参数的宏
    • 条件编译
    • 头文件包含
    • 预定义宏
    • 宏的高级技巧
    • 预处理器陷阱
    • 编译器特定扩展
  • 第11章 标准库概览

    • 标准库概述
    • assert.h
    • ctype.h
    • errno.h
    • float.h
    • limits.h
    • locale.h
    • math.h
    • setjmp.h
    • signal.h
    • stdarg.h
    • stddef.h
    • stdlib.h
  • 第12章 进阶主题

    • 内联函数
    • 变长数组 VLA
    • 复数类型
    • 布尔类型
    • stdint 与 inttypes
    • 灵活数组成员
    • 匿名结构体与联合体
    • 静态断言
    • 线程支持
    • 原子操作

goto 语句

goto 语句无条件跳转到程序中标记的位置。它是 C 语言中最具争议的流程控制语句——滥用 goto 会破坏代码的结构化,使程序难以理解和维护;但在特定场景下(如深层嵌套中的集中错误处理),goto 可以简化代码、减少重复。C 语言保留了 goto,但建议谨慎使用。

基本语法

goto 标签;

/* ... */

标签: 语句
int i = 0;

loop:
    printf("%d\n", i);
    i++;
    if (i < 5)
        goto loop;

标签的命名规则与变量相同,后面跟冒号 :。标签有自己的名字空间,不会与变量名冲突。

适用场景

集中错误处理:

在函数中有多处可能出错,需要统一清理资源时,goto 比多层嵌套的 if 更清晰:

int process_file(const char *filename)
{
    FILE *fp = fopen(filename, "r");
    if (fp == NULL)
        goto error_open;
    
    char *buffer = malloc(1024);
    if (buffer == NULL)
        goto error_malloc;
    
    int *data = malloc(100 * sizeof(int));
    if (data == NULL)
        goto error_data;
    
    /* 主逻辑 */
    while (fgets(buffer, 1024, fp) != NULL) {
        /* 处理数据 */
    }
    
    /* 成功:清理并返回 */
    free(data);
    free(buffer);
    fclose(fp);
    return 0;

error_data:
    free(buffer);
error_malloc:
    fclose(fp);
error_open:
    perror("Error");
    return -1;
}

没有 goto 的等效代码需要重复清理逻辑:

int process_file(const char *filename)
{
    FILE *fp = fopen(filename, "r");
    if (fp == NULL) {
        perror("Error");
        return -1;
    }
    
    char *buffer = malloc(1024);
    if (buffer == NULL) {
        fclose(fp);         /* 重复 */
        perror("Error");
        return -1;
    }
    
    int *data = malloc(100 * sizeof(int));
    if (data == NULL) {
        free(buffer);       /* 重复 */
        fclose(fp);         /* 重复 */
        perror("Error");
        return -1;
    }
    
    /* ... */
    
    free(data);
    free(buffer);
    fclose(fp);
    return 0;
}

goto 将错误处理集中到函数末尾,避免了清理代码的重复。

跳出深层嵌套:

for (...) {
    for (...) {
        for (...) {
            if (error_condition)
                goto cleanup;   /* 跳出三层循环 */
        }
    }
}

cleanup:
    /* 统一清理 */

禁止滥用

以下用法被认为是滥用 goto,应避免:

构造循环:

/* 不要用 goto 构造循环 */
int i = 0;
loop:
    printf("%d\n", i);
    i++;
    if (i < 10)
        goto loop;

/* 用 for */
for (int i = 0; i < 10; i++)
    printf("%d\n", i);

跳过代码块:

/* 不要用 goto 跳过初始化 */
goto skip;
int x = 10;                 /* 被跳过 */
skip:
    printf("Skipped\n");

C99 不允许 goto 跳过变长数组的初始化,但跳过普通变量的初始化是合法的(结果是未初始化)。

向上跳转形成复杂控制流:

/* 混乱的控制流 */
if (a) goto L1;
if (b) goto L2;
L1: /* ... */
if (c) goto L3;
L2: /* ... */
L3: /* ... */

这种"意大利面条式"代码是 goto 被诟病的原因。

限制

goto 不能跳转到其他函数:

void func1(void)
{
    goto label;             /* 错误:label 在另一个函数中 */
}

void func2(void)
{
label:
    /* ... */
}

goto 不能跳过变长数组(VLA)的声明进入其作用域:

{
    goto skip;              /* 错误:不能跳过 VLA */
    int n = 10;
    int arr[n];             /* VLA */
skip:
    /* ... */
}

现代替代方案

许多 goto 的使用场景可以用结构化方式替代:

goto 场景替代方案
循环for、while、do-while
条件分支if-else、switch
跳出多层循环标志变量、提取函数
集中错误处理do { ... } while(0) + break、提取函数
/* 用 do-while(0) 替代 goto 进行错误处理 */
int process_file(const char *filename)
{
    FILE *fp = NULL;
    char *buffer = NULL;
    int *data = NULL;
    int result = -1;
    
    do {
        fp = fopen(filename, "r");
        if (fp == NULL) break;
        
        buffer = malloc(1024);
        if (buffer == NULL) break;
        
        data = malloc(100 * sizeof(int));
        if (data == NULL) break;
        
        /* 主逻辑 */
        result = 0;
    } while (0);
    
    /* 统一清理 */
    free(data);
    free(buffer);
    if (fp) fclose(fp);
    
    if (result != 0)
        perror("Error");
    
    return result;
}

do { ... } while(0) 配合 break 是 Linux 内核等代码中常见的模式,它提供了类似 goto 的集中退出点,但保持了结构化。

最佳实践

  • 优先使用 for、while、if、switch 等结构化控制流
  • goto 仅在集中错误处理、跳出深层嵌套时使用
  • goto 的目标标签放在函数末尾,用于清理和返回
  • 不要向上跳转(向后跳)
  • 不要跳过变量初始化
  • 不要构造循环
  • 标签名应清晰表达目的,如 cleanup、error_handler
上一页
break 与 continue
下一页
return 语句