포인터
포인터(pointer)는 프로그래밍 언어에서 다른 변수, 혹은 그 변수의 메모리 공간 주소를 가리키는 변수를 말한다. 포인터가 가리키는 값을 가져오는 것을 역참조라고 한다. 포인터를 표시하기 위해 *(참조 연산자) 및 &(주소 연산자) 표시를 사용한다.
목차
선언과 참조[편집]
C언어에서 포인터는 다음 문법에 따라 선언한다.
타입* 포인터이름;
타입이란 포인터가 가리키고자 하는 변수의 타입을 명시한다. 포인터 이름은 포인터가 선언된 후에 포인터에 접근하기 위해 사용된다. 포인터를 선언한 후 참조 연산자(*)를 사용하기 전에 포인터는 반드시 먼저 초기화되어야 한다. 그렇지 않으면 의도하지 않은 메모리의 값을 변경하게 되기 때문이다. 따라서 C 컴파일러는 초기화하지 않은 포인터에 참조 연산자를 사용하면 오류를 발생시킨다. 따라서 다음과 같이 포인터의 선언과 동시에 초기화를 함께 하는 것이 좋다.
타입* 포인터이름 = &변수이름; 또는 타입* 포인터이름 = 주소값;[1]
C언어에서 포인터 참조는 다음 문법에 따라 참조한다.
타입* 포인터이름 = &참조할 변수이름;
C언어의 포인터[편집]
메모리 주소[편집]
컴퓨터와 관련하여 메모리 주소는 저장 매체 내부의 단일 데이터 요소를 참조하는 숫자 값이다. 컴퓨터 메모리의 위치는 컴퓨터의 램(RAM) 내부, 하드 드라이브 또는 파일 시스템 또는 사용 가능한 시스템 메모리가 충분하지 않은 경우 가상 메모리의 형태로 사용되는 임시 저장 장치에 있을 수 있다. 메모리 위치의 크기는 컴퓨터 시스템 또는 장치의 아키텍처에 따라 다르지만 일반적으로 8 비트 바이트에서 64 비트 정수까지이다. 메모리에 액세스하고 관리하는 데 사용되는 다양한 방법이 있으며, 그중 많은 방법이 메모리 관리 장치(MMU)라고 하는 하드웨어를 사용하는 반면 다른 방법은 소프트웨어에 전적으로 의존한다. 모든 시스템에는 액세스할 수 있는 최대 메모리 주소에 대한 제한이 있다. 일반적으로 시스템에서 사용할 수 있는 가장 큰 정수 유형의 최대 크기이다. 가장 일반적인 유형의 메모리 주소는 컴퓨터 시스템의 램 메모리 내에서 동적으로 변경되는 데이터에 빠르게 액세스할 수 있는 위치를 나타낸다. 컴퓨터 메모리에 저장된 실제 정보는 수정 또는 보고 있는 숫자 또는 텍스트 문서와 같은 원시 데이터부터 실행될 때 특정 메모리 주소에 저장된 실제 프로그램 코드까지 다양하다. 프로그램 실행이 완료되면 다음 프로그램에서 사용할 램이 비워지면서 사용 중인 메모리 주소 정보가 유효하지 않게 된다. 기술이 발전함에 따라 "메모리 주소"라는 용어가 변경되었으며 2011 년 현재 실제 주소를 항상 나타내는 것은 아니다. 대신 컴퓨터 나 장치의 메모리 관리 장치로 해결할 수있는 위치를 가리킬 수 있다. 이는 메모리 관리 장치가 프로그래머와 프로그램간에 추상화 수준을 제공하는 대신 운영 체제 또는 기타 하드웨어가 메모리 이동 및 할당을 적절하게 관리 할 수 있도록 한다. 메모리 주소의 중간 변환은 프로그래머가 새로운 메모리 체계를 배우거나 다른 유형의 컴퓨터 아키텍처에 대한 소스 코드를 수정할 필요가 없음을 의미한다. 많은 컴퓨터 시스템 및 운영 체제에서 메모리 주소가 항상 메모리의 데이터 또는 코드를 참조하는 것은 아니다. 주소가 모니터와 같은 주변 장치 또는 소켓과 같은 가상 장치에 대한 입력 또는 출력 지점을 참조할 수 있는 체계가 있다. 이 경우 특정 주소에 배치된 정보는 실제로 해당 주소가 나타내는 하드웨어 장치로 전송된다. 이것은 프린터와 같은 장치에 액세스하는 데 매우 효율적인 방법일 수 있지만 프로그램을 디버깅할 때 심각한 취약점과 혼동을 일으킬 수 있다.[2]
모든 변수는 메모리에 값을 저장한다. const와 같은 고정값 변수 외의 모든 변수는 메모리 중에 램에 할당된다. 이러한 메모리의 공간을 구별하는 것이 메모리 주소 값이다. 주소로 각각의 위치를 구별한다. 포인터 변수 모두는 메모리의 주소를 지정하는 값을 가진다. 데이터가 존재하는 주소값을 사용하여 액세스한다. 즉, 어떤 번지의 메모리에 값을 쓰거나 또는 읽어 오는 방식이다. 정적변수 역시 메모리에 배치되고 결국은 주소값을 가질 것이다. 그러나 차이점은 기계어 코드에 주소값을 고정하여 액세스된다. 그러나 포인터 변수는 주소값을 가지고 액세스하기 때문에 임의의 위치를 바꿀 수도 있게 된다. 전역변수의 정적변수는 기계어 코드에 주소값을 고정하는 방식이 일반적이다. 지역변수는 스택 또는 중앙처리장치의 레지스터를 써서 위치값을 설정한다. 포인터 변수는 메모리에 주소값을 저장하는 방식이기 때문에 이 주소값을 읽어 실제 데이터를 액세스한다. 메모리 액세스 모드 중에 직접 주소방식으로 데이터를 액세스한다. 메모리의 주소는 중앙처리장치를 설계한 설계 기준에 따라 주소값의 길이와 방식이 결정된다. 일반적인 용도의 대부분의 중앙처리장치는 메모리를 지정하는 길이, 즉 비트 수는 동일하다. 램이나 롬(ROM) 혹은 플래시(flash) 메모리든 모든 주소는 같다. 마이크로프로세서(8051)는 오히려 많은 경우 메모리 영역을 나누어 다른 주소 체계를 사용한다. 8051은 내부의 256바이트 내에 변수를 할당한다. 256 바이트는 적기 때문에 많은 데이터를 처리하기 위해 변수는 잡을 수 없다. 많은 양의 데이터를 저장하기 위해 16비트의 저장 공간을 갖는 주소 체계를 같이 사용한다. 그리고 양쪽의 액세스는 기계어 코드를 분리해서 액세스한다. 이럴 경우는 주소값이 8비트 또는 16비트가 필요하다. C언어 컴파일러에서 이를 지정할 수 있는 방법을 제시한다. 포인터 변수의 유연성은 프로그램 작성의 유연성과 연관된다. C언어가 유닉스(Unix) 계열의 운영체제(OS)를 작성할 때 사용하였으므로 커널의 프로그램 소스를 보면 상당히 많은 부분 포인터 변수를 볼 수 있다. 유연성은 경우에 따라서 단점으로도 작용할 수 있다. 포인터 값에 따라 정의되지 않는 메모리 영역을 액세스할 수 있기 때문이다. 물론 정적변수도 스스로의 배열 등의 공간 밖을 액세스할 수 있지만, 포인터를 사용하면 이것이 좀 더 복잡해진다.[3]
배열[편집]
배열의 이름은 포인터이다. 단 그 값을 바꿀 수 없는 '상수 형태의 포인터'이다. 서식문자 %p 는 주소값의 출력에 사용되는 서식 문자이다. 포인터와 배열은 거의 비슷한 구조를 가지고 있지만 포인터 변수는 지시하는 주소값의 변경이 가능하지만 배열의 이름은 주소값 변경이 불가능하다는 점이다. 즉 포인터 변수와 배열의 이름의 차이점은 포인터는 변수, 배열은 상수라는 것이다.[4]
할당 및 해제[편집]
C언어에서 변수는 정적변수와 동적변수가 있다. 처리할 데이터의 숫자를 예측할 수 있다면 정적으로 선언하면 된다. 예를 들어 배열변수가 그렇다. 그러나 데이터 처리를 미리 예측할 수 없다면 최대의 데이터 처리량을 정하고 정적으로 선언할 수밖에 없다. 이 문제를 해결하기 위해 동적변수를 사용한다. C언어에서는 전통적으로 동적 할당 변수를 잡을 경우 malloc() 함수를 사용하여 데이터 저장 공간을 확보한다. 이와 관련된 함수는 다음과 같다.
- 메모리 할당 : malloc() 힙영역으로부터 데이터 공간을 할당받는다.
- 메모리 재할당 : realloc() 이미 할당된 메모리 공간의 크기를 조정한다.
- 메모리 해제 : free() 더 이상 쓸 필요가 없는 메모리 공간을 힙영역에 반환한다.
그리고 사용이 완료되면 free() 함수로 해제한다. 해제된 메모리 영역은 다시 재활용된다. 프로세서의 메모리 맵에서 힙(heap) 영역을 활용하여 동적 저장 공간을 확보한다. 각 프로세스마다 힙 영역은 크기가 정해져 있기 때문에 무한정 malloc() 함수를 사용하여 동적으로 잡을 수 없다. 따라서 개발자는 free() 함수로 해제해 주어야 한다. 만약 동적 할당을 하지 못하면 주소값이 NULL로 반환된다. C++에서는 new와 delete가 추가되었다. 이것은 내부에서 malloc() 함수 기능을 하는 기능을 수행한다. 따라서 표현법이 확장되었고, 힙영역을 활용하는 것은 같다.[3]
포인터 연산[편집]
포인터의 실제 형식은 시스템 종속 정수형이다. 즉, 32비트 플랫폼에서 포인터는 4바이트 정수이며, 64비트 플랫폼에서 포인터는 8바이트 정수형이다. 정수이기 때문에 n바이트 더해진 주소의 메모리를 참조한다.[3]
- 주소 연산자(&) : 변수의 이름 앞에 사용하여, 해당 변수의 주소값을 반환한다. & 기호는 앰퍼샌드(ampersand)라고 읽으며, 번지 연산자라고도 불린다.
- 참조 연산자(*) : 포인터의 이름이나 주소 앞에 사용하여, 포인터에 가리키는 주소에 저장된 값을 반환한다. C언어에서 * 기호는 사용하는 위치에 따라 다양한 용도로 사용된다. 이항 연산자로 사용하면 곱셈 연산으로 사용되며, 포인터의 선언 시나 메모리에 접근할 때도 사용된다.[1]
예제[편집]
포인터 스왑[편집]
#include <stdio.h> void swap(int a, int b); int main() { int a=5; int b=10; printf("값 교환 전 a: %d b: %d\n",a,b); swap(a,b); printf("값 교환 후 a: %d b: %d\n",a,b); } void swap(int a, int b) { int temp; temp=a; a=b; b=temp; }
위 코드는 a와 b의 숫자를 변경하는 swap 함수를 만들어서 호출하는 예제이다. 알고리즘에는 문제 없이 바뀔 것 같지만 결과를 보면 a와 b의 값이 바뀌지 않는 것을 볼 수 있다. 이유는 함수는 호출되고 그 변수가 메모리에 남아있지 않고 삭제되기 때문이다. 그렇기 때문에 정상적인 결과 값을 얻으려면 포인터를 사용해 메모리에 값을 넣어 줘야 한다. 그 코드는 다음과 같다.
- include <stdio.h>
void swap(int a, int b); int main() {
int a=5; int b=10; printf("값 교환 전 a: %d b: %d\n",a,b); swap(&a,&b); printf("값 교환 후 a: %d b: %d\n",a,b);
} void swap(int *a, int *b) {
int temp; temp=*a; *a=*b; *b=temp;
} 위 코드는 swap 함수에 매개변수를 포인터 변수로 선언해 주었다. 그러므로 메인에서 값을 넣게 되면 a와 b 변수의 주소를 넘겨 주었기 때문에 swap 함수에서는 포인터 변수 int *a 와 int *b로 그 주소들을 받고, swap 구문을 통해 각각 포인터 변수들이 주소에 있는 값들을 교환한다.[5]
동적 할당과 정적 할당[편집]
정적 할당[편집]
정적 메모리 할당은 메모리 할당 방법 중에 하나로, 메모리의 크기가 하드코딩되어 있기 때문에 프로그램이 실행될 때 이미 해당 메모리의 크기가 결정되는 것이 특징이다. 사용하는 운영체제에 따라 각 데이터형에 약속된 기억공간의 크기가 있고, 지정된 만큼 프로그램을 실행하는 프로세서에 필요한 자원으로 할당시켜 준다. 이것을 정적 할당이라고 한다. 즉, 프로그램 실행의 시작 부분에서 필요한 만큼 미리 기억 공간을 할당받고 시작하는 것을 의미한다.
동적 할당[편집]
동적 할당 또는 메모리 동적 할당은 컴퓨터 프로그래밍에서 실행 시간 동안 사용할 메모리 공간을 할당하는 것을 말한다. 사용이 끝나면 운영체제가 쓸 수 있도록 반납하고 다음에 요구가 오면 재할당을 받을 수 있다. 이것은 프로그램이 실행하는 순간 프로그램이 사용할 메모리 크기를 고려하여 메모리의 할당이 이루어지는 정적 메모리 할당과 대조적이다. 동적으로 할당된 메모리 공간은 프로그래머가 명시적으로 해제하거나 쓰레기 수집이 일어나기 전 까지 그대로 유지된다. C언어 및 C++와 같이 쓰레기 수집이 없는 언어의 경우, 동적 할당하면 사용자가 해제하기 전까지는 메모리 공간이 계속 유지된다. 동적 할당은 프로세스의 힙 영역에서 할당하므로 프로세스가 종료되면 운영체제에 메모리 리소스가 반납되므로 해제된다. 그러나 프로세스가 계속 실행될 때에는 동적 할당 된 영역은 유지되므로 프로그램이 정해진 힙 영역의 크기를 넘는 메모리 할당을 요구하면 할당되지 않는다. 따라서 사용이 완료된 영역은 반납하는 것이 유리한데, 프로그래머가 함수를 사용해서 해제해야 한다. 동적 할당은 함수가 종료되거나 변수 영역을 벗어나면 자동으로 공간 해제가 이루어지는 스택을 사용한 자동 변수와 대조적이다. 프로세스의 정적 메모리 할당은 프로세스가 시작할 때 이미 정해진 메모리량으로 한정되어 있기 때문에, 프로세스가 시작할 때부터 끝날 때까지 유지되는데 반해, 동적 할당은 프로세스의 실행 과정 중에 필요한 메모리를 운영체제에 요구해 할당받고 해제하는 것이 가능하다.[6]
각주[편집]
- ↑ 1.0 1.1 〈코딩의 시작, TCP School〉, 《TCP스쿨》
- ↑ 〈메모리 주소 란?〉, 《넷인백》
- ↑ 3.0 3.1 3.2 〈포인터 (프로그래밍)〉, 《위키백과》
- ↑ 〈C언어 포인터와 배열의 관계〉, 《티스토리》, 2019-03-19
- ↑ itng, 〈IT & G :: c언어 포인터를 이용한 스왑함수 구현〉, 《티스토리》
- ↑ hohyunera,〈hohyunera :: 동적할당과 정적할당〉, 《티스토리》, 2015-08-26
참고자료[편집]
- 〈포인터 (프로그래밍)〉, 《위키백과》
- itng, 〈IT & G :: c언어 포인터를 이용한 스왑함수 구현〉, 《티스토리》
- 〈C언어 포인터와 배열의 관계〉, 《티스토리》, 2019-03-19
- hohyunera,〈hohyunera :: 동적할당과 정적할당〉, 《티스토리》, 2015-08-26
- 〈코딩의 시작, TCP School〉, 《TCP스쿨》
- 〈메모리 주소란?〉, 《넷인백》
같이 보기[편집]