C언어, '왜?'라는 질문으로 깊어지다: 포인터부터 메모리까지C언어, '왜?'라는 질문으로 깊어지다: 포인터부터 메모리까지

C언어, '왜?'라는 질문으로 깊어지다: 포인터부터 메모리까지

초보자를 넘어 진짜 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

strlensizeof는 길이나 크기를 반환하는데, 그 타입은 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!

💬 댓글