본문 바로가기
C & C++/C++

[C++] 클래스

by 거북이 코딩 2024. 7. 13.

C++로 마스터하는 객체지향 언어와 STL

Ch 01. 클래스

객체지향 개괄

객체지향도 절차지향적으로 코드를 작성하지만 객체를 중심으로 기술한다. 어떤 방식으로 사고를 하며 코드를 작성하는 것인지가 중요하다.

 { //C++ 스타일
     string s0 = "hello";
     string s1 = "world";
     s0.append(s1); //s0 += s1; 

     cout << s0 << endl;
 }
 { // C 스타일
     char s2[100] = "hello"; 
     char s3[] = "world";

     strcat(s2, s3);
     cout << s2 << endl;
 }

C style : 내부 구현이 드러나 있고 이를 고려해서 작성해야한다.

C++ Style : 내부 구현이 드러나 있지 않은 추상화 된 상태로 작성한다.

클래스와 객체

class Player //클래스
{
	int health;
	int damage;
};

int main()
{
	Player player0; //객체
	return 0;
}

struct와 유사하지만 단 하나의 차이점이 있다. 클래스는 기본 접근 지정자가 private이고 구조체는 public 이다.

클래스의 필드(데이터)를 private으로 제한하는 이유는 아무곳에서나 접근할 수 있으면 오류가 발생할 확률이 높아지기 때문이다.

class Player
{
private:
    int _health;
    int _power;
    string _name;

public:
    Player(int health, int power, string name) //생성자
    {
        _health = health;
        _power = power;
        _name = name;
    }

    void attack(Player4& target)
    {
        cout << "Attack " << _name << "->" << target._name << endl;
        target.damaged(_power);
    }

    void damaged(int power)
    {
        _health -= power;
        if (_health <= 0)
            cout << "Died" << _name << endl;
    }
};

int main()
{
    {
        Player david{ 200, 100, "David" };
        Player daniel{ 200, 100, "Daniel" };
        
        //daniel._health -= 300; // 불가능하다
        david.attack(daniel);
        david.attack(daniel);
    }
}

생성자

생성자란 객체가 생성될 때 호출되는 함수로, 반환형이 없고 클래스의 이름과 함수의 이름이 같다. 생성자는 private 영역의 데이터를 초기화 해주는 역할을 한다.

class Player
{
private:
    int _health;
    int _power;
    string _name;

public:
    Player(int health, int power, string name) //생성자
    {
        _health = health;
        _power = power;
        _name = name;
    }
};

생성자를 따로 정의하면 디폴트 생성자는 삽입되지 않기 때문에 직접 정의해주어야 한다.

class Player0
{
private:
    int _health;
    int _power;

public:
    Player0()
    {
        // 디폴트 생성자(파라메터가 한 개도 없는 생성자)
        // 한 개의 생성자도 주어지지 않는다면 컴파일러가 암시적으로 만들어준다
        _health = 100;
        _power = 50;
        cout << "Player " << _health << ", " << _power << endl;
    }
    Player0(int health, int power)
    {
        _health = health;
        _power = power;

        cout << "Player " << _health << ", " << _power << endl;
    }
};

void player0()
{
    Player0 player0(100, 50);
    Player0 player1 = Player0(300, 150);
    Player0* player2 = new Player0(400, 250);

    Player0 player3;
    Player0 player4 = Player0();
    Player0* player5 = new Player0();
}

위임 생성자는 생성자 내에서 다른 생성자를 호출하는 방법이다.

class Player3
{
private:
    int _health;
    int _power;
public:
    Player3() : Player3(100, 50) // 위임 생성자
    {
    }
    Player3(int health, int power)
    {
        _health = health;
        _power = power;

        cout << "Player " << _health << ", " << _power << endl;
    }
    void print()
    {
        cout << _health << endl;
        cout << _power << endl;
    }
};

void player3()
{
    Player3 player;
    player.print();
}

이니셜라이저로 const 멤버를 초기화 하는 방법

Player(int health, int power, string name)
    : _health(health), _power(power), _name(name) //이니셜 라이저
    // 멤버 초기화 리스트, 순서가 바뀌어도 초기화 순서는 클래스에 선언된 순서이다
{
    cout << "Player " << _health << ", " << _power << endl;
}

참조자 멤버 변수를 초기화 할때도 위와 같이 초기화 한다. 성능에서 약간의 이점이 있다.

default, delete 키워드를 사용하여 생성자 관리

class Player
{
private:
    // 인라인 초기화, 명시적으로 초기화 하지 않으면 아래값으로 초기화 된다.
    int _health = 100;
    int _power = 50;
    const string _name = "noname";
public:
		Player5() = default; // 컴파일러가 만든 생성자를 사용하겠다는 것을 명시
		Player5() = delete; // 컴파일러가 생성자를 암시적으로 만들지 말 것을 명시
};

파괴자

파괴자는 클래스가 소멸하는 시점에 데이터를 정리하는 역할을 맡는다. 만약 동적할당으로 생성된 객체가 있다면 delete해주어야 파괴자가 호출된다.

class String
{
private:
    char* _str;

public:
    String(const char* str)
    {
        int len = strlen(str);
        _str = new char[len + 1]; // '\\0;
        strcpy(_str, str);
    }

    ~String()
    {
        delete[] _str; // 파괴자에서 해제를 해줘야한다
    }

    void print()
    {
        cout << _str << endl;
    }
};

선언과 정의의 분리 & 전방 선언

선언의 두가지 종류

  • 정의가 포함된 선언 : 함수의 정의
  • 정의가 포함되지 않은 선언 : 함수의 프로토타입

C++ 에서는 원칙적으로 중복정의가 허용되지 않는다. 하지만 예외적인 경우가 몇가지 있다.

  • 클래스
  • 구조체
  • 열거형
  • 인라인 함수

한 파일 내에서는 한번만 정의되야 하지만 다른 소스파일에서 사용하려면 같은 내용으로 해당 소스파일내에서 한번 더 정의해주어야 한다. 하지만 매번 다른 소스파일에서 재정의 해주는것은 매우 번거롭기 때문에 헤더파일을 이용한다.

파일을 분리할 때는 크게 세가지 분류로 분리할 수 있다.

  • main.cpp : 메인 함수를 포함하는 소스파일
  • util.cpp : 클래스의 멤버함수를 포함한 다른 함수들의 정의가 포함된 소스파일
  • util.h : 클래스와 함수의 선언이 포함된 헤더파일

헤더파일에 함수의 정의까지 넣게 되면 중복 정의가 되기 때문에 오류가 발생한다. 그래서 cpp파일로 분리하는 것이다. 또한 클래스내부에서 외부로 함수의 정의를 분리하게 되면 캡슐화를 할 수 있지만 자동 인라인화가 되지 않는다. (클래스 내부 정의는 자동 인라인화 됨)

클래스가 직접 사용되지 않고 단순히 포인터나 참조로 사용된다면 전방선언으로 해결할 수 있다.

#include <iostream>
#include "function.h"
#include "Villiage.h"
#include "person.h"

using namespace std;

void foo1(); // util1.cpp

int main()
{
    func();
    foo1();

    Villiage v;
    v.add(new Person(10, 10, "david"));
}
#pragma once
// #pragma once -> 헤더파일은 한 번만 include 하도록 함, 비표준
// #ifndef #define #endif -> 헤더파일을 한 번만 include 하도록 함, 표준

#include <iostream>
#include <string>

// Person 클래스의 선언 분리
class Person
{
private:
    float _weight;
    float _height;
    const std::string _name;

public:
    Person(float weight, float height, const std::string& name);
    void print();
};

// 함수의 선언
void foo();
#include "person.h"

// Person 클래스 함수들의 정의 분리
Person::Person(float weight, float height, const std::string& name)
    : _weight(_weight), _height(height), _name(name)
{

}

void Person::print()
{
    using namespace std;
    cout << _name << endl;
}

// 함수의 정의 분리
void foo()
{
    Person p(60.f, 160.f, "davoid");
    p.print();
}
#pragma once

#include <vector>

//#include "person.h" // 헤더 파일 include는 최소화 하자

class Person; // 전방 선언
class Villiage
{
private:
    // 전방 선언을 위해서는 포인터나 레퍼런스 형이어야 함.
    // 포인터나 레퍼런스는 사이즈가 알려져있기 떄문
    std::vector<Person*> persons;
public:
    void add(Person* person);
};
#include "Villiage.h"
#include "person.h"

void Villiage::add(Person* person)
{
	//함수의 정의
}

this 포인터

this 포인터는 자기 자신을 가르키는 포인터이다. this 포인터를 사용하면 변수 충돌을 방지할 수 있고, 자기 자신을 반환 할 때 유용하게 사용한다.

class Person
{
private:
    float weight;
    float height;
    string name;

public:
    Person(float weight, float height, const string& NAME)
        : weight(weight), height(height), name(NAME)
    {
        // 이름이 같은 경우 this로 구분 가능
        weight; // 파라메터 weight
        this->weight; // 멤버 변수 weight
        
        // 이름이 같지 않기 때문에 this 생략 가능
        name; // 멤버 변수 name 
        this->name; // 멤버 변수 name
    }

    void loseWeight(float weight)
    {
        // 아래의 둘은 같다
        this->weight;
        weight;

        this->weight -= weight;
        if (this->weight < 0)
            this->weight = 0;
    }

    float getBMI()
    {
        return weight / (height * 100 * height * 100);
    }

    Person& complete(Person& person)
    {
        if (this->getBMI() < person.getBMI())
            return *this; // this의 반환
        else
            return person;
    }

    void doCeremony()
    {
        cout << name << " win!!" << endl;
    }
};

void personFunc()
{
    Person david(67.3f, 172.3f, "david");
    Person daniel(58.3f, 167.3f, "daniel");

    daniel.complete(david).doCeremony(); //승리한 객체가 세레모니를 호출함
}

this 포인터를 이용한 연쇄 호출

struct Transaction
{
    const int txID;
    const int fromID;
    const int toID;
    const int value;

    class Builder // nested class
    {
    private:
        int _fromID;
        int _toID;
        int _value;

    public:
        Transaction build()
        {
            int txID = _fromID ^ _toID ^ _value;
            return Transaction{txID, _fromID, _toID, _value};
        }

        Builder& setFromID(int fromID)
        {
            _fromID = fromID;
            return *this;
        }

        Builder& setToID(int toID)
        {
            _toID = toID;
            return *this;
        }

        Builder& setValue(int value)
        {
            _value = value;
            return *this;
        }
    };
};

void buildTransaction()
{
    Transaction::Builder builder;
    builder.setFromID(1).setToID(2).setValue(100); //연쇄 호출
    Transaction tx = builder.build();
}

const

일반 포인터에 컨스트 포인터를 대입할 수 없다. 왜냐하면 일반 포인터를 통해 간접적으로 값을 바꿀 수 있기 때문이다. 또한 컨스트 객체는 컨스트 함수만 호출할 수 있다. 그래서 값을 변경하지 않는 함수에는 const 키워드를 붙여 const 객체에서 호출할 수 있게 한다.

class Person
{
private:
    const string _name = "david"; // const 멤버 변수는 인라인으로 초기화 가능
    float _weight;
    float _height;

public:
    Person(const string& name, float weight, float height)
        : _name(name), _weight(weight), _height(height) 
        // const 멤버 변수는 멤버 초기화 리스트에서 초기화 가능
    {}

    float getWeight(/* const Person* this */) const
    {
        // const가 붙은 멤버 함수에서의 this 포인터의 타입
        // const Person*
        return _weight;
    }

    float getHeight(/* Person* this */)
    {
        // this 포인터의 타입
        // Person*
        return _height;
    }
};
int main()
{
    const Person person("David", 75.f, 181.f);
    cout << person.getWeight() << endl; // getWeight에 const Person*가 넘어간다.
    //cout << person.getHeight() << endl; // getHeight에 const Person*을 넘길 수 없다.

    Person *person0 = new Person("Daniel", 57.f, 175.f);
    const Person* person1 = person0; // 변환 가능
    //Person* person2 = person1; // 변환 불가능
}

정적 멤버

자동 지역변수 : 지역을 벗어나면 메모리가 해제됨

정적 지역변수 : 실행 중 한번만 초기화되고 프로그램이 끝날때 해제됨

클래스 정적 멤버 변수도 똑같이 시행된다. 접근 제한이 클래스 이름인 하나의 전역변수로 볼 수 있다. 클래스 멤버 변수 또한 객체가 여러개 생성되어도 한번만 생성된다.

  • 정적 멤버 함수 : static 멤버함수 또는 static 멤버변수에만 접근이 가능하다.
  • 일반 멤버 함수 : 모든 멤버에 접근이 가능하다.
class Person
{
private:
    static int num0;
public:
    static int num1; //inline static int num1 = 0; 처럼 인라인 초기화 가능
    static const int num2; //인라인 초기화 가능

    Person();
    void print();
    static void staticPrint();
};
void personFunc();

헤더파일에서 멤버변수로 다른 클래스를 포함하고 있다면 전방선언으로 해결할 수 있다. 하지만 인라인 초기화까지 한다면 전방선언으로 되지 않는다.

// 클래스 멤버 변수 초기화
int Person::num0 = 0;
int Person::num1 = 0;
const int Person::num2 = 0; // const 초기화

Person::Person()
{
    num0++;
}

void Person::print()
{
    cout << num0 << endl;
}

void Person::staticPrint()
{
    cout << num0 << endl;
    //print(); // 비 정적 멤버 접근 불가, 즉 this가 없다
}

void personFunc()
{
    Person p0;
    Person p1;

    p0.print();
    p1.print();

    // public static 멤버 접근
    cout << Person::num1 << endl;

    Person::staticPrint();
    p0.staticPrint();
}

멤버 함수 포인터

class Person
{
public:
    void print(int i) //일반 멤버 함수
    {
        cout << i << endl;
    }

    static void staticPrint(int i) //정적 멤버 함수
    {
        cout << i << endl;
    }
};

int main()
{
    void (Person:: * fn)(int) = &Person::print; // &을 꼭 붙여야한다.

    Person person;
    (person.*fn)(1); // this를 넘겨줘야 함

    typedef void (Person::* Func0)(int); // typedef 를 사용하여 간략히 표기
    Func0 fn0 = &Person::print;
    (person.*fn0)(2);

    using Func1 = void (Person::*)(int); //using 사용
    Func1 fn1 = &Person::print;
    (person.*fn1)(3);

    function<void(Person*, int)> fn2 = &Person::print; //function 사용
    fn2(&person, 4);

    // static은 일반 함수와 동일
    void (*fn3)(int) = &Person::staticPrint; // &없이도 가능
    fn3(5);
}

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

[C++] 상속  (0) 2024.08.05
[C++] 연산자 오버로딩  (4) 2024.07.16
[C++] 함수, 범위, 공간  (0) 2024.07.08
[C++] 포인터, 참조  (1) 2024.07.01
[C++] 흐름 제어, 복합 데이터  (0) 2024.06.26