클래스 정의와 객체 생성
이번 장에서는 클래스를 실제로 어떻게 정의하고, 정의된 클래스로 객체를 생성하는 다양한 방법에 대해 더 자세히 살펴보겠습니다.
특히 객체 생성 시 메모리 할당 방식(스택, 힙)에 따른 차이점과 포인터를 사용한 객체 접근 방법을 깊이 있게 다룰 것입니다.
클래스 정의하기 (Class Definition)
클래스 정의는 클래스의 이름, 멤버 변수, 멤버 함수의 선언을 포함합니다.
일반적으로 클래스 정의는 .h
(헤더) 파일에 작성하여 다른 소스 파일에서 포함(include
)하여 사용할 수 있도록 합니다.
멤버 함수의 구현(정의)은 보통 .cpp
(소스) 파일에 작성합니다.
클래스 선언과 정의의 분리 (헤더 파일/소스 파일)
#pragma once // 한 번만 포함되도록 지시
#include <string> // std::string을 위해
#include <iostream> // std::cout을 위해
// 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();
int getAge();
void introduce();
};
#include "Person.h" // Person 클래스 선언을 포함
// 생성자 정의
// '클래스이름::' 스코프 결정 연산자를 사용하여 Person 클래스의 생성자임을 명시
Person::Person(std::string n, int a) {
name = n;
age = a; // 또는 setAge(a); 를 호출하여 유효성 검사 적용 가능
}
// 멤버 함수 정의 (클래스이름::멤버함수이름 형태로 정의)
void Person::setName(std::string n) {
name = n;
}
void Person::setAge(int a) {
if (a >= 0) { // 간단한 유효성 검사
age = a;
} else {
std::cout << "오류: 나이는 음수가 될 수 없습니다." << std::endl;
}
}
std::string Person::getName() {
return name;
}
int Person::getAge() {
return age;
}
void Person::introduce() {
std::cout << "안녕하세요, 저는 " << name << "입니다. 나이는 " << age << "살입니다." << std::endl;
}
#pragma once
: 이 헤더 파일이 한 번만 포함되도록 보장하는 지시어입니다. (매우 흔하게 사용됨)- 스코프 결정 연산자 (
::
): 멤버 함수를 클래스 외부에서 정의할 때, 이 함수가 어느 클래스에 속하는지를 명시하기 위해클래스이름::
을 사용합니다. 예를 들어,Person::setName
은Person
클래스에 속하는setName
함수임을 나타냅니다.
객체 생성하기 (Object Creation)
클래스가 정의되면, 이를 사용하여 메모리에 객체를 생성할 수 있습니다.
객체는 주로 스택(Stack) 또는 힙(Heap) 두 가지 방식으로 메모리에 할당됩니다.
스택에 객체 생성 (정적/자동 할당)
가장 일반적인 객체 생성 방식입니다. 지역 변수를 선언하는 것과 동일하게 객체를 선언합니다.
스택에 할당된 객체는 해당 객체가 선언된 스코프(Scope)를 벗어나면 자동으로 소멸됩니다.
클래스이름 객체이름; // 기본 생성자 호출
클래스이름 객체이름(인자1, 인자2, ...); // 인자를 받는 생성자 호출
예시: 스택에 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 함수가 종료되면 p1과 p2 객체는 자동으로 소멸됩니다.
return 0;
}
힙에 객체 동적 생성 (동적 할당)
프로그램 실행 중에 필요한 만큼 메모리를 동적으로 할당하여 객체를 생성하는 방식입니다.
new
연산자를 사용하며, 힙에 할당된 객체는 포인터를 통해 접근해야 합니다.
스택에 할당된 객체와 달리, 힙에 할당된 객체는 프로그래머가 직접 delete
연산자를 사용하여 메모리를 해제해야 합니다.
클래스이름* 포인터변수 = new 클래스이름; // 기본 생성자 호출
클래스이름* 포인터변수 = new 클래스이름(인자1, 인자2, ...); // 인자를 받는 생성자 호출
예시: 힙에 Person
객체 동적 생성
#include "Person.h" // Person 클래스의 선언 포함
int main() {
// 1. Person 클래스의 객체를 힙에 동적 생성하고, 그 주소를 포인터 p3에 저장
Person* p3 = new Person("박영희", 22);
// 2. 포인터를 통해 객체의 멤버에 접근 (화살표 연산자 `->` 사용)
// p3->introduce()는 (*p3).introduce()와 동일합니다.
p3->introduce(); // 출력: 안녕하세요, 저는 박영희입니다. 나이는 22살입니다.
// 멤버 변수 값 변경
p3->setAge(23);
p3->introduce(); // 출력: 안녕하세요, 저는 박영희입니다. 나이는 23살입니다.
// 3. 사용이 끝난 동적 할당된 객체는 반드시 delete로 메모리 해제
delete p3;
p3 = nullptr; // 댕글링 포인터 방지 (권장)
// 해제된 메모리에 접근 시도 (위험! 런타임 오류 발생 가능)
// if (p3 != nullptr) {
// p3->introduce();
// }
// 4. 배열 형태의 객체 동적 생성
Person* people = new Person[3]{ // 배열 초기화 (C++11부터 가능)
Person("첫째", 10),
Person("둘째", 12),
Person("셋째", 14)
};
// 배열의 각 객체에 접근
people[0].introduce(); // 출력: 안녕하세요, 저는 첫째입니다. 나이는 10살입니다.
(people + 1)->introduce(); // 포인터 산술 연산으로 접근: 안녕하세요, 저는 둘째입니다. 나이는 12살입니다.
// 5. 동적 할당된 배열은 delete[]로 해제
delete[] people;
people = nullptr;
return 0;
}
```cpp title="main.cpp"
#include "Person.h" // Person 클래스의 선언 포함
int main() {
// 1. Person 클래스의 객체를 힙에 동적 생성하고, 그 주소를 포인터 p3에 저장
// (이때 Person::Person("박영희", 22) 생성자가 호출됨)
Person* p3 = new Person("박영희", 22);
// 2. 포인터를 통해 객체의 멤버에 접근 (화살표 연산자 `->` 사용)
// p3->introduce()는 (*p3).introduce()와 동일합니다.
p3->introduce(); // 출력: 안녕하세요, 저는 박영희입니다. 나이는 22살입니다.
// 멤버 변수 값 변경
p3->setAge(23);
p3->introduce(); // 출력: 안녕하세요, 저는 박영희입니다. 나이는 23살입니다.
// 3. 사용이 끝난 동적 할당된 객체는 반드시 delete로 메모리 해제
delete p3;
p3 = nullptr; // 댕글링 포인터 방지 (권장)
// 해제된 메모리에 접근 시도 (위험! 런타임 오류 발생 가능)
// if (p3 != nullptr) {
// p3->introduce();
// }
// 4. 배열 형태의 객체 동적 생성
Person* people = new Person[3]{ // 배열 초기화 (C++11부터 가능)
Person("첫째", 10),
Person("둘째", 12),
Person("셋째", 14)
};
// 배열의 각 객체에 접근
people[0].introduce(); // 출력: 안녕하세요, 저는 첫째입니다. 나이는 10살입니다.
(people + 1)->introduce(); // 포인터 산술 연산으로 접근: 안녕하세요, 저는 둘째입니다. 나이는 12살입니다.
// 5. 동적 할당된 배열은 delete[]로 해제
delete[] people;
people = nullptr;
return 0;
}
- 점 연산자 (
.
) vs 화살표 연산자 (->
):- 점 연산자 (
.
): 스택에 생성된 객체(myCar.model
), 또는 참조자(refCar.speed
)를 통해 멤버에 접근할 때 사용합니다. - 화살표 연산자 (
->
): 포인터를 통해 객체의 멤버에 접근할 때 사용합니다.p->member
는(*p).member
와 동일한 의미입니다.
- 점 연산자 (
객체의 생명주기 (Object Lifetime)
객체의 생명주기, 즉 객체가 생성되고 소멸되는 시점은 메모리 할당 방식에 따라 달라집니다.
-
스택 객체 (자동 저장 객체)
- 생성: 변수가 선언된 스코프에 진입할 때 자동으로 생성됩니다.
- 소멸: 변수가 선언된 스코프를 벗어날 때 자동으로 소멸됩니다.
- 장점: 메모리 관리가 자동으로 이루어져 편리하고 안전합니다.
- 단점: 스코프를 벗어나면 사라지므로, 함수 종료 후에도 객체가 유지되어야 할 때는 사용할 수 없습니다. 스택 공간은 제한적입니다.
-
힙 객체 (동적 저장 객체)
- 생성:
new
연산자가 호출될 때 생성됩니다. - 소멸:
delete
연산자가 호출될 때 소멸됩니다. 프로그래머가 직접 관리해야 합니다. - 장점: 프로그램 실행 중에 필요한 만큼 유동적으로 메모리를 할당하고 해제할 수 있으며, 스코프에 구애받지 않고 객체 수명을 제어할 수 있습니다.
- 단점:
delete
호출을 잊어버리면 메모리 누수(Memory Leak) 가 발생할 수 있고, 잘못된delete
사용(이중 해제, 댕글링 포인터)은 심각한 오류를 유발할 수 있습니다.
- 생성:
지역 클래스 (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;
}
지역 클래스는 해당 함수 내에서만 유효하며, 외부에서는 접근할 수 없습니다.
하지만 지역 클래스는 여러 제약(예: 정적 멤버 변수 가질 수 없음, 외부 함수의 지역 변수 참조 불가 등)이 있어 실제 프로그래밍에서는 자주 사용되지 않습니다.
대부분의 클래스는 전역 스코프나 네임스페이스 내에서 정의됩니다.