728x90

객체 포인터의 참조 관계


근로학생은 학생이자 사람이다.

따라서 상속관계는 다음과 같다.


class Student : public Person

{  };


class PartTimeStudent : public Student

{  };


따라서 아래와 같이 포인터 변수를 통해 참조가 가능하다.


Person * ptr = new Student();

Person * ptr = new PartTimeStudent();

Student * ptr = new PartTimeStudent();


다음과 같은 참조는 불가능하다. 모든 학생이 근로학생이란건 논리적으로도 맞지 않다.

PartTimeStudent * ptr = new Student();




Person * ptr;

따라서, 위와 같은 포인터 변수는, Person 클래스의 객체 또는 Person을 직간접적으로 상속하는 모든 클래스의 객체를 가리킬 수 있다. (객체 주소 값 저장 가능)



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

상속의 이해  (0) 2016.10.13
상속  (0) 2016.09.30
const, friend, static, mutable 선언  (0) 2016.09.29
복사 생성자, 얕은 복사와 깊은 복사, 임시 객체  (0) 2016.09.28
객체 배열과 this포인터  (0) 2016.09.27
728x90

유도 클래스의 객체 생성 과정


유도 클래스의 객체 과정에서 기초 클래스의 생성자는 100% 호출된다.


유도클래스의 생성자에서 기초 클래스의 생성자 호출을 명시하지 않으면, 기초 클래스의 void 생성자가 호출된다.

(기초 클래스의 void생성자가 정의되있어야 함)



#include <iostream>
using namespace std;

class SoBase
{
private:
	int baseNum;
public:
	SoBase() : baseNum(20)
	{
		cout << "SoBase()" << endl;
	}
	SoBase(int n) : baseNum(n)
	{
		cout << "SoBase(int n)" << endl;
	}
	void ShowBaseData()
	{
		cout << baseNum << endl;
	}
};

class SoDerived : public SoBase	// public 상속
{
private:
	int derivNum;
public:
	SoDerived() : derivNum(30)	
	{ 
		// 생성자, 기초 클래스의 생성자 호출을 명시하지 않음
		cout << "SoDerived()" << endl;
	}
	SoDerived(int n) : derivNum(n)	
		// 역시 기초 클래스 생성자 호출 명시하지 않음
	{
		cout << "SoDerived(int n)" << endl;
	}
	SoDerived(int n1, int n2) : SoBase(n1), derivNum(n2) 
		// n1을 인자로 받는 기초 클래스 생성자 호출 명시
	{
		cout << "SoDerived(int n1, int n2)" << endl;
	}
	void ShowDerivData()
	{
		ShowBaseData();
		cout << derivNum << endl;
	}
};

int main(void)
{
	cout << "case1..... " << endl;
	SoDerived dr1;
	dr1.ShowDerivData();
	cout << "-------------------" << endl;
	cout << "case2..... " << endl;
	SoDerived dr2(12);
	dr2.ShowDerivData();
	cout << "-------------------" << endl;
	cout << "case3..... " << endl;
	SoDerived dr3(23, 24);
	dr3.ShowDerivData();
	return 0;
};


Output : 

case1.....

SoBase()

SoDerived()

20

30

-------------------

case2.....

SoBase()

SoDerived(int n)

20

12

-------------------

case3.....

SoBase(int n)

SoDerived(int n1, int n2)

23

24



출력결과를 보면 유도 클래스의 객체를 생성할 때 기초 클래스의 생성자가 먼저 호출됨을 알 수 있다.



case 3의 객체 생성 과정은 다음과 같다.


유도 클래스의 객체를 생성할 때 생성자를 호출하고 이니셜라이저에 의해 n1, n2 인자가 전달되면서 유도 클래스의 생성자가 호출된다.

이 때 생성자가 호출되었다고 해서 바로 실행이 되는 것이 아니다.

기초 클래스의 생성자 호출이 우선되어야 한다.  


이니셜라이저의 SoBase(n1)에 의해 매개 변수 n1으로 전달된 값을 인자로 전달받을 수 있는 기초 클래스의 생성자를 호출한다. 호출이 완료되면, 

기초 클래스의 멤버 변수가 초기화되고 이어 유도 클래스의 생성자 실행이 완료되면서 유도 클래스 멤버 변수도 초기화가 이루어진다.



이렇게 객체가 생성되며, 앞서 이야기했듯이 이니셜라이저에서 기초 클래스 생성자 호출이 명시되지 않으면 void 생성자를 호출한다.


유도 클래스에서도 다음의 원칙은 적용된다.

"클래스의 멤버는 해당 클래스의 생성자를 통해 초기화해야한다."




유도 클래스 객체의 소멸 과정


소멸자는 명시적 선언이 필요없다. 다음 예제를 통해, 유도 클래스의 객체가 소멸될 때, 어느 시점에 소멸자가 호출되고 실행되는지 알 수 있다.


#include <iostream>
using namespace std;

class SoBase
{
private:
	int baseNum;
public:
	SoBase(int n) : baseNum(n)
	{
		cout << "SoBase() : " << baseNum << endl;
	}
	~SoBase()
	{
		cout << "~SoBase() : " << baseNum << endl;
	}
};

class SoDerived : public SoBase
{
private:
	int derivNum;
public:
	SoDerived(int n) : SoBase(n), derivNum(n)
	{
		cout << "SoDerived() : " << derivNum << endl;
	}
	~SoDerived()
	{
		cout << "~SoDerived() : " << derivNum << endl;
	}
};

int main(void)
{
	SoDerived drv1(15);
	SoDerived drv2(27);
	return 0;
};


Output :

SoBase() : 15

SoDerived() : 15

SoBase() : 27

SoDerived() : 27

~SoDerived() : 27

~SoBase() : 27

~SoDerived() : 15

~SoBase() : 15


출력결과를 보면 유도 클래스의 객체가 소멸될 때, 유도 클래스의 소멸자가 실행되고 기초 클래스의 소멸자가 실행된다. 

(즉 객체에 소멸순서는 생성순서와 반대)



유도 클래스의 생성자 및 소멸자 정의 모델

생성자에서 동적 할당한 메모리 공간은 소멸자에서 해제한다. 상속과 연관된 클래스의 소멸자도 이러한 원칙을 지켜 정의해야 한다.


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
32
33
34
35
36
37
38
39
40
41
class Person
{
private:
    char * name;
public:
    Person(char * myname)
    {
        name=new char[strlen(myname)+1];
        strcpy(name, myname);
    }
    ~Person()
    {
        delete []name;
    }
    void WhatYourName() const
    {
        cout<<"My name is "<<name<<endl;
    }
};
 
class UnivStudent : public Person
{
private:
    char * major;
public:
    UnivStudent(char * myname, char * mymajor)
        :Person(myname)
    {
        major=new char[strlen(mymajor)+1];
        strcpy(major, mymajor);
    }
    ~UnivStudent()
    {
        delete []major;
    }
    void WhoAreYou() const
    {
        WhatYourName();
        cout<<"My major is "<<major<<endl<<endl;
    }
};
cs


상속과 생성자의 호출 예제


https://goo.gl/4Ibgjm



생성자와 소멸자 정의 예제

#include <iostream>
using namespace std;

class MyFriendInfo
{
private:
	char * name;
	int age;
public:
	MyFriendInfo(char * fname, int fage) : age(fage)
	{
		name = new char[strlen(fname) + 1];
		strcpy(name, fname);
	}
	void ShowMyFriendInfo()
	{
		cout << "이름: " << name << endl;
		cout << "나이: " << age << endl;
	}
	~MyFriendInfo()
	{
		delete[]name;
	}
};

class MyFriendDetailInfo : public MyFriendInfo
{
private:
	char * addr;
	char * phone;
public:
	MyFriendDetailInfo(char *fname, int fage, char *faddr, char *fphone)
		:MyFriendInfo(fname, fage)
	{
		addr = new char[strlen(faddr) + 1];
		phone = new char[strlen(fphone) + 1];
		strcpy(addr, faddr);
		strcpy(phone, fphone);
	}
	void ShowMyFriendDetailInfo()
	{
		ShowMyFriendInfo();
		cout << "주소: " << addr << endl;
		cout << "전화: " << phone << endl << endl;
	}
	~MyFriendDetailInfo()
	{
		delete[]addr;
		delete[]phone;
	}
};

int main(void)
{
	MyFriendDetailInfo myfren("철수", 23, "서울종로구 사직동", "010-0000-0000");
	myfren.ShowMyFriendDetailInfo();
	return 0;
};

Output:
이름: 철수
나이: 23
주소: 서울종로구 사직동
전화: 010-0000-0000


protected로 선언된 멤버가 허용하는 접근 범위

세 가지 형태의 상속
private < protected < public



상속을 할 경우, 유도클래스에서는 private와 달리 protected로 선언된 멤버 변수에는 접근 가능

private멤버는 클래스내에서만 접근 가능하다.

주의할 것은 어떠한 형태로 상속하던지, 유도 클래스에서 상속한 private멤버는 접근이 불가능하단 것이다.


실제로는 C++에서 public 이외의 상속은 다중 상속과 같이 특별한 경우가 아니면 잘 사용하지 않는다.




상속의 기본 조건


is a 관계의 성립


전화 -> 무선 전화기            

컴퓨터 -> 노트북 컴퓨터    (Notebook Computer is a Computer)


컴퓨터를 두고 이동성이 있다는 이야기를 하진 않는다.

노트북 컴퓨터는 컴퓨터의 특성에 이동성이라는 특성이 추가된 것이다.


전화기가 더욱 넓은 범주지만 기능은 무선 전화가 더욱 많다고 이해하면 된다.

따라서 전화기가 기초 클래스가 되고, 무선전화기가 유도 클래스가 된다.



has a 관계의 성립


경찰은 총을 소유한다. (has a)


기초 클래스가 Gun, 유도 클래스가 Police


이 경우 의존도(커플링)가 높다.

Police와 Gun이 강한 연관성을 띠게되어, 총을 소유하지 않은 경찰이나 다른 무기를 소유하는 경찰을 표현하기가 쉽지 않아진다.


따라서 이런 has a관계의 상속은 좋지못하며, has a 관계는 포함으로 표현한다. 

따라서 아래와 같이 gun을 police의 멤버로 선언하는 것이 일반적이고 단순하며, 확장성 높은 방법이다.


위와 같은 클래스 정의는 권총을 소유하지 않거나(NULL), 다른 무기를 가진 경찰을 표현하기 쉬워진다.


커플링이 높은 클래스는 유연성이 낮기 때문에 주의해야한다.



728x90

상속(Inheritance)


상속에 대한 과거의 관점

: 기존에 정의해놓은 클래스의 재활용을 만들어진 문법적 요소



하지만 상속은 위와 같이 한 문장으로 정의할 수 있는 것이 아니며, 상속의 이점이 재활용에만 있는 것은 아니다.



데이터의 성격을 지닌 클래스 정의


급여관리 시스템에서 직원의 이름과 급여정보를 저장할 수 있는 클래스 정의


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class PermanentWorker
{
private:
    char name[100];    
    int salary;
public:
    PermanentWorker(char* name, int money)
        : salary(money)
    {
        strcpy(this->name, name);
    }
    int GetPay() const
    {
        return salary;
    }
    void ShowSalaryInfo() const
    {
        cout<<"name: "<<name<<endl;
        cout<<"salary: "<<GetPay()<<endl<<endl;
    }
};
cs


control 클래스(handler 클래스) 정의

: 기능의 처리를 실제로 담당하는 클래스

  • 새로운 직원 정보의 등록                 AddEmployee
  • 모든 직원의 이번 달 급여정보 출력    ShowAllSalaryInfo
  • 이번 달 급여의 총액 출력                ShowTotalSalary

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
class EmployeeHandler
{
private:
    PermanentWorker* empList[50];
    int empNum;
public:
    EmployeeHandler() : empNum(0)
    { }
    void AddEmployee(PermanentWorker* emp)
    {
        empList[empNum++= emp;
    }
    void ShowAllSalaryInfo() const
    {
        for (int i = 0; i<empNum; i++)
            empList[i]->ShowSalaryInfo();
    }
    void ShowTotalSalary() const
    {
        int sum = 0;
        for (int i = 0; i<empNum; i++)
            sum += empList[i]->GetPay();
        cout << "salary sum: " << sum << endl;
    }
    ~EmployeeHandler()
    {
        for (int i = 0; i<empNum; i++)
            delete empList[i];
    }
};
cs

이러한 컨트롤 클래스는 기능 제공의 핵심이기 때문에 OOP에서 반드시 존재하는 클래스이다.



위의 두 클래스를 기반으로 하는 main함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(void)
{
    // 직원관리를 목적으로 설계된 컨트롤 클래스의 객체생성
    EmployeeHandler handler;
 
    // 직원 등록
    handler.AddEmployee(new PermanentWorker("KIM"1000));
    handler.AddEmployee(new PermanentWorker("LEE"1500));
    handler.AddEmployee(new PermanentWorker("JUN"2000));
 
    // 이번 달에 지불해야 할 급여의 정보
    handler.ShowAllSalaryInfo();
 
    // 이번 달에 지불해야 할 급여의 총합
    handler.ShowTotalSalary();
    return 0;
}
cs


Output :

name: KIM

salary: 1000


name: LEE

salary: 1500


name: JUN

salary: 2000


salary sum: 4500



위의 프로그램은 큰 문제가 없어보이지만, 객체지향뿐만 아니라 소프트웨어 설계에 있어 중요시하는 다음의 요소를 만족하지 않는다.

  • 요구 사항의 변경에 대응하는 프로그램의 '유연성'
  • 기능의 추가에 따른 프로그램의 '확장성'


 

만약 직원의 고용형태가 다양해지고, 급여 계산 방식도 달라진다면 클래스의 추가 및 컨트롤 클래스안에 여러 소스의 추가수정이 불가피해진다.

따라서 이러한 문제는 '상속'을 통해 해결이 가능하다.



상속의 문법적 이해


 현실에서 상속(물려받음)하면 재산만을 떠올리기 쉽지만, 물려받는 것은 재산이 아닌 여러 특성일 수도 있다.


"지수는 아버지로부터 좋은 목소리와 큰 키, 재산을 물려받았다"


class Jisu : public Father    // Father클래스의 상속을 의미 (Jisu 클래스가 Father클래스로부터 물려받음을 의미)



상속의 방법과 그 결과


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person
{
private:
    int age;        // 나이
    char name[50];     // 이름
public:
    Person(int myage, char * myname) : age(myage)
    {
        strcpy(name, myname);
    }
    void WhatYourName() const
    {
        cout << "My name is " << name << endl;
    }
    void HowOldAreYou() const
    {
        cout << "I'm " << age << " years old" << endl;
    }
};
cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class UnivStudent : public Person    // Person 클래스를 상속 
{
private:
    char major[50];     // 전공과목
public:
    UnivStudent(char * myname, int myage, char * mymajor)
        : Person(myage, myname)
    {
        strcpy(major, mymajor);
    }
    void WhoAreYou() const 
    {
        WhatYourName();
        HowOldAreYou();
        cout<<"My major is "<<major<<endl<<endl;
    }
};
cs



// 다음 선언은 public 상속을 의미

class UnivStudent : public Person // Person 클래스의 상속을 의미

{

. . . .

}


이 때 상속받은 클래스의 객체 UnivStudent 객체의 구성은 다음과 같다.


상속을 하게 되면, 상속의 대상이 되는 클래스의 멤버가 객체내에 포함,

따라서 UnivStudent 클래스내에서 Person 클래스의 멤버 함수까지도 호출이 가능해진다.


상속과 관련된 용어

: Person과 같이 상속의 대상이 되는 클래스와 UnivStudent같이 상속을 하는 클래스를 다음과 같이 여러가지로 표현한다.


Person            ->    UnivStudent

상위 클래스        ->    하위 클래스

기초(base)클래스   ->    유도(derived) 클래스

슈퍼(super)클래스  ->    서브(sub) 클래스

부모 클래스        ->    자식 클래스  



유도 클래스(derived)의 생성자 정의


유도 클래스는 기초(base) 클래스의 초기할 데이터를 인자로 전달받을 책임과,

그 데이터를 기초 클래스의 생성자 호출을 통해 전달할 책임을 진다.


1
2
3
4
5
UnivStudent(char * myname, int myage, char * mymajor)
        : Person(myage, myname)
    {
        strcpy(major, mymajor);
    }
cs

유도 클래스의 생성자는 기초 클래스의 생성자를 호출하는 형태로 멤버 초기화가 이뤄진다.


유도클래스의 생성자에서 이니셜라이저가 의미하는 바는 생성자의 호출이다.

Person클래스의 생성자를 호출하면서 인자로 myage와 myname에 저장된 값을 전달하는 것이다.



상속에 관련된 예제


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

class Person
{
private:
	char name[50];	// 이름
	int age;		// 나이	
public:
	Person(char * myname, int myage)
	{
		strcpy(name, myname);
		age = myage;		
	}
	void WhatYourName() const
	{
		cout << "My name is " << name << endl;
	}
	void HowOldAreYou() const
	{
		cout << "I'm " << age << " years old" << endl;
	}
};

class UnivStudent : public Person
{
private:
	char major[50];     // 전공과목
public:
	UnivStudent(char * myname, int myage, char * mymajor)
		: Person(myname, myage)
	{
		strcpy(major, mymajor);
	}
	void WhoAreYou() const
	{
		WhatYourName();
		HowOldAreYou();
		cout << "My major is " << major << endl << endl;
	}
};

int main(void)
{
	UnivStudent ustd1("Lee", 22, "Computer eng.");
	ustd1.WhoAreYou();

	UnivStudent ustd2("Yoon", 21, "Electronic eng.");
	ustd2.WhoAreYou();
	return 0;
};


Output :

My name is Lee

I'm 22 years old

My major is Computer eng.


My name is Yoon

I'm 21 years old

My major is Electronic eng.



위의 예제에서 보듯 Person의 멤버 name과 age는 private 선언이 되어 있지만
Person 클래스에 정의된 public 함수를 통해 간접적으로 접근이 가능하다.

유도 클래스의 생성자에서 기초 클래스의 생성자를 호출한다.
기초 클래스의 생성자에서는 클래스내 private의 멤버에 접근이 가능하다.

이렇게 생성자의 호출을 통해 기초 클래스의 멤버를 초기화할 수 있는 것이다.



728x90

const 객체와 const 객체의 특성들

 

변수를 상수화 하듯이, 객체도 상수화 가능하다.

const SoSimple sim(20);

 

객체에 const 선언이 붙으면, 이 객체를 대상으로는 const 멤버함수만 호출이 가능하다.

(객체의 데이터 변경을 허용하지 않음)

 

아래 예제에서 const객체 sim의 멤버 함수 AddNum을 호출하려고 할 때, 호출이 불가능함을 확인할 수 있다


 


 

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

#include <iostream>

using namespace std;

 

class SoSimple

{

private:

        int num;

 

public:

        SoSimple(int n) : num(n)

        { }

        SoSimple AddNum(int n)

        {

               num += n;

               return *this;

        }

        void ShowData() const

        {

               cout << "num: " << num << endl;

        }

};

 

 

int main(void)

{

        const SoSimple sim(20);        // const 객체 생성

        // sim.AddNum(30);             // 멤버함수 AddNum const함수가 아니라서 호출 불가

        sim.ShowData();                // 멤버함수 ShowData const함수이니 호출 가능

        return 0;

}

 

Output:

1

num: 20

 

 

const와 함수 오버로딩

함수의 오버로딩이 성립하려면 매개변수 수나 자료형이 달라야 한다.

그 외에 const 선언 유무도 함수 오버로딩의 조건에 해당된다.

 

void SimpleFunc() { . . . . }

void SimpleFunc() const { . . . . }

 

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

32

33

34

35

36

37

38

39

40

41

42

#include <iostream>

using namespace std;

 

class SoSimple

{

private:

        int num;

public:

        SoSimple(int n) : num(n)

        { }

        SoSimple& AddNum(int n)

        {

               num += n;

               return *this;

        }

        void SimpleFunc()

        {

               cout << "SimpleFunc: " << num << endl;

        }

        void SimpleFunc() const       // Const Overloading

        {

               cout << "const SimpleFunc: " << num << endl;

        }

};

 

void YourFunc(const SoSimple &obj)    // 전달되는 인자가 const 참조자

{

        obj.SimpleFunc();              // 참조자를 이용한 함수 호출결과로 const멤버함수 호출

}

 

int main(void)

{

        SoSimple obj1(2);

        const SoSimple obj2(7);        // const 객체 생성

 

        obj1.SimpleFunc();    

        obj2.SimpleFunc();     // const 객체를 대상으로 함수를 호출했기에 const 멤버 함수가 호출됨

 

        YourFunc(obj1); // const 멤버 함수 호출

        YourFunc(obj2); // const 멤버 함수 호출

        return 0;

}


Output:

1

2

3

4

SimpleFunc: 2

const SimpleFunc: 7

const SimpleFunc: 2

const SimpleFunc: 7

 

 

friend 선언

: private 멤버의 접근을 허용하는 선언

 

  • A 클래스가 B 클래스를 대상으로 friend 선언을 하면, B 클래스는 A 클래스의 private 멤버에 직접 접근이 가능하다.
  • , A 클래스도 B 클래스의 private 멤버에 직접 접근 가능하려면, B 클래스가 A클래스를 대상으로 friend 선언을 해줘야 한다.

1

2

3

4

5

6

7

8

9

10

11

12

13

class Boy

{

private:

    int height;

    friend class Girl;    // Girl 클래스를 friend 

public:

    Boy(int len) : height(len)

    { }

    void ShowYourFriendInfo(Girl &frn);

};

Colored by Color Scripter

cs

 

Colored by Color Scripter

cs



5행의 friend선언으로 Girl 클래스 내에서는 Boy 클래스의 모든 private 멤버에 직접 접근이 가능해진다.

(물론 Girl 클래스에서 따로 friend 선언을 하지 않으면, Boy 클래스에선 Girl 클래스의 private 멤버 접근이 불가)

 

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

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

#include <iostream>

#include <cstring>

using namespace std;

 

class Girl;    // Girl이 클래스의 이름임을 알리는 선언

 

class Boy

{

private:

        int height;

        friend class Girl;     // Girl 클래스에 대한 friend 선언 (private 영역에서도 friend선언 가능)

public:

        Boy(int len) : height(len)

        { }

        void ShowYourFriendInfo(Girl &frn);  

};

 

class Girl

{

private:

        char phNum[20];

public:

        Girl(char * num)

        {

               strcpy(phNum, num);

        }

        void ShowYourFriendInfo(Boy &frn);

        friend class Boy;

};

 

void Boy::ShowYourFriendInfo(Girl &frn)

{

        cout << "Her phone number: " << frn.phNum << endl;

}

 

void Girl::ShowYourFriendInfo(Boy &frn)

{

        cout << "His height: " << frn.height << endl;

}

 

int main(void)

{

        Boy boy(170);

        Girl girl("010-1234-5678");

 

        boy.ShowYourFriendInfo(girl);

        girl.ShowYourFriendInfo(boy);

        return 0;

}

 

Output :

Her phone number: 010-1234-5678

His height: 170

 

 

위에서 5행의 클래스 선언이 없이도 컴파일이 되는데 11행의 선언이 Girl이 클래스의 이름임을 알리는 역할도 있기 때문이다.

 

       

friend class Girl;    // Girl은 클래스의 이름이며, Girl 클래스를 friend 선언한다!

 

 

 

friend 선언은 신중해야한다

: friend 선언은 객체지향의 '정보 은닉'에 반하는 선언이므로  신중히 선언해야 한다.

 

friend 선언은 연산자 오버로딩에서 유용하게 쓰이게 된다.

 

 

함수의 friend 선언

전역변수를 대상으로도, 클래스의 멤버 함수를 대상으로도 friend 선언이 가능하다.

friend로 선언된 함수는 자신이 선언된 클래스의 private 영역에 접근 가능하다.


#include <iostream>
using namespace std;
 
class Point;   // Point가 클래스의 이름임을 알림
 
class PointOP
{
private:
        int opcnt;
public:
        PointOP() : opcnt(0)
        {  }
 
        Point PointAdd(const Point&, const Point&);  
        Point PointSub(const Point&, const Point&);  
        ~PointOP()
        {
               cout << "Operation times: " << opcnt << endl;
        }
};
 
class Point
{
private:
        int x;
        int y;
public:
        Point(const int &xpos, const int &ypos) : x(xpos), y(ypos)
        {  }
        friend Point PointOP::PointAdd(const Point&, const Point&);       // PointOp클래스의 멤버함수 PointAdd에 대한 friend 선언
        friend Point PointOP::PointSub(const Point&, const Point&);       // PointOp클래스의 멤버함수 PointSub에 대한 friend 선언
        friend void ShowPointPos(const Point&);       // 58행에 정의된 함수 ShowPointPos에 대한 friend선언
};
 
Point PointOP::PointAdd(const Point& pnt1, const Point& pnt2)
{
        opcnt++;
        return Point(pnt1.x + pnt2.x, pnt1.y + pnt2.y);      // 30행 friend선언으로 인해 private 멤버 접근 가능
}
 
Point PointOP::PointSub(const Point& pnt1, const Point& pnt2)
{
        opcnt++;
        return Point(pnt1.x - pnt2.x, pnt1.y - pnt2.y);      // 31행 friend선언으로 인해 private 멤버 접근 가능
}
 
int main(void)
{
        Point pos1(1, 2);
        Point pos2(2, 4);
        PointOP op;
 
        ShowPointPos(op.PointAdd(pos1, pos2));
        ShowPointPos(op.PointSub(pos2, pos1));
        return 0;
}
 
void ShowPointPos(const Point& pos)
{
        cout << "x: " << pos.x << ", ";       // 32행 friend선언으로 인해 private 멤버 접근 가능
        cout << "y: " << pos.y << endl;       // 32행 friend선언으로 인해 private 멤버 접근 가능
}


Output:

1
2
3
x: 3, y: 6
x: 1, y: 2
Operation times: 2




1

friend void ShowPointPos(const Point&);

cs

이는 friend 선언이외에 함수 원형 선언이 포함되어 있다.

 

void ShowPointPos(const Point&);



C++에서의 static


C에서와 같이 전역변수와 지역변수에 내릴수 있는 static 선언의 의미는 C++에서도 통용된다.


전역변수에 선언된 static

: 선언된 파일내에서만 참조를 허용 

분할 컴파일시, 파일간에 접근이 불가능해진다. (개별화 됨)


지역변수에 선언된 static

: 전역변수의 성격을 지니게 됨, 지역변수와 달리 함수를 빠져나가도 소멸되지 않음

처음 1회만 초기화되고, 프로그램 종료시까지 메모리 상주(전역변수의 특성)

선언된 함수내에서만 접근 가능(지역변수의 특성)



static선언된 변수는 전역변수와 같이 따로 초기화하지 않을 시, 0으로 자동 초기화된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

void Counter()
{		
	static int cnt;	
	cout << "Current cnt: " << cnt << endl;
	cnt++;
}

int main()
{
	for (int i = 0; i<10; i++)
	{
		Counter();
	}	
	return 0;
}


Output:

1
2
3
4
5
6
7
8
9
10
Current cnt: 0
Current cnt: 1
Current cnt: 2
Current cnt: 3
Current cnt: 4
Current cnt: 5
Current cnt: 6
Current cnt: 7
Current cnt: 8
Current cnt: 9

 


 static 멤버 변수 (클래스 변수)

: static 변수는 객체 별로 존재하는 변수가 아니라, 프로그램 전체 영역에서 하나만 존재하는 변수로 

프로그램 실행과 동시에 초기화되어 메모리 공간에 할당된다.



아래 예제의 14행과 같이, 함수밖에서 static 멤버변수의 초기화를 해야한다.

생성자로는 매번 값의 변경이 일어나기 때문에, 별도의 초기화 문법이 제공된 것이다.


#include <iostream>
using namespace std;

class SoSimple
{
public:
	static int simObjCnt;
public:
	SoSimple()
	{
		simObjCnt++;
	}
};
int SoSimple::simObjCnt = 0;

int main(void)
{
	cout << SoSimple::simObjCnt << "번째 SoSimple 객체" << endl;
	SoSimple sim1;
	SoSimple sim2;

	cout << SoSimple::simObjCnt << "번째 SoSimple 객체" << endl;
	cout << sim1.simObjCnt << "번째 SoSimple 객체" << endl;
	cout << sim2.simObjCnt << "번째 SoSimple 객체" << endl;
	return 0;
}

Output:
1
2
3
4
0번째 SoSimple 객체
2번째 SoSimple 객체
2번째 SoSimple 객체
2번째 SoSimple 객체

 

위 예제에서 23,24행과 같이 객체의 이름을 이용해 접근하는 것은 좋지 않다.

멤버 변수에 접근하는 건지 static 변수에 접근하는 건지 혼동될 수 있어 잘 쓰지 않는다.


따라서 22행과 같이 class를 지정해 접근하는 것이 일반적이고 좋은 방법이다. 


또한 6행과 같이 public 선언되어있지않다면 class 외부에서는 static 멤버 변수에 접근이 불가능하다.



static 멤버 함수


- 특징 (static 멤버 변수의 특징과 일치)

  • 선언된 클래스의 모든 객체가 공유한다.
  • public 선언되면, 클래스를 지정해 호출 가능
  • 객체의 멤버로 존재하는 것이 아니다!

위의 그림을 다시 상기해보면 static 변수와 static 함수는 객체의 생성과 아무런 연관이 없으며, 

객체 내부에 존재하는 변수가 아님을 알 수 있다.


객체와 동시에 생성되는 것이 아닌 전역 변수와 같이 앞서 메모리 공간에 할당된 상태이다.


아래 예제에서 14행의 주석을 해제하면 

"비정적 멤버 'SoSimple::num1'에 대한 참조가 잘못되었습니다. " 라는 에러 메시지가 뜨게 된다.


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
#include <iostream>
using namespace std;
 
class SoSimple
{
private:
    int num1;
    static int num2;
public:
    SoSimple(int n) : num1(n)
    { }
    static void Adder(int n)
    {
        // num1+=n;
        num2 += n;
    }
    void ShowData() const
    {
        cout << "num: " << num1 << num2 << endl;
    }
};
int SoSimple::num2 = 0;
 
int main(void)
{    
    return 0;
}
cs

static 함수는 객체내에 존재하는 함수가 아니기 때문에, 멤버 변수나 멤버함수에 접근이 불가능하다.

당연히 static 변수에만 접근 가능하고, static 함수만 호출 가능하다.



const static 멤버


상수 정보를 담기위한 클래스 CountryArea


const static 멤버변수는, 클래스가 정의될때 지정된 값이 유지되는 상수이다. 따라서 이 경우 아래와 같이 선언과 동시에 초기화가 가능하다.


#include <iostream>
using namespace std;

class CountryArea
{
public:
	const static int RUSSIA			=1707540;
	const static int CANADA			=998467;
	const static int CHINA			=957290;
	const static int SOUTH_KOREA	        =9922;
};

int main(void)
{
	cout<<"러시아 면적: "<<CountryArea::RUSSIA<<"㎢"<<endl;
	cout<<"캐나다 면적: "<<CountryArea::CANADA<<"㎢"<<endl;
	cout<<"중국 면적: "<<CountryArea::CHINA<<"㎢"<<endl;
	cout<<"한국 면적: "<<CountryArea::SOUTH_KOREA<<"㎢"<<endl;
	return 0;
}


Output:

1
2
3
4
러시아 면적: 1707540㎢
캐나다 면적: 998467㎢
중국 면적: 957290㎢
한국 면적: 9922㎢



mutable 선언

: mutable로 선언된 멤버 변수는 const 함수내에서도 값의 변경이 가능


#include <iostream>
using namespace std;

class SoSimple
{
private:
	int num1;
	mutable int num2;
public:
	SoSimple(int n1, int n2)
		: num1(n1), num2(n2)
	{  }
	void ShowSimpleData() const
	{
		cout<<num1<<", "<<num2<<endl;
	}
	void CopyToNum2() const
	{
		num2=num1;
	}
};

int main(void)
{
	SoSimple sm(1, 2);
	sm.ShowSimpleData();
	sm.CopyToNum2();
	sm.ShowSimpleData();
	return 0;
}


Output:

1
2
1, 2
1, 1

mutable의 선언은 신중해야 한다!
: 이와 같은 선언은 const 선언에 반하기 때문에, 타당한 이유를 가지고 가급적 제한적으로 선언하는 것이 좋다.


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

상속의 이해  (0) 2016.10.13
상속  (0) 2016.09.30
복사 생성자, 얕은 복사와 깊은 복사, 임시 객체  (0) 2016.09.28
객체 배열과 this포인터  (0) 2016.09.27
생성자와 소멸자, 클래스  (0) 2016.09.26
728x90

C++ 스타일의 초기화

int num(20); // num=20;

int &ref(num); // &ref=num;


SoSimple sim2=sim1;

바로 위 문장은,

다음 문장과도 동일하게 해석된다.

SoSimple sim2(sim1);


객체 단위의 대입은 멤버 대 멤버의 복사가 일어난다. 이는 복사 생성자의 정의에 따라 달라질 수 있다.


 복사 생성자가 정의되지 않은 SoSimple 클래스
1
2
3
4
5
6
7
8
9
class SoSimple
{
private:
    int num;
public:
    SoSimple(int n) : num(n)
    {  }
    . . . .
};
cs

복사 생성자를 정의하지 않으면, 멤버 대 멤버 복사가 이뤄지는
default 복사 생성자가 아래와 같이 자동 삽입된다. (8-9행)
1
2
3
4
5
6
7
8
9
10
class SoSimple
{
private:
    int num;
public:
    SoSimple(int n) : num(n)
    {  }
    SoSimple(const SoSimple& copy) : num(copy.num)
    {  }
};
cs

복사 생성자를 활용한, 객체 단위의 대입 예제
#include <iostream>
using namespace std;

class SoSimple
{
private:
	int num1;
	int num2;
public:
	SoSimple(int n1, int n2)
		: num1(n1), num2(n2)
	{
		// empty
	}

	SoSimple(const SoSimple &copy)	// 복사 생성자의 정의
		: num1(copy.num2), num2(copy.num1)	// num1, num2값을 swap하도록 이니셜라이저
	{
		cout << "Called SoSimple(SoSimple &copy)" << endl;
	}

	void ShowSimpleData()
	{
		cout << num1 << endl;
		cout << num2 << endl;
	}
};

int main(void)
{
	SoSimple sim1(15, 30);
	sim1.ShowSimpleData();
	cout << "생성 및 초기화 직전" << endl;	
	SoSimple sim2(sim1);	// SoSimple sim2=sim;
	cout << "생성 및 초기화 직후" << endl;
	sim2.ShowSimpleData();
	return 0;
}


Output:

1
2
3
4
5
6
7
15
30
생성 및 초기화 직전
Called SoSimple(SoSimple &copy)
생성 및 초기화 직후
30
15



키워드 explicit

: 묵시적 형 변환을 막는 키워드


SoSimple sim2=sim1;

위의 문장은, 

아래와 같이 묵시적 형 변환이 일어난다.

SoSimple sim2(sim1); 


위와 같은 묵시적 형 변환은 복사 생성자를 explicit으로 선언하면 막을 수 있다.

SoSimple sim2=sim1; 과 같은 형태의 객체 생성과 초기화를 막아줌


1
2
3
explicit SoSimple(const SoSimple &copy)
    : num1(copy.num1), num2(copy.num2)
{ }
cs


이는 복사 생성자 뿐만 아닌 생성자도 마찬가지이다.

1
2
3
4
5
6
7
8
class AAA
{
private:
    int num;
public:
    explicit AAA(int n) : num(n) { }
    . . . .
};
cs

AAA 생성자를 explicit 선언하면

다음과 같은 형태의 객체 생성은 불가능해진다.


AAA obj=3;

이는 아래와 같이 묵시적 형 변환이 이뤄지기 때문이다.

AAA obj(3);


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
#include <iostream>
using namespace std;
 
class SoSimple
{
private:
    int num1;
public:
    explicit SoSimple(int n1) // 생성자를 explicit 선언
        : num1(n1)
    {
        // empty
    }
    void ShowSimpleData()
    {
        cout << "num1 : " << num1 << endl;
    }
};
 
int main(void)
{
    SoSimple sim1(15);
    //SoSimple sim1=15;
    sim1.ShowSimpleData();    
    return 0;
}
cs

위의 소스에서 22행을 주석처리 후 23행의 주석 부분을 해제해  컴파일해보면, SoSimple로 변환하기 위한 적절한 생성자가 없으며,

초기화 중': 'int'에서 'SoSimple'(으)로 변환할 수 없다는 오류를 보게 된다.


c++의 대입의 의미가 다른 부분인데, 묵시적 호출을 원하지 않는다면 이렇게 explicit 키워드를 활용할 수 있다.




얕은 복사 (default 복사 생성자의 문제점)

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

class Person
{
	char * name;
	int age;
public:
	Person(char * myname, int myage)	// 생성자 정의
	{
		int len = strlen(myname) + 1;
		name = new char[len];	// new 키워드를 통한 동적 할당
		strcpy(name, myname);
		age = myage;
	}
	void ShowPersonInfo() const
	{
		cout << "이름: " << name << endl;
		cout << "나이: " << age << endl;
	}
	~Person()
	{
		delete[]name;	// delete 키워드를 이용한 메모리 공간 해제
		cout << "called destructor!" << endl;
	}
};

int main(void)
{
	Person man1("Lee dong woo", 29);
	Person man2 = man1;		// default 복사 생성자에 의한 멤버대 멤버 복사가 이뤄짐
	man1.ShowPersonInfo();	
	man2.ShowPersonInfo();
	return 0;
}


Output :

이름: Lee dong woo

나이: 29

이름: Lee dong woo

나이: 29

called destructor!


위 예제 실행시 에러가 나며, 실행결과를 보면 문제점을 볼 수 있다.

25행에는 소멸자의 호출할 확인할 문장을 출력하도록 한 것을 볼 수 있는데, 실행결과에서는 소멸자가 딱 한번 호출된다.


이는 default 복사 생성자 호출에 의한 당연한 결과이다.


man1 객체의 name은 문자열이 저장된 메모리 공간은 가리키는 형태가 되며, 복사된 man2 객체의 name도 마찬가지로 이 메모리 공간을 가리킬 뿐이다. 

참조만 하는 멤버 name을 단순 복사했기 때문에 하나의 문자열을 두 객체가 가리키게 된 것이다.


또한 man1 객체가 소멸될 때 man1 객체의 멤버 name이 참조하는 문자열이 사라져버린다. ( delete [ ]name; )

이후 man2 객체가 소멸될 때 다음의 문장은 문제가 된다.


 delete [ ]name; 


이미 지워진 문자열을 대상으로 delete 연산을 하려 하는 것이다. 따라서 복사 생성자를 새롭게 정의해 이러한 문제를 해결 할 필요가 있다.



'깊은 복사'를 위한 복사 생성자의 정의

동일한 문자열을 포인터 참조하는 것이 아닌 객체의 문자열 자체를 복사해 참조하도록 해야한다.

따라서 이 경우 인자는 &ref 가 되게 된다.

1
2
3
4
5
6
7
Person(const Person &ref)     
        : age(ref.age)  // 이니셜라이저를 통한 초기화  
    {
        int len = strlen(ref.name) + 1;
        name = new char[len];    // new 키워드를 통한 동적 할당
        strcpy(name, ref.name);        
    }
cs


아래에서 깊은 복사를 위한 복사 생성자를 정의한 실행결과를 확인해볼 수 있다.

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

class Person
{
	char * name;
	int age;
public:
	Person(char * myname, int myage)	// 생성자 정의
	{
		int len = strlen(myname) + 1;
		name = new char[len];	// new 키워드를 통한 동적 할당
		strcpy(name, myname);
		age = myage;
	}
	Person(const Person &copy)	// 깊은 복사를 위한 복사 생성자의 정의
		: age(copy.age)	
	{
		int len = strlen(copy.name) + 1;
		name = new char[len];	// new 키워드를 통한 동적 할당
		strcpy(name, copy.name);		
	}
	void ShowPersonInfo() const
	{
		cout << "이름: " << name << endl;
		cout << "나이: " << age << endl;
	}
	~Person()
	{
		delete[]name;	// delete 키워드를 이용한 메모리 공간 해제
		cout << "called destructor!" << endl;
	}
};

int main(void)
{
	Person man1("Lee dong woo", 29);
	Person man2 = man1;		// 깊은 복사를 위한 복사 생성자 추가로 깊은 복사가 이뤄짐
	man1.ShowPersonInfo();
	man2.ShowPersonInfo();
	return 0;
}

Output :

이름: Lee Chul Soo

나이: 30

이름: Lee Chul Soo

나이: 30

called destructor!

called destructor!



복사 생성자의 호출 시점


1. 기존에 생성된 객체를 이용해 새로운 객체를 초기화는 경우


Person man1("Lee Chul Soo");

Person man2(man1); // 복사 생성자 호출


이 경우 man1과 man2의 멤버대 멤버 복사가 일어남은 앞의 예제에서 보았다.



2. Call-by-value 방식의 함수 호출 과정에서 객체를 인자로 전달하는 경우


#include <iostream>
using namespace std;

class SoSimple
{
private:
	int num;
public:
	SoSimple(int n) : num(n)
	{  }
	SoSimple(const SoSimple& copy) : num(copy.num)
	{
		cout << "Called SoSimple(const SoSimple& copy)" << endl;
	}
	void ShowData()
	{
		cout << "num: " << num << endl;
	}
};

void SimpleFuncObj(SoSimple ob)
{
	ob.ShowData();
}

int main(void)
{
	SoSimple obj(7);
	cout << "함수호출 전" << endl;
	SimpleFuncObj(obj);
	cout << "함수호출 후" << endl;
	return 0;
}


Output:

1
2
3
4
함수호출 전
Called SoSimple(const SoSimple& copy)
num: 7
함수호출 후


위의 예제 30행에서 아래와 같이 obj 객체가 생성 후 SimpleFuncObj 함수가 호출 될 때, 

obj의 복사 생성자가 호출되고 obj와 ob의 멤버대 멤버 복사가 일어나게 된다.


함수 호출과 함께 복사가 이뤄지는 형태는 다음 그림과 같다.





3. 객체를 반환하되, 참조형으로 반환하지 않는 경우



아래 예제에서는 obj객체 생성후 SimpleFuncObj 함수 호출과정에서 obj 객체를 인자로 전달하고 있다.


또한 함수 호출과정에서 ob 객체가 반환된다.

객체의 반환이란건 객체의 생성과 함께, 함수가 호출된 영역으로 복사해 넘긴다는 뜻과 같다.


객체가 반환이 될 때는 임시 객체 (Temporary Object)가 생성되게 된다.


이러한 임시객체의 특성은 리터럴 상수와도 비슷하다. (int num=3+4; 에서 3과 4)

메모리 공간에 할당이 되지만 금방 사라지게 될 객체이다.


AddNum 함수의 반환형이 참조형(SelfRef &)이므로 객체 자신을 참조할 수 있는 참조자를 반환함에 유의하자. (반환된 참조값을 가지고 함수를 연이어 호출)


#include <iostream>
using namespace std;

class SoSimple
{
private:
	int num;
public:
	SoSimple(int n) : num(n)
	{ }
	SoSimple(const SoSimple& copy) : num(copy.num)
	{
		cout << "Called SoSimple(const SoSimple& copy)" << endl;
	}
	SoSimple& AddNum(int n)
	{
		num += n;
		return *this;
	}
	void ShowData()
	{
		cout << "num: " << num << endl;
	}
};

SoSimple SimpleFuncObj(SoSimple ob)
{
	cout << "return 이전" << endl;
	return ob;
}

int main(void)
{
	SoSimple obj(7);
	SimpleFuncObj(obj).AddNum(30).ShowData();
	obj.ShowData();
	return 0;
}


Output:

1
2
3
4
5
Called SoSimple(const SoSimple& copy)
return 이전
Called SoSimple(const SoSimple& copy)
num: 37
num: 7



반환할 때 만들어진 객체의 소멸시점

#include <iostream>
using namespace std;

class Temporary
{
private:
	int num;
public:
	Temporary(int n) : num(n)
	{
		cout << "create obj: " << num << endl;
	}
	~Temporary()
	{
		cout << "destroy obj: " << num << endl;
	}
	void ShowTempInfo()
	{
		cout << "My num is " << num << endl;
	}
};

int main(void)
{
	Temporary(100);
	cout << "********** after make!" << endl << endl;

	Temporary(200).ShowTempInfo();
	cout << "********** after make!" << endl << endl;

	const Temporary &ref = Temporary(300);	// 참조값이 반환되므로 참조자로 참조 가능하다.
	cout << "********** end of main!" << endl << endl;
	return 0;
}


Output:

1
2
3
4
5
6
7
8
9
10
11
12
13
create obj: 100
destroy obj: 100
********** after make!

create obj: 200
My num is 200
destroy obj: 200
********** after make!

create obj: 300
********** end of main!

destroy obj: 300


위의 예제에서 객체 생성은 하지만 객체의 이름이 없으며, 이는 임시 객체의 생성을 뜻한다.


하지만 참조자를 이용하면 반환된 참조값을 이용해 참조 가능하다.


실행 결과를 보면 참조자에 참조되는 임시객체는 메모리 공간이 바로 소멸되지 않음을 알 수 있다.

return 전에 소멸자의 호출됨을 출력된 문장을 통해 확인이 가능하다.

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

상속  (0) 2016.09.30
const, friend, static, mutable 선언  (0) 2016.09.29
객체 배열과 this포인터  (0) 2016.09.27
생성자와 소멸자, 클래스  (0) 2016.09.26
class  (0) 2016.09.25
728x90

객체 배열

: 객체를 기반으로 객체 배열의 선언이 가능하다.


클래스 이름이 SoSimple일 때, 객체 기반의 배열을 다음과 같이 선언한다.


SoSimple arr[10];


동적 할당

SoSimple * ptrArr = new SoSimple[10];


이러한 형태의 배열 선언은, 열개의 SoSimple 객체가 모여서 배열을 구성하는 형태이다.

이 경우에도 생성자는 호출이 되나, 배열 선언과정에서는 호출할 생성자를 별도로 명시하지 못한다. (생성자에 인자를 전달하지 못함)


위와 같이 객체 기반 배열이 생성되려면 다음 형태의 생성자가 반드시 정의되야 하낟.

So Simple() { . . . .}


#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;
	}
	Person()	// 배열 생성시 필요한 생성자 정의
	{
		name = NULL;
		age = 0;
		cout << "called Person()" << endl;
	}

	void SetPersonInfo(char * myname, int myage)	// 초기화 함수
	{
		name = myname;
		age = myage;
	}
	void ShowPersonInfo() const
	{
		cout << "이름: " << name << ", ";
		cout << "나이: " << age << endl;
	}

	~Person()
	{
		delete[]name;
		cout << "called destructor!" << endl;
	}
};

int main(void)
{
	Person parr[3];		// 객체 기반 배열의 선언

	char namestr[100];
	char * strptr;
	int age;
	int len;

	for (int i = 0; i<3; i++)	// for문을 통해 이름, 나이 입력받아서 객체를 초기화
	{
		cout << "이름: ";
		cin >> namestr;
		cout << "나이: ";
		cin >> age;

		len = strlen(namestr) + 1;
		strptr = new char[len];						
		strcpy(strptr, namestr);
		parr[i].SetPersonInfo(strptr, age);
	}

	parr[0].ShowPersonInfo();
	parr[1].ShowPersonInfo();
	parr[2].ShowPersonInfo();

	return 0;
}


Output :

called Person()

called Person()

called Person()

이름: 가가가가가가

나이: 1

이름: 나나나나나나나나

나이: 2

이름: 다다다다다다다다다

나이: 3

이름: 가가가가가가, 나이: 1

이름: 나나나나나나나나, 나이: 2

이름: 다다다다다다다다다, 나이: 3

called destructor!

called destructor!

called destructor!



객체 포인터 배열

: 객체의 주소 값 저장이 가능한 포인터 변수로 이뤄진 배열


#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;
	}
	Person()
	{
		name = NULL;
		age = 0;
		cout << "called Person()" << endl;
	}
	void SetPersonInfo(char * myname, int myage)
	{
		name = myname;
		age = myage;
	}
	void ShowPersonInfo() const
	{
		cout << "이름: " << name << ", ";
		cout << "나이: " << age << endl;
	}
	~Person()
	{
		delete[]name;
		cout << "" << endl;		// 소멸자 호출과 delete 연산을 확인하기 위함
	}
};

int main(void)
{
	Person * parr[3];	// 객체 포인터 배열 선언

	char namestr[100];
	char * strptr;
	int age;
	int len;

	for (int i = 0; i < 3; i++)
	{
		cout << "이름: ";
		cin >> namestr;
		cout << "나이: ";
		cin >> age;

		parr[i] = new Person(namestr, age);	// 객체를 생성해, 주소값을 배열에 저장

	}
	for (int i = 0; i < 3; i++)
	{
		parr[i]->ShowPersonInfo();
		delete parr[i];		// new 연산을 한만큼 delete 연산 진행
	}

	return 0;
}


Output : 

이름: 가가가가, 나이: 22

이름: 나나나나나, 나이: 33

이름: 다다다다다다다, 나이: 44


이름: 가가가가, 나이: 22


이름: 나나나나나, 나이: 33


이름: 다다다다다다다, 나이: 44



this 포인터의 이해

: 멤버 함수 내에서 객체 자신을 가리키는 용도로 사용되는 포인터


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

class SoSimple
{
	int num;
public:
	SoSimple(int n) : num(n)	// 생성자, 멤버 이니셜라이저 int num=n;
	{
		cout << "num=" << num << ", ";
		cout << "address=" << this << endl;
	}

	void ShowSimpleData()
	{
		cout << num << endl;
	}

	SoSimple * GetThisPointer()
	{
		return this;	
		// this를 반환. 이는 this가 가리키는 객체의 포인터를 반환하라는 의미로, 반환형이 SoSimple * 형 
	}
};

int main(void)
{
	SoSimple sim1(100);
	SoSimple * ptr1 = sim1.GetThisPointer();	// sim1 객체의 주소값을 * ptr1 에 저장	
	cout << ptr1 << ", ";		// ptr1에 저장된 주소값 출력
	ptr1->ShowSimpleData();		// ptr1이 가리키는 객체의 ShowSimpleData 함수 호출

	SoSimple sim2(200);
	SoSimple * ptr2 = sim2.GetThisPointer();
	cout << ptr2 << ", ";
	ptr2->ShowSimpleData();
	return 0;
}


Output:

1
2
3
4
num=100, address=0xffeb3708
0xffeb3708, 100
num=200, address=0xffeb3704
0xffeb3704, 200


this는 객체 자신을 가리키며, 위치한 객체내에서 자료형과 주소 값이 결정된다.

0xffeb3708에 할당된 sim1 객체 내에서는 this는 sim1 형의 포인터이면서 그 주소 값은 0xffeb3708 이 된다.



this 포인터 활용

객체의 주소 값으로 접근가능한 대상은 멤버 변수이다.

따라서 this->num1은 멤버 변수 num1을 의미한다.


위의 생성자는 아래의 같은 형태의 생성자로 대신할 수도 있다.


// this 포인터가 아닌 이니셜라이저를 통한 초기화 예

1
2
3
4
5
6
TwoNumber (int num1, int num2)
    : num1(num1), num2(num2)    
    // 이니셜라이저의 괄호안 num1은 매개변수 num1을 의미
{
    // empty
}
cs


멤버 이니셜라이저에서는 this포인터 사용불가. 



Self-Reference의 반환

: 객체 자신을 참조할 수 있는 참조자


this포인터를 이용해, 객체가 자신의 참조에 사용할 수 있는 참조자의 반환문을 구성 가능


#include <iostream>
using namespace std;

class SelfRef
{
private:
	int num;
public:
	SelfRef(int n) : num(n)
	{
		cout << "객체생성" << endl;
	}
	SelfRef& Adder(int n)
	{
		num += n;
		return *this;	// 이는 객체 자신의 포인터가 아닌 객체 자신을 반환
		// 함수의 반환형이 참조형(SelfRef &)이므로 객체 자신을 참조할 수 있는 참조자 반환
	}
	SelfRef& ShowTwoNumber()
	{
		cout << num << endl;
		return *this;
	}
};

int main(void)
{
	SelfRef obj(3);
	SelfRef &ref = obj.Adder(2);	// 객체 obj의 참조값을 반환, 참조자 ref가 객체 obj의 참조자가 됨

	obj.ShowTwoNumber();
	ref.ShowTwoNumber();

	ref.Adder(1).ShowTwoNumber().Adder(2).ShowTwoNumber();	
	return 0;
}


Output:

1
2
3
4
5
객체생성
5
5
6
8


34행에서  객체 ref의 Adder함수가 호출. Adder함수는 참조값을 반환하므로, 반환된 참조 값을 가지고 ShowTwoNumber 함수 호출이 가능하다.

마찬가지로 ShowTwoNumber가 반환하는 참조 값을 이용해 다시금 함수를 이어서 호출 할 수 있게 된다.


이는 두 함수 Adder, ShowTwoNumber가 객체의 참조 값을 반환하기 때문에 가능하다.



728x90

생성자 (constructor)

: 클래스의 이름과 동일한 이름의 함수이면서,

반환형이 선언되지 않았고 실제 반환하지 않는 함수가 생성자


생성자는 객체 생성시 멤버변수의 초기화에 사용가능 

(객체 생성시 생성자가 딱 한번 호출)



- 위에서 생성자가 정의되었으니 객체 생성과정에서 자동호출되는 생성자에게 전달할 인자의 정보를 추가


SimpleClass sc(20) // 생성자에 20 전달 (stack 할당)

SimpleClass * ptr = new SimpleClass(30); // (heap에 동적 할당)



생성자도 오버로딩과 매개변수의 default값 설정이 가능하다.


생성자의 오버로딩

#include <iostream>
using namespace std;

class SimpleClass
{
	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;
	}

	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;
}


Output:

1
2
3
0 0
100 0
100 200



매개변수의 default 값 설정

#include <iostream>
using namespace std;

class SimpleClass
{
	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)
{
	SimpleClass sc1;
	sc1.ShowData();

	SimpleClass sc2(100);
	sc2.ShowData();

	SimpleClass sc3(100, 200);
	sc3.ShowData();
	return 0;
}


Output:

1
2
3
0 0
100 0
100 200



클래스에 생성자가 하나도 정의되어 있지 않아도, 객체 생성시 생성자가 호출

-> 컴파일러에 의해 다음과 같은 default 생성자가 자동 삽입 

SimpleClass() { }


이는 new 연산자를 이용하는 객체의 생성에도 해당된다.


단, 다음과 같이 new연산자 대신 malloc 함수를 이용하면 생성자는 호출되지 않는다.

AAA * ptr = (AAA*)malloc(sizeof(AAA));    // AAA 클래스의 크기 정보만 바이트 단위로 전달



객체의 생성 예들

SimpleClass * ptr1=new SimpleClass();    (o)


SimpleClass * ptr1=new SimpleClass;    (o)


SimpleClass sc1;   (o)


SimpleClass sc1(); (x)

-> 이러한 형태의 객체 생성은 불가

컴파일러가 함수의 원형 선언인지 객체 생성인지 판단 불가

(sc1 함수이며, 인자가 void형이고 반환형이 SimpleClass인 함수라고 볼 수도 있다.)


다음예제와 같이 SimpleClass sc1()같은 유형의 문장은 함수의 원형선언으로 볼 수도 있으므로, 

이러한 유형의 문장은 객체 생성이 아닌 함수의 원형선언에만 사용가능하다.


#include <iostream>
using namespace std;

class SimpleClass
{
	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)
{
	SimpleClass sc1();    // 함수의 원형 선언!
	SimpleClass mysc=sc1(); // sc1 함수를 호출해, 이 때 반환되는 객체의 값으로 mysc 객체를 초기화
	mysc.ShowData();

	return 0;
}

SimpleClass sc1()
{
	SimpleClass sc(20, 30);
	return sc;
}


Output:

1
20 30



Member Initializer(멤버 이니셜라이저)를 이용한 멤버 초기화



위에서 Point 클래스에 생성자를 적용하고 객체를 생성하는 것은 무리가 없다.

하지만 Rectangle 클래스의 생성자 정의는 큰 문제가 있다. Rectangle 클래스는 두 개의 Point 객체를 멤버로 지니고 있기 때문이다. (upLeft, lowRight)


위의 클래스에서 생성자가 삽입되었으므로 default 생성자는 호출되지 않는다.


그렇다고 하여 void 생성자를 별도로 추가한다면? 생성자도 오버로딩이 되기 때문에 객체는 생성되겠지만 유효한 값을 전달받지 못하게 된다. (쓰레기 값)

따라서 Rectangle 객체가 생성될 때, 두 개의 Point 객체가 생성되며, 각 인자가 유효한 값을 전달받을 수 있는 생성자를 호출 할 수 있어야 한다.


이 문제 해결을 위해 '멤버 이니셜라이저'를 이용하면, 객체의 초기화를 수월하게 해줄 생성자를 호출할 수 있게 된다.


// 생성자가 추가된 Rectangle 클래스의 선언

1
2
3
4
5
6
7
8
9
class Rectangle
{
    Point upLeft;
    Point lowRight;
 
public:
    Rectangle(const int &x1, const int &y1, const int &x2, const int &y2);
    void ShowRecInfo() const;
};
cs


// Rectangle 클래스의 생성자 정의

1
2
3
4
5
Rectangle::Rectangle(const int &x1, const int &y1, const int &x2, const int &y2)
            :upLeft(x1, y1), lowRight(x2, y2)
{
    // empty
}
cs


다음의 문장이 '멤버 이니셜라이저'를 뜻하며, 이것을 통해 수월하게 멤버 초기화가 가능하다.

:upLeft(x1, y1), lowRight(x2, y2)


"객체 upLeft의 생성과정에서 x1, y1을 인자로 전달받는 생성자 호출,

객체 lowRight의 생성과정에서 x2, y2을 인자로 전달받는 생성자 호출"



범위지정 연산자 ::

: namespace외에도 명시적(explicitly)으로 클래스(구조체)멤버나 전역변수 지정



다음은 Point, Rectangle 클래스에 생성자를 추가한 예제이다.


 Point.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#ifndef __POINT_H_
#define __POINT_H_
 
class Point
{
    int x; 
    int y;    
public:
    Point(const int &xpos, const int &ypos);
    int GetX() const;
    int GetY() const;
    bool SetX(int xpos);
    bool SetY(int ypos);
};
 
#endif
cs


 Point.cpp

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
32
33
34
35
#include <iostream>
#include "Point.h"
using namespace std;
 
Point::Point(const int &xpos, const int &ypos)
{
    x=xpos;
    y=ypos;
}
 
int Point::GetX() 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;
}
cs


 Rectangle.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#ifndef __RECTANGLE_H_
#define __RECTANGLE_H_
 
#include "Point.h"
 
class Rectangle
{
    Point upLeft;
    Point lowRight;
 
public:
    Rectangle(const int &x1, const int &y1, const int &x2, const int &y2);
    void ShowRecInfo() const;
};
 
#endif
 
cs


 Rectangle.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include "Rectangle.h"
using namespace std;
 
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;
}
cs


 RectangleConstructor.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream>
#include "Point.h"
#include "Rectangle.h"
using namespace std;
 
int main(void)
{
 
    Rectangle rec(1155);
    rec.ShowRecInfo();
    return 0;
}
 
cs


Output : 

좌 상단: [1, 1]

우 하단: [5, 5]



멤버 이니셜라이저를 이용한 변수 및 const 상수(변수) 초기화
const는 변수를 상수화 하는 키워드이다. const 선언에 의해 상수화된 변수를 가리켜  'const 변수'라고도 하고 'const 상수'라고도 한다. 즉, 같은 의미


 객체가 아닌 멤버 변수의 초기화

: 이렇게 초기화 하는 경우 멤버 변수가 선언과 동시에 초기화


1
2
3
4
5
6
7
8
9
10
11
class SoSimple
{
private:
    int num1;
    int num2;
public :
    SoSimple(int n1, int n2) : num1(n1)
    {
        num2=2;
    }
};
cs

위의 클래스에서 생성자의 몸체에서 보인 초기화,

num2=2;


는 다음의 두 문장과 같다.

int num2;

num2=n2;


하지만 이니셜라이저를 이용한 다음의 초기화,

num1(n1)


는 다음의 문장과 같다.

int num1=n1;




따라서 이러한 특성을 이용해 const 멤버변수도 이니셜라이저를 이용해 초기화 가능하다.

#include <iostream>
using namespace std;

class FruitSeller
{
	const int APPLE_PRICE;
	int numOfApples;
	int myMoney;
public:
	FruitSeller(int price, int num, int money)
		: APPLE_PRICE(price), numOfApples(num), myMoney(money)
	{
	}
	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;
	}
};

class FruitBuyer
{
	int myMoney;
	int numOfApples;
public:
	FruitBuyer(int money)
		: myMoney(money), numOfApples(0)
	{
	}
	void BuyApples(FruitSeller &seller, int money)
	{
		numOfApples+=seller.SaleApples(money);
		myMoney-=money;
	}
	void ShowBuyResult() const
	{
		cout<<"현재 잔액: "<<myMoney<<endl;
		cout<<"사과 개수: "<<numOfApples<<endl<<endl;
	}
};

int main(void)
{
	FruitSeller seller(1000, 20, 0);
	FruitBuyer buyer(5000);
	buyer.BuyApples(seller, 2000);
	
	cout<<"과일 판매자의 현황"<<endl;
	seller.ShowSalesResult();
	cout<<"과일 구매자의 현황"<<endl;
	buyer.ShowBuyResult();

	return 0;
}

Output:
1
2
3
4
5
6
7
8
과일 판매자의 현황
남은 사과: 18
판매 수익: 2000

과일 구매자의 현황
현재 잔액: 3000
사과 개수: 2


이렇게 생성자의 몸체에서 초기화 하는 방법 대신에 이니셜라이저를 이용한 변수 및 const 상수의 초기화 방법은 이 점이 있다.

const 멤버 변수를 이니셜라이저를 이용한 초기화가 가능한 거 외에도 다음의 두 가지 이점이 있다.


- 초기화 대상을 명확히 인식가능

- 성능에 약간의 이점



멤버 변수로 참조자 선언하기 (이니셜라이저를 이용)

이니셜라이저가 선언과 동시에 초기화 되는 특성을 이용해, 참조자의 초기화도 가능

1
2
3
4
5
6
7
8
9
10
11
class BBB
{
private:
    AAA &ref;
    const int &num;
 
public:
    BBB(AAA &r, const int &n) // 생성자
        :ref(r), num(n) // 멤버 이니셜라이저
    {    // 빈 생성자 몸체
    }
cs


private 생성자

: 클래스 내부에서만 객체의 생성을 허용


다음 예제에서 클래스 외부에서는 9행의 public 생성자를 기반으로 객체를 생성한다.


10행에서는 19행에 정의된 private 생성자 기반으로 AAA 객체를 생성 및 반환하고 있다.

이 떄, heap 영역에 생성된 객체를 참조의 형태로 반환하고 있다.


#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) {}
};

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;
}


Output:

1
2
3
0
3
12



소멸자 (Destructor)

: 객체 소멸시 호출되며 다음과 같은 형태를 갖는다.


클래스의 이름앞에 ~ 가 붙음

반환형이 선언되어 있지않고, 실제 반환도 없다.


default 생성자와 마찬가지로 소멸자를 정의하지 않았다면 아무런 일을 하지 않는 default 소멸자가 자동 삽입된다.


1
2
3
4
class AAA
{    
// empty class
};
cs

이는 다음의 클래스 정의와 100% 동일하다.


1
2
3
4
5
6
class AAA
{
public:
AAA() { }
~AAA() { }
};
cs


이러한 소멸자는 대게 생성자에서 할당한 리소스의 소멸에 사용된다.

다음 예제에서 new키워드를 이용해 동적할당한 메모리 공간을 소멸시키는 소멸자를 볼 수 있다.


13-14행 : 필요한 만큼 메모리 공간을 사용하기 위해 문자열의 길이만큼 메모리 공간을 동적 할당

23행 : 소멸자를 통해 할당된 메모리 공간을 소멸 시킴. delete 키워드를 이용하며 소멸되었음을 확인할 수 있는 출력문장이 삽입


#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;
}



Output :

이름: Lee dong woo

나이: 29

이름: Jang dong gun

나이: 41

called destructor!

called destructor!




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

복사 생성자, 얕은 복사와 깊은 복사, 임시 객체  (0) 2016.09.28
객체 배열과 this포인터  (0) 2016.09.27
class  (0) 2016.09.25
CPP-Object Oriented Programming (객체지향 프로그래밍)  (0) 2016.09.23
c++ Class  (0) 2016.09.22
728x90

정보 은닉 (Information)

클래스의 멤버변수를 private로 선언, 해당 변수에 접근하는 함수를 별도로 정의해, 

 안전한 형태로 멤버 변수의 접근을 유도한다.


Point 클래스의 정보은닉

1
2
3
4
5
6
7
8
9
10
11
12
13
class Point
{
private:
    int x; 
    int y;    
 
public:
    bool InitMembers(int xpos, int ypos);
    int GetX() const;
    int GetY() const;
    bool SetX(int xpos);
    bool SetY(int ypos);
};
cs

8 행 : 벗어난 범위의 값 저장을 막는 초기화 함수

9-12 행 : 정보 은닉으로 인해 추가되는 접근(access) 함수들 


get은 값을 반환, set은 값을 저장하는 함수로 엑세스 함수를 추가하는 것이 일반적


1
2
3
4
5
6
7
8
9
10
11
bool Point::SetX(int xpos)
{
    if(0>xpos || xpos>100)
    {
        cout<<"벗어난 범위의 값 전달"<<endl;
        return false;
    }
 
    x=xpos;
    return true;
}    
cs



Rectangle 클래스의 정보 은닉

1
2
3
4
5
6
7
8
9
10
class Rectangle
{
private:
    Point upLeft;
    Point lowRight;
 
public:
    bool InitMembers(const Point &ul, const Point &lr);
    void ShowRecInfo() const;
};
cs



1
2
3
4
5
6
7
8
9
10
11
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;
}
cs

3-7 행 : 좌 상단과 우 하단이 바뀌는 것을 막는다. (우 하단의 좌표값이 더 작은것은 말이 안되므로)



const 함수


멤버 함수의 const 선언

: 동일 클래스에 선언된 멤버 변수의 값을 변경하지 못하도록 선언

1
2
3
4
// const함수내에서는 동일 클래스에 선언된 멤버 변수의 값을 변경하지 못함
int GetX() const;
int GetY() const;
void ShowRecInfo() const;
cs



다음과 같은 경우는 주의!

const 함수내에서는 const 선언되지 않은 함수를 호출하지 못함


다음과 같은 함수 호출은 컴파일 에러를 일으킨다. 

GetNum함수가 값을 변경할 수 있는 가능성이 내포되어 있기 때문이며 이를 해결하려면 GetNum함수에 const선언을 추가해야 한다.

1
2
3
4
5
6
7
8
9
// 멤버 함수 GetNum, ShowNum
int GetNum()
{
    return num;
}
void ShowNum() const
{
    cout<<GetNum()<<endl;    // 컴파일 에러
}
cs

const로 상수화된 객체를 대상으로도 const 멤버함수만 호출 가능. 역시 이를 해결하려면 GetNum함수에 const선언을 추가해야 함. 
1
2
3
4
5
// GetNum이 const 선언되지 않았다고 가정
void InitNum(const EasyClass &easy)
{
    num=easy.GetNum();    // 컴파일 에러 
}
cs



캡슐화 (Encapsulation)
: 관련있는 모든 것들을 하나의 클래스 안에 묶는 것

아래의 class들은 감기알약에 대해 나름의 캡슐화를 한 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SinivelCap    // 콧물 처치용 캡슐
{
public
    void Take() const {cout<<"콧물이 싹~ 납니다."<<endl;}
};
 
class SneezeCap    // 재채기 처치용 캡슐
{
public
    void Take() const {cout<<"재채기가 멎습니다."<<endl;}
};
 
class SnuffleCap   // 코막힘 처치용 캡슐
{
public
    void Take() const {cout<<"코가 뻥 뚫립니다."<<endl;}
};
 
cs

하지만 다음과 같은 조건이라면 이는 캡슐화가 제대로 되지 않은 것이다.
 1. 코 감기가 하나의 증상이 아닌 여러 증상을 동반한다.
 2. 복용에 대한 순서나 방법이 정해져있다.

따라서 이 경우 제대로 된 캡슐화가 필요하며, 다음과 같이 관련 클래스를 하나로 묶을 필요성이 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CONTAC600 
{
private:
    SinivelCap sin;
    SneezeCap sne;
    SnuffleCap snu;
 
public:
    void Take() const
    {
        sin.Take();
        sne.Take();
        snu.Take();
    }
};
cs

4-6행 : 코 감기와 관련된 캡슐을 하나로 묶음
9행 : 약의 복용 순서를 정의


1
2
3
4
5
class ColdPatient
{
public:
    void TakeCONTAC600(const CONTAC600 &cap) const { cap.Take(); }
};
cs

위의 ColdPatient 클래스는 CONTAC600 클래스의 내용이 수정 및 추가되라도 바뀔 필요가 없다.


 캡슐화 관련 예제 

#include <iostream>
using namespace std;

class SinivelCap    // 콧물 처치용 캡슐
{
public:
	void Take() const { cout << "콧물이 싹~ 납니다." << endl; }
};

class SneezeCap    // 재채기 처치용 캡슐
{
public:
	void Take() const { cout << "재채기가 멎습니다." << endl; }
};

class SnuffleCap   // 코막힘 처치용 캡슐
{
public:
	void Take() const { cout << "코가 뻥 뚫립니다." << endl; }
};

class CONTAC600
{
private:
	SinivelCap sin;
	SneezeCap sne;
	SnuffleCap snu;

public:
	void Take() const
	{
		sin.Take();
		sne.Take();
		snu.Take();
	}
};

class ColdPatient
{
public:
	void TakeCONTAC600(const CONTAC600 &cap) const
	{
		cap.Take();
	}
};

int main(void)
{
	CONTAC600 cap;
	ColdPatient sufferer;
	sufferer.TakeCONTAC600(cap);
	return 0;
}


Output:

1
2
3
콧물이 싹~ 납니다.
재채기가 멎습니다.
코가 뻥 뚫립니다.


해당 함수가 속한 객체의 멤버를 변경할 수 없도록 const 함수



캡슐화의 이점

: A 클래스가 잘 캡슐화 되었다면, 이 A클래스가 변경되더라도,

A와 연관된 다른 클래스는 변경될 필요가 없거나 변경 범위가 최소화된다는 것이다.





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

객체 배열과 this포인터  (0) 2016.09.27
생성자와 소멸자, 클래스  (0) 2016.09.26
CPP-Object Oriented Programming (객체지향 프로그래밍)  (0) 2016.09.23
c++ Class  (0) 2016.09.22
c++ 기초3  (0) 2016.09.21
728x90

Object Oriented Programming (객체지향 프로그래밍)

 

C++은 객체지향 언어

C언어가 절차지향적 언어라면 C++은 객체지향 언어

 

"나는 과일장수에게 두 개의 사과를 구매했다!"

위 문장에서 object(객체)는 나, 과일장수, 사과

 

OOP는 A.P.I.E

A(Abstraction, 추상화)

P(Polymorphism, 다형성)

I(Inheritance, 상속성)

E(Encapsulation, 은닉성 = 캡슐화)

 

- 설계이전에 분석이 필요

(요구사항)분석-설계-구현

 

1.분석 단계 = 객체 모델링

-객체 구성요소(속성), 동작(기능) 추출

 

현실에서의 요소 = 객체

객체를 프로그래밍으로 옮긴 것 = 인스턴스

: 혼용해도 딱히 틀리진 않다

 

 

객체를 이루는 것은 데이터와 기능

프로그램상에서 바라보는 과일장수

- 과일장수는 과일을 판다                            (행동 behavior)

- 과일장수는 사과 20개, 오렌지 10개를 보유    (상태 state)

- 과일장수의 판매 수익은 현재 50,00원           (상태 state)

 

객체는 하나 이상의 상태정보(데이터) + 하나 이상의 행동(기능)으로 구성

 

상태 정보 : 변수를 통해서 표현(변수에 상태 정보 저장)

행동 : 함수를 통해서 표현

 

 

과일 장수의 상태 정보 (변수로 표현하며, 사과를 판다고 가정)

 - 보유 사과 수    -> int numOfApples;

 - 판매 수익        -> int myMoney;

 

과일 장수의 행위 (판매를 함수로 표현)

 

1
2
3
4
5
6
7
int SaleApples(int money)      // 함수의 인자는 사과 구매 금액
{
    int num = money / 1000;    // 사과는 개당 1000원
    numOfApples -= num;        //  사과수 감소  
    myMoney += money;          // 판매 수익 발생
    return num;                // 판매된 사과의 수를 반환
}
cs
 
 
class는 객체를 생성하기 위한 틀
 
변수 초기화 함수와 과일장수의 판매수익, 남는 사과 개수를 보여주는 함수도 정의
 
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
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;
    }
    int SaleApples(int money)  
    {
        int num=money/APPLE_PRICE;
        numOfApples-=num;
        myMoney+=money;
        return num;
    }
    void ShowSalesResult()
    {
        cout<<"남은 사과: "<<numOfApples<<endl;
        cout<<"판매 수익: "<<myMoney<<endl<<endl;
    }
};
cs
위에서 사과의 판매 가격을 의미하는 멤버변수 APPLE_PRICE를 상수화 하고 싶지만 다음과 같은 이유로 '현재 공부한 내용에서는 불가능'하다.
 
 클래스의 멤버변수 선언문에서 초기화까지 하는 것은 허용하지 않는다. const int APPLE_PRICE=1000;    (x)
 상수는 선언과 동시에 초기화 해야한다. const int APPLE_PRICE;    (x)
 
이 문제는 후에 생성자(Constructor)를 공부하게 되면 해결할 수 있게 된다.
 
 
 
구매자인 나(me)를 표현하는 클래스 정의
과일 구매자를 뜻하는 FruitBuyer 클래스
 
멤버 변수
 소유한 현금 -> int myMoney;
 소유한 사과 -> int numOfApples;
 
 
과일구매자의 기능
: 과일의 구매 (함수 BuyApple)
 
이를 기반으로 정의한 FruitBuyer 클래스
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class FruitBuyer
{
    int myMoney;        // private:
    int numOfApples;    // private:
 
public:
    void InitMembers(int money)
    {
        myMoney = money;
        numOfApples = 0;
    }
    void BuyApples(FruitSeller &seller, int money)
    {
        numOfApples += seller.SaleApples(money);
        myMoney -= money;
    }
    void ShowBuyResult()
    {
        cout << "현재 잔액: " << myMoney << endl;
        cout << "사과 개수: " << numOfApples << endl << endl;
    }
};
cs
 
3,4행에서 클래스에 아무런 선언이 존재하지 않을 때 private로 간주 (구조체는 public)
 
 
클래스 기반의 두 가지 객체 생성 방법  (클래스의 실체화 : 객체화)
앞서말했듯이 클래스는 객체를 생성하기 위한 틀에 불가하다.
 
그러니 객체를 생성하지 않고 두 클래스 안에 존재하는 변수에 접근하고, 함수를 호출하는 것은 불가능하다. 
 
C++에서 객체를 생성하는 두 가지 방법은 다음과 같다.
 
ClassName objName;                // 일반적 변수 선언방식
ClassName * ptrobj = new ClassName; // 동적 할당방식 (heap 할당)
 
 
일반적 변수 선언방식으로 FruitSeller 클래스와 FruitBuyer 클래스 객체 생성
 FruitSeller seller;
 FruitBuyer buyer;
 
이를 동적 할당 (heap 할당)
 FruitSeller * objPtr1 = new FruitSeller;
 FruitBuyer * objPtr2 = new FruitBuyer;
 
 
 
Message Passing (객체간의 메시지 전달)
 
 "과일장수(seller) 아저씨, 사과 2000원어치 주세요"
-> '나'라는 객체가 '과일장수'라는 객체로부터 '과일'객체를 구매하는 행위의 표현
(이 행위의 결과는 아래 예제에서 보듯이 사과의 갯수가 증가하고 보유 현금이 줄어드는 것이 됨)
 
이는 하나의 객체가 다른 하나의 객체에게 메시지를 전달하는 것과 같다.
과일장수(seller) 아저씨, 사과 2000원어치 주세요
이처럼 하나의 객체가 다른 하나의 객체에게 메시지를 전달하는 것은 함수 호출을 기반으로 하며,
이러한 형태의 함수호출을 'Message Passing (객체간의 메시지 전달)' 이라고 한다.
 
사과 장수 시뮬레이션 예제
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 
#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; } int SaleApples(int money) { int num = money / APPLE_PRICE; numOfApples -= num; myMoney += money; return num; } void ShowSalesResult() { cout << "남은 사과: " << numOfApples << endl; cout << "판매 수익: " << myMoney << endl << endl; } }; class FruitBuyer { int myMoney; // private: int numOfApples; // private: public: void InitMembers(int money) { myMoney = money; numOfApples = 0; } void BuyApples(FruitSeller &seller, int money) { numOfApples += seller.SaleApples(money); myMoney -= money; } void ShowBuyResult() { cout << "현재 잔액: " << myMoney << endl; cout << "사과 개수: " << numOfApples << endl << endl; } }; int main(void) { FruitSeller seller; seller.InitMembers(1000, 20, 0); FruitBuyer buyer; buyer.InitMembers(5000); buyer.BuyApples(seller, 2000); cout << "과일 판매자의 현황" << endl; seller.ShowSalesResult(); cout << "과일 구매자의 현황" << endl; buyer.ShowBuyResult(); return 0; } 


Output:
1 2 3 4 5 6 7 8 
과일 판매자의 현황 남은 사과: 18 판매 수익: 2000 과일 구매자의 현황 현재 잔액: 3000 사과 개수: 2
 
 
 
 
 
 
 
 
 
 
 
 
 

 

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

생성자와 소멸자, 클래스  (0) 2016.09.26
class  (0) 2016.09.25
c++ Class  (0) 2016.09.22
c++ 기초3  (0) 2016.09.21
c++ 기초  (0) 2016.09.19
728x90

C++에서의 구조체


c++에서는 구조체 변수 선언시,

struct 키워드의 생략을 위한 typedef 선언 불필요


c의 구조체 변수 초기화         c++에서 구조체 변수 초기화

struct Car basicCar;      Car basicCar


#include <iostream>
using namespace std;

#define ID_LEN  20
#define MAX_SPD  200
#define FUEL_STEP 2
#define ACC_STEP 10
#define BRK_STEP 10

struct Car
{
	char gamerID[ID_LEN]; // 소유자ID
	int fuelGauge;  // 연료량
	int curSpeed;  // 현재속도
};

void ShowCarState(const Car &car)
{
	cout << "소유자ID: " << car.gamerID << endl;
	cout << "연료량: " << car.fuelGauge << "%" << endl;
	cout << "현재속도: " << car.curSpeed << "km/s" << endl << endl;
}
void Accel(Car &car)
{
	if (car.fuelGauge <= 0)
		return;
	else
		car.fuelGauge -= FUEL_STEP;

	if (car.curSpeed + ACC_STEP >= MAX_SPD)
	{
		car.curSpeed = MAX_SPD;
		return;
	}

	car.curSpeed += ACC_STEP;
}
void Break(Car &car)
{
	if (car.curSpeed < BRK_STEP)
	{
		car.curSpeed = 0;
		return;
	}

	car.curSpeed -= BRK_STEP;
}

int main(void)
{
	Car run99 = { "run99", 100, 0 };
	Accel(run99);
	Accel(run99);
	ShowCarState(run99);
	Break(run99);
	ShowCarState(run99);

	Car sped77 = { "sped77", 100, 0 };
	Accel(sped77);
	Break(sped77);
	ShowCarState(sped77);

	return 0;
}

Output :

소유자ID: run99

연료량: 96%

현재속도: 20km/s


소유자ID: run99

연료량: 96%

현재속도: 10km/s


소유자ID: sped77

연료량: 98%

현재속도: 0km/s



바로 위 예제의 함수들은 구조체 Car에 종속적인 함수들이지만 전역함수의 형태를 띠고 있다.

따라서 다른 영역에서 이 함수를 호출하는 실수를 할 수도 있기 때문에 연관된 함수를 하나의 데이터형으로 묶을 필요성이 있다.


c++에서는 구조체 안에 함수 삽입 가능

위의 예제에서 Car에 종속적인 함수들을 구조체 안에 함께 묶을 수도 있다.


#include <iostream>
using namespace std;

#define ID_LEN		20
#define MAX_SPD		200
#define FUEL_STEP	2
#define ACC_STEP	10
#define BRK_STEP	10

struct Car	// 함수를 멤버로 지니는 구조체
{
	char gamerID[ID_LEN];	// 소유자ID
	int fuelGauge;			// 연료량
	int curSpeed;			// 현재속도

	void ShowCarState()
	{
		cout << "소유자ID: " << gamerID << endl;
		cout << "연료량: " << fuelGauge << "%" << endl;
		cout << "현재속도: " << curSpeed << "km/s" << endl << endl;
	}
	void Accel()
	{
		if (fuelGauge <= 0)
			return;
		else
			fuelGauge -= FUEL_STEP;

		if (curSpeed + ACC_STEP >= MAX_SPD)
		{
			curSpeed = MAX_SPD;
			return;
		}

		curSpeed += ACC_STEP;
	}
	void Break()
	{
		if (curSpeed<BRK_STEP)
		{
			curSpeed = 0;
			return;
		}

		curSpeed -= BRK_STEP;
	}
};

int main(void)
{
	Car run99 = { "run99", 100, 0 };	// 초기화의 대상은 변수
	run99.Accel();			// 구조체 내에 선언된 변수에 접근하듯이 함수 호출
	run99.Accel();
	run99.ShowCarState();
	run99.Break();
	run99.ShowCarState();

	Car sped77 = { "sped77", 100, 0 };
	sped77.Accel();
	sped77.Break();
	sped77.ShowCarState();
	return 0;
}

Output은 동일



구조체안에 enum 상수의 선언

#define ID_LEN    20

( ...)

위의 예제에서는 매크로 상수들이 존재하는데 이들 상수는 구조체 Car에게만 의미가 있는 상수들이기에, 상황에 따라 구조체 내에 포함시키는 것이 좋을수도 있다.

이러한 경우에는 구조체안에 열거형 enum 상수를 선언할 수 있다.


struct Car

{

enum

{

ID_LEN = 20,

MAX_SPD =200,

FUEL_STEP =2

};

char gamerID[ID_LEN];

( ...)

}


혹은 하나의 namespace에  enum 상수의 선언을 하기도 한다.

namespace CAR_CONST

{

enum

{

ID_LEN = 20,

MAX_SPD =200,

FUEL_STEP =2

};

}



구조체 안에는 함수의 선언을 넣고, 함수의 정의는 외부로 뺄수도 있다.


//구조체 안에 삽입된 함수의 선언

struct Car

{

. . . .

void ShowCarState();

void Accel();

. . . . 

};


//구조체 외부에 선언된 함수의 정의

void Car::ShowCarState()

{

. . . . 

}

void Car::Accel()

{

. . . .

}


구조체 안에 함수가 정의되어 있으면, inline 선언된 것으로 간주


함수의 정의가 구조체 외부에 있을 때, inline 처리를 하기를 바란다면

 다음과 같이 명시적으로 inline 선언을 하기도 한다.

inline 함수 -> http://smart2016.tistory.com/88


inline void Car::Break() { . . . . }



클래스와 구조체의 차이점

키워드 struct 를 대신해 class를 사용했을 때, 외형적 차이는 별반 다를게 없어 보인다.

하지만 단순히 키워드만 class로 바꾼다면 선언된 멤버의 접근이 불가능해진다.


class에는 멤버에 대한 접근 제어 지시자를 통해 접근하어야 한다.


접근 제어 지시자

 private

 클래스의 내부에서만 접근가능.


 public

 클래스의 내부/외부 모든 곳에서 접근 가능


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Car
{
private:
    char gamerID[CAR_CONST::ID_LEN];    
    int fuelGauge;        
    int curSpeed;        
public:
    void InitMembers(char * ID, int fuel);
    void ShowCarState();
    void Accel();
    void Break();
};
 
void Car::InitMembers(char * ID, int fuel)
{
    strncpy_s(gamerID, ID, sizeof(CAR_CONST::ID_LEN));
    fuelGauge=fuel;
    curSpeed=0;
};
cs

객체, 멤버 변수와 멤버 함수

위에서 Car class를 대상으로 생성된 변수를

'객체(object)'라고 한다.


위에서 Car 클래스내에 선언된 변수를 가리켜

'멤버 변수'라고 한다.


위에서 Car 클래스내에 정의된 함수를 가리켜

'멤버 함수'라고 한다.


struct(구조체) 와 class(클래스)는 동일
흔히 구조체는 클래스 이전에 선언되고 주로 여러종류의 기억공간을 선언하기 위한 형태로만 보는 경우가 일반적인데, 이는 틀린 생각이다. 구조체 역시 클래스와 동일하게 생성자와 소멸자를 지정할 수 있고, 코딩이 가능하며 내부호출과 캡슐화가 가능하다. 따라서, 하나의 물체(Object) 를 하나의 구조체(struct) 로 선언하고 사용할 수 있다. 따라서, 구조체 내에서도 모든 함수와 명령을 클래스와 동일하게 사용 가능하다. 다만, 멤버에 대해 접근 지정자를 써 주지 않는 경우 구조체는 public이 되고, 클래스는 private이 된다는 차이가 있다.




C++에서의 일반적인 파일 분할


클래스의 선언은 헤더파일에 삽입

클래스의 멤버 함수의 정의는 소스파일


Car.h

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
#ifndef __CAR_H__
#define __CAR_H__
 
namespace CAR_CONST
{
    enum
    {
        ID_LEN      =20,
        MAX_SPD     =200,
        FUEL_STEP   =2,
        ACC_STEP    =10,
        BRK_STEP    =10
    };
}
 
class Car
{
private:
    char gamerID[20];    
    int fuelGauge;        
    int curSpeed;        
public:
    void InitMembers(char * ID, int fuel);
    void ShowCarState();
    void Accel();
    void Break();
};
 
#endif
 
cs


Car.cpp

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
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <cstring>
#include "Car.h"
using namespace std;
 
void Car::InitMembers(char * ID, int fuel)
{
    strcpy(gamerID, ID);
    fuelGauge=fuel;
    curSpeed=0;
};
 
void Car::ShowCarState()
{
    cout<<"소유자ID: "<<gamerID<<endl;
    cout<<"연료량: "<<fuelGauge<<"%"<<endl;
    cout<<"현재속도: "<<curSpeed<<"km/s"<<endl<<endl;
}
 
void Car::Accel()
{
    if(fuelGauge<=0)
        return;
    else
        fuelGauge-=CAR_CONST::FUEL_STEP;
 
    if((curSpeed+CAR_CONST::ACC_STEP)>=CAR_CONST::MAX_SPD)
    {
        curSpeed=CAR_CONST::MAX_SPD;
        return;
    }
    curSpeed+=CAR_CONST::ACC_STEP;
}
 
void Car::Break()
{
    if(curSpeed<CAR_CONST::BRK_STEP)
    {
        curSpeed=0;
        return;
    }
    curSpeed-=CAR_CONST::BRK_STEP;
}
cs


RacingMain.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "Car.h"
 
int main(void)
{
    Car run99;
    run99.InitMembers("run99"100);
    run99.Accel();
    run99.Accel();
    run99.Accel();
    run99.ShowCarState();
    run99.Break();
    run99.ShowCarState();
    return 0;
}
cs



다만, inline함수는 컴파일 과정에서 함수의 몸체 부분이 함수 호출 문장으로 대체되기 때문에 다음과 같이 헤더파일에 함께 정의되어야 한다.


CarInline.h

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#ifndef __CARINLINE_H__
#define __CARINLINE_H__
 
#include <iostream>
using namespace std;
 
namespace CAR_CONST
{
    enum
    {
        ID_LEN        =20,
        MAX_SPD        =200,
        FUEL_STEP    =2,
        ACC_STEP    =10,
        BRK_STEP    =10
    };
}
 
class Car
{
private:
    char gamerID[CAR_CONST::ID_LEN];    
    int fuelGauge;        
    int curSpeed;        
public:
    void InitMembers(char * ID, int fuel);
    void ShowCarState();
    void Accel();
    void Break();
};
 
inline void Car::ShowCarState()
{
    cout<<"소유자ID: "<<gamerID<<endl;
    cout<<"연료량: "<<fuelGauge<<"%"<<endl;
    cout<<"현재속도: "<<curSpeed<<"km/s"<<endl<<endl;
}
 
 
inline void Car::Break()
{
    if(curSpeed<CAR_CONST::BRK_STEP)
    {
        curSpeed=0;
        return;
    }
    curSpeed-=CAR_CONST::BRK_STEP;
}
 
#endif
 
cs


CarInline.cpp

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
#include <cstring>
#include "CarInline.h"
using namespace std;
 
void Car::InitMembers(char * ID, int fuel)
{
    strcpy(gamerID, ID);
    fuelGauge=fuel;
    curSpeed=0;
};
 
void Car::Accel()
{
    if(fuelGauge<=0)
        return;
    else
        fuelGauge-=CAR_CONST::FUEL_STEP;
 
    if((curSpeed+CAR_CONST::ACC_STEP)>=CAR_CONST::MAX_SPD)
    {
        curSpeed=CAR_CONST::MAX_SPD;
        return;
    }
    curSpeed+=CAR_CONST::ACC_STEP;
}
 
cs


main은 동일



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

생성자와 소멸자, 클래스  (0) 2016.09.26
class  (0) 2016.09.25
CPP-Object Oriented Programming (객체지향 프로그래밍)  (0) 2016.09.23
c++ 기초3  (0) 2016.09.21
c++ 기초  (0) 2016.09.19

+ Recent posts