[C++] 포인터 변수

2024. 2. 28. 01:13C++

프로그래머를 꿈꾸는 사람들에게 가장 많이 좌절을 준다는 고난 중에 하나, 포인터 변수

 

너무나도 심오하여 어떻게 사용해야하는지 감도 잡히지 않는다.

개인적으로 공부하는 지금도 완벽하게 숙달되지 않았다는 것을 느낀다.

 

그러나 포기하면 앞으로 나아가지 못하는 법, 따라서 지금부터 포인터 변수를 공부하며 같이 정리해 보도록 하겠다.

 

인수 전달 방식


포인터 변수를 쉽게 이해하기 위해서는 인수를 전달하는 방식에 대하여 아는 것이 우선이다.

이는 메모리 접근 방식과도 관련이 있다.

 

이번 글에서는 주로 사용되는 2가지의 인수 전달 방식을 정리하겠다.

1. 값에 의한 전달 (Call by Value)
변수가 가지고 있는 값을 매개변수에게 복사하여 전달하는 방식
호출된 함수에서 매개변수 값을 변경하여도 원본 데이터에게 영향을 미치지 않는다.
인수를 전달할 때 별도의 복사본이 생성되기 때문에 메모리 상에서 독립적인 작업이 이루어진다.
2. 참조에 의한 전달 (Call by Reference)  -> 변수에 대한 별칭을 만들어낸다.
인수로 받는 변수의 메모리 주소값을 공유받는 방식
매개변수의 값을 변경하면 원본 데이터의 값도 변화한다.
메모리 주소를 공유하기 떄문에 공간을 아낄 수 있어 효율적으로 작업이 가능하다.

 

이들 중 오늘 우리가 다루고자하는 포인터 변수는 2번인 참조에 의한 전달에 속한다.

 

포인터 변수


쉽게 이야기하면 주소를 저장하는 변수이다.

 

일반적인 변수는 데이터를 저장하는 반면, 포인터는 메모리 공간의 주소(데이터가 할당 받은 주소)를 저장한다.

 

int a = 10;

예를 들어 10이라는 값을 가진 a라는 변수가 있다.

int num = a;

일반적인 변수는 a를 대입받으면 10이라는 값을 대입받는다.

int* num;
num = &a;
std::cout << num << std::endl;

그러나 (*)포인터 변수는 a의 값이 아닌 a의 주소값을 대입받는 것을 볼 수 있다.

& 연산자를 변수의 앞에 두면 해당 변수의 주소값이 반환된다.

& 연산자를 씌우지 않으면 컴파일러 불일치 오류가 발생한다.

 

출력 결과는 아래와 같다.

 

그러면 포인터 변수가 a의 값을 나타내도록 만들기 위해서는 어떻게 해야 하는가?

간단하게 출력하려는 부분에 *num을 대입하면 된다.

int* num;
num = &a;
std::cout << *num << std::endl;

 

그러면 아래와 같이 출력된다.

 

 

이것만 보면 포인터 변수가 쉽다고 생각할 수 있다.

그러나 개인적으로 생각하기에 포인터 변수의 무서움은 배열과 구조체, 함수 등을 만났을 때 진면목을 볼 수 있다.

 

포인터 변수와 배열의 조합


c++ 에서 매우 흔하게 사용되는 패턴 중 하나로 포인터 변수를 사용하여 배열에 접근하고 조작하는 방식이다.

어떤 부분에서는 서로를 대체할 수도 있을 정도로 매우 긴밀한 관계를 가지고 있다.

 

배열은 메모리 상에서 연속되는 메모리 공간을 가지고 있다는 특성을 가지고 있는데,

이때문에 배열의 이름은 해당 배열의 첫번째 요소를 가리키는 포인터로 볼 수 있다.

다른 의미로 포인터 변수는 1차원 배열이라고도 생각할 수 있다.

 

이때문에 배열을 포인터 변수로 대입받을 때는 다른 변수들과 달리 & 연산자를 삽입하지 않는 것을 볼 수 있다.

int arr[3] = {1, 2, 3};

// 배열 이름은 첫 번째 요소를 가리키는 포인터로 해석됨
int* ptr = arr;

std::cout << "arr[0]: " << arr[0] << std::endl;
std::cout << "*ptr: " << *ptr << std::endl;

 

그러나 우리는 배열을 첫번째 요소만 사용하는 것이 아니다.

따라서 포인터 변수로 배열의 요소를 자유롭게 사용하는 방법을 익혀야한다.

 

어떻게 해야 원하는 요소를 선택할 수 있을까?

 

포인터 변수에 정수를 더하거나 뺌으로써 생각보다 간단하게 해결할 수 있다고 이야기하는 사람들도 존재한다.

int arr[3] = {1, 2, 3};
int* ptr = arr;
std::cout << "arr[2]: " << *ptr+1 << std::endl;

그런 경우 입력된 메모리의 주소만큼 이동하게 되어 틀린 것은 아니다.

 

원리는 조금전에도 이야기한 것처럼 연속되는 메모리 공간을 가지고 있는 배열의 특징을 이요한 것으로,

포인터 증가 또는 감소를 통해 원하는 요소의 주소로 이동하는 방식이다.

 

그러나 개인적으로 추천하는 방식은 포인터 배열을 사용하는 것이다.

포인터 배열
포인터들의 집합으로 구성되어 있는 배열.
다양한 데이터 유형을 저장하고 관리할 때 사용된다.

선언 방식
int* num[3] = 0;
//[데이터 타입] [포인터 변수명] [배열의 길이]

* 포인터 연산자는 데이터 타입과 변수명 사이에 존재하면 어느 위치에 있든지 상관없다.

 

그러면 ㅇ2차원의 배열은 어떻게 포인터 변수에 대입시킬 수 있을까?

이번에도 포인터 배열을 사용하려는 사람도 있지만 개인적으로 추천하는 방식은 배열 포인터를 사용하는 것이다.

배열 포인터
배열 전체를 을 가리킬 수 있는 포인터.
주로 2차원 이상의 배열을 가리킬 때 사용된다.

선언 방식
int (*num)[3] = 0;
//[데이터 타입] [괄호 안에 변수명과 포인터] [배열의 크기]

여기서 중요한 것은 변수명과 포인터를 괄호() 안에 넣는 것이다.

 

아래의 코드는 2차원 배열을 포인터 변수로 대입받는 것을 보여주는 예시문이다.

 int arr[2][3];

 for (int i = 0; i < 2; i++)
 {
     for (int k = 0; k < 3; k++) {
         arr[i][k] = k + 1;
     }
 }

//배열 포인트의 대괄호[] 안에는 배열의 열 길이를 표시한다.
 int (*ptr)[3] = arr;

 std::cout << "arr[1][2]: " << arr[0][1] << std::endl;
 std::cout << "arr[1][2]: " << ptr[0][1] << std::endl;

 

실행시켜보면 배열과 똑같이 index의 값을 넣음으로 원하는 요소의 값을 출력할 수 있다는 사실을 알 수 있다.

출력 결과는 아래와 같다.

 

 

포인터 변수와 함수


포인터는 함수에서도 많이 사용된다.

함수에 포인터를 전달하여 원본 데이터(변수)의 값을 변경하거나 다양한 함수를 호출할 수 있도록 만들어 준다.

 

해당 내용은 글이나 말로 설명하는 것보다 직접 예문을 보거나 코드를 작성해보는 것이 이해하기 쉽다.

따라서 간단한 예문을 보여주는 것으로 넘기겠다.

#include <iostream>

void Func(int *a, int *b)
{
	//num1과 num2의 주소값이 가리키는 값인 100, 200을 가지고 처리한다
	int temp = *a;
	*a = *b;
	*b = *a;
}

int main() {
	int num1 = 100;
	int num2 = 200;

	//num1과 num2의 주소값을 넣는다.
	Func(&num1, &num2);

	std::cout << num1 << " " << num2 << std::endl;
}

 

추가 설명

함수인 Func에 대입되는 값을 보면 배열이 아니라서 변수 앞에 & 연산자가 붙어있는 것을 볼 수 있다.

Func에 들어가는 인수 값은 num1과 num2의 주소값이다.

 

포인터 변수와 구조체


구조체는 여러개의 변수를 묶어서 하나의 데이터 단위로 정의하는 것을 의미한다.

 

포인터는 이러한 구조체의 멤버로 사용되어

구조체의 동적 메모리 할당, 함수 인수 전달, 구조체 배열 등 다양한 상황에서 유용하게 활용된다.

 

간단한 예시

struct Point {
	int x;
	int y;
};

Point* p = new Point;
p->x = 10;
p->y = 20;

std::cout << p->x<<std::endl;
std::cout << p->y << std::endl;

 

출력 결과

'C++' 카테고리의 다른 글

[C++/STL] std::map  (0) 2024.03.03
2024_02_22 [C++] 반복문  (0) 2024.02.22
2024_02_21 [C++] 조건문 / 분기문  (0) 2024.02.21
2024_02_20 [C++] 연산자  (0) 2024.02.20
2024_02_20 [C++] 이름  (0) 2024.02.20