🔵
시스템 프로그래밍

C 언어 완전 가이드

👥 방문자 수

모든 프로그래밍의 근간, C 언어를 기초부터 실전까지 학습합니다. 메모리 구조와 포인터, 동적 할당 등 시스템 프로그래밍의 핵심 개념을 완벽하게 정리했습니다.

C11 / C17 GCC / Clang 포인터 · 메모리 관리 구조체 · 파일 I/O

1. C란?

C는 1972년 Bell 연구소의 Dennis Ritchie가 만든 절차적 시스템 프로그래밍 언어입니다. 50년이 넘은 지금도 운영체제 커널과 고성능 소프트웨어의 핵심 언어로 사용됩니다.

2. 컴파일러 설치 및 개발 환경 구축

C 언어 코드를 컴파일하고 실행하기 위해서는 운영체제에 맞는 컴파일러(GCC, Clang, MSVC 등)와 코드를 작성할 텍스트 에디터(VS Code 등)가 필요합니다. 각 운영체제별로 가장 널리 사용되는 환경을 구축해보겠습니다.

2.1 운영체제별 컴파일러 설치

🪟 Windows (MSYS2 & MinGW-w64 설치)

Windows 환경에서는 GNU 컴파일러 모음인 GCC의 Windows 이식 버전인 MinGW-w64를 설치하는 것이 가장 범용적이고 표준적입니다.

  1. MSYS2 다운로드: MSYS2 공식 웹사이트에 접속하여 인스톨러(.exe)를 다운로드하고 기본 설정으로 설치합니다. (기본 설치 경로: C:\msys64)
  2. 패키지 매니저로 GCC 설치: 설치 완료 후 실행되는 터미널(MSYS2 UCRT64) 창에서 아래 명령어를 입력하여 컴파일러 툴체인을 설치합니다.
    # GCC 컴파일러 및 관련 개발 도구 모음(Toolchain) 설치
    pacman -S mingw-w64-ucrt-x86_64-toolchainbash

    (설치 시 선택 항목을 물어보면 Enter를 눌러 전체 설치를 진행합니다.)

  3. 환경 변수(Path) 등록: Windows 시스템이 어디서든 gcc 명령어를 인식할 수 있도록 시스템 경로에 등록해야 합니다.
    • 키보드의 Win + S를 누르고 "시스템 환경 변수 편집"을 검색하여 실행합니다.
    • 우측 하단의 [환경 변수] 버튼을 클릭합니다.
    • '시스템 변수' 목록에서 Path 변수를 찾아 선택하고 [편집]을 누릅니다.
    • [새로 만들기]를 클릭한 뒤 컴파일러 실행 파일이 위치한 경로인 C:\msys64\ucrt64\bin를 입력하고 확인을 누릅니다.
  4. 설치 확인: 새로운 명령 프롬프트(cmd) 또는 PowerShell을 열고 아래 명령어를 입력하여 설치가 잘 되었는지 확인합니다.
    gcc --versionbash
💡
대안 옵션 (Visual Studio MSVC): 만약 더 강력한 통합 개발 환경이 필요하다면 Visual Studio를 설치하고, 설치 관리자에서 "C++를 사용한 데스크톱 개발" 워크로드를 체크하면 Microsoft 공식 C/C++ 컴파일러(MSVC)를 즉시 사용할 수 있습니다.

🍎 macOS (컴파일러 설치 및 세부 설정)

macOS에서는 시스템 기본 컴파일러인 Apple Clang(Xcode Command Line Tools)을 설치하거나, 필요에 따라 Homebrew를 통해 표준 GNU GCC를 직접 설치할 수 있습니다.

방법 A: Xcode Command Line Tools 설치 (가장 추천 & 초간단)

macOS가 공식 제공하는 컴파일 툴체인(Clang 기반)을 설치하는 표준 방법입니다.

  1. 터미널 앱(Terminal.app)을 켭니다. (Cmd + Space 누른 후 '터미널' 검색)
  2. 아래 명령어를 입력하고 실행합니다.
    xcode-select --installbash
  3. 화면에 "명령줄 개발 도구 설치가 필요합니다"라는 안내 팝업창이 나타나면 [설치] 버튼을 누르고 사용권 계약에 동의합니다.
  4. 설치가 자동으로 진행되며 완료되면 아래 명령어로 설치 상태를 최종 확인합니다.
    clang --versionbash
방법 B: Homebrew를 통한 순수 GNU GCC 설치 (고급 사용자용)

macOS에서 기본 gcc 명령어는 사실 Apple Clang에 링크되어 있습니다. 만약 표준 C 라이브러리와 완전한 GNU GCC 컴파일러를 그대로 사용하고 싶다면 패키지 관리자인 Homebrew를 이용해야 합니다.

  1. 터미널 앱에서 Homebrew 설치 스크립트를 실행합니다. (Homebrew가 이미 설치되어 있다면 생략 가능)
    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"bash
  2. 아래 명령어를 입력하여 순수 GNU GCC 최신 버전을 설치합니다.
    brew install gccbash
  3. 설치가 끝나면 gcc-13 또는 gcc-14(설치된 버전에 맞춰 명령어 생성) 명령어를 터미널에 쳐서 버전 번호가 올바르게 나오는지 확인합니다.
    gcc-14 --versionbash

🐧 Linux (Ubuntu / Debian 설치)

Linux 환경은 패키지 매니저를 통해 컴파일에 필요한 필수 도구 세트(build-essential)를 아주 쉽게 설치할 수 있습니다.

sudo apt update
sudo apt install build-essentialbash

(설치가 끝나면 gcc --versionmake --version으로 컴파일러 작동 여부를 확인합니다.)

2.2 VS Code에서 C 개발 환경 및 실행 활용하기

가장 강력하고 가벼운 텍스트 에디터인 Visual Studio Code (VS Code)에서 C 코드를 작성하고 빌드/디버깅하는 방법입니다.

  1. VS Code 다운로드 및 설치: VS Code 공식 홈페이지에서 운영체제에 맞는 버전을 설치합니다.
  2. 필수 확장(Extension) 설치: VS Code 실행 후 왼쪽 사이드바의 확장 아이콘(Ctrl + Shift + X)을 클릭하고 다음 확장을 설치합니다.
    • C/C++ (Microsoft): 코드 자동 완성, 디버깅, 구문 강조 등 필수 기능 제공
    • Code Runner (Jun Han): 단축키 하나로 C 코드를 즉시 컴파일하고 실행할 수 있는 편리한 도구
  3. 프로젝트 폴더 열기: 원하는 빈 폴더를 생성한 후 VS Code에서 File > Open Folder로 해당 폴더를 엽니다.
  4. C 소스코드 작성: 새 파일 만들기 아이콘을 클릭하고 hello.c 파일을 생성한 뒤 아래의 기본 코드를 입력합니다.
    #include 
    
    int main(void) {
        printf("Visual Studio Code에서 C 실행 완료!\n");
        return 0;
    }c
  5. 코드 빌드 및 실행하기 (2가지 방법)
    • 방법 A (Code Runner 이용 - 가장 간편함):

      코드 편집 창 우측 상단의 ▶ (Run Code) 버튼을 클릭하거나, 단축키 Ctrl + Alt + N(macOS는 Ctrl + Option + N)을 입력하면 하단 출력 창에 즉시 결과가 나타납니다.

      💡
      입력값(scanf)을 처리하고 싶다면: VS Code 설정(Ctrl + ,)에서 code-runner.runInTerminal을 검색한 뒤 "Run In Terminal" 옵션을 체크해주세요. 기본 출력 창 대신 실제 터미널을 사용하여 키보드 입력을 처리할 수 있게 됩니다.
    • 방법 B (통합 터미널 직접 빌드 - 표준 방법):

      VS Code 하단에 터미널(Ctrl + `)을 열고 직접 컴파일러 명령어를 입력하여 빌드 및 실행합니다.

      # 1. gcc 컴파일러로 hello.c를 빌드하여 hello.exe(Linux/macOS는 hello) 파일 생성
      gcc hello.c -o hello
      
      # 2. 실행 파일 실행하기
      # Windows:
      .\hello.exe
      
      # macOS & Linux:
      ./hellobash

3. Hello World

#include <stdio.h>

int main(void) {
    printf("Hello, World!\n");
    return 0;
}
/* 컴파일 & 실행
   gcc -Wall -Wextra -o hello hello.c
   ./hello
*/c

4. 변수와 자료형 (Variables & Data Types)

C는 정적 타입 언어로 변수 선언 시 반드시 자료형을 명시해야 합니다.

#include <stdio.h>
#include <stdint.h>   // 고정 크기 정수 타입
#include <stdbool.h>  // bool 타입 (C99)

int main(void) {
    /* 정수형 */
    char   c  = 'A';        // 1 byte  (-128 ~ 127)
    short  s  = 1000;       // 2 bytes
    int    n  = 42;         // 4 bytes
    long   l  = 100000L;    // 4 or 8 bytes
    long long ll = 9e18;    // 8 bytes

    /* 고정 크기 정수 (이식성 보장) */
    int8_t   i8  = 127;
    int32_t  i32 = 2147483647;
    uint64_t u64 = 18446744073709551615ULL;

    /* 실수형 */
    float  f  = 3.14f;      // 4 bytes, ~7자리 정밀도
    double d  = 3.14159265; // 8 bytes, ~15자리 정밀도

    /* bool (C99) */
    bool flag = true;

    /* 상수 */
    const int MAX = 100;

    /* 형변환 */
    int   a = 7, b = 2;
    double result = (double)a / b; // 3.5 (정수 나눗셈 방지)

    printf("char='%c' int=%d double=%.2f bool=%d\n", c, n, result, flag);
    return 0;
}c
ℹ️
크기 확인: 자료형의 실제 크기는 플랫폼마다 다를 수 있습니다. sizeof(int)로 확인하거나 <stdint.h>의 고정 크기 타입(int32_t, uint64_t 등)을 사용하면 이식성을 보장합니다.

5. 연산자와 제어문 (Operators & Control Flow)

#include <stdio.h>

int main(void) {
    /* 산술 연산자 */
    int a = 10, b = 3;
    printf("%d %d %d %d %d\n", a+b, a-b, a*b, a/b, a%b); // 13 7 30 3 1

    /* 비트 연산자 */
    unsigned int flags = 0b1010;
    flags |=  0b0001;  // 비트 SET   → 1011
    flags &= ~0b0010;  // 비트 CLEAR → 1001
    flags ^=  0b0100;  // 비트 FLIP  → 1101

    /* if-else */
    int score = 85;
    if      (score >= 90) printf("A\n");
    else if (score >= 80) printf("B\n");
    else                  printf("C\n");

    /* switch */
    int day = 3;
    switch (day) {
        case 1: printf("월요일\n"); break;
        case 2: printf("화요일\n"); break;
        case 3: printf("수요일\n"); break;
        default: printf("기타\n");
    }

    /* 반복문 */
    for (int i = 0; i < 5; i++) printf("%d ", i);  // 0 1 2 3 4

    int j = 0;
    while (j < 3) { printf("w%d ", j); j++; }

    /* 삼항 연산자 */
    int max = (a > b) ? a : b;  // 10
    return 0;
}c

6. 함수 (Functions)

C에서 함수는 코드를 재사용 가능한 단위로 분리하는 핵심 도구입니다. 선언(prototype)과 정의(definition)를 분리할 수 있습니다.

#include <stdio.h>

/* 함수 선언 (prototype) — 헤더 파일에 위치 */
int    add(int a, int b);
double power(double base, int exp);
void   swap(int *a, int *b);   // 포인터로 출력 파라미터 전달

/* 함수 정의 */
int add(int a, int b) { return a + b; }

double power(double base, int exp) {
    double result = 1.0;
    for (int i = 0; i < exp; i++) result *= base;
    return result;
}

/* 값이 아닌 주소로 전달 (call-by-reference 시뮬레이션) */
void swap(int *a, int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

/* 재귀 함수 */
int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

/* 가변 인자 */
#include <stdarg.h>
int sum(int count, ...) {
    va_list ap;
    va_start(ap, count);
    int total = 0;
    for (int i = 0; i < count; i++) total += va_arg(ap, int);
    va_end(ap);
    return total;
}

int main(void) {
    printf("add: %d\n",    add(3, 4));           // 7
    printf("pow: %.0f\n",  power(2.0, 10));      // 1024
    printf("5!: %d\n",     factorial(5));         // 120
    printf("sum: %d\n",    sum(4, 1, 2, 3, 4));  // 10

    int x = 10, y = 20;
    swap(&x, &y);
    printf("swap: %d %d\n", x, y);               // 20 10
    return 0;
}c

7. 배열과 문자열 (Arrays & Strings)

#include <stdio.h>
#include <string.h>

int main(void) {
    /* 1차원 배열 */
    int nums[5] = {10, 20, 30, 40, 50};
    for (int i = 0; i < 5; i++) printf("%d ", nums[i]);

    /* 2차원 배열 */
    int matrix[3][3] = {{1,2,3},{4,5,6},{7,8,9}};
    printf("\n%d\n", matrix[1][2]); // 6

    /* 문자열 = char 배열 + NULL 종단('\0') */
    char name[20] = "TestForge";
    char greeting[50];

    printf("길이: %zu\n",   strlen(name));        // 9
    strcpy(greeting, "안녕하세요, ");
    strcat(greeting, name);
    printf("%s\n", greeting);

    /* 문자열 비교 */
    if (strcmp("abc", "abc") == 0) printf("같음\n");

    /* 문자열 탐색 */
    char *pos = strstr(name, "Forge");            // "Forge" 시작 포인터
    if (pos) printf("위치: %ld\n", pos - name);  // 4

    /* snprintf: 안전한 문자열 포맷 */
    char buf[64];
    snprintf(buf, sizeof(buf), "score=%d%%", 95);
    printf("%s\n", buf); // score=95%

    return 0;
}c
⚠️
버퍼 오버플로우: gets()는 버퍼 크기를 검사하지 않아 보안 취약점의 원인이 됩니다. fgets(buf, sizeof(buf), stdin)을 사용하세요. 마찬가지로 strcpy 대신 strncpy, sprintf 대신 snprintf를 사용합니다.

8. 포인터 (Pointers)

포인터는 변수의 메모리 주소를 저장하는 변수입니다. C 언어의 핵심 기능으로, 동적 메모리 할당·배열·함수 포인터 모두 포인터 기반입니다.

#include <stdio.h>

int main(void) {
    int  x   = 42;
    int *ptr = &x;       // &: 주소 연산자

    printf("값:  %d\n",  *ptr);   // 역참조: 42
    printf("주소: %p\n", (void*)ptr);

    /* 포인터로 값 수정 */
    *ptr = 100;
    printf("x = %d\n", x); // 100

    /* 포인터 산술 */
    int arr[5] = {1,2,3,4,5};
    int *p = arr;   // 배열 이름 = 첫 요소 주소
    printf("%d %d\n", *p, *(p+2)); // 1  3

    for (int *q = arr; q < arr + 5; q++)
        printf("%d ", *q); // 1 2 3 4 5

    /* 이중 포인터 */
    int  val = 7;
    int  *p1 = &val;
    int **p2 = &p1;      // 포인터의 포인터
    printf("이중: %d\n", **p2); // 7

    /* const 포인터 */
    const int *cp  = &x;  // 가리키는 값 변경 불가, 포인터 자체는 가능
    int * const pc = &x;  // 포인터 자체 변경 불가, 가리키는 값은 가능

    return 0;
}c

함수 포인터

/* 함수 포인터: 콜백 패턴 */
#include <stdlib.h>

int compare_asc(const void *a, const void *b) {
    return (*(int*)a - *(int*)b);
}
int compare_desc(const void *a, const void *b) {
    return (*(int*)b - *(int*)a);
}

typedef int (*Comparator)(const void*, const void*);

void sort_array(int *arr, size_t n, Comparator cmp) {
    qsort(arr, n, sizeof(int), cmp);
}

int main(void) {
    int nums[] = {5, 2, 8, 1, 9, 3};
    sort_array(nums, 6, compare_asc);
    for (int i = 0; i < 6; i++) printf("%d ", nums[i]); // 1 2 3 5 8 9
    return 0;
}c

9. 구조체 (struct)

구조체는 서로 다른 자료형의 변수를 하나로 묶는 사용자 정의 타입입니다.

#include <stdio.h>
#include <string.h>

/* 구조체 정의 */
typedef struct {
    int   id;
    char  name[50];
    float score;
} Student;

/* 구조체 배열과 포인터 */
void print_student(const Student *s) {
    printf("[%d] %s: %.1f\n", s->id, s->name, s->score);
}

Student create_student(int id, const char *name, float score) {
    Student s;
    s.id = id;
    strncpy(s.name, name, sizeof(s.name) - 1);
    s.score = score;
    return s;
}

int main(void) {
    Student a = create_student(1, "김철수", 92.5f);
    Student b = {2, "이영희", 88.0f};

    Student class[2] = {a, b};
    for (int i = 0; i < 2; i++)
        print_student(&class[i]);

    /* 비트 필드 — 메모리 절약 */
    typedef struct {
        unsigned int is_admin : 1;  // 1비트
        unsigned int level    : 4;  // 4비트 (0~15)
        unsigned int status   : 3;  // 3비트 (0~7)
    } UserFlags;

    UserFlags uf = {1, 5, 2};
    printf("admin=%u level=%u\n", uf.is_admin, uf.level);
    return 0;
}c

연결 리스트 (Linked List)

#include <stdlib.h>

typedef struct Node {
    int data;
    struct Node *next;
} Node;

Node *create_node(int data) {
    Node *n = malloc(sizeof(Node));
    if (!n) return NULL;
    n->data = data;
    n->next = NULL;
    return n;
}

void push_front(Node **head, int data) {
    Node *n = create_node(data);
    n->next = *head;
    *head = n;
}

void free_list(Node *head) {
    while (head) {
        Node *tmp = head;
        head = head->next;
        free(tmp);
    }
}

int main(void) {
    Node *list = NULL;
    push_front(&list, 3);
    push_front(&list, 2);
    push_front(&list, 1);
    for (Node *p = list; p; p = p->next)
        printf("%d ", p->data); // 1 2 3
    free_list(list);
    return 0;
}c

10. 동적 메모리 (Dynamic Memory)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
    /* malloc: 미초기화 메모리 할당 */
    int n = 10;
    int *arr = malloc(n * sizeof(int));
    if (!arr) { perror("malloc"); return 1; }

    for (int i = 0; i < n; i++) arr[i] = i * i;

    /* realloc: 크기 변경 */
    int *bigger = realloc(arr, 20 * sizeof(int));
    if (!bigger) { free(arr); return 1; }
    arr = bigger;
    for (int i = n; i < 20; i++) arr[i] = i;

    /* calloc: 0으로 초기화된 메모리 */
    double *matrix = calloc(4 * 4, sizeof(double));
    if (!matrix) { free(arr); return 1; }
    matrix[0 * 4 + 0] = 1.0; // [0][0]
    matrix[1 * 4 + 1] = 1.0; // [1][1] — 단위 행렬

    free(arr);
    free(matrix);
    return 0;
}c
⚠️
흔한 메모리 버그:
메모리 누수(leak): malloc 후 free 누락
이중 해제(double-free): free 두 번 호출 → UB(Undefined Behavior)
댕글링 포인터: free 후 포인터를 NULL로 초기화하지 않고 사용
valgrind ./program 또는 -fsanitize=address 컴파일 옵션으로 감지하세요.

11. 파일 입출력 (File I/O)

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    /* 쓰기 */
    FILE *fp = fopen("data.txt", "w");
    if (!fp) { perror("fopen"); return 1; }
    fprintf(fp, "이름,점수\n철수,92\n영희,88\n");
    fclose(fp);

    /* 읽기 — 한 줄씩 */
    fp = fopen("data.txt", "r");
    if (!fp) { perror("fopen"); return 1; }
    char line[256];
    while (fgets(line, sizeof(line), fp))
        printf("> %s", line);
    fclose(fp);

    /* 이진 파일 읽기/쓰기 */
    typedef struct { int id; double score; } Record;
    Record rec = {1, 95.5};

    FILE *bin = fopen("data.bin", "wb");
    fwrite(&rec, sizeof(rec), 1, bin);
    fclose(bin);

    Record loaded = {0};
    bin = fopen("data.bin", "rb");
    fread(&loaded, sizeof(loaded), 1, bin);
    fclose(bin);
    printf("id=%d score=%.1f\n", loaded.id, loaded.score);

    return 0;
}c

12. 전처리기 (Preprocessor)

/* ── 매크로 ──────────────────────────────── */
#define PI          3.14159265
#define MAX(a, b)   ((a) > (b) ? (a) : (b))
#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))

/* 사용 */
double area = PI * 5 * 5;          // 78.539...
int   bigger = MAX(10, 20);        // 20
int   len    = ARRAY_LEN((int[]){1,2,3,4}); // 4

/* ── 조건부 컴파일 ────────────────────────── */
#ifdef DEBUG
  #define LOG(fmt, ...) fprintf(stderr, "[DEBUG] " fmt "\n", ##__VA_ARGS__)
#else
  #define LOG(fmt, ...)  /* 릴리즈에선 아무것도 하지 않음 */
#endif

/* ── 헤더 가드 (include guard) ─────────────── */
/* mylib.h */
#ifndef MYLIB_H
#define MYLIB_H

typedef struct { int x, y; } Point;
Point point_add(Point a, Point b);

#endif /* MYLIB_H */

/* ── 컴파일 시점 검사 ──────────────────────── */
#include <assert.h>
_Static_assert(sizeof(int) == 4, "int must be 4 bytes");
/* 런타임 assert */
int divide(int a, int b) {
    assert(b != 0 && "division by zero");
    return a / b;
}c

13. 다음 단계 (Next Steps)

🚀
C 중급 이후 로드맵

핵심 심화 주제:
멀티스레딩: POSIX Threads (pthread) — 뮤텍스, 조건 변수, 경쟁 조건
소켓 프로그래밍: BSD 소켓 API로 TCP/UDP 서버 구현
시스템 콜: fork/exec/wait, 파이프, 시그널
빌드 시스템: Makefile → CMake → Meson

추천 연계 학습:
Linux 서버 관리 가이드
Go 언어 — C 다음으로 배우기 좋은 시스템 언어
Docker — C 기반 컨테이너 런타임 이해