- UID
- 1
- 精华
- 积分
- 76361
- 威望
- 点
- 宅币
- 个
- 贡献
- 次
- 宅之契约
- 份
- 最后登录
- 1970-1-1
- 在线时间
- 小时
|
1、如果你希望编写一个能同时被C和C++兼容的头文件,那么你就需要考虑到C和C++在语法上的区别。
为了能兼容C语言,我们在定义结构体struct的时候,我们需要这样定义:
typedef struct tag结构体名 /* 也可以不写这个“tag结构体名” */
{
成员;
}结构体名;
其中的“tag”可以换成其它的标识。之所以用typedef,是因为在C语言里面定义一个结构体变量是这样的格式:
struct 结构体名 变量名;
但是在C++里面则是这样定义的:
结构体名 变量名;
这个时候我们就需要将其统一一下,其中一种方法,就是先定义“tag结构体名”,然后用typedef将其定义为“结构体名”。这样就能做到在定义结构体的时候做到C/C++兼容。
使用宏#define也是可以起到差不多的效果的。
只不过使用#define的话,好处是你能用#ifdef、#ifndef、defined()等宏来判断是否定义了某结构体,坏处是如果出现了不同的类型被定义为相同的标识符的话,typedef会引发error来提示程序员解决这个冲突问题,但#define有时候只是丢出一个warning,然后编译器会继续编译代码,而不会停止编译。
然后就是一些防止重复包含头文件的办法,VC++的方法是在头文件开头加上这样的一句:
#pragma once
这样的话可以有效防止头文件被重复包含。但是这样也有它的缺点:
·不是所有编译器都支持这个语法。
·#pragma once是通过文件名来识别是否包含头文件的。如果你包含了C:\func.h(里面第一句就是#pragma once),然后又包含了另一个内容完全不一样的头文件D:\SDK\func.h(也有#pragma once),两个头文件名字一样,就只有一个会被包含进去,这是很愚蠢的。
所以,一个比较好的办法,就是通过宏定义来解决头文件重复包含的问题,例子如下:
#ifndef _MAIN_H_WAS_INCLUDED_ /*如果没有定义_MAIN_H_WAS_INCLUDED_*/
#define _MAIN_H_WAS_INCLUDED_ /*那就定义_MAIN_H_WAS_INCLUDED_*/
/*内容*/
#endif
2、C语言代码和C++的代码在编译后的中间文件里,编译器对符号进行的修饰是不一样的
所谓的“符号”,就是链接器在链接多个中间文件的时候用来识别多个中间文件之间的联系的。比如我在main.cpp里面定义了一个变量:
HWND g_hWnd;
那么如果我想在func.cpp里面引用main.cpp定义的g_hWnd变量,我需要声明如下:
extern HWND g_hWnd;
那么我就可以在func.cpp里面使用g_hWnd这个变量了。链接的时候,必须将main.cpp和func.cpp链接到一起,否则链接会失败。
但是变量在目标文件main.obj里面的实际符号却是_g_hWnd,多了一个下划线“_”。这是编译器给目标文件故意加上的。而在func.obj里面也引用到了_g_hWnd这个符号。两个符号一样,一个攻一个受,链接器就能将其识别并且合体。但是如果我们需要再把一个NASM汇编的文件链接进去,并且允许其它的CPP模块也能引用到NASM汇编模块定义的变量,我们就需要在NASM汇编文件里面也像这样定义(变量前面加一个“_”。因为NASM汇编的编译器通常不会对符号进行特殊的修饰)。
对于函数的符号修饰,在C语言,_cdecl风格的函数符号的变化仍然只是加上一个“_”,_stdcall风格的函数也差不多,就是先在左边加上“_”再在右边加上“@参数占用内存大小”,但在C++,因为有函数重载(面向对象的“多态”概念),符号的修饰变得复杂许多。
比如C定义的函数int Init();产生的符号是_Init,但是在C++,产生的符号就成了?_Init@@YAHXZ(不要把那个问号忽略掉,那确实是一个问号,不是乱码)。而如果你在func.c里面定义了int _stdcall GetCount(int index);那么产生的符号就是_GetCount@4。
这样新的问题又出现了,因为你在main.cpp里面声明int _stdcall GetCount(int index);的时候,虽然能通过编译,但是链接器会为main.obj去其它obj里面找?_GetCount@@YGHH@Z这个符号,而事实上我们在func.c里面定义的函数在func.obj里面的符号却是_GetCount@4,于是链接器就会打印出一行“错误”:
main.obj : error LNK2001: unresolved external symbol "int _stdcall GetCount(int)" (?_GetCount@@YGHH@Z)
解决的办法:
如果你在XXX.c(C语言的文件)定义了函数想让XXXX.cpp(C++的文件)调用那么你需要在C++文件里声明如下:
extern "C" ...
例如:
extern "C" int _stdcall GetCount(int);
而如果你在XXX.cpp(C++的文件)里面定义了函数但是又想在XXXX.c(C语言的文件)里面调用那么你需要在C里面定义如下:
extern "C++" ...
格式同上。
而如果你希望让C++的目标文件产生的符号是C语言的风格,你可以这样定义函数:
extern "C" int DrawItems()
{
return 0;
}
也就是在函数最前面加上一句extern "C"即可。注意:不是所有的编译器都支持这么玩。
3、有关无参数的函数加不加void的问题
比如有个函数如下:
void Init();
如果你在C++里面声明了这个函数,你在调用的时候,像这样写是无法通过编译的:
Init(0);
想必大家也知道原因吧。因为在C++,void Init();和void Init(void);是一样的。
但是如果你在C里面声明了这个函数:
void Init();
然后你像这样调用:
Init(0);
它通过编译了!
通过反汇编我们会注意到,调用者真的传递了一个参数(0)给Init。
而如果我们这样声明:
void Init(void);
那么Init(0)就无法通过编译了。因此,在C语言,void Init();和void Init(void);是不一样的!
所以我们写代码的时候一定要注意这个区别。好的习惯可以让你的代码更健壮。
4、变量的定义和使用
在不同的平台和编译器上,指针的长度和int的长度通常都是变化的,比如32位下的void*是4个字节的,64位的void*则是8个字节。又或者int的长度,在16位编译器下多半都是2字节,但在32位、64位编译器下多半都是4字节的。如果你不想让你的工作变成“一次性苦劳”的话,还是建议写一些靠谱的代码。
通常stdint.h在不同的平台都有这个头文件,它定义了许多固定长度的类型,比如uint8_t、int8_t、uint16_t、int16_t、uint32_t、int32_t、uint64_t和int64_t等。除了这些固定长度的类型以外,还有一类是与指针同长度的变量类型,比如size_t,用来定义“个数”、“字节数”等内容比较合适(说的是内存里存储的东西的长度)。
Windows平台,记得有几个API是有不同位数(32、64)兼容的版本的,比如SetWindowLong和SetWindowLongPtr,后者是跨位数兼容的版本。SetWindowLong的第三个参数dwNewLong是LONG类型的,但SetWindowLongPtr的则是LONG_PTR类型的。INT_PTR、UINT_PTR、LONG_PTR等,带“PTR”后缀的,都是指自身的长度和指针一样长。类似于size_t。记得在转换的时候,使用(LONG_PTR)代替(LONG)。
使用它们来定义你的变量对你的代码的重复利用是很有好处的,至少比使用int,short,char,long要好。
5、未初始化的变量。
1、未初始化的全局变量通常都在.bss段里面,值全部是0。
2、未初始化的非static局部变量的值,通常都会在建立栈帧的时候被初始化为11001100110011001100110011001100b(有多少1100取决于变量占栈的总大小),裸函数除外。非裸函数在开头都会往局部变量区域填写0xCCCCCCCC。 |
|