Notice
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
Today
Total
관리 메뉴

Study Note

[C/C++ Syntax] #012 포인터(Pointer) 본문

- 프로그래밍 언어(Programming Language)/C\C++

[C/C++ Syntax] #012 포인터(Pointer)

mymir 2021. 1. 16. 09:03

이번 글에선 C/C++의 하이라이트인 포인트의 기본 개념을 다룰 것입니다. 사실 포인터는 다른 언어들에서도 프로그래머가 직접 사용하지 않을 뿐 내부적으로 사용되곤 합니다. 그럼에도 C/C++에서 포인터가 강조되는 이유는 바로 직접 사용이 가능하기 때문인데요, 그 중요도만큼이나 단순한 개념 나열이 아닌 하나씩 의미를 짚어보며 학습하는 것을 권장합니다.

 

 

 

메모리란?


저장 공간 혹은 기억 장치라고도 불리는 메모리(Memory)는 데이터를 담아두는 물리적인(하드웨어) 공간을 의미합니다. 예를 들어 변수가 선언되면 해당 변수를 위한 메모리 상의 공간이 책정되어 그 공간에 값을 실제로 저장하게 되는 것입니다.

 

이처럼 메모리는 데이터를 기록하고, 기록한 데이터를 읽어내기도 합니다. 이를 위해서 필요한 것이 바로 주소(address)라는 개념입니다. 주소는 기록된 수 많은 데이터를 구분하고, 데이터를 읽기 위해 필요한 위치 표시를 할 수 있도록 16진수의 숫자로 나타내어지는 하나의 값입니다.

조금 자세하게 들어가면 공간을 바이트 단위로 나눈 각 위치에 주소 값을 부여하며, 주소는 컴파일러에 따라 8자리 혹은 16자리의 16진수로 표기합니다.

 

간단한 예로 32바이트의 공간이 있다고 합시다. 이를 바이트 단위로 나누면 32개의 공간으로 구분되고, 따라서 32개의 서로 다른 주소 값이 필요할 것입니다. 따라서 주소를 나타내기 위해선 최소 5비트 길이 이상이 필요하게 되겠지요.

 

이로써 우리는 변수가 이름과 값뿐만이 아니라 그 주소도 포함하고 있다는 것을 알았습니다. C/C++에선 다음과 같이 주소값을 참조 연산자 & (주소 연산자라고도 불립니다.)를 통해 실현시킬 수 있습니다.

#include <iostream>

using namespace std;

int main() {
	int a = 1;

	cout << "a = " << a << endl;
	cout << "&a = " << &a << endl;
}

참고로 이런 변수의 주소는 컴파일러에 의해 할당되는 값으로, 프로그래머에 의해 임의로 변경시킬 수 없습니다.

 

 

 

포인터란?


포인터(pointer)는 주소값을 담는 일종의 변수입니다. 정수형 변수에는 정수가, 문자형 변수에는 문자가 담기듯이 포인터에는 16진수 8자(혹은 16자)의 주소 자료가 담기는 것입니다. (주소와 포인터를 명확히 구분하시길 바랍니다.)

 

포인터의 선언 규칙은 다음과 같습니다.

자료형과 이름을 적는 것은 이전에 배운 기본 변수와 동일하지만 그 중간에 별 문자 *가 추가되어 구분됩니다. 예를 들어 int* p; 라고 작성하면 'int형 자료의 주소를 담는 포인터 p'라는 의미를 갖습니다. *의 위치는 자료형과 포인터 사이에만 있으면 되며, 띄어쓰기는 상관 없습니다. 개인적으로는 의미를 살리기 위해 자료형쪽에 *을 붙여 묶은 단위로 사용하는 편입니다.

(다만 여러 포인터를 한번에 선언하는 경우 각 포인터 변수 이름 앞에 *를 붙여주어야 합니다.)

 

이렇게 선언된 포인터에는 16진수의 수를 직접 입력하여 값을 담기 보다는, 위에서 배운 참조 연산자와 대입연산자 이용하여 다음과 같이 지정된 변수의 주소를 담는 것이 기본 형태입니다. 이때, 포인터에서 선언하는 자료형과 담기는 주소의 자료형이 일치해야합니다.

#include <iostream>

using namespace std;

int main(){
    int i = 1;
    int* pi = &i;
    
    cout << pi << endl;
 }

이렇게 pi를 출력해보면 i의 주소를 출력하는 것을 확인할 수 있습니다.

참고로 지역변수로 선언된 포인터는 초기화를 하지 않으면 쓰레기 값을 갖고 있습니다. 아직 와닿기는 힘들겠으나, 포인터는 그 값을 정확히 알고 사용하지 않으면 컴퓨터 자체에 굉장히 치명적일 수 있기에 선언과 동시에 초기화를 해서 사용하는 것이 바람직합니다. (선언 시 초기화할 특정 값이 없으면 NULL로 초기화를 하도록 합니다.)

 

 

또한 포인터도 일종의 변수이므로 다음과 같이 포인터 자체의 주소도 갖고 있습니다.

#include <iostream>

using namespace std;

int main() {
	int i = 1;
	int* pi = &i;

	cout << "  i = " << i << endl;
	cout << " &i = " << &i << endl;
	cout << " pi = " << pi << endl;
	cout << "&pi = " << &pi << endl;
}

이를 도식화하여 비교해보도록 합니다.

 

 

간접 참조 연산자 *


간접 참조(indirection) 혹은 역참조(dereferencing)라 불리는 연산자 *는 주소의 저장공간을 의미합니다. 하나의 주소를 피연산자로 하는 단항 연산자로, 이항 연산자 *(곱셈)과 선언 시 사용되었던 *과 모두 분명하게 구분됩니다. 다음의 예를 살펴봅시다.

#include <iostream>

using namespace std;

int main() {
	int i = 10;
	cout << *(&i) << endl;
}

i의 주소를 나타내는 &i에 간접 참조 연산자 *를 붙여 출력하였더니 i의 값인 10을 출력하는 것을 확인할 수 있습니다. 즉, i의 주소의 저장공간에 담긴 값을 출력하는 것이죠.

 

 

주소의 저장공간이란 말은 변수와 굉장히 유사합니다. 단지 변수는 변수명이라는 요소가 하나 더 있을 뿐입니다. 지금은 *(기존 사용되던 주소)의 형식이므로 복사의 의미로 사용하지만, 이후 동적할당의 개념을 배우면 *(새로 만드는 주소)의 형식으로 일종의 이름 없는 변수로 사용될 수 있습니다.

 

일단 다음의 복사의 의미를 살린 예시를 살펴보겠습니다.

#include <iostream>

using namespace std;

int main() {
	int i = 1;
	int* pi = &i;

	cout << *pi << endl;
	
	(*pi)++;
	cout << i << endl;
	
	i++;
	cout << *pi << endl;
}

먼저 첫 줄의 출력은 i가 복사된 상태인 *pi의 값을 확인할 수 있습니다.

다음 두 번째 출력은 *pi의 값을 증가시켰더니 i의 값도 증가된 것을 확인할 수 있고, 세 번째 출력은 그 반대의 형태도 가능하다는 것을 확인할 수 있습니다.

즉, 포인터(pi)에 특정 변수(i)의 주소를 담으면 *(pi)는 i 그 자체가 된다는 것입니다. *(pi)를 i의 다른 이름이라고 의미할 수 있습니다.

 

 

 

포인터를 사용하는 이유


포인터는 메모리를 직접 사용할 수 있기에 많은 위험 요소가 있고, 코드의 가독성을 떨어트리기도 합니다. 그럼에도 포인터를 사용하는 것은 그만큼 많은 장점이 있기 때문입니다. 이후 차근차근 배워나갈 개념들이지만 간략하게 소개하고 넘어가겠습니다.

 

1. 포인터를 연산하여 많은 양의 데이터를 쉽게 다룰 수 있습니다.

당장 다음 글에서 배울 배열부터 나타나는 장점인데요, 예를 들어 두 개의 데이터를 위해 변수 a, b를 생성했다고 가정하면 두 변수의 값을 변경하려면 각각 수정해야하는 불편함이 있었습니다. 하지만, 포인터의 연산을 이용하면 반복문을 통해 한 번에 변경할 수 있을 것입니다.

 

2. 함수 호출 시 함수 내부에서 함수 외부의 값들을 변경할 수 있습니다.

바로 이전 글에서도 언급했던 내용입니다. 함수는 한 개의 반환만을 할 수 있기 때문에 함수로 두 개 이상의 외부 값을 변경하는데에 어려움이 있었습니다. 하지만 포인터를 사용하면 주소를 전달할 수 있어 참조를 통해 함수 외부의 값들을 쉽게 변경할 수 있습니다.

 

3. 동적 메모리를 사용하여 프로그래머 임의로 데이터의 생존, 다량 연산 등을 다룰 수 있습니다.

1번의 내용과 교집합이 있는 부분입니다. 위에서 언급했다시피 기존 변수는 이름으로 사용하고, 주소를 알 수 있다고 하여도 컴파일러에 의해 임의로 부여되는 값이고 통제가 되기도 합니다. 하지만 동적 메모리를 사용하면 연산 가능한 주소를 프로그래머 임의로 생성하고 소멸시키며 자유롭게 사용할 수 있습니다.

 

이외에도 배열이나 연결리스트 등의 구조화된 자료를 쉽게 만들고 다룰 수 있는 등 여러 장점이 있습니다.

 

Comments