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(); // 1Counter는 자료형 이름이고 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의 값 |
|---|---|
| a | 2 |
| b | 1 |
일반적인 비정적 멤버 변수는 객체마다 별도로 존재한다. 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;
};
#endifcounter.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() const | const 멤버 함수이며, 읽기 전용 정수 참조를 반환 |
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는 0reset() 같은 일반 함수는 호출자가 직접 호출해야 하지만, 생성자는 객체 초기화 과정에서 호출된다. 생성자를 사용하면 객체를 만든 직후부터 필요한 초기 상태를 갖추도록 할 수 있다
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(); // 90value는 함수를 호출한 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: int | private 정수형 멤버 value |
+ set(n: int): void | public 함수, int 인수를 받고 반환값 없음 |
+ get(): int | public 함수, int 반환 |
UML의 속성 표기는 이름 다음에 자료형을 적지만, C++ 선언에서는 자료형 다음에 이름을 적는다
class Value
{
private:
int value;
public:
Value() : value(0) {}
void set(int n) { value = n; }
int get() const { return value; }
};