티스토리 뷰

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;
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/07   »
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
글 보관함