C언어와 UNIX

C와 UNIX는 서로의 개발에 영향을 주며 발전했다. Dennis Ritchie의 역사 기록에 따르면 C는 B에서 발전했고, 1973년 무렵 UNIX 커널의 상당 부분을 C로 작성할 수 있게 됐다. 하드웨어별 어셈블리만으로 운영체제를 작성하는 것보다 다른 환경으로 옮길 수 있는 코드의 범위가 넓어졌다.

그러나 C가 모든 환경에서 그대로 실행되는 언어라는 뜻은 아니다. 데이터 타입 크기, 정렬, 엔디언, 운영체제 API와 정의되지 않은 동작에 대한 의존은 여전히 이식성 문제를 만든다. C 소스의 재컴파일 가능성과 실행 파일의 바이너리 호환성은 구분해야 한다.

선언, 구현, 링크를 나누는 작은 예제

add.h는 다른 소스가 사용할 함수의 선언을 제공한다.

#ifndef ADD_H
#define ADD_H
int add(int a, int b);
#endif

add.c는 구현을 담는다. 자신의 헤더를 포함하면 선언과 구현의 타입이 다른 실수를 컴파일러가 확인할 수 있다.

#include "add.h"
int add(int a, int b) {
    return a + b;
}

main.c는 헤더를 통해 함수를 사용한다.

#include <stdio.h>
#include "add.h"
int main(void) {
    printf("%d\n", add(2, 3));
    return 0;
}
cc -Wall -Wextra -std=c11 -c add.c -o add.o
cc -Wall -Wextra -std=c11 -c main.c -o main.o
cc main.o add.o -o demo
./demo

출력은 5다. 앞의 두 명령은 각 소스를 목적 파일로 만들고, 마지막 컴파일러 호출은 목적 파일들을 링크한다. main.o만 링크하면 add 구현을 찾지 못한다. 헤더를 포함하는 것과 구현을 실행 파일에 연결하는 것은 서로 다른 단계다.

이 분리는 코드를 파일로 나누는 데서 끝나지 않는다. 공개할 함수와 타입을 헤더에 제한하고 내부 구현을 감추면, 호출부가 알아야 하는 정보를 줄일 수 있다. 운영체제 기능이 필요한 코드는 POSIX 인터페이스에 대한 의존까지 따로 확인한다.

자료: Dennis Ritchie의 C 언어 역사, GCC 전체 옵션과 컴파일 단계