본문으로 바로가기
반응형

생성자

생성자는 객체를 초기화시키는 기능을 갖고 있다.

클래스의 인스턴스를 생성하면 객체 크기(멤버 변수의 크기)만큼 메모리가 할당된다.

할당만 될 뿐 아니라 일반 변수와 마찬가지로 초기화되지 않은 쓰레기값을 가진다.

이대로는 객체를 쓸 수 없으므로 선언 직후에 각 멤버에 원하는 값을 일일이 대입해야한다.

Human kim;
strcpy(kim.name, "홍길동");
kim.age = 29;

이는 가장 쉬운 초기화 방법이지만 멤버가 많으면 일일이 대입하기 까다롭다.

어자피 초기화해야한다면 선언과 동시에 하는것이 간편할 것이다. 클래스는 구조체의 확장이므로 { } 괄호안에 초기값을 순서대로 나열하면 된다.

Human hong = {"홍길동", 30};

간단하지만 이 방법은 클래스에 어울리지 않는다. 외부에서 초기값을 지정하려면 모든 멤버가 공개되어야하는데 이는 정보 은폐가 기본인 클래스와 맞지 않는다.

또, 모든 멤버가 대입만으로 초기화되는 것은 아니며 계산이 필요하거나 메모리를 할당하는 능동적인 동작이 필요한 경우도 있다.

 

그래서 클래스는 초기식을 쓰지않고 객체를 초기화하는 생성자(Constructor)라는 특별한 함수를 사용한다.

생성자는 클래스 스스로 초기화 방법을 캡슐화하여 부품으로서의 완성도를 높이고 기본 타입과 동등해지는 장치이다.

생성자는 컴파일러가 자동으로 호출하기때문에 클래스와 이름이 같고 초기화만 담당하기때문에 return 값은 없다.

예제로 생성자를 정의해보자.

// 파일이름 : Constructor.cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>

class Human
{
private:
	char name[12];
	int age;

public:
	// 생성자
	Human(const char* aname, int aage)  // 클래스의 이름과같고, 리턴값이없으면 생성자이다.
	{ 
		strcpy(name, aname);
		age = aage;
	}

	void intro() 
	{
		printf("이름 : %s\n", name);
		printf("나이 : %d\n", age);
	}
};

int main() 
{
	Human hong("홍길동", 30);
	hong.intro();
}

생성자는 클래스의 이름과 이름이 같고, 리턴값이 없는 것이 특징이다.

위 예제코드에서 클래스 이름과 똑같은 Human 함수를 정의하고 두 개의 인수를 받아 대응되는 멤버에 대입한다.

클래스에 초기화 코드가 포함되어 있기때문에 main 코드가 단순해진다.

명시적인 방법 : Human hong = Human("홍길동", 30);
암시적인 방법 : Human hong("홍길동", 30);

함수를 호출하는 것처럼 생성자 이름과 인수 목록을 쓰는 것이 원칙이나 타입명과 같은 생성자 이름을 또 적는것이

번거롭기때문에 변수명 뒤의 괄호에 인수를 전달하는 간편한 방식을 주로 사용한다.

변수 선언문 뒤의 괄호에 생성자로 전달할 인수를 나열하면 생성자가 이 값을 받아 멤버를 초기화한다.

 

 

생성자의 인수

생성자도 함수이므로 인수를 받는다. 

이때 인수는 주로 멤버의 초기값인데 수가 많으면 대응 관계를 찾기 어려워 멤버와 유사한 이름을 붙일 수 있다.

하지만 똑같은 이름은 쓸 수 없는데, 위의 코드의 생성자를 예시로 들어보자.

// 생성자
Human(const char* name, int age)
{ 
	strcpy(name, name);
	age = age;
}

같은 이름을 가진 매개변수를 클래스의 멤버변수에 대입하는 생성자인데, 대응관계를 찾을수는 있어도 이름이 충돌하여 제대로 동작하지 않는다.

(age = age; 대입문에서 두 변수의 명칭이 같아 원하는대로 동작하지 않는 것이다.)

 

명칭이 충돌할때는 좁은 범위가 우선이라 이 대입문의 age는 인수를 의미하며 자기 자신에게 똑같은 값을 대입하는 의미없는 문장이된다. 에러는 발생하지않지만 초기화는 제대로 수행되지 않을것이다.

따라서 생성자를 초기화할때는 멤버와 인수를 구분할 수 있어야하며 이 문제를 해결할 수 있는 해결책이 있다.

 

1. 인수의 이름에 접두를 붙여 멤버이름을 구분한다.

예제의 경우 'a'(argument의 약자)를붙여 aname, aage식으로 이름을 붙였다. 접두 뒤쪽은 멤버의 이름과같기 때문에 대응관계를 파악할 수 있다.

 

2. 멤버의 이름에 접두를 붙여 일반 변수와 구분한다.

알파벳 m을 붙이거나 m_을 붙여 m_name, m_age 식으로 접두를 붙이면 좀 더 직관적으로 대응관계를 파악할 수 있다.

 

3. 같은 이름을 쓰되 this 키워드를 사용하여 멤버임을 명시한다.

this 키워드는 객체 자신을 의미하는 포인터 상수이며, this->age는 멤버를 의미하며 인수와 분명하게 구분할 수 있다.

Human(const char* name, int age)
{ 
	strcpy(this->name, name);
	this->age = age;
}

 

4. 클래스 이름과 멤버 연산자를 사용하여 구분할 수도 있다. 클래스명 :: 멤버 식으로 칭하면 인수가 아닌 멤버를 의미한다.

Human(const char* name, int age)
{ 
	strcpy(Human::name, name);
	Human::age = age;
}

이 방법은 잘 사용되지 않으며, 위의 세 가지 방법 중 하나를 주로 사용한다. 

 

어떤 방법을 쓰든 생성자의 인수와 멤버의 이름이 구분되기만 하는 것을 기억하면 된다.

모든 방법이 장단점이있어 기호에 따라 선택하되 한 가지 방법을 일관되게 사용하는 것이 좋다.

앞으로는 a 접두를 붙이는 방법을 사용하여 예제를 작성해보도록 하겠다.

 

 

생성자 오버로딩

생성자 오버로딩을 사용하면 디폴트 생성자는 사라진다.

// 파일이름 : ConstructorOverloading.cpp
#include <stdio.h>

class Time 
{
private:
	int hour, min, sec;

public:
	// 생성자1
	Time(int h, int m, int s) 
	{
		hour = h;
		min = m;
		sec = s;
	}

	// 생성자2
	Time(int abssec) 
	{
		hour = abssec / 3600;
		min = (abssec / 60) % 60;
		sec = abssec % 60;
	}

	void OutTime() 
	{
		printf("현재 시간은 %d:%d:%d 입니다.\n", hour, min, sec);
	}
};

int main() 
{
	Time now(12, 30, 40);
	now.OutTime();
	Time noon(44000);
	noon.OutTime();
}

 

앞서 하던 예제에서 생성자를 1개 더 추가한 코드이다. 예제의 Time 클래스는 2개의 생성자를 정의한다.

인수 목록이 다르면 개수에 상관없이 얼마든지 많은 생성자를 정의할 수 있다. 일반 함수와 마찬가지로 어떤 생성자를 호출할 것인가는 객체 선언문의 인수에의해 결정된다.

 

now는 세개의 정수를 주었으므로 생성자1이 호출되고,

noon은 한개의 정수만 주었으니 생성자2가 호출된다.

생성자2는 경과 초를 받아서 초의 값을 나누어 시간과 분을 계산하여 초기화하는 생성자이다.

(초를 3600으로 나누면 시간이고, 60으로 나누면 분이되 60단위의 몫은 시간이므로 버린다)

 

매개변수가 없는 생성자를 디폴트 생성자라고하는데, 생성자 오버로딩을하면 디폴트 생성자를 없애게 된다.

따라서 실인수를 알맞게 넣어서 제대로 초기화시켜줘야 프로그램에 오류가 뜨지 않을것이다.

 

따라서 main에서 선언한 실인수에따라서 그에 맞는 생성자를 호출하는것이 생성자 오버로딩이라고 할 수 있다.

 

 

소멸자(파괴자)

프로그램의 동작은 항상 생성되기 전의 상태로 환경을 되돌려야 시스템이 항상성을 유지할 수 있다.

객체도 마찬가지로 자신이 생성되기 전의 상태로 정리해야하는데, 이런 뒷처리를 담당하는 함수가 소멸자이다.

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>

class Human
{
private:
	char* pname;
	int age;

public:
	Human(const char* aname, int aage) 
	{
		pname = new char[strlen(aname) + 1];
		strcpy(pname, aname);
		age = aage;
		printf("%s 객체의 생성자가 호출되었습니다.\n", pname);
	}

	~Human()
	{
		printf("%s 객체가 파괴되었습니다.\n", pname);
	}

	void intro() 
	{
		printf("이름 : %s\n", pname);
		printf("나이 : %d\n", age);
	}
};

int main() 
{
	Human boy("김수만후거북이와두루미", 12);
	boy.intro();
}

소멸자의 이름은 '~클래스명' 으로 정해져 있으며 Human 클래스의 소멸자는 ~Human이 된다.

객체가 소멸될 때 소멸자가 자동으로 호출되는데 main의 지역 변수인 boy 객체는 main이 리턴할 때 소멸하며 이때 소멸자가 호출되는 것이다.

 

생성자에서 new[] 연산자로 메모리를 할당했으니 소멸자는 반대로 delete[] 연산자로 메모리를 회수한다.

생성자에서 단순히 멤버값만 대입했다면 소멸자는 굳이 필요하지않지만, 메모리를 할당했다면 객체가 사용하던 메모리를 소멸자가 해체해주어야한다.

 

 

생성자와 소멸자의 특징

클래스는 기본형에 비해 훨씬 거대하고 복잡한 정보를 다루기 때문에 초기화 방법이 특수하다. 그래서 초기화를 전담하는 생성자가 필요하고 생성자가 벌려 놓은 일을 정리해줘야하는 소멸자도 필요하다.

이 두 함수는 임무가 정해져있고 자동으로 호출된다는 면에서 일반 함수와는 독특한 특징이 있다.

 

- 컴파일러에 의해 자동으로 호출되므로 이름이 정해져있고 임의의 이름을 붙일 수 없다.

생성자는 클래스명과 같고 소멸자는 이름앞에 ~를 붙인다. 이름이 고정되어있어야 컴파일러가 필요할때 이 함수를 찾아 호출할 수 있다.

 

- 생성과 소멸은 동작일뿐 조사나 계산이 아니기 때문에 리턴값이 없다.

리턴을 하더라도 컴파일러가 자동으로 호출하는 것이어서 리턴받을 주체도 없다. 일반 함수에 비한다면 void형 함수와 유사하되 리턴의 개념이 없기때문에 void 라는것조차 밝힐 필요 없이 리턴 타입을 생략한다.

 

- 외부에서 자동으로 호출되므로 public 액세스 속성을 가져야한다.

생성자를 숨겨버리면 외부에서 객체를 생성할 수 없다.

 

- 생성자는 인수를 가진다.

따라서 오버로딩하여 여러개의 초기화 방법을 제공할 수 있다. 반면 소멸자는 인수를 받지않으며 오버로딩할 수 없다.

 

- 둘 다 디폴트가 있어 개발자가 정의하지 않으면 컴파일러가 아무것도 하지 않는 생성자와 소멸자를 알아서 만든다.

 

자세한건 뒤에서 더 알아보도록하고 지금은 클래스가 생성되면 생성자가 호출되고, 프로그램이 종료될때 소멸자가 호출된다는 사실만 기억하자.

 

 

객체의 동적생성

정수형 변수가 필요하면 언제든지 int i; 구문으로 선언해 사용한다. 이것을 정적 할당이라고 하며 스택에 변수가 생성된다. 실행 중에 변수가 필요하다면 힙에 동적 할당을 하는데 다음은 정수형 변수를 할당하는 예시이다.

int *pi;
pi = new int(1234);
*pi 사용;
delete pi;

new 연산자로 할당하고 초기값을 준 후 그 포인터를 pi로 대입받아 사용한 후에 delete 연산자로 해제한다.

클래스는 일종의 타입이므로 정수형 변수와 마찬가지로 정적할당과 동적할당이 모두 가능하다.

(new는 메모리를 할당받을때 생성자를 호출하기때문에 객체지향에서 초기화가 된다 -> 생성자가 호출된다 라는말과 같다)

// 파일이름 : new연산자와 Human.cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>

class Human
{
private:
	char* pname;
	int age;

public:
	Human(const char* aname, int aage) 
	{
		pname = new char[strlen(aname) + 1];
		strcpy(pname, aname);
		age = aage;
		printf("== <%s> 객체 생성 == \n", pname);
	}

	~Human() 
	{
		printf("== <%s> 객체가 파괴 ==\n", pname);
		delete[] pname;
	}

	void intro() 
	{
		printf("이름 : %s\n", pname);
		printf("나이 : %d\n", age);
	}
};

int main() 
{
	Human boy("김수한무거북이와두루미", 12);
	boy.intro();

	Human* leo;
	leo = new Human("레오나르도 디카프리오", 40);
	leo->intro();
	delete leo;
}

김수한무 생성, 디카프리오 생성, 디카프리오 파괴, 김수한무 파괴 순으로 진행되었다 => 가는데는 순서없다

boy 객체는 정적으로 할당했으며 스택에 생성된다.

leo 객체는 동적으로 할당했는데 이때 new 연산자를 사용한다.

new 연산자 다음에 할당할 타입과 생성자의 인수를 나열하면 힙에 메모리를 할당하고 생성자를 호출하여 객체를 초기화한다. new 연산자가 리턴하는 포인터를 Human 타입의 포인터 leo로 받으면 이후부터 leo 포인터를 통해 객체를 사용할 수 있다.

 

실행 중에 생성한 객체는 포인터 타입이어서 멤버 함수는 leo.intro()가 아니라 leo0->intro()로 호출한다는 것에 유의하자.

정적 할당한 객체는 범위를 벗어날때 자동으로 소멸하지만,

동적 할당한 객체는 사용한 후에 delete 연산자로 해제해야한다. 이때 객체의 소멸자가 호출되어 할당한 메모리를 해제해준다.

 

객체를 동적으로 생성할때는 new 연산자를 사용한다. new 연산자는 객체를 저장할 메모리를 할당할 뿐만아니라 생성자를 호출하여 객체를 초기화한다.

 

마찬가지로 delete 연산자는 메모리를 회수하기전에 소멸자를 호출하여 정리할 기회를 준다.

위의 예제에서 정적으로 선언한 boy나 동적으로 할당한 leo 둘 다 출력문을 넣어서 확인해보았다.

 

C에서는 메모리 할당과 해제를 위해 malloc/free 함수를 사용하지만 ,

C++에서는 new/delete 연산자를 사용한다.

 

두 방법의 가장 큰 차이는 생성자와 소멸자를 호출하는지, 아닌지 이다.

객체는 메모리만 할당한다고해서 초기화되지 않으므로 반드시 new/delete 연산자를 사용해야 한다.

객체 내부에서 사용하는 pname 버퍼는 단순 메모리이므로 malloc/free 함수를 사용해도 상관없을것이다.

그러나 두 방법을 섞어서 사용하면 일관성이 떨어지므로 가급적이면 new/delete 연산자만 사용하는 것이 좋을것이다.

반응형