brunch

You can make anything
by writing

C.S.Lewis

by Moai Nov 07. 2020

C++ 포인터

C++ 포인터, 배열

처음 프로그램을 만들었을 때 프로그램이 실행되는 원리가 무척 궁금했었다. 프로그램을 실행시키면 어떻게 실행돼서 키보드로부터 데이터를 입력받고 메모리에 저장하고 연산하며 화면에 그 결과가 보여지는 것일까? 단순 이론으로는 위 과정을 완벽히 이해할 수 없다. CPU 또는 그보다 간단한 마이크로컨트롤러의 데이터시트를 읽고  회로를 구성한 뒤 칩이 이해할 수 있는 저수준의 언어인 어셈블리 언어로 프로그램을 작성 후 ROM에 프로그램 코드를 올리는 경험을 해야 데이터를 메모리에 읽고 쓰고 하는 작업을 이해할 수 있다.


요즘은 JAVA, C#, Python, Javascript 같은 언어가 대세가 되다 보니 컴퓨터의 동작원리를 몰라도, 컴퓨터에 대한 기초가 없어도 얼마든지 프로그램을 만들 수 있다. 다만 이러한 언어는 모든 데이터가 객체로 이루어져 있다 보니 포인터에 대한 지식이 없으면 한계에 부딪힐 수밖에 없다. 그렇기에 넘어갈 수도 없고 필수적으로 알아야 하는데 하드웨어에 가까운 지식이다 보니 컴퓨터 공학과에 입학 후 포인터를 이해하지 못한 채 떠나는 학생이 매우 많다. 사적인 얘기이지만 컴퓨터공학과와 전자전기공학부를 복수전공한 입장에서 컴퓨터공학과는 최소 6년 과정으로 이루어져야 된다고 생각한다. 4년 동안 컴퓨터를 이해하고 훌륭한 개발자가 되기엔 그 시간이 매우 부족하다. 그러니 대학생이거나 대학교를 졸업했지만 프로그래밍이 이해가 안 되고 실력이 부족하다고 개발자의 길을 포기하지 말자. 시간이 해결해줄 것이다.


오늘 다뤄볼 주제는 포인터이다.


우리는 초등학교 때 덧셈, 뺄셈, 곱셉, 나눗셈과 같은 사칙연산에 대한 방법을 배웠고 중학교, 고등학교부턴 함수, 미지수를 이용해 문제를 해결하는 과정을 배운다. 모든 문제를 암산으로 해결할 수 있는 천재가 아니라면 문제집이 너덜너덜해질 정도로 풀이과정이 잔뜩 써져있을 것이다. 이 과정을 컴퓨터가 해결한다고 생각해보자.


문제집은 하드디스크에 저장되어있다고 생각하고 문제는 메모리에 올라와있다고 가정하자. 이제 사칙연산을 하기 위해 뇌세포에서 숫자를 넣고 덧셈, 뺼셈, 곱셉, 나눗셈을 한다. 컴퓨터에서 뇌는 CPU이고 뇌세포는 레지스터이다. 숫자를 머릿속으로 계산하다 보면 어디까지 연산했는지 놓치곤 한다. 그래서 중간 과정을 노트에 적어놓는다. 컴퓨터에선 이 저장하기 위해 사용되는 노트가 메모리이다. 적어놓았는데 한 달이 지나 어디에 적어놓았는지도 잊을 수 있다. 이를 위해 페이지 번호를 기억하면 되는데 컴퓨터에서 페이지 번호는 메모리 주소에 해당한다. 페이지 번호는 고정이지만 페이지에 저장되는 내용은 바뀔 수 있다.


위 과정을 프로그램으로 구현해보겠다.


우선 문제집을 클래스로 구현해보자.


workbook.h

workbook.cpp

main.cpp


score1과 score2를 노트(메모리)에 저장한다고 가정할 때 페이지번호(메모리 주소)는 어떻게 알 수 있을까?

다음과 같이 &기호를 이용하면 실제 메모리 주소를 알 수 있다.


그럼 이 페이지 번호(메모리 주소)를 어딘가에 보관을 해야 하는데 어떻게 해야 할까? 이때 포인터 변수를 이용하면 된다. 포인터 변수는 메모리 주소를 기억하기 위한 변수이다. 포인터 변수는 변수명 앞에 *을 붙여서 사용하면 된다.

그러면 페이지 번호(메모리 주소)를 가지고 그 페이지(메모리)에 있는 내용(데이터)을 보려면 어떻게 해야 할까?


선언했던 것과 같이 다시 *을 붙여주면 실제 메모리 주소에 있는 값을 알 수 있다.


여기까지는 아주 기본적인 내용이다.



살짝 어려운 내용을 원한다면?

만약 페이지 번호(메모리 주소)를 다음 페이지(+1 연산)로 넘어가 주면 어떻게 될까? 페이지 단위로 이동해야 한다. 즉 변수의 메모리 크기 (4바이트)만큼 증가한다. 이유는 +1 작업은 다음 메모리 주소에 있는 값을 읽기 위해서인데 지금 메모리 주소에 있는 변수 4바이트를 차지하고 있었기 때문이다.

포인터는 메모리 주소를 보관하는 변수이다. 변수는 4바이트 단위(int)이므로 +1을 하게 되면 자동으로 주소 값이 4바이트 늘어나게 된다


난이도를 더 올려보자. 함수를 실행하게 되면 함수 인자, 함수 실행 후 돌아올 주소, 함수에서 쓰이는 변수 공간을 미리 할당하게 되며 연산을 위해 건네받은 인자를 함수 지역변수에 복사한다.


함수에서 값을 100 증가시켰지만 출력 값은 그대로 500이다. 왜냐하면 함수 호출 시 넣은 인자(mydata)와 함수에 사용되는 int a는 다른 변수이기 때문이다.

함수에서 100을 증가시켜주려면 포인터를 사용하면 되는데 함수 호출 시 변수를 건네주는 게 아니라 &기호를 이용해 메모리 주소를 건네준다. 이후 test함수에서는 *기호를 이용해 실제 값을 수정했다.




마지막으로 배열과 리스트에 대해서 간단히 설명해보려고 한다.


함수를 호출할 때 배열 10만 개 크기의 데이터를 건네준다고 가정해보자. 우리가 기존에 하던 식으로 건네주면 데이터 10만 개를 복사해야 할 것이다. 그러지 말고 데이터가 저장된 주소를 건네주면 매우 편하다.


아래 코드는 정말 많은 내용을 담고 있다.

 - 배열은 메모리에 연속적으로 한 번에 공간을 할당한다.

 - 배열 변수 (mydata)는 mydata[0]의 주소를 가지고 있다. 즉 포인터이다.

 - mydata[i]는 mydata+i와 동일한 주소를 가지고 있다.


이것은 배열 변수가 사실 포인터라고만 생각하면 모든 게 쉽게 풀린다. 이제 배열을 함수에 건네줄까?

당연히 같은 주소를 전달했으니 같은 메모리를 가리킬 것이다.


이제 리스트에 대해 설명해보겠다. 위처럼 함수 실행 시 배열을 한 번에 할당하는 방식이 아닌 동적으로 메모리를 할당하게 되면 메모리 주소가 연속적이지 못하다. 리스트는 동적으로 데이터를 추가하게 되면 메모리를 더 할당해야 하는데 현재 위치에서 다음 위치의 데이터를 읽으려면 어떻게 해야 할까

사실 저 리스트가 int 데이터를 담고 있다고 한다면 저 블록 하나는 4바이트가 아닌 최소 12바이트 공간을 할당해야 한다. 왜냐하면 앞에 연결된 블록의 주소(4바이트), 뒤에 연결된 블록의 주소(4바이트), 현재 데이터(4바이트) 만큼 공간이 필요하기 때문이다. 그럼 마지막 블록의 경우 더 이상 연결될 블록이 없을 것이다. 이는 어떻게 표시해야 할까. 이 때는 포인터에 NULL을 넣어주면 된다. 메모리 주소가 아무것도 참조하고 있지 않다는 뜻이다.

보면 NULL을 넣은 포인터는 아무것도 참조하지 못하므로 값도 못 읽고 종료하게 된다.


매거진의 이전글 C++ 위상정렬
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari