클래스 정의와 객체 생성
클래스와 멤버 함수 정의를 구분하고 객체의 저장 기간·수명·소유권에 맞는 RAII 생성 방식과 멤버 접근법을 익힙니다.
이번 장에서는 클래스를 실제로 정의하는 방법과 정의된 클래스로 객체를 생성하는 방법을 더 자세히 다룹니다.
특히 객체 생성 시 저장 기간(storage duration)과 수명(lifetime), 소유권(ownership)을 구분하고, 객체·참조·포인터 표현식에 맞는 멤버 접근 방법을 다룹니다.
C++ · OBJECT OWNERSHIP
객체의 수명과 소유 책임에 맞춰 생성 방식을 고른다
클래스 정의와 멤버 함수 정의는 타입과 동작을 준비한다. 실제 객체의 수명·개수·관찰자 요구를 정한 뒤 아래 후보를 선택한다.
현재 블록의 지역 값
현재 블록의 수명으로 충분하면 객체를 직접 보유한다.
Person p1{"김영희", 25};automatic storage duration이며 블록 종료 때 자동 소멸한다. 더 긴 수명도 값 반환·이동이나 상위 객체의 멤버로 표현할 수 있는지 먼저 본다.
가변 개수의 연속 값
요소의 주소 안정성이나 다형적 간접 소유가 필요 없는 연속 값 컬렉션을 보유한다.
std::vector<Person> people;컨테이너가 요소의 동적 저장 공간과 수명을 관리한다. 재할당은 기존 요소의 참조·포인터·반복자를 무효화한다. 요구가 다르면 다른 소유 구조를 선택한다.
단일 동적 소유
동적 객체 한 개의 해제 책임을 하나의 소유자로 표현한다.
auto owner =
std::make_unique<Person>(
"박영희", 22);unique_ptr 소멸·reset 시 대상 객체를 정리한다. 이동은 소유권을 넘기는 방법이며 raw 관찰자는 수명을 연장하지 않는다.
실제 공동 소유
둘 이상의 소유자가 대상의 수명 종료 책임을 함께 가져야 할 때만 선택한다.
std::shared_ptr<Person> owner;마지막 강한 소유자가 사라지면 대상이 소멸한다. shared_ptr 순환은 적절한 weak_ptr 관찰 관계로 끊는다. 예제 선언은 비어 있는 핸들이다.
automatic과 dynamic은 저장 기간, 소유권은 수명 종료 책임을 설명한다. 물리적 “스택/힙” 별명으로 고르지 않으며, 정의만 작성했다고 객체별 저장 공간이 생기지는 않는다.
클래스 정의하기 (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)
클래스가 정의되면 그 타입으로 실제 객체를 생성할 수 있습니다. 클래스 정의 자체와 멤버 함수 정의는 객체의 형식과 동작을 준비할 뿐, 객체별 저장 공간을 만들지는 않습니다.
세 파일을 실제로 연결하기
위 Person.h·Person.cpp와 아래 첫 main.cpp를 같은 실습 폴더에 저장합니다. GCC에서는 g++ -std=c++20 -Wall -Wextra Person.cpp main.cpp -o person.exe로 두 소스를 함께 빌드합니다. Windows PowerShell에서는 .\person.exe, Linux 셸에서는 ./person.exe를 해당 폴더에서 실행합니다. 헤더는 두 소스가 include하므로 별도 실행 파일로 만들지 않습니다. Person.cpp를 빼면 main에서 사용할 함수 정의를 링크할 수 없습니다. 뒤의 unique_ptr·vector 실습은 별도의 main 예제이므로 main.cpp의 내용을 교체하거나 다른 폴더에서 빌드합니다. 두 main을 한 프로그램에 합치지 마세요.
직접 std::cout로 결과를 출력하는 변형에서는 main.cpp에도 #include <iostream>을 추가합니다. 다른 소스인 Person.cpp의 include가 main.cpp로 전달되지는 않습니다.
첫 main에서 p1의 나이를 26으로 바꾼 뒤 p1.setAge(-1);을 추가하고, p2를 생성한 뒤 두 객체의 getAge를 출력한다고 가정합니다. p1·p2는 각각 얼마인가요? 생성 자체를 Person p1("김영희", -1);로 바꾼 새 실행에서는 무엇이 달라지나요?
정의·실행·객체 상태의 풀이
기존 생성 뒤 −1 변경만 추가하면 p1의 나이는 26, p2의 나이는 30입니다. setAge가 실패해도 이전 값 26은 남습니다. 새 실행의 생성 인자를 −1로 바꾸면 생성자에서 먼저 설정한 age 값 0이 남고, 이후 원래 setAge(26) 호출이 나이를 26으로 바꿉니다. 생성 직후와 후속 호출 뒤를 구분해야 합니다. 이 Person은 0 이상만 검사하며 앞 절 Student의 120 상한을 자동으로 가져오지 않습니다.
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
왼쪽 표현식이 멤버 접근 문법을 정한다
객체·참조는 점, raw pointer·smart pointer는 화살표를 쓴다. 접근 문법과 대상의 유효성·소유권은 각각 확인해야 한다.
객체 · 점 연산자
값 자체가 Person 객체다. 블록 지역 객체는 블록이 끝날 때 소멸한다.
Person p1{"김영희", 25};
p1.introduce();점 연산자 자체가 저장 기간을 뜻하지 않는다. 다른 저장 기간의 객체 표현식에도 같은 문법을 쓴다.
참조 · 점 연산자
기존 객체의 non-owning 별명이며 대상을 다시 묶지 않는다.
Person& ref = p1;
ref.setAge(26);선언의 =는 초기 바인딩이다. 이후 ref=other는 p1에 대입한다. 이 예제의 lvalue 참조는 대상 수명을 연장하지 않는다.
raw pointer · 화살표
타입만으로 소유권을 나타내지 않는다. 관찰자는 null이거나 재대입될 수 있다.
Person* observer = &p1;
observer->introduce();살아 있는 대상을 가리켜야 한다. non-null만으로 유효 수명을 증명하지 못한다. built-in p->m은 (*p).m과 같다.
smart pointer · 화살표
unique_ptr는 단일 소유, 실제 공동 소유가 필요하면 shared_ptr를 검토한다.
owner->introduce();unique_ptr/shared_ptr 접근 전 get()!=nullptr여야 한다. 소유자가 정리한 뒤 꺼내 둔 raw 포인터·참조는 dangling일 수 있다.
pointer=nullptr는 그 변수 하나만 null로 바꾼다. 다른 별명의 dangling을 막지 않으며, owner의 수명과 컨테이너 무효화 계약을 지켜 관찰자를 사용한다.
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;
}이 예제의 LocalClass 이름은 함수의 블록 범위에 있으므로 함수 밖에서 그 이름으로 타입을 직접 사용할 수 없습니다.
하지만 지역 클래스는 정적 데이터 멤버를 가질 수 없고, 바깥 함수의 automatic 지역 상태를 람다처럼 캡처할 수도 없는 등 제약이 있어 실제 프로그래밍에서는 자주 사용되지 않습니다.
대부분의 클래스는 전역 스코프나 네임스페이스 내에서 정의됩니다.
객체 생성 방식은 문법 암기보다 수명과 소유권 책임을 먼저 정하면 선택이 단순해집니다.
블록 범위 지역 객체는 스코프가 정리 책임을 갖고, 동적 객체는 unique_ptr·shared_ptr·vector 같은 RAII 소유자가 해제 시점을 코드에 드러내야 합니다.
점 연산자와 화살표 연산자는 왼쪽 표현식이 객체·참조인지, raw pointer·smart pointer인지에 따라 결정됩니다. 어느 연산자를 쓰든 관찰자는 대상 객체보다 오래 살아서는 안 됩니다.