C语言中容易忽视的小知识
一、在C文件中引用C++函数
- 将要使用的函数声明放在一个头文件中
- 把要被C语言调用的C++函数的声明放在
extern "C"{ ... }
语句块里 - 注意:标准C++的头文件包含不能放在
extern "C"{ ... }
语句块里
当然,要在C++文件调用C文件的函数也是可行的。
而要在C文件中引用C++函数,下面给出一个示例:
1 |
|
用 extern "C" { ... }
语句块将 C++ 函数的声明包裹起来。然后,完成C++函数的定义:
1 |
|
注意到,add()
和 sum()
函数都是C++函数,使用了很多“C++才有的”技术,但这并不妨碍C函数使用它们:
1 |
|
为什么要这么做?
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般之包括函数名。
二、宏操作——陷阱
宏是C语言中的历史产物,C++为了向前兼容C,完全继承了C语言中的预处理器和宏操作部分。目前主流的认知就是尽量不要用宏实现函数,而改用内联函数或者模板代替。因此这里也建议不要过于依赖宏定义。
宏以及处理它的预处理器最大的缺陷就是,将用宏的地方进行简单的文本替换。这样一来,宏是根本不会进入编译器的symbol table,编译器在报错时就会出现魔数情况,也不会处理可能出现的宏命名重复,此外,宏也没有变量的作用域功能,只有简单的 #ifndef
和 #undef
来控制宏的作用范围。这些还只是最经常出现的小问题。更大的问题在于括号有无或者++
、--
等情况,例如:
1 |
|
那么i和j最终等于多少呢?得到的结果又是多少呢?非常烧脑的问题。而且就算我们理清了i++
了几次,那么当宏的具体实现出现变化,结果可能会出现变化。
又比如:
1 |
|
当然,如果你一定要用宏实现,为避免用户使用不当(用户是真的不知道该用上面的哪一种),可以加一个小技巧:
1 |
|
此时无论选取那一种写法,都是正确的。
此外该技巧也可以用在其他地方:它可以避免使用goto
语句。若使用do {...} while(0)
包裹整个过程,在过程中出现错误或异常时,可以直接使用 break
跳出循环,直接来到循环外的错误处理,而不需要goto
。这在程序结构、编译优化上有很多好处。
总之,使用宏时一定要对宏有充分了解,不然会出现非常多奇怪的问题。
三、宏操作——技巧
不过,即使宏的缺点很多,而且大多宏函数都可被内联函数、模板替代,但是宏仍有存在的意义。即使是在 C++ 中,编译器也赐予了一些预定义宏:
__FILE__
:编译的文件的绝对路径;__LINE__
:当前行号;__TIME__
:当前时间;__DATE__
:当前日期。__cplusplus
:当前编译器完全遵循的 C++ 版本。C++98
和C++03
对应了199711L
,C++11
对应了201103L
,C++14
对应了201402L
这些预定义的宏是 C++ 标准规定的,此外不同版本的 C++ 也有更多的预定义宏。
其次是另一个老生常谈的作用。确保头文件不被二次 include
,从而报出重定义的错误。不过也有#pragma once
这种写法。
1 |
|
此外,还有很多有趣的用法,比如字符串连接:
1 |
|
又比如一些耍杂技的宏函数(仍建议使用内联函数):
1 |
|
在特殊情况下,使用宏可以快速定义一批变量,或者写完一整片相同形式的代码:
1 |
|
四、可变参数函数
在编写程序时,我们经常会遇到函数在编译期间接收到的参数数量不确定的情况。比如,经常使用的printf()
函数,我们可以传入任意数量的参数。那么,这一切究竟如何实现?
事实上,C语言就已经提供了这样的一个解决方案,示例代码如下:
1 |
|
add()
最后一个参数写成省略号,即三个点号(…),省略号之前的那个参数是 int
,代表了要传递的可变参数的总数。为了使用这个功能,需要 #include <stdarg.h>
,该文件提供了实现可变参数功能的函数和宏。具体步骤如下:
- 定义一个函数,最后一个参数为省略号,省略号前面可以设置自定义参数。
- 在函数定义中创建一个
va_list
类型变量,该类型是在stdarg.h
头文件中定义的。 - 使用
int
参数和va_start
宏来初始化va_list
变量为一个参数列表。宏va_start
是在stdarg.h
头文件中定义的。 - 使用
va_arg
宏和va_list
变量来访问参数列表中的每个项。 - 使用宏
va_end
来清理赋予va_list
变量的内存。
除此之外,还有一个不被用到的 va_copy
,示例如下:
1 |
|
五、setjmp 和 longjmp
C语言中,即使是goto
语句也是不能跨函数的。为了从深层次的函数调用中立刻返回错误信息(尤其是函数递归调用时),C语言添加了两个用于跨函数跳转的函数: setjmp
和 longjmp
。再说一遍,这两个函数可以实现非本地跳转,将程序控制直接从一个函数转移到另一个函数,不需要经过正常的调用——返回,比如:
1 |
|
上述程序有非常多的函数调用,且嵌套多层,设想若在 cmd_add
函数发现一个错误,需要返回 main
函数并读下一个输入行,我们就不得不将函数返回值逐层返回,操作非常麻烦。
使用 setjmp
和 longjmp
函数,可以使程序在栈上跳过若干调用帧,返回到当前函数调用路径上的某一个函数。
1 |
|
其中,sigsetjmp
和 siglongjmp
函数都是 setjmp
和 longjmp
的可以被信号处理程序使用的版本。
首先要在希望返回到的位置调用 setjmp()
,setjmp
的参数 env
的类型是 jmp_buf
。这一数据类型是某种形式的数组,用于存放在调用 longjmp()
时,用来恢复栈状态的所有信息。通常 env
为全局变量,因为需要在另一个函数中使用。然后,longjmp()
函数在出现错误时被调用,程序跳转到调用 setjmp()
的地方。来看一个具体示例:
1 |
|
当一个错误发生,longjmp
函数跳过了所有中间调用,直接返回到 switch
语句中,其中第二个函数参数 val
将成为从 setjmp
处返回的值。显然,对于一个 setjmp
可以收到多个 longjmp
的返回值,并用 switch
语句处理。
如果还是难以记住,可以将 setjmp
看作 Java 中 try
中的 catch
语句,将 longjmp
函数看作 throw
语句。
五、C中的类型
C语言中有哪些类型是被重新命名的?它们的用处是什么?本质又是什么呢?
注意:在C++中用到 size_t
或其他在C中定义的类型时,推荐使用 #include<stddef.h>
。
类型 | 实质 | 描述 |
---|---|---|
ptrdiff_t | signed int | 两个指针相减的结果的有符号整数类型 |
size_t | unsigned int | 通常用于数组的索引以及表示变量存储空间的大小 |
wchar_t | int | 宽字节字符类型,可以表示,在支持的语言环境中,指定的最大扩展字符集的所有成员 |
nullptr_tC++11 | 关键字nullptr 的类型 |
该类型表示它不是指向某个东西的指针类型,当多个重载函数接收不同的指针类型时,重载一个接收std::nullptr_t 用于处理传入null指针的情况 |
max_align_tC++11 | 实现定义 | 一种对齐要求,至少与每个标量类型一样严格(大)的类型 |
NULL | 宏 (void*) 0 or 0 | C语言中表示空指针,但在C++中仅表示0,C++中的空指针推荐使用nullptr |
六、GNC C中的Attributes
在GNC C中,可以将你要调用的函数用“特殊的东西”声明,可以帮助编译器优化你的函数,并可以帮你挑出一些更加细微的错误。
那么这些特殊的东西,就是这里介绍的关键字__attribute__
了。在声明时,它可以帮你给函数设定一个特殊的属性。这个关键字通常会带有如下几个东西:
-
noreturn
1
void err_sys(const char * fmt, ...) __attribute__((noreturn))
通常用在一些标准库函数中,例如
abort
或者exit
,意思是不能return
,GCC会得知这一点,并且GCC就会对其进行优化:不会考虑如果err_sys
出现返回值的情况。更重要的是,可以避免出现未初始化变量的警告。 -
noinline
阻止函数内联。
事实上,GCC的attribute有三大类:
- Function attributes described here
- Variable attributes described here
- Type attributes described here
这里就不一一介绍了。