본문 바로가기
c++/기초

c++ 기초 - 배열 (array)

by 농사짓는 도동 2024. 2. 4.

배열이란?

같은 자료형의 데이터가 연속적으로 이어져 있는 형태이다. 

선언

대괄호를 사용해 배열 변수와 크기를 지정한다. (이때 크기의 경우 컴파일 타임 상수여야한다.  non const variable과 런타임 상수는 사용 X)

자료형 이름[개수];

 

int numbers[10];

// non const variable
int length;
int numbers[length]; // 에러!

// 런타임 상수
int temp = 5;
const int length = temp;
int numbers[length]; // 에러!

 

 

❗ 여기서 배열 변수의 이름은 "배열의 시작 주소"를 가리킨다. 정확하게는 시작 위치를 가지는 자료형* 포인터라고 볼 수 있다.

 

접근

그럼 배열에 값을 어떻게 넣고 가져오게 될까? 크게 3가지 방법으로 배열에 접근하게 된다.

 

일단 square라는 구조체를 만들고 이것을 담는 squares라는 배열을 선언하였다. 이 배열에 접근하는 방식들을 하나씩 살펴보도록 하자.

struct Square
{
  int height;
  int width;
}

int main()
{
  Square squares[10];
}

 

1. 포인터 접근

다음과 같이 작성하면 배열의 시작 주소를 가져오게 된다. 해당 변수에 접근해 값을 변경할 수 있다.

자료형* 변수명 = 배열이름;

 

그 다음 요소에 접근하기 위해서는 포인터 덧셈을 이용한다. 선언한 자료형의 크기만큼 이동해 원하는 요소의 위치에 접근할 수 있다.

자료형* 변수명 = 배열변수명 + (찾고자하는 위치);

이는 즉 다음과 같이 해석할 수 있다.
찾고자 하는 위치의 주소 = 배열 시작 주소 + 찾고자 하는 위치
int main()
{
    Square* square1 = squares;
    square1->height = 100;
    square1->width = 10;

    Square* square2 = squares + 1; // 포인터 덧셈 -> square 타입 바구니 한개씩 이동
    square2->height = 200;
    square2->width = 20;
}

 

위의 코드를 실행하면 다음과 같이 메모리상에 위치하게 된다. (시작 주소의 경우 예시용으로 임의로 잡았다.)

2. 참조 접근

참조로 접근하는 것과 결국 포인터로 접근하는 방식과 내부적으로 같다. 그래서 배열 또한 참조로도 접근할 수 있다.

자료형& 변수이름 = *(배열이름 + 찾고자하는위치);
Square& square3 = *(squares + 2);
square3.height = 300;
square3.width = 30;

 

❗ 참고로 &를 빼먹게 되면 완전히 다른 의미를 가지게 된다. &를 빼면 주소가 아니라 데이터 그 자체를 다루게 된다. 즉, 해당 내용물을 temp에 넣는 것이지 원본 데이터에 접근하여 수정하는 것이 아니다.

// Square& 대신 Square을 넣는다면?
Square temp = *(squares + 1); // 그림 상 1번
// 그림상 2번
tem.height = 300;
temp.width = 30;

// 위의 코드는 다음과 같이 풀어쓴 코드와 동일하다!
Square temp;
temp = *(squares + 1);
tem.height = 300;
temp.width = 30;

 

위의 상황을 메모리 상 도식화해보면 다음과 같다.

한편, 포인터 덧셈을 사용한 접근법은 불편하고 가독성이 떨어지는 단점이 있다.

 

3. 인덱스 접근

배열에 들어있는 각 변수를 요소라고 부른다. 배열의 개별 요소에 접근하기 위해 인덱스라는 매개 변수를 사용하게 된다. (배열은 0번 인덱스부터 시작한다.)

배열이름[인덱스]

 

  • (squares + i) == squares[i]라고 보면 된다.
squares[0].width = 100;

 

반복문과 조합

요소들을 하나씩 선언해서 배열에 접근하고 수정하는 것은 상당히 번거롭다. 그래서 다음과 같이 반복문과 함께 사용해 데이터에 접근하는 식으로 자주 사용하게 된다.

for (int i = 0; i < 10; i++)
{
    Square& square = *(squares + i);
    square.height = 100 * (i+1);
    square.width = 10 * (i+1);
}

for (int i = 0; i < 10; i++)
{
    squares[i].height = 100 * (i+1);
    squares[i].width = 10 * (i+1);
}

 

초기화

배열은 다양한 방식으로 초기화할 수 있다.

 

1) 빈 중괄호

주어진 크기만큼 모든 요소가 0으로 초기화된다.

int numbers[3] = {}; // 빈 것 주면 0으로 초기화!

 

2) 배열 크기보다 더 작은 개수의 수만큼 중괄호에 정의

앞의 값은 정의한대로 들어가고, 그 뒤는 0으로 초기화된다.

int numbers1[5] = {1, 2, 3}; // 설정한 애들은 설정한 값으로, 나머지 값들은 0으로 초기화

 

3) 크기 지정 안하고 중괄호에 넣을 데이터 작성

데이터의 개수만큼 크기에 해당하는 배열로 만들어줌

int numbers2[] = {1,2,3,4,5,6,7,8,9};

함수 매개변수로서의 사용

기본적인 값 타입 변수들은 다음과 같이 복사되어 함수 인자로 넘겨졌다. 그래서 큰 공간을 차지할 때 복사가 부담되는 상황이 있었다. 배열은 함수의 인자로 넘겨질 때 어떻게 넘겨질까?

 

배열의 경우 함수 인자로 넘길 때 컴파일러가 알아서 포인터로 치환해준다. 즉, 배열을 인자로 넘길 때 데이터를 복사해서 넘기지 않고 배열의 시작 주소(포인터)만 넘기게 된다.  (char[] -> char*)

 

어찌보면  여러 타입들이 모여있기 때문에 하나의 변수와 비교해 기본적으로 더 큰 공간을 쓰는 편이다. (복사로 매개변수를 넘기면 엄청난 부담일 수 있다!) 그래서 다음과 같이 동작하도록 만든 것 같다!

void Test(char temp[])
{
	temp[0] = 'x'; // 주소를 받은 것이기에 원본을 변경하게 된다!
}

int main()
{
    char test[] = "test";
    Test(test);
    cout << test << endl;
    
    // result: xest   
}

 

❗ 따라서 sizeof 연산자를 쓸때는 주의가 필요하다! (배열 데이터 모음 전체 크기 받을려고 sizeof 쓰는 경우가 많은데, 받은 매개 변수에 sizeof 씌우면 포인터 크기 나옴)

포인터 vs 배열

❓ 배열 변수가 시작 주소를 가지고 있으니 주소를 담는 변수인 포인터와의 차이가 무엇일까 의문이 들 수 있다.

일단, 포인터와 배열을 간단하게 다시 정리해보자. Hello World라는 문자열을 각각 포인터와 배열로 정의해서 차이를 살펴보자.

 

포인터

데이터 영역에 char형 배열을 만들고 그 시작주소를 ptr 포인터 변수에 넣어주는 코드이다. 그리고 포인터가 별도의 메모리 공간에 할당되게 된다. 

const char* ptr = "Hello World";

 

메모리상 다음과 같이 저장될 것이다.

배열

arr는 배열의 시작 주소를 가리킨다. 별도의 주소를 담는 공간이 할당되는 개념이 아니며 이것이 포인터와 가장 큰 차이다.

rdata에 데이터가 있으면 포인터 크기만큼 끊어 읽어와서 메모리에 복사해준다. 이렇게 배열 메모리에 데이터가 들어차게 된다.

char arr[12] = "Hello World";

 

메모리를 살펴보면 다음과 같다.

정리하면 배열은 같은 데이터끼리 연속적으로 이어져 있는 데이터 모음이라고 볼 수 있다. 여기서 배열 이름은 데이터 모음의 시작 주소를 가리킨다. 즉, 포인터주소 자체를 변수에 저장하는 개념이면 배열은 변수에 따로 주소를 저장한다는 개념이 아니고 데이터를 저장할 연속적인 메모리 공간이라고 보면 된다.

 

그리고 포인터는 데이터 자체의 크기 (여기선 string)가 늘어나도 포인터 변수 자체의 크기는 변화가 없다. 한편, 배열의 경우 string의 크기가 변하는 만큼 데이터 모음이 크게 잡힌다. 즉, 데이터 공간 측면에서 포인터는 주소 크기만큼만 차지하지만 배열은 데이터 모음의 크기를 차지한다고 볼 수 있다. 

 

 

+) 번외로.. c++에서는 배열에 배열 대입이 되지 않는다.

다른 프로그래밍 언어에서는 다음과 같이 배열 자체를 데이터를 담고 있는 객체 그 자체로 보고 배열에 배열을 대입하는 식으로 사용이 가능했다. 하지만 c++에서는 시작 주소를 가리키기 때문에 다음와 같이 작성하면 에러가 뜬다. 배열을 대입하는 것이 안되었던 것은 배열이 주소를 나타내는 것이기 때문이다! 

int numbers[10];
int numbers2[10];

// numbers2 = numbers; >> 에러 발생!

 

728x90

'c++ > 기초' 카테고리의 다른 글

c++ 기초 - 동적 할당  (0) 2024.02.05
c++ 기초 - static 변수 및 함수, 정적 지역 객체  (0) 2024.02.05
c++ 기초 - 클래스 (생성자, 소멸자)  (0) 2024.02.04
c++ 기초 - 참조형 변수  (0) 2024.02.04
c++ 기초 - 포인터  (0) 2024.02.03