본문으로 건너뛰기
Previous
Next
C 언어 시리즈 #01 — 기초와 실행 모델: 객체 표현·정렬·번역 단위

C 언어 시리즈 #01 — 기초와 실행 모델: 객체 표현·정렬·번역 단위

C 언어 시리즈 #01 — 기초와 실행 모델: 객체 표현·정렬·번역 단위

이 글의 핵심

C가 소스를 어떻게 “객체”로 취급하는지, 정렬과 패딩이 왜 생기는지, 한 .c 파일이 왜 링커 관점에서는 불완전한지까지 번역 단위와 실행 모델로 설명합니다.

C 언어 시리즈 #01(입문·기초 편) | 전체 목차 | 이어서 읽기: #02 타입·변환·정수 표현

일단 재밌는 것부터 던질게. 밑에 지루한 이론이 있으니, 여기서 한 번 웃고 가면 된다.

먼저 돌아가는 거 하나

터미널에 숫자 두 개랑 연산자 넣으면 툭 뱉는 초간단 계산기. fgets로 한 줄 읽고 sscanf로 쪼개는 이유는, scanf만 열심히 쓰다가 입력 버퍼 꼬여서 울었던 날이 있어서… 정도로만 기억해 두면 돼.

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

int main(void)
{
    char line[64];
    double a, b;
    char op;

    printf("예: 3 + 4 이렇게 쳐봐\n");
    if (fgets(line, sizeof line, stdin) == NULL)
        return 1;
    if (sscanf(line, "%lf %c %lf", &a, &op, &b) != 3) {
        printf("형식이 이상한데?\n");
        return 1;
    }
    double r = 0.0;
    switch (op) {
    case '+': r = a + b; break;
    case '-': r = a - b; break;
    case '*': r = a * b; break;
    case '/':
        if (b == 0.0) { printf("0으로는 못 나눔\n"); return 1; }
        r = a / b; break;
    default:
        printf("그 연산자는 모름: %c\n", op);
        return 1;
    }
    printf("=> %.6g\n", r);
    return 0;
}

이제 본론(?)으로.

처음 segfault 만났을 때

나 예전에 포인터도 모를 때 강의에 나온 예제를 반쯤 이해한 채로 scanf 쪽을 만져 보다가, 뭔가 NULL이랑 엮인 채로 잘못 짠 코드를 돌렸는데, 화면에 에러 문구 대신 그냥 툭 하고 죽었어. “Segmentation fault”. 한국어로 말하자면 “그만… 여기는 내가 읽을 수 있는 메모리가 아니야” 같은 엄한 경고인데, 처음엔 그냥 컴퓨터가 싫은 느낌이야.

이게 C가 친절하다/안 친절하다를 가르는 대목이기도 해. 파이썬이면 None 만지다 Traceback 뜨는 수준이었을 텐데, C는 너 믿고 돌리다가 한 방이거든. 그래서 scanf& 꼭 붙이기, printf엔 서식이랑 인자 맞추기, 이런 게 다 “UB(미정의 동작) 맞을 짓 하지 말자” 쪽의 생존 루틴이야. 프론트매터 FAQ에도 비슷한 이야기 나와 있지.

segfault 썰은 여기까지만. 이제 딱 필요한 만큼만 지나가는 이론.

지루해지니까 짧게: C가 뭐냐

옛날 유닉스 짤 때 나온 시스템에 가깝게 짜는 언어야. OS, 임베디드, 네이티브 라이브러리, 그냥 “빨리, 예측 가능하게”를 원하는 데가 아직도 많이 씀. 툴은 Linux면 보통 gccclang, 맥이면 clang, 윈도는 VS나 MSYS2 한 가지만 정해서 쓰는 게 정신 건강에 좋아. (경로·DLL 꼬이는 거 겪어보면 안다.)

Hello랑, 텍스트가 실행 파일이 되는 과정

#include <stdio.h>
int main(void) {
    printf("Hello, World!\n");
    return 0;
}

#include는 “여기 printf 있어” 알려주는 표고, main이 시작점, return 0은 “성공으로 끝냄” 정도.

.c가 실행 파일이 되는 건, 대략 전처리(헤더 뻥튀기) → 컴파일(기계어 비슷한 걸로) → 어셈블(목적 파일) → 링크(다른 .o·라이브러리랑 합침) 네 군데를 거친다고 보면 돼. 표로 안 쓰고 말하자면, 전처리는 #include·#define 처리, 컴파일은 문법·최적화, 링크는 printf 본문은 어딘가 libc에 있으니까 거기랑 이어 붙이기 쪽. 한 파일만으로 완벽한 프로그램이 아니고, 번역 단위(전처리 끝난 .c 하나) 마다 따로 뽑힌 다음 마지막에 합쳐지는 느낌.

빌드 예시는 이 정도.

gcc -Wall -Wextra -std=c11 -O0 -g -o hello hello.c

-Wall은 경고 좀 보자, -g는 나중에 디버거 잡을 때, -O0은 일단 느슨하게(디버깅 편하게). gdb에선 b main 걸고 r, n 정도만 써 봐도 “아 소스 라인에 멈추네”가 체감돼.

문법, 진짜로 뼈대만

  • 변수: int n = 0; 처럼 초기화해 두는 습관이 좋다. const는 읽기 전용.
  • printf"%d"int를, "%f" 쪽은 double 기대같이 서식이랑 타입 맞춰야 함. scanf("%d", &n) — 정수 담을 땐 주소 & 잊지 말기. 배열/포인터 예외는 다음 편.
  • if, for, while은 다른 언어랑 비슷한데, C는 조건이 0이면 거짓, 0 아니면 참. switchcase 끝에 break 빼먹으면 아래로 주르륵 떨어질 수 있으니 그것도 함정.
  • 함수는 “위에 선언(prototype) 보이게” 해 두는 게 덜 아픔. 파일 안에서만 쓰는 애는 static 붙이는 것도 있음.
  • static 지역은 함수 들어갈 때마다 새로 생기는 게 아니라 한 번만 살고 끝까지 감. 전역은 최소로.

전처리기 #include / #define / #if도 쓰긴 쓰는데, 여기서 길게 늘어놓으면 백과사전 되니까 시리즈 다음 글에서 이어가면 돼. 객체·정렬·번역 단위·UB 같은 “바닥” 이야기도 #02완벽 가이드 쪽이 더 잘 풀려 있다.

정리

C는 작은 문법인데, 대신 “내가 뭔 짓을 했는지”에 대가가 확 와. 그게 segfault이든, 엉뚱한 최적화로 터지는 UB이든. 첫날엔 gcc -Wall이랑 scanf& 정도만 지켜도 살고, 다음엔 타입·포인터 쪽으로 넘어가면 된다.

이어 읽기: C 언어 시리즈 #02 — 타입·승격·정수 표현 · 목차