728x90

지난 번에 올린 이 글에 이어지는 내용이다. (링크↓)

https://breakcoding.tistory.com/294

 

[C++] 포인터와 배열 (포인터 심화)

지난 번에 올린 이 글에 이어지는 내용이다. (링크↓) https://breakcoding.tistory.com/81 [C++] 포인터 포인터를 어려워하는 분들이 많은 것 같다. 물론 나도 겁먹었었다. 하지만 전혀 어렵지 않다는 것을..

breakcoding.tistory.com

이전 포스팅의 마지막 부분에 함수의 리턴값으로 배열을 반환할 수는 없지만 배열의 주소 즉 포인터를 리턴할 수는 있다고 말했었다.

그러면 한 번 실험해보자.

 

이 함수는 반환값으로 포인터를 return하므로 반환타입은 int*이다.

#include <iostream>
using namespace std;

int* makeArray() {
	int arr[5] = { 1, 2, 3, 4, 5 };
	return arr; //배열의 주소 반환
}

int main() {
	int* myArray = makeArray(); //myArray라는 포인터변수에 배열의 주소를 받음
	for (int i = 0; i < 5; i++) {
		cout << myArray[i] << endl;
	}
	return 0;
}

이 코드를 실행하면

 

위 코드의 실행 결과

 

1, 2, 3, 4, 5가 아니라 엉뚱한 쓰레기값을 출력하는 것을 볼 수 있다.

이런 결과가 나오는 이유는 함수가 호출될 때 메모리의 스택이 어떻게 동작하는지를 알면 알 수 있다.

 

#include <iostream>
using namespace std;

int add(int num1, int num2) {
	int sum = num1 + num2;
	return sum;
}

int main() {
	int a = 5;
	int b = 2;
	int aPlusB = add(a, b);
	return 0;
}

이런 코드가 있다고 하자.

 

이 코드가 실행될 때 스택의 모습은 다음과 같다.

 

 

 

 

이것과 마찬가지로 위에서 본 이 코드는

#include <iostream>
using namespace std;

int* makeArray() {
	int arr[5] = { 1, 2, 3, 4, 5 };
	return arr; //배열의 주소 반환
}

int main() {
	int* myArray = makeArray(); //myArray라는 포인터변수에 배열의 주소를 받음
	for (int i = 0; i < 5; i++) {
		cout << myArray[i] << endl;
	}
	return 0;
}

 

 

 

위 코드가 실행될 때 스택의 모습

 

이 그림에서 4번째 스택 모습에서 문제가 생기는 것이다.

 

 

바로 이 상태이다.

makeArray() 함수가 종료되면서 배열 arr의 시작 주소인 1000번지만 넘겨주고 배열 arr은 스택에서 사라졌는데 1000번지에 가서 1000번지에 있는 쓰레기 값, 1004번지에 있는 쓰레기 값, 1008번지에 있는 쓰레기 값, 1012번지에 있는 쓰레기 값, 1016번지에 있는 쓰레기 값을 출력하고 종료하는 것이다.

 

 

바로 이렇게 말이다.

1은 우연히 스택에서 makeArray 함수가 끝나도 그 메모리가 쓰던 곳은 손상되지 않고 남아있어서 아주 운 좋게 1이 출력된 것이다.

 

일반적으로 프로그램을 실행하면서 필요한 변수들은 대부분 메모리의 스택 영역에 할당된다. 이것을 활성레코드라고 한다. 프로그램이 실행될 때에는 활성레코드 부분이 늘었다 줄었다 하면서 실행되는 것이다. 위에서 본 스택 그림처럼 말이다. 이 문제는 배열이 이러한 스택 영역에 할당되었기 때문에 생기는 문제이다.

 

이러한 문제를 해결할 수 있는 것이 바로 동적할당이다. 이게 바로 동적 할당이 필요한 이유이다.

스택에 저장된 활성레코드는 그 부분이 실행되고 나면 사라진다. 하지만 메모리에는 스택 영역 말고 힙(heap)이라는 영구 저장소가 있다. 동적 할당을 하면 힙(heap)에 저장이 된다. 힙(heap)에 저장된 것들은 delete를 사용하여 명시적으로 제거하지 않는 이상 프로그램이 종료될 때까지 존재한다.

 

동적할당은 다음과 같이 new 연산자를 사용하여 한다.

int* p = new int;

new 연산자를 사용하여 동적 배열을 생성하고 싶다면

int* list = new int[SIZE];

이렇게 하면 된다.

동적할당은 포인터 없이는 할 수 없다.

 

변수에는 동적으로 할당되는 변수가 있고 정적으로 할당되는 변수가 있다. 컴파일러는 컴파일을 할 때 이 프로그램에서 사용할 메모리를 대충 계산한다. 그 다음에 이거를 운영체제에게 알려준다. 예를 들어 컴파일러가 대충 계산했을 때 800바이트가 필요하다 하면 OS는 800바이트 이상을 스택에 여유있게 잡아놓는다. 이게 정적으로 할당하는 것이다.

 

그렇기 때문에 다음과 같은 코드는 컴파일 에러가 나는 것이다.

#include <iostream>
using namespace std;

int main() {
	int size;
	cin >> size;
	int arr[size];
	return 0;
}

사용자에게 size를 입력받아서 그 크기만큼 배열을 잡고 싶은 건데 이러한 코드는 불가능하다. 컴파일 할 때 필요한 메모리를 계산을 해야 하는데 size는 사용자에게 입력받는 것이기 때문에 컴파일 할 때에는 알 수가 없고 실행될 때 값이 결정된다. 따라서 컴파일러는 arr 배열을 메모리에 얼만큼 잡아야 하는지 모르므로 컴파일 에러가 나는 것이다. 따라서 배열의 크기를 정하는 변수는 반드시 const 변수여야 한다.

#include <iostream>
using namespace std;

int main() {
	int size = 5;
	int arr[size];
	return 0;
}

이 코드를 실행해도 컴파일에러가 난다.

배열의 크기를 정하는 저 자리에는 상수 혹은 const 변수(상수 변수)만 들어갈 수 있다.

#include <iostream>
using namespace std;

int main() {
	const int size = 5;
	int arr[size];
	return 0;
}

컴파일에러가 나지 않는다.

이렇게 const를 써줘야만 컴파일 에러가 나지 않는다.

 

그러면 사용자에게 크기를 입력 받아 그 크기 만큼의 배열을 생성하고 싶다면 어떻게 해야 할까?

 

동적할당을 사용하면 된다.

#include <iostream>
using namespace std;

int main() {
	int size;
	cin >> size;
	int* arr = new int[size];
	return 0;
}

동적할당으로 배열을 생성하면 이렇게 메모리를 얼만큼 잡을지 메모리의 크기를 실행 도중에 정할 수가 있다.

(컴퓨터에서 '동적'이라는 단어가 나오면 실행 도중이라는 뜻이고 '정적'이라는 것은 프로그램 실행 전을 말한다.)

 

위에서 봤던 코드를 다시 보자.

int* p = new int;

여기에서 포인터변수 p는 정적으로 할당된 변수이다. 포인터변수이므로 컴파일러가 4바이트 만큼을 메모리의 스택 부분에 잡는다. (int라서 4바이트를 잡는 것이 아니다. 이 부분이 헷갈리면 포인터 글을 다시 참고하면 된다.)

하지만 포인터변수 p가 가리키는 int는 동적으로 실행 중에 메모리의 힙(heap) 영역에 잡히게 된다.

.

.

곧 이어서 쓰겠습니다.

728x90

+ Recent posts