Array와 Pointer의 관계
메모리 주소에 직접적으로 접근할 수 있다는 장점이 있는 Pointer라는 개념은 Array와 밀접한 관련이 있다. 왜냐하면 Array는 메모리에 연속적으로 값을 저장하고, Array를 초기화한 변수 자체는 Array의 첫 번째 요소의 주소를 담고 있기 때문이다. 다음 예시를 통해 Array와 Pointer의 밀접한 관계를 이해할 수 있겠다.
#include <iostream>
using namespace std;
int main()
{
int x[] = { 2, 3, 4, 5 };
cout << x << endl // 0x7ffca35d1f10
<< &x[0] << endl // 0x7ffca35d1f10
<< &x[1] << endl // 0x7ffca35d1f14
<< &x[2] << endl // 0x7ffca35d1f18
<< &x[3] << endl // 0x7ffca35d1f1c
return 0;
}
x 자체로는 x의 첫 번째 요소의 메모리 주소를 의미한다. 따라서 x와 &x[0]을 출력하면 같은 값이 나오는 것을 알 수 있다.
또 바로 옆 요소는 배열이 연속적으로 메모리에 저장되므로, int의 크기인 4 bytes만큼 차이가 나는 것을 알 수 있다.
x[3] = 4;
*(x+3) = 4;
이어서 해당 코드를 살펴보자. x는 그 자체로 x의 첫 번째 요소의 메모리 주소를 담고 있고, 거기에 +3을 하게 되면 해당 값만큼 메모리 주소를 옆으로 이동한다. 이어서 그 값을 참조한 *(x+3)은 역시 4를 나타낸다. 따라서 해당 두 코드는 같은 의미를 담고 있다.
int x[] = { 3, 4, 5, 6, 7 };
int* y = x + 2;
int* z = y - 1;
cout << z[3] << endl;
한 단계 더 나아가보자. 이번에는 좀 더 생각해 볼게 많아 보인다. 먼저 y는 x의 첫 번째 요소의 메모리 주소보다 두 칸 더 이동한 녀석, 즉 x[2]의 메모리 주소를 담고 있다. 다음으로 z는 x[2]에서 왼쪽으로 한 칸 이동한 녀석, 즉 x[1]의 메모리 주소를 담고 있다. 그렇다면 z[3]은 뭘까? z의 입장에서는 x의 2번째 요소가 자신에게는 첫 번째 요소이므로, z[3]은 x[1]에서 3칸 더 이동한 x[4]를 가리키고, 따라서 7을 출력하게 된다.
Arrays cannot be passed to arrays
배열은 다른 배열로 보낼 수 없다라는 말은 무슨 말일까? 말 그대로다. 배열을 어떤 함수의 매개변수로 전달할 수도 없고, 다른 배열에 할당하는 것이 불가능하다는 것. 이때 필요한 것이 Pointer다. 앞서 말했듯 배열의 변수명은 그 자체로 배열의 첫 번째 요소의 메모리 주소를 담고 있기에, 포인터 변수를 매개변수로 하여 배열을 할당하거나 함수를 사용할 수 있다. 해당 예시를 살펴보자.
int foo(int* x) {
return x[0] + x[1];
}
int main() {
int z[] = { 3, 4, 5 };
cout << foo(z) << endl;
return 0;
}
foo라는 함수는 int형의 포인터 변수가 매개변수이다. 따라서 해당 위치에는 어떤 메모리 주소가 들어가야 한다. main함수에서 foo(z)을 출력하였는데, z는 z배열의 첫 번째 요소의 메모리 주소가 해당된다. 따라서 x[0]는 3, x[1]은 4이므로 출력값은 7이 나오게 된다.
Passing in the size of the array
배열의 값을 전달할 때 배열의 첫번째 요소의 메모리 주소를 보내는 것만으로는 부족하다. 위와 같은 예시에서 만약 z의 요소가 하나밖에 없다면, foo함수에서 x[1]에 해당하는 값이 없으므로 에러가 발생할 것이다. 따라서 우리는 배열을 보내줄 때에 배열의 크기도 같이 보내주는 방식을 이용해야 한다. 해당 예시를 살펴보자.
double sum(double* vals, size_t n) {
double s = 0;
for (size_t i = 0; i < n; i ++) {
s += vals[i];
}
return s;
}
int main() {
double x[] = { 7e2, 3.1e-5, -6 };
cout << sum(x, sizeof x / sizeof x[0]) << endl;
return 0;
}
sizeof는 해당 변수의 메모리 크기를 의미한다. 요소가 3개인 x의 크기는 24bytes이고, 요소 하나의 크기는 8 bytes이므로 배열의 요소 개수는 3이 나오게 된다. 따라서 sum함수는 x의 첫 번째 요소의 메모리 주소와 크기를 받아 오버플로우 에러를 내지 않고 안전하게 연산을 수행할 수 있게 된다.
Changing Value vs Memory Address
어떤 배열이 있고, 이 배열의 최솟값과 총합을 구하는 문제가 있다고 생각해보자. 총합을 먼저 구해야 하는 경우에는 상관없겠지만, 최솟값을 먼저 구해야 하는 상황이라면 조심해야 할 것이다. 해당 예시를 살펴보자.
#include <iostream>
#include <iomanip>
using namespace std;
int main()
{
long prices[] = {5000, 3500, 15000, 30000, 8000};
long *smallest = &prices[0];
for (size_t i = 0; i < sizeof prices / sizeof prices[0]; i++)
{
if (prices[i] < *smallest)
*smallest = prices[i];
}
cout << "Your cheapest item had a price of " << *smallest << endl;
long total = 0;
for (size_t i = 0; i < sizeof prices / sizeof prices[0]; i++)
{
cout << setw(10) << prices[i] << '\n';
total += prices[i];
}
cout << "Total: " << total << endl;
return 0;
}
smallest는 prices배열 첫 번째 요소의 메모리 주소를 담는다. 반복문을 돌리면서 smallest가 참조하는 값을 더 작은 값으로 할당시켜주며 최솟값을 찾아내는 방식이다. 해당 방식을 이용한다면 최솟값을 구하는 데에는 문제가 없겠지만 해당 메모리 주소의 값이 실질적으로 변경이 되므로, 총합을 구할 때에는 prices = { 3500, 3500, 15000, 30000, 8000 }으로 첫 번째 요소의 값이 변경되어 다른 값을 출력하게 될 것이다. 따라서 우리는 smallest가 참조하는 값을 변경시키는 것이 아니라, smallest의 메모리 주소를 prices 최솟값의 메모리 주소로 바꿔주고, 참조를 통해 출력해줘야 한다.
long prices[] = {5000, 3500, 15000, 30000, 8000};
long *smallest = &prices[0];
for (size_t i = 0; i < sizeof prices / sizeof prices[0]; i++)
{
if (prices[i] < *smallest)
smallest = &prices[i];
}
cout << "Your cheapest item had a price of " << *smallest << endl;
'Computer Science and Engineering > OOP (Object Oriented Programming)' 카테고리의 다른 글
[Syntax, C++] struct, enum, union을 활용하여 계산기 구현하기 (0) | 2023.05.02 |
---|---|
[Syntax, C++] struct를 활용하여 다각형의 둘레의 길이 구하기 (0) | 2023.04.27 |
[Syntax, C++] struct, enum class를 활용하여 날짜(D - day) 계산하기 (0) | 2023.04.27 |
[Syntax, C++] 입력값과 선언한 자료형이 다를 때는 어떻게 처리해야 할까? (with simple I/O, ignore and clea (0) | 2023.03.14 |
[Syntax, C] - 포인터(Pointer)란? (0) | 2023.03.08 |