1. 개요

정보처리기사 실기 시험을 준비 없이 치렀다가 예상치 못한 문제에 호되게 당했다. 시험 당시에는 당황해서 제대로 풀지 못했지만, 틀린 문제를 다시 학습하고 넘어가는 것이 중요하다고 판단했다. 이 글은 당시 마주했던 C언어 코드의 동작 방식을 분석하고, 이 과정에서 새롭게 알게 된 사실들을 정리한 기록이다.

처음에는 문제를 보고 ‘함수 포인터’라고 섣불리 짐작했지만, 분석 결과 핵심은 전역 변수(Global Variable)포인터(Pointer)의 동작 원리에 대한 이해였다. 이 글을 통해 코드의 실행 결과를 분석하고, 왜 그런 결과가 나왔는지 명확히 설명하고자 한다. 더불어, 헷갈렸던 ‘함수 포인터’의 개념과 C언어에서 gets() 함수 사용을 지양해야 하는 이유까지 함께 다룬다.

2. 문제의 코드와 실행 결과

시험에서 마주했던 코드는 아래와 같다. test() 함수를 세 번 호출하여 그 반환값을 각기 다른 포인터 변수에 저장하고, 마지막에 이 변수들을 출력하는 간단한 코드였다.

#include<stdio.h>
#include<stdlib.h>
 
// 입력을 저장할 전역 변수
char n[30];
 
// 입력을 받아 n의 주소를 반환하는 함수
char *test() {
  printf("입력하세요 : ");
  gets(n);
  return n;
}
 
int main()
{
  char * test1;
  char * test2;
  char * test3;
 
  test1 = test();
  test2 = test();
  test3 = test();
 
  printf("%s\n",test1);
  printf("%s\n",test2);
  printf("%s",test3);
}

나는 당연히 입력한 순서대로 “홍길동”, “김철수”, “박영희”가 출력될 것이라 예상했다. 하지만 실제 실행 결과는 예상과 전혀 달랐다.

# 컴파일 시 gets() 함수에 대한 경고가 발생한다.
main.c:(.text+0x27): warning: the `gets' function is dangerous and should not be used.
 
# 실행 결과
입력하세요 : 홍길동
입력하세요 : 김철수
입력하세요 : 박영희
박영희
박영희
박영희

마지막에 입력한 “박영희”만 세 번 출력되었다.

3. 원인 분석: 모든 포인터는 같은 곳을 가리키고 있었다

결과가 이렇게 나온 이유는 포인터 변수 test1, test2, test3가 모두 동일한 메모리 주소를 가리키고 있었기 때문이다.

  1. char n[30];전역 변수로 선언되었다. 전역 변수는 프로그램이 시작될 때 메모리의 특정 공간(Data 영역)에 자리를 잡고, 프로그램이 종료될 때까지 그 주소를 유지한다.
  2. test() 함수는 gets(n);을 통해 이 고정된 공간에 문자열을 입력받고, return n;을 통해 n의 시작 주소값을 반환한다.
  3. 따라서 test1 = test();, test2 = test();, test3 = test(); 세 번의 호출을 통해 test1, test2, test3 변수에는 모두 n의 동일한 주소값이 저장된다.
  4. gets(n)이 호출될 때마다 n의 메모리 공간에 저장된 내용은 새로 덮어써진다. 처음에는 “홍길동”이, 그 다음엔 “김철수”가, 마지막엔 “박영희”가 덮어쓴 것이다.
  5. 결론적으로 printf로 각 포인터를 출력하는 시점에는, 세 포인터 모두 마지막 값인 “박영희”가 저장된 동일한 메모리 주소를 참조하게 되므로, “박영희”만 세 번 출력되는 것이었다.

결국 이 문제는 포인터가 ‘값’이 아닌 ‘주소’를 저장한다는 기본 원리를 제대로 이해하고 있는지를 묻는 문제였다고 판단했다.

4. 함수 포인터(Function Pointer)란 무엇인가?

나는 처음에 char *test()라는 함수 선언을 보고 ‘함수 포인터’ 문제라고 착각했다. 하지만 이는 **‘char형 포인터를 반환하는 함수’**를 의미하는 것이지, 함수 포인터와는 전혀 다른 개념이다.

함수 포인터(Function Pointer)는 말 그대로 함수의 시작 주소를 저장하는 포인터 변수다. 이를 통해 함수를 직접 호출하는 대신, 포인터를 통해 간접적으로 호출할 수 있다.

  • 함수 포인터 선언: 반환타입 (*포인터이름)(매개변수타입);
  • 예시 코드
    #include<stdio.h>
     
    void say_hello() {
        printf("Hello!\n");
    }
     
    int main() {
        // say_hello 함수의 주소를 저장할 함수 포인터 func_ptr 선언
        void (*func_ptr)();
     
        // 함수 포인터에 함수의 주소 할당
        func_ptr = &say_hello;
     
        // 함수 포인터를 통해 함수 호출
        (*func_ptr)();
     
        return 0;
    }

이처럼 함수 포인터는 함수의 선언부와 유사한 형태로 포인터를 선언하며, 콜백(Callback) 함수를 구현하거나, 실행 시점에 호출할 함수를 동적으로 결정하는 등 고급 프로그래밍 기법에 활용된다.

5. gets() 함수를 사용하면 안 되는 이유

코드를 컴파일할 때 the 'gets' function is dangerous and should not be used. 라는 경고가 발생했다. 그 이유는 gets() 함수가 버퍼 오버플로우(Buffer Overflow)에 매우 취약하기 때문이다.

gets() 함수는 입력을 저장할 버퍼(n[30])의 크기를 확인하지 않는다. 만약 사용자가 30바이트보다 긴 문자열을 입력하면, n 배열에 할당된 메모리 공간을 넘어서 인접한 다른 메모리 영역까지 침범하게 된다. 이는 프로그램의 오작동을 유발하거나, 악의적인 코드가 실행될 수 있는 심각한 보안 취약점으로 이어질 수 있다.

IMPORTANT

이러한 이유로 gets() 함수는 C11 표준부터 공식적으로 C 표준 라이브러리에서 제거되었다. 따라서 절대 사용해서는 안 된다.

대신, 버퍼의 크기를 함께 인자로 전달하여 오버플로우를 방지할 수 있는 fgets() 함수를 사용하는 것이 안전하다.

// fgets(저장할 버퍼, 버퍼의 최대 크기, 입력 스트림);
fgets(n, sizeof(n), stdin);

6. 결론

단순한 시험 문제 복기를 통해 C언어의 핵심 개념들을 다시 한번 되짚어볼 수 있었다.

  • 포인터는 값을 저장하는 변수가 아니라, 메모리의 특정 위치를 가리키는 주소를 저장한다.
  • 전역 변수는 프로그램 전체에서 단 하나의 고정된 주소를 가지며, 여러 포인터가 이 주소를 동시에 참조할 수 있다.
  • ‘포인터를 반환하는 함수’와 ‘함수를 가리키는 포인터’는 이름은 비슷하지만 완전히 다른 개념이므로 명확히 구분해야 한다.
  • gets()와 같이 보안에 취약한 함수는 사용을 지양하고, fgets()와 같은 안전한 대안을 사용하는 습관을 들여야 한다.

소는 잃었지만, 덕분에 튼튼한 외양간을 지을 수 있게 되었다고 생각한다.

7. 참고