我正在阅读K&R的“ C编程语言”,并发现了这一说法[Introduction,p。1。3]:
因为大多数计算机直接支持C提供的数据类型和控制结构,所以实现自包含程序所需的运行时库很小。
粗体字表示什么意思?是否有计算机不直接支持的数据类型或控制结构的示例?
我正在阅读K&R的“ C编程语言”,并发现了这一说法[Introduction,p。1。3]:
因为大多数计算机直接支持C提供的数据类型和控制结构,所以实现自包含程序所需的运行时库很小。
粗体字表示什么意思?是否有计算机不直接支持的数据类型或控制结构的示例?
Answers:
是的,有些数据类型不直接支持。
在许多嵌入式系统上,没有硬件浮点单元。因此,当您编写如下代码时:
float x = 1.0f, y = 2.0f;
return x + y;
它被翻译成这样的东西:
unsigned x = 0x3f800000, y = 0x40000000;
return _float_add(x, y);
然后,编译器或标准库必须提供的实现_float_add()
,该实现占用嵌入式系统上的内存。如果要在一个很小的系统上计算字节数,这可能会加起来。
另一个常见的例子是64位整数(long long
自1999年以来为C标准),而32位系统不直接支持它们。旧的SPARC系统不支持整数乘法,因此必须由运行时提供乘法。还有其他例子。
相比之下,其他语言具有更复杂的原语。
例如,Lisp符号需要大量的运行时支持,就像Lua中的表,Python中的字符串,Fortran中的数组等等。C中的等效类型通常根本不是标准库的一部分(没有标准符号或表),或者它们更简单并且不需要太多的运行时支持(C中的数组基本上只是指针,以n结尾的字符串是几乎一样简单)。
C语言中缺少的一个值得注意的控制结构是异常处理。非本地出口仅限于setjmp()
和longjmp()
,它们仅保存和恢复处理器状态的某些部分。相比之下,C ++运行时必须遍历堆栈并调用析构函数和异常处理程序。
实际上,我敢打赌,自1978年Kernighan和Ritchie在书的第一版中首次编写这些内容以来,引言的内容并没有太大变化,并且它们在那时比在更现代的意义上参考了C的历史和演变。实现。
从根本上说,计算机只是存储库和中央处理器,每个处理器都使用机器代码运行。每个处理器设计的一部分是指令集架构,称为汇编语言,该将一组人类可读的助记符一对一地映射到机器码(全数字)。
C语言的作者以及紧随其后的B和BCPL语言的作者都希望使用尽可能有效地编译为汇编语言的结构来定义……实际上,由于目标的局限性,他们不得不这样做硬件。正如其他答案所指出的那样,这涉及分支(GOTO和C中的其他流控制),移动(分配),逻辑运算(&| ^),基本算术(加,减,递增,递减)和内存寻址(指针)。 )。一个很好的例子是C中的pre / post递增和递减运算符,据说它们是Ken Thompson添加到B语言中的,特别是因为它们能够在编译后直接转换为单个操作码。
这就是作者说“大多数计算机直接支持”的意思。它们并不意味着其他语言包含不直接支持的类型和结构,而是意味着通过设计 C结构可以最直接地翻译(有时字面意思是直接)转换为Assembly。
与底层程序集的这种紧密联系,尽管仍然提供了结构化编程所需的所有元素,却导致了C语言的早期采用,并使它在当今仍然是关键的编译效率环境中成为流行语言。
有关该语言历史的有趣文章,请参见《 C语言的发展-Dennis Ritchie》。
简短的答案是,目标计算机的微处理器也支持C支持的大多数语言构造,因此,编译后的C代码非常好且高效地转换为微处理器的汇编语言,从而导致代码更小,占用空间更小。
较长的答案需要一点汇编语言知识。在C中,这样的语句:
int myInt = 10;
会在汇编中转换为如下形式:
myInt dw 1
mov myInt,10
将此与类似C ++的东西进行比较:
MyClass myClass;
myClass.set_myInt(10);
产生的汇编语言代码(取决于MyClass()的大小)可能会增加多达数百条汇编语言行。
如果没有实际使用汇编语言创建程序,纯C可能是您可以在其中编写程序的“最细”和“最紧”的代码。
编辑
鉴于对我的回答的评论,我决定进行测试,只是出于我自己的理智。我创建了一个名为“ 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
然后,我创建一个名为“ 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”进行比较,也有很多额外的内容。
MyClass myClass { 10 }
C ++ 这样的真实代码很可能会编译为完全相同的程序集。现代C ++编译器消除了抽象损失。结果,它们经常可以击败C编译器。例如,C中的抽象代价qsort
是真实的,但是std::sort
即使经过基本优化,C ++ 也没有抽象代价。
K&R表示大多数C表达式(技术含义)映射到一个或几个汇编指令,而不是对支持库的函数调用。通常的例外情况是在没有硬件div指令的体系结构上进行整数除法,或者在没有FPU的计算机上进行浮点运算。
有一个报价:
C将汇编语言的灵活性和强大功能与汇编语言的用户友好性相结合。
(在这里找到。我想我还记得一个不同的变化,例如“汇编语言的速度与汇编语言的便捷性和表达性”。)
一些高级语言定义了其数据类型的确切宽度,并且所有计算机上的实现都必须工作相同。但是不是C。
如果要在x86-64上使用128位整数,或者在一般情况下使用任意大小的BigInteger,则需要一个函数库。现在,所有CPU都使用2s补码作为负整数的二进制表示形式,但是即使在设计C时也是如此。(这就是为什么在非2s补码机上会产生不同结果的某些事情在C标准中在技术上是未定义的。)
如果您需要引用计数引用,则必须自己进行。如果要让c ++虚拟成员函数根据指针所指向的对象类型调用不同的函数,则C ++编译器必须生成的不仅仅是一个call
一个固定地址指令。
在库函数之外,仅提供的字符串操作是读/写字符。没有concat,没有子字符串,没有搜索。(字符串'\0'
以8位整数的nul终止()数组存储,而不是指针+长度,因此,要获得子字符串,必须将nul写入原始字符串。)
CPU有时具有专为字符串搜索功能使用的指令,但通常仍会在循环中每执行一条指令处理一个字节。(或使用x86 rep前缀。也许如果C是在x86上设计的,则字符串搜索或比较将是本机操作,而不是库函数调用。)
许多其他答案还提供了本机不支持的示例,例如异常处理,哈希表,列表。K&R的设计理念是C本身没有这些的原因。
有没有计算机不直接支持的数据类型或控制结构的示例?
所有基本数据类型及其在C语言中的操作都可以通过一个或几个机器语言指令来实现,而无需循环-它们(实际上是每个CPU)直接支持它们。
几种流行的数据类型及其操作需要数十条机器语言指令,或者需要迭代某些运行时循环,或者两者都需要。
许多语言对于此类类型及其操作都有特殊的缩写语法-在C语言中使用此类数据类型通常需要键入更多代码。
这些数据类型和操作包括:
所有这些操作都需要数十条机器语言指令,或者需要在几乎每个处理器上迭代一些运行时循环。
一些流行的控制结构也需要数十种机器语言指令或循环,包括:
无论用C语言还是其他某种语言编写,当程序处理此类数据类型时,CPU最终都必须执行处理这些数据类型所需的任何指令。这些说明通常包含在“库”中。每种编程语言,甚至C语言,都有针对每个平台的“运行时库”,默认情况下包含在每个可执行文件中。
大多数编写编译器的人都将用于处理“内置于语言中”的所有数据类型的指令放入其运行时库中。由于C 语言没有在语言中内置上述任何数据类型,操作和控制结构,因此C运行时库中都不包含它们-这使得C运行时库小于运行时库其他编程语言的时间库,其中包含上述语言的更多内容。
当程序员希望使用C或他选择的任何其他语言的程序来处理非“内置于该语言中”的其他数据类型时,该程序员通常会告诉编译器在该程序中包括其他库,有时(以“避免依赖关系”)直接在程序中编写这些操作的另一个实现。
什么是内置数据类型 C
?他们之类的东西int
,char
,* int
,float
,数组等等......这些数据类型由CPU的理解。CPU知道如何使用数组,如何取消对指针的引用以及如何对指针,整数和浮点数执行算术运算。
但是,当您使用高级编程语言时,您已经内置了抽象数据类型和更复杂的结构。例如,查看C ++编程语言中大量的内置类。CPU不理解类,对象或抽象数据类型,因此C ++运行时弥合了CPU和语言之间的鸿沟。这些是大多数计算机不直接支持的数据类型的示例。
这取决于计算机。在发明了C的PDP-11上,long
支持不佳(您可以购买一个可选的附加模块来支持某些(但不是全部)32位操作)。在任何16位系统上,包括原始IBM PC,在不同程度上都是如此。同样,对于32位计算机或32位程序中的64位操作,尽管在编写K&R书时的C语言根本没有任何64位操作。当然,整个80年代和90年代有很多系统[包括386和486处理器],甚至今天的某些嵌入式系统都没有直接支持浮点算术(float
或double
)。
举一个更奇特的例子,某些计算机体系结构仅支持“面向字的”指针(指向内存中的两个字节或四个字节的整数),并且字节指针(char *
或void *
)必须通过添加额外的偏移量字段来实现。这个问题会详细介绍此类系统。
它所指的“运行时库”功能不是您将在手册中看到的功能,而是现代编译器的运行时库中的此类功能,这些功能用于实现机器不支持的基本类型操作。K&R自己指的运行时库可以在Unix Heritage Society的网站上找到 -您可以看到类似ldiv
的函数(与同名的C函数不同,当时还不存在),这些函数用于实现PDP-11不支持的32位值,即使使用该插件也是如此csv
(以及cret
csv.c中的值),这些值在堆栈上保存和恢复寄存器以管理函数的调用和返回。
他们可能还会提到他们选择不支持底层机器不直接支持的许多数据类型,这与其他现代语言(例如FORTRAN)不同,后者的数组语义不能很好地映射到CPU的底层指针支持,例如C的数组。C数组始终为零索引并且在所有列中始终具有已知大小,但第一个事实意味着无需存储数组的索引范围或大小,也不需要运行时库函数来访问它们-编译器可以简单地对必要的指针算法进行硬编码。
该语句仅表示C中的数据和控制结构是面向机器的。
这里有两个方面需要考虑。一种是C语言有一个定义(ISO标准),它允许自由定义数据类型。这意味着C语言实现是针对计算机量身定制的。C编译器的数据类型与编译器针对的计算机中的可用数据类型匹配,因为该语言对此具有一定的自由度。如果一台机器的字长异常,例如36位,则可以使类型int
或long
符合该字长。假定int
正好是32位的程序将中断。
其次,由于这种便携性问题,还有第二个效果。在某种程度上,K&R中的语句已经变成一种自我实现的预言,或者可能是相反的。也就是说,新处理器的实现者意识到支持C编译器的迫切需求,并且他们知道存在许多C代码,这些代码假定“每个处理器看起来都像80386”。架构设计时要考虑C:不仅要考虑C,而且还要考虑到关于C可移植性的常见误解。您根本不能介绍具有9位字节的机器或任何其他用于通用用途的机器。假定类型的程序char
恰好8位宽会中断。只有可移植性专家编写的某些程序才能继续工作:可能还不足以合理的努力将一个完整的系统与一个工具链,内核,用户空间和有用的应用程序组合在一起。换句话说,C类型看起来像可从硬件获得的类型,因为使该硬件看起来像是为其编写了许多不可移植的C程序的某些其他硬件。
有没有计算机不直接支持的数据类型或控制结构的示例?
许多机器语言不直接支持的数据类型:多精度整数;链表;哈希表; 字符串。
大多数机器语言不直接支持的控制结构:一流的延续;协程/线程; 发电机; 异常处理。
所有这些都需要使用大量通用指令和更多基本数据类型创建的可观的运行时支持代码。
C具有某些机器不支持的一些标准数据类型。从C99开始,C具有复数。它们由两个浮点值组成,并可以与库例程一起使用。有些机器根本没有浮点单元。
关于某些数据类型,尚不清楚。如果机器支持使用一个寄存器作为基址,另一个使用缩放位移来寻址内存,这是否意味着数组是直接支持的数据类型?
同样,谈到浮点,存在标准化:IEEE 754浮点。为什么您的C编译器具有double
与处理器支持的浮点格式一致的原因,不仅是因为两者是一致的,而且还因为该表示具有独立的标准。
直接支持应该被理解为有效地映射到处理器的指令集。
除了长(可能需要扩展的算术例程)和短大小(可能需要屏蔽)之外,直接支持整数类型是规则。
对浮点类型的直接支持要求FPU可用。
直接支持位字段是例外。
结构和数组需要地址计算,在某种程度上可以直接支持。
始终总是通过间接寻址直接支持指针。
无条件/条件分支直接支持goto / if / while / for / do。
应用跳转表时可以直接支持该开关。
堆栈功能直接支持函数调用。