본문 바로가기
SW/C,C++

[C/C++] C언어에서의 객체지향

by FastBench 2024. 3. 31.

 

아래 코드는 구조체를 이용해 Stack 자료구조를 작성한 예시이다.

typedef struct Stack {
    int top; // Index of the top element in the stack. Initially -1 because the stack is empty.
    unsigned capacity; // Maximum number of items that can be stored in the stack.
    int* array; // Pointer to the array that will store the stack's elements.
} Stack;

Stack* createStack(unsigned capacity) {
    Stack* stack = (Stack*) malloc(sizeof(Stack)); // Allocate memory for the stack structure.
    stack->capacity = capacity; // Set the capacity of the stack.
    stack->top = -1; // Initialize top as -1, indicating the stack is empty.
    stack->array = (int*) malloc(stack->capacity * sizeof(int)); // Allocate memory for the stack's data array.
    return stack; // Return the pointer to the newly created stack.
}

 

Stack 구조체 데이터를 다루는 함수는 전역으로 아래와 같이 선언한다.

int isFull(Stack* stack) {
    return stack->top == stack->capacity - 1; // If top is at the last position, stack is full.
}

int isEmpty(Stack* stack) {
    return stack->top == -1; // If top is -1, the stack is empty.
}

void push(Stack* stack, int item) {
    if (isFull(stack))
        return; // If stack is full, do not push any item.
    stack->array[++stack->top] = item; // Increment top and add the item to the array.
    printf("%d pushed to stack\n", item);
}

int pop(Stack* stack) {
    if (isEmpty(stack))
        return INT_MIN; // If stack is empty, return INT_MIN as error indication.
    return stack->array[stack->top--]; // Return the top item and decrement top.
}

int peek(Stack* stack) {
    if (isEmpty(stack))
        return INT_MIN; // If stack is empty, return INT_MIN as error indication.
    return stack->array[stack->top]; // Return the top item without removing it.
}

void freeStack(Stack* stack) {
    free(stack->array); // Free the memory allocated for the stack's data array.
    free(stack); // Free the memory allocated for the stack structure.
}

아래는 메인함수의 예시이다.

 

int main() {
    Stack* stack = createStack(100); // Create a stack with capacity for 100 items.

    push(stack, 10); // Push items onto the stack.
    push(stack, 20);
    push(stack, 30);

    printf("%d popped from stack\n", pop(stack)); // Pop an item and print it.
    printf("Top item is %d\n", peek(stack)); // Peek at the top item and print it.

    freeStack(stack); // Free the allocated memory for the stack.

    return 0;
}

 

동작은 잘 하지만 코드의 구조가 명확하지 않다는 단점이 있다.

객체 지향으로 코드를 짜면, push pop 등의 함수가 Stack 구조체와 밀접한 관련이 있다는 것을 읽는 사람으로 하여금 확실하게 알게 할 수 있다.

 

절차지향 구현의 단점

  • 코드 분산: C언어에서는 데이터 구조와 이를 조작하는 함수들이 분리되어 있어, 관련 코드가 여러 곳에 흩어지기 쉽습니다. 이는 코드의 이해와 관리를 어렵게 만듭니다.
  • 재사용성과 유지보수의 어려움: 코드의 재사용과 수정을 위해서는 관련 함수들을 찾아내고 이해하는 추가적인 노력이 필요합니다.
  • 데이터 보호 부족: 데이터 구조를 조작하는 모든 함수가 해당 데이터에 접근할 수 있으므로, 데이터의 무결성을 보장하기 어렵습니다.

위 단점을 극복하게끔 C++ 를 통해 객체지향방식으로 작성해 보면 아래와 같다.

#include <iostream>
#include <limits.h> // For INT_MIN

class Stack {
private:
    int* array;
    int top;
    unsigned capacity;

public:
    // Constructor to initialize the stack
    Stack(unsigned size) {
        array = new int[size];
        capacity = size;
        top = -1;
    }

    // Destructor to free allocated memory
    ~Stack() {
        delete[] array;
    }

    // Check if the stack is full
    bool isFull() const {
        return top == static_cast<int>(capacity) - 1;
    }

    // Check if the stack is empty
    bool isEmpty() const {
        return top == -1;
    }

    // Add an element to the stack
    void push(int item) {
        if (isFull()) {
            std::cout << "Stack is full. Cannot push " << item << std::endl;
            return;
        }
        array[++top] = item;
        std::cout << item << " pushed to stack\n";
    }

    // Remove the top element from the stack
    int pop() {
        if (isEmpty()) {
            std::cout << "Stack is empty. Cannot pop\n";
            return INT_MIN;
        }
        return array[top--];
    }

    // Get the top element of the stack
    int peek() const {
        if (isEmpty()) {
            std::cout << "Stack is empty. Cannot peek\n";
            return INT_MIN;
        }
        return array[top];
    }
};

int main() {
    Stack stack(3);

    stack.push(10);
    stack.push(20);
    stack.push(30);

    std::cout << "Top element: " << stack.peek() << std::endl;
    std::cout << stack.pop() << " popped from stack\n";
    std::cout << "Top element: " << stack.peek() << std::endl;

    // Trying to push another element when the stack is full
    stack.push(40);

    return 0;
}

 

객체지향 구현의 장점

  • 캡슐화: 클래스를 사용하면 데이터와 관련 기능을 하나의 단위로 묶을 수 있습니다. 이는 코드의 응집도를 높이고, 재사용성과 유지보수성을 개선합니다.
  • 추상화: 사용자는 스택의 내부 구현을 몰라도, 제공되는 인터페이스(메서드)를 통해 스택을 사용할 수 있습니다. 이는 사용의 단순성을 보장합니다.
  • 확장성: 클래스를 상속받아 새로운 기능을 추가하거나 기존 기능을 수정하는 것이 용이합니다. 예를 들어, 다른 데이터 타입을 저장하는 스택을 구현하거나, 추가적인 기능을 제공하는 스택을 쉽게 만들 수 있습니다.
  • 데이터 보호: private 키워드를 사용하여 클래스 내부의 데이터를 외부에서 직접 접근하는 것을 제한할 수 있습니다. 이는 데이터의 안전한 관리를 가능하게 합니다.

 

하지만 절차지향언어(C)가 마냥 단점만 있는 것은 아니다.

  1. 간결성: 절차 지향 프로그래밍은 간단하고 직관적인 경우가 많습니다. 스택 구현 같은 비교적 단순한 자료 구조에서는 클래스나 객체보다 함수 몇 개로 구성된 코드가 더 이해하기 쉬울 수 있습니다.
  2. 효율성: 절차 지향 코드는 종종 객체 지향 코드보다 실행 속도가 빠릅니다. 객체 지향 프로그래밍에서는 추가적인 추상화 레이어가 메모리 사용과 실행 시간에 영향을 줄 수 있지만, 절차 지향 프로그래밍에서는 이러한 추가 오버헤드가 적습니다.
  3. 하드웨어 제어: 절차 지향 프로그래밍은 하드웨어와 밀접하게 연결된 프로그램을 작성할 때 유리합니다. 예를 들어, 임베디드 시스템 또는 시스템 프로그래밍에서는 메모리 관리와 실행 효율성이 중요한데, 이러한 환경에서 절차 지향 방식이 장점을 발휘할 수 있습니다.

이제 함수포인터를 이용하여 C언어에서 객체지향을 모방하는 방식에 대해 아래에서 언급하고자 한다.

#include <stdio.h>
#include <stdlib.h>
#include <limits.h>

typedef struct Stack Stack;

struct Stack {
    int top;
    unsigned capacity;
    int* array;
    // Function pointers as member methods
    int (*isFull)(Stack* stack);
    int (*isEmpty)(Stack* stack);
    void (*push)(Stack* stack, int item);
    int (*pop)(Stack* stack);
    int (*peek)(Stack* stack);
    void (*freeStack)(Stack* stack);
};

먼저 위와 같이 구조체 내에 함수포인터를 선언해주고, 아래와 같이 기능을 정의해준다.

 

int Stack_isFull(Stack* stack) {
    return stack->top == stack->capacity - 1;
}

int Stack_isEmpty(Stack* stack) {
    return stack->top == -1;
}

void Stack_push(Stack* stack, int item) {
    if (stack->isFull(stack))
        printf("Stack is full\n");
    else
        stack->array[++stack->top] = item;
}

int Stack_pop(Stack* stack) {
    if (stack->isEmpty(stack))
        return INT_MIN;
    else
        return stack->array[stack->top--];
}

int Stack_peek(Stack* stack) {
    if (stack->isEmpty(stack))
        return INT_MIN;
    else
        return stack->array[stack->top];
}

void Stack_freeStack(Stack* stack) {
    free(stack->array);
    free(stack);
}

 

이후 createStack 함수 내에서 각 멤버 함수 포인터를 해당 스택 연산을 수행하는 구체적인 함수들로 초기화하여 할당한다. 이를 통해, 구조체 인스턴스는 이 함수 포인터를 통해 자신의 상태를 관리하고 조작하는 메서드를 갖게 되며, 이는 객체지향 프로그래밍에서 인스턴스 메서드를 사용하는 것과 유사한 방식으로 작동한다.

 

 

Stack* createStack(unsigned capacity) {
    Stack* stack = (Stack*)malloc(sizeof(Stack));
    if (!stack) return NULL; // Check for successful allocation

    stack->capacity = capacity;
    stack->top = -1;
    stack->array = (int*)malloc(stack->capacity * sizeof(int));
    if (!stack->array) { // Check for successful allocation
        free(stack);
        return NULL;
    }

    // Assign function pointers to the struct's methods
    stack->isFull = Stack_isFull;
    stack->isEmpty = Stack_isEmpty;
    stack->push = Stack_push;
    stack->pop = Stack_pop;
    stack->peek = Stack_peek;
    stack->freeStack = Stack_freeStack;

    return stack;
}

아래 메인함수를 통해 잘 동작하는 것을 확인할 수 있다.

int main() {
    Stack* stack = createStack(100);

    stack->push(stack, 10);
    stack->push(stack, 20);
    stack->push(stack, 30);

    printf("%d popped from stack\n", stack->pop(stack));
    printf("Top item is %d\n", stack->peek(stack));

    stack->freeStack(stack);

    return 0;
}

 

이번 게시글을 통해, 절차지향과 객체지향 프로그래밍의 기본 개념과 차이점을 익히고, C언어에서 객체지향적 설계를 모방하는 방법에 대해 알아보았다.

 

댓글