← Embedded C 강의 목록으로
🔌
기초
기초 · 선수: 04강

05. 함수·헤더 분리·#define 매크로

펌웨어는 "GPIO 다루는 부분", "UART 다루는 부분", "응용 로직" 식으로 모듈로 나눕니다. 모듈을 나누는 도구인 함수(동작 단위), 헤더(.h)/소스(.c) 분리(인터페이스/구현), 전처리기(#include·#define·헤더 가드)를 익힙니다. 비트 조작 로직을 ledbank 모듈로 떼어내고, 헤더 가드와 매크로 괄호 함정도 다룹니다. 모듈로 나누는 감각은 23편(HAL 추상화)까지 이어집니다.

함수헤더#define매크로헤더가드모듈
소요 시간
약 1.5시간
난이도
📊 초급
선수 조건
🎯 04강
결과물
동작을 함수로 나누고 헤더/소스로 분리해 여러 파일을 함께 빌드하며, 헤더 가드와 매크로 괄호 규칙으로 펌웨어를 모듈답게 구성할 수 있습니다.

이 강의에서 배우는 것

  • 1함수 선언(프로토타입)과 정의를 구분한다
  • 2헤더(.h, 인터페이스)와 소스(.c, 구현)를 나눠 함께 빌드한다
  • 3헤더 가드(#ifndef/#define/#endif)로 중복 포함을 막는다
  • 4#define 상수와 함수형 매크로를 구분하고 괄호 함정을 피한다
  • 5조건부 컴파일(#ifdef)로 플랫폼/옵션을 분기한다

소개

코드가 길어지면 한 파일에 다 몰아넣을 수 없습니다. 이번 편은 기초의 마지막으로, 비트 조작 로직을 ledbank 모듈(ledbank.h + ledbank.c)로 떼어내고 main 이 인터페이스만 보고 쓰는 구조를 만듭니다.

핵심 개념

1) 함수 — 선언과 정의 / 헤더와 소스

파일역할담는 것
ledbank.h인터페이스#define·타입·함수 선언
ledbank.c구현함수 정의(몸체)
main.c사용#include "ledbank.h" 후 호출

빌드할 때는 사용하는 .c 들을 함께 컴파일합니다: `gcc main.c ledbank.c -o main`. μVision에서는 두 .c 를 모두 Source Group 에 추가합니다.

2) 헤더 가드 — 중복 포함 방지

c
#ifndef LEDBANK_H      /* 아직 정의 안 됐으면 */
#define LEDBANK_H      /* 정의 표시하고 */
... 헤더 내용 ...
#endif /* LEDBANK_H */ /* 두 번째부터는 통째로 건너뜀 */

#pragma once 도 같은 일을 하지만 모든 컴파일러가 보장하지 않으므로 이식성을 위해 #ifndef 가드를 권합니다.

3) #define — 상수 vs 함수형 매크로

c
#define LED_COUNT 8u            /* 상수 매크로 */
#define BIT(n)    (1u << (n))   /* 함수형 매크로 */

#define SQ(x) x*x          /* 나쁨: SQ(a+1) → a+1*a+1 */
#define SQ(x) ((x)*(x))    /* 좋음: 인자·식을 모두 괄호로 */
ℹ️

매크로는 타입 검사가 없고 인자를 여러 번 평가합니다(SQ(i++) 위험). 단순 상수·간단한 식이 아니면 static 함수(또는 inline)가 더 안전합니다.

4) 조건부 컴파일 #ifdef

c
#ifdef USE_UART
    uart_send(msg);     /* UART 빌드에서만 포함 */
#endif

핵심 예제

ledbank.h 가 인터페이스(선언), ledbank.c 가 구현(정의)을 담고, main 은 함수 이름만으로 의도를 표현합니다.

c
/* ledbank.h — 인터페이스 */
#ifndef LEDBANK_H
#define LEDBANK_H
#include <stdint.h>
#define BIT(n) (1u << (n))
uint8_t led_set(uint8_t bank, uint8_t pos);
uint8_t led_toggle(uint8_t bank, uint8_t pos);
uint8_t led_count_on(uint8_t bank);
#endif /* LEDBANK_H */
c
/* ledbank.c — 구현 */
#include "ledbank.h"
uint8_t led_set(uint8_t b, uint8_t p)    { return (uint8_t)(b |  BIT(p)); }
uint8_t led_toggle(uint8_t b, uint8_t p) { return (uint8_t)(b ^  BIT(p)); }
uint8_t led_count_on(uint8_t b) {
    uint8_t pos, n = 0;
    for (pos = 0; pos < 8u; pos++) n = (uint8_t)(n + ((b >> pos) & 1u));
    return n;
}
bash
gcc main.c ledbank.c -o main && ./main   # 두 .c 를 함께 빌드

자주 하는 실수

Q. multiple definition / redefinition 오류가 나요.

A. 헤더에 함수 정의(몸체)를 넣고 여러 .c 에서 포함하면 정의가 여러 번 생깁니다. 헤더에는 선언만, 정의는 .c 에 두고, 헤더 가드(#ifndef)를 빠뜨리지 마세요.

Q. undefined reference to led_set 링크 오류가 나요.

A. ledbank.c 를 함께 빌드하지 않았기 때문입니다. 헤더는 선언만 주므로 구현이 든 .c 를 같이 컴파일/링크해야 합니다: gcc main.c ledbank.c. μVision이면 두 .c 를 프로젝트에 추가하세요.

Q. #define SQ(x) x*x 가 이상한 값을 줘요.

A. 괄호가 없어서입니다. SQ(a+1) 이 a+1*a+1 로 전개됩니다. 함수형 매크로는 인자·전체 식을 모두 괄호로: #define SQ(x) ((x)*(x)). 부작용 있는 인자는 함수를 쓰세요.

정리

  • 동작은 함수로 나누고, 선언은 헤더(.h)에·정의는 소스(.c)에 둔다
  • 사용하는 .c 들을 함께 빌드해야 링크된다
  • 헤더 가드(#ifndef/#define/#endif)로 중복 포함을 막는다
  • 함수형 매크로는 인자·식을 모두 괄호로 감싼다 — 복잡하면 함수가 안전
  • #ifdef 로 플랫폼/옵션별 코드를 골라 컴파일한다

과제

  1. led_clear() 함수를 ledbank 모듈에 추가하고 main 에서 호출
  2. 헤더 가드를 일부러 빼고 같은 헤더를 두 번 #include 해 오류를 재현한 뒤 가드로 해결
  3. MIN(a,b) 함수형 매크로를 괄호 규칙을 지켜 작성하고 MIN(x++, y) 가 왜 위험한지 설명
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드(과제·정답 포함)는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗