本文目录
- c语言中extern的意思是什么
- 使用C语言中的头文件有什么技巧和注意事项吗为什么不直接包含C文件呢
- C语言中的extern关键字是怎么用的老师说还有个与它对应的关键字,是哪个啊
- 如何混和使用c与c++编程
c语言中extern的意思是什么
extern的作用是告诉下面的程序,用它修饰的变量的定义在其它地方,编译器在编译时不用管它在哪定义的!否则编译器会找变量的定义!定义就是给变量分配空间!这个通常用在引用别人定义的变量,而别人给你的确是库文件!这时用这个办法编译时不去找定义,而是在链接时才去找定义!因为链接时,才去看库文件的情况!
使用C语言中的头文件有什么技巧和注意事项吗为什么不直接包含C文件呢
谢邀。
一般来说,C语言项目中的文件以其后缀名来看,常用的只有两种:*.c文件和 *. h 文件,后者常被称作“头文件”,因为这种类型的文件常常和 #include 关键字一起放在 *.c文件的头部, * . h 中的“h”常被认为是“header(头)”的缩写。
大型C语言项目一般都是采用多文件编程的开发方式,fun.c模块里定义了 add() 函数,想在别的模块里使用 add() 函数,只需要从 fun.c 文件 extern add() 函数即可。
关于 extern 关键字的介绍,可参考我的上一个回答。但是使用 fun.c 模块的每个文件都需要重新声明 add() 函数也是很麻烦的。而且,在C语言程序开发中,重复的代码应该尽量避免。
在C语言程序开发中使用头文件
为了不重复声明 fun.c 模块里定义的函数,有什么办法呢?答案就是使用头文件。新建 fun.h 文件,把
fun.c
模块中能够提供给外界使用的函数或变量声明在头文件里,以后要使用这些函数或者变量,只需要包含头文件就可以了。请看相关C语言代码:如果想在main.c文件中使用fun.c文件中定义的函数,只需要包含fun.h就可以了,下面是一段C语言代码例子,请看:
事实上,以后任何模块想调用fun.c中的函数,都只需包含fun.h即可。关于fun.h中内容,有几点细节需要说明
先说说为什么 #include 《stdio.h》用角括号,而#include “fun.h”用引号。对于用尖括号包含的头文件,C语言编译器会从系统的头文件目录查找,例如我的 codeblocks 的系统头文件路径:
对于引号包含的头文件,C语言编译器会首先查找包含头文件的 .c 文件所在目录,没有找到再查找系统的头文件目录。
因为我们建立的 fun.h 文件和 main.c 在同一目录,所以在 main.c 文件中包含 fun.h 头文件要用引号,如果用尖括号包含编译器就找不到 fun.h 文件了,编译就会报错。
避免头文件的重复包含
下面这几条C语言代码属于条件编译语句,意思是如果没有 define __FUN_H__ 就 define __FUN_H__ ,如果之前 define 过,#ifndef 到 #endif 的C语言代码段就不参与编译了,这样可以避免 #ifndef 到 #endif 的代码段被重复包含。
__FUN_H__ 当然也可以改成其他名字,只需要确保唯一性就可以了。那为什么需要防止重复包含呢?谁会把一个头文件包含两次呢?像上面那么明显的错误没人会犯,但有时候重复包含的错误并不是那么明显的。
在规模较大的C语言项目中头文件包含头文件的情况很常见,经常会包含四五层,这时候重复包含的问题就很难发现了。
比如在我的系统头文件录/usr/include中,errno.h包含了bits/errno.h,后者又包含了linux/errno.h,后者又包含了asm/errno.h,后者又包含了asm-generic/errno.h。另外一个问题是,就算我是重复包含了头文件,那有什么危害么?像上面的三个函数声明,在C语言程序中声明两次也没有问题,对于具有External Linkage的函数,声明任意多次也都代表同一个函数。在C语言项目中重复包含头文件有以下问题:
- 一是使预处理的速度变慢了,要处理很多本来不需要处理的头文件。
- 二是如果有互相包含的情况,预处理器就陷入死循环了(不过现在编译器都会规定一个包含层数的上限)。
- 三是头文件里有些C语言代码不允许重复出现,虽然变量和函数允许多次声明(只要不是多次定义就行),但头文件里有些C语言代码是不允许多次出现的,比如typedef类型定义和结构体Tag定义等,在一个程序文件中只允许出现一次。
还有一个问题,为什么不直接包含 .c 文件呢?
我在 main.c 文件里直接 #include “fun.c” 不更方便吗?当然,这样的C语言代码编译也能通过,可是以后要是又有一个模块需要用到 fun.c 中定义的函数呢?再包含一次 fun.c ?这样不就相当于 add() 函数有多处定义了吗?这样在程序链接阶段就会有麻烦,或者根本无法生成可执行程序。如果包含的是头文件,那无论包含多少次,add() 函数也只有一处定义,链接是不会有问题的了。
欢迎在评论区一起讨论,质疑。文章都是手打原创,每天最浅显的介绍C语言、linux等嵌入式开发,喜欢我的文章就关注一波吧,可以看到最新更新和之前的文章哦。
C语言中的extern关键字是怎么用的老师说还有个与它对应的关键字,是哪个啊
谢邀。
在回答这个问题之前,题主应该明白,实际上程序员的工作就是把一个较复杂的需求,分解成若干个较独立的模块,然后继续把每个模块分解成若干更简单的工作,并编写C语言代码逐个实现,最后再合并完成需求。
在实际的C语言程序开发中,每一个独立模块单独占用一个文件是合适的,extern 关键字正好可以方便程序员进行多文件编程。
例如在某个项目中,text.c 负责处理文本,picture.c 负责处理图片,video.c 负责处理视频。整个项目的构成一目了然,维护很方便。多文件编程
我们从实例出发,建立
fun.c
文件,接下来自定义函数的C语言代码都在此文件编写。再建立main.c
文件,main() 函数的代码编写在此文件中,文件布局如下图,请看:然后在 fun.c 文件里定义 add() 函数和全局变量 cnt,相关C语言代码如下,请看:
再在 main.c 文件里的 main() 函数中调用 add() 函数,相关C语言代码如下:
编译并执行这段C语言代码,得到如下输出结果:
虽然程序按照预期执行了,但是应该能在编译时发现警告信息:
这是因为编译器在处理 add() 函数调用时没有找到 add() 函数的原型,只能根据 add(3, 4) 函数调用“推测”隐式声明:
所幸编译器“推测”正确,因此C语言程序得以正常执行。
C语言编译器为什么需要函数原型?
这是因为编译器必须知道函数的参数类型和个数,以及返回值的类型,才能知道该生成什么样的指令。
那为什么不从函数调用的隐式声明中“推测”呢?这是因为并不是所有情况编译器都能“推测”正确的,一旦编译器“推测”错误,要么C语言程序无法编译通过,要么无法得到预期结果。
例如,add() 函数的形参都是 int 型的,实际上我们也能传入 char 型实参,这时编译器就无法获得正确的参数类型了。
对于 printf() 这种参数可变的函数,编译器就更不能确定它的参数类型了。另外,函数的返回值类型,编译器也往往无法正确获得。
既然如此,为什么编译器不自己去搜索函数的定义呢?这是因为编译器不知道去哪里找,例如我们在 main.c 里调用的 add 函数在 fun.c 里,编译器又怎么会知道呢?
extern 和 static 关键字
extern 的字面意思是“外部的”,它是C语言中的一个关键字,表示“外部符号”。你已经知道C语言编译器需要知道函数的原型,所以在
main.c
中,正确的做法应该是下面这样的,请看相关C语言代码:这样编译器就知道 add() 函数的原型了,也知道它来自于外部文件。实际上,函数声明中的 extern 也可以不写,不过不写 extern 仍然表示 add 函数是外部符号。
应该注意到 fun.c 文件里有个全局变量 cnt,我们能否在 main.c 中使用呢?答案是肯定的,使用 extern 引入定义就可以了。
引入外部变量时,extern 不能省略。extern int cnt; 不是定义变量,因此不会为 cnt 分配空间。另外,在 fun.c 中,我们可以把 cnt 初始化为 0 :
而在 main.c 中,则不能再对 cnt 做初始化,下面这种做法是非法的,编译器会报错:
如果不写 extern,意思就变了,int cnt;显然表示定义了一个变量。如果不希望外界使用本文件里定义的函数,或者变量,该怎么办呢?答案是使用 static 关键字(这个就是题目中说的“与 extern 对应的关键字”)。
以前题主可能使用过 static 来定义静态变量,它其实还表示变量或者函数属于“内部符号”(extern 则表示“外部符号”),有 static 修饰的全局变量和函数在外部文件中都是不可见的。
这时,cnt 和 add() 函数只能在 fun.c 文件中使用,在 main.c 中即使使用 extern 也是不能访问 cnt 和 add() 函数的。
可见,因为C语言有了 extern(“外部符号”) 和 static(“内部符号”) 关键字,所以我们可以在不同的文件里定义不同的模块时,就能方便的控制变量或者函数的访问范围了。
欢迎在评论区一起讨论,质疑。文章都是手打原创,每天最浅显的介绍C语言、linux等嵌入式开发,喜欢我的文章就关注一波吧,可以看到最新更新和之前的文章哦。
如何混和使用c与c++编程
在用C++的项目源码中,经常会不可避免的会看到下面的代码:
1
#ifdef __cplusplus
2
extern "C" {
3
#endif
4
5
/*...*/
6
7
#ifdef __cplusplus
8
}
9
#endif
它到底有什么用呢,你知道吗?而且这样的问题经常会出现在面试or笔试中。下面我就从以下几个方面来介绍它:
1、#ifdef _cplusplus/#endif _cplusplus及发散
2、extern "C"
2.1、extern关键字
2.2、"C"
2.3、小结extern "C"
3、C和C++互相调用 4、C和C++混合调用特别之处函数指针
3.1、C++的编译和连接
3.2、C的编译和连接
3.3、C++中调用C的代码
3.4、C中调用C++的代码
1、#ifdef _cplusplus/#endif _cplusplus及发散
在介绍extern "C"之前,我们来看下#ifdef
_cplusplus/#endif
_cplusplus的作用。很明显#ifdef/#endif、#ifndef/#endif用于条件编译,#ifdef
_cplusplus/#endif
_cplusplus——表示如果定义了宏_cplusplus,就执行#ifdef/#endif之间的语句,否则就不执行。
在这里为什么需要#ifdef _cplusplus/#endif
_cplusplus呢?因为C语言中不支持extern "C"声明,如果你明白extern
"C"的作用就知道在C中也没有必要这样做,这就是条件编译的作用!在.c文件中包含了extern "C"时会出现编译时错误。
既然说到了条件编译,我就介绍它的一个重要应用——避免重复包含头文件。还记得腾讯笔试就考过这个题目,给出类似下面的代码(下面是我最近在研究的一个开源web服务器——Mongoose的头文件mongoose.h中的一段代码):
01
#ifndef MONGOOSE_HEADER_INCLUDED
02
#define MONGOOSE_HEADER_INCLUDED
03
04
#ifdef __cplusplus
05
extern "C" {
06
#endif /* __cplusplus */
07
08
/*.................................
09
* do something here
10
*.................................
11
*/
12
13
#ifdef __cplusplus
14
}
15
#endif /* __cplusplus */
16
17
#endif /* MONGOOSE_HEADER_INCLUDED */
然后叫你说明上面宏#ifndef/#endif的作用?为了解释一个问题,我们先来看两个事实:
这个头文件mongoose.h可能在项目中被多个源文件包含(#include
"mongoose.h"),而对于一个大型项目来说,这些冗余可能导致错误,因为一个头文件包含类定义或inline函数,在一个源文件中mongoose.h可能会被#include两次(如,a.h头文件包含了mongoose.h,而在b.c文件中#include
a.h和mongoose.h)——这就会出错(在同一个源文件中一个结构体、类等被定义了两次)。
从逻辑观点和减少编译时间上,都要求去除这些冗余。然而让程序员去分析和去掉这些冗余,不仅枯燥且不太实际,最重要的是有时候又需要这种冗余来保证各个模块的独立。
为了解决这个问题,上面代码中的
#ifndef MONGOOSE_HEADER_INCLUDED
#define MONGOOSE_HEADER_INCLUDED
/*……………………………*/
#endif /* MONGOOSE_HEADER_INCLUDED */
就起作用了。如果定义了MONGOOSE_HEADER_INCLUDED,#ifndef/#endif之间的内容就被忽略掉。因此,编译时第一次看到mongoose.h头文件,它的内容会被读取且给定MONGOOSE_HEADER_INCLUDED一个值。之后再次看到mongoose.h头文件时,MONGOOSE_HEADER_INCLUDED就已经定义了,mongoose.h的内容就不会再次被读取了。
2、extern "C"
首先从字面上分析extern "C",它由两部分组成——extern关键字、"C"。下面我就从这两个方面来解读extern "C"的含义。
2.1、extern关键字
在一个项目中必须保证函数、变量、枚举等在所有的源文件中保持一致,除非你指定定义为局部的。首先来一个例子:
1
//file1.c:
2
int x=1;
3
int f(){do something here}
4
//file2.c:
5
extern int x;
6
int f();
7
void g(){x=f();}
在file2.c中g()使用的x和f()是定义在file1.c中的。extern关键字表明file2.c中x,仅仅是一个变量的声明,其并不是在定义变量x,并未为x分配内存空间。变量x在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。但是可以声明多次,且声明必须保证类型一致,如:
1
//file1.c:
2
int x=1;
3
int b=1;
4
extern c;
5
//file2.c:
6
int x;// x equals to default of int type 0
7
int f();
8
extern double b;
9
extern int c;
在这段代码中存在着这样的三个错误:
x被定义了两次
b两次被声明为不同的类型
c被声明了两次,但却没有定义
回到extern关键字,extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。
与extern对应的关键字是 static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。
2.2、"C"
典型的,一个C++程序包含其它语言编写的部分代码。类似的,C++编写的代码片段可能被使用在其它语言编写的代码中。不同语言编写的代码互相调用是困难的,甚至是同一种编写的代码但不同的编译器编译的代码。例如,不同语言和同种语言的不同实现可能会在注册变量保持参数和参数在栈上的布局,这个方面不一样。
为了使它们遵守统一规则,可以使用extern指定一个编译和连接规约。例如,声明C和C++标准库函数strcyp(),并指定它应该根据C的编译和连接规约来链接:
1
extern "C" char* strcpy(char*,const char*);
注意它与下面的声明的不同之处:
1
extern char* strcpy(char*,const char*);
下面的这个声明仅表示在连接的时候调用strcpy()。
extern "C"指令非常有用,因为C和C++的近亲关系。注意:extern "C"指令中的C,表示的一种编译和连接规约,而不是一种语言。C表示符合C语言的编译和连接规约的任何语言,如Fortran、assembler等。
还有要说明的是,extern "C"指令仅指定编译和连接规约,但不影响语义。例如在函数声明中,指定了extern "C",仍然要遵守C++的类型检测、参数转换规则。
再看下面的一个例子,为了声明一个变量而不是定义一个变量,你必须在声明时指定extern关键字,但是当你又加上了"C",它不会改变语义,但是会改变它的编译和连接方式。
如果你有很多语言要加上extern "C",你可以将它们放到extern "C"{ }中。
2.3、小结extern "C"
通过上面两节的分析,我们知道extern "C"的真实目的是实现类C和C++的混合编程。在C++源文件中的语句前面加上extern "C",表明它按照类C的编译和连接规约来编译和连接,而不是C++的编译的连接规约。这样在类C的代码中就可以调用C++的函数or变量等。(注:我在这里所说的类C,代表的是跟C语言的编译和连接方式一致的所有语言)
3、C和C++互相调用
我们既然知道extern "C"是实现的类C和C++的混合编程。下面我们就分别介绍如何在C++中调用C的代码、C中调用C++的代码。首先要明白C和C++互相调用,你得知道它们之间的编译和连接差异,及如何利用extern "C"来实现相互调用。
3.1、C++的编译和连接
C++是一个面向对象语言(虽不是纯粹的面向对象语言),它支持函数的重载,重载这个特性给我们带来了很大的便利。为了支持函数重载的这个特性,C++编译器实际上将下面这些重载函数:
1
void print(int i);
2
void print(char c);
3
void print(float f);
4
void print(char* s);
编译为:
1
_print_int
2
_print_char
3
_print_float
4
_pirnt_string
这样的函数名,来唯一标识每个函数。注:不同的编译器实现可能不一样,但是都是利用这种机制。所以当连接是调用print(3)时,它会去查找_print_int(3)这样的函数。下面说个题外话,正是因为这点,重载被认为不是多态,多态是运行时动态绑定(“一种接口多种实现”),如果硬要认为重载是多态,它顶多是编译时“多态”。
C++中的变量,编译也类似,如全局变量可能编译g_xx,类变量编译为c_xx等。连接是也是按照这种机制去查找相应的变量。
3.2、C的编译和连接
C语言中并没有重载和类这些特性,故并不像C++那样print(int
i),会被编译为_print_int,而是直接编译为_print等。因此如果直接在C++中调用C的函数会失败,因为连接是调用C中的print(3)时,它会去找_print_int(3)。因此extern
"C"的作用就体现出来了。
3.3、C++中调用C的代码
假设一个C的头文件cHeader.h中包含一个函数print(int i),为了在C++中能够调用它,必须要加上extern关键字(原因在extern关键字那节已经介绍)。它的代码如下:
1
#ifndef C_HEADER
2
#define C_HEADER
3
4
extern void print(int i);
5
6
#endif C_HEADER
相对应的实现文件为cHeader.c的代码为:
1
#include 《stdio.h》
2
#include "cHeader.h"
3
void print(int i)
4
{
5
printf("cHeader %d\n",i);
6
}
现在C++的代码文件C++.cpp中引用C中的print(int i)函数:
1
extern "C"{
2
#include "cHeader.h"
3
}
4
5
int main(int argc,char** argv)
6
{
7
print(3);
8
return 0;
9
}
执行程序输出:
3.4、C中调用C++的代码
现在换成在C中调用C++的代码,这与在C++中调用C的代码有所不同。如下在cppHeader.h头文件中定义了下面的代码:
1
#ifndef CPP_HEADER
2
#define CPP_HEADER
3
4
extern "C" void print(int i);
5
6
#endif CPP_HEADER
相应的实现文件cppHeader.cpp文件中代码如下:
1
#include "cppHeader.h"
2
3
#include 《iostream》
4
using namespace std;
5
void print(int i)
6
{
7
cout《《"cppHeader "《《i《《endl;
8
}
在C的代码文件c.c中调用print函数:
1
extern void print(int i);
2
int main(int argc,char** argv)
3
{
4
print(3);
5
return 0;
6
}
注意在C的代码文件中直接#include "cppHeader.h"头文件,编译出错。而且如果不加extern int print(int i)编译也会出错。
4、C和C++混合调用特别之处函数指针
当我们C和C++混合编程时,有时候会用一种语言定义函数指针,而在应用中将函数指针指向另一中语言定义的函数。如果C和C++共享同一中编译和连接、函数调用机制,这样做是可以的。然而,这样的通用机制,通常不然假定它存在,因此我们必须小心地确保函数以期望的方式调用。
而且当指定一个函数指针的编译和连接方式时,函数的所有类型,包括函数名、函数引入的变量也按照指定的方式编译和连接。如下例:
01
typedef int (*FT) (const void* ,const void*);//style of C++
02
03
extern "C"{
04
typedef int (*CFT) (const void*,const void*);//style of C
05
void qsort(void* p,size_t n,size_t sz,CFT cmp);//style of C
06
}
07
08
void isort(void* p,size_t n,size_t sz,FT cmp);//style of C++
09
void xsort(void* p,size_t n,size_t sz,CFT cmp);//style of C
10
11
//style of C
12
extern "C" void ysort(void* p,size_t n,size_t sz,FT cmp);
13
14
int compare(const void*,const void*);//style of C++
15
extern "C" ccomp(const void*,const void*);//style of C
16
17
void f(char* v,int sz)
18
{
19
//error,as qsort is style of C
20
//but compare is style of C++
21
qsort(v,sz,1,&compare);
22
qsort(v,sz,1,&ccomp);//ok
23
24
isort(v,sz,1,&compare);//ok
25
//error,as isort is style of C++
26
//but ccomp is style of C
27
isort(v,sz,1,&ccopm);
28
}
注意:typedef int (*FT) (const void* ,const void*),表示定义了一个函数指针的别名FT,这种函数指针指向的函数有这样的特征:返回值为int型、有两个参数,参数类型可以为任意类型的指针(因为为void*)。
最典型的函数指针的别名的例子是,信号处理函数signal,它的定义如下:
1
typedef void (*HANDLER)(int);
2
HANDLER signal(int ,HANDLER);
上面的代码定义了信函处理函数signal,它的返回值类型为HANDLER,有两个参数分别为int、HANDLER。 这样避免了要这样定义signal函数:
1
void (*signal (int ,void(*)(int) ))(int)
比较之后可以明显的体会到typedef的好处。