본문 바로가기
Computer Science/Algorithm

C++ 이중 포인터 완벽 이해

by 이은선 2023. 10. 1.
728x90
SMALL

이중 포인터

포인터를 가리키는 포인터는 다른 포인터의 주소를 보유하는 포인터로 생각하면 된다.

 

1) 단일 포인터 - int에 대한 일반 포인터 : 하나의 별표(*)만 사용해서 선언

int* ptr;

2) 이중 포인터 - int에 대한 포인터를 가리키는 포인터 : 두개의 별표(**)를 사용해서 선언

int** ptrptr;

 

▪︎ 단일 포인터와 이중 포인터 사용 예시

int value = 5;
int* ptr = &value;
cout << *ptr; // 5

int** ptrptr = &ptr; // 단일 포인터의 주소값을 이중 포인터에 넣어줌.
cout << **ptrptr; // 5

 

이중 포인터 사용 예제

void main(){
	char ch = 'A';
    char* ptr = NULL;
    char** ptr2 = NULL;
    
    ptr = &ch; // 단일 포인터
    ptr2 = &ptr; // 이중 포인터
    
    cout << ch << endl; // A
    cout << ptr << endl; // 008FFD93
    cout << ptr2 << endl; // 008FFD84
    
    cout << &ch << endl; // 008FFD93
    cout << &ptr << endl; // 008FFD84
    cout << &ptr2 << endl; // 008FFD78
    
    cout << *ptr << endl; // A
    cout << *ptr2 << endl; // 008FFD93 (*ptr2 == ptr)
    cout << **ptr2 << endl; // A
}

char 변수 ch의 주소값을 포인터 변수 ptr이 참조하고 있고, 이 ptr 포인터 변수의 주소값을 또 다시 이중 포인터 ptr2가 참조하고 있다. 따라서 이중 포인터에서 이를 역참조하면 char 변수 ch까지 접근 가능하다.

 

 

 

*& 

&은 주소를 나타내지만 변수명 앞에 붙을 경우 참조자가 됨.

*은 변수명 앞에 붙을 경우 포인터 변수로 사용하겠다는 의미

*&은 이중 포인터를 참조자로 표현한 개념

 

▪︎ 레퍼런스

int a = 5;
int &b = a;

cout << a << endl; // 5
cout << b << endl; // 5

다음과 같이 작성하면 b는 a의 주소값을 공유하게 된다. 즉, b는 a의 레퍼런스로 작동한다. 실제로 컴파일된 코드를 보면 포인터와 동일한 구조로 처리하고 있음을 볼 수 있다. C++ 복사 생성자가 인수를 레퍼런스로 처리한다.

 

▪︎ 간단한 예시 1)

int n = 5;
int *pn = &n;
int *&ppn = pn;

C언어만 공부하였을 시 **ppn = pn;이 되어야 하는게 아닌가라는 의문을 가질 수도 있지만, C++에서는 C와 달리 레퍼런스라는 개념이 존재하고 *&는 레퍼런스하는 대상이 포인터를 말하는 것이다.

 

▪︎ 간단한 예시 2)

void Fun(int **ppInterface){
 *ppInterface = (int*)0x00467700;
}
// 사용하는 경우
int *pInter = NULL;
Fun(&pInter);

레퍼런스를 이용하면
void Fun(int *&pInterface){
 pInterface = (int*)0x00467700;
}
// 사용하는 경우
int *pInter = NULL;
Fun(pInter);

이중 포인터와 달리 실질적으로 &를 앞에 붙일 필요가 없다는 점에서 편리하다. 하지만,  c++의 복사생성자 같이 필요불가결한 경우 아니면 별로 추천은 하지 않는다. 보통 함수의 인수를 비포인터 형식으로 전달할 때에는 그 값을 복사형식으로 전달한다고 생각하는 것이 일반적이고, 포인터로 전달하는 경우에는 포인트하는 값을 참조하거나 값이 변경가능하겠다고 생각하는 것이 일반적이다.

 

▪︎ 간단한 예시 3)

--> 참조자(레퍼런스) 사용안하는 경우
void Exam(int **ppn)
{
  *ppn = (int *)0x00001111;
}

int main()
{
  int *pn=nullptr;
  Exam(&pn);
}


--> 참조자(레퍼런스) 사용하는 경우
void Exam(int *&ppn)
{
   ppn = (int *)0x00001111;	
}

int main()
{
  int *pn=nullptr;
  Exam(&pn); 
}

 

▪︎ 간단한 예시 4)

#include <iostream>
using namespace std;

void func(int* p1, int*& pp1){
    p1++;
    pp1++;
}

int main(){
    int arr[4];

    int* a = &arr[0];
    int* b = &arr[0];

    cout << "a: " << a << endl; // 0x16d863248
    cout << "b: " << b << endl; // 0x16d863248

    func(a, b);

    cout << "a: " << a << endl; // 0x16d863248
    cout << "b: " << b << endl; // 0x16d86324c

    return 0;
}

int* p1은 int* a의 복사본이기 때문에 p1의 값이 변경되어도 a의 값이 변경되지 않는다. 하지만, int* pp1의 경우에는 b의 레퍼런스이기 때문에 pp1의 값이 변경되면 b의 값도 변경된다.

 

2차원 배열 동적 할당

int row = 5;
int col = 3;

int** arr = new int*[row];
for (int i = 0; i < row; i++){
	arr[i] = new int[col];
}
        
for (int j = 0; j < row; j++){
	delete[] arr[j];
}
delete[] arr;

 

다중 포인터 쓰는 이유

다중 포인터를 쓰는 이유를 제 나름대로(?) 정리해보았습니다! 틀린 점이 있을 수도 있습니다 ㅠ

#include <iostream>
using namespace std;

int global_value = 3000;

void func2(int* p1){
    // *p1 = 1000; // value = 1000으로 바뀜.
    p1 = &global_value;
}

void func3(int** pp1){
    // **pp1 = 2000; // value = 2000으로 바뀜.
    *pp1 = &global_value;
}

int main(){   
    int value = 5;
    int* pointer = &value;

    func2(pointer);
    cout << "func2 value: " << value << "*pointer: " << *pointer << endl;
    
    func3(&pointer);
    cout << "func3 value: " << value << "*pointer: " << *pointer << endl;
    return 0;
}

▪︎ func2와 func3에서 주석처리한 첫번째 줄을 실행 시 원하는대로 값이 변한다. 

value : 5 -> 1000
value : 5 -> 2000

 - func2 : p1 -> value 에서 *p1(p1을 역참조)하여 value의 값을 직접 변경하기 때문

 - func3 : pp1 -> pointer -> value 에서 **pp1(pp1을 역참조)하여 value의 값을 직접 변경하기 때문 - value는 main()함수 내에서 선언한 지역변수이므로 func2와 func3를 호출한 후에 돌아와서 값을 출력해보면 값이 변경된 것을 볼 수 있다. 즉, value의 변수값을 직접 바꾸었으므로.

 

▪︎ func2와 func3에서 두번째 줄을 실행 시 value값은 변경되지 않는데, pointer가 역참조하는 값은 값이 변경됐음을 확인할 수 있다.

func2 value: 5*pointer: 5
func3 value: 5*pointer: 3000

 - func2 : p1 -> value 에서 p1 포인터 변수가 지니는 값 자체를 바꿀 경우에는 값이 변경되지 않음을 확인할 수 있다. p1은 func2의 지역변수로 func2 함수를 리턴할 땐 call by value 형식처럼 메모리가 정리되므로.

 - func3 : pp1 -> pointer -> value 에서 pointer가 가지는(가리키는 주소 값)를 global_value의 주소값으로 변경해주었다.      global_value는 전역변수이므로 func3 함수를 리턴한 후 *pointer를 출력해보면 3000으로 바뀌어있는 것을 볼 수 있다. 첫번째 주석을 실행했을 때처럼 value값을 직접 변경해준 것이 아닌 pointer가 가지는 주소값을 value의 주소값에서 global_value의 주소값으로 변경해주었기 때문에 value가 가지는 값은 그대로임을 확인할 수 있다.

 

-> 따라서, 두번째 예시처럼 함수 내에서 포인터 변수가 가지는 값을 변경해줄 필요가 있을 땐 이중 포인터를 사용해야 함수를 리턴한 후에도 변경된 값이 반영됨을 알 수 있다.

 

https://boycoding.tistory.com/212

https://coding-factory.tistory.com/659

https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=yoonemong&logNo=221363417470 

https://chanywa.com/343

 

728x90
LIST

'Computer Science > Algorithm' 카테고리의 다른 글

순열, 조합, 중복순열, 중복조합  (0) 2024.01.09