内容简介:最早的 C 编程语言标准由本文将分为第一步,我们先来尝试编写一个老派的 Hello World 程序,
最早的 C 编程语言标准由 美国国家标准协会(ANSI) 在 1989 年首次发布( C89 版本 ),后于 1990 年由 国际标准化组织(ISO) 修订后发布( C90 版本 )标准,而后经历了 C99、C11 等一系列主要版本的演进,截止目前最新的版本是 2018 年 10 月发布的 C18 版本。笔者当前使用的 Linux C 编译 工具 是 2018 年 1 月 25 日释出的 GCC 7.3.0 版本,提供了 C89/C90 、C99、C11 等一系列 ISO 标准特性的支持。
本文将分为 《语法规范》 与 《应用程序》 两个姊妹篇,前者侧重于介绍 GCC、GDB 工具链的使用,以及 Linux C 各种数据类型的存储模型,并概括了函数、指针等 C 语言基本概念。后者将涉及 Linux 文件系统 IO、进程间通信、多线程、网络编程等应用程序开发方面的内容。
Hello Wrold
第一步,我们先来尝试编写一个老派的 Hello World 程序, main
函数是 Linux C 语言程序的执行入口,因此也称为 主函数 。主函数的 argc
参数为整型,用于统计程序执行时传递给主函数的命令行参数的个数。而 argv
参数是一个字符串数组,用于存放指向字符串参数的指针数组,数组中每个元素指向一个命令行输入的参数。
不同于 Python 、JavaScript 这样的脚本语言,函数内部每条语句尾部的分号 ;
都不能被省略。代码开头的 #include
预处理指令用于包含标准 IO 头文件,从而能够在后续主函数中调用 printf()
方法。另外值得注意的是,C 语言代码当中存在 /*块注释*/
和 //行注释
两种注释风格,开发人员可以按需酌情使用。
#include <stdio.h> /* 块注释 */ int main(int argc, char *argv[]) { printf("hello world!\n"); // 行注释 return 0; }
C99 标准规定主函数执行完成之后,需要显式书写 return 0;
语句表示程序正常退出,主函数返回类型的声明也需要显式的设置为 int
。
GNU 编译套件
GNU 编译器套件( GCC , GNU Compiler Collection )最初的目标是作为一款 GNU 操作系统的通用编译器,包含有 C、C++、Objective-C、Objective-C++、Fortran、Ada、 Go 、BRIG(HSAIL)等语言的前端及其相关的 libstdc++
、 libgcj
等库,目前已经移植到 Windows、Mac OS X 等商业化操作系统。GCC 编译器套件当中包含了诸多的软件包,主要的软件包如下面表格所示:
名称 | 描述 |
---|---|
cpp | C 预处理器。 |
gcc | C 编译器。 |
g++ | C++ 编译器。 |
gccbug | 用于创建 BUG 报告的 Shell 脚本。 |
gcov | 覆盖测试工具,用于分析程序需要优化的位置。 |
libgcc | GCC 运行库。 |
libstdc++ | 标准 C++库。 |
libsupc++ | C++语言支持函数库。 |
Ubuntu、Mint 等使用 deb 格式软件包的 Linux 发行版通常会默认安装 GCC 编译器,但是由于相关的软件包可能并不完整,因此可以通过如下命令安装完整的 GCC 编译环境。
sudo apt-get install build-essential
基本使用
由于当前需要编译的是 C 语言程序,因此需要使用到 gcc
软件包提供的命令,这些命令的基本使用格式如下:
gcc [-options] [filename]
将上一节编写的 Hello World 程序保存至一个 main.c
源代码文件当中,然后执行 gcc
编译命令得到可执行的 a.out
文件:
➜ gcc main.c ➜ ls a.out main.c ➜ ./a.out hello world!
如果需要指定输出的可执行文件名称,那么可以添加 -o
选项:
➜ gcc main.c -o main ➜ ls main main.c ➜ ./main hello world!
编译信息
如果需要查看编译的过程,那么可以使用 -v
命令选项:
➜ gcc -v main.c Using built-in specs. COLLECT_GCC=gcc COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper OFFLOAD_TARGET_NAMES=nvptx-none OFFLOAD_TARGET_DEFAULT=1 Target: x86_64-linux-gnu Configured with: ../src/configure -v --with-pkgversion='Ubuntu 7.3.0-27ubuntu1~18.04' --with-bugurl=file:///usr/share/doc/gcc-7/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++ --prefix=/usr --with-gcc-major-version-only --program-suffix=-7 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu Thread model: posix gcc version 7.3.0 (Ubuntu 7.3.0-27ubuntu1~18.04) COLLECT_GCC_OPTIONS='-v' '-mtune=generic' '-march=x86-64' /usr/lib/gcc/x86_64-linux-gnu/7/cc1 -quiet -v -imultiarch x86_64-linux-gnu main.c -quiet -dumpbase main.c -mtune=generic -march=x86-64 -auxbase main -version -fstack-protector-strong -Wformat -Wformat-security -o /tmp/cc9yluga.s GNU C11 (Ubuntu 7.3.0-27ubuntu1~18.04) version 7.3.0 (x86_64-linux-gnu) compiled by GNU C version 7.3.0, GMP version 6.1.2, MPFR version 4.0.1, MPC version 1.1.0, isl version isl-0.19-GMP ... ...
优化选项
GCC 编译优化等级由低到高分为 -O0
、 -O1
、 -O2
、 -O3
, o
即单词 Optimization 的首字母,不同优化等级下得到的文件体积与执行效率各不相同。此外,嵌入式开发当中还经常使用到一个 -Os
,其优化等级介于 -O2
和 -O3
之间。
➜ gcc -os main.c -o main ➜ ll 总用量 16K -rwxrwxr-x 1 hank hank 8.2K 2月 24 17:51 main -rw-rw-r-- 1 hank hank 136 2月 21 18:12 main.c
调试信息
开启优化选项后编译的代码,并不会保留任何关于调试与 debug 的信息,如果需要保留这些信息,可以开启 -g
选项( gdb ),此时得到的文件体积会增大。
➜ gcc main.c ➜ ll -rwxrwxr-x 1 hank hank 8.2K 2月 24 17:58 a.out ➜ gcc -g main.c ➜ ll -rwxrwxr-x 1 hank hank 11K 2月 24 17:58 a.out
包含头文件
Linux C 语言程序当中存在如下两种头文件的包含情况:
-
#include <head.h>
:预处理程序会在 编译系统指定的目录 当中去搜索头文件。 -
#include "head.h"
:预处理器会在当前 目标文件所在的文件夹内 搜索头文件,如果未找到则进入编译系统指定目录搜索。
GCC 当中可以通过 -I
参数( include )将指定目录添加到头文件的搜索列表当中:
➜ gcc -v main.c -I /workspace #include "..." search starts here: #include <...> search starts here: /workspace /usr/lib/gcc/x86_64-linux-gnu/7/include /usr/local/include /usr/lib/gcc/x86_64-linux-gnu/7/include-fixed /usr/include/x86_64-linux-gnu /usr/include
编译步骤
实质上从 hello.c
源代码到 hello
或 a.out
可执行文件,GCC 的编译过程大致经历了下面 4 个步骤:
- 预处理 :C 编译器对各种预处理命令进行处理,包括头文件包含、宏定义的扩展、条件编译的选择等( 使用
gcc -E
);
➜ gcc -E main.c -o main.i ➜ ls main.c main.i
- 编译 :对预处理得到的源代码文件进行翻译转换,产生由机器语言描述的 汇编文件 ( 使用
gcc -S
);
➜ gcc -S main.i ➜ ls main.c main.i main.s
- 汇编 :将汇编代码转译成为 机器码 ( 使用
gcc -c
);
➜ gcc -c main.s ➜ ls main.c main.i main.s main.o
- 链接 :将机器码中的各种符号引用与定义转换为 可执行文件 中的相应信息(如虚拟地址)。
➜ gcc main.o -o main ➜ ls main.c main.i main.o main.s main
为了便于查找,下表列出了编译和链接 C/C++ 程序时各类文件扩展名的释义:
后缀名称 | 描述内容 |
---|---|
.c |
C 语言源码,必须经过预处理。 |
.C 、 .cc 、 .cxx |
C++源代码,必须经过预处理。 |
.h |
C/C++语言源代码的头文件。 |
.i |
由 .c 文件预处理后生成。 |
.ii |
由 .C 、 .cc 、 .cxx 源码预处理后生成。 |
.s |
汇编语言文件,是 .i 文件编译后得到的中间文件。 |
.o |
目标文件,是编译过程得到的中间文件。 |
.a |
由目标文件构成的文件库,也称为 静态库 。 |
.so |
共享对象库,也称为 动态库 。 |
链接库文件
GCC 对于库文件的链接存在 动态 和 静态 两种方式:
- 静态链接方式 :使用 静态链接库 进行链接,由于包含了程序运行所需的所有库,因此生成的文件体积较大但是能够直接运行。
- 动态链接方式 :使用 动态链接库 进行链接,类似于 Windows 系统下的
.dll
文件,执行时需要依赖相应的动态链接库。
GCC 默认使用动态链接 -shared
方式,可以通过加入 -static
参数来指定使用静态链接方式。注意观察下面以不同方式编译代码后,所得到的可执行文件的体积:
➜ gcc main.c ➜ ll 总用量 16K -rwxrwxr-x 1 hank hank 8.2K 2月 22 18:25 a.out -rw-rw-r-- 1 hank hank 136 2月 21 18:12 main.c ➜ gcc main.c -static ➜ ll 总用量 832K -rwxrwxr-x 1 hank hank 825K 2月 22 18:25 a.out -rw-rw-r-- 1 hank hank 136 2月 21 18:12 main.c
创建静态链接库
静态链接库是由 GCC 在汇编阶段产生的 .o
文件构成的集合,以 .a
作为文档后缀名称,Linux 下也称存档( archive ),通常使用 ar
工具命令来进行打包管理。
➜ ls func1.c func2.c main.c ➜ gcc -c func1.c func2.c ➜ ls func1.c func1.o func2.c func2.o main.c ➜ ar -r libmain.a func1.o func2.o ar: 正在创建 libmain.a ➜ ll 总用量 36K -rw-rw-r-- 1 hank hank 84 2月 24 20:06 func1.c -rw-rw-r-- 1 hank hank 1.6K 2月 24 20:16 func1.o -rw-rw-r-- 1 hank hank 84 2月 24 20:16 func2.c -rw-rw-r-- 1 hank hank 1.6K 2月 24 20:16 func2.o -rw-rw-r-- 1 hank hank 3.3K 2月 24 20:16 libmain.a -rw-rw-r-- 1 hank hank 136 2月 21 18:12 main.c
创建动态链接库
动态链接库也称为共享对象( shared object ),通常以 .so
作为文件后缀名,由 GCC 编译器通过添加 -fpic
参数( pic 指位置独立代码,即 Position Independent Code 缩写 )方式生成,共享对象模块的每个地址( 函数调用和变量引用 )都是相对地址,允许程序在执行时动态的加载与运行。
➜ ls func1.c func2.c main.c ➜ gcc -c -fpic func1.c func2.c ➜ ls func1.c func1.o func2.c func2.o main.c ➜ gcc -shared func1.o func2.o -o libmain.so ➜ ll 总用量 28K -rw-rw-r-- 1 hank hank 84 2月 24 20:06 func1.c -rw-rw-r-- 1 hank hank 1.6K 2月 24 21:31 func1.o -rw-rw-r-- 1 hank hank 84 2月 24 20:16 func2.c -rw-rw-r-- 1 hank hank 1.6K 2月 24 21:31 func2.o -rwxrwxr-x 1 hank hank 7.8K 2月 24 21:32 libmain.so -rw-rw-r-- 1 hank hank 136 2月 21 18:12 main.c
上面的步骤比较繁琐,可以将 汇编 和 链接 两条命令合并为一条命令,编译 C 语言代码的同时得到 .so
动态链接库文件。
➜ ls func1.c func2.c main.c ➜ gcc -fpic -shared func1.c func2.c -o libmain.so ➜ ls func1.c func2.c main.c libmain.so
指定编译规范
由于 GCC 同时支持多套 C 程序语言规范,因而编译时可以通过选项指定当前需要遵循的语言规范,具体请参考下表:
规范 | 规范 | 选项 | 补充 |
---|---|---|---|
C89 / C90 | ANSI C (X3.159-1989) 或 ISO/IEC 9899:1990 | -std=c90 |
-std=iso9899:1990 、 -ansi |
C94 / C95 | 95 年发布的 C89/C90 修正版,此次修正通常称作 AMD1 | - | -std=iso9899:199409 |
C99 | ISO/IEC 9899:1999 | -std=c99 |
-std=iso9899:1999 |
C11 | ISO/IEC 9899:2011 | -std=c11 |
-std=iso9899:2011 |
GNU C89 / C90 | 带 GNU 扩展的 C89/C90 | -std=gnu90 |
- |
GNU C99 | 带 GNU 扩展的 C99 | -std=gnu99 |
- |
GNU C11 | 带 GNU 扩展的 C11 | -std=gnu11 |
- |
例如下面代码当中,指定了 GCC 的编译过程遵循 C89/C90 规范,结果编译时提示错误信息: C++ style comments are not allowed in ISO C90
。
➜ gcc main.c -std=c90 main.c: In function ‘main’: main.c:7:36: error: C++ style comments are not allowed in ISO C90 printf("hello world!\n"); // 行注释 ^ main.c:7:36: error: (this will be reported only once per input file)
缺省情况下,GCC 默认使用的是 -std=gnu11
规范,即带携带 GNU 扩展的 C11 标准。
GNU 项目调试器
GNU 项目调试器( GDB , GNU Project Debugger )是一款可以调试 Ada、汇编、C\C++、D、Fortran、Go、Objective-C、OpenCL、Modula-2、Pascal、Rust 等多种语言的跨平台程序调试工具。为了捕获程序中的各类 Bug,GNU 项目调试器可以胜任下面 4 方面的工作:
- 启动程序,并指定能够影响其行为的任意内容。
- 在指定条件下停止程序的执行。
- 当程序停止时,检查发生了什么问题。
- 通过修改程序中的内容,从而尝试修复 bug。
向 Linux 命令控制台键入 gdb
即可运行 GDB 调试程序
➜ gdb GNU gdb (Ubuntu 8.1-0ubuntu3) 8.1.0.20180409-git Copyright (C) 2018 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". Type "show configuration" for configuration details. For bug reporting instructions, please see: <http://www.gnu.org/software/gdb/bugs/>. Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word". (gdb) |
进入 GDB 之后直接输入 help
即可以获取各类型命令的使用帮助,如果需要进一步查看指定类型命令的帮助则可以键入相应的命令分类,例如 help data
。
(gdb) help List of classes of commands: aliases -- Aliases of other commands breakpoints -- Making program stop at certain points data -- Examining data files -- Specifying and examining files internals -- Maintenance commands obscure -- Obscure features running -- Running the program stack -- Examining the stack status -- Status inquiries support -- Support facilities tracepoints -- Tracing of program execution without stopping the program user-defined -- User-defined commands Type "help" followed by a class name for a list of commands in that class. Type "help all" for the list of all commands. Type "help" followed by command name for full documentation. Type "apropos word" to search for commands related to "word". Command name abbreviations are allowed if unambiguous.
如果需要退出当前的 GDB 命令行调试界面,则可以输入 quit
,下面的表格列出了 GDB 当中常用的一些命令:
命令 | 描述 | 命令 | 描述 |
---|---|---|---|
break |
设置断点, break 断点所在行号 。 |
list |
列出产生执行文件的部分源码。 |
clear |
清除断点, clear 断点所在行号 。 |
next |
执行一行源码,但是不进入函数内部。 |
delete |
清除断点和自动显示的表达式 | step |
执行一行源码,并且进入函数内部。 |
disable |
使所设断点暂时失效,多个行号可用空格分隔。 | run |
正常执行当前被调试的程序。 |
enable |
生效所设的断点,与 disable 作用相反。 |
quit |
退出当前 GDB 命令行调试。 |
run |
运行调试程序。 | watch |
监视指定变量的值。 |
countinue |
继续执行正在调试的程序。 | make |
在 GDB 重新生成可执行文件。 |
file |
装载需要调试的可执行文件。 | shell |
在 GDB 当中执行 UNIX Shell 命令。 |
kill |
终止正在调试的程序。 | file |
加载可执行的文件。 |
提示:GDB 当中即可以像 Bash 或 Z-Shell 那样使用 Tab 键命令自动补齐,也能够通过方向键上下翻阅历史命令。
debug 范例
接下来,下面例程用于打印当前执行程序的名称以及命令行执行时所携带的参数,我们将会通过它来演示 GDB 调试程序的过程。
#include <stdio.h> int main(int argc, char *argv[]) { printf("当前执行程序的名称:%s\n", argv[0]); int index; for (index = 1; index < argc; index++) { printf("执行命令时输入的第%d个参数为:%s\n", index, argv[index]); } return 0; }
(1)首先需要使用 gcc -g main.c
命令编译程序并保留调试 debug 信息:
➜ gcc -g main.c ➜ ls a.out main.c
(2)进入 GDB 然后装载需要进行调试的可执行文件:
➜ gdb ... ... (gdb) file a.out Reading symbols from a.out...done.
(3)输入 GDB 的 run
命令,执行已经装载的 bugging 文件,并在命令后跟随需要传入程序的参数。
(gdb) run 这是一个Hank的测试程序! Starting program: /workspace/c-test/a.out 这是一个Hank的测试程序! 当前执行程序的名称:/workspace/c-test/a.out 执行命令时输入的第1个参数为:这是一个Hank的测试程序! [Inferior 1 (process 7997) exited normally]
(4)通过 where
命令,查看程序运行中出现的错误堆栈:
(gdb) where No stack.
(5)使用 list
命令查看当前执行程序的源码,每次能够查看 10 行,需要查看更多可以直接回车重新执行上一次输入的命令。
(gdb) list 1 #include <stdio.h> 2 3 int main(int argc, char *argv[]) { 4 printf("当前执行程序的名称:%s\n", argv[0]); 5 int index; 6 for (index = 1; index < argc; index++) { 7 printf("执行命令时输入的第%d个参数为:%s\n", index, argv[index]); 8 } 9 return 0; 10 }(gdb)
(6)利用 break
命令在程序的第 5 行位置设置一个断点:
(gdb) break 5 Breakpoint 1 at 0x555555554674: file main.c, line 5.
(7)重新输入 run
命令,此时程序会运行到第 5 行断点位置并停止:
(gdb) run Starting program: /workspace/c-test/a.out 这是一个Hank的测试程序! 当前执行程序的名称:/workspace/c-test/a.out Breakpoint 1, main (argc=2, argv=0x7fffffffded8) at main.c:6 6 for (index = 1; index < argc; index++) {
(8)输入 next
命令,在断点位置开始单步执行:
(gdb) next 7 printf("执行命令时输入的第%d个参数为:%s\n", index, argv[index]); (gdb) next 执行命令时输入的第1个参数为:这是一个Hank的测试程序! 6 for (index = 1; index < argc; index++) {
(9)断点执行过程中,可以使用 print
命令查看程序中指定变量的当前值:
(gdb) print index $1 = 1 (gdb) print argc $2 = 2 (gdb) print argv $3 = (char **) 0x7fffffffded8
(10)当发现程序状态出现错误的原因之后,就可以使用 kill
退出当前 debug 的程序,然后 quit
离开 GDB 调试器。
(gdb) kill Kill the program being debugged? (y or n) y (gdb) quit
TUI 模式
直接通过 GDB 命令行进行 debug 工作显然比较繁琐,因此 GDB 内置的 TUI ( TextUser Interface )模式提供了一套文本 UI 界面,能够方便的显示源码、汇编、寄存器的状态。可以直接通过 gcb -tui
命令直接进入 TUI 模式,或者在进入 GDB 命令行后使用 CTRL+X+A 快捷键进入。进入 TUI 模式后,GDB 窗口划分为 源代码查看 和 GDB 命令行 两个子窗口。
┌──main.c───────────────────────────────────────────────────────────────────┐ │1 #include <stdio.h> │ │2 │ │3 int main(int argc, char *argv[]) { │ │4 printf("当前执行程序的名称:%s\n", argv[0]); │ │5 int index; │ │6 for (index = 1; index < argc; index++) { │ │7 printf("执行命令时输入的第%d个参数为:%s\n", index, argv[index]); │ │8 } │ │9 return 0; │ │10 }^? │ │11 │ │12 │ │13 │ │14 │ └───────────────────────────────────────────────────────────────────────────┘ exec No process In: L?? PC: ?? Find the GDB manual and other documentation resources online at: <http://www.gnu.org/software/gdb/documentation/>. For help, type "help". Type "apropos word" to search for commands related to "word"... Reading symbols from a.out...done. ---Type <return> to continue, or q <return> to quit---2 in /workspace/c-test/main.c (gdb) |
DDD 图形前端
DDD( Data Display Debugger ) 是一款简洁的 GDB 图形调试界面,Ubuntu 系统当中可以通过如下命令进行安装:
sudo apt-get install ddd
变量与常量
变量是一个具有名称的存储单元,编译系统会自动为 变量名 分配对应的内存地址。 C 程序中的变量都是数据的补码形式进行存储 ,程序运行时计算机会通过 变量名 查找对应的内存单元地址,然后通过该地址操作其中保存的 变量值 。
下面代码当中,声明了一个整型变量 date
,并将其赋值为 2019
。
int date = 2019;
常量在程序运行期间不允许修改其值,C99 规范允许使用 const
关键字声明一个常量,下面将声明一个常量 USER
并赋值为 Hank
( 通常约定常量名称全部大写 )。
const int USER = "Hank";
数据类型
C 语言是强类型语言,ANSI C 当中无论定义变量还是常量都需要事先声明数据类型,编译器将会根据数据类型来为变量和常量分配相应存储空间,不同数据类型具有不同的 存储长度 与 存储方式 ,C99 标准中常用的数据类型见下表:
注意:红色部分表示 C99 标准新增的特性。
整型 int
整型数据会以整数补码的方式存储,Keil C51 编译器会分配 2 个字节共 16 位空间,而 GCC 编译器则会分配 4 个字节共 32 位空间。32 位当中最左边的一位是 符号位 ,该位为 0
表示正数,为 1
则表示负数。接下来的表格展示了整型数据的存储空间以及取值范围:
数据类型 | 字节数 | 取值范围 |
---|---|---|
int 基本整型 |
4 个字节 | -2147483648 ~ 2147483647 ,即$-2^{31} \Rightarrow (2^{31}-1)$。 |
unsigned int 无符号基本整型 |
4 个字节 | 0 ~ 4294967295 ,即$0 \Rightarrow (2^{32}-1)$。 |
short 短整型 |
2 个字节 | -32768 ~ 32767 ,即$-2^{15} \Rightarrow (2^{15}-1)$。 |
unsigned short 无符号短整型 |
2 个字节 | 0 ~ 65535 ,即$0 \Rightarrow (2^{16}-1)$。 |
long 长整型 |
8 个字节 | -9223372036854775808 ~ 9223372036854775807 ,即$-2^{63} \Rightarrow (2^{63}-1)$。 |
unsigned long 无符号长整型 |
8 个字节 | 0 ~ 18446744073709551615 ,即$0 \Rightarrow (2^{64}-1)$。 |
long long 双长整型 |
8 个字节 | -9223372036854775808 ~ 9223372036854775807 ,即$-2^{63} \Rightarrow (2^{63}-1)$。 |
unsigned long long 无符号双长整型 |
8 个字节 | 0 ~ 18446744073709551615 ,即$0 \Rightarrow (2^{64}-1)$。 |
注意上面表格当中,无符号类型由于需要有 1 位来作为符号位,因此取值范围计算公式的 指数部分 需要相应的减去 1
位(例如:$2^{15}$、$2^{31}$、$-2^{63}$),而有符号类型计算公式的指数部分则与该数据类型可存储的字节数相匹配。另外,取值范围计算公式$2^{n}-1$中出现的减去 1
的情况,是由于正整数一侧的取值范围包含了 0
(*虽然数学上 0
并非正整数_),因而需要将正整数部分的取值范围相应的减掉一。
为了更加清晰的理解整型数据存储空间分配与取值范围的关系,下面的示意图展示了 短整型 short
的最大取值 32767
、 无符号短整型 unsigned short
的最大取值 65535
的存储空间占用情况:
sizeof()
并非一个函数调用,而是标准 C 语言提供的一个单目操作符;其作用是以 long unsigned int
数据类型返回当前操作数所占用存储空间的 字节 大小,因此下面例子的 printf()
语句中,格式化字符串需要使用 %ld
进行接收。
#include <stdio.h> int main() { printf("int %ld byte\n", sizeof(int)); // int 4 byte printf("unsigned int %ld byte\n", sizeof(unsigned int)); // unsigned int 4 byte printf("short %ld byte\n", sizeof(short)); // short 2 byte printf("unsigned short %ld byte\n", sizeof(unsigned short)); // unsigned short 2 byte printf("long %ld byte\n", sizeof(long)); // long 8 byte printf("unsigned long %ld byte\n", sizeof(unsigned long)); // unsigned long 8 byte printf("long long %ld byte\n", sizeof(long long)); // long long 8 byte printf("unsigned long long %ld byte\n", sizeof(unsigned long long)); // unsigned long long 8 byte return 0; }
如果需要使用 printf()
输出中文,源代码文件必须以 GB2312 编码格式保存。
值得提醒的是,基本整型 int
默认是有符号的数据类型,而无符号类型变量原则上不能存放 -3
这样的负数;使用 printf()
输出无符号整型数据的时候,格式字符串需要选择 %u
进行输出。
#include <stdio.h> int main() { int birthday= -1988; printf("%d\n", birthday); // -1988 printf("%u\n", birthday); // 4294965308 return 0; }
字符型 char
Linux C 当中字符型数据必须以单引号 'c'
进行声明,每个字符型变量只能保存 1 个 ASCII 有效字符,这是由于字符类型实际存储的是该字符的 ASCII 编码,因为 ASCII 字符集编码通常表达为一个整型数据,所以 C99 规范当中也将其视为一种整型数据。下面的表格展示了字符型占用的存储空间以及取值范围:
数据类型 | 字节数 | 取值范围 |
---|---|---|
signed char 有符号字符型 |
1 个字节 | -128 ~ 127 ,即$-2^{7} \Rightarrow (2^{7}-1)$。 |
unsigned char 无符号字符型 |
1 个字节 | 0 ~ 255 ,即$0 \Rightarrow (2^{8}-1)$。 |
#include <stdio.h> int main() { printf("char %ld byte\n", sizeof(char)); // char 1 byte printf("unsigned char %ld byte\n", sizeof(unsigned char)); // unsigned char 1 byte return 0; }
有符号字符型数据允许存储的取值范围在 -128 ~ 127
之间,但字符型的 ASCII 编码不可能为负值,因而实际只会使用到 0 ~ 127
,即最左侧符号位总是为 0
。如果将负整数直接赋值给字符型变量,操作虽然合法但并不代表一个有效字符,而仅仅保存了一个负整数值。接下来的图片展示了保存字符型变量 '1'
时的存储情况,由于字符 '1'
的 ASCII 码为 49
,因此存储器中实质保存的是数字 49
的二进制表达形式。
printf()
输出字符型数据时,格式字符串需要选择 %c
;如果格式字符串选择为 %d
,则会输入该变量的 ASCII 码表达形式。
#include <stdio.h> int main() { char test= 'h'; printf("%d\n", test); // 104 printf("%c\n", test); // h return 0; }
需要注意,关键字 char
前的 signed
或者 unsigned
是否能够缺省由具体的编译器决定,这一点与 int
等其它数据类型不同。在 GCC 编译器当中, char
缺省为 有符号类型 。
#include <stdio.h> int main() { char test = 255; // GCC默认字符型变量为有符号类型 printf("%c \n", test); // � printf("%d \n", test); // -1 return 0; }
由于 char
默认为有符号类型,所以赋值为 255
超出了有符号字符类型的表示范围,导致后面打印输出为 -1
。如果这里显式声明字符型 test
的值为无符号类型 unsigned
则能够正确的打印数值 255
。
int main() { unsigned char test = 255; // 显式声明字符型变量为无符号类型 printf("%c \n", test); // � printf("%d \n", test); // 255 return 0; }
浮点型 float
浮点型用来表示具有小数点的 实数 ,并以规范化的二进制数指数形式存放在存储单元。之所以称为浮点型,是由于实数的指数形式有多种,比如对于 3.1416
,可以表示为$3.14159 × 10^0$、$0.314159 × 10^1$、$0.0314159 × 10^2$等形式,小数点的位置可以自由进行浮动。
数据类型 | 字节数 | 有效数字 | 取值范围(绝对值) |
---|---|---|---|
float 单精度浮点型 |
4 个字节 | 6 | $0$ 以及 $(1.2×10^{-38}) \Rightarrow (3.4×10^{38})$。 |
double 双精度浮点型 |
8 个字节 | 15 | $0$ 以及 $(2.3×10^{-308}) \Rightarrow (1.7×10^{308})$。 |
long double 长双精度浮点型 |
16 个字节 | 19 | $0$ 以及 $(3.4×10^{-4932}) \Rightarrow (1.1×10^{4932})$。 |
为了保持存储结构的一致性,必须将实数转换为 规范化的指数形式 后再保存至存储单元,即小数点前数字为 0
,小数点之后第 1 位数字不为 0
,对应于前面例子 3.1416
的规范化的指数形式是$0.314159 × 10^1$,下图展示了其具体的存储结构,注意小数部分 .314159
实际是以二进制形式保存在存储单元中的。
C99 和 C11 标准并未明确定义浮点类型的指数和小数部分各占据总存储单元的多少,具体数值由各个编译器指定。同时,各个编译器对于浮点数据类型所占用的存储空间长度也有所不同,例如 long double
类型的长度在 GCC 当中被定义为 16 个字节。
#include <stdio.h> int main() { float pi_f = 3.14; double pi_b = 3.14; long double pi_ld = 3.14; printf("float %ld byte\n", sizeof(pi_f)); // float 4 byte printf("double int %ld byte\n", sizeof(pi_b)); // double int 8 byte printf("long double %ld byte\n", sizeof(pi_ld)); // long double 16 byte return 0; }
值得注意的是:Linux C 当中对于浮点类型的 常量值 默认会按照 double
类型来进行处理。
#include <stdio.h> int main() { float i = 3.14159; /* 打印一个变量占用的空间 */ printf("float is %d byte\n", sizeof(i)); // float is 4 byte /* 打印一个常量占用的空间 */ printf("double is %d byte\n", sizeof(3.14159)); // double is 8 byte return 0; }
基本数据类型转换
自动类型转换
当两个不同的基本数据类型进行算术运算时,所得结果的数据类型总是存储占用空间更大的那一个。比如下面例子中,整型的 i
(占用 4 字节存储空间)与字符类型的 c
(占用 1 字节存储空间)分别与浮点类型 f
相加(占用 4 字节存储空间)时,得到的 sizeof
结果总是 4 个字节。
#include <stdio.h> int main() { int i = 5; float f = 3.14; char c = 'H'; printf("result have %ld byte\n", sizeof(i+f)); // result have 4 byte printf("result have %ld byte\n", sizeof(c+f)); // result have 4 byte return 0; }
强制类型转换
可以利用强制类型转换将数据转换为需要的数据类型,使用格式为 (目标数据类型)表达式
,例如: (double)1
、 (double)(3+5)
都会将整型的结果转换为双精度浮点类型。
#include <stdio.h> int main() { char i = 'A'; printf("result still have %ld byte\n", sizeof((float)i)); // result still have 4 byte return 0;44 }
布尔类型 bool
C++当中存在专门的 bool
类型,但是 C89/90 当中没有提供专用的布尔类型,因此通常会在代码中使用基本整型来模拟布尔类型数据。
#include <stdio.h> #define true 1 #define false 0 typedef int bool; void main() { bool test = false; if (test != true) { printf("工作情况良好!\n"); } }
C99 规范里新增了 _Bool
关键字,用于原生定义布尔类型,该类型占用一个字节存储空间,仅拥有 0
和 1
两个取值。
#include <stdio.h> void main() { _Bool true = 0; _Bool false = 1; printf("_Bool is %ld byte\n", sizeof(_Bool)); // _Bool is 1 byte }
C99 当中由 <stdbool.h>
头文件将 _Bool
关键字重新定义为别名 bool
, 1
和 0
分别被定义为了 true
和 false
,因此引入该头文件后,可以直接使用 bool
作为声明布尔数据类型的关键字,使用 true
和 false
作为布尔类型的取值。
#include <stdio.h> #include <stdbool.h> int main(void) { bool truly = true; bool falsely = false; printf("true is %d and false is %d.\n", truly, falsely); // true is 1 and false is 0. return 0; }
进行数据类型转换的时候, GCC 会将任意非零值自动转换为 1
,也就是 true
。
#include <stdio.h> #include <stdbool.h> #include <stdlib.h> int main(void) { bool year = 1985; printf("Ops, year equals %d!\n", year); // Ops, year equals 1! return EXIT_SUCCESS; }
复数类型 complex
complex.h
是 C 标准函数库中的头文件,提供了复数算术所需要的宏定义与函数声明。
数组 array
数组是一组有序数据的集合,数组中每一个元素都是相同的数据类型,并且保存在 一个连续的存储空间 。C 语言中使用数组必须先声明其长度,以便于事先开辟一个指定大小的存储空间。例如:下图展示了一个包含有 10 个元素的数组 int a[10]
,该数组每个元素的存储空间只能用于存放整型数据。
结构体 struct
由于数组只能存放相同数据类型的数据,而结构体是一种由 不同类型数据 组成的组合型数据结构。使用时需要先声明结构体类型,再建立结构体变量。下面的 C 语言代码定义了一个 Data
结构体和 Student
结构体:
/* 声明Date结构体类型 */ struct Date { int year; int month; int day; } /* 声明Student结构体类型 */ struct Student { int age; char name[10]; char address[20]; struct Date birthday; // 声明Date结构体变量 } struct Student hank; // 声明Student结构体变量
定义结构体变量以后,系统会对其分配内存单元。结构体变量占用的内存长度是各数据类型成员所占用的长度之和,每个成员都拥有自己独立的内存单元。例如对于上面代码中定义的 Data
和 Student
结构体,声明并定义之后的内存布局如下图所示:
共用体 union
共用体可以在同一个地址开始的内存单元中存放几种不同的数据类型,正是由于占用内存的起始地址相同,所以 共用体某一时刻只能存放一个数据,而不能同时存放多个 。下面代码中,我们来声明一个叫做 Variable
的结构体变量,该变量具备存放字符型、整型、浮点型数据的能力。
union Variable { char character; // 有符号字符型,占用1个字节空间。 int integer; // 基本整型,占用2个字节空间。 float real; // 单精度浮点型,占用4个字节空间。 } Variable.integer = 100; // 将整数100存储在Variable结构体变量。
共用体变量所占用的内存长度,等于内存占用最长的那个数据类型的变量长度。因此,上面定义的 Variable
结构体变量占用的存储空间为 4
个字节,即占用内存空间最大的浮点型数据 real
的长度。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
编程珠玑(第二版)
[美] Jon Bentley / 谢君英、石朝江 / 中国电力出版社 / 2004-4 / 28.00元
《编程珠玑(第2版)》是计算机科学方面的经典名著。书的内容围绕程序设计人员面对的一系列实际问题展开。作者Jon Bentley 以其独有的洞察力和创造力,引导读者理解这些问题并学会解决方法,而这些正是程序员实际编程生涯中至关重要的。一起来看看 《编程珠玑(第二版)》 这本书的介绍吧!