c语言快速入门2020版2快速入门
本章将开发你的第一个C语言程序:传统的 "Hello, world!"程序。然后讨论一些编辑器和编译器的选项,并阐述移植性问题。
Hello, world!
#include <stdio.h>
#include <stdlib.h>
int main(void) {
puts("Hello, world!");
return EXIT_SUCCESS;
}
在Linux和其他类似Unix的操作系统上,你可以用cc命令调用系统编译器:
$cc hello.c
$ls
a.out hello.c
$./a.out
Hello, world!
% cc -o hello hello.c
% ./hello
Hello, world!
cc命令有许多标志和编译器选项。例如,-o文件标志让你给可执行文件起名字,而不是a.out。下面的编译器调用命名了可执行文件hello。
hello.c程序的前两行使用了#include预处理器指令,它的行为就像你在完全相同的位置用指定文件的内容替换它一样。我们包括<stdio.h>和<stdlib.h>头文件来访问这些头文件中声明的函数,然后我们可以在程序中调用这些函数。puts函数在<stdio.h>中声明,而EXIT_SUCCESS宏在<stdlib.h>中定义。正如文件名所示,<stdio.h>包含了C语言标准I/O函数的声明,而<stdlib.h>则包含了一般实用函数的声明。你需要包括你在程序中使用的任何库函数的声明。
C定义了两种可能的执行环境:独立的和托管的。独立环境可能不提供操作系统,通常用于嵌入式编程。这些执行环境提供了一套最小的库函数,程序启动时调用的函数的名称和类型是执行环境定义的。
我们定义main返回int类型的值,并将void放在括号内,表示该函数不接受参数。int类型是有符号的整数类型,可以用来表示正、负整数值以及零。与其他程序性语言类似,C语言程序由可以接受参数和返回值的过程(称为函数)组成。每个函数都可重用,你可以根据需要在程序中频繁调用。在本例中,主函数返回的值表示程序是否成功终止。
puts("Hello, world!")打印出"Hello, world!"。 puts函数是标准库函数,它将字符串参数写入stdout(通常代表控制台或终端窗口),并在输出中附加一个换行符。
return语句退出程序,向主机环境或调用脚本返回一个整数值。EXIT_SUCCESS是类似对象的宏,通常扩展为0,通常定义为:#define EXIT_SUCCESS 0。
检查函数的返回值
函数通常会返回一个计算结果的值,或者表示函数是否成功完成了它的任务。例如,我们在 "Hello, world!"程序中使用的puts函数需要打印字符串并返回一个int类型的值。如果发生写入错误,puts函数返回宏EOF的值(一个负整数);否则,它返回一个非负的整数值。
尽管对于我们的简单程序来说,puts函数不太可能失败并返回EOF,但这是可能的。因为对puts的调用可能会失败并返回EOF,这意味着你的第一个C程序有bug,或者,可以按以下方法改进。
#include <stdio.h>
#include <stdlib.h>
int main(void) {
if (puts("Hello, world!") == EOF) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
注意(puts("Hello, world!")一定要有括号,否则编译会报错:
c$ cc hello2.c
hello2.c: In function ‘main’:
hello2.c:5:5: error: expected ‘(’ before ‘puts’
5 | if puts("Hello, world!") == EOF {
| ^~~~
| (
格式化的输出
puts函数是一种将字符串写入stdout的简单好方法,但最终你会需要使用printf函数来打印格式化的输出--例如,打印字符串以外的参数。printf函数接收一个定义了输出格式的格式化字符串,然后是可变数量的参数,这些参数是你想打印的实际数值。例如,如果你想用printf函数来打印Hello, world!,你可以这样写。
printf("%s\n", "Hello, world!")。
第一个参数是格式字符串"%s\n"。%s是一个转换规范,指示printf函数读取第二个参数(一个字符串字面)并将其打印到stdout。\n是一个字母转义序列,用于表示非图形字符,并告诉函数在该字符串后面包括新行。
注意不要将用户提供的数据作为第一个参数的一部分传递给printf函数,因为这样做会导致格式化输出的安全漏洞(Seacord 2013)。
输出字符串的最简单方法是使用puts函数,如前所述。如果你在修订版的 "Hello, world!"程序中真的使用printf而不是puts,你会发现它不再有效,因为printf函数返回与puts不同。printf函数如果成功返回打印的字符数,如果发生输出或编码错误,则返回一个负值。
编辑器和集成开发环境
可以使用各种编辑器和集成开发环境来开发你的C语言程序。图1-1显示了最常用的编辑器,根据2018年JetBrains的调查。
对于Microsoft Windows,Microsoft的Visual Studio IDE(https://visualstudio.microsoft.com/)是不错的选择。Visual Studio有三个版本。社区版、专业版和企业版。社区版的优点是免费,而其他版本的功能则需要付费。
对于Linux来说,Vim、Emacs、Visual Studio Code和Eclipse都可选择。Vim是许多开发者和高级用户的首选编辑器。它是一个基于vi编辑器的文本编辑器,由Bill Joy在1970年代为Unix的一个版本编写。它继承了vi的按键绑定,但也增加了原vi所缺少的功能和可扩展性。你可以选择安装Vim插件,如YouCompleteMe(https://github.com/Valloric/YouCompleteMe/)或deoplete(https://github.com/Shougo/deoplete.nvim/),为C语言编程提供本地语义完成。
GNU Emacs是可扩展的、可定制的、免费的文本编辑器。它的核心是Emacs Lisp的解释器,这是一种Lisp编程语言的方言,具有支持文本编辑的扩展功能--尽管我从未发现这是个问题。
Visual Studio Code(VS Code)是精简的代码编辑器,支持开发操作,如调试、任务运行和版本控制。它提供了开发人员所需的工具,以实现快速的代码构建--调试循环。VS Code可以在macOS、Linux和Windows上运行,对私人或商业使用都是免费的。
编译器
现在有很多C语言编译器,他么编译器实现了不同版本的C标准。许多用于嵌入式系统的编译器只支持C89/C90。用于Linux和Windows的流行编译器更努力地支持现代版本的C标准,直到并包括对C2x的支持。
- GNU 编译器集
GNU编译器集合(GCC)包括C、C++和Objective-C以及其他语言的前台(https://gcc.gnu.org/)。GCC的开发在GCC指导委员会的指导下遵循明确的开发计划。
GCC已经被采纳为Linux系统的标准编译器,尽管也有用于微软Windows、macOS和其他平台的版本。在Linux上安装GCC很容易。例如,下面的命令在Ubuntu上安装GCC 8。
$ sudo apt-get install gcc-9
$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ sudo dnf install gcc # Fedora
- Clang
另一个流行的编译器是Clang(https://clang.llvm.org/)。在Linux上安装Clang也很容易。例如,下面的命令应该在Ubuntu上安装Clang。
$ sudo apt-get install clang
你可以用下面的命令测试你所使用的Clang的版本。
% clang --version
$ clang --version
clang version 10.0.0-4ubuntu1
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
- 微软Visual Studio
Windows最流行的开发环境是Microsoft Visual Studio,它包括IDE和编译器。它与Visual C++ 2019捆绑在一起,其中包括C和C++编译器。
你可以在项目属性页上为Visual Studio设置选项。在C/C++下的高级选项卡上,确保你通过使用编译为C代码(/TC)选项而不是编译为C++代码(/TP)选项来编译为C代码。默认情况下,当你命名一个以.c为扩展名的文件时,它是用/TC编译的。如果文件被命名为.cpp、.cxx或其他一些扩展名,则用/TP编译。
移植性
每个C语言编译器的实现都至少有一点不同。编译器不断发展,因此,例如,像GCC这样的编译器可能提供对C17的完全支持,但正在努力实现对C2x的支持,在这种情况下,它可能有一些C2x的功能实现,但没有其他。因此,编译器支持全部的C标准版本(包括中间的版本)。C语言实现的总体发展是缓慢的,许多编译器明显落后于C标准。
如果为C语言编写的程序只使用标准中规定的语言和库的那些功能,就可以认为是严格符合标准的。这些程序的目的是为了最大限度地提高可移植性。然而,由于实现行为的范围,现实世界中没有一个C语言程序是严格符合要求的,也不会是(可能也不应该是)。相反,C标准允许你编写符合要求的程序,这些程序可能依赖于非可移植的语言和库特性。
通常的做法是为一个参考实现编写代码,或者有时为几个实现编写代码,这取决于你打算在哪个平台上部署你的代码。C标准是确保这些实现不会有太大的差异,并允许你一次针对几个实现,而不必每次都学习一种新的语言。
在C标准文件的附件J中列举了五种可移植性问题。
-
实现定义的行为
-
未指定的行为
-
未定义的行为
-
针对本地的行为
-
常见的扩展
-
实现定义的行为
实现定义的行为是指C语言标准中没有规定的程序行为,它可能在不同的实现中提供不同的结果,但在一个实现中具有一致的、有记录的行为。实现定义的行为的一个例子是一个字节中的位数。
实现定义的行为大多是无害的,但在移植到不同的实现时可能会导致缺陷。在可能的情况下,避免编写依赖于实现定义的行为的代码,这些行为在你可能用来编译你的代码的C实现中是不同的。C标准的附件J.3中列举了实现定义行为的完整列表。你可以通过使用static_assert声明来记录你对这些实现定义的行为的依赖。
- 未指定的行为
未指定的行为是指标准提供了两个或多个选项的程序行为。该标准对在任何情况下选择哪个选项没有要求。每次执行一个给定的表达式可能会有不同的结果,或者产生与之前执行相同表达式不同的值。未指定行为的一个例子是函数参数存储布局,它在同一程序中的不同函数调用中可能会有所不同。避免编写依赖于C标准附件J.1中列举的非指定行为的代码。
-
未定义的行为
未定义的行为是指C标准没有定义的行为,或者说是 "在使用不可移植的或错误的程序结构或错误的数据时,标准没有规定的行为"。未定义行为的例子包括有符号的整数溢出和解读一个无效的指针值。具有未定义行为的代码往往是错误的,但比这更有细微差别。标准中对未定义行为的识别如下。 -
当违反了 "应当 "或 "不应当 "的要求,并且该要求出现在约束条件之外时,该行为是未定义的
-
当行为被明确规定为 "未定义行为 "时
-
通过省略任何明确的行为定义
前两种未定义行为经常被称为显式未定义行为,而第三种则被称为隐式未定义行为。这三者之间的重点没有区别,它们都描述了未定义的行为。C语言标准附件J.2 "未定义行为 "包含了C语言中显式未定义行为的列表。
开发者经常误认为未定义的行为是C标准中的错误或遗漏,但将行为归为未定义的决定是有意的,也是经过考虑的。C标准委员会将行为归类为未定义的行为是为了做到以下几点。
- 给予实现者许可,使其不去捕捉难以诊断的程序错误
- 避免定义晦涩难懂的案例,使之有利于一种实现策略而不是另一种策略
- 识别可能的符合要求的语言扩展领域,在这些领域中,实现者可以通过提供官方未定义行为的定义来增强语言。
这三个原因实际上是完全不同的,但都被认为是可移植性问题。编译器(实现)有做以下事情的余地。 - 完全忽略未定义的行为,产生不可预测的结果
- 以环境特征的文件方式行事(有或没有发出诊断书)。
- 终止翻译或执行(发出诊断)。
这些选项都不是很好(尤其是第一个),所以最好避免未定义的行为,除非实现指定这些行为的定义是为了让你调用一个语言增强功能。
- 特定于本地的行为和通用扩展
特定于本地的行为取决于每个实现所记录的国籍、文化和语言的本地惯例。通用扩展在许多系统中被广泛使用,但并不能移植到所有的实现中。
小结
在这章中,你学会了如何编写简单的C语言程序,编译它,并运行它。然后,我们看了几个编辑器和交互式开发环境,以及一些编译器,你可以用它们来开发Windows、Linux和macOS系统上的C语言程序。一般来说,你应该使用较新版本的编译器和其他工具,因为它们往往支持C编程语言的较新功能,并提供更好的诊断和优化。如果较新版本的编译器破坏了你现有的代码,或者你正准备部署你的代码,你可能不想使用较新版本的编译器,以避免在你已经测试过的应用程序中引入不必要的变化。在本章的最后,我们讨论了C语言程序的可移植性。