C语言开发总结(二十二)

 

C/C++程序编译运行生成过程分析

C/C++程序编译步骤 如何生成可执行文件

     **************************************************************************篇一******************************************************************************************** 

     电子计算机所使用的是由“0”和“1”组成的二进制数,二进制是计算机的语言的基础。计算机发明之初,人们只能降贵纡尊,用计算机的语言去命令计算机干这干那,一句话,就是写出一串串由“0”和“1”组成的指令序列交由计算机执行,这种语言,就是机器语言。想象一下老前辈们在打孔机面前数着一个一个孔的情景,嘘,小声点,你的惊吓可能使他们错过了一个孔,结果可能是导致一艘飞船飞离轨道阿。

        为了减轻使用机器语言编程的痛苦,人们进行了一种有益的改进:用一些简洁的英文字母、符号串来替代一个特定的指令的二进制串,比如,用“A D D”代表加法,“M O V”代表数据传递等等,这样一来,人们很容易读懂并理解程序在干什么,纠错及维护都变得方便了,这种程序设计语言就称为汇编语言,即第二代计算机语言。然而计算机是不认识这些符号的,这就需要一个专门的程序,专门负责将这些符号翻译成二进制数的机器语言,这种翻译程序被称为汇编程序。因为汇编指令和机器语言之间有着一一对应的关系,这可比英译汉或汉译英简单多了。

      高级语言是偏向人,按照人的思维方式设计的,机器对这些可是莫名奇妙,不知所谓。鱼与熊掌的故事在计算机语言中发生了。于是必须要有一个桥梁来衔接两者,造桥可不是一件简单的事情。当你越想方便,那桥就得越复杂。那高级语言是如何变成机器语言的呢,这个过程让我慢慢道来。

       编译:将源代码转换为机器可认识代码的过程。编译程序读取源程序(字符流),对之进行词法和语法的分析,将高级语言指令转换为功能等效的汇编代码,再由汇编程序转换为机器语言,并且按照操作系统对可执行文件格式的要求链接生成可执行程序。

C源程序->编译预处理->编译->优化程序->汇编程序->链接程序->可执行文件 

1.编译预处理  读取c源程序,对其中的伪指令(以#开头的指令)和特殊符号进行处理。 

伪指令主要包括以下四个方面

(1)宏定义指令,如# define Name TokenString,#undef等。对于前一个伪指令,预编译所要作得的是将程序中的所有Name用TokenString替换,但作为字符串常量的Name则不被替换。对于后者,则将取消对某个宏的定义,使以后该串的出现不再被替换。 

(2)条件编译指令,如#ifdef,#ifndef,#else,#elif,#endif,等等。这些伪指令的引入使得程序员可以通过定义不同的宏来决定编译程序对哪些代码进行处理。预编译程序将根据有关的文件,将那些不必要的代码过滤掉。 

(3)头文件包含指令,如#include "FileName"或者#include <FileName>等。在头文件中一般用伪指令#define定义了大量的宏(最常见的是字符常量),同时包含有各种外部符号的声明。采用头文件的目的主要是为了使某些定义可以供多个不同的C源程序使用。因为在需要用到这些定义的C源程序中,只需加上一条#include语句即可,而不必再在此文件中将这些定义重复一遍。预编译程序将把头文件中的定义统统都加入到它所产生的输出文件中,以供编译程序对之进行处理。 

包含到c源程序中的头文件可以是系统提供的,这些头文件一般被放在/usr/include目录下。在程序中#include它们要使用尖括号(<>)。另外开发人员也可以定义自己的头文件,这些文件一般与c源程序放在同一目录下,此时在#include中要用双引号("")。

(4)特殊符号,预编译程序可以识别一些特殊的符号。例如在源程序中出现的LINE标识将被解释为当前行号(十进制数),FILE则被解释为当前被编译的C源程序的名称。预编译程序对于在源程序中出现的这些串将用合适的值进行替换。

预编译程序所完成的基本上是对源程序的“替代”工作。经过此种替代,生成一个没有宏定义、没有条件编译指令、没有特殊符号的输出文件。这个文件的含义同没有经过预处理的源文件是相同的,但内容有所不同。下一步,此输出文件将作为编译程序的输出而被翻译成为机器指令。

2.编译阶段

经过预编译得到的输出文件中,将只有常量。如数字、字符串、变量的定义,以及C语言的关键字,如main,if,else,for,while,{,},+,-,*,\,等等。预编译程序所要作得工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。 

3.优化阶段

优化处理是编译系统中一项比较艰深的技术。它涉及到的问题不仅同编译技术本身有关,而且同机器的硬件环境也有很大的关系。优化一部分是对中间代码的优化。这种优化不依赖于具体的计算机。另一种优化则主要针对目标代码的生成而进行的。上图中,我们将优化阶段放在编译程序的后面,这是一种比较笼统的表示。

对于前一种优化,主要的工作是删除公共表达式、循环优化(代码外提、强度削弱、变换循环控制条件、已知量的合并等)、复写传播,以及无用赋值的删除,等等。

后一种类型的优化同机器的硬件结构密切相关,最主要的是考虑是如何充分利用机器的各个硬件寄存器存放的有关变量的值,以减少对于内存的访问次数。另外,如何根据机器硬件执行指令的特点(如流水线、RISC、CISC、VLIW等)而对指令进行一些调整使目标代码比较短,执行的效率比较高,也是一个重要的研究课题。 

经过优化得到的汇编代码必须经过汇编程序的汇编转换成相应的机器指令,方可能被机器执行。 

4.汇编过程

汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程。对于被翻译系统处理的每一个C语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。

 目标文件由段组成。通常一个目标文件中至少有两个段:

代码段  该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。  

数据段  主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。

UNIX环境下主要有三种类型的目标文件: 

(1)可重定位文件  其中包含有适合于其它目标文件链接来创建一个可执行的或者共享的目标文件的代码和数据。

 (2)共享的目标文件  这种文件存放了适合于在两种上下文里链接的代码和数据。第一种事链接程序可把它与其它可重定位文件及共享的目标文件一起处理来创建另一个目标文件;第二种是动态链接程序将它与另一个可执行文件及其它的共享目标文件结合到一起,创建一个进程映象。

 (3)可执行文件  它包含了一个可以被操作系统创建一个进程来执行之的文件。

汇编程序生成的实际上是第一种类型的目标文件。对于后两种还需要其他的一些处理方能得到,这个就是链接程序的工作了。

5.链接程序

由汇编程序生成的目标文件并不能立即就被执行,其中可能还有许多没有解决的问题。例如,某个源文件中的函数可能引用了另一个源文件中定义的某个符号(如变量或者函数调用等);在程序中可能调用了某个库文件中的函数,等等。所有的这些问题,都需要经链接程序的处理方能得以解决。

链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够被操作系统装入执行的统一整体。

根据开发人员指定的同库函数的链接方式的不同,链接处理可分为两种: 

(1)静态链接  在这种链接方式下,函数的代码将从其所在地静态链接库中被拷贝到最终的可执行程序中。这样该程序在被执行时这些代码将被装入到该进程的虚拟地址空间中。静态链接库实际上是一个目标文件的集合,其中的每个文件含有库中的一个或者一组相关函数的代码。

 (2)动态链接  在此种方式下,函数的代码被放到称作是动态链接库或共享对象的某个目标文件中。链接程序此时所作的只是在最终的可执行程序中记录下共享对象的名字以及其它少量的登记信息。在此可执行文件被执行时,动态链接库的全部内容将被映射到运行时相应进程的虚地址空间。动态链接程序将根据可执行程序中记录的信息找到相应的函数代码。 

     对于可执行文件中的函数调用,可分别采用动态链接或静态链接的方法。使用动态链接能够使最终的可执行文件比较短小,并且当共享对象被多个进程使用时能节约一些内存,因为在内存中只需要保存一份此共享对象的代码。但并不是使用动态链接就一定比使用静态链接要优越。在某些情况下动态链接可能带来一些性能上损害。

  经过上述五个过程,C源程序就最终被转换成可执行文件了

上一节我们介绍了编程语言的种类,其中包括机器语言、汇编语言和高级语言。

*************************************************************************篇二**********************************************************************************************

C/C++程序编译步骤详解
 
 


C/C++程序编译步骤详解
C/C++语言很多人都比较熟悉,这基本上是每位大学生必学的一门编程语言,通常还都是作为程序设计入门语言学的,并且课程大多安排在大一。刚上大学,孩子们还都很乖,学习也比较认真,用心。所以,C/C++语言掌握地也都不错,不用说编译程序,就是写个上几百行的程序都不在话下,但是他们真的知道C/C++程序编译的步骤么?
我想很多人都不甚清楚,如果他接下来学过“编译原理”,也许能说个大概。VC的“舒适”开发环境屏蔽了很多编译的细节,这无疑降低了初学者的入门门槛,但是也“剥夺”了他们“知其所以然”的权利,致使很多东西只能死记硬背,遇到相关问题就“丈二”。实际上,我也是在学习Linux环境下编程的过程中才逐渐弄清楚C/C++源代码是如何一步步变成可执行文件的。
总体来说,C/C++源代码要经过:预处理、编译、汇编和连接四步才能变成相应平台下的可执行文件。大多数时候,程序员通过一个命令就能完成上述四个步骤。比如下面这段C的“Hello world!”代码:
File: hw.c
#include stdio.h>
int main(int argc, char *argv[])
{
        printf("Hello World!\n");
        return 0;
}
如果用gcc编译,只需要一个命令就可以生成可执行文件hw:
xiaosuo@gentux hw $ gcc -o hw hw.c
xiaosuo@gentux hw $ ./hw Hello World! 
我们可以用-v参数来看看gcc到底在背后都做了些什么动作:
Reading specs from /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/specs
Configured with: /var/tmp/portage/sys-devel/gcc-3.4.6-r2/work/gcc-3.4.6/configure --prefix=/usr --bindir=/usr/i686-pc-linux-gnu/gcc-bin/3.4.6 --includedir=/usr/lib/gcc/i686-pc-linux-gnu/3.4.6/include --datadir=/usr/share/gcc-data/i686-pc-linux-gnu/3.4.6 --mandir=/usr/share/gcc-data/i686-pc-linux-gnu/3.4.6/man --infodir=/usr/share/gcc-data/i686-pc-linux-gnu/3.4.6/info --with-gxx-include-dir=/usr/lib/gcc/i686-pc-linux-gnu/3.4.6/include/g++-v3 --host=i686-pc-linux-gnu --build=i686-pc-linux-gnu --disable-altivec --enable-nls --without-included-gettext --with-system-zlib --disable-checking --disable-werror --enable-secureplt --disable-libunwind-exceptions --disable-multilib --disable-libgcj --enable-languages=c,c++,f77 --enable-shared --enable-threads=posix --enable-__cxa_atexit --enable-clocale=gnu
Thread model: posix
gcc version 3.4.6 (Gentoo 3.4.6-r2, ssp-3.4.6-1.0, pie-8.7.10)
/usr/libexec/gcc/i686-pc-linux-gnu/3.4.6/cc1 -quiet -v hw.c -quiet -dumpbase hw.c -mtune=pentiumpro -auxbase hw -version -o /tmp/ccYB6UwR.s
ignoring nonexistent directory "/usr/local/include"
ignoring nonexistent directory "/usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../../i686-pc-linux-gnu/include"
#include "..." search starts here:
#include ...> search starts here:
/usr/lib/gcc/i686-pc-linux-gnu/3.4.6/include
/usr/include
End of search list.
GNU C version 3.4.6 (Gentoo 3.4.6-r2, ssp-3.4.6-1.0, pie-8.7.10) (i686-pc-linux-gnu)
        compiled by GNU C version 3.4.6 (Gentoo 3.4.6-r2, ssp-3.4.6-1.0, pie-8.7.9).
GGC heuristics: --param ggc-min-expand=81 --param ggc-min-heapsize=97004
/usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../../i686-pc-linux-gnu/bin/as -V -Qy -o /tmp/ccq8uGED.o /tmp/ccYB6UwR.s
GNU assembler version 2.17 (i686-pc-linux-gnu) using BFD version 2.17
/usr/libexec/gcc/i686-pc-linux-gnu/3.4.6/collect2 --eh-frame-hdr -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o hw /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../crt1.o /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../crti.o /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/crtbegin.o -L/usr/lib/gcc/i686-pc-linux-gnu/3.4.6 -L/usr/lib/gcc/i686-pc-linux-gnu/3.4.6 -L/usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../../i686-pc-linux-gnu/lib -L/usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../.. /tmp/ccq8uGED.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/crtend.o /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../crtn.o
稍微整理一下,去掉一些冗余信息后,如下:
cc1 hw.c -o /tmp/ccYB6UwR.s
as -o /tmp/ccq8uGED.o /tmp/ccYB6UwR.s
ld -o hw /tmp/ccq8uGED.o
以上三个命令分别对应于编译步骤中的预处理+编译、汇编和连接。预处理和编译还是放在了一个命令(cc1)中进行的,可以把它再次拆分为以下两步:
cpp -o hw.i hw.c
cc1 hw.i -o /tmp/ccYB6UwR.s
一个精简过的能编译以上hw.c文件的Makefile如下:
.PHONY: clean
all: hw
hw: hw.o
        ld -dynamic-linker /lib/ld-linux.so.2 -o hw /usr/lib/crt1.o \
                /usr/lib/crti.o \
                /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/crtbegin.o \
                hw.o -lc \
                /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/crtend.o \
                /usr/lib/crtn.o
hw.o: hw.s
        as -o hw.o hw.s
hw.s: hw.i
        /usr/libexec/gcc/i686-pc-linux-gnu/3.4.6/cc1 -o hw.s hw.c
hw.i: hw.c
        cpp -o hw.i hw.c
clean:
        rm -rf hw.i hw.s hw.o
当然,上面Makefile中的一些路径是我系统上的具体情况,你的可能与我的不同。
接下来我们按照编译顺序看看编译器每一步都做了什么。
首先是预处理,预处理后的文件hw.i:
# 1 "hw.c"
# 1 ""
# 1 ""
...
__extension__ typedef __quad_t __off64_t;
__extension__ typedef int __pid_t;
__extension__ typedef struct { int __val[2]; } __fsid_t;
...
extern int remove (__const char *__filename) __attribute__ ((__nothrow__));
extern int rename (__const char *__old, __const char *__new) __attribute__ ((__nothrow__));
...
int main(int argc, char *argv[])
{
printf("Hello World!\n");
return 0;
}
注:由于文件比较大,所以只留下了少部分具有代表性的内容。
可以看见预处理器把所有要包含(include)的文件(包括递归包含的文件)的内容都添加到了原始的C源文件中,然后把其输出到输出文件,除此之外,它还展开了所有的宏定义,所以在预处理器的输出文件中你将找不到任何宏。这也提供了一个查看宏展开结果的简便方法。
第二步“编译”,就是把C/C++代码“翻译”成汇编代码:
.file "hw.c"
        .section .rodata
.LC0:
        .string "Hello World!\n"
        .text
.globl main
        .type main, @function
main:
        pushl %ebp
        movl %esp, %ebp
        subl $8, %esp
        andl $-16, %esp
        movl $0, %eax
        addl $15, %eax
        addl $15, %eax
        shrl $4, %eax
        sall $4, %eax
        subl %eax, %esp
        subl $12, %esp
        pushl $.LC0
        call printf
        addl $16, %esp
        movl $0, %eax
        leave
        ret
        .size main, .-main
        .section .note.GNU-stack,"",@progbits
        .ident "GCC: (GNU) 3.4.6 (Gentoo 3.4.6-r2, ssp-3.4.6-1.0, pie-8.7.10)"
这个汇编文件比预处理后的C/C++文件小了很多,去除了很多不必要的东西,比如说没用到的类型声明和函数声明等。
第三步“汇编”,将第二步输出的汇编代码翻译成符合一定格式的机器代码,在Linux上一般表现为ELF目标文件。
xiaosuo@gentux hw $ file hw.o
hw.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
最后一步“连接”,将上步生成的目标文件和系统库的目标文件和库文件连接起来,最终生成了可以在特定平台运行的可执行文件。为什么还要连接系统库中的某些目标文件(crt1.o, crti.o等)呢?这些目标文件都是用来初始化或者回收C运行时环境的,比如说堆内存分配上下文环境的初始化等,实际上crt也正是C RunTime的缩写。这也暗示了另外一点:程序并不是从main函数开始执行的,而是从crt中的某个入口开始的,在Linux上此入口是_start。以上Makefile生成的是动态连接的可执行文件,如果要生成静态连接的可执行文件需要将Makefile中的相应段修改:
hw: hw.o
    ld -m elf_i386 -static -o hw /usr/lib/crt1.o \
        /usr/lib/crti.o \
        /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/crtbeginT.o \
        -L/usr/lib/gcc/i686-pc-linux-gnu/3.4.6 \
        -L/usr/i686-pc-linux-gnu/lib \
        -L/usr/lib/ \
        hw.o --start-group -lgcc -lgcc_eh -lc --end-group \
        /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/crtend.o \
        /usr/lib/gcc/i686-pc-linux-gnu/3.4.6/../../../crtn.o
至此,一个可执行文件才最终创建完成。通常的项目中并不需要把编译过程分得如此之细,前三步一般是合为一体的,在Makefile中表现如下:
hw.o: hw.c
    gcc -o hw.o -c hw.c
实际上,如果对hw.c进行了什么更改,那么前三步大多数情况下都是不可避免的。所以把他们写在一起也并没有什么坏处,相反倒可以用--pipe参数告诉编译器用管道替代临时文件,从而提升编译的效率。


C 语言main 函数终极探秘

 所有的C程序必须定义一个称之为main的外部函数,这个函数是程序的入口,也就是当程序启动时所执行的第一个函数,当这个函数返回时,程序也将终止,并且这个函数的返回值被看成是程序成功或失败的标志,如果在到达main函数体的末尾时没有遇到返回语句 ,它就被看看成是执行了return 0; 语句。

        C的设计原则是把函数作为程序的构成模块。main()函数称之为主函数,一个C程序总是从main()函数开始执行的

        在最新的C99中,main函数只有两种标准形式:

       int  main(void )  {  }

       int  main(int  argc ,char *argv[ ]) { }

       在C99之前,main函数返回类型常常被省略,采用默认类型 int,其实,如果没有返回值, 编译器会给你加上。

      老版本的C代码,将会发现程序常常以

       main()
   
   这种形式开始。C90标准允许这种形式,但是C99标准不允许。因此即使你当前的编译器允许,也不要这么写。
    
     你还可能看到过另一种形式:

      void main()    
    有些编译器允许这种形式,但是还没有任何标准考虑接受它。C++ 之父 Bjarne Stroustrup 在他的主页上的 FAQ 中明确地表示:void main( ) 的定义从来就不存在于 C++ 或者 C 。所以,编译器不必接受这种形式,并且很多编译器也不允许这么写,这个形式只是编译器扩展的,并不是属于C标准。。
    
    坚持使用标准的意义在于:当你把程序从一个编译器移到另一个编译器时,照样能正常运行。

   返回值:

     由于是 int main( ..) 那么当时 应当返回 int 但是  return 2.3 ;也能运行正确,这是因为编译器自动转换2.3为int,截断后为return 2;  

     如果写为 return "abc";那么会报错,  error C2440: “return”: 无法从“const char [4]”转换为“int”

     C语言有两个宏

     #define EXIT_SUCCESS    0
     #define EXIT_FAILURE       1

     默认EXIT_SUCCESS 为成功返回, EXIT_FAILURE 为出错返回,相当于abort();
返回值的意义:

    return返回的数值由程序的作者自定。返回不同的值可以代表不同的含义,一般是代表出错的原因。传统上返回0代表程序正常结束(其它返回值代表什么含义,需要程序的开发者向程序的用户说明)。
在UNIX中,一个程序仅仅完成一个简单但有用的操作;不像Windows中的应用程序那样试图包办一切。所以,UNIX中很多程序都是可以分工协作的。后面程序的输入可以从前面程序的输出获得。这样在一些关键应用中,后面的程序可以检测一下前面的程序是否正常退出,如果是正常退出的,再按照预先的流程进行下面的操作;如果前面的程序不是正常退出,那么前面的程序的输出很可能不是后面程序需要的输入数据,这就需要进行特殊的处理。
而后面的程序就是靠前面程序main函数的返回值判断的。程序结束时,把值交给操作系统,然后后面的程序或者shell可以从操作系统中取得这个值。

      以下example 转自编程中国:

====================================================================================

      从前面我们知道main()函数的返回值类型是int型的,而程序最后的 return 0; 正与之遥相呼应,0就是main()函数的返回值。那么这个0返回到那里呢?返回给操作系统,表示程序正常退出。因为return语句通常写在程序的最后,不管返回什么值,只要到达这一步,说明程序已经运行完毕。而return的作用不仅在于返回一个值,还在于结束函数。
    现在我们来做一个小试验(注意:本人的系统是Windows XP, 编译环境是TC)来观察main()函数的返回值。编写如下代码并编译运行:

   //a.c    
    #include "stdio.h"
    int main(void)
    {
        printf("I love you.");
    
        return 0;
    }

    将这个文件保存为a.c,编译运行后会生成一个a.exe文件。现在打开命令提示符,在命令行里运行刚才编译好的可执行文件,然后输入   echo %ERRORLEVEL% ,回车,就可以看到程序返回 一个0 。如果把 return 0; 改为 return 99; ,那么很显然,再次执行上述步骤以后你可以看到程序返回99。要是你这样写 return 99.99; 那还是返回99,因为99.99被传给操作系统之前,被强制类型转换成整数类型了。
    现在,我们把a.c改回原来的代码,然后再编写另一个程序b.c:

    //b.c
    #include "stdio.h"
    int main(void)
    {
        printf("\nI'm too.");
    
        return 0; 
    }

    编译运行后打开命令提示符,在命令行里输入a&&b 回车,这样你就可以看到《人鬼情未了》里面经典的爱情对白:
    
    
I love you.
    I'm too.

    && 的含义是:如果 && 前面的程序正常退出,则继续执行 && 后面的程序,否则不执行。所以,要是把a.c里面的 return 0; 删除或者改为 return 99; ,那么你只能看到 I love you. 。也就是说,程序b.c就不执行了。现在,大家该明白 return 0; 的作用了吧。
从前面我们知道main()函数的返回值类型是int型的,而程序最后的 return 0; 正与之遥相呼应,0就是main()函数的返回值。那么这个0返回到那里呢?返回给操作系统,表示程序正常退出。因为return语句通常写在程序的最后,不管返回什么值,只要到达这一步,说明程序已经运行完毕。而return的作用不仅在于返回一个值,还在于结束函数。
    现在我们来做一个小试验(注意:本人的系统是Windows XP, 编译环境是TC)来观察main()函数的返回值。编写如下代码并编译运行:

   //a.c    
    #include "stdio.h"
    int main(void)
    {
        printf("I love you.");
    
        return 0;
    }

    将这个文件保存为a.c,编译运行后会生成一个a.exe文件。现在打开命令提示符,在命令行里运行刚才编译好的可执行文件,然后输入   echo %ERRORLEVEL% ,回车,就可以看到程序返回 一个0 。如果把 return 0; 改为 return 99; ,那么很显然,再次执行上述步骤以后你可以看到程序返回99。要是你这样写 return 99.99; 那还是返回99,因为99.99被传给操作系统之前,被强制类型转换成整数类型了。
    现在,我们把a.c改回原来的代码,然后再编写另一个程序b.c:

    //b.c
    #include "stdio.h"
    int main(void)
    {
        printf("\nI'm too.");
    
        return 0; 
    }

    编译运行后打开命令提示符,在命令行里输入a&&b 回车,这样你就可以看到《人鬼情未了》里面经典的爱情对白:
    
    
I love you.
    I'm too.

    && 的含义是:如果 && 前面的程序正常退出,则继续执行 && 后面的程序,否则不执行。所以,要是把a.c里面的 return 0; 删除或者改为 return 99; ,那么你只能看到 I love you. 。也就是说,程序b.c就不执行了。现在,大家该明白 return 0; 的作用了吧。

===================================================================================

  main函数参数:

     int main(void)  {  } 

     未声明任何参数 ,不会从环境向main函数传递任何信息,不过可以使用想getnev或system这样的库函数获取此类信息。

    int main(int argc,char *argv[ ] )  {       }

    声明的这些参数是由执行环境所创建的(执行环境在下面讨论),而不是在C语言的直接控制下,argc参数表示当用户或其他程序调用这个程序时传递给它的 “程序的参数”或 “选项”的数量,argv 参数是个指针向量,其中每个字符串指针分别表示传递给程序的参数, 第一个字符串argv[0]是程序的名称,包含路径,如果这个名称不可用,则argv[0]必须是 '\0',字符串arav[i](i=1,...argc-1)表式第 i 个程序参数,标准C 要求argv[argc]是个null指针,但在有些旧时编译器中却不是这样的,argv向量以及它所指向的字符串必须是可以修改的,并且他们的值在程序执行期间不能被编译器或操作系统所修改。如果编译器并不允许大小写混合的字符串 ,则存储在argv中的字符串必须采用小写形式。

    独立性C编译器以及一些软件框架(比如Microsoft Windows MFC) 对C程序的启动可能采用一些特殊的约定。

    当然argc ,argc形参名可以任意修改 ,只要不改变类型都可以。

     这个小程序打印出它的名称和参数:

      #include <stdio.h>

     int  main(int  argc,  char *argv[])

      {

         int i;

         printf("Name : %s\n",argv[0]);

         for(i=1 ; i<argc; i++)

           printf("%s \t",argv[i]);

         return 0;

      }

      运行程序后生成 debug.exe 文件,比如路径为 C:\ debug.exe

       打开cmd 输入 c:\debuge.exe I love you

       那么debug.exe 会运行,并在屏幕上打印出:

       C:\debuge.exe 

       I love you

-----------------------------------------------------------------------------------------------------------------------------------

   main函数之前的执行环境初始化,来自百度空间的文章:

       

main函数之前--真正的函数执行入口或开始

main函数之前--真正的函数执行入口或开始

一种解释

实际上,在可执行文件被加载之后,控制权立即交给由编译器插入的Start函数,它将对后面这些全局变量进行准备:   _osver 操作系统的构件编号      _winmajor 操作系统的主版本号      _winminor 操作系统的次版本号      _winver 操作系统完全版本号      __argc 命令行参数个数      __argv 指向参数字符串的指针数组      _environ 指向环境变量字符串的指针数组Start函数初始化堆并调用main函数.mian函数返回之后,Start函数调用Exit函数结束该进程.启动函数Start的源代码在:   crt0.c Microsoft Visual C++

      c0w.asm Borladn C++

另一种解释

Some of the stuff that has to happen before main():
set up initial stack pointer 
initialize static and global data 
zero out uninitialized data 
run global constructors
Some of this comes with the runtime library's crt0.o file or its __start() function. Some of it you need to do yourself.

Crt0 is a synonym for the C runtime library.
Depending on the system you're using the follwing may be incomplete, but it should give you an idea. Using newlib-1.9.0/libgloss/m68k/crt0.S as an outline, the steps are:
1. Set stack pointer to value of __STACK if set 
2. Set the initial value of the frame pointer 
3. Clear .bss (where all the values that start at zero go) 
4. Call indirect of hardware_init_hook if set to initialize hardware 
5. Call indirect of software_init_hook if set to initialize software 
6. Add __do_global_dtors and __FINI_SECTION__ to the atexit function so destructors and other cleanup functions are called when the program exits by either returning from main, or calling exit
7. setup the paramters for argc, argv, argp and call main 
8. call exit if main returns

第三种解释:囫囵C语言(三):谁调用了我的 main?
        
    我们都听说过一句话:“main是C语言的入口”。我至今不明白为什么这么说。就好像如果有人说:“挣钱是泡妞”,肯定无数砖头拍过来。这句话应该是“挣钱是泡妞的一个条件,只不过这个条件特别重要”。那么上面那句话应该是 “main是C语言中一个符号,只不过这个符号比较特别。”
    
    我们看下面的例子:
    
    /* file name test00.c */
    
    int main(int argc, char* argv)
    {
     return 0;
    }
    
    编译链接它:
    cc test00.c -o test.exe
    会生成 test.exe
    
    但是我们加上这个选项: -nostdlib (不链接标准库)
    cc test00.c -nostdlib -o test.exe
    链接器会报错:
    undefined symbol: __start
    
    也就是说:
    1. 编译器缺省是找 __start 符号,而不是 main
    2. __start 这个符号是程序的起始点
    3. main 是被标准库调用的一个符号
    
    再来思考一个问题:
    我们写程序,比如一个模块,通常要有 initialize 和 de-initialize,但是我们写 C 程序的时候为什么有些模块没有这两个过程么呢?比如我们程序从 main 开始就可以 malloc,free,但是我们在 main 里面却没有初始化堆。再比如在 main 里面可以直接 printf,可是我们并没有打开标准输出文件啊。(不知道什么是 stdin,stdout,stderr 以及 printf 和 stdout 关系的群众请先看看 C 语言中文件的概念)。
    
    有人说,这些东西不需要初始化。如果您真得这么想,请您不要再往下看了,我个人认为计算机软件不适合您。
    
    聪明的人民群众会想,一定是在 main 之前干了些什么。使这些函数可以直接调用而不用初始化。通常,我们会在编译器的环境中找到一个名字类似于 crt0.o 的文件,这个文件中包含了我们刚才所说的 __start 符号。(crt 大概是 C Runtime 的缩写,请大家帮助确认一下。)
    
    那么真正的 crt0.s 是什么样子呢?下面我们给出部分伪代码:
    
    ///////////////////////////////////////////////////////
    section .text:
    __start:
    
     :
     init stack;
     init heap;
     open stdin;
     open stdout;
     open stderr;
     :
     push argv;
     push argc;
     call _main; (调用 main)
     :
     destory heap;
     close stdin;
     close stdout;
     close stderr;
     :
     call __exit;
    ////////////////////////////////////////////////////
    
    实际上可能还有很多初始化工作,因为都是和操作系统相关的,笔者就不一一列出了。

    
    注意:
    1. 不同的编译器,不一定缺省得符号都是 __start。
    2. 汇编里面的 _main 就是 C 语言里面的 main,是因为汇编器和C编译器对符号的命名有差异(通常是差一个下划线'_')。
    3. 目前操作系统结构有两个主要的分支:微内核和宏内核。微内核的优点是,结构清晰,简单,内核组件较少,便于维护;缺点是,进程间通信较多,程序频繁进出内核,效率较低。宏内核正好相反。我说这个是什么目的是:没办法保证每个组件都在用户空间(标准库函数)中初始化,有些组件确实可能不要初始化,操作系统在创建进程的时候在内核空间做的。这依赖于操作系统的具体实现,比如堆,宏内核结构可能在内核初始化,微内核结构在用户空间;即使同样是微内核,这个东东也可能会被拿到内核空间初始化。
    
    随着 CPU 技术的发展,存储量的迅速扩展,代码复杂程度的增加,微内核被越来越多的采用。你会为了 10% 的效率使代码复杂度增加么?要知道每隔 18 个月 CPU 的速度就会翻一番。所以我对程序员的要求是,我首先不要你的代码效率高,我首先要你的代码能让 80% 的人迅速看懂并可以维护。

总结:

main函数执行之前,主要就是初始化系统相关资源:

1.设置栈指针

2.初始化static静态和global全局变量,即data段的内容

3.将未初始化部分的赋初值:数值型short,int,long等为0,bool为FALSE,指针为NULL,等等,即.bss段的内容

4.运行全局构造器,估计是C++中构造函数之类的吧

5.将main函数的参数,argc,argv等传递给main函数,然后才真正运行main函数



你可能感兴趣的:(C语言开发总结(二十二))