C++ 추상 클래스와 정적 멤버

Seongcheol Jeon·2024년 11월 13일
0

CPP

목록 보기
13/47
post-thumbnail

추상 클래스

추상 클래스(abstract class)는 이름처럼 추상적인 클래스이다. 이때 추상적이라는 말은 구체적이지 않다는 뜻이다. 클래스가 구체적이지 않다는 것은 명확한 정의가 없다고 할 수 있다.

클래스에 순수 가상 함수가 하나라도 있으면, 추상 클래스라고 한다.

일반 함수나 멤버 변수를 가지고 있어도 순수 가상 함수를 포함한다면 추상 클래스이다.

C++ 언어에서 추상 클래스는 일반 클래스와는 달리 객체를 직접 생성할 수 없으며, 추상 클래스를 상속받은 자식 클래스에서 객체를 선언한다.
자식 클래스에서는 순수 가상 함수를 반드시 오버라이딩해야 한다.

추상 클래스와 대비되는 일반 클래스를 구상 클래스(concrete class)라고 한다.

추상 클래스는 객체지향 원칙의 다형성을 구현할 때 사용한다. 추상 클래스에는 특정 역할을 담당할 함수를 선언만 해놓고, 여러 자식 클래스에서 각기 다른 알고리즘으로 동작하도록 정의할 수 있다.

#include <iostream>
#include <list>

using namespace std;


class FileType;

class _File
{
public:
    _File(const string name): filename(name) {}
    virtual ~_File() = default;
    virtual void SaveFile(const FileType* filetype) = 0;

protected:
    float filesize{0.0f};
    string filename;
};


class Alembic: public _File
{
public:
    Alembic(const string name): _File(name) {}
    void SaveFile(const FileType* filetype) override
    {
        cout << filename << " -> " << "Alembic file saved" << endl;
    }
};


class USD: public _File
{
public:
    USD(const string name) : _File(name) {}
    void SaveFile(const FileType* filetype) override
    {
        cout << filename << " -> " << "USD file saved" << endl;
    }
};


class FileType
{
public:
    FileType() = default;
    virtual ~FileType() = default;
};


class Ascii : public FileType
{
public:
    Ascii() = default;
};


class Binary: public FileType
{
public:
    Binary() = default;
};


void SaveFileRoutine(_File* file, FileType* filetype)
{
    file->SaveFile(filetype);
}


int main()
{
    list<_File*> files;
    Alembic abc1("box.abc");
    Alembic abc2("sphere.abc");
    USD usd1("payload.usd");
    USD usd2("scene.usd");
    Binary binary;

    files.push_back(&abc1);
    files.push_back(&abc2);
    files.push_back(&usd1);
    files.push_back(&usd2);

    for(auto file: files)
    {
        SaveFileRoutine(file, &binary);
    }

    return 0;
}

결과는 다음과 같다.

box.abc -> Alembic file saved
sphere.abc -> Alembic file saved
payload.usd -> USD file saved
scene.usd -> USD file saved

위의 코드에서 전역 함수로 정의한 SaveFileRoutine은 매개변수로 전달받은 부모 클래스의 포인터로(업캐스팅) SaveFile 함수를 호출하여 파일 저장 방식을 각각 다른 알고리즘으로 동작하도록 하였다. 이렇게 설계하는 것을 전략 패턴(strategy pattern)이라고 한다.

전략 패턴같은 기능을 다른 알고리즘으로 정의하고 싶을 때 각각을 캡슐화하여 교체할 수 있게 하는 것이다.

알고리즘을 사용하는 곳(SaveFileRoutine)과 제공하는 클래스(Alembic, USD)를 분리하여, 사용하는 곳에서는 알고리즘이 어떻게 구현되었는지 신경 쓰지 않아도 되며, 다른 알고리즘으로 쉽게 교체할 수도 있다.

이렇게 하면 소프트웨어의 유지/보수성을 높이고 새로운 알고리즘을 추가하기도 쉬워진다.

추상 클래스는 전략 패턴 외에도 여러 가지 디자인 패턴에 활용할 수 있으며 객체지향 설계 원칙을 따를 수 있도록 돕는다.

추상 클래스와 인터페이스

추상 클래스를 인터페이스(interface)라고 하기도 한다. 다만 C++ 표준 명세에는 인터페이스에 대한 정의가 없다. 대신 추상 클래스와 순수 가상 함수로 비슷한 기능을 구현할 수 있다.
인터페이스는 프로그래밍에서 많이 사용되는 개념이며, 비주얼 스튜디오에서는 __interface 키워드로 인터페이스를 선언할 수 있도록 지원한다.

__interface는 표준 C++ 문법은 아니지만 비주얼 스튜디오에서 클래스를 선언할 때, class 대신 사용하면 인터페이스를 선언할 수 있다. 인터페이스에서는 순수 가상 함수임을 나타내는 =0을 가상 함수 선언 뒤에 추가하지 않아도 모두 순수 가상 함수로 선언된다.

비주얼 스튜디오에서 __interface 를 사용하는 방법은 __interface 문서 를 참고하면 된다.


정적 멤버

클래스에 멤버 변수나 함수를 선언할 때 static 키워드를 사용할 수 있으며, 이렇게 선언한 멤버 변수나 함수를 정적 멤버 변수, 정적 멤버 함수라고 한다.

정적 멤버 특징

정적 멤버는 클래스에 속하지만 특정 인스턴스에 종속되지 않으므로 메모리에 한 번만 할당되어 모든 인스턴스가 공유할 수 있다. 즉, 클래스로 선언하는 객체와는 독립적이다.

따라서 외부에서 정적 멤버 변수를 사용하거나 정적 멤버 함수를 호출할 때는 범위 연산자 (::)로 해당 클래스의 이름을 붙여서 소속을 밝혀야 한다. 객체를 생성한 후에도 정적 멤버를 사용하는 방법은 같다.
즉, 객체를 생성했다고해서 객체 이름으로 사용하는 것이 아니라, 여전히 클래스 이름을 붙여서 사용한다.

class_name::static_variable = 5;
class_name::static_function();

정적 멤버를 범위 연산자로 접근하는 것을 보면 어디서나 접근할 수 있을 것 같지만, 정적 멤버도 클래스에 속하는 것이므로 사용 범위는 일반 멤버처럼 클래스에서 지정한 접근 지정자에 따른다. 이는 정적 멤버가 전역 변수나 함수는 다르다는 것을 나타낸다.
즉, 정적 멤버는 접근 지정자로 사용 범위를 통제할 수 있다. 또한 클래스로 캡슐화할 수 있어서 어느 소속인지 이해하기가 쉽다.

정적 멤버 정리

  • 클래스당 한 번만 메모리에 할당된다.
  • 해당 클래스로 선언된 모든 인스턴스가 공유한다.
  • 소속 클래스를 범위 지정자로 붙여서 사용한다.
  • 정적 멤버 변수는 클래스 안에 선언하고 밖에서 초기화해야 한다.
  • 정적 멤버 함수는 클래스 안에 정의해도 메모리에는 별도로 존재하므로 인스턴스에 독립적이다.
class Game {
public:
	// 정적 멤버 함수 선언
    static Game* create_game();
private:
	// 정적 멤버 변수 선언
    static int game_count;
};

// 정적 멤버 변수 초기화
int Game::game_count = 0;

정적 멤버 활용

이러한 특징의 정적 멤버는 어느 때에 활용할 수 있을까? 클래스에서 어떤 자료는 모든 객체가 공유해야 할 때 정적 멤버 변수로 선언할 수 있다. 그리고 정적 멤버 함수는 클래스의 객체를 클래스 내부에서 직접 관리하고 싶을 때, 또는 유틸리티 함수를 만들 때 활용할 수 있다.

유틸리티 함수 (utility function)란, 문자열 조작, 날짜나 시간 계산, 수학적 계산, 파일 입출력 등 특정 작업을 지원하는 함수를 의미한다. 클래스나 객체에 독립적으로 프로그램의 다양한 부분에서 편리하게 재사용할 수 있으며 유지/보수성을 높이는 데에 좋다.

예를 들어 행렬이나 벡터를 처리하는 함수를 만든다고 가정해 보자. 선형대수학 클래스를 정의하면서 행렬과 벡터는 데이터를 저장해야 하므로 객체로 생성하고, 연산 함수는 선형대수학 클래스에 포함하면 될 것 같다.
그런데 이렇게 하면 행렬 연산이 필요할 때마다 객체를 생성해야 하거나 연산에 필요한 선형대수학 객체를 함수의 매개변수로 매번 전달해야 한다. 꽤 귀찮은 작업이 될 것이다.

이렇 때 행렬, 벡터 연산 함수를 선형대수학 클래스의 정적 멤버 함수로 선언하면 연산이 필요한 행렬, 벡터 객체가 있는 함수에서 바로 호출해서 사용할 수 있다. 주의할 점은 정적 멤버 함수에서는 정적 멤버 변수에만 접근할 수 있으므로 객체를 입력받아야 한다.

다음은 행렬끼리 곱셈, 행렬과 벡터를 곱셈하는 정적 멤버 함수의 예이다.

#include <iostream>
#include <list>


using namespace std;


class MyVector {
public:
    MyVector(int size);

private:
    int32_t vectorSize;
    list<int32_t> contents;
};

MyVector::MyVector(int32_t size) : vectorSize(size) {
    for (int i = 0; i < vectorSize; i++) {
        contents.push_back(0);
    }
}


class Matrix {
public:
    Matrix(int32_t row, int32_t col);
    ~Matrix();

private:
    int32_t rowCount;
    int32_t colCount;
    list<MyVector*> contents;
};

Matrix::Matrix(int32_t row, int32_t col) : rowCount(row), colCount(col) {
    for (int i = 0; i < rowCount; i++) {
        MyVector* newVector = new MyVector(colCount);
        contents.push_back(newVector);
    }
}

Matrix::~Matrix() {
    for (auto item : contents) {
        delete item;
    }
}


// 정적 클래스
class LinearAlgebra {
public:
    // 정적 멤버 함수 선언
    static void MatrixMultiply(Matrix& operand1, Matrix& operand2);
    static void DotProduct(MyVector& operand1, MyVector& operand2);

private:
    // 정적 멤버 변수 선언
    static void MatrixVectorMultiply(Matrix& operand1, MyVector& operand2);
};

void LinearAlgebra::MatrixMultiply(Matrix& operand1, Matrix& operand2) {
    cout << "caculate two matrix multiply." << endl;
}

void LinearAlgebra::MatrixVectorMultiply(Matrix& operand1, MyVector& operand2) {
    cout << "caculate matrix and vector multiply." << endl;
}

void LinearAlgebra::DotProduct(MyVector& operand1, MyVector& operand2) {
    cout << "caculate two vector dot product." << endl;
}


// 전역 함수에서 공개 정적 멤버에 접근 가능
void DoMatrixMultiply(Matrix& matrix1, Matrix& matrix2) {
    LinearAlgebra::MatrixMultiply(matrix1, matrix2);
}

// 전역 함수에서 비공개 정적 멤버에 접근 불가능
void DoMatrixVectorMultiply(Matrix& matrix1, MyVector& vector1) {
    // 다음 코드는 접근 지정자에 따라 접근할 수 없음
    //LinearAlgebra::MatrixVectorMultiply(matrix1, vector1);
}

void DoVectorInnerProduct(MyVector& vector1, MyVector& vector2) {
    LinearAlgebra::DotProduct(vector1, vector2);
}



int main()
{
    Matrix matrix1(10, 10), matrix2(10, 10);
    MyVector vector1(10), vector2(10);

    DoMatrixMultiply(matrix1, matrix2);
    DoMatrixVectorMultiply(matrix1, vector2);
    DoVectorInnerProduct(vector1, vector2);

    return 0;
}

실행 결과는 다음과 같다.

caculate two matrix multiply.
caculate two vector dot product.

LinearAlgebra 클래스는 정적 멤버로만 설계했다. 이처럼 정적 멤버로만 구성된 클래스를 정적 클래스라고 한다. 정적 클래스라는 용어는 C++에서 공식적으로 정의돼 있지는 않지만, 개발자들 사이에서 많이 사용한다.

전역 함수에서는 public 정적 멤버에 접근할 수 있지만, private 정적 멤버에 접근할 수 없다. 이처럼 접근 지정자로 정적 멤버의 접근을 통제할 수 있다는 점이 전역 변수/함수와 다른 점이다.

0개의 댓글