객체가 만들어질 때 반드시 수행해야 하는 초기화 과정을 실수로 누락하면 프로그램은 올바르게 동작하지 않는다. => 반드시 수행해야 하는 초기화 과정을 자동화할 필요가 있다.
객체의 초기화 과정을 자동화하여 수행해줄 목적으로 사용하는 것이 바로 생성자이다
생성자의 특징은 다음과 같다.
특수한 멤버 함수
객체가 생성될 때 자동으로 호출(객체 정의 시 자동 호출)
초기화 목적으로 사용
클래스와 동일한 이름을 갖는 멤버 함수
반환 타입이 없다.
오버로딩이 가능하다.
생성자 선언(구현)
class ClassName {
public: // public으로 선언돼야지만 외부에서 해당 타입의 객체를 생성할 수 있다.
ClassName(parameterList) { ... } // 생성자
}
// Counter.hpp
#ifndef COUNT_H_INCLUDED
#define COUNT_H_INCLUDED
class Counter {
int value; // 접근 제한 private(디폴트)
public:
Counter(){ value = 0; } // 생성자
void reset(){ value = 0; }
void count(){ ++value; }
int getValue() const { return value; }
}
#endif
객체 정의와 생성자 자동 호출
// CntMain.cpp
include <iostream>
include "Counter.hpp"
int main(){
Counter counter; // 객체 정의, Counter 객체 생성자 자동 호출 => value 자동으로 0
cout << "계수기의 현재 값: " << cnt.getValue() << endl;
return 0;
}
// Person.hpp
#ifndef PERSON_H_INCLUEDED
#define PERSON_H_INCLUEDED
class Person {
char* name;
char* addr;
public:
Person(const char* name, const char* addr); // 생성자
~Person(); // 소멸자!!
void print() const; // 이름과 주소 출력
void chAddr(const char* newAddr);
}
#endif
// Person.cpp
include <iostream>
include <cstring>
include "Person.hpp"
using namespace std;
Person::Person(const char* name, const char* addr) // 생성자: 객체 생성 시 호출 실행
{
this -> name = new char[strlen(name)-1];
// this는 현재 객체 자신의 포인터 변수
// 문자배열 공간 확보, 동적 메모리 할당!!
strcpy(this->name, name);
this -> addr = new char[strlen(addr)-1];
// this는 현재 객체 자신의 포인터 변수
// 문자배열 공간 확보, 동적 메모리 할당!!
strcpy(this->addr, addr);
}
Person::~Person() // 소멸자: 객체 소멸 시 자동 호출 실행, "객체 소멸" 시 호출!!
{
cout << "Person 객체 제거함(" << name << ")"" << endl;
delete[] name; // 동적 할당된 메모리 공간(배열 공간) 해제
delete[] addr; // 동적 할당된 메모리 공간(배열 공간) 해제
}
void Person::print() const
{
cout << addr << "에 사는 " << name << "입니다" << endl;
}
void Person::chAddr(const char* newAddr)
{
delete[] addr; // 동적 할당된 메모리 공간(배열 공간) 해제
addr = new char[strlen(newAddr)-1];
// this 생략 가능, this -> addr과 동일
// 동적 메모리 할당
strcpy(addr, newAddr); // this 생략 가능, this -> addr과 동일
}
// PrsnMain.cpp
include <iostream>
include "Person.hpp"
using namespace std;
int main(){
Person p1{"박상훈", "경기도 수원시"};
// 스택에 Person 타입 객체 할당, scope 닫힐 때 소멸자 자동 호출
Person* p2 = new Person("이철수", "서울시 종로구");
// 힙 영역에 Person 타입 객체 동적 할당
// scope 닫혀도 소멸되지 않음 => delete 연산 필요
Person* p3 = new Person("박은미", "강원도 동해시");
// 힙 영역에 Person 타입 객체 동적 할당
// scope 닫혀도 소멸되지 않음 => delete 연산 필요
p1.print();
p2->print();
p3->print();
cout << endl << "주소 변경: ";
p1.chAddr("경기도 성남시");
p1.print();
p2->chAddr("대전시 서구");
p2->print();
delete p2; // p2가 가리키는 Person 객체 소멸
delete p3; // p3가 가리키는 Person 객체 소멸
} // p1에 저장된 객체 소멸
생성자 오버로딩
생성자도 멤버 함수와 마찬가지로 오버로딩 가능
멤버 함수와 동일한 오버로딩 규칙이 적용된다. : 각각의 생성자는 고유해야 한다, 즉 매개변수가 달라야 한다.
// Player.hpp
class Player {
private:
std::string name;
int helth;
int xp;
public:
Player();
Player(std::String nameVal);
Player(std::string nameVal, int healthVal, int xpVal);
}
// Player.cpp
Player::Player()
{
name = "NONE";
health = 0;
xp = 0;
}
Player::Player(std::String nameVal)
{
name = nameVal;
health = 0;
xp = 0;
}
Player::Player(std::string nameVal, int healthVal, int xpVal)
{
name = nameVal;
health = healthVal;
xp = xpVal;
}
// Main.cpp
int main(){
Player empty; // None, 0, 0
Player hero {"Hero"}; // Hero, 0, 0
Player kim {"Kim", 100, 5}; // Kim, 100, 5
Player* enemy = new Player{"Enemy", 1000, 0}; // Enemy, 1000, 0
delete enemy;
}
생성자 초기화 리스트
생성자 초기화 리스트를 사용할 경우 생성과 동시에 즉시 값이 지정됨
// Player.cpp
Player::Player()
: name{"None"}, health{0}, xp{0}
{
}
Player::Player(std::string nameVal)
: name{nameVal}, health{0}, xp{0}
{
}
Player::Player(std::string nameVal, int healthVal, int xpVal)
: name{nameVal}, health{healthVal}, xp {xpVal}
{
}
생성자 위임
생성자 오버로딩으로 인해 유사한 코드 중복되는 생성자들이 발생 -> 오류 발생 가능성이 높아짐 -> 생성자 위임을 통해서 코드 중복을 제거하고 오류 가능성을 낮출 수 있음
// Player.hpp
class Player {
private:
std::string name;
int helth;
int xp;
public:
Player(std::string nameVal="None", int healthVal=0, int xpVal=0);
}
// Player.cpp
Player::Player(std::string nameVal, int helthVal, int xpVal)
: name {nameVal}, health {healthVal}, xpVal {xpVal}
{
}
// Main.cpp
Player empty; // None, 0, 0
Player hero {"Hero"}; // Hero, 0, 0
Player kim {"Kim", 100, 5}; // Kim, 100, 5
Player* enemy = new Player{"Enemy", 1000, 0}; // Enemy, 1000, 0
delete enemy;
this
this는 C++ 키워드
멤버 함수를 호출한 실제 객체의 주소값을 저장한 포인터 타입의 공간
사용 예
void Player::setPosition(int x, int y)
{
this -> x = x;
this -> y = y;
}
생성자의 종류
디폴트 생성자(기본 생성자)
인자가 없는 생성자
클래스의 생성자를 직접 구현하지 않으면, 컴파일러가 기본적으로 만들어줌
객체를 인자없이 정의하면 됨.
인자가 있는 생성자만 구현한 경우 기본 생성자가 자동 생성되지 않는다.
복사 생성자
객체가 복사될 때, 기존 객체를 기반으로 새로운 객체가 생성됨
객체가 복사되는 경우
객체를 pass by value 방식으로 함수의 매개변수로 전달할 때
함수에서 value의 형태로 결과를 반환할 때
기존 객체를 기반으로 새로운 객체를 생성할 때
Player hero {1, 1, 1};
Player another_hero = hero // 복사 생성자 호출, Player another_hero{hero}와 동일
Player another_hero;
another_hero = hero; // 복사 생성자 호출되지 않는다!! 단순 대입 연산이다
객체가 어떤 방식으로 복사될 지를 결정해줘야 한다. 즉, 복사 생성자를 선언(구현)해줘야 한다.
사용자가 구현하지 않는 경우 컴파일러에서 자동으로 복사 생성자를 구현
복사 생성자가 중요한 이유
복사하는 객체의 멤버로 포인터가 있는 경우, 복사 과정에서 문제가 발생할 수 있기 때문에 이를 잘 처리해줘 한다.
얕은 복사의 문제점 : 아직 해제되면 안 되는 동적 할당된 메모리 공간을 해제할 수 있다.
// Shallow.hpp
class Shallow
{
private:
int *data;
public:
Shallow(int d);
Shallow(const Shallow& source);
~Shallow();
}
// Shallow.cpp
Shallow::Shallow(int d)
// 생성자 구현: int 동적 메모리 할당하고 포인터 타입 멤버가 가리키는 공간에 값 저장
{
data = new int;
*data = d;
}
Shallow::Shallow(const Shallow& source)
: data {source.data}
// 얕은 복사를 수행하는 복사 생성자(주소를 복사)
{
cout << "Copy constructor, shallow" << endl;
}
Shallow::~Shallow() // 소멸자 구현:
{
delete data;
// 객체 안에 포인터 타입 변수(data)가 가리키는 동적 할당된 메모리 공간 해제
cout << "free storage" << endl;
}
// Main.cpp
void displayShallow(Shallow shallow){ // 복사 생성자 호출
cout << "value : " << *(shallow.data);
} // 소멸자 호출
int main(){
Shallow obj{100};
displayShallow(obj) // value : 100 출력
return 0;
} // 소멸자 호출 -> ERROR: 이미 해제된 obj.data가 가리키는 공간을 소멸자로 또 해제 시도
displayShallow 함수 호출 시 Shallow 복사 생성자 호출 -> displayShallow 함수가 종료되면서 Shallow 소멸자 호출
displayShallow 함수 호출하면서 복사 생성된 객체 안의 data가 가리키는 공간과 기존의 obj 객체 안의 data가 가리키는 공간이 동일하므로
displayShallow 함수 종료되면서 소멸자 호출로 인해 해제되는 공간은 기존의 obj 객체 안의 data가 가리키는 공간과 동일하다. => 해제되선 안되는 데 의도치 않게 해제되었다.
깊은 복사로 해결
// Deep.hpp
class Deep
{
private:
int *data;
public:
Deep(int d);
Deep(const Deep& source);
~Deep();
}
// Deep.cpp
Deep::Deep(int d)
// 생성자 구현: int 동적 메모리 할당하고 포인터 타입 멤버가 가리키는 공간에 값 저장
{
data = new int;
*data = d;
}
Deep::Deep(const Deep& source)
{
data = new int;
*data = *(source.data);
cout << "Copy constructor, deep" << endl;
}
Deep::~Deep() // 소멸자 구현:
{
delete data;
// 객체 안에 포인터 타입 변수(data)가 가리키는 동적 할당된 메모리 공간 해제
cout << "free storage" << endl;
}
// Main.cpp
void displayDeep(Deep deep){ // 복사 생성자 호출
cout << "value : " << *(deep.data);
} // 소멸자 호출
int main(){
Deep obj{100};
displayDeep(obj) // value : 100 출력
return 0;
}
이동 생성자
// VecF.h
#ifndef VEC_F_H_INCLUDED
#define VEC_F_H_INCLUDED
#include <iostream>
#include <cstring>
using namespace std;
class VecF{
int n;
float* arr;
public:
VecF(int d, float* a=nullptr): n {d} { // 생성자
arr = new floatp[d];
if(a){
memcpy(arr, a, sizeof(float) * n);
}
}
VecF(const VecF& fv): n {fv.n} { // 복사 생성자
arr = new float[n];
memcpy(arr, fv.arr, sizeof(float) * n);
}
~VecF(){
deletep[] arr;
}
VecF add(const VecF& fv) const {
VecF tmp(n);
for(int i = 0; i < n; i++){
tmp.arr[i] = arr[i] + fv.arr[i];
}
return tmp;
}
void print() const {
cout << "[ ";
for(int i = 0; i < n; i++){
cout << arr[i] << " ";
}
cout << "]";
}
}
VecF v2(v1);와 VecF v3(v1.add(v2)); 모두 기존에 만들어진 객체를 기반으로 객체를 생성하기 때문에 두 구문 모두에서 복사 생성자가 호출되어 깊은 복사가 이루어진다.
하지만 VecF v2(v1);에서 v1은 복사 생성자 호출 스택이 종료돼도 객체가 소멸되지 않지만 VecF v3(v1.add(v2));에서 인자로 주어지는 객체는 복사 생성자 호출 스택이 종료되면 객체가 소멸되는 임시 객체이다.
애초에 복사 호출 스택이 종료되면 바로 소멸될 임시 객체를 깊은 복사까지 하면서 새로운 객체를 생성하는 것은 비효율적이다.
호출 스택이 종료되면 바로 소멸될 임시 객체를 깊은 복사하지 않으면서 새로운 객체를 생성하기 위한 목적의 생성자를 따로 만들어 이 비효율성을 해결할 수 있다.
v1.add(v2)의 반환값은 r-value인 점을 이용해 r-value 참조를 파라미터로 하는 생성자를 만들어주면 된다.
r-value 참조를 인자로 받는 생성자에서는 인자로 받은 임시 객체 안의 필드값들을 새로 생성하려는 객체의 필드로 옮겨준 후 임시 객체의 필드들을 전부 초기화 시켜주면 된다.
이러한 생성자를 이동 생성자라 한다.
class VecF{
...
public:
VecF(int d, float* a=nullptr): n {d} { // 생성자
arr = new floatp[d];
if(a){
memcpy(arr, a, sizeof(float) * n);
}
}
VecF(const VecF& fv): n {fv.n} { // 복사 생성자
arr = new float[n];
memcpy(arr, fv.arr, sizeof(float) * n);
}
VecF(VecF&& fv): n {fv.n}, arr {fv.arr} { // 이동 생성자
// fv가 r-value 참조하는 객체는 이동 생성자를 호출한 스택 종료 이후로는 소멸된다.
// 즉, 복사 객체를 위한 데이터 공간을 새로 동적 할당할 필요가 없다는 것이다.
fv.arr = nullptr;
fv.n = 0;
}
~VecF(){
deletep[] arr;
}
...
}
동적 할당한 객체 복사 생성
Buffer* p = new Buffer(5);
// 1) 객체 복사 (깊은 복사): 복사 생성자 호출
Buffer b1 = *p;
// 2) 포인터 복사 (얕은 복사): 주소만 복사
Buffer* p2 = p;
// 정리는 반드시 한 번만:
delete p; // p와 p2는 같은 객체를 가리켰으므로, 한 번만 delete해야 함
// delete p2; // 다시 지우면 이중 해제(UB)
static 데이터 멤버와 static 멤버 함수
static 키워드
멤버 함수들은 개별 객체에 대한 상태를 저장하는 것이 아니므로 멤버 함수들을 위한 프로그램 메모리 공간은 개별 객체가 아닌 클래스마다 하나씩만 있으면 된다.
하지만 데이터 멤버는 각각의 객체별 상태를 저장해야 하므로 객체가 정의되면 그 객체의 데이터 멤버를 위해 고유한 메모리 공간이 할당된다.
그러나 때로는 데이터 멤버를 클래스에 속한 객체들이 함께 공유하는 것이 필요할 때가 있다.
변수를 공유하기 위해 전역변수를 사용할 수도 있지만 전역변수는 특정 클래스의 객체들만 공유하는 것이 아니어서 프로그램 해석을 어렵게 한다.
따라서 특정 클래스에 속하는 객체들끼리만 공유할 수 있는 공간을 static이라는 키워드로 할당할 수 있게 하였다.
static 클래스 멤버 변수
객체가 아닌 클래스에 속하는 변수
개별적인 객체의 데이터가 아닌 클래스에 공통 데이터 구현이 필요할 때 사용
static 클래스 멤버 함수
멤버 함수도 static으로 정의할 수 있는데 static 멤버 함수는 특정 객체에 대한 처리를 하는 것이 아니라 클래스의 이름으로 어떠한 작업을 수행하는 함수이다.
객체가 아닌 클래스에 속하는 함수
클래스 이름 하에서 바로 호출 가능
static 클래스 멤버 함수는 static 클래스 멤버 변수에만 접근 가능
friend 함수와 friend 클래스
friend 함수와 friend 클래스는 클래스의 멤버가 아니다.
friend 키워드
특정 클래스의 private 멤버에 접근할 수 있는 함수나 클래스를 선언(구현)할 때 사용
비대칭 : A가 B 클래스의 friend라 해도 B 클래스가 A의 friend는 아니다.
전이되지 않음: A가 B 클래스의 friend이고 B 클래스가 C 클래스의 friend라고 해서 A가 C 클래스의 friend는 아니다.
friend 함수
class Player {
friend void displayPlayer(const Player& p); // Player의 friend이지 멤버가 아님
private:
int x, y;
int speed;
public:
Player(int x, int y, int speed): x{x}, y{y}, speed{speed}
{
count << this << endl;
}
void setPosition(int x, int y){
this -> x = x;
this -> y = y;
}
}
void displayPlayer(const Player& p){
cout << p.x << ", " << p.y << endl; // friend가 아니었으면 접근 불가
}
friend 클래스
class Player {
friend class Game; // Player의 friend이지 멤버가 아님
private:
int x, y;
int speed;
public:
Player(int x, int y, int speed): x{x}, y{y}, speed{speed}
{
count << this << endl;
}
void setPosition(int x, int y){
this -> x = x;
this -> y = y;
}
}
class Game {
public:
void getPlayer(const Player& p)
{
cout << p.x << ", " << p.y << endl; // friend가 아니었으면 접근 불가
}
}