Core Programming/C for Systems Engineering

C언어 함수 총정리: 매개변수 전달 방식(값/참조)부터 함수 포인터까지 완벽 마스터

임베디드 친구 2024. 12. 13. 13:39
반응형

처음 프로그래밍을 시작할 때는 코드가 짧아서 main 함수 안에 모든 로직을 다 때려 넣어도 아무 문제가 없습니다. 하지만 프로그램의 덩치가 커져 소스 코드가 수천, 수만 줄이 넘어가면 이야기가 완전히 달라집니다. 똑같은 계산 코드를 여기저기 복사/붙여넣기 하다가 오타가 나기도 하고, 어디가 고장 났는지 흐름을 쫓아가기도 불가능에 가까워집니다. 이럴 때 필요한 구원투수가 바로 함수(Function)입니다. 함수는 특정 작업을 수행하는 코드들을 하나의 상자 안에 예쁘게 포장해 둔 것과 같습니다. 필요할 때마다 이 상자의 이름만 불러주면 언제든 똑같은 기능을 재사용할 수 있죠. 코드를 청소하고 구조화하는 함수 디자인 기법에 대해 자세히 알아보겠습니다.

Generated by Gemini AI.

📌 핵심 요약 3줄

  • 코드의 모듈화와 재사용: 반복되는 복잡한 로직을 하나의 독립된 함수로 분리하면 중복 코드가 사라지고 전체적인 가독성이 비약적으로 상승합니다.
  • 인자 전달의 두 가지 메커니즘: 값을 복사해서 넘기는 방식(Call by Value)과 주소를 넘겨 원본을 수정하는 방식(Call by Reference)의 메모리 동작 원리를 이해해야 합니다.
  • 함수의 확장, 재귀와 포인터: 자기 자신을 호출하는 재귀 구조나 함수 자체의 메모리 주소를 가리키는 함수 포인터를 활용하면 한 차원 높은 동적 코딩이 가능해집니다.

1. 함수의 구조와 구성 요소

함수는 입력값(매개변수)을 받아 상자 안에서 가공한 뒤, 결과물(반환값)을 밖으로 뱉어내는 공장 라인과 같습니다.

함수의 구성 요소 역할 및 기능 설명 비유 및 특징
반환형 (Return Type) 함수가 모든 연산을 끝내고 최종적으로 돌려줄 데이터의 자료형을 명시합니다. 공장의 최종 생산품 종류 (없으면 void)
함수 이름 (Function Name) 해당 함수 코드 블록을 호출하기 위해 프로그래머가 부여한 고유 명칭입니다. 공장 라인의 이름 (예: add, stringLength)
매개변수 (Parameter) 함수가 작동하기 위해 외부로부터 입력받아야 하는 변수들의 목록입니다. 공장에 투입되는 원자재 데이터
반환값 (Return Value) return 키워드 뒤에 오는 값으로, 함수를 부른 원래 자리로 결과 데이터를 전송합니다. 완성된 제품 배달

2. 가장 중요한 개념: 매개변수 전달 방식 2가지

함수를 호출할 때 데이터를 인자로 넘겨주는 방식은 메모리 내부 동작에 따라 완전히 다른 두 갈래 길로 나뉩니다. C 언어 개발자라면 이 차이점을 반드시 뼈에 새기고 있어야 합니다.

전달 방식 영문 명칭 데이터 전달의 본질 원본 데이터의 안전성
값에 의한 전달 Call by Value 인자로 넘어온 값을 그대로 복사하여 함수 내부의 새로운 독립된 지역 변수에 담습니다. 완벽히 안전함 (함수 안에서 가공해도 원본 변수는 끄떡없음)
참조에 의한 전달 Call by Reference 원본 변수의 실제 **메모리 주소(&)**를 포인터를 통해 통째로 넘겨줍니다. 수정 가능 (함수 내부에서 역참조*로 원본을 직접 변형시킴)

💻 Call by Value vs Call by Reference 비교 예제

C
 
#include <stdio.h>

// 1. 값에 의한 전달: 메모리에 새로운 방 x를 파서 값을 복사해 옵니다.
void printValue(int x) { 
    x = 10; 
    printf("Call by Value 함수 안 x의 값: %d\n", x); 
} 

// 2. 참조에 의한 전달: 포인터 변수 x가 원래 변수의 주소를 직접 가리킵니다.
void updateValue(int *x) { 
    *x = 10; // 주소로 찾아가서 알맹이를 직접 바꿉니다.
} 

int main() { 
    int a = 5; 
    
    printValue(a); 
    printf("printValue 호출 후 메인의 a 값: %d\n\n", a); // 여전히 5입니다.

    updateValue(&a); // 주소를 넘겨줍니다.
    printf("updateValue 호출 후 메인의 a 값: %d\n", a); // 10으로 변경되었습니다!
    
    return 0; 
}

3. 알고리즘의 단골 손님, 재귀 함수 (Recursive Function)

재귀 함수는 함수가 실행 도중에 자기 자신을 다시 이름으로 불러내는 독특한 구조를 가집니다. 수학의 점화식을 코드로 그대로 옮겨 담을 수 있어 트리 구조나 팩토리얼 같은 알고리즘을 짤 때 코드를 엄청나게 압축해 줍니다.

C
 
#include <stdio.h>

// 팩토리얼(n!)을 계산하는 재귀 함수입니다.
int factorial(int n) {
    if (n <= 1) {
        return 1; // [필수] 이 종료 조건이 있어야 탈출할 수 있습니다!
    }
    return n * factorial(n - 1); // 자기 자신을 다시 호출합니다.
}

int main() {
    int result = factorial(5); // 5 * 4 * 3 * 2 * 1
    printf("5! 팩토리얼 계산 결과: %d\n", result); // 120 출력
    return 0;
}

4. 함수를 가리키는 나침반, 함수 포인터 (Function Pointer)

우리가 만든 함수 코드들도 컴파일이 끝나면 결국 메모리의 코드 영역(Text Segment)이라는 특정 주소 자리에 둥지를 틀게 됩니다. 일반 포인터가 변수의 주소를 저장하듯, 함수의 시작 주소를 저장하는 포인터를 '함수 포인터'라고 부릅니다. 이를 쓰면 상황에 따라 실행할 함수를 동적으로 교체할 수 있습니다.

C
 
#include <stdio.h>

int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }

int main() {
    // 정수형 인자 2개를 받고 정수를 반환하는 함수 포인터 operation을 선언합니다.
    int (*operation)(int, int);

    operation = add; // add 함수의 주소를 포인터에 대입합니다.
    printf("함수 포인터 더하기 결과: %d\n", operation(5, 3)); // 8

    operation = subtract; // 원할 때 언제든 다른 함수로 갈아 끼웁니다.
    printf("함수 포인터 빼기 결과: %d\n", operation(5, 3)); // 2

    return 0;
}

💡 C 언어 함수 디자인을 위한 꿀팁 (Tip)

  1. 함수의 선언(Prototype)과 정의를 분리하세요: main 함수 아래쪽에 새로운 함수를 만들면 컴파일러가 위에서 읽어 내려오다가 "이게 무슨 함수냐"라며 에러를 뱉습니다. 이를 방지하려면 main 함수 위쪽에 int add(int a, int b); 처럼 함수의 이름과 뼈대만 선언(원형 선언)해 두고, 실제 상세 구현은 main 함수 아래로 빼주는 것이 거대한 프로젝트를 관리하는 실무 표준 가이드라인입니다.
  2. 함수의 역할은 무조건 '단 하나'로 제한하세요: 하나의 함수 안에서 입력도 받고, 사칙연산도 하고, 파일 저장까지 전부 처리하게 짜면 재사용성이 제로가 됩니다. calculateGrade(), saveToFile() 처럼 동사형 이름으로 기능을 쪼개어 단 하나의 책임만 주도록 설계하는 것이 가독성에 좋습니다.
  3. 읽기 전용 포인터 인자에는 const를 붙이세요: 배열이나 문자열의 주소를 함수에 넘길 때, 내부에서 내용물이 실수로 손상되는 것을 막고 싶다면 매개변수 선언부에 int stringLength(const char *str) 처럼 const 키워드를 명시하세요. 코드의 안정성이 올라가고 컴파일러가 의도치 않은 수정을 실시간으로 감시해 줍니다.

⚠️ 초보자가 흔히 하는 실수 (Mistakes)

  • 재귀 함수의 탈출(종료) 조건 누락: 재귀 함수를 설계할 때 바닥에 언제 멈춰야 할지 알려주는 if문을 빼먹거나 잘못 설정하면, 함수가 끝없이 메모리를 파고들며 자취를 남기다가 결국 메모리 한계치를 넘겨 프로그램이 즉사하는 Stack Overflow 에러를 유발합니다.
  • 함수 내부 지역 변수의 주소를 반환하는 행위: 함수 안에서 선언된 일반 변수는 함수가 return문으로 끝나는 순간 메모리에서 연기처럼 완전히 증발(소멸)합니다. int* 잘못된함수() { int temp = 10; return &temp; } 처럼 소멸할 변수의 주소를 밖으로 던진 뒤 외부에서 이 주소를 열어보려고 하면 이미 파괴된 껍데기 공간을 건드리게 되어 치명적인 메모리 오염 버그를 만나게 됩니다.
  • 함수 포인터 선언 시 괄호() 생략 에러: 함수 포인터를 선언할 때 int (*operation)(int, int); 처럼 변수명 주위의 괄호를 빼먹고 int *operation(int, int); 라고 적으면, 주소를 담는 포인터 변수가 아니라 정수형 포인터 주소를 반환하는 일반 함수로 완전히 다르게 해석되므로 문법 기호에 주의해야 합니다.

🔚 맺음말

오늘 C 언어 포스팅에서는 복잡하게 얽힌 소스 코드를 구획별로 깔끔하게 정리 정돈해 주는 일등 공신인 함수에 대해 완벽하게 정복해 보았습니다. 함수를 깨우치고 나면 프로그램의 거대한 빌딩을 조립식 블록 완구처럼 부품별로 나누어 조립할 수 있는 넓은 시야가 생기게 됩니다.

오늘 공부한 내용 중 작동이 잘 안 되는 코드가 있다면 언제든 소프트웨어 공장 댓글 창에 남겨주세요. 즐거운 코딩 하세요!

반응형