티스토리 뷰

프로그래밍 공부/C++

C++ (2)

빅토리아레몬탄산수 2021. 7. 8. 14:12

7강입니다 constructor 01

생성자와 소멸자에 대해 설명합니다.

sturct 를 클래스로 변경했습니다. class로 변환 시 안에 모든 데이터 멤버들을 public으로 전환해줘야합니다. 이렇게 변환 시 바꾸기 전 sturct와 유사하게 변환됩니다. 

#include <iostream>



class KPeople 
{
public:
	int age; // 데이터 멤버(date member)
	char name[12];

	void PrintPeople() // 멤버 함수 (member function)
	{
		printf("%s : %i\r\n", name, age);
	}
	void SetAge(int age_) 
	{
		age = age_;
	}
	void SetName(const char* name_)
	{
		strcpy_s(name, name_);
	}
};

void main() 
{
	KPeople a;
	a.SetAge(48);
	a.SetName("John");
	a.PrintPeople();
}

 

 

클래스를 설계하다 보면 객체를 만들자 마자 초기화를 해 줄 필요가 있습니다. 위 코드에선 age, name을 초기화 할 필요가 있다고 과정해보겠습니다. 초기화하지 않은 변수를 접근하는걸 위험하기 떄문입니다.

그리고 어떤 이유에서 객체를 파기하기전에 마무리 해줄 필요가 있다면 멤버 함수로 정의 할 수 있습니다.

class KPeople 
{
public:
	int age; // 데이터 멤버(date member)
	char name[12];

	
	void Initialize() 
	{
		age = 0;
		name[0] = 0;
	}

	void Finalize() 
	{
		// 마지막에 해체 전 실행될 멤버 함수
	}

	void PrintPeople() // 멤버 함수 (member function)
	{
		printf("%s : %i\r\n", name, age);
	}
	void SetAge(int age_) 
	{
		age = age_;
	}
	void SetName(const char* name_)
	{
		strcpy_s(name, name_);
	}
};

위와 같이 멤버 함수의 Initialize와 Finalize를 만들어주고 사용하게 되면 아래와 같습니다.

void main() 
{
	KPeople a;
	a.Initialize();

	a.SetAge(48);
	a.SetName("John");
	a.PrintPeople();
	
	a.Finalize();
}

따로 메인에서 초기화를 안할 시 쓰레기값이 들어가 잘못된 결과값이 날 올 수 있기때문에 외에도 여러 상황에서도 사용 가능합니다. 이와 같이 객체를 만들자 마자 초기화하는 함수를 생성자라고합니다. 반드시 자기 자신과 똑같은 이름을 가져야합니다. 객체를 만들자마자 자동으로 호출되기때문에 함수 호출 이름을 명시 할 방법이 없습니다. 함수가 종료되기전에 Finalize도 자동으로 명시 할 방법 또한 없습니다. 그래서 특별한 이름을 가져야합니다.

그래서 C++에서는 객체를 만들자 마자 자동으로 호출되는 초기화 함수를 생성자라 합니다.

클래스의 생성자를 정의하는 방법은 아래와 같습니다.

KPeople() //constructor
{
  age = 0;
  name[0] = 0;
  printf("constructor()\r\n");
}

소멸자 함수는 특별한 문법을 따라줘여합니다. 그리고 소멸자는 객체가 파괴되기 바로 전에 호출되는 함수입니다.

~KPeople() //destructor
{
	printf("constructor()\r\n");
}

이렇게 하는 경우 이제는 자동으로 호출되므로 따로 생성자, 소멸자를 만들고 main에 올릴 필요 없습니다.

void main() 
{
	KPeople a;
	a.SetAge(48);
	a.SetName("John");
	a.PrintPeople();
}

이제 KPeople을 동적할당 해보겠습니다.

	KPeople* p;
	p = (KPeople*)malloc(sizeof(KPeople));
    
	free(p);

malloc이 리턴하는 값은 보이드 포인터이기 때문에 캐스팅을 해줘야합니다. (KPeople*) 그리고 사이즈는 KPeople만큼 필요하므로 KPeople을 넣어줍니다. 그리고 메모리 해제 또한 해줘야합니다. 이렇게 될 시 이제 p를 통해서 접근이 가능하게됩니다.

요기서 빌드 시 쓰레기값이 나옵니다. 생성자에서 초기화를 해줬지만 쓰레기 값이 나오는 이유는 메모리 동적 할당 시 생성자를 자동 호출해주지 않습니다. 그래서 p를 이용해서 생성자를 호출해주는 부분을 직접 작성해줘야합니다. 소멸자도 마찬가지 입니다.

void main() 
{
	KPeople* p;
	p = (KPeople*)malloc(sizeof(KPeople));
	p->KPeople::KPeople();

	p->~KPeople();
	free(p);
}

KPeople만 입력 시 클래스 이름과 같아 모호함이 발생하므로 KPeople에 생성자를 호출한다고 명시해줍니다. 소멸자 경우 ~가 붙어있어 상관없습니다.

하지만 전에도 말했듯이 C++에선 new와 delete를 사용합니다.

	KPeople* p;
	p = (KPeople*)operator new (sizeof(KPeople)); // new 함수 ( new function)
	p->KPeople::KPeople();

	p->~KPeople();
	operator delete(p);

operator new 또한 보이스 포인터이므로 (KPeople*)로 캐스팅 해줍니다. operator new 경우 전역함수라 생각하면됩니다. operator new를 함수 이름이라고 생각하는게 나중에 헷갈리지 않는다고 합니다. 

하지만 이또한 생성자를 따로 호출해줘야합니다. 하지만 C++ 설계자들이 동적으로 할당 하고 자동으로 생성자를 호출 하도록 operator new와 생성자를 호출하도록 new expression을 만들었습니다 .

	p = new KPeople;
    
	delete p;

이것의 의미는 동적으로 KPeople만큼 메모리에 할당하고 자동으로 생성자 자동으로 호출하라는 뜻입니다.

소멸자 경우도 마찬가지 입니다. p의 경우는 KPeople 포인터입니다.

하지만 그럼 operator new는 필요없는거 아니냐 할 수 있지만 new expression은 오버로딩이 불가합니다 그래서 new의 오버로딩이 필요한 경우 operatorn new로 오버로딩을 해야합니다. 하지만 동적으로 메모리를 할당할 일이 있다면 new expression, delete expression을 사용하면 됩니다.

8강입니다.

 생성자의 파라미터가 들어갈 시 () 안에 들어갑니다. 없을 시 생략해도됩니다. 있어도 생략안하고 적어도 작동합니다.

p = new KPeople();

이런 식으로 처리 가능합니다.

p = new KPeople[3];

연속적으로 메모리 할당이 가능합니다. 뜻은 KPeople의 크기의 메모리를 3개를 할당한것입니다. new 에 []를 사용 시 delete[] p 와 같이 딜리트에도 표시해줘야합니다. 물론 3개를 만들기때문에 생성자 3번 소멸자 3번이 작동합니다.

그리고 접근 시엔 아래와 같이 접근 가능합니다. 0 , 1, 2 등 직접적인 접근이기 때문에 포인터가 아닌 객체가 되므로 . 을 사용합니다.

p[0].PrintPeople();
p[1].PrintPeople();
p[2].PrintPeople();

그리고 오버로딩을 생성자를 오버로딩 할 수 있습니다.

class KPeople 
{
public:
	int age; // 데이터 멤버(date member)
	char name[12];

	
	KPeople() //constructor
	{
		age = 0;
		name[0] = 0;
		printf("constructor()\r\n");
	}
	KPeople(const char* name_) // overloading
	{
		age = 0;
		strcpy(name, name_);
		printf("constructor(char*)\r\n");
	}

	~KPeople() //destructor
	{
		printf("destructor()\r\n");
	}

	void PrintPeople() // 멤버 함수 (member function)
	{
		printf("%s : %i\r\n", name, age);
	}
	void SetAge(int age_) 
	{
		age = age_;
	}
	void SetName(const char* name_)
	{
		strcpy_s(name, name_);
	}
};
// 1번째 생성자
KPeople p;

// 2번째 생성자
KPeople p("John");
KPeople p = "John"; // 암시적인 생성자 호출 implicit construton call

 

void Test( KPeople p )
{
	p.PrintPeople();
}

void main()
{
	KPeople p = "John";
	Test("Mike");
}

암시적 생성자 호출을 할 시 이런식으로 사용 가능하지만 이렇게 사용 할 시 문제가 될 수 있습니다. 그래서 생성자 호출을 할 때 명시적인 호출만 받겠다라고 지정할 수 있습니다.

explicit KPeople(const char* name_)
{
  age = 0;
  strcpy(name, name_);
  printf("constructor(char*)\r\n");
}

생성자에 explicit을 앞에 붙여주면 됩니다. 이렇게 될 시 암시적 호출을 할 시 컴파일 에러를 발생 시킵니다. 이렇게 바꾸고 위위 코드에서 컴파일 할 시 오류를 발생하는걸 볼 수 있습니다. 그래서 사용을 하려면 아래와 같이 수정합니다.

void main()
{
	KPeople p ("John"); // implicit construction call
	
    	KPeople temp = KPeople("Mike");
	Test(KPeople("Mike")); //temporary object
}

KPeople temp에 생성자로 KPeople("Mike")를 전달 시키는 겁니다.

Test 함수에 직접 단독으로 쓰인 경우는 KPeople("Mike")를 KPeople타입에 객체 파라미터로 "Mike"를 전달하고 임시 스택에 만들어지는 임시 객체를 생성한다는 뜻입니다. 그래서 이문장은 생성자를 호출합니다. 

근데 이런식으로 코드를 짠 후 컴파일 해서 결과를 보면 생성자 3번 소멸자가 4번이 호출되는 현상이 생깁니다.

9강입니다.

위와 같이 할 시 생성자가 p , temp, KPeople , Test 에서의 KPeople p 에서 생성자가 총 4번 생성 소멸자가 똑같이 4번 생성되어야하지만 생성자가 3번만 생성됩니다. 왜 그런지 메모리를 그려보겠습니다.

정리도 할겸 천천히 메모리를 그려가며 내려가겠습니다. 항상 주소는 임의에 값을 적어논겁니다.

class KPeople 
{
public:
	int age; // 데이터 멤버(date member)
	char name[12];
    
void main() 
{
	KPeople p("John");

메인에 들어왔을때 p가 할당됩니다. p는 KPeople 타입으로 int 4바이트 char[12] 12바이트 총 16바이트를 차지합니다. name의 시작주소는 5004 +4된 값이 됩니다. 5000+15까지 차지하는 16바이트의 크기의 메모리가 됩니다. 

	KPeople() //default constructor
	{
		age = 0;
		name[0] = 0;
		printf("constructor()\r\n");
	}
	explicit KPeople(const char* name_)
	{
		age = 0;
		strcpy(name, name_);
		printf("constructor(char*)\r\n");
	}

"John" 전달합니다. 그렇게되면 2번째 생성자가 호출되어서 name을 복사하고 age를 0으로 만듭니다.

이렇게 들어옵니다. 하지만 age랑 name이라는 값은 이곳에 표기되어있지 않습니다. 혼동을 피하기 위해서 지워줍니다. 그리고 이제 temp 객체를 만듭니다.

KPeople temp = KPeople("John");

Temp를 만들었습니다 . 다음 KPeople("Mike")를 임시로 호출합니다.

Test(KPeople("Mike")); //temporary object

이 객체는 어디로 만들어질지 모릅니다. 실제로 스택에 만들어질 확률이 높습니다. 주소와 이름이 없습니다. 그래서 temporary object 라 부릅니다.

이렇게 생성자가 총 3번 호출됐습니다. 임시로 만들어진 객체를 이제 Test에 전달됩니다.

void Test(KPeople p) 
{
	p.PrintPeople();
}

파라미터로 KPeople p를 메모리에 할당합니다. 요기서 생성자가 3번만 호출된 이유를 알 수 있습니다. Test 함수를 진행하며 KPeople p가 생성된게 아닌 임시객체로 생성된 ?를 그대로 복사한 형태여서 Test 함수에선 생성자를 호출하지 않습니다.

이렇게 Test 함수를 작동하면 출력하고 일을 끝낸 Test 함수를 메모리 해제하며 그전에 소멸자가 호출됩니다.

그리고 이제 main으로 돌아가 스택메모리에서 순서대로 메모리 해제 전에 소멸자가 호출됩니다. 그래서 총 4번 호출이 됩니다.

생성자와 소멸자가 쌍으로 안이루어질때 오류가 발생할 수 있습니다. 그럼 근본적인 문제인 객체가 복사가 되는 경우에 호출되는 특별한 생성자를 정의를 해줘야합니다. 이걸 copy constructor라고 합니다.

copy constructor는 객체가 복사되는 문법을 표현해주기 위해서 자기 자신의 타입에 레퍼런스(&)를 매개변수로 받습니다. 복사되는 시점에 다시 복사를 막기 위해서 입니다. 

KPeople(const KPeople& rhs) // copy constructor
	{
		printf("copy constructor()\r\n");
	}

문법 기억

class KPeople 
{
public:
	int age; // 데이터 멤버(date member)
	char name[12];

	
	KPeople() //default constructor
	{
		age = 0;
		name[0] = 0;
		printf("constructor()\r\n");
	}

	explicit KPeople(const char* name_)
	{
		age = 0;
		strcpy_s(name, name_);
		printf("constructor(char*)\r\n");
	}

	KPeople(const KPeople& rhs) // copy constructor
	{
		age = rhs.age;
		strcpy_s(name, rhs.name);
		printf("copy constructor()\r\n");
	}
	~KPeople() //destructor
	{
		printf("destructor()\r\n");
	}
};

void Test(KPeople p) 
{
	p.PrintPeople();
}

void main() 
{
	KPeople p("John"); // 암시적인 생성자 호출 implicit construton call
	KPeople temp = KPeople("John");
	Test(temp); //temporary object
}

오류 시 strcpy_s를 사용합니다. 요기서 주의 해야할 점은 이미 복사한 값을 호출 할 시 복사 생성자가 호출된다라는 점입니다.

Test(temp)로 변경 이유는 KPeople("Mike")를 작성 시 생성자 한번 복사 생성자 한번이 호출이 되는데 컴파일 최적화로 생성자 한번만 호출되어 복사생성자를 호출하지 않는 문제가 있어서 이미 복사된 값을 사용했습니다. 위 메모리를 그리며 정리한걸 봐도 ?를 Test로 복사한 형태라서 생성 후 복사 호출되는게 맞지만 최적화된것 같습니다. 

객체가 복사되는 시점과 생성되는 시점을 이해하면 좋습니다.

10강입니다. 

객체를 만드는 팩토리 함수를 만듭니다.

KPeople GetPeople()
{
	KPeople t;
	return t;
}
or
KPeople GetPeople()
{
	return KPeople("Mary"); //RVO
}


void main() 
{
	KPeople t;
	t = GetPeople();
	//Test(t); // temporary object
}

 

생성자가 2번 작동합니다. 하지만 복사생성자가 작동하지 않는건 아닙니다. class 내에서 private 처리해버리면 오류가 발생합니다. 이유는 RVO에 의해 스킵될뿐 복사생성자를 검사를 하지 않는게 아니기 때문입니다.

RVO는 컴파일 최적화 형식으로 생성자를 최소화 합니다.

'프로그래밍 공부 > C++' 카테고리의 다른 글

C++ (1)  (0) 2021.07.07
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함