C语言学习笔记:控制流

栏目: C · 发布时间: 4年前

内容简介:三元表达式是最简单的分支结构,具体逻辑为如果a为true,则执行b,否则执行c。当使用if和else,主要需要注意的是“悬空”else的问题。C语言中默认else与离它最近的if绑定。该分支结构中,传入的条件只能是int型变量(包含char、enum这些int型的拓展)。每个分支需要以break结束,否则分支会依次执行下去不会停止。

分支结构

a ? b : c

三元表达式是最简单的分支结构,具体逻辑为如果a为true,则执行b,否则执行c。

if…else…

当使用if和else,主要需要注意的是“悬空”else的问题。C语言中默认else与离它最近的if绑定。

switch…case…

该分支结构中,传入的条件只能是int型变量(包含char、enum这些int型的拓展)。每个分支需要以break结束,否则分支会依次执行下去不会停止。

循环控制

while  (表达式) 语句

当表达式为真时,执行语句,知道表达式不成立。

do  语句  while  (表达式)

无论表达式是否为真,语句至少执行一遍。

for (表达式1; 表达式2; 表达式3)

  • 表达式1:循环开始前的初始化步骤
  • 表达式2:用来控制循环的终止(只要表达式为真,循环持续进行)
  • 表达式3:每次循环中最后被执行的操作

for表达式等价于如下while循环:

表达式1;
while(表达式2){
 语句
 表达式3;
}

备注:C99后,变量可以直接到表达式1中声明。

退出循环

break

作用:

  • 跳出switch语句
  • 跳出do、while、for循环

要点:只能跳出一层嵌套

continue

作用:跳过循环体剩余部分(跳转到该次循环末尾)

要点:只能应用与循环语句

goto

goto可以将程序跳转到任何标号的语句处。如果使用太多goto语句,程序代码会变得相当不宜阅读,所以只有在非常有必要时才应该使用goto语句,比如从很深的嵌套循环中跳出。

使用示例:

#include <stdio.h>
 
int main() {
    int i, j;
 
    for (i = 0; i < 10; i++) {
        printf("Outer loop executing. i = %d\n", i);
        for (j = 0; j < 3; j++) {
            printf(" Inner loop executing. j = %d\n", j);
            if (i == 2)
                goto stop;
        }
    }
 
    /* This message does not print: */
    printf("Loop exited. i = %d\n", i);
 
    stop:
    printf("Jumped to stop. i = %d\n", i);
}

输出内容:

Outer loop executing. i = 0
 Inner loop executing. j = 0
 Inner loop executing. j = 1
 Inner loop executing. j = 2
Outer loop executing. i = 1
 Inner loop executing. j = 0
 Inner loop executing. j = 1
 Inner loop executing. j = 2
Outer loop executing. i = 2和
 Inner loop executing. j = 0
Jumped to stop. i = 2

相关内容:setjmp()、longjmp()。见下文。

错误处理

assert.h

assert通常被翻译为断言,有时也被称为诊断,主要功能是声明某种东西为真,如果为假则向标准错误打印一条诊断信息并终止程序,如果为真则不做任何操作,程序继续执行。函数原型如下:

void assert(int expression)

使用示例:

#include <stdio.h>
#include <assert.h>
 
void main() {
    while(1){
        int a, b;
        double c;
        scanf("%d/%d", &a, &b);
        assert(b != 0);
        c = (double) a / b;
        printf("%lf", c);
    }
}

以上为一个简单的除法程序,我们在这里断言分母不为0,如果为0,则会输出报错信息(如果不使用断言,当分母为0时,c语言默认返回无穷大inf)。具体如下:

5/2
2.500000
4/0
LearnC: /home/qw/CLionProjects/LearnC/main.c:9: void main(): Assertion `b != 0' failed.

assert有一个缺点:因为它引入了额外的检查,因此会增加程序的运行时间,偶尔使用一次assert可能对程序的运行速度没有很大的影响,所以通常在测试的使用使用assert,正式的时候不使用。通常禁用assert有两种方式,一种是在assert.h文件引入前,添加#define NDEBUG,另外一种方式是在编译时加上-DNDEBUG,当NDEBUG被定义后,预处理器将丢弃所有的断言,这样就消除了对性能的影响,而不用将所有断言从源文件移除。

errno.h

标准库中的一些函数通过向errno中声明的int类型errno变量存储一个错误码来表示有错误发生。我们可以在调用库函数之后检查errno是否为0来判断函数在调用过程中是否发生错误。

#include <stdio.h>
#include <math.h>
#include <errno.h>
 
int main(void)
{
    errno = 0;
    sqrt(-1);
    printf("%d\n",errno);
    return 0;
}

需要注意,在使用errno前需要手动置0,上一次错误产生后该值并不会清0。以上代码执行后,输出的内容为33,33到底是什么意思内。在我的电脑中,errno定义的内容在

  • /usr/include/asm-generic/errno-base.h
  • /usr/include/asm-generic/errno.h

具体内容类似:

/* SPDX-License-Identifier: GPL-2.0 WITH Linux-syscall-note */
#ifndef _ASM_GENERIC_ERRNO_BASE_H
#define _ASM_GENERIC_ERRNO_BASE_H
 
#define	EPERM		 1	/* Operation not permitted */
#define	ENOENT		 2	/* No such file or directory */
#define	ESRCH		 3	/* No such process */
#define	EINTR		 4	/* Interrupted system call */
#define	EIO		 5	/* I/O error */
#define	ENXIO		 6	/* No such device or address */
#define	E2BIG		 7	/* Argument list too long */
#define	ENOEXEC		 8	/* Exec format error */
#define	EBADF		 9	/* Bad file number */
#define	ECHILD		10	/* No child processes */
#define	EAGAIN		11	/* Try again */
#define	ENOMEM		12	/* Out of memory */
#define	EACCES		13	/* Permission denied */
#define	EFAULT		14	/* Bad address */
#define	ENOTBLK		15	/* Block device required */
#define	EBUSY		16	/* Device or resource busy */
#define	EEXIST		17	/* File exists */
#define	EXDEV		18	/* Cross-device link */
#define	ENODEV		19	/* No such device */
#define	ENOTDIR		20	/* Not a directory */
#define	EISDIR		21	/* Is a directory */
#define	EINVAL		22	/* Invalid argument */
#define	ENFILE		23	/* File table overflow */
#define	EMFILE		24	/* Too many open files */
#define	ENOTTY		25	/* Not a typewriter */
#define	ETXTBSY		26	/* Text file busy */
#define	EFBIG		27	/* File too large */
#define	ENOSPC		28	/* No space left on device */
#define	ESPIPE		29	/* Illegal seek */
#define	EROFS		30	/* Read-only file system */
#define	EMLINK		31	/* Too many links */
#define	EPIPE		32	/* Broken pipe */
#define	EDOM		33	/* Math argument out of domain of func */
#define	ERANGE		34	/* Math result not representable */
 
#endif

可以看到这里的错误是参数超过函数的定义范围。像这样每次发生错误需要打开定义文件查看具体什么错误还是比较麻烦的,C语言中还提供了一个strerror函数,可以将具体的错误内容输出出来。需要注意的是strerror并不是存在与errno.h中,而是存在于string.h中,具体使用方法如下:

#include <stdio.h>
#include <math.h>
#include <errno.h>
#include <string.h>
 
int main(void)
{
    errno = 0;
    sqrt(-1);
    printf("%d\n",errno);
    fprintf(stderr, "%s\n", strerror(errno));
    return 0;
}

输出内容为:

33
Numerical argument out of domain

除了strerror外,还有另外一个函数也可以输出错误信息,那就是perror。perror定义在stdio.h中,具体使用方式如下:

#include <stdio.h>
#include <math.h>
#include <errno.h>
 
int main(void)
{
    errno = 0;
    sqrt(-1);
    if (errno)
        perror("Error Content");
    return 0;
}

输出内容为:

Error Content: Numerical argument out of domain

可以看到这里输出的内容为传入“字符串: 错误信息”。备注以上内容会输出到stderr。

signal.h

signal.h提供了处理异常情况的工具。C语言中将异常情况称为信号。信号有2种类型:运行时错误和发生程序以外的错误(例如用户中断目前正在运行的程序)。当有错误或外部事件发生时,我们称为一个信号。大多数信号是异步的:它们可以在程序执行的任意时刻发生,而不仅仅是在 程序员 所知道的特定时刻。由于信号可能会在任意想不到的地方发生,因此需要用独特的方式处理他们。

signal.h定义了一系列的宏,比较常见的。下表列出了一些常见信号:

信号名称 数字表示 说明
SIGHUP 1 终端挂起或控制进程终止。当用户退出 Shell 时,由该进程启动的所有进程都会收到这个信号,默认动作为终止进程。
SIGINT 2 键盘中断。当用户按下<Ctrl+C>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为终止进程。
SIGQUIT 3 键盘退出键被按下。当用户按下<Ctrl+D>或<Ctrl+\>组合键时,用户终端向正在运行中的由该终端启动的程序发出此信号。默认动作为退出程序。
SIGFPE 8 发生致命的运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为0等所有的算法错误。默认动作为终止进程并产生core文件。
SIGKILL 9 无条件终止进程。进程接收到该信号会立即终止,不进行清理和暂存工作。该信号不能被忽略、处理和阻塞,它向系统管理员提供了可以杀死任何进程的方法。
SIGALRM 14 定时器超时,默认动作为终止进程。
SIGTERM 15 程序结束信号,可以由 kill 命令产生。与SIGKILL不同的是,SIGTERM 信号可以被阻塞和终止,以便程序在退出前可以保存工作或清理临时文件等。

以下为我电脑中定义的一些内容:

#define _NSIG		64
#define _NSIG_BPW	__BITS_PER_LONG
#define _NSIG_WORDS	(_NSIG / _NSIG_BPW)
 
#define SIGHUP		 1
#define SIGINT		 2
#define SIGQUIT		 3
#define SIGILL		 4
#define SIGTRAP		 5
#define SIGABRT		 6
#define SIGIOT		 6
#define SIGBUS		 7
#define SIGFPE		 8
#define SIGKILL		 9
#define SIGUSR1		10
#define SIGSEGV		11
#define SIGUSR2		12
#define SIGPIPE		13
#define SIGALRM		14
#define SIGTERM		15
#define SIGSTKFLT	16
#define SIGCHLD		17
#define SIGCONT		18
#define SIGSTOP		19
#define SIGTSTP		20
#define SIGTTIN		21
#define SIGTTOU		22
#define SIGURG		23
#define SIGXCPU		24
#define SIGXFSZ		25
#define SIGVTALRM	26
#define SIGPROF		27
#define SIGWINCH	28
#define SIGIO		29
#define SIGPOLL		SIGIO
/*
#define SIGLOST		29
*/
#define SIGPWR		30
#define SIGSYS		31
#define	SIGUNUSED	31
 
/* These should not be considered constants from userland.  */
#define SIGRTMIN	32
#ifndef SIGRTMAX
#define SIGRTMAX	_NSIG
#endif

signal提供了两个函数raise和signal。

  • raise:动触发信号。
  • signal:监测并处理特定信号。

raise函数使用起来非常的简单,只需将信号编码传递给他即可。signal函数共有两个参数,第一个为特定的信号编码,第二个为处理这个信号的函数的指针,其中处理函数必须有一个int型的参数(信号编码会传入),并返回为void。

#include <signal.h>
#include <stdio.h>
 
volatile sig_atomic_t gSignalStatus;
 
void signal_handler(int signal) {
    gSignalStatus = signal;
}
 
int main(void) {
    signal(SIGINT, signal_handler);
 
    printf("SignalValue: %d\n", gSignalStatus);
    printf("Sending signal: %d\n", SIGINT);
    raise(SIGINT);
    printf("SignalValue: %d\n", gSignalStatus);
}

sig_atomic_t,这是 int 类型,在信号处理程序中作为变量使用。它是一个对象的整数类型,该对象可以作为一个原子实体访问,即使存在异步信号时,该对象可以作为一个原子实体访问。除非信号是由调用abort函数或raise函数引发的,否则新还处理函数不应该调用库函数或具有静态存储期限的变量。注意:如果信号是由信号处理函数引发的,则有可能引发无限递归,所以处理函数尽可能的简单。另外signal.h中也预定义了信号处理函数:

  • SIG_DFL:按默认方式处理信号(大多数情况会导致程序终止)
  • SIG_IGN:忽略该信号
  • SIG_ERR:看起来像信号处理函数,实际上是用来在安装处理函数时检测是否发生错误的。如果一个signal调用失败(即不能对所指定的信号安装处理函数),就会返回SIG_ERR并在errno中传入一个值。使用场景如下:
if (signal(SIGINT, signal_handler) == SIG_ERR){
    perror("signal(SIGINT, signal_handler failed");
}

除了raise函数外,有多种方式向程序发送信号,例如按下<Ctrl+C>组合键会发送SIGINT信号,终止当前进程。还可以通过 kill 命令发送信号,语法为:kill -signal pid

setjmp.h

goto语句只允许局部性跳;也就是“在自己的函数内跳转”。C语言也提供了非局部跳转的库setjmp.h。它常用于深层嵌套的函数调用链。如果某个低层的函数中检测到一个错误,你可以立即返回到顶层函数,不必向调用链中的每个中间层函数返回一个错误标志。

在setjmp.h中最重要的内容就是setjmp宏和longjmp函数。setjmp宏标记程序中的一个位置,随后可以通过longjmp函数跳转到该位置。如果要为将来的跳转标记一个位置,可以调用setjmp宏,调用的参数是一个jmp_buf类型的变量。setjmp会在第一次调用时返回0。代码实例:

#include <stdio.h>
#include <setjmp.h>
 
jmp_buf env;
 
void f1(void);
void f2(void);
 
int main()
{
    if (setjmp(env) == 0)
        printf("setjmp returned 0\n");
    else {
        printf("Program terminates: longjmp called\n");
        return 0;
    }
 
    f1();
    printf("Program terminates normally\n");
    return 0;
}
 
void f1(void)
{
    printf("f1 begins\n");
    f2();
    printf("f1 returns\n");
}
 
void f2(void)
{
    printf("f2 begins\n");
    longjmp(env, 1);
    printf("f2 returns\n");
}

输出内容为:

setjmp returned 0
f1 begins
f2 begins
Program terminates: longjmp called

setjmp宏的最初调用返回0,因此main函数会调用f1,接着,f1调用f2,f2使用longjmp函数将控制全重新转给main函数,而不是返回到f1。当longjmp函数被执行时,控制权重回到setjmp宏调用。这次setjmp宏返回1(就是longjmp调用时所指定的值)。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Ruby on Rails敏捷开发最佳实践

Ruby on Rails敏捷开发最佳实践

李刚 / 电子工业出版社 / 2008-4 / 79.80元

《Ruby on Rails敏捷开发最佳实践》适用于正在使用Ruby On Rails进行应用开发的开发人员、渴望了解Ruby On Rails框架的开发人员,尤其适合有初步的Java EE开发经验,想从Java EE平台过渡到Ruby On Rails开发平台的开发者。 Ruby On Rails框架一经推出,立即引起B/S结构应用开发领域革命性的变化:开发者无需理会架构,只需要按Rail......一起来看看 《Ruby on Rails敏捷开发最佳实践》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试