Coding/Unreal, C++

[C++ Primer Plus] 10. Objects and Classes

답해야 할 질문

  • new와 malloc()의 차이가 무엇인가
  • Abstraction, 추상화란 무엇인가
  • inline 메소드는 어떻게 작동하는가
  • enum class는 enum과 무엇이 다른가, 왜 필요한가

여는 글

OOP는 프로그램 디자인에 대한 하나의 방법일 뿐이다. C++은 프로그래머가 OOP적 접근을 더욱 쉽게 할 수 있도록 여러 기능을 추가했다.

  • Abstraction
  • Encapsulation and data hiding
  • Polymorphism
  • Inheritance
  • Reusability of code

class는 C++가 이러한 특징들을 한데모아 구현한 집합체다.

Procedural and Object-Oriented Programming

  • 절차지향적 접근은 먼저 어떤 일을 해야할 지 결정하고 나서 어떻게 데이터를 나타내야 할지 생각한다.
  • OOP적 접근은 유저의 인식하는 오브젝트를 생각한다. 그후에 유저가 이 오브젝트를 잘 쓸 수 있도록 오브젝트에 어떤 데이터와 기능을 담아야 하는지 생각한다.

Abstraction and Classes

  • 현실은 아주 복잡하다. 이 복잡함을 해결하는 방법 중 하나가 추상화를 통해 단일 개체를 만들어내는 것이다. 인간은 수많은 원자들의 결합이다. 하지만 인간을 그렇게만 이해하면 어렵다. 우리는 인간을 그저 한 명이라고만 이해한다. 그게 쉬우니까.
  • 프로그래밍에서도 이 '추상화'는 중요하다. 프로그래머는 유저가 오브젝트를 완전히 이해하지 않아도 상호작용할 수 있도록 인터페이스를 만든다. 

What is a Type?

  • Type을 선언하는 것은 다음의 3가지를 함께 선언하는 것과 같다.
    1. Data object를 만드는 데 드는 메모리
    2. 각 메모리 비트가 어떻게 해석돼야 하는지 (long으로 해석해야 하는지, float로 해석해야 하는지)
    3. operation, method 등 Data object를 가지고 어떤 일을 할 수 있는지 

Classes in C++

  • class는 추상화를 유저가 정의한 타입으로 만드는 수단이다.
  • interface는 두 시스템 사이에 공유되고 있는 프레임워크다.

Access Control

  • private와 public이라는 키워드도 추가됐다. 해당 키워드는 클래스 멤버에 대한 access control을 나타낸다.
  • data에 대한 직접 접근을 막는 것이 data hiding이다.
  • data hiding은 data에 대한 직접 접근을 막을 뿐만이 아니라 data가 어떻게 나타내지고 있는지 사용자가 알 필요가 없게 한다.

Implementing Class Member Functions

  • 멤버 함수를 정의할 때는 scope-resolution operator(::)를 사용해서 해당 함수가 어떤 클래스에 속해 있는지 서술해야 한다.
  • 클래스 메소드는 해당 클래스의 private 멤버에 접근할 수 있다.

Inline Methods

  • 함수의 호출 과정:
    1. 스택에 함수로 전달할 매개변수와 함수가 끝난 뒤 돌아갈 주소값을 저장한다.
    2. 프로그램의 제어가 함수의 위치로 넘어와 함수 내에 선언된 지역 변수도 스택에 저장한다.
    3. 함수가 끝나면 반환값을 넘겨 준다.
    4. 프로그램의 제어는 스택에 저장된 돌아갈 반환 주소값으로 이동하여, 스택에 저장된 함수 호출 정보를 제거한다.
  • 문제는 생각보다 함수는 비싸다는 것이다.
  • 하지만 다음과 같은 상황을 생각해보자.
    • 여러가지 이유로 간단한 계산식을 함수로 만들어야 한다.
    • 해당 함수는 여러번 호출될 가능성이 있다.
  • 이러면 프로그래머는 해당 함수 머리에 inline 키워드를 넣어두면 된다.
  • inline 함수는 macro 함수와 유사하게 동작한다. 해당 함수를 함수 안 코드로 치환하는 것이다. 다만 매크로는 전처리기가 처리하지만 inline은 컴파일타임에 컴파일러가 코드를 생성한다.
  • 가장 중요한 사실은 요즘 컴파일러는 똑똑해서 대충 알맞게 inline을 붙여준다.

Class Constructors and Destructors

  • struct에서 list initialization을 배웠다. 이걸 클래스에도 적용할 수 있을까?
struct thing
{
	char* pn;
    int m;
};

class Stock
{
public:
	char* pn;
    int m;
}

thing t = {"wodget", -23}; // VALID
Stock s = {"wodget", -23}; // INVALID
  • 이게 안되는 이유는 Stock 클래스에 적절한 생성자를 정의하지 않았기 때문이다. 때문에 다음과 같이 정의하면 list initialization을 쓸 수 있다.
class Stock
{
public:
	Stock(char* str, int m)
    {
    	this->str = str;
        this->m = m;
    }
private:
	char* str;
    int m;
};

Stock hot_tip = {"Hello", -23}; // C++11
  • 생성자를 이용해 객체를 생성하는 방법은 다양하다. 한데 모아서 보자.
Stock st = Stock("hello", -23);
Stock st("hello", -23);
Stock* ptr = new Stock("hello", -23);
// C++11 way
Stock st = {"hello", -23};
Stock st {"hello", -23};

Default Constructors

  • 객체를 생성할 때, 따로 입력값을 주지 않으면 Default Constructor가 호출된다.
  • 클래스 명세에 Default Constructor를 안 넣고 다른 생성자를 구현했으면 Default Contructor 호출이 금지된다.
  • Default Constructor는 괄호를 쓰지 않아도 된다.
Stock* ptr = new Stock; // valid

Destructors

  • Constructor가 new를 통해 객체를 생성할 때 호출된다면, Destructor는 delete 연산자로 객체를 삭제할 때 호출된다.
  • Destructor는 생성자처럼 반환값 없이 클래스 이름으로 함수를 만들고 앞에 ~를 붙여준다. 매개변수는 받지 않는다.
Stock::~Stock()
{
	std::cout << "Bye bye~\n";
}

const Member Functions

class Stock
{
public:
	void show();
}

const Stock st = Stock("Hello", -23);
st.show(); // Invalid
  • st.show()가 안되는 이유는 st가 const라서 생성 이후로 어떤 변경도 있으면 안되는데, show가 그것을 보장하지 못하기 때문이다. (실제로 클래스 멤버 변수를 바꾸지 않더라도)
  • const를 유지하는, 즉 일관성을 유지한다는 보장을 하기 위해서 함수 명세 뒤에 const를 붙일 수 있다. 
void show() const;

An Array of Objects

  • 다음과 같이 객체 배열을 선언할 수 있다.
Stock myStuff[4];
  • 이렇게 하면 Default Constructor가 호출된다. 매개변수를 통한 Constructor를 호출하려면 다음과 같이 배열을 선언해야 한다.
Stock stocks[STKS] = {
	Stock("aa", 1),
    Stock("bb", 2),
    Stock("cc", 3),
    Stock("dd", 4)
};
  • C에는 local scope와 global scope가 있었다. C++은 새로운 class scope가 추가됐다.
  • class scope가 있기 때문에 다른 클래스에서 같은 변수 이름을 쓸 수 있다.

Class Scope Constants

  • 클래스에서 상수(constant)가 있으면 편한 경우가 있다. 예를 들어 다음 클래스를 보자.
class Bakery
{
private:
	const int Months = 12; // declare a constant? FAILS
    double costs[Months];
    ...
}
  • 이렇게 class scope에서 상수를 만드는 방법은 두가지가 있다.
    1. static const로 변수를 선언한다.
    2. enum을 활용한다.
  • enum을 활용해서 상수를 만드는 방법은 다음과 같다.
class Bakery
{
private:
	enum {Month = 12};
    double cost[Months];
    ...
}

Scoped Enumerations (C++11)

  • 전통적인 Enumeration은 여러 문제가 있다. 그 중 하나가 다른 enum에서 같은 이름으로 정의하면 충돌한다는 것이다.
enum egg {Small, Medium, Large, Jumbo};
enum t_shirt {Small, Medium, Large, Xlarge};
  • 충돌하는 이유는 두 enum이 같은 scope에 있기 때문이다. (scope의 종류 중엔 enum scope는 없다)
  • C++11은 이를 위해 enum class를 제공한다. 즉, class scope를 사용해서 해결한다. (enum struct도 동일하다)
enum class egg {Small, Medium, Large, Jumbo};
enum class t_shirt {Small, Medium, Large, XLarge};

egg choice = egg::Large;
t_shirt Floyd = t_shirt::Large;
  • 이외에도 enum class는 중요한 기능을 제공한다.
    1. 암시적 형변환을 금지한다.
    2. 기본적으로 enum은 내부적으로 int로 구현되는데, 이 자료형을 바꿀 수 있다.
int Frodo = t_shirt::Medium; // Error
int Frodo = int(t_shirt::Medium); // OK

enum class : short pizza {Small, Medium, Large, XLarge};

Abstract Data Types

  • Class를 잘 이용하는 방법 중 하나가 ADT를 이용하는 것이다. 어떤 코드들은 타입이 달라져도 거의 동일하게 작동한다. 이 특징을 이용한다.
typedef unsigned long Item;

class Stack
{
private:
	enum {MAX = 10};
    Item items[MAX];
    ...
};

 

참고