C 언어로 OOP 스타일의 Stack 구현하기

최근 C 언어에서 객체지향 프로그래밍(OOP)을 구현하려고 시도하면서, C++에서 많이 사용되는 벡터(Vector)를 구현하기 전에 먼저 스택(Stack)을 구현해보기로 했습니다. 자료구조를 공부하면서 Stack의 push 연산부터 구현해보기로 했고, 이를 OOP 스타일로 어떻게 구현할 수 있는지 살펴보았습니다.

이번에 구현한 스택은 C 언어에서 객체지향 프로그래밍의 개념을 적용하여, 스택에 데이터를 push, pop, clear, delete하는 메서드를 갖는 구조체를 작성하는 방식입니다.

프로젝트 링크

이 프로젝트는 GitHub - Object-Oriented-C-Language에서 확인할 수 있습니다. 이곳의 algorithm.h와 algorithm.c 파일을 참고하여 스택을 구현했습니다.

algorithm.h (헤더 파일)

#include "main.h"

#ifndef __ALGORITHM_H
#define __ALGORITHM_H

#pragma pack(push, 1)
typedef struct STACK {
    void** data;               // 스택에 저장될 데이터
    size_t top;                // 스택의 탑(가장 최근에 저장된 데이터)
    size_t size;               // 스택이 차지하는 메모리 크기
    size_t byteSize;           // 각 데이터 항목의 크기
    void (*push)(struct STACK*, void*);  // push 함수 포인터
    void* (*pop)(struct STACK*);         // pop 함수 포인터
    void (*clear)(struct STACK*);        // clear 함수 포인터
    void (*delete)(struct STACK*);       // delete 함수 포인터
} Stack;
#pragma pack(pop)

Stack new_stack(size_t type);  // 새로운 스택 생성 함수

#define new_Stack(type) new_stack(sizeof(type))  // 매크로를 사용하여 타입에 맞는 스택 생성
#endif

이 헤더 파일에서는 Stack 구조체를 정의하고, 스택을 조작하는 메서드들을 함수 포인터로 연결했습니다. 각 메서드는 스택에 데이터를 추가하거나 제거하고, 스택을 초기화하거나 삭제하는 기능을 합니다. 또한, new_stack() 함수는 새로운 스택을 생성하는 함수로, 각 데이터 항목의 크기를 인자로 받습니다.

algorithm.c (구현 파일)

#include "algorithm.h"

// stackPush: 스택에 데이터를 추가하는 함수
// stack: 스택 객체, data: 스택에 넣을 데이터
static void stackPush(Stack* stack, void* data) {
    // 스택에 저장된 데이터의 크기를 확장하기 위해 realloc 호출
    // 현재 스택의 크기보다 하나 더 큰 크기로 재할당
    stack->data = (void**)realloc(stack->data, (stack->top + 1) * sizeof(void*));
    
    // 새 공간에 데이터를 할당하고, 해당 위치에 data 복사
    *(stack->data + stack->top) = malloc(stack->byteSize);  // 새 데이터를 위한 메모리 할당
    memcpy(*(stack->data + stack->top), data, stack->byteSize);  // 실제 데이터 복사

    // 스택의 top을 증가시키고, 스택의 크기를 다시 계산
    stack->top++;
    stack->size = stack->top * sizeof(void*);
}

// stackPop: 스택에서 데이터를 꺼내는 함수
// stack: 스택 객체
static void* stackPop(Stack* stack) {
    // 스택의 맨 위에서 데이터를 제거하고, 그 데이터를 반환
    void* removedData = *(stack->data + stack->top - 1);

    // top을 하나 줄이고, 스택 크기 재계산
    stack->top--;
    stack->size = stack->top * sizeof(void*);
    
    // 스택 메모리를 재할당하여 크기 줄이기
    stack->data = (void**)realloc(stack->data, stack->size);
    
    // 꺼낸 데이터 반환
    return removedData;
}

// stackClear: 스택에 저장된 모든 데이터를 해제하고, 스택을 비우는 함수
// stack: 스택 객체
static void stackClear(Stack* stack) {
    // 스택에 있는 각 데이터를 해제
    for (size_t i = 0; i < stack->top; i++) {
        free(*(stack->data + i));  // 각 데이터 메모리 해제
    }
    
    // 스택 정보를 초기화
    stack->top = 0;   // 스택의 top을 0으로 초기화
    stack->size = 0;   // 스택의 크기도 0으로 초기화
    free(stack->data); // 스택 데이터 메모리 해제
    stack->data = NULL;  // 데이터 포인터 초기화
}

// stackDelete: 스택을 삭제하는 함수
// stack: 스택 객체
static void stackDelete(Stack* stack) {
    // 스택을 비운 후
    stackClear(stack);
    
    // 스택 관련 정보를 초기화
    stack->byteSize = 0;  // 각 데이터 항목의 크기 초기화
    stack->data = NULL;    // 데이터 포인터 초기화
}

// new_stack: 새로운 스택을 생성하는 함수
// type: 스택에 저장될 데이터 타입의 크기
Stack new_stack(size_t type) {
    Stack stack;
    stack.data = NULL;  // 초기 데이터 포인터는 NULL
    stack.push = stackPush;   // push 함수 포인터 연결
    stack.pop = stackPop;     // pop 함수 포인터 연결
    stack.clear = stackClear; // clear 함수 포인터 연결
    stack.delete = stackDelete; // delete 함수 포인터 연결
    stack.top = 0;  // 초기 top 값은 0
    stack.size = 0; // 초기 크기는 0
    stack.byteSize = type;  // 데이터 항목 크기 설정 (type)
    
    return stack;  // 초기화된 스택 객체 반환
}

각 함수 설명:

  1. stackPush:
    • 스택에 데이터를 추가하는 함수입니다.
    • realloc을 사용해 기존의 data 배열을 확장하고, 새 데이터를 할당하여 복사합니다.
  2. stackPop:
    • 스택에서 데이터를 제거하는 함수입니다.
    • top을 감소시키고, 스택의 크기를 다시 계산하여 realloc을 사용해 메모리를 조정합니다.
  3. stackClear:
    • 스택에 저장된 모든 데이터를 해제하고 스택을 초기화합니다.
  4. stackDelete:
    • stackClear를 호출하여 스택을 비운 후, 스택 구조체 내의 추가적인 메모리 해제를 수행합니다.
  5. new_stack:
    • 새로운 스택을 생성하는 함수입니다. 타입 크기를 인자로 받아 스택 구조체를 초기화하고 반환합니다.

매크로 new_Stack:

매크로 new_Stack(type)은 new_stack(sizeof(type))을 호출하는 방식으로, 스택을 쉽게 생성할 수 있게 도와줍니다. 예를 들어, new_Stack(int)를 사용하면 int 타입에 맞는 스택을 생성할 수 있습니다.

예시 사용법:

int main() {
    Stack intStack = new_Stack(int);  // int 타입의 스택 생성
    int value = 10;

    intStack.push(&intStack, &value);  // 값 10을 스택에 push
    int* poppedValue = (int*)intStack.pop(&intStack);  // 값을 pop하고 출력

    printf("Popped value: %d\n", *poppedValue);  // 출력: Popped value: 10

    intStack.delete(&intStack);  // 스택 삭제
    return 0;
}

결론

C 언어에서 객체지향적인 방식으로 스택을 구현하면, 코드의 재사용성과 유지보수성이 높아지며, 다양한 데이터 타입을 처리할 수 있는 유연성을 갖게 됩니다. void* 포인터를 활용해 어떤 데이터 타입도 저장할 수 있도록 구현한 것이 큰 특징입니다. 이를 통해 C 언어에서도 객체지향 프로그래밍의 핵심 개념을 적용할 수 있다는 점에서 유용한 학습이 되었습니다.

얼마 전 친구들이 디코에서 회원가입/로그인 시스템을 만들고 있길래 재밌어 보여서 C로 만들어 봤다.

https://github.com/sunwookim05/Member-management-system

 

GitHub - sunwookim05/Member-management-system

Contribute to sunwookim05/Member-management-system development by creating an account on GitHub.

github.com

먼저 main.h 를 만든다 모든 코드의 베이스가 됄 코드이다.

#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <string.h>
#include <time.h>

#pragma once
#ifndef __MAIN_H
#define __MAIN_H

#define import extern
#define final const
#define null NULL
#define false 0
#define true 1

typedef char *String;
typedef char byte;
typedef char int8_t;
typedef short int16_t;
typedef int int32_t;
typedef long long int64_t;
typedef unsigned char ubyte;
typedef unsigned char uint8_t;
typedef unsigned short uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long long uint64_t;
typedef uint8_t boolean;

#pragma warning(pop)
#endif

그리고 전에 만들었던 Scanner-with-C 를 활용하고 싶어 Scanner와 System 클래스를 구현했다.

 

System.h

#include "main.h"

#pragma once
#ifndef __SYSTEM_H
#define __SYSTEM_H

struct __stdin_t;

#pragma pack(push, 1)
/**
 * The {@code System} class contains several useful class fields
 * and methods. It cannot be instantiated.
 *
 * Among the facilities provided by the {@code System} class
 * are standard input, standard output, and error output streams;
 * access to externally defined properties and environment
 * variables; a means of loading files and libraries; and a utility
 * method for quickly copying a portion of an array.
 *
 * @since   1.0
 */
typedef struct _System{
    struct __stdout_t{
        /**
        * @brief System out print function
        * @param format
        * @return void
        */
        void (*print)(const String, ...);
        /** 
        * @brief System out println function
        * @param format
        * @return void
        */
        void (*println)(const String, ...);
    }out;
    struct __stdin_t{
        int (*read)();
    } in;
}SYSTEM;
#pragma pack(pop)

#pragma warning(pop)
#endif

Scanner.h

#include "main.h"

#pragma once
#ifndef __Scanner_H
#define __Scanner_H

struct __stdin_t;

#pragma pack(push, 1)
/** 
 * @brief Scanner class
 */
typedef struct Scanner{
    /**
    * @brief User input char
    * @param void
    * @return char
    */    
    char (*nextChar)(void);

    /**
     * @brief User input Byte
     * @param void
     * @return byte
     */
    byte (*nextByte)(void);

    /**
     * @brief User input Short
     * @param void
     * @return int16_t
     */
    int16_t (*nextShort)(void);

    /**
     * @brief User input Int
     * @param void
     * @return int32_t
     */
    int32_t (*nextInt)(void);

    /**
     * @brief User input Long
     * @param void
     * @return int64_t
     */
    int64_t (*nextLong)(void);

    /**
     * @brief User input Unsigned Byte
     * @param void
     * @return ubyte
     */
    ubyte (*nextUByte)(void);

    /**
     * @brief User input Unsigned Short
     * @param void
     * @return uint16_t
     */
    uint16_t (*nextUShort)(void);

    /**
     * @brief User input Unsigned Int
     * @param void
     * @return uint32_t
     */
    uint32_t (*nextUInt)(void);

    /**
     * @brief User input Unsigned Long
     * @param void
     * @return uint64_t
     */
    uint64_t (*nextULong)(void);

    /**
     * @brief User input boolean
     * @param void
     * @return boolean
     */
    boolean (*nextBoolean)(void);

    /**
     * @brief User input Float
     * @param void
     * @return float
     */
    float (*nextFloat)(void);

    /**
     * @brief User input Double
     * @param void
     * @return double
     */
    double (*nextDouble)(void);

    /**
     * @brief User input Long Double
     * @param void
     * @return long double
     */
    long double (*nextLDouble)(void);

    /**
     * @brief User input String no Spaces
     * @param void
     * @return String
     */
    String (*next)(void);

    /**
     * @brief User input String with Spaces
     * @param void
     * @return String
     */
    String (*nextLine)(void);
}Scanner;
#pragma pack(pop)

Scanner new_Scanner(struct __stdin_t);

#pragma warning(pop)
#endif

Systemout.c

#include "main.h"
#include "System.h"

void print(const String format, ...) {va_list ap;char buf[4096];va_start(ap, format);vsprintf(buf, format, ap);va_end(ap);fprintf(stdout, "%s", buf);}
void println(const String format, ...) {va_list ap;char buf[4096];va_start(ap, format);vsprintf(buf, format, ap);va_end(ap);fprintf(stdout, "%s\n", buf);}

SYSTEM System = {print, println};

Scanner.c

#include "main.h"
#include "System.h"
#include "Scanner.h"

char nextChar(void){
    char c;
    scanf("%c", &c);
    getchar();
    return c;
}

int8_t nextByte(void){
    int8_t b;
    scanf("%hhd", &b);
    getchar();
    return b;
}

int16_t nextShort(void){
    int16_t s;
    scanf("%hd", &s);
    getchar();
    return s;
}

int32_t nextInt(void){
    int32_t i;
    scanf("%d", &i);
    getchar();
    return i;
}

int64_t nextLong(void){
    int64_t l;
    scanf("%lld", &l);
    getchar();
    return l;
}

uint8_t nextUByte(void){
    uint8_t b;
    scanf("%hhu", &b);
    getchar();
    return b;
}

uint16_t nextUShort(void){
    uint16_t s;
    scanf("%hu", &s);
    getchar();
    return s;
}

uint32_t nextUInt(void){
    uint32_t i;
    scanf("%u", &i);
    getchar();
    return i;
}

uint64_t nextULong(void){
    uint64_t l;
    scanf("%llu", &l);
    getchar();
    return l;
}

boolean nextBoolean(void){
    String s = (String)calloc(0, sizeof(char) * 5);
    scanf("%s", s);
    if(atoi(s)|| !strncmp(s, "true", 4) || !strncmp(s, "True", 4) || !strncmp(s, "TRUE", 4)){
        free(s);
        return true;
    }else{
        free(s);
        return false;
    }
}

float nextFloat(void){
    float f;
    scanf("%f", &f);
    getchar();
    return f;
}

double nextDouble(void){
    double d;
    scanf("%lf", &d);
    getchar();
    return d;
}

long double nextLDouble(void){
    long double ld;
    scanf("%Lf", &ld);
    getchar();
    return ld;
}

String next(void){
    String s = (String)calloc(0, sizeof(char) * 4096);
    scanf("%s", s);
    s = (String)realloc(s, sizeof(char) * (strlen(s) + 1));
    getchar();
    return s;
}

String nextLine(void){
    String s = (String)calloc(0, sizeof(char) * 4096);
    scanf("%[^\n]", s);
    getchar();
    return s;
}

/**
* Constructs a new {@code Scanner} that produces values scanned
* from the specified input stream. Bytes from the stream are converted
* into characters using the underlying platform's
*
* @param  source An input stream to be scanned
*/
Scanner new_Scanner(struct __stdin_t source){
    Scanner scanner = {
        .nextChar = nextChar,
        .nextByte = nextByte,
        .nextShort = nextShort,
        .nextInt = nextInt,
        .nextLong = nextLong,
        .nextUByte = nextUByte,
        .nextUShort = nextUShort,
        .nextUInt = nextUInt,
        .nextULong = nextULong,
        .nextBoolean = nextBoolean,
        .nextFloat = nextFloat,
        .nextDouble = nextDouble,
        .nextLDouble = nextLDouble,
        .next = next,
        .nextLine = nextLine
    };
    return scanner;
}

다음으론 User 의 정보를 저장하고 처리할 수 있는 User 클래스를 구현하기 위해 User.h 와 User.c 를 만들었다.

 

User.h

#include "main.h"

#pragma once
#ifndef __User_H
#define __User_H

typedef struct __USERDATA__{
    String id;
    String password;
}USERDATA;

typedef struct __USERS__{
    void (*writeLog)(uint8_t, USERDATA, FILE *);
    boolean (*logout)(USERDATA *);
    boolean (*signup)(USERDATA *, FILE *);
    boolean (*login)(USERDATA *, FILE *);
}USERS;

typedef enum __MENU__{
    SIGNUP = 1, LOGIN, LOGOUT, USERINFO,  EXIT
}__MENU__;

typedef uint8_t MENU;

USERS new_User();
USERDATA new_UserData();
void delete_UserData(USERDATA *);

#pragma warning(pop)
#endif

User.c

#include "User.h"
#include "System.h"
#include "Scanner.h"

import SYSTEM System;

void delete_UserData(USERDATA *info){
    free(info->id);
    free(info->password);
}

void writeLog(uint8_t logCate, USERDATA info, FILE *fp){
    String logCateStr[3] = {"SIGNUP", "LOGIN", "LOGOUT"};
    time_t t = time(NULL);
    struct tm tm = *localtime(&t);
    fp = fopen("log.txt", "a");
    fprintf(fp, "%d-%d-%d %d:%d:%d %s\nID: %s\nPassword: %s\n\n", tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, logCateStr[logCate - 1], info.id, info.password);
    fclose(fp);
}

boolean signup(USERDATA *info, FILE *fp){
    Scanner sc = new_Scanner(System.in);
    fp = fopen("user.txt", "r");
    System.out.println("Sign up");
    System.out.print("ID: ");
    info->id = sc.next();
    System.out.print("Password: ");
    info->password = sc.next();

    USERDATA user = new_UserData();
    while (fscanf(fp, "%s", user.id) != EOF){
        if(!strcmp(info->id, user.id)){
            delete_UserData(&user);
            return false;
        }
    }
    delete_UserData(&user);
    fclose(fp);

    fp = fopen("user.txt", "a");
    fprintf(fp, "%s %s\n", info->id, info->password);
    fclose(fp);
    return true;
}

boolean login(USERDATA *user, FILE *fp){
    Scanner sc = new_Scanner(System.in);
    fp = fopen("user.txt", "r");
    System.out.println("Login");
    System.out.print("ID: ");
    user->id = sc.next();
    System.out.print("Password: ");
    user->password = sc.next();
    
    USERDATA info = new_UserData();
    
    while (fscanf(fp, "%s %s", info.id, info.password) != EOF){
        if(!strcmp(user->id, info.id) && !strcmp(user->password, info.password)){
            delete_UserData(&info);
            fclose(fp);
            return true;
        }
    }
    fclose(fp);
    delete_UserData(&info);
    return false;
}

boolean logout(USERDATA *info){
    Scanner sc = new_Scanner(System.in);
    System.out.print("Do you want to logout? (y/n): ");
    char bf = sc.nextChar();
    if(bf == 'y' | 'Y'){
        info->id = NULL;
        info->password = NULL;
        delete_UserData(info);
        System.out.println("Logout success");
        return true;
    }else{
        System.out.println("Logout failed");
        return false;
    }
}

USERS new_User(){
    USERS user;
    user.logout = logout;
    user.signup = signup;
    user.login = login;
    user.writeLog = writeLog;
    return user;
}

USERDATA new_UserData(){
    USERDATA info;
    info.id = (String)calloc(0, sizeof(char) * 20);
    info.password = (String)calloc(0, sizeof(char) * 20);
    return info;
}

마지막으로 클래스들을 활용하여 호출할 메인 클레스를 만든다.

 

main.c

#include "main.h"
#include "System.h"
#include "Scanner.h"
#include "User.h"

import SYSTEM System;

int main(void){
    Scanner sc = new_Scanner(System.in);
    USERS users = new_User();
    USERDATA user = new_UserData();
    FILE *fp;
    boolean isSignup = false;
    boolean isLogin = false;
    boolean isLogout = true;
    MENU menu = 0;

    while (true){
        System.out.print("Select menu\n1. Sign up\n2. Login\n3. Logout\n4. LOGININFO\n5. EXIT\n");
        menu = sc.nextUByte();
        if(menu == SIGNUP){
            isSignup = users.signup(&user, fp);
            System.out.println(isSignup ? "Sign up success" : "Sign up failed");
            if (isSignup) users.writeLog(SIGNUP, user, fp);
        }else if(menu == LOGIN){
            if(isLogin){
                System.out.println("Already login");
            }else{
                isLogin = users.login(&user, fp);
                System.out.println(isLogin ? "Login success" : "Login failed");
                users.writeLog(LOGIN, user, fp);
            }
        }else if(menu == LOGOUT){
            if(!isLogin){
                System.out.println("Already logout");
            }else{
                users.writeLog(LOGOUT, user, fp);
                isLogout = users.logout(&user);
                if(isLogout) isLogin = false;
            }
        }else if(menu == USERINFO){
            if(isLogin) System.out.println("ID: %s\nPassword: %s", user.id, user.password);
            else System.out.println("Please login");
        }else if(menu == EXIT){
            break;
        }else{
            System.out.println("Wrong menu");
        }
    }

    delete_UserData(&user);
    return 0;
}

콘솔에 이렇게 입력하면 실행이 된다.

gcc -c src\main.c -I inc\

gcc -c src\Scanner.c -I inc\

gcc -c src\Systemout.c -I inc\

gcc -c src\User.c -I inc\

gcc -o main main.o Scanner.o Systemout.o User.o

.\main

 

+ Recent posts