본문 바로가기

Language/C++

[C++] 포인터

728x90
반응형

포인터를 이해하기에 앞서

모든 데이터들은 메모리 상에 특정한 공간에 저장되어 있다. 편의상 메모리의 특정한 공간을 '방'이라고 하겠다. 각 방에 데이터들이 들어가게 되는 것이다. 한 방의 크기는 보통 1바이트라고 정의된다. 만약 4바이트짜리 int형 변수를 정의한다면 메모리 상의 4칸을 차지하게 된다.

 

프로그램 작동 시 컴퓨터는 여러 방들에 있는 데이터들을 필요로 한다. 따라서, 어떤 방에서 데이터를 가져올지 구분하기 위해 각 방에 고유의 주소를 붙여주었다.

 

예를 들어 아래와 같은 int 변수 a를 정의하였다면 특정한 방에 아래 그림처럼 변수 a가 정의된다.

int a = 123;

0x152839는 이 방의 시작 주소이다. 16진수로 152839라는 위치에서부터 4바이트 공간을 차지하며 123이라는 값이 저장되어 있게 하라는 뜻이다.

 

포인터

포인터: 메모리 상에 위치한 특정한 데이터의 (시작)주소값을 보관하는 변수

포인터에도 형이 있다. int형 데이터의 주소값을 저장하는 포인터와, char형 데이터의 주소값을 저장하는 포인터가 서로 다르다는 말이다. 포인터에는 시작 주소만 들어가 있기 때문에 *p라고 했을 때 메모리에서 얼마만큼을 읽어들어야 할지 알 길이 없다. int *p라고 하면 시작 주소로부터 정확히 4바이트를 읽어들여 값을 바꾸게 된다.

 

포인터가 int 데이터를 가리키고 싶다면

int* p; //라고 하거나
int *p; //로 하면 된다

 

포인터도 변수이기 때문에, 포인터에 들어간 주소값이 바뀔 수 있다. 처음에는 변수 a를 가리켰다가, 나중에 변수 b를 가리킬 수 있다.

 

주소 연산자 ( & )

변수 이름 앞에 사용하여 변수에 할당된 메모리 주소 값을 반환한다.

실행할 때마다 결과가 달라진다.

 

역참조 연산자 ( * )

포인터의 이름이나 주소 앞에 사용하며 해당 주소를 참조하여 주소에 저장되어 있는 값을 반환한다.

위 코드에서 포인터 p에 a의 주소를 집어 넣고 a에 2를 대입하였다. *p는 통해 p에 저장된 주소(변수 a의 주소)에 해당하는 데이터, 즉 변수 a 그 자체를 의미한다.

 

상수 포인터

const int*의 의미는 const int형 변수를 가리킨다는 것이 아니다. int형 변수를 가리키는데 그 값을 절대로 바꾸지 말라는 의미이다. pa는 int형 변수를 가리키고 있는데, const가 붙었으므로 pa가 가리키는 변수의 값은 절대 바뀌면 안된다.

const 키워드가 int* 앞에 있는 것이 아니라 int*와 pa 사이에 놓이고 있다. pa 값이 바뀌면 안된다는 의미이다. 즉, pa가 처음에 가리키는 것(a) 말고 다른 것은 절대로 가리킬 수 없다는 것이다.

 

포인터의 덧셈

pa의 경우 4가 더해졌고, pb의 경우 2가 더해졌고, pc의 경우 8이 더해졌다. 포인터가 가리키는 형의 크기만큼 더해졌다.

뺄셈도 덧셈과 유사한 결과가 나타난다. int형이므로 4가 빠졌다.

컴파일 오류가 발생한다. C++에서 두 포인터끼리의 덧셈을 허용하지 않는다. 두 변수의 메모리 주소를 더해서 나오는 값은 이전의 포인터들이 가리키던 두 개의 변수와 아무 관련이 없는 메모리 속 임의의 지점이기 때문에 의미가 없고 필요하지도 않다.

 

배열과 포인터

배열들의 각 원소는 메모리 상에 연속되게 놓인다.

int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

포인터로도 배열 원소에 쉽게 접근할 수 있다. 배열의 시작 부분을 가리키는 포인터를 정의한 뒤에 포인터에 1을 더하면 그 다음 원소를 가리킨다. 위와 같은 일이 가능한 이유는 포인터가 자신이 가리키는 데이터의 '형'의 크기를 곱한 만큼 덧셈을 수행하기 때문이다. parr + 1 을 할 때 p의 주소값에 사실은 1*4가 더해지고, parr + 3을 하면 p의 주소값에 3*4인 12가 더해진다.

*를 이용하여 원소들과 똑같을 역할을 할 수 있게 된다. C++에서 []라는 연산자가 쓰이면 자동으로 형태를 바꾸어 처리한다. arr[3]이라 사용한 것은 컴파일러가 *(parr + 3)으로 바꿔서 처리한다는 뜻이다.

 

배열은 배열이고 포인터는 포인터이다.

arr과 arr[0]의 주소값이 동일하다. 따라서 배열 이름은 배열의 첫 번째 원소의 주소값을 나타내고 있다는 사실을 알 수 있다. 그러나 배열 이름은 배열의 첫 번째 원소를 가리키는 포인터가 아니다.

sizeof 는 크기를 알려주는 연산자이다. arr에 sizeof 연산자를 사용하였을 경우 배열의 실제 크기가 나온다. arr 배열에는 int 원소 6개가 있으므로 크기는 24가 된다. 반면 parr에 sizeof 연산자를 사용하였을 경우 배열 자체의 크기가 아니라 포인터의 크기를 알려 준다. 배열의 이름과, 첫 번째 원소의 주소값은 엄밀히 다른 것이다.

 

그렇다면 왜 두 값을 출력 했을 때 같은 값이 나왔을까? 그 이유는 배열의 이름이 sizeof 연산자나 주소값 연산자(&)와 사용될 때를 빼면, 배열의 이름을 사용 시 암묵적으로 첫 번째 원소를 가리키는 포인터로 타입 변환되기 때문이다. 

 

1차원 배열 가리키기

arr은 배열의 첫 번째 원소를 가리키는 포인터로 변환되고, 그 원소의 타입이 int이므로, 포인터의 타입은 int*가 된다. 따라서 parr을 통해서 arr을 이용했을 때와 동일하게 배열의 원소에 마음껏 접근할 수 있는 것이 된다.

 

포인터의 포인터

같은 행에 있는 값들이 모두 같다. (세번째 줄)ppa에는 pa의 주소값이 들어간다. 따라서 &pa와 ppa는 같은 값이다. (두번째 줄)pa가 a를 가리키고 있으므로 pa에는 a의 주소값이 들어간다. 따라서 &a와 pa는 같은 값이고, ppa가 pa를 가리키고 있으므로 *ppa를 하면 pa를 지칭하는 것이 된다. (첫번째 줄)pa가 a를 가리키고 있으므로 *pa를 하면 a를 지칭하는 것이 되어 a의 값이 출력된다. **ppa를 다시 써보면 *(*ppa)가 되는데 *ppa는 pa를 지칭하는 것이기 때문에 *pa가 되서 a를 지칭하는 것이 된다. 

 

배열 이름의 주소값?

&arr에서 주소값 연산자가 왔으므로 암묵적 변환이 이루어지지 않는다.  

 

arr이 크기가 3인 배열이기 때문에, &arr을 보관할 데이터는 크기가 3인 배열을 가리키는 포인터가 되어야 한다. parr을 정의할 때 *parr을 꼭 ()로 감싸야 하는데, 만약 괄호를 빼버린다면 int *parr[3]와 같이 되어서 컴파일러가 int* 원소 3개를 가지는 배열을 정의한 것으로 오해하게 된다. parr은 크기가 3인 배열을 가리키는 포인터이기 때문에 배열을 직접 나타내기 위해서는 * 연산자를 통해 원래의 arr을 참조해야 한다. 따라서 (*parr)[1]과 arr[1]은 같은 문장이 된다.

 

2차원 배열의 [] 연산자

int a[2][3];

2차원 배열은 1차원 배열이 여러 개 있다고 생각하면 된다. 위 경우 int a[3]짜리 배열 2개가 메모리에 연속적으로 존재한다.

 

메모리에 2차원으로 존재하는 것은 아니고, 컴퓨터 메모리 구조는 1차원이기 때문에 항상 선형으로 퍼져있다.

arr[0]의 값이 arr[0][0]의 주소값과 같고 arr[1]의 값이 arr[1][0]의 주소값과 같다. 이를 통해 알 수 있는 사실은 1차원 배열과 마찬가지로 sizeof나 주소값 연산자로 사용되지 않을 경우, arr[0]은 arr[0][0]을 가리키는 포인터로 암묵적 타입 변환되고, arr[1]은 arr[1][0]을 가리키는 포인터로 타입 변환된다는 뜻이다.

 

따라서 sizeof를 사용하였을 경우 2차원 배열의 열의 개수를 계산할 수 있다.

전체 배열에 sizeof 를 할 경우 당연하게 배열의 전체 크기가 나온다. sizeof(arr[0])를 하면 0번째 행의 길이(총 열의 개수)가 나온다. sizeof 연산자의 경우 포인터로 타입 변환을 시키지 않기 때문에 sizeof(arr[0])는 sizeof에 1차원 배열을 전달한 것과 같다. sizeof(arr[0][0])을 통해 int 크기인 4를 리턴하여 총 열의 개수를 알 수 있게 된다. 그리고 총 행의 기수는 전체 크기를 열의 크기로 나눈 것이 된다.

 

그렇다면 2차원 배열의 이름을 포인터에 전달하기 위한 해당 포인터의 타입은 무엇일까? arr[0]은 int*가 보관할 수 있으니까, arr은 int**가 보관할 수 있을까? NO

 

포인터의 형(type)을 결정짓는 두 가지 요소

int arr[10] 라는 배열에서 x번째 원소의 주소값을 알아내는 방법을 생각해보자. 이 배열의 시작주소를 arr이라 한다면 arr[x]의 주소값은 arr + 4x 와 같이 나타낼 수 있다. 

 

int arr[a][b]라고 정의된 2차원 배열의 경우 arr[x][y]라는 원소를 참조할 때 이 원소의 주소값은 어떻게 알 수 있을까? int arr[a][b]는 int arr[b] 짜리 배열이 메모리에 a개 존재하는 것이므로, arr[x][0]의 주소값은 x번째 int arr[b] 짜리 배열이 된다. 그렇다면 arr[x][0]의 주소값은 arr + 4bx가 된다. arr[b] 배열의 크기는 4b 이니까 x번째 배열의 시작 주소는 4bx가 되기 때문이다. 따라서 arr[x][y]의 시작 주소값은 arr + 4bx + 4y

 

따라서 2차원 배열을 가리키는 포인터를 통해 원소들에 정확히 접근하기 위해서는 가리키는 원소의 크기, b의 값 두 정보가 포인터의 타입에 명시되어 있어야 컴파일러가 원소에 올바르게 접근할 수 있다.

 

2차원 배열을 가리키는 포인터는 아래와 같이 써주면 된다.

/* (배열의 형) */ (*/* (포인터 이름) */)[/*2차원 배열의 열 개수*/];
// 예를 들어서 
int (*parr)[3];

위와 같이 정의한 포인터 parr을 해석해 보면, int형 이차원 배열을 가리키는데 배열 한 행의 길이가 3이라는 사실을 알 수 있다. parr은 사실 크기가 3인 배열을 가리키는 포인터를 의미한다. 1차원 배열에서 배열의 이름이 첫 번째 원소를 가리키는 포인터로 타입 변환이 되는 것처럼, 2차원 배열에서 배열의 이름이 첫 번째 행을 가리키는 포인터로 타입 변환 되어야 한다. 그 첫 번째 행은 크기가 3인 1차원 배열이다!

 

포인터 배열

위에서 설명한 포인터는 배열을 가리키는 포인터고, 포인터 배열은 포인터들의 배열이다. 두 용어가 헷갈리는데, 진짜는 뒷부분이라고 생각하면 된다. 즉, 포인터 배열은 정말로 배열이고, 배열 포인터는 정말로 포인터이다.

배열의 형을 int*로 했다. 다시 말해, 배열의 각 원소는 int를 가리키는 포인터형으로 선언된 것이다. 따라서 배열에 각 원소의 주소값이 들어간다. 각각의 원소는 int형 변수 a, b, c를 가리키게 된다.

728x90
반응형

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

[C++] OOP - 생성자와 소멸자  (0) 2021.07.09
[C++] 공용체, 열거체  (0) 2021.07.03
[C++] 구조체  (0) 2021.07.03
[C++] 동적 할당  (0) 2021.07.01
[C++] 참조자(레퍼런스)  (0) 2021.06.30