나는 코더다

1편 : C언어 포인터? vs. 자바 참조변수

by Younggi Seo





일전에 C언어의 포인터 개념을 스스로 환기한 적이 있었다. 내용은 한 번 보면, 초보가 절대 이해할 수 없다. 그래서 다시 이 '포인터'를 스스로 환기하기보다는 코더(개발자 말고) 수준의 프로그래머도 이해할 수 있도록 정리해 봤다.



그전에 코딩도 식(디저트) 후경부터~


3월 2일은 대체휴일이라, 동네에 있는 쌍팔년도 분위기의 황토탕에서 때 빼고 광내고(무한도전이 방문한 그 목욕탕?), 근처 카페에서 플랫화이트 한 잔 음미하고 코딩 시작!




1. 포인터 의미


1) 행선지에 비유


어제 필자는 링글 1:1 원어민 화상 영어 수업을 마칠 때쯤, 링글 튜터(하버드생 Linguistics(언어학과) 튜터)의 이메일 주소을 얻었다. '몽테네그로(Montenegro)'에서 휴가를 보내고, 이제 곧 '세르비아'로 짐을 챙기고 떠날 예정인 튜터에게 응원의 메시지를 보냈고, 아래와 같은 답장을 받았다.



그녀가 회신한 이메일 메시지 전체다음 여행의 목적지담겨 있는 변수(메모리의 저장 공간)라면, 개발언어에서 포인터이메일의 메시지와 같은 역할을 한다. 즉, 다음 행선지(주소)를 가리키는 역할을 한다.



2) 포인터의 정의


그녀의 다음 행선지가 '세르비아'라는 것은 메일 메시지(포인터)를 통해서 확인할 수 있다. 개발언어에서 포인터는 변수라는 어떤 값을 담는 그릇의 주소(변수의 별칭, 아래 도식에서 변수 a)를 가리키고, 이 포인터가 그릇의 실제 주소(아래 도식에서 100번지)를 참조함으로써 그 그릇에 담긴 데이터*를 참조한다.


역참조는 포인터가 가리키는 값을 거꾸로 포인터 변수 P를 통해 확인하는 과정(간접참조 연산)이다.


그래서 포인터는 메모리를 사용하는 또 다른 방법이다. 굳이 일반 변수를 두고 왜 이 포인터를 통해 그 변수의 주솟값을 저장하는 용도로 포인터 변수를 사용해야 하는 걸까?



* 데이터(자료) : 위에서 예시로 든 목적지 지명, Serbia는 C언어에서 문자열(string)이므로 char 배열( char dest[7] = "Serbia"; )이라는 문자열(String) 자료형(Data Type)으로 변수 선언과 초기화할 수 있다. 만약 char destination = 'Serbia'로 단일문자(Char) 형 변수로 초기화를 해버리면, 문자열이 아닌 단일 문자(멀티 캐릭터 상수**)로 취급되어 마지막 'a'만 남고 나머지는 버려짐. 이러한 메모리 할당으로 인해 글의 맨 마지막 부분에서 언급한 메모리 누수(Leakage)에 발생함.


** 멀티 캐릭터 상수(Multi-character constant)로 인식하여 정수형(int) 값으로 처리됨.


2. 포인터의 목적


1) 사용까닭


자~ 여기서부터가, 주입식 교육만으로 C언어를 배웠다면 포인터를 뒤집어서 볼 까닭까진 없었을 거다. C언어를 배우기 전에 Java(객체지향언어, 실제론 함수형 개발언어는 아니다.)를 먼저 배웠던 필자로서 이 포인터의 역할은 단순히 메모리에 접근하기 위해서라고만 생각했었다.


틀린 말은 아니지만, 태초에 C언어를 만들 때(C lang 이전엔 B lang, A lang이라는 언어가 있어다는 사실...) 그 개발자는 유닉스 시스템에서 하드웨어를 조작하기 위한 언어로 C를 만들었다고 한다. 그런데, 시스템으로까지 개발영역을 확장하면 scope(영역)이라는 게 아주 중요해질 것 같다. 왜냐하면 변수의 boundary(경계)가 명확하지 않으면 특정 영역(a Local)에서 선언했던 변수가 다른 특정 영역(other Local)에서의 변수와 이름이 동일하여 겹칠 수 있기 때문이다. 아니, 서로 공유가 필요하기 때문이다.


즉, 모든 영역(Global)에서 공통적으로 쓰는 변수(Global Variable, 전역 변수)를 지정할 필요가 있고, 이것을 모든 영역에서 변수명(아까 말한 별칭, Alias)으로 통일시켜 버리면 초창기 시스템 개발자들끼리도 혼란스러울 수가 있다. 그렇지 않을까? 그래서 포인터라는 전역적으로(모든 함수) 사용가능한 변수의 주소를 포인터 변수에 저장시켜서, 이 포인터 변수를 통해 값을 호출할 수 있다(*Call by Reference).


*Call by Reference: 함수 내부에서 외부 포인터가 가리키는 '주소' 자체를 바꾸려면, 그 포인터 변수의 주소를 넘겨받아야 하는데, 이 구현 과정이 Call by Reference이며, 포인터 변수 자체를 수정하려면 그 포인터의 주소(&p)를 넘겨받는 이중 포인터(int **pp)가 필요하다(Call by Reference for Pointers).



2) 매개변수(parameter), 인자, 인수(argument)라는...


필자처럼 자바를 먼저 배운 코더라면, 위의 용어들 많이 들어봤을 것이다.


사실 앞서 말한 변수나 그 변수의 값을 때에 따라서 부르는 용어들의 집합에 불과하다. 이것들이 바로 포인터라는 개념으로 함수 선언을 할 때, 달리 불러진다. 그런데, 포인터라는 변수의 주소를 가리키는 역할의 개념이 자바에서는 new라는 연산자다.


Class obj = new Class();


자바에서 new라는 연산자를 통해 Class 인스턴스를 위한 힙 메모리를 확보하고, Class() 생성자를 '딱 한번' 호출하여 초기화한 후 객체 주소(Reference Variable, 참조 주소)를 obj에 반환하는 실행구문이다.


자바의 포인터 개념

포인터 vs 참조: 자바는 C/C++처럼 포인터의 주소 값을 직접 연산할 수 없지만, 참조 변수가 실제 객체의 메모리 주소를 가리킨다는 점에서 개념적으로 포인터와 유사함.


참조 변수: new로 생성된 객체는 힙메모리에 존재하며 이 객체를 가리키는 변수(참조 변수)는 스택(Stack)에 주소값을 저장함.


가비지 컬렉션(GC): C++과 달리 new로 생성한 후 사용이 끝난 객체는 자바 가상 머신(JVM)의 가비지 컬렉터가 자동으로 메모리에서 제거하여 안전한 메모리 관리를 제공함.


결론적으로 자바는 new 연산자를 통해 힙 메모리에 객체를 생성하고, 포인터와 유사한 역할을 하는 참조 변수를 통해 객체에 접근할 수 있는데, 이러한 과정을 수동으로 처리할 때 C에서 필요한 포인터다.



3. 정리


1) 결론


결국 포인터라는 개념은 함수 선언 시 필요한 변수들의 별칭(겉으로 부여한 변수명)으로 공용으로 쓰기가 어려우니, 일반 변수의 시작 주솟값을 가리키는 포인터 연산(주소 연산자 &, 간접참조 연산자 *)을 통해 사용 범위를 벗어난 경우인데도 데이터를 공유할 수 있게 해 준다.


주의할 점은, 포인터 변수를 지정할 때는 가리키는 변수의 자료형(Data type)과 동일해야, 기존의 변수가 할당된 동일한 메모리 크기(그룹)로 호환이 가능하다. 자료형이 정수형이냐, 실수형이냐 등에 따라서 하드웨어 내부에서 메모리에 할당(변수 초기화)한 메모리 영역의 누수가 발생하지 않도록 주의해야 한다. 왜냐하면, 결국 프로그래밍의 상위 수준에서는 코딩 이후 컴파일 속도(성능 빨)가 중요하기 때문이다.


자바는 앞서 말한 가비지 컬렉션이라는 자바 내부의 컴파일러(JVM)에 의해 사용하고 남은 메모리 영역에 대해서 알아서 수거(청소)를 하나, C는 태생이 시스템 내부의 컴파일러를 개발할 때 필요한 가장 밑단(바로 하드웨어)의 엔지니어링 언어이기 때문에 개발자가 메모리 누수(Leakage)에 대해서도 알아야 한다는 게 포인트다.



2) 세 줄 정리


포인터는 메모리를 사용하는 또 다른 방법이다.

메모리의 주소를 필요할 때마다 계속 연산하는 것보다 한 번 구한 주소를 저장해서 사용하면 편리하고, 포인터가 바로 변수의 메모리 주소를 저장하는 변수다.

포인터의 주요 기능 중 하나는 함수 간에 효과적으로 데이터를 공유하는 것이다.




참조)

혼자 공부하는 C언어, 서현우 저