C ++에 왜 '정의되지 않은 행동'(UB)이 있고 C #이나 Java와 같은 다른 언어에는 없는가?


51

이 스택 오버플로 게시물 에는 C / C ++ 언어 사양이 '정의되지 않은 동작'으로 선언 된 상당히 포괄적 인 상황 목록이 나와 있습니다. 그러나 C #이나 Java와 같은 다른 현대 언어에 왜 '정의되지 않은 동작'이라는 개념이 없는지 이해하고 싶습니다. 컴파일러 디자이너가 가능한 모든 시나리오 (C # 및 Java)를 제어 할 수 있습니까 (C 및 C ++)?




3
아직 이 SO 포스트 심지어 자바 스펙에 정의되지 않은 동작을 말한다!
gbjbaanb

"C ++에 왜 '정의되지 않은 행동' 이 있는가 ?" 불행히도, 이것은 X, Y 및 / 또는 Z (이들 모두가있을 수 있음 nullptr) 이유 " 제안 된 명세를 작성 및 / 또는 채택함으로써 행동을 정의하는 데 방해가되었다 "고 말했다. : c
code_dredd

전제에 도전합니다. 최소한 C #에는 "안전하지 않은"코드가 있습니다. 마이크로 소프트는 "안전하지 않은 코드를 작성하는 것은 C # 프로그램 내에서 C 코드를 작성하는 것과 매우 유사하다"고 쓰고 하드웨어 또는 OS에 액세스하기 위해 속도를 높이기위한 이유를 제시합니다. 이것이 C가 발명 한 것입니다 (지옥 은 C로 OS를 썼습니다 !).
피터-모니카 복원

답변:


73

정의되지 않은 행동은 회고에서만 매우 나쁜 생각으로 인식 된 것들 중 하나입니다.

첫 번째 컴파일러는 대단한 성과였으며 기계 언어 또는 어셈블리 언어 프로그래밍의 대안에 비해 크게 개선되었습니다. 이 문제는 잘 알려져 있으며, 이러한 문제를 해결하기 위해 특별히 고급 언어가 개발되었습니다. (당시의 열정은 너무 커서 HLL이 "프로그래밍의 끝"으로 불리우는 솜씨였습니다. 이제부터 우리가 원하는 것을 사소하게 작성하면 컴파일러가 모든 실제 작업을 수행 할 수 있습니다.)

우리가 새로운 접근 방식으로 발생하는 새로운 문제를 깨닫기까지는 이르지 않았습니다. 코드가 실행되는 실제 컴퓨터에서 멀어지면 예상대로 수행하지 않는 일이 더 많아 질 수 있습니다. 예를 들어, 변수를 할당하면 일반적으로 초기 값이 정의되지 않은 상태가됩니다. 값을 유지하지 않으려는 경우 변수를 할당하지 않기 때문에 이것은 문제로 간주되지 않았습니다. 전문 프로그래머가 초기 값을 할당하는 것을 잊지 않을 것이라고 기대하는 것은 그리 많지 않았습니까?

보다 강력한 프로그래밍 시스템으로 가능해진 더 큰 코드 기반과 더 복잡한 구조로 인해 많은 프로그래머들이 실제로 그러한 감독을 때때로 수행 할 것이며 결과적으로 정의되지 않은 행동이 큰 문제가되었다는 것이 밝혀졌습니다. 오늘날에도 보안 위협의 대부분은 작은 형태에서 끔찍한 형태로 정의되지 않은 행동으로 인해 발생합니다. (일반적으로 정의되지 않은 동작은 실제로 컴퓨팅의 다음 하위 수준에있는 것들에 의해 매우 많이 정의되기 때문에 그 수준을 이해하는 공격자는 해당 흔들림 공간을 사용하여 프로그램이 의도하지 않은 것뿐만 아니라 정확하게 그들은 의도한다.)

이를 인식 한 이후, 고급 언어에서 정의되지 않은 동작을 제거하는 일반적인 추진이 이루어졌으며 Java는 이에 대해 특히 철저했습니다 (어쨌든 자체적으로 설계된 가상 머신에서 실행되도록 설계 되었기 때문에 비교적 쉬웠습니다). C와 같은 오래된 언어는 대량의 기존 코드와의 호환성을 잃지 않으면 서 쉽게 개조 할 수 없습니다.

편집 : 지적한 바와 같이 효율성이 또 다른 이유입니다. 정의되지 않은 동작은 컴파일러 작성자가 대상 아키텍처를 악용 할 여지가 많으므로 각 구현이 각 기능의 가능한 가장 빠른 구현으로 벗어날 수 있음을 의미합니다. 프로그래머 급여가 종종 소프트웨어 개발의 병목 현상 인 오늘날보다 저조한 기계에서 이것은 더 중요했습니다.


56
저는 C 커뮤니티의 많은 사람들이이 진술에 동의 할 것이라고 생각하지 않습니다. C를 개조하고 정의되지 않은 동작을 정의하는 경우 (예 : 모든 것을 초기 초기화하고, 함수 매개 변수에 대한 평가 순서를 선택하는 등), 올바르게 작동하는 큰 코드는 계속 완벽하게 작동합니다. 오늘날 잘 정의되지 않은 코드 만 중단됩니다. 다른 한편으로, 오늘처럼 정의되지 않은 채로두면 컴파일러는 CPU 아키텍처 및 코드 최적화의 새로운 발전을 계속 자유롭게 이용할 수 있습니다.
Christophe

13
대답의 주요 부분이 실제로 설득력있는 것은 아닙니다. int32_t add(int32_t x, int32_t y)C ++ 에서 두 개의 숫자를 안전하게 추가하는 함수를 작성하는 것은 기본적으로 불가능합니다 . 그 주위의 일반적인 주장은 효율성과 관련이 있지만 종종 이식성 주장과 함께 산재되어 있습니다 ( "한 번 작성하고 실행 한 플랫폼에서 ... 그리고 다른 곳에서는 ;-)"). 대략 한 가지 주장은 다음과 같습니다. 16 비트 마이크로 컨트롤러 또는 64 비트 서버 (약한 서버이지만 여전히 논쟁)에 있는지 알지 못하기 때문에 일부 정의되지 않은 사항
Marco13

12
@ Marco13 동의- "정의되지 않은 동작"대신 "정의 된 동작"을 만들면 "정의되지 않은 동작"대신 "사용자가 원하던 동작이 발생했을 때 경고하지 않고 반드시 정의되지 않은 동작"문제를 제거 할 수 있습니다. .
alephzero

9
"오늘날에도, 소규모에서 끔찍한 보안 유출의 대부분은 어떤 형태로든 정의되지 않은 행동의 결과입니다." 인용이 필요했습니다. 나는 지금 그들 대부분이 XYZ 주사라고 생각했다.
여호수아

34
"정의되지 않은 행동은 회고에서만 매우 나쁜 생각으로 인식 된 것들 중 하나입니다." 당신의 의견입니다. 많은 사람들이 자신을 포함하지 않습니다.
Monica와 Lightness Race

104

기본적으로 Java 및 유사한 언어의 디자이너는 자신의 언어에서 정의되지 않은 동작을 원하지 않기 때문입니다. 정의되지 않은 동작을 허용하면 성능을 향상시킬 수 있지만 언어 설계자는 안전성과 예측 가능성을 우선시했습니다.

예를 들어 C로 배열을 할당하면 데이터가 정의되지 않습니다. Java에서는 모든 바이트를 0 (또는 다른 지정된 값)으로 초기화해야합니다. 즉, 런타임은 배열을 통과해야하며 (O (n) 연산) C는 즉시 할당을 수행 할 수 있습니다. 따라서 C는 항상 이러한 작업에 더 빠릅니다.

배열을 사용하는 코드가 읽기 전에 어쨌든 채워질 경우 기본적으로 Java에 대한 노력이 낭비됩니다. 그러나 코드를 먼저 읽는 경우 Java에서는 예측 가능하지만 C에서는 예측할 수없는 결과가 나타납니다.


19
HLL 딜레마의 탁월한 표현 : 안전성과 사용 편의성 및 성능. 은 총알이 없습니다. 각 측면에 대한 사용 사례가 있습니다.
Christophe

5
@Christophe 공정하게 말하면 UB가 C 및 C ++처럼 완전히 경쟁하지 않는 것보다 문제에 대한 더 나은 접근 방식이 있습니다. 유익한 곳에 적용하기 위해 안전하지 않은 지역으로 탈출구가있는 안전하고 관리되는 언어를 사용할 수 있습니다. TBH, C / C ++ 프로그램을 "필요한 값 비싼 런타임 기계를 삽입하십시오. 상관 없습니다. "
Alexander

4
초기화되지 않은 위치 를 의도적으로 읽는 데이터 구조의 좋은 예 는 Briggs and Torczon의 희소 세트 표현입니다 (예 : codingplayground.blogspot.com/2009/03/… ). 이러한 세트의 초기화는 C에서 O (1)이지만 O ( n) Java의 강제 초기화.
Arch D. Robison

9
데이터를 강제로 초기화하면 손상된 프로그램을 훨씬 더 예측 가능하게 만들 수 있지만 의도 된 동작을 보장 할 수는 없습니다. 알고리즘이 암시 적으로 초기화 된 0을 잘못 읽는 동안 의미있는 데이터를 읽을 것으로 예상되는 경우 이는 마치 버그처럼 쓰레기를 읽어보십시오. C / C ++ 프로그램 valgrind을 사용하면 초기화되지 않은 값이 사용 된 위치를 정확하게 보여주는 아래의 프로세스를 실행하여 이러한 버그를 볼 수 있습니다 . valgrind런타임이 초기화를 수행하여 valgrind검사를 쓸모 없게 만들기 때문에 Java 코드 에서는 사용할 수 없습니다 .
cmaster

5
@cmaster 그렇기 때문에 C # 컴파일러는 초기화되지 않은 로컬에서 읽을 수 없습니다. 런타임 검사가 필요없고 초기화가 필요 없으며 컴파일 타임 분석 만 가능합니다. 그래도 여전히 트레이드 오프입니다. 할당되지 않은 지역 주변의 분기를 처리하는 좋은 방법이없는 경우가 있습니다. 실제로, 나는 이것이 처음에는 나쁜 디자인이 아니고 복잡한 분기 (사람들이 파싱하기 어려운)를 피하기 위해 코드를 다시 생각함으로써 더 잘 해결 된 사례를 찾지 못했지만 적어도 가능합니다.
루안

42

정의되지 않은 동작은 컴파일러가 특정 경계 또는 다른 조건에서 이상한 또는 예기치 않은 (또는 정상적인) 작업을 수행 할 수 있도록함으로써 상당한 최적화를 가능하게합니다.

http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html을 참조하십시오

초기화되지 않은 변수 사용 : 이것은 일반적으로 C 프로그램에서 문제의 원인으로 알려져 있으며 컴파일러 경고에서 정적 및 동적 분석기에 이르기까지이를 포착 할 수있는 많은 도구가 있습니다. 이는 모든 변수가 범위 내에있을 때 (자바처럼) 초기화되지 않아도되므로 성능이 향상됩니다. 대부분의 스칼라 변수의 경우 오버 헤드가 거의 발생하지 않지만 스택 배열과 malloc'd 메모리는 스토리지의 memset을 유발할 수 있습니다. 특히 스토리지가 일반적으로 완전히 덮어 쓰여지기 때문에 비용이 많이들 수 있습니다.


부호있는 정수 오버플로 : 'int'형식 (예 :)의 산술 오버플로가 발생하면 결과가 정의되지 않습니다. 한 가지 예는 "INT_MAX + 1"이 INT_MIN 인 것은 아닙니다. 이 동작은 일부 코드에 중요한 특정 클래스의 최적화를 가능하게합니다. 예를 들어, INT_MAX + 1이 정의되지 않았다는 것을 알면 "X + 1> X"를 "true"로 최적화 할 수 있습니다. 곱하기 "cannot"overflow를 알면 (정의되지 않기 때문에) "X * 2 / 2"를 "X"로 최적화 할 수 있습니다. 사소한 것처럼 보일 수 있지만 이러한 종류의 항목은 일반적으로 인라인 및 매크로 확장으로 노출됩니다. 이것이 허용하는 더 중요한 최적화는 "<="에 대한 루프입니다 :

for (i = 0; i <= N; ++i) { ... }

이 루프에서 컴파일러는 오버플로에서 "i"가 정의되지 않은 경우 루프가 N + 1 번 반복된다고 가정 할 수 있습니다.이 경우 광범위한 루프 최적화가 시작됩니다. 반면에 변수가 오버플로로 랩핑하면 컴파일러는 루프가 무한하다고 가정해야합니다 (N이 INT_MAX 인 경우 발생)-이 중요한 루프 최적화를 비활성화합니다. 많은 코드에서 "int"를 유도 변수로 사용하기 때문에 특히 64 비트 플랫폼에 영향을줍니다.


27
물론 부호있는 정수 오버플로가 정의되지 않은 실제 이유는 C가 개발 될 때 사용중인 부호있는 정수의 적어도 3 가지 표현 (1 개의 보수, 2의 보수, 부호 크기 및 아마도 오프셋 이진)이 있었기 때문입니다. , 각각 INT_MAX + 1에 대해 다른 결과를 제공합니다. 오버플로를 정의되지 않은 상태로 만들면 컴파일러에서 부호있는 정수 산술의 다른 형식을 시뮬레이션 할 필요없이 모든 상황에서 a + b기본 add b a명령어 로 컴파일 할 수 있습니다 .
마크

2
정수 오버플로가 느슨하게 정의 된 방식 으로 작동하도록 허용하면 모든 가능한 동작이 응용 프로그램 요구 사항을 충족하는 경우 상당한 최적화 가 가능합니다 . 그러나 프로그래머가 모든 비용으로 정수 오버플로를 피해야하는 경우 이러한 최적화의 대부분은 상실됩니다.
supercat

5
@supercat 이것은 최근 언어에서 정의되지 않은 동작을 피하는 것이 더 일반적인 또 다른 이유입니다. 프로그래머 시간은 CPU 시간보다 훨씬 더 중요합니다. UB 덕분에 C가 할 수있는 최적화의 종류는 현대 데스크탑 컴퓨터에서 본질적으로 무의미하며 코드에 대한 추론을 훨씬 어렵게 만듭니다 (보안 관련 사항은 말할 것도 없습니다). 성능이 중요한 코드조차도 C에서 수행하기가 다소 어려울 수있는 고급 최적화의 이점을 얻을 수 있습니다. C #에는 자체 소프트웨어 3D 렌더러가 있으며 예를 들어 a를 사용할 수 있습니다 HashSet.
루안

2
@supercat : Wrt_loosely defined_, 정수 오버플로의 논리적 선택은 구현 정의 동작 을 요구하는 것 입니다. 이는 기존 개념이며 구현에 과도한 부담이 아닙니다. 대부분은 "랩 어라운드를 가진 2의 보수"라고 생각합니다. <<어려운 경우 일 수 있습니다.
MSalters

@MSalters 정의되지 않은 동작이나 구현 정의 동작이 아닌 간단하고 잘 연구 된 솔루션이 있습니다. 비 결정적 동작입니다. 즉, " x << y유효한 유형의 값으로 평가 int32_t되지만 어떤 것을 말하지는 않을 것"이라고 말할 수 있습니다. 이를 통해 구현자는 빠른 솔루션을 사용할 수 있지만 비결 정성은이 한 작업의 출력으로 제한되므로 시간 여행 스타일 최적화를 허용하는 잘못된 전제 조건으로 작동하지 않습니다. 스펙은 메모리, 휘발성 변수 등이 눈에 띄게 영향을받지 않도록 보장합니다. 표현 평가에 의해. ...
마리오 카네이로

20

C의 초기에는 많은 혼란이있었습니다. 다른 컴파일러는 언어를 다르게 취급했습니다. 언어에 대한 사양을 작성하고자한다면, 해당 사양은 프로그래머가 컴파일러에 의존하고있는 C와 상당히 역 호환되어야합니다. 그러나 이러한 세부 사항 중 일부는 이식성이 없으며 특정 엔디안 또는 데이터 레이아웃을 가정 할 때 일반적으로 의미가 없습니다. 따라서 C 표준은 정의되지 않은 또는 구현 지정 동작으로 많은 세부 정보를 보유하므로 컴파일러 작성자에게 많은 유연성을 제공합니다. C ++은 C를 기반으로하며 정의되지 않은 동작을 제공합니다.

자바는 C ++보다 훨씬 더 안전하고 간단한 언어가 되려고 노력했다. Java는 완전한 가상 머신의 관점에서 언어 의미를 정의합니다. 이것은 정의되지 않은 동작을위한 공간을 거의 남기지 않습니다. 반면에 Java 구현이 어렵게 할 수있는 요구 사항을 만듭니다 (예 : 참조 지정은 원 자성이거나 정수 작동 방식). Java가 잠재적으로 안전하지 않은 작업을 지원하는 경우 일반적으로 런타임시 가상 시스템에서 확인합니다 (예 : 일부 캐스트).


그렇다면 C와 C ++가 정의되지 않은 동작에서 벗어나지 않는 유일한 이유는 이전 버전과의 호환성입니다.
Sisir

3
@Sisir는 확실히 더 큰 것 중 하나입니다. 심지어 숙련 된 프로그래머들 사이에서, 당신은 놀라게 될 것입니다 얼마나 파괴해서는 안 물건 않는 컴파일러는 그것이 정의되지 않은 동작을 처리하는 방법을 변경하면 휴식. ( "이다 GCC 밖으로 최적화 시작했을 때 케이스는 점에서, 혼돈의 조금 있었다 this? 널 (null)"의 이유로, 다시 한동안를 확인 this존재가 nullptr실제로 일어날 수 없다 따라서 UB, 그리고.)
저스틴 시간

9
@Sisir, 또 다른 큰 것은 속도입니다. C 초기에는 하드웨어가 오늘날보다 훨씬 이질적이었습니다. INT_MAX에 1을 추가 할 때 발생하는 동작을 지정하지 않으면 컴파일러가 아키텍처에 가장 빠른 것을 수행하도록 할 수 있습니다 (예 : 하나의 보완 시스템은 -INT_MAX를 생성하고 두 개의 보완 시스템은 INT_MIN을 생성 함). 마찬가지로, 배열 끝을 지나서 읽을 때 발생하는 사항을 지정하지 않으면 메모리 보호 기능이있는 시스템이 프로그램을 종료하도록 할 수 있지만 값 비싼 런타임 경계 검사를 구현하지 않아도됩니다.
마크

14

JVM 및 .NET 언어는 다음과 같이 쉽습니다.

  1. 하드웨어와 직접 작업 할 필요가 없습니다.
  2. 그들은 현대 데스크탑 및 서버 시스템 또는 합리적으로 유사한 장치 또는 적어도 그것들을 위해 설계된 장치에서만 작동해야합니다.
  3. 모든 메모리에 가비지 수집을 적용하고 강제 초기화하여 포인터 안전성을 얻을 수 있습니다.
  4. 그들은 단일 구현을 제공 한 단일 행위자에 의해 지정되었습니다.
  5. 성능보다 안전을 선택하게됩니다.

선택에 대한 좋은 점이 있습니다.

  1. 시스템 프로그래밍은 완전히 다른 볼 게임이며, 응용 프로그래밍을 위해 타협하지 않고 최적화하는 것이 합리적입니다.
  2. 물론, 항상 덜 이국적인 하드웨어는 있지만 소형 임베디드 시스템이 여기에 있습니다.
  3. GC는 대체 불가능한 자원에 적합하지 않으며 성능을 향상시키기 위해 훨씬 더 많은 공간을 교환합니다. 그리고 대부분의 (거의 전부는 아님) 강제 초기화를 최적화 할 수 있습니다.
  4. 더 많은 경쟁에는 이점이 있지만위원회는 타협을 의미합니다.
  5. 대부분의 범위를 최적화 할 수는 있지만 모든 경계 검사 더해집니다. 널 주소 확인은 가상 주소 공간으로 인해 오버 헤드가없는 액세스를 트래핑하여 수행 할 수 있지만 최적화는 여전히 제한되어 있습니다.

이스케이프 해치가 제공되면 완전히 정의되지 않은 동작을 다시 시작합니다. 그러나 최소한 일반적으로 아주 짧은 시간 동안 만 사용되므로 수동으로 쉽게 확인할 수 있습니다.


3
과연. 나는 직업을 위해 C #으로 프로그램한다. 때때로 나는 안전하지 않은 망치 중 하나 ( unsafe키워드 또는 속성)에 도달합니다 System.Runtime.InteropServices. 관리되지 않는 항목을 디버깅하는 방법을 알고있는 소수의 프로그래머에게이 내용을 유지하고 실용적이지 않은 수준으로 유지함으로써 문제를 해결합니다. 마지막 성능 관련 불안전 한 해머 이후 10 년이 지났지 만 문자 그대로 다른 솔루션이 없기 때문에 수행해야 할 때가 있습니다.
여호수아

19
sizeof (char) == sizeof (short) == sizeof (int) == sizeof (float) == 1 인 아날로그 장치의 플랫폼에서 자주 작업합니다. C에 대한 좋은 점은 합리적인 코드를 생성하는 적합한 컴파일러를 가질 수 있다는 것입니다. 명령 된 언어가 2 개가 줄 바꿈을 보완한다고 말하면 모든 추가는 테스트와 분기로 끝납니다. 이것은 DSP에 초점을 둔 부분의 시작이 아닙니다. 이것은 현재 생산 부품입니다.
Dan Mills

5
@BenVoigt 우리 중 일부는 작은 컴퓨터가 4k 코드 공간, 고정 8 레벨 콜 / 리턴 스택, 64 바이트 RAM, 1MHz 클럭 및 1,000 달러 미만의 비용이 $ 0.20 인 세계에 살고 있습니다. 현대의 휴대 전화는 모든 의도와 목적을 위해 거의 무제한 저장 공간을 갖춘 작은 PC이며 PC로 취급 될 수 있습니다. 모든 세계가 멀티 코어가 아니며 실시간 제약이 부족한 것은 아닙니다.
Dan Mills

2
@DanMills : Arm Cortex A 프로세서를 사용하여 최신 휴대 전화에 대해서는 말하지 않고 2002 년경 "기능 전화"에 대해 이야기합니다. 예 192kB의 SRAM은 64 바이트 이상 ( "소형"이 아니라 "소형")이지만 192kB는 또한 "현대"데스크탑 또는 서버라고 30 년 동안 정확하게 불려지지 않았습니다. 또한 요즘 20 센트는 64 바이트 이상의 SRAM을 가진 MSP430을 제공합니다.
Ben Voigt

2
@BenVoigt 192kB는 지난 30 년 동안 데스크탑이 아닐 수도 있지만, 웹 페이지를 제공하는 것만으로도 충분하다는 것을 확신 할 수 있습니다. 사실 이것은 구성 웹 서버를 포함하는 많은 임베디드 응용 프로그램에 대해 완전히 합리적이며 넉넉한 양의 램입니다. 물론, 아마 그것에 아마존을 실행하지 않을 수도 있지만, 나는 그러한 코어 (시간과 여유 공간이 있음)에서 IOT crapware가 완비 된 냉장고를 실행하고있을 것입니다. 아무도 그것을 위해 통역 또는 JIT 언어를 필요로하지 마십시오!
Dan Mills

8

Java와 C #은 개발 초기에 지배적 인 벤더가 특징입니다. (각각 Sun과 Microsoft). C와 C ++는 다릅니다. 그들은 초기부터 여러 경쟁 구현을했습니다. C는 특히 이국적인 하드웨어 플랫폼에서도 실행되었습니다. 결과적으로 구현간에 차이가있었습니다. C 및 C ++를 표준화 한 ISO위원회는 큰 공통 분모에 동의 할 수 있지만 구현에 따라 구현 표준이 다른 가장자리에 있습니다.

또한 다른 선택에 편향되어있는 하드웨어 아키텍처에서 하나의 동작을 선택하는 것이 비용이 많이들 수 있기 때문에 엔디안이 확실한 선택입니다.


“큰 공통 분모” 란 문자 그대로 무엇을 의미 합니까? 서브 세트 또는 수퍼 세트에 대해 이야기하고 있습니까? 당신은 정말 충분한 공통 요소를 의미합니까? 이것은 최소 공배수 또는 최대 공약수와 같은 것입니까? 이것은 거리 용어를 사용하지 않고 수학 만하는 로봇에게는 매우 혼란 스럽습니다. :)
tchrist

@tchrist : 일반적인 행동은 부분 집합이지만이 부분 집합은 매우 추상적입니다. 공통 표준에 의해 지정되지 않은 많은 영역에서 실제 구현은 선택해야합니다. 이제 이러한 선택 중 일부는 명확하고 구현에 따라 정의되지만 다른 선택은 더 모호합니다. 이 있어야한다 : 런타임에 메모리 레이아웃은 예입니다 선택,하지만 당신이 그것을 문서화 할 방법을 분명하지 않다.
MSalters

2
원래 C는 한 사람이 만들었습니다. 이미 의도적으로 UB가 많았습니다. C가 대중화되면서 상황은 확실히 악화되었지만 UB는 처음부터 존재했습니다. Pascal과 Smalltalk는 UB가 훨씬 적었고 거의 동시에 개발되었습니다. C의 가장 큰 장점은 이식이 매우 쉽다는 것입니다. 모든 이식성 문제는 응용 프로그램 프로그래머에게 위임되었습니다. LISP 또는 Smalltalk와 같은 작업을 수행하는 데 훨씬 많은 노력이 들었습니다.
루안

@Luaan : Kernighan 또는 Ritchie일까요? 그리고 정의되지 않은 행동은 없었습니다. 저는 책상에 AT & T 스텐실 컴파일러 원본을 가지고있었습니다. 구현은 그랬습니다. 지정되지 않은 동작과 정의되지 않은 동작 사이에는 차이가 없었습니다.
MSalters

4
@MSalters Ritchie가 첫 번째 사람이었습니다. Kernighan은 나중에 만 (많이) 참여하지 않았습니다. 글쎄, 그 용어는 아직 존재하지 않았기 때문에 "정의되지 않은 행동"이 없었습니다. 그러나 그것은 오늘날 정의되지 않은 것과 같은 행동을했습니다. C에는 스펙이 없었기 때문에 "지정되지 않은"조차도 늘었습니다. 휴대용 응용 프로그램 을 생성하도록 설계되지 않았 으며 컴파일러 만 이식 하기 쉬워졌습니다.
루안

6

실제 이유는 한편으로는 C와 C ++의 의도가 근본적으로 다르고 다른 한편으로는 Java와 C # (몇 가지 예만)의 근본적인 차이에 기인합니다. 역사적 이유로, 여기에서 논의하는 많은 부분이 C ++보다는 C에 대해 이야기하지만 C ++은 C의 직계 후손이므로 C에 대한 내용은 C ++에 동일하게 적용됩니다.

그것들은 대부분 잊혀졌고 (때로는 존재조차 거부 되기는하지만) 최초의 UNIX 버전은 어셈블리 언어로 작성되었습니다. C의 원래 목적의 대부분은 (전적으로는 아니지만) 어셈블리 언어에서 고급 언어로 포트 UNIX를 사용하는 것이 었습니다. 의도의 일부는 어셈블리 언어로 작성해야하는 양을 최소화하기 위해 가능한 한 많은 운영 체제를 더 높은 수준의 언어로 작성하거나 다른 방향에서 보는 것입니다.

이를 달성하기 위해 C 는 어셈블리 언어와 거의 동일한 수준의 하드웨어 액세스 권한을 제공해야 했습니다 . PDP-11 (예를 들어)은 I / O 레지스터를 특정 주소에 매핑했습니다. 예를 들어, 시스템 콘솔에서 키를 눌렀는지 확인하기 위해 하나의 메모리 위치를 읽었습니다. 읽기 대기중인 데이터가있을 때 해당 위치에 1 비트가 설정되었습니다. 그런 다음 다른 지정된 위치에서 바이트를 읽고 누른 키의 ASCII 코드를 검색합니다.

마찬가지로 일부 데이터를 인쇄하려면 지정된 다른 위치를 확인하고 출력 장치가 준비되면 데이터를 다른 지정된 위치에 기록합니다.

이러한 장치의 드라이버 작성을 지원하기 위해 C에서는 정수 유형을 사용하여 임의의 위치를 ​​지정하고 포인터로 변환 한 다음 해당 위치를 메모리에서 읽거나 쓸 수있었습니다.

물론 이것은 매우 심각한 문제가 있습니다. 지구상의 모든 기계가 1970 년대 초반부터 PDP-11과 동일하게 메모리를 배치하는 것은 아닙니다. 따라서 정수를 가져 와서 포인터로 변환 한 다음 해당 포인터를 통해 읽거나 쓰면 아무도 얻을 수있는 것에 대해 합리적인 보장을 제공 할 수 없습니다. 명백한 예를 들어, 읽기와 쓰기는 하드웨어에서 별도의 레지스터에 매핑 될 수 있으므로 무언가를 쓰면 다시 읽고 읽으십시오. 읽은 내용이 쓴 내용과 일치하지 않을 수 있습니다.

떠날 가능성이 몇 가지 있습니다.

  1. 가능한 모든 하드웨어에 대한 인터페이스를 정의하십시오. 어떤 방식 으로든 하드웨어와 상호 작용하기 위해 읽거나 쓸 수있는 모든 위치의 절대 주소를 지정하십시오.
  2. 그러한 수준의 접근을 금지하고 그러한 일을하고 싶은 사람은 누구나 어셈블리 언어를 사용해야한다고 선언합니다.
  3. 사람들이 그렇게 할 수는 있지만 대상 하드웨어에 대한 설명서를 읽고 사용중인 하드웨어에 맞는 코드를 작성하도록하십시오.

이 중 1 개는 충분히 터무니없는 것으로 보이므로 더 이상 논의 할 가치가 없습니다. 2는 기본적으로 언어의 기본 의도를 버리고 있습니다. 따라서 세 번째 옵션은 본질적으로 그들이 합리적으로 고려할 수있는 유일한 옵션으로 남습니다.

상당히 자주 나타나는 또 다른 요점은 정수 유형의 크기입니다. C는 int아키텍처가 제안한 자연스러운 크기 여야 하는 "위치"를 취합니다 . 따라서 32 비트 VAX를 프로그래밍하는 경우 int아마도 32 비트이어야하지만 36 비트 Univac을 프로그래밍하는 경우 int아마도 36 비트이어야합니다. 크기가 8 비트의 배수로 보장되는 유형 만 사용하여 36 비트 컴퓨터 용 운영 체제를 작성하는 것은 합리적이지 않으며 가능하지 않을 수도 있습니다. 어쩌면 나는 피상적이지만 36 비트 시스템 용 OS를 작성하는 경우 36 비트 유형을 지원하는 언어를 사용하고 싶을 것입니다.

언어 관점에서 볼 때 이것은 여전히 ​​더 정의되지 않은 동작으로 이어집니다. 32 비트에 맞는 가장 큰 값을 취하면 1을 추가하면 어떻게됩니까? 일반적인 32 비트 하드웨어에서는 롤오버되거나 하드웨어 오류가 발생할 수 있습니다. 반면에 36 비트 하드웨어에서 실행 중이면 하나만 추가하면됩니다. 언어가 운영 체제 작성을 지원할 경우 두 가지 동작을 모두 보장 할 수 없습니다. 유형의 크기와 오버플로 동작이 서로 다를 수 있도록 허용하면됩니다.

Java와 C #은이 모든 것을 무시할 수 있습니다. 운영 체제 작성을 지원하지 않습니다. 그들과 함께, 당신은 몇 가지 선택이 있습니다. 하나는 하드웨어가 원하는 것을 지원하도록하는 것입니다. 8, 16, 32 및 64 비트 유형을 요구하기 때문에 이러한 크기를 지원하는 하드웨어 만 구축하면됩니다. 또 다른 확실한 가능성은 언어가 기본 하드웨어가 무엇을 원하든 원하는 환경을 제공하는 다른 소프트웨어에서만 실행될 수 있다는 것입니다.

대부분의 경우, 이것은 실제로 하나 또는 선택이 아닙니다. 오히려 많은 구현이 두 가지 모두를 약간 수행합니다. 일반적으로 운영 체제에서 실행중인 JVM에서 Java를 실행합니다. 종종 OS는 C로 작성되고 JVM은 C ++로 작성됩니다. JVM이 ARM CPU에서 실행되는 경우 CPU에 ARM의 Jazelle 확장이 포함되어있어 하드웨어를 Java의 요구에 더 가깝게 맞추므로 소프트웨어에서 수행 할 필요성이 줄어들고 Java 코드가 더 빨리 (또는 더 적게) 실행됩니다. 어쨌든 천천히).

요약

C와 C ++는 의도하지 않은 동작을 수행 할 수있는 수용 가능한 대안을 정의한 사람이 없기 때문에 정의되지 않은 동작을합니다. C #과 Java는 다른 접근 방식을 취하지 만, 그 접근 방식은 C와 C ++의 목표에 맞지 않습니다. 특히, 대부분의 임의로 선택된 하드웨어에서 시스템 소프트웨어 (예 : 운영 체제)를 작성하는 합리적인 방법을 제공하지는 않습니다. 둘 다 일반적으로 작업을 수행하기 위해 기존 시스템 소프트웨어 (보통 C 또는 C ++로 작성)에서 제공하는 기능에 의존합니다.


4

C 표준의 저자는 독자들이 자신들이 생각한 것이 분명하다고 인정하고 출판 된 이론적 근거에서 언급 한 것을 인정할 것을 기대했지만,위원회는 고객의 요구를 충족시키기 위해 컴파일러 작성자를 주문할 필요가 없습니다. 고객은위원회보다 자신의 요구가 무엇인지 더 잘 알아야하기 때문입니다. 특정 종류의 플랫폼에 대한 컴파일러가 특정 방식으로 구성을 처리 할 것으로 예상되는 경우, 표준이 해당 구성이 정의되지 않은 동작을 호출하는지 여부를 신경 쓰지 않아야합니다. 표준이 준수하는 컴파일러가 코드를 유용하게 처리 할 것을 요구하지 않는 것은 프로그래머가 그렇지 않은 컴파일러를 기꺼이 구매해야한다는 의미는 아닙니다.

언어 설계에 대한 이러한 접근 방식은 컴파일러 작성자가 유료 고객에게 상품을 판매해야하는 세계에서 매우 효과적입니다. 컴파일러 작성자가 시장의 영향과 분리되어있는 세계에서는 완전히 분리되어 있습니다. 1990 년대에 대중화되었던 언어를 조종하는 방식으로 언어를 조종하기에 적절한 시장 조건이 존재할 것이라는 것은 의심의 여지가 없으며, 제정신의 언어 디자이너가 그러한 시장 조건에 의존하기를 원한다는 것은 더욱 의심 스럽다.


나는 당신이 여기서 중요한 것을 설명했다고 생각하지만, 그것은 나를 피합니다. 답을 명확하게 설명해 주시겠습니까? 특히 두 번째 단락 : 그것은 현재의 조건과 이전의 조건이 다르다고 말하지만 나는 그것을 얻지 못합니다. 정확히 무엇이 바뀌 었습니까? 또한 "길"은 이제 이전과 다릅니다. 아마도 이것도 설명해 주시겠습니까?
anatolyg

4
정의되지 않은 모든 동작을 지정되지 않은 동작으로 바꾸는 캠페인이 더 제한적이거나 더 제한적인 작업이 계속 진행되고있는 것 같습니다.
중복 제거기

1
@anatolyg : 아직 작성하지 않은 경우 공개 된 C 이론 문서 (Google의 C99 이론 유형)를 읽으십시오. 페이지 11 줄 23-29는 "시장"에 대해 말하고, 13 페이지 줄 5-8은 이식성과 관련하여 의도 된 것에 대해 이야기합니다. 컴파일러 작성자가 옵티마이 저가 표준에 의해 정의되지 않은 동작을 수행하기 때문에 다른 모든 컴파일러가 자신의 코드가 "손상된"코드를 유용하게 처리했다는 불만을 제기 한 프로그래머에게 말하면 상용 컴파일러 회사의 상사는 어떻게 반응 할 것이라고 생각하십니까? 그것이 계속되는 것을 촉진 할 것이기 때문에 그것을지지하지 않았다.
supercat

1
... 이러한 구조의 사용? 이러한 관점은 clang 및 gcc 지원 보드에서 쉽게 알 수 있으며, 깨진 언어 gcc 및 clang이 지원하고자하는 것보다 훨씬 쉽고 안전하게 최적화를 촉진 할 수있는 내장 함수의 개발을 방해하는 역할을했습니다.
supercat

1
@ supercat : 컴파일러 공급 업체에 불만을 토로하는 것입니다. 언어위원회에 관심을 가지시겠습니까? 그들이 당신에게 동의하면, 정오표가 발행되어 컴파일러 팀을 이길 수 있습니다. 그리고 그 과정은 새로운 버전의 언어를 개발하는 것보다 훨씬 빠릅니다. 그러나 그들이 동의하지 않으면 최소한 실제적인 이유를 얻게 될 것입니다. 반면 컴파일러 작성자는 반복해서 반복 할 것입니다. "우리는 코드가 깨진 것을 지정하지 않았습니다. "그들의 결정을 따르십시오."
Ben Voigt

3

C ++과 c는 모두 기술 표준 (ISO 버전)을 갖 습니다.

언어가 어떻게 작동하는지 설명하고 언어가 무엇인지에 대한 단일 참조를 제공하기 위해 존재합니다. 일반적으로 컴파일러 공급 업체 및 라이브러리 작성자는 주요 ISO 표준에 일부 제안 사항이 포함되어 있습니다.

Java와 C # (또는 Visual C #, 나는 당신이 생각한다고 가정)에는 규범적인 표준을 가지고 있습니다. 사전에 언어로 무엇이 있는지, 어떻게 작동하는지, 허용되는 행동으로 간주되는 것을 알려줍니다.

그보다 더 중요한 것은 Java는 실제로 Open-JDK에서 "참조 구현"을 가지고 있다는 것입니다. ( Roslyn 은 Visual C # 참조 구현으로 간주하지만 그 소스를 찾을 수는 없습니다.)

Java의 경우 표준에 모호성이 있으면 Open-JDK는 특정 방식으로 수행합니다. Open-JDK가하는 방식이 표준입니다.


상황이 그것보다 더 나쁘다 : 나는위원회가 그것이 서술 적이거나 규범 적이어야한다고 합의한 적이 없다고 생각한다.
supercat

1

정의되지 않은 동작을 통해 컴파일러는 다양한 아키텍처에서 매우 효율적인 코드를 생성 할 수 있습니다. Erik의 답변은 최적화에 대해 언급하지만 그 이상입니다.

예를 들어, 부호있는 오버플로는 C에서 정의되지 않은 동작입니다. 실제로 컴파일러는 CPU가 실행할 간단한 부호있는 추가 opcode를 생성 할 것으로 예상되었으며 해당 동작은 특정 CPU가 수행 한 모든 작업입니다.

이를 통해 C는 대부분의 아키텍처에서 매우 우수한 성능과 매우 작은 코드를 생성 할 수있었습니다. 표준에서 부호있는 정수가 특정 방식으로 오버 플로우되어야한다고 명시한 경우 다르게 동작하는 CPU는 단순한 부호있는 추가를 위해 더 많은 코드 생성이 필요할 것입니다.

이것이 C에서 정의되지 않은 많은 동작의 이유이며, 크기와 같은 것들이 int시스템마다 다른 이유 입니다. Int아키텍처에 따라 다르며 일반적으로 a보다 큰 가장 빠르고 가장 효율적인 데이터 유형으로 선택됩니다 char.

C가 처음 등장했을 때 이러한 고려 사항이 중요했습니다. 컴퓨터의 성능이 떨어지고 처리 속도와 메모리가 제한되는 경우가 종종있었습니다. C는 성능이 실제로 중요한 곳에서 사용되었으며 개발자는 이러한 정의되지 않은 동작이 특정 시스템에서 실제로 어떤 역할을하는지 알기 위해 컴퓨터가 어떻게 잘 작동했는지 이해해야했습니다.

Java 및 C #과 같은 이후의 언어는 원시 성능보다 정의되지 않은 동작을 제거하는 것을 선호했습니다.


-5

어떤 의미에서 Java도 있습니다. Arrays.sort에 대해 잘못된 비교기를 제공했다고 가정하십시오. 감지하면 예외를 던질 수 있습니다. 그렇지 않으면 특정이 아닌 방식으로 배열을 정렬합니다.

마찬가지로 여러 스레드에서 변수를 수정하면 결과도 예측할 수 없습니다.

C ++은 정의되지 않은 더 많은 상황을 만들거나 더 많은 작업을 정의하기로 결정한 Java의 이름을 가지기 위해 계속 진행되었습니다.


4
그것은 우리가 여기서 말하는 일종의 정의되지 않은 행동이 아닙니다. "잘못된 비교기"는 총 주문을 정의하는 것과 그렇지 않은 것의 두 가지 유형으로 나뉩니다. 항목의 상대적 순서를 일관되게 정의하는 비교기를 제공하면 동작이 잘 정의되어 있으며 이는 프로그래머가 원하는 동작이 아닙니다. 상대 순서에 대해 일관되지 않은 비교자를 제공하는 경우 동작은 여전히 ​​잘 정의되어 있습니다. 정렬 함수는 예외를 발생시킵니다 (아마도 프로그래머가 원하는 동작이 아닐 수도 있음).
마크

2
변수 수정과 관련하여 경쟁 조건은 일반적으로 정의되지 않은 동작으로 간주되지 않습니다. Java가 공유 데이터에 대한 할당을 처리하는 방법에 대한 세부 정보는 알지 못하지만 언어의 일반적인 철학을 알고 있기 때문에 원자 적이어야합니다. 53과 71을 동시에 할당 a하면 정의되지 않은 동작이되지만, 51이나 73을 얻을 수있는 경우에는 53 또는 71 만 가져올 수 있습니다.
마크

@Mark 시스템의 기본 단어 크기보다 큰 데이터 청크 (예 : 16 비트 단어 크기 시스템의 32 비트 변수)를 사용하면 각 16 비트 부분을 별도로 저장해야하는 아키텍처를 가질 수 있습니다. (SIMD는 그러한 잠재적 인 또 다른 상황이다.)이 경우, 간단한 소스 코드 레벨 할당조차도 그것이 원자 적으로 실행되도록 컴파일러가 특별한주의를 기울이지 않는 한 반드시 원자 성일 필요는 없다.
CVn
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.