클래스 정의와 객체 생성
클래스와 멤버 함수 정의를 구분하고 객체의 저장 기간·수명·소유권에 맞는 RAII 생성 방식과 멤버 접근법을 익힙니다.
이번 장에서는 클래스를 실제로 어떻게 정의하고, 정의된 클래스로 객체를 생성하는 방법을 더 자세히 다룹니다.
특히 객체 생성 시 저장 기간(storage duration)과 수명(lifetime), 소유권(ownership)을 구분하고, 객체·참조·포인터 표현식에 맞는 멤버 접근 방법을 다룹니다.
C++ · object lifetime · ownership flowchart
저장 위치의 별명보다 수명과 소유권을 먼저 결정한다
class Person { ... };은 타입을 정의하고, Person::introduce()는 클래스 밖 멤버 함수 정의입니다. 이 정의들만으로 객체별 저장 공간이 생기지는 않습니다. 객체가 필요할 때 수명과 소유권을 따라 생성 방식을 고릅니다.
class Person { ... };과 멤버 함수 정의를 구분한다
클래스 정의는 타입의 멤버 구성을 완성하고, Person::introduce()는 그 멤버 함수 본문을 정의합니다. 정의만 작성한 단계에는 객체별 저장 공간이 없습니다.
현재 블록의 지역 값으로 충분한가?
YES →
Person p1{...};처럼 automatic storage duration 지역 객체를 둡니다. 초기화가 끝나면 수명이 시작되고 블록을 벗어날 때 소멸합니다. NO → 값 반환이나 상위 객체의 멤버로 수명을 표현할 수 있는지 먼저 확인하고, 동적 소유가 필요하면 2번으로 갑니다.가변 개수의 연속 값 컬렉션이고, 요소 주소 안정성이나 다형적 간접 소유가 불필요한가?
YES →
std::vector<Person>이 요소의 동적 저장 공간과 수명을 관리하게 합니다. 재할당은 기존 요소의 참조, raw pointer와 iterator를 모두 무효화합니다. NO → 요구에 맞는 다른 소유 구조를 검토한 뒤 3번으로 갑니다.둘 이상의 owner가 실제로 공동 소유해야 하는가?
YES →
std::shared_ptr<Person>를 쓰고 마지막 owner가 사라질 때 정리합니다. 순환은std::weak_ptr로 끊습니다. NO → DEFAULTstd::unique_ptr<Person>로 단일 소유를 표현하고 이동으로만 넘깁니다.
automatic과 dynamic은 저장 기간의 분류이고, 소유권은 누가 수명 종료를 책임지는지에 대한 코드 계약입니다. 값 반환·이동이나 상위 owner의 멤버로 충분하면 동적 할당은 필요하지 않으며, 구현이 흔히 쓰는 “스택/힙”이라는 물리적 별명만으로 선택하지 않습니다.
클래스 정의하기 (Class Definition)
클래스 정의는 클래스의 이름과 멤버 구성을 완성합니다. class Person { ... };은 완전한 클래스 정의이고, class Person;처럼 본문이 없는 형태만 전방 선언입니다.
일반적으로 클래스 정의는 .h (헤더) 파일에 작성하여 다른 소스 파일에서 포함(include)하여 사용할 수 있도록 합니다.
클래스 정의 안에는 멤버 함수 선언만 두고, 멤버 함수의 본문인 멤버 함수 정의는 보통 .cpp (소스) 파일에 작성할 수 있습니다.
#pragma once // 한 번만 포함되도록 지시
#include <string> // std::string을 위해
// Person 클래스 정의
class Person {
private: // private 멤버 변수
std::string name;
int age;
public: // public 멤버 함수
// 생성자
Person(std::string n, int a);
// 멤버 함수 선언 (프로토타입)
void setName(std::string n);
void setAge(int a);
std::string getName() const;
int getAge() const;
void introduce() const;
};#include "Person.h" // Person 클래스 정의를 포함
#include <iostream> // std::cout, std::cerr를 위해
// 생성자 정의
// '클래스이름::' 스코프 결정 연산자를 사용하여 Person 클래스의 생성자임을 명시
Person::Person(std::string n, int a) : name(n), age(0) {
setAge(a); // 유효성 검사를 거쳐 age를 설정
}
// 멤버 함수 정의 (클래스이름::멤버함수이름 형태로 정의)
void Person::setName(std::string n) {
name = n;
}
void Person::setAge(int a) {
if (a >= 0) { // 간단한 유효성 검사
age = a;
} else {
std::cerr << "오류: 나이는 음수가 될 수 없습니다." << std::endl;
}
}
std::string Person::getName() const {
return name;
}
int Person::getAge() const {
return age;
}
void Person::introduce() const {
std::cout << "안녕하세요, 저는 " << name << "입니다. 나이는 " << age << "살입니다." << std::endl;
}#pragma once: 주요 컴파일러가 널리 지원하며, 지원되는 구현에서는 헤더가 한 번만 포함되게 합니다. 다만 C++ 표준 지시어는 아니고#pragma의 동작은 구현 정의입니다.- 스코프 결정 연산자 (
::): 멤버 함수를 클래스 외부에서 정의할 때, 이 함수가 어느 클래스에 속하는지를 명시하기 위해클래스이름::을 사용합니다. 예를 들어,Person::setName은Person클래스에 속한 멤버 함수의 정의임을 나타냅니다.
객체 생성하기 (Object Creation)
클래스가 정의되면 그 타입으로 실제 객체를 생성할 수 있습니다. 클래스 정의 자체와 멤버 함수 정의는 객체의 형식과 동작을 준비할 뿐, 객체별 저장 공간을 만들지는 않습니다.
C++ 표준은 흔히 말하는 “스택 객체/힙 객체”보다 automatic storage duration과 dynamic storage duration 같은 저장 기간으로 수명 경계를 설명합니다. 블록 범위 지역 객체는 automatic storage duration이 일반적이고, 동적 객체는 RAII 소유 객체나 컨테이너가 정리 책임을 나타내게 합니다.
블록 범위 지역 객체 (Automatic Storage Duration)
가장 단순한 기본 선택은 블록 범위에 객체를 직접 선언하는 것입니다.
선언문이 실행되어 초기화가 끝나면 객체의 수명이 시작되고, 그 블록을 벗어날 때 소멸자가 자동으로 호출됩니다. 구현에서 호출 스택을 쓰는 경우가 흔하지만, C++ 언어 계약의 핵심은 물리적 위치가 아니라 automatic storage duration과 블록 경계입니다.
#include "Person.h"
void greetOnce() {
Person person("김영희", 25);
person.introduce();
} // person의 소멸자가 자동으로 호출됨Person 객체 생성
#include "Person.h" // Person 클래스 정의 포함
int main() {
// 1. Person 클래스의 블록 범위 지역 객체 p1 생성
// (Person::Person("김영희", 25) 생성자가 호출됨)
Person p1("김영희", 25);
// 객체의 멤버 함수 호출
p1.introduce(); // 출력: 안녕하세요, 저는 김영희입니다. 나이는 25살입니다.
// 멤버 변수 값 변경 (세터 함수 사용)
p1.setAge(26);
p1.introduce(); // 출력: 안녕하세요, 저는 김영희입니다. 나이는 26살입니다.
// 2. 독립된 상태를 가진 또 다른 Person 객체 p2 생성
Person p2("이철수", 30);
p2.introduce(); // 출력: 안녕하세요, 저는 이철수입니다. 나이는 30살입니다.
// main 블록이 끝나면 p2, p1의 역순으로 자동 소멸
return 0;
}동적 저장 기간 객체와 RAII
객체가 현재 블록보다 오래 살아야 한다면 먼저 값 반환·이동이나 상위 owner의 멤버로 수명을 표현할 수 있는지 확인합니다. 안정된 간접 소유가 필요하거나 실행 중에 개수가 달라지는 경우에는 동적 저장 기간이 유용할 수 있습니다. 이때 raw owning new/delete를 직접 짝맞추기보다 std::unique_ptr, 필요한 경우의 std::shared_ptr, 또는 std::vector처럼 소유권과 정리를 표현하는 RAII 타입을 사용합니다.
단일 동적 객체는 기본적으로 std::make_unique로 생성해 한 소유자를 명확히 합니다. 가변 개수의 객체를 값으로 연속 저장하고 stable identity·address나 다형적 간접 소유가 필요하지 않다면 std::vector가 요소 저장 공간과 수명을 관리하게 합니다.
std::vector가 재할당하면 기존 요소를 가리키던 참조, raw pointer와 iterator는 모두 무효화됩니다. 요소의 주소 안정성이 필요하다면 관찰자를 오래 보관하지 않거나 요구에 맞는 다른 소유 구조를 선택해야 합니다.
#include "Person.h"
#include <memory>
void greetLater() {
auto owner = std::make_unique<Person>("박영희", 22);
owner->introduce();
} // owner 소멸과 함께 소유한 Person도 자동 정리std::unique_ptr와 std::vector로 수명 관리
#include "Person.h" // Person 클래스 정의 포함
#include <memory>
#include <vector>
int main() {
// 1. unique_ptr가 동적 Person 객체를 단독 소유
auto p3 = std::make_unique<Person>("박영희", 22);
// smart pointer는 화살표 연산자로 멤버에 접근
p3->introduce(); // 출력: 안녕하세요, 저는 박영희입니다. 나이는 22살입니다.
p3->setAge(23);
p3->introduce(); // 출력: 안녕하세요, 저는 박영희입니다. 나이는 23살입니다.
// 2. vector가 가변 개수 Person 요소의 저장 공간과 수명을 관리
std::vector<Person> people{
Person("첫째", 10),
Person("둘째", 12),
Person("셋째", 14)
};
people[0].introduce();
people[1].introduce();
// 블록 종료 시 vector의 요소와 p3의 Person이 RAII로 정리됨
return 0;
}- 점 연산자 (
.): 객체 표현식(p1.introduce())과 참조 표현식(ref.setAge(26))에 사용합니다. - 화살표 연산자 (
->): raw pointer나 smart pointer 표현식에 사용합니다. built-in pointer에서p->member는(*p).member와 같은 의미이고, smart pointer는 자신의operator->를 제공합니다. 실제 접근 시 raw pointer는 살아 있는 객체를 가리켜야 하며,unique_ptr·shared_ptr는 비어 있지 않아get() != nullptr여야 합니다.
객체, 참조, raw pointer, smart pointer가 같은 Person을 다룰 수 있어도 멤버 접근 문법과 소유권 계약은 다릅니다.
C++ · member access · semantic table
왼쪽 표현식의 형식이 .과 ->를 결정한다
객체와 참조에는 ., raw pointer와 smart pointer에는 ->를 사용합니다. 접근 문법은 유효성을 보장하지 않습니다. raw pointer는 살아 있는 객체를 가리켜야 하고, smart pointer는 비어 있지 않아야 하며, 관찰자는 lifetime·invalidation 계약 안에서만 사용합니다.
| 왼쪽 표현식 | 멤버 접근 | 소유권과 수명 | 지켜야 할 계약 |
|---|---|---|---|
객체 · Person p1 | p1.introduce() | 값 자체가 Person 객체입니다. 예시의 블록 범위 지역 객체는 automatic storage duration이며 블록을 벗어날 때 소멸합니다. | 점 연산자는 저장 기간을 뜻하지 않습니다. 다른 storage duration의 객체 표현식에도 같은 .을 사용합니다. |
참조 · Person& ref = p1 | ref.setAge(26) | 기존 객체의 non-owning 별명입니다. 일반 lvalue reference는 null이거나 다른 객체로 다시 묶일 수 없습니다. | 선언의 = p1은 초기 바인딩입니다. 이후 ref = other는 재바인딩이 아니라 p1 객체에 대입하며, 참조는 대상 수명을 연장하지 않습니다. |
raw pointer · Person* observer = &p1 | observer->introduce() | 타입만으로 소유권을 표현하지 않습니다. 현대 C++에서는 보통 null 또는 재대입 가능한 non-owning 관찰자로 제한합니다. | null은 직접 검사할 수 있지만, non-null은 대상 수명을 증명하지 않습니다. dangling은 lifetime·container invalidation 계약으로 예방합니다. built-in p->m은 (*p).m과 같습니다. |
smart pointer · std::unique_ptr<Person> | owner->introduce() | unique_ptr는 단일 소유를, 필요한 경우의 shared_ptr는 공동 소유를 표현합니다. | operator->의 전제는 get() != nullptr입니다. owner가 정리된 뒤 꺼내 둔 raw pointer나 참조는 dangling일 수 있습니다. |
Person p1 → p1.introduce()
값 자체가 객체이므로 .을 씁니다. 예시의 블록 범위 지역 객체는 automatic storage duration이지만, 점 연산자 자체가 저장 기간을 뜻하지는 않습니다.
Person& ref = p1 → ref.setAge(26)
참조는 non-owning 별명이므로 .을 씁니다. 선언의 = p1은 초기 바인딩입니다. 이후 ref = other는 재바인딩이 아니라 p1 객체에 대입하며, 참조는 대상 수명을 연장하지 않습니다.
Person* observer = &p1 → observer->introduce()
raw pointer는 소유권을 표현하지 않고 null이거나 다시 대입될 수 있으므로, 보통 non-owning 관찰자로 제한합니다. null은 검사할 수 있지만 non-null은 대상 수명을 증명하지 않으므로, dangling은 lifetime·container invalidation 계약으로 예방합니다. p->m은 (*p).m과 같습니다.
std::unique_ptr<Person> → owner->introduce()
smart pointer의 operator->는 get() != nullptr일 때 사용합니다. unique_ptr는 단일 소유를 표현하고, 실제 공동 소유에만 shared_ptr를 검토합니다. owner 소멸 뒤 observer는 dangling일 수 있습니다.
pointer = nullptr;은 그 포인터 변수 하나만 null로 바꿉니다. non-null이어도 대상이 살아 있음을 증명하지 않으며, 같은 주소를 복사한 다른 raw pointer alias의 dangling을 막거나 대상 객체의 수명을 연장하지 않습니다.
pointer = nullptr;은 그 포인터 변수 하나만 null로 바꿉니다. null은 직접 검사할 수 있지만 non-null이라는 사실만으로 대상 객체가 아직 살아 있음은 증명되지 않습니다. 같은 주소를 복사한 다른 raw pointer alias는 대상 객체의 소멸이나 vector 재할당·요소 제거 뒤 dangling 상태로 남을 수 있습니다. 따라서 dangling은 포인터 값 검사로 판별하려 하지 말고, RAII owner의 수명과 container invalidation 계약을 지켜 예방해야 합니다.
객체의 생명주기 (Object Lifetime)
저장 기간은 객체가 사용할 저장 공간의 최소 지속 범주를 말하고, 객체의 수명은 초기화가 끝나 실제 객체로 존재하기 시작한 때부터 소멸할 때까지를 말합니다. 둘은 관련되지만 같은 용어는 아닙니다.
-
블록 범위 지역 객체 (automatic storage duration)
- 생성: 선언문이 실행되고 초기화가 끝나면 객체 수명이 시작됩니다.
- 소멸: 정상 흐름이나 예외로 블록을 벗어날 때 소멸자가 자동으로 호출됩니다.
- 소유권: 변수가 객체를 값으로 보유하므로 별도의 해제 코드가 필요하지 않습니다. 현재 블록과 같은 수명이라면 기본 선택입니다.
-
동적 객체 (dynamic storage duration)
- 생성:
std::make_unique나 컨테이너 연산 등이 동적 저장 공간에 객체를 구성합니다. - 소멸:
unique_ptrowner가 소멸·reset되거나, 마지막shared_ptrowner가 사라지거나, 컨테이너가 요소를 제거·소멸할 때 해당 객체의 수명이 끝납니다. - 소유권: 단일 소유는
std::unique_ptr, 실제 공동 소유는std::shared_ptr, 가변 연속 컬렉션은std::vector처럼 의도를 드러내는 RAII 타입으로 표현합니다. - 관찰자: raw pointer와 참조는 보통 소유하지 않는 관찰자로 사용하며, owner가 먼저 정리되거나
vector의 재할당·요소 제거로 대상 객체가 무효화되면 dangling이 될 수 있습니다.
- 생성:
프로그램 전체에 가까운 static storage duration과 스레드별 thread storage duration도 있습니다. 이 장에서는 지역 객체의 automatic storage duration과 RAII가 관리하는 dynamic storage duration을 중심으로 다룹니다.
객체 생성 방식을 고를 때는 물리적 “스택/힙” 위치를 추측하기보다 언제 수명이 끝나야 하는지, 누가 그 종료를 책임지는지, 관찰자가 언제까지 유효한지를 먼저 정합니다.
지역 클래스 (Local Class) (참고)
클래스를 함수 내부에 정의하는 것도 가능합니다.
이를 지역 클래스(Local Class)라고 합니다.
#include <iostream>
void myFunction() {
class LocalClass { // 함수 내부에 정의된 클래스
public:
void display() {
std::cout << "안녕하세요, 저는 지역 클래스입니다." << std::endl;
}
}; // 지역 클래스 정의 끝
LocalClass obj; // 지역 클래스의 객체 생성
obj.display();
}
int main() {
myFunction(); // 출력: 안녕하세요, 저는 지역 클래스입니다.
// LocalClass obj2; // 컴파일 오류: LocalClass는 myFunction() 외부에서 접근 불가
return 0;
}지역 클래스는 해당 함수 내에서만 유효하며, 외부에서는 접근할 수 없습니다.
하지만 지역 클래스는 정적 데이터 멤버를 가질 수 없고, 바깥 함수의 automatic 지역 상태를 람다처럼 캡처할 수도 없는 등 제약이 있어 실제 프로그래밍에서는 자주 사용되지 않습니다.
대부분의 클래스는 전역 스코프나 네임스페이스 내에서 정의됩니다.
객체 생성 방식은 문법 암기보다 수명과 소유권 책임을 먼저 정하면 선택이 단순해집니다.
블록 범위 지역 객체는 스코프가 정리 책임을 갖고, 동적 객체는 unique_ptr·shared_ptr·vector 같은 RAII 소유자가 해제 시점을 코드에 드러내야 합니다.
점 연산자와 화살표 연산자는 왼쪽 표현식이 객체·참조인지, raw pointer·smart pointer인지에 따라 결정됩니다. 어느 연산자를 쓰든 관찰자는 대상 객체보다 오래 살아서는 안 됩니다.