성공할 게임개발자

[C] 3. 포인터 본문

C

[C] 3. 포인터

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

 

단항연산자 &

단항연산자 &는 피연산자의 주소값을 불러옴

사용방법

&/* 주소값을 계산할 데이터 */

 

using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    int* p;
    int a;

    p = &a;

    cout << p << endl;
    cout << &a << endl;


    return 0;
}

예를 들면 이 코드의 결과 값은 

 

 

두 값이 같음에 유의!

한 번 정의된 변수의 주소값은 바뀌지 않음

 

 

연산자 *

포인터는 특정한 데이터의 주소값을 보관한다. 이 때 포인터는 주소값을 보관하는 데이터형에 *를 붙임으로써 정의되고, &연산자로 특정한 데이터의 메모리 상의 주소값을 알아올 수 있다.

 

& 연산자가 어떠한 데이터의 주소값을 얻어내는 연산자라면 거꾸로 주소값에서 해당 주소값에 대응되는 데이터를 가져오는 연산자가 *

 

* 연산자의 역할을 쉽게 풀이하자면

"나(포인터)를 나에게 저장된 주소값에 위치한 데이터로 생각해줘!"
using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    int* p;
    int a = 3;

    p = &a;

    cout << *p << endl;
    cout << a << endl;

    return 0;
}

이 코드의 결과 값의 경우 

 

3으로 똑같이 나옴. 왜냐면 &a 를 p에 넣어 주소값을 가지게 만들고 *로 &a의 값을 가진 주소값에 위차한 데이터로 생각해 달라고 했기 때문

 

 

포인터 p에 어떤 변수 a의 주소값이 저장되어 있다면 포인터 p는 변수 a를 가리킨다 라고 말함. 

포인터도 엄연한 변수이기 때문에 특정한 메모리 공간을 차지함, 따라서 포인터도 자기자신만의 주소를 가지고 있음

 

 

상수 포인터

절대로 바뀌지 않을 거 같은 값에는 무조건 const 키워드를 붙여주는 습관 기르기

/* 상수 포인터? */
#include <stdio.h>
int main() {
  int a;
  int b;
  const int* pa = &a;

  *pa = 3;  // 올바르지 않은 문장
  pa = &b;  // 올바른 문장
  return 0;
}

 

위의 예제에서 두 문장의 차이는 무엇일까?

우선 const 라는 키워드는 이 데이터의 값은 절대로 바뀌면 안된다 라고 알려주는 키워드

const int a 라는 변수는 그냥 int 형 변수 a 인데 값이 절대로 바뀌면 안되는 변수

const int* 의 의미는 const int 형 변수를 가리키는 것이 아닌. int 형 변수를 가리키는데 그 값을 절대로 바꾸지 말라 라는 뜻

 

pa 를 통해서 a 를 간접적으로 가리킬 때 에는 컴퓨터가 내가 const 인 변수를 가리키고 있구나 로 생각하기 때문에 값을 바꿀 수 없음

 

하지만 아래 예제의 경우

using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    int a;
    int b;

    const int* pa = &a;

    a = 4;
    cout << *pa << endl;
    cout << a << endl;

    a = 5;
    cout << *pa << endl;
    cout << a << endl;

    return 0;
}

a 는 const 가 아니기 때문에.. 값을 수정할 수 있음

 

정리하면 

const int* pa;   // "pa가 가리키는 값"이 변경 불가 (값은 const)
int* const pa;   // "pa 포인터 자체"가 변경 불가 (주소 변경 불가)
const int* const pa; // "값도 변경 불가 + 포인터도 변경 불가" (완전한 상수 포인터)

 

 

 

포인터의 덧셈

using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    int a;
    int* pa;
    pa = &a;

    cout << "pa 값 : " << pa << endl;
    cout << "(pa+1)의 값 : " << pa + 1 << endl;

    return 0;
}

 

+1 했는데도 4차이가 난다

컴퓨터는 포인터의 자료형 크기를 기반으로 주소연산을 수행함

pa 가 int* 타입이기 때문에 4바이트 증가

pa + 1 은 실제로 pa + 1 * sizeof(int) 와 같음

 

 

배열과 포인터

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

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

이라는 배열 정의 시 메모리 상에서 다음과 같이 나타남

 

 

arr 은 int 형 이기 때문에 4바이트를 차지함.

위와 같은 일이 가능한 이유는 포인터는 자신이 가리키는 데이터의 형(int) 크기를 곱한 만큼 덧셈을 수행하기 때문

예를 들어 p 라는 포인터가 int a 를 가리킨다면 ( int* p = &a ) p + 1 을 할 때 p 의 주소 값에 1 * 4가 더해짐 위의 설명 대로

"pa + 1 은 실제로 pa + 1 * sizeof(int) 와 같음을 인용.

만약 p + 3 을 하면 p + 3 * sizeof(int) 으로 p 의 주소값에 12 가 더해지게 됨

 

 

 

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

using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    int arr[6] = { 1, 2, 3, 4, 5, 6 };
    int* parr = arr;


    cout << "sizeof(arr) 값 : " << sizeof(arr) << endl;
    cout << "sizeof(parr)의 값 : " << sizeof(parr) << endl;

    return 0;
}

arr은 배열 전체의 사이즈를 출력하고 parr은 포인터의 크기를 알려준다

 

따라서 배열의 이름과 첫 번째 원소의 주소값은 엄밀히 다른 것

 

그 이유는 C언어 상에서 배열의 이름을 사용시 암묵적으로 첫 번째 원소를 가리키는 포인터로 타입 변환 되기 때문

 

parr = &arr[0] 으로 암묵적으로 변환 됨

 

 

[ ] 연산자의 역할

using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

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

    cout << "a[3] : " << arr[3] << endl;
    cout << "*(a+3) : " << *(arr + 3) << endl;

    return 0;
}

 

[ ] 라는 연산자는 위의 경우 처럼 형태를 바꾸어서 처리하게 됨. 우리가 arr[3] 이라 사용한 것은 사실 *(arr + 3) 으로 바꾸어서 처리 

 

 

 

포인터의 포인터

int **p;

위는 int 를 가리키는 포인터를 가리키는 포인터

 

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    int a;
    int* pa;
    int** ppa;

    pa = &a;
    ppa = &pa;

    a = 3;

    cout << " a = " << a << " *pa = " << *pa << " **ppa = " << **ppa << endl;
    cout << " &a = " << &a << " pa = " << pa << " *ppa = " << *ppa << endl;
    cout << " &pa = " << &pa << " ppa = " << ppa << endl;

    return 0;
}

 

같은 행의 있는 값들은 모두 같은 값을 가짐

 

 

ppa 라는 변수에는 pa의 주소값이 들어가 있고

pa라는 변수에는 a의 주소값이 들어가 있음

 

위에서 말했듯이 연산자* 의 역할은

"나(포인터)를 나에게 저장된 주소값에 위치한 데이터로 생각해줘!"

 

*ppa를 할시

나(*ppa)를 나(ppa)에게 저장된 주소값(pa의 주소 (0x1234568E))에 위치한 데이터(0x1234569C)로 생각해줘! 가 된다

 pa 에 저장된 것a의 주소 값 이므로 *ppa는 a의 주소값을 나타낸다.

 

 **ppa 를 하면 

나(**ppa)를 나(*ppa)에게 저장된 주소값(a의 주소)에 위치한 데이터(3)로 생각해줘!

**ppa 은 3을 나타낸다

 

 

 

 

배열 이름의 주소값

배열 이름에 sizeof 연산자주소값 연산자 & 를 사용할 때 빼고는 전부 다 포인터로 암묵적 변환이 이루어짐

 

using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    int arr[3] = { 1,2,3 };
    int (*parr)[3] = &arr;

    cout << arr[1] << endl;
    cout << (*parr)[1] << endl;

    return 0;
}

똑같은 2 출력

 

위와 같은 경우는 arr 이 주소값 연산자 &을 사용하기 때문에 암묵적변환(배열의 첫 번째 요소의 주소로 변환) 되지 않음

 

  • arr → int[3]
  • &arr → int (*)[3] (배열 포인터)

parr의 타입이 int (*)[3]이므로 &arr을 저장할 수 있음.(타입 일치)

 

parr을 통해 값에 접근

cout << arr[1] << endl;     // 출력: 2 (일반 배열 접근)
cout << (*parr)[1] << endl; // 출력: 2 (배열 포인터 역참조)

 

  • (*parr)[1] → parr이 &arr이므로, *parr은 arr과 같음.
  • 즉, (*parr)[1]은 arr[1]과 동일.
  • 타입을 int (*)[3]으로 맞추어 주었기 때문에 *parr = &arr 성립

 

using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    int arr[3] = { 1,2,3 };
    int (*parr)[3] = &arr;

    cout << arr << endl;
    cout << parr << endl;

    return 0;
}

 

위 경우는 출력 시 arr에게 암묵적 변환이 일어나서 배열의 첫번 째 원소의 주소값을 출력함

물론, parr과 arr은 다른 타입임 ( parr은 int(*)[3], arr 은 int ) 

 

 

이 차이 때문에 포인터 연산이 다르게 동작

using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    int arr[3] = { 1,2,3 };
    int (*parr)[3] = &arr;

    cout << arr << endl;
    cout << arr + 1 << endl;
    cout << parr + 1 << endl;

    return 0;
}

 

000000F4AE55FA98 은 배열의 첫 번째 요소의 주소

arr + 1 → int* 포인터라서 4바이트 증가

parr + 1 → int (*)[3] 포인터라서 3 * 4 = 12바이트 증가

 

 

 

 

2차원 배열의 [ ] 연산자

int a[2][3];

이 경우 int a[3] 짜리 배열 2개가 메모리에 연속적으로 존재한다고 생각하면 됨

 

컴퓨터 메모리 구조는 1차원 이기 때문에 항상 선형으로 퍼져서 있음

 

예시

 

2차원 배열에서 arr[0]의 의미는 

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    int arr[2][3];

    cout << arr[0] << endl;
    cout << &arr[0][0] << endl;

    cout << arr[1] << endl;
    cout << &arr[1][0] << endl;

    return 0;
}

 

 

arr[0]이 arr[0][0]의 주소값과 같고 arr[1]의 값이 arr[1][0]의 주소값과 일치함

이를 통해 arr[0]은 arr[0][0]을 가리키는 포인터로 암묵적 타입변환 되고, arr[1]은 arr[1][0]을 가리키는 포인터로 타입변환 된다는 것을 알 수 있음.

 

 

 

만일 2차원 배열의 이름을 포인터에 전달하기 위해서는 해당 포인터의 타입은?

arr[0] 은 int* 가 보관할 수 있으니까, arr은 int** 이 보관가능한가?

int* 를 가리키는 포인터는 int** 라고 위에서 라고 설명했지만 그렇지 않다

 

 

 

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

만약 arr은 int**이 보관 가능하다면 다음의 코드도 실행 되어야한다

using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    int arr[2][3] = { {1,2,3}, {4,5,6} };
    int** parr = arr;

    cout << arr[1][1] << endl;
    cout << parr[1][1] << endl;

   

    return 0;
}

하지만 초기화 되지 않은 값 오류가 뜬다. 왜그럴까?

 

먼저 int arr[10] 이라는 배열에서 x 번째 원소의 주소 값을 알아내는 방법은 

arr[x]의 주소값 arr + 4x

와 같이 나타낼 수 있음 (arr은 int형 이니까 4)

 

이번에는 int arr[a][b] 라고 정의된 2 차원 배열을 생각해 보면 arr[x][y]의 원소를 참조할 때 이 원소의 주소 값은

int arr[a][b]은 int arr[b] 짜리 배열이 메모리에 a 개 존재하는 것임

따라서, arr[x][0]의 주소값은 x 번째 int arr[b]짜리 배열 

이 그림을 생각 하면 된다

arr + 4 * b * x 가 된다. 

 

4(int 형) 의 크기를 가진 int arr[b] 짜리 배열의 크기는  4 * b

이것은 x 번째는 4 * b * x

 

따라서 arr[x][y]의 시작 주소값은 

arr + 4bx + 4y 가 된다. ( b 는 정의 된 크기)

여기서 중요한 점은 b의 크기를 필수적으로 알아야 한다는 것이다.

 

따라서 2차원 배열을 가리키는 포인터를 통해 원소들을 정확히 접근하기 위해서는

1. 가리키는 원소의 크기(여기서는 4)
2. b의 값

위 두 정보가 포인터의 타입에 명시되어 있어야 컴파일러가 원소를 올바르게 접근 가능

 

using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    int arr[2][3] = { {1,2,3}, {4,5,6} };
    int (*parr)[3] = arr;


    cout << arr[1][1] << endl;
    cout << parr[1][1] << endl;

   

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

 

앞서말한 2가지 정보가 다 들어가 있다.

 

 

저 parr은 사실 크기가 3인 배열을 가리키는 포인터

위의 "배열 이름의 주소값" 을 참고 하면 됨

using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    int arr[3] = { 1,2,3 };
    int (*parr)[3] = &arr;

    cout << arr[1] << endl;
    cout << (*parr)[1] << endl;

    return 0;
}

이 코드에서 parr 은 arr배열 전체의 주소 값을 받게 됨. arr 배열 자체가 int [3]이기 때문에 서로 타입이 맞음

 

 

    int arr[2][3] = { {1,2,3}, {4,5,6} };
    int (*parr)[3] = arr;

여기에서는 어떨까?

arr은 암묵적 변환(int (*)[3]) 이 일어남

 

다차원 배열의 암묵적 변환은 첫 번째 차원의 주소까지만 허용됨

    int arr[3] = { 1,2,3 };
    int (*parr)[3] = &arr;
    
    int arr[2][3] = { {1,2,3}, {4,5,6} };
    int (*parr)[3] = arr;

둘다 배열의 주소를 받음. 즉, &arr 을 사용하는 경우와 arr을 직접 사용하는 경우 둘 다 같은 주소를 가리키게 됨

 

첫 번째 코드: 1차원 배열

arr 의 타입 : int[3]

&arr의 타입 : int(*)[3] (배열 전체를 가리키는 포인터)

parr도 int(*)[3] 타입이므로, &arr을 저장할 수 있음

 

두 번째 코드: 2차원 배열

arr의 원래 타입 : int[2][3] (2차원 배열)

암묵적 변환이 발생 -> arr 이 int(*)[3]로 변환

따라서 parr의 타입 int(*)[3]과 일치하므로 할당 가능

 

 

    int arr[2][3] = { {1,2,3}, {4,5,6} };
    int (*parr)[3] = &arr;

이 코드가 불가능한 이유는 &arr 과 parr의 타입이 다르기 때문

 

int arr[2][3];

arr 의 타입 : int[2][3]

&arr 의 타입 : int(*)[2][3] (배열 전체를 가리키는 포인터)

parr의 타입 : int(*)[3] (배열의 한 행을 가리키는 포인터)

 

 

만약 &arr을 사용하려면

 

int arr[2][3] = { {1,2,3}, {4,5,6} };
int (*parr)[2][3] = &arr;  // ✅ 가능 (배열 전체를 가리키는 포인터)

parr 의 타입 : int(*)[2][3]

&arr 의 타입 : int(*)[2][3]

타입이 일치하므로 가능!

 

 

 

그렇다면 이 코드는 무슨일을 했었을까?

 

using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    int arr[2][3] = { {1,2,3}, {4,5,6} };
    int** parr = arr;

    cout << arr[1][1] << endl;
    cout << parr[1][1] << endl;

   

    return 0;
}

parr[1][1]의 해석

parr[1][1]은 *( *( parr + 1 ) + 1 ) 과 동일한 문장이다. parr+1을 하면 뭐가 될까?

parr은 int*를 가리키는 포인터이고 int* 크기는 8바이트 이기 때문에 parr+1을 하면 실제 주소값이 8 증가하게 된다.

따라서 parr+1은 arr 배열의 세 번째 원소의 주소 값을 가지게 됨. *(parr+1)은 3이 됨

 

그다음에 *(parr+1) + 1을 하면 int 의 크기 만큼인 4가 늘어나게 됨 결국 *(parr+1) +1 은 7이 됨 ( 3을 가리킴 + 4)

결국 *( *( parr + 1 ) + 1 ) 은 마치 주소 값 7에 있는 값을 읽어라 라는 말과 동일. 따라서 말이 안됨

 

 

 

 

예시를 통한 연습

using namespace std;

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    cout.tie(NULL);

    
    int arr[2][3] = {  {1,2,3}, {4,5,6}  };
    int (*parr)[3] = arr;

    cout << parr[1][1];
    
   

    return 0;
}

 

parr 의 타입 : int(*)[3] -> 3개의 int를 가지는 배열을 가리키는 포인터

 

parr[1][1] 은 *(*(parr + 1) + 1) 와 동일

 

① parr + 1

parr 은 int(*)[3] 타입 -> 즉, 3개짜리 int 배열을 가리키는 포인터

parr + 1 하면 3개의 int크기(3 * 4 = 12 바이트)만큼 증가

 

② *(parr + 1)

*(parr + 1) 은 parr + 1이 가리키는 배열의 첫 번째 요소 참조

즉, *(parr + 1) == arr[1], 그리고 arr[1] 타입은 int[3] , int*처럼 동작

arr[1] = {4,5,6} 이므로 *(parr + 1)은 {4,5,6}의 시작 주소를 가짐

 

③ *(parr + 1) + 1

arr[1]은 int[3]이므로, *(parr + 1)의 타입은 int* 

포인터 연산에서 포인터가 가리키는 데이터 타입의 크기 만큼 증가

따라서 *(parr + 1) + 1 -> 4바이트 증가하여 arr[1][1]을 가리킴

결론: *(parr + 1) + 1에서 +1은 int* 포인터 연산이므로 4바이트 증가.

 

④ *(*(parr + 1) + 1)

*(parr + 1) + 1은 arr[1][1]의 주소

'C' 카테고리의 다른 글

[C] 2. switch문이 왜 필요한데? if문 쓰면 되잖아  (0) 2025.02.11
[C] 1. 변수 크기  (0) 2025.02.11
[C] 0. 기본 프로그래밍의 역사  (0) 2025.02.11