학습목표

  • 클래스 멤버를 위한 동적 메모리 대입
  • 암시적/명시적 복사 생성자
  • 암시적/명시적 오버로딩 대입 연산자
  • 생성자에 new 사용하기
  • static 클래스 멤버
  • 객체에 위치지정 new 사용하기
  • 객체를 지시하는 포인터
  • 큐 ADT 구현

12.1 동적 메모리와 클래스

사용할 메모리의 크기를 컴파일할 때 결정하지 않고 프로그램 실행시에 상황에 따라 융통성있게 결정하는 것이 더 유용함
따라서 C++에서는 newdelete를 클래스와 함께 사용하게되며 생성자와 파괴자를 다루는데 주의가 필요함

복습을 위한 예제와 static 클래스 멤버

// strngbad.h

#include <iostream>
#ifndef STRINGBAD_H_
#define STRINGBAD_H_

class StringBad
{
	private:
		char * str;
		int len;
		static int num_strings;
	public:
		StringBad(const char * s);
		StringBad();
		~StringBad();
		friend std::ostream & operator<<(std::ostream & os, const StringBad & st);
};

#endif

생성자의 매개변수로 char 포인터형을 받고, 생성자 안에서 new를 사용하여 가변적인 크기의 문자열을 사용함
num_string 멤버를 static으로 선언하여 모든 객체들이 동일한 멤버를 공유하도록 함

// strngbad.cpp

#include <cstring>
#include "strngbad.h"
using std::cout;

int StringBad::num_strings = 0;

StringBad::StringBad(const char * s)
{
	len = std::strlen(s);
	str = new char[len + 1];
	std::strcpy(str, s);
	num_strings++;
	cout << num_strings << " : \"" << str
		 << "\" 객체 생성\n";
}

StringBad::StringBad()
{
	len = 4;
	str = new char[4];
	std::strcpy(str, "C++");
	num_strings++;
	cout << num_strings << " : \"" << str
		 << "\" 디폴트 객체 생성\n";
}

StringBad::~StringBad()
{
	cout << "\"" << str << "\" 객체 파괴, ";
	--num_strings;
	cout << "남은 객체 수 : " << num_strings << "\n";
	delete [] str;
}

std::ostream & operator<<(std::ostream & os, const StringBad & st)
{
	os << st.str;
	return os;
}

static 멤버 변수는 클래스 선언 안에서 초기화할 수 없음
클래스 선언이 일반적으로 들어있는 헤더 파일은 여러 파일에 포함될 수 있기 때문에, 초기화는 클래스 선언 파일이 아닌 메소드 구현 파일에 넣음
단, static 데이터 멤버가 정수형 또는 열거형의 const로 클래스 상수를 만드는데 사용되는 경우에는 클래스 선언 안에서 초기화할 수 있음

객체가 파괴될 경우 객체 자체가 차지하고있던 메모리는 해제되지만, 객체의 멤버가 지시하는 메모리는 자동으로 해제되지 않음
따라서 파괴자에서 delete를 사용해 더이상 사용하지 않을 str포인터가 지시하는 메모리를 해제하는 것이 중요함

// vegnews.cpp

#include <iostream>
using std::cout;
#include "strngbad.h"

void callme1(StringBad &);
void callme2(StringBad);

int main()
{
	using std::endl;
	{
		cout << "내부 블록을 시작한다.\n";
		StringBad headline1("Celery Stalks at Midnight");
		StringBad headline2("Lettuce Prey");
		StringBad sports("spinach Leaves Bowl for Dollars");
		cout << "headline1 : " << headline1 << endl;
		cout << "headline2 : " << headline2 << endl;
		cout << "sports : " << sports << endl;
		callme1(headline1);
		cout << "headline1 : " << headline1 << endl;
		callme2(headline2);
		cout << "headline2 : " << headline2 << endl;
		cout << "하나의 객체를 다른 객체로 초기화 : \n";
		StringBad sailor = sports;
		cout << "sailor : " << sailor << endl;
		cout << "하나의 객체를 다른 객체에 대입 : \n";
		StringBad knot;
		knot = headline1;
		cout << "knot : " << knot << endl;
		cout << "이 블록을 빠져나온다.\n";
	}
	cout << "main()의 끝\n";

	return 0;
}

void callme1(StringBad & rsb)
{
	cout << "참조로 전달된 StringBad : \n";
	cout << "	\"" << rsb << "\"\n";
}

void callme2(StringBad sb)
{
	cout << "값으로 전달된 StringBad : \n";
	cout << "	\"" << sb << "\"\n";
}

callme2(headline2)의 경우 객체를 참조로 전달하지 않고 값으로 전달하여 파괴자가 호출되었기 때문에, 원본 headline2의 문자열을 알아볼 수 없게 됨

StringBad sailor = sports;StringBad sailor = StringBad(sports);와 같고, 이 때 사용되는 생성자는 StringBad(const StringBad &);의 형태를 가짐
이렇게 하나의 객체를 다른 객체로 초기화할시 자동으로 발생되는 생성자를 복사 생성자(copy constructor)라고 함


특별 멤버 함수

C++는 여러가지 멤버 함수를 자동으로 제공함

  • 생성자를 전혀 정의하지 않았을 경우의 디폴트 생성자
  • 디폴트 파괴자를 정의하지 않았을 경우의 디폴트 파괴자
  • 복사 생성자를 정의하지 않았을 경우의 복사 생성자
  • 대입 연산자를 정의하지 않았을 경우의 대입 연산자
  • 주소 연산자를 정의하지 않았을 경우의 주소 연산자
  • C++11에서는 move 생성자와 move 대입 연산자도 제공함

암시적 주소 연산자는 호출한 객체의 주소를 리턴함
암시적 디폴트 파괴자는 아무 일도 하지 않음
암시적 디폴트 생성자 역시 아무 일도 하지 않으며, 디폴트 생성자 호출을 통해 생성된 객체를 보통의 자동 변수처럼 초기에 그 값이 알려지지 않게 만듬

  • 디폴트 생성자는 매개변수를 사용하지 않는 생성자임
  • 단, 매개변수를 사용하는 생성자의 경우에도 모든 매개변수에 디폴트 값을 제공할시에는 디폴트 생성자가 될 수 있음
  • 디폴트 생성자는 단 하나만 가질 수 있음

복사 생성자는 어떤 객체를 새로 생성되는 객체에 복사하는데에 사용함
즉, 값 전달에 의한 함수 매개변수 전달을 포함한 초기화 작업에 사용

  • 클래스 복사 생성자는 일반적으로 Class_name(const Class_name &); 형태의 원형을 가짐
  • 복사 생성자는 새로운 객체가 생성되어 같은 종류의 기존 객체로 초기화될 때 사용됨
  • 프로그램이 객체의 복사본을 생성할 때에도 사용되며, 특히 함수가 객체를 값으로 전달하거나 객체를 리턴할 때도 사용됨
  • 따라서 참조로 전달하여 생성자를 호출하는 시간과 새로운 객체를 저장하는 메모리 공간을 절약하는 것이 권장됨
  • 디폴트 복사 생성자는 static멤버를 제외한 멤버들을 멤버별로 복사함

Stringbad로 회귀 : 복사 생성자에서 어디가 잘못되었나

StringBad 클래스의 암시적 복사 생성자에서는 문자열을 지시하는 포인터가 복사됨
따라서 복사된 객체가 소멸되며 포인터가 지시하는 메모리를 해제하면, 원본 객체가 소멸될 때 이미 해제된 메모리를 다시 한번 해제하게되므로 알 수 없는 결과가 발생함

StringBad::StringBad(Const StringBad & st)
{
	num_strings++;
	len = st.len;
	str = new char[len + 1];
	std::strcpy(str, st.str);
	cout << num_strings << " : \"" << str << "\" 객체 생성\n";
}

이 문제를 해결하기 위해 복사 생성자가 문자열의 주소를 복사하는 것이 아닌, 문자열 자체를 복사한 후 복사본의 주소를 대입하도록하는 깊은 복사(deep copy)를 사용할 수 있음
이와같이 클래스 멤버에 new에 의해 초기화되는 데이터를 지시하는 포인터가 있다면 복사 생성자를 필수적으로 정의해야함


기타 Stringbad 문제점 : 대입 연산자

디폴트 대입 연산자는 자동으로 오버로딩되며 Class_name & Class_name::operator=(const Class_name &); 형태의 원형을 가짐
단, 객체를 초기화할 때는 항상 복사 생성자가 호출되며 이후 대입될 수 있음
대입 연산자 역시 복사 생성자와 같이 동일한 문자열 주소에 대해 메모리를 해제하는 경우가 생길 수 있기 때문에 문제가 발생함

StringBad & StringBad::operator=(const StringBad & st)
{
	if (this == &st)
		return *this;
	delete [] str;
	len = st.len;
	str = new char[len + 1];
	str::strcpy(str, st.str);
	return *this;
}

대입에서 발생하는 문제들은 깊은 복사를 하는 대입 연산자 사용자 정의를 제공하여 해결할 수 있음

  • 타깃 객체가 이전에 대입된 데이터를 참조하고있을 가능성이 있으므로 delete []를 사용하여 메모리를 해제시켜야함
  • 객체에 자기 자신을 대입하는 경우를 막아야함
  • 함수는 호출한 객체에 대한 참조를 리턴해야함


새롭게 개선된 String 클래스

개선된 디폴트 생성자

String::String()
{
	len = 0;
	str = new char[1];
	str[0] = '\0';
}

str = new char 대신 str = new char[1]을 사용하여 파괴자의 delete [] str; 코드와 호환되도록 만듬
delete []new []를 사용한 포인터 및 null 포인트와 호환되므로 str = new char[1]; str[0] = '\0';str = 0;으로도 대체 가능함
이 때 null 포인터를 나타내는 0을 C++11에서는 nullptr 키워드로 나타낼 수 있음


비교 멤버

bool operator<(const String & st1, const String & st2)
{
	if (std::strcmp(st1.str, st2.str) < 0)
		return true;
	else
		return false;
}

strcmp()함수는 첫번째 매개변수가 두번째 매개변수보다 앞에오면 음수, 반대면 양수, 같으면 0을 리턴함
내장된 < 연산자가 bool 연산자를 리턴하기 때문에 위 코드를 아래와 같이 간단하게 만들 수 있음

bool operator<(const String * st1, const String & st2)
{
	return (std::strcmp(st1.str, str2.str) < 0);
}

[]표기를 사용하여 개별 문자에 접근하기

[]도 하나의 연산자로서 operator[]() 메소드를 사용하여 오버로딩할 수 있음

char & String::operator[](int i)
{
	return str[i];
}

단, const String answer("futile")과 같이 const 객체가 있다면 위 메소드 사용시 에러가 발생함
이 때는 const 유무에 따라 시그내처가 구별된다는 점을 이용하여 const char & String::operator[](int i) const 메소드를 사용함으로써 문제를 해결할 수 있음


static 클래스 멤버 함수

멤버 함수를 static으로 선언하는 것이 가능함

  • static 멤버 함수는 객체에 의해 호출될 필요가 없음
    • 즉, static int HowMany(){return num_strings;} 함수가 있을 경우 이는 int count = String::HowMany();로 호출될 수 있음
  • static 멤버 함수는 객체와 결합하지 않기 때문에 static으로 선언된 데이터 멤버에만 접근할 수 있음

대입 연산자 오버로딩에 대한 보충

String name;
char temp[40];
cin.getline(temp, 40);
name = temp;

문자열 복사시 위와 같이 생성자로 임시 객체 생성 > 임시 객체 복사 > 파괴자로 임시 객체 파괴의 과정을 거칠 수 있음
그러나 아래와 같은 대입 연산자를 오버로딩하여 문자열을 직접 다루게 하는 것이 더 효율적임

String & String::operator=(const char * s)
{
	delete [] str;
	len = std::strlen(s);
	str = new char[len + 1];
	std::strcpy(str, s);
	return *this;
}

String 클래스 개선판

// string1.h

#ifndef STRING1_H_
#define STRING1_H_
#include <iostream>
using std::ostream;
using std::istream;

class String
{
	private:
		char * str;
		int len;
		static int num_strings;
		static const int CINLIM = 80;
	public:
		String(const char * s);
		String();
		String(const String & st);
		~String();
		int length()const { return len; };
		String & operator=(const String & st);
		String & operator=(const char * s);
		char & operator[](int i);
		const char & operator[](int i) const;
		friend bool operator<(const String & st1, const String & st2);
		friend bool operator>(const String & st1, const String & st2);
		friend bool operator==(const String & st1, const String & st2);
		friend ostream & operator<<(ostream & os, const String & st);
		friend istream & operator>>(istream & is, String & st);
		static int HowMany();
};

#endif
// string1.cpp

#include <cstring>
#include "string1.h"
using std::cin;
using std::cout;

int String::num_strings = 0;

int String::HowMany()
{
	return num_strings;
}

String::String(const char * s)
{
	len = std::strlen(s);
	str = new char[len + 1];
	std::strcpy(str, s);
	num_strings++;
}

String::String()
{
	len = 4;
	str = new char[1];
	str[0] = '\0';
	num_strings++;
}

String::String(const String & st)
{
	num_strings++;
	len = st.len;
	str = new char[len + 1];
	std::strcpy(str, st.str);
}

String::~String()
{
	--num_strings;
	delete [] str;
}

String & String::operator=(const String & st)
{
	if (this == &st)
		return *this;
	delete [] str;
	len = st.len;
	str = new char[len + 1];
	std::strcpy(str, st.str);
	return *this;
}

String & String::operator=(const char * s)
{
	delete [] str;
	len = std::strlen(s);
	str = new char[len + 1];
	std::strcpy(str, s);
	return *this;
}

char & String::operator[](int i)
{
	return str[i];
}

const char & String::operator[](int i) const
{
	return str[i];
}

bool operator<(const String & st1, const String & st2)
{
	return (std::strcmp(st1.str, st2.str) < 0);
}

bool operator>(const String & st1, const String & st2)
{
	return st2 < st1;
}


bool operator==(const String & st1, const String & st2)
{
	return (std::strcmp(st1.str, st2.str) == 0);
}


ostream & operator<<(ostream & os, const String & st)
{
	os << st.str;
	return os;
}

istream & operator>>(istream & is, String & st)
{
	char temp[String::CINLIM];
	is.get(temp, String::CINLIM);
	if (is)
		st = temp;
	while (is && is.get() != '\n')
		continue;
	return is;
}
// sayings1.cpp

#include <iostream>
#include "string1.h"
const int ArSize = 10;
const int MaxLen = 81;

int main()
{
	using std::cout;
	using std::cin;
	using std::endl;
	String name;
	
	cout << "안녕하십니까? 성함이 어떻게 되십니까?\n>> ";
	cin >> name;

	cout << name << " 씨! 간단한 영어 속담 " << ArSize
		 << "개만 입력해 주십시오(끝내려면 Enter) : \n";
	String sayings[ArSize];
	char temp[MaxLen];
	int i;
	for (i = 0; i < ArSize; i++)
	{
		cout << i + 1 << " : ";
		cin.get(temp, MaxLen);
		while (cin && cin.get() != '\n')
			continue;
		if (!cin || temp[0] == '\0')
			break;
		else
			sayings[i] = temp;
	}
	int total = i;
	if ( total > 0 )
	{
		cout << "(다음과 같은 속담들을 입력하셨습니다.)\n";
		for (i = 0; i < total; i ++)
			cout << sayings[i][0] << " : " << sayings[i] << endl;
		
		int shortest = 0;
		int first = 0;
		for (i = 1; i < total; i++)
		{
			if (sayings[i].length() < sayings[shortest].length())
				shortest = i;
			if (sayings[i] < sayings[first])
				first = i;
		}
		cout << "가장 짧은 속담 : \n" << sayings[shortest] << endl;
		cout << "사전순으로 가장 앞에 나오는 속담 : \n" << sayings[first] << endl;
		cout << "이 프로그램은 " << String::HowMany()
			 << "개의 String 객체를 사용하였습니다. 이상!\n";
	}
	else
		cout << "입력 금지! 이상.\n";
	return 0;
}


12.3 생성자에 new를 사용할 때 주의할 사항

생성자에서 new를 사용하여 포인터 멤버를 초기화했을시, 파괴자에서 반드시 delete해줘야함
newdelete, new []delete []와 짝을 이루어야함
모든 생성자에서는 같은 방법으로 new가 사용되어야하고, 파괴자는 그 방법과 어울려야함
단, new와 널 포인터 초기화는 똑같이 delete로 처리되기 때문에 혼용 가능함

String::String(const String & st)
{
	num_string++;
	len = st.len;
	str = new char[len + 1];
	std::strcpy(str, st.str);
}

깊은 복사를 통해 하나의 객체를 다른 객체로 초기화하는 복사 생성자를 정의해야함
형태는 일반적으로 위와 같음

  • static 멤버 갱신 처리
  • 같은 크기로 설정, 기억 공간을 대입하고 문자열을 새 위치로 복사
String & String::operator=(const String & st)
{
	if (this == &st)
		return *this;
	delete [] str;
	len = st.len;
	str = new char[len + 1];
	std::strcpy(str, st.str);
	return *this;
}

깊은 복사를 통해 하나의 객체를 다른 객체에 대입하는 대입 연산자를 정의해야함
형태는 일반적으로 위와 같음

  • 객체에 자기 자신이 대입되었을시 자기 자신을 리턴하며 종료
  • 아닐 경우 기존의 문자열을 해제하고, 새 문자열을 위한 공간을 확보한 뒤 복사
  • 마지막으로 호출한 객체에 대한 참조를 리턴함

사용하지 말아야 할 것과 사용해도 좋은 것

String::String()
{
	str = "디폴트 문자열";
	len = std::strlen(str);
}

str 초기화시 new를 사용하지 않았기 때문에 파괴자에서 delete 적용시 문제가 발생함

String::String(const char * s)
{
	len = std::strlen(s);
	str = new char;
	std::strcpy(str, s);
}

new를 적용하지만 정확한 크기의 메모리를 요청하지 못하기 때문에 영문자 하나가 들어갈 수 있는 크기의 블록을 리턴함
따라서 긴 문자열 복사 시도시 메모리 문제가 발생함

String::String(const String & st)
{
	len = st.len;
	str = new char[len + 1];
	std::strcpy(str, st.str);
}

String::String()
{
	len = 0;
	str = new char[1];
	str[0] = '\0';
}

String::String()
{
	len = 0;
	str = 0;
}

String::String()
{
	static const char * s = "C++";
	len = std::strlen(s);
	str = new char[len + 1];
	std::strcpy(str, s);
}

위 생성자들은 모두 문제 없이 사용할 수 있음


클래스 멤버로 클래스를 가지는 경우의 멤버별 복사

class Magazine
{
	privae:
		String title;
		string publisher;
	...
};

사용자가 Magazine 객체를 복사하거나 다른 객체에 대입할경우 멤버별 복사 기능은 해당 멤버형의 복사 생성자와 대입 연산자를 사용함
그러나 Magazine 클래스가 여타 다른 클래스 멤버에 대한 복사 생성자 혹은 대입 연산자를 필요로 할 경우에는 사용 함수에서 Stringstring 복사 생성자와 대입 연산자를 명시적으로 불러줘야함



12.4 객체 리턴에 대한 관찰

const 객체에 대한 참조 리턴

어떤 함수가 객체 호출 방식 혹은 메소드 매개변수로 자신에게 전달된 객체를 리턴시, 참조를 전달함으로써 해당 메소드의 효율성을 높일 수 있음

// 버전 1
Vector Max(const Vector & v1, const Vector & v2)
{
	if (v1.magval() > v2.magval())
		return v1;
	else
		return v2;
}

// 버전 2
const Vector & Max(const Vector & v1, const Vector & v2)
{
	if (v1.magval() > v2.magval())
		return v1;
	else
		return v2;
}

버전 1과 같이 객체 리턴시 복사 생성자를 호출하지만, 버전 2와 같이 참조 리턴시 그렇지 않기 때문에 더 효율적임
참조는 호출하는 함수가 실행중일 때 존재하는 객체에 대한 참조여야함
v1v2 모두 const 참조로 선언되어있기 때문에 리턴형 역시 const여야함


const가 아닌 객체에 대한 참조 리턴

cout과 함께 사용하기 위한 대입 연산자의 오버로딩은 효율성을 위해, << 연산자의 오버로딩은 필요성 때문에 const가 아닌 객체를 리턴함

  • 대입 연산자의 경우 객체를 리턴하는 것과 참조를 리턴하는 것 양쪽 모두 동작함
  • 그러나 참조를 사용할 경우 복사 생성자를 호출하여 임시 객체가 만들어지는 것을 피할 수 있기 때문에 효율적임
  • << 연산자의 경우 ostream 클래스가 public 복사 생성자를 만들지 않기 때문에 ostream 리턴형을 사용할 수 없음
  • 따라서 대신 ostream &을 리턴함

객체 리턴

리턴되는 함수가 지역적인 경우 함수가 종료될 때 객체가 파괴자에 의해 파괴되기 때문에 참조로 리턴할 수 없음
일반적으로 오버로딩 산술 연산자들이 이 범주에 속함

Vector Vector::operator+(coonst Vector & b) const
{
	return Vector(x + b.x, y + b.y);
}

위 함수에서는 리턴되는 객체를 만들기 위해 복사 생성자를 암시적으로 호출함


const 객체 리턴

net = force1 + froce2; // 1
force1 + force2 = net; // 2
cout << (force1 + force2 = net).magval() << endl; // 3

리턴값을 나타내기 위해 복사 생성자가 임시 객체를 생성하기때문에 구문 2와 3도 가능함
구문 2,3과 같은 형태로의 사용을 막고싶다면 리턴형을 const로 선언하는 방법을 사용할 수 있음



12.5 객체를 지시하는 포인터

// sayings2.cpp

#include <iostream>
#include <cstdlib>
#include <ctime>
#include "string1.h"
const int ArSize = 10;
const int MaxLen = 81;

int main()
{
	using namespace std;
	String name;

	cout << "안녕하십니까? 성함이 어떻게 되십니까?\n>> ";
	cin >> name;

	cout << name << " 씨! 간단한 우리 속담 " << ArSize
		 << "개만 입력해 주십시오(끝내려면 Enter) : \n";
	String sayings[ArSize];
	char temp[MaxLen];
	int i;
	for (i = 0; i < ArSize; i++)
	{
		cout << i + 1 << " : ";
		cin.get(temp, MaxLen);
		while (cin && cin.get() != '\n')
			continue;
		if (!cin || temp[0] == '\0')
			break;
		else
			sayings[i] = temp;
	}
	int total = i;
	if ( total > 0 )
	{
		cout << "(다음과 같은 속담들을 입력하셨습니다.)\n";
		for (i = 0; i < total; i ++)
			cout << sayings[i] << "\n";

		String * shortest = &sayings[0];
		String * first = &sayings[0];
		for (i = 1; i < total; i++)
		{
			if (sayings[i].length() < shortest->length())
				shortest = &sayings[i];
			if (sayings[i] < *first)
				first = &sayings[i];
		}
		cout << "가장 짧은 속담 : \n" << *shortest << endl;
		cout << "사전순으로 가장 앞에 나오는 속담 : \n" << *first << endl;
		srand(time(0));
		int choice = rand() % total;
		String * favorite = new String(sayings[choice]);
		cout << "내가 가장 좋아하는 속담:\n" << *favorite << endl;
		delete favorite;
	}
	else
		cout << "알고있는 속담이 하나도 없어요?\n";
	cout << "프로그램을 종료합니다.\n";
	return 0;
}

객체를 지시하는 포인터를 사용하여 메모리를 추가로 대입하기 위한 new를 사용할 필요가 없음
String * favorite = new String(sayings[choice])new에 의해 새롭게 생성된 객체에 sayings[choice]객체를 사용하여 초기화하며, 해당 객체에는 favorite 포인터만이 접근 가능함

new와 delete 복습

객체가 자동 변수이면 해당 객체의 파괴자는 프로그램이 객체가 정의된 블록을 벗어날 때 호출됨
객체가 static 변수이면 해당 객체의 파괴자는 프로그램이 종료될 때 호출됨
new에 의해 생성된 객체라면 해당 객체의 파괴자는 객체에 대해 명시적으로 delete를 사용할 때 호출됨


포인터와 객체에 대한 요약

객체를 지시하는 포인터는 일반적인 포인터 표기 형식을 사용하여 선언
기존 객체를 지시하도록 포인터를 초기화할 수 있음
new를 사용하여 새 객체를 생성하면서 포인터를 초기화할 수 있음
new를 클래스와 함께 사용시 새로 생성되는 객체를 초기화하기 위해 적절한 클래스 생성자가 호출됨
포인터를 사용하여 클래스 메서드에 접근시 -> 연산자를 사용
객체를 얻기 위해서는 객체를 지시하는 포인터에 내용 참조 연산자 *를 적용


위치 지정 new 다시 살펴보기

// placenew1.cpp

#include <iostream>
#include <string>
#include <new>
using namespace std;
const int BUF =512;

class JustTesting
{
	private:
		string words;
		int number;
	public:
		JustTesting(const string & s = "Just Testing", int n = 0)
		{ words = s; number = n; cout << words << "생성\n"; };
		~JustTesting() { cout << words << "파괴\n"; };
		void Show() const { cout << words << ", " << number << endl; };
};

int main()
{
	char * buffer = new char[BUF];

	JustTesting *pc1, *pc2;

	pc1 = new (buffer) JustTesting;
	pc2 = new JustTesting("Heap1", 20);

	cout << "메모리 블록 주소 : \n" << "buffer : "
		 << (void *) buffer << "	heap : " << pc2 << endl;
	cout << "메모리 내용 : \n";
	cout << pc1 << " : ";
	pc1->Show();
	cout << pc2 << " : ";
	pc2->Show();

	JustTesting *pc3, *pc4;
	pc3 = new (buffer) JustTesting("Bad Idea", 6);
	pc4 = new JustTesting("Heap2", 10);

	cout << "메모리 내용 : \n";
	cout << pc3 << " : ";
	pc3->Show();
	cout << pc4 << " : ";
	pc4->Show();

	delete pc2;
	delete pc4;
	delete [] buffer;
	cout << "종료\n";
	return 0;
}

pc3 객체 생성시 pc1 객체에 사용된 것과 동일한 위치를 새로운 객체로 덮어썼기 때문에 pc1 객체의 파괴자가 호출되지 못함
bufferdelete []으로 해제해버렸기 때문에 pc3 객체의 파괴자 역시 호출되지 못함

// placenew2.cpp

#include <iostream>
#include <string>
#include <new>
using namespace std;
const int BUF =512;

class JustTesting
{
	private:
		string words;
		int number;
	public:
		JustTesting(const string & s = "Just Testing", int n = 0)
		{ words = s; number = n; cout << words << "생성\n"; };
		~JustTesting() { cout << words << "파괴\n"; };
		void Show() const { cout << words << ", " << number << endl; };
};

int main()
{
	char * buffer = new char[BUF];

	JustTesting *pc1, *pc2;

	pc1 = new (buffer) JustTesting;
	pc2 = new JustTesting("Heap1", 20);

	cout << "메모리 블록 주소 : \n" << "buffer : "
		 << (void *) buffer << "	heap : " << pc2 << endl;
	cout << "메모리 내용 : \n";
	cout << pc1 << " : ";
	pc1->Show();
	cout << pc2 << " : ";
	pc2->Show();

	JustTesting *pc3, *pc4;
	pc3 = new (buffer + sizeof(JustTesting)) JustTesting("Bad Idea", 6);
	pc4 = new JustTesting("Heap2", 10);

	cout << "메모리 내용 : \n";
	cout << pc3 << " : ";
	pc3->Show();
	cout << pc4 << " : ";
	pc4->Show();

	delete pc2;
	delete pc4;
	pc3->~JustTesting();
	pc1->~JustTesting();
	delete [] buffer;
	cout << "종료\n";
	return 0;
}

따라서 위치지정 new를 사용할시 위치가 중복되지 않도록 해당 버퍼 내의 서로 다른 두 주소를 제공해야함
또한 위치지정 new를 사용하여 생성된 객체들은 직접 명시적으로 파괴자를 호출해주어야 하며, 이 때 객체들의 생성 순서의 역순으로 파괴되어야 함



12.6 테크닉의 복습

« 연산자의 오버로딩

ostream & operator<<(ostream & os, const c_name & obj)
{
	os << ...;
	return os;
}

cout과 함께 사용하여 객체의 내용을 출력할 수 있도록 << 연산자를 오버로딩하기 위해서는 위와 같은 프렌드 연산자 함수를 정의해야함


변환 함수들

어떤 하나의 값을 클래스형으로 변환하기 위해서는 c_name(type_name value); 형식의 원형을 가진 클래스 생성자를 작성해야함

클래스형을 다른 데이터형으로 변환하기 위해서는 operator type_name(); 형식의 원형을 가진 클래스 멤버 함수를 작성해야함


생성자가 new를 사용하는 클래스

new에 의해 대입된 메모리를 지시하는 클래스 멤버는 파괴자에서 delete 연산자를 반드시 적용해야함
파괴자가 클래스 멤버인 포인터에 delete를 적용하여 해제한다면 해당 클래스의 모든 생성자들은 new를 사용하거나 null 포인터로 설정하여 포인터를 초기화해야함
생성자가 new를 사용한다면 파괴자는 delete를, 생성자가 new []를 사용한다면 파괴자도 delete []를 사용해야함

기존의 메모리를 지시하는 포인터를 복사하는 대신 새로운 메모리를 대입하는 복사 생성자를 정의해야함

  • className(const className &); 형태의 원형을 가짐
  • 복사 생성자를 사용하여 하나의 객체를 다른 객체로 초기화할 수 있음

대입 연산자를 오버로딩하는 클래스 멤버 함수를 정의해야함

  • c_name & c_name::operator=(const c_name & cn); 형태의 원형을 가짐


12.7 큐 시뮬레이션

큐(queue) : 항목들을 순서대로 보관하는 추상화 데이터형(ADT)

  • 새로운 항목은 큐의 꼬리 부분에 추가되며, 삭제는 큐의 머리 부분에서만 가능함
  • 스택은 추가와 삭제가 동일한 한쪽 끝에서만 이루어지지만, 큐는 추가와 삭제의 방향이 다름
  • 스택은 LIFO(후입선출), 큐는 FIFO(선입선출) 구조를 가짐

Queue 클래스에 필요한 속성

  • 항목들을 도착한 순서대로 보관
  • 보관할 수 있는 항목 수에는 한계가 있음
  • 비어있는 큐를 생성할 수 있어야함
  • 큐가 비어있는지 검사할 수 있어야함
  • 큐가 가득 차있는지 검사할 수 있어야함
  • 큐의 꼬리 부분에 항목을 추가할 수 있어야함
  • 큐의 머리 부분에서 항목을 삭제할 수 있어야함
  • 큐의 항목 수를 알 수 있어야함

큐의 구조에는 링크드 리스트(linked list)가 어울림

  • 노드(node)들이 연결되어 구성되는 형태
  • 각 노드는 구조체로 나타낼 수 있으며, 각 노드가 다음 노드를 지시하는 하나의 링크만을 가지고 있을 경우 단순 링크드 리스트(singly linked list)라고 부름

클래스 안에 구조체(혹은 클래스 선언)를 내포시킬 시, 구조체는 해당 클래스의 사용 범위를 가짐
즉, 구조체형으로 클래스 멤버들을 선언하면 해당 구조체형은 그 클래스 안에서만 사용하도록 제한되어 다른 클래스에서 선언된 구조체형이나 전역적으로 선언된 구조체형과의 충돌이 발생하지 않음

const 데이터 멤버는 객체가 생성될 때 초기화해야함

  • 이는 멤버 초기자 리스트(member initializer list)라는 특수한 문법으로 처리할 수 있음
  • 멤버 초기자 리스트는 매개변수 리스트의 닫는 소괄호 뒤, 함수 몸체의 여는 중괄호 앞에 놓임
  • 즉, Queue::Queue(int qs) : qsize(qs)의 형태로 사용됨
  • 멤버 초기자 리스트 문법은 생성자만이 사용할 수 있으며, const 클래스 멤버와 참조로 선언된 클래스 멤버들에 대해 이 문법을 사용해야함
  • C++11에서는 In-Class 초기화도 가능하며, 이는 생성자 내의 멤버 초기화 리스트를 사용하는 것과 동일함

큐에 객체를 추가할 때 새로운 노드를 생성하기 위해 new를 사용하므로 명시적 파괴자를 필요로 함

큐를 복제하거나 복사하려면 깊은 복사를 수행하는 복사 생성자와 대입 연산자를 제공해야함
필요한 메소드들을 명목상의 private 메소드로 정의하여 디폴트 메소드 정의들을 무시하고, 외부에서 함부로 사용되는 것을 막을 수 있음


클래스 설정

// queue.h

#ifndef QUEUE_H_
#define QUEUE_H_

class Customer
{
	private:
		long arrive;
		int processtime;
	public:
		Customer() { arrive = processtime = 0; };
		void set(long when);
		long when() const { return arrive; };
		int ptime() const { return processtime; };
};

typedef Customer Item;

class Queue
{
	private:
		struct Node { Item item; struct Node * next; };
		enum { Q_SIZE = 10 };
		Node * front;
		Node * rear;
		int items;
		const int qsize;
		Queue(const Queue & q) : qsize(0) {};
		Queue & operator=(const Queue & q) { return *this; };
	public:
		Queue(int qs = Q_SIZE);
		~Queue();
		bool isempty() const;
		bool isfull() const;
		int queuecount() const;
		bool enqueue(const Item & item);
		bool dequeue(Item & item);
};

#endif
// queue.cpp

#include "queue.h"
#include <cstdlib>

Queue::Queue(int qs) : qsize(qs)
{
	front = rear = NULL;
	items = 0;
}

Queue::~Queue()
{
	Node * temp;
	while (front != NULL)
	{
		temp = front;
		front = front->next;
		delete temp;
	}
}

bool Queue::isempty() const
{
	return items == 0;
}

bool Queue::isfull() const
{
	return items == qsize;
}

int Queue::queuecount() const
{
	return items;
}

bool Queue::enqueue(const Item & item)
{
	if (isfull())
		return false;
	Node * add = new Node;
	add->item = item;
	add->next = NULL;
	items++;
	if (front == NULL)
		front = add;
	else
		rear->next = add;
	rear = add;
	return true;
}

bool Queue::dequeue(Item & item)
{
	if (front == NULL)
		return false;
	item = front->item;
	items--;
	Node * temp = front;
	front = front->next;
	delete temp;
	if (items == 0)
		rear = NULL;
	return true;
}

void Customer::set(long when)
{
	processtime = std::rand() % 3 + 1;
	arrive = when;
}

ATM 시뮬레이션

// bank.cpp

#include <iostream>
#include <cstdlib>
#include <ctime>
#include "queue.h"
const int MIN_PER_HR = 60;

bool newcustomer(double x);

int main()
{
	using std::cin;
	using std::cout;
	using std::endl;
	using std::ios_base;
	std::srand(std::time(0));

	cout << "사례 연구 : 히서 은행의 ATM\n";
	cout << "큐의 최대 길이를 입력하십시오 : ";
	int qs;
	cin >> qs;
	Queue line(qs);

	cout << "시뮬레이션 시간 수를 입력하십시오 : ";
	int hours;
	cin >> hours;
	long cyclelimit = MIN_PER_HR * hours;

	cout << "시간당 평균 고객 수를 입력하십시오 : ";
	double perhour;
	cin >> perhour;
	double min_per_cust;
	min_per_cust = MIN_PER_HR / perhour;

	Item temp;
	long turnaways = 0;
	long customers = 0;
	long served = 0;
	long sum_line = 0;
	int wait_time = 0;
	long line_wait = 0;

	for (int cycle = 0; cycle < cyclelimit; cycle++)
	{
		if (newcustomer(min_per_cust))
		{
			if (line.isfull())
				turnaways++;
			else
			{
				customers++;
				temp.set(cycle);
				line.enqueue(temp);
			}
		}
		if (wait_time <= 0 && !line.isempty())
		{
			line.dequeue(temp);
			wait_time = temp.ptime();
			line_wait += cycle - temp.when();
			served++;
		}
		if (wait_time > 0)
			wait_time--;
		sum_line += line.queuecount();
	}

	if (customers > 0)
	{
		cout << " 큐에 줄을 선 고객 수 : " << customers << endl;
		cout << "거래를 처리한 고객 수 : " << served << endl;
		cout << " 발길을 돌린 고객 수 : " << turnaways << endl;
		cout << "	평균 큐의 길이 : ";
		cout.precision(2);
		cout.setf(ios_base::fixed, ios_base::floatfield);
		cout.setf(ios_base::showpoint);
		cout << (double) sum_line / cyclelimit << endl;
		cout << "	평균 대기 시간 : "
			 << (double) line_wait / served << "분\n";
	}
	else
		cout << "고객이 한 명도 없습니다!\n";
	cout << "완료!\n";
	return 0;
}

bool newcustomer(double x)
{
	return (std::rand() * x / RAND_MAX < 1);
}


연습문제

  1. class String
    {
    private:
        char * str;
        int len;
    ...
    };
    

    a) String::String() {} 생성자는 str을 초기화하지 않은 채로 남겨두었다는 문제가 있음
    b)

    String::String(const char * s)
    {
    str = s;
    len = strlen(s);
    }
    

    위 생성자는 깊은 복사를 사용하지 않고 문자열 포인터를 그대로 대입하였기 때문에 메모리 중복 해제의 경우가 발생하여 문제가 생길 수 있음
    c)

    String::String(const char * s)
    {
    strcpy(str, s);
    len = strlen(s);
    }
    

    위 생성자는 str의 메모리가 할당되지 않은 상태로 문자열을 복사함

    1. 객체의 수명이 끝나 제거되어도 new로 메모리가 할당된 포인터 멤버는 자동으로 메모리가 해제되지 않기 때문에, 파괴자에서 delete를 이용하여 메모리를 해제해주어야함
    2. 객체를 다른 객체로 초기화할시 깊은 복사를 사용한 복사 생성자를 정의하지 않으면 메모리 중복 해제 문제가 발생할 수 있음
    3. 객체를 다른 객체에 대입할 시에는 포인터가 아닌 데이터 자체가 복사되도록 깊은 복사를 사용한 대입 연산자를 오버로딩해야함
    • 생성자를 전혀 정의하지 않았을 경우의 디폴트 생성자
      아무 일도 하지 않으나 배열 및 초기화되지 않은 객체를 선언할 수 있음
    • 디폴트 파괴자를 정의하지 않았을 경우의 디폴트 파괴자
      아무 일도 하지 않음
    • 복사 생성자를 정의하지 않았을 경우의 복사 생성자, 대입 연산자를 정의하지 않았을 경우의 대입 연산자
      맴버별 대입을 사용
    • 주소 연산자를 정의하지 않았을 경우의 주소 연산자
      호출한 객체의 주소(this 포인터의 값)을 리턴
  2.  #include <iostream>
     #include <cstring>
    
     class nifty
     {
         private: // 생략 가능
             char * personality;
             int talents;
         public: // 생략 불가능
             nifty();
             nifty(const char * s);
             nifty(const nifty & n);
             ~nifty() { delete [] personality; };
             nifty & operator=(const nifty & n) const;
             friend ostream & operator<<(ostream & os, const nifty & n);
     };
    
     nifty:nifty()
     {
         personality = NULL;
         talents = 0;
     }
    
     nifty:nifty(const char * s)
     {
         personality = new char [strlen(s) + 1];
         strcpy(personality, s);
         talents = 0;
     }
    
     ostream & nifty:operator<<(ostream & os, const nifty & n)
     {
         os << n.personality << std::endl;
         os << n.talents << std::endl;
         return os;
     }
    
  3.  class Golfer
     {
         private:
             char * fullname;
             int games;
             int * scores;
         public:
             Golfer();
             Golfer(const char * name, int g = 0);
             Golfer(const Golfer & g);
             ~Golfer();
     };
    

    a)

    • Golfer nancy; : Golfer(); 호출
    • Golfer lulu("Little Lulu"); : Golfer(const char * name, int g = 0); 호출
    • Golfer roy("Roy Hobbs", 12); : Golfer(const char * name, int g = 0); 호출
    • Golfer * par = new Golfer; : Golfer(); 호출
    • Golfer next = lulu; : Golfer(const Golfer & g); 호출
    • Golfer hazzard = "Weed Thwacker"; : Golfer(const char * name, int g = 0); 호출
    • *par = nancy; : 디폴트 대입 연산자 호출
    • nancy = "Nancy Putter"; : Golfer(const char * name, int g = 0); 및 디폴트 대입 연산자 호출

    b) 깊은 복사를 수행하는 대입 연산자를 추가로 정의해야함

Tags:

Categories:

Updated: