C++ 스터디 #10: 포인터(2)

시간 2021년 7월 13일 화요일 20:00 ~ 22:00
장소 ZOOM
참가자 -

출처: 천인국, “어서와 C++는 처음이지!”, INFINITY BOOKS

    https://boycoding.tistory.com/category [소년코딩]

만약 포인터가 선언만 되고 초기화되지 않았다면 포인터는 임의의 주소를 가리키게 됩니다. 따라서 이런 상태에서 포인터를 사용하여 메모리의 내용을 변경한다면 문제가 발생합니다.

int *p;		//포인터 p는 초기화가 안 되어 있음
*p = 100;	//위험한 코드
그래서 포인터가 아무것도 가리키고 있지 않을 때는 nullptr로 설정하는 것이 바람직합니다. 왜냐하면 nullptr을 사용하여서 어딘가에 접근하려하면 시스템에서 자동적으로 오류를 감지할 수 있기 때문입니다.
int *p = nullptr;

C++은 세 가지 기본 타입의 메모리 할당을 지원합니다.

1. 정적 메모리 할당(static memory allocation)은 정적 변수와 전역변수에 대해 발생한다. 이러한 타입의 변수에 대한 메모리는 프로그램이 실행될 때 한 번 할당되며, 프로그램 수명 내내 지속한다.
2. 자동 메모리 할당(auto memory allocation)은 함수 매개 변수와 지역 변수에 대해 발생한다. 이러한 타입의 변수에 대한 메모리는 관련 블록을 입력할 때 할당되고, 블록을 종료할 때 필요에 따라 여러 번 해제된다.
3. 동적 메모리 할당(dynamic memory allocation)은 이 스터디의 주된 내용이다.

정적 및 자동 메모리 할당에는 두 가지 공통점이 있다.
⦁변수/배열의 크기는 컴파일 할 때 알아야 한다.
⦁메모리 할당 및 해제가 자동으로 수행된다. (변수가 인스턴스화/제거되는 경우)
그래서 외부(사용자 또는 파일) 입력을 처리할 때(ex. cin으로 배열 크기를 받아오고 싶을 때) 이러한 제약 조건으로 인해 문제가 발생하는 상황이 생길 수 있다.

예를 들어, 문자열을 사용하여 누군가의 이름을 저장할 수 있지만, 입력할 때까지 이름의 길이를 알 수 없습니다. 또는 디스크에서 여러 레코드를 읽으려고 할 수 있지만, 실제로 얼마나 많은 레코드가 있는지 알 수 없다. 또한, 게임에서 얼마나 많은 몬스터를 가지는 배열을 만들어야 하는지 알 수 없다.

컴파일 시간에 모든 것의 크기를 선언해야 한다면, 할 수 있는 최선의 방법은 필요로 하는 변수의 최대 크기를 추측하고 충분하다고 가정하는 것이다.

char name[25]; // let's hope their name is less than 25 chars!
Record record[500]; // let's hope there are less than 500 records!
Monster monster[40]; // 40 monsters maximum
위 프로그램은 몇 가지 이유로 좋지 않다.

첫째, 변수가 실제로 사용되지 않으면 낭비되는 메모리가 많다.

둘째, 고정 배열을 포함한 대부분의 일반 변수는 스택(stack)이라는 메모리 영역에 할당된다. 일반적으로 Visual Studio는 스택 크기를 1MB로 기본 설정하며, 이 크기를 초과하면 스택 오버플로가 발생하고 운영 체제가 프로그램을 종료한다. 많은 프로그램, 특히 그래픽을 다루는 프로그램에서 메모리가 1MB로 제한되면 문제가 발생한다.

셋째, 가장 중요한 것은 인위적인 한계 및 또는 배열 오버플로가 발생할 수 있다는 것이다. 사용자가 디스크에서 600개의 레코드를 읽으려고 하지만 최대 500개의 레코드에 대해서만 메모리를 할당하면 어떻게 될까? 오류가 나거나, 500개의 레코드만 읽거나, 레코드 배열을 오버플로 하는 현상이 일어난다.

이러한 문제는 동적 메모리 할당(dynamic memory allocation)을 통해 쉽게 해결할 수 있다.

동적 메모리 할당은 프로그램 실행 중에 필요한 메모리를 운영체제에 요청하는 방법이다. 이 메모리는 프로그램의 제한된 스택(stack) 메모리에서 할당되는 것이 아니라 힙(heap)이라는 운영체제에서 관리하는 훨씬 더 큰 메모리 풀에서 할당된다. 최신 시스템에서는 힙 크기가 기가바이트 단위가 될 수 있다.

동적 메모리 할당을 이용하면 필요한 때에 필요한 만큼만 할당을 받을 수 있어서 메모리를 효울적으로 사용할 수 있다. 동적 메모리 할당을 할 때에는 얼마나 할당받을 것인지 결정하고 운영체제에게 메모리를 요청하는 단계가 필요합니다. 프로그램은 할당된 메모리를 사용하여 작업을 합니다. 사용이 끝나면 메모리를 다시 운영 체제에 반납하는 단계가 필요하다. 지금부터 메모리를 할당하고 반납하는 방법에 대해 살펴보자.

동적 메모리는 new 연산자를 이용하여서 할당된다. new 뒤에는 자료형을 적는다. 만약 하나 이상의 요소가 필요하다면 [ ] 안에 요소의 숫자를 적습니다. 이는 아래서 이야기할 1차원 배열과 관련이 있다. new 연산자는 할당되는 동적 메모리의 시작 주소를 반환한다.

int *p;		//포인터 선언
p = new int;	//p는 동적 메모리를 가리키는 포인터, int는 동적 메모리의 타입
p = new int[N]; //[N]은 동적 메모리의 개수
p = new int[N] {x1, x2, ---, xN};. //{ }안 의 값은 동적 메모리의 초기값들
이제 동적 메모리를 사용하는 법을 알았으니 동적 메모리를 해제하는 방법을 알아보자.
delete p;
delete [] p;
동적 할당받은 메모리 공간을 반납하려면 위와 같은 방법을 사용하면 됩니다. 주의할 점은 만약 p = new int[10];과 같이 할당한 경우는 반드시 delete [] p;로 해제해야한다.

이제 동적 메모리 할당을 통해 1차원 배열을 다뤄보자.
우선 배열을 할당하고 해제하는 방법은 위에서도 언급했다.

int *p;		//포인터 선언
p = new int[5]; //int형 변수 5개를 저장할 수 있는 공간 할당

delete [] p;
위의 문장을 실행하면 int형 변수 5개를 저장할 수 있는 공간이 할당되고 첫 번째 변수를 가리키는 주소를 반환한다.
따라서 p가 사리키는 첫 번째 변수는 p[0] 또는 *p로 접근할 수 있다. 두 번째 변수는 p[1] 또는 *(p+1)로 접근 할 수 있다.

동적 배열을 0이나 원하는 값으로 초기화하는 문법은 매우 간단하다.
#include <iostream>
using namespace std;


int main()
{
	int length = 3;
	int* array = new int[length]();

	for (int i = 0; i < length; i++)
		cout << array[i] << endl;
	
	array = new int[5]{ 9, 7, 5, 3, 1 };

	for (int i = 0; i < 5; i++)
		cout << array[i] << endl;

	delete[] array;
	return 0;
}
단 초기화 할 때 동적 배열은 명시적으로 길이를 설정해 선언해야 한다.
int fixedArray[] {1, 2, 3}; // okay: implicit array size for fixed arrays

int* dynamicArray1 = new int[] {1, 2, 3}; // not okay: implicit size for dynamic arrays

int* dynamicArray2 = new int[3] {1, 2, 3}; // okay: explicit size for dynamic arrays

정수 5개를 저장할 수 있는 동적 배열 생성후 난수를 저장해 출력하기

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

int main()
{
	int *ptr;

	srand(time(NULL));
	ptr = new int[10];

	for (int i = 0; i<10; i++)
		ptr[i] = rand();

	for (int i = 0; i<10; i++)
		cout << ptr[i] << " ";

	delete[] ptr;
	cout << endl;
	return 0;
}

앞에서는 포인터로 1차원 배열을 다뤘다. 이번에는 이중 포인터 또는 그 이상을 이용하면 다차원 배열을 다룰 것이다.
동적으로 포인터의 배열을 할당할 때 이중 포인터를 사용하는 방법은 앞에서 배운 방법보단 복잡하다. 2차원 고정 배열 선언은 몹시 간단하다.

int array[10][5];
그렇다면 처음에는 동적으로 2차원 배열을 할당할 때 이런 식으로 시도할 수 있을 것이다.
int** array = new int[10][5];	//잘못된 코드
하지만 위의 코드는 작동하지 않을 것이다. 이제 제대로 동적으로 2차원 배열을 할당해보자.
int** m = new int*[3]; // 행(row)의 개수가 3이다.
for (int row = 0; row < 10; row++)
	m[row] = new int[4]; //열(column)의 개수가 4이다.

이후 일반적인 방법으로 포인터를 사용해 배열에 접근할 수 있다.
m[2][1] = 3;
2차원 동적 배열을 할당할 때 루프를 사용한 것처럼 해제하려면 루프가 필요하다.
for (int row = 0; row < 10; row++)
	delete[] m[row];
delete[] m;
이때 반드시 할당한 순서의 반대로 해제해야 한다. 왜냐하면 전체 배열을 먼저 해제하면 배열의 요소를 해제하기 위해 전체 배열에 접근해야 하는데 이때 정의되지 않은 동작이 발생한다.

#include <iostream>
using namespace std;

int main()
{
	int row=0, col=0;
	cout << "열과 행 입력: ";
	cin >> row >> col;

	int** m = new int* [row];
	for (int i = 0; i < row; i++)
		m[i] = new int[col];

	for (int i = 0; i < row; i++)
	{
		for (int j = 0; j < col; j++)
			m[i][j] = i + j;
	}

	for (int i = 0; i < row; i++)
	{
		for (int j = 0; j < col; j++)
			cout<<m[i][j]<<" ";
		cout << endl;
	}
	
	for (int i = 0; i < row; i++)
		delete[]m[i];
	delete[]m;

	return 0;
}

입력받을 정수 개수를 입력받고 정수를 입력한 후 입력받은 정수들 출력하기
Tip. 동적 배열은 p = new int[i]와 같이 생성한다.

입력받을 이름 개수를 입력받고 이름을 입력한 후 입력받은 이름들 출력하기
Tip. 문자열을 저장하는 동적 배열은 string* names = new string[length]; 와 같이 생성한다.

cin을 사용해 정사각행렬의 크기가 입력됩니다.(n×n 행렬) 이때 입력된 크기의 단위행렬을 출력하기.

  • activity/public/2021/cpp/210713.txt
  • 마지막으로 수정됨: 3년 전
  • 저자 bkparks12