Home C언어 (11) 포인터와 배열2
Post
Cancel

C언어 (11) 포인터와 배열2

C언어 (11) 포인터와 배열2

포인터와 배열의 관계를 이어서 더 보자.

포인터 변수를 통한 1차원 배열 접근

배열의 이름은 배열의 시작 주소와 같다고 했다.
그리고 배열의 주소에 +1, +2를 하면 주소는 배열의 요소 크기만큼 증가한다.

1
2
3
4
5
6
7
8
#include <stdio.h>
int main() {
    int arr[5] = { 1, 2, 3, 4, 5 };
    double ar[5];
    printf("%p %p %p\n", arr, arr+1, arr+2);
    printf("%p %p %p\n", ar, ar+2, ar+4);
    return 0;
}
1
2
00000018DC3BF860 00000018DC3BF864 00000018DC3BF868
00000018DC3BF878 00000018DC3BF888 00000018DC3BF898

윗 줄은 int의 크기만큼 4씩 증가, 밑 줄은 주소에 +2씩 하므로 double의 크기 8*2 = 16씩 증가한다.
주소에 참조해 값을 가져오는 *연산자를 사용하면 배열의 값에도 접근이 가능하다.

1
2
3
4
5
6
#include <stdio.h>
int main() {
    int arr[5] = { 1, 7, 3, 4, 5 };
    printf("%d %d %d %d\n", *arr, *(arr+1), *(arr+2), *arr+1);
    return 0;
}
1
1 7 3 2

이렇게 *(배열의 이름+i)를 통해 i번째 원소에 접근할 수 있다. 이 때 꼭 괄호가 필요하다.
괄호가 없으면 *연산자가 +보다 우선순위가 높아 *arr+1은 *arr로 1을 가져온 후 +1이 되어 2가 출력된다.

이제 포인터 변수에 배열의 이름을 쓰면, 포인터를 통해 배열에 접근이 가능하다..

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() {
    int arr[5] = { 1, 7, 3, 4, 5 };
    int* p = arr;
    printf("%d %d %d\n", *p, *(p+1), *(p+2));
    p++;
    printf("%d %d %d\n", *(p-1), *p, *(p+1));
    return 0;
}

p++로 증가시키면, 각각의 요소에 배열에 접근할 때 -1을 해주면 똑같은 결과가 나온다.

[ ] 연산자

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
int main() {
    int arr[5] = { 1, 7, 3, 4, 5 };
    int* p = arr;
    printf("%d %d %d\n", *(p+0), *(p+1), *(p+2));
    printf("%d %d %d\n", p[0], p[1], p[2]);
    p[0] = 15;
    *(p + 1) = 20;
    printf("%d %d %d\n", *(p+0), *(p+1), *(p+2));
    printf("%d %d %d\n", p[0], p[1], p[2]);
    return 0;
}

이렇게 *(p + 1) = 20;로 배열의 값을 수정할 수도 있다.
알 수 있는 것은 p[0]과 *(p+0)은 완전히 똑같은 기능을 한다.
그 이유는 배열에서 [] 연산자는 배열의 특정 원소를 접근한다는 의미이기 때문이다.
즉 arr[i]라는 표현은 *(arr+i)와 동일하다.

포인터 변수를 통한 2차원 배열 접근

그러면 2차원 배열에는 어떻게 접근할 수 있을까?
image

이차원 배열의 주소의 관계는 이렇다.
이차원 배열이란 배열의 배열이므로, arr[5]의 배열에서 시작 주소가 arr이었던 것 처럼
arr[5][5]의 배열에서는 arr[i]가 주소를 표현하게 된다.
image

arr[i]가 각각의 배열의 시작 주소가 되는 것이다.
이제 접근하는 방법은 같다.

1
2
3
4
5
6
7
8
#include <stdio.h>
int main() {
    int arr[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    printf("%d %d %d\n", *(*(arr + 0) + 0), *(*(arr + 0) + 1), *(*(arr + 0) + 2));
    printf("%d %d %d\n", *(*(arr + 1) + 0), *(*(arr + 1) + 1), *(*(arr + 1) + 2));
    printf("%d %d %d\n", *(*(arr + 2) + 0), *(*(arr + 2) + 1), *(*(arr + 2) + 2));
    return 0;
}

*(arr+0)은 arr[0]이고, arr[0][1]에 접근하고 싶으면 *(*(arr+0)+1)을 하면 된다.
그러면 포인터 변수로는 어떻게 접근을 해야할까?

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() {
    int arr[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    int* p = arr;
    printf("%d %d %d\n", *(*(p + 0) + 0), *(*(p + 0) + 1), *(*(p + 0) + 2));
    printf("%d %d %d\n", *(*(p + 1) + 0), *(*(p + 1) + 1), *(*(p + 1) + 2));
    printf("%d %d %d\n", *(*(p + 2) + 0), *(*(p + 2) + 1), *(*(p + 2) + 2));
    return 0;
}

이렇게 int *p = arr을 넣은다음 arr대신 포인터 변수 p를 넣으면 될까?
이것은 오류가 발생하는 코드이다.
p는 1차원 포인터이므로 2차원 배열의 주소를 저장해도 1차원 배열처럼만 접근이 가능하다.
image

이 그림을 다시 보면 2차원이라고 표현하지만 실제로는 선형적으로 데이터가 들어있다.
그러므로 arr[1][0]은 *(arr+3)이라고 표현 할 수 있는 것이다.(12byte 차이이므로)

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() {
    int arr[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    int* p = arr;
    printf("%d %d %d\n", *(p + 0), *(p + 1), *(p + 2));
    printf("%d %d %d\n", *(p + 3), *(p + 4), *(p + 5));
    printf("%d %d %d\n", *(p + 6), *(p + 7), *(p + 8));
    return 0;
}

그러면 이렇게 이차원 배열에 접근 할 수 있다.
이렇게 1차원 포인터로 접근할 수 있지만 알아보기에 좋다고 말할 수는 없을 것 같다.
*(p+7)이 배열의 무슨 요소인지 한 눈에 보이지 않기 때문이다.

위로 돌아가서 1차원 포인터에 2차원 배열의 주소를 저장하는 것이 문제라면 2차원 포인터를 사용하면?

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() {
    int arr[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    int** p = arr;
    printf("%d %d %d\n", *(*(p + 0) + 0), *(*(p + 0) + 1), *(*(p + 0) + 2));
    printf("%d %d %d\n", *(*(p + 1) + 0), *(*(p + 1) + 1), *(*(p + 1) + 2));
    printf("%d %d %d\n", *(*(p + 2) + 0), *(*(p + 2) + 1), *(*(p + 2) + 2));
    return 0;
}

이렇게 p를 이차원 포인터로 선언했지만 실행해보면 결과는 출력되지 않는다.
*(*(p + i) + j)는 arr[i][j]의 값을 가져오기 위해 사용하는 것이다.
그런데 프로그램이 arr[i][j]에 접근하려면 어떤 연산 과정을 거쳐야 할까?

arr[i][j]와 arr[0][0]의 주소값 차이를 다시 계산해보자.
image

주소값의 차이는 (i * (배열의 크기) + j) * (자료형의 크기) 이다.
이 때 배열의 크기란 2차원 배열 중 1차원 배열 하나의 크기를 의미한다.
하지만 int **p = arr이라는 식에는 배열의 크기에 대한 정보가 없다.
그렇기 때문에 주소의 차이를 계산할 수 없어 배열에서 값을 정확히 꺼내올 수가 없게 되는 것이다.

이제 배열의 크기를 제대로 전해주면 2차원 배열의 요소에 접근할 수 있음을 알았다.
이 문제를 해결하기 위해 배열 포인터라는 것이 존재한다.

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() {
    int arr[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    int (*p)[3] = arr;
    printf("%d %d %d\n", *(*(p + 0) + 0), *(*(p + 0) + 1), *(*(p + 0) + 2));
    printf("%d %d %d\n", *(*(p + 1) + 0), *(*(p + 1) + 1), *(*(p + 1) + 2));
    printf("%d %d %d\n", *(*(p + 2) + 0), *(*(p + 2) + 1), *(*(p + 2) + 2));
    return 0;
}

이렇게 3열짜리 2차원 배열의 주소를 저장할 수 있는 배열 포인터 p를 선언한다.
그리고 실행을 해보면 원하는 결과가 출력된다.
int (*p)[3]에서 [3]이 배열의 크기 정보이다. 만약 배열의 크기가 달라지면 저 숫자도 달라진다.

또 1차원 배열에서 arr[i]가 *(arr+i)와 같은 것처럼 ar[i][j]는 *(*(ar+i)+j)와 같다.

1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main() {
    int arr[3][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    int(*p)[3] = arr;
    printf("%d %d %d\n", p[0][0], p[0][1], p[0][2]);
    printf("%d %d %d\n", p[1][0], p[1][1], p[1][2]);
    printf("%d %d %d\n", p[2][0], p[2][1], p[2][2]);
    return 0;
}

그러므로 이렇게 바꿔도 같은 결과가 나온다.
위의 코드보다는 이 코드가 더 직관적인 것 같다.

포인터 배열

위에서 특정 크기의 배열의 주소를 저장할 수 있는 배열 포인터를 봤다.
그런데 주소를 저장하는 포인터로 이루어진 배열도 존재한다.
여러개의 포인터를 만들어야 한다면 배열로 저장하는 것이 편할 것이다.

1
2
3
4
5
6
7
8
#include <stdio.h>
int main() {
    int a, b, c;
    int* p[3] = { &a, &b, &c };
    printf("%p %p %p\n", &a, &b, &c);
    printf("%p %p %p\n", p[0], p[1], p[2]);
    return 0;
}

사용 방법은 배열과 똑같다. 그냥 자료형이 포인터인 요소를 저장하는 배열이다.

포인터 배열과 배열 포인터의 차이

이 두개는 이름도 비슷하고 생긴것도 비슷해서 혼란스러울만 하다.

  • 배열 포인터
    • 선언 방법 : 타입 (*포인터 이름)[배열 크기];
    • 사용처 : 배열을 가리키는 포인터 (특정 크기의 배열을 가리킴)
    • 예제 : int (*p)[3]; → 3열짜리 2차원 배열의 주소 저장
    • 배열의 첫 번째 원소를 가리키는 포인터. 배열을 가리키며, 배열 크기를 알아야 함.
  • 포인터 배열
    • 타입* 포인터 이름[배열 크기];
    • 사용처 : 배열의 각 원소가 포인터인 배열 (포인터들을 저장)
    • 예제 : int* p[3]; → 3개의 int* 포인터를 저장하는 배열
    • 포인터 배열은 여러 포인터를 원소로 가지며, 각 포인터가 다른 변수나 배열을 가리킬 수 있음.

포인터 배열 예시

1
2
3
4
int arr[3][3] = {
	{1,2,3}, {4,5,6}, {7,8,9}
	};
int (*p)[3] = arr;

배열 포인터 예시

1
2
int a = 10, b = 20, c = 30;
int* p[3] = {&a, &b, &c};

포인터 배열은 배열, 배열 포인터는 포인터이다!

포인터 변수의 상수화

변수와 상수에서 const 키워드를 통해 어떤 변수를 상수로 바꿀 수 있다고 했다.

1
2
3
4
5
6
#include <stdio.h>
int main() {
    const int a = 3;
    a = 5; //a는 상수이므로 변경 불가능
    return 0;
}

그러면 포인터를 상수화한다는 것은 무슨 의미가 있을까?

  1. 포인터 변수에 다른 메모리 공간의 주소를 저장하지 못하게 한다.
  2. 포인터 변수를 통해 메모리 공간의 값을 변경하지 못하게 한다.
  3. 1번과 2번을 둘 다 못하게 한다.

이렇게 3가지로 사용할 수 있다. 각각 예제를 보자.

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main() {
    int num = 10, num2 = 20;
    int* const pa = &num; //pa = &num 부분을 상수화
    printf("%d", *pa); //10 출력
    *pa = 20; //*pa의 값을 20으로 바꿈
    printf("%d", *pa); //20 출력
    pa = &num2; //상수 pa를 변경하려해서 오류가 발생
    return 0;
}

우선 1번의 경우이다. const가 int* pa 중간에 들어있다.
이러면 상수화되는 범위는 pa = &num이므로, *pa를 통해 num에 접근해 값을 바꿀 수 있다.
하지만 pa = &num2는 상수 pa를 변경하려 하기에 오류가 발생한다.
이것이 포인터 변수에 다른 메모리 공간의 주소를 저장하지 못하게 한다는 것이다.

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main() {
    int num = 10, num2 = 20;
    const int* pa = &num; //pa = &num 부분을 상수화
    printf("%d", *pa); //10 출력
    *pa = 20; //오류 발생
    printf("%d", *pa); //20 출력
    pa = &num2; //&num2로 바뀜
    return 0;
}

이번에는 2번의 경우로 const가 int* pa 앞에 있다.
이러면 상수화되는 범위는 *pa = &num이므로 *pa를 통해 값을 바꿀 수 없다.
하지만 pa는 상수가 아니기 때문에 pa=&num2를 통해 값을 바꿀 수 있다.
이것이 포인터 변수를 통해 메모리 공간의 값을 변경하지 못하게 한다는 것이다.

그러면 1번과 2번을 둘 다 못하게 하려면 어떻게 해야할까?

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
int main() {
    int num = 10, num2 = 20;
    const int* const pa = &num; //pa = &num 부분을 상수화
    printf("%d", *pa); //10 출력
    *pa = 20; //오류 발생
    printf("%d", *pa); //20 출력
    pa = &num2; //&num2로 바뀜
    return 0;
}

이렇게 2번의 const를 사용해주면 1번과 2번을 둘 다 못하게 할 수 있다.
포인터 변수가 저장하는 주소나 간접 접근하는 값을 수정하지 못하도록 프로그램의 안정성을 위해 사용된다.


포인터를 글 3개에 거쳐 설명했는데 중요한 부분이니까 제대로 이해하자.
처음 보면 무슨 소리인가 싶을 수 있지만 코드 몇 번 따라서 해보면 다 이해하는 것 같다.
이제 다음에 할 것은 int main()과 같은 함수라는 주제다.
이 함수가 어떤 값을 주고 받는지를 보며 지금까지 한 배열과 포인터의 필요성을 알 수 있다.

This post is written by PRO.