C언어, '왜?'라는 질문으로 깊어지다: 포인터부터 메모리까지
C언어, ‘왜?‘라는 질문으로 깊어지다: 포인터부터 메모리까지
안녕하세요! C언어의 세계를 탐험하는 모든 개발자 여러분.
C언어를 공부하다 보면, “이건 그냥 이렇게 쓰는 거야”라고 외우고 넘어가는 수많은 문법과 함수들을 만나게 됩니다. 하지만 어느 순간, “대체 왜 이건 되고 저건 안 되지?”라는 질문의 벽에 부딪히게 되죠. 바로 그 ‘왜?‘라는 질문이 우리를 초보자에서 한 단계 성장시켜주는 열쇠입니다.
이 포스팅은 지난 며칠간 C언어의 깊은 곳을 탐험하며 나눈 질문과 답변들을 정리한 기록입니다. 여러분이 C언어의 큰 산을 넘는 데 훌륭한 가이드가 되어줄 것입니다.
1. 문자열의 두 얼굴: char[] 배열과 char* 포인터
가장 먼저 마주치는 거대한 산은 바로 ‘문자열’과 ‘포인터’의 관계입니다.
char s[] = "hello"; // 내 노트에 복사한 사본이 코드는 새로운 배열 공간을 만듭니다. 그리고 그 공간에 "hello"라는 내용을 복사해 넣습니다. 이것은 내 노트에 적은 글씨와 같아서, s[0] = 'H'; 처럼 자유롭게 수정할 수 있습니다.
char *p = "hello"; // 도서관 원본 책의 위치이 코드는 데이터를 복사하지 않습니다. 대신 포인터 p에, 프로그램의 ‘읽기 전용(Read-only)’ 공간에 저장된 원본 "hello"의 주소만 저장합니다. p[0] = 'H'; 와 같이 수정을 시도하면, 도서관 원본 책에 낙서하려는 것과 같아 운영체제가 즉시 프로그램을 중지시킵니다. 이것이 바로 세그멘테이션 폴트(Segmentation Fault) 입니다.
핵심 기억: char[]는 데이터 사본을 담는 ‘상자’이고, char*는 원본의 ‘주소’가 적힌 쪽지다.
2. 포인터의 기본 연장: &와 *
포인터를 다루기 위한 가장 기본적인 도구는 &(주소 연산자)와 *(역참조 연산자)입니다.
&(Address-of): “이 변수의 주소를 알려줘.”int num = 10;이라는 집이 있다면,&num은 그 집의 주소(번지)입니다.*(Dereference): “이 포인터가 가리키는 주소로 찾아가서 내용물을 가져와.”int *ptr = #이라는 주소 쪽지가 있다면,*ptr은 그 주소로 찾아가서 가져온 물건, 즉 값 10입니다.
이 둘의 관계만 명확히 이해하면, C언어 포인터의 절반을 정복한 것이나 다름없습니다.
3. 표준 라이브러리 함수의 ‘비밀 신호’들
C언어의 표준 함수들은 저마다 특별한 약속과 비밀 신호를 가지고 있습니다.
strtok(NULL, ...)의 마법
strtok을 두 번째 호출할 때 NULL을 넣는 이유는 “아무것도 없는 곳을 찾으라”는 뜻이 아닙니다. 이것은 strtok 함수에게 보내는 비밀 신호, 즉 “아까 작업하던 그 문자열, 내가 끊었던 바로 그 다음 위치부터 계속해서 다음 조각을 찾아줘” 라는 의미입니다. strtok은 내부에 책갈피처럼 위치를 기억하는 ‘상태 저장’ 기능이 있기 때문에 가능한 일입니다.
atoi()의 역할: 번역가
"123"은 사람이 읽는 ‘문자’이지, 컴퓨터가 계산할 수 있는 ‘숫자’가 아닙니다. atoi(“ASCII to Integer”) 함수는 바로 이 "123"이라는 문자열을 실제 연산 가능한 정수 123으로 바꿔주는 필수적인 번역가 역할을 합니다.
fgets가 남긴 `
` 처리하기
fgets는 사용자가 누른 Enter 키( )까지 문자열에 포함시킵니다. 이것을 제거하기 위해 strcspn 같은 어려운 함수 대신, 배운 strchr을 활용할 수 있습니다. strchr로 의 위치(주소)를 찾아, 그 주소에 있는 값(*ptr)을 �으로 바꿔주면 됩니다.
4. 크기와 타입에 대한 진실: sizeof, size_t, %zu
strlen과 sizeof는 길이나 크기를 반환하는데, 그 타입은 int가 아닙니다. 바로 size_t 입니다.
size_t: 시스템이 표현할 수 있는 가장 큰 메모리 크기를 담을 수 있는 부호 없는 정수(unsigned) 타입입니다. 32비트 시스템에서는unsigned int와 같을 수 있지만, 64비트 시스템에서는unsigned long long과 같습니다.%zu: 이렇게 시스템에 따라 크기가 달라지는size_t를 어떤 환경에서든 안전하고 올바르게 출력하기 위한 전용printf형식 지정자입니다.sizeof(arr)나strlen(str)의 결과를 출력할 땐 항상%zu를 사용하는 것이 표준입니다.
5. 동적 2차원 배열의 설계도: sizeof(int*) vs sizeof(int)
int **arr를 이용해 2차원 배열을 동적 할당할 때, 왜 malloc을 두 번이나 다른 sizeof로 호출할까요?
arr = (int **)malloc(sizeof(int *) * x);1단계: 안내판 만들기. 실제 데이터를 담는 공간이 아닌, 각 행의 시작 주소를 담을 포인터들을 저장할 공간 x개를 먼저 만듭니다. 포인터 하나의 크기는 sizeof(int *)입니다.
arr[i] = (int *)malloc(sizeof(int) * y);2단계: 실제 방 만들기. 각 행(i)마다, 실제 정수 데이터를 담을 y개의 공간을 만듭니다. 정수 하나의 크기는 sizeof(int)입니다.
이처럼 C언어의 동적 2차원 배열은 ‘포인터들의 배열’이라는 구조로, 한 번에 거대한 격자를 만드는 것이 아니라 안내판과 각 층을 따로 건설하는 방식으로 만들어집니다.
마치며
C언어는 정직합니다. 모든 동작에는 명확한 이유가 있습니다. ‘원래 그런 것’은 없습니다. char[]와 char*의 차이, strtok의 숨겨진 상태, size_t의 존재 이유 등 오늘 살펴본 ‘왜?‘라는 질문들은 우리를 더 나은 C 프로그래머로 만들어 줄 것입니다.
이 글이 여러분의 C언어 여정에 등불이 되기를 바랍니다. Happy Coding!
💬 댓글