Skip to content

5. 배열(Array)과 포인터(Pointer)


1. 키워드

  • 배열(Array)
  • 배열 요소(Element)와 인덱스(Index)
  • 포인터(Pointer)
  • &(주소 연산자)와 *(참조 연산자)
  • 메모리의 동적 할당(Dynamic Allocation)
  • newdelete 연산자


2. 1차원 배열

1) 배열이란?

  • 배열은 같은 타입의 변수들로 이루어진 유한 집합으로 정의할 수 있다.
  • 배열을 구성하는 각각의 값을 배열 요소라고 하며, 배열에서의 위치를 가리키는 숫자를 인덱스라고 한다.
  • C++에서 인덱스는 언제나 0부터 시작하며, 0을 포함한 양의 정수만을 가질 수 있다.
  • 배열은 같은 종류의 데이터를 많이 다뤄야 하는 경우에 사용할 수 있는 가장 기본적인 자료 구조이다.
  • 배열은 선언되는 형식에 따라 1차원 배열, 2차원 배열뿐만 아니라 그 이상의 다차원 배열로도 선언할 수 있다.


2) 1차원 배열

  • 1차원 배열은 가장 기본적인 배열로 다음과 같은 문법에 따라 선언한다.


타입 배열이름[배열길이];


  • 타입은 배열 요소로 들어가는 변수의 타입을 명시한다.
  • 배열 이름은 배열이 선언된 후에 배열에 접근하기 위해 사용된다.
  • 배열의 길이는 해당 배열이 몇 개의 배열 요소를 가지게 되는지 명시한다.
  • 열은 선언만 하고 초기화하지 않으면, 모든 배열 요소가 쓰레깃값으로 채워진다.


#include <iostream>
using namespace std;

int main() {
    int sum = 0;
    int grade[3];  // 길이가 3인 int 타입 배열 선언

    // 인덱스를 이용한 배열의 초기화
    grade[0] = 85;  // 국어 점수
    grade[1] = 65;  // 영어 점수
    grade[2] = 90;  // 수학 점수

    for (int i = 0; i < 3; i++) {
        sum += grade[i];  // 인덱스를 이용한 배열로의 접근
    }

    cout << "국영수 과목 총 점수 합계는 " << sum << "점이고, 평균 점수는 " << (double)sum / 3 << "점입니다." << endl;

    return 0;
}

// 국영수 과목 총 점수 합계는 240점이고, 평균 점수는 80점입니다.


  • 위의 코드는 int 타입 데이터를 3개 저장할 수 있는 배열을 선언하고 있다.
  • 또한, 0부터 시작하는 인덱스를 이용하면 각각의 배열 요소에 따로 접근할 수 있다.


  • 다음 그림은 위의 코드에서 사용된 배열 grade가 메모리 상에서 어떻게 저장되는지를 보여준다.


001


  • 위의 그림처럼 언제나 배열의 이름은 배열의 첫 번째 요소와 같은 주소를 가리키고 있다.


3) 배열의 선언과 동시에 초기화하는 방법

  • C++에서는 변수와 마찬가지로 배열도 선언과 동시에 초기화할 수 있다.
  • 다음과 같이 {}(중괄호)를 사용하여 초깃값을 나열한 것을 초기화 리스트라고 한다.


타입 배열이름[배열길이] = {배열요소1, 배열요소2, ...};


  • 단, 초기화 리스트의 타입과 배열의 타입은 반드시 일치해야 한다.
  • 만약 초기화 리스트의 개수가 배열의 총 길이보다 적으면, 배열의 앞에서부터 차례대로 초기화될 것이다.
  • 이때 초기화되지 못한 나머지 배열 요소는 모두 0으로 초기화된다.


  • 초기화 리스트를 이용한 초기화 방식은 반드시 배열의 선언과 함께 정의되어야 한다.
  • 배열이 먼저 선언된 후에는 이 방식으로 배열의 요소를 초기화할 수 없다.


int arr1[3] = {0, 1, 2};  // 배열의 선언과 동시에 초기화는 가능함

int arr2[3];              // 배열의 선언
arr2[3] = {0, 1, 2};      // 배열이 먼저 선언된 후에는 이 방식으로 초기화될 수 없음 오류가 발생함

arr2 = arr1;              // 길이가 같더라도 하나의 배열을 다른 배열에 통째로 대입할 수는 없음. 오류가 발생함


  • 다음의 코드는 앞선 코드와 같은 배열을 선언과 동시에 초기화 리스트로 초기화하는 것이다.


#include <iostream>
using namespace std;

int main() {
    int sum = 0;
    int grade[3] = {85, 65, 90};  // 길이가 3인 int 타입 배열의 선언과 동시에 초기화

    for (int i = 0; i < 3; i++) {
        sum += grade[i];  // 인덱스를 이용한 배열로의 접근
    }

    cout << "국영수 과목 총 점수 합계는 " << sum << "점이고, 평균 점수는 " << (double)sum / 3 << "점입니다." << endl;

    return 0;
}

// 국영수 과목 총 점수 합계는 240점이고, 평균 점수는 80점입니다.


4) 배열의 길이 자동 설정

  • C++에서는 초기화 리스트에 맞춰 자동으로 배열의 길이를 설정할 수도 있다.


타입 배열이름[] = {배열요소1, 배열요소2, ...};


  • 배열의 길이를 따로 입력하지 않은 배열은 초기화 리스트의 배열 요소 개수에 맞춰 자동으로 배열의 길이가 설정된다.


int arr[] = {1, 2, 3};


  • 위의 코드에서 int 타입 배열 arr의 길이는 자동으로 3으로 설정됨과 동시에 초기화 리스트에 의해 초기화된다.


5) 배열의 특징

  • C++에서 배열은 다음과 같은 특징을 가진다.


1] 배열의 길이를 선언할 때에는 반드시 상수를 사용해야 한다.

2] 배열 요소의 인덱스는 언제나 0부터 시작한다.

3] C++ 컴파일러는 배열의 길이를 전혀 신경쓰지 않는다.


#include <iostream>
using namespace std;

int main() {
    int sum = 0;
    int grade[3] = {85, 65, 90};  // grade[0], grade[1], grade[2]만 선언 및 초기화

    grade[3] = 100;  // grade[3]를 선언하지 않고 초기화 진행

    for (int i = 0; i < 4; i++)  // grade[3]도 수식에 포함
    {
        sum += grade[i];  // 인덱스를 이용한 배열의 접근
    }

    cout << "국영수 과목 총 점수 합계는 " << sum << "점이고, 평균 점수는 " << (double)sum / 3 << "점입니다." << endl;

    return 0;
}

// 국영수 과목 총 점수 합계는 340점이고, 평균 점수는 113.333점입니다.


  • 위의 코드에서는 길이가 3int 타입 배열 grade를 선언하고 있다.
  • 즉, 배열 grade의 배열 요소는 grade[0], grade[1], grade[2]만이 존재한다.
  • 하지만 존재하지도 않는 grade[3]이라는 배열 요소의 초기화를 진행하고, 반복문을 통해 수식에서도 이용한다.
  • 이때 C++ 컴파일러는 오류는 커녕 수식에서까지 이 배열 요소를 이용하여 결과까지 출력해 준다.
  • 하지만 이 결과는 개발자가 전혀 의도하지 않은 결과물이며, 이러한 프로그램은 종종 예상하지 못한 결과를 내주기도 한다.


C++ 컴파일러

  • C++에서는 컴파일러가 일일이 배열의 길이 등을 검사하여 오류를 출력해 주지 않는다.
  • 따라서 C++로 프로그래밍할 때에는 언제나 이런 계산을 개발자가 직접 해줘야 한다.


6) 배열이 차지하는 메모리의 크기

  • C++에서 배열을 복사하거나 배열 요소에 특정 작업을 하고 싶을 때는 해당 배열이 차지하는 메모리의 크기를 정확히 알고 있는 것이 좋다.


  • 배열이 차지하는 총 메모리의 크기는 다음 수식을 사용하여 구할 수 있다.


배열이차지하는메모리의크기 = 배열의길이 * sizeof(타입)


  • 그리고 배열의 길이를 알고 싶을 때에는 다음 수식을 사용하여 구할 수 있다.


배열의길이 = sizeof(배열이름) / sizeof(배열이름[0])


  • 위의 수식에서 배열이름[0]은 해당 배열의 타입을 나타내기 위해서 사용되었다.


#include <iostream>
using namespace std;

int main() {
    int grade[] = {85, 65, 90};                  // 배열의 길이를 명시하지 않음
    int len = sizeof(grade) / sizeof(grade[0]);  // 배열의 길이를 구하는 공식

    cout << "배열 grade의 길이는 " << len << "입니다." << endl;

    return 0;
}

// 배열 grade의 길이는 3입니다.


7) C++11에서의 배열 초기화

  • C++11에서는 배열의 초기화에 관해 다음과 같은 사항들이 변경되었다.


1] 배열을 초기화할 때에 =(대입 연산자)를 사용하지 않아도 된다.

2] 값을 명시하지 않고 {}만을 사용하여 초기화하면, 모든 배열 요소를 0으로 초기화할 수 있다.

3] 초기화 리스트를 사용하여 배열을 초기화할 경우 Narrowing Cast를 할 수 없다.

4] array 템플릿 클래스가 추가되었다.


8) Narrowing Cast

  • 이전 버전의 C++에서 발생하던 수많은 호환성 문제를 C++11에서는 초기화 리스트를 사용하여 방지할 수 있다.
  • C++11에서는 초기화 리스트를 사용하여 변수나 배열을 초기화할 경우에 Narrowing Cast를 허용하지 않는다.
  • 따라서 초기화 리스트를 이용한 초기화에서 Narrowing Cast가 발생하면, 경고를 발생시킨다.


  • Narrowing Cast란 다음의 코드와 같이 초기화를 통해 발생하는 암시적인 데이터의 손실을 의미한다.


int var = 3.14;  // Narrowing Cast


  • 위의 코드는 int 타입 변수를 실숫값으로 초기화함으로써 데이터의 손실이 발생한다.
  • 이렇게 데이터의 손실이 발생하는 암시적인 타입 변환을 Narrowing Cast라고 한다.


  • C++11에서는 초기화 리스트를 이용하여 이러한 Narrowing Cast로 인한 데이터의 손실 및 호환성 문제를 미리 방지할 수 있다.


int var = {3.14};  // 초기화 리스트를 통한 Narrowing Cast는 허용하지 않으므로, 경고를 발생시킴


3. 다차원 배열

  • 다차원 배열이란 2차원 이상의 배열을 의미하며, 배열 요소로 또 다른 배열을 가지는 배열을 의미한다.


1) 2차원 배열

  • 2차원 배열이란 배열의 요소로 1차원 배열을 가지는 배열이다.
  • C++에서는 2차원 배열을 나타내는 타입을 따로 제공하지 않는다.
  • 대신에 1차원 배열의 배열 요소로 또 다른 1차원 배열을 사용하여 2차원 배열을 나타낼 수 있다.


  • 2차원 배열은 다음과 같은 문법에 따라 선언할 수 있다.


타입 배열이름[행의길이][열의길이];


  • 타입은 배열 요소로 저장되는 변수의 타입을 설정한다.
  • 배열이름은 배열이 선언된 후에 배열에 접근하기 위해 사용된다.


  • 다음 그림은 2차원 배열을 이해하기 쉽도록 도식적으로 표현한 것이다.


002


  • 하지만 컴퓨터의 메모리는 위와 같은 입체적 공간이 아닌 선형 공간이므로 실제로는 다음 그림과 같이 저장된다.


003


  • 다음의 코드는 앞선 그림을 C++ 프로그램으로 작성한 것이다.


int arr1[6] = {10, 20, 30, 40, 50, 60};
int arr2[2][3] = {10, 20, 30, 40, 50, 60};


2) 배열의 선언과 동시에 초기화하는 방법

  • 1차원 배열과 마찬가지로 2차원 배열도 선언과 동시에 초기화할 수 있다.
  • 2차원 배열은 1차원 배열과는 달리 다음과 같이 여러 방식으로 초기화할 수 있다.


1] 1차원 배열의 초기화 형태를 따르는 방식

2] 배열의 모든 요소를 초기화하는 방식

3] 배열의 일부 요소만을 초기화하는 방식


(1) 1차원 배열의 초기화 형태를 따르는 방식

  • C++에서는 2차원 배열을 1차원 배열의 초기화 형태로도 초기화할 수 있다.


타입 배열이름[행의길이][열의길이] = {배열요소[0][0], 배열요소[0][1], ..., 배열요소[1][0], 배열요소[1][1], ..., 배열요소[2][0], 배열요소[2][1], ...};


  • 이 방식으로는 2차원 배열의 배열요소[0][0]부터 차례대로 초기화된다.
  • 만약 초기화하는 배열 요소의 개수가 배열의 총 길이보다 적으면, 나머지 배열 요소는 모두 0으로 초기화된다.


(2) 배열의 모든 요소를 초기화하는 방식

  • C++에서는 2차원 배열의 모든 요소를 좀 더 직관적으로 초기화할 수도 있다.


타입 배열이름[행의길이][열의길이] =
{
    {배열요소[0][0], 배열요소[0][1], ...},
    {배열요소[1][0], 배열요소[1][1], ...},
    {배열요소[2][0], 배열요소[2][1], ...},
    ...
};


  • 이 방식은 앞서 살펴본 1차원 배열의 초기화 형태를 따르는 방식과 결과는 같다.
  • 하지만 좀 더 직관적으로 2차원 배열의 모습을 알 수 있으므로, 보통 이 방식을 많이 사용한다.


int arr1[2][3] = {10, 20, 30, 40};
int arr2[2][3] = {
    {10, 20, 30},
    {40, 50, 60}};


(3) 배열의 일부 요소만을 초기화하는 방식

  • C++에서는 2차원 배열의 일부 요소만을 초기화할 수도 있다.


  • 이 방식으로는 다음의 코드처럼 2차원 배열의 원하는 배열 요소만을 초기화할 수 있다.
  • 이때 초기화하지 않은 배열 요소는 모두 0으로 자동 초기화된다.


#include <iomanip>
#include <iostream>
using namespace std;

int main() {
    int arr_col_len, arr_row_len;
    int arr[3][4] = {
        {10, 20},
        {30, 40, 50, 60},
        {0, 0, 70, 80}};

    // 2차원 배열의 열의 길이를 계산함
    arr_col_len = sizeof(arr[0]) / sizeof(int);
    // 2차원 배열의 행의 길이를 계산함
    arr_row_len = (sizeof(arr) / arr_col_len) / sizeof(int);

    cout << "arr의 배열 요소의 값" << endl;

    for (int i = 0; i < arr_row_len; i++) {
        for (int j = 0; j < arr_col_len; j++) {
            cout << setw(4) << arr[i][j];
        }

        cout << endl;
    }

    return 0;
}

// arr의 배열 요소의 값
//   10  20   0   0
//   30  40  50  60
//    0   0  70  80


  • 위의 코드에서 2차원 배열 열의 길이를 구할 때 사용하는 수식은 다음과 같다.


arr_col_len = sizeof(arr[0]) / sizeof(int);


  • 열의 길이는 sizeof(arr[0])으로 2차원 배열 한 행의 길이를 먼저 구한 후에, 그 값을 배열 타입의 크기로 나누어서 구한다.
  • 열의 길이를 이용하면 2차원 배열 행의 길이도 구할 수 있다.


arr_row_len = (sizeof(arr) / arr_col_len) / sizeof(int);


  • 행의 길이는 (sizeof(arr) / arr_col_len)으로 2차원 배열 한 열의 길이를 먼저 구한 후에, 그 값을 배열 타입의 크기로 나누어서 구할 수 있다.


3) 배열의 길이 자동 설정

  • 1차원 배열과 마찬가지로 2차원 배열도 배열의 길이를 명시하지 않고, 자동으로 배열의 길이를 설정할 수 있다.
  • 단, 행의 길이는 생략할 수 있지만, 열의 길이는 반드시 명시해야 한다.


  • 다음의 코드는 앞선 코드에서 행의 길이를 생략한 것으로, 같은 결과를 출력한다.
  • 이 코드에서 행의 길이를 명시하고, 열의 길이를 생략하면 컴파일할 때 오류가 발생하는 것을 확인할 수 있다.


#include <iomanip>
#include <iostream>
using namespace std;

int main() {
    int arr_col_len, arr_row_len;
    int arr[][4] = {
        {10, 20},
        {30, 40, 50, 60},
        {0, 0, 70, 80}};

    // 2차원 배열의 열의 길이를 계산함
    arr_col_len = sizeof(arr[0]) / sizeof(int);
    // 2차원 배열의 행의 길이를 계산함
    arr_row_len = (sizeof(arr) / arr_col_len) / sizeof(int);

    cout << "arr의 배열 요소의 값" << endl;

    for (int i = 0; i < arr_row_len; i++) {
        for (int j = 0; j < arr_col_len; j++) {
            cout << setw(4) << arr[i][j];
        }

        cout << endl;
    }

    return 0;
}

// arr의 배열 요소의 값
//   10  20   0   0
//   30  40  50  60
//    0   0  70  80


4. 포인터의 개념

1) 주소값의 이해

  • 데이터의 주소값이란 해당 데이터가 저장된 메모리의 시작 주소를 의미한다.
  • C++에서는 이러한 주소값을 1바이트 크기의 메모리 공간으로 나누어 이해할 수 있다.
  • 예를 들어, int 타입 데이터는 4바이트의 크기를 가지지만, int 타입 데이터의 주소값은 시작 주소 1바이트만을 가리키게 된다.


004


2) 포인터란?

  • C++에서 포인터란 메모리의 주소값을 저장하는 변수이며, 포인터 변수라고도 부른다.
  • char 타입 변수가 문자를 저장하고, int 타입 변수가 정수를 저장하는 것처럼 포인터는 주소값을 저장하는 데 사용된다.


int n = 100;    // 변수의 선언
int *ptr = &n;  // 포인터의 선언


  • 다음 그림은 위의 코드에서 사용된 변수와 포인터가 메모리에서 어떻게 저장되는지를 보여주는 것이다.


005


3) 포인터 연산자

  • C++에서 포인터와 연관되어 사용되는 연산자는 다음과 같다.


1] &(주소 연산자)

2] *(참조 연산자)


  • &는 변수의 이름 앞에 사용하여, 해당 변수의 주소값을 반환한다.
  • *는 포인터의 이름이나 주소 앞에 사용하여, 포인터에 저장된 주소에 저장되어 있는 값을 반환한다.


4) 포인터의 선언

  • C++에서 포인터는 다음 문법에 따라 선언할 수 있다.


타입 *포인터이름;


  • 타입이란 포인터가 가리키고자 하는 변수의 타입을 명시한다.
  • 포인터이름은 포인터가 선언된 후에 포인터에 접근하기 위해 사용된다.
  • 포인터를 선언할 때 *의 앞과 뒤에 존재하는 공백은 무시된다.


5) 포인터의 동시 선언

  • C++에서는 여러 개의 포인터를 동시에 선언할 수 있다.
  • 하지만 여러 개의 포인터를 동시에 선언할 때에는 다음과 같은 점에 주의해야 한다.


  • 다음의 코드는 두 개의 int 타입 포인터를 동시에 선언하려고 하는 것이다.
  • 하지만 ptr1int 타입 포인터로, ptr2는 그냥 int 타입 변수로 선언될 것이다.


int *ptr1, ptr2;


  • 따라서 두 개의 int 타입 포인터를 선언하고 싶을 때에는 다음과 같이 각각의 포인터 변수 이름 앞에 *를 따로 사용하여 선언해야 한다.


int *ptr1, *ptr2;


6) 포인터의 선언과 초기화

  • 포인터를 선언한 후 *를 사용하기 전에 포인터는 반드시 초기화되어야 한다.
  • 초기화되지 않은 채로 *를 사용하게 되면, 어딘지 알 수 없는 메모리 장소에 값을 저장하는 것이 된다.
  • 이러한 동작은 매우 위험한 결과를 초래할 수도 있으며, 이렇게 발생한 오류는 디버깅하기도 매우 힘들다.


  • 따라서 다음과 같이 포인터의 선언과 동시에 초기화를 함께 하는 것이 좋다.


타입 *포인터이름 = &변수이름;
또는
타입 *포인터이름 = &주소값;


7) 포인터의 참조

  • C++에서 선언된 포인터는 *를 사용하여 참조할 수 있다.


  • 다음의 코드는 포인터의 주소값과 함께 포인터가 가리키고 있는 주소값의 데이터를 참조하는 것이다.


int x = 7;          // 변수의 선언
int *ptr = &x;      // 포인터의 선언
int **pptr = &ptr;  // 포인터의 참조


006


#include <iostream>
using namespace std;

int main() {
    int num1 = 1234;
    double num2 = 3.14;

    int *ptr_num1 = &num1;
    double *ptr_num2 = &num2;

    cout << "포인터의 크기는 "  // ①
         << sizeof(ptr_num1) << "입니다." << endl;
    cout << "포인터 ptr_num1가 가리키고 있는 주소값은 "  // ②
         << ptr_num1 << "입니다." << endl;
    cout << "포인터 ptr_num1가 가리키고 있는 주소에 저장된 값은 "  // ③
         << *ptr_num1 << "입니다." << endl;
    cout << "포인터 ptr_num2가 가리키고 있는 주소값은 "
         << ptr_num2 << "입니다." << endl;
    cout << "포인터 ptr_num2가 가리키고 있는 주소에 저장된 값은 "
         << *ptr_num2 << "입니다." << endl;

    return 0;
}

// 포인터의 크기는 8입니다.
// 포인터 ptr_num1가 가리키고 있는 주소값은 0x16b0fb478입니다.
// 포인터 ptr_num1가 가리키고 있는 주소에 저장된 값은 1234입니다.
// 포인터 ptr_num2가 가리키고 있는 주소값은 0x16b0fb470입니다.
// 포인터 ptr_num2가 가리키고 있는 주소에 저장된 값은 3.14입니다.


  • ①번 라인에서는 sizeof 연산자를 사용하여 포인터 변수의 크기를 구하고 있다.
  • 포인터 변수는 메모리에서 변수의 위치를 나타내는 주소를 다루는 변수이므로, 그 크기는 일반적으로 CPU에 따라 결정된다.
  • 따라서 32비트 CPU에서는 1워드(Word)의 크기가 4바이트이므로, 포인터 변수의 크기 또한 4바이트가 될 것이다.
  • 하지만 이러한 포인터 변수의 크기는 컴파일러로 컴파일할 때 그 크기까지 직접 명시할 수 있다.
  • 따라서 포인터 변수의 크기는 CPU의 종류와 컴파일할 때 사용된 컴파일러의 정책에 따라서 달라질 수 있다.
  • 또한,②번과 ③번 라인에서처럼 포인터가 가리키는 변수의 타입에 따라 포인터의 타입도 같이 바꿔주고 있다.
  • 포인터의 타입은 참조 연산자를 통해 값을 참조할 때, 참조할 메모리의 크기를 알려주는 역할을 하기 때문이다.


  • 다음 그림은 char 타입 포인터와 int 타입 포인터가 각각 메모리 상에서 해당 타입의 변수를 가리키는 것을 보여준다.


007


워드(Word)

  • 워드란 CPU가 한 번에 처리할 수 있는 데이터의 크기이다.
  • 1바이트는 8바이트이므로 32비트 시스템에서는 4바이트가 1워드로 처리된다.
  • 64바이트 시스템에서는 8바이트가 1워드로 처리된다.


5. 포인터 연산

  • 포인터는 값을 증가시키거나 감소시키는 등의 제한된 연산만을 할 수 있다.
  • C++의 포인터 연산에는 다음과 같은 규칙이 있다.


1] 포인터끼리의 덧셈, 곱셈, 나눗셈은 아무런 의미가 없다.

2] 포인터끼리의 뺄셈은 두 포인터 사이의 상대적 거리를 나타낸다.

3] 포인터에 정수를 더하거나 뺄 수는 있지만, 실수와의 연산은 허용하지 않는다.

4] 포인터끼리 대입하거나 비교할 수 있다.


1) 타입별 포인터 연산

  • C++의 포인터 연산에서 포인터 연산 후 각각의 포인터가 가리키고 있는 주소는 포인터의 타입에 따라 달라진다.
  • 그 증가 폭은 포인터가 가리키는 변수의 타입의 크기와 같다.


008


  • 예를 들어, int 타입 포인터의 증가폭은 int 타입의 크기인 4바이트만큼 증가하게 된다.
  • 이 법칙은 포인터의 뺄셈에서도 똑같이 적용된다.


2) 포인터와 배열의 관계

  • 포인터와 배열은 매우 긴밀한 관계를 맺고 있으며, 어떤 부분에서는 서로를 대체할 수도 있다.
  • 배열의 이름은 그 값을 변경할 수 없는 상수라는 점을 제외하면 포인터와 같다.
  • C++에서는 배열의 이름을 포인터처럼 사용할 수 있을 뿐만 아니라, 포인터를 배열의 이름처럼 사용할 수도 있다.
  • 즉, C++에서는 배열의 이름이 주소로 해석되며, 해당 배열의 첫 번째 요소의 주소와 같게 된다.


#include <iostream>
using namespace std;

int main() {
    int arr[3] = {10, 20, 30};  // 배열 선언
    int *ptr_arr = arr;         // 포인터에 배열의 이름을 대입함

    cout << "배열의 이름을 이용하여 배열 요소에 접근: "
         << arr[0] << ", " << arr[1] << ", " << arr[2] << endl;
    cout << "포인터를 이용하여 배열 요소에 접근: "
         << ptr_arr[0] << ", " << ptr_arr[1] << ", " << ptr_arr[2] << endl;
    cout << "배열의 이름을 이용한 배열의 크기 계산: " << sizeof(arr) << endl;
    cout << "포인터를 이용한 배열의 크기 계산: " << sizeof(ptr_arr) << endl;

    return 0;
}

// 배열의 이름을 이용하여 배열 요소에 접근: 10, 20, 30
// 포인터를 이용하여 배열 요소에 접근: 10, 20, 30
// 배열의 이름을 이용한 배열의 크기 계산: 12
// 포인터를 이용한 배열의 크기 계산: 8


  • 위의 코드에서는 포인터에 배열의 이름을 대입한 후, 해당 포인터를 배열의 이름처럼 사용한다.
  • 하지만 배열의 크기를 계산할 때에는 배열의 이름과 포인터 사이에 큰 차이가 발생한다.
  • 배열의 이름을 이용한 크기 계산에서는 배열의 크기가 int 타입 배열 요소 3개의 크기인 12바이트로 제대로 출력된다.
  • 하지만 포인터를 이용한 크기 계산에서는 배열의 크기가 아닌 포인터 변수 자체의 크기가 출력되는 차이가 생긴다.


3) 배열의 포인터 연산

  • 다음의 코드는 앞선 코드와는 반대로 배열의 이름을 포인터처럼 사용하는 것이다.
  • 이 코드에서는 배열의 이름으로 포인터 연산을 진행하여 배열의 요소에 접근한다.


#include <iostream>
using namespace std;

int main() {
    int arr[3] = {10, 20, 30};  // 배열 선언

    cout << "배열의 이름을 이용하여 배열 요소에 접근: "
         << arr[0] << ", " << arr[1] << ", " << arr[2] << endl;
    cout << "배열의 이름으로 포인터 연산을 해 배열 요소에 접근: "
         << *(arr + 0) << ", " << *(arr + 1) << ", " << *(arr + 2) << endl;

    return 0;
}

// 배열의 이름을 이용하여 배열 요소에 접근: 10, 20, 30
// 배열의 이름으로 포인터 연산을 해 배열 요소에 접근: 10, 20, 30


009


  • 배열의 이름과 포인터 사이에는 다음과 같은 공식이 성립함을 알 수 있다.


arr이 배열의 이름이거나 포인터이고 n이 정수일 ,
arr[n] == *(arr + n)


  • 위의 공식은 1차원 배열뿐만 아니라 다차원 배열에서도 언제나 성립한다.


배열의 크기

  • 배열에 관계된 연산을 할 때는 언제나 배열의 크기를 넘어서는 접근을 하지 않도록 주의해야 한다.
  • 포인터 연산을 이용하여 계산하다가 배열의 크기를 넘어서는 접근을 하는 경우, C++ 컴파일러는 어떠한 오류도 발생시키지 않는다.
  • 다만 잘못된 결과만을 반환하므로 C++로 프로그래밍할 때에는 언제나 배열의 크기에 주의해야 한다.


6. 메모리의 동적할당

  • 데이터 영역과 스택 영역에 할당되는 메모리의 크기는 컴파일 타임(Compile Time)에 미리 결정된다.
  • 하지만 힙 영역의 크기는 프로그램이 실행되는 도중인 런 타임(Run Time)에 사용자가 직접 결정하게 된다.
  • 이렇게 런 타임에 메모리를 할당 받는 것을 메모리의 동적 할당이라고 한다.
  • 포인터의 가장 큰 목적은 런 타임에 이름 없는 메모리를 할당 받아 포인터에 할당하여, 할당 받은 메모리에 접근하는 것이다.
  • C에서는 malloc() 함수 등의 라이브러리 함수를 제공하여 이러한 작업을 수행할 수 있게 해준다.


  • C++에서도 C의 라이브러리 함수를 사용하여 메모리의 동적 할당 및 해제를 할 수 있다.
  • 하지만 C++에서는 메모리의 동적 할당 및 해제를 위한 더욱 효과적인 방법을 new 연산자와 delete 연산자를 통해 제공한다.


1) new 연산자

  • C에서는 malloc()이나 calloc() 함수 등을 이용하여 메모리의 동적 할당을 수행한다.
  • C++에서도 위의 함수를 사용할 수 있지만, 더 나은 방법인 new 연산자를 이용한 방법을 제공하고 있다.


  • C++에서 new 연산자는 다음과 같은 문법으로 사용한다.


타입 *포인터이름 = new 타입;


  • 첫 번째 타입은 데이터에 맞는 포인터를 선언하기 위해, 두 번째 타입은 메모리의 종류를 지정하기 위해 사용된다.
  • 만약 사용할 수 있는 메모리가 부족하여 새로운 메모리를 만들지 못했다면, new 연산자는 널 포인터를 반환한다.
  • new 연산자는 자유 기억 공간(Free Store)이라고 불리는 메모리 공간(Memory Pool)에 객체를 위한 메모리를 할당 받는다.
  • 또한, new 연산자를 통해 할당 받은 메모리는 따로 이름이 없으므로 해당 포인터로만 접근할 수 있게 된다.


2) delete 연산자

  • C에서는 free() 함수를 이용하여 동적으로 할당 받은 메모리를 다시 OS로 반환한다.
  • 이와 마찬가지로 C++에서는 delete 연산자를 사용하여, 더는 사용하지 않는 메모리를 다시 메모리 공간에 돌려줄 수 있다.


  • C++에서 delete 연산자는 다음과 같은 문법으로 사용한다.


delete 포인터이름;


  • 다음의 코드는 런 타임에 int 타입과 double 타입 데이터를 위한 메모리를 할당 받고, delete 연산자를 사용하여 더 이상 사용하지 않는 메모리를 반환하는 것이다.


#include <iostream>
using namespace std;

int main() {
    int *ptr_int = new int;
    *ptr_int = 100;

    double *ptr_double = new double;
    *ptr_double = 100.123;

    cout << "int 타입 숫자의 값은 " << *ptr_int << "입니다." << endl;
    cout << "int 타입 숫자의 메모리 주소는 " << ptr_int << "입니다." << endl;
    cout << "double 타입 숫자의 값은 " << *ptr_double << "입니다." << endl;
    cout << "double 타입 숫자의 메모리 주소는 " << ptr_double << "입니다." << endl;

    delete ptr_int;
    delete ptr_double;

    return 0;
}

// int 타입 숫자의 값은 100입니다.
// int 타입 숫자의 메모리 주소는 0x600001d18040입니다.
// double 타입 숫자의 값은 100.123입니다.
// double 타입 숫자의 메모리 주소는 0x600001d18050입니다.


  • 이때 new 연산자를 통해 생성한 메모리가 아닌 변수를 선언하여 생성한 메모리는 delete 연산자로 해제할 수 없다.
  • delete 연산자는 반드시 new 연산자를 통해 할당된 메모리를 해제할 때에만 사용해야 한다.
  • 또한, 한 번 해제한 메모리를 다시 해제하려고 시도하면 오류가 발생한다.

References