qq_43801388
Sailor-jade
2021-03-14 10:43
采纳率: 100%
浏览 113

求助:C++如何在一个程序中调用其他文件的函数

想实现的功能是在file2.cpp中调用file1.cpp的func1(int x,int y)函数,代码如下:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
然后运行file1.cpp没问题,运行file2.cpp就会报错:

在这里插入图片描述

有没有人能帮我看看原因o(╥﹏╥)o

  • 点赞
  • 写回答
  • 关注问题
  • 收藏
  • 邀请回答

2条回答 默认 最新

  • include_iostream_
    include_iostream_ 2021-03-14 11:21
    已采纳

    你的代码不涉及C/C++互调(这种情况涉及符号mangle,需要extern "C"),也不涉及变量导出(这种情况要头文件声明源代码定义),所以就我所知,你的问题跟extern没有任何关系。

    首先明确一点:两个文件都有main函数时尝试互调对方的函数是行不通的,会引发symbol redefined。从你的file1.cpp和file2.cpp都有int main(void),以及试图单独编译file2.cpp(这是导致符号未定义错误的关键原因)来看,你对多文件联合编译的了解应该是0(对于尝试过这个做法的工程师而言,这种错误实在过于低级,所以我倾向于认为你是新手)。因此我稍微写得细一点。

    ====一点前提结论====

    第一,引用另一个可执行文件的函数是不可能做到的,除非利用一些涉及到信息安全领域的hacker trick(很显然很容易被当成malware,而这也的确是debug和malware中才比较容易见到的做法),涉及到系统底层原理,比较复杂。正常程序不应该这样做。

    第二,由于C++的编译过程的特点,没有头文件时使用extern也能正确构建程序。但是,许多CS界大牛说过,人是第一位的,计算机是第二位的,因此,使用头文件进行声明才是常规的做法,这样做比较便于程序员查看和维护你的代码。使用extern而忽略头文件看起来很方便,但是在工程上我个人非常不推荐这么做,除了不方便维护者查看外,还有其他更重要的理由。

    ====不推荐忽略头文件使用extern的理由====

    第一,头文件是两个不同.cpp/.cc/.c编译模块之间的桥梁,使用头文件可以方便快速找到对应实现位置。IDE也依赖头文件来实现方便的查找功能,如果没有头文件,进行refactor等操作会很麻烦,而且由于缺乏header,IDE将无法通过header快速准确地定位引用,这样去refactor可能会出错。

    第二,现代工程开发讲究高效,而使用doxygen生成文档无疑成为了高效文档生成的首选。我个人喜欢忽略.cc/.cpp/.c文件,这是因为你的用户查阅文档时只需要关心头文件就好,如果你的设计导致用户不得不查看你的头文件之外的源代码,那说明这个设计耦合性过高,是不合适的设计。如果没有头文件,文档就不得不和私有方法、辅助函数等杂七杂八的函数写在一起,那样的话整个项目的代码会变得一团糟。

    第三,这么写很方便,但对于大型工程而言,无疑是坏习惯(原因我们解释过了:文档、公有接口、仅在当前文件有效的辅助函数,各种东西会完全混在一起。对于上千行的工程而言,这种代码无疑是维护者眼中的灾难。而且过上两个月,连编写者自己也会看不懂混乱的代码)。坏习惯是很难改变的,就我看到的学生而言,有很多人被GUI IDE惯坏了,根本不会用CLI,甚至觉得CLI难用。而用惯了VS的有些学生甚至学不会在脱离VS的情况下链接外部库。这些都充分说明了一点:初学者养成坏习惯是很不好的事情。

    第四,不用头文件的话,实现闭源商用会比较麻烦。如果你的商业客户听说要用你的库还得自己写extern,那这个合同十有八九谈不成(至少对面的技术员会抱怨)。

    当然,怎么写代码最终还是由你自己决定,这些只是建议和经验,并非不可违抗的铁律。

    ====C++构建大致流程====

    C++源程序的构建(build)大致可以分为以下步骤:

    1,预编译preprocess,这一步去掉注释并处理井号起始的预处理指令。

    2,编译compile,这一步将源码变成中间代码,早期常见的是汇编码。

    3,汇编assemble,把中间代码变成二进制可执行代码。到这一步时程序通常还无法执行,因为有些函数只有声明没有定义。

    4,链接link,这一步把程序可执行代码组件的各个部分拼起来。

    整个过程完全结束后,依然不代表程序可以执行。OS还会把程序装入(load),执行基础的动态链接。到这一步时,所有符号必须具备定义,而且必须只能有一次定义,否则将无法正常启动。

    符号(symbol)是C++中的标识符(identifier,主要包括函数名、变量名之类的)转化而来的。我们已经提到过,符号不可以有重复,而C++标识符却是可以重复的,这是因为中途出现了mangle过程。mangle过程一般只对函数名标识符有效,其输出的符号与函数的名称、参数值都有关,有时还附带返回值类型等信息,因此,mangle后的symbol绝对不会重名(因为会导致symbol重名的identifier一定具有相同的参数列表,这样的重载过不了编译,也就不会产生symbol)。

    ====正确的多文件联合编译手法====

    你的问题出在上述过程的第2-3-4步。正确的方法是:多个cpp文件需要分别编译,整体链接。你的做法是把file2.cpp单独编译单独链接,这当然是错误的。Dev-cpp的默认编译器是MinGW(魔改版GNU编译套装),用命令行写出来的话,正确编译流程是这样的:

    g++ file1.cpp -o file1.cpp.o -c

    g++ file2.cpp -o file2.cpp.o -c

    g++ file1.cpp.o file2.cpp.o -o main.exe

    (执行之前,你需要把file1.cpp中的main函数删除。否则会报符号重定义。)

    解释一下指令的具体含义。g++是调用编译器的指令,-c选项告知编译器只执行1至3步骤(预处理、编译、汇编),不进行汇编。我们不在这里进行汇编的原因已经说过了:多文件联合编译需要单独编译整体链接。-o选项指定输出文件名,我个人习惯输出为.cc.o和.c.o,但其实只要后缀是.o,叫什么名字影响不大。前两条指令将file1.cpp和file2.cpp分别处理到准备链接这一步,第三步把处理好的两个可执行代码文件链接在一起,输出的才是exe可执行文件。(实际上这时的可执行文件符号依然是不全的,除非你采用静态标准库链接。不过编译器一般会选择动态链接库,这意味着OS装载器会在你执行exe时再链接标准库。当然这是题外话了。)

    ===使用Dev-Cpp构建具有多个文件的项目====

    考虑到你可能不熟悉CLI,我这里写个样例工程代码作为例子。例子就采用简单的a+b问题:main.cpp调用add函数,add函数内部计算a和b两个整数的和。

    首先,在IDE中选择文件(File)->新建(New)->项目(Project),在窗口中选择空项目(Empty Project),单选框选C++ Project不要选C Project(Dev-Cpp的功能比较局限,实际上我一般用VSCode+Makefile,不用在乎C和C++谁才是重点,是可以混编的,写起代码来比较自由,不过要配置的东西比较多)。项目名称随便写什么。

    新建的项目会有一个新的文件,保存在和.dev文件同一目录下,就叫main.cpp即可。写入代码:

    #include <iostream>
    #include "add.h"
    using namespace std;
    
    int main() {
    	int a, b;
    	cin >> a >> b;
    	cout << add(a, b);
    }
    

    这是我们的主文件,只有它拥有main函数。

    再新建一个文件,IDE会提示Add new file to the current project?(将新文件加入当前项目?),选择yes。文件保存为add.h,写入代码:

    #ifndef ADD_H
    #define ADD_H 1
    // 这个1是什么值、是否存在都无所谓,但我习惯设置为1
    
    int add(int a, int b);
    
    #endif
    

    (我个人比较习惯保存为.hh后缀,不过很多普本的老教授恐怕不吃这套。估计你还是学生,保持普本老教授们的过时作风可能显得很落伍,但至少不会被扣分。)

    再新建一个文件,对同样提示仍然选yes,文件保存为add.cpp,写入代码:

    #include "add.h"
    // 这里也包含,是工程上为了避免写错而约定的做法
    // 如果实现的函数原型不小心和头文件写得不一样,IDE中能比较方便地看出问题
    
    int add(int a, int b) {
    	return a + b;
    }
    

    (如果你需要在add.cpp内cout,别忘了在这里也加上#include<iostream>和using namespace std;)

    按下编译运行,输入1 2,输出3,结果符合预期。

    点赞 2 评论
  • SoftwareTeacher
    SoftwareTeacher 2021-03-14 11:04
    点赞 1 评论

相关推荐