*윤성우의 열혈 C++ 프로그래밍 교재로 학습하며 정리하였습니다.
생성자의 이해
지금까지는 객체를 생성하고 멤버변수 초기화를 목적으로 InitMembers 라는 이름의 함수를 정의하고 호출하였다.
이를 대신하여 '생성자'를 이용하면 객체도 생성과 동시에 초기화 할 수 있다.
생성자는 다음과 같은 특징들을 가진다.
클래스의 이름과 함수의 이름이 동일하다.
반환형이 선언되어 있지 않으며, 실제로 반환하지 않는다.
객체 생성시 딱 한번 호출된다.
#include <iostream>
using namespace std;
class SimpleClass {
private:
int num1;
int num2;
public:
//1번
SimpleClass() {
num1 = 0;
num2 = 0;
}
//2번
SimpleClass(int n) {
num1 = n;
num2 = 0;
}
//3번
SimpleClass(int n1, int n2) {
num1 = n1;
num2 = n2;
}
//4번
//생성자의 오버로딩이 가능하다.
/*
SimpleClass(int n1 = 0, int n2 = 0) {
num1 = n1;
num2 = n2;
}
*/
//매개변수의 디폴트 값 설정이 가능하다.
void ShowData() const {
cout << num1 << ' ' << num2 << endl;
}
};
int main()
{
SimpleClass sc1;
sc1.ShowData();
SimpleClass sc2(100);
sc2.ShowData();
SimpleClass sc3(100, 200);
sc3.ShowData();
return 0;
}
1~3번 생성자를 통해 함수의 오버로딩이 가능함을 알 수 있다.
4번에서는 생성자의 매개변수에 '디폴트 값'을 설정할 수 있다.
2번 생성자는 int형 데이터를 인자로 요구하므로, 객체를 생성하려면 다음과 같이 문장을 구성해야한다.
SimpleClass sc2(100);//전역 및 지역변수의 형태
SimpleClass* ptr2 = new SimpleClass(100);//동적 할당의 형태
3번 생성자를 이용하여 객체 생성
SimpleClass sc3(100,200);
SimpleClass* ptr3 = new SimpleClass(100,200);
1번 생성자를 이용하여 객체 생성
SimpleClass sc1(); //(X)
SimpleClass sc1; //(O)
SimpleClass* ptr1 = new SimpleClass; //(O)
SimpleClass* ptr1 = new SimpleClass(); //(O)
SimpleClass sc1();이 허용되지 않는 이유 :
함수의 원형을 함수 내에 지역적으로 선언했을 경우,
SimpleClass sc1();와 같은 문장이 객체 생성문인지, 함수의 원형 선언인지 구분할 수 없기 때문이다.
'멤버 이니셜라이저(Member Initializer)'를 이용한 멤버 초기화
Rectangle::Rectangle(const int &x1, const int &y1, const int &x2, const int &y2)
:upLeft(x1,y1),lowRight(x2,y2)//멤버 이니셜라이저
//객체 upLeft의 생성과정에서 x1과 y1을 인자로 전달받는 생성자를 호출하라.
//객체 lowRight의 생성과정에서 x2와 y2를 인자로 전달받는 생성자를 호출하라.
{
//empty
}
:upLeft(x1,y1),lowRight(x2,y2) 이 부분이 '멤버 이니셜라이저' 이며, 다음과 같은 의미를 담고있다.
객체 upLeft의 생성과정에서 x1과 y1를 인자로 전달받는 생성자를 호출하라.
객체 lowRight의 생성과정에서 x1과 y1를 인자로 전달받는 생성자를 호출하라.
멤버 이니셜라이저를 이용한 변수 및 const상수(변수) 초기화
'멤버 이니셜라이저'는 객체가 아닌 멤버의 초기화에도 사용할 수 있다.
class FruitSeller {
private:
const int APPLE_PRICE;
int numOfApples;
int myMoney;
public:
FruitSeller(int price, int num, int money)
: APPLE_PRICE(price), numOfApples(num),myMoney(money)
{
//const 멤버변수도 이니셜라이저를 이용하면 초기화 가능하다.
}
int SaleApples(int money) {
int num = money / APPLE_PRICE;
numOfApples -= num;
myMoney += money;
return num;
}
void ShowSalesResult() const {
cout << "남은 사과: " << numOfApples << endl;
cout << "판매 수익: " << myMoney << endl << endl;
}
};
APPLE_PRICE(price)는 APPLE_PRICE의 값을 price의 값으로 초기화하라는 뜻이다.
이니셜라이저를 사용하여 멤버변수를 초기화하면 다음과 같은 이점이 있다.
1. 초기화의 대상을 명확히 인식할 수 있다.
2. 성능에 약간의 이점이 있다.
APPLE_PRICE(price)는 int APPLE_PRICE = price;에 비유할 수 있다.
이니셜라이저를 이용하면 선언과 동시에 초기화가 이루어지는 형태로 바이너리 코드가 생성된다.
따라서 const 멤버변수도 이니셜라이저를 이용하면 초기화가 가능하다는 사실을 알 수 있다.
마찬가지로 선언과 동시에 초기화가 이루어져야하는 '참조자'도
이니셜라이저를 이용하면 멤버변수로 선언될 수 있다.
#include <iostream>
using namespace std;
class AAA {
public:
AAA() {
cout << "empty object" << endl;
}
void ShowYourName() {
cout << "I'm class AAA" << endl;
}
};
class BBB {
private:
AAA& ref;//참조자가 멤버변수로 선언됨
const int& num;
public:
BBB(AAA& r, const int& n)
:ref(r), num(n)//참조자도 이니셜라이저를 이용하면 멤버변수로 선언될 수 있다.
{//empty
}
void ShowYourName() {
ref.ShowYourName();
cout << "and" << endl;
cout << "I ref num" << num << endl;
}
};
int main(void) {
AAA obj1;
BBB obj2(obj1, 20);
obj2.ShowYourName();
return 0;
}
디폴트 생성자(Default Constructor)
C++의 모든 객체는 세가지 과정을 순서대로 거쳐서 생성된다.
1. 메모리 공간의 할당
2. 이니셜라이저를 이용한 멤버변수(객체)의 초기화
3. 생성자의 몸체 부분 실행
이니셜라이저는 선택적으로 존재하지만, 객체가 되기 위해서는 반드시 하나의 생성자가 호출되어야 한다.
생성자는 정의하지 않으면 '디폴트 생성자'가 자동으로 삽입되어 호출된다.
class AAA{
private:
int num;
public:
int GetNum{return num;}
};
class AAA{
private:
int num;
public:
AAA(){ } //디폴트 생성자
int GetNum{return num;}
};
위 두개의 코드는 동일하다.
모든 객체는 한번의 생성자 호출을 동반한다.
AAA* ptr = new AAA:
클래스를 위의 형태로 생성해도 객체의 생성과정에서 생성자가 호출된다.
AAA* ptr = (AAA*)malloc(sizeof(AAA));
단, new 연산자가 아닌, C언어의 malloc 함수 호출 시,
실제로는 AAA 클래스의 크기 정보만 바이트 단위로 전달되기 때문에 생성자가 호출되지 않는다.
따라서, 객체를 동적으로 할당하려는 경우에는 반드시 new 연산자를 이용해야 한다.
생성자 불일치
디폴트 생성자는 생성자가 하나도 정의되어있지 않을 때에만 삽입된다.
즉, 다음과 같이 정의된 클래스에는 디폴트 생성자가 삽입되지 않는다.
class SoSimple{
private:
int num;
public:
SoSimple(int n) : num(n) { }
};
SoSimple simObj(10); //O
SoSimple* simPtr = new SoSimple(2); //(O)
SoSimple simObj2; //(X)
SoSimple* simPtr2 = new SoSimple; //(X)
private 생성자
객체의 생성이 클래스의 외부에서 진행되면 생성자는 public으로 선언되어야 한다.
클래스 내부에서 객체를 생성한다면, 생성자를 private으로 선언해도 된다.
#include <iostream>
using namespace std;
class AAA {
private:
int num;
public:
//public 생성자 정의.
//클래스 외부에서는 이 생성자를 기반으로 객체를 생성해야한다.
AAA() : num(0){}
//함수 내에서 18행에 정의된 private 생성자를 이용해서 AAA객체를 생성 및 반환한다.
AAA& CreateInitObj(int n) const {
AAA* ptr = new AAA(n);
return *ptr;
}
void ShowNum() const { cout << num << endl; }
private:
AAA(int n):num(n) {}
};
int main(void) {
AAA base;
base.ShowNum();
AAA& obj1 = base.CreateInitObj(3);
obj1.ShowNum();
AAA& obj2 = base.CreateInitObj(12);
obj2.ShowNum();
delete& obj1;
delete& obj2;
return 0;
}
위 예제에서는 힙 영역에 생성된 객체를 참조의 형태로 반환하고 있다.
힙에 할당된 메모리 공간은 변수로 간주하여, 참조자를 통한 참조가 가능하다.
private으로 선언된 생성자를 통해서도 객체의 생성이 가능함을 알 수 있다.
소멸자의 이해와 활용
객체 생성시 반드시 호출되는 생성자와 마찬가지로,
객체 소멸시 반드시 '소멸자'가 호출된다.
클래스의 이름 앞에 '~'가 붙은 형태의 이름을 갖는다.
반환형이 선언되어 있지 않으며, 실제로 반환하지 않는다.
매개변수는 void형으로 선언되어야하기 때문에 오버로딩 및 디폴트 값 설정이 불가능하다.
소멸자를 정의하지 않으면, 디폴트 소멸자가 자동으로 삽입된다.
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>
using namespace std;
class Person {
private:
char* name;
int age;
public:
Person(char* myname, int myage) {
int len = strlen(myname) + 1;
name = new char[len];
//불필요한 메모리 공간의 낭비를 막기 위해 문자열의 길이만큼 메모리 공간을 동적 할당
strcpy(name, myname);
age = myage;
}
void ShowPersonInfo() const {
cout << "이름: " << name << endl;
cout << "나이: " << age << endl;
}
//소멸자
~Person() {
delete []name;
cout << "called destructor!" << endl;
}
};
int main(void) {
Person man1("Lee dong woo", 29);
Person man2("Jang dong gun", 41);
man1.ShowPersonInfo();
man2.ShowPersonInfo();
return 0;
}