리얼 개발

[C++] 7. 클래스 본문

C++/어려운 C++

[C++] 7. 클래스

econo-my 2024. 7. 30. 16:30

클래스를 만든다는 것은 객체 지향 프로그래밍의 시작으로 볼 수 있다.

 

오늘은 C++에서 객체 지향 프로그래밍 파트를 다뤄보겠다.

우선 객체 지향 프로그래밍에서 흔히 말하는 특징 몇 가지가 있다.

 

1. 추상화

2. 상속

3. 다형성

4. 캡슐화

 

4가지의 특징을 C++ 코드 예시를 통해 이해해보자.

 

 

추상화

클래스는 함수와 변수를 가지고 있는 코드 덩어리로써 이를 통해 실제 Stack, Heap 메모리를 할당하여 객체를 생성할 수 있다.

 

클래스를 만드는 행위 자체를 추상화라고 한다. 단어 뜻 그대로 너무 두루뭉실한 말이다. 내가 이해한 방법은 클래스란 흩어져있던 전역 함수와, 전역 변수들을 울타리로 감싸놓은 것이다. 울타리로 감싸놓음으로써 공통된 동작과 특징을 가지는 하나의 틀을 만들고, 틀 안에 어떤 것들이 들어있는지 몰라도 틀을 사용할 수 있다. 

 

한 마디로 공통된 특징을 모아 클래스로 표현하는 것을 추상화(abstraction) 이라고 한다. 

class Animal
{
public:
	Animal() {}

	void Walk() {}
	void Eat() {}

private:
	string Name;
	int Age;
};

 

Animal 클래스를 만들어, 동물들이 행하는 동작인 걷기와 먹기를 멤버 함수로 클래스에 구현할 수 있으며, 동물들의 특징인 이름과 나이를 멤버 변수로 가지고 있게 만들었다.

 

 

상속

추상화가 클래스를 만드는 작업이라면 상속은 클래스를 확장하는 작업으로 볼 수 있겠다.

 

C++에서 상속을 사용해 얻을 수 있는 이득은 크게 두가지가 있다.

  • 상위 클래스를 상속받아 기능을 확장해 나가기 쉽다.
  • 클래스들에 중복된 코드가 있을 때 이것을 모아 상위 클래스로 만듦으로써 코드를 깔금하게 관리할 수 있다.
class Animal
{
public:
	Animal() {}

	void Walk() {}
	void Eat() {}

private:
	string Name;
	int Age;
};

class Cat : public Animal
{
public:
	Cat() {}

	void Grooming() {}
	void Punch() {}
};

class Dog : public Animal
{
public:
	Dog() {}

	void Bark() {}
};

 

Animal 클래스를 상속받은 고양이 클래스는 걷기와 먹기를 할 수 있으며 그루밍과 펀치라는 기능이 확장되었다. Dog 클래스는 똑같이 걷기와 먹기를 상속받아 사용할 수 있으며 짖기 기능이 확장되었다.

 

 

 

다형성

객체지향 프로그래밍에서 다형성은 모습은 같지만 형태는 다른 것을 의미한다.

C++에서 다형성은 가상 함수, 연산자 오버 로딩, 템플릿 등이 있다.

 

다형성을 잘 보여주는 것은 가상 함수인데, 가상 함수를 보기 전 알아야할 것들이 있다.

기본적으로 C++은 타입을 보고 타입에 따라 메모리에 크기를 할당한다. 이를 정적 바인딩이라고 하며 컴파일 타임에 해당 타입의 객체에 어떤 함수가 호출될지 결정하는 것이다. 반대로 가상 함수는 동적 바인딩을 통해 런타임에 어떤 함수가 실행될지 결정한다.

 

가상 함수는 함수 앞에 virtual 키워드를 사용하며 이 키워드가 붙은 함수를 가지고 있는 클래스는 가상 함수 테이블과 테이블을 가리키는 포인터를 가지게 된다. 포인터를 통해 테이블에 있는 함수를 호출하기에 가상 함수의 호출은 속도가 느려진다. 

#include <iostream>
using namespace std;

class Animal
{
public:
	Animal() { cout << "Animal 클래스 생성자" << endl; }
	~Animal() { cout << "Animal 클래스 소멸자" << endl; }

	virtual void Walk() {}
	void Eat() {}

private:
	string Name;
	int Age;
};

class Cat : public Animal
{
public:
	Cat() { cout << "Cat 클래스 생성자" << endl; }
	~Cat() { cout << "Cat 클래스 소멸자" << endl; }

	virtual void Walk() override { cout << "Cat Walk" << endl; }
	void Grooming() {}
	void Punch() {}
};

class Dog : public Animal
{
public:
	Dog() { cout << "Dog 클래스 생성자" << endl; }
	~Dog() { cout << "Dog 클래스 소멸자" << endl; }

	virtual void Walk() override { cout << "Dog Walk" << endl; }
	void Bark() {}
};


int main()
{
	Animal* animal = new Cat;
	animal->Walk();

	delete animal;
}


실핼결과
Animal 클래스 생성자
Cat 클래스 생성자
Cat Walk
Animal 클래스 소멸자

 

Animal 클래스의 Walk 함수에 virtual 키워드를 붙여 가상함수로 만들었다. Animal을 상속받은 Cat과 Dog  클래스에서는 해당 함수를 "재정의" 하여 런타임에 자신의 함수가 불릴 수 있도록 한다.

 

C++에서 기본타입끼리는 형변환이 가능하다. 클래스끼리도 마찬가지이다. 

부모 클래스 타입에서 자식 클래스 타입을 가리킬 수 있으며 자식 클래스 타입에서 부모 클래스 타입을 가리킬 수 있다. 기본적으로는 부모 타입에서 자식 타입을 가리키는 업캐스팅을 자주 사용하며 위의 예시가 그 상황이다. 그래서 Animal 타입의 포인터가 Cat 타입의 객체를 가리킬 수 있게 된 것이다. 

 

근데 한 가지 이상한 점이 Cat 클래스의 소멸자가 불리지 않는다. 그 이유는 위에서 말했듯이 기본적으로 C++은 정적 바인딩을 통해 컴파일 타임에서 포인터 타입을 검사 후 호출할 함수를 결정한다. Walk 함수의 경우에는 가상 함수로 런타임에 부모 객체를 가리키는지 자식 객체를 가리키는지 검사하지만 소멸자의 경우는 그러지 않는다. 따라서 소멸자 또한 가상 함수로 만들어 주면 Cat 클래스의 소멸자도 호출되게 할 수 있다.

 

#include <iostream>
using namespace std;

class Animal
{
public:
	Animal() { cout << "Animal 클래스 생성자" << endl; }
	virtual ~Animal() { cout << "Animal 클래스 소멸자" << endl; }

	virtual void Walk() {}
	void Eat() {}

private:
	string Name;
	int Age;
};

class Cat : public Animal
{
public:
	Cat() { cout << "Cat 클래스 생성자" << endl; }
	virtual ~Cat() { cout << "Cat 클래스 소멸자" << endl; }

	virtual void Walk() override { cout << "Cat Walk" << endl; }
	void Grooming() {}
	void Punch() {}
};

class Dog : public Animal
{
public:
	Dog() { cout << "Dog 클래스 생성자" << endl; }
	virtual ~Dog() { cout << "Dog 클래스 소멸자" << endl; }

	virtual void Walk() override { cout << "Dog Walk" << endl; }
	void Bark() {}
};


int main()
{
	Animal* animal = new Cat;
	animal->Walk();

	delete animal;
}

실행결과
Animal 클래스 생성자
Cat 클래스 생성자
Cat Walk
Cat 클래스 소멸자
Animal 클래스 소멸자

 

 

 

캡슐화

클래스를 보면 public과 private 와 같은 단어들을 볼 수 있다. 이를 접근 지정자라고 부르며 캡슐화의 핵심 키워드이다.

 

public으로 지정된 멤버 함수나 멤버 변수는 해당 클래스로 객체가 만들어졌을 때 객체를 통해 사용 및 접근할 수 있다.

protected으로 지정된 멤버 함수, 변수는 해당 클래스를 상속받은 클래스에서만 접근할 수 있다. 

private으로 지정된 멤버 함수나 멤버 변수는 해당 클래스로 객체가 만들어졌을 때 객체를 통해 사용 및 접근할 수 없다. 

#include <iostream>
using namespace std;

class Animal
{
public:
	Animal(string Name, int Age) : Name(Name), Age(Age)
	{ 
		cout << "Animal 클래스 생성자" << endl; 
	}

	virtual ~Animal() { cout << "Animal 클래스 소멸자" << endl; }

	virtual void Walk() {}
	void Eat() {}

	const string GetName() const { return Name; }
	const int GetAge() const { return Age; }

private:
	string Name;
	int Age;
};

class Cat : public Animal
{
public:
	Cat(string Name, int Age) : Animal(Name, Age)
	{
		cout << "Cat 클래스 생성자" << endl; 
	}
	virtual ~Cat() { cout << "Cat 클래스 소멸자" << endl; }

	virtual void Walk() override { cout << "Cat Walk" << endl; }
	void Grooming() {}
	void Punch() {}
};

class Dog : public Animal
{
public:
	Dog(string Name, int Age) : Animal(Name, Age)
	{ 
		cout << "Dog 클래스 생성자" << endl; 
	}
	virtual ~Dog() { cout << "Dog 클래스 소멸자" << endl; }

	virtual void Walk() override { cout << "Dog Walk" << endl; }
	void Bark() {}
};


int main()
{
	Animal* animal = new Cat("나비", 5);
	animal->Walk();
	cout << "고양이 이름 : " << animal->GetName() << '\n' <<
		"고양이 나이 : " << animal->GetAge() << endl;

	delete animal;
}

 

일반적으로 멤버 변수는 private 으로 설정 후 public 에 선언된 함수를 통해 접근하는 것이 일반적이다. 이렇게 함으로써 외부에서 객체의 중요한 데이터를 함부로 바꾸는 것을 예방할 수 있으며, 인터페이스 사용자들에게 해당 객체를 사용할 때 뒤에 () 괄호를 붙여야하는지 말아야하는지 통일해줄 수도 있다.

 

protected 와 public, private, protected 상속 및 클래스에 관한 더 자세한 내용은 다음 포스팅에 기술해 보겠다!

'C++ > 어려운 C++' 카테고리의 다른 글

[C++] RVO, NRVO  (0) 2025.04.30
[C++] 초기화  (0) 2025.04.15
[C++] 6. 포인터와 참조  (0) 2024.06.28
[C++] 5. 배열과 구조체 (Array and Struct)  (0) 2024.06.26
[C++] 4. 함수(Function)  (0) 2024.06.26