[C] 이중, 배열, void 포인터

방법이있지·2025년 6월 12일
post-thumbnail

포인터 배열의 배열명을 저장할 땐 이중 포인터가, 이차원 배열의 배열명을 저장할 땐 배열 포인터가 필요합니다. 특히 매개변수로 배열명을 보낼 때 중요해요.

이중 포인터

이중 포인터의 선언과 사용

  • 포인터도 메모리에 저장되어야 하므로, 주소를 가짐
  • 이중 포인터: 포인터의 주소를 저장하는 포인터
  • 자료형 **포인터와 같이 선언
    • e.g., int **ppiint *형 포인터를 가리키는 포인터 ppi 선언
int a = 33;
int *pi = &a;       // int형 변수 a의 주소를 저장
int **ppi = π    // int*형 포인터 pi의 주소를 저장

printf("a: 값 %d\n", a);
printf("pi: 값 %p, &연산 %p, *연산 %d\n", pi, &pi, *pi);
printf("ppi: 값 %p, &연산 %p, *연산 %p, **연산 %d\n", ppi, &ppi, *ppi, **ppi);

// (주소값은 편의상 16자리 중 마지막 6자리만 명시)
// a: 값 33
// pi: 값 5FFE8C, &연산 5FFE80, *연산 33
// ppi: 값 5FFE80, &연산 5FFE78, *연산 5FFE8C, **연산 33

  • 이중 포인터 ppi는 포인터 pi를 가리킴
  • 포인터 pi는 변수 a를 가리킴
  • ppi로 변수 a의 값을 사용하려면, **ppi와 같이 * 연산자를 2회 사용해야 함
    • *ppi -> 단일 포인터 pi 반환
    • *pi -> 변수 a 반환

이중 포인터의 형태

  • e.g., 포인터 piint형 변수의 주소를 저장할 때
    • pi(int *)
double k = 13.37;       // double형 변수
double *pk = &k;        // pk는 (double)형을 가리킴
double **ppk = &pk;     // ppk는 (double *)형을 가리킴
  • 위 코드의 포인터 pk(double *)
  • ppk(double *)형의 pk의 주소를 저장함
  • 따라서, ppk(double *)*... 즉 double **

이중 포인터 활용 예제

포인터 값을 바꾸는 함수

  • 두 변수의 값을 바꾸는 함수를 만들 땐

    • 각 변수의 주소를 인수로 받아 포인터 매개변수에 저장하고
    • * 연산자를 사용해 값을 바꿔야 함
  • 포인터의 값을 바꾸는 경우

    • 각 포인터의 주소를 인수로 받아야 하니 이중 포인터가 매개변수로 필요
  • e.g., 두 char *형 포인터의 값 바꾸기

#include <stdio.h>

void swap_ptr(char **ppa, char **ppb);

int main(void){
    char *pa = "무적 LG 박용택";
    char *pb = "오오 오오오오오 오 오오오오오";

    printf("%s %s\n", pa, pb);
    // [출력] 무적 LG 박용택 오오 오오오오오 오 오오오오오
    swap_ptr(&pa, &pb);
    // char*형 포인터의 주소를 매개변수로 보냄
    printf("%s %s\n", pa, pb);
    // [출력] 오오 오오오오오 오 오오오오오 무적 LG 박용택
}

void swap_ptr(char **ppa, char **ppb){
    // ppa엔 pa의 주소, ppb엔 pb의 주소가 저장됨
    char *temp;

    // pa와 pb에 저장된 주소를 바꿈
    temp = *ppa;
    *ppa = *ppb;
    *ppb = temp;
}

포인터 배열을 처리하는 함수

  • 배열을 처리하는 함수는 매개변수로 배열명, 즉 첫 요소를 가리킬 포인터가 필요함
  • 이때 포인터 배열의 첫 요소는 포인터
    • 따라서, 포인터 배열을 처리하는 함수는 포인터를 가리킬 이중 포인터를 매개변수로 가져야 함
// 포인터 배열을 매개변수로 받기

#include <stdio.h>

void print_str(char **str, int count);

int main(void){
    // char *형 포인터를 요소로 갖는 배열
    char *dinner[] = {"군만두", "찐만두", "물만두", "딤섬"};
    // 배열 요소의 수
    int count = sizeof(dinner) / sizeof(dinner[0]);
    print_str(dinner, count);
    return 0;
}

// 매개변수로 이중 포인터 사용
// char *형인 배열명을 넘기므로 이중 포인터를 사용
void print_str(char **str, int count){
    for (int i = 0; i < count; i++){
        // 배열 요소 (char *형 포인터) 순회하며 각 문자열 출력
        printf("%s ", str[i]);
    }
}

// [출력] 군만두 찐만두 물만두 딤섬

배열 포인터

  • 앞선 포인터 배열(포인터가 구성 요소인 배열)과 다름
  • 배열 포인터는 배열을 가리키는 포인터

배열 요소의 주소 vs 배열의 주소

  • e.g., 배열 int arr[5]를 선언했을 때
    • 배열명 arr은 배열 첫 번째 요소의 주소로 해석됨
    • &arr 사용 시, 배열 전체를 하나의 변수로 간주하고 주소를 구함
    • arr&arr은 전혀 다른 주소
// 편의상 주소를 16진수가 아니라 10진수로 표현
int arr[5];
printf("arr: %u\n", arr);
printf("arr + 1: %u\n", arr + 1);
printf("&arr: %u\n", &arr);
printf("&arr + 1: %u\n", &arr + 1);

// [출력]
// arr: 6291056
// arr + 1: 6291060
// &arr: 6291056
// &arr + 1: 6291076

  • arr을 주소로 사용할 때: 첫 요소를 가리킴
    • 따라서 가리키는 대상의 크기는 4
    • 1을 더하면 주소에 int의 크기인 4가 더해짐
  • &arr을 주소로 사용할 때: 배열 전체를 가리킴
    • 따라서 가리키는 대상의 크기는 4 * 5 = 20
    • 1을 더하면 주소에 배열 전체의 크기인 20이 더해짐

2차원 배열과 배열 포인터

  • 2차원 배열의 각 구성 요소는 1차원 배열
  • 배열명은 첫 번째 요소의 주소
  • 배열명은 1차원 배열의 주소. (절대로 0행 0열의 요소의 주소가 아님!!!)
    • 따라서, 2차원 배열의 배열명은 배열을 가리키는 포인터에 저장해야 함
    • 그 역할을 수행하는 포인터가 배열 포인터
int arr[3][4] = {{5, 7, 3, 1}, {2, 8, 10, 4}, {9, 6, 4, 11}};   // 2차원 배열 초기화, 선언
int (*pa)[4];   // int형 변수 4개의 배열을 가리키는 포인터 선언

pa = arr;

// pa를 2차원 배열처럼 사용
for (int i = 0; i < 3; i++){
    for (int j = 0; j < 4; j++){
        printf("%4d", pa[i][j]);
    }
    printf("\n");
}

//    5   7   3   1
//    2   8  10   4
//    9   6   4  11
  • e.g., int형 원소가 4개인 1차원 배열을 가리키는 경우
    • int (*parr)[4]와 같이 선언
    • int *parr[4]처럼 선언하면, 배열 포인터가 아닌 포인터 배열이 되므로 주의할 것
    • 이때 parrint (*)[4] 형 포인터라고 부름

2차원 배열을 처리하는 함수

  • 배열을 처리하는 함수는 매개변수로 배열명, 즉 첫 요소를 가리킬 포인터가 필요함
  • 이때 2차원 배열의 첫 요소는 1차원 배열
    • 따라서, 포인터 배열을 처리하는 함수는 1차원 배열을 가리킬 배열 포인터를 매개변수로 가져야 함
#include <stdio.h>

// 2차원 배열을 출력하는 함수
void print_array(int (*)[4]);

int main(void){
    int arr[3][4] = {{1, 5, 7, 7}, {8, 2, 8, 2}, {2, 5, 8, 8}};
    print_array(arr);
    return 0;
}

// 매개변수는 배열 포인터
void print_array(int (*pa)[4]){
    for (int i = 0; i < 3; i++){
        for (int j = 0; j < 4; j++){
            printf("%3d", pa[i][j]);
        }
        printf("\n");
    }
}

// [출력]
//   1  5  7  7
//   8  2  8  2
//   2  5  8  8
  • pa[i][j]arr[i][j]와 동일한 결과 반환

2차원 배열 인덱싱

  • 위와 같이 int arr[3][4]로 배열을 선언했을 때, pa[1][2], 즉 arr[1][2]의 값은?
    • 배열이 100번지부터 시작한다고 가정
  • 이때 array[1][2]*(*(arr + 1) + 2)와 같은 연산
  • (1단계) arr + 1 계산
    • arrarr[0]의 주소, 덧셈 연산 시 sizeof(arr[0]) = 4 * 4 = 16만큼 증가
    • arr + 1 -> 100 + 1 * 16 = 116
  • (2단계) *(arr + 1) 계산
    • *(arr + 1)은 주소 116에 있는 부분배열 arr[1]의 시작 주소
    • 따라서 *(arr + 1) 역시 116
  • (3단계) arr[1][2]의 위치 확인
    • *(arr + 1)arr[1][0]의 주소, 덧셈 연산 시 sizeof(arr[1][0]) = 4만큼 증가
    • *(arr + 1) + 2 -> 116 + 2 * 4 = 124

void 포인터

  • 가리키는 자료형이 정해지지 않은 포인터
  • void *포인터명으로 선언
    • void 포인터의 자료형은 void *
int a = 35;
double b = 10.5;
void *vp;       // void 포인터 선언

vp = &a;        // int형 변수의 주소 저장
// 단, vp를 (int *)형으로 변환한 다음 사용해야 함
printf("a: %d\n", *(int *)vp);

vp = &b;
// 단, vp를 (double *)형으로 변환한 다음 사용해야 함
printf("b: %.1lf\n", *(double *)vp);

vp = "포인터 너무 어려워 ㅠㅠ";
// 단, vp를 (char *)형으로 변환한 다음 사용해야 함
printf("%s\n", (char *)vp);

// [출력]
// a: 35
// b: 10.5
// 포인터 너무 어려워 ㅠㅠ
  • void 포인터에 바로 간접참조 연산(*)이나 포인터 연산을 수행할 수 없음
    • 두 연산 모두 가리키는 데이터의 크기를 확실히 알아야 하기 때문
  • 원하는 형태의 포인터 ((int *), (double *))로 변환해 사용해야 함
  • 다른 포인터에 void 포인터를 대입할 수 있으나, 형 변환 후 대입하는 것을 권장
    • int *pi = (int *)vp와 같이

연습문제

다음과 같이 배열이 선언되어 있을 때, 각 값을 저장할 포인터를 선언하세요.
변수 선언은 p로 합니다.

char *ps[5] = {"baseball", "football", "volleyball", "swimming", "golf"}; // 포인터 배열
int arr[2][3] = {{1, 2, 3}, {4, 5, 6}}; // 2차원 배열
  • ps: char **p로 선언
    • pschar *형 포인터 배열
    • 배열명 ps는 배열의 첫 요소인 char *형 포인터를 가리킴
    • 따라서, char **이중 포인터로 저장
  • arr: int (*p)[3]으로 선언
    • arrint형 2차원 배열
    • 배열명 arr은 배열의 첫 요소인, 원소가 3개인 int형 배열을 가리킴
    • 따라서, int (*)[3]배열 포인터로 저장
  • arr[0]: int *p로 선언
    • arr[0]int형 1차원 배열
    • 부분배열명 arr[0]은 배열의 첫 요소인, int형 원소를 가리킴
    • 따라서, int *형 포인터로 저장
  • &ps[2]: char **p로 선언
    • ps[2]는 포인터 배열의 원소로, char *형 포인터
    • 따라서, char **이중 포인터로 저장
  • &arr: int (*p)[2][3]으로 선언
    • &arr은 2행 3열의 배열 전체를 나타내는 주소
    • 따라서, 배열 전체를 가리키는 int (*)[2][3]배열 포인터로 저장

2명의 나이와 키를 입력한 후 바꾸어 출력합니다.
단, 나이와 키를 바꾸는 함수는 int형과 double형을 모두 교환할 수 있도록 하나의 함수로 구현하세요.

  • void 포인터를 매개변수로 두어, 인수로 주소를 우선 입력받음
    • 이후 (int *)형 또는 (double *) 형으로 형변환
  • strcmp(문자열A, 문자열B)를 이용해 두 문자열 비교
    • 반환값이 0이면 동일 문자열
#include <stdio.h>
#include <string.h>

void swap(char* mode, void *pa, void *pb);

int main(void){
    int age_a, age_b;
    double height_a, height_b;
    printf("첫 번째 사람의 나이와 키 입력: ");
    scanf("%d %lf", &age_a, &height_a);
    printf("두 번째 사람의 나이와 키 입력: ");
    scanf("%d %lf", &age_b, &height_b);

    swap("int", &age_a, &age_b);
    swap("double", &height_a, &height_b);

    printf("첫 번째 사람의 나이와 키 : %d, %.1lf\n", age_a, height_a);
    printf("두 번째 사람의 나이와 키 : %d, %.1lf\n", age_b, height_b);

    return 0;
}

void swap(char* mode, void *pa, void *pb){
	// strcmp 이용해서 "int"인지 "double"인지 확인
    // 이후 void 포인터 형변환을 통해, 값을 서로 바꿈
    if (strcmp(mode, "int") == 0){
        int temp;
        temp = *(int *)pa;
        *(int *)pa = *(int *)pb;
        *(int *)pb = temp;
    } else if (strcmp(mode, "double") == 0){
        double temp;
        temp = *(double *)pa;
        *(double *)pa = *(double *)pb;
        *(double *)pb = temp;
    }
}
profile
뭔가 만드는 걸 좋아하는 개발자 지망생입니다. 프로야구단 LG 트윈스를 응원하고 있습니다.

0개의 댓글