C编译器剖析_2.4 C语言的类型系统

C语言基础之类型系统 一、类型系统介绍类型系统是每个编程语言都会有的基本概念,C语言的类型系统可以概括为基本类型和复合类型,基本类型又可以细分为整型数值类型和浮点数数值类型,不同类型所占用的内存长度不相同,下面先来介绍一下基本类型。整型数值基本类型1、char  占用一个字节2、short 占用两个字节3、int 目前基本都是4字节4、long int (可以简写为 long) (32位系统是4字节,64位系统是8字节... 阅读详情

2.4  C语言的类型系统

    这一节,我们准备初步讨论一下C语言的类型系统,相关的代码主要在ucl\type.c和ucl\type.h中。我们知道,一个进程的地址空间可分为代码区和数据区。

    对于数据区,C语言提供了char、short、int、long、float和double等基本类型来刻画基本的操作数。char、short、int和long等整型还进一步分有unsigned和signed,对大多数编译器而言,缺省时,整型默认为signed。当然也有C编译器默认char为unsigned char。C标准对基本类型要占多大内存空间,并没有规定得非常死。比如,在一些面向16位单片机的C编译器中,int就只占2个字节;而在32位机器上,int一般占4个字节。这些基本类型,就如最基本的化学元素一样,按照一定的组合规则,就可以构成更复杂的物质,最终构成了纷繁的大千世界。在C语言中,指针、数组和结构体等概念,就相当于是化学元素的组合规则,通过这些概念,C程序员可以描述更加复杂的数据。

C语言通过引入“函数”的概念,刻画了代码区,对C程序员而言,要访问代码区的函数代码时,我们需要知道这段代码的首地址、函数参数和返回值这样的信息,函数名实际上就代表了这段代码的首地址,这些信息可用C语言的函数声明来表达,如下所示。

         int f(int,int);

         int g(int,int);

    如果忽略掉函数名,则函数f和函数g拥用相同的特征,同样的参数类型和同样的返回值类型。我们可以说函数f和函数g的类型是一样的。把“指针”这样的组合规则作用到“函数”上,我们就有了“函数指针 pointer to function”的概念,由此,我们可以把函数的首地址也当作一种数据来处理;而函数的参数和返回值,实际上表达了代码区要如何访问数据区。因为整数和浮点数等基本类型相当于基本的化学元素,而指针、结构体、数组和函数等组合规则是在这些基本类型之上衍生出来的,所以我们称这些类型为衍生类型(derived type)。如果把基本类型看成是操作数,把这些组合规则看成是类型运算符,则C程序员通过函数或变量的声明,实际上构建了类型表达式,来告诉C编译器我们要如何访问代码区和数据区。

    在UCC编译器内部,我们需要建立相应的数据结构来刻画基本类型和衍生类型。UCC编译器是用C语言来实现的,很自然的,我们就会用结构体来描述相关类型信息。图2.4.1中的代码来源于ucl\type.h,为了表述方便,删去了原有的一些注释。


图2.4.1  struct type

    我们通过图2.4.1第17行的struct type来描述类型信息,而数组类型需要记录更多的信息,我们就用第22行的struct arrayType。可以看到,这两种结构体的开始部分都是TYPE_COMMON,我们在第1章时介绍过,这相当于struct arrayType继承了struct type。而宏定义TYPE_COMMON如图第10至15行所示,第11行的categ用来记录类别,取值范围由第2至第6行的枚举常量来定义。例如CHAR对应的是char,而UCHAR对应的是unsigned char。第12行的qual用来记录类型声明时,是否有添加const或volatile等限定符(qualifier),其取值范围由第8行的枚举常量CONST和VOLATILE来指示。第12行的align表示是按多少字节进行对齐的,而第13行的size则记录该类型要占多少个字节。如果是衍生类型,第15行的bty用于指向其基类。

    我们举以下几个具体的例子来说明。通过ucl\type.c中的函数ArrayOf()、PointerTo()和Qualify(),我们可以在基本类型int的基础上构造如图2.4.2所示的类型结构。结合图2.4.2,不难理解这几个函数的源代码,这里就不展开讨论。

         int a;

         const int b;

         int c[4];

         int * d;


图2.4.2             类型结构

    由图2.4.2,我们可以知道数组c的类型由4个int构成,共占16字节,按4字节进行对齐,属于ARRAY类别;而指针d属于POINTER类别,占4字节。接下来我们再来看一下如何描述结构体类别,如图2.4.3所示。


图2.4.3   结构体的类型描述

    我们仍然结合一个例子来说明。通过ucl\type.c的StartRecord()、AddField()和EndRecord()等函数,我们会为以下结构体struct Data构造一个形如图2.4.4的类型结构。

struct Data{

         int abc:8;

         int def:24;

         double f;

} dt;

 

图2.4.4   结构体的类型结构

    在图2.4.4中,recordType中记录了结构体struct Data类型的大小为16字节,其类别为STRUCT,按8字节进行对齐,其中的flds指针指向由struct field对象构成的链表。每个struct field对象描述了结构体中的一个数据域成员,tail指针相当于指向链尾的struct field对象。对于位域成员,在struct field的bits记录了其所占用的位数,UCC使用一个int或者unsigned int来存放位域成员,而pos则记录了位域在一个整数中的起始位置。例如,在结构体struct Data中,成员abc和def都是位域,abc占8个bit,而def占24个bit,它们一共占了32位的空间,即4字节。UCC编译器为struct Data对象dt构造的内存布局如图2.4.5所示。由于在UCC对double类型按8字节进行对齐,所以在偏移offset为4开始的4个字节实际上没有放置任何数据。位域abc和def都处于偏移0字节处,但它们的pos信息是不一样的;而双精度浮点数f位于偏移8字节处,占用了8字节的内存空间。整个dt对象共占16字节。不同C编译器采取的对齐策略是不一样的,所产生的对象内存布局会有所不同。


图2.4.5 struct Data对象dt的内存布局

    接下来,我们来看一下UCC的类型系统是如何描述“函数”的,如图2.4.6所示。


图2.4.6 函数的类型描述

    图2.4.6的第159行的functionType描述了与函数相关的类型信息,TYPE_COMMON中的bty记录了函数返回值的类型信息,而第162行的sig则记录了参数列表的类型信息。C语言函数分为旧式风格old-style或者新式风格new-style,如图2.4.7所示的f1和f2实际上就是旧式风格;而f3和f4为对应的新式风格函数。由图2.4.7的第17至22行可知,旧式风格的函数甚至连实参的个数都不进行检查;由第23至28行可知,新式风格的函数会对参数进行检查,这也是图2.4.6第154行的hasProto的含义,hasProto是”has prototype”的意思,换言之,参数列表成为函数接口的一部分。作为C程序员,应尽量不去使用旧式风格的函数定义或声明,毕竟,因为历史上使用旧式风格的函数引起了不少问题,我们才会引入类型检查更严格的新式风格函数。但是作为C编译器,却需要背上这个历史的包袱,新旧风格都需要去支持。而图2.4.6第155行的hasEllipsis则用于判断新式风格的函数中是否存在变参,ellipsis是省略号的意思,在C语言中printf就是一个最典型的变参函数,其函数接口如下所示。

int printf(const char *format, ...);


                                      图2.4.7   旧式风格和新式风格

    图2.4.8给出了图2.4.7的代码上机运行后的结果,我们给出了GCC、UCC和Clang三者的对比。Clang编译器给出的提示信息确实是最具可懂性的。在UCC编译器的警告和错误提示上,我们有时有意去模仿Clang或者GCC。


                                      图2.4.8  新旧风格函数运行结果

   接下来,我们就以如下所示的函数f5为例。通过ucl\type.c中的FunctionReturn()等函数,我们为之构造一个如图2.4.9所示的类型结构。

int f5(double a,float b);


                                     图2.4.9 函数的类型结构

    在图2.4.9中,结构体struct parameter描述了函数的某个参数的相关信息,id为形参的名字,ty为形参的类型,而reg表示形参声明时是否有register这样的说明符,该说明符只是建议C编译器把形参尽量放到寄存器中。而structsignature则描述了参数形表的相关信息,hasProto为1时,表示是新式风格的函数,此时params域指向一个向量,该向量包含多个struct parameter对象。

    通过这一节的图2.4.2、图2.4.4和图2.4.9,我们对UCC编译器是如何刻画C语言的数组、结构体和函数等类型信息会有一个非常直观的感觉。UCC编译器会在语法分析和语义检查时进行这些类型结构的构建,我们会在后续章节再进行讨论。而int和double等基本类型的结构,在main()函数中调用SetupTypeSystem()函数来构建,如图2.4.10所示。第1259至1276行创建了所有的基本类型,指定了这些类型的大小size、对齐align和类别categ信息。第1281至1293创建了一个形如”int f();”的缺省函数类型DefaultFunctionType。


图2.4.10  SetupTypeSystem()

    在C语言中,如果一个函数未经声明就直接使用,则C编译器会把这个函数的类型视为DefaultFunctionType。第1290行告诉我们这个类型的函数是旧式风格的,不对函数参数进行任何的检查。我们举一个例子来说明这会引起多让人莫名其妙的问题。

    假设有两个C文件,一个文件名为b.c,其中定义了一个函数fadd,用于对两个float类型的浮点数进行加法运算;而另一个文件名为a.c,其中调用了fadd(3.0f,3.0f)。这个程序非常简单,只要学过几天C语言的人几乎都会预期这个程序的结果是6.0。

//a.c

#include <stdio.h>

int main(){

         fadd(3.0f,3.0f);        

         return 0;

}

//b.c

#include <stdio.h>

void fadd(float a,float b){

         float c;

         c = a+b;

         printf("%f\n",c);

}

    但上机运行后的结果竟然是2.125,这一定会让我们大吃一惊。如图2.4.11所示。再一次的,我们看到Clang至少给了我们一点警告提示,让我们知道原来a.c中对函数fadd是没有声明就直接使用。这就导致在编译单元a.c中,函数fadd沦为旧风格的函数。C编译器视fadd的类型为图2.4.10中的DefaultFunctionType。


图2.4.11 调用未声明的函数

    这就是噩梦的源头。只要在a.c中加上声明” void fadd(floata,float b);”后,再调用fadd()函数,我们就得到了想要的结果6.0。下面,我们就来分析一下,为什么旧式风格的函数会带到这么怪异的问题。为了说明方便,我们把上述程序稍微修改一下,适当加了一些输出语句,如图2.4.12所示。


图2.4.12 沦为oldstyle

    再次上机运行,我们会得到如图2.4.13所示结果。由图2.4.13,我们可知,float类型的浮点数3.0f在内存中对应的十六进制数值为0x40400000,而2.125f则对应0x40080000,这也正好吻合IEEE754的浮点数编码;double类型的3.0在内存中对应8个字节,内容为[ 0x00000000  0x40080000]。同时,我们发现,在b.c中,形参a的值竟然是0x00000000,形参b的值竟然是0x40080000,这就相当于是2.125f和0.0f相加,结果当然是2.125f。


图2.4.13 沦为oldstyle的运行结果

    在a.c中把fadd当作旧式风格的函数时,按IT大佬们的约定,C编译器会进行一个被称为实参提升的动作。如ucl\type.c中的Promote()函数所示,凡是低于int型的其他整型,包括char和short都会被提升为int,而单精度float则会被提升为double;其他类型保持不变。

Type Promote(Type ty){

         return ty->categ< INT ? T(INT) : (ty->categ == FLOAT ? T(DOUBLE) : ty);

}

    C编译器面对“未声明就使用”的函数调用”fadd(3.0f,3.0f);”时,默默地进行了实参提升的操作。真正执行的函数调用是”fadd(3.0,3.0);”,压入栈的是两个double类型的浮点数3.0,共占了16字节。如图2.4.14所示。在小端机器上,浮点数3.0的存放如图所示,在低地址4字节存放了0x00000000,在高地址的4字节中存放了0x40080000。在b.c的函数fadd()中,仍然是把形参a和b当作float来处理,按照C调用约定,参数向右向左入栈,所以形参a对应的是0x00000000,形参b对应的是0x40080000。


图2.4.14 栈示意图

    总之,远离旧式风格的C函数,同时记住,函数要先声明再使用,否则我们就不知不觉地在使用旧式风格的函数声明。上述例子阐述了旧式风格的函数所带来的噩梦。

C编译器剖析_前言 让我们站在C编译器实现的角度重新认识自己朝夕相伴的C语言,如果你熟悉C语言,并且有基本的数据结构的知识,那就让我们一起开启C编译器剖析这趟旅程。众里寻她千百度,蓦然回首,She is C。那我们就启程吧。 阅读详情

相关推荐

c4编译器源码剖析

c4_source 1. 概述 c4是500行代码实现一个c语言编译器 简单暴力,适合了解基础的编译器原理 2. 主流程 1.建立系统符号表 2.读取源代码 3.一次遍历源代码,同时进行词法分析,语法分析和中间代码生成。 4.执行中间代码 3. 中间代码 3.1. 基础 中间代码 操作 意义 LEA 2 将栈往上的2个位置变量加载到寄存器 加载本地变量 IM

fishmei的博客 6057

C语言编译处理深度剖析

C语言编译处理是一个复杂但精巧的系统工程, 它像一座精心设计的工厂流水线, 将人类可读的高级语言代码转化为机器能高效执行的指令. 从预处理器的文本处理, 到编译器的深度分析和优化, 再到汇编器和链接器的精密装配, 每个阶段都有其独特的数据结构、算法和设计哲学正如计算机科学家Niklaus Wirth所言: “程序 = 算法 + 数据结构”, 而编译器正是将这一公式转化为可执行现实的关键桥梁. 掌握编译原理, 你将在软件开发的殿堂中拥有更深刻的洞察力和更强大的解决问题的能力。

X 529

C编译器剖析_C类型系统_相容类型Compatible Type

在前文对函数调用进行语义检查时,我们用函数CanAssign()来判断“能否能把实参赋值给形参”,在函数CanAssign中,我们又用宏IsCompatiblePtr来判断两个指针是否相容。而如果指针变量T1 * ptr1和T2 * pt2相容,则意味着类型T1和T2是相容的。UCC编译器中ucl\type.c的函数IsCompatibleType()用于判断类型是否相容,与类型系统相关的数据结构

SheIsC的专栏 2177

看完这五个问题后你真的了解C语言吗?(深度剖析C语言第二期)

C语言是一门非常经典的语言,远没有我们在学校学的那几个语法而已,让小生再来带大家重新认识一遍C语言吧🥳🥳🥳

qq_59955115的博客 3万+

【C语言进阶教程】编译器优化

本节将帮助您了解本教程的目的、结构,以及在开始学习之前您需要具备的基础知识和准备工作。编译器优化是指通过编译器对代码进行分析和转换,以提高程序效率和性能的过程。优化目标通常是减少执行时间、降低内存使用、提高代码稳定性和可读性等。为了在不改变程序正确行为的情况下做到这一点,编译器使用各种技巧和策略。优化的基本概念编译器优化是自动化的优化过程,旨在提高代码的质量和效率。通过简化代码、减少冗余、改善内存使用等方式来提高程序性能。优化可能涉及多个方面:包括执行速度、内存占用、功耗等。为什么需要优化。

2072

C编译器剖析_1.4 UCC编译器预览_UCC的使用

1.4.1  UCC的使用     通过第1.3节的例子ucc\examples\sc,我们对如何根据语言的文法来编写语法分析器,建立语法树,之后在语法树的基础上生成中间代码有了一个感性的直观认识。当然,光有这些中间代码,C程序员还不能得到其所要的计算结果。C编译器还要由中间代码产生汇编代码。在剖析UCC编译器前,让我们先熟悉一下UCC编译器的使用。UCC编译器的大部分代码都是用标准C语言并调用

SheIsC的专栏 4600

Lean 4编译器与运行时系统剖析

Lean 4编译器与运行时系统剖析 【免费下载链接】lean4 Lean 4 programming language and theorem prover 项目地址: https://gitcode.com/GitHub_Tre...

gitblog_00198的博客 1274

C编译器剖析_4.1 语义检查_语义检查简介

4.1  语义检查简介     在这一章中,我们需要在语法分析阶段建立的语法树的基础上,进行语义检查。UCC编译器中与此相关的代码主要在ucl\declchk.c,ucl\stmtchk.c和ucl\exprchk.c,分别用于对声明Declaration、语句Statement和表达式Expression进行语义检查SemanticsCheck。其中,最重要的工作就是建立C语言的类型系统,我们

SheIsC的专栏 2350

C语言中的性能调优与代码剖析

C语言中的性能调优与代码剖析。

D_ovis的博客 952

深入浅出C语言编译器源码剖析

本文还有配套的精品资源,点击获取 简介:编译器是将高级编程语言转换为机器语言的关键工具,本资源提供的“简单编译器”源码用C语言编写,涵盖编译器设计的各个主要阶段,如词法分析、语法分析、语义分析、中间代码生成、代码优化和目标代码生成。通过学习和调试这些源码,开发者能够深入理解编译原理和编程语言的内部工作,提升C语言编程技能,掌握复杂数据结构和算法的应用。建议学习者先了解编译原...

weixin_42451850的博客 1499

C编译器剖析PDF文档_UCC162.3源代码

包含C编译器剖析PDF文档,及修改少量BUG后的编译器源代码UCC162.3, 欢迎访问对应博客http://blog.csdn.net/sheisc

C编译器剖析_4.4 语义检查_外部声明_类型结构的构建(1)

4.4.1  类型结构的构建(1)     int * arr1[5]和int (*arr2)[5]的类型结构

SheIsC的专栏 1269

C 编译器剖析

C 编译器剖析,讲述编译器的构造及实现,内容经典、详实。

C编译器剖析_1.5 结合C语言来学汇编

1.5  结合C语言来学汇编      汇编语言的语法和语义都不复杂,如果你会C语言,那你就一定能在很短时间内看懂本节介绍的汇编语言。但是要很熟练地阅读和编写汇编程序,则需要长期的训练。上世纪80年代末,求伯君闭关一年多,单枪匹马用洋洋洒洒数万行汇编代码开发出DOS版WPS,由此奠定江湖大佬地位,但时至今日,这样的个人英雄主义时代已经一去不复还。除了信息安全和软件逆向分析等少数领域外,需要使用汇

SheIsC的专栏 2097

C编译器剖析

c编译器

SIC知识(8)--碳化硅的制备难点

6. 器件制造难点:碳化硅器件制造过程中的掺杂步骤需要在高温下完成,且需要采用高温离子注入的方式,这与传统硅基器件的扩散工艺不同。这些步骤需要精密的设备和严格的工艺控制,以确保器件的结构和功能。综上所述,碳化硅的制备难点相较于传统硅基器件主要在于更高的制备温度、更慢的生长速度、晶型控制的复杂性、后加工的难度、外延工艺的低效率以及器件制造的特殊要求和较高的成本。- **数据管理**:需要有效的数据管理系统来跟踪和记录生产过程中的每一个步骤,包括原料的批次、生长条件、加工参数等,以确保产品质量的可追溯性。

阿拉伯梳子的专栏 1595

C编译器剖析_C语言的变参函数

C语言的变参函数     UCC编译器中有不少地方使用了C语言的变参函数,这里我们专门用一小节来对C语言变参函数的实现原理进行分析。C标准库中的printf函数就是一个典型的变参函数,其接口如下所示,函数声明中的省略号…表明这是一个变参函数。          int printf(const char *format, ...);     下面我们举一个简单的例子来说明printf函数的调

SheIsC的专栏 2106

智能电网中的智能调度与优化算法研究

本文研究智能电网中的智能调度与优化算法,旨在提高电网运行的效率与可靠性,减少能源浪费和设备故障的风险。在智能电网日益复杂化的背景下,传统的调度方法已难以满足高效、稳定的运行要求,因此亟需借助先进的优化算法对电网进行智能调度。本文第一深入分析智能电网调度中的关键问题与挑战,探讨优化算法在智能电网中的应用现状,并分析常见的优化算法,如遗传算法、粒子群算法和模拟退火算法。随后,本文构建智能电网的调度优化数学模型,并对电网负荷、发电模型及约束条件进行详细建模。在算法部分,本文提出改进的粒子群算法和遗传算法,并结合M

weixin_44553616的博客 1889

你真的了解C语言吗?(深度剖析C语言第一期)

xdm,从今天开始我们来重新拾起C语言,或许有人会问,C语言不是学完了吗?其实有时候可以仔细思考一下,作为经典语言之一,多少前辈多少大牛为其付之一生,岂是你我一年半载能学完的?哈哈,至少小生在学校确实只是学到了C的一点皮毛语法罢了。因此,我想带着大家重新拾起C语言,我们更深入地去学一下,加油,技术人!!!! 我们先打个招呼: 保持空杯心态,我们重新审视这个第一个C语言程序。我们以上在vs写的代码时是用文本写的代码,在编译链接的时候编译器将该文本代码变为二进制序列的可执行程序。同时这个可执行程序也..

qq_59955115的博客 4万+

别再花钱买商业软件了!这4款免费DIC工具,从散斑生成到应变分析全搞定

本文介绍了4款免费开源的DIC(数字图像相关技术)工具,涵盖从散斑生成到应变分析的全流程,帮助用户零成本构建专业级DIC工作流。这些工具在精度、算法透明度和生态整合方面具有优势,适用于材料力学和实验力学研究,可替代昂贵的商业软件。

weixin_42525369的博客 168

国产麒麟系统下用Rider开发C#桌面应用:从环境配置到Avalonia UI实战

本文详细介绍了在国产麒麟系统上使用JetBrains Rider开发C#桌面应用的完整流程,重点讲解了Avalonia UI框架的实战应用。从环境配置、Rider安装到Avalonia项目创建和UI开发技巧,全面覆盖了在非Windows环境下进行C#开发的关键步骤和优化建议,助力开发者高效构建跨平台桌面应用。

weixin_27791839的博客 387
上一篇: C编译器剖析_2.3 内存管理
下一篇: C编译器剖析_2.5 UCC编译器的符号表管理
SheIsC
博客等级 码龄12年 224粉丝 64原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值