일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- Web
- BOF
- backend
- 리눅스
- hackthissite
- 백엔드
- Linux
- deep learning
- php
- 인공지능
- c
- 경제
- 러닝스칼라
- hackerschool
- mysql
- webhacking
- 러닝 스칼라
- ChatGPT
- 챗GPT
- 딥러닝
- Python
- 웹해킹
- Javascript
- Scala
- BOF 원정대
- Shellcode
- c++
- flask
- hacking
- 파이썬
- Today
- Total
jam 블로그
[C++] 004. 클래스의 완성 본문
I. 정보 은닉
정보 은닉의 필요성
- 은닉화 : 객체의 회부에서 객체 내에 존재하는 멤버 변수에 직접 접근하는 것을 허용하지 않으면 된다.(접근을 하지 못하게 하려면 private를 쓰면 된다.)
- #include <iostream>
- using namespace std;
- class Point
{
public:
int x;
int y;
}; - int main()
{
int x,y;
cout<<"좌표 입력 : ";
cin>>x>>y;
Point p;
p.x=x;
p.y = y;
cout<<"입력된 데이터를 이용해서 그림을 그림"<<endl;
return 0;
}
- 이 소스의 가장 큰 문제 : 객체의 외부에서 객체 내에 존재하는 멤버 변수에 직접 접근할 수 있는 권한을 준다는 것이다.
정보 은닉의 적용
- 위의 소스의 정보는 은닉하려면 아래와 같이 소스를 짜면 된다.
- #include <iostream>
- using namespace std;
- class Point
{
int x;
int y;
public:
int GetX(){return x;}
int GetY(){return y;}
void SetX(int _x){x= _x;}
void SetY(int _y){y= _y;}
}; - int main()
{
int x,y;
cout<<"좌표 입력 : ";
cin>>x>>y; - Point p;
p.SetX(x);
p.SetY(y);
cout<<"입력된 데이터를 이용해서 그림을 그림"<<endl;
return 0;
}
클래스 안에 public: 처럼 선언이 없으면 기본적으로 private로 선언된다.
- 멤버 함수의 이름 중에서 Get, Set으로 시작되는 함수들은 대부분 멤버 변수의 접근을 위한 것이다. 보통 access 매소드라고 한다.
- 위의 소스에서 오류 검사 같은 것을 추가 하려면 밑의 소스처럼 바꾸자.
- #include <iostream>
- using namespace std;
- class Point
{
int x;
int y;
public:
int GetX(){return x;}
int GetY(){return y;}
void SetX(int _x);
void SetY(int _y);
}; - void Point::SetX(int _x)
{
if (_x<0||_x>100)
{
cout<<"X 좌표 입력 오류, 확인 요망"<<endl;
}
x = _x;
} - void Point::SetY(int _y)
{
if (_y<0||_y>100)
{
cout<<"X 좌표 입력 오류, 확인 요망"<<endl;
}
y = _y;
}
int main()
{
int x,y;
cout<<"좌표 입력 : ";
cin>>x>>y; - Point p;
p.SetX(x);
p.SetY(y);
cout<<"입력된 데이터를 이용해서 그림을 그림"<<endl;
return 0;
}
- x와 y의 좌표를 0~100까지의 범위를 두고 그 이하 또는 그 이상일 경우 오류를 처리하는 조건문은 넣은 것이다.
II. 캡슐화
캡슐화의 기본 개념
- 관련 있는 데이터와 함수를 하나의 단위로 묶는 것이다.
- #include <iostream>
- using namespace std;
- class Point
{
int x;
int y;
public:
int GetX(){return x;}
int GetY(){return y;}
void SetX(int _x);
void SetY(int _y);
}; - void Point::SetX(int _x)
{
if (_x<0||_x>100)
{
cout<<"X 좌표 입력 오류, 확인 요망"<<endl;
}
x = _x;
} - void Point::SetY(int _y)
{
if (_y<0||_y>100)
{
cout<<"X 좌표 입력 오류, 확인 요망"<<endl;
}
y = _y;
}
class PointShow
{
public:
void ShowData(Point p)
{
cout<<"x좌표 : "<<p.GetX()<<endl;
cout<<"y좌표 : "<<p.GetY()<<endl;
}
};
int main()
{
int x,y;
cout<<"좌표 입력 : ";
cin>>x>>y; - Point p;
p.SetX(x);
p.SetY(y);
PointShow show;
show.ShowData(p);
return 0;
}
위의 소스를 보면 캡슐화 처럼 보이지만(안보일 수도 있다) 이거는 캡슐화에 실패한 예제이다
- 캡슐화는 하나의 클래스로 정의하는 것인데 위에 소스를 보면 클래스가 2개로 나눠져 있다.
- #include <iostream>
- using namespace std;
- class Point
{
int x;
int y;
public:
int GetX(){return x;}
int GetY(){return y;}
void SetX(int _x);
void SetY(int _y);
void ShowData();
}; - void Point::SetX(int _x)
{
if (_x<0||_x>100)
{
cout<<"X 좌표 입력 오류, 확인 요망"<<endl;
}
x = _x;
} - void Point::SetY(int _y)
{
if (_y<0||_y>100)
{
cout<<"X 좌표 입력 오류, 확인 요망"<<endl;
}
y = _y;
}
void Point::ShowData()
{
cout<<"x좌표 : "<<x<<endl;
cout<<"y좌표 : "<<y<<endl;
} - int main()
{
int x,y;
cout<<"좌표 입력 : ";
cin>>x>>y; - Point p;
p.SetX(x);
p.SetY(y);
p.ShowData();
return 0;
}
- 위의 소스를 보면 Point 클래스는 x,y좌표에 관련된 데이터와 함수가 하나의 클래스 안에 존재한다. 적절히 캡슐화가 된것이다.
III. 생성자(constructor)와 소멸자(destructor)
생성자의 필요성
- 객체를 생성과 동시에 초기화 하기 위해서
- #include <iostream>
- using namespace std;
- const int SIZE = 20;
- class Person
{
char name[SIZE];
char phone[SIZE];
int age;
public:
void ShowData();
}; - void Person::ShowData()
{
cout<<"name : "<<endl;
cout<<"phone : "<<endl;
cout<<"age : "<<endl; - }
- int main()
{
Person p = {"KIM","013-123-1234",22};
p.ShowData();
return 0;
}
위의 소스를 보면 Person p = {"KIM","013-123-1234",22}; 이걸로 객체를 초기화 하려고 하지만 오류가 난다. 왜?
- 당연히 private로 선언되어 있는 변수들을 객체의 외부에서 접근해서 초기화 하려고 하니 오류가 난다. 다음 예제를 보자
- #include <iostream>
- using namespace std;
- const int SIZE = 20;
- class Person
{
char name[SIZE];
char phone[SIZE];
int age;
public:
void ShowData();
void SetData(char* _name,char* _phone,int _age);
}; - void Person::ShowData()
{
cout<<"name : "<<name<<endl;
cout<<"phone : "<<phone<<endl;
cout<<"age : "<<age<<endl; - }
- void Person::SetData(char *_name, char *_phone, int _age)
{
strcpy(name,_name);
strcpy(phone,_phone);
age = _age;
} - int main()
{
Person p;
p.SetData("KIM","013-123-1234",22);
p.ShowData();
return 0;
}
- SetData라는 함수를 만들어서 초기화를 시키고 있다. 하지만 이는 바람직한 형태가 아니다.
- 다음과 같은 형태의 초기화는 허용되지 않는다.
- class AAA
- {
int a =10;
int b =20;
- };
- struct AAA
- {
int a =10;
int b = 20;
- }
- 단, JAVA나 C#은 허용이 된다고 합니다.
생성자와 객체의 생성 과정
모든 객체는 다음 두 단계를 반드시 거친다.
- 메모리 할당
- 생성자 호출
생성자의 외형적 특징
- 함수이다.
- 클래스의 이름과 같은 이름을 지닌다.
- 리턴하지도 않고, 리턴 타입도 선언되지 않는다.
- #include<iostream>
- using namespace std;
- class AAA
{
int i,j;
public:
AAA()
{
cout<<"생성자 call"<<endl;
i = 10; j = 20;
}
void ShowData()
{
cout<<i<<' '<<j<<endl;
}
}; - int main()
{
AAA aaa;
aaa.ShowData();
return 0;
}
위에 소스를 보면 클래스 안에 AAA() 라는 것을 볼수 있는데 그것이 생성자 이다.
- 생성자를 호출하면서 i,j를 초기화 시킨다.
- 첫번째 그림은 객체생성 1단계(메모리 할당)을 나타낸거다.
- 두번째 그림은 객체생성 2단계(생성자 호출)이다.
- #include<iostream>
- using namespace std;
- class AAA
{
int i,j;
public:
AAA(int _i,int _j)
{
cout<<"생성자 call"<<endl;
i = _i;
j = _j;
}
void ShowData()
{
cout<<i<<' '<<j<<endl;
}
}; - int main()
{
AAA aaa(111,222);
aaa.ShowData();
return 0;
}
위의 소스는 객체를 생성과 동시에 초기화되면서, 원하는 값을 초기화했기 대문이다.
- 생성자는 다른 용도(?)로 사용될 수 있지만 가급적 멤버변수를 초기화하는 용도로만 사용.
- #include <iostream>
- using namespace std;
- const int SIZE = 20;
- class Person
{
char name[SIZE];
char phone[SIZE];
int age;
public:
Person(char* _name,char* _phone, int _age);
void ShowData();
}; - void Person::ShowData()
{
cout<<"name : "<<name<<endl;
cout<<"phone : "<<phone<<endl;
cout<<"age : "<<age<<endl; - }
- Person::Person(char *_name, char *_phone, int _age)
{
strcpy(name,_name);
strcpy(phone,_phone);
age = _age;
} - int main()
{
Person p("KIM","013-123-1234",22);
p.ShowData();
return 0;
}
생성자를 생성한 다음 메인에서 Person p(("KIM","013-123-1234",22);로 초기화 하고 있다.
- Person p("KIM","013-123-1234",22);와 같은 방법으로는
- Person p = Person("KIM","013-123-1234",22); 이지만 위에 방법을 더 잘 쓴다.
디폴트 생성자와 생성자의 특징
- 생성자를 하나도 정의하지 않으면 디폴트 생성자가 자동 삽입
- 생성자도 함수이므로 오버로딩이 가능하다
- 생성자도 함수이므로 디폴트 매개 변수의 설정이 가능하다
- class Point
- {
int x, y;
public:
Point(){}
- }
- 위의 소스에서 생성자인 Point(){}를 보면 인자값을 받지도 않고 아무런 기능이 없다. 이것이 디폴트 생성자이다.
- #include <iostream>
- using namespace std;
- class Point
{
int x, y; - public:
Point(int _x, int _y)
{
x = _x;
y = _y;
}
void ShowData()
{
cout<<x<<' '<<y<<endl;
} - };
- int main()
{
Point p1(10,20);
p1.ShowData(); - Point p2; //error
p2.ShowData();
return 0;
}
위 소스는 잘못 되었다. 무엇이 잘못 되었을까?
- 메인에 Point p2에서 에러가 나는데 이유는 p2는 디폴트 생성자를 호출하는데 클래스 안에는 디폴트 생성자가 없다.(생성자가 하나라고 있으면 디폴트 생성자는 없다.)
- 2가지 방법으로 오류를 고쳐보겠다.
- class Point
{
int x, y; - public:
Point(int _x, int _y)
{
x = _x;
y = _y;
}
Point()
{
x=y=0;
}
void ShowData()
{
cout<<x<<' '<<y<<endl;
} - };
- 클래스 안에 디폴트생성자를 만들어주면 된다. (이것이 첫번째 방법)
- class Point
{
int x, y; - public:
Point(int _x = 0, int _y = 0)
{
x = _x;
y = _y;
}
void ShowData()
{
cout<<x<<' '<<y<<endl;
} - };
- 생성자도 함수이므로 오버로딩뿐만 아니라 디폴트 매개변수를 설정할 수 있다.
- 생성자와 동적 할당
- #include <iostream>
- using namespace std;
- class Person
{
char *name;
char *phone;
int age;
public:
Person(char* _name,char* _phone,int _age);
void ShowData();
}; - Person::Person(char* _name,char* _phone,int _age)
{
name = new char[strlen(_name)+1];
strcpy(name,_name); - phone = new char[strlen(_phone)+1];
strcpy(phone, _phone); - age = _age;
}
void Person::ShowData()
{
cout<<"name : "<<name<<endl;
cout<<"phone : "<<phone<<endl;
cout<<"age : "<<age<<endl;
}
int main()
{
Person p("KIM","013-123-1234",22);
p.ShowData();
return 0;
}
위 예제 처럼 하면 중요한 문제가 있다.
- 동적으로 할당한 메모리 공간을 해제를 안해주는 것이다. 그래서 메모리 누수 현상이 발생 하므로 해제를 시켜줘야 한다.
소멸자의 특징과 필요성
모든 객체는 생성과정 처럼 반드시 소멸 거친다.
- 소멸자 호출
- 메모리 반환(해제)
특징
- 함수이다.
- 클래스의 이름 앞에 ~(틸드)가 붙는 형태의 이름을 지닌다.(즉, 생성자 앞에 ~를 붙이면 된다.)
- 리턴하지도 않고 리턴타임도 선언되지 않는다.
- 매개 변수를 받을 수 없다. 따라서 오버로딩도 불가능하고, 디폴트 매개 변수 선언도 불가능하다.
- #include <iostream>
- using namespace std;
- class AAA
{
public:
AAA()
{
cout<<"생성자 호출"<<endl;
}
~AAA()
{
cout<<"소멸자 호출"<<endl;
} - };
- int main()
{
AAA aaa1;
AAA aaa2;
return 0;
}
- AAA aaa1;, AAA aaa2가 실행 되면서 생성자를 호출 하여 2번 생성자 호출이라는 문구가 뜨고 main함수 호출이 끝나면서(프로그램 종료하면서) 두번 소멸자를 호출 하게 된다.
디폴트 소멸자
- 디폴트 생성자 처럼 생성하면 되며(또는 알아서 컴퓨터가 생성 하지만) 디폴트 생성자 앞에 ~만 붙이면 된다.
IV. 클래스와 배열
- 객체배열과 생성자
- #include <iostream>
- using namespace std;
- class Point
{
int x;
int y; - public:
Point()
{
cout<<"Point() call!"<<endl;
x=y=0;
}
Point(int _x, int _y)
{
x= _x;
y = _y;
}
int GetX(){return x;}
int GetY(){return y;}
void SetX(int _x){x = _x;}
void SetY(int _y){y = _y;}
}; - int main()
{
Point arr[5]; - for (int i = 0;i<5;i++)
{
arr[i].SetX(i*2);
arr[i].SetY(i*3);
}
for (int j = 0; j<5;j++)
{
cout<<"x : "<<arr[j].GetX()<<' ';
cout<<"y : "<<arr[j].GetY()<<endl;
}
return 0;
}
- Point 클래스를 배열로 선언 한 것이다. 밑의 그림처럼 되어있다.
- 객체 포인터 배열
- #include <iostream>
- using namespace std;
- class Point
{
int x;
int y; - public:
Point()
{
cout<<"Point() call!"<<endl;
x=y=0;
}
Point(int _x, int _y)
{
x= _x;
y = _y;
}
int GetX(){return x;}
int GetY(){return y;}
void SetX(int _x){x = _x;}
void SetY(int _y){y = _y;}
}; - int main()
{
Point *arr[5]; - for (int i = 0;i<5;i++)
{
arr[i] = new Point(i*2,i*3);
}
for (int j = 0; j<5;j++)
{
cout<<"x : "<<arr[j]->GetX()<<' ';
cout<<"y : "<<arr[j]->GetY()<<endl;
}
for (int k=0;k<5;k++)
{
delete arr[k];
}
return 0;
}
- 추가가 된 부분은 Point *arr[5]와 new로 할당해주는 부분과 delete로 해제 해주는 부분이다.
V. this 포인터의 의미
- this 포인터에 관한 간단한 설명을 하면 자기자신을 가리키는 포인터라고 보면 됩니다.
- #include <iostream>
- using namespace std;
- class Person
{
public:
Person* GetThis()
{
return this;
}
}; - int main()
{
cout<<"***** p1의 정보 *****"<<endl;
Person *p1 = new Person();
cout<<"포인터 p1 : "<<p1<<endl;
cout<<"p1의 this : "<<p1->GetThis()<<endl; - cout<<"***** p2의 정보 *****"<<endl;
Person *p2 = new Person();
cout<<"포인터 p2 : "<<p2 <<endl;
cout<<"p2의 this : "<<p2->GetThis()<<endl;
return 0;
}
- 위의 소스를 실행시켜 보면 출력값이 포인터 값이나 그에 대한 this 값이나 같다.
- this 포인터의 용도
- #include <iostream>
- using namespace std;
- class Data
{
int aaa;
int bbb;
public:
Data(int aaa, int bbb)
{
this->aaa = aaa; - this->bbb = bbb;
}
void printAll()
{
cout<<aaa<<" "<<bbb<<endl;
}
};
int main()
{
Data d(100,200);
d.printAll();
return 0;
}
- 용도로는 연산자 오버로딩에 쓰인다.
VI. 전역함수에 대한 friend 선언
- friend는 private으로 선언된 멤버 변수의 접근을 허용할 수 있다.
- #include <iostream>
- using namespace std;
- class Counter
{
int val;
public:
Counter()
{
val = 0;
}
void Print() const
{
cout<<val<<endl;
}
friend void SetX(Counter& c,int val);
}; - void SetX(Counter&c, int val)
{
c.val = val;
}
int main()
{
Counter cnt;
cnt.Print(); - SetX(cnt,2002);
cnt.Print();
return 0;
}
- 위 소스를 보면 friend void SetX 라고 되어있다. SetX 함수를 friend로 설정하였기 때문에 void SetX는 private에 속해있는 val를 쓸 수 있다.
- #include <iostream>
- using namespace std;
- class AAA
{
private:
int data;
friend class BBB;
}; - class BBB
{
public:
void SetData(AAA& aaa, int val)
{
aaa.data = val;
}
}; - int main()
{
AAA aaa;
BBB bbb; - bbb.SetData(aaa,10);
return 0;
}
- 위의 소스를 보면 (실행해도 보이는건 없음 ㅋㅋ) friend class BBB를 썼는데 class BBB를 friend 하겠다는 것으로 AAA의 private에 있는 val를 참조 할 수 있다.
- friend는 연산자 오버로딩에 많이 쓰이는데 너무 쓰면 유지 보수 하기 힘든 프로그램이 되어 버린다.
문제
4 -1
문제1
직사각형을 나타내는 Rectangle 클래스와 원을 나타내는Circle 클래스를 디자인 해 보자. 이 두 클래스는 넓이를 구하는 기능과 둘레를 구하는 기능을 지녀야 한다.
문제2
시,분,초 정보를 지닐수 있는 Time 클래스를 정의해 보자. 이 클래스는 멤버 변수가 지니고 있는 데이터를 적절히 출력하는 기능을 지녀야 한다. 하나는 [시,분,초]의 형식을 띄며, 또 하나는 초 단위로 계산한 출력 결과를 보여준다.
문제3
명함 정보를 지닐수 있는 클래스를 정의해 보자. 믈래스의 이름은 NameCard이고 이름, 전화번호, 주소, 직급 정보를 저장할 수 있어야 한다. 생성자 내에서 동적 할당하고, 소멸자에게 할당 받은 메모리를 해제하는 형식으로 구현하자.
4 -2
연습문제 4 -1의 3번 문제를 통해서 NameCard 클래스를 정의하였다. 이제 이 클래스를 적절히 활용하는 main 함수를 만들어 보기로 하자.
사용자로부터 NameCard 객체에 들어갈 데이터를 입력받기로 하자. 총 3명의 데이터를 순차적으로 입력받고, 다시 순차적으로 출력하는 형태로 예제를 작성하자. 단 3명의 데이터를 입력받기 위해서 배열을 선언 하되, 객체 포인터 배열을 선언하는 형태를 취하도록 하자.
'개발 및 관련 자료 > C' 카테고리의 다른 글
[C++] 006. static 멤버와 const 멤버 (0) | 2013.05.12 |
---|---|
[C++] 005. 복사 생성자 (0) | 2013.05.12 |
[C++] 003. 클래스의 기본 (0) | 2013.05.12 |
[C++] 002. C기반의 C++ 2 (0) | 2013.05.12 |
[C++] 001. C기반의 C++ 1 (0) | 2013.05.12 |