在分析类的相互引用之前,我们需要了解一个程序的整个执行过程。
集成开发环境(IDE)整合了编辑器、编译器、链接器,调试、部署等功能,我们在编辑器里写好的C/C++文件一开始以ASCII字符集存储在硬盘里,计算机只能识别机器语言(二进制指令,又称BCD码),为此,编译器的功能是将写好的源文件(C/C++)按照一定的对应规则映射成计算机能够识别的二进制指令,也就是我们常说的编译过程。
编译是指把文本形式源代码翻译为机器语言形式的目标文件(windows下是.obj,linux下是.o的二进制文件)的过程。
链接是把目标文件、操作系统的启动代码、以及用到的库文件进行组织,形成最终可执行的exe文件。
首先分析C/C++程序的整个执行过程:
编译预处理->编译->汇编->链接。
编译器实现编译过程分为两个阶段:编译和汇编;
在进行编译之前,编译器会有一个编译预处理阶段
编译预处理:
对其中的伪指令(以#开头的指令)和特殊符号进行处理
伪指令主要包括以下五个方面:
1) 宏定义指令,如 # define City BeiJing,将所有City变量都替换为BeiJing(字符串常量City不被替换);
2.条件编译指令,如#ifdef,#ifndef,#else,#endif ,通过定义不同的宏来决定编译程序对哪些代码进行处理
3.#include指令,将#include<>或#include""(函数或变量,机大量外部符号声明)的文件按先后顺序插入到源文件中。
4.特殊符号,预编译程序识别一些特殊的符号。如LINE标识解释为当前行号,FILE则被解释为当前编译的源程序名称。
5.预处理模块,#pragma once,#pragma warning(disable:4996)等,设定编译器状态或者指定编译器完成特定动作。
编译:
编译主要是通过词法分析与语法分析,并对中间代码进行优化(删除公共表达式,循环优化,无用赋值的删除等),在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码(汇编语言代码)。
经过编译得到的输出文件中,只有常量,如数字,字符串、变量的定义,以及C语言关键字
汇编:
实际上是把汇编语言代码翻译成目标机器指令。把这些指令打包成可重定位目标程序(relocateble object program)的格式,并把结果保存在.o(unix中为.o,windows下是.obj)文件中。.o文件是二进制目标文件,他的字节编码是机器语言指令而不是ascii码,目标文件中所存放的也就是与源程序等效的目标的机器语言代码。
下面开始分析类之间相互引用(循环依赖)
对一个类(如类B)只引用另一个类(如类C)的情况:
1.C类对象是非引用型(非指针)变量作类B的成员时,此时必须包含C的头文件(头文件为类C的定义),那么程序预编译时,将所有头文件里的内容都插入到cpp里,此时类C的声明定义在使用之前,可以为B类的C对象分配确定内存,编译通过,如下:
2.C类对象是引用型变量(如指针),作类B的成员时,此时即可以包含C的头文件,也可以只做前向声明(最好做前向声明,以免同一文件被多次编译或不被编译,造成编译器报重定义或者未定义错误),因为指针所占内存是确定的,即是完整的类型。如下:
当两个类之间存在相互引用时;
1.如果引用的是普通非指针类型变量,则会发生头文件的的相互包含问题,编译器在编译这两个类时,引用的类型变量的内存大小无法确定(编译器要求使用前必须先定义),故无法通过编译。(实际上两个变量CrefrenceB和BregrenceC都未定义。)
2.其中一个为指针类型,另一个为非指针类型。
指针所指对象在堆里开辟内存,非指针对象在栈里开辟内存。堆内存开辟不能在类构造函数里进行,因为非指针类型的变量定义未完成,会造成自身的无限嵌套,(B生A,A生B,B生A,A生B)无限死循环,造成内存溢出。
此时可以在主函数里进行堆内存申请。
3.相互引用的都是指针类型时
都只能在堆里开辟内存,且只能有一个在构造函数里构造申请堆内存,如果两个构造函数同时构造这两个对象,虽然编译时可以确定开辟的内存大小,编译通过,但构造函数会无限地为彼此申请内存,造成内存崩溃。
最好不在构造函数里分配内存,而是在需要用到时在主函数内调用构造函数进行分配内存。