Developer

8.(C++) 클래스(2)_생성자,파괴자(소멸자) 본문

Programming Language/C++

8.(C++) 클래스(2)_생성자,파괴자(소멸자)

DPhater 2020. 8. 1. 22:49

생성자(Constructor)는 객체의 생성 즉, 클래스 변수를 선언할 때 선언과 동시에 초기화를 가능하게 해주는 함수이다.

그럼 생성자는 왜 필요할까? 이전에 클래스도 사용자가 직접만든 자료형과 같다고 했다. 기본 자료형의 변수와 마찬가지로 객체를 생성하면 메모리가 할당되고, 쓰레기값을 가지게된다. 따라서 객체 생성이후에 각 멤버에 값을 직접 넣어주어야한다. 하지만 각 멤버를 직접 일일히 작성해 주는것은 매우 귀찮은 작업이고, 혹시 중간에 깜빡해서 값을 할당하지 않은 멤버가 있을 수 도 있다.

이러한 작업을 좀 더 간결하고 혹시 입력되지 않은 값에 대한 default 값까지 설정할 수 있게 해주는게 생성자이다.

생성자

생성자는 class와 같은 이름의 멤버 함수이다. 코드를 통해 사용법을 알아보자.

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

class student{
private:
	string name;
	int id;
	int age;
	string phonenumber;
	
public:
		
	student(string aname,int aid,int aage,string aphonenumber){
		name=aname;
		id=aid;
		age=aage;
		phonenumber=aphonenumber;
	}
	void print(){
		cout<<"이름: "<<name<<" 학번: "<<id<<" 나이: "<<age<<" 전화번호: "<<phonenumber<<endl;
	}
};
int main(){
    //student x();      필요한 인수를 제공하지 않아서 error
	student a("김모군",123456,20,"010xxxxxxxx");
	student b {"이모군",234567,21,"010yyyyyyyy"};
	student c = {"박모군",345678,22,"010zzzzzzzz"};
	
	a.print();
	b.print();
	c.print(); 
	
	return 0;
}

코드1 실행 결과

student 클래스를 보면 멤버 함수로 자신의 이름과 같은 함수가 있는것을 알 수 있다. 이것이 바로 생성자이다. 필요한 인수를 받아 각각의 멤버들을 초기화 해주고있다. 그리고 main함수를 보면 여러가지 방법의 생성자 호출 방법이 작성되어 있다. 현재는 모두 같은 동작을 수행하지만 a와 같이 작성할 경우 원하지 않는 결과가 나올 수 있다. {}를 사용하는것이 표준적인 방법이다.

#include<iostream>

using namespace std;

class test{
	public:
		test(){
			cout<<"constructor"<<endl;
		}
};

int main(){
	test a();
}

코드2 실행 결과

객체를 소괄호를 사용해 생성했을 때 원하지 않는 결과는 위의 코드를 통해 알아볼 수 있다. a객체를 만들 때 인수가 필요하지 않은 생성자이므로 빈 소괄호를 사용해 선언해 주었다. 우리의 기대대로 라면"constructor"가 화면에 출력되어야 한다. 하지만 아무것도 출력되지 않고 프로그램이 종료되어 버린다.

하지만 {}를 사용해 객체를 선언하거나 괄호를 아예 작성하지 않으면 정상적으로 "constructor"가 화면에 출력되는것을 확인할 수있다.

이유가 무엇일까? 우리는 ()를 함수를 선언할 때에도 사용한다. 위 코드의 test a()는 리턴형이 test이고, 전달인자가 없는 함수 a를 선언한것으로 컴파일러는 해석한다.

그러면 생각할 것이다. 위와같이 전달인자가 필요없는 생성자는 괄호를 사용하지 않고 사용하면 되지 않느냐고. 물론 그래도 상관은 없다. 하지만 ()를 사용해 객체를 생성할 경우 int형 전달인자가 필요한 생성자에 double형을 전달해도 문제없이 캐스팅되어 전달된 3으로 생성자가 호출된다. 하지만 {}는 이를 금지한다(손실있는 변환 금지). {}를 사용하면 앞의 경우에서 에러가 발생된다.

이 외에도 여러 이유가있지만 이 글에서는 여기까지 작성하고 자세한것은 따로 작성하게다.

생성자 오버로딩(중복 정의)

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

class Date{
private:
	int month;
	int day;
	string week;
	
public:
	Date(){
		month=1;
		day=1;
		week="월";
	}
	Date(int m,int d,string w){
		month=m;
		day=d;
		week=w;
	}
	void print(){
		cout<<month<<"월 "<<day<<"일 "<<week<<"요일 "<<endl;
	}
};


int main(){
	Date a;
	Date b(3,21,"수");
	a.print();
	b.print();
	return 0;
}

코드3 실행 결과

코드3과 같이 생성자를 중복생성할 수 있다. 인수가 필요없는 생성자를 default 생성자라고 한다. 코드3에서는 default생성자가 임의의 고정된 값으로 초기화를 해준다. 하지만 아무런 생성자를 작성하지 않으면 컴파일러는 자동으로 비어있는 default 생성자를 정의해 준다. 그렇기 때문에 생성자를 선언하지 않았어도 student a;와 같이 객체 선언이 가능했던 것이다. 하지만 직접 생성자를 하나라도 작성하였다면 컴파일러는 default 생성자를 정의해주지 않기때문에 직접 작성해주어야 한다. (객체를 선언해 나중에 값을 할당하고 싶거나, 배열을 만들고 싶다면 반드시 default 생성자가 있어야한다.)

디폴트 인수

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

class Date{
private:
	int month;
	int day;
	string week;
	
public:
	Date(int m=1,int d=1,string w="월"){
		month=m;
		day=d;
		week=w;
	}
	void print(){
		cout<<month<<"월 "<<day<<"일 "<<week<<"요일 "<<endl;
	}
};


int main(){
	Date a;
	Date b(3,21,"수");
	a.print();
	b.print();
	return 0;
}

코드4의 실행결과는 3과 똑같다. 생성자의 매개변수는 default 값을 가질 수 있다. 만약 입력되지 않는다면 해당 default값을 가지게된다. 또한 뒤에서부터 차례대로 생략할 수 도 있다. 예를들어 Date b(3,21)으로 객체를 선언하면 요일은 default값인 월요일을 가진다.

복사 생성자&파괴자(소멸자)

복사 생성자는 다음과 같은 상황에 사용된다.

time a={3,2,"수"};
time b=a;

위의 경우 컴파일러가 각 멤버를 1:1로 대입(= 연산자)하는 복사 생성자를 만들어주기 때문에 따로 작성할 필요가 없다. 하지만 포인터를 멤버변수를 가지는 클래스라면 =연산자를 사용해 1:1로 대입하면 같은 메모리를 공유하게된다.

다음 코드를 통해 소멸자 및 복사 생성자에 대해 알아보자.

#include<iostream>
#include<string.h>          
using namespace std;

class People{
private:
	char *name;
	int age;
public:
	People(const char *aname,int a){
		name=new char[strlen(aname)+1];
		strcpy(name,aname);
		age=a;
	}
	~People(){
		delete[] name;
	}
	
	void print(){
		cout<<name<<" "<<age<<endl;
	}
};


int main(){
	People a("김모군",21);
	People b=a;
	b.print();
	return 0;
}

코드5 실행 결과

출력은 정상적으로 되지만 예외가 발생한것을 볼 수있다. 우선 소멸자가 무엇인지 알아보자. ~로 사작하는 클래스와 같은 이름의 함수가 바로 소멸자이다.소멸자는 매개변수를 사용할 수 없고, 객체가 그만 사용될 때 (소멸될 때) 동작하는 함수이다. 이는 메모리 관리에 중요하다. 특히 위의 코드와같이 동적할당한 멤버를 객체가 소멸될 때 해제해 주어야한다.

이제 복사 생성자를 알아보자. 컴파일러가 만들어주는 복사 생성자는 다음과 같이 작성되어있다.

People(const People &other){
    name=other.name;
    age=other.age;
}

포인터에 포인터를 대입 했으므로 같은 주소를 가리키게 되는것이다.

그림1

그림1과 같은 모습이 되는것이다. 그리고 프로그램 종료 전까지 이는 문제가 발생하지 않는다. 프로그램이 종료되는 순간을 생각해보자. 객체 a,b의 소멸자가 호출된다. a의 소멸자가 name이 가리키고있는 주소공간을 해제한다. 이후 b의 소멸자가 name이 가리키고 있는 주소공간을 해제 해야 하는데 이는 a와 같은 주소를 가리키고 있었으므로 해제할 메모리가 없어 문제가 발생하는 것이다.

이 문제를 해결하기 위해서는 깊은 복사를 수행하는 복사 생성자를 선언해 주어야한다. 방법은 간단하다.

	People(const People &other){
		name=new char[strlen(other.name)+1];
		strcpy(name,other.name);
		age=other.a;
	}

위의 코드와 같이 복사 생성자를 작성해주면 된다. 레퍼런스로 전달해야 하는 이유는 People(const People other)과 같이 작성할 경우 People a=b에서 other=b가 수행될 때 또 다시 복사생성자가 호출되고, 그 이후에도 계속 무한히 복사생성자가 호출되기 때문이다.

초기화 리스트

아래의 코드가 초기화 리스트를 사용한 것이다.

    Date(int m,int d,string w):month(m),day(d),week(w){
    }

위의 생성자는 아래의 생성자와 같은 생성자이다.

	Date(int m,int d,string w){
		month=m;
		day=d;
		week=w;
	}

초기화 리스트는 특별한 경우에 사용된다. 초기화 리스트는 객체가 생성되기 이전에 할당과 동시에 값을 대입할 수 있는 특별한 기능을 한다. 예를들어 정수형 상수 변수가 멤버로 있다고 생각해보자. 상수는 선언한 후에 값을 변경, 대입할 수 없다. 하지만 클래스 내에서 멤버를 초기화 할 수도 없다. 왜냐하면 클래스는 그냥 설계도일뿐 실제로 메로리를 가지고 있지도 않기 때문이다. 객체가 생성될 때 값을 넣을 수 있는것이다. 이러한 경우에 초기화 리스트를 사용할 수 있다.

생성자가 호출된다면 이미 객체가 만들어져 있는것이기 때문에 상수에 값을 대입할 수 없다. 따라서 생성자 본체가 실행되기 전(중괄호 전)에 초기화 리스트 라는 영역을 만들어 객체가 생성되기 전에 할당과 동시에 값을 대입할 수 있게 해준다.

'Programming Language > C++' 카테고리의 다른 글

7.(C++) 클래스 (1)  (0) 2020.08.01
6.(C++) 구조체  (0) 2020.08.01
5.(C++) 함수  (0) 2020.08.01
4.(C++) 레퍼런스 (Reference)  (0) 2020.08.01
3.(C++) 제어문  (0) 2020.08.01
Comments