16진수 (Hexadecimal, Hex)
- 16진수는총 16개 기호를 사용해서 수를 표현합니다.
- 0 ~ 9 그리고 A(10), B(11), C(12), D(13), E(14), F(15)까지
- 10진수로 255는 16진수로 FF입니다.
- 16진수를 구분하기 위해 숫자 앞에 "0x"를 붙여 사용합니다.
1 Byte와 16진수
- 1 byte = 8 bit
- 8 bit로 나타낼 수 있는 최대 수:
- 2^8 = 256 (0부터 255까지)
- 이 256개의 값을 16진수 2자리로 표현할 수 있습니다.
- 예: 0x00 ~ 0xFF
- 읽을 때는 “one zero” (16진수 10)처럼 읽어야 하고,
- 10진수의 “ten”과 헷갈리면 안 됩니다.
RGB 색상에서 16진수
- 웹 색상(RGB)은 16진수로 표현합니다.
- #FFFFFF → 흰색 (255, 255, 255)
- #FF0000 → 빨간색 (255, 0, 0)
메모리 주소와 포인터
C 언어는 메모리 주소를 직접 다룰 수 있습니다.
#include <stdio.h>
int main(void)
{
int n = 50;
printf("%p\n", &n);
}
- &n: 변수 n의 메모리 주소를 가져온다.
- %p: 포인터(메모리 주소)를 출력할 때 사용하는 포맷 문자열
실행 결과 예시:
0x16f542f3c
컴퓨터가 n을 메모리 어딘가에 저장했음을 의미합니다. 주소는 16진수로 표시됩니다.
&와 *의 의미
& | 주소 연산자(Address-of) | “이 변수의 주소를 알려줘” |
* | 간접 참조(Deference) | “이 주소로 가서 값을 가져와” |
#include <stdio.h>
int main(void)
{
int n = 50;
printf("%i\n", *&n);
}
- &n: n의 주소를 가져옴
- *(&n): 그 주소로 가서 실제 값을 가져옴
결과적으로 n의 값 50이 출력됩니다.
- n이 메모리에 저장됨 (예: 0x12345678 주소에 저장)
- &n → 주소 0x12345678을 가져옴
- *(&n) → 주소 0x12345678로 가서 거기에 저장된 50을 읽어옴
포인터란?
- 포인터(pointer) 는 “메모리 주소를 저장하는 변수“입니다.
- 일반 변수는 값을 저장하지만, 포인터는 어떤 값이 저장된 메모리 위치(주소) 를 저장합니다.
기본 문법
int n = 50; // n이라는 정수(int) 변수에 50 저장
int *p = &n; // p는 int를 가리키는 포인터, n의 주소를 저장
int *p | “p는 int를 가리키는 포인터다” |
&n | n의 메모리 주소를 가져온다 |
왜 포인터는 별도로 필요한가?
- 메모리 주소는 특별한 데이터이기 때문입니다.
- 단순한 int, float 같은 타입에 주소를 저장할 수 없습니다.
잘못된 예 (컴파일 에러):
int p = &n; // ❌ 주소를 int에 넣으려 해서 경고나 오류 발생
- 주소는 반드시 포인터 타입(int *, float *, char *)에 저장해야 합니다.
포인터 사용 방법
주소를 출력
#include <stdio.h>
int main(void)
{
int n = 50;
int *p = &n;
printf("%p\n", p); // p는 n의 주소를 저장하고 있음
}
출력 예:
0x16d296f3c
(메모리 주소, 16진수로 출력됨)
값을 간접 참조(Dereferencing)
#include <stdio.h>
int main(void)
{
int n = 50;
int *p = &n;
printf("%i\n", *p); // p가 가리키는 주소에 저장된 값 출력
}
출력:
50
- *p는 “p가 가리키는 메모리 주소로 가서 그 값을 가져온다“는 뜻입니다.
포인터 개념을 비유로 이해하기
- n = 편지 (안에 숫자 50이 적혀있음)
- p = 지도 (n 편지가 어디에 있는지 알려줌)
- &n = “편지가 여기 있어!“라는 정확한 주소
- * = “그 주소로 가서 편지를 열어라”
그림처럼 표현하면
p ----> (주소 0x12345678) ----> 50
- p는 0x12345678을 저장하고
- 그 주소에는 50이 저장되어 있습니다.
포인터
“포인터가 다른 포인터를 가리키고, 또 다른 포인터를 가리키고,…” → 복잡한 연결 구조를 만들 수 있습니다.
- 연결 리스트 (Linked List)
- 트리 (Tree)
- 그래프 (Graph)
- 스택, 큐 같은 자료구조
이런 구조를 만들 수 있게 해줍니다
→ 결국 포인터는 컴퓨터 과학의 기본 도구입니다.
포인터의 크기
- 64비트 컴퓨터에서는
- 메모리 주소 자체가 64비트(= 8바이트) 크기입니다.
- 모든 포인터(int *, float *, char *)의 크기는 동일합니다.
- C 언어에는 string(문자열 타입) 이 따로 없습니다.
- “문자들의 배열 + 끝 표시(\0)를 이용” 해서 문자열을 구현합니다.
따라서 실제로는:
char *s = "EMMA";
- s는 'E', 'M', 'M', 'A', '\0'가 연속으로 저장된 메모리 블록의 시작 주소를 가리킵니다.
CS50 라이브러리에서 string
CS50 라이브러리에서는 편하게 쓰기 위해 typedef로 미리 별명(alias)을 만들었습니다.
typedef char *string;
즉:
- string이라는 자료형은 사실 char * 포인터입니다.
- 문자열은 “첫 번째 문자 주소를 저장하는 포인터“입니다.
메모리 구조로 이해
코드 설명
#include <stdio.h>
int main(void)
{
char *s = "EMMA";
printf("%s\n", s); // %s(string)으로 출력
printf("%p\n", s); // s 자체 (E의 주소)
printf("%p\n", &s[0]); // E의 주소
printf("%p\n", &s[1]); // M의 주소
printf("%p\n", &s[2]); // M의 주소
printf("%p\n", &s[3]); // A의 주소
}
출력 예시:
EMMA
0x1001c850c
0x1001c850c
0x1001c850d
0x1001c850e
0x1001c850f
- 첫 번째 문자는 s[0] (E)
- 두 번째 문자는 s[1] (M)
- 세 번째 문자는 s[2] (M)
- 네 번째 문자는 s[3] (A)
주소가 1씩 증가하는 이유:
- char 타입은 크기가 1바이트(8비트) 이기 때문입니다.
- 메모리에서 문자 하나당 1바이트를 차지합니다.
포인터로 문자열 접근
#include <stdio.h>
int main(void)
{
char *s = "EMMA";
printf("%c\n", *s); // s[0]
printf("%c\n", *(s+1)); // s[1]
printf("%c\n", *(s+2)); // s[2]
printf("%c\n", *(s+3)); // s[3]
}
출력:
E
M
M
A
핵심 개념:
- s[i]는 사실 *(s + i)로 해석됩니다.
- 즉, 포인터 + 인덱스로 메모리 이동한 후, 그 위치의 값을 읽습니다.
%s는 왜 문자열 전체를 출력할까?
printf("%s\n", s);
- %s는 “메모리 주소를 받아서 \0(널문자) 를 만날 때까지 문자를 차례로 출력”합니다.
- 그래서 EMMA\0이 메모리에 저장되어 있을 때,\0을 만나면 멈춥니다.
- E → M → M → A를 출력하고
- %c : 문자 하나 출력
- %s : 시작 주소부터 \0까지 쭉 출력
int형 비교는 잘 작동한다
#include <cs50.h>
#include <stdio.h>
int main(void)
{
int i = get_int("i: ");
int j = get_int("j: ");
if (i == j)
{
printf("Same\n");
}
else
{
printf("Different\n");
}
}
- int는 값(value) 을 저장합니다.
- 그래서 i == j는 값이 같은지 비교합니다.
string 비교는 조심해야 한다
#include <cs50.h>
#include <stdio.h>
int main(void)
{
string s = get_string("s: ");
string t = get_string("t: ");
if (s == t)
{
printf("Same\n");
}
else
{
printf("Different\n");
}
}
- 여기서 s == t는 문자열의 내용이 아니라
- s와 t 포인터(주소)가 같은지를 비교합니다.
- 입력을 각각 emma, emma라고 해도,
- s와 t는 메모리의 다른 위치에 저장됩니다.
결론:
- 문자열 내용이 같아도 주소가 다르면 s == t는 false가 됩니다.
문자열 내용을 비교하려면?
문자열 비교 함수 strcmp()를 사용해야 합니다!
#include <cs50.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
string s = get_string("s: ");
string t = get_string("t: ");
if (strcmp(s, t) == 0)
{
printf("Same\n");
}
else
{
printf("Different\n");
}
}
설명:
- strcmp(s, t) 함수는:
- 같으면 0
- 다르면 양수 또는 음수를 반환합니다.
문자열 비교는 항상 strcmp를 사용해야 안전합니다!
string 복사의 함정
string s = get_string("s: ");
string t = s;
t[0] = toupper(t[0]);
- string은 사실 char *입니다.
- (CS50 string → C의 char * (포인터))
- t = s;를 하면 s와 t 모두 같은 메모리 주소를 가리킵니다.
- 그래서 t를 수정하면 s도 같이 바뀝니다.
출력 결과
s: emma
Emma
Emma
메모리 복사를 위한 malloc
포인터 s와 t를 완전히 독립된 메모리 공간에 복사하고 싶다면, 새로운 공간을 만들어야 합니다.
→ 여기서 malloc을 사용합니다.
char *t = malloc(strlen(s) + 1);
- strlen(s) : 문자열의 길이 (종단 문자 \0은 포함되지 않음)
- + 1 : \0까지 복사해야 하므로 한 칸 추가
- malloc은 힙(Heap) 메모리에 새로운 공간을 할당해줍니다.
직접 한 글자씩 복사
for (int i = 0, n = strlen(s); i < n + 1; i++)
{
t[i] = s[i];
}
- 한 글자씩 s의 문자를 t로 복사
- 마지막 \0도 복사됨
strcpy로 복사를 더 간편하게
strcpy(t, s);
- 문자열 전체를 복사해주는 함수
- 복사 대상(t) 은 충분한 메모리 공간을 미리 갖고 있어야 함 (malloc으로 확보)
- 종단문자 \0도 자동으로 복사해줍니다.
최종 코드
#include <cs50.h>
#include <stdio.h>
#include <ctype.h>
#include <string.h>
#include <stdlib.h>
int main(void)
{
char *s = get_string("s: ");
char *t = malloc(strlen(s) + 1); // 종단 문자까지 공간 확보
strcpy(t, s); // 문자열 전체 복사
t[0] = toupper(t[0]); // 복사한 문자열의 첫 글자만 대문자화
printf("%s\n", s);
printf("%s\n", t);
free(t); // 사용이 끝나면 반드시 메모리 해제!
}
malloc과 free
- malloc(size) : 메모리 크기만큼 동적으로 할당 (메모리 “빌리기”)
- free(pointer) : 빌린 메모리를 사용 후 반드시 “반납”
메모리 누수(memory leak) 를 방지하기 위해, malloc을 썼으면 반드시 free도 써야 합니다.
약어와 의미
malloc | memory allocation | 메모리를 할당하다 |
strcpy | string copy | 문자열을 복사하다 |
strcmp | string compare | 문자열을 비교하다 |
C에서 malloc처럼 메모리 할당 후 반드시 해제해야 하는 대표적인 함수
malloc만 해제하면 되나요?
아니요. malloc 이외에도 동적으로 메모리를 할당하는 모든 함수는 프로그램이 끝나기 전에 반드시 free를 호출해서 메모리를 해제해야 합니다.
malloc처럼 free가 필요한 대표 함수들
malloc (memory allocation)
- 사용자가 원하는 크기의 메모리를 할당합니다.
- 사용 예:
int *p = malloc(10 * sizeof(int));
...
free(p);
calloc (contiguous allocation)
- malloc처럼 메모리를 할당하는데,
- 0으로 모두 초기화합니다.
- 사용 예:
int *p = calloc(10, sizeof(int));
...
free(p);
realloc (reallocate)
- 이미 할당된 메모리 크기를 변경합니다.
- 내부적으로 새로운 메모리를 할당하고, 옛 데이터를 복사할 수 있습니다.
int *p = malloc(10 * sizeof(int));
p = realloc(p, 20 * sizeof(int));
...
free(p);
strdup (string duplicate)
- 문자열을 복사해서
- 새로운 메모리 공간에 저장합니다.
strdup은 내부적으로 malloc을 사용합니다!
char *copy = strdup("hello");
...
free(copy);
strdup = string duplicate의 줄임말입니다.
getline
- 사용자가 입력한 한 줄을
- 동적으로 메모리에 저장해주는 함수입니다.
- getline은 호출 시 메모리를 직접 할당하므로,
- 읽고 나면 free를 호출해야 합니다.
char *line = NULL;
size_t len = 0;
getline(&line, &len, stdin);
...
free(line);
free가 아니라 다른 해제를 사용하는 함수 예외
- fopen으로 파일을 열면
- 파일 핸들을 반환합니다.
- 이때는 free로 해제하는 게 아니라 fclose()로 닫아야 합니다.
FILE *fp = fopen("test.txt", "r");
...
fclose(fp);
free(fp); ❌ (오류 발생)
fclose(fp); ✅ (정상)
1. Valgrind란?
- 메모리 누수(memory leak), 잘못된 메모리 접근(segfault), 할당/해제 문제(double free, etc.) 같은 문제를 찾아주는 C/C++ 전용 디버깅 도구입니다.
프로그램이 메모리를 “잘 쓰고 있는지” 자동으로 분석해줍니다.
2. Mac에서 Valgrind 설치 방법
- Valgrind는 macOS에서 공식적으로 100% 지원되지 않습니다.
- 하지만 특정 버전(구버전) 과 수정된 포크 버전(forked version) 은 설치할 수 있습니다.
Valgrind 설치 시도
일단 기본적으로 설치를 시도해봅니다.
brew install valgrind
M1/M2 Mac 사용자의 경우
Apple Silicon (M1, M2) 맥에서는 수정된 비공식 포크(Fork)를 설치해야 합니다.
- 터미널에서 아래 명령어 입력:
brew tap LouisBrunner/valgrind
brew install --HEAD LouisBrunner/valgrind/valgrind
- --HEAD는 최신 개발 버전(unstable)이라는 의미입니다.
- 설치 완료 후 확인:
valgrind --version
버전이 출력되면 성공입니다.
3. Valgrind 사용법
기본 명령어
valgrind ./your_program
- your_program은 컴파일된 실행 파일입니다.
C 프로그램 예시
- 예를 들어, 메모리 누수가 있는 프로그램이 있다고 해봅시다:
// leak.c
#include <stdlib.h>
int main(void)
{
int *x = malloc(10 * sizeof(int));
x[0] = 42;
// free(x); // ❌ 누수 발생 (free를 안 했음)
return 0;
}
- 컴파일:
clang -g leak.c -o leak
- -g는 디버깅 정보를 추가해서 valgrind가 더 자세히 분석할 수 있게 해줍니다.
- valgrind 실행:
valgrind ./leak
- 출력 예시:
==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345== at 0x10000F5AA: malloc (in /usr/lib/system/libsystem_malloc.dylib)
==12345== by 0x100000F42: main (leak.c:5)
- “40 bytes definitely lost” → 메모리 누수 발생
- leak.c:5 → 문제 코드 줄까지 정확히 알려줍니다.
메모리 문제를 더 깔끔히 보고 싶으면
valgrind --leak-check=full ./your_program
- leak 정보, 메모리 손상 정보까지 자세히 보여줍니다.
1. Docker 리눅스 컨테이너 안에서 Valgrind 사용 (가장 현실적)
- M1 맥에서도 잘 동작하는 방법입니다.
- Docker로 리눅스(Ubuntu) 환경을 만든 다음,
- 거기 안에 Valgrind를 설치해서 사용합니다.
2. 완전히 다른 메모리 디버깅 툴 사용 (예: AddressSanitizer)
AddressSanitizer (줄여서 ASan)는 clang/gcc에 기본 내장된 메모리 오류 탐지 기능입니다. Mac에서도 완벽히 지원됩니다. 하지만 기본적으로 메모리 오류를 탐지하는 것이고, 메모리 Leak(누수)는 탐지하지 못한다.
사용 방법
- 컴파일할 때:
clang -fsanitize=address -g yourfile.c -o yourfile
- 실행:
./yourfile
- 메모리 오류가 자동으로 감지되고 보고됩니다.
메모리 누수를 잡기 위해서 ASAN_OPTIONS=detect_leaks=1 ./yourfile 을 추가해 사용해야 하는데, M1, M2에서는 활용할 수 없다.
clang -fsanitize=address -g yourfile.c -o yourfile -lcs50
ASAN_OPTIONS=detect_leaks=1 ./yourfile
장점:
- ARM 맥(M1/M2) 완벽 지원
- 매우 빠르고 정확함
- valgrind보다 최신 기술 기반
문제 코드 분석
#include <stdlib.h>
void f(void)
{
int *x = malloc(10 * sizeof(int));
x[10] = 0; // 잘못된 접근
// free(x); // 메모리 해제 누락
}
int main(void)
{
f();
return 0;
}
무슨 일이 일어났는가?
1 | malloc으로 할당한 메모리를 free하지 않았다. (메모리 누수 발생) |
2 | 배열 경계를 넘어 접근했다. (버퍼 오버플로우 발생) |
(1) 메모리 누수 (Memory Leak)
- malloc(10 * sizeof(int)) → 10개의 int (약 40바이트)를 동적 할당
- 하지만 free(x);를 호출하지 않았음
- 이 메모리는 프로그램 종료 시까지 사용되지 않고 버려짐
- 결과: 메모리 누수 발생
(2) 버퍼 오버플로우 (Buffer Overflow)
- malloc(10 * sizeof(int))으로 만들어진 배열은
- 유효한 인덱스가 x[0]부터 x[9]까지입니다.
- x[10] = 0;는 11번째 요소에 접근하는 것.
- 할당된 메모리 블록을 넘어서 접근한 것이라
- 메모리 손상(memory corruption)
- 세그멘테이션 폴트(segmentation fault)
- 예측 불가능한 버그
- 프로그램 동작이 비정상적이 될 수 있습니다.
배열의 인덱스는 항상 “0부터 (n-1)까지”가 원칙입니다.
올바르게 고친 코드 예시
#include <stdlib.h>
void f(void)
{
int *x = malloc(10 * sizeof(int));
if (x == NULL)
{
return; // malloc 실패 대비
}
x[9] = 0; // x[0] ~ x[9]까지만 유효
free(x); // 메모리 해제
}
int main(void)
{
f();
return 0;
}
- malloc은 항상 NULL 체크하는 것이 안전합니다.
- 배열은 경계를 벗어나지 않아야 합니다.
- 사용이 끝나면 반드시 free() 해야 합니다.
디버깅 도구를 썼다면 어떤 오류를 볼 수 있을까?
- AddressSanitizer를 붙여 컴파일하면:
clang -fsanitize=address -g yourfile.c -o yourfile
./yourfile
실행 결과 예시:
== AddressSanitizer: heap-buffer-overflow on address 0x602000000028
READ of size 4 at 0x602000000028 thread T0
#0 0x.... in f yourfile.c:6
- heap-buffer-overflow : heap(동적 메모리)에서 버퍼 초과 접근
- yourfile.c:6 : 문제 발생한 줄
복습: 값에 의한 호출(Call by Value)
잘못된 swap 예제
#include <stdio.h>
void swap(int, int);
int main(void)
{
int x = 1;
int y = 2;
printf("x is %i, y is %i\n", x, y);
swap(x, y);
printf("x is %i, y is %i\n", x, y);
}
void swap(int a, int b)
{
int tmp = a;
a = b;
b = tmp;
}
왜 실패할까?
- C 언어에서 함수에 값을 전달하면,
- 원본 값이 아니라 “복사본”이 전달됩니다.
- 여기서 a와 b는 x와 y의 복사본입니다.
- 복사본끼리 값을 교환했지만,
- 원본(x, y)은 그대로 남아 있습니다.
출력 결과:
x is 1, y is 2
x is 1, y is 2
메모리 구조 이해 (Heap, Stack)
표를 메모리 영역이라고 생각해보면,
텍스트 영역 (Text Segment) | 실행할 프로그램 코드가 저장되는 영역 |
데이터 영역 (Data Segment) | 전역 변수(static) 등이 저장 |
힙 (Heap) 영역 ↓ [malloc() - free()로 할당 취소] |
동적 메모리 할당 공간 (malloc/free로 관리), 아래로 성장 |
[swap()의 a, b, tmp - 함수 사용이 끝나면 해제] ↑ [main()의 x, y] ↑ 스택 (Stack) 영역 |
함수 호출 시 생성되는 지역 변수 저장, 위로 성장 |
- main 함수가 호출될 때 스택 프레임이 생깁니다.
- swap 함수가 호출될 때도 스택 프레임이 생깁니다.
- 각 함수마다 독립적인 스택 공간이 생성되기 때문에
- a와 b는 x와 y와 값만 같고, 실제 주소는 다릅니다.
포인터로 주소를 전달하면?
주소를 넘기기
#include <stdio.h>
void swap(int *, int *);
int main(void)
{
int x = 1;
int y = 2;
printf("x is %i, y is %i\n", x, y);
swap(&x, &y);
printf("x is %i, y is %i\n", x, y);
}
void swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
- &x → x의 주소를 넘긴다
- *a → 그 주소로 가서 값을 읽는다
- *a = *b → 그 주소의 값을 직접 수정한다!
출력 결과:
x is 1, y is 2
x is 2, y is 1
힙과 스택 충돌 (Stack Overflow, Heap Overflow)
기본 개념
- 스택(Stack) : 함수 호출 시 생성되는 지역 변수 공간, 위로 쌓인다.
- 힙(Heap) : malloc 같은 동적 메모리 할당 공간, 아래로 확장된다.
정리: 스택은 위로 커지고, 힙은 아래로 커집니다.
만약 둘이 너무 커져서 중간에서 만난다면?
→ 스택 오버플로우(stack overflow)
→ 힙 오버플로우(heap overflow)
→ 컴퓨터가 프로그램을 강제 종료하거나 이상동작합니다.
버퍼 오버플로우(Buffer Overflow)
Stack/Heap Overflow | 메모리 영역 자체가 넘쳐서 서로 충돌 |
Buffer Overflow | 정해진 배열, 버퍼를 넘어 잘못된 메모리에 접근 |
버퍼 오버플로우는 배열(버퍼) 경계 침범 문제이고, 스택/힙 오버플로우는 메모리 구조 전체 문제입니다.
scanf와 포인터, 메모리 문제
(1) 올바른 int 입력 예시
int x;
scanf("%i", &x);
printf("x: %i\n", x);
- &x : x의 주소를 scanf에 넘겨줍니다.
- scanf는 그 주소로 가서 사용자가 입력한 값을 저장합니다.
핵심: scanf는 메모리 주소를 필요로 합니다. (int, float, double 모두 마찬가지)
(2) 잘못된 char* 입력 예시
char *s = NULL;
scanf("%s", s);
printf("s: %s\n", s);
- s는 NULL입니다. 즉, 아무 메모리 공간도 가리키지 않습니다.
- 그런데 scanf는 s가 가리키는 공간에 문자를 저장하려고 합니다.
- 결과: Segmentation Fault (메모리 오류) 발생.
문제 핵심:
- char s = NULL;* 로는 저장할 메모리가 없습니다.
- 메모리를 직접 확보해야 합니다.
(3) 배열로 안전하게 입력 받기
char s[5];
scanf("%s", s); // s가 주소기 때문에 &가 필요없다.
printf("s: %s\n", s);
- s는 크기 5인 메모리를 갖는 배열입니다.
- scanf는 배열 s의 첫 번째 요소 주소(s[0])로 가서 입력을 저장합니다.
- 그래서 이 경우는 정상적으로 동작합니다.
하지만 여기에도 문제!
- 사용자가 5글자 넘게 입력하면 → 배열 크기를 초과해서 버퍼 오버플로우 발생합니다.
- scanf는 문자열 길이 제한이 없기 때문에 → 실제 프로그램에서는 매우 위험합니다.
(문자열 안전 입력은 fgets 같은 것을 써야 합니다.)
파일 입출력 (FILE*, fopen, fprintf)
#include <cs50.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
/*
FILE은 자료형, fopen(열고 싶은 파일 이름, 동작[r=읽기, w=쓰기, a=덧붙이기])
fopen은 해당 파일을 가르키는 포인터를 반환
*/
// open file
FILE *file = fopen("phonebook.csv", "a");
// get string from user
char *name = get_string("Name: ");
char *number = get_string("Number: ");
// print (write) strings to file
fprintf(file, "%s,%s\n", name, number);
// close file
fclose(file);
}
파일열기:
FILE *file = fopen("phonebook.csv", "a");
- FILE*은 C 표준 라이브러리의 파일 포인터 자료형입니다.
- fopen("파일명", "모드")
- "r" : 읽기
- "w" : 새로 쓰기 (파일 초기화)
- "a" : 덧붙이기 (append)
입력받기:
char *name = get_string("Name: ");
char *number = get_string("Number: ");
- CS50의 get_string 사용
- 입력된 문자열을 동적 메모리에 저장합니다 (malloc 내부 사용)
파일에 쓰기:
fprintf(file, "%s,%s\n", name, number);
- fprintf는 file 포인터를 받아서
- 문자열을 파일로 출력합니다.
파일 닫기:
fclose(file);
- 파일을 연 다음은 반드시 fclose()로 닫아야 합니다.
- 안 닫으면 데이터가 디스크에 완전히 저장되지 않을 수 있습니다.
결과:
phonebook.csv 파일 안에
Emma,617-555-0100
Rodrigo,617-555-1000
이런 식으로 이름과 번호가 저장됩니다.
파일에서 바이트를 읽어 JPEG 포맷 판별하기
1. 프로그램 개요
#include <stdio.h>
int main(int argc, char *argv[])
{
// ensure user ran program with two words at prompt
if (argc != 2)
{
return 1;
}
// open file
FILE *file = fopen(argv[1], "r");
if (file == NULL)
{
return 1;
}
// read 3 bytes from file
unsigned char bytes[3];
fread(bytes, 3, 1, file);
// check if bytes are 0xff 0xd8 0xff
if (bytes[0] == 0xff && bytes[1] == 0xd8 && bytes[2] == 0xff)
{
printf("Maybe\n");
}
else
{
printf("No\n");
}
fclose(file);
return 0;
}
2. 핵심 동작 설명
(1) 명령줄 인자 확인
if (argc != 2)
{
return 1;
}
- 프로그램 이름(argv[0]) + 파일 이름(argv[1]) → 총 2개가 전달되어야 함
- 파일 이름이 없으면 바로 종료
(2) 파일 열기
FILE *file = fopen(argv[1], "r");
- "r" 모드는 파일을 읽기 전용으로 엽니다.
- 파일 열기에 실패하면 (예: 파일이 없거나 권한 문제) 프로그램 종료
(3) 파일에서 3바이트 읽기
unsigned char bytes[3];
fread(bytes, 3, 1, file);
- fread(배열, 읽을 바이트 수, 읽을 횟수, 파일포인터)
- 이 코드는 파일에서 3바이트를 한 번에 읽어 bytes[0], bytes[1], bytes[2]에 저장합니다.
unsigned char를 사용하는 이유: 바이트 데이터는 0~255 범위를 가지므로 char 대신 unsigned char를 써야 음수 해석을 방지할 수 있습니다. (기본적으로 (char)bytes는 -128~127 범위를 갖는다.)
#include <stdio.h>
int main(void)
{
char c = 200;
printf("%i\n", c);
}
// 만약 char가 signed라면: 200은 오버플로우 돼서 음수가 출력될 것
// 만약 char가 unsigned라면: 200 그대로 출력될 것
// Mac, Linux 대부분에서는 -56이 출력됩니다.
// (즉, char가 signed라서 -128~127 범위를 가집니다.)
(4) JPEG 파일 판별
if (bytes[0] == 0xff && bytes[1] == 0xd8 && bytes[2] == 0xff)
- JPEG 파일은 파일 맨 처음이 0xff 0xd8 0xff로 시작하는 특징이 있습니다.
- 이 3바이트를 확인하면 “JPEG 파일일 가능성”을 유추할 수 있습니다.
매직 넘버란?
1. 매직 넘버(Magic Number)란?
- 매직 넘버란 파일의 제일 처음(헤더 부분)에 들어 있는 고유한 고정 패턴입니다.
- 이 패턴을 보면 파일이 어떤 종류인지 컴퓨터나 프로그램이 빠르게 식별할 수 있습니다.
- 매직 넘버는 파일 확장자(.jpg, .png 등) 와는 별개로,
- 파일 내용 그 자체를 식별합니다.
즉, 파일 이름을 바꿔도(.gif → .jpg) 매직 넘버가 맞지 않으면 진짜 타입은 절대 변하지 않습니다.
2. JPEG 파일 내부 구조
JPEG 매직 넘버
바이트 번호값 (16진수)의미
0 | FF | Start of Image (SOI) |
1 | D8 | Start of Image (SOI) |
2 | FF | Marker Start |
- JPEG 파일은 항상 0xFF 0xD8 0xFF로 시작합니다.
JPEG 기본 구조
[SOI] (0xFFD8)
[APPn Marker + Segment] (파일 정보: 예, 카메라 정보)
[DQT] (Quantization Tables)
[SOF] (Start of Frame - 이미지 크기, 컬러 정보)
[DHT] (Huffman Tables)
[Start of Scan (SOS)]
[압축된 이미지 데이터]
[EOI] (0xFFD9, End of Image)
설명
SOI | 이미지의 시작 (Start of Image) |
APPn | 메타데이터 (EXIF 정보, 썸네일 등) |
DQT | 압축 품질을 결정하는 테이블 |
SOF | 이미지 해상도, 색상 구성 정보 |
DHT | 압축에 사용하는 허프만 테이블 정보 |
SOS | 실제로 압축된 이미지 데이터 시작 |
EOI | 이미지의 끝 (End of Image) |
3. GIF 파일 구조
GIF 매직 넘버
바이트 번호 | 값 (16진수) | 의미
0~2 | 47 49 46 | “GIF” 문자열 (ASCII) |
3~5 | 38 39 61 or 38 37 61 | “89a” 또는 “87a” (버전 정보) |
ASCII 해석:
- GIF89a (1990년 이후 GIF 표준)
- GIF87a (1987년 GIF 표준)
GIF 기본 구조
[Header] ("GIF87a" or "GIF89a")
[Logical Screen Descriptor]
[Global Color Table] (선택사항)
[Image Descriptors + Local Color Tables + Pixel Data]
[Trailer] (파일 끝, 0x3B)
설명
Header | “GIF87a” 또는 “GIF89a”로 파일 종류 식별 |
Logical Screen Descriptor | 화면 크기, 색상 정보 |
Global Color Table | 전체 공통 색상 팔레트 |
Image Descriptors | 각각의 프레임(이미지) 정보 |
Trailer | 파일 종료 (0x3B) |
4. PNG 파일 구조
PNG 매직 넘버
바이트 번호 | 값 (16진수) | 의미
0 | 89 | 비ASCII 문자 (파일이 텍스트 파일이 아님을 나타냄) |
1~3 | 50 4E 47 | “PNG” |
4~7 | 0D 0A 1A 0A | 줄바꿈 + 파일 전환 표식 |
ASCII 해석:
- \x89PNG\r\n\x1a\n
PNG는 8바이트 매직넘버를 가집니다. (JPEG보다 더 길고 복잡함)
PNG 기본 구조
[Signature] (8바이트 매직넘버)
[Chunks (청크들)]
- IHDR (이미지 헤더 정보)
- PLTE (팔레트, 선택사항)
- IDAT (이미지 데이터 블록)
- IEND (이미지 끝)
설명
IHDR | 이미지 크기, 색상 타입, 압축 방식 등 정의 |
PLTE | 팔레트 (필요시) |
IDAT | 실제 이미지 데이터 |
IEND | 파일의 끝 |
(5) 결과 출력
- 조건을 만족하면 Maybe 출력
- 아니면 No 출력
3. 실행 예시
./jpeg ./jpgtest.jpg
Maybe
./jpeg ./giftest.gif
No
- .jpg 파일은 JPEG의 매직 넘버(magic number)를 가지고 있어 Maybe
- .gif 파일은 GIF만의 다른 헤더(예: GIF89a)를 가지므로 No
4. 사진 파일, 2진 데이터 이해
- 사진 파일은 기본적으로 2진 데이터(binary data) 입니다.
- 이미지 내부는 수많은 픽셀로 이루어져 있고, 픽셀마다 색상 정보(예: RGB)가 저장되어 있습니다.
- 2진 데이터이기 때문에 사람이 열어도 의미 있는 글자가 보이지 않습니다.
5. 픽셀과 확대
“사진을 확대한다고 해서 픽셀이 개선되는 것은 아니다.”
- 사진을 확대하면 픽셀 하나가 더 커보일 뿐, 원본 데이터는 변하지 않습니다.
- 그래서 사진을 많이 확대하면 깨짐(픽셀화, blurring) 현상이 생깁니다.
'C' 카테고리의 다른 글
모두를 위한 컴퓨터 과학(하버드CS50 2019)(4) (0) | 2025.04.29 |
---|---|
모두를 위한 컴퓨터 과학(하버드CS50 2019)(2) (0) | 2025.04.28 |
모두를 위한 컴퓨터 과학(하버드CS50 2019)(1) (1) | 2025.04.27 |
C (1) | 2025.04.13 |