프로토타입이 선언되지 않은 함수의 호출 설명해주실 분?

kkb의 이미지

안녕하세요.

각각의 서로 다른 파일에서 동일한 함수를 호출하는데

하나의 함수는 호출하는 함수의 프로토타입이 선언되어 있는 헤더파일을 포함시켰고
다른 함수는 헤더파일을 포함시키지 않았습니다.

그런데 헤더파일을 포함시킨 함수는 매개변수가 제대로 넘어가는데
그렇지 않은 함수는 제대로 넘어가지 않는 것 같습니다.

$ gcc -o test.out test_main.c test1.c test2.c print_f.c && ./test.out
13 : 171 : 12500.000000 : d
13 : 171 : 0.000000 :
 
$

차라리 에러를 발생시키면 좋은데 에러도 발생하지 않습니다.
물론 warning 옵션을 주면 경고는 발생합니다.

$ gcc -W -Wall -o test.out test_main.c test1.c test2.c print_f.c && ./test.out
test2.c: In function 'test2':
test2.c:5: warning: implicit declaration of function 'print_f'
13 : 171 : 12500.000000 : d
13 : 171 : 0.000000 :
 
$

여기서 int형은 왜 정상적으로 넘어가고
float형과 char형은 제대로 넘어가지 않는지
설명해주실 수 있으신 분 있으신가요?

설명이 가능하다면 컴파일러가 error는 발생하지 않고 warning만 발생하게 하는 이유와
프로토타입 선언 없이 호출은 어떻게 되었는지도 궁금합니다.

아래에 모든 소스 코드를 붙여넣었습니다.

test_main.c 소스 파일

#include <string.h>
 
#include "test_head.h"
 
int main(void)
{
    int a, b;
    float c;
    char d;
 
    a = 13;
    b = 171;
    c = 12500;
    d = 'd';
 
    test1(a, b, c, d);
 
    test2(a, b, c, d);
 
    return 0;
}

test2.c 소스 파일

#include "test_head.h"
 
int test1(int a, int b, float c, char d)
{
    print_f(a, b, c, d);
 
    return 0;
}

test2.c 소스 파일

// #include "test_head.h"
 
int test2(int a, int b, float c, char d)
{
    print_f(a, b, c, d);
 
    return 0;
}

print_f.c 소스 파일

#include <stdio.h>
 
int print_f(int a, int b, float c, char d)
{
    printf("%d : %d : %f : %c\n", a, b, c, d);
 
    return 0;
}

test_head.h 헤더 파일

int test1(int a, int b, float c, char d);
int test2(int a, int b, float c, char d);
int print_f(int a, int b, float c, char d);
HDNua의 이미지

구형 C에서는 함수로 보이는 식별자가 선언되지 않았다면, 그냥 int 값을 반환하는 함수가 있다고 가정했습니다. (C99부터는 이런 암묵적 선언 또한 금지되었다고 합니다.)
링크 첨부합니다.
https://kldp.org/node/151875

원래는 인자 값이 잘 전달 안 되는 것을,
애초에 선언되지 않은 함수를 호출하는 행위가 Undefined behavior이므로 분석하는 것이 의미 없다는 글을 썼었는데,
다시 생각해보니 저도 잘 설명이 안 돼서 삭제했습니다.

저는 이렇게 생각했습니다.

익명 사용자의 이미지

1. 다른 해석 단위(translation unit)에 있는 함수를 호출하면서 매개변수를 넘길 때에는, 정확한 함수 원형(function prototype)이 필요합니다. 구체적으로는 함수 원형에 명시된 매개변수(argument) 타입이 제대로 전달되어야 매개변수(argument)를 정확히 전달할 수 있습니다.

구체적으로 매개변수가 전달되는 방법은 아키텍처마다, OS마다, 컴파일러마다 다를 수 있지만(이러한 요소들을 이하 구현환경(implementation) 이라고 하겠습니다.) 어쨌건 보내는 쪽과 받는 쪽이 같은 타입 정보를 가지고 있어야 한다는 건 분명합니다. 호출하는 쪽은 int값 10을 보냈는데 받는 쪽은 char 값으로 해석하려 한다던가, 호출하는 쪽은 float 값 10.0을 보냈는데 받는 쪽은 double 값으로 해석하려 한다던가 하면 안 된다는 얘깁니다. 만약 그랬다가는, 구현환경에 따라 문제 없을 수도 있고 이상한 값을 받게 될 수도 있습니다. 특히 32bit x86에서 주로 쓰이는 cdecl 호출 규약의 경우 모든 매개변수가 스택에 올라가는데, 매개변수의 타입을 모를 경우 매개변수의 크기(바이트 수)조차 알 수 없게 됩니다. 자칫하다간 답안지 밀려 쓰듯 쭉 망할 수도 있는 거지요.

2. 그러므로, 다른 해석 단위에 있는 함수를 매개변수와 함께 호출하려 할 때, 호출하려는 함수의 함수 원형이 제공되어 있지 않다는 건 일종의 넌센스입니다. 매개변수를 어떤 형식으로 어떻게 전달해야 되는지도 모르는데 어떻게 함수를 호출하겠다는 얘깁니까? 이런 함수 호출은 금지되어야 마땅합니다. 잘못될 수 있는 여지가 너무 많으니까요. 외부 함수를 호출하려면 반드시 함수 원형을 제공받아야 하고, 그러기 위한 가장 쉬운 방법은 함수 원형이 담긴 헤더 파일을 포함하는 것입니다.

3. 근데 왜 그게 금지가 안 되었느냐, 함수 원형 없이 어떻게 매개변수를 넘겨 함수를 호출할 수 있느냐고 묻는다면, "원래 그래 왔었기 때문에" 라고밖에 할 말이 없습니다. 역사 공부 좀 해 보죠. 아래는 C89 시절의 C언어 표준 드래프트인데, 비록 드래프트이긴 하지만 표준과 상당히 가까울 겁니다. ISO 표준 문서는 제법 비싸요.

http://port70.net/~nsz/c/c89/c89-draft.html#3.3.2.2

조금 발췌합니다.

If the expression that precedes the parenthesized argument list in a function call consists solely of an identifier, and if no declaration is visible for this identifier, the identifier is implicitly declared exactly as if, in the innermost block containing the function call, the declaration

extern int identifier();

appeared.[30](That is, a function with external linkage and no information about its parameters that returns an int . If in fact it is not defined as having type ``function returning int ,'' the behavior is undefined.)

An argument may be an expression of any object type. In preparing for the call to a function, the arguments are evaluated, and each parameter is assigned the value of the corresponding argument.31 The value of the function call expression is specified in 3.6.6.4

If the expression that denotes the called function has a type that does not include a prototype, the integral promotions are performed on each argument and arguments that have type float are promoted to double. These are called the default argument promotions. If the number of arguments does not agree with the number of parameters, the behavior is undefined. If the function is defined with a type that does not include a prototype, and the types of the arguments after promotion are not compatible with those of the parameters after promotion, the behavior is undefined. If the function is defined with a type that includes a prototype, and the types of the arguments after promotion are not compatible with the types of the parameters, or if the prototype ends with an ellipsis ( ", ..." ), the behavior is undefined.

중점적으로 봐야 할 내용은 아래 두 가지로 요약됩니다.

1) 어떤 식별자(identifier)가 함수로서 호출되는데 그 식별자에 대한 함수 원형이 보이지(visible) 않을 경우, 그 식별자는 (매개변수에 대한 정보는 없고) int를 반환하며 외부 연결(external linkage, C언어에 대해 깊이 알고 있지 않다면 잘 모를 용어인데, 그냥 extern 함수라고 생각하세요)을 가진 함수로 간주됩니다. "실제로 그런 함수가 아닐 경우의 동작은 정의되지 않습니다(the behavior is undefined)"

2) 함수 호출 과정에서 매개변수 타입 정보 없이 매개변수를 전달할 때에는, 기본적으로 char, short int, int bit-field 및 이들의 signed/unsigned 타입, 혹은 enumeration 타입의 매개변수에 대해서 integral promotion이 적용되고, float 형식의 매개변수는 double 타입으로 변환되어 전달됩니다. 여기서 integral promotion이 뭐냐면, 쉽고 간단하고 (약간 모호하게) 말해서 "기본적으로 int로 변환하되, 원래 타입의 값 범위가 int의 범위 밖이면 unsigned int로 변환한다"는 겁니다. 이러한 일련의 절차를 "default argument promotions" 이라고 합니다. 더 간단히 (더 모호하게) 말하면, 매개변수의 타입 정보가 안 주어졌을 땐 대충 int, unsigned int, double 등으로 찍는다는 겁니다. 이렇게 찍은 타입이 실제 함수의 매개변수 타입과 호환되지 않을 경우에는, 마찬가지로 그 동작이 정의되지 않습니다.

4. 위의 요약을 대충 숙지하고, 이제 질문자님이 예시로 든 코드에서 무슨 일이 일어나고 있는지를 쫓아가 보죠.

1) main은 test1과 test2를 호출하는데, 둘 다 함수 원형이 정확히 주어져 있습니다. 그러므로 모든 매개변수를 옳은 타입으로 전달합니다.
2) test1은 매개변수를 올바르게 전달받습니다. 또한 print_f의 함수 원형도 정확히 알고 있으므로 역시나 매개변수를 정확히 전달합니다.
3) print_f는 test1이 전달해 준 매개변수를 정확히 전달받아 정확히 출력합니다.
* 약간 논외이기도 하지만 관련성이 있으니 말씀드립니다. 사실 print_f가 printf를 호출하는 과정에서도 default argument promotions이 일어납니다. printf는 이를 염두에 두고 만들어져 있으므로 따로 신경쓸 필요는 없지만 이 점을 엿볼 수 있는 한 가지 특이한 점이 있는데요. 혹시 printf의 서식화 문자열(%s, %d 등) 중에 float 타입을 출력하는 것은 없다는 점을 알고 계셨는지요. default argument promotions의 특성상 printf에 float 타입의 값을 넘겨도 반드시 double로 변환되어 전달되기에, printf는 결코 float 값을 볼 수 없습니다.
4) test2 역시 매개변수를 올바르게 전달받습니다만, 이 함수는 print_f에 대한 선언(및 함수 원형)을 보지 못합니다! 따라서, extern int print_f() 와 같은 선언이 있는 것으로 간주합니다. (이를 implicit function declaration이라고 합니다.) 매개변수의 타입 정보가 없으므로...
i) a는 int형이므로 int형 그대로 전달합니다.
ii) b도 마찬가지.
iii) c는 float형입니다. 따라서 double로 전달합니다.
iv) d는 char형입니다. char 타입이 부호를 가지는지 안 가지는지는 구현환경에 따라 다릅니다만, 어쨌든 둘 다 int 안에 커버되므로 d는 int로 전달됩니다.

즉, test2는 print_f가 마치 extern int print_f(int a, int b, double c, int d) 와 같이 선언된 것처럼 호출합니다.

다른 건 몰라도 float과 double이 호환이 될 것 같지는 않죠. 즉 프로그램은 미정의 동작에 빠집니다. 표준에서 "the behavior is undefined"이라고 언급한 건 무척 무서운 의미이고, 프로그래머는 절대 그런 상황을 초래하는 프로그램을 짜서는 안 되는 겁니다.

표준에서 그 동작을 정의하지 않았을 경우 구현환경에서는 말 그대로 어떤 일이든 일어날 수 있기 때문에, 이후의 상황은 그저 "버그"일 뿐 그 이상 구체적인 분석은 무의미합니다. 하지만 특정 구현환경 및 함수 호출 규약을 가정한다면 더 진행해 볼 수 있죠. 제게 가장 익숙한 x86, 32bit, cdecl 호출 규약을 가정하면

1) 매개변수는 스택에 역순으로 push되어 첫 매개변수가 가장 낮은 메모리 주소에 오게 됩니다. 따라서, 마침 맨 앞에 있었고 타입 정보도 정확한 a와 b는 정확히 전달됩니다.
2) c부터 문제인데, 시작 지점은 정확하지만 double(아마도 IEEE754 배정밀도, 8바이트)로 push한 값을 float(아마도 IEEE754 단정밀도, 4바이트)으로 해석할 테니 문제가 없을리가요. 왜 0.000000이 찍혀 나왔는지는 double과 float의 비트 구성을 따져보면 알 수 있겠지만, 솔직히 저도 귀찮네요. 아무튼 기적이 터지지 않는 이상은 정확히 전달될 리가 없습니다.
3) d는 아예 시작점부터 잘못되어 있습니다. x86은 little endian이라, char 값이 int 값으로 변환되어 넘어간다 한들 시작점을 올바르게 잡으면 전달될 수도 있었을 텐데요. c와 d의 순서를 바꿔서 시도해보세요. 그럼 아마 d도 잘 전달될 겁니다.

어쨌거나, 미정의 동작이므로 분석이 큰 의미가 있지는 않습니다.

5. implicit function declaration은 비록 문제 없이 돌아가는 경우가 있기는 하나, 이렇듯 위험한 코드를 에러 없이 컴파일하여 실행 시점에 미정의 동작을 유발할 수 있으므로 별로 바람직하지 못한 기능입니다.

실제로 이 기능은 최신 C언어 표준(C99 등)에서는 빠진 것으로 알고 있습니다. 함수를 호출하려면 반드시 그 전에 선언하라는 거죠. 예전에는 함수 선언이 없었어도 컴파일러가 대충 찍어서 넘겨줬지만, 그에 따른 득(프로그래머가 코딩할 때 약간 덜 귀찮음)보다는 실(미정의 동작에 빠진 프로그램을 디버깅하는데 들어가는 상당한 노력)이 더 크다는 겁니다. 문제는, 그렇다고 이 기능을 막무가내로 없애 버리면, implicit function declaration 기능에 의존했던 수많은 옛날 코드들이 컴파일이 "당장" 안 될 거라는 거죠. 때문에 요즘 컴파일러도 울며 겨자 먹기로 implicit function declaration을 지원은 하되, 경고를 해 주는 겁니다.

kkb의 이미지

이렇게 친절하게 고급 답변을 해주셨는데 익명이시라 감사의 제 뜻을 확인하실련지도 모르겠네요.
추가 설명도 너무 재미있게 읽었습니다.
정말 이럴 때는 어떻게 감사를 드려야 감사가 될까도 잘 모르겠습니다.
진심으로 감사드립니다.

익명 사용자의 이미지

일단 C99에서 암묵적인 함수 선언이 금지인건 제껴두고(그렇지 않으면 말이 안되니까)
그 이전 표준 기준으로 말씀드리자면
선언이 없는 함수는 다음과 같은꼴의 비원형 선언으로 간주합니다.

int func(); /* 매개변수 목록 없음*/

이런 비원형 함수 선언일 때는 각 매개변수에 윗분이 설명해 주신 default argument promotion, 기본 인자 진급이라는 과정을 거치게 되며, 여기에 또하나 복잡한 제한이 따라붙게 됩니다.

함수 선언이 비원형이고 함수 정의도 비원형일 때 : 함수의 실제 호출시 각 매개변수에 기본 인자 진급을 거친 데이터형과, 함수 정의의 각 매개변수에 기본 인자 진급을 거친 데이터형이 서로 호환되어야 한다.
함수 선언이 비원형이고 함수 정의는 원형일 떄 : 함수의 실체 호출시 각 매개변수에 기본 인자 진급을 거친 데이터형이 함수 정의의 각 매개변수가 가지는 데이터형과 호환되어야 한다.

질문글의 예제는 함수 선언이 비원형이고 함수 정의는 원형인 경우에 해당하고, 이 경우엔 함수 정의의 매개변수 타입이 각각
int, int, float->double, char->int가 되어야 합니다.

참고로 비원형 함수 정의란 다음과 같은 형태를 말합니다.

func(a, b, c, d) int a; char b; int d[5]; /* 반환 타입은 붙이지 않으면 int로 간주. 타입을 따로 정해주지 않은 c는 int형으로 간주 */
{
/* ... */
}

케케묵은 유닉스 코드에서나 볼 수 있는 형태고, 지금은 쓰지 않는게 좋습니다. 위에서 말한 대로 기본 인자 진급과 얽힌 복잡한 문제들이 있어서...

오래된 C언어 책에서 보이는 main함수의 형태인

main()
{
}

이건 비원형 함수 정의 스타일입니다.

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.