简明数据类型指南
char | 字符在计算机的存储器中以字符编码的形式保存,字符编码是一个数字,因此在计算机看 来, A 与数字65完全一样。65是A的ASCII码 |
int | 如果你要保存一个整数,通常可以使用 int 。不同计算机中 int 的大小不同,但至少应该有16 位。一般而言, int 可以保存几万以内的数字 |
short | 但有时你想节省一点空间,毕竟如果只想保存一个几百、几千的数字,何必用 int ?可以用 short , short 通常只有 int 的一半大小 |
long | 在某些计算机中, long 的 大小是 int 的两倍,所以可以保存几十亿以内的数字;但大部分计算机的 long 和 int 一样大, 因为在这些计算机中 int 本身就很大。 long 至少应该有32位。 |
float | float是保存浮点数的基本数据类型 。平时你会碰到很多浮点数,比如一杯香橙摩卡冰乐有多 少毫升,就可以用 float 保存 |
double | 如果想让计算结果精确到小数点以后很多位,可以使用 double 。 double 比 float 多占一倍空间,可以保存更大、更精确的数字 |
勿以小杯盛大物
赋值时要保证值的类型与保存它的变量类型相匹配。 不同数据类型的大小不同,千万别让值的大小超过变 量。 short 比 int 的空间小, int 又比 long 小。所以下面的代码是可行的。
- short x = 15;
- int y = x;
- printf("y 的值是 %i\n", y);
但是反过来,比如你想在 short 变量中保存 int 值,就 不行。
- int x = 100000;
- short y = x;
- print("y 的值是 %hi\n", y); //%hi用来格式化
有时,编译器能发现你想在小变量中保存大值, 然后给出一条警告,但大多数情况下编译器不会 发现。 这时当你运行代码,计算机无法在 short 变量中保存100 000。计算机能装多少0、1就装多 少,而最终 保存在变量 y 中的数字已面目全非。
使用类型转换把float值存进整型变量
- int x = 7;
- int y = 2;
- //float z = x / y;
- //printf("z = %f\n", z);
- float z = (float)x / (float)y;
- printf("z = %f\n", z);
因为 x 和 y 都是整型,而两个整型相除, 结果是一个舍入的整数,在这个例子中z= 3 。 如果希望两个整数 相除的结果 是浮点数,应该先把整数保存到 float 变量 中,做法就是使用类型转换临时转换数值的类型。 (float) x会把 int 值转换为 float 值,计算时就可以把变量当成浮点数来 用。 事实上,如果编译器发现有整数在加、减、乘、除浮点数,会自动替 你完成转换,因此可以减少代码中显式类型转换的次数:
- float z = (float)x / y; // y会被编译器自动转换成float
数据类型前的关键字 unsigned
用unsigned修饰的数值只能是非负数。由于无需记录 负数,无符号数有更多的位可以使用,因此它可以保 存更大的数。unsigned int可以保存0到最大值的数。 这个最大值是int可以保存最大值的两倍左右。还有 signed关键字,但你几乎从没见过,因为所有数据类型 默认都是有符号的。
- unsigned char c; // 保存0到255的数
long 你可以在数据类型前加long,让它变长。long int是加长版的int;long int可以保存范围更广的数 字; long long比long更长;还可以对浮点数用long。
- long double d;
- long long l; //注意:只有C99和C11标准支持long long。
代码示例:
- # include <stdio.h>
-
- float total = 0.0;
- short count = 0;
- short tax_percent = 6;
-
- float add_with_tax(float f) {
-
- float tax_rate = 1 + tax_percent/100.0; // 有了.0,计算就会以浮点数进行,否则表达式会返回整数
-
- total = total + (f * tax_rate);
- count = count+1;
- return total;
- }
-
- int main() {
- float val;
- printf("Price of item:");
- while(scanf("%f",&val)==1) {
- printf("Total so far: %.2f\n",add_with_tax(val)); // %.2f把浮点数格式化为小数点后两位。
- printf("Price of item:");
- }
- printf("\nFinal total: %.2f\n", total);
- printf("Number of items: %hi\n", count); %hi用来格式化short
- return 0;
- }
运行结果
聚焦数据类型大小
不同平台上数据类型的大小不同, 下面这个程 序将告诉你 int 与 float 的最大值和所占字节数。
- #include <stdio.h>
- #include <limits.h>
- #include <float.h>
- int main() {
- printf("The value of INT_MAX is %i\n", INT_MAX);
- printf("The value of INT_MIN is %i\n", INT_MIN);
- printf("An int takes %i bytes\n", sizeof(int));
- printf("The value of FLT_MAX is %f\n", FLT_MAX);
- printf("The value of FLT_MIN is %.50f\n", FLT_MIN);
- printf("A float takes %i bytes\n", sizeof(float));
-
- //如果你想知道char、 double或long的细节呢?也很简单,只要把
- //INT 和 FLT 替换成 CHAR (char) 、 DBL (double) 、 SHRT (short) 或
- //LNG (long) 即可。
-
- printf("The value of CHAR_MAX is %i\n", CHAR_MAX);
- printf("The value of CHAR_MIN is %i\n", CHAR_MIN);
- printf("A char takes %i bytes\n", sizeof(char));
-
- printf("The value of DBL_MAX is %f\n", DBL_MAX);
- printf("The value of DBL_MIN is %.50f\n", DBL_MIN);
- printf("A double takes %i bytes\n", sizeof(double));
-
- printf("The value of SHRT_MAX is %i\n", SHRT_MAX);
- printf("The value of SHRT_MIN is %i\n", SHRT_MIN);
- printf("A short takes %i bytes\n", sizeof(short));
-
- // printf("The value of LNG_MAX is %l\n", LNG_MAX);
- // printf("The value of LNG_MIN is %l\n", LNG_MIN);
- printf("A long takes %i bytes\n", sizeof(long));
-
- return 0;
- }
运行结果:
问: 为什么不同操作系统的数据类型大小不同?设成一样不是更明了? 答: 为了适应硬件,C语言在不 同的操作系统与处理器上使用不同的 数据类型大小。 C语言诞生之初还是8位机 的天下, 但现在大部分计算机都是32 位和64位的,因为C语言没有指定数 据类型的具体大小, 所以才能与时俱 进。即使新的计算机出来, C语言还 是能够很好地适应。
问: 8位、64位到底是什么意思? 答: 从技术上讲,计算机的位数 有多种含义,它既可以代表CPU指令 的长度,也可以代表CPU一次从存储 器读取数据的大小。 实际上,位数是 计算机能够处理的数值长度。
问: 这和int、double的大小有什么关系? 答: 如果一台计算机能处理32位 的数值,就会把基本数据类型(例如 int)的大小设为32位。
正确的顺序 (在不使用头文件的情况下)同一个源文件中,如果一个函数的定义放在了其调用的地方之后,一般就会报编译器错误。 因为计算机从上往下执行代码时,调用一个函数如果发现前面代码中没有的话,它并不知道该函数的返回值类型, 因此他会先假设函数返回int,但实际上函数可能返回了float ,这就造成了冲突。
在某些场景中,没有正确的顺序
如果有两个函数,它们互相调用对方,那么总有一个函数在定义前被 调用。
声明与定义分离
如果编译器一开始就知道函数的返回类型,就不用自己假设为int了 , 你可以通过 函数声明 显式地告诉编 译器函数会返回什么类型,如:
- float add_with_tax()(float f); // 声明没有函数体
如果代码中有很多函数,你又不想管它们在文件中的顺序,可以在 代码的开头列出函数声明:
- float do_something_fantastic();
- double awesomeness_2_dot_0();
- int stinky_pete();
- char make_maguerita(int count);
甚至可以把这些声明拿到代码外,放到一个头文件中。 如何创建你自己的头文件
1.创建一个扩展名为.h的文件,并把你的声 明写在里面。 2.在主代码中包含该头文件
- #include "totaller.h" // #include是预处理命令
注意:头文件的名字用双引号括起来,而不是尖括号,它们的区别 是:当编译器看到尖括号,就会到标准库代码所在目录 查找头文件,但现在你的头文件和.c文件在同一目录下,用 引号把文件名括起来,编译器就会在本地查找文件。 本地头文件也可以带目录名,但通常会把 它和C文件放在相同目录中
问: 你提到了编译器预处理,为什么编译器需要预处理? 答: 严格意义上讲,编译器只 完成编译的步骤,即把C源代码转化 为汇编语言。但宽泛地讲,编译是将 C源代码转化为可执行文件 的整个过 程,这个过程由很多阶段组成,而 gcc允许你控制这些阶段。gcc会预 处理和编译代码。
问: 什么是预处理? 答: 预处理是把C源代码转化为可 执行文件的第一个阶段。预处理会在正 式编译开始之前修改代码,创建一个新 的源文件。 拿你的代码来说,预处理会 读取头文件中的内容,插入主文件。
问: 预处理器会真的创建一个文件吗? 答: 不会,为了提高编译的效 率,编译器通常会用管道在两个阶段 之间发送数据。
问: gcc就是在哪里找到 stdio.h头文件的? 答: 是的,在类Unix操作系统 中, stdio.h 位于 /usr/include/stdio.h ; 如果在Windows中安装了MinGW编译 器, stdio.h 就很有可能在 C:\MinGW\ include\stdio.h 中。
C语言是一种很小的语言,所有的保留字都在这里(排名不分先后)。
auto | if | break |
int | case | long |
char | register | continue |
return | default | short |
do | sizeof | double |
static | else | struct |
entry | switch | extern |
typedef | float | union |
for | unsigned | goto |
while | enum | void |
const | signed | volatile |
编译器的工作原理
1.预处理:修改代码。
编译器需要用 #include 指令 添加相关头文件;编译器可能还需要跳过程序中的某些代码, 或补充 一些代码。改完以后就可以随时编译源代码了。 2.编译:转换成汇编代码。 C语言看似底层,但计算机还是无法理解它。计算机只理解更低层 的机器代码指令, 汇编语言描述了程序运行时中央处理器需要执行的指 令。 3.汇编:生成目标代码。 编译器需要将这些符号代码汇编成机器代码或目标代码,即 CPU内部电路执行的二进制代码。 4.链接:放在一起。 一旦有了全部的目标代码,就需要像拼“七巧板”那样把它们拼在 一起,构成可执行程序。当某个目标代码的代码调用了另一个目标 代码的函数时,编译器会把它们连接在一起。同时,链接还会确保 程序能够调用库代码。最后,程序会写到一个可执行程序文件中, 文件格式视操作系统而定,操作系统会根据文件格式把程序加载到 存储器中运行。
共享代码需要自己的头文件
如果想在多个程序之间共享encrypt.c代码,需要想办 法让这些程序知道它,为此你可以用头文件。 比如你想让其他c文件调用下面这个函数
- // encrypt.c 文件
-
- #include "encrypt.h"
- void encrypt(char *message) {
- char c;
- while (*message) {
- *message = *message ^ 31;
- message++;
- }
- }
先要把该函数声明到一个头文件中,并像上面那样自己include该头文件
- // encrypt.h 文件
-
- void encrypt(char *message);
接下来,在你想调用该函数的源文件中也include上面这个头文件即可。
- //message_hider.c文件
-
- #include <stdio.h>
- #include "encrypt.h" //将包含encrypt.h,这样程序就有了encrypt()函数的声明
-
- int main() {
- char msg[80];
- while(fgets(msg,80,stdin)) {
- encrypt(msg);
- printf("%s",msg);
- }
- }
为了把多个源文件编译成一个可执行文件,只需执行下面的命令: gcc message_hider.c encrypt.c -o message_hider 运行结果:
共享变量
你已经知道如何在不同的文件之间 共享函数,但如果你想共享变量呢? 为了防止两个源文件中的同名变量相 互干扰, 变量的作用域仅限于某个文 件内。如果你想共享变量,就应该 在头文件中声明, 并在你当前文件中声明该变量,并在变量名前加上 extern关键字:
- // password.h文件
- int passcode = 100;
- //message_hider.c文件
-
- #include <stdio.h>
- #include "password.h"
- extern int password; // 说明该变量是外部的(这句可不写,写了更好)
-
- int main() {
- printf("%i",password);
- }
运行结果:
不要重新编译所有文件
如果只修改了一两个源文件便为程序重新编译所有源文件就是浪费。
更有效率的做法: 首先,把源代码编译为目标文件
- // 得到单个源文件的目标文件
- 命令:gcc -c xxx.c ---->将生成对应的.o目标文件
- // 为了得到所有源文件的目标代码,可以输入以下命令:
- 命令:gcc -c *.c --->将生成 所有源文件对应的.o目标文件
然后,把目标文件链接起来
- // 链接两个目标文件成可执行文件
- 命令:gcc xxx.o yyy.o -o launch --->将生成可执行文件launch.exe(windows下)
- // 链接所有目标文件成可执行文件
- 命令:gcc *.o -o launch --->将生成可执行文件launch.exe(windows下)
这样的话,如果你只改了xxx.c文件,就只需再编译一次xxx.c,然后再链接即可,不用再编译所有源文件了。
问题: 如果只改了一个源文件,那还好,但如果你改了很 多文件,就很容易忘记哪些改过哪些没改过 。 要是有工具能自动重新编译那些修改 过的源文件就好了。
解决问题 用make工具自动化构建
make 编译的文件叫目标(target)。严格意义上讲, make 不仅 仅可以用来编译文件。目标可以是任何用其他文件生成的文件, 也就是说目标可以是一批文件压缩而成的压缩文档。 对每个目标, make 需要知道两件事: 1. 依赖项----> 生成目标需要用哪些文件。 2. 生成方法----> 生成该文件时要用哪些指令 依赖项和生成方法合在一起构成了 一条规则。有了规则, make 就知道如何生成目标。
注意:make在Windows中另有其名。 来自UNIX世界的 make在Windows 中有很多“艺名”,MinGW 版 的make叫mingw32-make,而 微软有自己的NMAKE。
用makefile向make描述代码
所有目标、依赖项和生成方法的细节信息需要保存在一 个叫makefile或Makefile的文件中。 假设由下面这几个文件生成launch.
那么makefile文件应该这么写:
注意:生成方法都必须以tab开头。如果尝试用空格缩进,就无法生成程序。
将 make 规则保存在当前目录下一个叫Makefile的文本文件中,
然后执行命令:make launch
注意:上面的是类Unix系统中的做法。
windows下用makefile命令有所不同: mingw32-make -f makefile.txt 假如makefile.txt文件中是这么写的,那么他只会执行第一条命令 gcc -c encrypt.c。
- encrypt.o: encrypt.c encrypt.h
- gcc -c encrypt.c
- launch:encrypt.o message_hider.o
- gcc encrypt.o message_hider.o -o launch
- message_hider.o:encrypt.h message_hider.c
- gcc -c message_hider.c
想要一次执行所有的命令,就把最后一条合成的命令写到最前面,如
- launch:encrypt.o message_hider.o
- gcc encrypt.o message_hider.o -o launch
- message_hider.o:encrypt.h message_hider.c
- gcc -c message_hider.c
- encrypt.o: encrypt.c encrypt.h
- gcc -c encrypt.c
这样的话他的执行次序就是:
- gcc -c encrypt.c
- gcc -c message_hider.c
- gcc encrypt.o message_hider.o -o launch
最后生成了一个launch.exe文件。
运行结果:
如果现在我只修改了encrypt.c文件,再执行命令,你发现只有修改的文件由重新编译了,这就是我们想要的效果!
问: 除了编译代码,我能用make做其他事情吗? 答: 可以,虽然make一般用来编译代码,但你也可 以用它充当命令行下的安装程序或源代码控制工具。事实 上,任何可以在命令行中执行的任务,你都可以用make 来做。 make的更多信息和功能,请浏览 GNU Make Manual : http://tinyurl.com/yczmjx
make减轻了编译文件时的痛苦,但如果你觉得它还 不够自动化,可以试一试这个叫autoconf的工具: http://www.gnu.org/software/autoconf/ autoconf可以用来生成makefile。