F.R.I.D.A.Y.

포인터(pointer) part2. 다차원 포인터 본문

DEV/C C++

포인터(pointer) part2. 다차원 포인터

F.R.I.D.A.Y. 2020. 1. 17. 23:44
반응형

 지난 시간에 우리는 포인터의 기본적인 내용을 배웠습니다.

 

포인터(pointer)

잘 배워가던 사람들도 멈추는, 일명 C언어에서 첫 번째 고비라 일컬어지는 포인터입니다. 하다 보면 쉽지만 막상 처음 하면 난생처음 보는 문법에 쓰는 방법도 독특한지라 많이들 힘들어합니다. # 선행으로 함수..

pang2h.tistory.com

 이젠 포인터의 한 단계 더 높은, 다차원[# 지난 시간에도 말씀드렸지만 원서의 포인터 파트에서는 차원(dimension)이라는 표현은 없습니다. 역자들이 번역해올 때 정확한 명칭을 정하지 못해 배열의 차원이라는 단어를 가져온 것입니다. 이번 포스트에서는 차원이란 단어로 소개했으므로 차원으로 통일하겠습니다.\n용어를 올바르게 바꾸고자 노력하는 분이 계시니 만일 더 나은 단어나 반대 의견이 있다면 이 포스트에서 대화해주세요.] 포인터에 대해 배워보겠습니다.

 

# 기초적인 동적 할당 함수 사용법을 알고 있어야 합니다.


학생 관리하기

 우리가 코드에 아래와 같이 포인터 변수를 만들었다고 해봅시다. 코드 설명을 하면 한 반의 학생 수를 받아서 그들의 1과목 점수를 받는 프로그램입니다.

#include <stdio.h>
#include <stdlib.h>

int main(void){
    int num;
    int* p;
    printf("반 학생 수: ");
    scanf("%d", &num);
    p = malloc(sizeof(int) * num);
    
    for(int i = 0; i < num;++i)
        scanf("%d", p + i); // 이 표현법은 오른쪽 링크 참고
    
    free(p);
    return 0;
}

 여기에서 우리는 1개 반의 학생들 정보만 받는 것이 아니라 사용자로부터 반의 수까지 받아서 1개 학년의 학생 데이터를 받을 수 있도록 해보려고 합니다.

 

 어떻게 해야 할까요? 포인터 배열을 이용해볼 수도 있습니다. 포인터 배열을 이용한 아래의 코드[#참고 이 코드는 단순히 입력만 받도록 했기 때문에 각 배열의 최대 길이가 몇인지 알지 못합니다. 만일 값을 입력받고 그 내용을 다른 때에 출력하기를 원한다면 첫 번째 반복문(for)의 num 변수 데이터를 보관해야 합니다.]는 최대 배열의 길이[# 여기에서는 10]만큼의 반을 구성할 수 있게 됩니다.

#include <stdio.h>
#include <stdlib.h>

int main(void){
    int num, classNum;
    int* p[10];
    printf("반 수: ");
    scanf("%d", &classNum);
    for(int i = 0 ; i< classNum;++i){
        printf("반 학생 수: ");
        scanf("%d", &num);
        p[i] = malloc(sizeof(int) * num);
        
        for(int k = 0; k < num;++k)
            scanf("%d", p[i] + k);
        free(p[i]);
    }

    return 0;
}

 이 방식의 문제점은 반 수 길이에 제한을 가져온다는 것입니다. 길이를 크게 해 두면 어느 정도 문제를 해결해줄 수 있겠지만 그만큼 메모리를 상시로 낭비하고  있다는 의미가 됩니다.

 

 포인터는 변수를 가리키는 문법입니다. 그렇다면 포인터를 가리키는 문법은 없을까요? 있습니다. 포인터는 범위가 확장된 다른 포인터로 가리킬 수 있습니다.

int   var = 5;
int*  p1 = &var;
int** p2 = &p1; // 여기!

주석이 포함된 세 번째 줄의 포인터가 바로 포인터 변수를 가리키는 데 사용하는 포인터입니다.


차원의 개념을 적용한 포인터

 일반적으로 0차원을 점, 1차원을 선, 2차원을 면, 3차원을 입체(공간)로 말합니다. 포인터 또한 그와 비슷하게 차원의 개념을 적용해 사용할 수 있습니다. 포인터에서는 * 문자[# 해당 변수가 포인터 변수임을 명시하는 형식 문자.\n간접 참조 연산자와는 다릅니다.]를 이용해 차원의 개념을 이용할 수 있습니다.

 

 일반적으로 변수는 * 문자를 하나도 작성하지 않습니다. 일반 변수[# 포인터가 아닌 일반적으로 선언한 변수\nint var; // 등으로 선언한 변수를 말합니다.]는 0차원이라고 볼 수 있을 것입니다. 다른 영역을 건들 수도 없으며 오로지 자신이 가지고 있는 값을 관리할 수 있으니까요.

 포인터 문자 하나를 작성한 포인터 변수는 1차원이라고 볼 수 있겠습니다. 자신을 포함해 한 단계 낮은 일반 변수의 값을 변경할 수 있습니다.

 그렇다면 포인터 문자 N개를 작성한 다차원 포인터 변수는 포인터 변수를 N차원이라고 볼 수 있습니다. 어떻게 이렇게 볼 수 있는지 아래를 보겠습니다.

 

포인터 문자 = 간접 참조 가능 횟수

 재차 말하는 것이지만, 우리가 포인터 변수를 선언할 때는 포인터임을 나타내는 * 문자를 사용했습니다. 처음 포인터를 사용할 때 간접 참조 연산자를 하나 사용했습니다. 이 연산자를 한 변수에 여러 개 사용할 수도 있습니다.

 

int ** depth2p;

 이렇게 작성했을 때 depth2p 포인터 변수는 간접 참조 연산자를 두 개 까지 사용할 수 있습니다. 처음 포인터 변수를 선언할 때 앞의 포인터 문자를 몇 개를 사용하는지에 따라 간점 참조 연산자의 사용 가능 개수가 달라지는 것입니다.


어떻게 사용할까

 처음 코드에서 반의 수를 사용자로부터 입력받게 하기 위해서 포인터 배열을 사용했습니다. 그러나 이제 다차원 포인터를 배웠으니 다차원 포인터를 이용해보겠습니다.

#include <stdio.h>
#include <stdlib.h>

int main(void){
    int num, classNum;
    int** p;
    printf("반 수: ");
    scanf("%d", &classNum);
    p = malloc(sizeof(int*) * classNum);
    for(int i = 0 ; i< classNum;++i){
        printf("반 학생 수: ");
        scanf("%d", &num);
        p[i] = malloc(sizeof(int) * num);
        
        for(int k = 0; k < num;++k)
            scanf("%d", p[i] + k);
        free(p[i]);
    }
	free(p);
    return 0;
}

 포인터 배열을 차원 포인터로 전환하는 방법 또한 간단합니다. 배열 연산자[# 대괄호 문자 쌍을 말함. 영어로는 indicator]의 개수만큼 포인터 기호를 붙여주고 배열 연산자를 지워주면 됩니다.

 포인터는 단순히 어떤 공간을 가리키는 용도로 사용되므로 실제 값을 넣을 수 있도록 추가적인 동적할당이 필요합니다.

더보기

# 그림으로 알아보기(Stack - Heap 메모리 영역)

# 스택 영역은 초록색, 힙 영역은 주황색으로 표시했습니다.

 

 우리가 처음 사용했던 배열 포인터는 아래와 같이 메모리가 구성되어 있습니다.

 포인터 배열 p는 배열의 각 요소 타입이 int*인 변수를 10개 만든 것으로, p 변수로 만들어진 10개의 공간을 스택에 쌓게 됩니다. 그리고 malloc 함수를 이용해 할당받은 공간의 주소를 스택에 생성된 p 변수의 각각 공간에 저장해 관리하게 됩니다. 따라서 우리는 스택에 4 * 10 = 40byte[# (포인터 크기) * (포인터 개수)]를 사용하게 됩니다.

 

 그러나 포인터 배열이 아닌 다차원 포인터를 이용하게 되면 아래와 같이 상황이 달라집니다.

 이미지를 보면 이차원 포인터 변수 p는 스택 공간 4바이트[# 포인터 변수는 여하를 불문하고 32비트 프로그램에서는 4Byte(32bit), 64비트 프로그램에서는 8Byte(64bit)]를 사용하게 됩니다. 그리고 반 수를 물은 뒤 1차로 동적할당을 받은 메모리 주소를 변수 p에 저장합니다. 그리고 p에 간접 참조 연산을 통해 1차 동적 할당 받은 공간에 학생의 정보를 담은 메모리[# 2차 동적 할당을 받은 메모리] 주소를 저장합니다.

 상대적으로 포인터가 이해나 사용함에 있어 조금 어려울 수 있기에 배열이라는 문법이 추가된 것입니다. 자세한 내용은 다른 포스트에서 다루겠지만, 서로 장단점이 존재하니 그 차이를 알아보는 재미를 느껴보기 바랍니다.

 

 다차원 포인터에서 값을 읽어들일 때는 아래와 같이 작성하면 됩니다. 우리는 6개의 반, 그리고 각 반당 3명의 학생이 존재한다고 생각해봅니다. 그중, 2반의 2번 째 학생의 정보를 불러들이고 싶습니다. 그렇다면 아래와 같이 코드를 작성하면 읽을 수 있게 됩니다.

#include <stdio.h>
#include <stdlib.h>

int main(void){
    int num, classNum;
    int** p;
    printf("반 수: ");
    scanf("%d", &classNum);
    p = malloc(sizeof(int*) * classNum);
    for(int i = 0 ; i< classNum;++i){
        printf("반 학생 수: ");
        scanf("%d", &num);
        p[i] = malloc(sizeof(int) * num);
        
        for(int k = 0; k < num;++k)
            scanf("%d", p[i] + k);
    }
    
    printf("2반의 2번 학생의 정보:%d\n", *(*(p+1)+1)); // 2반의 2번 학생 정보 읽기

    for(int i = 0; i< classNum; ++i) free(p[i]);
    free(p);
    return 0;
}

*(*(p+1)+1)

 이 값을 찾는 과정은 다음과 같습니다.

 p가 가지고 있는 값은 반 정보를 가지고 있는 메모리의 시작 주소입니다. 따라서 1번 인덱스에 존재하는 값이 2반 정보의 시작을 담고 있는 주소겠지요. 그래서 *(p+1)를 가지게 됩니다.

 간접 참조를 통해 1번 인덱스 값을 가지고 왔으므로 이제 평가값은 아래와 같아집니다.

*(*(p+1)+1) => *(0x200 + 1)

 이 때, 0x200 + 1에서 1은 일반 산술 연산 대상이 아닌 포인터 산술 연산 대상이므로 1이 더해지지 않고 4가 더해[# x86 프로그래밍 기준]지게 됩니다.

 0x204의 가 2반의 2번 학생의 값을 가지고 있는 공간이 됩니다.


불편한 간접 참조 연산자

 다차원 포인터는 동적으로 포인터의 수를 결정지을 수 있다는 이점이 있지만 각 값에 접근하려면 간접 참조 연산자와 괄호를 많이 사용해야하는 단점을 가지고 있습니다. 2차원 포인터까지는 어떻게 해볼 수 있겠지만, 3차원, 혹은 그 이상의 데이터를 다루어야한다면 얘기가 달라집니다.

 x, y, z, w 축을 가진 데이터 d가 존재한다고 봅시다. 여기서 우리는 (1, 5, 7, 4)에 위치한 값을 불러와보겠습니다. 순서대로 w-z-y-x 순으로 접근해야하므로[# 이렇게 접근해야하는 이유는 이 포스트를 참고하세요.] 코드를

*(*(*(*(d+4)+7)+5)+1)

 이렇게 작성해야합니다. 직관적으로 보이지도 않을 뿐더러 조금만 착각하면 엉뚱한 값을 불러오기 십상입니다.

 

 이 문제를 해결하는 연산자가 존재합니다. 우리가 배열을 사용했을 때 대괄호 연산자[# indicator operator]를 사용했습니다. 다차원 포인터에서도 이 연산자를 이용할 수 있습니다. 우리가 방금 d(1, 5, 7, 4)의 값을 불러오려 했을 때 간접 참조 연산자를 사용했을 때와 대괄호 연산자를 사용했을 때를 비교해보세요.

간접 참조 연산자 대괄호 연산자
*(*(*(*(d+4)+7)+5)+1) d[4][7][5][1]

 접근하고자 하는 순서대로 대괄호를 이어 작성해주면 됩니다.


해제는 선택이 아닌 [필수]입니다.

 동적 할당을 이용해 시스템 자원인 메모리를 소비했다면 다 사용하고 나서는 꼭 해제를 해주어야합니다. 그렇지 않으면 메모리 점유율이 계속 증가해서 프로그램이 중단되거나 시스템 속도가 느려질 수 있습니다.

 

 우리가 동적 할당을 했을 때, 어떤 순서로 할당을 받았나요?

 그렇습니다. 반 데이터를 저장할 메모리를 1차로 받고, 각 반에 포함된 학생 데이터를 저장할 공간을 이어서 받았죠. 그렇다면 해제는 이와는 역순으로 처리해주어야합니다.

#include <stdlib.h>

int main(void){
    int** p;
    
    p = malloc(sizeof(int *) * 10);
    for(int i = 0; i < 10; ++i){
        p[i] = malloc(sizeof(int) * 10);
    }
    
    for(int i = 0; i < 10; ++i){
        free(p[i]);
    }
    free(p);
    return 0;
}
더보기

# 역순으로 해제하지 않으면 어떻게 되나요?

 1차 메모리를 먼저 해제하게 되면, 그 메모리를 다른 프로그램이 사용하거나 속의 값이 변경될 수 있습니다. 혹은 접근, 읽기조차 할 수 없죠. 이렇게 되면 결국 2차 메모리의 주소를 알지 못하게 되는 것이며, 할당받은 메모리 주소를 모른다는 것은 해제할 수 없다는 것을 의미합니다. 이 과정이 프로그램이 실행되는동안 계속 일어나게 되면 앞서 언급한 것과 같은 문제가 발생할 수 있습니다.

 OS에서는 프로그램이 종료되면 그동안 할당받은 모든 메모리를 회수하니 영영 사용하지 못하는 두려움에선 잠시 벗어날지라도 메모리 누수를 하는 것은 굉장히 좋지 않은 습관입니다. 이 문제를 해결하려면 동적 할당 코드를 작성하면 바로 이어서 해제해주는 코드를 쌍으로 작성해주는 것이 좋습니다.


더 읽어보기

 

포인터(pointer) part3. 함수 포인터

포인터는 신기하게도 함수까지 가리킬 수 있습니다. 어차피 이름이 있으니 이름으로 사용하면 될텐데 뭐가 좋느냐는 생각을 할 수 있겠지만 이번에 그 생각이 바뀌실겁니다. # 포인터에 대한 내용을 알고 있어야..

pang2h.tistory.com

 

포인터와 배열

배열 변수의 이름이 0번 인덱스의 시작 주소인 이유 이번 포스트는 제목 그대로 배열 변수의 이름이 어째서 해당 배열의 0번 인덱스의 주소가 되는지 알아봅니다. 간단해요! int arr[10]; &arr[0]; // 처음 배울..

pang2h.tistory.com

# index

728x90
반응형

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

배열(array) part2. 다차원 배열  (0) 2020.01.20
배열(array) part1. default  (1) 2020.01.19
CallByValue vs CallByReference  (0) 2020.01.09
함수 호출 구조  (0) 2020.01.07
포인터 part1. default  (0) 2020.01.07
Comments