icon
7장 : 포인터와 참조

포인터와 배열

지금까지 포인터가 메모리 주소를 저장하고 접근하는 도구임을 배웠고, 참조자는 변수의 별명으로 사용됨을 이해했습니다.

이제 포인터의 가장 흔하고 강력한 응용 분야 중 하나인 배열(Array) 과의 관계를 깊이 있게 살펴보겠습니다.

C++에서 배열 이름은 특정 문맥에서 첫 번째 요소의 주소로 해석됩니다.

이러한 특성 때문에 포인터와 배열은 매우 밀접하게 관련되어 있으며, 서로 변환 가능하고 유사한 문법으로 사용될 수 있습니다.

이 장에서는 배열 이름이 포인터처럼 동작하는 원리, 포인터 산술 연산, 그리고 배열과 포인터의 차이점에 대해 상세히 알아보겠습니다.


배열 이름은 첫 번째 요소의 주소이다

C++에서 배열의 이름은 특별한 의미를 가집니다.

일반적으로 배열 이름은 배열의 첫 번째 요소의 메모리 주소를 나타내는 상수 포인터(constant pointer) 처럼 동작합니다.

배열 이름과 주소
#include <iostream>

int main() {
    int arr[5] = {10, 20, 30, 40, 50}; // 5개 int 요소를 가진 배열

    // 배열 이름 arr 자체의 주소 (배열 전체가 시작하는 주소)
    std::cout << "배열 arr의 주소 (&arr): " << &arr << std::endl;

    // 배열의 첫 번째 요소 arr[0]의 주소
    std::cout << "arr[0]의 주소 (&arr[0]): " << &arr[0] << std::endl;

    // 배열 이름 arr이 가리키는 주소 (첫 번째 요소의 주소와 동일)
    std::cout << "배열 이름 arr 자체의 값: " << arr << std::endl;

    /* 예상 출력 (주소는 시스템마다 다름):
    배열 arr의 주소 (&arr): 0x7ffee0000000
    arr[0]의 주소 (&arr[0]): 0x7ffee0000000
    배열 이름 arr 자체의 값: 0x7ffee0000000
    */

    return 0;
}

위 예시에서 볼 수 있듯이, arr (배열 이름)과 &arr[0] (첫 번째 요소의 주소)는 동일한 메모리 주소를 나타냅니다. 이는 배열 이름이 첫 번째 요소의 주소를 값으로 가지는 포인터처럼 행동한다는 것을 의미합니다.

이러한 특성 때문에, 배열 이름을 포인터 변수에 대입할 수 있습니다.

배열 이름을 포인터에 대입
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr; // arr은 arr[0]의 주소이므로, p는 arr[0]을 가리키게 됨
std::cout << *p << std::endl; // 출력: 1 (p가 가리키는 값)
std::cout << p[1] << std::endl; // 출력: 2 (포인터도 배열처럼 인덱스 접근 가능)

포인터 산술 연산 (Pointer Arithmetic)

포인터에 정수를 더하거나 빼는 포인터 산술 연산은 배열 요소를 순회하는 데 매우 유용합니다.

일반적인 숫자 연산과 달리, 포인터 산술 연산은 포인터가 가리키는 데이터 타입의 크기를 고려하여 주소를 이동합니다.

  • 포인터 + 정수: 포인터가 가리키는 주소에서 (정수 * 데이터타입의 크기)만큼 메모리 주소를 증가시킵니다. 이는 배열의 다음 요소로 이동하는 것과 같습니다.
  • 포인터 - 정수: 포인터가 가리키는 주소에서 (정수 * 데이터타입의 크기)만큼 메모리 주소를 감소시킵니다. 이는 배열의 이전 요소로 이동하는 것과 같습니다.
  • 포인터1 - 포인터2: 동일한 배열 내의 두 포인터 간의 거리를 (요소의 개수) 반환합니다.
포인터 산술 연산 예시
#include <iostream>

int main() {
    int numbers[] = {10, 20, 30, 40, 50}; // int 타입은 보통 4바이트
    int* p = numbers; // p는 numbers[0]의 주소를 가리킴

    std::cout << "p가 가리키는 값: " << *p << std::endl; // 출력: 10
    std::cout << "p의 주소: " << p << std::endl;

    p++; // p를 다음 int형 요소의 주소로 이동 (현재 주소 + sizeof(int) 만큼)
    std::cout << "\np++ 후:" << std::endl;
    std::cout << "p가 가리키는 값: " << *p << std::endl; // 출력: 20
    std::cout << "p의 주소: " << p << std::endl; // 이전 주소 + 4바이트 (64비트 시스템 가정)

    p += 2; // p를 2칸 더 이동 (현재 주소 + 2 * sizeof(int) 만큼)
    std::cout << "\np += 2 후:" << std::endl;
    std::cout << "p가 가리키는 값: " << *p << std::endl; // 출력: 40
    std::cout << "p의 주소: " << p << std::endl; // 이전 주소 + 8바이트

    p -= 3; // p를 3칸 뒤로 이동 (현재 주소 - 3 * sizeof(int) 만큼)
    std::cout << "\np -= 3 후:" << std::endl;
    std::cout << "p가 가리키는 값: " << *p << std::endl; // 출력: 10
    std::cout << "p의 주소: " << p << std::endl; // 원래 주소로 돌아옴

    // 포인터 인덱싱 (배열 인덱싱과 동일)
    std::cout << "\n포인터 인덱싱: " << p[2] << std::endl; // 출력: 30 (p가 가리키는 주소 + 2 * sizeof(int) 위치의 값)

    // 배열 순회 예시
    std::cout << "\n포인터로 배열 순회:" << std::endl;
    for (int* currentPtr = numbers; currentPtr < numbers + 5; ++currentPtr) {
        std::cout << *currentPtr << " ";
    }
    std::cout << std::endl;

    return 0;
}

배열 이름과 포인터의 차이점 (중요!)

비록 배열 이름이 포인터처럼 동작하지만, 몇 가지 중요한 차이점이 있습니다.

  1. 값 변경 가능성

    • 포인터 변수: 다른 주소를 가리키도록 변경할 수 있습니다. (p = &arr[2];)
    • 배열 이름: 배열 이름 자체는 상수 포인터이므로, 다른 주소를 가리키도록 변경할 수 없습니다. (arr = &arr[2];는 컴파일 오류)
  2. sizeof 연산자

    • 포인터 변수: 포인터 변수 자체의 크기(예: 8바이트 for 64비트 시스템)를 반환합니다.
    • 배열 이름: 배열 전체의 총 크기(sizeof(요소타입) * 요소개수)를 반환합니다.
    sizeof 배열과 포인터
    #include <iostream>
    
    int main() {
        int arr[5] = {1, 2, 3, 4, 5};
        int* p = arr;
    
        std::cout << "sizeof(arr): " << sizeof(arr) << " bytes" << std::endl; // 5 * sizeof(int) = 20 (assuming int is 4 bytes)
        std::cout << "sizeof(p): " << sizeof(p) << " bytes" << std::endl;     // 8 (on a 64-bit system)
    
        return 0;
    }

    이 차이는 특히 배열을 함수에 인자로 전달할 때 중요합니다. 배열을 함수에 전달하면 실제로는 배열의 첫 번째 요소에 대한 포인터가 전달되기 때문에, 함수 내에서 sizeof 연산자를 사용하면 배열 전체의 크기가 아니라 포인터의 크기가 반환됩니다.

  3. 대입 연산자

    • 포인터 변수: 다른 포인터 값을 대입할 수 있습니다. (p1 = p2;)
    • 배열 이름: 배열을 다른 배열에 직접 대입할 수 없습니다. 요소별로 복사해야 합니다. (arr1 = arr2;는 오류)

C-스타일 문자열과 포인터

C-스타일 문자열은 널(\0) 문자로 끝나는 char 배열이므로, 당연히 포인터와 밀접한 관계를 가집니다. char 포인터는 문자열을 다루는 데 자주 사용됩니다.

C-스타일 문자열과 포인터
#include <iostream>
#include <cstring> // strlen, strcpy 등을 위해

int main() {
    char greeting[] = "Hello World"; // char 배열이자 C-스타일 문자열
    char* pChar = greeting;          // char 포인터가 문자열 시작을 가리킴

    std::cout << "greeting: " << greeting << std::endl; // 출력: Hello World
    std::cout << "pChar: " << pChar << std::endl;       // 출력: Hello World (cout은 char*를 문자열로 해석)

    // 포인터 산술 연산으로 문자열 일부 접근
    std::cout << "pChar + 6: " << (pChar + 6) << std::endl; // 출력: World

    // strlen은 char*를 인자로 받음
    std::cout << "문자열 길이: " << strlen(greeting) << std::endl; // 출력: 11
    std::cout << "문자열 길이 (pChar로): " << strlen(pChar) << std::endl; // 출력: 11

    // 포인터를 사용하여 문자열 요소 변경
    pChar[0] = 'h';
    std::cout << "변경된 문자열: " << greeting << std::endl; // 출력: hello World

    return 0;
}

std::coutchar* 타입을 만나면 이를 메모리 주소가 아니라 널 종료된 C-스타일 문자열로 해석하여 출력하는 특별한 규칙을 가지고 있습니다.


다차원 배열과 포인터 (간략히)

2차원 배열은 '배열의 배열'이므로, 포인터로 다룰 때 더 복잡해집니다.

int matrix[2][3]은 두 개의 int[3] 배열로 구성된 것으로 볼 수 있습니다.

  • matrix: int[3] 배열을 가리키는 포인터. (정확히는 int(*)[3] 타입)
  • matrix[0]: int 요소를 가리키는 포인터 (int*).
다차원 배열과 포인터
#include <iostream>

int main() {
    int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};

    // matrix는 첫 번째 행(배열)의 주소
    std::cout << "matrix: " << matrix << std::endl;       // 첫 번째 행 배열의 주소
    std::cout << "matrix[0]: " << matrix[0] << std::endl; // 첫 번째 행 배열의 첫 번째 요소의 주소 (동일)
    std::cout << "&matrix[0][0]: " << &matrix[0][0] << std::endl; // (동일)

    // matrix[0]는 int* 타입처럼 동작
    std::cout << "matrix[0][1]: " << matrix[0][1] << std::endl; // 2
    std::cout << "*(matrix[0] + 1): " << *(matrix[0] + 1) << std::endl; // 2 (포인터 산술)

    // matrix 자체에 대한 포인터 산술은 다음 행으로 이동
    std::cout << "*(matrix + 1)[0]: " << *(matrix + 1)[0] << std::endl; // 4 (두 번째 행의 첫 요소)
    std::cout << "*(*(matrix + 1) + 0): " << *(*(matrix + 1) + 0) << std::endl; // 4 (더 명시적인 포인터 문법)

    return 0;
}

다차원 배열과 포인터의 관계는 초보자에게 혼란스러울 수 있으므로, 처음에는 간단한 1차원 배열과 포인터의 관계를 확실히 이해하는 데 집중하는 것이 좋습니다.