포인터의 개념은 C프로그래밍, 자료구조와 컴퓨터구조를 학습할 때 반드시 숙지하여야 하는 개념입니다.
포인터의 개념과 사용방법은 나와 있지만
왜 불편하게 이런 걸 사용하게 되었을까라는 의문에 대한 설명은 없어서 정리했습니다. (Gemini 이용)
============================================================================
포인터가 제공하는 기능(직접적인 메모리 접근, 주소 전달 등)이 왜 절실하게 필요했는지 이해하려면,
C 언어가 탄생했던 1970년대의 컴퓨팅 환경과 C 언어의 탄생 목적을 들여다봐야 합니다.
한마디로 말하면 **"극도로 부족한 자원 환경에서, 운영체제(OS)를 직접 만들기 위해서"**였습니다.
주요 이유는 크게 3가지입니다.
1. 하드웨어 자원이 극도로 부족했습니다.
지금 우리는 수십 기가바이트(GB)의 램(RAM)을 쓰지만, C 언어가 개발된 1970년대 초반 컴퓨터(예: DEC PDP-11)의 메모리는 수십 킬로바이트(KB) 수준이었습니다. (지금의 스마트폰 사진 한 장 크기보다도 훨씬 작습니다.)
-
문제점: 함수로 데이터를 넘길 때마다 값을 복사(Copy)하면, 아까운 메모리가 금방 가득 차고, 복사하는 데 CPU 시간을 다 써버려 컴퓨터가 느려집니다.
-
해결책 (포인터): "데이터를 복사하지 말고, 데이터가 어디 있는지(주소)만 알려주자."
-
이렇게 하면 1MB짜리 데이터를 넘기든, 1KB짜리를 넘기든, 딱 주소값 크기(약 2~4바이트)만 사용하면 되므로 메모리와 속도 문제를 동시에 해결할 수 있었습니다.
-
2. 운영체제(Unix)를 만들기 위해 탄생한 언어입니다.
C 언어의 아버지 데니스 리치는 **유닉스(Unix)**라는 운영체제를 만들기 위해 C를 만들었습니다.
운영체제는 하드웨어를 관리하는 대장 프로그램입니다.

Shutterstock
-
문제점: 모니터에 글자를 띄우거나, 키보드 입력을 받거나, 디스크를 돌리려면 특정한 하드웨어 메모리 주소에 직접 신호를 줘야 합니다. (이를 Memory Mapped I/O라고 합니다.)
-
해결책 (포인터): "특정 메모리 번지(예: 0xA0000000)를 콕 집어서 값을 쓰거나 읽을 수 있는 기능이 필요하다."
-
포인터가 있으면
*0xA0000000 = 1;처럼 하드웨어의 특정 위치를 직접 조작할 수 있습니다. 다른 고급 언어(Python, Java 등)에서는 보안상의 이유로 막아둔 기능이지만, OS 개발에는 필수적입니다.
-
3. '이식성 있는 어셈블리어'가 필요했습니다.
C 언어 이전에는 시스템 프로그래밍을 **어셈블리어(기계어와 1:1 대응되는 매우 어려운 언어)**로 했습니다.
어셈블리어는 빠르고 하드웨어 제어가 가능하지만, CPU 종류가 바뀌면 코드를 처음부터 다시 짜야 했습니다.
-
문제점: 하드웨어를 제어할 만큼 강력하면서도, CPU 종류에 상관없이 쓸 수 있는 언어가 필요했습니다.
-
해결책 (포인터): 어셈블리어의 **'간접 주소 지정 방식(Indirect Addressing)'**을 고수준 언어로 구현한 것이 바로 포인터입니다.
-
포인터를 사용하면 기계어 수준의 정밀한 메모리 제어가 가능하면서도, 문법은 사람이 읽을 수 있는 형태를 유지할 수 있었습니다.
-
요약: 그 당시에는 '선택'이 아니라 '필수'
| 당시 상황 | 포인터가 필요했던 이유 |
| 메모리가 너무 비싸고 작음 | 데이터를 복사할 여유가 없어서 **참조(주소)**만 넘겨야 했음 |
| 하드웨어를 직접 제어해야 함 | 비디오 카드, 사운드 카드 등의 특정 주소에 직접 접근해야 했음 |
| 속도가 생명임 | 불필요한 연산을 줄이기 위해 메모리 다이렉트 접근이 필요했음 |
즉, 포인터는 "하드웨어의 성능을 극한으로 끌어내면서도, 사람이 코딩하기 편하게 만들기 위한" 천재적인 발명품이었던 것입니다.
이중 포인터가 필요한 이유는?
**이중 포인터(Type **ptr)**는 말 그대로 **"포인터의 주소를 담는 포인터"**입니다.
싱글 포인터(*)만으로도 많은 것을 할 수 있는데 굳이 별을 두 개(**)나 붙여서 사용하는 핵심적인 이유는
"외부에서 선언된 포인터 변수 자체를 바꾸기 위해서"입니다.
가장 중요한 이유 3가지를 정리해 드립니다.
1. 함수 안에서 '포인터가 가리키는 방향'을 바꾸고 싶을 때 (가장 중요)
함수에 변수를 보낼 때 값을 바꾸려면 주소(포인터)를 보냈죠?
마찬가지로 포인터 변수의 값(주소)을 함수 안에서 바꾸려면 이중 포인터를 보내야 합니다.
-
싱글 포인터: "A번지에 가서 내용물(값)을 바꿔라."
-
이중 포인터: "내 수첩에 적힌 A번지라는 주소 자체를 B번지로 고쳐 써라."
실패하는 경우 (싱글 포인터 사용)
=======================================================
void try_change(int *p) {
int b = 20;
p = &b; // 함수 안에서만 p가 b를 가리키게 됨.
} // 함수가 끝나면 이 작업은 증발함. 원본 포인터는 여전히 엉뚱한 곳을 가리킴.
=======================================================
성공하는 경우 (이중 포인터 사용)
=======================================================
void real_change(int **pp) { // 포인터의 주소를 받음
static int b = 20; *pp = &b; // "주소의 주소"를 찾아가서, "저장된 주소값"을 바꿔버림 } int main() { int a = 10; int *ptr = &a; real_change(&ptr); // ptr 자체의 주소를 넘김 // 이제 ptr은 b를 가리키고 있음 }===============================================================================현실 예시: 친구에게 "내비게이션 목적지 좀 바꿔줘"라고 부탁하려면, 내비게이션(포인터) 자체를 친구에게 건네줘야(참조) 친구가 조작할 수 있는 것과 같습니다.
2. 동적 2차원 배열을 만들 때
C 언어에서 int arr[3][4]; 처럼 크기가 고정된 2차원 배열 말고, 실행 중에 크기가 결정되는 2차원 배열을 만들 때 이중 포인터가 필수적입니다.
-
원리:
-
int **포인터로 '행(Row)'들의 시작 주소를 담을 리스트를 만듭니다. -
각각의 행 포인터가 실제 데이터가 들어있는 '열(Column)' 1차원 배열을 가리킵니다.
-
이렇게 하면 각 행마다 길이가 다른 '들쑥날쑥한 배열(Jagged Array)'도 만들 수 있어 메모리를 효율적으로 쓸 수 있습니다.
3. 문자열 목록(배열)을 다룰 때
여러 개의 단어(문자열)를 묶어서 처리할 때 사용합니다. 우리가 main 함수를 쓸 때 자주 보는 그 형태입니다.
C
int main(int argc, char **argv)
여기서 char **argv는 문자열들의 배열을 의미합니다.
-
char *(문자열 하나) -
char *들이 여러 개 모여 있으니, 이것들을 가리키는 것은char **(이중 포인터)가 됩니다.
예를 들어, "Hello", "World", "C" 세 단어를 저장한다면:
-
각 단어는 메모리 어딘가에
char배열로 저장됩니다. -
이중 포인터는 이 세 단어의 시작 주소들을 가지고 있는 배열을 가리킵니다.
요약
| 구분 | 설명 |
| 변수 값 변경 | int * 필요 (변수의 주소를 알아야 함) |
| 포인터 값 변경 | int ** 필요 (포인터의 주소를 알아야 함) |
| 2차원 구조 | 행과 열로 이루어진 동적 메모리 관리 시 필요 |
| 문자열 리스트 | 여러 개의 문자열을 관리할 때 필요 (char **) |
열공 하시기 바랍니다.



로그인 후에 바로 열람 가능합니다 ^^