본문 바로가기

C++

[C++] 객체지향 프로그래밍 - 클래스와 객체

 

 

절차적 프로그래밍이란?

  • 데이터와 작업(함수)가 분리되어 있는 개념
  • 즉, 데이터는 함수의 매개변수로 입력될 뿐이며 이후 함수의 동작에 의해 작업이 수행되어 output이 출력된다.
  • 굉장히 이해가 쉬운 방식이다.

 

 

 

절차적 프로그래밍의 단점

  • 어떤 함수에 모든 형식의 데이터가 입력될 수 있는 것이 아니다. 👉 func(int ~) 처럼, 함수에 입력될 수 있는 데이터의 형식이 지정되어 있다.
  • 따라서 함수가 데이터의 구조를 정확히 알고 있어야 하며, 데이터가 변하면 함수의 수정 또한 필요하다.
  • 이를 데이터와 함수가 Tightly coupled 되었다고 말하는데, 이는 함수의 재사용성을 낮추는 좋지 못한 특성이다. (loosely coupled한 프로그래밍을 작성하는 것이 좋다)
  • 이 경우 프로그램의 규모가 작을 때는 큰 문제가 없지만, 규모가 커지면 프로그램을 이해하기 어렵고, 유지보수 또한 어려우며 오작동할 확률이 커진다.

 


 

 

객체지향 프로그래밍이란?

  • 절차적 프로그래밍의 단점을 극복하기 위해 제안된 패러다임
  • C++, C#, Java 등은 객체지향 프로그래밍을 쉽게 구현할 수 있는 문법을 제공한다. (해당 언어들에서도 절차적 프로그래밍을 하고자 한다면 충분히 가능하다.)
  • 클래스와 객체를 기반으로 하여, 데이터와 함수(작업)을 하나로 묶어 표현한다. 이를 통해 Tightly coupled 문제를 해소함

 

 

 

객체지향 프로그래밍의 특징

  • 캡슐화 👉 객체는 [데이터+함수] 단위로 구현된다.
  • 정보 은닉 👉 사용자는 객체의 내부 구현에 대해 알 필요가 없으며, 알아서도 안 된다. (오사용 및 프로그램 수정 방지)
    • 따라서 사용자는 외부로 노출된 인터페이스만 활용이 가능하다.
  • 상속
  • 다형성 

 

 

 

객체지향 프로그래밍의 단점

  1. 절차적 프로그래밍의 상위 호환은 아니다.
    • 잘 설계된 절차적 프로그램이 잘못 설계된 객체지향 프로그램보다 낫다.
    • 또한, 객체지향 프로그램이라고 해서 모든 문제에 적합한 것은 아니다.
    • 모든 대상이 클래스로 치환되는 것도 아니다.
  2. 객체지향 프로그램은 어렵다.

 

 

 

 

 


 

 

클래스와 객체

 

 

클래스

  • 클래스는 객체(Object)가 생성되기 위한 기본 틀이다. (객체를 찍어내는 도장 역할)
  • 객체가 가져야 할 데이터와 기능(함수)를 정의한다. (어떤 동작을 하는 객체를 만들 것인가?)
  • 멤버 변수(데이터)를 갖는다.
    • 동의어  👉 속성(property, attribute),  필드(field), 클래스 변수(class variable)
  • 멤버 함수(함수, 동작)를 갖는다.
    • 동의어 👉 Method
  • 데이터와 함수를 은닉하고, 인터페이스를 공개할 수 있다.

 

 

객체

  • 클래스로부터 생성되는 단위 (메모리에 올라간 객체는 instance라고 구분하여 부르기도 함)
  • 서로 다른 객체는 개별적으로 관리되며, 원하는 개수만큼 생성이 가능하다.
  • 객체를 통해 클래스에 정의된 멤버 함수를 호출하여 동작한다.

 

 

 


 

 

 

클래스 정의

 

#include <iostream>


class Player
{
	// member variable
	std::string name;
	int health;
	int xp;

	// member function
	void Talk(std::string text);
	bool IsDead();
};

int main()
{
	Player p;
}

 

클래스를 생성하는 방법은 위와 같다.

 

위 코드에서는 Player 라는 이름을 갖는 class를 설정하였고, 해당 클래스 내에 멤버 변수와 멤버 함수를 설정하였다.

이렇게 만들어진 클래스는 main 함수에서 Player p;를 이용하여 객체 p를 만드는 데 사용된다.

 

 

 

#include <iostream>


class Player
{
	// member variable
	std::string name;
	int health;
	int xp;

	// member function
	void Talk(std::string text);
	bool IsDead();
};

int main()
{
	Player p;

	Player* p2 = new Player();
	delete p2;
}

 

클래스를 포인터로 할당하는 것 또한 가능하다. 클래스 객체를 포인터로 생성하여 heap 메모리에 할당하고, 다 쓴 후 delete 할 수 있다.

 

 

 

 

 

#include <iostream>


class Account
{
	std::string name;
	double balance;

	bool Withdraw(double amount);
	bool Deposit(double amount);
};

int main()
{
	Account kimAccount;
	Account leeAccount;

	Account* parkAccount = new Account();

	(*parkAccount).balance;
	(*parkAccount).Deposit(1000.00);

	parkAccount->balance;
	parkAccount->Deposit(1000.00);

	delete parkAccount;

}

 

위와 같은 코드가 있다고 하자. parkAccount는 클래스의 객체가 포인터 형태로 생성되었다.

이때 parkAccount라는 객체가 new를 통해 heap 공간에 할당되었으므로, 해당 객체의 멤버 변수와 멤버 함수는 heap 공간에 정의되어 있다. 또한, 해당 stack 메모리에는 해당 heap 메모리에 저장된 parkAccount의 요소의 시작 주소가 담겨있다.

 

기존에 포인터 역참조를 수행할 때는, *변수를 이용했었다.

하지만 클래스 객체에 역참조 할 때는 (*객체).멤버 변수(또는 함수) 를 수행해야 하므로, 약간 번거롭다.

따라서 이 경우에는 객체->멤버 변수(또는 함수)로 편리하게 접근한다.

 


 

정보 은닉

 

 

클래스 멤버 접근 제한자

 

  • public
    • 어디서든 접근 가능

 

  • private
    • 클래스의 멤버 (friend 클래스에서만 접근 가능)

 

  • protected
    • 상속된 클래스의 객체에서만 접근 가능

 

 

 

 

포인터 클래스 간략 예제

#include <iostream>

class Player
{
public:
	std::string name;
	int health;
	int xp;
};


int main()
{
	Player p;
	Player* p_ptr = new Player();

	p.health = 10;
	p_ptr->health = 10; // (*p_ptr).health = 10;과 동일

	std::cout << p.health << std::endl;
	std::cout << p_ptr->health << std::endl;
}

 

 

 

 

 

private 사용 예제

#include <iostream>
#include <string>


class Player
{
public:
	std::string name;

private:
	int health;
	int xp;

};

int main()
{
	Player p;
	Player* p_ptr = new Player();

	p.health = 10;
	p_ptr->health = 10; // (*p_ptr).health = 10;과 동일
	p_ptr->name = 'hi';

	std::cout << p.health << std::endl;
	std::cout << p_ptr->health << std::endl;

}

 

여기서는 위와 다르게 health, xp라는 int 변수를 public이 아닌 private에 할당하였다.

이렇게 되면, p 혹은 p_ptr에서 health에 접근하는 것이 불가능하다. (위 코드를 실행해보면 해당 부분에서 오류가 난다.)

하지만, name은 여전히 public이므로 p_ptr->name은 가능하다.

 

 

 

 

그렇다면 private(정보 은닉)은 왜 사용하는 것일까?

 

직접 변경되어서는 안되는 변수들이 존재하기 때문이다. 예를 들어 위 코드에서 health라는 변수를 게임에서 사용한다고 해보자. health는 회복하거나, 공격을 받았을 때만 변경되어야 하는 값이므로, 어떤 객체에 의해서 health = 100; 을 할당하여 변경하는 것은 불가능해야 한다. 

 

이러한 경우에 해당 변수를 은닉하기 위하여 private을 사용하는 것이다.

 

 

 

 

class Account
{
public:
	bool Withdraw(double amount) // 출금
	{
		if (balance - amount < 0)
		{
			return false;
		}

		balance -= amount;
	}
	bool Deposit(double amount) // 예금
	{
		balance += amount;
	}

private:
	std::string name;
	double balance = 0;
	
};

int main()
{
	Account kim_account;
	kim_account.Withdraw(100);
}

 

위 클래스에서는 balance가 private에 지정되어 있다.

이 경우에 kim_account.balance = 100; 같은 작업을 통하여 balance에 직접 접근하는 것은 불가능하다.

하지만, 클래스의 멤버 함수인 Withdraw과 Deposit은 함수 내에서 balance를 사용하여 값을 변경할 수 있다.

 

'C++' 카테고리의 다른 글

[C++] 객체지향 프로그래밍 - 클래스와 객체 (2)  (0) 2024.10.26
[C++] 참조자 Reference  (0) 2024.10.20
[C++] 포인터를 함수에 input, return  (1) 2024.10.13