[C++ Primer Plus] Chapter 10. 객체와 클래스
Table of Contents
C++ 기초 플러스 책을 읽고 공부한 노트입니다.
클래스 #
- 추상화란 어떤 정보를 사용자 인터페이스로 표현하는 것이다. 따라서 클래스는 추상화 인터페이스를 구현하는 사용자 정의 데이터형이라고 볼 수 있다.
public
으로 정의된 인터페이스를 통해서만 접근이 가능하다. - 클래스는 데이터와 메서드를 하나로 결합한다.
// cpp 파일에서 클래스의 정의를 제공한다.
// = 캡슐화(세부적인 구현들을 추상화와 분리한다)
class Stock // 사용자 정의 데이터형인 class.
{
private: // 정보 은닉. 데이터 무결성을 보호한다. = 캡슐화
char company[30];
int shares;
double shareVal;
double totalVal;
void SetTotal() { totalVal = shares * shareVal; }
public: // 설계를 추상화하여 인터페이스로 제공한다
void Acquire (const char * co, int n, double pr);
void Buy (int num, double price);
void Sell (int num, double price);
void Update (double price);
void Show ();
};
- 구조체와 클래스의 차이점
- 구조체는 디폴트 접근 제어가
public
이지만, 클래스는private
이다.
- 구조체는 디폴트 접근 제어가
- 인라인 메서드
- 클래스 선언 안에 정의를 가지는 모든 함수는 자동으로 인라인 함수가 된다.
- 원한다면, 선언 외부에 함수를 정의하고
inline
키워드를 넣어서 인라인 함수로 만들 수 있다.
class Stock
{
private:
void SetTotal();
//…
};
inline void Stock::SetTotal()
{
//…
}
- 객체는 각기 자체의 데이터를 갖지만 동일한 클래스 메서드 집합을 공유한다.
Stock s1; // 각각의 데이터 공간을 가진다.
Stock s2;
s2.Show(); // 하나의 멤버 함수를 공유한다.
s2.Show();
클래스의 생성자 #
- 클래스의 데이터들은
private
접근제어를 가지고 있기 때문에 프로그램이 직접 데이터 멤버에 접근할 수 없다. 따라서struct
와 같은 방법으로 초기화할 수가 없다.
struct Student
{
char* name;
int age;
};
Student s = { "John", 12 };
Stock ss = { "Sukie", 200, 50.25 } // (X) 컴파일 에러. ss객체의 private 데이터에 접근 불가.
- 그렇기 때문에 C++은 객체를 생성할 때 자동으로 초기화하는 특별한 멤버함수인 ‘클래스 생성자’를 제공한다. 프로그램이 객체를 선언할 때 자동으로 생성자가 호출된다.
- 생성자는 리턴값을 가질 수 없으며, 명시적, 암시적으로 호출할 수 있다.
class Stock
{
public:
Stock(const string& co, int n, double pr);
//…
};
Stock s1 = Stock("Sukie", 200, 50.25); // 명시적 호출
Stock s2 ("Sukie", 200, 50.25); // 암시적 호출
Stock* s3 = new Stock("Sukie", 200, 50.25); // 동적 객체
// C++11에서 허용 (초기화 리스트를 사용)
Stock s4 = Stock{"Sukie", 200, 50.25};
Stock s5 {"Sukie", 200, 50.25};
Stock* s6 = new Stock{"Sukie", 200, 50.25};
- 디폴트 생성자는 명시적인 초기화값을 제공하지 않을 때 사용하는 생성자이다. C++는 사용자가 생성자를 제공하지 않을 때 자동으로 디폴트 생성자를 제공한다.
- 만약, 다른 생성자를 사용자가 만들었다면 디폴트 생성자를 제공하지 않는다.
Stock s1 = Stock(); // 디폴트 생성자를 사용한다. 만약 다른 생성자가 있다면 에러가 발생한다.
Stock s2;
- 디폴트 생성자 정의 방법
- 생성자의 모든 매개변수에 디폴트 값을 제공한다.
- 함수 오버로딩을 사용하여 매개변수가 없는 또 하나의 생성자를 정의한다.
- 그러나 두 가지 방법을 동시에 사용하는 것은 안 된다.
class Stock
{
public:
Stock(const string& co = "Error", int n = 0, double pr = 0.0); // 방법1
Stock(); // 방법2
//...
};
클래스의 파괴자 #
- 객체의 수명이 끝나는 시점에서 프로그램은 파괴자를 자동으로 호출한다. 파괴자는 클래스 이름 앞에 틸데(
~
)가 붙으며 매개변수와 리턴값을 가질 수 없다. - 사용자가 명시적으로 파괴자를 호출하면 안 된다. 파괴자는 컴파일러에 의해 자동으로 불려지는 것이다. 예를 들어, 정적 기억 공간에 클래스 객체를 생성한다면, 프로그램이 종료될 때 자동으로 파괴자가 호출된다.
- 파괴자는 반드시 있어야 하며, 사용자가 파괴자를 제공하지 않으면 컴파일러가 디폴트 파괴자를 선언한다.
- 이미 존재하는 객체에 생성자를 사용하면, 생성자가 새로운 임시객체를 생성하고, 새로 생성된 임시 객체의 내용이 복사된다. 그리고 임시객체의 파괴자가 호출된다.
Stock s = Stock("Sukie", 200, 50.25);
s = Stock("Nifty", 100, 20.5); // 임시 객체를 생성하고, s 객체에 복사한 후, 임시 객체를 파괴한다.
const 멤버 함수 #
- 어떤 멤버함수가 호출 객체를 변경하지 않는다고 약속하기 위해
const
를 함수 괄호 뒤에 넣는다.
class Stock
{
public:
void Show() const;
//...
};
this 포인터 #
this
포인터는 멤버 함수를 호출하는 데 사용된 객체를 지시한다. 일반적으로, 모든 클래스의 멤버 함수들은this
를 가지며,this
는 그 해당 객체의 주소로 설정된다. 따라서*this
는 해당 객체 자체를 의미한다.
class Stock
{
public:
//…
// 매개변수로 전달된 Stock 객체와 비교해서 totalVal이 높은 객체를 리턴한다
const Stock& TopValue(const Stock& s) const
{
if (s.totalVal > totalVal) // totalVal은 this->totalVal의 약식 표기이다.
return s;
else
return *this // 이 멤버함수를 호출한 객체 s1을 리턴한다
}
};
Stock s1 = Stock("Sukie", 200, 50.25);
Stock s2 = Stock("Nifty", 100, 20.5);
s1.TopValue(s2); // this 포인터를 s1 객체의 주소로 설정하고, TopValue에서 그 포인터를 사용할 수 있게 한다.
객체 배열 #
- 표준 데이터형의 배열을 선언하는 것과 동일한 방법으로 선언한다.
Stock myStocks1 [4]; // 배열 각 원소 마다 디폴트 생성자가 호출된다.
Stock myStocks2 [4] = {
Stock("NanoSmart", 12, 20),
Stock("Boffo", 200, 2.0),
Stock("Monolithic", 130, 3.27),
Stock("Fleep", 60, 6.5)
};
클래스 사용 범위 #
-
C++ 클래스는 전역 사용 범위와 지역 사용 범위와 다르게 클래스 사용 범위라는 새로운 종류의 사용 범위를 가지고 있다. 클래스 사용 범위를 갖는 것들은 클래스 안에는 알려지지만 클래스 바깥에는 알려지지 않는다.
-
그렇다면 클래스 사용 범위를 가지는 기호 상수는 어떻게 만들 수 있을까?
class Stock
{
private:
const int Months = 12;
double costs[Months]; // (X)
//…
};
- 클래스를 선언하는 것은 객체가 어떻게 생겼는지 서술하는 것이지, 그 객체를 생성하는 것은 아니다. 따라서 값을 저장할 기억 공간은 객체가 생성될 때까지 마련되지 않는다. 그렇기 때문에 위의 예제는 실패한다.
- 이러한 문제의 해결 방안은 두 가지가 있다.
- 클래스 안에 열거체를 선언한다. 단, 각 객체는 그 안에 열거체를 담지 않는다. 컴파일 할 때 클래스 사용 범위에 있는 코드에서 기호이름
Months
가 발견되면, 컴파일러는 그것을 단순히12
로 대체할 뿐이다. static
키워드를 사용하여 정적 클래스 멤버를 선언한다. 그래서 해당 클래스의 모든 객체들이 하나의 상수를 공유한다.
class Stock
{
private:
enum { Months = 12 }; // 방법 1
static const int Months = 12; // 방법 2
double costs[Months]; // (O)
//…
};
범위가 정해진 열거 (C++11) #
- 전통적인 열거 방식은 다음과 같은 경우 충돌한다는 문제점을 안고 있다.
enum Egg { Small, Medium, Large };
enum Shirts { Small, Medium, Large };
- 따라서
class
키워드를 사용해서 열거자에게 클래스 사용 범위를 갖게 함으로써 해결할 수 있다. 이 경우 열거자를 사용하려면 이름을 사용해야 한다. 그러나 이 경우int
형으로의 암시적 변환이 이루어지지 않는다.
enum class Egg { Small, Medium, Large }; // class 키워드 사용
enum class Shirts { Small, Medium, Large };
Egg e = Egg::Large; // 사용하려면 열거자 이름을 사용해야 함
Shirts s = Shirts::Large;
int number1 = e; // (X) int형으로의 암시적 변환 불가능
int number2 = int(e); // (O) 명시적 변환은 가능
- 범위가 정해진 열거의 내재적 형태는
int
형이다. 이것을 선택적으로 바꿀 수도 있다.
enum class : short Pizza { Small, Medium, Large }; // : short 는 기본형이 short형임을 의미한다.