관리 메뉴

기억을 위한 기록들

[Fundamental C++] 5. 배열 본문

C & CPP/Fundamental C++

[Fundamental C++] 5. 배열

에드윈H 2021. 4. 27. 17:33

함수에서의 배열 전달

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

int DoSomething(int arr[])
{
	arr[0] = 1;
	arr[1] = 2;
	arr[2] = 3;

	return sizeof(arr);
}



int main()
{
	int myArr[3] = { 0 };

	int s = DoSomething(myArr);

	for (auto value : myArr)
	{
		cout << value << endl;
	}

	cout <<"myArr size : "<< sizeof(myArr) << endl;
	cout <<"arr size :"<< s << endl;


	return 0;
}

값에 의한 호출이지만 myArr이 나타내는 것은 r-value로 myArr의 메모리 블록의 주소이다. 값에 의한 호출이지만 참조 방식으로 전달되도록 만든 이유는 단순하다. 바로 효율성과 안전성 때문이다. 배열은 크기가 얼마나 될지 알 수 없다.

 

배열이 참조 방식으로 전달되는 이유?

요소의 개수가 엄청나게 많다면 꽤 많은 메모리르 차지하게 될 것이고, 만약 값에 의한 호출로 복사가 이루어져야 한다면, 배열 요소의 개수가 많을 경우만큼 복사하게 될 것이다. 즉 효율성이 떨어지게 된다. 게다가 만약에 전역 배열이 수 MB를 차지하는 배열을 전달하게 되면 해당 배열을 스택에 복사하게 될 경우 스택의 기본 크기는 대략 1MB이기 때문에 복사하게 되면 바로 스택 오버플로가 발생할 것이다. 결국 배열 객체를 참조 방식으로 전달되도록 만든 이유로 효율성도 있지만, 안정성을 위해서라고 할 수 있다.

vector는 값에 의한 호출 그대로 진행되긴 한다. 그런데 스택 오버플로가 발생하진 않는다. vector 요소들은 힙에 존재하기 때문이다. 안정성에는 문제가 없지만 비효율적일 수도 있다. 따라서 가능하면 vector도 인자로 넘길 때는 참조 방식을 사용해야 한다.

 

그래서 현재 DoSomething 함수는 arr의 주소가 전달된 상태이다. 반환형은 sizeof(int*)와 같다고 할 수 있다. 그래서 s의 값은 4가 출력이 된다.

 

 

int arr []같이 함수 인자에 사용될 경우 int*로 대체된다고 할 수 있다. 그러나 2차원 배열을 표현하기 위해 int arr [][] 같은 식은 허용되지 않는다. 그렇다고 int** 은 배열과 잘 호환되지 않는다. 그래서 참조 타입으로 받아야 한다.

 

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

int DoSomething(int (&arr)[3])  //참조타입으로 변경
{
	arr[0] = 1;
	arr[1] = 2;
	arr[2] = 3;

	return sizeof(arr);
}

int main()
{
	int myArr[3] = { 0 };

	int s = DoSomething(myArr);

	for (auto value : myArr)
	{
		cout << value << endl;
	}

	cout <<"myArr size : "<< sizeof(myArr) << endl;
	cout <<"arr size :"<< s << endl;


	return 0;
}

함수의 파라미터 입력만 변경했다. 이렇게 되면 배열 arr의 타입으로 어떤 문제도 일으키지 않고 안전하게 배열을 사용할 수 있다. 그런데 배열을 인자로 전달할 경우 반드시 참조 타입을 사용하라는 건 아니다.

 

 

비슷하면서도 똑같지 않은 둘의 관계이다.

 

 

 

배열의 크기

int main()
{
	int myArr[3] = { 1,2,3 };

	int arrSize01 = sizeof(int) * 3;

	int arrSize02 = sizeof(myArr);
	
	int arrIndexCnt = sizeof(myArr)/sizeof(int);


	cout << arrSize01 << endl; //배열의 크기
	cout << arrSize02 << endl; //배열의 크기
	cout << arrIndexCnt << endl; //배열 요소의 개수

	return 0;
}

2차원 배열의 크기

int main()
{
	int myArr[2][3];

	int arrSize01 = sizeof(myArr);  //2차원 배열 전체의 크기 (총 6개)

	int arrSize02 = sizeof(myArr[0]);  //0번째 객체의 크기 (총 3개 들어있음)
	
	int arrSize03 = sizeof(myArr[0][1]); //0번째의 1번째 객체의 크기 (1개)


	cout << arrSize01 << endl;
	cout << arrSize02 << endl;
	cout << arrSize03 << endl;

	return 0;
}

1번. 2차원 배열의 요소는 총 6개 int는 4바이트 이므로 24바이트

2번. 2차원 배열의 0번째 객체들의 크기 0번째 객체에 3개, 1번째 객체에 3개씩 들어 있고, 0번째 객체엔 4바이트 3개가 있으므로 12바이트

3번. 한 요소의 크기는 4바이트 

 

2차원 배열을 int**에 대입한 크기

int main()
{
	int myArr[2][3];

	int** ppArr = (int**)myArr;
	int arrSize01 = sizeof(ppArr);  

	int arrSize02 = sizeof(ppArr[0]);  
	
	int arrSize03 = sizeof(ppArr[0][1]); 


	cout << arrSize01 << endl;
	cout << arrSize02 << endl;
	cout << arrSize03 << endl;

	return 0;
}

이전과 같이 다양하게 크기가 나올 것 같지만, ppArr은 배열의 크기를 나타내고 있지 않고 포인터의 크기를 나타내고 있다.

출력 결과는 즉, int**의 크기 4, int*의 크기 4, int의 크기 4가 나온다.

 

배열의 요소 개수에 관해

배열 요소 개수가 정해지는 시점은 코드가 작성되는 시점에 이미 결정되었고, 해당 코드가 컴파일되면서 어셈블리로 변환되며, 실행 중에 이미 정해진 크기의 배열 객체가 생성된다는 것이다.

int main()
{
	int num = 10;

	int arr[num] = { 0 }; //컴파일에러!

	return 0;
}

왜냐하면 num변수는 실행 중에 10이라는 값을 가지게 되고, 이것이 바로 arr의 요소 개수가 되기 때문에, arr의 요소 개수가 실행 중에 정해지게 된다. 실행중에 결정되서는 안 된다.

코드가 작성되는 시점에 배열 요소의 개수를 정해야 하는데, 일종의 설계도라는 개념으로 생각할 수 있다. 건축가들이 집을 짓기 전에 방을 몇 칸으로 정할지와 같은..

 

하지만 num이 상수(const)로 선언돼있다면 가능하다.

int main()
{
	const int num = 10;

	int arr[num] = { 0 }; //성공!

	return 0;
}

const로 지정되어 컴파일러가 보기에 값이 고정되어 있다면 배열 요소 개수로 사용할 수 있다. 

한마디로 값을 고정시킨다고 하면, 배열 요소 개수로 정의할 수 있다는 것인데 뭔가 이상한 점이 하나 있다.

 

 

int main()
{
	const int num = 10;
	int* ptr = (int*)&num; 
	*ptr = 100; //100으로 변경 시킨다

	int arr[num] = { 0 }; //성공!

	return 0;
}

위의 코드는 문제없이 작동된다. 상수로 선언된 num의 주소를 int*형으로 타입 변환시켜와 강제로 num의 값을 100으로 변경하였다. 하지만 여전히 arr은 처음 선언된 10개의 배열 공간으로 선언된다. 잘만했으면 가변적인 배열을 만들 수도 있을 텐데, 불가능하다.

num이 100 변경 되었으나, 여전히 요소의 개수가 10개인 arr

컴파일러는 const가 지정된 변수를 거의 매크로처럼 취급한다. 이것은 속도 향상을 위한 것이기도 한데 다시 변경될 수 없는 변수의 값을 메모리에 접근하면서 확인할 필요가 없기 때문이다.

즉 배열 요소의 개수는 실행 시점에 결정될 수 없다는 것이다.

 

그래서 자주 사용되는 방법인 나열자 둔갑술(enum hack)이다.

int main()
{
	enum {num=3};

	int arr[num] = { 0 }; //성공!

	return 0;
}

참고 : 

hyo-ue4study.tistory.com/266

 

[CPP-effective] 1-1 #define쓰려거든 다른걸 떠올리자.

1. #define을 쓰려거든 const, enum, inline을 떠올리자. (전처리기보단 컴파일러를 가까이 하자.) #define PI 3.1415926535 - 해당 PI가 있으면 컴파일러에게 넘어가기전에 전처리기가 밀어버리고 숫자 상수로

hyo-ue4study.tistory.com

가변의 크기 배열을 어떻게든 만들 수도 있겠지만, 어마어마하게 큰 크기의 가변 배열이 만들어지게 되면 자칫 스택에 엄청난 용량의 크기가 할당되어 스택 오버플로가 발생할 수도 있다.

가변의 배열을 사용하고 싶다면 STL의 Vector를 사용하자. (그런데 배열은 배열이고 vector는 vector다 완전히 똑같다고 생각하면 안 된다.)