C언어

[C23-] 함수 파라미터에 void를 써야 할까? ─ int func(), int func(void)

카루-R 2023. 12. 4. 13:05
반응형

환영합니다, Rolling Ress의 카루입니다.

이 글은 C23 이전 표준안에 대해 다루고 있습니다. 사실 아직까지도 C11은 커녕 C99를 주로 쓰는 경우가 많아서, 대부분 이 글에 적용된 내용이 해당됩니다. 한 20년쯤 지나면 이 글이 필요 없어질 수도 있겠군요. 참고로 C++에는 해당이 되지 않는 이야기입니다. C11 이전의 C 표준에서만 해당됩니다.

흔히 C언어에서는 함수를 선언할 때 꼭 void를 쓰라고 합니다.

 
int func();
int func(void);

C++ 에서는 위 둘의 뜻이 정확히 같습니다. 매개변수를 받지 않는다는 뜻이죠. 오히려 func(void)보다는 func()를 권장합니다. C# 같은 언어에서는 아예 첫 번째 표현만 가능하고요. (두 번째 표현을 쓰면 오류가 나버립니다.)

C언어에서는 둘의 뜻이 다릅니다. 첫 번째 표현은 '매개변수가 얼마나 들어올지 정해지지 않았다' 라는 뜻이고, 두 번째 표현은 '매개변수를 받지 않는다' 라는 뜻입니다. 그런데 이 문장만 가지고는 무슨 뜻인지 이해가 잘 안 가죠. 매개변수가 얼마나 들어올지 정해지지 않았다니, 가변인수랑 무슨 차이일까요?

결론부터 말하자면 이건 가변인수와는 상관 없는 전혀 다른 문법으로, K&R 스타일 함수 정의에서 유래했습니다.

// ANSI style, New style
int sum(int a, int b)
{
    return a + b;
}

// K&R style, Old style
int sum(a, b)
    int a;
    int b;
{
    return a + b;
}

// 더 옛날
sum(a, b)          // return type이 없는 경우 int로 간주
{
    return(a + b); // 괄호가 반드시 필요했음
}

첫 번째 문법은 흔히 우리가 사용하던 거죠. 그런데 두 번째 문법은 처음 보는 분들이 많으실 겁니다. 아주 예전 C언어는 저런 문법을 사용했어요. 그보다 더 옛날로 가면(C89/90 이전) 반환 타입을 적지 않아도 작동했습니다. K&R 스타일 자체가 문제가 많아서 지금도 이미 deprecated 되었는데, C23에서는 아예 없애버렸습니다.

// ANSI style 선언
int sum(int a, int b);

// K&R style 선언
int sum();

참고로 C에서는 함수를 선언하지 않고 사용하면 해당 함수의 반환값은 int, 매개변수는 ()로 간주합니다. 즉, 매개변수가 void가 아닌 "정해지지 않음"으로 간주합니다. 따라서 int형을 반환하는 함수의 경우 main() 함수 뒤에 정의가 되어 있고 선언을 따로 하지 않아도 정상 작동합니다. 실제 정의에서 매개변수를 어떤 걸, 얼마나 사용하든 선언부는 그냥 int func(); 로 되어 있을 테니까요. 근데 이런 거에 의존하지 말고 선언부는 무조건 작성하세요.

Promotion

K&R Style에서 중요한 요소가 또 있습니다. 승격(Promotion)입니다. int보다 작은 모든 정수형 변수는 int / unsigned int로, 모든 float은 double로 바뀝니다. 근데 호출 과정에서만 이렇게 바뀌는 거고 함수 내부에서는 정의된 대로 사용됩니다.

// void func();

char c;
short s;

func(c, s); // c, s가 각각 int로 변환된 후 넘어감

컴파일러는 선언부인 void func()만 볼테니, 매개변수의 타입과 수를 모르죠. 그래서 마치 가변인자 함수와 비슷한 방식으로 동작합니다. char, short는 int로, float은 double로 승격되는 거죠.

그렇다면 아래의 경우는 어떨까요?

short add(a, b)
    short a, b;
{
    return a + b;
}

short t, u;
add(t, u); // ???

간략하게 축약한 코드이므로 일부러 초기화하지 않았습니다. 이 경우, short형 변수 t, u는 우선 int로 형변환 되어 전달된 후, 함수 내에서 다시 short로 변환됩니다. 비효율적이기 짝이 없죠.

결론

void를 반드시 쓰세요.

int func1() { return 0; }
int func2(void) { return 0; }

int main(void) { // 당연히 여기도 void를 넣는 게 좋습니다.
    int a = func1(); // ok
    int b = func2(); // ok
    int c = func1(100); // 정의되지 않은 동작. 여기서는 마치 정상처럼 동작할 수 있습니다.
    int d = func2(100); // 명시적 컴파일 에러
    return 0;
}

void를 넣지 않고 저렇게 빈 칸으로 놔두면, 컴파일러는 이게 매개변수가 제대로 들어간 건지 확인을 할 수가 없습니다. 넣지 말아야 할 함수에 넣으면 명시적으로 컴파일 에러를 때려줄 수 있는데, 그걸 못 하는 거죠.

특히 선언부/정의부의 mismatch도 걸러줄 수 없습니다.

int f1(); // 선언부: 오류 없음!
int f1(int a, int b) { return 0; }

int f2(void); // 선언부
int f2(int a, int b) { return 0; } // 에러! 선언과 정의 타입 불일치

만약 정의부에서는 파라미터를 두 개 받는데, 선언부에서는 ()로만 선언했다면 오류가 나지 않습니다. 어차피 unknown으로 선언한 거니까요. (void)로 선언하면 명시적으로 없다고 선언한 것이니, 정의부와 다르다고 오류가 나겠죠. 첫 번째처럼 의도적으로 선언할 리는 거의 없겠지만, 실수를 막을 수 없습니다.

반응형

'C언어' 카테고리의 다른 글

[C11] C언어도 제네릭이? _Generic 키워드 살펴보기  (0) 2023.12.31