C数据types如何“被大多数计算机直接支持”?

我正在阅读K&R的“C语言程序devise语言”,并接触到了这个陈述[Introduction, 3]:

由于C提供的数据types和控制结构是大多数计算机直接支持的 ,因此实现自包含程序所需的运行时库很小。

粗体的陈述是什么意思? 有没有一个数据types或控制结构的例子, 被计算机直接支持?

是的,有不直接支持的数据types。

在许多embedded式系统中,没有硬件浮点单元。 所以,当你写这样的代码:

float x = 1.0f, y = 2.0f; return x + y; 

它被翻译成这样的东西:

 unsigned x = 0x3f800000, y = 0x40000000; return _float_add(x, y); 

然后编译器或标准库必须提供一个_float_add()的实现,它占用embedded式系统的内存。 如果你在一个非常小的系统上计算字节数,这可以加起来。

另一个常见的例子是64位整数(自1999年以来C标准中的long long整数),这并不是32位系统直接支持的。 旧的SPARC系统不支持整数乘法,所以乘法必须由运行时提供。 还有其他的例子。

其他语言

相比之下,其他语言有更复杂的原语。

例如,一个Lisp符号需要大量的运行时支持,就像Lua中的表,Python中的string,Fortran中的数组等等。 C中的等价types通常不是标准库的一部分(没有标准符号或表),或者它们更简单,不需要太多的运行时支持(C中的数组基本上只是指针,nul结尾的string是几乎一样简单)。

控制结构

C中缺less一个值得注意的控制结构是exception处理。 非局部出口仅限于setjmp()longjmp() ,它们只是保存和恢复处理器状态的某些部分。 相比之下,C ++运行时必须遍历堆栈并调用析构函数和exception处理程序。

实际上,我敢打赌,自从1978年Kernighan和Ritchie在本书的第一版中首次写到这本介绍的内容以来,这个介绍的内容并没有太大改变,他们提到当时C的历史和演变,而不是现代实现。

计算机基本上只是存储库和中央处理器,每个处理器都使用机器码进行操作; 每个处理器的devise的一部分是一个指令集体系结构,称为汇编语言 ,它从一套人类可读的助记符一一映射到机器码,这是所有的数字。

C语言的作者 – 以及紧接其后的B和BCPL语言 – 都试图用语言来定义结构,尽可能有效地汇编到汇编中…实际上,它们被迫通过限制目标硬件。 正如其他答案已经指出的,这涉及到分支(C中的GOTO和其他stream控制),移动(赋值),逻辑运算(&| ^),基本算术(加法,减法,递增,递减)和内存寻址(指针)。 一个很好的例子是C语言中的前/后增加和减less运算符,Ken Thompson特意将其添加到B语言中,因为它们能够直接翻译为一个编译完成的操作码。

这就是作者所说的“大多数计算机直接支持”的意思。 他们并不意味着其他语言包含了直接支持的types和结构 – 这意味着通过devise, C构造可以直接(有时直接)直接翻译成Assembly。

与底层大会的这种密切的联系虽然仍然提供了结构化编程所需的全部元素,但却导致了C的早期采用,以及在编译代码的效率仍然至关重要的环境中,它仍然是当今stream行的语言。

关于语言历史的有趣的写作,请参阅C语言的发展 – Dennis Ritchie

简而言之,C语言所支持的语言结构大部分也是由目标计算机的微处理器所支持的,因此编译后的C代码对于微处理器的汇编语言来说是非常好的和高效的,从而导致代码更小,占用空间更小。

较长的答案需要一点汇编语言知识。 在C中,像这样的陈述:

 int myInt = 10; 

会在汇编中转化为这样的东西:

 myInt dw 1 mov myInt,10 

比较这个像C ++的东西:

 MyClass myClass; myClass.set_myInt(10); 

生成的汇编语言代码(取决于MyClass()的大小)可以添加多达数百个汇编语言行。

如果不用真正的汇编语言编写程序,纯C可能是你可以编写程序的“最瘦”和“最紧凑”的代码。

编辑

鉴于我对答案的评论,我决定进行testing,仅仅是为了我自己的理智。 我创build了一个名为“test.c”的程序,如下所示:

 #include <stdio.h> void main() { int myInt=10; printf("%d\n", myInt); } 

我把它编译成使用gcc进行汇编。 我用下面的命令行来编译它:

 gcc -S -O2 test.c 

这是汇编语言:

  .file "test.c" .section .rodata.str1.1,"aMS",@progbits,1 .LC0: .string "%d\n" .section .text.unlikely,"ax",@progbits .LCOLDB1: .section .text.startup,"ax",@progbits .LHOTB1: .p2align 4,,15 .globl main .type main, @function main: .LFB24: .cfi_startproc movl $10, %edx movl $.LC0, %esi movl $1, %edi xorl %eax, %eax jmp __printf_chk .cfi_endproc .LFE24: .size main, .-main .section .text.unlikely .LCOLDE1: .section .text.startup .LHOTE1: .ident "GCC: (Ubuntu 4.9.1-16ubuntu6) 4.9.1" .section .note.GNU-stack,"",@progbits 

然后我创build一个名为“test.cpp”的文件,它定义了一个类并输出与“test.c”相同的东西:

 #include <iostream> using namespace std; class MyClass { int myVar; public: void set_myVar(int); int get_myVar(void); }; void MyClass::set_myVar(int val) { myVar = val; } int MyClass::get_myVar(void) { return myVar; } int main() { MyClass myClass; myClass.set_myVar(10); cout << myClass.get_myVar() << endl; return 0; } 

我用相同的方式编译它,使用这个命令:

 g++ -O2 -S test.cpp 

这是生成的汇编文件:

  .file "test.cpp" .section .text.unlikely,"ax",@progbits .align 2 .LCOLDB0: .text .LHOTB0: .align 2 .p2align 4,,15 .globl _ZN7MyClass9set_myVarEi .type _ZN7MyClass9set_myVarEi, @function _ZN7MyClass9set_myVarEi: .LFB1047: .cfi_startproc movl %esi, (%rdi) ret .cfi_endproc .LFE1047: .size _ZN7MyClass9set_myVarEi, .-_ZN7MyClass9set_myVarEi .section .text.unlikely .LCOLDE0: .text .LHOTE0: .section .text.unlikely .align 2 .LCOLDB1: .text .LHOTB1: .align 2 .p2align 4,,15 .globl _ZN7MyClass9get_myVarEv .type _ZN7MyClass9get_myVarEv, @function _ZN7MyClass9get_myVarEv: .LFB1048: .cfi_startproc movl (%rdi), %eax ret .cfi_endproc .LFE1048: .size _ZN7MyClass9get_myVarEv, .-_ZN7MyClass9get_myVarEv .section .text.unlikely .LCOLDE1: .text .LHOTE1: .section .text.unlikely .LCOLDB2: .section .text.startup,"ax",@progbits .LHOTB2: .p2align 4,,15 .globl main .type main, @function main: .LFB1049: .cfi_startproc subq $8, %rsp .cfi_def_cfa_offset 16 movl $10, %esi movl $_ZSt4cout, %edi call _ZNSolsEi movq %rax, %rdi call _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ xorl %eax, %eax addq $8, %rsp .cfi_def_cfa_offset 8 ret .cfi_endproc .LFE1049: .size main, .-main .section .text.unlikely .LCOLDE2: .section .text.startup .LHOTE2: .section .text.unlikely .LCOLDB3: .section .text.startup .LHOTB3: .p2align 4,,15 .type _GLOBAL__sub_I__ZN7MyClass9set_myVarEi, @function _GLOBAL__sub_I__ZN7MyClass9set_myVarEi: .LFB1056: .cfi_startproc subq $8, %rsp .cfi_def_cfa_offset 16 movl $_ZStL8__ioinit, %edi call _ZNSt8ios_base4InitC1Ev movl $__dso_handle, %edx movl $_ZStL8__ioinit, %esi movl $_ZNSt8ios_base4InitD1Ev, %edi addq $8, %rsp .cfi_def_cfa_offset 8 jmp __cxa_atexit .cfi_endproc .LFE1056: .size _GLOBAL__sub_I__ZN7MyClass9set_myVarEi, .-_GLOBAL__sub_I__ZN7MyClass9set_myVarEi .section .text.unlikely .LCOLDE3: .section .text.startup .LHOTE3: .section .init_array,"aw" .align 8 .quad _GLOBAL__sub_I__ZN7MyClass9set_myVarEi .local _ZStL8__ioinit .comm _ZStL8__ioinit,1,1 .hidden __dso_handle .ident "GCC: (Ubuntu 4.9.1-16ubuntu6) 4.9.1" .section .note.GNU-stack,"",@progbits 

正如您可以清楚地看到的那样,C ++文件生成的汇编文件比C文件大得多。 即使你把所有其他的东西都删掉了,只是比较C“main”和C ++“main”,还有很多额外的东西。

K&R意味着大多数Cexpression式(技术含义)映射到一个或几个汇编指令,而不是对支持库的函数调用。 通常的例外是没有硬件div指令的架构上的整数除法,或者没有FPU的机器上的浮点数。

有一个报价:

C将汇编语言的灵活性和强大function与汇编语言的用户友好性相结合。

( 在这里find了 ,我想我记得不同的变体,比如“汇编语言的速度与汇编语言的便利性和expression性”)。

long int通常与本地机器寄存器的宽度相同。

一些更高级别的语言定义了它们的数据types的确切宽度,所有机器上的实现必须相同。 不是C,但。

如果您想在x86-64上使用128位整数,或者在任意大小的一般情况下使用BigInteger,则需要一个函数库。 现在所有的CPU都使用二进制补码作为负整数的二进制表示,但即使这样,在deviseC时也不是这样。 (这就是为什么一些能够在非二进制补码机器上得到不同结果的东西在C标准中在技术上是不确定的。)

C指向数据或函数的指针与汇编地址的工作方式相同。

如果你想重新计算参考,你必须自己做。 如果你希望c ++虚拟成员函数根据你的指针所指向的对象来调用不​​同的函数,那么C ++编译器必须产生的不仅仅是一个带有固定地址的call指令。

string只是数组

在库函数之外,唯一提供的string操作是读/写字符。 没有concat,没有子串,没有search。 (string存储为8位整数的nul-terminated( '\0' )数组,而不是指针+长度,所以要得到一个子string,你必须写一个nul到原始string中。

有时CPU有指令供stringsearchfunction使用,但通常每个指令在一个循环中执行一个字节。 (或者用x86 rep前缀,也许如果C是在x86上devise的,那么stringsearch或比较就是一个本地操作,而不是一个库函数调用)。

许多其他的答案给出了不是本机支持的东西的例子,比如exception处理,散列表,列表。 K&R的devise理念是C本身没有这些的原因。

一个进程的汇编语言通常涉及跳转(去),语句,移动语句,二进制关节炎(XOR,NAND,AND或等),存储器字段(或地址)。 内存分为指令和数据两种types。 这就是所有的汇编语言(我相信汇编程序员会争辩说,除此之外还有更多的东西,但归结起来就是这样)。 C非常类似于这种简单。

C就是把算术组装成什么代数。

C封装了汇编的基础知识(处理器的语言)。 可能比“由于C提供的数据types和控制结构直接被大多数计算机支持”

小心误导性的比较

  1. 这个声明依赖于“运行时间库”概念,因为至less对于主stream的高级语言而言,这个概念已经大大地失去了意义。 (对于最小的embedded式系统来说,它仍然是相关的。)运行时间是当你只使用内置在语言中的构造时(与显式地调用库提供的函数相反) 。
  2. 相比之下,现代语言倾向于不区分运行时和标准库 ,后者往往相当广泛。
  3. 在K&R书的时候, C甚至没有一个标准的图书馆 。 相反,可用的C库在不同风格的Unix之间差别很大。
  4. 为了理解这个语句,你不应该把它与带有标准库的语言 (比如其他答案中提到的Lua和Python)进行比较,而应该使用更多内置结构的语言(比如旧版LISP和其他旧版FORTRAN答案)。 其他的例子是BASIC(交互式的,就像LISP)或者PASCAL(像FORTRAN一样编译),它们都具有内置在语言本身中的input/输出特性。
  5. 相比之下,没有一个标准的方法可以从仅使用运行时的C程序中获得计算结果,而不是任何库。

有没有一个数据types或控制结构的例子,不被计算机直接支持?

所有的基本数据types及其在C语言中的操作都可以通过一个或几个机器语言指令来实现,而不需要循环 – 它们直接由(实际上每个)CPU支持。

一些stream行的数据types及其操作需要数十个机器语言指令,或者需要迭代一些运行时循环,或者两者兼而有之。

许多语言对这些types及其操作都有特殊的缩写语法 – 在C中使用这种数据types通常需要input更多的代码。

这些数据types和操作包括:

  • 任意长度的文本string操作 – 连接,子string,将一个新的string赋值给一个用其他string初始化的variables等等('s =“Hello World!”; s =(s + s)[2:-2] '在Python中)
  • 对象与嵌套的虚拟析构函数,在C + +和其他每个面向对象的编程语言
  • 2Dmatrix乘法和除法; 在MATLAB和许多数组编程语言中求解线性系统(“C = B / A; x = A \ b”)
  • 常用expression
  • 可变长度的数组 – 特别是将一个项目附加到数组的末尾,这有时需要分配更多的内存。
  • 读取在运行时改变types的variables的值 – 有时是浮点数,有时是string
  • 关联数组(通常称为“地图”或“字典”)
  • 名单
  • 比率(“(+ 1/3 2/7)” 在Lisp中给出了“13/21”)
  • 任意精度算术(通常称为“bignums”)
  • 将数据转换成可打印的表示(JavaScript中的“.tostring”方法)
  • 饱和定点数字(通常用于embedded式C程序)
  • 评估在运行时键入的string就好像它是一个expression式(许多编程语言中的“eval()”)。

所有这些操作都需要几十个机器语言指令,或者需要在几乎每个处理器上迭代一些运行时循环。

一些stream行的控制结构也需要数十种机器语言指令或循环,包括:

  • closures
  • 延续
  • 例外
  • 懒惰的评价

无论是用C还是其他语言编写,当程序操纵这些数据types时,CPU必须最终执行所需的任何指令来操作这些数据types。 这些指示通常包含在“图书馆”中。 每一种编程语言,甚至是C语言,都为每个可执行文件默认包含的每个平台都有一个“运行时库”。

编写编译器的大多数人都将操作“内置在该语言中”的所有数据types的指令放入其运行时库中。 由于C没有任何上述的数据types以及内置在该语言中的操作和控制结构,它们都不包含在C运行时库中 – 这使得C运行时库比运行时库小,其他编程语言的实时库有更多的上述内置的语言。

当一个程序员想要一个程序 – 用C语言或者他select的其他语言 – 来操作其他没有被“内置到语言中”的数据types时,程序员通常会告诉编译器在程序中包含额外的库,或者有时候(“避免依赖”)直接在程序中写入这些操作的另一个实现。

C中的内置数据types是什么? 它们是像intchar* intfloat ,arrays等等这些数据types可以被CPU理解。 CPU知道如何处理数组,如何解引用指针以及如何对指针,整数和浮点数进行算术运算。

但是当你使用更高级的编程语言时,你已经build立了抽象的数据types和更复杂的结构。 例如,查看C ++编程语言中的大量内置类。 CPU不理解类,对象或抽象数据types,因此C ++运行时桥接了CPU和语言之间的差距。 这些是大多数计算机不直接支持的数据types的示例。

这取决于电脑。 在发明C的PDP-11上, long得不到支持(有一个可选购的附加模块可以支持一些但不是全部的32位操作)。 在任何16位系统(包括原来的IBM PC)上也是如此。 对于32位机器或32位程序中的64位操作也是如此,尽pipeK&R书籍的C语言根本没有任何64位操作。 当然,在80年代和90年代,还有许多系统(包括386和486处理器),甚至是一些当前没有直接支持浮点运算( floatdouble float )的embedded式系统。

对于一个更奇特的例子,一些计算机体系结构只支持“面向字的”指针(指向内存中的两字节或四字节整数),而字节指针( char *void * )必须通过添加额外的抵消领域。 这个问题对这样的系统进行了一些细节。

它所指的“运行时库”function并不是您将在手册中看到的function,而是在现代编译器的运行时库中用于实现机器支持的基本types操作的function。 K&R自己所指的运行时库可以在Unix遗产协会的网站上find – 你可以看到ldiv这样的函数(与当时不存在的同名C函数截然不同)即使使用附加组件,PDP-11也不支持的32位值,以及保存和恢复堆栈上的寄存器来pipe理调用和从函数返回的csv (和cret也在csv.c中)。

他们可能也提到他们select不支持底层机器不直接支持的许多数据types,而不像其他当代语言,如FORTRAN,它的数组语义并没有映射到CPU的底层指针支持, C的数组。 C数组始终是零索引,并且在所有行中总是已知大小的事实,但首先意味着不需要存储数组的索引范围或大小,也不需要运行时库函数来访问它们,编译器可以简单地硬编码必要的指针算术。

声明只是意味着C中的数据和控制结构是面向机器的。

这里有两个方面要考虑。 一个是C语言有一个定义(ISO标准),它允许定义数据types的纬度。 这意味着C语言的实现是为机器量身定制的 。 C编译器的数据types与编译器所针对的机器中可用的数据types相匹配,因为该语言具有这种自由度。 如果一台机器有一个不寻常的字大小,比如36位,那么可以使intlongtypes符合那个。 假定int正好是32位的程序将会中断。

其次,由于这样的便携性问题,还有第二个影响。 在某种程度上,K&R中的陈述已经成为一种自我实现的预言 ,或者也许是相反的。 也就是说,新处理器的实现者已经意识到支持C编译器的强烈需求,并且他们知道存在大量的C代码,它假定“每个处理器看起来像80386”。 C架构的devise思想是:C不仅考虑C,而且也考虑到C可移植性的普遍误解。 你根本不能引入一台具有9位字节的机器或其它用于一般用途的机器。 假定chartypes正好是8位宽的程序将会中断。 只有可移植性专家编写的一些程序才能继续工作:通过合理的努力,可能不足以将一个完整的系统与工具链,内核,用户空间和有用的应用程序结合在一起。 换句话说,Ctypes看起来像硬件中可用的东西,因为硬件看起来像许多其他硬件,而这些硬件都是由许多不可移植的C程序编写的。

有没有一个数据types或控制结构的例子,不被计算机直接支持?

数据types在许多机器语言中不直接支持:多精度整数; 链表 哈希表; string。

大多数机器语言不直接支持控制结构:一级继续; 协同程序/线程; 发电机; exception处理。

所有这些都需要使用大量通用指令创build的相当长的运行时支持代码,以及更多的基本数据types。

C有一些机器不支持的标准数据types。 由于C99,C有复数。 它们由两个浮点值组成,并与库例程一起工作。 有些机器完全没有浮点单元。

关于一些数据types,目前尚不清楚。 如果一台机器支持使用一个寄存器作为基地址来寻址内存,另一个作为一个缩放的位移,这是否意味着数组是一个直接支持的数据types?

另外,就浮点而言,标准化是IEEE 754浮点。 为什么你的C编译器有一个与处理器支持的浮点格式相一致的double ,不仅仅是因为两者是一致的,而是因为这个表示有一个独立的标准。

事情如

  • 列表几乎在所有的function语言中使用。

  • 例外

  • 关联数组 (地图) – 包括在PHP和Perl中。

  • 垃圾收集

  • 包含在多种语言中的数据types/控制结构,但不直接由CPU支持。

直接支持应理解为有效映射到处理器的指令集。

  • 直接支持整数types是规则,除了长(可能需要扩展算术例程)和短尺寸(可能需要掩蔽)。

  • 直接支持浮点types需要FPU可用。

  • 直接支持位字段是例外。

  • 结构和数组需要地址计算,在一定程度上直接支持。

  • 指针总是通过间接寻址直接支持。

  • goto / if / while / for / do直接受到无条件/条件分支的支持。

  • 开关可以直接支持跳转表适用。

  • 函数调用通过堆栈特性直接支持。