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

我正在阅读 K & R 的 “The C Programming Language”,偶然发现了这句话[引言,第3页] :

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

这个粗体的声明是什么意思?isn't直接由计算机支持的数据类型或控制结构是否有示例?

7875 次浏览

是的,存在不直接支持的数据类型。

在许多嵌入式系统中,没有硬件浮点运算单元,因此,当你编写这样的代码时:

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

它被翻译成这样的东西:

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

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

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

其他语言

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

例如,Lisp 符号需要大量的运行时支持,就像 Lua 中的表、 Python 中的字符串、 Fortran 中的数组等等。C 中的等价类型通常要么根本不是标准库的一部分(没有标准符号或表) ,要么更简单,不需要太多运行时支持(C 中的数组基本上只是指针,以 null 结尾的字符串几乎同样简单)。

控制结构

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

C中的内置数据类型是什么? 它们是像 intchar* intfloat,数组等等。 这些数据类型由 CPU 理解。CPU 知道如何处理数组,如何取消引用指针,以及如何对指针、整数和浮点数执行算术运算。

但是,当您使用更高级的编程语言时,您已经内置了抽象数据类型和更复杂的结构。例如,看看 C++程式语言中大量的内置类。CPU 不理解类、对象或抽象数据类型,所以 C + + 运行时在 CPU 和语言之间架起了桥梁。这些是大多数计算机不直接支持的数据类型的示例。

简而言之,C 支持的大多数语言结构也得到了目标计算机的微处理器的支持,因此,编译后的 C 代码可以很好地高效地转换为微处理器的汇编语言,从而产生更小的代码和更小的内存占用。

较长的答案需要一点汇编语言知识:

int myInt = 10;

在汇编中可以翻译成这样的东西:

myInt dw 1
mov myInt,10

比较一下 C + + :

MyClass myClass;
myClass.set_myInt(10);

The resulting assembly language code (depending on how big MyClass() is), could add up to hundreds of assembly language lines.

如果不用汇编语言实际创建程序,纯 C 可能是可以用来编写程序的“最简洁”和“最紧凑”的代码。

剪辑

Given the comments on my answer, I decided to run a test, just for my own sanity. I created a program called "test.c", which looked like this:

#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”,还是有很多额外的东西。

进程的汇编语言通常处理跳转(转到)、语句、移动语句、二进制关节炎(XOR、 NAND、 AND OR 等)、内存字段(或地址)。将内存分为指令和数据两类。这就是汇编语言的全部意义(我相信汇编程序员会争辩说,汇编语言不仅仅是汇编语言的全部意义,而是一般意义上的汇编语言)。C 非常类似于这种简单性。

C is to assemble what algebra is to arithmetic.

C 语言封装了汇编的基础知识(处理器语言)。可能比“因为大多数计算机直接支持 C 提供的数据类型和控制结构”更真实

Actually, I'll bet that the contents of this introduction haven't changed much since 1978 when Kernighan and Ritchie first wrote them in the First Edition of the book, and they refer to the history and evolution of C at that time more than modern implementations.

计算机基本上只是内存银行和中央处理器,每个处理器使用一个机器代码运行,每个处理器设计的一部分是一个指令集架构,称为 汇编语言,它将一对一从一组人类可读的助记符映射到机器代码,这些代码都是数字。

The authors of the C language – and the B and BCPL languages that immediately preceded it – were intent upon defining constructs in the language that were as efficiently compiled into Assembly as possible ... in fact, they were forced to by limitations in the target hardware. As other answers have pointed out, this involved branches (GOTO and other flow control in C), moves (assignment), logical operations (& | ^), basic arithmetic (add, subtract, increment, decrement), and memory addressing (pointers). A good example is the pre-/post-increment and decrement operators in C, which supposedly were added to the B language by Ken Thompson specifically because they were capable of translating directly to a single opcode once compiled.

This is what the authors meant when they said "supported directly by most computers". They didn't mean that other languages contained types and structures that were 没有 supported directly - they meant that 设计好的 C constructs translated 大部分 directly (sometimes 真的 directly) into Assembly.

这种与底层 Assembly 的密切关系,虽然仍然提供了结构化编程所需的所有元素,但正是这种关系导致了 c 的早期采用,并使其在编译代码的效率仍然至关重要的环境中保持了今天的流行语言地位。

有关该语言历史的有趣文章,请参阅 < strong > C 语言的发展-Dennis Ritchie

小心误导性的比较

  1. 这个声明依赖于 “运行时库”的概念,至少对于主流高级语言来说,“运行时库”的概念已经过时了。(它仍然适用于最小的嵌入式系统。)当只使用内置在语言中的构造(而不是显式调用库提供的函数)时,运行时是该语言中的程序执行所需的最小支持。
  2. 相比之下,现代语言往往不是 区分运行时和标准库,后者往往是相当广泛的。
  3. 在绑架和绑架案发生的时候,C 语言甚至没有一个 < em > 标准 库。相反,可用的 C 库在不同风格的 Unix 之间有很大的不同。
  4. 为了理解 不应该与使用标准库的语言进行比较语句(例如其他答案中提到的 Lua 和 Python) ,但是要理解具有更多内置结构的语言(例如其他答案中提到的旧式 LISP 和旧式 FORTRAN)。其他的例子包括 BASIC (交互式的,像 LISP)或者 PASCAL (编译的,像 FORTRAN) ,它们都有内置在语言本身中的输入/输出特性。
  5. 相比之下,没有标准的方法可以从一个只使用运行时而不使用任何库的 C 程序中获得计算结果。

这取决于电脑。在发明 C 的 PDP-11上,对 long的支持很差(你可以购买一个可选的附加模块,它支持一些但不是全部32位操作)。对于任何16位系统,包括最初的 IBM PC,情况都是如此。同样,对于32位机器或32位程序中的64位操作也是如此,尽管在 K & R 书籍中的时代,C 语言根本没有任何64位操作。当然,在80年代和90年代有很多系统(包括386和一些486处理器) ,甚至今天的一些嵌入式系统,不直接支持浮点运算(floatdouble)。

对于一个更奇特的例子,一些计算机架构只支持“面向词的”指针(指向内存中的两字节或四字节整数) ,而字节指针(char *void *)必须通过添加额外的偏移量字段来实现。这个问题详细介绍了这类系统。

它引用的“运行时库”函数不是您在手册中看到的那些,而是像 在现代编译器的运行时库中这样的函数,它们用于实现机器支持的 not基本类型操作。K & R 本身引用的运行时库可以在 Unix 遗产协会的网站上找到——你可以看到像 ldiv这样的函数(不同于当时还不存在的同名 C 函数) ,它用于实现32位值的划分,PDP-11甚至不支持附加组件; 还有 csv(和 csv.c 中的 cret) ,它在堆栈上保存和恢复寄存器,以管理函数的调用和返回。

他们可能也指的是他们选择不支持许多不被底层机器直接支持的数据类型,不像其他当代语言,比如 FORTRAN,它的数组语义不像 C 语言的数组那样映射到 CPU 的底层指针支持。事实上,C 数组总是索引为零,并且在所有等级中的大小总是已知的,但是第一个意味着不需要存储数组的索引范围或大小,也不需要有运行时库函数来访问它们——编译器可以简单地硬编码必要的指针算法。

比如

  • Lists 几乎用于所有函数式语言。

  • 例外

  • 关联数组 (Maps)-包含在 PHP 和 Perl 中。

  • 垃圾收集

  • 数据类型/控制结构包含在许多语言中,但不直接受到 CPU 的支持。

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

有句话是这么说的:

C 语言将汇编语言的灵活性和强大功能与汇编语言的用户友好性结合起来。

(found here. I thought I remembered a different variation, like "speed of assembly language with the convenience and expressivity of assembly language".)

long int is usually the same width as the native machine registers.

一些高级语言定义其数据类型的确切宽度,所有计算机上的实现必须相同。但不是 C。

如果希望在 x86-64上使用128bit int,或者在一般情况下使用任意大小的 BigInteger,则需要一个函数库。现在所有的 CPU 都使用2s 补码作为负整数的二进制表示,但是在 C 被设计出来的时候情况并非如此。(这就是为什么一些在非2s 补码机上会得到不同结果的东西在 C 标准中在技术上没有定义的原因。)

指向数据或函数的 C 指针的工作方式与程序集地址相同。

如果你想要参考资料,你必须自己做。如果希望 c + + 虚拟成员函数根据指针指向的对象类型调用不同的函数,那么 C + + 编译器需要生成的不仅仅是具有固定地址的 call指令。

字符串只是数组

除了库函数之外,提供的唯一字符串运算是读/写一个字符。没有 concat,没有子字符串,没有搜索。(字符串存储为以 null 结尾的('\0')8位整数数组,而不是指针 + 长度,因此要获得子字符串,必须在原始字符串中写入一个 null。)

CPU 有时具有专门为字符串搜索函数设计的指令,但通常仍然在循环中对每条执行的指令处理一个字节。(或使用 x86 rep 前缀。如果 C 是在 x86上设计的,那么字符串搜索或比较可能是本机操作,而不是库函数调用。)

许多其他的答案给出了本机不支持的例子,比如异常处理、哈希表、列表。K & R 的设计理念就是 C 没有这些的原因。

语句仅仅意味着 C 语言中的数据和控制结构是面向机器的。

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

Secondly, because of such portability problems, there is a second effect. In a way, the statement in the K&R has become a sort of 自我实现的预言, or perhaps in reverse. That is to say, implementors of new processors are aware of the keen need for supporting C compilers, and they know that there exists a lot of C code which assumes that "every processor looks like an 80386". Architectures are designed with C in mind: and not only C in mind, but with common misconceptions about C portability in mind also. You simply can't introduce a machine with 9 bit bytes or whatever for general purpose use any more. Programs which assume that the type char is exactly 8 bits wide will break. Only some programs written by portability experts will continue to work: likely not enough to pull together a complete system with a toolchain, kernel, user space and useful applications, with reasonable effort. In other words, C types look like what is available from the hardware because the hardware was made to look like some other hardware for which many nonportable C programs were written.

是否有计算机不直接支持的数据类型或控制结构的示例?

许多机器语言不直接支持的数据类型: 多精度整数; 链表; 哈希表; 字符串。

在大多数机器语言中不直接支持的控制结构: 第一类延续; 协同程序/线程; 生成器; 异常处理。

All of these require considerable run-time support code created using numerous general purpose instructions, and more elementary data types.

C 有一些标准的数据类型,有些机器不支持这些类型。从 C99开始,C 就有了复数。它们由两个浮点值组成,用于处理库例程。有些机器根本没有浮点运算器。

关于某些数据类型,目前还不清楚。如果一台计算机支持使用一个寄存器作为基地址寻址内存,而使用另一个寄存器作为基地址寻址内存 作为一个比例位移,这是否意味着数组是一个直接支持的数据类型?

此外,说到浮点,还有标准化: IEEE754浮点。为什么你的 C 编译器有一个与处理器支持的浮点格式一致的 double,不仅仅是因为这两者是一致的,而且因为有一个独立的表示标准。

是否有数据类型或控件结构的示例不是 直接由计算机支持?

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

一些流行的数据类型及其操作需要几十条机器语言指令,或者需要迭代某些运行时循环,或者两者兼而有之。

许多语言对于这些类型及其操作都有特殊的缩写语法——在 C 语言中使用这些数据类型通常需要输入更多的代码。

这些数据类型和操作包括:

  • 任意长度的文本字符串操作——连接、子字符串、将新字符串分配给使用其他字符串初始化的变量等等(’s = “ Hello World!”在 Python 中,s = (s + s)[2:-2]’)
  • sets
  • 具有嵌套虚析构函数的对象,如在 C + + 和其他所有面向对象程序设计语言中
  • 二维矩阵乘法和除法; 求解线性系统(MATLAB 和许多阵列编程语言中的“ C = B/A; x = a b”)
  • 正则表达式
  • 可变长度数组——特别是在数组的末尾附加一个项,这(有时)需要分配更多的内存。
  • 读取在运行时改变类型的变量的值——有时是浮点数,有时是字符串
  • 联合数组(通常称为“映射”或“字典”)
  • 清单
  • (“(+ 1/32/7)”代表“13/21”在 Lisp 中)
  • 高精度计算(通常被称为“ bignums”)
  • 将数据转换为可打印的表示形式(JavaScript 中的“ . tostring”方法)
  • 饱和定点数(常用于嵌入式 C 程序)
  • 计算在运行时输入的字符串,就像它是一个表达式一样(在许多编程语言中是“ eval ()”)。

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

一些流行的控制结构也需要大量的机器语言指令或循环,包括:

  • 关闭
  • 延续
  • 例外
  • 懒惰的评估

不管是用 C 还是其他语言编写的,当程序操作这些数据类型时,CPU 最终必须执行操作这些数据类型所需的任何指令。 这些指令通常包含在“库”中。 每种编程语言,甚至是 C,对于每个平台都有一个默认包含在每个可执行文件中的“运行时库”。

大多数编写编译器的人将操作“内置到语言中”的所有数据类型的说明放入他们的运行时库中。 Because C doesn't have 任何 of the above data types and operations and control structures built into the the language, none of them are included in the C run-time library -- which makes the C run-time library smaller than the run-time library of other programming languages that have more of the above stuff built-in to the language.

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

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

  • 直接支持整数类型是规则,除了长(可能需要扩展的算术例程)和短大小(可能需要屏蔽)。

  • 对浮点类型的直接支持需要一个可用的 FPU。

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

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

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

  • Goto/if/while/for/do 直接由无条件/有条件分支支持。

  • 当应用跳转表时,可以直接支持开关。

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