C语言基础(3)—使用多个源文件

数据类型

简明类型指南

char
字符在计算机的存储器中以字符编码的形式保存,字符编码是一个数字,因此在计算机看来,A与数字65完全一样。
int
如果你要保存一个整数,通常可以使用int。不同计算机中int的大小不同,但至少应该有16位。一般而言,int可以保存几万以内的数字。
long
但如果想保存一个很大的计数呢?long数据类型就是为此而生的。在某些计算机中,long的大小是int的两倍,所以可以保存几十亿以内的数字;但大部分计算机的long和int一样大,因为在这些计算机中int本身就很大。long至少应该有32位。
float
float是保存浮点数的基本数据类型。平时你会碰到很多浮点数,比如一杯香橙摩卡冰乐有多少毫升,就可以用float保存。
double
但如果想表示很精确的浮点数呢?如果想让计算结果精确到小数点以后很多位,可以使用double。double比float多占一倍空间,可以保存更大、更精确的数字。

tips

赋值时要保证值的类型与保存它的变量类型相匹配。不同数据类型的大小不同,千万别让值的大小超过变量。short比int的空间小,int又比long小。在这里插入图片描述
如果我们尝试用小杯盛大物,那么通常就会发生保存的数字改变,且通常变为负数,为什么把一个很大的数保存到short中会变成负数?数字以二进制保存,二进制的100 000看起来像这样:
x <- 0001 1000 0110 1010 0000
当计算机想把这个值保存到short时,发现只能保存2个字节,所以只保存了数字右半边:
y <- 1000 0110 1010 0000
最高位是1的二进制有符号数会被当成负数处理,它等价于下面的十进制数:
-31072

强制类型转换

在我们使用多个源文件时,难免会遇到相除的情况,计算机在两个整数进行相除时只会保留整数位,而抹去小数位,此时若我们想到得到确切准确的数字,就需要我们用到强制类型转换,(float)会把int值转换为float值,计算时就可以把变量当成浮点数来用。事实上,如果编译器发现有整数在加、减、乘、除浮点数,会自动替
你完成转换,因此可以减少代码中显式类型转换的次数:float z = (float)x / y;
当然我们若在数据类型原有的基础上加上一些关键字,就可以改变他原有的意义了,比如unsigned,用unsigned修饰的数值只能是非负数。由于无需记录
负数,无符号数有更多的位可以使用,因此它可以保存更大的数。unsigned int可以保存0到最大值的数。这个最大值是i nt可以保存最大值的两倍左右。还有
signed关键字,但你几乎从没见过,因为所有数据类型默认都是有符号的。
unsigned char c;
long
没错,你可以在数据类型前加long,让它变长。long int是加长版的int;long int可以保存范围更广的数字;long long比long更长;还可以对浮点数用long。
long double d;

编写顺序

在了解一些基本知识后,我们可以了解一些深入的内容了,看下面的程序,如果我们直接编译会出现错误,错误的原因则就在于顺序。

#include <stdio.h>
float total = 0.0;
short count = 0;
/* 6%,比我的经纪人拿的少多了…… */
short tax_percent = 6;
int main()
{
 /* 嘿,我将和梁朝伟联袂出演一部电影 */
 float val;
 printf("Price of item: ");
 while (scanf("%f", &val) == 1) {
 printf("Total so far: %.2f\n", add_with_tax(val));
 printf("Price of item: ");
 }
 printf("\nFinal total: %.2f\n", total);
 printf("Number of items: %hi\n", count);
 return 0;
}
float add_with_tax(float f)
{
 float tax_rate = 1 + tax_percent / 100.0;
 /* 小费呢?口头传授也是要收费的 */
 total = total + (f * tax_rate);
 count = count + 1;
 return total;
}

当编译器看到这行代码时printf("Total so far: %.2f\n", add_with_tax(val));
他看到了一个不认识的函数调用。编译器没有报错,而是认为它会在之后的源文件中找到这个函数的详细信息。编译器先记下,随后会在文件中查找函数,很不幸,问题就出在这个地方……
编译器需要知道函数的返回类型,当然了,编译器现在还不知道函数的返回类型,所以只好假设它返回int。
等编译器看到实际的函数,返回了“conflicting types for ‘add_with_tax’”错误。
因为编译器认为有两个同名的函数,一个是文件中的函数,一个是编译器假设返回int的那个。
最简单的解决方法就是你只要把函数放回正确的位置,在main()调用它之前先定义。改变函数的顺序,编译器就不用假设未知函数的返回类型,因为这样的假设通常很危险。但如果总是以特定的顺序定义函数,会带来很多后遗症。而且在某些场合里,比如说递归函数,二者是互相调用的,这个时候就没有正确的顺序了,所以我们需要更加自由地定义函数。

解决方法

声明和定义分离就是最好的解决方法,如果编译器一开始就知道函数的返回类型,就不用稍后再找了。为了防止编译器假设函数的返回类型,你可以显式地告诉它。告诉编译器函数会返回什么类型的语句就叫函数声明
声明只是一个函数签名:一条包含函数名、形参类型与返回类型的记录。一旦声明了函数,编译器就不需要假设,完全可以先调用函数,再定义函数。如果代码中有很多函数,你又不想管它们在文件中的顺序,可以在代码的开头列出函数声明:

float do_something_fantastic();
double awesomeness_2_dot_0();
int stinky_pete();
char make_maguerita(int count);

甚至可以把这些声明拿到代码外,放到一个头文件中。

创建头文件

创建自己的头文件很简单,基本步骤:

  1. 创建头文件:

    • 创建一个新的文件,并将其命名为具有 .h 扩展名的文件,例如 MyHeader.h
    • 将函数声明、宏定义、结构体等放入此头文件中。
  2. 添加预处理指令:

    • 为了防止头文件被多次包含,通常在头文件的开头和结尾添加预处理指令 #ifndef, #define, 和 #endif。例如:
      #ifndef MYHEADER_H
      #define MYHEADER_H
      // 头文件内容
      #endif
      
      这种做法称为“包含卫士”(include guard),可以防止因重复包含同一头文件而引起的编译错误。
  3. 在源文件中引用头文件:

    • .c.cpp 源文件中,使用 #include "MyHeader.h" 来引用你的头文件。确保头文件与源文件在同一目录下,或者在编译器的包含路径中。
  4. 定义函数和实现细节:

    • 在源文件(.c.cpp 文件)中实现头文件中声明的函数。
  5. 编译程序:

    • 当编译程序时,编译器会处理这些包含的头文件,将声明和定义联系起来,生成最终的可执行文件。

小干货: 头文件的名字用双引号括起来,而不是尖括号,它们的区别是什么?当编译器看到尖括号,就会到标准库代码所在目录查找头文件,但现在你的头文件和.c文件在同一目录下,用引号把文件名括起来,编译器就会在本地查找文件。

代码共享

你用C语言写了几个程序以后,就想从其他程序中复用某些函数或特性,这时就需要知道如何实现代码共享。
如果想让多个文件共享一组代码,自然要把共享的代码放在一个单独的.c文件中。只要编译器在编译程序时包含共享代码,就可以在多个程序中使用相同的代码了。一旦你想修改共享代码,只要修改一处就行了在这里插入图片描述

编译花絮

但现在你想给编译器一组源文件,然后说:“用这些文件创建程序。”你要怎么做呢?应该用什么gcc语法?更重要的是,对编译器来说,用多个文件创建一个可执行程序是什么意思?要知道答案,就得了解编译器是如何编译的

1.预处理:修改代码

  • 编译器要做的第一件事就是修改代码。编译器需要用#include指令添加相关头文件;编译器可能还需要跳过程序中的某些代码,或补充一些代码。改完以后就可以随时编译源代码了。

2.编译:转换成汇编代码

  • C语言看似底层,但计算机还是无法理解它。计算机只理解更低层的机器代码指令。而生成机器代码的第一步就是把C语言源代码转化为汇编语言代码,看起来像这样:
    movq -24(%rbp), %rax
    movzbl (%rax), %eax
    movl %eax, %edx

3.汇编:生成目标代码

  • 编译器需要将这些符号代码汇编成机器代码或目标代码,即CPU内部电路执行的二进制代码。

4.链接:放在一起

  • 一旦有了全部的目标代码,就需要像拼“七巧板”那样把它们拼在一起,构成可执程序。当某个目标代码的代码调用了另一个目标代码的函数时,编译器会把它们连接在一起。同时,链接还会确保程序能够调用库代码。最后,程序会写到一个可执行程序文件中,文件格式视操作系统而定,操作系统会根据文件格式把程序加载到存储器中运行。

共享变量

要想让gcc知道我们想用几个单独的源文件生成可执行程序,共享代码需要自己的头文件如果想在多个程序之间共享encrypt.c代码,需要想办法让这些程序知道它,为此你可以用头文件。比如,

  • 你可以定义一个名为encrypt.h内容是
void encrypt(char *message);

的文件,

  • 然后在encrypt.c中包含该头文件
#include "encrypt.h"
void encrypt(char *message)
{
 char c;
 while (*message) {
 *message = *message ^ 31;
 message++;
 }
}
  • 在这里使用头文件不是为了能够调整函数之间的顺序,而是为了让其他程序message_hider.c知道encrypt()函数,从而可以调用不会报错
#include <stdio.h>
#include "encrypt.h"
int main()
{
 char msg[80];
 while (fgets(msg, 80, stdin)) {
 encrypt(msg);
 printf("%s", msg);
 }
}

主程序有encrypt.h,这表示编译器知道encrypt()函数,这样才能编译代码。在链接阶段,编译器会把message_hider.c中的encrypt(msg)调用连接到encrypt.c中的encrypt()函数。

  • 最后,为了把所有东西编译到一起,只需把源文件传给gcc:
    gcc message_hider.c encrypt.c -o message_hider

非全部重编译

如果我们想要修改一两个源文件,就需要全部编译所有源文件,为程序重新编译所有源文件就是浪费,当运行下面这条命令时,它会对所有源文件以及那些没有改动过的文件分别运行预处理器、编译器和汇编器。既然源代码没有变,它们生成的目
标代码也不会变,如此就造成了时空的浪费,为避免浪费,我们就可以保存目标代码的副本
gcc reaction_control.c pitch_motor.c ... engine.c -o launch

  • 如果让编译器把生成的目标代码保存到文件中,就不需要重新生成它了,除非你修改了源代码。假如你修改了某个源文件,可以重新创建这一个文件的目标代码,然后把所有的目标文件传给编译器,让编译器把它们链接起来。

在这里插入图片描述

gcc把目标代码保存在文件并链接

1.把源代码编译为目标文件

  • 为了得到所有源文件的目标代码,可以输入以下命令:gcc -c *.c
  • *.c会匹配当前目录下所有的C源文件,-c告诉编译器你想为所有源文件创建目标文件,但不想把目标文件链接成完整的可执行程序。
    2.把目标文件链接起来
  • 这次要把目标文件的名字给编译器,而不是C源文件的名字,
    gcc *.o -o launch
  • 编译器能够识别这些文件是目标文件,而非源文件,因此它会跳过大部分编译步骤,直接把目标文件链接为一个叫launch的可执行程序。
  • 如果要修改其中一个文件,只需要重新编译这一个文件,
    然后重新链接程序即可:
gcc -c thruster.c
gcc *.o -o launch

在这里插入图片描述

make

如果你认为人工记忆文件是否修改,是一件有难度的事情,你可以学习以下make来实现自动化。你怎么知道thruster.o文件是否需要重新编译呢?只要看一下这两
个文件的时间差就行了,如果thruster.o文件比thruster.c文件旧,就需要重新创建thruster.o;否则就说明thruster.o已经是最新的了。非常简单的规则。make就是一个可以替你运行编译命令的工具。make会检查源文件和目标文件的时间差,如果目标文件过期,make就会重新编译它。

  • make编译的文件叫目标(target)。严格意义上讲,make不仅仅可以用来编译文件。目标可以是任何用其他文件生成的文件,也就是说目标可以是一批文件压缩而成的压缩文档。对每个目标,make需要知道两件事:依赖项:生成目标需要用哪些文件;生成方法:生成该文件时要用哪些指令。

依赖项和生成方法合在一起构成了一条规则。有了规则,make就知道如何生成目标。

makefile

所有目标、依赖项和生成方法的细节信息需要保存在一个叫makefile或Makefile的文件中,为了弄明白它是怎么工作的,下面假设要用一对源文件创建launch程序:在这里插入图片描述
launch程序由launch.o和thruster.o文件链接而成,这两个文件又是由相应的C文件和头文件编译而成,launch.o文件还依赖thruster.h文件,因为thruster.c需要调用thruster.h中的函数。
在这里插入图片描述
你在makefile中需要这样描述构建过程

launch.o: launch.c launch.h thruster.h
			gcc -c launch.c
thruster.o: thruster.h thruster.c
			gcc -c thruster.c
launch: launch.o thruster.o
			gcc launch.o thruster.o -o launch
  • 28
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值