와와

씹어먹는 C++ 공부하기 본문

개발/C++

씹어먹는 C++ 공부하기

정으주 2024. 5. 16. 01:09

1-1. 자~ C++의 세계로

시작

1-2. 첫 C++ 프로그램 분석하기

- iostream 헤더파일을 포함하여 std::count, std::endl와 같은 표준 입출력 객체를 사용할 수 있다.

  • 표준 입출력 라이브러리 iostream은 스트림 기반 입출력 기능을 제공함

- using namespace std;를 통해 std::를 생략할 수 있지만, 권장하지 않음

문제 1

문제 2
hi
my name is Psi 라고 출력될 것이다.

1-3. C++은 C친구 - C와 공통점

- Google의 https://google.github.io/styleguide/cppguide.html#Variable_Names

2. C++ 참조자(레퍼런스)의 도입

레퍼런스

변수를 가리키는 또 다른 방법 ( 별명 )

- 레퍼런스 vs 포인터

레퍼런스 불가능 불가능 컴파일러에서 레퍼런스를 통해 접근하는 모든 부분을 원래 변수로 치환할 수 있기 때문에, 별도의 메모리를 할당하지 않을 수 있음
포인터 가능 가능 항상 메모리 상에 존재

- 참조자의 참조자는 불가능 "레퍼런스의 레퍼런스,레퍼런스의 배열, 레퍼런스의 포인터는 존재할 수 없다."
- std::cin 에서 레퍼런스로 변수에 값 전달 ( scanf는 포인터 )
- 상수 리터럴을 레퍼런스로 참조하는 것은 불가능하지만 상수 참조자로 선언한다면 가능함

int &ref = 4; // 오류

const int &ref = 4; 
int a = ref; // a=4와 동일

- 배열의 레퍼런스

- 레퍼런스를 리턴하는 이유

  • 큰 구조체나 객체를 리턴할 때 복사 비용이 크기 때문에, 레퍼런스를 리턴하면 메모리 주소만 복사하게 되어 효율적입니다.
  • -Dangling reference 주의 > 상수 참조자로 받으면 리턴된 값은 상수로 취급, 참조자가 살아있는 동안 값이 유지됨

문제 1

단순히 변수의 별명으로 사용될 때는 컴파일러에서 기존 변수로 치환된다고 하니 메모리 상에 존재할 필요가 없겠지만, 상수 참조자처럼 임시 객체가 생성되어야 할 때는 메모리를 사용하게 될 것이다... 그 외 다른 경우들이 뭐가 있을까

3. C++ 의 세계로 오신 것을 환영합니다. (new, delete)

- new, delete를 사용하여 힙 메모리에서 동적 메모리를 할당/ 해제할 수 있음

- 돌아온 마이펫

#include <iostream>

typedef struct Animal {
    char name[30]; //이름
    int age; //나이
    int health; //체력
    int food; //배부름
    int clean; //청결
} Animal;

void create_animal(Animal* animal) { //동물 추가
    std::cout << "이름? ";
    std::cin >> animal -> name;

    std::cout << "나이? ";
    std::cin >> animal -> age;

    animal -> health = 100;
    animal -> food = 100;
    animal -> clean = 100;
}

void play(Animal* animal) { //동물 상태 변경
    animal->health += 10;
    animal->food -= 20;
    animal->clean -= 30;
}

void show_stat(Animal* animal) { //동물 상태 출력
    std::cout << "이름: " << animal->name << std::endl;
    std::cout << "나이: " << animal->age << std::endl;
    std::cout << "체력: " << animal->health << std::endl;
    std::cout << "배부름: " << animal->food << std::endl;
    std::cout << "청결: " << animal->clean << std::endl;
}

void one_day_pass(Animal* animal) { //하루가 지남에 따라 동물 상태 변화
    animal->health -= 10;
    animal->food -= 30;
    animal->clean -= 20;
}

int main()
{
    //돌아온 마이펫
    Animal* list[30];
    int animal_count = 0;

    while (true) {
        std::cout << "1. 동물 추가하기" << std::endl;
        std::cout << "2. 놀기" << std::endl;
        std::cout << "3. 상태 보기" << std::endl;
        std::cout << "원하는 기능을 선택하세요: ";

        int input;
        std::cin >> input;

        switch (input) {
        case 1:
            list[animal_count] = new Animal;
            create_animal(list[animal_count]);
            animal_count++;
            break;

        case 2:
            int num;
            std::cout << "동물 번호? ";
            std::cin >> num;

            play(list[num]);
            break;

        case 3:
            int num;
            std::cout << "동물 번호? ";
            std::cin >> num;

            show_stat(list[num]);
            break;

        case 4:
            return 0;
        }

        for (int i = 0; i < animal_count; i++) {
            one_day_pass(list[i]);
        }
    }
}

4-1. 이 세상은 객체로 이루어져 있다

객체 지향 프로그래밍의 도래

1 세대: 진공관들 사이의 전선 연결을 바꾸어 가며 전기 신호를 전달....

2 세대: 어셈블리어 (Low level) , COBOL, FORTRAN, BASIC (High level)

3 세대: 절차 지향 언어 (Procedural programming language) - 파스칼, C언어

  • Procedure(함수) 를 지향하는 언어 (함수의 인자라는 개념이 생김)
  • 프로그램을 설계할 때 중요한 부분을 하나의 프로시져로 만들어서 쪼개어 처리

이후 : 객체 지향 언어 (Object oriented language) - C++, Java, Python, C#

객체란 무엇인가, 클래스란 무엇인가

  • 접근 지시자 (public, private)
#include <iostream>

class Date {
    int year_;
    int month_;        // 1 부터 12 까지.
    int day_;      // 1 부터 31 까지.
    int monthDate[12] = {31, 28, 31,30,31,30,31,31,30,31,30,31};

    bool IsLeapYear(int year) {        //윤년
        if (year % 4 == 0) {
            if (year % 100 == 0) {
                if (year % 400 == 0)
                    return true;
                else
                    return false;
            }
            else
                return true;
        }
        else
            return false;
    }

public:
    void SetDate(int year, int month, int date) {
        year_ = year;
        month_ = month;
        day_ = date;
    };

    void AddDay(int inc) {
        day_ += inc;

        while (day_ > monthDate[month_ - 1]) {
            if (month_ == 2 && IsLeapYear(year_)) {
                if (day_ > 29) {
                    day_ -= 29;
                    AddMonth(1);
                }
                else {
                    break;
                }
            }
            else {
                day_ -= monthDate[month_ - 1];
                AddMonth(1);
            }
        }
    };

    void AddMonth(int inc) {
        month_ += inc;

        while (month_ > 12) {
            month_ -= 12;
            AddYear(1);
        }
    };

    void AddYear(int inc) {
        year_ += inc;
    };

    void ShowDate() {
        std::cout << year_ << "년 " << month_ << "월 " << day_ << "일" << std::endl;
    };
};

int main() {
    Date date;
    date.SetDate(1999, 1, 8);
    date.AddDay(1000);
    date.ShowDate();
    return 0;
}

 

4-2. 생성자와 함수의 오버로딩

Date day1(2011, 3, 1); // 암시적 방법 (implicit)
Date day2 = Date(2012, 3, 1); // 명시적 방법 (explicit)
Date day3; // 디폴트 생성자

생각해보기

1.

생성자 만들어보기

교점 개수 구하는거... 잘 못했습니다

4-3. 스타크래프트를 만들자 ① (복사 생성자, 소멸자)

New vs Malloc

1. 메모리 할당과 해제
- new로 할당 -> delete로 해제
- malloc으로 할당 -> free로 해제

2. 생성자와 소명자
- malloc은 new와는 달리 생성자, 소멸자 호출 없음

3. 타입 안정성과 형 변환
- new는 타입이 알아서 지정됨
- malloc은 적절한 형변환이 필요함

4. 예외 처리
- new는 타입 안전성과 예외 처리 지원
- malloc은 void 포인터를 반환 -> null처리 권장

// new
MyClass* obj = new MyClass();
delete obj;

// malloc
MyClass* obj = (MyClass*)malloc(sizeof(MyClass));
free(obj);

new (생성자)를 통해 할당해준 메모리는 delete (소멸)를 통해 해제해주어야 한다.

~ ( 클래스의 이름 )

매개변수 형태 알아보기

const char* : 문자열의 주소값을 보낼 때 사용. 원본 값에 영향을 미치지 않도록 const로 선언한다.

const & : 불필요한 메모리 할당을 막기 위해 레퍼런스로 보냄. 마찬가지로 원본 값을 지키기 위해 const로 선언한다.

복사 생성자

얕은 복사 (주소 복사)

Photon_Cannon::Photon_Cannon(const Photon_Cannon& pc) {
  std::cout << "복사 생성자 호출 !" << std::endl;
  hp = pc.hp;
  shield = pc.shield;
  coord_x = pc.coord_x;
  coord_y = pc.coord_y;
  damage = pc.damage;
}

주소를 복사하기 때문에 두 객체가 동일한 메모리 주소를 가리킨다. 소멸시킬때 문제가 생길 수 있음

깊은 복사 (값 복사)

Photon_Cannon::Photon_Cannon(const Photon_Cannon &pc) {
  std::cout << "복사 생성자 호출! " << std::endl;
  hp = pc.hp;
  shield = pc.shield;
  coord_x = pc.coord_x;
  coord_y = pc.coord_y;
  damage = pc.damage;

  name = new char[strlen(pc.name) + 1];
  strcpy(name, pc.name);
}

메모리를 할당하고 데이터를 복사하여 각각 독립적인 주소를 가지게 됨.

생각해보기

#include <iostream>
#include <string.h>

class string {
    char* str;
    int len;

public:
    string(char c, int n);  // 문자 c 가 n 개 있는 문자열로 정의
    string(const char* s);
    string(const string& s);
    ~string();

    void add_string(const string& s);   // str 뒤에 s 를 붙인다.
    void copy_string(const string& s);  // str 에 s 를 복사한다.
    int strlen();                       // 문자열 길이 리턴
};

string::string(char c, int n) {
    len = n;
    str = new char[n + 1];

    for (int i = 0; i < n; i++) {
        str[i] = c;
    }
    str[n] = '\0';
}

string::string(const char* s)
{
    len = std::strlen(s);
    str = new char[len + 1];
    strcpy(str, s);
}

string::string(const string& s)
{
    len = s.len;
    str = new char[len+1];
    strcpy(str, s.str);
}

string::~string()
{
    if (str)
    {
        delete[] str;
    }
}

void string::add_string(const string& s)
{
    char* newStr = new char[len + s.len + 1];
    strcpy(newStr, str);
    strcat(newStr, s.str);

    delete[] str;
    str = newStr;
    len += s.len;
}

void string::copy_string(const string& s)
{
    delete[] str;

    len = s.len;
    str = new char[len + 1];
    strcpy(str, s.str);
}

int string::strlen()
{
    return len;
}

4-4. 스타크래프트를 만들자 ② (const, static)

  • 생성자 초기화 리스트(initializer list)
Marine::Marine() : hp(50), coord_x(0), coord_y(0), damage(5), is_dead(false) {}
Marine::Marine(int x, int y, int default_damage)
    : coord_x(x),
      coord_y(y),
      hp(50),
      default_damage(default_damage),  //이렇게 함수의 인자로 받아 초기화 시킬 수도 있음
      is_dead(false) {}

생성과 초기화가 동시에 수행됨

상수, 레퍼런스는 생성 이후 변경이 안되니까 클래스 내부에 넣고 싶다면 생성시 초기화리스트를 이용해야 한다.

  • 클래스의 const, static
    • 정적 변수 (Static Variable)
      • 지역을 빠져나가도 파괴되지 않는 변수
      • 멤버 변수인 경우, 클래스의 모든 객체들이 공유
      • 모든 전역 및 static 변수들은 정의와 동시에 값이 자동으로 0 으로 초기화 되지만
      • 클래스 static 변수들의 경우, 내부에서 초기화할 수 없고(중복 정의), 무조건 외부에서 한번 초기화 해주어야 한다.
    • 정적 상수 (Const Static)
      • 클래스 수준의 상수: 정적 상수는 클래스의 모든 인스턴스에서 공유, 하나의 메모리만을 차지하여 메모리를 절약할 수 있다.
      • 변경 불가능: 초기화된 이후 값이 변경되지 않음
      • 컴파일 타임 상수: 초기화 값이 컴파일 시간에 결정되므로 컴파일러가 최적화를 수행할 수 있음
      • 클래스 내부에서 초기화 가능
    • 정적 함수
//선언
static void show_total_marine();

//정의
void Marine::show_total_marine() {
  std::cout << "전체 마린 수 : " << total_marine_num << std::endl;
}

//호출
Marine::show_total_marine();

정적 함수 내부에서는 정적 변수밖에 사용하지 못한다. -> 어떤 객체에도 속해 있지 않는 함수이기 때문에 어떤 인스턴스의 지역 변수를 써야 하는지 알 방법이 없음

  • this 포인터
Marine& Marine::be_attacked(int damage_earn) {
  hp -= damage_earn;
  if (hp <= 0) is_dead = true;

  return *this;
}

다음과 같이 this 포인터를 사용하여 객체를 반환할 수 있다. 이 부분에서 this가 객체 그 자체를 의미하는 C#과 조금 헷갈렸는데,

this: 객체의 포인터 변수 ( 객체의 주소값을 저장함 )

*this: 객체 그 자체

Marine&: 객체 레퍼런스

라고 이해하면 될 듯 하다..

  • 레퍼런스 반환과 값 반환의 차이
#include <iostream>

class A {
    int x;

public:
    A(int c) : x(c) {}

    int& access_x() { return x; }
    int get_x() { return x; }
    void show_x() { std::cout << x << std::endl; }
};

int main() {
    A a(5);
    a.show_x();

    int& c = a.access_x();
    c = 4;
    a.show_x();

    int d = a.access_x(); //레퍼런스에서 값으로 암묵적 형변환
    d = 3;
    a.show_x();

    // 아래는 오류
    //    int값을 반환할 때 임시 값인 x'가 x의 값을 받아 복사 생성됨. 이 생성된 값은 문장이 끝나면 소멸되기 때문에 레퍼런스 변수로 받을 수 없음
    // 
    // int& e = a.get_x();
    // e = 2;
    // a.show_x();

    int f = a.get_x();
    f = 1;
    a.show_x();

    a.access_x() = 3;    // a.x = 3 과 같음
}
  • const 함수
    • 변수들의 값을 바꾸지 않고 읽기 만 하는, 마치 상수 같은 멤버 함수를 상수 함수 로써 선언할 수 있음
    • 상수 함수 내에서는 객체들의 '읽기' 만이 수행되며, 상수 함수 내에서 호출 할 수 있는 함수로는 다른 상수 함수 밖에 없음
//선언
int attack() const;

//정의
int Marine::attack() const { return default_damage; }

- 생각해보기

#pragma warning(disable : 4996)
#include <iostream>

class A {
    int x;

public:
    A(int c) : x(c) {}
    A(const A& a) {
        x = a.x;
        std::cout << "복사 생성" << std::endl;
    }
};

class B {
    A a;

public:
    B(int c) : a(c) {}
    B(const B& b) : a(b.a) {}
    A get_A() {
        A temp(a);
        return temp;
    }
};

int main() {
    B b(10);

    std::cout << "---------" << std::endl;
    A a1 = b.get_A();
    // 해당 라인에서 호출한 get_A() 함수 내부에서 temp를 생성하며 1번
    // 이후 return 값으로 temp를 내보내고 새로운 객체 a1에게 복사 생성하며 1번
    // 총 2번 호출된다.
}

4-5. 내가 만드는 String 클래스

- 필요 기능

  1. 문자(char) 로 부터의 문자열 생성, C 문자열 (char *) 로 부터의 생성
  2. 문자열 길이를 구하는 함수
  3. 문자열 뒤에 다른 문자열 붙이기
  4. 문자열 내에 포함되어 있는 문자열 구하기
  5. 문자열이 같은지 비교
  6. 문자열 크기 비교 (사전 순)
#include <iostream>

class MyString {
    char* string_content;  // 문자열 데이터를 가리키는 포인터 (null제외 실제 '문자'만을 저장)
    int string_length;     // 문자열 길이
    int memory_capacity;    // 현재 할당된 용량

public:
    // 문자 하나로 생성
    MyString(char c);
    // 문자열로 부터 생성
    MyString(const char* str);
    // 복사 생성자
    MyString(const MyString& str);
    //소멸자
    ~MyString();

    //문자열 길이 구하는 함수
    int length() const;
    int capacity() const;
    void reserve(int size);

    //문자열 출력
    void print() const;
    void println() const;

    //문자열 넣기
    MyString& assign(const MyString& str);
    MyString& assign(const char* str);

    //임의의 위치의 문자 리턴
    char at(int i) const;

    //문자열 삽입하기
    MyString& insert(int loc, const MyString& str);
    MyString& insert(int loc, const char* str);
    MyString& insert(int loc, char c);

    //문자열 지우기
    MyString& erase(int loc, int num);

    //문자열 찾기
    int find(int find_from, MyString& str) const;
    int find(int find_from, const char* str) const;
    int find(int find_from, char c) const;

    //문자열 비교
    int compare(const MyString& str) const;
};

MyString::MyString(char c) {
    string_content = new char[1];
    string_content[0] = c;
    string_length = 1;
    memory_capacity = 1;
}

MyString::MyString(const char* str) {
    string_length = strlen(str);
    string_content = new char[string_length];
    for (int i = 0; i < string_length; i++) {
        string_content[i] = str[i];
    }
    memory_capacity = string_length;
    //strncpy(string_content, str, string_length);
}

MyString::MyString(const MyString& str) {
    string_length = str.string_length;
    string_content = new char[string_length];
    memory_capacity = string_length;
    for (int i = 0; i < string_length; i++) {
        string_content[i] = str.string_content[i];
    }
}

MyString::~MyString() {
    delete[] string_content;
}

int MyString::length() const{
    return string_length;
}

int MyString::capacity() const {
    return memory_capacity;
}

void MyString::reserve(int size) {
    if (size > memory_capacity) {
        char* prev_string_content = string_content;

        string_content = new char[size];
        memory_capacity = size;

        for (int i = 0; i != string_length; i++)
            string_content[i] = prev_string_content[i];

        delete[] prev_string_content;
    }
}

void MyString::print() const {
    for (int i = 0; i < string_length; i++) {
        std::cout << string_content[i];
    }
}

void MyString::println() const {
    for (int i = 0; i < string_length; i++) {
        std::cout << string_content[i];
    }
    std::cout << std::endl;
}

MyString& MyString::assign(const MyString& str) {
    if (str.string_length > memory_capacity) {
        delete[] string_content;
        string_content = new char[str.string_length];
        memory_capacity = str.string_length;
    }
    for (int i = 0; i < str.string_length; i++) {
        string_content[i] = str.string_content[i];
    }
    string_length = str.string_length;

    return *this;
}

MyString& MyString::assign(const char* str) {
    int str_length = strlen(str);

    if ( str_length > memory_capacity) {
        delete[] string_content;
        string_content = new char[str_length];
        memory_capacity = str_length;
    }

    for (int i = 0; i < str_length; i++) {
        string_content[i] = str[i];
    }
    string_length = str_length;

    return *this;
}

char MyString::at(int i) const {
    if (i > string_length || i < 0) {
        return NULL;
    }
    else
        return string_content[i];
}

MyString& MyString::insert(int loc, const MyString& str) {
    if (loc < 0 || loc > string_length) {
        return *this;
    }

    //용량할당....
    if (string_length + str.string_length > memory_capacity) {
        char* string_tmp = string_content;

        if (memory_capacity * 2 > string_length + str.string_length)
            memory_capacity *= 2;
        else
            memory_capacity = string_length + str.string_length;

        string_content = new char[memory_capacity];

        int j = 0;
        int k = 0;
        for (int i = 0; i < string_length + str.string_length; i++) {
            if (loc<=i && i<loc+ str.string_length) {
                string_content[i] = str.string_content[j];
                j++;
            }
            else {
                string_content[i] = string_tmp[k];
                k++;
            }
        }
        delete[] string_tmp;
    }

    else {
        for (int i = string_length - 1; i >= loc; i--) {
            string_content[i + str.string_length] = string_content[i];
        }
        for (int i = 0; i < str.string_length; i++)
            string_content[i + loc] = str.string_content[i];
    }
    string_length = string_length + str.string_length;
    return *this;

}

MyString& MyString::insert(int loc, const char* str) {
    MyString tmp(str);
    return insert(loc, tmp);;
}

MyString& MyString::insert(int loc, char c) {
    MyString tmp(c);
    return insert(loc, tmp);
}

MyString& MyString::erase(int loc, int num) {
    if (loc < 0 || num <0 || loc>string_length) return *this;

    for (int i = loc+num; i < string_length; i++) {
        string_content[i - num] = string_content[i];
    }

    string_length -= num;
    return *this;
}

int MyString::find(int find_from, MyString& str) const {
    if (str.string_length == 0) return -1;

    int i, j;
    for (i = find_from; i <= string_length - str.string_length; i++) {
        for (j = 0; j < str.string_length; j++) {
            if (string_content[i + j] != str.string_content[j]) break;
        }

        if (j == str.string_length) return i;
    }

    return -1; 
}

int MyString::find(int find_from, const char* str) const {
    MyString temp(str);
    return find(find_from, temp);
}

int MyString::find(int find_from, char c) const {
    MyString temp(c);
    return find(find_from, temp);
}

int MyString::compare(const MyString& str) const {
    int limit = std::min(string_length, str.string_length);

    for(int i=0; i<limit; i++){
        if (string_content[i] > str.string_content[i])
            return 1;

        else if (string_content[i] < str.string_content[i])
            return -1;
    }

    if (string_length == str.string_length) {
        return 0;
    }
    else if (string_length > str.string_length) {
        return 1;
    }

    return -1;
}

생각해보기

1. erase 예외 처리

MyString& MyString::erase(int loc, int num) {
    if (loc < 0 || num <0 || loc>string_length) return *this;

//지우려는 문자가 기존 문자열을 넘어설 경우 예외 처리
    if (loc + num > string_length) {
        num = string_length - loc;
    }

    for (int i = loc+num; i < string_length; i++) {
        string_content[i - num] = string_content[i];
    }

    string_length -= num;
    return *this;
}

2. find 구현

...

과제

더보기

출력 함수 구현하기

#include "Print.h"
#include <string>
#include <iomanip>

void PrintIntegers(std::istream& in, std::ostream& out) {
    out << " .........oct........dec......hex" << std::endl;
    out << " ------------.----------.--------" << std::endl;

    std::string token;
    while (in >> token) {
        try {
            int num = stoi(token);

            if (num < 0) continue;

            out << std::setw(12) << std::oct << num << "."
                << std::setw(10) << std::dec << num << "."
                << std::setw(8) << std::uppercase << std::hex << num << std::dec << std::endl;
        }
        catch (std::invalid_argument&) {
        }
        catch (std::out_of_range&) {
        }
    }
}

void PrintMaxFloat(std::istream& in, std::ostream& out) {
    std::string token;
    float max = FLT_MIN;

    while (in >> token) {
        try {
            float num = stof(token);
            bool isNegative = num < 0;

            max = std::max(max, num);

            out <<"....." << ( isNegative? '-':'+' ) << std::fixed << std::setprecision(3) << abs(num) << std::endl;
        }
        catch (std::invalid_argument&) {
        }
        catch (std::out_of_range&) {
        }
    }
    out << "max:." << (max<0 ? '-' : '+') << std::fixed << std::setprecision(3) << abs(max) << std::endl;
}

결과

 

4-6. 클래스의 explicit 과 mutable 키워드

explicit

explicit MyString(int capacity);

암시적 형변환을 수행하지 못하게 하는 키워드

 

mutable

class A {
  mutable int data_;

 public:
  A(int data) : data_(data) {}
  void DoSomething(int x) const {
    data_ = x;  // 가능!
  }

const 함수 내부에서도 값을 변경할 수 있도록 하는 키워드

 

5-1. 내가 만든 연산자 - 연산자 오버로딩

 

연산자 오버로딩

(리턴 타입) operator(연산자) (연산자가 받는 인자)    //형식

bool MyString::operator==(MyString& str) {
  return !compare(str);  // str 과 같으면 compare 에서 0 을 리턴한다.
}

 

 

복소수 클래스 만들기

 

사칙 연산

Complex Complex::operator+(const Complex& c) const {    // 맞음
  Complex temp(real + c.real, img + c.img);
  return temp;
}

Complex& operator+(const Complex& c) {    // 안됨XXXXXX
  real += c.real;
  img += c.img;
  return *this;
}

/*
레퍼런스를 반환하면 안되는 이유는 b + c + b와 같은 연속적인 연산을 해야할 때
먼저 계산한 b + c 로 인해 이후 계산해야할 b의 값이 변화하기 때문이다
*/

대입 연산

Complex& Complex::operator=(const Complex& c)
{
  real = c.real;
  img = c.img;
  return *this;
}

/*
자기 자신을 리턴하는 이유는 
a = b = c와 같은 연산을 할 때, b = c 에서 b를 리턴해야 이후 a = b가 수행될 수 있기 때문이다.
레퍼런스 타입으로 리턴하는 이유는 불필요한 복사를 방지하기 위해서임.
*/

복사 생성 vs 기본 생성 후 대입

 

Some_Class a = b;    //디폴트 복사 생성자 호출 (얕은 복사)

Some_Class a;    // 기본 생성자 호출
a = b;    // 디폴트 대입 연산자 실행 (얕은 복사)

* 연산자 오버로딩을 사용하게 된다면 모든 연산자들에 대한 개별적인 정의가 필요하다.

 

Complex 클래스

#include <cstring>
#include <iostream>

class Complex {
private:
    double real, img;

    double get_number(const char* str, int from, int to) const;

public:
    Complex(double real, double img) : real(real), img(img) {}
    Complex(const Complex& c) { real = c.real, img = c.img; }
    Complex(const char* str);

    Complex operator+(const Complex& c) const;
    Complex operator-(const Complex& c) const;
    Complex operator*(const Complex& c) const;
    Complex operator/(const Complex& c) const;

    Complex operator+(const char* str) const;
    Complex operator-(const char* str) const;
    Complex operator*(const char* str) const;
    Complex operator/(const char* str) const;

    Complex& operator+=(const Complex& c);
    Complex& operator-=(const Complex& c);
    Complex& operator*=(const Complex& c);
    Complex& operator/=(const Complex& c);

    Complex& operator=(const Complex& c);

    void println() {
        std::cout << "( " << real << " , " << img << " ) " << std::endl;
    }
};
Complex::Complex(const char* str) {

    int begin = 0, end = strlen(str);
    img = 0.0;
    real = 0.0;

    // 먼저 가장 기준이 되는 'i' 의 위치를 찾는다.
    int pos_i = -1;
    for (int i = 0; i != end; i++) {
        if (str[i] == 'i') {
            pos_i = i;
            break;
        }
    }

    // 만일 'i' 가 없다면 이 수는 실수 뿐이다.
    if (pos_i == -1) {
        real = get_number(str, begin, end - 1);
        return;
    }

    // 만일 'i' 가 있다면,  실수부와 허수부를 나누어서 처리하면 된다.
    real = get_number(str, begin, pos_i - 1);
    img = get_number(str, pos_i + 1, end - 1);

    if (pos_i >= 1 && str[pos_i - 1] == '-') img *= -1.0;
}
double Complex::get_number(const char* str, int from, int to) const {
    bool minus = false;
    if (from > to) return 0;

    if (str[from] == '-') minus = true;
    if (str[from] == '-' || str[from] == '+') from++;

    double num = 0.0;
    double decimal = 1.0;

    bool integer_part = true;
    for (int i = from; i <= to; i++) {
        if (isdigit(str[i]) && integer_part) {
            num *= 10.0;
            num += (str[i] - '0');
        }
        else if (str[i] == '.')
            integer_part = false;
        else if (isdigit(str[i]) && !integer_part) {
            decimal /= 10.0;
            num += ((str[i] - '0') * decimal);
        }
        else
            break;  // 그 이외의 이상한 문자들이 올 경우
    }

    if (minus) num *= -1.0;

    return num;
}
Complex Complex::operator+(const Complex& c) const {
    Complex temp(real + c.real, img + c.img);
    return temp;
}
Complex Complex::operator-(const Complex& c) const {
    Complex temp(real - c.real, img - c.img);
    return temp;
}
Complex Complex::operator*(const Complex& c) const {
    Complex temp(real * c.real - img * c.img, real * c.img + img * c.real);
    return temp;
}
Complex Complex::operator/(const Complex& c) const {
    Complex temp(
        (real * c.real + img * c.img) / (c.real * c.real + c.img * c.img),
        (img * c.real - real * c.img) / (c.real * c.real + c.img * c.img));
    return temp;
}
Complex Complex::operator+(const char* str) const {
    Complex temp(str);
    return (*this) + temp;
}
Complex Complex::operator-(const char* str) const {
    Complex temp(str);
    return (*this) - temp;
}
Complex Complex::operator*(const char* str) const {
    Complex temp(str);
    return (*this) * temp;
}
Complex Complex::operator/(const char* str) const {
    Complex temp(str);
    return (*this) / temp;
}
Complex& Complex::operator+=(const Complex& c) {
    (*this) = (*this) + c;
    return *this;
}
Complex& Complex::operator-=(const Complex& c) {
    (*this) = (*this) - c;
    return *this;
}
Complex& Complex::operator*=(const Complex& c) {
    (*this) = (*this) * c;
    return *this;
}
Complex& Complex::operator/=(const Complex& c) {
    (*this) = (*this) / c;
    return *this;
}
Complex& Complex::operator=(const Complex& c) {
    real = c.real;
    img = c.img;
    return *this;
}

 

 

생각해보기

 

문제1

  • =, ==, + 연산자 오버로딩 추가
MyString& MyString::operator=(const MyString& str) {
    if (this == &str) return *this;

    delete[] string_content;

    string_length = str.string_length;
    memory_capacity = str.memory_capacity;
    string_content = new char[string_length];
    for (int i = 0; i < string_length; i++) {
        string_content[i] = str.string_content[i];
    }

    return *this;
}

bool MyString::operator==(const MyString& str) const {
    return compare(str) == 0;
}

MyString MyString::operator+(const MyString& str) const {
    MyString result;
    result.reserve(string_length + str.string_length);

    for (int i = 0; i < string_length; i++) {
        result.string_content[i] = string_content[i];
    }
    for (int i = 0; i < str.string_length; i++) {
        result.string_content[i + string_length] = str.string_content[i];
    }

    result.string_length = string_length + str.string_length;
    return result;
}

 

문제2

double Complex::get_number(const char *str, int from, int to) const {
    std::string sub_str(str + from, str + to + 1);
    return atof(sub_str.c_str());
}

 

5-2. 입출력, 첨자, 타입변환, 증감 연산자 오버로딩

 

Friend

: private 으로 정의된 변수나 함수들에 접근이 가능하도록 하는 키워드

class A {
 private:
  int x;

  friend class B;
};

class B {
 private:
  int y;
};

A에서 B로의 접근이 가능함

 

멤버 함수가 아닌 연산자 함수 오버로딩

  • [],->,=,() 의 연산자들은 멤버 함수로만 존재할 수 있다
  • 통상적으로 자기 자신을 리턴하지 않는 이항 연산자들은 모두 외부 함수로 선언하는 것, 자기 자신을 리턴하는 이항 연산자들은 멤버 함수로 선언하는 것이 원칙이다.

입출력 연산자 오버로딩 (정확히 보면 <<, >> 연산자)

std::ostream& operator<<(std::ostream& os, const Complex& c) {
  os << "( " << c.real << " , " << c.img << " ) ";
  return os;
}

 

첨자 연산자 [] 오버로딩

char& MyString::operator[](const int index) {
    return string_content[index];
}

리턴 값으로 *(string_content + index) 이렇게 줘도 되려나?

 

타입 변환 연산자 오버로딩

: 기본 자료형을 객체로 다루기 위한 클래스

#include <iostream>

class Int {
  int data;    //int 데이터 저장

 public:
  Int(int data) : data(data) {}    //생성자: int 데이터를 받아 초기화
  Int(const Int& i) : data(i.data) {}    //복사 생성자: 다른 Int 객체를 받아 초기화

  operator int() { return data; }    //int 타입 변환 연산자: Int 객체를 int로 변환
};

int main() {
  Int x = 3;     // Int 객체 초기화
  int a = x + 4; // Int 객체를 int로 변환하여 연산

  x = a * 2 + x + 4; // 다양한 연산 후 Int 객체에 대입
  std::cout << x << std::endl; // 결과 출력: 21
}

 

 

증감 연산자 ++, -- 오버로딩

 

- 전위 연산자

  Test& operator++() {
    x++;
    std::cout << "전위 증감 연산자" << std::endl;
    return *this;
  }

 

- 후위 연산자

  // 전위 증감과 후위 증감에 차이를 두기 위해 후위 증감의 경우 인자로 int 를
  // 받지만 실제로는 아무것도 전달되지 않는다.
  Test operator++(int) {
    Test temp(*this);
    x++;
    std::cout << "후위 증감 연산자" << std::endl;
    return temp;
  }

 

 

해결해보기

더보기

문제 1

N 차원 배열을 제공하는 클래스를 만들어보세요. 이는 여러분이 여태까지 배운 내용을 시험할 것입니다. 참고로, 원소에 접근하기 위해서는 a[1][2][3] 과 같은 방법으로 접근하겠지요. 다만 N 차원 배열이기 때문에 (N은 객체 생성시에 사용자가 입력합니다) 2 차원 배열은 a[1][2], 3 차원 배열은 a[1][2][3] 과 같은 방식으로 접근할 것입니다. (난이도 : 最上)

 

#include <iostream>
#include <cassert>

class MyArray {
private:
    const int dim;   // 몇 차원 배열인지
    int* size;       // 인덱스를 저장해놓는 배열
    int* data;       // 실제 배열 데이터 저장소
    int total_size;  // 전체 배열 크기
    int calculateIndex(const int* indices) const;    // 다차원 인덱스를 1차원 인덱스로 변환

public:
    MyArray(int dim, int* array_size);
    ~MyArray();

    class Proxy {       // Proxy 클래스
    private:
        MyArray& array;
        int* indices;   // 각 차원에 대한 인덱스 저장
        int level;  //현재 접근하고 있는 차원

    public:
        Proxy(MyArray& arr, int* idx, int lvl);
        Proxy operator[](int index);
        operator int& ();
        Proxy& operator=(const int& value);
    };

    Proxy operator[](int index);    // 배열 접근용 operator[]
};

int MyArray::calculateIndex(const int* indices) const
{
    int flat_index = 0;
    int multiplier = 1;
    for (int i = dim - 1; i >= 0; --i) {
        flat_index += indices[i] * multiplier;
        multiplier *= size[i];
    }
    return flat_index;
}

MyArray::MyArray(int dim, int* array_size) : dim(dim)
{
    size = new int[dim];
    total_size = 1;
    for (int i = 0; i < dim; ++i) {
        size[i] = array_size[i];
        total_size *= size[i];
    }
    data = new int[total_size]();   //메모리 할당 후 0으로 초기화
}

MyArray::~MyArray()
{
    delete[] size;
    delete[] data;
}

MyArray::Proxy MyArray::operator[](int index)
{
    int* indices = new int[dim]();
    indices[0] = index;
    return Proxy(*this, indices, 1);
}

MyArray::Proxy MyArray::Proxy::operator[](int index)
{
    indices[level] = index;
    return Proxy(array, indices, level+1);
}

MyArray::Proxy::Proxy(MyArray& arr, int* idx, int lvl): array(arr), indices(idx), level(lvl) {}

MyArray::Proxy::operator int& ()
{
    int flat_index = array.calculateIndex(indices);
    return array.data[flat_index];
}

MyArray::Proxy& MyArray::Proxy::operator=(const int& value)
{
    int flat_index = array.calculateIndex(indices);
    array.data[flat_index] = value;
    return *this;
}

 

 

 

 

5-3. 연산자 오버로딩 프로젝트 - N 차원 배열

 

 

 

C++ 스타일의 캐스팅 (static_cast 등등)

 

암시적 캐스팅

 : 컴파일러에서 알아서 캐스팅해줌 ( ex. int + double 덧셈 연산 )

 

명시적 캐스팅

ptr = (Something *)other_ptr;	// void* 타입의 주소 -> 특정 구조체 포인터 타입의 주소 
int_variable = (int)float_variable;	//	float -> int

 

 

C++에서 제공하는 4가지 캐스팅

  • static_cast : 우리가 흔히 생각하는, 언어적 차원에서 지원하는 일반적인 타입 변환
  • const_cast : 객체의 상수성(const) 를 없애는 타입 변환. ( const int -> int )
  • dynamic_cast : 파생 클래스 사이에서의 다운 캐스팅
  • reinterpret_cast : 위험을 감수하고 하는 캐스팅으로 서로 관련이 없는 포인터들 사이의 캐스팅 등

 

사용 방식

(원하는 캐스팅 종류)<바꾸려는 타입>(무엇을 바꿀 것인가?)

 

 

N차원 배열 구현

 

Array, Int, Iterator 클래스 구현

더보기

전체 코드

#include <iostream>

namespace MyArray {
class Array;
class Int;

class Array {
  friend Int;

  const int dim;  // 몇 차원 배열 인지
  int* size;  // size[0] * size[1] * ... * size[dim - 1] 짜리 배열이다.

  struct Address {
    int level;
    // 맨 마지막 레벨(dim - 1 레벨) 은 데이터 배열을 가리키고, 그 위 상위
    // 레벨에서는 다음 Address 배열을 가리킨다.
    void* next;
  };

  Address* top;

 public:
  class Iterator {
    int* location;
    Array* arr;

    friend Int;

   public:
    Iterator(Array* arr, int* loc = NULL) : arr(arr) {
      location = new int[arr->dim];
      for (int i = 0; i != arr->dim; i++)
        location[i] = (loc != NULL ? loc[i] : 0);
    }
    Iterator(const Iterator& itr) : arr(itr.arr) {
      location = new int[arr->dim];
      for (int i = 0; i != arr->dim; i++) location[i] = itr.location[i];
    }
    ~Iterator() { delete[] location; }
    // 다음 원소를 가리키게 된다.
    Iterator& operator++() {
      if (location[0] >= arr->size[0]) return (*this);

      bool carry = false;  // 받아 올림이 있는지
      int i = arr->dim - 1;
      do {
        // 어차피 다시 돌아온다는 것은 carry 가 true
        // 라는 의미 이므로 ++ 을 해야 한다.
        location[i]++;
        if (location[i] >= arr->size[i] && i >= 1) {
          // i 가 0 일 경우 0 으로 만들지 않는다 (이러면 begin 과 중복됨)
          location[i] -= arr->size[i];
          carry = true;
          i--;
        } else
          carry = false;

      } while (i >= 0 && carry);

      return (*this);
    }
    Iterator& operator=(const Iterator& itr) {
      arr = itr.arr;
      location = new int[itr.arr->dim];
      for (int i = 0; i != arr->dim; i++) location[i] = itr.location[i];

      return (*this);
    }
    Iterator operator++(int) {
      Iterator itr(*this);
      ++(*this);
      return itr;
    }
    bool operator!=(const Iterator& itr) {
      if (itr.arr->dim != arr->dim) return true;

      for (int i = 0; i != arr->dim; i++) {
        if (itr.location[i] != location[i]) return true;
      }

      return false;
    }
    Int operator*();
  };

  friend Iterator;
  Array(int dim, int* array_size) : dim(dim) {
    size = new int[dim];
    for (int i = 0; i < dim; i++) size[i] = array_size[i];

    top = new Address;
    top->level = 0;

    initialize_address(top);
  }
  Array(const Array& arr) : dim(arr.dim) {
    size = new int[dim];
    for (int i = 0; i < dim; i++) size[i] = arr.size[i];

    top = new Address;
    top->level = 0;

    initialize_address(top);
    // 내용물 복사
    copy_address(top, arr.top);
  }

  void copy_address(Address* dst, Address* src) {
    if (dst->level == dim - 1) {
      for (int i = 0; i < size[dst->level]; ++i)
        static_cast<int*>(dst->next)[i] = static_cast<int*>(src->next)[i];
      return;
    }
    for (int i = 0; i != size[dst->level]; i++) {
      Address* new_dst = static_cast<Address*>(dst->next) + i;
      Address* new_src = static_cast<Address*>(src->next) + i;
      copy_address(new_dst, new_src);
    }
  }

  // address 를 초기화 하는 함수이다. 재귀 호출로 구성되어 있다.
  void initialize_address(Address* current) {
    if (!current) return;
    if (current->level == dim - 1) {
      current->next = new int[size[current->level]];
      return;
    }
    current->next = new Address[size[current->level]];
    for (int i = 0; i != size[current->level]; i++) {
      (static_cast<Address*>(current->next) + i)->level = current->level + 1;
      initialize_address(static_cast<Address*>(current->next) + i);
    }
  }
  void delete_address(Address* current) {
    if (!current) return;
    for (int i = 0; current->level < dim - 1 && i < size[current->level]; i++) {
      delete_address(static_cast<Address*>(current->next) + i);
    }

    if (current->level == dim - 1) {
      delete[] static_cast<int*>(current->next);
    }
    delete[] static_cast<Address*>(current->next);
  }
  Int operator[](const int index);
  ~Array() {
    delete_address(top);
    delete[] size;
  }

  Iterator begin() {
    int* arr = new int[dim];
    for (int i = 0; i != dim; i++) arr[i] = 0;

    Iterator temp(this, arr);
    delete[] arr;

    return temp;
  }
  Iterator end() {
    int* arr = new int[dim];
    arr[0] = size[0];
    for (int i = 1; i < dim; i++) arr[i] = 0;

    Iterator temp(this, arr);
    delete[] arr;

    return temp;
  }
};
class Int {
  void* data;

  int level;
  Array* array;

 public:
  Int(int index, int _level = 0, void* _data = NULL, Array* _array = NULL)
      : level(_level), data(_data), array(_array) {
    if (_level < 1 || index >= array->size[_level - 1]) {
      data = NULL;
      return;
    }
    if (level == array->dim) {
      // 이제 data 에 우리의 int 자료형을 저장하도록 해야 한다.
      data = static_cast<void*>((
          static_cast<int*>(static_cast<Array::Address*>(data)->next) + index));
    } else {
      // 그렇지 않을 경우 data 에 그냥 다음 addr 을 넣어준다.
      data = static_cast<void*>(static_cast<Array::Address*>(
                                    static_cast<Array::Address*>(data)->next) +
                                index);
    }
  };

  Int(const Int& i) : data(i.data), level(i.level), array(i.array) {}

  operator int() {
    if (data) return *static_cast<int*>(data);
    return 0;
  }
  Int& operator=(const int& a) {
    if (data) *static_cast<int*>(data) = a;
    return *this;
  }

  Int operator[](const int index) {
    if (!data) return 0;
    return Int(index, level + 1, data, array);
  }
};
Int Array::operator[](const int index) {
  return Int(index, 1, static_cast<void*>(top), this);
}
Int Array::Iterator::operator*() {
  Int start = arr->operator[](location[0]);
  for (int i = 1; i <= arr->dim - 1; i++) {
    start = start.operator[](location[i]);
  }
  return start;
}
}  // namespace MyArray
int main() {
  int size[] = {2, 3, 4};
  MyArray::Array arr(3, size);

  MyArray::Array::Iterator itr = arr.begin();
  for (int i = 0; itr != arr.end(); itr++, i++) (*itr) = i;
  for (itr = arr.begin(); itr != arr.end(); itr++)
    std::cout << *itr << std::endl;

  for (int i = 0; i < 2; i++) {
    for (int j = 0; j < 3; j++) {
      for (int k = 0; k < 4; k++) {
        arr[i][j][k] = (i + 1) * (j + 1) * (k + 1) + arr[i][j][k];
      }
    }
  }
  for (int i = 0; i < 2; i++) {
    for (int j = 0; j < 3; j++) {
      for (int k = 0; k < 4; k++) {
        std::cout << i << " " << j << " " << k << " " << arr[i][j][k]
                  << std::endl;
      }
    }
  }
}

 

생각해보기

더보기

문제 1

앞서 N 차원 배열을 구현하는 또 다른 방법 (그냥 x1 * ... * xn 개의 1 차원 배열을 만든 뒤에, [] 연산자를 위와 같이 특별한 방법을 이용하여 접근할 수 있게 하는 것) 으로 N 차원 배열을 구현해봅시다. (난이도 : 上)

 

#include <iostream>

class MyArray {
private:
      int dim;
      int* size;
      int* data;
      int totalSize;

      int getIndex(const int* indices) const {
            int index = 0;
            int multiplier = 1;
            for (int i = dim - 1; i >= 0; --i) {
                  index += indices[i] * multiplier;
                  multiplier *= size[i];
            }
            return index;
      }

public:
      MyArray(int dim, const int* array_size) : dim(dim), totalSize(1) {
            size = new int[dim];
            for (int i = 0; i < dim; ++i) {
                  size[i] = array_size[i];
                  totalSize *= array_size[i];
            }
            data = new int[totalSize](); // Initialize with zeros
      }

      ~MyArray() {
            delete[] size;
            delete[] data;
      }

      class Proxy {
      private:
            MyArray& array;
            int* indices;
            int level;

      public:
            Proxy(MyArray& array, int level) : array(array), level(level) {
                  indices = new int[array.dim]();
            }

            Proxy(MyArray& array, const int* indices, int level) : array(array), level(level) {
                  this->indices = new int[array.dim]();
                  for (int i = 0; i < level; ++i) {
                        this->indices[i] = indices[i];
                  }
            }

            ~Proxy() {
                  delete[] indices;
            }

            Proxy operator[](int index) {
                  if (level >= array.dim) {
                        throw std::out_of_range("Too many indices.");
                  }
                  int* newIndices = new int[array.dim];
                  for (int i = 0; i < level; ++i) {
                        newIndices[i] = indices[i];
                  }
                  newIndices[level] = index;
                  Proxy nextProxy(array, newIndices, level + 1);
                  delete[] newIndices;
                  return nextProxy;
            }

            operator int() {
                  if (level != array.dim) {
                        throw std::out_of_range("Not enough indices.");
                  }
                  return array.data[array.getIndex(indices)];
            }

            Proxy& operator=(int value) {
                  if (level != array.dim) {
                        throw std::out_of_range("Not enough indices.");
                  }
                  array.data[array.getIndex(indices)] = value;
                  return *this;
            }
      };

      Proxy operator[](int index) {
            int* initialIndices = new int[dim]();
            initialIndices[0] = index;
            Proxy p(*this, initialIndices, 1);
            delete[] initialIndices;
            return p;
      }
};

 

 

6-1. C++ 표준 문자열 & 부모의 것을 물려쓰자 - 상속

 

1. C++ 표준 문자열 (std::string)

  • std::string: C++ 표준 라이브러리에서 제공하는 문자열 클래스. 문자열을 동적으로 관리하고 다양한 편리한 기능을 제공.(참조: https://modoocode.com/233)

2. 상속 (Inheritance)

  • 상속: 기존 클래스(Base)의 특성을 새로운 클래스(Derived)가 물려받아 사용하는 것.
  • 기반 클래스(Base): 상속의 대상이 되는 클래스.
  • 파생 클래스(Derived): 상속을 통해 기반 클래스의 특성을 물려받는 클래스.
class Base {
public:
    Base() { std::cout << "Base 클래스" << std::endl; }
    void what() { std::cout << "기반 클래스" << std::endl; }
};

class Derived : public Base {
public:
    Derived() { std::cout << "Derived 클래스" << std::endl; }
};

 

  • public 상속: 기반 클래스의 public, protected 멤버는 파생 클래스에서 각각 그대로 public, protected로 상속.
  • protected 상속: 기반 클래스의 public 멤버는 파생 클래스에서 protected로 상속, 나머지는 그대로.
  • private 상속: 기반 클래스의 모든 멤버가 파생 클래스에서 private로 상속.

 

 

3. 오버라이딩 (Overriding)

  • 오버라이딩: 파생 클래스에서 기반 클래스의 멤버 함수를 재정의하는 것. 함수의 이름과 인자가 같아야 함.

 

 

사원 관리 시스템 ( Employee, Manager 클래스 )

 

Employee

class EmployeeList {
    int alloc_employee;  // 할당한 총 직원 수
    int current_employee;  // 현재 직원 수
    int current_manager;   // 현재 매니저 수

    Employee** employee_list;  // 직원 데이터
    Manager** manager_list;    // 매니저 데이터

public:
    EmployeeList(int alloc_employee) : alloc_employee(alloc_employee) {
        employee_list = new Employee*[alloc_employee];
        manager_list = new Manager*[alloc_employee];
        current_employee = 0;
        current_manager = 0;
    }

    void add_employee(Employee* employee) {
        employee_list[current_employee] = employee;
        current_employee++;
    }

    void add_manager(Manager* manager) {
        manager_list[current_manager] = manager;
        current_manager++;
    }

    int current_employee_num() { return current_employee + current_manager; }

    void print_employee_info() {
        int total_pay = 0;
        for (int i = 0; i < current_employee; i++) {
            employee_list[i]->print_info();
            total_pay += employee_list[i]->calculate_pay();
        }
        for (int i = 0; i < current_manager; i++) {
            manager_list[i]->print_info();
            total_pay += manager_list[i]->calculate_pay();
        }
        std::cout << "총 비용 : " << total_pay << "만원 " << std::endl;
    }

    ~EmployeeList() {
        for (int i = 0; i < current_employee; i++) {
            delete employee_list[i];
        }
        for (int i = 0; i < current_manager; i++) {
            delete manager_list[i];
        }
        delete[] employee_list;
        delete[] manager_list;
    }
};

 

 

Manager ( Employee 상속 )

class Manager : public Employee {
    int year_of_service;

public:
    Manager(std::string name, int age, std::string position, int rank, int year_of_service)
        : Employee(name, age, position, rank), year_of_service(year_of_service) {}

    Manager(const Manager& manager)
        : Employee(manager.name, manager.age, manager.position, manager.rank) {
        year_of_service = manager.year_of_service;
    }

    Manager() : Employee() {}

    int calculate_pay() { return 200 + rank * 50 + 5 * year_of_service; }

    void print_info() {
        std::cout << name << " (" << position << " , " << age << ", "
                  << year_of_service << "년차) ==> " << calculate_pay() << "만원"
                  << std::endl;
    }
};

 

 

 

 

6-2. 상속

 

 

A <- B : A는 기반, B는 파생, 

is a 또는 has a 의 관계를 가지게 된다.

파생될수록 특수화, 기반일수록 일반화

 

int main() {
  Base p;
  Derived c;

  std::cout << "=== 포인터 버전 ===" << std::endl;
  Base* p_c = &c;
  p_c->what();

  return 0;
}

 

파생 클래스에서 기반 클래스로 업캐스팅이 가능

기반 클래스에서 파생클래스로 다운캐스팅이 불가능하지만

 

  std::cout << "=== 포인터 버전 ===" << std::endl;
  Base* p_p = &p;

  Derived* p_c = static_cast<Derived*>(p_p);

 

이런식으로 강제 다운 캐스딩이 가능하긴 하다. 하지만 권장하지 않음

 

 

Dyanmic_cast : 상속 관계에 있는 두 포인터들 간의 캐스

 

 

동적 바인딩 (Dynamic binding): 컴파일 시에 어떤 함수가 실행될 지 정해지지 않고 런타임 시에 정해지는 일을 가리켜서 동적 바인딩이라고 부른다.

- 기반 클래스에 virtual 키워드를 써서 가상 함수를 정의하고 파생 클래스에서 해당 가상 함수를 재정의한다. 이때 override라는 키워드를 덧붙이는데, override는 파생 클래스의 함수가 기반 클래스의 가상 함수와 똑같은 형태를 유지하도록 검토해준다.

#include <iostream>

// 기반 클래스
class Base {

 public:
  Base() { std::cout << "기반 클래스" << std::endl; }

// 가상 함수
  virtual void what() { std::cout << "기반 클래스의 what()" << std::endl; }
};

// 파생 클래스
class Derived : public Base {

 public:
  Derived() : Base() { std::cout << "파생 클래스" << std::endl; }

// 오버라이딩
  void what() override { std::cout << "파생 클래스의 what()" << std::endl; }
};


int main() {
  Base p;
  Derived c;

  Base* p_c = &c;
  Base* p_p = &p;

  std::cout << " == 실제 객체는 Base == " << std::endl;
  p_p->what();

  std::cout << " == 실제 객체는 Derived == " << std::endl;
  p_c->what();

  return 0;
}

 

 

 

생각해보기

더보기

문제 1

그렇다면 프로그램 내부적으로 virtual 함수들은 어떻게 처리될까요? 즉, 이 포인터가 어떠한 객체를 가리키는지 어떻게 알 수 있을까요? (난이도 : 上)

 

 

  • 각 클래스는 자신의 가상 함수에 대한 주소를 포함하는 vtable을 가집니다.
  • 각 객체는 해당 클래스의 vtable을 가리키는 vptr를 가집니다.
  • 가상 함수 호출 시, 객체의 vptr을 통해 vtable을 참조하여 올바른 함수 포인터를 사용하여 함수를 호출합니다.

 


가상 함수 테이블 (Virtual Table, vtable)

가상 함수의 호출을 처리하기 위해, 컴파일러는 가상 함수 테이블(vtable)을 사용합니다. 각 클래스는 그 클래스에 정의된 가상 함수에 대한 포인터를 포함하는 vtable을 가집니다. 또한, 각 객체는 vtable에 대한 포인터를 가지고 있습니다.

1. vtable 생성

클래스에 가상 함수가 정의되어 있으면, 컴파일러는 해당 클래스에 대한 vtable을 생성합니다. 이 vtable은 그 클래스에서 정의된 모든 가상 함수의 주소를 포함합니다.

예를 들어, 다음과 같은 클래스 계층 구조가 있다고 가정합니다:

 

class Base {
public:
    virtual void foo();
    virtual void bar();
};

class Derived : public Base {
public:
    void foo() override;
    void bar() override;
};
 
 

이 경우, Base 클래스와 Derived 클래스는 각각의 vtable을 가지게 됩니다.

  • Base의 vtable: foo -> Base::foo, bar -> Base::bar
  • Derived의 vtable: foo -> Derived::foo, bar -> Derived::bar

2. 객체에 vtable 포인터 추가

각 클래스의 객체에는 해당 클래스의 vtable을 가리키는 숨겨진 포인터가 추가됩니다. 이를 vptr(virtual table pointer)라고 부릅니다.

Base *b = new Derived();

 

이 할당이 이루어지면, b는 Derived 객체를 가리키게 되고, 이 Derived 객체는 Derived 클래스의 vtable을 가리키는 vptr를 가지게 됩니다.

 

3. 가상 함수 호출

가상 함수를 호출할 때, 컴파일러는 객체의 vptr을 통해 vtable을 참조합니다. 그런 다음, 해당 함수 포인터를 사용하여 올바른 함수 구현을 호출합니다.

예를 들어, b->foo();를 호출하면 다음과 같은 과정이 이루어집니다:

  1. b가 가리키는 객체의 vptr을 통해 vtable에 접근.
  2. vtable에서 foo에 해당하는 함수 포인터를 찾음.
  3. 해당 함수 포인터를 호출 (이 경우 Derived::foo가 호출됨).

 

6-3. 가상함수와 상속에 관련한 잡다한 내용들

 

Virtual 소멸자

상속 시에, 소멸자를 가상함수로 만들어야 한다.

#include <iostream>

class Parent {
 public:
  Parent() { std::cout << "Parent 생성자 호출" << std::endl; }
  ~Parent() { std::cout << "Parent 소멸자 호출" << std::endl; }
};
class Child : public Parent {
 public:
  Child() : Parent() { std::cout << "Child 생성자 호출" << std::endl; }
  ~Child() { std::cout << "Child 소멸자 호출" << std::endl; }
};
int main() {
  std::cout << "--- 평범한 Child 만들었을 때 ---" << std::endl;
  { Child c; }
  std::cout << "--- Parent 포인터로 Child 가리켰을 때 ---" << std::endl;
  {
    Parent *p = new Child();
    delete p;
  }
}

 

위와 같이 Parent 포인터가 Child를 가리킬 때는 Child의 소멸자는 호출되지 않는다. 이는 메모리 누수를 유발할 수 있음

 

따라서

#include <iostream>

class Parent {
 public:
  Parent() { std::cout << "Parent 생성자 호출" << std::endl; }
  virtual ~Parent() { std::cout << "Parent 소멸자 호출" << std::endl; }
};
class Child : public Parent {
 public:
  Child() : Parent() { std::cout << "Child 생성자 호출" << std::endl; }
  ~Child() { std::cout << "Child 소멸자 호출" << std::endl; }
};
int main() {
  std::cout << "--- 평범한 Child 만들었을 때 ---" << std::endl;
  { 
    // 이 {} 를 빠져나가면 c 가 소멸된다.
    Child c; 
  }
  std::cout << "--- Parent 포인터로 Child 가리켰을 때 ---" << std::endl;
  {
    Parent *p = new Child();
    delete p;
  }
}

 

위와 같이 소멸자를 Virtual로 만들어주면 

--- 평범한 Child 만들었을 때 ---
Parent 생성자 호출
Child 생성자 호출
Child 소멸자 호출
Parent 소멸자 호출
--- Parent 포인터로 Child 가리켰을 때 ---
Parent 생성자 호출
Child 생성자 호출
Child 소멸자 호출
Parent 소멸자 호출

 

이렇게 잘 호출된다.

 

 

가상함수의 구현원리

 

상속할 때 기반 클래스를 모두 가상함수로 만들면 편하지 않을까?

=> 가상 함수는 보통의 함수보다 호출 시간이 좀 더 걸린다.

 

 

가상 함수 테이블

 

 Virtual 함수를 만들면 함수의 이름과 실제로 어떤 함수가 대응되는지를 가상 함수 테이블로 저장한다.

class Parent {
 public:
  virtual void func1();
  virtual void func2();
};
class Child : public Parent {
 public:
  virtual void func1();
  void func3();
};

 

위 코드에서 생성되는 가상 함수 테이블의 구조는 아래와 같다.

 

https://modoocode.com/211 출처 모두의 코드..

 

Parent* c = Child();
c->func1();

 

이런 경우에  c는 Parent 타입의 포인터지만, 가리키는 실제 객체는 Child이다. 따라서 Child의 vtable이 사용된다.

Child의 vtable에서 fun1이 가리키는 함수를 실행하게 됨

 

 

순수 가상 함수와 추상 클래스

 

class Animal {
 public:
  Animal() {}
  virtual ~Animal() {}
  virtual void speak() = 0;
};

 

가상 함수에 = 0; 을 붙여서, 반드시 오버라이딩 되도록 만든 함수를 완전한 가상 함수라 해서, 순수 가상 함수(pure virtual function)라고 부릅니다.

 

 순수 가상 함수를 최소 한개 포함하고 있는- 반드시 상속 되어야 하는 클래스를 가리켜 추상 클래스 (abstract class)라고 부릅니다. 

 

추상 클래스를 '설계도' 라고 생각하면 좋습니다.

즉, 이 클래스를 상속받아서 사용하는 사람에게 "이 기능은 일반적인 상황에서 만들기 힘드니 너가 직접 특수화 되는 클래스에 맞추어서 만들어서 써라." 라고 말해주는 것이지요.

 

 

다중 상속

 

#include <iostream>

class A {
 public:
  int a;

  A() { std::cout << "A 생성자 호출" << std::endl; }
};

class B {
 public:
  int b;

  B() { std::cout << "B 생성자 호출" << std::endl; }
};

class C : public A, public B {	// 이 상속 순서에 맞춰서 기반 생성자가 호출됨
 public:
  int c;

  C() : A(), B() { std::cout << "C 생성자 호출" << std::endl; }
};
int main() { C c; }

 

 

다중 상속 시 주의할 점

 

1. 동일한 이름의 멤버 변수나 함수가 상속받은 여러 클래스에 존재할 경우

class A {
 public:
  int a;
};

class B {
 public:
  int a;
};

class C : public B, public A {
 public:
  int c;
};

int main() {
  C c;
  c.a = 3;  // 오류 발생: 'a'가 B::a인지 A::a인지 모호함
}

 

 

2. 다이아몬드 상속 문제

 공통 베이스 클래스를 가진 두 클래스의 다중 상속 시, 해당 베이스 클래스의 멤버가 중복

class Human {
  // ...
};

class HandsomeHuman : public Human {
  // ...
};

class SmartHuman : public Human {
  // ...
};

class Me : public HandsomeHuman, public SmartHuman {
  // ...
};

 

가상 상속을 통한 해결

virtual 키워드를 사용해 가상 상속을 적용하면, 베이스 클래스의 인스턴스를 한 번만 포함하게 됩니다.

class Human {
 public:
  // ...
};

class HandsomeHuman : public virtual Human {
  // ...
};

class SmartHuman : public virtual Human {
  // ...
};

class Me : public HandsomeHuman, public SmartHuman {
  // ...
};

 

 

다중 상속 사용 가이드라인

  1. 다중 상속이 필요한 상황
    • 다형성을 이용해 베이스 클래스의 포인터나 레퍼런스를 통해 상속받은 클래스의 오버라이드된 멤버 함수를 호출해야 할 때.
  2. 다중 상속을 대체할 수 있는 다른 설계 방법 
    1. 브리지 패턴: 멤버 포인터를 사용해 런타임에 필요한 동력원을 설정.
    2. 중첩된 일반화 방식: 각 계층마다 별도의 파생 클래스를 생성해 보다 세밀한 제어를 가능하게 함.
    3. 다중 상속: 각 카테고리에 해당하는 클래스를 상속받아 컴파일 타임 타입 체크와 섬세한 제어를 가능하게 함.

 

참고 자료: https://isocpp.org/wiki/faq/multiple-inheritance#virtual-inheritance-where

 

Standard C++

 

isocpp.org

 

 

7-1. C++에서의 입출력

 

C++ 입출력 라이브러리

 

입출력 라이브러리 구조

 

  • ios_base: 모든 입출력 클래스의 기본이 되는 클래스로, 입출력 형식 관련 데이터를 처리
  • ios: ios_base를 상속받아 스트림 버퍼를 초기화하고 입출력 작업의 상태 관리
  • istream: 입력을 담당하는 클래스로, cin이 이 클래스의 객체
  • ostream: 출력을 담당하는 클래스로, cout이 이 클래스의 객체

 

std::cin >> a;

 

 

와 같은 작업이 가능한 이유는

istream 클래스 내부에 Operator>>가 모든 기본 타입들에 대해 정의되어 있고,

cin은 istream 클래스의 객체 중 하나이기 때문이다.

 

istream& operator>>(istream& in, std::string& s)

{
  // 구현한다
}

  string 타입에 대한 정의는 없다. 하지만 위와 같이 클래스 외부에서의 연산자 오버로딩을 통해 가능하다.

 

 

ios클래스에서의 스트림 플래그

  • goodbit: 스트림이 정상 상태일 때
  • badbit: 복구 불가능한 오류 발생 시
  • failbit: 복구 가능한 오류 발생 시 (예: 잘못된 형식의 입력)
  • eofbit: 파일의 끝에 도달했을 때 이러한 상태 플래그들은 fail(), good(), eof() 등의 함수로 확인할 수 있음

 

 

입력 오류 처리: 잘못된 입력이 들어왔을 때의 처리

  • cin.fail(): 입력 오류 발생 여부 확인
  • cin.clear(): 오류 플래그 초기화
  • cin.ignore(n, delim): 버퍼에서 n개의 문자 또는 delim 문자까지 무시
#include <iostream>
#include <string>

int main() {
  int t;
  while (true) {
    std::cin >> t;
    std::cout << "입력 :: " << t << std::endl;
    if (std::cin.fail()) {
      std::cout << "제대로 입력해주세요" << std::endl;
      std::cin.clear();            // 플래그들을 초기화 하고
      std::cin.ignore(100, '\n');  // 개행문자가 나올 때 까지 무시한다
    }
    if (t == 1) break;
  }
}

 

 

스트림 버퍼에 대해

스트림: 데이터의 연속적인 흐름

스트림 버퍼: 스트림을 임시로 저장하고 관리하는 메모리 영역

 

 

streambuf 클래스

  • C++의 모든 입출력 객체(cin, cout 등)는 streambuf 객체를 가지고 있다.
  • 실제 데이터의 입출력을 관리

스트림 버퍼의 구조

  • 입력 영역(get area) + 출력 영역(put area)
  • 각 영역에는 세 개의 포인터가 있다
    • 시작 포인터: 버퍼의 시작을 가리킴
    • 현재 포인터: 다음에 읽거나 쓸 위치를 가리킴
    • 끝 포인터: 버퍼의 끝을 가리킴 

streambuf의 주요 기능

  • 버퍼 관리: 데이터를 효율적으로 저장하고 전송
  • 포인터 조작: 버퍼 내에서 읽기/쓰기 위치 조정
  • 다양한 입출력 소스 지원: 파일, 메모리, 네트워크 등

 

snextc()

char peek = std::cin.rdbuf()->snextc();
  • 현재 포인터를 한 칸 전진시킴
  • 그 위치의 문자를 '엿봄'(읽지 않고 확인만 함)
  • 포인터는 그 자리에 그대로 둠

 

 

 

7-2. C++ 에서 파일 입출력 - std::ifstream. std::ofstream, std::stringstream

 

 파일 읽기

// 파일에서의 입출력
#include <fstream>
#include <iostream>
#include <string>

int main() {
  // 파일 읽기 준비
  std::ifstream in("test.txt");
  std::string s;

  if (in.is_open()) {
    in >> s;
    std::cout << "입력 받은 문자열 :: " << s << std::endl;
  } else {
    std::cout << "파일을 찾을 수 없습니다!" << std::endl;
  }
  return 0;
}

 

in을 이용해 다른 경로의 파일을 열고자 하면

in.close() 해주어야 함.

 

 

이진수로 읽기

// 이진수로 읽기
#include <fstream>
#include <iostream>
#include <string>

int main() {
  // 파일 읽기 준비
  std::ifstream in("test.txt", std::ios::binary);
  std::string s;

  int x;
  if (in.is_open()) {
    in.read((char*)(&x), 4);
    std::cout << std::hex << x << std::endl;
  } else {
    std::cout << "파일을 찾을 수 없습니다!" << std::endl;
  }

  return 0;
}

 

in.read((char*)(&x), 4);

 

4 바이트의 내용을 읽으라는 의미. 첫 번째 인자에 해당하는 버퍼( int 변수를 마치 4 바이트 짜리 char 배열이라 생각하게 해서 이를 전달)를 전달하고, 두 번째 인자로 반드시 몇 바이트를 읽을 지 전달

 

 

파일 전체 읽기

#include <fstream>
#include <iostream>
#include <string>

int main() {
  // 파일 읽기 준비
  std::ifstream in("test.txt");
  std::string s;

  if (in.is_open()) {
    // 위치 지정자를 파일 끝으로 옮긴다.
    in.seekg(0, std::ios::end);

    // 그리고 그 위치를 읽는다. (파일의 크기)
    int size = in.tellg();

    // 그 크기의 문자열을 할당한다.
    s.resize(size);

    // 위치 지정자를 다시 파일 맨 앞으로 옮긴다.
    in.seekg(0, std::ios::beg);

    // 파일 전체 내용을 읽어서 문자열에 저장한다.
    in.read(&s[0], size);
    std::cout << s << std::endl;
  } else {
    std::cout << "파일을 찾을 수 없습니다!" << std::endl;
  }

  return 0;
}

 

 

파일 전체를 한 줄씩 읽기

// getline 으로 읽어들이기
#include <fstream>
#include <iostream>
#include <string>

int main() {
  // 파일 읽기 준비
  std::ifstream in("test.txt");
  char buf[100];

  if (!in.is_open()) {
    std::cout << "파일을 찾을 수 없습니다!" << std::endl;
    return 0;
  }

  while (in) {
    in.getline(buf, 100);
    std::cout << buf << std::endl;
  }

  return 0;
}

 

 getline 함수는 개행 문자 (혹은 지정한 문자) 가 나오기 전에 지정한 버퍼의 크기가 다 차게 된다면 failbit 를 키게 되므로 버퍼의 크기를 너무 작게 만든다면 정상적으로 데이터를 받을 수 없다. 

 

// std::string 에 정의된 getline 사용
#include <fstream>
#include <iostream>
#include <string>

int main() {
  // 파일 읽기 준비
  std::ifstream in("test.txt");

  if (!in.is_open()) {
    std::cout << "파일을 찾을 수 없습니다!" << std::endl;
    return 0;
  }

  std::string s;
  while (in) {
    getline(in, s);
    std::cout << s << std::endl;
  }

  return 0;
}

 

std::string의 getline 함수는 굳이 버퍼의 크기를 지정하지 않아도 알아서 개행문자 혹은 파일에 끝이 나올 때까지 입력받게 해준다.

 

 

파일에 쓰기

#include <iostream>
#include <fstream>
#include <string>

int main() {
  // 파일 쓰기 준비
  std::ofstream out("test.txt");

  std::string s;
  if (out.is_open()) {
    out << "이걸 쓰자~~";
  }

  return 0;
}

 

 

out 객체를 생성할 때 여러 옵션을 택할 수 있다.

  1. ios::out (기본): 출력 모드로 파일 열고, 없으면 생성하고, 있으면 내용 지움.
  2. ios::app: 파일 끝에 내용 추가하고, 없으면 새로 만듦.
  3. ios::ate: 파일 끝에서 시작하지만 어디든 쓸 수 있음.
  4. ios::trunc: 파일 열 때 기존 내용 모두 지움.
  5. ios::binary: 파일을 이진 모드로 열어 특수 문자 처리 안 함.
std::ofstream out("file.txt", std::ios::out | std::ios::app | std::ios::binary);

 이렇게 조합도 가능함

 

 

std::ofstream 연산자 오버로딩

#include <fstream>
#include <iostream>
#include <string>

class Human {
  std::string name;
  int age;

 public:
  Human(const std::string& name, int age) : name(name), age(age) {}
  std::string get_info() {
    return "Name :: " + name + " / Age :: " + std::to_string(age);
  }

  friend std::ofstream& operator<<(std::ofstream& o, Human& h);
};

std::ofstream& operator<<(std::ofstream& o, Human& h) {
  o << h.get_info();
  return o;
}
int main() {
  // 파일 쓰기 준비
  std::ofstream out("test.txt");

  Human h("이재범", 60);
  out << h << std::endl;

  return 0;
}

 

 

문자열 스트림 (std::stringstream)

#include <iostream>
#include <sstream>

int main() {
  std::istringstream ss("123");
  int x;
  ss >> x;

  std::cout << "입력 받은 데이터 :: " << x << std::endl;

  return 0;
}

 

 

 

8-1. Excel 만들기 프로젝트 1부

 

utils 클래스

- 벡터, 스택 구현

더보기

utils.h

#ifndef UTILS_H
#define UTILS_H

#include <string>
using std::string;

namespace MyExcel {
	
	class Vector {
		string* data;
		int capacity;
		int length;

	public:
		// 생성자
		Vector(int n = 1);

		// 맨 뒤에 새로운 원소를 추가한다.
		void push_back(string s);

		// 임의의 위치의 원소에 접근한다.
		string operator[](int i);

		// x 번째 위치한 원소를 제거한다.
		void remove(int x);

		// 현재 벡터의 크기를 구한다.
		int size();

		~Vector();
	};

	class Stack {
		struct Node {
			Node* prev;
			string s;

			Node(Node* prev, string s) : prev(prev), s(s) {}
		};

		Node* current;
		Node start;

	public:
		Stack();

		// 최상단에 새로운 원소를 추가한다.
		void push(string s);

		// 최상단의 원소를 제거하고 반환한다.
		string pop();

		// 최상단의 원소를 반환한다. (제거 안함)
		string peek();

		// 스택이 비어있는지의 유무를 반환한다.
		bool is_empty();

		~Stack();
	};

	class NumStack {
		struct Node {
			Node* prev;
			double s;

			Node(Node* prev, double s) : prev(prev), s(s) {}
		};

		Node* current;
		Node start;

	public:
		NumStack();
		void push(double s);
		double pop();
		double peek();
		bool is_empty();

		~NumStack();
	};
}
#endif

 

 

utils.cpp

#include "utils.h"

namespace MyExcel {
      Vector::Vector(int n) : data(new string[n]), capacity(n), length(0) {}
      void Vector::push_back(string s) {
            if (capacity <= length) {
                  string* temp = new string[capacity * 2];
                  for (int i = 0; i < length; i++) {
                        temp[i] = data[i];
                  }
                  delete[] data;
                  data = temp;
                  capacity *= 2;
            }

            data[length] = s;
            length++;
      }
      string Vector::operator[](int i) { return data[i]; }
      void Vector::remove(int x) {
            for (int i = x + 1; i < length; i++) {
                  data[i - 1] = data[i];
            }
            length--;
      }
      int Vector::size() { return length; }
      Vector::~Vector() {
            if (data) {
                  delete[] data;
            }
      }

      Stack::Stack() : start(NULL, "") { current = &start; }
      void Stack::push(string s) {
            Node* n = new Node(current, s);
            current = n;
      }
      string Stack::pop() {
            if (current == &start) return "";

            string s = current->s;
            Node* prev = current;
            current = current->prev;

            // Delete popped node
            delete prev;
            return s;
      }
      string Stack::peek() { return current->s; }
      bool Stack::is_empty() {
            if (current == &start) return true;
            return false;
      }
      Stack::~Stack() {
            while (current != &start) {
                  Node* prev = current;
                  current = current->prev;
                  delete prev;
            }
      }
      NumStack::NumStack() : start(NULL, 0) { current = &start; }
      void NumStack::push(double s) {
            Node* n = new Node(current, s);
            current = n;
      }
      double NumStack::pop() {
            if (current == &start) return 0;

            double s = current->s;
            Node* prev = current;
            current = current->prev;

            // Delete popped node
            delete prev;
            return s;
      }
      double NumStack::peek() { return current->s; }
      bool NumStack::is_empty() {
            if (current == &start) return true;
            return false;
      }
      NumStack::~NumStack() {
            while (current != &start) {
                  Node* prev = current;
                  current = current->prev;
                  delete prev;
            }
      }
}

 

excel 클래스

- 전체 excel 을 의미하는 Table 클래스와

- Table을 이루고 있는 각각의 요소 Cell 클래스 구현

더보기

excel.h

#pragma once
#ifndef EXCEL_H
#define EXCEL_H

#include <string>
#include "utils.h"

namespace MyExcel {

	class Table;

	class Cell {
	protected:
		int x, y;
		Table* table;
		std::string data;

	public:
		virtual std::string stringify();
		virtual int to_numeric();
		Cell(std::string data, int x, int y, Table* table);
	};

	class Table {
	protected:
		int max_row_size, max_col_size;
		Cell*** data_table;

	public:
		Table(int max_row_size, int max_col_size);
		~Table();
		void reg_cell(Cell* c, int row, int col);
		int to_numeric(const std::string& s);
		int to_numeric(int row, int col);
		std::string stringify(const std::string& s);
		std::string stringify(int row, int col);
		virtual std::string print_table() = 0;
	};

	class TxtTable : public Table {
		std::string repeat_char(int n, char c);
		std::string col_num_to_str(int n);

	public:
		TxtTable(int row, int col);
		std::string print_table() override;
	};

	class CSVTable : public Table {
	public:
		CSVTable(int row, int col);
		std::string print_table() override;
	};

	class HtmlTable : public Table {
	public:
		HtmlTable(int row, int col);
		std::string print_table() override;
	};

}  // namespace MyExcel

std::ostream& operator<<(std::ostream& o, MyExcel::Table& table);

#endif

 

 excel.cpp

#include "excel.h"
#include <iostream>
#include <sstream>

namespace MyExcel {

      // Cell implementation
      Cell::Cell(std::string data, int x, int y, Table* table)
            : data(data), x(x), y(y), table(table) {}

      std::string Cell::stringify() { return data; }
      int Cell::to_numeric() { return 0; }

      // Table implementation
      Table::Table(int max_row_size, int max_col_size)
            : max_row_size(max_row_size), max_col_size(max_col_size) {
            data_table = new Cell * *[max_row_size];
            for (int i = 0; i < max_row_size; i++) {
                  data_table[i] = new Cell * [max_col_size];
                  for (int j = 0; j < max_col_size; j++) {
                        data_table[i][j] = nullptr;
                  }
            }
      }

      Table::~Table() {
            for (int i = 0; i < max_row_size; i++) {
                  for (int j = 0; j < max_col_size; j++) {
                        if (data_table[i][j]) delete data_table[i][j];
                  }
                  delete[] data_table[i];
            }
            delete[] data_table;
      }

      void Table::reg_cell(Cell* c, int row, int col) {
            if (!(row < max_row_size && col < max_col_size)) return;
            if (data_table[row][col]) {
                  delete data_table[row][col];
            }
            data_table[row][col] = c;
      }

      int Table::to_numeric(const std::string& s) {
            int col = s[0] - 'A';
            int row = std::stoi(s.substr(1)) - 1;

            if (row < max_row_size && col < max_col_size) {
                  if (data_table[row][col]) {
                        return data_table[row][col]->to_numeric();
                  }
            }
            return 0;
      }

      int Table::to_numeric(int row, int col) {
            if (row < max_row_size && col < max_col_size && data_table[row][col]) {
                  return data_table[row][col]->to_numeric();
            }
            return 0;
      }

      std::string Table::stringify(const std::string& s) {
            int col = s[0] - 'A';
            int row = std::stoi(s.substr(1)) - 1;

            if (row < max_row_size && col < max_col_size) {
                  if (data_table[row][col]) {
                        return data_table[row][col]->stringify();
                  }
            }
            return "";
      }

      std::string Table::stringify(int row, int col) {
            if (row < max_row_size && col < max_col_size && data_table[row][col]) {
                  return data_table[row][col]->stringify();
            }
            return "";
      }

      // TxtTable implementation
      TxtTable::TxtTable(int row, int col) : Table(row, col) {}

      std::string TxtTable::print_table() {
            std::string total_table;

            int* col_max_wide = new int[max_col_size];
            for (int i = 0; i < max_col_size; i++) {
                  unsigned int max_wide = 2;
                  for (int j = 0; j < max_row_size; j++) {
                        if (data_table[j][i] &&
                              data_table[j][i]->stringify().length() > max_wide) {
                              max_wide = data_table[j][i]->stringify().length();
                        }
                  }
                  col_max_wide[i] = max_wide;
            }

            total_table += "    ";
            int total_wide = 4;
            for (int i = 0; i < max_col_size; i++) {
                  if (col_max_wide[i]) {
                        int max_len = std::max(2, col_max_wide[i]);
                        total_table += " | " + col_num_to_str(i);
                        total_table += repeat_char(max_len - col_num_to_str(i).length(), ' ');
                        total_wide += (max_len + 3);
                  }
            }

            total_table += "\n";
            for (int i = 0; i < max_row_size; i++) {
                  total_table += repeat_char(total_wide, '-');
                  total_table += "\n" + std::to_string(i + 1);
                  total_table += repeat_char(4 - std::to_string(i + 1).length(), ' ');

                  for (int j = 0; j < max_col_size; j++) {
                        if (col_max_wide[j]) {
                              int max_len = std::max(2, col_max_wide[j]);
                              std::string s = "";
                              if (data_table[i][j]) {
                                    s = data_table[i][j]->stringify();
                              }
                              total_table += " | " + s;
                              total_table += repeat_char(max_len - s.length(), ' ');
                        }
                  }
                  total_table += "\n";
            }

            delete[] col_max_wide;
            return total_table;
      }

      std::string TxtTable::repeat_char(int n, char c) {
            return std::string(n, c);
      }

      std::string TxtTable::col_num_to_str(int n) {
            std::string s = "";
            if (n < 26) {
                  s.push_back('A' + n);
            }
            else {
                  char first = 'A' + n / 26 - 1;
                  char second = 'A' + n % 26;
                  s.push_back(first);
                  s.push_back(second);
            }
            return s;
      }

      // CSVTable implementation
      CSVTable::CSVTable(int row, int col) : Table(row, col) {}

      std::string CSVTable::print_table() {
            std::string s;
            for (int i = 0; i < max_row_size; i++) {
                  for (int j = 0; j < max_col_size; j++) {
                        if (j >= 1) s += ",";
                        std::string temp;
                        if (data_table[i][j]) temp = data_table[i][j]->stringify();
                        for (char& c : temp) {
                              if (c == '"') {
                                    s += "\"\"";
                              }
                              else {
                                    s += c;
                              }
                        }
                        s = "\"" + s + "\"";
                  }
                  s += '\n';
            }
            return s;
      }

      // HtmlTable implementation
      HtmlTable::HtmlTable(int row, int col) : Table(row, col) {}

      std::string HtmlTable::print_table() {
            std::string s = "<table border='1' cellpadding='10'>";
            for (int i = 0; i < max_row_size; i++) {
                  s += "<tr>";
                  for (int j = 0; j < max_col_size; j++) {
                        s += "<td>";
                        if (data_table[i][j]) s += data_table[i][j]->stringify();
                        s += "</td>";
                  }
                  s += "</tr>";
            }
            s += "</table>";
            return s;
      }

}  // namespace MyExcel

std::ostream& operator<<(std::ostream& o, MyExcel::Table& table) {
      o << table.print_table();
      return o;
}

 

 

8-2. Excel 만들기 프로젝트 2부

 

Cell 클래스를 상속받는 클래스들 생성

 

Cell

class Cell {
 protected:
  int x, y;
  Table* table;

 public:
  virtual string stringify() = 0;
  virtual int to_numeric() = 0;

  Cell(int x, int y, Table* table);
};

 

StringCell

: 문자열 정보 보관 클래스

더보기
class StringCell : public Cell {
  string data;

 public:
  string stringify();
  int to_numeric();

  StringCell(string data, int x, int y, Table* t);
};


StringCell::StringCell(string data, int x, int y, Table* t)
    : data(data), Cell(x, y, t) {}
string StringCell::stringify() { return data; }
int StringCell::to_numeric() { return 0; }

 

DateCell

: 날짜 정보 보관 클래스 (yyyy-mm-dd 형식)

더보기
class DateCell : public Cell {
  time_t data;

 public:
  string stringify();
  int to_numeric();

  DateCell(string s, int x, int y, Table* t);
};

string DateCell::stringify() {
  char buf[50];
  tm temp;
  localtime_s(&temp, &data);

  strftime(buf, 50, "%F", &temp);

  return string(buf);
}
int DateCell::to_numeric() { return static_cast<int>(data); }

DateCell::DateCell(string s, int x, int y, Table* t) : Cell(x, y, t) {
  // 입력받는 Date 형식은 항상 yyyy-mm-dd 꼴이라 가정한다.
  int year = atoi(s.c_str());
  int month = atoi(s.c_str() + 5);
  int day = atoi(s.c_str() + 8);

  tm timeinfo;

  timeinfo.tm_year = year - 1900;
  timeinfo.tm_mon = month - 1;
  timeinfo.tm_mday = day;
  timeinfo.tm_hour = 0;
  timeinfo.tm_min = 0;
  timeinfo.tm_sec = 0;

  data = mktime(&timeinfo);
}

 

NumberCell

: 정수 정보 보관 클래스

더보기
class NumberCell : public Cell {
  int data;

 public:
  string stringify();
  int to_numeric();

  NumberCell(int data, int x, int y, Table* t);
};

NumberCell::NumberCell(int data, int x, int y, Table* t)
    : data(data), Cell(x, y, t) {}

string NumberCell::stringify() { return to_string(data); }
int NumberCell::to_numeric() { return data; }

 

ExprCell

더보기
class ExprCell : public Cell {
  string data;
  string* parsed_expr;

  Vector exp_vec;

  // 연산자 우선 순위를 반환합니다.
  int precedence(char c);

  // 수식을 분석합니다.
  void parse_expression();

 public:
  ExprCell(string data, int x, int y, Table* t);

  string stringify();
  int to_numeric();
};

int ExprCell::to_numeric() {
  double result = 0;
  NumStack stack;

  for (int i = 0; i < exp_vec.size(); i++) {
    string s = exp_vec[i];

    // 셀 일 경우
    if (isalpha(s[0])) {
      stack.push(table->to_numeric(s));
    }
    // 숫자 일 경우 (한 자리라 가정)
    else if (isdigit(s[0])) {
      stack.push(atoi(s.c_str()));
    } else {
      double y = stack.pop();
      double x = stack.pop();
      switch (s[0]) {
        case '+':
          stack.push(x + y);
          break;
        case '-':
          stack.push(x - y);
          break;
        case '*':
          stack.push(x * y);
          break;
        case '/':
          stack.push(x / y);
          break;
      }
    }
  }
  return stack.pop();
}

void ExprCell::parse_expression() {
  Stack stack;

  // 수식 전체를 () 로 둘러 사서 exp_vec 에 남아있는 연산자들이 push 되게
  // 해줍니다.
  data.insert(0, "(");
  data.push_back(')');

  for (int i = 0; i < data.length(); i++) {
    if (isalpha(data[i])) {
      exp_vec.push_back(data.substr(i, 2));
      i++;
    } else if (isdigit(data[i])) {
      exp_vec.push_back(data.substr(i, 1));
    } else if (data[i] == '(' || data[i] == '[' ||
               data[i] == '{') {  // Parenthesis
      stack.push(data.substr(i, 1));
    } else if (data[i] == ')' || data[i] == ']' || data[i] == '}') {
      string t = stack.pop();
      while (t != "(" && t != "[" && t != "{") {
        exp_vec.push_back(t);
        t = stack.pop();
      }
    } else if (data[i] == '+' || data[i] == '-' || data[i] == '*' ||
               data[i] == '/') {
      while (!stack.is_empty() &&
             precedence(stack.peek()[0]) >= precedence(data[i])) {
        exp_vec.push_back(stack.pop());
      }
      stack.push(data.substr(i, 1));
    }
  }
}
int ExprCell::precedence(char c) {
  switch (c) {
    case '(':
    case '[':
    case '{':
      return 0;
    case '+':
    case '-':
      return 1;
    case '*':
    case '/':
      return 2;
  }
  return 0;
}

 

 

문제 1

ExprCell 의 쉭에서 셀의 이름은 A3 과 같이 단 두 글자만 가능하다는 제약 조건이 있었습니다. 이를 임의의 크기의 이름도 가능하게 확장해보세요. (난이도 : 下)

문제 2

마찬가지로 가능한 숫자도 임의의 길이가 상관없게 확장해보세요. (난이도 : 下)

더보기
for (size_t i = 0; i < data.length(); i++) {
        if (isalpha(data[i])) {
            size_t j = i + 1;
            while (j < data.length() && (isalnum(data[j]) || data[j] == '$')) {
                j++;
            }
            exp_vec.push_back(data.substr(i, j - i));
            i = j - 1;
        } else if (isdigit(data[i])) {
            size_t j = i + 1;
            while (j < data.length() && (isdigit(data[j]) || data[j] == '.')) {
                j++;
            }
            exp_vec.push_back(data.substr(i, j - i));
            i = j - 1;
        } else {
            // 
        }
    }