纠正错误
再开始正文之前,要纠正一个长久的错误认知。在刚刚入门 C 语言的时候,下载的 Microsoft Visual C++ 6.0 ,不是编译器,它的真实名字叫做集成开发环境。那么现在来看看,什么才是编译器。
编译器和链接器
打开 Microsoft Visual C++ 6.0 安装目录下的 VC98 下的 Bin 目录,里面的 CL.EXE 是编译器,LINK.EXE 是链接器。选择 CL.EXE,发现只有 64kb
一个编译器难道只有 64kb ,这么大吗?答案肯定是:不是的。CL.EXE 只是编译器的 shell,只是一个外壳而已,相当于现实世界中的管家,它只接受命令,而真正做事的是 C1.DLL,C1XX.DLL,C2.DLL。
编写 Hello World
编写 Hello World,命名为 hello.c
#include int main(){ printf("Hello world\r\n"); return 0;}
编译和链接
接下来编译 hello.c 程序,打开命令行输入 cl /c hello.c 。/c 代表只编译不链接。执行命令后成功,编译为 hello.obj 文件。
cl 命令常用的还有,/W4 /WX
W4 VC提供了很多检查级别,级别越高检查越严格,默认是 1 ,W4 是 4 级检查
WX WX的意思是将 warning(警告)转变成 error(错误)
因为代码定义的是 int main 所以必须返回整形,为了测试,所以将代码中的 return 0;改为 return 3.14;
#include int main(){ printf("Hello world\r\n"); return 3.14;}
接下来打开命令行,仍输入 cl /c hello.c,命令执行成功,成功生成 hello.obj 文件。
命令行输入 cl /c /W4 hello.c,编译仍然通过,可生成 hello.obj 文件,但是多了一处警告,warning C4244: 'return' : conversion from 'const double ' to 'int ', possible loss of data。警告 C4244,返回:从常量 double 转换到 int 可能数据丢失。
命令行输入 cl /c /W4 /WX hello.c,编译器没有通过。不但有一处警告还把警告当成了错误。
hello.c(7) : error C2220: warning treated as error - no object file generated。错误 C220:把警告当成了错误,没有目的文件生成
hello.c(7) : warning C4244: 'return' : conversion from 'const double ' to 'int ', possible loss of data。警告 C4244,返回:从常量 double 转换到 int 可能数据丢失。
为了让代码通过,把 return 3.14,进行强转
#include int main(){ printf("Hello world\r\n"); return (int)3.14;}
保存后输入命令 cl /c /W4 /WX hello.c ,因为强转 (int)3.14 是预计之中的事情,所以成功编译通过,并生成 hello.obj 文件。
那么生成的 hello.obj 文件是什么呢?用 WinHex 打开 hello.obj 来进行查看。里面是满满的的二进制文件,在右侧可以看到字符串 Hello world 。所以得出结论,编译就是将人类阅读的文本代码转换为机器能理解的 2 进制代码。
虽然编译后是 2 进制代码,但机器仍然不可以执行。所以这时要用到 link ,输入命令 link hello.obj 。成功执行后生成了 hello.exe 。所以得出结论,链接就是从指定的 obj 文件中,抽取二进制代码,数据以及其他相关所需信息,按约定的操作系统中执行文件格式打造一个符合要求的执行文件。Windows 中可执行文件格式是 PE 格式,Linux 中可执行文件格式是 ELF 格式
Hello World 的运行过程
include
从第一行 include 说起,都知道它是包含某个文件,有时候见到的是 include<>,有时候见到的是 include"",但是编译之后发现效果是一样的。其实不同点只是查找顺序不一样,<> 代表官方文件,先找环境变量,后找程序所在目录。""代表自定义文件,先在文件所在目录进行寻找,后找环境变量。以<>为例,找到环境变量,看到有一个 include
打开 include ,里面的内容为:
D:\Microsoft Visual Studio\VC98\atl\include
D:\Microsoft Visual Studio\VC98\mfc\include
D:\Microsoft Visual Studio\VC98\include
所以编译器会先在 D:\Microsoft Visual Studio\VC98\atl\include 找,有没有 stdio.h 。发现没有,继续在 D:\Microsoft Visual Studio\VC98\mfc\include 找有没有 stdio.h ,发现也没有。最终在 D:\Microsoft Visual Studio\VC98\include 找到了 stdio.h 。如果还没有找到 stdio.h 文件,编译器就会在程序所在目录进行寻找。如果还没找到程序就会报错。
打开 stdio.h ,复制下来,将 hello.c 的第一行代码 include 替换为 stdio.h 中的代码
输入命令 cl /c /W4 /WX hello.c 可成功编译,link hello.obj 也可成功链接,最后成功执行
进一步测试 include 做了什么事,新建一个 test.bmp 文件,里面的代码为:
printf("Hello world\r\n");return (int)3.14;
hello.c 的代码为:
#include int main(){ #include "test.bmp"}
输入命令 cl /c /W4 /WX hello.c 可成功编译,link hello.obj 也可成功链接,最后成功执行
输入命令 cl /c /P hello.c ,/P 参数是将预处理器输出写入文件,打开预处理后生成的文件,截取其中需要分析的部分,将 test.tmp 中的代码复制粘贴到 hello.c 中。所以 include 的作用,就是把 include 指定的文件,粘贴复制到指定位置。
int main(){ #line 1 "test.bmp"printf("Hello world\r\n");return (int)3.14;#line 6 "hello.c"}
main()
main() 叫做程序入口是不对的。举一个反例,学过 C 的都知道,完整的 hello world 应该是:
#include int main(int argc, char argv[], char *envp[]){ printf("Hello world!\r\n"); return (int)3.14;}
上面的代码,int main 有三个参数,如果 main() 是程序的入口,那三个参数又是从哪里来的呢?显然说 main() 是程序的入口是不对的
int main(int argc, char argv[], char *envp[])
为了证明 main() 是程序的入口是不对的,以代码为证,找到 \VC98\CRT\SRC 下的 CRT0.C ,打开并找到 mainCRTStartup 函数。CRT 是 C Run Time 的缩写,mainCRTStartup 也就是 main 函数运行时的启动函数,大概来读一读这个程序,首先获取操作系统的版本,然后通过位运算,获得他的主版本号和次版本号,初始化堆环境,初始化IO,取得命令行,取得环境变量,格式化命令行,格式化环境变量,初始化浮点寄存器以及初始化全局变量,根据编码方式选择对应窗口或控制台,然后才到 main 函数
void mainCRTStartup(#endif /* WPRFLAG */#endif /* _WINMAIN_ */ void ){ int mainret;#ifdef _WINMAIN_ _TUCHAR *lpszCommandLine; STARTUPINFO StartupInfo;#endif /* _WINMAIN_ */ /* * Get the full Win32 version */ _osver = GetVersion(); _winminor = (_osver >> 8) & 0x00FF ; _winmajor = _osver & 0x00FF ; _winver = (_winmajor << 8) + _winminor; _osver = (_osver >> 16) & 0x00FFFF ;#ifdef _MT if ( !_heap_init(1) ) /* initialize heap */#else /* _MT */ if ( !_heap_init(0) ) /* initialize heap */#endif /* _MT */ fast_error_exit(_RT_HEAPINIT); /* write message and die */#ifdef _MT if( !_mtinit() ) /* initialize multi-thread */ fast_error_exit(_RT_THREAD); /* write message and die */#endif /* _MT */ /* * Guard the remainder of the initialization code and the call * to user's main, or WinMain, function in a __try/__except * statement. */ __try { _ioinit(); /* initialize lowio */#ifdef WPRFLAG /* get wide cmd line info */ _wcmdln = (wchar_t *)__crtGetCommandLineW(); /* get wide environ info */ _wenvptr = (wchar_t *)__crtGetEnvironmentStringsW(); _wsetargv(); _wsetenvp();#else /* WPRFLAG */ /* get cmd line info */ _acmdln = (char *)GetCommandLineA(); /* get environ info */ _aenvptr = (char *)__crtGetEnvironmentStringsA(); _setargv(); _setenvp();#endif /* WPRFLAG */ _cinit(); /* do C data initialize */#ifdef _WINMAIN_ StartupInfo.dwFlags = 0; GetStartupInfo( &StartupInfo );#ifdef WPRFLAG lpszCommandLine = _wwincmdln(); mainret = wWinMain(#else /* WPRFLAG */ lpszCommandLine = _wincmdln(); mainret = WinMain(#endif /* WPRFLAG */ GetModuleHandleA(NULL), NULL, lpszCommandLine, StartupInfo.dwFlags & STARTF_USESHOWWINDOW ? StartupInfo.wShowWindow : SW_SHOWDEFAULT );#else /* _WINMAIN_ */#ifdef WPRFLAG __winitenv = _wenviron; mainret = wmain(__argc, __wargv, _wenviron);#else /* WPRFLAG */ __initenv = _environ; mainret = main(__argc, __argv, _environ);#endif /* WPRFLAG */#endif /* _WINMAIN_ */ exit(mainret); } __except ( _XcptFilter(GetExceptionCode(), GetExceptionInformation()) ) { /* * Should never reach here */ _exit( GetExceptionCode() ); } /* end of try - except */}
printf
下面来说一说 printf ,printf 最基本的功能就是输出,但是 printf 的特性是,格式化输出到标准输出设备上(默认是显示器)。
编程过的都知道,printf 可以接收不定参数, 也就是1 个或多个参数,例如下面这个程序
#include int main(int argc, char argv[], char *envp[]){ printf("%sHello %fworld!%c%d\r\n", "haha", 3.14, 'a', 1); return (int)3.14;}
编译链接生成可执行程序后的结果就是:hahaHello 3.140000world!a1
为了证明 printf 可以接受不定参数,打开 stdio.h ,并找到 printf 函数声明,可以看到 const char *,这是固定的必须接受一个参数,... 代表接受不定参数
_CRTIMP int __cdecl printf(const char *, ...);
那 printf 的结果是如何输出到显示屏的呢?好多人认为,C 语言直接操纵了底层,printf 操作了显存,其实是错误的,操作显存是操作系统的事情。printf 只是激活协调操作系统的功能,按操作系统的要求,把输出内容提交给操作系统,而操作系统再来操作显存。在现代的操作系统中,是有 3 环(用户应用程序(最低特权))和 0 环(内核(最高特权))的区别的。printf 只是一个工作在 3环的函数。也就是所有和硬件访问相关的,包括上面说的显存,都是 0 环做的事情,现在的操作系统不允许用户的程序真正的影响到硬件。就好比你(3 环)去肯德基吃汉堡,肯德基不会允许客户进厨房(0 环)的,如果要吃汉堡或鸡翅,只能通过工作人员(接口),等做好了,你来取餐就可以了。其中厨房的火候,料理(0 环),你(3 环)是接触不到的。