[C++ Programming] Chapters 8 : Class, etc.
🏫Chapters 8 : Class
📖Classes
- 변수에는 local과 global이 있다.
- 그러나 global var은 두가지 측면에서 나쁘다.
하나는 메모리를 프로그램 시작부터 종료까지 붙잡고 있기 때문이고,
두번째는 global var은 모든 곳에서 접근이 가능하기 때문에 값에 대한 안정성이 떨어진다. -
global이 가지는 장점은 일종의 쉐어되는 메모리라는 것이다. 파라미터와 리턴을 굳이 통하지 않고 쉐어되는 메모리가 있다는 측면은 편리한 것이 확실하다.
-
변수와 달리, function을 생각해보면 scope 면에서 글로벌 func밖에는 없다. function은 변경의 대상은 아니지만 변경의 수단이 될 수 있다.
그렇기 때문에 모든 function이 모든 곳에서 공개되어 있다는 것은 간접적으로 변수에 대한 상태변경을 일으킬 수 있는 위험이 있기 때문에 안정성이 역시나 떨어진다.
함수와 변수 사이에 일종의 벽이 필요하다. 서로간의 접근이 필요한 애들끼리 모아두고 서로에 대해 칸막이를 두자는 개념이다.(은닉)이 칸막이, 벽의 이름이 클래스이다.
-
기본적으로 데이터간의 쉐어 및 접근, 함수간의 호출이 필요하다는 것은 서로 무언가의 연결점이 있기 때문이다. 남남이 이메일 번호를 공유할 필요 없듯이, 서로 데이터를 공유할 필요가 있다는 것은 둘 사이의 관계성이 존재한다는 것이다. 연결점이 있는 변수와 함수들끼리 묶고, 그 외에 관계성이 없는 것들과는 모르는 것이 낫다는 것이다. 얘는 왜 비밀번호를 1234를 쓸까. 어느날 로그인 했을때 되고, 이메일은,,? 이러면서 무언가의 실수를 일으킬 가능성을 소지한다. 따라서 차라리 접근할 필요가 없는 것들끼리는 아예 모르는 것이 약이라는 것이다.
- 우리가 원하는 바는 글로벌 변수와 함수를 C처럼 다 보이게 하지 말고 클래스로 묶고, 각각의 클래스는 서로 볼 수 없게 만드는 것이 1차 원칙이다.
이것을 Encapsulization 캡슐화 라고 한다
-
이러한 특성을 통해 안정성을 높일 수 있다.
-
이때, 서로 거들떠도 보지 않을 거면 왜 굳이 같은 프로그램에 있어야 할까?
넓게 보면 같은 프로그램에 있는 클래스 사이에 무언가의 연결점이 있기 때문일 것이다.
즉, 역설적으로 클래스는 기본적인 상호연관성을 열 필요가 있다.
따라서 최소한의 공개 원칙이 있다. -
최소한의 공개 원칙은 연결점이 있는 것에 한해 최소한만 외부에서 볼 수 있도록 해준다.
public, private(default)
📖Members and member access
- Ex
class X {
public:
int m; // data member
int mf(int v) { int old = m; m=v; return old; } // function member
};
X var; // var is a variable of type X
var.m = 7; // access var’s data member m
int x = var.mf(9);// call var’s member function mf()
public
으로 설정되어 있기 때문에- 클래스 외부에서
.
을 통해 멤버들을 부를 수 있다. x
는 하나의 인스턴스인 것이고 각각의 인스턴스는 별도로 멤버변수를 갖는다.
#include<iostream>
using namespace std;
class Sample{
int m;
public:
int n;
};
int main(void){
Sample x, y;
x.n = 1;
y.n = 10;
x.m = 2; // m은 private이기 때문에 접근할 수 없어 컴파일에러 발생한다
cout << "x.n = " << x.n;
cout << "y.n = " << y.n;
}
- 각각의 인스턴스의 프로퍼티는 따로 메모리에 할당된다. 즉,
x.n != y.n
-
private m
을 사용하고 싶다면 액세스함수를 만든다.class Sample{ int m; public: int n; void setM(int x){ m = x; } int getM(){ return m; } };
- 위의 멤버함수는 public으로 선언되었기 때문에 외부에서 접근이 가능하다. 이러한 액세스함수를 통해 private한 멤버변수에 접근할 수 있다.
- 더 권장되는 방식은 변수는
private
으로 선언하고 함수를 통해 접근하는 방식이다.
한 번 더 검수하는 과정을 거치는 셈이므로 감시할 수 있는 수단이 되고, 아래와 같이 안정성을 높일 수 있다.
class Error{}; class Sample{ int m; // default = private public: int n; void setM(int x){ if(x < 0) throw Error(); m = x; } int getM(){ return m; } };
📖Constructor
- Constructor 생성자
- 이름은 반드시 클래스의 이름과 동일해야한다.
-
클래스를 최초로 정의될 때 자동으로 호출되는 녀석이다. 즉, 객체 생성 시 따로 명시적으로 호출하지 않아도 자동으로 호출된다.
- 이 때, 다형성을 생각해보자. 생성자가 하나만 있으리라는 법은 없다.
class Sample{ int m; // default = private int n; public: void Sample(){ // default 생성자 cout << "Sample() is called" << endl; } void Sample(int i){ // 다형성. 인자에 의해 알아서 생성자가 호출된다 cout << "Sample(int i) is called" << endl; } void setM(int x){ if(x < 0) throw Error(); m = x; } int getM(){ return m; } }; int main(void){ Sample x, y; Sample z(100); } /* 결과 Sample() is called Sample() is called Sample(int i) is called */
- 이 때 생성자를 만들지 않았을 경우, 컴파일러가 알아서 디폴트 생성자를 만든다.
그리고, 이 생성자는 텅 비어있기 때문에 여기에 인자를 전달하면 에러가 난다. 디폴트 생성자에는 파라미터를 받지 않기 때문이다.
📖Classes
Ex
```cpp
// simple Date (control access)
class Date {
int y,m,d; // year, month, day
public:
Date(int y, int m, int d); // constructor: check for valid date and initialize
// access functions:
void add_day(int n); // increase the Date by n days
int month() { return m; }
int day() { return d; }
int year() { return y; }
};
// …
Date my_birthday {1950, 12, 30}; // ok
cout << my_birthday.month() << endl; // we can read
my_birthday.m = 14; // error: Date::m is private
```
+ 멤버함수는 각각의 클래스가 공유한다. 값이 바뀌거나 그러지 않기 때문에 함수의 메모리 주소를 모두 공유한다.
그러나 변수는 각각의 객체마다 값이 달라질 수 있기 때문에 따로 저장한다.
따라서 메모리에서 함수는 고려되지 않고 변수만 고려한다. 함수의 메모리는 멤버변수만큼 크기를 갖는다.
파일 분리
```cpp
class Sample{
int m; // default = private
int n;
public:
void Sample();
void Sample(int i);
void setM(int x);
int getM();
};
void Sample::Sample(){ // Sample 클래스의 생성자를 정의하는 것을 알려줌
cout << "Sample() is called" << endl;
}
void Sample::Sample(int i){
cout << "Sample(int i) is called" << endl;
}
void Sample::setM(int x){ // Sample 클래스의 멤버함수임을 알려줌
if(x < 0) throw Error();
m = x;
}
int Sample::getM(){
return m;
}
```
+ 이 방법이 권장사항이다
+ C는 멤버함수에 대한 선언만 클래스 안에 명시하고 정의는 그 밖에 명시하는 것이 권장사항인데, 그 이유는 C가 오픈소스의 문화를 모를 때 만들어졌기 때문이다.
남들에게 재산과 마찬가지인 멤버함수를 어떻게 구현했는지 쉽게 보여주지 않고, 그 결과를 low-level 언어로 보여주겠다는 것이다.
즉, 보여주고 싶은 선언과 보여주고 싶지 않은 정의를 분리하는 습관이 깃들어져 있다. 따라서 선언은 `.h`로 정의는 `.cpp`로 분리한다.
```cpp
// Sample.cpp
#include"Sample.h"
void Sample::Sample(){
cout << "Sample() is called" << endl;
}
void Sample::Sample(int i){
cout << "Sample(int i) is called" << endl;
}
void Sample::setM(int x){
if(x < 0) throw Error();
m = x;
}
int Sample::getM(){
return m;
}
// Sample.h 선언만 있는 헤더파일만 공개한다
class Sample{
private:
int m;
int n;
public:
void Sample();
void Sample(int i);
void setM(int x);
int getM();
};
```
private, public
- private에는 멤버변수를 넣어놓는다.
- public에는 멤버변수에 접근할 수 있는 액세스함수를 만들어 놓는 것을 권장한다.
📖Destructor 소멸자
- ~클래스이름(); –>
~Sample();
- 메모리에서 사라질 때 호출됨
Sample::~Sample(){
cout << m << " destructor is called!" << endl;
}
- 직접적으로 함수를 호출하는 것이 아니기 때문에 파라미터를 결정할 수 없음
class Sample{
private:
int m;
int n;
public:
void Sample();
void Sample(int i);
void setM(int x);
int getM();
void ~Sample();
};
int main(void){
Sample x;
Sample *w;
Sample y2(x);
w = new Sample(); // 포인터, 동적할당 받음, 힙에 저장
{
delete w; // java는 문법적으로 소멸자가 없지만, C++은 delete가 있으므로 소멸자가 있다.
/* java는 가비지컬렉터가 알아서 메모리를 해제해주고 관리하기 때문.
프로그래머가 메모리를 직접 할당하고 해제하는 것을 허락해줄 경우,
메모리 릭leak이 잘 발생하기 때문에 자바는 프로그래머에게 결정권을 주지 않는다.
따라서 new로 heap에 할당한 메모리는 delete가 명시되면 소멸자를 호출하며 메모리 해제.
stack에 할당된 메모리는 그 변수를 감싼 중괄호가 끝날 때, 소멸자를 호출하며 메모리 해제.
*/
// 포인터 변수 선언에 대해서는 생성자가 호출되지 않는다. 포인터는 단순히 주소를 저장하는 변수이기 때문.
Sample y;
x.setM(2);
y.setM(20);
{
Sample z
cout<<"x.m=" << x.getM() << endl;
cout<<"y.m=" << x.getM() << endl;
cout<<"z.m=" << x.getM() << endl;
cout<<"y2.m=" << x.getM() << endl;
cout<<"z.m=" << z.sizeof(() << endl;
}
}
}
/*result
x constructor is called!
w constructor is called!
w destructor is called!
y constructor is called!
z constructor is called!
z destructor is called!
y destructor is called!
z destructor is called!
*/
-
w는 원래 포인터 변수. w는 로컬변수이기 때문에 자신을 감싸고 있는 가까운 중괄호를 만나면 사라짐. 그러나 포인터변수가 가리키는 데이터가 저장되어있는 곳은 힙이다. 따라서
delete
가 없으면 포인터변수는 중괄호를 만나면 사라지게 될 때, 그 포인터 변수가 가리키는 힙에 할당되어 있는 데이터는 그대로 남아있게 되고, 그러나 힙에 존재하는 그 메모리를 가리킬 변수가 없음으로써 가리킬 방법이 사라지고, 힙에 할당된 데이터가 불릴 수도 없고 사용될 수도 없고 할당만 되어있는 이것이 메모리 릭 leak이다. -
자기의 멤버 변수 중 포인터가 있는 경우, 소멸자에서 delete 해줘야 한다.
-
위와 같은 마무리가 C++에서는 책임이 프로그래머에게 있으므로 뒤처리를 확실히 해주어야한다.
📖Copy Constructor 복사생성자
- 나 자신을 파라미터로 받는 것은 디폴트로 자동 생성해줌
Sample::Sample(Sample &s){
m = s.m + 1000;
}
-
클래스 내부이기 때문에 private에 직접 접근 가능
Sample y2 = y1;
Assignment- 모든 사용자정의에 대해 연산자 오버로딩을 자동으로 해줌. 멤버변수를 복사해준다.
- 최초로 정의될 때 한 번 호출되는 복사생성자와 달리 시도때도 없이 불릴 수 있다.
- 얕은 복사/ 깊은 복사
📖Initializer
- 선언과 동시에 초기화하는 방법으로 속도가 좀 더 빠르다. 성능이 좀 더 좋아진다.
📖this
Sample::Sample(int m){
m = m; // 둘 다 로컬변수 m을 가리키게 된다
this->m = m; // this는 자신을 가리키는 포인터
// 로컬에 대한 멤벼변수는 . 스택, 포인터 변수는 -> 힙
}
-
자바에서는 primitive타입, 클래스 오브젝트는 무조건 힙에 저장하기 때문에 new를 써주어야 한다. 그래서 자바는 다른 언어에 비해 상대적으로 힙 메모리 관리가 굉장히 중요하다. 선택권이 없어 무조건 힙으로 보내기 때문에
.
과->
를 구분할 필요가 없다. 굳이 구별하지 않고 모두 점으로 쓴다. primitive 타입을 억지로 힙에 보내기 위해서 감싸고 있는 Wrapper클래스를 만들어 사용한다. -
this는 포인터이기 때문에 힙이라고 생각.
📖Const
Sample::Sample(const Sample &a){
m = a.getM() + 100; // 에러
}
int getM() const { // 여기에도 const 선언을 해주어야한다.
return m;
}
-
위 코드에서 컴파일 에러가 뜨는 이유는 a에 const 선언이 되어 있으므로 a의 값이 바뀔 수 없는 상황에서,
호출된 getM에서 a의 값이 바뀔 수도 있다고 컴파일러가 판단하기 때문이다. 간접적 변화도 차단하는 셈이다.
따라서 getM에도 const 선언을 해주어야한다. -
즉, const로 선언된 함수만 호출할 수 있다.
What makes a good interface?
-
귀차니즘이 발동하면 public으로 많이 열게 되는데, 이는 바람직하지 않다.
필요한 최소한만큼만 public으로 열어야 한다. -
타입의 문제나 const의 정확성을 유지해야한다.
-
자동으로 만들어주는 함수 몇가지가 있는데, 생성자, 소멸자, 복사생성자, copy assignment