2차원 배열과 포인터의 이해
초보자를 위한 C++ 200제(박준태 저) 85번 예제 입니다.
#include <iostream>
using namespace std;
void Func1(int arr[2][2])
{
arr[0][0] = 1000;
}
void Func2(int arr[][2], int row)
{
arr[row - 2][1] = 2000;
}
void Func3(int* arr, int row, int col)
{
*((arr + row - 1) + col - 1) = 3000;
}
int main()
{
int data[2][2] { { 1, 2 }, {3, 4} };
Func1(data);
Func2(data, 2);
Func3(*data, 2, 2);
cout << " == 결과 ==" << endl;
for(int i = 0; i < 2; i++)
{
for(int j = 0; j < 2; j++)
{
cout << data[i][j] << endl;
}
cout << endl;
}
return 0;
}
main 함수의 첫번째 행에 보면
int data[2][2] { {1, 2} , {3, 4} };
이 문장은 배열을 이런식으로도 선언 할 수 있다는것을 보여줍니다.
int data[2][2] = { {1, 2} , {3, 4} };
원래 이렇게 = 연산자를 사용하는 것이 맞지만 생략도 가능하네요.
Func3의 함수 정의부에서 쓰인 식 *((arr + row - 1)+col - 1) 이 처음에 어떤 알고리즘인지 궁금했는데
해설에서는 괄호식을 사용해서 매개변수 row, col을 조절하는 알고리즘인 것 처럼 쓰여 있지만 대입해보면 예측가능한 값이 나오지 않습니다.
2번째 행의 1번 열을 가져오고 싶어서 각각 row, col 값으로 각각 2, 1을 넣고 함수를 호출 했을 때와
1번째 행의 2번 열을 가져오고 싶어서 각각 1, 2를 넣고 함수를 호출 했을 때를 비교 해보면
*((arr + row - 1)+col - 1) 의 값이 각각 *((arr + 2 - 1) + 1 - 1) 과 *((arr + 1 - 1) + 2 - 1) 로 둘다 결국 *(arr+1)를 의미합니다. 바꿔말하면 둘다 a[1] 이죠.
이 Func3의 알고리즘을 사용자가 행과 열을 입력 해서 원하는 위치에 접근할 수 있도록 만들어 보겠습니다.
int data[3][3] = {{100, 200, 300}, {400, 500, 600}, {700, 800, 900}}; 배열이 있다고 가정 하겠습니다.
이 배열이 함수에 인수로 넘겨졌다고 하면 이런 모양이 됩니다.
Func1과 Func2 를 보면 매개변수도 2차원 배열 형태입니다.
Func1(int arr[2][2])
Func2(int arr[][2], int row)
이런 형태를 보면 컴파일러는 2차원 배열 형태를 보고
각각 이런 사실을 알게 됩니다.
int 2개 크기의 배열이 2개 선형으로 존재한다는것
int 2개 크기의 배열이 선형으로 존재한다는것 (몇개 존재하는지는 알 수 없음)
이렇게만 해주면 함수 안에서 arr[row+2][1] 처럼 2차원 배열 형태로 사용하는것이 가능합니다.
배열의 최대 열(colomn) 길이만 알면 2차원 배열 처럼 쓸 수가 있다는 것이죠. 행은 몰라도 되죠.
하지만, Func3의 모습을 보면 매개변수가 *arr와 같은 포인터 변수 형태 입니다.
제어권이 함수로 넘어오고 나면 컴퓨터가 활용할 수 있는 단서는 주소 값 하나, 그리고 그 주소가 가리키는 값이 int형이라는 점 2가지 뿐입니다.
이런 형태이더라도 몇가지 단서만 있으면 다차원 배열처럼 사용자가 접근 가능합니다.
행이 변경되더라도 데이터가 연속적이라는 것에는 변함이 없기 때문입니다. 포인터 연산을 사용하면 되죠.
위 이해를 바탕으로 최대 열 값만 알고 있으면 어떤 형태의 2차원 배열이 오더라도 입력값으로 원하는 값을 호출하는 함수의 알고리즘을 만들어 보았습니다.
#include <iostream>
using namespace std;
void Func4(int *arr, int row, int col, int maxcol)
{
*(arr + ((maxcol) * (row - 1) + (col - 1))) = 3000;
}
int main()
{
int data[3][3]{ {1,2,3},{4,5,6},{7,8,9} };
Func4(*data, 2, 3, 3);
Func4(*data, 3, 2, 3);
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
cout << data[i][j] << endl;
}
cout << endl;
}
}
직접 만들어본 Func4 함수의 알고리즘입니다.
배열의 최대 열(colomn) 길이를 인수로 넘겨받아서 2차원 배열처럼 활용 할 수 있게 됩니다.
void Func4(int *arr, int row, int col, int maxcol)
{
*(arr + ((maxcol) * (row - 1) + (col - 1))) = 3000;
}
테스트를 해보면 [4][4] [6][9] 배열 등, 2차원 배열이라면 크기에 상관없이 동작합니다.
2차원 배열 부터 인수가 포인터 타입인 이유
void Func(int *arr) {함수 본문}
위와 같은 함수가 있다고 가정 하겠습니다.
이 함수를 호출 할 때 1차원 배열 arr1[1]이 있다고 가정할 경우
Func(arr1) 이면 에러 없이 잘 작동합니다.
2차원 배열 arr2[2][2] 가 있다고 가정할 경우에는 이야기가 달라집니다.
Func(*arr2) 와 같이 호출 해야 하고
3차원 배열 arr3[3][3][3] 이 있을 경우에는
Func(**arr3) 와 같이 호출해야 합니다.
이 이유는 무엇일까요?
우리는 배열의 속성을 공부 하게되면서 배열 이름 arr 은 그자체로 주소값을 가지고 있다는 것을 알 수 있습니다.
이는 포인터의 속성과 유사합니다. 그래서 포인터를 배열처럼 사용할 수도 있고
배열이름에 포인터 연산을 사용할 수도 있었죠
아래는 이해를 돕기 위한 예제입니다.
#include <iostream>
using namespace std;
int main()
{
int arr[2]{ 1,4 };
int num = 100;
arr[0] = num;
for (int i = 0; i < 2; i++)
{
cout << arr[i] << endl;
}
cout << arr << "=" << &arr[0] << endl;
cout << *arr << "=" << arr[0] << endl;
}
1차원 배열에서 arr 자체가 포인터 속성을 가지고 있기 때문에 인수로 포인터 변수를 넘겨야 할 경우 별다른 처리 없이 Func(arr1)만 하면 끝입니다.
2차원 배열에서는 이야기가 달라집니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
#include <iostream>
using namespace std;
void ArrFunc(int* arr)
{/*Function Body*/}
int main()
{
int arr1[10] = { 1,2,3 };
int arr2[2][2] = { {1,2},{3,4} };
for (int i = 0; i < 10; i++)
{
cout << arr1[i] << " ";
}
cout << endl << endl;
for (int i = 0; i < 2; i++)
{
for (int j = 0; j < 2; j++)
{
cout << arr2[i][j] << endl;
}
cout << endl;
}
ArrFunc(arr1);
ArrFunc(*arr2);
//The line below causes compilation error
//ArrFunc(arr2);
int *ptr1, *ptr2;
ptr1 = arr1;
//this can be true in 1-demensional array
//1차원 배열에서는 이런 등식이 유효하게 동작할 수 있습니다.
cout << "ptr1 = arr1 = &arr1[0]" << endl;
cout << ptr1 << "=" << arr1 << "=" << &arr1[0] << endl;
//arr1 as the array name, have a address, that address meaning single type of int range, ptr can compatible with arr1.
//배열 이름인 arr1 은 주소를 가지고 있으며, 그 주소는 int 자료형 1개 만큼의 크기를 뜻하기에 같은 자료형을 가진 ptr1 과 호환 가능한 것입니다.
cout << "===================================" << endl << endl;
ptr2 = *arr2;
//The line below causes compilation error
//ptr2 = arr2;
cout << "ptr2 = *arr2 = &arr2[0][0]" << endl;
cout << ptr2 << "=" << *arr2 << "=" << &arr2[0][0] << endl;
cout << endl;
cout << "arr2 = arr2[0] != &arr2[0][0]" << endl;
cout << arr2 << "=" << arr2[0] << "!=" << &arr2[0][0] << endl;
cout << endl;
cout << "arr2 + 1 = arr2[1] != &arr2[1][0]" << endl;
cout << arr2 + 1 << "=" << arr2[1] << "!=" << &arr2[1][0] << endl;
//arr2 have a address too. However, it is not meaning single type of int range, But double type of int range.
//The name of 'arr2' means not single index's address like before but '1-demensional array's address.
//arr2 역시 주소를 가지고 있습니다. 하지만, 그것이 int type 하나의 주소를 의미하는것은 아닙니다. int 2개의 주소를 의미하죠.
//arr2 는 원소가 2개인 1차원 배열의 1차원 배열이기 때문입니다. 따라서 배열 이름은 열이 2개인 1차원 배열의 주소를 의미합니다.
cout << endl << endl;
cout << "(*arr2)[0] = " << (*arr2)[0] << endl;
cout << "(*arr2)[1] = " << (*arr2)[1] << endl;
cout << "(*arr2)[2] = " << (*arr2)[2] << endl;
cout << "(*arr2)[3] = " << (*arr2)[3] << endl << endl << endl;
//*arr2를 1차원 배열처럼 활용
cout << "*arr2[0] = " << *arr2[0] << endl;
cout << "*arr2[1] = " << *arr2[1] << endl << endl << endl;
//괄호가 없으면 arr2[0], arr2[1] 의 값이 되어 버립니다.
cout << "*(*arr2) = " << *(*arr2) << endl;
cout << "*(*arr2 + 1) = " << *(*arr2 + 1) << endl;
cout << "*(*arr2 + 2) = " << *(*arr2 + 2) << endl;
cout << "*(*arr2 + 3) = " << *(*arr2 + 3) << endl << endl << endl;
//1차원 배열의 덧셈 연산을 통해 값을 호출 할 수 있음
cout << "sizeof(*arr2) / sizeof(int) = " << sizeof(*arr2) / sizeof(int) << endl;
//*arr2 의 크기는 int 2개의 크기이다. int type 원소를 2개 갖고 있는 1차원 배열이기 때문이다.
cout << "sizeof*(*arr2 + 0) / sizeof(int) = " << sizeof(*(*arr2 + 0)) / sizeof(int) << endl;
cout << "sizeof(*arr2)[0] / sizeof(int) = " << sizeof(*arr2)[0] / sizeof(int) << endl;
cout << "sizeof(arr2) / sizeof(int) = " << sizeof(arr2) / sizeof(int) << endl;
cout << "*ptr2 = " << *ptr2 << endl;
cout << "ptr2[1] = " << ptr2[1] << endl;
return 0;
}
|
cs |
2차원 배열에서 arr2는 이름 그자체로 주소를 가지고 있기는 하지만 1차원 배열때와는 다르게 이는 int 1개의 주소가 아닌 int 가 n개 있는 배열의 주소 입니다.(여기서 n은 배열의 열 길이를 의미함 )
즉, 2차원 배열은 1차원 배열을 원소로 가진 1차원 배열을 의미하며, 여기서 부터 배열 이름은 인덱스 1개의 주소가 아닌,1차원 배열 구성원 전체의 주소를 의미하게 되는 겁니다.
여기서 배열의 성질을 하나 알고 가면 됩니다.
배열의 이름은 배열이 시작하는 첫번째 원소(element)의 주소를 가리키는 포인터를 의미한다.
Index 0의 주소이다.
2차원 배열은 1차원 배열을 구성원으로 가진 1차원 배열임을 그림으로 표현해 보았습니다.
cout << arr2 << ", " << *arr2;
여기서 위 문장을 통해 출력을 해본다고 가정하겠습니다. 둘다 100, 100 으로 출력 될 것입니다.
이것은 함정입니다. 주소값이 같다고 해서 같다고 생각하면 안됩니다.
처음 출력된 100은 사실상 100, 200이 합쳐져 있지만 100이라고 표현한 것이고, 나중에 출력된 100은 100 그 자체만을 의미하는 것입니다. 포인터의 크기는 4바이트로 동일하기 때문입니다.
아래 그림은 문법적으로 틀린 표현이지만 이해를 돕기 위해 작성 해 보았습니다.
여기서 2차원 배열명인 arr2에 * 포인터 연산자를 사용하게 되어 *arr2 가 되면 arr2 가 가리키고 있는 주소에 있는 값을 뜻하죠?
이 값이 1차원 배열인 겁니다. 이 때부터는 *arr2가 의미하는 것이 1차원 배열인 것이죠.
*arr2로 1차원 배열이 할 수 있는 연산을 동일하게 할 수 있어요.
예를 들면 *(arr2 + 1) 과 같은 포인터의 덧셈 연산도 가능합니다.
덧셈 연산은 다음번 주소를 뜻하죠. 덧셈 연산을 할 때는 괄호에 주의해야 하죠, 괄호가 많아질 수록 코드 보는것도 힘들어집니다.
그래서 포인터 변수를 사용하는것이죠.(94,95번행)
이것이 2차원 배열부터 인수로 사용 될 때 *를 사용하는 이유입니다.
2차원 배열은 1차원 배열의 1차원 배열이다.
배열의 이름은 Index 0의 주소를 가지고 있는 포인터이다.