C에서 배열 초기화에 대한 혼란


102

C 언어에서 다음과 같이 배열을 초기화하면 :

int a[5] = {1,2};

그러면 명시 적으로 초기화되지 않은 배열의 모든 요소는 암시 적으로 0으로 초기화됩니다.

그러나 다음과 같이 배열을 초기화하면 :

int a[5]={a[2]=1};

printf("%d %d %d %d %d\n", a[0], a[1],a[2], a[3], a[4]);

산출:

1 0 1 0 0

이해가 안되는데 왜 대신 a[0]인쇄 합니까? 정의되지 않은 동작입니까?10

참고 : 이 질문은 인터뷰에서 요청되었습니다.


35
식은로 a[2]=1평가됩니다 1.
tkausl

14
매우 깊은 질문입니다. 면접관이 스스로 답을 알고 있는지 궁금합니다. 나는하지 않는다. 실제로 표면 상 표현의 값 a[2] = 1입니다 1,하지만 당신은 첫 번째 요소의 값으로 지정된 initialiser 식의 결과를 취할 수 있습니다되어 있는지 확실하지 않습니다. 변호사 태그를 추가했다는 사실은 표준을 인용하는 답변이 필요하다는 것을 의미합니다.
Bathsheba

15
그들이 가장 좋아하는 질문이라면 총알을 피했을 수도 있습니다. 개인적으로 저는 위와 같은 "ace"스타일 질문보다 몇 시간에 걸쳐 작성되는 프로그래밍 연습 (컴파일러 및 디버거에 대한 액세스 포함)을 선호합니다. 대답을 추측 할 수 는 있지만 실제로는 사실에 근거하지 않을 것입니다.
밧세바

1
@Bathsheba 여기에 대한 대답이 이제 두 질문에 모두 답하기 때문에 그 반대입니다.
Goodbye SE

1
@Bathsheba가 최고 일 것입니다. 그래도 나는 그가 주제를 생각해 냈을 때 OP에게 질문에 대한 크레딧을 줄 것입니다. 그러나 이것은 내가 "옳은 일"이라고 느끼는 것을 결정하기위한 것이 아닙니다.
Goodbye SE

답변:


95

요약 : int a[5]={a[2]=1};최소한 C99에서는 의 동작 이 잘 정의 되어 있지 않다고 생각합니다 .

재미있는 부분은 나에게 이해되는 유일한 부분은 당신이 묻는 부분 이라는 것입니다. 할당 연산자가 할당 된 값을 반환 a[0]하기 1때문에 로 설정 됩니다. 명확하지 않은 다른 모든 것입니다.

코드가 있었다면 int a[5] = { [2] = 1 }, 모든 것을 쉽게 했 : 그건가 설정 초기화 지정의 a[2]1와에 다른 모든 것들 0. 그러나 { a[2] = 1 }우리는 할당 표현식을 포함하는 지정되지 않은 이니셜 라이저를 가지고 있으며 우리는 토끼 구멍에 빠지게됩니다.


지금까지 찾은 내용은 다음과 같습니다.

  • a 지역 변수 여야합니다.

    6.7.8 초기화

    1. 정적 저장 기간이있는 객체에 대한 이니셜 라이저의 모든 표현식은 상수 표현식 또는 문자열 리터럴이어야합니다.

    a[2] = 1상수 표현식이 아니므로 a자동 저장이 있어야합니다.

  • a 자체 초기화의 범위에 있습니다.

    6.2.1 식별자 범위

    1. 구조체, 공용체 및 열거 형 태그에는 태그를 선언하는 유형 지정자에서 태그가 나타난 직후에 시작되는 범위가 있습니다. 각 열거 형 상수에는 열거 자 목록에 정의 열거자가 나타난 직후에 시작되는 범위가 있습니다. 다른 식별자에는 선언자가 완료된 직후에 시작되는 범위가 있습니다.

    선언자는 a[5]이므로 변수는 자체 초기화의 범위에 있습니다.

  • a 자체 초기화에서 살아 있습니다.

    6.2.4 객체 저장 기간

    1. 연결없이 그리고 스토리지 클래스 지정자없이 식별자가 선언 된 객체 static자동 저장 기간을 갖습니다 .

    2. 가변 길이 배열 유형이없는 객체의 경우, 해당 블록의 실행이 어떤 방식 으로든 끝날 때까지 해당 수명이 항목에서 연결된 블록으로 확장됩니다 . (폐쇄 된 블록에 들어가거나 함수를 호출하면 현재 블록의 실행이 중지되지만 종료되지는 않습니다.) 블록이 반복적으로 입력되면 매번 객체의 새 인스턴스가 생성됩니다. 개체의 초기 값이 불확실합니다. 객체에 대해 초기화가 지정되면 블록 실행에서 선언에 도달 할 때마다 초기화가 수행됩니다. 그렇지 않으면 선언에 도달 할 때마다 값이 결정되지 않습니다.

  • 뒤에 시퀀스 포인트가 a[2]=1있습니다.

    6.8 문과 블록

    1. 전체 표현은 또 다른 표현의 또는 선언자의 일부가 아닌 표현이다. 다음은 각각 완전한 표현식입니다. 이니셜 라이저 ; 표현 문의 표현; 선택문 ( if또는 switch) 의 제어 표현식 whileor do문의 제어 표현 ; for문장 의 각 (선택적) 표현 ; return명령문 의 (선택적) 표현식 . 전체 표현식의 끝은 시퀀스 포인트입니다.

    참고 예에서 것을 부분은 이후에 연속 포인트가 각각의 이니셜 중괄호 둘러싸인 목록이다.int foo[] = { 1, 2, 3 }{ 1, 2, 3 }

  • 초기화는 이니셜 라이저 목록 순서로 수행됩니다.

    6.7.8 초기화

    1. 각 중괄호로 묶인 이니셜 라이저 목록에는 연결된 현재 객체가 있습니다. 지정이 없으면 현재 개체의 하위 개체는 현재 개체의 유형에 따라 초기화됩니다. 아래 첨자의 배열 요소, 선언 순서의 구조체 멤버, 공용체의 첫 번째 명명 된 멤버입니다. [...]

     

    1. 초기화는 이니셜 라이저 목록 순서로 발생해야하며, 각 이니셜 라이저는 특정 하위 개체에 대해 제공되며 동일한 하위 개체에 대해 이전에 나열된 이니셜 라이저를 재정의합니다. 명시 적으로 초기화되지 않은 모든 하위 객체는 정적 저장 기간이있는 객체와 동일하게 암시 적으로 초기화됩니다.
  • 그러나 이니셜 라이저 표현식이 반드시 순서대로 평가되는 것은 아닙니다.

    6.7.8 초기화

    1. 초기화 목록 표현식간에 부작용이 발생하는 순서는 지정되지 않습니다.

그러나 여전히 몇 가지 질문에 답이 없습니다.

  • 시퀀스 포인트도 관련이 있습니까? 기본 규칙은 다음과 같습니다.

    6.5 표현

    1. 이전 시퀀스 포인트와 다음 시퀀스 포인트 사이에서 객체는 표현식 평가에 의해 최대 한 번 수정 된 저장된 값을 가져야합니다 . 또한 이전 값은 저장 될 값을 결정하기 위해서만 읽어야합니다.

    a[2] = 1 은 표현식이지만 초기화는 아닙니다.

    이것은 Annex J에 의해 약간 모순됩니다.

    J.2 정의되지 않은 동작

    • 두 시퀀스 포인트 사이에서 객체가 두 번 이상 수정되거나 수정되고 저장 될 값을 결정하는 것 외에 이전 값을 읽습니다 (6.5).

    Annex J는 표현식에 의한 수정뿐만 아니라 모든 수정이 중요하다고 말합니다. 그러나 부록이 비 규범 적이라는 점을 감안할 때 우리는 아마도 그것을 무시할 수 있습니다.

  • 이니셜 라이저 표현식과 관련하여 하위 객체 초기화는 어떻게 순서가 지정됩니까? 모든 이니셜 라이저가 먼저 평가되고 (어떤 순서로) 하위 객체가 결과와 함께 초기화됩니까 (이니셜 라이저 목록 순서대로)? 아니면 인터리브 될 수 있습니까?


int a[5] = { a[2] = 1 }다음과 같이 실행된다고 생각 합니다.

  1. a포함 블록이 입력되면에 대한 스토리지 가 할당됩니다. 이 시점에서 내용은 불확실합니다.
  2. (유일한) 이니셜 라이저가 실행되고 ( a[2] = 1) 시퀀스 포인트가 이어집니다. 이 상점 1a[2]반환 1.
  3. 이는 1초기화에 사용됩니다 a[0](첫 번째 이니셜 라이저가 첫 번째 하위 객체를 초기화 함).

그러나 여기 일이 남아있는 요소 (때문에 퍼지 얻을 a[1], a[2], a[3], a[4])으로 초기화로되어있다 0,하지만 때 그것은 분명하지 않다 : 그것은 전에 발생합니까는 a[2] = 1평가? 그렇다면 a[2] = 1"승리"하고 덮어 a[2]쓰지만 0 초기화와 할당 표현식 사이에 시퀀스 포인트가 없기 때문에 할당이 정의되지 않은 동작을 갖습니까? 시퀀스 포인트도 관련이 있습니까 (위 참조)? 아니면 모든 이니셜 라이저가 평가 된 후에 초기화가 발생하지 않습니까? 그렇다면 a[2]끝나게한다 0.

C 표준은 여기서 일어나는 일을 명확하게 정의하지 않기 때문에 행동이 정의되지 않았다고 생각합니다 (누락으로 인해).


1
정의되지 않은 대신 나는 그것이 지정되지 않았기 때문에 구현에 의한 해석을 위해 열린 상태 라고 주장합니다 .
일부 프로그래머 친구

1
"토끼 구멍에 빠진다"LOL! UB 또는 지정되지 않은 항목에 대해서는 들어 본 적이 없습니다.
BЈовић

2
@Someprogrammerdude 표준이 실제로 어떤 가능성도 제공하지 않기 때문에 지정되지 않을 수 있다고 생각하지 않습니다 ( " 이 국제 표준이 두 개 이상의 가능성을 제공하고 어떤 경우에도 선택되는 추가 요구 사항을 부과하지 않는 동작 "). 고르다. 그것은 단순히 내가 "에 해당 믿는, 무슨 말을하지 않습니다 . 정의되지 않은 동작은 [...] 행동의 명시 적 정의의 누락으로이 규격 [...]에 표시된 "
멜 포메 네

2
@ BЈовић 정의되지 않은 동작뿐만 아니라 설명하기 위해 이와 같은 스레드가 필요한 정의 된 동작에 대한 매우 멋진 설명이기도합니다.
gnasher729

1
@JohnBollinger 차이점은 이니셜 라이저 a[0]를 평가하기 전에는 하위 개체를 실제로 초기화 할 수 없다는 것입니다. 그리고 이니셜 라이저 를 평가하면 시퀀스 포인트가 포함됩니다 ( "전체 표현식"이기 때문). 따라서 우리가 초기화하는 하위 객체를 수정하는 것은 공정한 게임이라고 생각합니다.
멜 포메 네

22

이해가 안되는데 왜 대신 a[0]인쇄 합니까?10

아마도 먼저 a[2]=1초기화 a[2]되고 표현식의 결과는 초기화에 사용됩니다.a[0] .

N2176 (C17 초안)에서 :

6.7.9 초기화

  1. 초기화 목록 표현식의 평가는 서로에 대해 불확실하게 순서 가 지정되므로 부작용이 발생하는 순서는 지정되지 않습니다. 154)

따라서 출력이 1 0 0 0 0 도 .

결론 : 초기화 된 변수를 즉시 수정하는 이니셜 라이저를 작성하지 마십시오.


1
해당 부분은 적용되지 않습니다. 여기에는 이니셜 라이저 표현식이 하나만 있으므로 어떤 것도 시퀀스 할 필요가 없습니다.
멜 포메 네

@melpomene가 {...}초기화 식 a[2]으로 0하고, a[2]=1초기화 서브 표현식 a[2]으로는 1.
user694733

1
{...}중괄호 초기화 목록입니다. 표현이 아닙니다.
멜 포메 네

@melpomene 좋아, 당신은 바로 거기있을 수 있습니다. 그러나 나는 여전히 2 개의 경쟁적인 부작용이있어 단락이 유효하다고 주장합니다.
user694733

@melpomene 시퀀싱 할 두 가지가 있습니다. 첫 번째 이니셜 라이저와 다른 요소를 0으로 설정
MM

6

저는 C11 표준이이 동작을 다루고 결과가 명시되지 않았다고 말합니다. 않았다고 . 그리고 C18이이 영역에서 관련 변경 사항을 적용하지 않았다고 생각합니다.

표준 언어는 파싱하기가 쉽지 않습니다. 표준의 관련 섹션은 §6.7.9 초기화 입니다. 구문은 다음과 같이 문서화됩니다.

initializer:
                assignment-expression
                { initializer-list }
                { initializer-list , }
initializer-list:
                designationopt initializer
                initializer-list , designationopt initializer
designation:
                designator-list =
designator-list:
                designator
                designator-list designator
designator:
                [ constant-expression ]
                . identifier

용어 중 하나는 assignment-expression 이며 a[2] = 1, 확실히 할당 표현식이므로 비 정적 기간을 가진 배열의 이니셜 라이저 내에서 허용됩니다.

§4 정적 또는 스레드 저장 기간이있는 개체에 대한 이니셜 라이저의 모든 식은 상수 식 또는 문자열 리터럴이어야합니다.

주요 단락 중 하나는 다음과 같습니다.

§19 초기화는 이니셜 라이저 목록 순서로 발생해야하며, 각 이니셜 라이저는 동일한 하위 개체에 대해 이전에 나열된 이니셜 라이저를 재정의하는 특정 하위 개체에 제공됩니다. 151) 명시 적으로 초기화되지 않은 모든 하위 객체는 정적 저장 기간이있는 객체와 동일하게 암시 적으로 초기화됩니다.

151) 재정의되어 해당 하위 개체를 초기화하는 데 사용되지 않는 하위 개체에 대한 이니셜 라이저는 전혀 평가되지 않을 수 있습니다.

또 다른 핵심 단락은 다음과 같습니다.

§23 초기화 목록 식의 평가는 서로에 대해 불확실하게 순서가 지정되므로 부작용이 발생하는 순서는 지정되지 않습니다. 152)

152) 특히, 평가 순서는 하위 객체 초기화 순서와 같을 필요는 없습니다.

나는 단락 §23이 질문의 표기법을 나타냅니다.

int a[5] = { a[2] = 1 };

지정되지 않은 동작으로 이어집니다. 할당 a[2]은 부작용이며 표현식의 평가 순서는 서로에 대해 불확실하게 순서가 지정됩니다. 결과적으로 나는 표준에 호소 할 방법이 없다고 생각하고 특정 컴파일러가 이것을 올바르게 또는 잘못 처리하고 있다고 주장합니다.


초기화 목록식이 하나만 있으므로 §23은 관련이 없습니다.
멜 포메 네

2

내 이해는 a[2]=11을 반환 하므로 코드는

int a[5]={a[2]=1} --> int a[5]={1}

int a[5]={1}a [0] = 1에 값 할당

따라서 a [0]에 대해 1 을 인쇄합니다 .

예를 들면

char str[10]={‘H’,‘a’,‘i’};


char str[0] = H’;
char str[1] = a’;
char str[2] = i;

2
이것은 [언어 변호사] 질문이지만 표준에 맞는 답변이 아니므로 관련성이 없습니다. 또한 2 개의 훨씬 더 심층적 인 답변을 사용할 수 있으며 귀하의 답변은 아무것도 추가하지 않는 것 같습니다.
Goodbye SE

의심 스럽습니다. 제가 게시 한 컨셉이 잘못 되었나요? 이걸로 설명해 주 시겠어요?
Karthika

1
표준의 관련 부분에 이미 주어진 아주 좋은 대답이 있지만 이유 때문에 추측 만 할뿐입니다. 어떻게 이런 일이 일어날 수 있는지 말하는 것만으로는 문제의 대상이 아닙니다. 그것은 표준이 일어나야한다고 말하는 것에 관한 것입니다.
Goodbye SE

그런데 위의 질문을 올린 사람이 그 이유와 그 이유를 물었습니다. 그래서이 대답을 떨어 뜨 렸지만 개념은 맞습니다. 맞죠?
Karthika

OP는 " 정의되지 않은 동작입니까? "라고 물었 습니다 . 당신의 대답은 말하지 않습니다.
멜 포메 네

1

나는 퍼즐에 대해 짧고 간단한 대답을하려고합니다. int a[5] = { a[2] = 1 };

  1. 첫 번째 a[2] = 1가 설정되었습니다. 즉, 배열에 다음과 같이 표시됩니다.0 0 1 0 0
  2. 그러나 보라, { }배열을 순서대로 초기화하는 데 사용되는 대괄호로 처리했다면 첫 번째 값 ()을 가져와로 1설정합니다 a[0]. 그것은 int a[5] = { a[2] };우리가 이미 얻은 곳에 남아있는 것처럼 보입니다 a[2] = 1. 결과 배열은 이제 다음과 같습니다.1 0 1 0 0

또 다른 예 : int a[6] = { a[3] = 1, a[4] = 2, a[5] = 3 };-순서가 다소 임의적이지만 왼쪽에서 오른쪽으로 진행한다고 가정하면 다음 6 단계로 진행됩니다.

0 0 0 1 0 0
1 0 0 1 0 0
1 0 0 1 2 0
1 2 0 1 2 0
1 2 0 1 2 3
1 2 3 1 2 3

1
A = B = C = 5선언 (또는 초기화)이 아닙니다. 연산자가 올바른 연관성 A = (B = (C = 5))이기 때문에 구문 분석하는 정규 표현식입니다 =. 초기화가 어떻게 작동하는지 설명하는 데는 도움이되지 않습니다. 배열은 정의 된 블록이 입력 될 때 실제로 존재하기 시작하며 실제 정의가 실행되기 훨씬 오래 걸릴 수 있습니다.
멜 포메 네

1
" 왼쪽에서 오른쪽으로 이동하며 각각 내부 선언으로 시작하는 "이 잘못되었습니다. C 표준은 명시 적으로 "라는 부작용이 초기화 목록 식 중 발생하는 순서는 지정되지 않습니다. "
멜 포메 네

1
" 내 예제의 코드를 충분히 테스트하고 결과가 일관 적인지 확인합니다. "작동 방식이 아닙니다. 정의되지 않은 동작이 무엇인지 이해하지 못하는 것 같습니다. C의 모든 것은 기본적으로 정의되지 않은 동작을 가지고 있습니다. 일부 부품에는 표준에 정의 된 동작이 있습니다. 무언가가 행동을 정의했음을 증명하려면 표준을 인용하고 어떤 일이 발생해야하는지 정의하는 위치를 보여야합니다. 이러한 정의가 없으면 동작이 정의되지 않습니다.
멜 포메 네

1
포인트 (1)의 주장은 여기서 핵심 질문에 대한 엄청난 도약입니다. a[2] = 1이니셜 라이저 표현식 의 부작용이 적용되기 전에 요소 a [2]의 암시 적 초기화가 0으로 발생 합니까? 관찰 된 결과는 마치 그랬던 것처럼 보이지만 표준은 그럴 것이라고 명시하지 않는 것 같습니다. 그것이 논란의 중심이며이 대답은 그것을 완전히 간과합니다.
John Bollinger

1
"정의되지 않은 동작"은 좁은 의미의 기술 용어입니다. 그것은 "우리가 정말로 확신하지 못하는 행동"을 의미하지 않습니다. 여기서 중요한 통찰력은 더 테스트, 아니 컴파일러, 어느 특정 프로그램 또는 잘 행동하지 보여줄 수 있다는 것입니다 표준에 따라 프로그램이 정의되지 않은 동작이있는 경우 때문에, 컴파일러는 할 수있다 아무것도 - 포함 작업 완벽하게 예측 가능하고 합리적인 방식으로. 컴파일러 작성자가 문서화하는 것은 단순히 구현 품질 문제가 아닙니다. 이는 지정되지 않거나 구현 정의 된 동작입니다.
Jeroen Mostert 2018 년

0

할당 a[2]= 1은 값이있는 표현식 1이며 기본적으로 썼습니다 int a[5]= { 1 };(부작용 a[2]도 할당 1됨).


그러나 부작용이 언제 평가되는지는 확실하지 않으며 컴파일러에 따라 동작이 변경 될 수 있습니다. 또한 표준은 이것이 컴파일러 특정 실현에 대한 설명이 도움이되지 않는 정의되지 않은 동작이라고 명시하는 것 같습니다.
Goodbye SE

@KamiKaze : 물론, 값 1은 우연히 거기에 도착했습니다.
Yves Daoust

0

나는 그것이 int a[5]={ a[2]=1 };프로그래머가 자신의 발에 자신을 쏘는 좋은 예 라고 생각 합니다.

나는 당신이 의미하는 바가 int a[5]={ [2]=1 };C99 지정 이니셜 라이저 설정 요소 2를 1로 설정하고 나머지는 0으로 설정하는 것이라고 생각하고 싶을 수도 있습니다.

당신이 정말로 의미하는 드문 경우에 int a[5]={ 1 }; a[2]=1;, 그것은 그것을 쓰는 재미있는 방법이 될 것입니다. 어쨌든, 이것은 쓰기 a[2]가 실제로 실행될 때 잘 정의되지 않았다고 여기에서 지적했지만 코드가 요약 되는 것입니다. 여기서 함정은 a[2]=1지정된 이니셜 라이저가 아니라 자체 값이 1 인 간단한 할당이라는 것입니다.


이 언어 변호사 주제가 표준 초안에서 참조를 요청하는 것 같습니다. 그것이 당신이 반대표를받는 이유입니다 (같은 이유로 내가 반대표를 받았다는 것을 알기 때문에 나는 그것을하지 않았습니다). 나는 당신이 쓴 것은 완전히 괜찮다고 생각하지만 여기에있는 모든 언어 변호사는 커밋이나 그와 비슷한 것 같습니다. 그래서 그들은 초안이 사건을 다루고 있는지 확인하려고 전혀 도움을 요청하지 않으며 여기에있는 대부분의 사람들은 당신이 그들을 돕는 것처럼 대답하면 트리거됩니다. 나는 아픈 내 대답을 삭제 같아요 :)이 주제 규칙을 넣으면 명확하게 도움이되었을 것이다
Abdurrahim
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.