The C++ Programming Language - 객체와 클래스

an_yan_yang·2025년 2월 8일

본 글을 개인적 학습을 위한 글입니다. 틀린 내용이 있을 시 마구 지적해주시면 감사합니다.

이번 글에서는 C++에 추가된 핵심 요소인 객체와 클래스에 대해 알아보겠다.


객체

세상 모든 것은 객체이다.

캡슐화(Encapsulation)

: 객체의 본질적인 특성

→ 객체를 캡슐로 싸서 그 내부를 보호하고 볼 수 없게 함.

목적 : 객체 내 데이터에 대한 보안, 보호, 외부 접근 제한

++ 하지만 객체의 일부 요소는 공개된다.

  • 외부와의 인터페이스(정보 교환 및 통신)를 위해 객체의 일부분 공개
  • TV 객체의 경우, On / Off 버튼, 밝기 조절, 채널 조절 등 리모콘 객체와 통신하기 위함

C++의 객체

멤버 함수와 멤버 변수로 구성된다.

++ 객체는 상태(state)와 행동(behavior)으로 구성

ex) TV 객체 사례

상태 : on, off 속성 / 채널 / 음량

행동 : 켜기, 끄기, 채널 증가, 채널 감소, 음량 증가, 음량 감소


C++ 클래스와 객체

  • 클래스
    • 객체를 만들어내기 위해 정의된 설계도, 틀
    • 클래스는 객체가 아님. 실체도 아님
    • 멤버 변수와 멤버 함수 선언
  • 객체
    • 객체는 생성될 때 클래스의 모양을 그대로 가지고 있음

    • 멤버 변수와 멤버 함수로 구성

    • 메모리에 생성, 실체(Instance)라고도 부름

    • 하나의 클래스 틀에서 찍어낸 여러 개의 객체 생성 가능

    • 객체들은 상호 별도의 공간에 생성(like 변수)


C++ 클래스 만들기

  • 클래스 작성
    • 멤버 변수와 멤버 함수로 구성
    • 클래스 선언부와 클래스 구현부로 작성
  • 클래스 선언부(Class Declaration)
    • Class 키워드를 이용하여 클래스 선언
    • 멤버 변수와 멤버 함수 선언
      • 멤버 변수는 클래스 선언 내에서 초기화할 수 없음(자바와의 차이점)
      • 멤버 함수는 원형(Prototype)의 형태로 선언
    • 멤버에 대한 접근 권한 지정
      • private, public and protected 중 하나
      • Default는 private으로 되어 있음
      • public : 다른 모든 클래스나 객체에서 멤버의 접근이 가능함을 표시
  • 클래스 구현부(Class Implementation)
    • 클래스에 정의된 모든 멤버 함수 구현

클래스 선언과 구현으로 분리하는 이유는 클래스를 다른 파일에서 활용하기 위함이다.

구조체에 Accessibility를 갖게 되면 Class라 한다.



클래스

생성자(Constructor)

객체가 생성되는 시점에 자동으로 호출되는 멤버 함수

(클래스 이름과 동일한 멤버 함수)

  • 목적
    • 객체가 생성될 때 객체에 필요한 초기화를 위함(멤버 변수 초기화, 메모리 할당, 파일 열기 등)
  • 이름
    • 반드시 클래스 이름과 동일
  • 리턴 타입을 선언하지 않는다.
    • void 타입도 선언하는 것을 엄격히 금지
  • 객체 생성 시 오직 한 번만 호출
    • 자동으로 호출, 임의로 호출할 수 없음. 객체마다 생성자 실행
  • 중복 가능
    • 생성자는 한 클래스 내에 여러개 가능. 그 중 하나만 실행(Overloading 가능)
  • 선언이 되어 있지 않으면, 기본 생성자 자동으로 생성
    • 기본 생성자(Default Constructor) - 매개 변수 없는 생성자

    • 컴파일러에 의해 자동으로 생성


기본 생성자(Default Constructor)

Q : 생성자는 꼭 있어야 하는가?
A : Yes. C++ 컴파일러는 객체가 생성될 때, 생성자를 반드시 호출한다.

Q : 개발자가 클래스에 생성자를 작성해 놓지 않는다면?
A : 컴파일러에 의해 기본 생성자가 자동으로 생성된다.

Q : 만일 하나라도 작성해 놓는다면?
A : 컴파일러는 기본 생성자(Default Constructor)를 생성하지 않는다.


소멸자(Destructor)

객체가 소멸되는 시점에서 자동으로 호출되는 함수

  • 오직 한 번만 자동 호출, 임의로 호출할 수 없음

  • 객체 메모리 소멸 직전에 호출됨

  • 목적

    • 객체가 사라질 때 마무리 작업을 위함
    • 실행 도중 동적으로 할당 받은 메모리 해제, 파일 저장 및 닫기, 네트워크 닫기 등
  • 소멸자 함수의 이름은 클래스 이름 앞에 ‘~’를 붙인 형태이다.

  • 소멸자도 리턴 타입이 없고, 어떤 값도 리턴하면 안 된다.

  • 중복 불가능

    • 소멸자는 한 클래스 내에 오직 한 개만 작성 가능
    • 소멸자는 매개 변수 없는 함수
  • 소멸자가 선언되어 있지 않으면 기본 소멸자가 자동 생성

    • 컴파일러에 의해 기본 소멸자 코드 생성
    • 이는 아무 것도 하지 않고 단순 리턴만 진행한다.

객체는 생성의 반대 순으로 소멸된다.

생성자, 소멸자는 반드시 public이어야 한다.

하지만 생성자는 private로 compile error가 발생하지 않는 한 가능하다.

왜 생성자를 private으로 선언하는 지에 대해서는 추후에 알아보겠다.

(대표적 예시 - 객체 복사 방지, 생성 방지 등)


접근 지정자(Access Modifiers)

캡슐화의 목적을 달성하기 위해 사용

→ 객체 보호, 보안

  • private : 동일한 클래스의 멤버 함수에만 제한함
  • public : 모든 다른 클래스에 허용
  • protected : 클래스 자신과 상속받은 자식 클래스에만 허용

접근 지정 중복은 사용 가능하다.

class Example {
public:
    void publicMethod1() { std::cout << "Public Method 1\n"; }

private:
    int privateVar;  // private 멤버 변수

public:
    void publicMethod2() { std::cout << "Public Method 2\n"; }

protected:
    void protectedMethod() { std::cout << "Protected Method\n"; }

private:
    void privateMethod() { std::cout << "Private Method\n"; }
};

멤버 변수는 private 지정이 바람직하다.

 이는 캡슐화의 주된 목적 때문에 특히 그렇다. 특정 값을 가져오기 위한 메소드를 Getter, Accessor이라 하고, 특정 값을 수정하기 위한 메소드를 Setter, Mutator라고 한다. 이러한 메소드를 잘 이용하여 캡슐화의 원칙이 무너지지 않게 끔하는 것이 좋은 C++ 코딩 습관이 될 것이다.



클래스와 동적 할당

객체 포인터

객체에 대한 포인터는 C언어의 포인터와 동일하다.

포인터 변수는 선언하고 반드시 주소를 부여하는 것이 좋다.

  이는 C에서도 중요하게 여겨지는 원칙 중 하나이다. 포인터는 선언 후에 비어있는 채로 두지 않는 것이 좋다고 여겨진다. 이유라 함은 초기화되지 않은 포인터는 쓰레기 값을 가지게 되는데, 이를 예측하지 못하고 그대로 사용하게 되면 실행 중에 문제가 발생할 수 있다. 만일 최소한 NULL(nullptr)로 초기화하는 것은 포인터가 NULL이라면 디버깅 시 포인터가 초기화되지 않았음을 파악할 수 있다. 즉, 실행 중 문제가 발생해도 예측 가능하기 쉬워지기 때문이라고 할 수 있다.


객체 배열의 생성 및 소멸

  • C++에서 객체 배열을 선언할 수 있다. (기존과 동일한 방식)
  • 객체 배열 선언
    • 객체 배열을 위한 공간 할당
    • 배열의 각 원소 객체마다 각각의 생성자 호출(이 때, 기본 생성자만 호출 가능)
    • 기본적으로, 매개 변수를 가진 생성자를 호출할 수는 없다.
  • 배열 소멸
    • 배열의 각 객체마다 소멸자 호출, 생성의 반대순으로 소멸
  • 객체 배열 초기화 방법
    • 배열의 각 원소 객체 당 생성자를 지정하는 방법은 다음과 같다.
Circle array[3] = { Circle(10), Circle(20), Circle() };

→ 2차원 배열도 동일한 원리로 동작한다.


동적 메모리 할당 및 변환 (Memory Allocate)

참고 - Heap Memory

Heap은 운영체제가 소유하고 관리하는 메모리이다. 이는 모든 프로세스가 공유할 수 있는 메모리이기도 하다.

  • C++의 동적 메모리 할당 / 반환

    • new 연산자 (객체 할당 시 생성자 호출)
    • delete 연산자 (소멸자 호출 뒤 객체를 힙에 반환)
  • delete 후

    • 그 위치를 가리키는 포인터는 살아있다. 하지만 포인터가 가리키는 곳에 접근하는 것은 안 된다.
  • delete 사용 시 주의 사항

    • 적절하지 않은 포인터로 delete를 하면 Runtime Error가 발생한다.

    • 혹은 동일한 메모리를 두 번 반환하는 경우에도 오류가 발생한다.


배열의 동적 할당 및 반환

  • 이 또한 마찬가지로 new, delete 연산자를 사용한다.

  • 동적 할당 메모리 초기화

    • 동적 할당 시 변수를 초기화할 수 있다.
      int * p = new int(20);
    • 반면에, 배열은 동적 할당 시 초기화 불가하다.
      int ** p = new int[10](20); // Compile Error
      int ** p = new int(20)[10]; // Compile Error
  • delete 시 [] 생략 (배열에서)
    • 컴파일 오류는 아니지만 비정상적인 반환을 수행한다.

할당 해제한 포인터를 NULL(nullptr)로 초기화한다면 더 좋은 코드를 짤 수 있다.

→ 선언 후, 초기화하는 것과 같은 맥락의 이야기


메모리 누수(Memory Leak)

  프로그램을 만들게 되면 사용하지 않는 메모리가 쌓이는 경우를 볼 수 있다. 프로그래밍 계에서 사용하지 않는 메모리라 함은 할당된 메모리이지만, 포인터로 가리킬 수 없는 메모리. 즉, 접근할 수 없는 메모리를 지칭한다. 자바와 같은 일부 언어는 이 사용하지 않는 메모리를 자체적으로 Collecting하여 정리를 해주지만, C++은 개발자가 이를 직접적으로 해제해주어야 한다. 이 때 이렇게 사용하지 않는(또는 할 수 없는) 메모리가 쌓이게 된다면, C와 C++에서는 메모리 누수(Memory Leak)이라 한다.

→ 프로그램이 종료되면, 운영체제는 누수 메모리를 모두 힙에 반환한다.

 사실 main() 내에 할당된 메모리는 프로그램이 종료되면 자동으로 정리되므로 괜찮다. 하지만 main()이 아닌 특정 함수 내에서 동적 할당된 메모리를 정리하지 않는다면, 함수를 호출할 때마다 지속적으로 메모리가 쌓여서 프로그램의 메모리가 커지게 되며, 이는 문제를 일으킬 수 있다.
예를 들어, 재귀 함수가 대표적이다.


“this” 포인터

객체 자신의 포인터, 클래스의 멤버 함수 내에서만 사용 가능하다.

개발자가 선언하는 변수가 아니고 컴파일러가 선언한 변수이다.

컴파일러에 의해 묵시적으로 삽입, 선언되는 매개 변수이다.

→ 각 객체 속의 this는 다른 객체의 this와는 철저히 구분된다.

  • 매개 변수의 이름이 멤버 변수의 이름과 같은 경우 this를 사용한다.
    Class Example{
    	int a;
    	
    	void set(int a){
    		this->a = a;
    	}
    }
  • 멤버 함수가 객체 자신의 주소를 리턴할 때도 사용한다.
    Class Example{
    	Example get(){
    		return this;	
    	}
    }
  • this의 제약 사항
    • 멤버 험수가 아닌 함수에서 this 사용 불가 → 사실 당연한 이야기이다.
    • static 멤버 함수에서 this 사용 불가 → 객체가 생기기 전에 static 함수 호출이 있을 수 있기 때문에 불가 → 또한 static 멤버는 객체에 소유가 아닌 클래스의 소유이기 때문에 불가


번외

효율적인 C++ 코드 관리

클래스를 헤더 파일과 cpp 파일로 분리하여 작성한다.

  • 클래스마다 분리 저장
  • 클래스 선언부(헤더 파일에 저장)
  • 클래스 구현부(cpp 파일에 저장, 클래스가 선언된 header를 include)
  • main() 등 전역 함수나 변수는 다른 cpp 파일에 분산 저장(필요하면 선언된 header include)

모두 클래스를 재사용하기 위한 목적이다.

헤더 파일은 중복 사용이 불가능하다.

 class 파일을 분리하여 코드를 짜다보면 헤더 파일이 중복되는 문제가 발생하곤 한다. header를 include한다는 것은 컴파일 과정(중 전처리 과정)에서 그 위치에 그 파일의 내용을 붙여넣기한다는 것이다. 당연히 중복된 내용이 반복해서 include 된다면 문제가 발생할 것이다.

이를 조건 컴파일로 해결할 수 있다. → header에 설정

#ifdef EXAMPLE_H
#define EXAMPLE_H
...
#endif


  오늘은 C++에서의 클래스에 대해 압축적으로 살펴 보았습니다. 다음 글에서는 클래스의 심화적인 내용들에 대해 훑어볼 수 있도록 하겠습니다.


참고 자료

profile
개발자가 되고 싶은 공대생

0개의 댓글