클래스와 인스턴스

  • 내장형 데이터 외에 새로운 타입을 만들 때 클래스를 선언한다.

    • 파이썬에서는 클래스가 정의되면 하나의 독립적인 네임스페이스를 생성한다.

    • 네임스페이스란 변수가 객체를 바인딩(대입 연산자를 사용해서 저장하는 것)할 때 그 둘 사이의 관계를 저장하고 있는 공간을 의미한다.

    • 클래스 내에 정의된 변수(=클래스 변수)는 생성된 네임스페이스 안에 딕셔너리 타입으로 저장된다.

>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__']

>>> class Obj:
...     number = 1
...

>>> dir() # 클래스가 생성되었다.
['Obj', '__annotations__', '__builtins__', '__doc__', '__loader__', '__name__',
'__package__', '__spec__']

>>> Obj.__dict__ # 네임스페이스에 추가된 변수를 확인할 수 있다.
mappingproxy({'__module__': '__main__',
'number': 1,
'__dict__': <attribute '__dict__' of 'Obj' objects>,
'__weakref__': <attribute '__weakref__' of 'Obj' objects>,
'__doc__': None})

>>> Obj.number # 클래스 변수에 접근하기
1
  • 클래스는 객체의 설계도이며, 인스턴스는 이 설계도를 기반으로 메모리에서 생성되어 여러 작업을 수행한다. 이는 하나의 클래스로 여러 객체가 생성될 수 있음을 의미한다.

  • 객체를 생성하는 과정을 인스턴스화라고 하며, 생성된 객체를 인스턴스라고 부른다.

    • 파이썬은 인스턴스마다 별도의 네임스페이스를 가진다. 즉, 클래스의 네임스페이스와 인스턴스의 네임스페이스는 메모리 상에 다른 위치에 있다.

    • 어떤 인스턴스의 멤버에 접근할 때는 인스턴스의 네임스페이스를 먼저 찾아보고 없으면 클래스의 네임스페이스를 찾는다. 만약 여기에도 없다면 AttributeError 예외를 던진다.

    • 참고: 인스턴스 외부에서 멤버를 추가하는 행위를 몽키 패치(monkey patch)라고 한다.

>>> o1 = Obj()
>>> o2 = Obj()
>>> o1.__dict__
{}
>>> o2.__dict__
{}
>>> o1.number = 4 # 인스턴스 변수 생성
>>> o1.__dict__
{'number': 4}
>>> o2.__dict__
{}
>>> o2.number # 클래스 변수에 접근
1
>>> Obj.number # 클래스 변수에 접근
1
>>> o2.x
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Obj' object has no attribute 'x'
  • 인스턴스는 런타임(run-time) 환경에서 메모리 상의 서로 다른 주소를 가진다. 즉, 클래스는 같더라도 인스턴스는 다를 수 있으며, isinstance(변수명, 클래스명) 함수로 해당 변수에 바인딩된 인스턴스의 클래스 타입을 확인할 수 있다.

    • 동일 객체라는 말은 메모리 주소가 같다는 의미이다.

>>> o1 = Obj()
>>> o2 = Obj()
>>> o1 is o2
False
>>> id(o1), id(o2) # 다른 인스턴스이므로 다른 주소값이 나온다.
(4518943184, 4519522896)
>>> isinstance(o1, Obj)
True
>>> isinstance(o2, Obj)
True

 

클래스 변수와 클래스 메소드

  • 클래스 네임스페이스에 저장된 변수와 메소드로 모든 인스턴스에 대해 독립적이다.

  • 클래스 메소드는 다음과 같이 @classmethod 를 써서 선언하며, 첫번째 인자로 cls 를 받는다.

    • 참고로 첫번째 인자는 파이썬에서 자동으로 넘겨주기 때문에 별도로 매개변수로 넣지 않아도 된다.

class Obj:
    number = 3
    
    @classmethod
    def print_number(cls):
        print('{}: number = {}'.format(cls.__name__, cls.number))
  • 클래스 메소드 내부에서는 cls.변수명 또는 cls.메소드명(...) 으로 클래스 멤버에 접근할 수 있다.

  • 클래스 외부에서는 클래스 이름으로 접근이 가능하다. 

  • 인스턴스에 영향을 받지 않고, 모든 인스턴스가 공유할 필요가 있다면, 클래스 변수 및 메소드로 선언하는 것이 좋다.

>>> class Obj:
...     number = 3
...
...     @classmethod
...     def print_number(cls):
...         print('{}: number = {}'.format(cls.__name__, cls.number))
...
>>> o1 = Obj()
>>> o1.print_number()
Obj: number = 3
>>> o2 = Obj()
>>> o2.print_number()
Obj: number = 3
>>> Obj.number = 10
>>> o1.print_number()
Obj: number = 10
>>> o2.print_number()
Obj: number = 10
>>> o1.number = 1 # 인스턴스 변수에 접근 (클래스 변수가 아니다)
>>> o1.print_number()
Obj: number = 10
>>> o1.number
1
  • 클래스에 종속적이란 의미는 상속 시 상위 클래스와 하위 클래스를 구분한다는 의미이다. 상위 클래스에 선언된 클래스 메소드라 할지라도 하위 클래스의 클래스 변수에 접근한다.

 

인스턴스 변수와 인스턴스 메소드

  • 인스턴스 네임스페이스에 있는 변수와 메소드로, 인스턴스에 종속적이며, 보통 인스턴스 메소드를 일반 메소드라고 한다.

  • 인스턴스에 종속적이기 때문에 인스턴스마다 변수가 가지고 있는 값도 다르며 멤버의 메모리 주소도 다르다.

    • 따라서 인스턴스끼리 구분해서 사용하는 변수나 메소드는 인스턴스 멤버로 선언해야 한다.

  • 클래스 메소드는 클래스 멤버에만 접근 가능하며, 인스턴스 메소드는 인스턴스 멤버와 type(self) 로 클래스 멤버에도 접근할 수 있다. 즉, 클래스 정의 시 변수의 범위(scope)를 먼저 정하고, 메소드가 인스턴스 멤버에 접근해야 한다면 인스턴스 메소드로, 클래스 멤버에만 접근한다면 클래스 메소드로 선언하는 것이 좋다.

    • 예를 들어, 사람마다 이름과 나이가 다르니 이름과 나이는 인스턴스 변수로, 이름을 말하는 행위는 인스턴스 메소드로 선언해야 한다. 사람은 모두 지구에 살고 있기 때문에 거주중인 행성 이름은 클래스 변수로, 다른 행성으로 이사가는 행위는 클래스 메소드로 선언해야 할 것이다.

  • 인스턴스 메소드에서는 첫번째 인자로 self 를 받으며, 메소드 내부에서는 self.변수명 또는 self.메소드명(...)으로 접근한다.

    • 메소드 외부에서는 객체를 바인딩한 변수 이름으로 인스턴스 멤버에 접근한다.

    • self 변수를 통해 현재 메소드를 실행할 인스턴스를 구분한다.

>>> class Obj:
...     def method1(self, name):
...             print(id(self))
...             print('Hi!', name)
...
>>> o1 = Obj()
>>> o1.method1('best')
4519961600
Hi! best
>>> o2 = Obj()
>>> o2.method1('Lisa') # 인스턴스의 주소가 다르게 나온다!
4519522896
Hi! Lisa
  • 접근 제한자: 파이썬에서 공식적인 접근 제한자(Access Modifier)는 없지만, 멤버의 이름에 언더스코어를 추가하여 이를 구분한다. 일반적으로 언더스코어 1개는 private 이며, 2개는 protected, 없으면 public 이다.

    • private 은 현재 인스턴스에서만 사용 가능한 것

    • protected 는 현재 인스턴스가 상속한 부모 인스턴스에도 접근할 수 있는 것

    • public 은 외부로부터 인스턴스 멤버에 접근을 허용하는 것

 

Magic Method

  • 종류가 많기 때문에 전부 설명하기 보다는 자주 사용되는 것들 위주로 나열해보겠다. 자세한 설명은 문서를 참고.

  • __init__(self), __del__(self) 은 생성자와 소멸자, 선언하지 않으면 self만 인자로 받는 메소드가 생성된다. 따라서, 생성할 때 추가로 인자가 필요하다면 두 메소드를 다시 선언해야 된다.

>>> class Obj:
...     def __init__(self, name):
...             self.name = name
...             print('hi', name)
...     def __del__(self):
...             print('bye', self.name)
...
>>> o = Obj('Lisa')
hi Lisa
>>> del o
bye Lisa
  • __iter__(self)이터레이터(Iterator) 객체(=자기자신)를 반환하는 메소드이다. 이 메소드를 선언하면 해당 인스턴스는 이터레이션을 제공하는 이터러블(itarable) 객체가 된다.

  • __next__(self) 은 이터레이터를 제공하는 경우 다음 원소를 반환하는 메소드이다. __iter__(self) 와 함께 선언되어 자주 쓰인다. 이 메소드를 선언하는 해당 인스턴스는 이터레이터(iterator) 객체가 된다.

>>> class IterableObj:
...    def __init__(self, end):
...        self.number = 0
...        self.end = end
...    
...    def __iter__(self):
...        return self
...    
...    def __next__(self):
...        if self.number < self.end:
...            num = self.number
...            self.number += 1
...            return num
...        else:
...            raise StopIteration
...
>>> o = IterableObj(3)
>>> next(o)
0
>>> next(o)
1
>>> next(o)
2
>>> next(o)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in __next__
StopIteration

>>> for i in IterableObj(3):
    print(i, end=' ')
...
0 1 2
  • __call__(self) 은 C언어에서 operator() 함수와 동일한 역할을 한다. 파이썬의 함수는 모두 객체이며 function 클래스 타입에서 __call__ 메소드가 실행된 것이다.

>>> class Obj:
...     def __call__(self):
...             print('__call__({})'.format(id(self)))
...
>>> o = Obj()
>>> o()
__call__(4519170592)
  • __enter__(self), __exit__(self, exc_type, exc_val, exc_tb)Context Manager 로 쓰이는 with 구문을 위한 메소드이다. with 문은 프로그램에서 리소스를 사용 후 자동으로 닫아주기 때문에 파일 읽기/쓰기 작업 시 사용이 권장된다. enter은 with 문으로 열기(open) 상태일 때 exit은 닫기(close) 상태일 때 각각 호출된다.

    • 대용량 데이터를 처리할 때, 중간에 끊기는 상황이 자주 생긴다. 이때 처음부터 다시 처리하지 않고 중간부터 시작하길 원한다면 __enter__ 메소드에서 시작할 위치를 찾는 코드 및 처리결과를 기록하는 파일을 여는 코드를 작성하고 __exit__ 메소드에서 처리결과 파일을 close 하도록 처리하면 될 것이다.

>>> class Obj:
...     def __enter__(self):
...         print('__enter__({})'.format(id(self)))
...
...     def __exit__(self, exc_type, exc_val, exc_tb):
...         print('__exit__({})'.format(id(self)))
...
>>> with Obj() as o:
...     print('This is inside with statement!')
...
__enter__(4557148704)
This is inside with statement!
__exit__(4557148704)

 

정적 메소드

  • 클래스 메소드는 cls를 인자로 받아서 호출한 클래스의 네임 스페이스에 접근할 수 있다. 메소드 주소를 확인해보면 동일한 주소값이 나온다.

>>> class Obj:
...     name = 'Lisa'
...
...     @classmethod
...     def get_name(cls):
...         print(id(cls))
...         print('hi', cls.name)
...
>>> o1 = Obj()
>>> o1.get_name()
140513752745968
hi Lisa
>>> o2 = Obj()
>>> o2.get_name()
140513752745968
hi Lisa
>>> Obj.get_name()
140513752745968
hi Lisa
  • 정적 메소드는 클래스와 인스턴스 모두에 독립적인 연산을 수행할 때 사용되는 메소드이며, 첫번째 인자로 아무것도 받지 않는다. 따라서 위와 같이 클래스를 구분하는 행위를 할 수 없다. 마찬가지로 인스턴스 멤버에도 접근할 수 없다.

  • 정적 메소드가 쓰일 때는 클래스 멤버나 인스턴스 멤버에 영향을 주지 않는 연산을 수행하는 경우이다. 단, 클래스 멤버의 경우 외부에서 클래스 이름으로 접근할 수 있듯이 정적 메소드에서 클래스 이름으로 접근할 수는 있다.

    • 정적 메소드도 마찬가지로 외부에서 클래스 이름으로 접근할 수 있다.

  • 메소드 선언 시 위에 @staticmethod 를 쓴다.

>>> class Parent:
...     number = 42 # 42는 Parent 클래스의 네임스페이스에 저장된다.
...
...     @staticmethod
...     def print_number():
...         print(Parent.number) # 항상 Parent 클래스의 네임스페이스에 접근한다.
...
>>> class Child(Parent): # 자식은 부모 클래스의 네임 스페이스에 접근 가능하므로
...     number = 24 # 24는 Child 클래스의 네임스페이스에 저장된다.
...
>>> Child.print_number() # 자식으로 부모 클래스의 정적 메소드를 호출 할 수 있다.
42

 

추상 메소드

  • 추상 클래스는 메소드를 정의만하고 실제 구현은 상속받은 하위 클래스들에게 위임하는 클래스이다.

  • 다음과 같이 abc 모듈을 import 하여 추상 베이스 클래스를 상속받아 구현할 수 있다.

  • 위임할 메소드 위에는 @abstractmethod를 쓴다.

  • 상속받은 하위 클래스들은 반드시 추상 메소드를 구현해야 한다.

>>> from abc import ABC, abstractmethod
>>> class BaseClass(ABC):
...     @abstractmethod
...     def fn(self):
...         raise NotImplementedError
...

>>> class MyObject(BaseClass):
...     def fn(self):
...         pass
...
>>> o = MyObject()
>>> o.fn()

>>> class MyObject(BaseClass):
...     pass
...
>>> o = MyObject()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class MyObject with abstract methods fn
  • 주로 여러 인스턴스가 공통된 행위를 해야할 때 추상 클래스에 해당 행위(=메소드)들을 모아놓는다.

  • 여러 디자인 패턴을 구현하는데 많이 활용되며 자바의 인터페이스와 같은 역할을 한다.

  • 흔히 OOP에서는 인스턴스에서 자주 변하는 부분에 대해 느슨한 결합을 강조하는데 이는 메소드의 구현을 강제하면서 동시에 실제 구현을 하위 클래스에 위임하는 성질과 찰떡궁합이다.

>>> import abc
>>> class Person(abc.ABC):
...     @abc.abstractmethod
...     def greet(self):
...         pass
...
>>> class Doctor(Person):
...     def __init__(self, name):
...         self.name = name
...
...     def greet(self):
...         print('Hi, I am', self.name, 'and I am a doctor!')
...
>>> d = Doctor('Lisa')
>>> d.greet()
Hi, I am Lisa and I am a doctor!

>>> class Student(Person):
...     def __init__(self, age):
...             self.age = age
...
>>> s = Student(17) # 추상 메소드를 정의하지 않을 경우 TypeError 발생
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Can't instantiate abstract class Student with abstract methods greet

 

연산자 오버로딩 (Operator Overloading)

  • 기본으로 제공하는 연산자를 함수처럼 매개변수의 개수나 자료형에 따라 오버로딩하여 사용하는 것.

  • +, -, *, / 등의 기본 이항 연산자부터 =, ->, * 등의 연산자까지 대부분의 연산자에 대해 오버로딩이 가능.

  • 단, 멤버 참조 연산자(operator.)와 조건 연산자(operator?:), 그리고 범위 결정 연산자(operator::) 및 멤버 포인트 연산자(operator.*)는 오버로딩이 불가능.

  • 객체를 매개변수로 받을 때, 복사에 의한 오버헤드를 줄이고 원본 객체의 안전성을 유지하기 위해 반드시 const reference 로 넘겨야 함.

#include <iostream>
using namespace std;


class WorkHour {
private:
    int workHour;
public:
    WorkHour(int w) : workHour(w) { }
    
    void printWorkHour() {
        cout << "WorkHour: " << workHour << endl;
    }
    
    WorkHour add(const WorkHour& work) {
        WorkHour wh(this->workHour + work.workHour);
        return wh;
    }
    
    WorkHour operator+(const WorkHour& rhs) {
        WorkHour wh(this->workHour + rhs.workHour);
        return wh;
    }
};


int main() {
    WorkHour worker1(2);
    WorkHour worker2(3);
    // WorkHour worker3 = worker1.add(worker2); // new object
    WorkHour worker3 = worker1 + worker2; // same as above
    worker3.printWorkHour(); // 5
    return 0;
}
  • 연산자 오버로딩 시, 객체를 매개변수로 받을 때 해당 객체 뿐만 아니라 객체 생성자의 매개변수들만 넘겼을 경우 객체를 자동으로 생성함.

int main() {
    WorkHour worker1(2);
    // WorkHour worker2(3);
    // WorkHour worker3 = worker1 + worker2;
    WorkHour worker3 = worker1 + 3; // same as above
    worker3.printWorkHour(); // 5
    return 0;
}
  • 단, 연산자의 좌측이 객체인 경우에만 위의 연산이 가능하며, 역으로 WorkHour worker3 = 3 + worker1; 는 "'WorkHour' 형식을 사용하는 전역 연산자가 없거나 허용되는 변환이 없습니다."라는 컴파일 오류를 일으킴.

  • 연산자의 좌우에서 객체가 아니더라도 동일하게 동작하려면 다음과 같은 friend 키워드를 사용해서 오버로딩 해야 함.

#include <iostream>
using namespace std;


class WorkHour {
private:
    int workHour;
public:
    WorkHour(int w) : workHour(w) { }

    void printWorkHour() {
        cout << "WorkHour: " << workHour << endl;
    }

    friend WorkHour operator+(const WorkHour& lhs,
                              const WorkHour& rhs);
};


WorkHour operator+(const WorkHour& lhs, const WorkHour& rhs) {
    WorkHour wh(lhs.workHour + rhs.workHour);
    cout << "operator+(lhs, rhs);" << endl;
    return wh;
}


int main() {
    WorkHour worker1(2);
    WorkHour worker2(3);
    WorkHour worker3 = 3 + worker1;
    WorkHour worker4 = worker2 + 4;
    worker3.printWorkHour(); // 5
    worker4.printWorkHour(); // 7
    return 0;
}

/*
operator+(lhs, rhs);
operator+(lhs, rhs);
WorkHour: 5
WorkHour: 7
*/
  • 연산자 오버로딩 시 호출 우선순위.

#include <iostream>
using namespace std;


class WorkHour {
private:
    int workHour;
public:
    WorkHour(int w) : workHour(w) { }

    void printWorkHour() {
        cout << "WorkHour: " << workHour << endl;
    }

    WorkHour operator+(const WorkHour& rhs);
    friend WorkHour operator+(const WorkHour& lhs,
                              const WorkHour& rhs);
};


WorkHour WorkHour::operator+(const WorkHour& rhs) {
    WorkHour wh(this->workHour + rhs.workHour);
    cout << "operator+(rhs);" << endl;
    return wh;
}


WorkHour operator+(const WorkHour& lhs, const WorkHour& rhs) {
    WorkHour wh(lhs.workHour + rhs.workHour);
    cout << "operator+(lhs, rhs);" << endl;
    return wh;
}


int main() {
    WorkHour worker1(2);
    WorkHour worker2(3);
    WorkHour worker3 = 3 + worker1;
    WorkHour worker4 = worker2 + 4;
    worker3.printWorkHour(); // 5
    worker4.printWorkHour(); // 7
    return 0;
}

/*
operator+(lhs, rhs);
operator+(rhs);
WorkHour: 5
WorkHour: 7
 */

 

클래스와 객체(Class and Object)

  • C언어에서는 절차 지향 프로그래밍만 지원함. 절차 지향 프로그래밍이란 프로그램을 단순히 데이터와 이를 처리하는 방법으로 작성하는 것을 의미함.

  • C++에서는 객체 지향 프로그래밍도 지원하는데, 객체 지향 프로그래밍이란 실세계의 특정 대상을 객체로 나타내어 수많은 객체들의 상호작용으로 프로그램을 서술하는 방식을 의미함.

  • 객체(Object)의 개념

    • 객체 지향 프로그래밍에서는 데이터와 기능을 하나로 묶어서 다룰 수 있는 자료구조.

    • 프로그램을 처리하는 절차보다도 동작되는 자료에 중점을 둔 것.

    • 객체는 프로그램의 기본 단위이며, 객체 간의 상호 관계로 프로그램의 처리를 표현함.

    • 객체는 멤버 변수(특성, Attribute)와 이를 대상으로 처리하는 동작인 멤버 함수(메서드, Method)를 하나로 묶어 만든 요소

  • 구조적 프로그래밍에서는 변수와 함수가 합쳐져 프로그램을 만들지만, 객체 지향 프로그래밍에서는 멤버 변수와 멤버 함수가 합쳐져 하나의 객체를 만들고, 객체와 객체가 합쳐져 하나의 프로그램을 만듬.

  • C언어에서 구조체는, 서로 다른 타입의 변수들을 저장할 수 있는 사용자 정의 타입으로만 사용되었으며, 함수를 저장할 수 없어서 구조체를 매개변수로 받는 함수를 따로 만들어 처리해야 했음.

  • C++에서는 변수와 함수를 함께 저장할 수 있는 클래스(Class)라는 개념을 도입하였는데, 클래스를 통해 만들어지는 변수를 객체라고 부름. 참고로 구조체(struct)도 클래스로 분류되며, 함수를 저장할 수 있음.

  • 클래스는 설계도, 객체는 이 설계도를 기반으로 만들어진, 런타임(Run-time) 시 동작하는 실체로 정의. 클래스에서 객체로 바꾸는 작업을 인스턴스화라고 함.

  • 클래스 선언 방법

    • class 클래스 이름 { 멤버 변수; 멤버 함수; ... } -> 외부에서 멤버 변수나 함수에 접근하는 범위를 지정 가능

    • struct 클래스 이름 { 멤버 변수; 멤버 함수; ... } -> 외부에서 항상 멤버 변수나 함수에 접근 가능

  • 클래스의 멤버 함수

    • 함수의 내용은 클래스 내부나 클래스 외부에서 구현 가능.

    • 클래스 내부에서 구현된 멤버 함수들은 모두 인라인 함수(inline function)로 정의. 인라인 함수란 C언어에서 매크로 함수와 비슷한 개념으로 프로그램의 실행 속도를 높이기 위해 도입되었음.

    • 함수의 본체가 긴 경우, 멤버 함수를 외부에서 정의하는 것이 편리함. -> 클래스 이름::함수 이름(매개변수) {...}

  • 데이터 추상화: 현실 세계의 사물을 데이터적인 측면과 기능적인 측면을 통해서 정의하여 추상화하는 방법

    • 예를 들어, 강아지는 다리가 4개이고, 종류(말티즈 등)와 색상(흰색 등) 등의 데이터로 정의할 수 있으며, 짖을 때는 "멍멍"이라는 소리를 내며 달릴 수도 있음.

    • 다리 개수, 종류, 색상이라는 멤버 변수와 짖기, 뛰기라는 멤버 함수로 강아지라는 클래스를 작성할 수 있음.

#include <iostream>
using namespace std;

// Define class
class Dog {
public:
    int numLegs;
    string kind;
    string color;
        
    void bark();
    void run();
};

// Instantiation: create object!
int main() {
    Dog dog = { 4, "시고르자브종", "검은색" };
    dog.bark();
    dog.run();
    return 0;
}

void Dog::bark() {
    cout << kind << ": 멍멍!" << endl;
}

void Dog::run() {
    cout << numLegs << "개 다리로 후다닥!" << endl;
}
  • C++에서는 구조체를 이용해서 클래스를 정의할 수 있음.

struct Dog {
    int numLegs;
    string kind;
    string color;

    void bark(); // { cout << "멍멍!" << endl; }
    void run(); // { cout << "후다닥!" << endl; }
};

void Dog::bark() {
	cout << kind << ": 멍멍!" << endl;
}

void Dog::run() {
	cout << numLegs << "개 다리로 후다닥!" << endl;
}
  • C언어의 경우 위와 같은 데이터 추상화는 다음과 같이 표현됨.

#include <stdio.h>

typedef struct Dog {
    int numLegs;
    char kind[100];
    char color[10];
} Dog;

void bark(Dog* dog) {
    printf("%s: 멍멍!\n", dog->kind);
}

void run(Dog* dog) {
    printf("%d개 다리로 후다닥!\n", dog->numLegs);
}

int main() {
    Dog dog = { 4, "시고르자브종", "검은색" };
    bark(&dog);
    run(&dog);
    return 0;
}

 

접근 제한자(Access Modifier)

  • C++에서는 접근 제한자를 통해 클래스 외부에서 멤버 변수나 멤버 함수에 접근할 수 있는 범위를 정함.

  • private: 자신의 멤버 함수 외에는 접근할 수 없음.

  • protected: 자식 클래스의 멤버 함수로부터의 접근만 허용

  • public: 모든 곳으로부터의 접근을 허용

  • 접근 제한 정도: private > protected > public

Class Parent
{
private:
    int priv;
protected:
    int prot;
public:
    int pub;
    void accessAllMembers();
};

void Parent::accessAllMembers() {
    priv = 100; // Success
    prot = 100; // Success
    pub = 100; // Success
}

// 자식 클래스
class Child : public Parent {
public:
    void accessParents() {
        int n;
        n = priv; // Fail
        n = prot; // Success
        n = pub; // Success
    }
};

int main() {
    Parent parent;
    int n;
    n = parent.priv; // Fail
    n = parent.prot; // Fail
    n = parent.pub; // Success
}

 

정보 은닉과 캡슐화

  • 정보 은닉(Information Hiding)

    • 프로그램을 사용하는 사용자가 알아야 하는 것은 프로그램을 사용하는 방법이기 때문에, 내부 동작이나 상세 구조를 드러낼 필요는 없음.

    • 정보 은닉이란 사용자가 굳이 알 필요가 없는 정보를 숨김으로써, 최소한 정보만으로 프로그램을 쉽게 사용하도록 하는 방법.

    • C++에서는 클래스의 정보 은닉 기능을 지원하기 위해 접근 제한자 키워드를 제공함.

    • 숨길 멤버와 공개할 멤버의 블록을 구성하여 공개된 멤버는 외부에서 자유롭게 접근 가능하나 숨겨진 멤버를 직접 참조하려고 하면 오류를 발생하도록 처리.

    • 따라서 간접적으로 접근할 필요가 있을 때는 메소드를 활용.

    • Getter: 값을 읽기 위한 멤버 함수

    • Setter: 값을 쓰기 위한 멤버 함수

    • 일반적으로, 멤버 변수는 private이고 멤버 함수는 public이며, 프로그램 설계에 따라 달라질 수 있음.

class Point {
private:
    double x;
    double y;

public:
    double getX();
    double getY();
    void setX(double _x);
    void setY(double _y);
};
  • 캡슐화(Encapsulation)

    • 관련 있는 데이터와 함수를 하나의 단위로 묶는 것.

    • 정보 은닉과는 다른 개념.

// 정보 은닉 없이 캡슐화만 된 경우
class Point {
public:
    double x;
    double y;
};

 

 

생성자(Constructor)

  • 객체가 선언될 때 실행되는 코드로, 멤버 변수들을 초기화하는 역할을 함.

  • 클래스와 같은 이름을 함수의 이름으로 가지며, 인수나 리턴 값이 없는 상태로 선언.

  • 생성자는 public 이지만 싱글턴 패턴과 같이 private 으로 선언하는 경우도 존재함.

  • 생성자가 호출되는 시기

    • 객체를 만들 때: Person p;

    • 메모리를 할당할 때: Person* p = new Person;

  • 함수 오버로딩과 같은 조건으로 생성자들도 오버로딩이 가능함.

  • 디폴트 생성자(default constructor)

    • 생성자를 만들지 않을 경우 C++에서 자동으로 만들어주는 생성자.

    • 디폴트 생성자는 인수를 가지지 않지만, 멤버 변수들이 초기화됨.

    • 단, int나 char 등의 기본 타입은 초기화되지 않음.

    • 단, 생성자를 하나라도 선언할 경우 디폴트 생성자는 만들어지지 않음.

#include <iostream>

class Person {
private:
    double height;
    double weight;
public:
    Person(double _height, double _weight) {
        height = _height;
        weight = _weight;
    }
};

int main() {
    Person p(178.5, 70.8);
    Person p; // Error
    return 0;
}
  • const나 레퍼런스 멤버 변수는 선언과 동시에 초기화를 해야하므로, 디폴트 생성자가 아닌 멤버 이니셜라이저(Member Initializer)로 초기화를 해주어야 함.

class Person {
private:
    const string SSN;
    double height;
    double weight;
public:
    Person(const string _SSN, double _height, double _weight)
        : SSN(_SSN), height(_height), weight(_weight)
    { }
};

 

소멸자(Destructor)

  • 생성자가 객체를 초기화하듯, 반대로 객체가 더 이상 필요하지 않을 때 정리하는 코드.

    • 객체가 삭제될 때 파괴: Person* p = new Person(); delete p;

    • 블록 밖으로 넘어갈 때 파괴: if (true) { Person p; } // 여기서 p의 소멸자 호출

    • 다른 소멸자에 의한 파괴: A 클래스가 멤버 변수로 B 클래스를 가질 때, A의 소멸자가 호출된 후 B의 소멸자가 호출됨.

  • 생성자가 메모리를 할당하면 사용 후에는 운영체제로 리턴되어야 함.

  • 소멸자는 생성자의 이름 앞에 ~를 붙여서 정의함.

class IntPointer {
private:
    int* intPtr;
    
public:
    IntPointer() {
        pi = new int[5];
    }
    
    ~IntPointer() {
        delete[] pi;
        pi = nullptr;
    }
};
  • 생성 순서와 소멸 순서는 반대

    • Person a; Person b; 가 있으면 생성 순서는 a, b가 되고 소멸 순서는 b, a가 된다.

    • 먼저 선언한 객체가 먼저 생성

    • 먼저 선언한 객체가 나중에 소멸

#include <iostream>
using namespace std;

class A {
public:
    A() { cout << "A() called!" << endl; }
    ~A() { cout << "~A() called!" << endl; }
};

class B {
public:
    B() { cout << "B() called!" << endl; }
    ~B() { cout << "~B() called!" << endl; }
};

int main() {
    A a;
    B b;
    return 0;
}

/* output:
 A() called!
 B() called!
 ~B() called!
 ~A() called!
 */

 

this 포인터

  • 클래스에서 사용할 수 있는 특별한 포인터로, 현재 멤버 함수가 호출된 인스턴스의 메모리 주소를 가리킴.

  • 멤버 변수들과 멤버 함수들을 연결해주는 이유.

    • 멤버 변수들은 각각의 인스턴스에서 저장할 내용이 다르기 때문에 반드시 별도로 존재해야 하지만, 멤버 함수들은 인스턴스가 늘어나더라도 바뀔 필요가 없음.

    • 따라서, 프로세스 구조상 멤버 변수들이 보관되는 영역(스택, 힙, 데이터)과 멤버 함수들이 존재하는 영역(코드)은 나누어져 있음.

    • 코드 영역은 실행 중에 변경을 막기 위해 보호되어 런타임 중에는 수정될 수 없음.

    • 즉, 멤버 함수는 멤버 변수에 접근하기 위해 인스턴스를 식별할 필요가 있음.

  • 인스턴스는 독자적인 멤버 변수들을 가지지만, 클래스 공통의 멤버 함수와 매칭됨.

  • 멤버 함수를 호출하게 되면, 멤버 함수에 호출한 인스턴스의 포인터를 같이 보내고, 멤버 함수는 인스턴스의 포인터(this)를 가지고 멤버 변수들에 접근함.

  • 멤버 함수 내에서 명칭의 우선순위: 지역 변수 > 멤버 변수 > 전역 변수

#include <iostream>
using namespace std;

class Person {
private:
    double height;
    double weight;
public:
    Person() { }
    Person(double _height, double _weight)
        : height(_height), weight(_weight) { }
        
    void setHeight_wrong(double height) {
        height = height; // Not changing
    }
    
    void setHeight_1(double height) {
        this->height = height;
    }
    
    double getHeight() { return height; }
};

int main() {
    Person p(183.4, 78.5);
    p.setHeight_wrong(182.8);
    cout << p.getHeight() << endl; // 183.4
    p.setHeight(182.8);
    cout << p.getHeight() << endl; // 182.8
    return 0;
}

 

+ Recent posts