1. 클래스와 객체

클래스(Class)와 객체(Object)

클래스는 데이터와 기능을 함께 정의하는 사용자 정의 자료형이다. 클래스를 바탕으로 생성한 개별 객체를 **인스턴스(Instance)**라고 한다

학생 정보를 표현한다면 이름과 점수는 객체의 상태이고, 점수를 변경하거나 조회하는 기능은 객체의 동작이다

구분객체에서의 의미C++ 클래스의 구성 요소
상태(State)객체가 보유한 데이터멤버 변수
동작(Behavior)객체가 수행하는 기능멤버 함수

C++에서 객체라는 용어는 클래스 인스턴스보다 넓은 의미로 사용하며, 정수 변수나 배열도 객체에 포함된다. 여기서는 주로 클래스 객체를 다룬다

1.1 클래스의 정의

class Counter
{
private:
    int count;
 
public:
    void reset()
    {
        count = 0;
    }
 
    void increment()
    {
        ++count;
    }
 
    int get() const
    {
        return count;
    }
};

Counter는 현재 횟수를 저장하고, 초기화·증가·조회 기능을 제공하는 자료형이다

구성 요소역할
count현재 횟수를 저장하는 멤버 변수
reset()횟수를 0으로 설정
increment()횟수를 1 증가
get()현재 횟수를 반환

클래스 정의 끝에는 세미콜론을 붙인다. class에서 접근 지정자를 생략한 멤버는 기본적으로 private이다

구조체도 멤버 함수와 접근 제어를 사용할 수 있다. C++의 struct와 class는 기본 멤버 접근 수준과 기본 상속 방식이 다르지만, 데이터와 기능을 묶는다는 점에서는 같은 기능을 제공한다

1.2 객체 생성과 멤버 함수 호출

Counter counter;
counter.reset();
counter.increment();
 
int value = counter.get(); // 1

Counter는 자료형 이름이고 counter는 그 자료형으로 만든 객체이다. 객체의 멤버 함수에는 .으로 접근한다

위 Counter에는 count의 초기값을 지정하는 생성자가 아직 없다. 따라서 값을 읽거나 증가시키기 전에 reset()으로 값을 설정해야 한다

Counter counter;
 
// counter.increment(); // count가 초기화되지 않은 상태에서 읽게 됨
counter.reset();
counter.increment();

객체를 생성할 때 필요한 초기화를 수행하는 생성자는 4절에서 다룬다

1.3 객체마다 독립적인 상태

Counter a;
Counter b;
 
a.reset();
b.reset();
 
a.increment();
a.increment();
b.increment();
객체count의 값
a2
b1

일반적인 비정적 멤버 변수는 객체마다 별도로 존재한다. a의 값을 변경해도 b의 값이 자동으로 변경되지는 않는다

같은 멤버 함수를 호출하더라도 어느 객체를 대상으로 실행하는지가 다르다. 객체마다 멤버 함수의 실행 코드까지 별도로 복사하는 것은 아니다

1.4 클래스 정의와 객체 생성의 구분

구분코드의미
클래스 정의class Counter { ... };자료형의 구성과 기능 정의
객체 생성Counter counter;해당 자료형의 개별 객체 생성
멤버 호출counter.reset();특정 객체를 대상으로 기능 실행

클래스 정의만 작성했다고 Counter 객체가 자동으로 하나 만들어지는 것은 아니다. 자료형을 정의하는 것과 개별 객체를 생성하는 것을 구분한다






2. 멤버 함수의 정의와 인라인 함수

2.1 클래스 내부와 외부에서의 정의

멤버 함수는 클래스 안에서 선언한다. 함수 본문은 클래스 안에 함께 작성하거나, 클래스 밖에 따로 작성할 수 있다

2.1.1 클래스 내부 정의

class Counter
{
private:
    int count;
 
public:
    void reset()
    {
        count = 0;
    }
};

2.1.2 클래스 외부 정의

class Counter
{
private:
    int count;
 
public:
    void reset();
};
 
void Counter::reset()
{
    count = 0;
}

Counter::reset()은 Counter에 속한 멤버 함수를 정의한다는 뜻이다. 클래스 밖에 본문을 작성했더라도 멤버 함수이므로 자신의 클래스에 있는 private 멤버에 접근할 수 있다

클래스 소속을 나타내는 Counter:: 없이 void reset()을 정의하면 별개의 일반 함수가 된다

2.2 선언과 정의의 일치

외부 정의의 반환형, 매개변수형, 함수 뒤의 const 등은 클래스 안의 선언과 맞아야 한다

class Counter
{
private:
    int count;
 
public:
    void reset();
    int get() const;
};
 
void Counter::reset()
{
    count = 0;
}
 
int Counter::get() const
{
    return count;
}

int get() const;를 선언한 뒤 int Counter::get()처럼 const를 빼고 정의하면 같은 함수의 정의가 되지 않는다

2.3 객체·참조·포인터를 통한 호출

Counter counter;
counter.reset();
 
Counter& ref = counter;
Counter* ptr = &counter;
 
int a = ref.get();
int b = ptr->get();
접근 대상사용 연산자예
객체.counter.get()
객체의 참조.ref.get()
객체를 가리키는 포인터->ptr->get()

ptr->get()은 (*ptr).get()과 같은 객체의 멤버를 사용한다. 포인터는 호출 시점에 유효한 객체를 가리켜야 한다

2.4 헤더 파일과 소스 파일의 분리

클래스의 구성과 멤버 함수 선언은 헤더에 두고, 일반적인 함수 구현은 소스 파일에 나눌 수 있다

counter.h

#ifndef COUNTER_H
#define COUNTER_H
 
class Counter
{
private:
    int count;
 
public:
    void reset();
    void increment();
    int get() const;
};
 
#endif

counter.cpp

#include "counter.h"
 
void Counter::reset()
{
    count = 0;
}
 
void Counter::increment()
{
    ++count;
}
 
int Counter::get() const
{
    return count;
}

사용하는 소스 파일에서는 counter.h를 포함한다. 실행 파일을 만들 때는 Counter의 구현이 들어 있는 목적 파일도 함께 링크해야 한다

파일역할
counter.h클래스의 구성과 호출 가능한 기능을 선언
counter.cpp멤버 함수의 동작을 정의
사용하는 소스 파일헤더를 포함하고 객체를 생성·사용

2.5 인라인 함수

인라인 함수(Inline Function)

inline으로 선언하거나 언어 규칙에 따라 인라인으로 취급되는 함수이다. 관련 조건을 만족하는 동일한 정의를 여러 번역 단위에 둘 수 있다

인라인 확장은 함수 호출 위치에 함수 본문의 동작을 반영하는 최적화이다. 인수 전달과 복귀 등에 드는 호출 비용을 줄일 수 있다

다만 inline 선언과 실제 인라인 확장은 별개이다. 컴파일러는 inline 함수의 호출을 그대로 둘 수도 있고, inline이 없는 함수의 호출을 인라인 확장할 수도 있다

2.5.1 클래스 내부에서 정의한 함수

일반적인 헤더 기반 코드에서 클래스 정의 안에 본문까지 작성한 멤버 함수는 암시적으로 인라인 함수가 된다

class Counter
{
private:
    int count;
 
public:
    int get() const
    {
        return count;
    }
};

2.5.2 클래스 외부에서 정의한 함수

class Counter
{
private:
    int count;
 
public:
    int get() const;
};
 
inline int Counter::get() const
{
    return count;
}

클래스 밖에서 정의할 때는 inline을 명시할 수 있다. 외부에 정의했다는 이유만으로 인라인 함수가 되지는 않는다

2.6 인라인 정의와 헤더의 관계

인라인 함수를 여러 소스 파일에서 사용하려면 일반적으로 헤더에 정의를 둔다. 함수를 사용하는 각 번역 단위에서 정의를 확인할 수 있도록 하기 위해서이다

함수 형태일반적인 정의 위치
클래스 내부에서 정의한 함수클래스 정의가 있는 헤더
클래스 외부에서 정의한 inline 함수헤더
클래스 외부에서 정의한 일반 비인라인 함수소스 파일

일반적인 비인라인 함수 정의를 헤더에 넣고 여러 소스 파일에서 포함하면 중복 정의로 링크 문제가 생길 수 있다

헤더 가드는 한 번역 단위 안에서의 중복 포함을 막는다. 여러 번역 단위 사이에 생긴 비인라인 함수의 중복 정의까지 해결하지는 않는다






3. 캡슐화, 정보 은닉과 const 멤버 함수

3.1 캡슐화와 정보 은닉

캡슐화(Encapsulation)와 정보 은닉(Information Hiding)

캡슐화는 관련 데이터와 기능을 하나의 단위로 묶는 것이고, 정보 은닉은 내부 구현과 상태의 직접 노출을 제한하고 필요한 인터페이스를 제공하는 것이다

class Circle
{
private:
    double radius;
 
public:
    Circle() : radius(0.0) {}
 
    void setRadius(double value)
    {
        if (value >= 0.0)
        {
            radius = value;
        }
    }
 
    double getRadius() const
    {
        return radius;
    }
};

Circle은 radius를 private으로 두고 공개 함수를 통해 다룬다. 생성 시에는 radius를 0.0으로 초기화하며, 음수 반지름을 설정하려 하면 기존 값을 유지한다

Circle circle;
circle.setRadius(3.0);
circle.setRadius(-2.0);
 
double radius = circle.getRadius(); // 3.0
// circle.radius = -2.0; // private 멤버에 외부에서 직접 접근 불가

값의 변경 조건을 클래스 내부에서 통제하면, 외부 코드마다 같은 유효성 검사를 반복하는 일을 줄일 수 있다

3.2 Getter와 Setter

구분역할예
Getter상태를 조회하는 인터페이스getRadius()
Setter상태를 변경하는 인터페이스setRadius()

모든 멤버마다 getter와 setter를 반드시 만들 필요는 없다. 외부에서 필요한 작업만 공개한다

setter가 모든 값을 그대로 대입하기만 한다면 잘못된 상태를 막는 효과는 제한적이다. 해당 클래스가 유지해야 할 조건에 맞추어 인터페이스를 설계한다

3.3 const 멤버 함수

const 멤버 함수

매개변수 목록 뒤에 const를 붙여, 그 함수를 통해 호출 대상 객체의 일반적인 비정적 멤버를 변경하지 못하도록 제한한 함수이다

double getRadius() const
{
    return radius;
}

함수 뒤의 const는 호출 대상 객체에 적용된다. 반환형 앞의 const와는 역할이 다르다

선언의미
int get() const호출 대상 객체를 읽기 전용으로 다루는 멤버 함수
const int& get() constconst 멤버 함수이며, 읽기 전용 정수 참조를 반환

const 멤버 함수에서는 같은 객체의 일반 비const 멤버 함수도 호출할 수 없다. 그 함수가 객체의 상태를 변경할 수 있기 때문이다

3.4 const 객체와 const 참조

void inspect(const Circle& circle)
{
    double radius = circle.getRadius(); // 가능
    // circle.setRadius(2.0);           // const 객체에 호출할 수 없음
}

const 객체나 const 참조를 통해서는 일반적으로 const 멤버 함수만 호출할 수 있다. 상태를 읽기만 하는 함수에 const를 붙이면 읽기 전용 인터페이스에서도 사용할 수 있다

다만 const 멤버 함수가 모든 대상을 불변으로 만드는 것은 아니다. mutable 멤버는 변경할 수 있고, 포인터 멤버가 가리키는 외부 객체도 별도의 const 제한이 없다면 변경할 수 있다






4. 생성자와 생성자 오버로딩

4.1 생성자

생성자(Constructor)

객체를 생성할 때 초기화를 수행하는 특별한 멤버 함수이다. 클래스와 이름이 같고, void를 포함하여 반환형을 적지 않는다

class Counter
{
private:
    int count;
 
public:
    Counter() : count(0) {}
    int get() const { return count; }
};
Counter counter; // 생성자가 호출되어 count는 0

reset() 같은 일반 함수는 호출자가 직접 호출해야 하지만, 생성자는 객체 초기화 과정에서 호출된다. 생성자를 사용하면 객체를 만든 직후부터 필요한 초기 상태를 갖추도록 할 수 있다

4.2 기본 생성자

기본 생성자(Default Constructor)

인수를 전달하지 않고 호출할 수 있는 생성자이다. 매개변수가 없거나 모든 매개변수에 기본 인수가 있는 생성자가 해당할 수 있다

Counter counter;   // 객체 정의
// Counter counter(); // 객체가 아니라 함수 선언

직접 생성자를 선언하지 않으면 컴파일러는 조건에 따라 기본 생성자를 암시적으로 선언한다. 하지만 기본 생성자가 있다고 해서 모든 정수와 실수 멤버를 자동으로 0으로 설정하는 것은 아니다

class Data
{
public:
    int number;
};
 
// 함수 내부에서
Data data; // number는 초기화되지 않음

4.3 매개변수가 있는 생성자

class Point
{
private:
    int x;
    int y;
 
public:
    Point(int px, int py) : x(px), y(py) {}
};
Point p(3, 4);
// Point other; // 오류: 인수 없이 호출할 생성자가 없음

사용자가 매개변수 생성자를 선언하면, 컴파일러가 인수 없는 기본 생성자를 추가해 주지 않는다. 필요하면 직접 선언한다

4.4 생성자 오버로딩

class Point
{
private:
    int x;
    int y;
 
public:
    Point() : x(0), y(0) {}
    Point(int px, int py) : x(px), y(py) {}
};
생성 코드선택되는 생성자
Point a;Point()
Point b(3, 4);Point(int, int)

일반 함수처럼 매개변수 구성에 따라 선택하며, 기본 인수와 함께 사용할 때는 모호성이 생기지 않게 한다

4.5 초기화 여부를 판단하는 기준

형태주의점
컴파일러가 제공한 기본 생성자기본 자료형 멤버가 언제나 0이 되는 것은 아님
Data() {}처럼 빈 생성자본문이 비어 있어도 정수 멤버는 초기화되지 않을 수 있음
Data() : number(0) {}number의 초기값을 명시

생성자 호출과 멤버 초기화의 구분

생성자가 호출된다는 사실만으로 모든 멤버의 초기값이 보장되지는 않는다. 각 멤버가 초기화 목록, 멤버 기본 초기값 또는 자체 생성자에 의해 어떻게 초기화되는지 확인한다






5. 멤버 초기화 목록과 동적 객체 생성

5.1 멤버 초기화 목록

멤버 초기화 목록(Member Initializer List)

생성자의 매개변수 목록 뒤에 :로 시작하여 기반 클래스와 멤버의 초기화 방법을 지정하는 부분이다. 생성자 본문보다 먼저 처리된다

#include <string>
 
class Stock
{
private:
    std::string company;
    long shares;
    double price;
 
public:
    Stock(const std::string& co, long n, double p)
        : company(co), shares(n), price(p)
    {
    }
};

5.2 초기화와 본문 대입

작성 위치처리
: company(co)company를 co로 직접 초기화
본문의 company = co;company가 먼저 초기화된 뒤 대입

기본 생성자가 없는 클래스형 멤버, 참조 멤버, const 정수형 멤버 등은 본문에서 나중에 대입하는 방식으로 초기화를 대신할 수 없다

class A
{
public:
    A(int value) {}
};
 
class B
{
private:
    A member;
 
public:
    B(int value) : member(value - 1) {}
};

A에는 기본 생성자가 없으므로 B가 생성될 때 member에 필요한 인수를 전달해야 한다

5.3 초기화 순서

비정적 데이터 멤버는 클래스에 선언한 순서대로 초기화된다. 초기화 목록에 적은 순서로 바뀌지 않는다

class Pair
{
private:
    int first;
    int second;
 
public:
    Pair(int value) : first(value), second(first + 1) {}
};

선언 순서와 초기화 목록 순서를 맞추면 의존 관계를 확인하기 쉽다

5.4 동적으로 객체 생성

Point* a = new Point;
Point* b = new Point(3, 4);
 
delete a;
delete b;

4.4절의 Point를 기준으로 첫 번째는 기본 생성자, 두 번째는 매개변수 생성자를 호출한다

표현처리
new T공간을 확보하고 T를 기본 초기화
new T(args)공간을 확보하고 인수에 맞게 초기화
delete ptr객체 소멸 후 저장 공간 해제

객체의 초기화와 저장 공간 할당은 구분해야 한다. new 표현식은 저장 공간 확보와 객체 초기화를 함께 처리하며, 생성자는 객체를 초기화하는 역할을 맡는다






6. 소멸자와 자원 관리

6.1 소멸자

소멸자(Destructor)

객체의 수명이 끝날 때 정리 작업을 수행하는 특별한 멤버 함수이다. 이름은 ~클래스이름이고, 반환형과 매개변수가 없다

class Counter
{
public:
    Counter() {}
    ~Counter() {}
};

생성자와 달리 인수가 다른 여러 소멸자를 오버로딩할 수 없다

6.2 소멸자가 호출되는 시점

객체소멸 시점
자동 지역 객체해당 블록을 벗어날 때
new로 생성한 객체대응하는 delete 실행 시
객체 배열배열 소멸 시 각 원소 소멸
멤버 객체바깥 객체의 소멸자 본문 실행 후

같은 블록의 자동 지역 객체는 일반적으로 생성의 역순으로 소멸한다. delete로 객체를 제거한 뒤 같은 객체를 다시 삭제하면 안 된다. 소멸자를 직접 호출하는 방식은 일반적인 객체 사용에서는 필요하지 않다

6.3 소유한 동적 메모리 정리

#include <cstddef>
 
class DoubleArray
{
private:
    double* data;
    std::size_t size;
 
    // 같은 배열을 중복 해제하지 않도록 복사를 금지
    DoubleArray(const DoubleArray&) = delete;
    DoubleArray& operator=(const DoubleArray&) = delete;
 
public:
    DoubleArray(std::size_t n) : data(new double[n]()), size(n) {}
    ~DoubleArray() { delete[] data; }
};

객체가 배열을 소유한다면 소멸자에서 해당 배열을 해제할 수 있다. = delete는 C++11 이상 문법이며 위 코드에서는 복사로 인한 중복 해제를 방지한다. 복사 생성자와 대입 연산자의 자세한 구현은 별도로 구분해서 다룰 내용이다

소유 포인터가 있는 객체의 기본 복사

포인터 멤버의 기본 복사는 주소만 복사한다. 두 객체가 같은 공간을 소유한 것으로 처리하여 각각 소멸자에서 해제하면 중복 해제가 발생한다. 복사를 금지하거나 적절한 복사 정책을 구현해야 한다

std::string처럼 자원을 스스로 관리하는 멤버는 자신의 소멸자가 정리한다. 모든 멤버에 delete를 적용하는 것이 아니며, 소유하지 않은 포인터도 임의로 해제하면 안 된다






7. this 포인터와 자기 참조 반환

7.1 this 포인터

this 포인터

비정적 멤버 함수 안에서 그 함수를 호출한 객체를 가리키는 포인터이다

클래스 T의 일반 비const 멤버 함수에서 this의 형식은 T이고, const 멤버 함수에서는 const T이다. static 멤버 함수는 특정 객체 없이 호출할 수 있으므로 this가 없다

7.2 멤버와 매개변수의 이름 구분

class Rect
{
private:
    int width;
    int height;
 
public:
    Rect() : width(1), height(2) {}
 
    void setValues(int width, int height)
    {
        this->width = width;
        this->height = height;
    }
};
표현의미
width같은 이름의 매개변수
this->width호출한 객체의 멤버

width = width;라고만 쓰면 매개변수를 자기 자신에게 대입하므로 객체의 width가 바뀌지 않는다

7.3 this와 *this

표현의미
this현재 객체의 주소
*this현재 객체 자체

동일한 클래스의 두 객체가 멤버 함수를 호출하면 각 호출에서 this가 가리키는 대상이 다르다

7.4 자기 참조 반환과 연속 호출

class Counter
{
private:
    int value;
 
public:
    Counter() : value(0) {}
 
    Counter& add(int n)
    {
        value += n;
        return *this;
    }
 
    int get() const { return value; }
};
Counter counter;
counter.add(1).add(2).add(3);
// counter.get()은 6

반환형이 Counter&이므로 매 호출에서 같은 객체를 돌려받는다. Counter처럼 값으로 반환하면 후속 호출은 복사된 별도 객체에 적용될 수 있어 원본의 결과가 달라진다

반환한 참조는 원본 객체가 살아 있는 동안만 유효하다

7.5 다른 객체와 비교하고 참조 반환

멤버 함수는 호출 대상 객체와 인수로 전달한 객체를 비교할 수도 있다

class Score
{
private:
    int value;
 
public:
    Score(int n) : value(n) {}
 
    const Score& higher(const Score& other) const
    {
        if (value >= other.value)
        {
            return *this;
        }
        return other;
    }
 
    int get() const { return value; }
};
Score a(80);
Score b(90);
const Score& result = a.higher(b);
int value = result.get(); // 90

value는 함수를 호출한 a의 멤버이고, other.value는 인수로 받은 b의 멤버이다. 같은 클래스의 멤버 함수는 다른 객체의 private 멤버에도 접근할 수 있다

위 higher는 값이 큰 객체를 새로 복사하지 않고 참조로 반환한다. result를 사용하는 동안 선택된 원본 객체가 살아 있어야 하므로, 수명이 곧 끝날 임시 객체를 전달한 뒤 반환 참조를 보관하는 방식은 피한다






8. 객체 배열과 UML 클래스 다이어그램

8.1 객체 배열

객체 배열(Array of Objects)

클래스 객체를 원소로 저장하는 배열이다. 각 원소는 독립적으로 생성되고 소멸한다

class Item
{
private:
    int value;
 
public:
    Item() : value(0) {}
    Item(int n) : value(n) {}
    int get() const { return value; }
};
 
Item items[3] = {Item(10), Item(20), Item(30)};

items[i].get()처럼 원소를 선택한 뒤 멤버 함수를 호출한다

8.2 생성 시 초기화와 생성 후 대입

Item a[2] = {Item(10), Item(20)};
 
Item b[2];
b[0] = Item(10);
b[1] = Item(20);
코드과정
a각 초기값으로 원소를 초기화
b기본 생성자로 두 원소 생성 후 대입

Item b[2]와 new Item[n]처럼 초기값 없이 객체 배열을 만들려면 접근 가능한 기본 생성자가 필요하다. 일부 원소의 초기값만 지정한 배열에서도 남은 원소를 빈 초기값으로 생성할 수 있어야 한다

배열 원소는 인덱스가 작은 순서로 생성되고 역순으로 소멸한다

8.3 동적 객체 배열

Item* items = new Item[3];
items[0] = Item(10);
items[0].get();
delete[] items;

객체 배열과 객체 포인터 배열의 차이는 앞서 다룬 다단계 동적 할당과 같다

형태저장 내용멤버 호출해제
Item* p = new Item[n]객체 n개p[i].get()delete[] p
Item** p = new Item*[n]()포인터 n개객체 생성 후 p[i]->get()소유 객체 개별 delete 후 배열 delete[]

8.4 UML 클래스 다이어그램

UML 클래스 다이어그램

클래스의 이름, 속성, 연산과 클래스 사이의 관계를 시각적으로 표현하는 표기이다

클래스 상자는 일반적으로 위에서부터 클래스 이름, 속성, 연산의 세 구역으로 나눈다

표기의미
+public
-private
#protected
- value: intprivate 정수형 멤버 value
+ set(n: int): voidpublic 함수, int 인수를 받고 반환값 없음
+ get(): intpublic 함수, int 반환

UML의 속성 표기는 이름 다음에 자료형을 적지만, C++ 선언에서는 자료형 다음에 이름을 적는다

class Value
{
private:
    int value;
 
public:
    Value() : value(0) {}
    void set(int n) { value = n; }
    int get() const { return value; }
};