저는 K & R의 “The C Programming Language” 를 읽고이 문장을 보았습니다. [Introduction, p. 삼]:
C에서 제공하는 데이터 유형과 제어 구조는 대부분의 컴퓨터 에서 직접 지원되기 때문에 자체 포함 프로그램을 구현하는 데 필요한 런타임 라이브러리는 매우 작습니다.
굵은 글씨는 무엇을 의미합니까? 컴퓨터에서 직접 지원 하지 않는 데이터 유형 또는 제어 구조의 예가 있습니까?
답변
예, 직접 지원되지 않는 데이터 유형이 있습니다.
많은 임베디드 시스템에는 하드웨어 부동 소수점 단위가 없습니다. 따라서 다음과 같은 코드를 작성할 때 :
float x = 1.0f, y = 2.0f;
return x + y;
다음과 같이 번역됩니다.
unsigned x = 0x3f800000, y = 0x40000000;
return _float_add(x, y);
그런 다음 컴파일러 또는 표준 라이브러리는 _float_add()
임베디드 시스템에서 메모리를 차지하는 의 구현을 제공 해야합니다. 정말 작은 시스템에서 바이트를 세는 경우이 값이 추가 될 수 있습니다.
또 다른 일반적인 예는 long long
32 비트 시스템에서 직접 지원되지 않는 64 비트 정수 ( 1999 년 이후 C 표준)입니다. 구형 SPARC 시스템은 정수 곱셈을 지원하지 않았기 때문에 런타임에서 곱셈을 제공해야했습니다. 다른 예가 있습니다.
다른 언어
이에 비해 다른 언어에는 더 복잡한 기본 형식이 있습니다.
예를 들어 Lisp 기호는 Lua의 테이블, Python의 문자열, Fortran의 배열 등과 같이 많은 런타임 지원이 필요합니다. C의 동등한 유형은 일반적으로 표준 라이브러리의 일부가 아니거나 (표준 기호 또는 테이블 없음) 훨씬 더 간단하고 많은 런타임 지원이 필요하지 않습니다 (C의 배열은 기본적으로 포인터 일 뿐이며 nul로 끝나는 문자열은 거의 간단합니다).
제어 구조
C에서 누락 된 주목할만한 제어 구조는 예외 처리입니다. 비 로컬 종료는 프로세서 상태의 특정 부분을 저장하고 복원하는 setjmp()
및 로 제한됩니다 longjmp()
. 이에 비해 C ++ 런타임은 스택을 탐색하고 소멸자 및 예외 처리기를 호출해야합니다.
답변
사실,이 소개의 내용은 Kernighan과 Ritchie가 책의 초판에 처음 썼던 1978 년 이후 크게 변하지 않았을 것입니다. 그리고 그들은 현대보다 그 당시 C의 역사와 진화를 언급합니다. 구현.
컴퓨터는 기본적으로 메모리 뱅크와 중앙 프로세서 일 뿐이며 각 프로세서는 기계 코드를 사용하여 작동합니다. 각 프로세서 설계의 일부는 어셈블리 언어 라고하는 명령어 세트 아키텍처로, 사람이 읽을 수있는 니모닉 세트에서 모든 숫자 인 기계어 코드로 일대일로 매핑됩니다.
C 언어의 작성자와 바로 앞의 B 및 BCPL 언어는 가능한 한 효율적으로 Assembly로 컴파일 된 언어로 구문을 정의하려고했습니다. 사실, 그들은 대상의 제한에 의해 강제되었습니다. 하드웨어. 다른 답변에서 지적했듯이 여기에는 분기 (GOTO 및 C의 기타 흐름 제어), 이동 (할당), 논리 연산 (& | ^), 기본 산술 (더하기, 빼기, 증가, 감소) 및 메모리 주소 지정 (포인터 ). 좋은 예는 C의 pre- / post-increment 및 decrement 연산자입니다.이 연산자는 컴파일 된 후 단일 opcode로 직접 변환 할 수 있었기 때문에 Ken Thompson이 B 언어에 추가 한 것으로 추정됩니다.
이것이 저자들이 “대부분의 컴퓨터에서 직접 지원한다”는 말의 의미입니다. 다른 언어에 직접 지원 되지 않는 유형과 구조가 포함되어 있다는 의미가 아니라 설계 상 C 구조 가 어셈블리로 가장 직접 (때로는 문자 그대로 직접) 번역 .
구조화 된 프로그래밍에 필요한 모든 요소를 제공하면서도 기본 어셈블리와의 밀접한 관계는 C를 조기 채택하게했으며, 코드 컴파일의 효율성이 여전히 핵심 인 환경에서 오늘날 널리 사용되는 언어로 유지되었습니다.
언어의 역사에 대한 흥미로운 글은 The Development of the C Language-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.c”와 동일한 내용을 출력하는 “test.cpp”라는 파일을 만듭니다.
#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은 대부분의 C 표현식 (기술적 의미)이 지원 라이브러리에 대한 함수 호출이 아니라 하나 또는 몇 개의 어셈블리 명령에 매핑됨을 의미합니다. 일반적인 예외는 하드웨어 div 명령어가없는 아키텍처의 정수 나누기 또는 FPU가없는 시스템의 부동 소수점입니다.
인용문이 있습니다.
C는 어셈블리 언어의 유연성과 힘을 사용자 친화적 인 어셈블리 언어와 결합합니다.
( 여기에서 찾을 수 있습니다 . “어셈블리 언어의 편리함과 표현력을 갖춘 어셈블리 언어의 속도”와 같은 다른 변형을 기억한다고 생각했습니다.)
long int는 일반적으로 기본 머신 레지스터와 동일한 너비입니다.
일부 상위 수준 언어는 데이터 유형의 정확한 너비를 정의하며 모든 시스템의 구현은 동일하게 작동해야합니다. 하지만 C는 아닙니다.
x86-64에서 128 비트 정수로 작업하거나 일반적으로 임의 크기의 BigInteger로 작업하려면이를위한 함수 라이브러리가 필요합니다. 모든 CPU는 이제 음의 정수의 이진 표현으로 2의 보수를 사용하지만 C가 설계되었을 때는 그렇지 않았습니다. (이것이 2s가 아닌 시스템에서 다른 결과를 제공 할 수있는 일부 항목이 C 표준에서 기술적으로 정의되지 않은 이유입니다.)
데이터 또는 함수에 대한 C 포인터는 어셈블리 주소와 동일한 방식으로 작동합니다.
참조 횟수 참조를 원하면 직접 수행해야합니다. 포인터가 가리키는 개체의 종류에 따라 다른 함수를 호출하는 C ++ 가상 멤버 함수를 원하는 경우 C ++ 컴파일러는call
고정 주소를 가진 명령어 합니다.
문자열은 배열 일뿐입니다.
라이브러리 함수 외부에서 제공되는 유일한 문자열 작업은 문자 읽기 / 쓰기입니다. 연결 없음, 하위 문자열 없음, 검색 없음. (문자열은 nul로 끝나는 ('\0'
포인터 + 길이가 아닌 8 비트 정수의 ) 배열 되므로 부분 문자열을 얻으려면 원래 문자열에 nul을 써야합니다.)
CPU에는 때때로 문자열 검색 기능에서 사용하도록 설계된 명령어가 있지만 일반적으로 루프에서 실행되는 명령어 당 1 바이트를 처리합니다. (또는 x86 rep 접두사를 사용합니다. C가 x86에서 설계된 경우 문자열 검색 또는 비교는 라이브러리 함수 호출이 아닌 기본 작업 일 수 있습니다.)
다른 많은 답변은 예외 처리, 해시 테이블, 목록과 같이 기본적으로 지원되지 않는 것들의 예를 제공합니다. K & R의 디자인 철학은 C가 기본적으로 이러한 것을 갖지 않는 이유입니다.
답변
프로세스의 어셈블리 언어는 일반적으로 점프 (이동), 문, 이동 문, 이진 관절염 (XOR, NAND, AND OR 등), 메모리 필드 (또는 주소)를 다룹니다. 메모리를 명령어와 데이터의 두 가지 유형으로 분류합니다. 그것은 어셈블리 언어에 관한 모든 것입니다 (어셈블리 프로그래머는 그것보다 더 많은 것이 있다고 주장 할 것이지만 일반적으로 이것으로 요약됩니다). C는이 단순함과 매우 유사합니다.
C는 대수와 산술을 조합하는 것입니다.
C는 어셈블리의 기본 (프로세서 언어)을 캡슐화합니다. “C에서 제공하는 데이터 유형과 제어 구조는 대부분의 컴퓨터에서 직접 지원되기 때문에”보다 더 사실 일 것입니다.
답변
오해의 소지가있는 비교주의
- 이 성명서 는 적어도 주류 고수준 언어의 경우 대부분 유행에서 벗어난 “런타임 라이브러리”개념에 의존 합니다. (이는 여전히 가장 작은 임베디드 시스템과 관련이 있습니다.) 런타임은 해당 언어에 내장 된 구성 만 사용할 때 해당 언어의 프로그램을 실행하는 데 필요한 최소한의 지원입니다 (라이브러리에서 제공하는 함수를 명시 적으로 호출하는 것과 반대). .
- 대조적으로, 현대 언어는 런타임과 표준 라이브러리 를 구별 하지 않는 경향이 있으며 후자는 종종 상당히 광범위합니다.
- K & R 책 당시 C는 표준 라이브러리 조차 없었습니다. . 오히려, 사용 가능한 C 라이브러리는 Unix의 다른 종류간에 상당히 달랐습니다.
- 문을 이해하기 위해 표준 라이브러리가있는 언어 (예 : 다른 답변에서 언급 된 Lua 및 Python) 와 비교해서는 안되며 , 더 많은 기본 제공 구조가있는 언어 (예 : 다른 곳에서 언급 된 이전 LISP 및 이전 FORTRAN)와 비교해야합니다. 답변). 다른 예로는 BASIC (LISP와 같은 인터랙티브) 또는 PASCAL (FORTRAN과 같은 컴파일)이 있으며 둘 다 언어 자체에 내장 된 입출력 기능이 있습니다.
- 반대로 라이브러리가 아닌 런타임 만 사용하는 C 프로그램에서 계산 결과를 얻는 표준 방법은 없습니다.
답변
컴퓨터에서 직접 지원하지 않는 데이터 유형 또는 제어 구조의 예가 있습니까?
C 언어의 모든 기본 데이터 유형과 해당 작업은 루프없이 하나 또는 몇 개의 기계 언어 명령으로 구현할 수 있습니다. 이는 (실제로 모든) CPU에서 직접 지원됩니다.
널리 사용되는 여러 데이터 유형 및 해당 작업에는 수십 개의 기계 언어 명령이 필요하거나 일부 런타임 루프를 반복하거나 둘 다 필요합니다.
많은 언어에는 이러한 유형 및 해당 작업에 대한 특수 축약 구문이 있습니다. C에서 이러한 데이터 유형을 사용하려면 일반적으로 더 많은 코드를 입력해야합니다.
이러한 데이터 유형 및 작업에는 다음이 포함됩니다.
- 임의 길이 텍스트 문자열 조작-연결, 부분 문자열, 다른 문자열로 초기화 된 변수에 새 문자열 할당 등. ( ‘s = “Hello World!”; s = (s + s) [2 : -2] ‘파이썬에서)
- 세트
- C ++ 및 기타 모든 객체 지향 프로그래밍 언어에서와 같이 중첩 된 가상 소멸자가있는 객체
- 2D 매트릭스 곱셈 및 나눗셈; 선형 시스템 풀기 ( “C = B / A; x = A \ b”(MATLAB 및 여러 배열 프로그래밍 언어))
- 정규식
- 가변 길이 배열-특히 배열의 끝에 항목을 추가하여 (때로는) 더 많은 메모리를 할당해야합니다.
- 런타임에 유형을 변경하는 변수 값 읽기-때로는 부동 소수점이고 다른 때는 문자열입니다.
- 연관 배열 (종종 “맵”또는 “사전”이라고 함)
- 기울기
- 비율 ( “(+ 1/3 2/7)”은 Lisp에서 “13/21” 을 나타냄 )
- 임의 정밀도 산술 (종종 “bignums”라고 함)
- 데이터를 인쇄 가능한 표현으로 변환 (JavaScript의 “.tostring”메소드)
- 포화 고정 소수점 숫자 (임베디드 C 프로그램에서 자주 사용됨)
- 런타임에 입력 된 문자열을 표현식 인 것처럼 평가합니다 (많은 프로그래밍 언어에서 “eval ()”).
이러한 모든 작업에는 수십 개의 기계 언어 명령이 필요하거나 거의 모든 프로세서에서 일부 런타임 루프를 반복해야합니다.
수십 개의 기계 언어 명령 또는 루핑이 필요한 일부 인기있는 제어 구조는 다음과 같습니다.
- 폐쇄
- 연속
- 예외
- 게으른 평가
C로 작성 되든 다른 언어로 작성 되든, 프로그램이 이러한 데이터 유형을 조작 할 때 CPU는 결국 해당 데이터 유형을 조작하는 데 필요한 명령을 실행해야합니다. 이러한 지침은 종종 “라이브러리”에 포함됩니다. C를 포함한 모든 프로그래밍 언어에는 모든 실행 파일에 기본적으로 포함 된 각 플랫폼에 대한 “런타임 라이브러리”가 있습니다.
컴파일러를 작성하는 대부분의 사람들은 “언어에 내장 된”모든 데이터 유형을 조작하기위한 지침을 런타임 라이브러리에 넣습니다. C는 없기 때문에 어떤 런인보다 C 런타임 라이브러리 작은 수 -하여 언어, 그들 중 누구도이 C 런타임 라이브러리에 포함되지 않습니다에 내장 된 위의 데이터 유형과 운영 및 제어 구조를 위의 내용이 더 많이 내장 된 다른 프로그래밍 언어의 시간 라이브러리.
프로그래머가 “언어에 내장”되지 않은 다른 데이터 유형을 조작하기 위해 C 또는 자신이 선택한 다른 언어로 된 프로그램을 원할 때 해당 프로그래머는 일반적으로 해당 프로그램에 추가 라이브러리를 포함하도록 컴파일러에 지시하거나 때로는 ( “종속성을 피하기 위해”) 프로그램에서 이러한 작업의 또 다른 구현을 직접 작성합니다.