스위치 문 : 마지막 경우가 기본값이어야합니까?


178

다음 switch진술을 고려하십시오 .

switch( value )
{
  case 1:
    return 1;
  default:
    value++;
    // fall-through
  case 2:
    return value * 2;
}

이 코드는 컴파일되지만 C90 / C99에 유효합니까 (= 정의 된 동작)? 기본 사례가 마지막 사례가 아닌 코드를 본 적이 없습니다.

편집 : Jon CageKillianDS가
것처럼 : 이것은 정말 추악하고 혼란스러운 코드이며 잘 알고 있습니다. 나는 일반적인 구문 (정의되어 있습니까?)과 예상 출력에 관심이 있습니다.


19
+1 그 행동을 전혀 고려하지 않았습니다
Jamie Wong

@ Peter Török : value == 2이면 6을 반환합니다.
Alexandre C.

4
@ Péter Török 아니오, 순서는 중요하지 않습니다-값이 대소 문자 레이블의 상수와 일치하면 제어는 레이블 다음에 해당 명령문으로 이동하고 그렇지 않으면 제어는 기본 레이블 다음에있는 명령문으로 이동합니다 (있는 경우).
피트 커캄

11
@Jon Cage goto는 사악하지 않습니다. 화물 컬트 추종자들은! 당신은 사람들이 피하려고 할 수있는 극단적 인 것을 상상할 수 없었습니다 goto.
Patrick Schlüter

3
내가 사용하는 goto유사한 시뮬레이션 뭔가 주로 finally능숙 (파일, 메모리), 중지 및 모든 오류 사건에 대한 목록을 반복 할 때 발표해야 할 기능의 절 freeclose가독성을 위해 도움이되지 않습니다. goto내가 피하고 싶지만 할 수없는 한 가지 용도가 있지만 루프에서 벗어나고 싶을 때 루프 안에 switch있습니다.
Patrick Schlüter

답변:


83

C99 표준은 이에 대해 명시 적이 지 않지만 모든 사실을 종합하면 완벽하게 유효합니다.

A casedefault레이블은 레이블과 동일합니다 goto. 6.8.1 레이블이있는 설명을 참조하십시오. 특히 흥미로운 것은 6.8.1.4이며 이미 언급 한 Duff의 장치를 활성화합니다.

모든 명령문 앞에는 식별자를 레이블 이름으로 선언하는 접두사가 올 수 있습니다. 레이블 자체는 제어 흐름을 변경하지 않으며,이를 통해 방해받지 않고 계속됩니다.

편집 : 스위치 내의 코드는 특별한 것이 아닙니다. if추가 점프 레이블이 있는 -statement에서와 같이 일반적인 코드 블록입니다 . 이것은 추락 행동과 이유를 설명합니다break 필요한지 .

6.8.4.2.7은 예를 제공합니다.

switch (expr) 
{ 
    int i = 4; 
    f(i); 
case 0: 
    i=17; 
    /*falls through into default code */ 
default: 
    printf("%d\n", i); 
} 

인공 프로그램 조각에서 식별자가 i 인 객체는 자동 저장 기간 (블록 내)으로 존재하지만 초기화되지 않으므로 제어 표현식이 0이 아닌 값을 갖는 경우 printf 함수에 대한 호출은 결정되지 않은 값에 액세스합니다. 마찬가지로 함수 f에 대한 호출에 도달 할 수 없습니다.

case 상수는 switch 문 내에서 고유해야합니다.

6.8.4.2.3 각 경우 라벨의 표현은 정수 상수 표현이어야하고, 동일한 스위치 문장에서 두 경우의 경우 상수 표현은 변환 후 동일한 값을 갖지 않아야한다. switch 문에는 기본 레이블이 하나 이상있을 수 있습니다.

모든 사례가 평가 된 후 다음과 같은 경우 기본 레이블로 이동합니다.

6.8.4.2.5 정수 승격은 제어 표현식에서 수행됩니다. 각 경우 레이블의 상수 표현식은 제어 표현식의 승격 된 유형으로 변환됩니다. 변환 된 값이 승격 된 제어 표현식의 값과 일치하면 제어는 일치하는 대소 문자 레이블 다음의 명령문으로 이동합니다. 그렇지 않으면 기본 레이블이 있으면 제어가 레이블이 지정된 명령문으로 이동합니다. 변환 된 대소 문자 상수 표현식이없고 기본 레이블이없는 경우 스위치 본문의 일부가 실행되지 않습니다.


6
@HeathHunnicutt 예제의 목적을 분명히 이해하지 못했습니다. 이 포스터는 코드를 구성하지는 않지만 이상한 스위치 문이 얼마나 나쁜지를 보여주고 나쁜 연습으로 인해 버그가 발생하는 방법을 보여주기 위해 C 표준에서 직접 가져 왔습니다. 코드 아래의 텍스트를 읽는 데 방해가 되었다면 많은 것을 알 것입니다.
Lundin

2
다운 보트를 보정하려면 +1 C 표준을 인용하여 누군가를 피하는 것은 상당히 가혹한 것 같습니다.
Lundin

2
@Lundin 저는 C 표준에 대해 투표권을 행사하지 않았으며 제안한대로 간과하지 않았습니다. 나는 불필요하고 불필요하게 모범을 보이는 나쁜 교육법을 다운 투표했다. 특히, 그 예는 요청 된 것과 완전히 다른 상황과 관련이 있습니다. 계속할 수는 있지만 "귀하의 의견에 감사드립니다."
Heath Hunnicutt

12
인텔은 가장 빈번한 코드를 Branch and Loop Reorganization 의 switch 문에서 가장 먼저 배치하여 오해를 방지하도록 지시 합니다. 나는 default다른 사건을 약 100 : 1로 지배 하는 사건 이 있기 때문에 여기에 왔으며 default, 첫 번째 사건 을 만드는 데 유효한지 또는 정의되지 않았는지 모르겠습니다 .
jww

@jww 인텔의 의미를 잘 모르겠습니다. 당신이 지능을 의미한다면 나는 그것을 가설이라고 부를 것입니다. 나는 같은 생각을 가졌지 만 나중에는 if 문과 달리 switch 문이 무작위 액세스라고 상태를 읽습니다. 따라서 마지막 경우는 첫 번째 경우보다 느리게 도달하지 않습니다. 이것은 상수 대소 문자 값을 해싱하여 수행됩니다. 따라서 분기가 많은 경우 switch 문이 if 문보다 빠릅니다.

91

case 문과 default 문은 switch 문에서 임의의 순서로 발생할 수 있습니다. 기본 절은 case 문에서 상수를 일치시킬 수없는 경우 일치하는 선택적 절입니다.

좋은 예 :-

switch(5) {
  case 1:
    echo "1";
    break;
  case 2:
  default:
    echo "2, default";
    break;
  case 3;
    echo "3";
    break;
}


Outputs '2,default'

사례가 코드에서 논리적 순서로 제시되기를 원할 때 (대소 문자 1, 사례 3, 사례 2 / 기본값을 말하지 않음) 사례가 너무 길어서 전체 사례를 반복하지 않으려는 경우 매우 유용합니다. 기본값은 하단에있는 코드


7
이것은 일반적으로 끝이 아닌 다른 곳에 기본값을 배치하는 시나리오입니다 ... 명시 적 사례 (1, 2, 3)에 논리적 순서가 있으며 기본값이 명시 적 사례 중 하나와 정확히 동일하게 작동하기를 원합니다. 마지막이 아닙니다.
ArtOfWarfare

51

어떤 경우에는 유효하고 매우 유용합니다.

다음 코드를 고려하십시오.

switch(poll(fds, 1, 1000000)){
   default:
    // here goes the normal case : some events occured
   break;
   case 0:
    // here goes the timeout case
   break;
   case -1:
     // some error occurred, you have to check errno
}

요점은 위 코드가 계단식 코드보다 읽기 쉽고 효율적이라는 것 if입니다. default마지막에 넣을 수는 있지만 일반적인 경우 (여기있는 default경우) 대신 오류 사례에주의를 기울이기 때문에 의미가 없습니다 .

실제로, 그것은 좋은 예가 아닙니다 poll. 최대 몇 개의 이벤트가 발생할 수 있는지 알고 있습니다. 나의 진짜 요점은 '예외'와 일반적인 경우가있는 정의 된 입력 값 세트 가 있는 경우 가 있다는 것 입니다. 예외 또는 정상적인 경우를 앞에 두는 것이 더 나은 선택입니다.

소프트웨어 분야에서 나는 매우 일반적인 또 다른 경우를 생각합니다. 일부 터미널 값을 사용한 재귀. 스위치를 사용하여 표현할 수있는 경우 default재귀 호출과 구별 요소 (개별 사례)가 포함 된 일반적인 값이 터미널 값이됩니다. 일반적으로 터미널 값에 초점을 맞출 필요가 없습니다.

또 다른 이유는 사례의 순서가 컴파일 된 코드 동작을 변경할 수 있으며 이는 성능에 중요합니다. 대부분의 컴파일러는 코드가 스위치에 나타나는 순서대로 컴파일 된 어셈블리 코드를 생성합니다. 첫 번째 경우는 다른 경우와 매우 다릅니다. 첫 번째 경우를 제외한 모든 경우에는 점프가 발생하고 프로세서 파이프 라인이 비게됩니다. 기본적으로 스위치에서 첫 번째로 나타나는 사례를 실행하는 분기 예측기처럼 이해할 수 있습니다. 다른 경우보다 훨씬 일반적인 경우 첫 번째 사례로 두어야 할 이유가 있습니다.

주석을 읽는 것이 코드 최적화에 대한 인텔 컴파일러 Branch Loop 재구성 을 읽은 후에 원래 포스터가 그 질문을 한 구체적인 이유 입니다.

그러면 코드 가독성과 코드 성능간에 중재가됩니다. 아마도 사건이 먼저 나타나는 이유를 미래 독자에게 설명하기 위해 의견을 작성하는 것이 좋습니다.


6
넘어지지 않는 행동없이 (좋은) 예를 들면 +1.
KillianDS

1
...하지만 그것에 대해 생각하면서, 나는 기본값을 찾는 사람이 거의 없기 때문에 기본값을 사용하는 것이 좋다고 확신하지 않습니다. case 문을 사용하여 리턴을 변수에 지정하고 if의 한 쪽에서는 성공을 처리하고 다른 쪽에서는 오류를 처리하는 것이 좋습니다.
Jon Cage

@ 존 : 그냥 작성하십시오. 가독성 이점없이 구문 노이즈를 추가합니다. 그리고 기본값이 맨 위에 있으면 실제로 볼 필요가 없습니다. 정말 분명합니다 (중간에 넣으면 더 까다로울 수 있습니다).
kriss

그건 그렇고 나는 C 스위치 / 사례 구문을 정말로 좋아하지 않습니다. 한 번의 사례 이후에 여러 개의 라벨을 붙이는 대신 여러 개의 라벨을 붙이는 것을 선호합니다 case. 우울한 점은 구문 설탕처럼 보이고 지원되는 경우 기존 코드를 중단하지 않는다는 것입니다.
kriss 2016 년

1
@kriss : 나는 "파이썬 프로그래머도 아니에요!" :)
Andrew Grimm

16

예, 이것은 유효하며 일부 상황에서는 유용합니다. 일반적으로 필요하지 않은 경우 필요하지 않습니다.


-1 : 나에게 악취가 난다. 코드를 한 쌍의 switch 문으로 분할하는 것이 좋습니다.
Jon Cage

25
@ 존 케이지 : 여기에 -1을 넣으면 불쾌합니다. 이것이 올바른 코드라는 것은 내 잘못이 아닙니다.
Jens Gustedt

궁금한 점이 있다면 어떤 상황에서 유용한 지 알고 싶습니다.
Salil

1
-1은 그것이 유용하다는 당신의 주장을 목표로했습니다. 클레임을 백업하기위한 유효한 예를 제공 할 수 있으면 +1로 변경하겠습니다.
Jon Cage

4
때로는 일부 시스템 기능에서 반환 된 errno로 전환 할 때. 클린 엑시트를해야한다는 것을 잘 알고 있지만이 클린 엑시트에는 반복하고 싶지 않은 코딩 라인이 필요할 수 있습니다. 그러나 개별적으로 처리하고 싶지 않은 다른 이국적인 오류 코드가 많이 있다고 가정 해보십시오. 나는 기본 사례에 perror를 넣고 다른 사례로 넘어 가서 깨끗하게 종료하는 것을 고려할 것입니다. 나는 당신이 그렇게해야한다고 말하지 않습니다. 그것은 단지 맛의 문제입니다.
Jens Gustedt

8

switch 문에 정의 된 순서가 없습니다. 사례를 레이블과 같은 명명 된 레이블과 같은 것으로 볼 수 있습니다 goto. 사람들이 여기에서 생각하는 것과 달리, 값 2의 경우 기본 레이블로 이동하지 않습니다. 고전적인 예를 들어 설명하기 위해 Duff의 장치switch/case 는 C 에서 극단의 포스터 자식입니다 .

send(to, from, count)
register short *to, *from;
register count;
{
  register n=(count+7)/8;
  switch(count%8){
    case 0: do{ *to = *from++;
    case 7:     *to = *from++;
    case 6:     *to = *from++;
    case 5:     *to = *from++;
    case 4:     *to = *from++;
    case 3:     *to = *from++;
    case 2:     *to = *from++;
    case 1:     *to = *from++;
            }while(--n>0);
  }
}

4
더프의 장치에 익숙하지 않은 사람은이 코드를 완전히 읽을 수 없습니다.
KillianDS

7

case 문의 끝 이외의 위치에 'default'를 두는 것이 적절하다고 생각되는 한 가지 시나리오는 상태가 유효하지 않은 상태 인 경우 머신을 재설정하고 초기 상태 인 것처럼 진행해야하는 상태 머신입니다. 예를 들면 다음과 같습니다.

스위치 (widget_state)
{
  기본값 : / * 레일에서 떨어짐-재설정하고 계속 * /
    widget_state = WIDGET_START;
    /* 실패로 끝나다 */
  사례 WIDGET_START :
    ...
    단절;
  사례 WIDGET_WHATEVER :
    ...
    단절;
}

유효하지 않은 상태가 기계를 재설정하지 않아야하지만 유효하지 않은 상태로 쉽게 식별 할 수있는 경우 대체 배치 :

스위치 (widget_state) { 사례 WIDGET_IDLE : widget_ready = 0; widget_hardware_off (); 단절; 사례 WIDGET_START : ... 단절; 사례 WIDGET_WHATEVER : ... 단절; 기본: widget_state = WIDGET_INVALID_STATE; /* 실패로 끝나다 */ 사례 WIDGET_INVALID_STATE : widget_ready = 0; widget_hardware_off (); ... "안전한"조건을 설정하기 위해 필요한 다른 조치를 취하십시오. }

그런 다음 다른 곳의 코드에서 (widget_state == WIDGET_INVALID_STATE)를 확인하고 오류보고 또는 상태 재설정 동작이 적절하다고 생각되는 것을 제공 할 수 있습니다. 예를 들어, 상태 표시 줄 코드에 오류 아이콘이 표시 될 수 있으며 대부분의 유휴 상태가 아닌 상태에서 비활성화 된 "위젯 시작"메뉴 옵션은 WIDGET_INVALID_STATE 및 WIDGET_IDLE에 대해 활성화 될 수 있습니다.


6

다른 예를 들어 보자 : "default"가 예상치 못한 경우이고 오류를 기록하지만 합리적인 작업을 수행하려는 경우에 유용 할 수 있습니다. 내 코드 중 일부의 예 :

  switch (style)
  {
  default:
    MSPUB_DEBUG_MSG(("Couldn't match dash style, using solid line.\n"));
  case SOLID:
    return Dash(0, RECT_DOT);
  case DASH_SYS:
  {
    Dash ret(shapeLineWidth, dotStyle);
    ret.m_dots.push_back(Dot(1, 3 * shapeLineWidth));
    return ret;
  }
  // more cases follow
  }

5

ENUM을 문자열로 변환하거나 파일을 쓰거나 읽을 때 문자열을 열거 형으로 변환하는 경우가 있습니다.

파일을 수동으로 편집하여 발생한 오류를 처리하기 위해 때때로 값 중 하나를 기본값으로 만들어야합니다.

switch(textureMode)
{
case ModeTiled:
default:
    // write to a file "tiled"
    break;

case ModeStretched:
    // write to a file "stretched"
    break;
}

2

default조건은 경우에 절이 존재할 수 스위치 내에서 어디든 될 수 있습니다. 마지막 조항 일 필요는 없습니다. 기본값을 첫 번째 절로 지정하는 코드를 보았습니다. 은 case 2:기본 조항이 위의 경우에도 정상적으로 실행됩니다.

테스트로 샘플 코드를 호출 test(int value){}하고 실행 한 함수에 넣었 습니다.

  printf("0=%d\n", test(0));
  printf("1=%d\n", test(1));
  printf("2=%d\n", test(2));
  printf("3=%d\n", test(3));
  printf("4=%d\n", test(4));

출력은 다음과 같습니다.

0=2
1=1
2=4
3=8
4=10

1

유효하지만 오히려 불쾌합니다. 나는 매우 복잡한 스파게티 코드로 이어질 수 있으므로 일반적으로 폴 스루를 허용하는 것이 좋지 않다고 제안합니다.

이러한 경우를 여러 개의 switch 문이나 더 작은 함수로 나누는 것이 거의 확실합니다.

[편집] @Tristopia : 예 :

Example from UCS-2 to UTF-8 conversion 

r is the destination array, 
wc is the input wchar_t  

switch(utf8_length) 
{ 
    /* Note: code falls through cases! */ 
    case 3: r[2] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x800; 
    case 2: r[1] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x0c0; 
    case 1: r[0] = wc;
}

다음과 같이 쓴다면 의도에 대해 더 명확 할 것입니다.

if( utf8_length >= 1 )
{
    r[0] = wc;

    if( utf8_length >= 2 )
    {
        r[1] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x0c0; 

        if( utf8_length == 3 )
        {
            r[2] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x800; 
        }
    }
}   

[edit2] @Tristopia : 두 번째 예는 후속 작업에 가장 적합한 예일 것입니다.

for(i=0; s[i]; i++)
{
    switch(s[i])
    {
    case '"': 
    case '\'': 
    case '\\': 
        d[dlen++] = '\\'; 
        /* fall through */ 
    default: 
        d[dlen++] = s[i]; 
    } 
}

.. 그러나 개인적으로 주석 인식을 자체 기능으로 나누었습니다.

bool isComment(char charInQuestion)
{   
    bool charIsComment = false;
    switch(charInQuestion)
    {
    case '"': 
    case '\'': 
    case '\\': 
        charIsComment = true; 
    default: 
        charIsComment = false; 
    } 
    return charIsComment;
}

for(i=0; s[i]; i++)
{
    if( isComment(s[i]) )
    {
        d[dlen++] = '\\'; 
    }
    d[dlen++] = s[i]; 
}

2
넘어지는 것이 정말로 좋은 아이디어 인 경우가 있습니다.
Patrick Schlüter

UCS-2에서 UTF-8 로의 변환 예제 r는 대상 배열이며 wc입력 wchar_t 스위치입니다 (utf8_length) {/ * 참고 : 코드는 대소 문자를 구분합니다! * / 사례 3 : r [2] = 0x80 | (wc & 0x3f); 화장실 >> = 6; wc | = 0x800; 사례 2 : r [1] = 0x80 | (wc & 0x3f); 화장실 >> = 6; wc | = 0xc0; 경우 1 : r [0] = wc; }
Patrick Schlüter

다음은 문자 이스케이프가 포함 된 문자열 복사 루틴입니다. for(i=0; s[i]; i++) { switch(s[i]) { case '"': case '\'': case '\\': d[dlen++] = '\\'; /* fall through */ default: d[dlen++] = s[i]; } }
Patrick Schlüter

예, 그러나이 루틴은 핫스팟 중 하나이며, 이는 가장 빠르고 이식 가능한 (어셈블리를 수행하지 않음) 방식이었습니다. 그것은 UTF 길이에 대해 단 하나의 테스트만을 가지고 있으며, 2 또는 3을 가지고 있습니다. 게다가, 나는 그것을 생각해 내지 못했습니다 .BSD에서 가져 왔습니다.
Patrick Schlüter

1
예, 특히 불가리아어 및 그리스어 (Solaris SPARC) 및 내부 마크 업 (3 바이트 UTF8)이 포함 된 텍스트가 변환되었습니다. 물론, 하드웨어 업데이트가 그다지 많지 않았으며, 지난 하드웨어 업데이트 이후에는 관련이 없지만, 작성 당시에는 약간의 차이가있었습니다.
Patrick Schlüter
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.