코딩 · 프로그래밍 언어
C언어 프로그래밍 기초 입문 튜토리얼
핵심 답변
C는 포인터로 메모리에 직접 접근할 수 있는 작고 빠른 컴파일 언어입니다. .c 파일에 main을 작성하고 gcc hello.c -o hello로 컴파일한 뒤 ./hello로 실행합니다. 핵심 개념은 크기가 정해진 자료형, 함수, 배열과 널 종료 문자열, 포인터와 주소, malloc과 free를 이용한 수동 메모리 관리, 반환값을 통한 오류 처리입니다.
C언어란 무엇이고 어디에 쓰이는가
C는 1970년대 초 Bell Labs의 데니스 리치(Dennis Ritchie)가 Unix 운영체제를 작성하려고 만들었습니다. ISO에서 표준화하며 C99, C11, C17, C23이 주요 버전입니다. C는 런타임이 거의 없는 기계어로 컴파일되므로 거의 모든 곳에서 실행됩니다. 운영체제 커널(Linux), 임베디드 펌웨어, 데이터베이스(SQLite), 언어 런타임(CPython은 C로 작성됨), 고성능 라이브러리에 쓰입니다. C를 배우면 메모리와 컴퓨터가 실제로 어떻게 동작하는지도 이해하게 됩니다.
컴파일러 설치와 Hello, world 실행
Linux에서는 gcc를 설치하세요(예: sudo apt install build-essential). macOS에서는 xcode-select --install로 clang을 설치하며 gcc라는 이름으로도 쓸 수 있습니다. Windows에서는 MSYS2, WSL, Visual Studio를 사용하세요.
#include <stdio.h>
int main(void) {
printf("Hello, world\n");
return 0;
}gcc -Wall -Wextra -std=c17 hello.c -o hello
./hello
# Hello, world#include는 헤더 파일의 선언을 가져오며, 여기서는 표준 입출력 라이브러리입니다. main에서 0을 반환하면 성공을 뜻합니다. 항상 경고를 켜고(-Wall -Wextra) 컴파일하세요.
변수와 기본 자료형
모든 변수는 크기와 의미를 정하는 타입을 가집니다. 표준이 따로 정한 경우를 제외하면 크기는 플랫폼에 따라 다르며, <stdint.h>는 int32_t 같은 정확한 폭의 타입을 제공합니다.
| 타입 | 일반적인 크기 | 예시 | printf 형식 |
|---|---|---|---|
char | 1바이트 | char c = 'A'; | %c |
int | 4바이트 | int n = 42; | %d |
long long | 8바이트 | long long big = 1LL << 40; | %lld |
double | 8바이트 | double x = 3.14; | %f |
bool | 1바이트 | bool ok = true; (C99 <stdbool.h>) | %d |
size_t | 64비트에서 8바이트 | size_t len = sizeof(arr); | %zu |
#include <stdio.h>
int main(void) {
int count = 7;
double avg = count / 2; /* 3.0: integer division first */
double avg2 = count / 2.0; /* 3.5 */
printf("%d %.1f %.1f\n", count, avg, avg2); /* 7 3.0 3.5 */
printf("%zu\n", sizeof(int)); /* 4 on most systems */
return 0;
}제어 흐름과 함수
함수는 사용하기 전에 선언해야 합니다. 인수는 값으로 전달되므로 함수는 복사본을 받습니다.
#include <stdio.h>
int square(int x) {
return x * x;
}
int main(void) {
int score = 72;
if (score >= 90) {
printf("A\n");
} else if (score >= 70) {
printf("B\n"); /* B */
} else {
printf("C\n");
}
for (int i = 0; i < 3; i++) {
printf("%d ", square(i)); /* 0 1 4 */
}
printf("\n");
int n = 10;
while (n > 1) n /= 2;
printf("%d\n", n); /* 1 */
return 0;
}C에는 do/while, case 레이블을 쓰는 switch(break를 잊지 마세요), 그리고 주로 오류 처리 경로에서 정리 작업에 쓰이는 goto도 있습니다.
배열과 문자열
배열은 서로 이웃해 저장된 고정된 개수의 요소입니다. C는 범위를 검사하지 않고 길이도 저장하지 않으므로 길이를 따로 넘겨야 합니다. 문자열은 0 바이트 '\0'로 끝나는 char 배열입니다.
#include <stdio.h>
#include <string.h>
int sum(const int *values, size_t n) {
int total = 0;
for (size_t i = 0; i < n; i++) total += values[i];
return total;
}
int main(void) {
int nums[] = {3, 1, 2, 5};
size_t n = sizeof nums / sizeof nums[0];
printf("%zu items, sum %d\n", n, sum(nums, n)); /* 4 items, sum 11 */
char name[16] = "Ada";
strncat(name, " Lovelace", sizeof name - strlen(name) - 1);
printf("%s (%zu chars)\n", name, strlen(name)); /* Ada Lovelace (12 chars) */
return 0;
}배열을 함수에 넘기면 첫 번째 요소를 가리키는 포인터로 변환됩니다(decay). 그래서 sum이 const int *와 길이를 받습니다.
대표 기능: 포인터
포인터는 다른 값의 메모리 주소를 담습니다. &x는 x의 주소를 주고, *p는 주소 p에 있는 값을 읽거나 씁니다. 포인터를 쓰면 함수가 호출자의 변수를 수정하고, 큰 데이터를 복사하지 않고 공유하며, 연결 자료구조를 만들 수 있습니다.
#include <stdio.h>
void swap(int *a, int *b) {
int tmp = *a;
*a = *b;
*b = tmp;
}
int main(void) {
int x = 1, y = 2;
int *p = &x;
*p = 10; /* changes x */
swap(&x, &y);
printf("x=%d y=%d\n", x, y); /* x=2 y=10 */
int arr[3] = {7, 8, 9};
int *q = arr;
printf("%d %d\n", *(q + 1), q[2]); /* 8 9: pointer arithmetic */
return 0;
}malloc과 free를 이용한 동적 메모리
지역 변수는 스택에 있고 함수가 반환되면 사라집니다. 크기를 실행 중에야 알 수 있거나 함수보다 오래 살아야 하는 데이터는 malloc으로 힙에 할당하고 free로 해제합니다. 성공한 malloc 하나마다 free가 정확히 한 번 필요합니다.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t n = 5;
int *squares = malloc(n * sizeof *squares);
if (squares == NULL) {
fprintf(stderr, "out of memory\n");
return 1;
}
for (size_t i = 0; i < n; i++) squares[i] = (int)(i * i);
printf("%d\n", squares[4]); /* 16 */
free(squares);
squares = NULL; /* avoid using a dangling pointer */
return 0;
}관련 함수로 calloc(할당 후 0으로 초기화)과 realloc(크기 변경)이 있습니다. 관련된 값은 struct로 묶습니다. 예: struct point { int x; int y; };.
오류 처리
C에는 예외가 없습니다. 함수는 반환값(주로 -1, NULL, 상태 코드)으로 오류를 알리고, 많은 라이브러리 함수는 전역 변수 errno도 설정합니다. 실패할 수 있는 모든 반환값을 확인하세요.
#include <errno.h>
#include <stdio.h>
#include <string.h>
int main(void) {
FILE *f = fopen("missing.txt", "r");
if (f == NULL) {
fprintf(stderr, "cannot open: %s\n", strerror(errno));
return 1; /* cannot open: No such file or directory */
}
fclose(f);
return 0;
}작은 완성 프로그램: 줄과 단어 세기
이 프로그램은 작은 wc처럼 동작합니다. 명령줄에 지정한 파일을 읽어 줄, 단어, 바이트 수를 출력합니다.
#include <ctype.h>
#include <stdio.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "usage: %s FILE\n", argv[0]);
return 1;
}
FILE *f = fopen(argv[1], "r");
if (f == NULL) {
perror(argv[1]);
return 1;
}
long lines = 0, words = 0, bytes = 0;
int c, in_word = 0;
while ((c = fgetc(f)) != EOF) {
bytes++;
if (c == '\n') lines++;
if (isspace(c)) {
in_word = 0;
} else if (!in_word) {
in_word = 1;
words++;
}
}
fclose(f);
printf("%ld lines, %ld words, %ld bytes\n", lines, words, bytes);
return 0;
}gcc -Wall -Wextra wc.c -o wc로 컴파일하고 ./wc notes.txt로 실행합니다. c는 char가 아니라 int여서 특수한 값 EOF를 담을 수 있다는 점에 주의하세요.
초보자가 자주 하는 실수
- 조건문에서
==대신=를 쓰는 경우. 경고(-Wall)가 대부분 잡아 줍니다. - 문자열 버퍼 크기를 정할 때 끝의
'\0'자리를 잊는 경우. gets나 길이 제한이 없는scanf("%s", buf)를 쓰는 경우. 대신fgets(buf, sizeof buf, stdin)을 사용하세요.- 지역 변수의 포인터를 반환하는 경우. 함수가 반환되면 그 변수는 더 이상 존재하지 않습니다.
size_t에%d를 쓰는 것처럼printf형식과 타입이 맞지 않는 경우.
다음 학습 단계
언어와 표준 라이브러리 레퍼런스로 cppreference.com의 C 섹션을 사용하고, 표준 자체는 ISO 작업 그룹(WG14) 페이지에서 읽어 보세요. 구조체, 연결 리스트, 헤더 파일, make로 빌드하는 다중 파일 프로그램으로 연습하세요. 디버거(gdb 또는 lldb)와 Valgrind 또는 AddressSanitizer는 많은 시간을 아껴 줍니다. 그다음 같은 개념 위에 더 많은 안전성을 얹은 C++나 Rust를 살펴보세요.
자주 묻는 질문
C언어는 지금도 배울 가치가 있나요?
네. 운영체제, 임베디드 기기, 성능이 중요한 라이브러리에 여전히 쓰이며, 메모리와 포인터, 컴파일된 코드가 실행되는 방식을 가르쳐 주어 다른 모든 언어를 배울 때도 도움이 됩니다.
C와 C++는 무엇이 다른가요?
C++는 C의 확장으로 시작해 클래스, 템플릿, 참조, 예외, 방대한 표준 라이브러리를 더했습니다. C는 작고 절차적인 언어로 남아 있습니다. 대부분의 C 코드는 유효한 C++에 가깝지만 두 언어는 별개의 표준입니다.
내 C 프로그램이 세그멘테이션 오류로 죽는 이유는 무엇인가요?
자신이 소유하지 않은 메모리에 접근했기 때문입니다. 대개 널, 초기화되지 않은, 또는 해제된 포인터를 쓰거나 배열 끝을 넘어 인덱싱할 때 발생합니다. 디버거나 AddressSanitizer로 실행하면 정확한 줄을 찾을 수 있습니다.
어떤 C 표준을 써야 하나요?
C17은 현재 컴파일러에서 널리 지원되고, C23은 가장 최신 표준입니다. 동작을 일관되게 하려면 -std=c17처럼 버전을 명시적으로 지정하세요.