반응형
아래 코드는 구조체를 이용해 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)가 마냥 단점만 있는 것은 아니다.
- 간결성: 절차 지향 프로그래밍은 간단하고 직관적인 경우가 많습니다. 스택 구현 같은 비교적 단순한 자료 구조에서는 클래스나 객체보다 함수 몇 개로 구성된 코드가 더 이해하기 쉬울 수 있습니다.
- 효율성: 절차 지향 코드는 종종 객체 지향 코드보다 실행 속도가 빠릅니다. 객체 지향 프로그래밍에서는 추가적인 추상화 레이어가 메모리 사용과 실행 시간에 영향을 줄 수 있지만, 절차 지향 프로그래밍에서는 이러한 추가 오버헤드가 적습니다.
- 하드웨어 제어: 절차 지향 프로그래밍은 하드웨어와 밀접하게 연결된 프로그램을 작성할 때 유리합니다. 예를 들어, 임베디드 시스템 또는 시스템 프로그래밍에서는 메모리 관리와 실행 효율성이 중요한데, 이러한 환경에서 절차 지향 방식이 장점을 발휘할 수 있습니다.
이제 함수포인터를 이용하여 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언어에서 객체지향적 설계를 모방하는 방법에 대해 알아보았다.
반응형
'SW > C,C++' 카테고리의 다른 글
[C/C++] 콜백함수의 인자로써 void 포인터 (0) | 2024.04.21 |
---|---|
[C/C++] 이중포인터와 void 포인터 (0) | 2024.04.20 |
[C/C++] 동적 2차원 배열과 가변길이배열(VLA) (0) | 2024.04.11 |
[C/C++] C++ 에서의 콜백함수 (0) | 2024.04.07 |
[C/C++] 콜백함수 (Callback Function) (0) | 2024.04.01 |
댓글