220914 C++ #5

김혜진·2022년 9월 14일
0

C++

목록 보기
5/12

C++ #5

new/delete 연산자와 동적 메모리

동적 메모리의 필요성

  • 동적 메모리는 실행 시간에 할당되어 사용되는 메모리 블록을 말한다.
  • 동적 메모리의 반댓말은 정적 메모리이다.
  • 동적 메모리는 프로그램 작성 시 얼마만큼의 메모리가 필요한지 알지 못하는 경우에 사용된다.
  • 동적 메모리 영역을 우리는 힙(heap) 영역이라고 한다.
  • C++에서는 동적 메모리 생성 시 new를, 소멸 시 delete 연산자를 사용

new 연산자

  • C++에서는 힙 메모리에 동적 메모리를 할당할 때 new 연산자를 사용
  • 자신이 할당하는 객체의 데이터형을 알고 있다.
  • 그 데이터형의 포인터를 반환한다.
pBuffer = new int[nLength];

객체가 넘어온다 = 주소값을 리턴한다!
힙 메모리에서 스택 메모리로 주소값을 넘겨준다.

int ar[100]; // 스택 메모리, 정적
new int[]; // 힙 메모리, 동적

delete 연산자

  • new 연산자는 delete 연산자와 짝을 이룬다.
  • new 연산자로 힙 영역에 동적으로 할당한 메모리를 해제시킨다.
  • 해제된 메모리 블록을 힙 영역에 다시 되돌려준다.
delete pBuffer;
  • 힙 영역에 할당한 메모리 블록이 배열의 형태로 할당되어 있는 경우
delete[] pBuffer;

힙 영역에 객체 생성

  • new 연산자를 통해 클래스 타입의 메모리를 생성 후 선언한 객체 포인터 변수에 생성 메모리 주소값을 넘겨준다.
MousePoint *pt;
pt = new MousePoint(10,20);
void main()
{
	MousePoint *pt = new MousePoint(10, 20);
	cout << pt->getX() << endl;
	cout << pt->getY() << endl;
	delete pt;
}

간접참조 ->는 C++에서만 쓴다. 다른 언어에서는 pt.getX()와 같은 직접참조 형식으로 사용한다.

객체 포인터 배열

  • 여러 개의 객체를 관리하는 방법으로 배열을 선언하여 사용할 수 있는데, 이를 '객체 포인터 배열'이라고 한다.
MousePoint *pArr[3];

  • 각 기억 공간에 다음과 같이 객체를 생성하여 주소값을 넘겨줄 수 있다.
pArr[0] = new MousePoint(10,20);
pArr[1] = new MousePoint(100,200);
pArr[2] = new MousePoint(1000,2000);

new 연산자로 생성되는 모든 것들은 객체!

void main()
{
	//MousePoint* pt1 = new MousePoint(10, 20);
	//MousePoint* pt2 = new MousePoint(100, 200);
	//MousePoint* pt3 = new MousePoint(1000, 2000);

	MousePoint* pt[3];
	
	pt[0] = new MousePoint(10, 20);
	pt[1] = new MousePoint(100, 200);
	pt[2] = new MousePoint(1000, 2000);


	for (int i = 0; i < 3; i++)
	{
		cout << pt[i]->getX() << endl;
		cout << pt[i]->getY() << endl;
	}

	for (int i = 0; i < 3; i++)
	{
		delete pt[i];
	}
}

힙 메모리가 배열로 할당이 됐을 때는 delete에 배열표시를 붙여줘야 하고, delete[] pt (배열이 오른쪽에서 할당)

pBuffer = new int[num];

힙 메모리 자체가 배열이 아닐 때는 배열 표시가 붙지 않고 배열의 요소를 써야한다. delete pt[i] (왼쪽에서 할당받는 배열)

pt[0] = new MousePoint(10, 20);

포인터 멤버변수를 갖는 클래스

클래스 내의 동적 메모리 할당

  • 새로운 클래스를 생성한다. 클래스 이름은 String이고 객체 생성 시 전달인자로 문자와 문자의 크기를 갖는다.

자바, C#
String str = "Hello";

C, C++
char str[] = "Hello";
char* pStr = str;

자바와 C# 등에서는 Stirng이라는 타입(클래스)가 있지만, C와 C++에는 없어서 char를 사용한다.

#include<iostream>

using namespace std;

class String
{
private:
	char* pBuffer;
	int nLength;

public:
	String(char ch, int size);
	~String();
};

String::String(char ch, int nSize)
{
	nLength = nSize;
	pBuffer = new char[nLength + 1]; // 문자열은 끝에 \0 (null) 값이 붙으므로 + 1
	memset(pBuffer, ch, nLength); // pBuffer에 ch를 nLength만큼 채우겠다.
	pBuffer[nLength] = '\0';
	cout << "pBuffer : " << pBuffer << endl;
	cout << "nLength : " << nLength << endl;
}

String::~String()
{
	delete pBuffer; // new로 동적 호출했으니 delete로 되돌린다.
}

void main()
{
	String str('A', 3);
}

void* memset(void* ptr, int value, size_t num);
첫번째 인자 void* ptr : 세팅하고자 하는 메모리의 시작 주소
두번째 인자 value : 메모리에 세팅하고자 하는 값
세번째 인자 size_t : 길이

여기서 str2 = str1 하면 오류는 나지만 실행은 된다.

void main()
{
	String str1('A', 3);
	String str2('B', 5);
	str2 = str1; //
}

스택 메모리에 의해 객체 str2가 먼저 삭제가 되고, str1이 삭제가 되는데 str2의 pBuffer가 삭제될 때, str2의 주소값이 str1의 주소값으로 대입되어 있기 때문에 str1의 주소값이 삭제가 된다.
따라서 이후 str1의 pBuffer가 삭제될 때 가리키고 있는 pBuffer는 존재하지 않아 오류가 나는 것이다.
⇒ delete 시도할 때 문제가 생김.

<대입 이전>

<대입 이후>

메모리 누수 문제가 발생한다.

객체끼리의 대입 및 문제점

  • 문제는 str2 = str1 구문에 있다. 객체끼리의 대입은 멤버 대 멤버의 복사가 일어난다.
str2.nLength = str1.nLength;
str2.pBuffer = str1.pBuffer;

nLength 멤버 변수끼리의 대입은 문제 없다.
pBuffer 멤버 변수는 포인터이기 때문에 문제가 발생한다.

  • 같은 주소값을 갖는 것은 문제가 되지 않는다!! 문제는 객체가 소멸할 때이다.
  • str2 객체가 먼저 소멸할 것인데, 소멸 시 소멸자를 호출하게 되면, str2의 pBuffer 멤버변수가 가리키고 있는 힙 영역을 delete 하게 된다.
  • 문제는 str2가 소멸 시 삭제한 메모리를 str1도 가리키고 있으므로 str1 객체 소멸 시 pBuffer가 가리키는 메모리 영역을 다시 delete 하려는 시도를 함으로써 치명적인 에러를 유발하는 것이다.
  • 이러한 문제를 해결하기 위해 '대입 연산자 오버로딩(Assignment Operator Overloading)'이라는 방법을 사용한다.

대입 연산자 오버로딩

연산자 오버로딩이란

  • 함수명이 같고, 전달인자의 타입이나 개수가 다른 함수 여러 개를 만들어 사용하는 방법을 오버로딩이라고 한다.
  • 마찬가지로 연산자 또한 기호가 같은 연산자를 여러가지 기능을 가질 수 있도록 정의할 수 있는데 이것을 '연산자 오버로딩'이라고 한다.

대입 연산자 오버로딩 멤버함수 작성


기존에 있는 것을 delete 하고, 새로운 메모리를 생성해서 새로운 주소값을 받아온다!

	void operator=(String& str1) // 대입 연산자 오버로딩 멤버함수
	{
		delete this->pBuffer;
		this->nLength = str1.nLength;
		this->pBuffer = new char[this->nLength + 1]; // 새로운 메모리를 할당해서 그 주소값을 pBuffer에 넘겨줌
		strcpy(this->pBuffer, str1.pBuffer);
	}

예제

#include<iostream>
#include<string.h>

using namespace std;

class String
{
private:
	char* pBuffer;
	int nLength;

public:
	String(char ch, int size);
	~String();
	void operator=(String& str1); // 대입 연산자 오버로딩 멤버함수
	void SetData();
};

String::String(char ch, int nSize)
{
	nLength = nSize;
	pBuffer = new char[nLength + 1]; // 문자열은 끝에 \0 (null) 값이 붙으므로 + 1
	memset(pBuffer, ch, nLength); // pBuffer에 ch를 nLength만큼 채우겠다.
	pBuffer[nLength] = '\0';
	cout << "pBuffer : " << pBuffer << endl;
	cout << "nLength : " << nLength << endl;
}

String::~String()
{
	delete pBuffer; // new로 동적 호출했으니 delete로 되돌린다.
}

void String::operator=(String& str1) // 대입 연산자 오버로딩 멤버함수
{
	delete this->pBuffer;
	//this->nLength = str1.nLength;
	this->pBuffer = new char[this->nLength + 1]; // 새로운 메모리를 할당해서 그 주소값을 pBuffer에 넘겨줌
	strcpy_s(this->pBuffer, this->nLength + 1, str1.pBuffer);
}
void String::SetData()
{
	cout << "pBuffer : " << this->pBuffer << endl;
	cout << "nLength : " << this->nLength << endl;
}

void main()
{
	String str1('A', 3);
	String str2('B', 5);
	//str2 = str1; // 에러

	cout << "대입 전" << endl;
	str2.SetData();

	str2.operator=(str1); // 대입 연산자 오버로딩
	// str2 = str1과 같은 문장

	cout << "대입 후" << endl;
	str2.SetData();
}

str2.operator=(str1);str2 = str1은 같은 기능을 하는 문장이다.

객체의 자기 자신 대입 시에 대한 처리

str1 = str1;

문제는 strcpy() 시에 s.pBuffer 값을 가져올 수 없으므로 복사 실패함.

새로운 메모리의 주소값을 만들어 넘겨 받는다.
하지만 값을 가져올 주소값을 잃었으므로 값을 가져올 수 없다. (원본이 사라짐)

profile
알고 쓰자!

0개의 댓글