이제까지 객체 생성, 객체의 멤버변수 초기화를 목적으로 InitMembers라는 이름의 함수를 정의하고 호출했고, 정보은닉을 목적으로 멤버변수들은 private으로 선언해왔다. 생성자를 이용하면 생성과 동시에 초기화 할 수 있다.
class SimpleClass
{
private:
int num;
public:
SimpelClass(int n) //constructor(생성자)
{
num = n;
}
int GetNum() const {
return num;
}
}
위에서 클래스의 이름과 함수의 이름이 동일, 반환형이 선언되어 있지 않고 반환하지 않는 함수가 생성자이다
생성자는 이런 함수이다.
클래스의 이름과 함수의 이름이 동일하다.
반환형이 선언되어 있지 않고 반환하지 않는다.
객체 생성 시 딱 1번 호출된다.
객체의 생성과 동시에 멤버변수를 초기화할 때 쓰인다.
생성자를 정의하지 않았을 때 아래처럼 객체를 생성했다.
SimpleClass sc; //전역, 지역, 매개변수의 형태
SimpleClass *ptr = new SimpleClass; //동적 할당의 형태
생성자가 정의되면 객체생성과정에서 자동으로 호출되는 생성자에게 전달할 인자의 정보를 다음과 같이 추가한다.
SimpleClass sc(20); // 생성자에 20을 전달
SimpleClass *ptr = new SimpleClass(30); //생성자에 30을 전달
생성자도 함수의 일종이니 오버로딩이 가능하다.
생성자도 함수의 일종이니 매개변수에 디폴트값을 설정할 수 있다.
매개변수에 디폴트값을 설정하는 예제
//Constructor1.cpp
#include <iostream>
using namespace std;
class SimpleClass
{
private:
int num1;
int num2;
public:
SimpleClass()
{
num1=0;
num2=0;
}
SimpleClass(int n)
{
num1=n;
num2=0;
}
SimpleClass(int n1, int n2)
{
num1=n1;
num2=n2;
}
// 오버로딩 가능
// 매개변수의 디폴트 값 설정 가능
// SimpleClass(int n1=0, int n2=0)
// {
// num1=n1;
// num2=n2;
// }
void ShowData() const {
cout<<num1<<' '<<num2<<endl;
}
};
int main(void)
{
SimpleClass sc1;
sc1.ShowData();
SimpleClass sc2(100);
sc2.ShowData();
SimpleClass sc3(100,200);
sc3.ShowData();
return 0;
}
코드에서 다른 생성자를 주석처리하지 않고 디폴트값 설정한 생성자를 주석 해제하면 어떤 생성자를 호출할지 애매하다는 오류 메시지가 출력된다. SimpleClass sc2(100); 처럼 객체를 생성하면 두 생성자 모두 호출이 가능하기 때문에 호출할 생성자를 결정하지 못해서다.
아래는 int형 데이터가 인자인 생성자를 이용해서 객체를 생성하는 예이다.
SimpleClass sc1; (0)
SimpleClass sc1(); (X)
SimpleClass * ptr1=new SimpleClass; (0)
SimpleClass * ptr1=new SimpleClass(); (0)
또한 SimpleClass() 생성자를 이용해서 객체를 생성하려면 SimpleClass sc1(); 과 같이 구성하면 안되며
아래처럼 SimpleClass sc1; 로 작성해야 한다. 그 이유는 아래 코드에..
// Constructor2.cpp
#include <iostream>
using namespace std;
class SimpleClass
{
private:
int num1;
int num2;
public:
SimpleClass(int n1=0, int n2=0)
{
num1=n1;
num2=n2;
}
void ShowData() const
{
cout<<num1<<' '<<num2<<endl;
}
};
int main(void)
{
// 함수의 원형이라고 이전에 전역적으로 (함수 밖에)
// 선언했지만 지역적으로 함수 내에 원형을 선언했다
// void형 (인자를 안받는)을 객체 생성문, 함수의 원형선언 둘 다 인정하지 않고
// 함수의 원형선언에만 사용하기로 약속함
SimpleClass sc1(); //지역적으로 함수의 원형 선언
SimpleClass mysc=sc1(); //반환객체값으로 mysc객체를 초기화
mysc.ShowData();
return 0;
}
SimpleClass sc1()
{
SimpleClass sc(20, 30);
return sc;
}
함수의 원형은 함수 밖에 선언하지만, 함수 내 지역적으로도 선언 가능하다. SimpleClass sc1(); 는 함수의 원형 선언에 해당한다. void형 생성자의 호출문으로도 인정하면 객체생성문인지 함수원형선언인지 혼란을 주니 함수 원형선언으로만 인식한다.
생성자로 객체 생성하는 예제
03단원의 FruitSaleSim1.cpp 예제를 생성자를 적용해 변경했다.
//FruitSaleSim2.cpp
#include <iostream>
using namespace std;
//사과장수
class FruitSeller
{
private:
int APPLE_PRICE; //사과가격 (상수)
int numOfApples; //재고
int myMoney; //사과장수의 지갑
public:
// 초기화
/*void InitMembers(int price, int num, int money)
{
APPLE_PRICE = price;
numOfApples = num;
myMoney = money;
}*/
// 생성자
FruitSeller(int price, int num, int money) {
APPLE_PRICE = price;
numOfApples = num;
myMoney = money;
}
// 사과 판매
int SaleApples(int money)
{
if (money < 0) {
cout << "잘못된 정보가 전달되어 구매를 취소합니다" << endl;
return 0;
}
int num = money / APPLE_PRICE; //팔린 갯수
numOfApples -= num;
myMoney += money; //사과장수의 지갑
return num;
}
// 수익과 재고 출력
// const : 이 함수 내에서는 멤버변수에 저장된 값을 변경하지 않겠다
void ShowSalesResult() const ////추가
{
cout << "남은 사과 : " << numOfApples << endl;
cout << "판매 수익: " << myMoney << endl << endl;
}
};
// 손님
class FruitBuyer {
//프라이빗
int myMoney; // 손님지갑
int numOfApples; //산 사과갯수
public:
/*void InitMembers(int money)
{
myMoney = money;
numOfApples = 0;
}*/
// 생성자
FruitBuyer(int money) {
myMoney = money;
numOfApples = 0;
}
// 구매. 구매대상, 구매금액이 전달
void BuyApples(FruitSeller& seller, int money) {
// unsigned int로 하면 없어도 됨..
if (money < 0) {
cout << "잘못된 정보가 전달되어 구매를 취소합니다" << endl;
return;
}
numOfApples += seller.SaleApples(money); //사과 구매하려고 함수 호출
myMoney -= money;
}
void ShowBuyResult() const ////추가
{
cout << "현재 잔액 : " << myMoney << endl;
cout << "사과 개수 : " << numOfApples << endl << endl;
}
};
int main(void)
{
//FruitSeller seller;
//seller.InitMembers(1000, 20, 0);
FruitSeller seller(1000, 20, 0);
//FruitBuyer buyer;
//buyer.InitMembers(5000);
FruitBuyer buyer(5000);
buyer.BuyApples(seller, 2000); //사과 구매
cout << "과일판매자 현황" << endl;
seller.ShowSalesResult();
cout << "과일구매자 현황" << endl;
buyer.ShowBuyResult();
return 0;
}
생성자 내에서 멤버의 초기화를 목적으로 함수를 호출하기도 한다.
즉 InitMembers 함수를 지우지 않고 생성자 내에서 함수를 호출하게끔 해도 된다.
아래 예제들에 생성자를 적용해본다.
Point 클래스의 선언 및 정의 - Point.h, Point.cpp
Rectangle 클래스의 선언 및 정의 - Rectangle.h, Rectangle.cpp
실행을 위한 main함수 정의 - RectangleFaultFind.cpp
Point 클래스에 생성자 적용
//Point.h
#ifndef __POINT_H_
#define __POINT_H_
class Point {
// 점을 표현
private:
int x; //x좌표 범위는 0~100
int y; //같음
public:
// 함수 내에서 멤버변수에 저장되는 값을 제한
// 변경 전
// bool InitMembers(int xpos, int ypos);
// 변경 후
Point(const int& xpos, const int& ypos); //생성자
int GetX() const;
int GetY() const;
bool SetX(int xpos);
bool SetY(int xpos);
};
#endif
//Point.cpp
#include <iostream>
#include "Point.h"
using namespace std;
//변경 전
//bool Point::InitMembers(int xpos, int ypos)
//{
// if (xpos < 0 || ypos < 0)
// {
//
// cout << "벗어난 범위의 값 전달" << endl;
// return false;
// }
// x = xpos;
// y = ypos;
// return true;
//}
//변경 후
Point::Point(const int &xpos, const int &ypos)
{
x = xpos;
y = ypos;
}
int Point::GetX() const { //const 함수 (나중에 설명볼것)
return x;
}
int Point::GetY() const {
return y;
}
bool Point::SetX(int xpos)
{
if (0 > xpos || xpos > 100) {
cout << "벗어난 범위의 값 전달" << endl;
return false;
}
x = xpos;
return true;
}
bool Point::SetY(int ypos)
{
if (0 > ypos || ypos > 100) {
cout << "벗어난 범위의 값 전달" << endl;
return false;
}
y = ypos;
return true;
}
Rectangle 클래스는 두 개의 Point 객체를 멤버로 갖고 있어서 Rectangle 객체가 생성되면 두 개의 Point 객체가 함께 생성된다. 생성자는 멤버변수 초기화를 목적으로 정의되니 객체생성과정에서의 생성자 호출은 객체초기화를 쉽게 한다.
객체 생성과정에서 Point 클래스의 생성자를 통해 Point 객체를 초기화 하려면 멤버 이니셜라이저(member initializer)를 사용하면 된다.
멤버 이니셜라이저를 통한 멤버 초기화
// Rectangle.h
#ifndef __RECTANGLE_H_
#define __RECTANGLE_H
#include "Point.h"
class Rectangle
{
private:
Point upLeft;
Point lowRight;
public:
//변경 전
//bool InitMembers(const Point& ul, const Point& lr);
//변경 후
Rectangle(const int& x1, const int& y1, const int& x2, const int& y2);
void ShowRecInfo() const;
};
#endif
객체 upLeft의 생성과정에서 x1과 y1을 인자로 전달받는 생성자를 호출하라.
객체 lowRight의 생성과정에서 x2와 y2을 인자로 전달받는 생성자를 호출하라. 는 뜻
// Rectangle.cpp
#include <iostream>
#include "Rectangle.h"
using namespace std;
//변경 전
//bool Rectangle::InitMembers(const Point& ul, const Point& lr)
//{
// if (ul.GetX() > lr.GetX() || ul.GetY() > lr.GetY()) {
// cout << "잘못된 위치정보 전달" << endl;
// return false;
// }
// upLeft = ul;
// lowRight = lr;
// return true;
//}
//변경 후
Rectangle::Rectangle(const int& x1, const int& y1, const int& x2, const int& y2)
:upLeft(x1, y1), lowRight(x2, y2) //멤버이니셜라이저
{
//empty
}
void Rectangle::ShowRecInfo() const
{
cout << "좌 상단 : " << '[' << upLeft.GetX() << ", ";
cout << upLeft.GetY() << ']' << endl;
cout << "우 하단 : " << '[' << lowRight.GetX() << ", ";
cout << lowRight.GetY() << ']' << endl <<endl;
}
// RectangleConstructor.cpp
#include <iostream>
#include "Point.h"
#include "Rectangle.h"
using namespace std;
//int main(void)
//{
// Point pos1;
// if (!pos1.InitMembers(-2, 4)) {
// cout << "초기화 실패" << endl;
// }
// if (!pos1.InitMembers(2, 4)) {
// cout << "초기화 실패" << endl;
// }
//
// Point pos2;
// if (!pos2.InitMembers(5, 9)) {
// cout << "초기화 실패" << endl;
// }
//
// Rectangle rec;
// if (!rec.InitMembers(pos2, pos1))
// cout << "직사각형 초기화 실패" << endl;
// if(!rec.InitMembers(pos1, pos2))
// cout << "직사각형 초기화 실패" << endl;
//
// rec.ShowRecInfo();
// return 0;
//}
//변경 후
//RectangleConstructor.cpp
int main(void)
{
Rectangle rec(1, 1, 5, 5);
rec.ShowRecInfo();
return 0;
}
멤버이니셜라이저는 멤버변수로 선언된 객체의 생성자 호출에 활용된다.
멤버이니셜라이저를 사용하다보면, 생성자의 몸체 부분이 위처럼 비어있기도 하다.
객체 생성과정 3단계
C++의 모든 객체는 이 3단계를 순서대로 거쳐서 객체 생성이 완성된다.
1. 메모리 공간의 할당
2. 이니셜라이저를 이용한 멤버변수(객체)의 초기화
3. 생성자의 몸체부분 실행
이니셜라이저가 없다면 메모리 공간의 할당, 생성자의 몸체부분 실행으로 객체생성이 완료된다.
생성자를 정의하지 않으면 디폴트생성자가 자동 삽입되어 반드시 호출된다.
멤버 이니셜라이저를 이용한 변수 및 상수 초기화
멤버이니셜라이저는 객체말고 멤버의 초기화에도 사용할 수 있다.
class SoSimple
{
private:
int num1;
int num2;
public:
// num1을 n1의 값으로 초기화
// int num1=n1;과 같다
SoSimple(int n1, int n2):num1(n1)
{
num2=n2;
}
.......
};
멤버변수 초기화 방법
1. 생성자의 몸체에서 초기화
아래 문장과 같다.
int num2;
num2=n2;
2. 이니셜라이저를 이용하는 초기화 (일반적으로 좀 더 선호)
아래 문장과 같다.
int num1=n1;
멤버 이니셜라이저로 멤버를 초기화하는 이유
1. 초기화의 대상 명확
2. 성능 약간 이점
선언과 동시에 초기화가 이뤄지는 형태로 바이너리 코드가 생성된다. 생성자의 몸체에서 대입연산으로 초기화를 하면 선언, 초기화를 별도 문장에서 진행하는 형태로 바이너리 코드가 생성된다.
멤버 이니셜라이저로 const 멤버변수 초기화
// FruitSaleSim3.cpp
#include <iostream>
using namespace std;
//사과장수
class FruitSeller
{
private:
const int APPLE_PRICE; //사과가격 (상수) ///const 추가
int numOfApples; //재고
int myMoney; //사과장수의 지갑
public:
// 초기화
/*void InitMembers(int price, int num, int money)
{
APPLE_PRICE = price;
numOfApples = num;
myMoney = money;
}*/
// 생성자
FruitSeller(int price, int num, int money)
:APPLE_PRICE(price), numOfApples(num), myMoney(money)
{
/// 멤버 이니셜라이저를 사용
/*APPLE_PRICE = price;
numOfApples = num;
myMoney = money;*/
}
// 사과 판매
int SaleApples(int money)
{
if (money < 0) {
cout << "잘못된 정보가 전달되어 구매를 취소합니다" << endl;
return 0;
}
int num = money / APPLE_PRICE; //팔린 갯수
numOfApples -= num;
myMoney += money; //사과장수의 지갑
return num;
}
// 수익과 재고 출력
// const : 이 함수 내에서는 멤버변수에 저장된 값을 변경하지 않겠다
void ShowSalesResult() const
{
cout << "남은 사과 : " << numOfApples << endl;
cout << "판매 수익: " << myMoney << endl << endl;
}
};
// 손님
class FruitBuyer {
//프라이빗
int myMoney; // 손님지갑
int numOfApples; //산 사과갯수
public:
/*void InitMembers(int money)
{
myMoney = money;
numOfApples = 0;
}*/
// 생성자
FruitBuyer(int money)
: myMoney(money), numOfApples(0)
{
/// 멤버 이니셜라이저
/*myMoney = money;
numOfApples = 0;*/
}
// 구매. 구매대상, 구매금액이 전달
void BuyApples(FruitSeller& seller, int money) {
// unsigned int로 하면 없어도 됨..
if (money < 0) {
cout << "잘못된 정보가 전달되어 구매를 취소합니다" << endl;
return;
}
numOfApples += seller.SaleApples(money); //사과 구매하려고 함수 호출
myMoney -= money;
}
void ShowBuyResult() const ////추가
{
cout << "현재 잔액 : " << myMoney << endl;
cout << "사과 개수 : " << numOfApples << endl << endl;
}
};
int main(void)
{
//FruitSeller seller;
//seller.InitMembers(1000, 20, 0);
FruitSeller seller(1000, 20, 0);
//FruitBuyer buyer;
//buyer.InitMembers(5000);
FruitBuyer buyer(5000);
buyer.BuyApples(seller, 2000); //사과 구매
cout << "과일판매자 현황" << endl;
seller.ShowSalesResult();
cout << "과일구매자 현황" << endl;
buyer.ShowBuyResult();
return 0;
}
const변수(=const 상수)처럼 참조자도 선언하면서 초기화해야한다.
이니셜라이저로 참조자를 멤버변수로 선언할 수 있다.
// ReferenceMember.cpp
#include <iostream>
using namespace std;
class AAA
{
public:
AAA()
{
cout << "empty object" << endl;
// 객체생성 시 실제로 생성자가 호출된다.
}
void ShowYourName()
{
cout << "Im class AAA" << endl;
}
};
class BBB
{
AAA& ref;
const int # //참조자가 멤버변수로 선언되어 이니셜라이저로 초기화해야한다.
public:
BBB(AAA& r, const int& n) :ref(r), num(n) {
//empty constructor body
}
void ShowYourName()
{
ref.ShowYourName();
cout << "and" << endl;
cout << "I ref num " << num << endl;
}
};
int main()
{
AAA obj1;
BBB obj2(obj1, 20);
obj2.ShowYourName();
return 0;
}
디폴트 생성자 Default Constuctor
객체 : 메모리 공간 할당 후 생성자의 호출까지 완료되어야 한다.
모든 객체는 한 번의 생성자 호출을 동반한다. (new 연산자를 사용한 객체의 생성에도 해당)
그래서 생성자를 정의하지 않은 클래스에는 컴파일러에 의해 디폴트 생성자가 자동 삽입된다.
디폴트 생성자는 인자를 받지 않고, 내부적으로 아무런 일도 하지 않는다.
class AAA
{
private:
int num;
public:
// AAA(){} // 디폴트 생성자
int GetNum
{
return num;
}
};
new연산자를 이용한 객체 생성에도 생성자가 호출되나
malloc함수를 사용하면 생성자가 호출되지 않는다. AAA클래스의 크기정보만 바이트 단위로 전달되서 그렇다.
AAA * ptr=new AAA;
// AAA * ptr=(AAA*)malloc(sizeof(AAA)); //xxxxx
private 생성자
객체의 생성이 클래스 외부에서 진행되면 생성자는 public으로 선언되야 한다.
클래스 내부에서 객체를 생성하면 당연히 생성자는 private으로 선언되어도 된다.
public과 private 선언에 따른 차이점을 확인하기 위한 예제이다.
주로 싱글톤 디자인 패턴을 사용할 때 private생성자를 사용한다.
// PrivateConstructor.cpp
#include <iostream>
using namespace std;
class AAA
{
private:
int num;
public:
AAA() :num(0) {}
AAA& CreateInitObj(int n) const
{
AAA* ptr = new AAA(n);
return *ptr;
}
void ShowNum() const
{
cout << num << endl;
}
private:
AAA(int n) :num(n) {}
//private 생성자
};
int main(void)
{
AAA base;
base.ShowNum();
// base 객체를 이용해서 하나의 객체를 또 생성한다.
AAA& obj1 = base.CreateInitObj(3);
obj1.ShowNum();
AAA& obj2 = base.CreateInitObj(12);
obj2.ShowNum();
delete &obj1;
delete &obj2;
return 0;
}
힙영역에 생성된 객체를 참조의 형태로 반환하고 있다.
힙에 할당된 메모리 공간은 변수로 간주하여 참조자를 통해 참조가 가능하다는 걸 알 수 있다.
소멸자
생성자에서 할당한 리소스의 소멸에 사용된다.
객체소멸 시 반드시 호출된다.
클래스 이름 앞에 ~를 붙인다.
반환형이 선언되어 있지 않으며 실제로 반환하지 않는다.
매개변수는 void형으로 선언되어야 해서 오버로딩, 디폴트값 설정이 불가하다.
소멸자를 직접 정의하지 않으면 생성자처럼 디폴트소멸자가 자동 삽입된다.
class AAA
{
public:
AAA() { .... }
~AAA() { .... }
}
생성자 내에서 new 연산자를 이용해 할당한 메모리공간이 있으면
소멸자에서는 delete 연산자를 이용해 메모리공간을 소멸한다.
// Destructor.cpp
#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; //myname길이 + 널문자열
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", 29);
Person man2("Jang", 30);
man1.ShowPersonInfo();
man2.ShowPersonInfo();
return 0;
}
'C, C++ > 열혈 C++ 프로그래밍' 카테고리의 다른 글
05.복사생성자 (0) | 2021.09.28 |
---|---|
04-4. 클래스와 배열 그리고 this 포인터 (책) (0) | 2021.09.25 |
04. 클래스의 완성 연습문제 (0) | 2021.09.23 |
3. 클래스의 기본 (0) | 2021.09.15 |
02. C언어 기반의 C++ 연습문제 (0) | 2021.09.14 |