
procedural programming (절차적 프로그래밍)
절차적 프로그래밍은 순차적인 명령 목록으로 구성되어 있고 이 명령에는 데이터와 해당 데이터를 이용하여 수행되는 연산으로 이루어져 있다
절차적 프로그래밍의 가장 핵심은 프로그램의 logic을 구현하는 절차 (함수)를 만드는 것이다
(데이터를 함수에 전달 -> 함수가 데이터를 가지고 연산 -> 결과 return 혹은 내부적으로 처리)
즉 절차적 프로그래밍에서 데이터와 함수는 별개의 개체라고 볼 수 있다
하지만 게임 프로그래밍에서 객체 (object)는 속성과 동작을 가지고 있고 이는 분리될 수 없다, 따라서 절자적 프로그래밍은 이러한 객체를 표현하기 힘들다 (데이터와 함수가 별개이기 때문에)
object oriented programming (객체 지향 프로그래밍)
C++에서 객체란 데이터를 저장하는데 사용할 수 있는 메모리의 일부로 정의할 수 있다, 그리고 이러한 객체에 이름을 붙히면 변수로 부른다
OOP에서는 데이터(속성), 동작(함수)으로 이루어져 있는 객체 (object)를 만들고 사용하는것이 핵심이다
절차적 프로그래밍과 다르게 데이터와 동작이 분리되지 않기 때문에 객체를 쉽게 모듈화하여 설계가 가능하다
다음은 이 두 프로그래밍 스타일의 차이를 쉽게 표현할 수 있는 코드이다
1. procedural programming
enum Color
{
red,
green,
blue,
};
constexpr std::string_view colorName(Color color)
{
switch (color)
{
case red: return "red";
case green: return "green";
case blue: return "blue";
default: return "";
}
}
int main()
{
constexpr Color color{ red };
std::cout << colorName(color) << "\n";
return 0;
}
이렇게 절차적 프로그래밍은 데이터와 해당 데이터를 처리하는 함수를 별개로 두고 순서대로 처리한다
(color데이터, color 데이터로 이름을 출력하는 함수)
근데 만약 여기서 pink라는 색을 추가하고 싶다면? enum도 수정해야하고 colorName()도 수정해야 한다
따라서 코드 재사용성이 떨어지고 유지보수에 좋지 않다
2. oop
struct Red
{
std::string_view colorName{ "red" };
};
struct Blue
{
std::string_view colorName{ "blue" };
};
struct Green
{
std::string_view colorName{ "Green" };
};
int main()
{
constexpr Red red;
std::cout << red.color << "\n";
return 0;
}
객체 지향 프로그래밍은 데이터(속성), 동작을 하나의 객체에서 관리한다
따라서 새로운 pink를 넣고 싶다면 pink 객체를 만들어주면 된다 (기존 코드 수정이 필요 없음, 재사용성 향상 및 코드 유지보수성 향상)
객체 지향 프로그래밍은 코드 모듈화에 좋고 유지보수에 좋다 또한 게임과 같은 프로그램을 개발할 때 필수적인 프로그래밍 스타일이다
struct의 한계
우선 struct는 관련된 데이터를 저장하고 전달하는데 편리한 기능을 제공한다
하지만 struct를 객체를 표현하는데 사용하기에는 큰 문제점이 존재한다
struct에는 특정 객체가 유효한 상태를 유지하기 위해 반드시 유효해야 하는 조건을 의미하는 class invariant (클래스 불변식)를 강제할 방법이 없다
클래스 불변식이 깨진 객체를 사용하면 정의하지 않은 동작이 발생할 수 있다
struct Foo
{
int one{};
int two{};
};
//클래스 불변식X
예를들어 분수를 표현하는 객체를 struct로 정의한다고 가정해보자
struct Fraction
{
int numerator{ 0 }; //분자
int denominator{ 1 }; //분보
};
Fraction f{ 10, 0 }; //분모 0
수학적으로 분모는 절대 0이 될 수 없다, 하지만 위와 같이 분모에 해당하는 데이터가 0으로 초기화 될 수 있다는것이다 (불변식 위반), 이렇게 되면 0으로 나누기가 발생하여 크래쉬가 날 수 있다
클래스를 사용하면 이러한 불변식을 강제할 수 있다 (추후에 정리)
class
class는 struct와 마찬가지로 관련된 데이터, 함수를 가질 수 있는 복합 데이터 타입이다, 하지만 불변식 강제가 가능하고 데이터를 더 안전하게 관리할 수 있는 기능을 제공한다
class 이름
{
멤버 데이터
};
class Knight
{
public:
int hp{ };
int mp{ };
void Attack()
{
std::cout << "Attack!" << '\n';
}
};
int main()
{
Knight k{ 10, 20 };
k.Attack();
}
결국 여기서 hp와 mp는 멤버변수가 되고 속성(데이터)를 의미한다, 그리고 Attack()은 멤버함수로서 동작(함수)를 의미한다
member function, implicit object (암시적 객체)
클래스 타입(struct, class, union)은 멤버 변수뿐 아니라 멤버 함수도 가질 수 있다
멤버 함수는 클래스 타입 내부에 있는 함수이기 때문에 멤버 데이터에 접근이 가능하다, 멤버 함수 호출 시 자기자신 객체가 암시적 객체로 전달된다
class Knight
{
public:
int hp{ };
int mp{ };
void Attack()
{
hp = 200; //멤버 데이터에 접근이 가능
std::cout << "Attack!" << '\n';
}
};
Knight* k;
k->Attack(); //Knight가 암시적 객체로 전달됨
Attack()에서 hp는 사실 this->hp로 내부적으로 변환되고 이 this 포인터를 통해 전달된 암시적 객체인 Knight객체(k)를 참조하게 된다
또한 멤버 함수도 비멤버 함수와 마찬가지로 함수 오버로딩이 가능하다
class Knight
{
public:
void Foo()
{
std::cout << "Attack!" << '\n';
}
void Foo(int a)
{
std::cout << a << '\n';
}
};
원래 C에서 struct는 멤버 변수만 가질 수 있었고 멤버함수는 가질 수 없었지만 C++에서는 struct도 멤버 함수를 가질 수 있도록 설계되었다
만약 class인데 멤버 변수는 없고 멤버 함수만 존재한다면 불필요한 객체 생성을 유발할 수 있다, 따라서 이러한 경우에는 namespace를 사용하는걸 권장한다
class Knight()
{
public:
void Attack();
};
//이것보다
namespace Knight
{
void Attack() { std::cout << "Attack!" << '\n';
}
//이런 방식을 더 권장함
const 클래스 객체, const 멤버 함수
C++에서는 const를 이용하여 상수 변수를 만들 수 있다, const 변수는 항상 선언과 동시에 초기화가 필요하다
const int a{ 10 };
const int b; //error
class, struct, union과 같은 클래스 타입 객체도 마찬가지로 const로 선언이 가능하다
일반 타입 const 변수와 마찬가지로 선언과 동시에 초기화가 필요하다
class Knight
{
public:
int hp{};
int mp{};
}
const Knight k{ 100, 50 };
이렇게 만든 const 클래스 타입 객체는 멤버 변수를 변경할 수 없다 (public: 멤버 데이터에 직접 접근해서 수정, getter,setter를 이용하는것 둘 다 불가능)
const Knight k{ 100, 50 };
k.hp = 200; //error
또한 const 클래스 타입 객체는 const가 아닌 멤버 함수 호출도 불가능하다 (const함수만 가능), 이는 함수 매개변수로 const&타입으로 넘길때도 마찬가지다
class Knight
{
public:
int hp{};
int mp{};
void Attack()
{
std::cout << "Attack!" << '\n';
}
}
const Knight k{ 100, 50 };
k.Attack() //error

따라서 const 클래스 타입 객체에서 함수를 호출하려면 해당 함수를 const 멤버 함수로 선언해야 한다
class Knight
{
public:
int hp{};
int mp{};
void Attack() const //const멤버함수
{
std::cout << "Attack!" << '\n';
}
}
만약 .h에서 class의 멤버함수를 선언하고 cpp에서 정의한다면 정의부에서도 const를 붙여야 한다
//Knight.h
class Knight
{
public:
void Attack() const;
};
//Knight.cpp
void Knight::Attack() const
{
}
이러한 const 멤버 함수는 멤버 변수를 수정할 수 없다 (멤버 데이터를 변경하지 않는 함수는 선언 시 const로 선언하는게 좋음 ex) getter)
그리고 const 객체가 아닌 객체에서도 const 함수 호출이 가능하다
Knight k;
k.Attack(); //비 const객체인 k에서도 호출이 가능
struct와 마찬가지로 class타입 객체를 함수의 인자로 넘길때는 &를 사용하는것이 복사를 방지하여 성능상 유리하다, 그리고 객체를 수정할 것이 아니라면 const&로 넘기는게 좋다
void Foo(const Knight& InKnight)
{
//마찬가지로 InKnight는 const이기 때문에 non-const 멤버 함수 호출이 불가능하다
}
C++에서는 const여부로 함수 오버로딩이 가능하다
struct Foo
{
void Temp()
{
std::cout << "non const Temp()" << '\n';
}
void Temp() const
{
std::cout << "const Temp()" << '\n';
}
};
Access specifier (접근 지정자)
C++에는 대표적으로 3개의 접근 지정자가 존재한다
(public(공개), private(비공개), protected(상속관계에서만 공개))
C++에서는 멤버에 접근할 때 마다 컴파일러가 해당 멤버의 접근 수준을 확인한다
만약 접근이 허용되지 않는다면 compile error를 발생시키며 이러한 접근 수준 시스템을 Access control이라고 한다
struct는 기본적으로 접근 지정자가 public이다, 접근에 제한이 없다
struct Foo
{
int count{}; //default로 public:임
};
int main()
{
Foo f{ 100 };
f.count; //public:이기 때문에 멤버 접근 가능
}
하지만 class의 멤버는 기본적으로 private이다, 따라서 기본적으로 클래스 외부에서는 접근이 불가능하다
class Knight
{
int hp{};
};
int main()
{
Knight K1{ 100 };
K1.hp; //private:이기 때문에 compile error
}
이러한 접근 지정자는 직접 지정할 수 있다
public:
private:
protected:
따라서 구조체는 단순한 데이터 그룹에 적합하고, 클래스는 보다 복잡한 구조와 데이터 보호에 적합하다
목적 자체가 구조체는 단순 데이터 그룹이 목적이고 클래스는 객체를 설계하기 위한 목적으로 사용한다