열혈 C++ - Chapter 03. 클래스의 기본

2024. 11. 19. 18:02Programming Language/C++

03-1. C++에서의 구조체

구조체의 등장 배경

연관있는 데이터를 하나로 묶으면 프로그램의 구현과 관리가 용이하다.

구조체는 연관 있는 데이를 하나로 묶는 문법적 장치이다.

 

구조체로 연관있는 데이터들을 묶으면 생성 및 소멸시점을 일치시키고, 이동 및 전달 시점 및 방법 또한 일치시키기 때문에 관리가 용이 해진다는 장점이 있다.

C++ 에서의 구조체 변수 선언

C에서는 struct라는 키워드를 통해 구조체 변수를 선언했는데 C++의 경우는 struct키워드가 필요 없다.

//C언어에서의 구조체 선언 방법
struct Car basicCar;
struct Car simpleCar;

↓

//C++에서의 구조체 선언 방법
Car basicCar;
Car simpleCar;

그렇기 때문에 변수를 선언할때 struct를 없에기 위해서 typedef를 선언했었는데 이 과정이 필요가 없게 된다.

 

struct Car{
    char gamerId[ID_LEN];         // 소유자ID
    int fuelGauge;                // 연료량
    int curSpeed;                 // 현재속도
}

이렇게 차량과 관련된 데이터들의 모임을 정의되었을때, 이 데이터 뿐만 아니라 해당 데이터와 연관된 함수들도

void ShowCarState(const Car &car){
    . . . .
}

void Accel(Car &car){
    . . . .
}

void Break(Car &car){
    . . . . 
}

함께 그룹으로 형성하기 때문에 함수도 하나로 묶는 것이 의미가 있을 수 있다.

 

그렇기에 C++의 경우는 구조체 안에 함수를 삽입 하는게 가능하고 C++에서는 구조체가 아닌 클래스라고 부른다.

struct Car{

    char gamerId[ID_LEN];         
    int fuelGauge;                
    int curSpeed;                 
    
    void ShowCarState(const Car &car){
        . . . .
    }

    void Accel(Car &car){
        . . . .
    }

    void Break(Car &car){
        . . . . 
    }
}

이렇게 구조체 안에 선언된 함수의 내부에서는 같은 구조체에서 선언된 변수에 직접 접근이 가능하다.

void ShowCarState(){
    cout << "소유자 ID : " << gamerID << endl;
    cout << "연료량 : " << fuelGauge << endl;
    cout << "현재속도 : " << curSpeed << "km/s" << endl << endl;
}

 

C++ 에서의 구조체 변수 선언

//변수의 생성
Car run99 = {"run99", 100, 0};
Car sped77 = {"sped77", 100, 0};

 

실제로 구조체 변수마다 함수가 독립적으로 존재하는 것은 아니긴 하나 논리적으로는 독립적으로 존재하는 형태로 보아도 문제 없다.

 

* 자바와 비슷하게 그냥 틀에다 찍어 낸 결과물들이라고 생각하면 그 구조체안에 존재하는 함수들의 기능을 탑재한 변수라고 생각하는게 더 좋을듯 하다.

 

구조체 안에 enum 상수의 선언

만약 Car 클래스를 위해서 상수가 정의 된다면 #define으로 정의 하는 것 보다

#define ID_LEN 20
#define MAX_SPD 200
#define FUEL_STEP 2
#define ACC_STEP 10
#define BEK_STEP 10

 

구조체 내부에 enum을 사용해서 선언 해주는 것이 더 좋다.

struct Car{

    enum{
        ID_LEN = 20,
        MAX_SPD = 200,
        FUEL_STEP = 2,
        ACC_STEP = 10,
        BRK_STEP = 10
    }

    char gamerId[ID_LEN];         
    int fuelGauge;                
    int curSpeed;                 
    
    void ShowCarState(const Car &car){
        . . . .
    }

    void Accel(Car &car){
        . . . .
    }

    void Break(Car &car){
        . . . . 
    }
}

이렇게 구조체 안에 enum을 선언해 두면 잘못된 외부 접근을 제한할 수 있다.

 

이와 비슷한 결로 

namespace CAR_CONST{
    enum{
        ID_LEN = 20,
        MAX_SPD = 200,
        FUEL_STEP = 2,
        ACC_STEP = 10,
        BRK_STEP = 10
    }
}

과 같이 연관있는 상수를 하나의 이름공간에 별도로 묶기도 한다.

 

함수는 외부로 뺄 수 있다.

// 구조체 안에 삽입된 함수의 선언
struct Car {
    . . . .
    void ShowCarState();
    void Accel();
    . . . .
}

//구조체 안에 선업된 함수의 정의
void Car::ShowCarState(){
    . . . . .
}

void Car::Acce(){
    . . . . .
}

 

이렇게 구조체 안에 정의된 함수는 inline선언된걸로 간주한다.

그렇기에 필요하다면 함수의 정의를 외부로 뺄 때에는 

inline void Car::ShowCarState() {. . . .}
inline void Car::Accel() {. . . .}
inline void Car::Break() {. . . .}

명시적으로 inline을 선언해줘야 한다.

 

# :: 연산자에 대해서

더보기

:: 연산자는 범위 지정 연산자(scope resolution operator)라고 불리며 C++에서는 다양한 용도로 사용된다.

 

1. namespace의 멤버에 접근

 

namespace MyNamespcae {
    int myVar = 10;
}

int main() {
    int val = MyNamespace::myVar; // MyNamespace의 myVar이라는 멤버에 접근
}

 

2. 클래스나 구조체의 정적 멤버에 접근

class MyClass {
public:
    static int staticVar;
};
int MyClass::staticVar = 5;

 

3. 클래스나 구조체의 멤버 함수 외부 정의

struct MyStruct {
    void myFunction();
};

void MyStruct::myFunction() {
    // 함수 구현
    // std::cout << "MyStruct의 myFunction 호출됨" << std::endl;
}

 

4. 전역 범위 접근

int x = 5;
void func() {
    int x = 10;
    cout << ::x; // 전역 변수 x 출력
}

 

5. 중첩 클래스나 구조체 접근

struct Outer {
    struct Inner {
        // 내부 구조체 정의
        int innerVar;
    };
};

Outer::Inner innerObj;

 

6. 열거형 멤버에 접근

enum class Color { RED, GREEN, BLUE };
Color c = Color::RED;

 

이렇게 다양한 사용 방식이 있다.

 

위에는 그중 3번에 해당하는 내용을 사용함

03-2. 클래스(Class)와 객체(Object)

클래스와 구조체의 유일한 차이점

클래스와 구조체의 차이점은 키워드가 struct가 아니라 class를 사용한다는 점 

class Car{
    char gamerID[CAR_CONST::ID_LEN];
    int fuelGauge;
    int curSpeed;
    
    void ShowCarState() {. . . .};
    void Accel() {. . . .};
    void Break() {. . . .};
};


그냥 구조체에 접근하듯이 접근하면 멤버에 접근할 수 없다는 점이다.

int main(void){
    Car run 99;
    strcpy(run99.gamerID, "run99");   // (X) - 사용불가능
    run99.fuelGauge = 100;            // (X) - 사용불가능
    run99.curSpeed = 0;               // (X) - 사용불가능
}

그렇기에 선언된 멤버에 접근 하기 위해서는 별도의 접근 제어와 관련된 선언이 추가적으로 필요하다.

 

접근제어 지시자

클래스의 접근 제어 지시자는 사용자가 해당 클래스에 접근하는 것을 제한하는 키워드로 세가지의 접근제어 지시자가 있다.

  • public                ----     어디에서든 접근을 허용
  • protected          ----      상속관계에 놓여있을때, 유도 클래스에서의 접근을 허용
  • private              ----      클래스 내(클래스 내에 정의된 함수)에서만 접근을 허용

아래와 같이 클래스가 정의되었을때

 

class Car {

private:
    char gamerID[CAR_CONST::ID_LEN];
    int fuelGauge;
    int curSpeed;
    
public:
    void InitMembers(char * ID, int fuel);
    void ShowCarState();
    void Accel();
    void Break();
}

Car의 멤버 함수에는 모두 public이기 때문에 클래스의 외부에 해당하는 main 함수에서 접근이 가능함

int main(void){

    Car run99;
    run99.InitMember("run99", 100);
    run99.Accel();
    run99.Accel();
    run99.Accel();
    run99.ShowCarState();
    run99.Break();
    run99.ShowCarState();
    
    return0;
}

 

용어정리: 객체(Object), 멤버변수, 멤버함수

 

이 코드 안에서 

이렇게 Car 클래스를 대상으로 생성된 변수를 객체라고 한다.

 

그리고 Car 클래스 내에 선언된 변수를 가리켜 멤버변수라고 한다

 

마지막으로 Car클래스 내에서 선언된 함수를 가리켜 멤버함수라고 한다.

 

C++에서의 파일 분할

클래스의 선언은 일반적으로 헤더 파일에 삽입한다.

객체생성문 및 멤버의 접근 문장을 컴파일하기 위해서 필요하다.

클래스의 이름을 따서 클래스명.h로 헤더 파일명을 붙이기도 한다.

 

#이때 인라인 함수(외부에 함수를 정의한 경우)는 컴파일 과정에서 함수 호출문을 대체해야 하기 때문에 헤더 파일에 함께 정의되어야 한다.

 

Car클래스의 멤버함수의 몸체는 다른 코드의 컴파일 과정에서 필요한게 아니라 링크의 과정을 통해서 하나의 바이너리로 구성만 되면 되기 때문에 cpp파일에 정의하는 것이 일반적이다.

클래스의 이름을 따서 Car.cpp로 소스파일의 이름을 정의 하기도 한다.

 

###여기부분 이해 잘안됨...

더보기

클래스 파일에는 선언만 하고 외부에서 함수를 정의한댔다.

그건 이해완.

 

그럼 클래스 파일이 헤더 파일이 된다면 이 함수들은 인라인 함수인데 그러면 선언해둔게 의미 없느게 아닌가? 

인라인 함수는 포함된 헤더파일 내부에 정의되어야 한다했으니..

 

가정 할 수 있는건 어차피 헤더 파일이란 건 컴파일 할때 다수의 파일이 공유하는 함수에 대한 선언 정보를 컴파일러가 알게 해주기 위해 사용하는 거라고 볼 수 있다.(물론 이게 끝은 아니고 로직을 추가할 수 도 있고,.. 일부의 기능을 이야기 하는것이지만.)

 

그렇기 때문에 컴파일러한테 얘 분명 있으니까 걱정마 라고 그냥 헤더를 넣어주는 것이고, 별개로 정의는 그 각각의 소스파일이 원하는 대로 구성하기 위해서라고 이해해도 될까..?

 

그러면 거기서 말한 인라인 함수는 정의를 함수 내부에서 해야한다는 클래스의 경우를 이야기 하는게 아니라 모든 로직을 구현할때를 이야기 하려고 그렇게 이야기 한걸까..?

 

잘 이해가 안된다...

 

도와줘요 스피드웨건 행님들..

 

03-3. 객체지향 프로그래밍의 이해

객체지향 프로그래밍은 현실에 존재하는 사물과 대상, 그리고 그들이 행하는 행동을 그대로 실체화 시키는 형태의 프로그래밍이다.

 

조금 이해를 돕자면 예를 들어서 A라는 사람을 객체로 봤을때 A는 남자이자, 프로그래머이자, 아버지이자, 친구이자, 동료이자, 어디엔가에선 리더일 수 도 있다.

이런 모든 관점에서 A를 바라보기엔 담아야할 내용이 너무 많기 때문에 이 A라는 사람을 어떤 하나의 관점에서 쳐다봐야 한다.

우리는 이 A를 프로그래머라는 관점에서만 쳐다보기로 하자.

 

A는 프로그래머로써 가지고 있는 어떤 것들이 있다.

예를 들어 프로그래머로써 사용가능한 언어의 종류, 프로그래머로서 일한 경력, 가지고 있는 컴퓨터 등 하드웨어 자원, 문서 작업 능력, 프로그램 이해 능력, 프로그램 구현 능력 등등이 존재한다.

그러면 이때 이 사람이 프로그래머로서 물적, 질적 자원들은 A가 가진 Data(경력, 사용가능한 언어, 가진 자원 등..)인 것들도 있고 A의 기능(문서작업능력, 프로그램 이해능력, 프로그램 구현능력 등..)일 수도 있다.

우리는 이렇게 어떤 사물이던 사람이던 지 간에 Data와 기능으로 그 것을 묘사할 수 있다.

 

우리가 앞서서 봤던 class 조차도 data와 기능(function)으로 나눌수 있었다.

이 말은 무엇이냐면 우리가 정의한 class는 결국 현실 세계의 객체를 추상화, 하나의 코드로 옮겨놓은 것이라고 생각할 수 있다.

 

즉, C++에서는 class의 설계가 곧 프로그래밍이다

그렇다는 이야기는 객체지향 관점에서 class를 설계한다는 것은 객체를 디자인해 나간다는 것이다.

내가 구현하고자 하는 소프트웨어의 영역에 원하는 현실세계의 객체를 코드로 옮겨 놓는 과정이라는 것이다.

 

결론적으로 현실에서 필요로하는게 소프트웨어이고 이 현실에서 필요로 하는 요구사항들을 그대로 옮겨 프로그램의 소스코드로 옮기는 것, 즉 현실세계의 객체를 지향하는 프로그램이 객체지향 프로그래밍 인것이다.

 

이렇게 객체지향 프로그래밍을 한다는 것은 class를 설계해 나간다는 것이고 그 class, 객체를 중심으로 프로그램이 동작되도록 프로그래밍 하는것이 객체지향 프로그래밍이다.

 

그러면 객체만 모아두면 프로그램이 동작할까?

현실세계를 보아도 A라는 프로그래머는 다른 프로그래머와 협업을 해야 업무를 할 수 있고 또 밥을 먹기 위해서 음식점 사장님과 상호작용을 해서 주문을 해야 밥을 먹을 수 있듯이 프로그램에서도 객체와 객체 사이의 상호작용이 있어야 하기 때문에 그 기능을 만들어놨다.

 

그렇기 때문에 객체간의 상호작용이 되어 현실세계에서 일어나는 일들을 모두 코드로 옮길 수 있다.

이런 내용들을 근거로 해서 여러가지 이론이 파생된다.

그게 바로 Object Oriented이다.

 

객체지향 프로그래밍의 이해

객체에 대한 간단한 정의

  • 사전적 의미 - 물건 또는 대상
  • 객체지향 프로그래밍 - 객체 중심의 프로그래밍
나는 과일장수에게 두 개의 사과를 구매했다.

나         -> 객체
과일장수   -> 객체
두 개      -> 데이터
사과       -> 객체
구매했다   -> 행위, 기능

 

객체지향 프로그래밍에서는 나, 과일장수, 사과라는 객체를 불러와서 두개의 사과를 구매한다라는 행위를 실체화한다.

 

여기서 두개의 사과에 속하는 두개라는 데이터는 과일장수가 갖고 있는 데이터였다가, 나라는 객체가 구매라는 행위, 기능을 하고서 나에게 옮겨온 데이터가 된다.

 

이런 과정들을 코드로 옮겨 가는 과정들이 객체지향 프로그래밍이다.

 

객체를 이루는 것은 데이터와 기능이다.

위에서 말했던 내용을 추가해서 구체화 해본다면

 

과일장수 객체에 대한 표현

  • 과일장수는 과일을 판다 --> 행위
  • 과일장수는 사과 20개, 오렌지 10개를 보유하고 있다. --> 상태
  • 과일장수의 과일 판매 수익은 현재 50,000원이다. --> 상태

과일장수 데이터 표현

  • 보유하고 있는 사과의 수 -> int numOfApples;
  • 판매 수익 -> int myMoney;

과일장수의 행위 표현

int SaleApples(int money){      // 사과 구매액이 함수의 인자로 전달
    int num = money / 1000;     // 사과가 개당 1000이라고 가정
    numOfApples -= num;         // 사과의 수가 줄어들고
    myMony += money;            // 판매 수익이 발생한다.
    return num;                 // 실제 구매가 발생한 사과의 수를 반환
}

 

이제 위의 데이터 표현, 행위 표현을 모두 묶어주는 것만 남았다.

 

이렇게 묶어주기 위해서 필요한게 class 인것이고, 이걸 기반으로 만들어내는 변수들이 객체인 것이다.

 

'과일장수'의 정의와 멤버변수의 상수화

이렇게 과일 장수에 대한 내용을 클래스로 생성했고 여기에 추가적으로 

InitMembers라는 함수가 추가로 생성되고

얼마나 파셨어요? 라고 물어보면 답변해줄 ShowSalesResult()함수 또한 

가 추가되었다.

그리고 데이터중 하나인 사과의 값은 변하지 않고 고정될것이라고 가정하고 프로그램을 생성했기 때문에 APPLE_PRICE는 변경되지 않을 값이기에 바뀌는 값인 state가 아니다.

그러면 변경되지 않는 값인 APPLE_PRICE는 혹시 실수로라도 바뀔 가능성을 차단 하기 위해서 이걸 그냥 상수화 시켜서 const선언 해주면 되지 않을까?

좋은 생각이지만 이후 추가로 생성되는 함수인 

이 InitMembers에 의해서 값이 변경될 예정이기 때문에 const 선언을 하면 에러가 발생한다.

 

그러면 그냥 생성할때 값을 초기화 해버리면서 생성하도록 만드는건 어떤가?

이것도 C++에서는 멤버 변수의 경우에 클래스를 정의할때 값을 지정할 수 없게 되어 있다.(나중에 예외에 대한 케이스를 이야기 해주긴 할것이긴 함)

 

일단은 이런 불안감을 인지한 채로 조금 더 알아보도록 하자( 추후에 해결해준다는 의미겠지..? )

 

'나'를 표현할 클래스의 정의와 객체 생성

라고 정리할 수 있다.

 

# 클래스에서 따로 접근자를 선언하지 않은 경우에는 private로 자동으로 선언된것으로 인식된다.

 

일반적인 변수 선언 방식을 사용해서 객체를 생성하는 방법은 

와 같고 동적 할당 방식으로 객체를 생성하는 방법은 

이렇게 생성할 수 있다.

 

이제 이 객체들을 이용해서 현실세계에서 일어나는 일들을 한번 시나리오를 통해서 생성해보자.

 

과일 장수 아저씨와 내가 현실에 있고 우리는 지금부터 과일을 팔고, 지금부터 과일을 사는 이야기를 만들 것이다.

먼저 '과일 장수 아저씨'를 그려보자

과일 장수 생성!

그리고 과일 장수 아저씨가 사과는 얼마에 팔예정이고, 얼마나 갖고 있고, 지금 돈이 얼마나 있는지를 먼저 설정해주자.

이제 과일 장수 아저씨에 대한 설정은 끝났다.

 

이제 '나'를 그려 보자.

'나' 생성

그리고 내가 돈을 얼마나 갖고 있는 지에 대해서 설정해주자.

 

이제 가장 중요한 과일을 사는 상황을 그려주자.

이때 위에서 말했다 싶이, 과일 장수에게 2000원에 사겠다고 하기 위해선 사장님 과일 2000원에 사십쇼! 라고 해야하기 때문에 과일장수를 첫번째 인자로 전달해줘야한다.

 

또 이 전달된 과일장수는 결국 판다는 행위를 저 함수 내부에서 진행해야 하고 그 판다는 행위는 결국 과일 장수의 잔고과 사과의 재고의 변화를 이끌어 내게 해야하기 때문에 저 함수 안에서 값의 변경이 적용되기 위해서는 참조자를 통해서 저 객체를 그대로 받아줘야 한다.

그래서 함수의 매개변수의 형태가 

이렇게 된다.

그리고 이렇게 객체와 객체간의 상호작용을 하는 함수를 호출하는 것 

이런 함수의 호출을 가리켜 "메세지 전달"이라고 한다.

 

이 부분은 메세지 전달은 아니고 상호작용의 시작을 했다고 봐야 한다.

 

아무튼 이 함수를 통해서 나는 사과를 샀고 과일 장수 아저씨는 사과를 팔았다.

 

그러면 과일을 판 과일 장수 아저씨의 근황을 한번 들어보자.

 

이제 과일 장수 아저씨가 더 팔기 위해서 나한테 묻는다.

그러면 결론적으로 이 시나리오의 결말은 

이렇게 된다.

 

이렇게 메세지 전달(Message Passing)에 대해서 잘 만들 수 있다면 좀 더 클래스를 생성하는 방향성의 가닥을 잡을 수 있을 것이다.