我是Linking的初学者,如果我的问题太基础了,抱歉。可以说我有两个.c文件
file1.c是
int main(int argc, char *argv[])
{
int a = function2();
return 0;
}
file2.c是
int function2()
{
return 2018;
}
我知道这是标准,创建一个 file2.h 并将其包含在file1.c中,我有一些疑问:
Q1。 #include在file1.c中并没有太大的改变或改善,我仍然可以在没有file2.h的情况下正确编译file1.c,编译器会警告我'隐式声明函数'function2 ”,但此警告有很大帮助吗?程序员可能知道在其他.c文件中定义了function2(如果您使用function2但未定义它,那么您当然知道定义在其他位置),并且链接程序将执行其工作以生成最终的可执行文件?因此,对我来说包含file2,c的唯一目的是,在编译过程中不显示任何警告,这是我的理解正确的。
Q2。在这种情况下,程序员在file1.c中定义了function2,直到链接器抛出错误,他才知道他的function2与file2.c中的那个冲突(很明显,他可以正确地单独编译file1.c。但是如果我们希望他在编译file1.c时知道他的错误,添加 file2.h 仍然无济于事,那么添加头文件的目的是什么?
Q3。我们应该添加些什么,以使程序员知道他应该为function2选择一个不同的名称,而不是在最后阶段由链接器告知错误。
答案 0 :(得分:1)
每个C89 3.3.2.2 Function calls重点:
如果函数调用中带括号的参数列表之前的表达式仅包含一个标识符,并且如果该标识符没有可见的声明,则该标识符隐含完全像在包含函数调用的最里面的块中那样声明:
extern int identifier();
出现
现在,请记住,空的参数列表(在()
括号内没有声明)声明了一个函数,该函数采用 unspecified 类型和数量的参数。在括号内输入void
,以声明函数不接受任何参数,例如int func(void)
。
第一季度:
此警告有很大帮助吗?
是,不是。这是一个主观的问题。它可以帮助使用它的人。作为个人说明,请始终将此警告视为错误。使用gcc编译器使用-Werror=implicit-function-declaration
。但是您也可以忽略此警告,并制作最简单的main() { printf("hello world!\n"); }
程序。
链接器将完成最终可执行文件的工作吗?因此,对我来说包含file2,c的唯一目的是,在编译过程中不显示任何警告,这是我的理解正确的。
不。如果使用不同/不兼容的指针类型调用该函数。它调用未定义的行为。如果将函数声明为void (*function2(void))(int a);
,则调用((int(*)())function2)()
就是UB,就像调用function2()
一样,无需事先声明。根据附件J.2(资料性):
在以下情况下,行为是不确定的:
使用指针来调用类型与指向的类型(6.3.2.3)不兼容的函数。
和每个C11 6.3.2.3p8:
指向一种类型的函数的指针可以转换为指向另一种类型的函数的指针,然后再返回;结果应等于原始指针。如果使用转换后的指针来调用类型与所引用类型不兼容的函数,则该行为是不确定的。
因此,在您幸运的情况下,int function2()
确实可行。例如,它也可以用于atoi()
函数。但是调用atol()
将调用未定义的行为。
第二季度:
链接器抛出错误
这应该发生,但实际上取决于链接器。如果您在gcc编译器中使用一个阶段编译所有源代码,则会抛出错误。但是,如果创建静态库,然后使用不带-Wl,-whole-archive
的gcc编译器链接它们,则它将选择第一个声明为sees,请参见this thread。
添加头文件的目的是什么?
我想简单和有秩序。这是在开发人员和库之间共享数据结构(枚举,结构,类型定义)和声明(函数和变量类型)的便捷且标准的方法。还可以共享预处理程序指令。图像您正在编写一个包含1000多个文件的大型库,它将与100多个其他库一起使用。在每个文件的开头,您会写struct mydata_s { int member1; int member2; ... }; int printf(const char*, ...); int scanf(const char *, ...); etc.
还是只写#include "mydata.h"
和#include <stdio.h>
?如果需要更改mydata_s
结构,则需要更改项目中的所有文件,并且使用该库的所有其他开发人员也需要更改定义。我并不是说您无法做到这一点,但是这样做会做更多的工作,而且没人会使用您的图书馆。
第三季度:
我们应该添加些什么,以使程序员知道他应该为function2选择一个不同的名称,而不是在最后阶段由链接程序告知错误。
在名称冲突的情况下,链接器将(希望)告知您它找到两个具有相同名称的标识符。您将需要创建一个工具来检查源代码。我不知道为什么需要这样做,链接器专门用于解析符号,因此可以自然地处理存在两个具有相同标识符的符号的情况。
答案 1 :(得分:1)
简短回答:
带走:编译器发出警报越早越好。
Q1:.h的含义:一致性和早期警报。提早就常见错误方法发出警报,可以提高代码的可靠性,并减少调试和生产崩溃的次数。
第二季度:名称冲突会给开发人员带来早期警报,通常更容易解决。
Q3:早期重复定义警报未纳入C标准。
锻炼: 1.在一个文件中定义一个函数,将printf(“%d \ n”,i)的参数设为int,然后在另一个文件中以42.0的浮点数调用该函数。 2.致电(double)42.0。 3.使用在%.s下打印的char * str参数定义函数,然后使用int参数调用。
更长的答案:
常用约定:在典型用法中,.h文件的名称是从.c文件或与之关联的文件中派生的。 file.h和file.c。对于具有许多定义的.h文件,例如string.h,请从更高的角度(如str ...函数中)得出文件名。
我的大法则:最好始终对代码进行结构化,以便编译器可以在编译时立即警告错误,而不是让编译器在调试或运行时依赖正确的查找方式来运行。运行时错误可能非常难以诊断,尤其是在程序生产后很长的时间内出现,并且维护成本高昂并降低了您的客户体验。请参阅“ yoda表示法”。
Q1:.h的含义:一致性和早期警报以及提高的代码可靠性。
C .h文件允许在不同时间编译的.c文件的开发人员共享公共声明。没有重复的代码。 .h文件还允许从所有文件中一致地调用函数,同时识别不正确的参数签名(参数计数,不良冲突等)。具有.c文件来定义函数也#include .h文件有助于确保定义中的参数与调用一致。这听起来可能很基础,但是如果没有它,签名冲突的所有人为错误都可以溜走。
仅当所有调用者的参数签名与定义中的参数签名完全匹配时,才忽略.h文件。通常不是这种情况,因此,如果没有.h文件,则任何冲突的签名都会产生错误的数字,除非在调用文件中还存在并行的外部字符(错误,错误,错误)。像int vs float这样的事情会产生非常错误的参数值。错误的指针会产生段错误和其他完全崩溃。
优点:.h文件中使用externs时,编译器可以将不匹配的参数正确地转换为正确的类型,以确保更好的调用。虽然您仍然可以修改参数,但可能性很小。它还可以帮助避免出现不匹配的情况,但这种情况不适用于一种实现方式。
隐式声明警告对我很有帮助,因为它们通常表示我忘记了.h文件或将名称的外部名称拼写错误。
Q2:名称冲突。早期警报。
名称冲突是不好的,避免问题是开发人员的责任。 C ++使用名称空间解决了该问题,而C作为一种较低级的语言却没有。
.h文件的使用可以使编译器诊断程序提醒开发人员游戏早期出现冲突的地方。如果编译器诊断程序不执行此操作,则希望链接程序将对多定义符号错误执行此操作,但这不是标准所保证的。
伪造名称空间的一种常见方法是在.h中以某些前缀(extern int filex_function1(int arg,char * string)或#define FILEX_DEF 42)开始所有潜在的冲突定义。
如果使用的两个不同的外部库共享相同的名称,该怎么办呢?
Q3:早期重复警报。抱歉,早期警报取决于实施情况。
这对于C标准很难定义。由于C是一门古老的语言,因此有许多创造性的方式来编写和存储C程序。 在使用名称之前寻找冲突的名称取决于开发人员。诸如交叉引用程序之类的工具可以提供帮助。甚至像vim或emacs关联的ctags这样的愚蠢东西也可以提供帮助。
答案 2 :(得分:1)
您误解了头文件和函数原型的使用。
头文件才能在多个代码文件之间共享公共信息。这些信息包括宏定义,数据类型以及可能的函数原型。
函数原型对于编译器来说是必需的,以便正确处理返回数据类型,并为您提供滥用函数返回类型和参数的早期警告。
功能原型可以在头文件中声明,也可以在使用它们的文件中声明(更多输入)。
您有一个非常简单的示例,只有2个文件。现在想象一下一个包含数百个文件和数千个功能的项目。您将因链接器错误而迷路。
由于传统原因,'c'允许您使用未声明的函数。在这种情况下,假定该函数的返回类型为'int'。但是,现代数据类型比早期具有更大的可靠性。该函数可以返回指针,64位数据,结构。表示必须使用原型,否则将无济于事。编译器必须知道如何正确处理函数返回。
此外,它还会向您发出有关参数类型使用不正确的警告。由于过时,这些仍然是警告,但在早期c ++中已得到解决,并转换为错误。
这些警告为您提供了早期调试功能。在某些情况下,类型不匹配警告可以为您节省几天的调试时间。
因此,在您的示例中,您不需要头文件。您可以使用“ extern”语法在“ main”文件中对该函数进行原型设计。您甚至可以不用原型制作。但是,在现实的现代编程世界中,您不能允许后者。特别是当您在团队中工作或希望您的程序可维护时。
将功能原型存储在头文件中是个好主意。这将是一个很好的文档来源,尤其是带有很好的注释。顺便说一句,函数名称必须有意义才能保持可维护性。
答案 3 :(得分:-1)
Q1。是。 C是一种低级语言,在历史上曾被用来将低级结构绑定到高级概念中。例如,传统上,标签_end位于程序的最后一个地址。标签是无类型的,但是您可以将其声明为任何方便的类型。一种“正确键入”的语言会使这种滥用变得困难。
Q2。按照约定,file1.c和file2.c都将包含file2.h;一个作为消费者,另一个作为生产者。遵循这个简单的习惯用法将捕获声明错误与定义错误;再次重申,不一定要执行“警告”。
Q3。许多软件组织采用“警告是错误”规则来对程序员进行社交控制。