추상 클래스(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
정적 멤버에 접근할 수 없다. 이처럼 접근 지정자로 정적 멤버의 접근을 통제할 수 있다는 점이 전역 변수/함수와 다른 점이다.