728x90
728x90

>> 비주얼스튜디오 주석 단축키

 

주석만 뽑아서 문서화 만들기, Doxygen

Doxygen은 소프트웨어 레퍼런스 문서 생성기입니다.
주석을 다는 목적은 코드에 관련된 기억을 쉽게 되살리고 남에게 코드에 대한 설명을 빠르게 전달하기 위해서입니다.

Doxygen을 사용하면 코드 버전에 따라 자동적으로 문서로 관리할 수 있습니다.
Doxygen의 주석 문법만 맞게 사용한다면 말입니다.

 

기본적인 Doxygen의 주석 형태는 다음과 같습니다.
@item들은 주석에 대한 메타데이터입니다.

/**
* 주석
*/

 

메인페이지 주석은 아래처럼 작성합니다.

/**
* @mainpage 메인페이지제목
* @brief 간략한 설명
* @details 자세한 설명
*/

 

파일 관련 주석은 다음처럼 작성합니다.

/**
* @file NewClass.java
* @brief 간략한 설명
* @details 자세한 설명
*/

 

클래스주석은 다음처럼 클래스 위에 작성합니다.

/**
* @brief 클래스 간략한 설명
* @details 자세한 설명
* @author lee
* @date 2021-11-09
* @version 0.0.1
*/

 

메서드 주석은 다음처럼 함수 위에 작성합니다.

/**
* @brief 간략한 설명
* @details 자세한 설명
* @param args 콘솔파라미터
* @return 프로그램 상태
*
* @bug 메모리 누수 있음
* @todo 해결 해야하는 버그
* @exception ...
*
* @see NewClass
* @see http:// ...
*/

 

 

더 자세한 item들에 대한 정보는 다음을 참고합니다.(이동)

Doxygen을 사용하려면 설치가 필요하며 함수 구조도, 클래스 구조도 등의 그래프를 문서에 포함시키기 위해 Graphviz 설치를 추천합니다.

 

 

Graphviz 설치 방법 (윈도우, 맥, 리눅스, Solaris 등)

https://graphviz.org/download/

Graphviz 설치

 

 

위 페이지에서 사양에 맞는 exe 설치 파일을 다운받아 실행합니다.
동의함을 선택하고 Add Graphviz to the system PATH for current user를 선택합니다. 

 

 

 

이러면 사용자변수 PATH를 지정할 필요가 없어 편하지만, 기본값으로 해도 나중에 별도로 지정할 수 있습니다.
설치할 폴더 경로를 선택한 후 다음을 클릭합니다. 바로가기 아이콘 여부를 선택하고 설치를 누르면 완료됩니다.

 

 

Doxygen 맥 설치 방법

터미널에 다음을 입력합니다.
Doxygen 패키지와 graphviz를 한꺼번에 설치하는 문장입니다.

sudo apt-get install doxygen graphviz

 

 

Doxygen 윈도우 설치 방법

https://www.doxygen.nl/download.html

Doxygen 윈도우 설치, 설정 방법

 

Doxygen 을 윈도우에 설치하면서 설치 과정을 캡쳐해 남겨 보았습니다.
Doxygen 설치 과정 중엔 특별히 할 것이 없습니다.

 

 

 

Doxygen 을 설치한 후 프로그램을 실행시켜 설정을 해 주어야 할 것들 몇 가지가 있습니다.

1) 프로젝트 루트 폴더 지정
2) 소스파일 폴더 지정
3) Scan recursively 체크박스에 체크
4) Destination directory 폴더 지정 (Doxygen이 문서를 생성할 폴더)
5) 프로젝트의 이름과 버전 지정

 

 

Doxygen 루트 설정

 

 

6) 각 함수마다 사용한 함수로의 링크 생성 체크
7) 해당 언어 선택


 

 

 

8) 문서 왼쪽에 탐색 트리 보이기 체크
9) 소스 간의 관계를 GraphViz로 표현할 것 체크

 

 

 

 


10) OUTPUT_LANGUAGE 체크 (출력 결과에 쓰여질 언어)
11) ALWAYS_DETAILED_SEC 체크 (항상 상세 정보 출력)
12) INLINE_INHERITED_MEMS 체크 

 

 

 

 

13) EXTRACT_ALL(소스코드의 모든 요소가 문서화 대상으로)
14) EXTRACT_PRIVATE,  EXTRACT_STATIC (체크 클래스 내 private, static 멤버 포함할 지)
15) INLINE_SOURCES 체크 (함수 설명 시 함수 코드 보임)

 

 

 

 


16) CLASS_DIAGRAMS 체크 (상속구조 다이어그램 그림)
17) UML_LOOK 체크 (다이어그램을 UML 형식으로 그림)
18) DOT_PATH (Graphviz를 의미)


 

 

Run doxygen 버튼을 눌러 문서를 생성시킨 후 , Show HTML output를 통해 생성된 문서를 조회할 수 있습니다.

 

 

the dot tool could not be found at 오류

원인 : OUTPUT_DIRECTORY가 C로 되어있어서 write가 안되서 그런 것 같습니다
그리고 그 전에 났던 오류 : OUTPUT_LANGUAGE가 Korean일 경우. Korean-en으로 바꾸니 문제없습니다.

 

 

 

참고

https://www.slideshare.net/arload/doxygen-33932243

728x90
728x90
블로그 이미지

coding-restaurant

코딩 맛집에 방문해주셔서 감사합니다.

,
728x90
728x90

개발하면서 나만의 주석 양식을 만들어서 잘 활용하는 것이 중요합니다.

 

 

C, C++ 주석 문자열 종류

C, C++ 다양한 주석에 대해 정리해보았습니다.

// 주석

/* 주석 */

/**
* 주석
*/

// 주석 \
주석 \
주석

#if 0
	주석
#endif


#if 1
	printf("hi");
#endif

 

 

 

비주얼스튜디오 주석 단축키

비주얼스튜디오 주석 단축키는 문자열 블록 후 "Ctrl + K", "Ctrl + U"를 차례로 누르면 됩니다.
또는 "Ctrl + K + C"를 눌러도 됩니다.

// 비주얼스튜디오 주석 단축키를 누른 결과물

/* 주석 */

 

 

 

어떤 주석 양식을 쓸 것인가

C, C++을 개발하는 개인에 따라 다양한 종류의 주석 스타일이 나오겠지만 추후 주석을 자동으로 문서화하는 경우도 생각하여 달아두면 좋습니다. 그 중 많이 사용되는 Doxygen의 양식에 맞춰 습관을 들이는 것도 괜찮을 듯 합니다.

 

>> Doxygen 설치와 작성 방법, Graphviz 설치까지

728x90
728x90
블로그 이미지

coding-restaurant

코딩 맛집에 방문해주셔서 감사합니다.

,
728x90
728x90

10-1 연산자 오버로딩의 이해와 유형

함수가 오버로딩되면, 이름은 하나지만 오버로딩 된 수만큼 다양한 기능을 제공한다.
연산자 오버로딩도 기존에 존재하던 연산자의 기본 기능 외 다른 기능을 추가할 수 있다.

 

operator+ 함수
// FirstOperationOverloading.cpp 

#include <iostream>
using namespace std;

class Point
{
private:
    int xpos, ypos;
public:
    Point(int x = 0, int y=0): xpos(x), ypos(y)
    {   }
    void ShowPosition() const // 내부출력문 const
    {
        cout << '[' << xpos << ", " << ypos << ']' << endl;
    } 
    Point operator+(const Point& ref) // 연산자 오버로딩
    {
        Point pos(xpos + ref.xpos, ypos + ref.ypos); 
        return pos;    //두 객체의 멤버별 덧셈결과로 복사생성자가 호출되며 새로운 Point 객체가 만들어지고 반환되어 pos3를 초기화 한다
    }
};

int main()
{
    Point pos1(3, 4);
    Point pos2(10, 20);
    Point pos3 = pos1.operator+(pos2);    //pos1객체의 멤버함수를 호출하면서 인자로 pos2를 전달
    
    // Point pos3 = pos1 + pos2; //컴파일 가능, 같은결과
    // pos1 + pos2는 pos1.operator+(pos2)의 다른 표현
    // 변환되기 위해서 약속된 변환의 규칙이 있다

    pos1.ShowPosition();
    pos2.ShowPosition();
    pos3.ShowPosition();

    return 0;
}

 

 

main함수를 변경하여 다시 실행해본다.

 

// OverloadingOperation.cpp  

#include <iostream>
using namespace std; 

class Point
{
private:
    int xpos, ypos;
public:
    Point(int x = 0, int y = 0) : xpos(x), ypos(y)
    {   }
    void ShowPosition() const // 내부출력문 const
    {
        cout << '[' << xpos << ", " << ypos << ']' << endl;
    }
    Point operator+(const Point& ref) // 연산자 오버로딩
    {
        Point pos(xpos + ref.xpos, ypos + ref.ypos);
        return pos;    //두 객체의 멤버별 덧셈결과로 복사생성자가 호출되며 새로운 Point 객체가 만들어지고 반환되어 pos3를 초기화 한다
    }
};

int main()
{
    Point pos1(3, 4);
    Point pos2(10, 20);
    // Point pos3 = pos1.operator+(pos2);    //pos1객체의 멤버함수를 호출하면서 인자로 pos2를 전달

    Point pos3 = pos1 + pos2; //컴파일 가능, 같은결과
    // pos1 + pos2는 pos1.operator+(pos2)의 다른 표현
    // 변환되기 위해서 약속된 변환의 규칙이 있다

    pos1.ShowPosition();
    pos2.ShowPosition();
    pos3.ShowPosition();

    return 0;
}

 

 

Point pos3 = pos1.operator+(pos2);    //pos1객체의 멤버함수를 호출하면서 인자로 pos2를 전달
Point pos3 = pos1 + pos2; //컴파일 가능, 같은결과로 위의 다른 표현
...변환되기 위해서 약속된 변환의 규칙이 있다.

 

 

복사생성자 (이동)

더보기

C++에서 복사 생성자란 자신과 같은 클래스 타입의 다른 객체에 대한 참조(reference)를 인수로 전달받아, 그 참조를 가지고 자신을 초기화하는 방법입니다. 복사 생성자는 새롭게 생성되는 객체가 원본 객체와 같으면서도, 완전한 독립성을 가지게 해줍니다. 왜냐하면, 복사 생성자를 이용한 대입은 깊은 복사(deep copy)를 통한 값의 복사이기 때문입니다.

Book(const Book&);

 

복사 생성자는 다음과 같은 상황에서 주로 사용됩니다. 

1. 객체가 함수에 인수로 전달될 때
2. 함수가 객체를 반환값으로 반환할 때
3. 새로운 객체를 같은 클래스 타입의 기존 객체와 똑같이 초기화할 때

 

 

 

연산자 오버로딩, C++의 약속

객체도 기본 자료형 변수처럼 취급하여 덧셈, 뺄셈, 곱셈 등의 연산을 가능하게 한다.

operator 키워드와 연산자를 묶어서 함수의 이름을 정의하면 함수의 이름을 이용한 호출과 연산자를 이용한 함수의 호출을 허용한다.

pos1, pos2가 기본 자료형 변수가 아닌 객체여도 pos1 + pos2는 pos1.operator+(pos2)라는 문장으로 바꿔 해석한다. 
객체를 피연산자로 사용한 연산자의 이름 앞에 operator라는 이름을 붙여서 완성되는 이름의 함수를 호출하며 연산자 우측에 있는 피연산자를 인자로 전달

pos1-pos2;는 pos1.operator-(pos2);로 변환된다.
연산자 앞에 operator를 붙여서, operator-라는 이름의 함수를 호출. 연산자 왼쪽의 pos1객체를 대상으로 operator-함수를 호출, -연산자의 우측에 있는 피연산자를 인자로 전달. 

 

 

오버로딩 된 연산자의 해석

pos1 + pos2
1   2	3

pos1.operator+(pos2)
1	2	3

 

1. 멤버함수를 호출할 객체
2. 함수의 이름
3. 함수의 전달인자

연산자를 오버로딩한 함수도 const로 선언하는 게 좋다. 덧셈연산은 연산자의 대상이 되는 피연산자 값의 변경이 아니고 새로운 연산의 결과를 만들어 내는 것이기 때문에 const 선언이 가능하다.

 

 

연산자 오버로딩의 두 가지 방법

1. 멤버함수에 의한 연산자 오버로딩
2. 전역함수에 의한 연산자 오버로딩

 

pos1+pos2;가 멤버함수로 오버로딩되면  pos1.operator+(pos2);

 pos1.operator+(pos2);


pos1+pos2;가 전역함수로 오버로딩되면  operator+(pos1, pos2);

operator+(pos1, pos2);

 

동일한 자료형을 대상으로 + 연산자를 전역함수 기반, 멤버함수 기반으로 동시에 오버로딩 하면 멤버함수 기반으로 오버로딩된 함수가 전역함수 기반으로 오버로딩된 함수보다 우선으로 호출 (가급적 이런 상황은 만들지 x)

 

 

 

전역함수 기반으로 연산자 오버로딩
// GFunctionOverloading.cpp

#include <iostream>
using namespace std;

class Point
{
private:
    int xpos, ypos;
public:
    Point(int x=0, int y=0): xpos(x), ypos(y)
    { }
    void ShowPosition() const //내부출력문 const
    {
        cout << '[' << xpos << ", " << ypos << ']' << endl;
    }
    friend Point operator+(const Point& pos, const Point& pos2);
    // 아래 operator+ 함수에 대해 private 접근을 허용하기 위한 friend 선언
};

// +연산자를 전역함수의 형태로 오버로딩
// Point 클래스는 operator+ 함수에 대해 freind선언을 하여 
// 함수 내에서는 Point 클래스의 private 멤버에 접근 가능
Point operator+(const Point& pos1, const Point &pos2) 
{
    Point pos(pos1.xpos + pos2.xpos, pos1.ypos + pos2.ypos);
    return pos;
}

int main()
{
    Point pos1(3, 4);
    Point pos2(10, 20);
    Point pos3 = pos1 + pos2; // +연산자가 전역함수의 형태로 오버로딩
    // operator+(pos1, pos2); 형태로 해석
    pos1.ShowPosition();
    pos2.ShowPosition();
    pos3.ShowPosition();

    return 0;
}

 

 

객체지향이기 때문에 특별한 경우가 아니면 멤버함수 기반으로 연산자를 오버로딩하는 게 낫다.
(예외는 아래에서 다룸)

 

 

 

오버로딩이 불가한 연산자의 종류

C++의 문법규칙 보존을 위해 아래 연산자들의 오버로딩을 제한한다.

1. 멤버 접근 연산자 (.)
2. 멤버 포인터 연산자(.*)
3. 범위 지정 연산자 (::)
4. 조건 연산자 (3항연산자 ?:)
5. sizeof (바이트 단위 크기 계산)
6. typeid (RTTI 관련 연산자) 
7. static_cast (형변환 연산자)
8. dynamic_cast (형변환 연산자)
9. const_cast (형변환 연산자)
10. reinterpret_cast (형변환연산자)

※ RTTI (runtime type information) :  RTTI는 C++ 컴파일러 내에 포함되어 있는 기능으로서, 객체의 유형을 실행 시에 결정할 수 있도록 허용한다. C++이나 다른 고급언어의 컴파일러에서 가지고 있는 이 기능은, 메모리 상주 객체에 유형 정보를 추가한다. 이렇게 함으로써, 실행 시스템은 객체의 캐스트가 유효한 것인지를 확실히 하기 위해, 그 객체가 특정 유형인지를 결정할 수 있다. 객체 기술의 기본 원리 중 하나가, 실행시 객체를 동적으로 변화시킬 수 있는 능력인 polymorphism이다. (출처로 이동)

 

 

멤버함수 기반으로만 오버로딩이 가능한 연산자

1. 대입 연산자 (=)
2. 함수 호출 연산자(())
3. 배열 접근 연산자([])
4. 멤버 접근을 위한 포인터 연산자 (->)

 

 

연산자 오버로딩 시 주의사항

1. 본래의 의도를 벗어난 형태의 연산자 오버로딩은 좋지 않다.
2. 연산자의 우선순위와 결합성은 바뀌지 않는다.
3. 매개변수의 디폴트 값 설정이 불가능하다.
- 피연산자의 자료형에 따라서 연산자를 오버로딩 한 함수의 호출이 결정되어 불가능하다.
- 매개변수의 디폴트 값이 설정되면 함수 호출관계가 불분명해진다.

4. 연산자의 순수 기능까지 빼앗을 수는 없다.
- int 데이터의 + 연산은 의미가 정해져 있고 그를 변경하는 아래와 같은 함수 정의는 허용되지 않는다.

int operator+(const int num1, const int num2) //정의 불가능
{
    return num1*num2;
}

 

 

※ 연산자 오버로딩 명칭의 이유

함수가 오버로딩되면 전달되는 인자에 따라 호출되는 함수가 달라지고, 이와 유사하게 연산자가 오버로딩되면 피연산자의 종류에 따라 연산의 방식이 달라지므로 연산자 오버로딩이라고 부르는 것이다.

int num = 3 + 4;
Point pos3 = pos1 + pos2; //pos1, pos2는 Point형 객체

 

 

 

 

 

10-2 단항 연산자의 오버로딩

이항연산자와 단항연산자는 피연산자의 개수가 2개, 1개로 다르고 이에 따른 오버로딩 차이점은 매개변수 개수이다.

 

증가, 감소 연산자의 오버로딩

단항 연산자로 ++, --이 있다. 1씩 증감하는 연산자다.
Point클래스에 ++ 연산자가 오버로딩 되어있다면 다음과 같은 문장이 가능하다.

++pos; //pos는 클래스객체
--pos;

호출되는 함수의 이름은 ++연산자와 키워드 operator를 연결해서 완성하여 operator++가 된다.
멤버함수의 형태로 오버로딩되면 pos의 멤버함수가 호출되는 형태이니 이렇게 해석된다.

pos.operator()++;

단항연산자를 오버로딩하여 전달인자는 없다! (하나 있는 피연산자의 멤버함수 호출하는 형태)
전역함수로 오버로딩된 경우 operator++가 전역함수의 이름이 되므로 이렇게 해석된다.
전역함수는 피연산자가 모두 인자로 전달된다.

operator++(pos); //전역함수는 피연산자가 모두 인자로 전달

 

++pos; 가 멤버함수로 오버로딩되면 pos.operator+();
++pos; 가 전역함수로 오버로딩되면 operator++(pos);

 

 

단항연산자 오버로딩 예제 :: 멤버함수 기반, 전역함수 기반으로 증가, 감소연산자를 오버로딩
// OneOpndOverloading.cpp 

#include <iostream>
using namespace std;

class Point
{
private:
    int xpos, ypos;
public:
    Point(int x = 0, int y = 0) : xpos(x), ypos(y) //멤버이니셜라이져
    {   }
    void ShowPosition() const //내부출력 const
    {
        cout << '[' << xpos << ", " << ypos << ']' << endl;
    }
    Point& operator++() //++연산자가 멤버함수의 형태로 오버로딩 
    {
        xpos += 1;
        ypos += 1;
        return *this; 
        // this : 객체 자신의 포인터 값
        // this* 는 객체 자신을 의미, 객체 자신을 반환 
        // 
        // 함수의 반환형이 참조형으로 선언되어 객체 자신을 참조할 수 있는 참조값이 반환        
        // 반환형이 Point형으로 선언됐다면 객체 자신의 복사본을 만들어서 반환 (여기서는 복사 안일어남)
        // 일반적인 ++연산자처럼 연산이 가능하게 하려고 참조값을 반환한다
    }
    friend Point& operator--(Point &ref); //아래 정의된 전역함수에 대해 friend 선언
};

Point& operator--(Point& ref) //--연산자가 전역함수의 형태로 오버로딩
{
    ref.xpos -= 1;
    ref.ypos -= 1;
    return ref; //전달된  pos객체를 참조자ref로 받아서 다시 참조형으로 반환한다.
}

int main()
{
    Point pos(1, 2);
    ++pos;  //멤버함수의 형태로 오버로딩 되었으므로 이 문장은 pos.operator++(); 로 해석    
    pos.ShowPosition();
    
    --pos;  //전역함수의 형태로 오버로딩 되었으므로 이 문장은 operator--(pos); 로 해석
    pos.ShowPosition();

    ++(++pos);  
    // -> ++(pos.operator++()); pos객체의 멤버변수 값은 1 증가하고 pos의 참조 값이 반환
    // -> ++(pos의 참조 값);    
    // -> (pos의 참조값).operator++(); 와 같다
    // pos의 참조값은 pos객체를 대상으로 하는 연산과 같다
    // 결론은 pos 멤버변수가 2 증가한다

    pos.ShowPosition(); //출력

    --(--pos);  
    // --(operator--(pos)); 
    // --(pos의 참조 값);    
    // -> operator--(pos의 참조값); 와 같다

    pos.ShowPosition(); //출력

    return 0;
}

 

 

 

++(++pos); -> ++(pos.operator++( )); -> ++(pos의 참조 값); -> (pos의 참조 값).operator++( );
--(--pos); -> --(operator--(pos)); -> --(pos의 참조 값); -> operator--(pos의 참조 값);

 

 

 

전위증가와 후위증가의 구분

++연산자와 --연산자는 피연산자의 위치에 따라 의미가 달라진다.

num++ // 출력 후 num값 증가
++num // num값 증가 후 출력

앞의 예제는 전위증가/전위감소 연산 형태였고 이제는 후위증가/후위감소 연산에 대한 연산자 오버로딩을 알아본다.
C++에서 전위/후위 연산에 대한 해석방식은 규칙으로 정해두고 있다.

++pos -> pos.operator++();
pos++ -> pos.operator++(int);

--pos -> pos.operator--();
pos-- -> pos.operator--(int);

키워드 int를 이용해서 후위연산에 대한 함수를 전위연산에 대한 함수와 구분하고 있다.
int는 후위연산 구분용이지 데이터를 인자로 전달하는 목적이 아니다.

 

 

후위증가 후위감소 연산자 오버로딩 예
// PostOpndOverloading.cpp  

#include <iostream>
using namespace std;

class Point
{
private:
    int xpos, ypos;
public:
    Point(int x = 0, int y = 0) : xpos(x), ypos(y) //멤버이니셜라이져
    {}
    
    void ShowPosition() const //내부출력 const
    {
        cout << '[' << xpos << ", " << ypos << ']' << endl;
    }
    
    // 전위증가
    Point& operator++() //++연산자가 멤버함수의 형태로 오버로딩
    {
        xpos += 1;
        ypos += 1;
        return *this; 
        // this는 객체 자신의 포인터 값
        // this* 는 객체 자신
        // 객체 자신을 반환
        // 
        // 함수의 반환형이 참조형으로 선언되어
        // 객체 자신을 참조할 수 있는 참조값이 반환

        // 반환형이 Point형으로 선언됐다면 객체 자신의 복사본을 만들어서 반환
    }

    //후위증가 -- int 매개변수로 구분!
    const Point operator++(int)
    {
        const Point retobj(xpos, ypos); //const Point retobj(*this);
        // 값의 증가에 앞서 반환에 사용할 복사본을 만든다
        // 값이 변경되면 안되서 const 선언
        // 물론 복사생성자 호출 형태로도 구현이 가능
        
        xpos += 1;
        ypos += 1;  // 멤버 값이 증가하기 이전에 만들어둔 복사본을 반환. 후위증가
        return retobj;
    }
    friend Point& operator--(Point &ref);
    friend const Point operator--(Point &ref, int); //후위감소
};

Point& operator--(Point &ref) //전위감소
{
    ref.xpos -= 1;
    ref.ypos -= 1;
    return ref;
}

// --를 후위증가 형태로 연산자를 전역함수 형태로 오버로딩
// 매개변수 선언에 int 를 추가하여 후위증가 명시
const Point operator--(Point &ref, int) // 후위감소
{
    const Point retobj(ref); // const 객체
    ref.xpos -= 1;
    ref.ypos -= 1;
    return retobj;
}

int main()
{
    Point pos(3, 5);
    Point cpy;
    cpy = pos--;
    cpy.ShowPosition();
    pos.ShowPosition();

    cpy = pos++;
    cpy.ShowPosition();
    pos.ShowPosition();
    return 0;
}

 

 

 

연산자오버로딩 예제 분할컴파일 (분리컴파일) 자체 실습

분할컴파일 연습을 해볼 겸 연산자오버로딩 예제 코드를 헤더와 cpp로 분리해보았습니다.
분할컴파일 연습으로 얻을 수 있는 것 : 반환형에 대한 정확한 이해

 

Point.h

#pragma once

/**
*
* @brief 후위증가 예시
* @details 연산자오버로딩
* @author lee
* @date 2021-11-23
* @version 0.0.1
*
*/

// TODO: 없음.20120
class Point
{
private:
    int xpos, ypos;
public:    
    Point(int x = 0, int y = 0);
    virtual ~Point();    
    void ShowPosition() const;      //내부출력 const    
    
    Point& operator++()             //전위증가
    {
        xpos += 1;
        ypos += 1;
        return *this; // 함수의 반환형이 참조형으로 선언되어 객체 자신을 참조할 수 있는 참조값이 반환
    } 

    const Point operator++(int);    //후위증가 - int 매개변수로 구분!
    
    friend Point& operator--(Point& ref); //전위감소
    friend const Point operator--(Point& ref, int);     //후위감소
};

 

Point.cpp

#include <iostream>
#include "Point.h"

using namespace std;

Point::Point(int x , int y)
	: xpos(x)
    , ypos(y) //생성자, 멤버이니셜라이져
{ 
    
}

Point::~Point()
{

}

void Point::ShowPosition() const //내부출력 const
{ 
	cout << '[' << xpos << ", " << ypos << ']' << endl; 
}
 
// Point& operator++()     전위증가 -- 정의도 h파일에 함

//point클래스의 멤버함수 
const Point Point::operator++(int) //후위증가 -- int 매개변수로 구분! 
{   //복사되어 들어옴
    const Point retobj(xpos, ypos); //const Point retobj(*this);
    // 반환에 사용할 복사본은 변경되면 안되서 const
    xpos += 1;
    ypos += 1;
    return retobj;  //멤버 값이 증가하기 전에 만들어뒀던 복사본 반환
}

Point& operator--(Point& ref) //전위감소
{
    ref.xpos -= 1;
    ref.ypos -= 1;
    return ref;
}

// 반환형 const는 함수의 반환으로 인해 생성되는 임시객체가 const로 생성 
// const 함수 내에서는 const 함수의 호출만 허용하도록 제한한다. 값의 변경유무와 별개로도 
// const 객체를 대상으로 참조자를 선언할 때 참조자도 const 선언해야 한다.
const Point operator--(Point &ref, int) // 후위감소 (전역함수 형태로 오버로딩)
{
    const Point retobj(ref); // 함수 내에서 retobj의 변경을 막는다 (후위증감 구현)
    //Point retobj(ref); // 함수 내에서 retobj의 변경을 막는다 (후위증감 구현) 
    ref.xpos -= 1;
    ref.ypos -= 1;
    return retobj; 
}

 

main.cpp

#include <iostream>
#include "Point.h"

int main()
{
    Point pos(3, 5);
    Point cpy;
    cpy = pos--;
    cpy.ShowPosition();
    pos.ShowPosition();

    cpy = pos++;    
    cpy.ShowPosition();
    pos.ShowPosition();

    //(pos++)++; //컴파일에러
    //(pos--)--; //컴파일에러
    // -> (Point형 const 임시객체)--;    
    // -> operator--(Point형 const 임시객체(상수니까)) 호출
    // 
    // const 객체 대상으로 const 선언되지 않은 멤버함수의 호출이 불가능하다
    // 
    // operator++ 멤버함수는 const로 선언된 함수가 아니고 (?잘모르겠음)
    // operator-- 전역함수는 매개변수로 선언된 참조자가 const 선언이 되지 않아서
    // 컴파일 에러가 발생한다
    
    cpy.ShowPosition();

    return 0;
}

 

 

 

반응형

 

 

10-3 교환법칙 문제의 해결

교환법칙은 'A+B의 결과는 B+A 결과와 같음'을 뜻한다.
연산자를 중심으로 한 피연산자의 위치는 연산결과에 영향이 없다.
교환법칙 성립 연산은 곱셈, 덧셈연산이 있다.

 

자료형이 다른 두 피연산자를 대상으로 연산

연산자 오버로딩으로 다른 자료형 데이터 2개를 연산하기
형변환 규칙에 따라 변환이 진행된 후 연산이 이루어진다.

 

Point.h

#pragma once
class Point
{
public:
	Point(int x = 0, int y = 0);
private:
	int xpos, ypos;
public:
	void ShowPosition() const;
	Point operator*(int times);
};

 

Point.cpp

#include <iostream>
#include "Point.h"
using namespace std;

Point::Point(int x, int y)
	: xpos(x), ypos(y)
{	}

void Point::ShowPosition() const
{
	cout << '[' << xpos <<", "<<ypos<< ']' << endl;
}

// 곱셈 연산자 오버로딩으로 객체와 정수간의 곱셈이 가능하게 됨
Point Point::operator*(int times)
{
	Point pos(xpos*times, ypos*times);
	return pos;
}

 

Main.cpp

// PointMultipleOperation 

#include <iostream>
#include "Point.h"

int main()
{
	Point pos(1, 2);
	Point cpy;

	cpy = pos*3; // pos.operator*(3) 로 해석되며 이 경우 Point객체가 곱셈연산자의 왼편에 와야한다
	// 교환법칙으로 3*pos;도 가능하게 하려면 오버로딩한 곱셈연산자를 바꿔야 한다.
	cpy.ShowPosition();

	cpy = pos*3*2; // 3을 곱했을 때 반환되는 객체를 대상으로 다시 2를 곱하고 결과가 cpy에 저장된다
	
	cpy.ShowPosition();

	return 0;
}

 

 

교환법칙 성립을 위한 구현

cpy = 3 * pos; 가 교환법칙이 성립되게 하려면 전역함수로 곱셈연산자를 오버로딩해야 한다.
cpy = operator*(3, pos); 처럼 해석되도록 연산자를 오버로딩 해야하고,
operator* 함수를 둘 중 하나로 정의해야 한다.

Point operator*(int times, Point& ref)
{
	Point pos(ref.xpos * times, ref.ypos * times);
	return pos;
}
Point operator*(int times, Point& ref)
{
	 return ref*times;
}

 

 

Point.h

#pragma once
class Point
{
public:
	Point(int x = 0, int y = 0);
private:
	int xpos, ypos;
public:
	void ShowPosition() const;
	Point operator*(int times); //곱셈연산자오버로딩
	friend Point operator*(int times, Point& ref); //교환법칙
};

 

Point.cpp

#include <iostream>
using namespace std;
#include "Point.h"

Point::Point(int x, int y)
		: xpos(x),
		ypos(y)
{	}

void Point::ShowPosition() const
{
	cout << '[' << xpos << ", " << ypos << ']' << endl;
}

Point Point::operator*(int times)
{
	Point pos(xpos * times, ypos * times);
	return pos;
}

// 교환법칙
Point operator*(int times, Point& ref)
{
	return ref*times;
}

 

Main.cpp

// CommunMultipleOperation 

#include <iostream>
using namespace std;
#include "Point.h"

int main()
{
	Point pos(1, 2);
	Point cpy;

	cpy = 3 * pos;
	cpy.ShowPosition();

	cpy = 2 * pos * 3;
	cpy.ShowPosition();

	return 0;
}

 

 

 

 

10-4  cout, cin  그리고 endl의 정체

cout과 endl

콘솔 입출력에 사용되는 키워드 cout과 endl을 유사하게 흉내내서 이해를 돕는 예제이다.
주어진 예제를 특성에 맞게 파일별로 분리해서 실습했다.

 

mystd.h

#pragma once
/**
*   cout과 endl 흉내내기를 위해 만든 mystd namespace
*   namespace는 소속을 정해 이름을 붙여 구분한다
*   이름이 같아 생길 수 있는 충돌을 방지한다
*/

namespace mystd
{
    using namespace std; // using namespace 이름;은 네임스페이스 모든 요소에 이름 없이 접근 가능
    // 함수 내부에서 선언해서 적용 범위는 함수 스코프 한정
    // printf 함수 호출을 위해 삽입  

	class ostream
	{
	public:
		void operator<<(char* str);	 // 문자열 출력
		void operator<<(char str);	 // 문자 출력
		void operator<<(int num);	 // 숫자 출력
		void operator<<(double e);   // 소수점 출력
		void operator<<(ostream& (*fp)(ostream& ostm));  // ??
	};
}

 

ostream.cpp

#include <stdio.h> 
#include "mystd.h"


namespace mystd
{
	void ostream::operator<<(char* str)
	{
		printf("%s", str);
	}

	void ostream::operator<<(char str)
	{
		printf("%c", str);
	}

	void ostream::operator<<(int num)
	{
		printf("%d", num);
	}

	void ostream::operator<<(double e)
	{
		printf("%g", e);
	}

	// ostream& endl(ostream& ostm) 함수를 인자로 전달받음
	// 추가공부 : 함수포인터
	void ostream::operator<<(ostream& (*fp)(ostream& ostm))
	{
		fp(*this);
	}

	// endl 함수
	ostream& endl(ostream& ostm)
	{
		ostm << '\n';
		fflush(stdout); //버퍼 비우기
		return ostm;
	}

	ostream cout; //객체의 이름 
	// 객체 내에서 다양한 기본자료형 데이터를 대상으로 << 연산자 오버로딩을 하고 있다.

}

 

 

main.cpp

// ConsoleOutput : cout과 endl 이해

#include <iostream>
#include "mystd.h" 

int main()
{
	// using 네임스페이스이름::요소 는
	// 네임스페이스 이름 없이도 사용하게 해줌
	// 지역적 using 선언
	using mystd::cout;
	using mystd::endl;

	cout << "Simple String";
	cout << endl;
	cout << 3.14;
	cout << endl;
	cout << 123;
	endl(cout);

	return 0;
}

 

 

아래처럼 연이은 <<연산을 진행하려면 <<연산 결과 cout이 반환되어야 한다. 그래서 예제를 수정해본다.

cout<<123<<endl<<3.14<<endl;

 

 

IterateConsoleOutput.cpp
// IterateConsoleOutput  

#include <iostream> 

namespace mystd
{
	using namespace std; // using namespace 이름;은 네임스페이스 모든 요소에 이름 없이 접근 가능
	// 함수 내부에서 선언해서 적용 범위는 함수 스코프 한정
	// printf 함수 호출을 위해 삽입  

	class ostream
	{
	public: 
		//void operator<<(const char* str)
		ostream& operator<<(char* str) //cout 객체의 참조값을 반환
		{
			printf("%s", str);
			return *this;
		}

		ostream& operator<<(char str)
		{
			printf("%c", str);
			return *this;
		}

		ostream& operator<<(int num)
		{
			printf("%d", num);
			return *this;
		}

		ostream& operator<<(double e)
		{
			printf("%g", e);
			return *this;
		}

		ostream& operator<<(ostream& (*fp)(ostream& ostm))
		{
			return fp(*this);
		}
	};


	// 원래 endl 처럼 개행하고 출력버퍼를 비우는 작업을 해 준다
	// 반환된 값을 재 반환하는 형태로 오버로딩
	ostream& endl(ostream& ostm)
	{
		ostm << '\n';
		fflush(stdout); //버퍼 비우기
		return ostm;
	}

	ostream cout; //객체의 이름. 객체 내에서 다양한 기본자료형 데이터를 대상으로 << 연산자 오버로딩을 하고 있다.

}

int main(void)
{
	// using 네임스페이스이름::요소 는
	// 네임스페이스 이름 없이도 사용하게 해줌
	// 지역적 using 선언
	using mystd::cout;
	using mystd::endl;

	cout << 3.14 << endl << 123 << endl;

	return 0;
}

 

 

<<, >> 연산자 오버로딩

<<연산자에 이어 >>연산자도 오버로딩 해 본다.

 

* 구현 전 알아야 할 것

- cout은 ostream 클래스의 객체
- ostream 은 이름공간 std 안에 선언되어 있으며, 헤더파일 <iostream>을 포함해야 한다.
- 멤버함수 형태로 오버로딩하면 cout.operator<<(pos)형태로 해석이 가능해야 한다.
- 전역함수 형태로 오버로딩하면 operator<<(cout, pos)형태로 해석이 가능해야 한다.

cout객체의 멤버함수를 추가하는 것 말고 전역함수에 의한 방법으로 구현한다.

// PointConsoleOutput

#include <iostream>
using namespace std;

class Point
{
public:
	Point(int x=0, int y=0) 
		: xpos(x),
		ypos(y)
	{	}
private:
	int xpos, ypos;
public:
	void ShowPosition() const
	{
		cout << '[' << xpos << ", " << ypos << ']' << endl;
	}
	friend ostream& operator<<(ostream&, const Point&);
};

ostream& operator<<(ostream& os, const Point& pos)
{
	os << '[' << pos.xpos << ", " << pos.ypos << ']' << endl;\
	// 인자로 전달된 cout의 참조자를 통한 출력을 구성
	return os;
}

int main(void)
{
	Point pos1(1, 3);
	cout << pos1; //ostream& operator<<(ostream& os, const Point& pos)가 호출된다
	// 연산자 우측이 Point객체
	Point pos2(101, 303);
	cout << pos2;
	return 0;
}

 

 

* 추가

- cin은 istream 클래스의 객체이다
- isteram은 이름공간 std 안에 선언되어 있으며, 헤더파일 <iostream>을 포함해야 한다.

728x90
728x90
블로그 이미지

coding-restaurant

코딩 맛집에 방문해주셔서 감사합니다.

,
728x90
728x90

기능 제공의 핵심이 되는 컨트롤 클래스를 적용한다.


컨트롤 클래스는 프로그램 전체의 기능을 담당하는 기능적 성격이 강한 클래스이다.
컨트롤 클래스만 봐도 프로그램의 전체 기능과 흐름을 파악할 수 있다.

엔터티 클래스는 컨트롤 클래스가 아닌 대부분의 클래스다.
엔터티 클래스는 데이터적 성격이 강해 파일 및 데이터 베이스에 저장되는 데이터를 소유한다.
엔터티 클래스는 프로그램 상 관리되는 데이터의 종류를 파악하는 데 도움이 된다.
엔터티 클래스는 프로그램의 기능을 파악하는데 도움을 주지는 못한다.

 

 

과거 진행한 OOP 단계별 프로젝트

[윤성우 열혈 c++] OOP 단계별 프로젝트 01단계

[윤성우 열혈 c++] OOP 단계별 프로젝트 02단계

[윤성우 열혈 c++] OOP 단계별 프로젝트 03단계

[윤성우 열혈 c++] OOP 단계별 프로젝트 04단계

 

 

프로그램 설명

프로그램의 주요기능은 계좌개설, 입금, 출금, 계좌정보 전체 출력이다. 그리고 전역함수로 구현되어 있다.
객체지향 프로그래밍을 위해 여러 기능의 전역함수들을 하나의 컨트롤 클래스로 묶어서 구현한다. 
컨트롤 클래스는 프로그램의 기능적 측면을 담당하게 되어 본래 성격에도 부합한다. 구현 방법은...

1. AccountHandler라는 이름의 컨트롤 클래스를 정의하고 앞서 정의한 전역함수들을 이 클래스의 멤버함수로 포함시킨다.
2. Account 객체의 저장을 위해 선언한 배열과 변수도 이 클래스의 멤버에 포함시킨다.
3. AccountHandler 클래스 기반으로 프로그램이 실행되도록 main 함수를 변경한다.

 

* 흰트가 필요하면 EmployeeManager1.cpp 참고 (7장 예제로 이동)

 

 

 

 

코드
//은행 계좌 관리 프로그램 5
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>

using namespace std;

const int NAME_LEN = 20; //이름의길이

//1. 계좌개설
//2. 입금
//3. 출금
//4. 계좌번호 전체 출력
//5. 프로그램 종료
enum { MAKE = 1, DEPOSIT, WITHDRAW, INQUIRE, EXIT };

/********** 엔터티 클래스 Account **********/
class Account {
private:
    int accID; //계좌 ID
    int balance; // 잔액
    char* cusName; //고객이름 
public:    
    Account(int _accID, int _balance, char* _name); //생성자
    Account(const Account& ref); //복사생성자
    int GetAccID() const;    // 계좌 ID 반환 
    void Deposit(int money); // 입금    
    int WithDraw(int money);  // 출금
    void ShowAccInfo() const;  // 개인계좌조회

    ~Account(); // 소멸자
};

// Account 생성자 
Account::Account(int _accID, int _balance, char* _name)  // 계좌개설 - ID, 고객이름, 돈
    : accID(_accID), balance(_balance)
{
    cusName = new char[strlen(_name) + 1]; //고객이름 객체 포인터로 동적할당
    strcpy(cusName, _name);
}

// 실제 호출되지는 않는 Account복사생성자
// 깊은 복사를 원칙으로 정의
Account::Account(const Account& ref)
    : accID(ref.accID), balance(ref.balance)
{
    cusName = new char[strlen(ref.cusName) + 1]; //고객이름 객체 포인터로 동적할당
    strcpy(cusName, ref.cusName);
}

// Account 소멸자
Account::~Account() // 소멸자
{
    delete[] cusName;
}

// 계좌 ID 반환
int Account::GetAccID() const  // 내부 출력문이나 값의 변동이 없는 경우 const
{
    return accID;
}

// 개인 계좌조회
void Account::ShowAccInfo() const
{
    // 내부 출력문이나 값의 변동이 없는 경우 const
    cout << "계좌ID: " << accID << endl;
    cout << "이름: " << cusName << endl;
    cout << "잔액: " << balance << endl << endl;
}

// 입출금
void Account::Deposit(int money)
{
    balance += money;
}
int Account::WithDraw(int money)
{
    if (balance < money) //잔액<출금액 
    {
        return 0;
    }
    balance -= money;
    return money;
}


/********** 컨트롤 클래스 AccountHandler **********/ 
class AccountHandler {
private:
    // 전역 변수였던 것들
    Account* accArr[100]; //Account 저장을 위한 배열
    int accNum; //저장된 Account 수    
public:
    AccountHandler();  //생성자

    // 전역함수였던 것들
    void ShowMenu(void) const; //메뉴출력
    void MakeAccount(void); //계좌개설
    void DepositMoney(void); //입금
    void WithdrawMoney(void); //출금
    void ShowAllAccInfo(void) const; //잔액조회

    ~AccountHandler(); //소멸자
};

//메뉴출력
void AccountHandler::ShowMenu(void) const {
    cout << "-----Menu-----" << endl;
    cout << "1. 계좌개설" << endl;
    cout << "2. 입금" << endl;
    cout << "3. 출금" << endl;
    cout << "4. 계좌번호 전체 출력" << endl;
    cout << "5. 프로그램 종료" << endl;
}

// AccountHandler 생성자
AccountHandler::AccountHandler() 
    : accNum(0)
{}

// AccountHandler 소멸자
AccountHandler::~AccountHandler()
{
    for (int i = 0; i < accNum; i++)
    {
        delete accArr[i];
    }

}

//계좌개설
void AccountHandler::MakeAccount(void)
{
    int id;
    char name[NAME_LEN];
    int balance;

    cout << "[계좌개설]" << endl;
    cout << "계좌ID:(숫자로 입력) "; cin >> id;
    cout << "이름: "; cin >> name;
    cout << "입금액: "; cin >> balance;
    cout << endl;

    accArr[accNum++] = new Account(id, balance, name);
}

//입금
void AccountHandler::DepositMoney(void) {
    int money;
    int id;
    cout << "[입  금]" << endl;
    cout << "계좌ID: "; cin >> id;
    cout << "입금액: "; cin >> money;


    for (int i = 0; i < accNum; i++)
    {
        if (accArr[i]->GetAccID() == id)
        {
            accArr[i]->Deposit(money);
            cout << "입금완료" << endl << endl;
            return;
        }
    }
    cout << "유효하지 않은 ID 입니다." << endl << endl;
}

//출금
void AccountHandler::WithdrawMoney(void) {
    int money;
    int id;
    cout << "[출  금]" << endl;
    cout << "계좌ID: "; cin >> id;
    cout << "출금액: "; cin >> money;

    for (int i = 0; i < accNum; i++)
    {
        if (accArr[i]->GetAccID() == id)
        {
            if (accArr[i]->WithDraw(money) == 0) //잔액보다 출금액이 크면
            {
                cout << "잔액부족" << endl << endl;
                return;
            }
            cout << "출금완료" << endl << endl;
            return;
        }
    }
    cout << "유효하지 않은 ID 입니다" << endl << endl;
}

// 전체고객 잔액조회
void AccountHandler::ShowAllAccInfo(void) const
{
    for (int i = 0; i < accNum; i++)
    {
        accArr[i]->ShowAccInfo();
    }
}


/********** 메인 클래스 AccountHandler **********/
int main()
{
    AccountHandler hdr;
    int choice;

    while (1)
    {        
        hdr.ShowMenu(); //메뉴선택모드

        cout << "선택(1~5까지의 숫자만 입력) : ";
        cin >> choice;
        cout << endl;

        switch (choice)
        {
        case MAKE:
            hdr.MakeAccount();
            break;
        case DEPOSIT:
            hdr.DepositMoney();
            break;
        case WITHDRAW:
            hdr.WithdrawMoney();
            break;
        case INQUIRE:
            hdr.ShowAllAccInfo();
            break;
        case EXIT:
            return 0;
        default:
            cout << "잘못된 선택" << endl;
        }
    }
    return 0;
}

 

 

 

728x90
728x90
블로그 이미지

coding-restaurant

코딩 맛집에 방문해주셔서 감사합니다.

,
728x90
728x90

프로젝트에 const 선언을 추가해서 코드의 안전성을 높인다.

 

과거 진행한 OOP 단계별 프로젝트

[윤성우 열혈 c++] OOP 단계별 프로젝트 01단계

[윤성우 열혈 c++] OOP 단계별 프로젝트 02단계

[윤성우 열혈 c++] OOP 단계별 프로젝트 03단계

 

 

프로그램 설명

Account 클래스의 멤버함수 중 일부를 const로 선언한다.
const이 선언 가능한 모든 멤버함수를 const로 선언한다.

//은행 계좌 관리 프로그램 4
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>

using namespace std;

const int NAME_LEN = 20; //이름의길이

void ShowMenu(void); //메뉴출력
void MakeAccount(void); //계좌개설
void DepositMoney(void); //입금
void WithdrawMoney(void); //출금
void ShowAllAccInfo(void); //잔액조회

//1. 계좌개설
//2. 입금
//3. 출금
//4. 계좌번호 전체 출력
//5. 프로그램 종료
enum { MAKE = 1, DEPOSIT, WITHDRAW, INQUIRE, EXIT };

class Account {
private:
    int accID; //계좌 ID
    int balance; // 잔액
    char* cusName; //고객이름 
public:
    // 생성자, 소멸자
    Account(int _accID, int _balance, char* _name)  // 계좌개설 - ID, 고객이름, 돈
        : accID(_accID), balance(_balance)
    {
        cusName = new char[strlen(_name) + 1]; //고객이름 객체 포인터로 동적할당
        strcpy(cusName, _name);
    }

    // 실제 호출되지는 않는 복사생성자
    // 깊은 복사를 원칙으로 정의
    Account(const Account& ref)
        : accID(ref.accID), balance(ref.balance)
    {
        cusName = new char[strlen(ref.cusName) + 1]; //고객이름 객체 포인터로 동적할당
        strcpy(cusName, ref.cusName);
    }

    ~Account()
    {
        delete[]cusName;
    }

    // ID     
    int GetAccID() const  // 내부 출력문이나 값의 변동이 없는 경우 const
    {
        return accID;
    }

    // 입금
    void Deposit(int money)
    {
        balance += money;
    }

    // 출금
    int WithDraw(int money) {
        if (balance < money) //잔액<출금액 
        {
            return 0;
        }
        balance -= money;
        return money;
    }

    // 개인 계좌조회
    void ShowAccInfo() const { // 내부 출력문이나 값의 변동이 없는 경우 const
        cout << "계좌ID: " << accID << endl;
        cout << "이름: " << cusName << endl;
        cout << "잔액: " << balance << endl << endl;
    }
};

Account* accArr[100]; //Account 저장을 위한 배열
int accNum = 0; //저장된 Account 수

int main()
{
    int choice;

    while (1)
    {
        ShowMenu();

        cout << "선택(1~5까지의 숫자만 입력) : ";
        cin >> choice;
        cout << endl;

        switch (choice)
        {
        case MAKE:
            MakeAccount();
            break;
        case DEPOSIT:
            DepositMoney();
            break;
        case WITHDRAW:
            WithdrawMoney();
            break;
        case INQUIRE:
            ShowAllAccInfo();
            break;
        case EXIT:
            return 0;
        default:
            cout << "잘못된 선택" << endl;
        }
    }
    return 0;
}

//메뉴출력
void ShowMenu(void) {
    cout << "-----Menu-----" << endl;
    cout << "1. 계좌개설" << endl;
    cout << "2. 입금" << endl;
    cout << "3. 출금" << endl;
    cout << "4. 계좌번호 전체 출력" << endl;
    cout << "5. 프로그램 종료" << endl;
}

//계좌개설
void MakeAccount(void)
{
    int id;
    char name[NAME_LEN];
    int balance;

    cout << "[계좌개설]" << endl;
    cout << "계좌ID:(숫자로 입력) "; cin >> id;
    cout << "이름: "; cin >> name;
    cout << "입금액: "; cin >> balance;
    cout << endl;

    accArr[accNum++] = new Account(id, balance, name);
}

//입금
void DepositMoney(void) {
    int money;
    int id;
    cout << "[입  금]" << endl;
    cout << "계좌ID: "; cin >> id;
    cout << "입금액: "; cin >> money;


    for (int i = 0; i < accNum; i++)
    {
        if (accArr[i]->GetAccID() == id)
        {
            accArr[i]->Deposit(money);
            cout << "입금완료" << endl << endl;
            return;
        }
    }
    cout << "유효하지 않은 ID 입니다." << endl << endl;
}

//출금
void WithdrawMoney(void) {
    int money;
    int id;
    cout << "[출  금]" << endl;
    cout << "계좌ID: "; cin >> id;
    cout << "출금액: "; cin >> money;


    for (int i = 0; i < accNum; i++)
    {
        if (accArr[i]->GetAccID() == id)
        {
            if (accArr[i]->WithDraw(money) == 0) //잔액보다 출금액이 크면
            {
                cout << "잔액부족" << endl << endl;
                return;
            }
            cout << "출금완료" << endl << endl;
            return;
        }
    }
    cout << "유효하지 않은 ID 입니다" << endl << endl;
}

// 전체고객 잔액조회
void ShowAllAccInfo(void)
{
    for (int i = 0; i < accNum; i++)
    {
        accArr[i]->ShowAccInfo();
    }
}

 

const로 선언된 함수는 const 객체를 대상으로 호출이 가능하다.
멤버변수에 저장된 값을 수정하지 않는 함수는 가급적 const로 선언한다.
내부 출력문이 있는 함수는 무조건 const를 붙여준다.
const 선언유무도 함수 오버로딩의 조건에 해당된다.

728x90
728x90
블로그 이미지

coding-restaurant

코딩 맛집에 방문해주셔서 감사합니다.

,
728x90
728x90
09-1 멤버함수와 가상함수의 동작원리

앞의 내용에서는 객체 내에 멤버함수가 존재한다고 했고 앞으로도 객체 그림을 그릴 때 그렇게 그려도 무방하지만,
(객체지향적 논리로는 이렇게 이해하는 게 옳다) 사실은 멤버함수가 객체 안에 존재하지 않는다.

 

ReObjUnder1.cpp

객체의 멤버함수와 멤버변수가 어떠한 형태로 구성되는지 설명하기 위해
모델이 되는 C++ 코드를 보고 C언어 스타일의 구조체와 전역함수를 이용해서 구현해본다.

 

// ReObjUnder1.cpp

#include <iostream>
using namespace std;

class Data
{
private:
    int data;
public:
    Data(int num) :data(num)
    {    }
    void ShowData()
    {
        cout << "Data: " << data << endl;
    }
    void Add(int num)
    {
        data += num;
    }
};

int main()
{
    Data obj(15);
    obj.Add(17);
    obj.ShowData();
    return 0;
}

 

 

* 함수 포인터 (참고)

 

 ReObjUnder2.cpp

C언어 스타일의 구조체와 전역함수를 이용해서 구현

// ReObjUnder2.cpp

#include <iostream>
using namespace std;

typedef struct Data
{
    int data;

    // 함수포인터 변수가 구조체의 멤버로 등장
    // 반환형식 (*식별자) (파마리터형 목록)
    void (*ShowData)(Data*);     
    // ShowData 함수의 주소값을 저장하기 위한 것

    void (*Add)(Data*, int); 
    // Add 함수의 주소값을 저장하기 위한 것
} Data;

void ShowData(Data* THIS) 
{
    cout << "Data: " << THIS->data << endl;
}

void Add(Data* THIS, int num)
{
    THIS->data += num;
}

int main()
{
    Data obj1 = { 15, ShowData, Add };
    Data obj2 = { 7, ShowData, Add };
    // 두 객체는 ShowData, Add 함수를 공유하는 것과 같다
    // 두 함수를 이용해 멤버인 함수 포인터 변수를 초기하기 떄문에

    obj1.Add(&obj1, 17);    //멤버함수를 호출하는 것과 유사
    obj2.Add(&obj2, 9);
    obj1.ShowData(&obj1);
    obj2.ShowData(&obj2);

    return 0;
}

 

 

두 개의 구조체 변수(객체)가 함수를 공유하고 있다. 실제로 C++의 객체와 멤버함수는 이런 관계를 갖는다.
객체가 생성되면 멤버변수는 객체 내에 존재하지만, 멤버함수는 메모리의 한 공간에 별도로 위치하고선 이 함수가 정의된 클래스의 모든 객체가 공유한다.

그리고 객체가 지니고 있는 멤버변수 대상의 연산이 진행되도록 함수를 호출한다; 

 

 

가상함수의 동작원리와 가상함수 테이블

가상함수의 동작원리를 이해하면 C++이 C보다 느린 이유를 유추 가능하다.

한 개 이상의 가상함수를 포함하는 클래스에 대해서 컴파일러는 가상함수 테이블(V-Table, Virtual Table)이라는 것을 만든다. 가상함수 테이블은 실제 호출되어야 할 함수의 위치정보를 담고 있는 테이블이다.
AAA 객체의 Func1 함수를 호출해야할 경우 테이블을 참조하여 0x1024번지에 등록된 Func1()를 호출한다.

* key는 호출하고자 하는 함수를 구분지어준다 (구분자)
* value는 구분자에 해당하는 함수의 주소정보를 알려준다.

오버라이딩 된 가상함수의 주소정보는 유도클래스의 가상함수 테이블에 포함되지 않는다.
=> 오버라이딩 된 가상함수를 호출하면 무조건 가장 마지막에 오버라이딩한 유도클래스의 멤버함수가 호출

 

// VirtualInternal.cpp

#include <iostream>
using namespace std;

class AAA
{
private:
	int num1;
public:
	// 한 개 이상의 가상함수를 포함하는 클래스에 대해서
	// 컴파일러는 가상함수 테이블(V-Table, Virtual Table)이라는 것을 만든다.
	// : 실제 호출되어야 할 함수의 위치정보를 담고 있는 테이블

	// [ AAA 클래스의 가상함수 테이블 ]
	// key						value
	// ------------------------------------------
	// void AAA::Func1()		0x1024번지
	// void AAA::Func2()		0x2048번지

	// * key는 호출하고자 하는 함수를 구분지어준다 (구분자)
	// * value는 구분자에 해당하는 함수의 주소정보를 알려준다.

	// AAA 객체의 Func1 함수를 호출해야할 경우
	// 테이블을 참조하여 0x1024번지에 등록된 Func1()를 호출

	virtual void Func1() {
		cout << "Func1" << endl;
	}
	virtual void Func2() {
		cout << "Func2" << endl;
	}
};

class BBB: public AAA
{
private:
	int num2;
public:

	// [ BBB 클래스의 가상함수 테이블 ]
	// key						value
	// ------------------------------------------
	// void BBB::Func1()		0x3072번지
	// void AAA::Func2()		0x2048번지
	// void BBB::Func3()		0x4096번지

	// AAA클래스의 오버라이딩 된 가상함수 Func1에 대한 정보가 없다.

	virtual void Func1() {
		cout << "BBB::Func1" << endl;
	}
	void Func3()
	{
		cout << "Func3" << endl;
	}
};

int main(void)
{
	AAA* aptr = new AAA();
	aptr->Func1();

	BBB* bptr = new BBB();
	bptr->Func1();

	return 0;
}

 

 

 

가상함수 테이블이 참조되는 방식

메인함수 호출 이전에 다음 구조로 가상함수 테이블이 메모리공간에 할당된다.

가상함수 테이블은 객체의 생성과 상관없이 메모리 공간에 할당된다. ***
가상함수 테이블이 멤버함수의 호출에 사용되는 데이터 중 하나이기 때문에.

 

 

메인함수가 호출되어 객체가 생성되고 나면 다음 구조로 참조 관계를 구성한다.
AAA 객체에는 AAA 클래스의 가상함수 테이블의 주소값이, BBB 객체에는 BBB 클래스의 가상함수 테이블의 주소값이 저장된다. (내부적으로 참조되는 주소값이지 우리가 어찌할 수는 없다)

AAA 객체를 통해 Func1 함수가 호출되었다고 가정하면 Func1 함수의 위치 확인을 위해서 AAA 클래스의 가상함수 테이블이 참조되고, 0x1024번지에 위치한 함수가 실행된다. BBB객체의 Func1함수가 호출되면 BBB클래스의 가상함수테이블이 참조되고, 테이블의 위치에 0x3072번지로 기록되어 있으므로 그곳에 위치한 BBB 클래스의 Func1 함수가 실행된다.

오버라이딩 된 AAA 클래스의 Func1 함수에 대한 정보가 없으므로 BBB 클래스의 Func1 함수가 대신 호출된다.

클래스에 가상함수가 포함되면 가상함수 테이블이 생성되고, 테이블을 참조하여 호출할 함수가 결정되기 때문에 실행속도가 감소하는 단점이 있지만 그 속도차는 미미하여 장점이 더 많은 가상함수이다.

 

 

09-2 다중상속에 대한 이해

다중상속은 둘 이상의 클래스를 동시 상속하는 것이다.

다중상속의 기본방법
// MultiInheri1.cpp

#include <iostream>
using namespace std;

class BaseOne
{
public:
	void SimpleFuncOne()
	{
	cout << "BaseOne" << endl;
	}
};

class BaseTwo
{
public:
	void SimpleFuncTwo()
	{
		cout << "BaseTwo" << endl;
	}
};

// 다중상속
class MultiDerived : public BaseOne, BaseTwo
{
public:
	void ComplexFunc()
	{
		SimpleFuncOne();
		SimpleFuncTwo();
	}
};

int main(void)
{
	MultiDerived mdr;
	mdr.ComplexFunc();

	return 0;
}

 

 

다중상속의 모호성

두 기초 클래스에 동일한 이름의 멤버가 존재할 경우 문제의 소지가 있다.

// MultiInheri2.cpp

#include <iostream>
using namespace std;

class BaseOne
{
public:
	void SimpleFunc()
	{
	cout << "BaseOne" << endl;
	}
};

class BaseTwo
{
public:
	void SimpleFunc()
	{
		cout << "BaseTwo" << endl;
	}
};

// 다중상속
class MultiDerived : public BaseOne, protected BaseTwo
{
public:
	void ComplexFunc()
	{
		// 어느 클래스에 정의된 함수의 호출을 원하는 지 명시해야 한다.
		BaseOne::SimpleFunc();
		BaseTwo::SimpleFunc();
	}
};

int main(void)
{
	MultiDerived mdr;
	mdr.ComplexFunc();

	return 0;
}

 

 

가상상속
//은행 계좌 관리 프로그램 3
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
//#include <cstring>
 
using namespace std;

 
/***** class Base ****/
class Base //부모클래스
{
public:
	Base(); //생성자
	void SimpleFunc(); //멤버함수
};
Base::Base()
{
	cout << "Base Constructor" << endl;
}
void Base::SimpleFunc()
{
	cout << "BaseOne" << endl;
}


/***** class MiddleDerivedOne ****/
class MiddleDerivedOne :virtual public Base //가상상속으로 LastDerived 객체 생성 시 Base가 한 번만 호출
{
public:
	MiddleDerivedOne(); //생성자
	void MiddleFuncOne(); //멤버함수
};
MiddleDerivedOne::MiddleDerivedOne() : Base() 
//기초 클래스의 생성자 호출을 명시하지 않아도 되나 
//기초 클래스의 생성자가 호출됨을 강조하기 위해 별도로 :Base()라고 명시
{
	cout << "MiddleDerivedOne Constructor" << endl;
}

void MiddleDerivedOne::MiddleFuncOne()	// 멤버함수
{
	SimpleFunc();
	cout << "MiddleDerivedOne" << endl;
}


/***** class MiddleDerivedTwo ****/
class MiddleDerivedTwo :
	virtual public Base
{
public:
	MiddleDerivedTwo(); //생성자
	void MiddleFuncTwo(); //멤버함수
};
MiddleDerivedTwo::MiddleDerivedTwo() : Base() //기초 클래스의 생성자가 호출됨을 강조하기 위해 별도로 :Base()라고 명시
{
	cout << "MiddleDerivedTwo Constructor" << endl;
}
void MiddleDerivedTwo::MiddleFuncTwo()
{
	SimpleFunc();
	cout << "MiddleDerivedTwo" << endl;
}


/***** class LastDerived ****/
class LastDerived
	: public MiddleDerivedOne, public MiddleDerivedTwo
{
	// 가상으로 Base 클래스를 상속하는 두 클래스를 다중으로 상속한다

public:
	LastDerived();
	//: MiddleDerivedOne(), MiddleDerivedTwo();
	void ComplexFunc();
};
LastDerived::LastDerived()
	: MiddleDerivedOne(), MiddleDerivedTwo()  //생성자
{
	cout << "LastDerived Constructor" << endl;
}
void LastDerived::ComplexFunc() //멤버함수
{
	MiddleFuncOne();
	MiddleFuncTwo();
	SimpleFunc(); // 가상상속으로 하나만 존재한다 
}


/***** main ****/
int main()
{
	cout << "객체생성 전....." << endl;
	LastDerived ldr;

	cout << "객체생성 후....." << endl;
	ldr.ComplexFunc();

	return 0;
}

 

 

Q. 헤더와 cpp 파일분할로 분할하면 오류가 생긴다 ---- 질문

 

 

LastDerived 클래스가 Base클래스를 간접적으로 두 번 상속하는 구조이다.
virtual 키워드가 없으면 SimpleFunc()를 호출 시 어느 클래스를 통해서 간접 상속한 Base 클래스의 멤버함수를 호출할 지 명시해야 한다. 

 

MiddleDerivedOne::SimpleFunc();

 

Base 클래스의 멤버가 LastDerived 객체에 하나씩만 존재하는 게 타당한 경우가 대부분이고,
MiddleDerivedOne::SimpleFunc();보다 Base 클래스가 딱 한번만 상속하는 게 더 현실적인 해결책이며
그를 위한 문법이 가상상속이다.

실제로 Base 클래스의 생성자도 한번만 호출된다. 가상상속을 하지 않으면 Base 클래스의 생성자는 두 번 호출된다. 

 

 

728x90
728x90
블로그 이미지

coding-restaurant

코딩 맛집에 방문해주셔서 감사합니다.

,

가상함수 테이블

C, C++ 2021. 11. 1. 17:53
728x90
728x90

정적바인딩 - 일반 함수 호출은 컴파일타임에 호출할 함수의 주소를 안다.
동적바인딩 - 가상 함수 호출은 런타임에 호출할 함수의 주소를 안다

 

가상함수 테이블은 각 클래스마다 있는 것이며
각 클래스가 호출해야 할 함수의 주소 정보가 이미 정해져 있다.

가상함수 테이블은 각 객체마다 가지고 있는 게 아니며
각 객체마다 갖고 있는 것은 가상함수 테이블 포인터이다.

 

 

 

728x90
728x90
블로그 이미지

coding-restaurant

코딩 맛집에 방문해주셔서 감사합니다.

,
728x90
728x90

Account 클래스에 깊은 복사를 진행하는 복사 생성자를 정의해본다. 깊은 복사를 원칙으로 한다면 클래스의 생성자만 봐도 복사 생성자의 필요성을 판단할 수 있다. 실제 복사 생성자의 호출여부는 중요하지 않으며 클래스는 그 자체로 완성품이 되어야 하기 때문에 당장 필요한 것만으로 채우지 않는다.

 

과거 진행한 OOP 단계별 프로젝트

[윤성우 열혈 c++] 01-6 OOP 단계별 프로젝트 01단계

[윤성우 열혈 c++] OOP 단계별 프로젝트 02단계

 

 

프로그램 설명

Account 클래스에 깊은 복사를 진행하는 복사 생성자를 정의해본다.
복사생성자가 호출되진 않지만 깊은 복사를 원칙으로 정하고, 이를 위한 복사생성자를 추가한다.

 

//은행 계좌 관리 프로그램 3
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>

using namespace std;

const int NAME_LEN = 20; //이름의길이

void ShowMenu(void); //메뉴출력
void MakeAccount(void); //계좌개설
void DepositMoney(void); //입금
void WithdrawMoney(void); //출금
void ShowAllAccInfo(void); //잔액조회

//1. 계좌개설
//2. 입금
//3. 출금
//4. 계좌번호 전체 출력
//5. 프로그램 종료
enum { MAKE = 1, DEPOSIT, WITHDRAW, INQUIRE, EXIT };

class Account {
private:
    int accID; //계좌 ID
    int balance; // 잔액
    char* cusName; //고객이름 
public:
    // 생성자, 소멸자
    Account(int _accID, int _balance, char* _name)  // 계좌개설 - ID, 고객이름, 돈
        : accID(_accID), balance(_balance)
    {
        cusName = new char[strlen(_name) + 1]; //고객이름 객체 포인터로 동적할당
        strcpy(cusName, _name);
    }

    // 실제 호출되지는 않는 복사생성자
    // 깊은 복사를 원칙으로 정의
    Account(const Account& ref)
        : accID(ref.accID), balance(ref.balance)
    {
        cusName = new char[strlen(ref.cusName) + 1]; //고객이름 객체 포인터로 동적할당
        strcpy(cusName, ref.cusName);
    }

    ~Account()
    {
        delete []cusName;
    }

    // ID
    int GetAccID()
    {
        return accID;
    }

    // 입금
    void Deposit(int money)
    {
        balance += money;
    }

    // 출금
    int WithDraw(int money) {
        if (balance < money) //잔액<출금액 
        {
            return 0;
        }
        balance -= money;
        return money;
    }

    // 개인 계좌조회
    void ShowAccInfo() {
        cout << "계좌ID: " << accID << endl;
        cout << "이름: " << cusName << endl;
        cout << "잔액: " << balance << endl << endl;
    }
};

Account* accArr[100]; //Account 저장을 위한 배열
int accNum = 0; //저장된 Account 수

int main()
{
    int choice;

    while (1)
    {
        ShowMenu();

        cout << "선택(1~5까지의 숫자만 입력) : ";
        cin >> choice;
        cout << endl;

        switch (choice)
        {
        case MAKE:
            MakeAccount();
            break;
        case DEPOSIT:
            DepositMoney();
            break;
        case WITHDRAW:
            WithdrawMoney();
            break;
        case INQUIRE:
            ShowAllAccInfo();
            break;
        case EXIT:
            return 0;
        default:
            cout << "잘못된 선택" << endl;
        }
    }
    return 0;
}

//메뉴출력
void ShowMenu(void) {
    cout << "-----Menu-----" << endl;
    cout << "1. 계좌개설" << endl;
    cout << "2. 입금" << endl;
    cout << "3. 출금" << endl;
    cout << "4. 계좌번호 전체 출력" << endl;
    cout << "5. 프로그램 종료" << endl;
}

//계좌개설
void MakeAccount(void)
{
    int id;
    char name[NAME_LEN];
    int balance;

    cout << "[계좌개설]" << endl;
    cout << "계좌ID:(숫자로 입력) "; cin >> id;
    cout << "이름: "; cin >> name;
    cout << "입금액: "; cin >> balance;
    cout << endl;

    accArr[accNum++] = new Account(id, balance, name);
}

//입금
void DepositMoney(void) {
    int money;
    int id;
    cout << "[입  금]" << endl;
    cout << "계좌ID: "; cin >> id;
    cout << "입금액: "; cin >> money;


    for (int i = 0; i < accNum; i++)
    {
        if (accArr[i]->GetAccID() == id)
        {
            accArr[i]->Deposit(money);
            cout << "입금완료" << endl << endl;
            return;
        }
    }
    cout << "유효하지 않은 ID 입니다." << endl << endl;
}

//출금
void WithdrawMoney(void) {
    int money;
    int id;
    cout << "[출  금]" << endl;
    cout << "계좌ID: "; cin >> id;
    cout << "출금액: "; cin >> money;


    for (int i = 0; i < accNum; i++)
    {
        if (accArr[i]->GetAccID() == id)
        {
            if (accArr[i]->WithDraw(money) == 0) //잔액보다 출금액이 크면
            {
                cout << "잔액부족" << endl << endl;
                return;
            }
            cout << "출금완료" << endl << endl;
            return;
        }
    }
    cout << "유효하지 않은 ID 입니다" << endl << endl;
}

// 전체고객 잔액조회
void ShowAllAccInfo(void)
{
    for (int i = 0; i < accNum; i++)
    {
        accArr[i]->ShowAccInfo();
    }
}

 

728x90
728x90
블로그 이미지

coding-restaurant

코딩 맛집에 방문해주셔서 감사합니다.

,
728x90
728x90

무엇을 클래스로 만들 것인지, 어떻게 클래스를 정의할 것인지를 더 신경을 써야 한다.
적어도 클래스를 제대로 만들어 낼 수 있어야 무리 없이 실무 프로젝트를 진행할 수 있다.

 

과거 진행한 OOP 단계별 프로젝트 01단계

[윤성우 열혈 c++] 01-6 OOP 단계별 프로젝트 01단계

 

 

프로그램 설명

1. 아래 내용을 고민하면서 1단계에서 정의한 구조체 Account를 클래스로 변경한다.

1) 어떻게 캡슐화를 시키고 정보를 은닉시켜야 할까?
2) 생성자와 소멸자는 어떻게 정의해야 할까?

 

2. 고객의 이름을 동적 할당의 형태로 구현한다. 

1) Account 클래스는 멤버변수로 문자열 포인터(char형 포인터)를 갖고 있어야 한다.

 

3. 객체를 저장하는 배열을 구현할 때 객체 배열 말고 객체 포인터 배열을 선언해서 객체를 저장한다.

1) 구조체 배열을 포인터 배열로 변경한다.

 

 

//은행 계좌 관리 프로그램 2
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>

using namespace std;

const int NAME_LEN = 20; //이름의길이

void ShowMenu(void); //메뉴출력
void MakeAccount(void); //계좌개설
void DepositMoney(void); //입금
void WithdrawMoney(void); //출금
void ShowAllAccInfo(void); //잔액조회

//1. 계좌개설
//2. 입금
//3. 출금
//4. 계좌번호 전체 출력
//5. 프로그램 종료
enum { MAKE = 1, DEPOSIT, WITHDRAW, INQUIRE, EXIT };

class Account {
private:
    int accID; //계좌 ID
    int balance; // 잔액
    char* cusName; //고객이름 
public:
    // 생성자, 소멸자
    Account(int _accID, int _balance, char* _name)  // 계좌개설 - ID, 고객이름, 돈
        : accID(_accID), balance(_balance)
    {
        cusName = new char[strlen(_name) + 1]; //고객이름 객체 포인터로 동적할당
        strcpy(cusName, _name);
    }
    ~Account()
    {
        delete[]cusName;
    }

    // ID
    int GetAccID() 
    { 
        return accID; 
    }

    // 입금
    void Deposit(int money)
    {        
        balance = balance + money;
    }

    // 출금
    int WithDraw(int money) {
        if (balance < money) //잔액<출금액 
        {
            return 0;
        }
        balance -= money;
        return money;
    }

    // 개인 계좌조회
    void ShowAccInfo() {
        cout << "계좌ID: " << accID << endl;
        cout << "이름: " << cusName << endl;
        cout << "잔액: " << balance << endl << endl;
    }
};

Account* accArr[100]; //Account 저장을 위한 배열
int accNum = 0; //저장된 Account 수

int main()
{
    int choice;

    while (1)
    {
        ShowMenu();

        cout << "선택(1~5까지의 숫자만 입력) : ";
        cin >> choice;
        cout << endl;

        switch (choice)
        {
        case MAKE:
            MakeAccount();
            break;
        case DEPOSIT:
            DepositMoney();
            break;
        case WITHDRAW:
            WithdrawMoney();
            break;
        case INQUIRE:
            ShowAllAccInfo();
            break;
        case EXIT:
            return 0;
        default:
            cout << "잘못된 선택" << endl;
        }
    }
    return 0;
}

//메뉴출력
void ShowMenu(void) {
    cout << "-----Menu-----" << endl;
    cout << "1. 계좌개설" << endl;
    cout << "2. 입금" << endl;
    cout << "3. 출금" << endl;
    cout << "4. 계좌번호 전체 출력" << endl;
    cout << "5. 프로그램 종료" << endl;
}

//계좌개설
void MakeAccount(void) 
{
    int id;
    char name[NAME_LEN];
    int balance;

    cout << "[계좌개설]" << endl;
    cout << "계좌ID:(숫자로 입력) "; cin >> id;
    cout << "이름: "; cin >> name;
    cout << "입금액: "; cin >> balance;
    cout << endl;

    accArr[accNum++] = new Account(id, balance, name);
}

//입금
void DepositMoney(void) {
    int money;
    int id;
    cout << "[입  금]" << endl;
    cout << "계좌ID: "; cin >> id;
    cout << "입금액: "; cin >> money;


    for (int i = 0; i < accNum; i++)
    {
        if (accArr[i]->GetAccID() == id)
        {
            accArr[i]->Deposit(money);
            cout << "입금완료" << endl << endl;
            return;
        }
    }
    cout << "유효하지 않은 ID 입니다." << endl << endl;    
}

//출금
void WithdrawMoney(void) {
    int money;
    int id;
    cout << "[출  금]" << endl;
    cout << "계좌ID: "; cin >> id;
    cout << "출금액: "; cin >> money;


    for (int i = 0; i < accNum; i++)
    {
        if (accArr[i]->GetAccID() == id)
        {
            if (accArr[i]->WithDraw(money) == 0) //잔액보다 출금액이 크면
            {
                cout << "잔액부족" << endl << endl;
                return;
            }            
            cout << "출금완료" << endl << endl;
            return;
        }
    }
    cout << "유효하지 않은 ID 입니다" << endl << endl; 
}

// 전체고객 잔액조회
void ShowAllAccInfo(void)
{
    for (int i = 0; i < accNum; i++)
    {
        accArr[i]->ShowAccInfo();
    }
}

 

728x90
728x90
블로그 이미지

coding-restaurant

코딩 맛집에 방문해주셔서 감사합니다.

,
728x90
728x90
9-1 객체의 멤버 함수가 사실은 어디에 있는가

* 예제를 이해하기 위한 사전지식 : 함수포인터

 

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;

class A
{
	int n;

public:
	A(int n) {
		this->n = n;  // 인덱스 단원 - this포인터 복습
	}
	void add() {
		n++;  // n을 증가
	}
};

int main()
{
	A a1(10);
	A a2(20);
	a1.add();
	a2.add();
	return 0;
}

 

모든 a클래스 객체가 하나의 add함수를 공유 (코드 영역 안에 있는 add 함수를 공유한다.)
그렇지만 각각 객체의 n은 구분한다. 어떻게?

 

void add(A* a) { (a->n)++; }


이렇게 변경된다. 자기자신의 주소값 this를 인자값으로 전달한다...

 

 

9-2 가상 함수가 동작하는 원리

* 예제를 이해하기 위한 사전지식 : 가상함수 정의, 장점

 

class A
{
    int a;
    int b;
public:
    virtual void fct1(){
        cout<<"fct1(...)"<<endl;  // 멤버함수는 코드영역으로 들어가 공유된다
    }
    virtual void fct2(){  
    // 클래스의 멤버함수 중 하나라도 virtual로 선언된 멤버함수가 있으면
    // 그 클래스의 멤버함수 정보를 지니는 a 클래스 객체의 virtual 테이블이 생성된다.
    // a 클래스 객체의 함수를 호출할 때마다 virtual 테이블을 참조하게 된다
    // 위치에 대한 정보를 갖고 있다    
        cout<<"fct2(...)"<<endl;
    }	
};

class B : public A
{
    int c;
    int d;
public:
    virtual void fct1(){  // 오버라이딩 + 재정의
        cout<<"overriding fct1(...)"<<endl;
    }
    void fct3(){
        cout<<"fct3(...)"<<endl;
    }
};

 

클래스의 멤버함수 중 하나라도 virtual로 선언된 멤버함수가 있으면
그 클래스의 멤버함수 정보를 지니는 a 클래스 객체의 virtual 테이블이 생성된다.
a 클래스 객체의 함수를 호출할 때마다 virtual 테이블을 참조하게 된다
위치에 대한 정보를 갖고 있다

int main()
{
	A a;
	a.fct();
}

 

호출하러 들어오면 테이블의 키값을 찾는다
직접가는 게 아니라 거쳐가게 된다

 

 

virtual 테이블에 의해 전에 배웠던 기능이 구현되었던 것!

 

//은행 계좌 관리 프로그램 3
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>

using namespace std;

class A
{
    int a;
    int b;
public:
    virtual void fct1() {
        cout << "fct1(...)" << endl;  // 멤버함수는 코드영역으로 들어가 공유된다
    }
    virtual void fct2() {
        // 클래스의 멤버함수 중 하나라도 virtual로 선언된 멤버함수가 있으면
        // 그 클래스의 멤버함수 정보를 지니는 a 클래스 객체의 virtual 테이블이 생성된다.
        // a 클래스 객체의 함수를 호출할 때마다 virtual 테이블을 참조하게 된다
        // 위치에 대한 정보를 갖고 있다    
        cout << "fct2(...)" << endl;
    }
};

class B : public A
{
    int c;
    int d;
public:
    virtual void fct1() {  // 오버라이딩 + 재정의
        cout << "overriding fct1(...)" << endl;
    }
    void fct3() {
        cout << "fct3(...)" << endl;
    }
};
 
// 오버라이딩 된 virtual은 테이블에 정보가 없어서 호출이 되지 않는다
// 

int main(void)
{
    A* aaa = new A();
    aaa->fct1();

    B* bbb = new B();
    bbb->fct1();

    return 0;
}

 

 

 

 

 

가상함수 단점

메모리공간 소모
함수호출이 늦어진다

=> 그러나 장점이 훨씬 크다!
virtual 테이블의 개념까지 정리해보자

 

 

9-3 다중 상속에 대한 이해
다중상속 : 둘 이상의 클래스를 동시에 상속하는 것

 

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>

using namespace std;

class AAA {
public:
    void String1() {
        cout << "AAA::String1" << endl;
    }
};

class BBB {
public:
    void String2() {
        cout << "BBB::String2" << endl;
    }
};

class CCC : public AAA, public BBB {
public:
    void ShowString() {
        String1();
        String2();
    }
};

int main(void)
{
    CCC ccc;
    ccc.ShowString();

    return 0;
}

 

 

9-4 다중 상속의 모호성
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>

using namespace std;

class AAA {
public:
    void String() {
        cout << "AAA::String1" << endl;
    }
};

class BBB {
public:
    void String() {
        cout << "BBB::String2" << endl;
    }
};

class CCC : public AAA, public BBB {
public:
    void ShowString() {
        String();
        String(); 
    }
};


int main(void)
{
    CCC ccc;
    ccc.ShowString();

    return 0;
}

 

 

9-5 virtual base 클래스
//은행 계좌 관리 프로그램 3
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>

using namespace std;

class AAA {
public:
    void String1() {
        cout << "AAA::String" << endl;
    }
};

class BBB : public AAA {
public:
    void String2() {
        cout << "BBB::String" << endl;
    }
};

class CCC : public AAA {
public:
    void String3() {
        cout << "CCC::String" << endl;
    }
};

class DDD : public BBB, public CCC //다중상속
{
public:
    void ShowString() {
        String1();
        String2();
        String3();
    }
};

int main(void)
{
    DDD ddd;
    ddd.ShowString();
    return 0;
}

 

 

virtual base 클래스

 A 클래스 멤버를 하나만 포함할 수 있게 만든 것.
B와 C클래스가 A클래스를 virtual로 상속을 하면 D가 상속하는 A클래스는 한번만 상속되는 규칙

=> 웬만하면 사용을 피할 것.

 

//은행 계좌 관리 프로그램 3
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <cstring>

using namespace std;

class AAA {
public:
    void String1() {
        cout << "AAA::String" << endl;
    }
};

class BBB : virtual public AAA {
public:
    void String2() {
        cout << "BBB::String" << endl;
    }
};

class CCC : virtual public AAA {
public:
    void String3() {
        cout << "CCC::String" << endl;
    }
};

// virtual을 넣으면 A클래스를 한번만 상속하게 된다
class DDD : public BBB, public CCC //다중상속
{
public:
    void ShowString() {
        //CCC::String1();
        String1();
        String2();
        String3();
    }
};

int main(void)
{
    DDD ddd;
    ddd.ShowString();
    return 0;
}

 

728x90
728x90
블로그 이미지

coding-restaurant

코딩 맛집에 방문해주셔서 감사합니다.

,
728x90
728x90
728x90
728x90
블로그 이미지

coding-restaurant

코딩 맛집에 방문해주셔서 감사합니다.

,
728x90
728x90
상속받는 클래스가 존재하는 경우 가상 소멸자를 사용하는 것이 중요하다

 

생성자는 부모 클래스의 생성자가 먼저 불려지고,
소멸자는 자식 클래스의 소멸자가 먼저 불려지고 나서 부모 클래스의 소멸자가 불려진다. 

다형성을 위해 부모 클래스의 포인터로부터 자식 클래스를 호출할 때
가상 함수로 정의되지 않은 자식 클래스의 오버라이딩된 함수를 호출하면 부모 클래스의 멤버 함수가 호출된다
소멸자도 자식클래스에서 오버라이딩된 함수의 일종이라 부모포인터로 객체를 삭제하면 부모클래스 소멸자가 호출된다

 

소멸자가 가상함수로 안 선언되어 있으면 자식클래스의 소멸자는 호출되지 않고
가상함수 키워드 virtual이 사용되었으면 자식 클래스에서 재정의될 수 있음을 뜻하여
포인터 종류에 상관없이 항상 자식 클래스의 메서드가 호출된다.
=> 자식클래스의 소멸자가 호출되고나서 부모클래스의 소멸자가 호출된다.

상속관계가 있는 클래스고 소멸자에서 리소스를 해제해야 하는 경우,
반드시 부모 클래스 안의 소멸자를 가상함수로 선언해야 한다.

 

 

클래스의 virtual 소멸자 예제

소멸자 앞에 virtual이 붙어있어 동적바인딩이 된다.
자바는 기본이 동적바인딩이만, C++은 기본이 정적바인딩이여서 virtual 키워드를 사용해야 동적바인딩을 구현할 수 있다. virtual이 붙는 함수가 곧 가상함수이다. 

 

#include <iostream> 
using namespace std;

class classA
{
public:
	classA();
	virtual ~classA();
};

class classB : public classA
{
public:
	classB();
	~classB();
};

// 부모 생성자 -> 자식 생성자 > 
// 자식 소멸자 -> 부모 소멸자가 호출된다.

classA::classA()
{
	cout << "A" << endl;
}
classA::~classA()
{
	cout << "~A" << endl;
}
classB::classB()
{
	cout << "B" << endl;
}
classB::~classB()
{
	cout << "~B" << endl;
}
int main()
{
	cout << "START" << endl;
	classB* B = new classB;
	classA* A = B;
	delete A;
	return 0;
}

deleteB를 안하고 A를 해도 가상함수 소멸자로 작성되어있으면 타고 들어가서 다 지운다.

 

 

 

정적바인딩, 동적바인딩

정적바인딩 : 컴파일 시에 메모리를 할당(바인드)한다. 일반함수, 일반변수 할당
동적바인딩 : 실행 시에 메모리를 할당(바인드)한다. 가상함수, 변수 동적 할당

다른 얘기로 정적바인딩은 클래스타입의 포인터에 따른 결합, 동적바인딩은 클래스타입의 포인터가 가르키는 대상체에 대한 결합 (출처)

컴파일 중 가상함수면 건드리지 않고, 가상함수가 아니라면 메모리공간에 함수를 할당시켜 놓는다.

프로그램 실행 중 가상함수인데 재정의된 함수가 없으면 가상함수를 올리고,
가상함수인데 재정의된 함수가 있으면 재정의된 함수를 올린다.
가상함수가 아니면 이미 할당된 함수를 갖고 온다.

 

 

아래는 부모 클래스의 소멸자 앞에 virtual 키워드를 지워본 예제이다. 두 코드를 비교해보자.

#include <iostream>
using namespace std;

class classA
{
public:
	classA();
	/*virtual ~classA();*/
	~classA();
};

class classB : public classA
{
public:
	classB();
	~classB();
};

// 부모 생성자 -> 자식 생성자 > 
// 자식 소멸자 -> 부모 소멸자가 호출된다.

classA::classA()
{
	cout << "A" << endl;
}
classA::~classA()
{
	cout << "~A" << endl;
}
classB::classB()
{
	cout << "B" << endl;
}
classB::~classB()
{
	cout << "~B" << endl;
}
int main()
{
	cout << "START" << endl;
	classB* B = new classB;
	classA* A = B;
	delete A;
	return 0;
}

 

 

 

 

결론

클래스의 virtual 소멸자의 역할은
상속관계가 있는 클래스고 소멸자에서 리소스를 해제해야 하는 경우,

반드시 부모 클래스 안의 소멸자를 가상함수로 선언이 필요해서이다.

 

virtual 키워드를 사용하면 가상함수 테이블 포인터가 생기고, 가상함수 테이블에 등록된다.
메모리 공간을 쓰게 되기 때문에 필요할 때만 사용한다. 라고 하지만 
실제 개발에서는 클래스 구조가 바뀔 수도 있기 때문에 virtual 키워드를 아낌 없이 써주는 것도 괜찮다

또한 가상소멸자가 존재하지 않는 기본 기능들(std::string) 등을 상속받아 클래스를 구현하고 업캐스팅해 사용한다면 가상소멸자가 없기 때문에 상속받은 클래스의 메모리가 해제되지 않음도 유의해야 한다.

 

 

가상함수 테이블 & 가상함수 테이블 포인터

가상함수 테이블 : 가상함수들의 주소목록. 테이블은 배열을 뜻하니 함수 포인터 배열이라고 보면 됨

가상함수 테이블 포인터 (vfptr) : 가상함수테이블을 가리키는 함수포인터변수.
가상함수가 하나라도 있으면 컴파일러가 만들어준다. 

 

 

 

추가 예제
#include <iostream> 
typedef struct _basestruct
{
	int m_a;
	int m_b;
	int m_c;
	int m_d;
	int m_e;

	char m_pchara;
	char m_pcharb;
	char m_pcharc;

}basestruct;

class classbase
{
public:
	classbase();
	~classbase();

	basestruct* m_pbasestruct;

};

classbase::classbase()
	:m_pbasestruct(NULL)
{
	m_pbasestruct = new basestruct();
	//m_pbasestruct->m_a;
}

classbase::~classbase()
{
	if (m_pbasestruct)
	{
		delete m_pbasestruct;
	}
}

using namespace std;

class classA
{
public:
	classA();
	/*virtual ~classA();*/
	virtual ~classA();

	classbase* m_nclassbase_A = NULL;
};

class classB : public classA
{
public:
	classB();
	virtual ~classB();

	 classbase* m_pclassbase_B = NULL;
};

// 부모 생성자 -> 자식 생성자 > 
// 자식 소멸자 -> 부모 소멸자가 호출된다.

classA::classA()
{
	cout << "A" << endl;
	m_nclassbase_A = new classbase();
	
}
classA::~classA()
{
	cout << "~A" << endl;
	if (m_nclassbase_A)
	{
		delete m_nclassbase_A;
	}
}
classB::classB()
{
	cout << "B" << endl;
	m_pclassbase_B = new classbase();
}
classB::~classB()
{
	cout << "~B" << endl;
	if (m_pclassbase_B)
	{
		delete m_pclassbase_B;
	}
}
int main()
{
	cout << "START" << endl;
	classB* B = new classB;
	classA* A = B;
	delete A;

	return 0;
}

 

 

 

참고 : 1

참고 : dydtjr1128

참고 : HyacinthWiki 

728x90
728x90
블로그 이미지

coding-restaurant

코딩 맛집에 방문해주셔서 감사합니다.

,

v