본문 바로가기

C 언어

c언어 : 예제로 보는 포인터와 배열의 관계

프로그래머스 문제 (왼쪽 오른쪽)

문제 

문자열 리스트 str_list에는 "u", "d", "l", "r" 네 개의 문자열이 여러 개 저장되어 있습니다. 
str_list에서 "l"과 "r" 중 먼저 나오는 문자열이 "l"이라면 해당 문자열을 기준으로 왼쪽에 있는 문자열들을 순서대로 담은 리스트를, 
먼저 나오는 문자열이 "r"이라면 해당 문자열을 기준으로 오른쪽에 있는 문자열들을 순서대로 담은 리스트를 return하도록 solution 함수를 완성해주세요. "l"이나 "r"이 없다면 빈 리스트를 return합니다.

제한사항
1 ≤ str_list의 길이 ≤ 20
str_list는 "u", "d", "l", "r" 네 개의 문자열로 이루어져 있습니다.

입출력 예
str_list	               result
["u", "u", "l", "r"]	["u", "u"]
["l"]	                     []

 

코드

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

// str_list_len은 배열 str_list의 길이입니다.
// result_len은 포인터로 전달하여 값이 반영되도록 설정
char** solution(const char* str_list[], size_t str_list_len, int* result_len) {
    // 각 문자열을 저장할 공간 할당 (문자열 포인터의 크기 * 길이)
    char** answer = (char**)malloc(sizeof(char*) * str_list_len);
    int index = 0;
    bool is_l = false;
    int is_l_index = 0;
    int is_r_index = 0;

    // 'l' 또는 'r'의 위치를 찾기 위한 반복문
    for (int i = 0; i < str_list_len; i++) {
        if (strcmp(str_list[i], "l") == 0) {
            is_l = true;
            is_l_index = i;
            break;
        } else if (strcmp(str_list[i], "r") == 0) {
            is_l = false;
            is_r_index = i;
            break;
        }
    }

    // 'l'이 발견된 경우: 'l' 이전의 문자열을 저장
    if (is_l) {
        for (int i = 0; i < is_l_index; i++) {
            answer[index] = (char*)malloc(strlen(str_list[i]) + 1);
            strcpy(answer[index++], str_list[i]);
        }
    }
    // 'r'이 발견된 경우: 'r' 이후의 문자열을 저장
    else {
        for (int i = is_r_index + 1; i < str_list_len; i++) {
            answer[index] = (char*)malloc(strlen(str_list[i]) + 1);
            strcpy(answer[index++], str_list[i]);
        }
    }

    // 반환할 문자열의 개수를 설정
    result_len = index;

    return answer;
}

int main(void) {
    const char* str_list[] = {"u", "u", "l", "r"};
    size_t str_list_len = sizeof(str_list) / sizeof(str_list[0]);
    int result_len = 0;

    // solution 함수 호출
    char** result = solution(str_list, str_list_len, &result_len);

    // 결과 출력
    if (result != NULL) {
        for (int i = 0; i < result_len; i++) {
            printf("%s\n", result[i]);  // 문자열 출력
            free(result[i]);  // 동적으로 할당된 메모리 해제
        }
        free(result);  // 배열 자체 해제
    }

    return 0;
}

 

포인터와의 관계 분석 

위 예제 코드에는 여러 포인터들이 등장한다. 솔직히 이론을 배우고 나서 다 안다고 생각했는데, 막상 문제를 풀 때 또 헷갈려서 예제 문제를 보고 이론을 정리해야 겠다는 생각이 들었다. 

 

머리에서 정리가 깔끔하게 안되는 포인터는 총 4 개였다.  

그래서 각 포인터 구문마다 정확히 어떤 의미를 하는건지 정리해보도록 하겠다. 

1. const char* str_list[] = {"u", "u", "l", "r"};

이 구문은 문자열 포인터 배열을 선언한다. 여기에서 포인터를 붙여야 하는 이유는 문자열이 배열처럼 동작하지만, 실제로는 메모리 상의 주소를 가리키는 포인터로 관리되고 있기 떄문이다. 

#배열
str_list[0] -> 0x001000 // 문자열 "u\0"의 시작 주소를 가리키는 포인터
str_list[1] -> 0x001008 // 두 번째 문자열 "u\0"의 시작 주소
str_list[2] -> 0x001016 // 세 번째 문자열 "l\0"의 시작 주소
str_list[3] -> 0x001032 // 네 번째 문자열 "r\0"의 시작 주소

#역참조 (역참조로 접근할 때는 단일 문자에 접근한다.)
*str_list[0] -> 'u'  // 첫 번째 문자열 "u\0"의 첫 번째 문자 'u'에 접근
*str_list[1] -> 'u'  // 두 번째 문자열 "u\0"의 첫 번째 문자 'u'에 접근
*str_list[2] -> 'l'  // 세 번째 문자열 "l\0"의 첫 번째 문자 'l'에 접근
*str_list[3] -> 'r'  // 네 번째 문자열 "r\0"의 첫 번째 문자 'r'에 접근

 

여기서 문자 배열과 문자열 포인터 배열의 차이점을 이해할 수 있어야 한다. 

문자열은 NUL 종료 문자(\0)로 끝나야 하며, 각 문자열을 배치할 때 연속적인 메모리 블록 한개 이상을 필요로 하니까 시작주소를 알려주는 포인터가 필요할 수 있지만, 단일 문자 배열의 경우에는 각 문자가 독립적으로 저장되기 때문에 (메모리 블록 한 개만 있으면 되니까) 포인터가 필요하지 않다. 

const char str_list[] = {'u', 'u', 'l', 'r'};
#메모리 구조
+--------------------+
|  'u' | 'u' | 'l' | 'r'  |
+--------------------+


2. char** solution(const char* str_list[], size_t str_list_len, int* result_len); 

여기서 solution이 더블 포인터인 이유는 함수의 반환값여러 개의 문자열을 가리키는 포인터 배열이기 때문이다.

 

  • char*는 단일 문자열을 가리킨다. 즉, 하나의 문자열만 가리킬 수 있다.
  • 반면, char**는 여러 개의 문자열을 가리킬 수 있는 포인터 배열이다. 즉, 이중 포인터는 포인터 배열을 가리키기 때문에 여러 개의 문자열을 반환할 수 있다. 즉 여러 개의 문자열을 반환하려면, 각 문자열의 시작 주소를 가리킬 포인터 배열이 필요하다.

3. char** answer = (char**)malloc(sizeof(char*) * str_list_len);

이 구문은 포인터 배열을 동적으로 할당하는 코드이다. 즉, str_list_len만큼의 문자열 포인터들을 저장할 공간을 할당하는 것이다. answer가 이중 포인터(char**)인 이유는 여러 개의 문자열을 가리킬 수 있는 포인터 배열을 만들기 위해서이다. 

포인터 배열은 여러 개의 문자열을 가리키는 포인터들을 저장할 수 있으며, answer는 이 포인터 배열의 시작 주소를 가리킨다. 그러므로, 배열의 각 요소는 문자열의 시작 주소를 저장하는 char* 포인터가 된다.

 

이때 주의할 점은, 이 할당은 문자열 포인터를 위한 공간만 할당한 것이지, 문자열 자체를 저장할 메모리는 아직 할당되지 않았다는 점이다. 각 포인터가 가리킬 문자열을 저장하기 위한 메모리는 이후에 따로 동적으로 할당해야 한다.

 

예를 들어, sample[2][3]라는 2차원 배열이 있다고 가정해보자.

char sample[2][3];

 

여기서 sample이라는 배열의 이름은 sample[0][0]의 시작 주소를 가리킨다. 하지만, 배열의 각 요소(sample[0][0], sample[0][1] 등)에 값을 할당하지 않으면, 그 값들은 초기화되지 않은 상태이다. answer도 마찬가지이다. 이 구문은 포인터 배열을 위한 메모리만 할당한 상태이며, 각 포인터가 가리킬 실제 문자열을 저장할 메모리는 아직 할당되지 않은 상태이다. 문자열을 저장할 공간은 이후에 추가적으로 할당해야 한다.

#메모리 구조
answer[0] -> 아직 문자열을 가리키지 않음 (NULL 또는 할당 전) 
answer[1] -> 아직 문자열을 가리키지 않음 (NULL 또는 할당 전) 
answer[2] -> 아직 문자열을 가리키지 않음 (NULL 또는 할당 전) 
answer[3] -> 아직 문자열을 가리키지 않음 (NULL 또는 할당 전)


4. answer[index] = (char*)malloc(strlen(str_list[i]) + 1);

이 구문은 **answer 의 각 포인터 배열 요소인 answer[index] 에 문자열을 입력하면, 문자열의 시작 주소를 가리키게 된다. 동적으로 공간을 할당하는 코드이다. 해당 구문이 싱글 포인터인 이유는  문자열은 여러 개의 문자의 배열이라서 문자열 배열을 선언할 떄는 더블 포인터를 쓰지만,  그 배열의 시작 주소는 단일 포인터로 표현할 수 있기 때문이다. 따라서 answer[index]는 싱글 포인터(char*)로 동작하여, 각 문자열의 시작 주소를 저장한다. 

#포인터 배열 할당
answer[0] -> 0x001000  (문자열 "u\0"의 시작 주소)
answer[1] -> 0x001008  (문자열 "u\0"의 시작 주소)
answer[2] -> 0x001016  (문자열 "l\0"의 시작 주소)
answer[3] -> 0x001032  (문자열 "r\0"의 시작 주소)

#역참조하여 값에 접근 
*answer[0] -> 'u'  (첫 번째 문자열의 첫 번째 문자에 접근)
*answer[1] -> 'u'  (두 번째 문자열의 첫 번째 문자에 접근)
*answer[2] -> 'l'  (세 번째 문자열의 첫 번째 문자에 접근)
*answer[3] -> 'r'  (네 번째 문자열의 첫 번째 문자에 접근)