스레드 관리.

 

실제 스레드는 일정한 백그라운드 작업을 맡아 처리하고 작업이 끝나면 종료되는 것이 일반적이다.

작업 스레드가 백그라운드 작업을 할 때 주 스레드는 작업 스레드를 만들기만 하고 종료 상태에는 별로 관심을 두지 않는 것이

보통이다. 두 스레드(주 스레드와 작업 스레드)는 서로 독립적으로 실행되기 때문이지만 주 스레드는 적어도 작업 스레드가 종료

되었는지의 여부는 주기적으로 조사해 봐야 하는데 이때는 다음 함수가 사용된다.

 

<Thread 종료 여부 확인 함수 - GetExitCodeThread 함수.>

 

1
2
3
4
5
6
7
BOOL
WINAPI
GetExitCodeThread(
    __in  HANDLE hThread,
    __out LPDWORD lpExitCode
    );
 
cs

 

 · Parameter 1 (HANDLE hThread)

 

  - 종료 여부를 확인하고자 하는 작업 스레드의 핸들을 넘긴다.

 

 · Parameter 2 (LPDWORD lpExitCode)

 

  - Parameter 1을 넘기면 이 스레드의 종료 코드를 Parameter 2로 리턴한다.

  - 스레드가 계속 실행중이라면 STILL_ACTIVE 가 리턴된다.

  - 스레드가 종료되었다면 스레드 시작함수(앞의 예제의 ThreadFunc 함수)가 리턴한 값이나 하기에 나오는 Thread 죵료 함수인

    ExitThread 함수를 통해 전달한 종료 코드가 리턴된다.

 

 만약 앞서 만든 예제처럼 작업 스레드가 무한 루프로 돌고 있다고 해도 모든 스레드는 프로세스가 종료되면 강제로 종료되므로

 무한 루프를 도는 스레드를 만들어도 상관없다.

 

때로는 사용자가 직접 작업 중간에 스레드를 종료해야 될 경우도 있다. 스레드를 강제 종료할 때는 하기 두 함수를 사용한다.

 

<Thread 강제 종료 함수>

 

 1) ExitThread 함수.

1
2
3
4
5
VOID
WINAPI
ExitThread(
    __in DWORD dwExitCode
    );
cs

 

 ExitThread는 스레드가 스스로를 종료할 때 사용하는데 인수로 종료 코드를 전달한다.

 종료 코도는 주 스레드에서 GetExitCodeThread 함수로 받을 수 있다. 스레드가 ExitThread를 호출하면 자신의 스택을 해제하고

 연결된 DLL을 모두 분리한 후 스스로 파괴된다.

 

 2) TerminateThread 함수.

1
2
3
4
5
6
BOOL
WINAPI
TerminateThread(
    __in HANDLE hThread,
    __in DWORD dwExitCode
    );
cs

 

 TerminateThread는 스레드 핸들을 인수로 전달받아 해당 스레드를 강제로 종료한다. 다른 스레드에서 특정 스레드를 강제 종료하고자

 할 때 사용된다. 이 함수는 스레드와 연결된 DLL에게 어떠한 통보도 하지 않으므로 DLL들이 제대로 종료 처리를 하지 못할 수도 있으며,

 할당된 자원들이 제대로 해제되지 않을 수도 있다.

 

 그러므로 스레드를 종료시킬 방법이 없을 경우에만 TerminateThread 함수를 사용해야 하며, 전역 변수나 기타 다른 방법을 통해 스레드에게

 종료 명령을 전달해 스레드가 ExtiThread를 통해 스스로 종료하도록 하는 것이 가장 좋다.

 그러나 ExitThread도 c++ 객체의 소멸자가 호출되지 않고 C 런타임이 만든 고유의 데이터 블록이 해제되지 않는 문제가 발생하기도 한다.

 결국, 스레드가 작업을 무사히 마치고 return 문으로 스레드 시작 함수를 종료하는 것이 가장 바람직하다.

 

<Thread 일시 정지 관련 함수>

 

 스레드의 동작을 잠시 중지시키고 다시 재개시킬 수도 있는데 이때는 하기 함수를 사용한다.

 

 1) Thread 중지 함수.

1
2
3
4
5
DWORD
WINAPI
SuspendThread(
    __in HANDLE hThread
    );
cs

 

 2) Thread 재개 함수.

1
2
3
4
5
DWORD
WINAPI
ResumeThread(
    __in HANDLE hThread
    );
cs

 

 SuspendThread는 스레드의 동작을 중지시키고 ResumeThread는 중지된 스레드를 다시 재개시킨다.

 스레드는 내부적으로 중지 카운트를 유지하는데 이 카운트는 SuspendThread 함수가 호출되면 증가하고 ResumeThread 함수가 호출되면 감소하며

 카운트가 0이 되면 스레드는 재개된다. 그러므로 SuspendThread 함수를 호출한 횟수만큼 ResumeThread 함수를 호출해야지만 스레드가 동작한다.

 

<Thread 중지 / 재개 프로그램>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
struct ThreadParam{
    int x,y,w,h;
    TCHAR* Mes[3];
    int interval;
} Param[3= {
    {101018050"지금 세 개의""배너가 동시에""실행되고 있습니다."100},
    {2101018050"각각 위치와""글자와 주기가""다릅니다."500},
    {4101018050"국민교육현장""국기이 대한 맹세""복무신조."1000},
};
 
 
// 버튼들의 ID
#define ID_R1 101
#define ID_R2 102
#define ID_R3 103
#define ID_PAUSE 104
#define ID_RUN 105
 
DWORD WINAPI ThreadFunc(LPVOID Param)
{
    HDC hdc;
    ThreadParam* p = (ThreadParam*)Param;
    int idx = 0;
    hdc = GetDC(hWndMain);
    for(;;)
    {
        Rectangle(hdc, p->x, p->y, p->+ p->w, p->+ p->h);
        TextOut(hdc, p->x+5, p->y+5, p->Mes[idx%3], strlen(p->Mes[idx%3]));
        GdiFlush();
        Sleep(p->interval);
        idx++;
    }
 
    ReleaseDC(hWndMain, hdc);
    return 0;
}
 
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
    static HANDLE hThread[3];
    static DWORD ThreadID[3];
    static int NowThread = 0;
    int i;
 
    switch(iMessage) 
    {
    case WM_CREATE:
        hWndMain = hWnd;
        CreateWindow("button""중지", BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE, 2001008025, hWnd, (HMENU)ID_PAUSE, g_hInst, NULL);
        CreateWindow("button""실행", BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE, 2001308025, hWnd, (HMENU)ID_RUN, g_hInst, NULL);
        CreateWindow("button""Thread0", WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON | WS_GROUP, 10010010030, hWnd, (HMENU)ID_R1, g_hInst, NULL);
        CreateWindow("button""Thread1", WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON, 10012010030, hWnd, (HMENU)ID_R2, g_hInst, NULL);
        CreateWindow("button""Thread2", WS_CHILD | WS_VISIBLE | BS_AUTORADIOBUTTON, 10014010030, hWnd, (HMENU)ID_R3, g_hInst, NULL);
        CheckRadioButton(hWnd, ID_R1, ID_R3, ID_R1);
 
        for (i=0; i<3++i) {
            hThread[i] = CreateThread(NULL,0,ThreadFunc, &Param[i], 0&ThreadID[i]);
        }
 
        return 0;
    case WM_COMMAND:
        switch(LOWORD(wParam)) {
        case ID_R1:
            NowThread = 0;
            break;
        case ID_R2:
            NowThread = 1;
            break;
        case ID_R3:
            NowThread = 2;
            break;
        case ID_PAUSE:
            SuspendThread(hThread[NowThread]);
            break;
        case ID_RUN:
            ResumeThread(hThread[NowThread]);
            break;
        }
 
        return 0;
    case WM_DESTROY:
        for (i=0; i<3++i)
            CloseHandle(hThread[i]);
 
        PostQuitMessage(0);
        return 0;
    }
    return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}
 
 
cs

'Programming > Thread' 카테고리의 다른 글

[API] Thread 6 (동기화 - 크리티컬 섹션)  (0) 2017.08.30
[API] Thread 5 (스케줄링)  (0) 2017.08.30
[API] Thread 4 (UI 스레드)  (0) 2017.08.30
[API] Thread 2 (MultiThread)  (0) 2017.08.29
[API] Thread 1 (단일 Thread)  (0) 2017.08.24

Thread 1에서는 단일 스레드를 생성하였고 이번에는 멀티 스레드를 생성하여 동작을 확인해 볼 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
DWORD WINAPI ThreadFunc(LPVOID prc)
{
    HDC hdc;
    BYTE Blue = 0;
    HBRUSH hBrush, hOldBrush;
    RECT rc = *(LPRECT)prc;
    hdc = GetDC(hWndMain);
    for(;;)
    {
        Blue+=5;
        Sleep(20);
        hBrush = CreateSolidBrush(RGB(0,0,Blue));
        hOldBrush = (HBRUSH)SelectObject(hdc, hBrush);
        Rectangle(hdc, rc.left,rc.top,rc.right,rc.bottom);
        SelectObject(hdc, hOldBrush);
        DeleteObject(hBrush);
    }
 
    ReleaseDC(hWndMain, hdc);
    return 0;
}
 
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
    HDC hdc;
    DWORD ThreadID;
    static RECT arRect[] = {
        {100100200200}, {300100400200},
        {100300200400}, {300300400400},
    };
 
    int i;
 
    switch(iMessage) 
    {
    case WM_CREATE:
        hWndMain = hWnd;
        for (i=0; i<4++i) {
            CloseHandle(CreateThread(NULL,0,ThreadFunc, &arRect[i], 0&ThreadID));
        }
        return TRUE;
    case WM_LBUTTONDOWN:
        hdc = GetDC(hWnd);
        Ellipse(hdc, LOWORD(lParam)-10, HIWORD(lParam)-10, LOWORD(lParam)+10, HIWORD(lParam)+10);
        ReleaseDC(hWnd, hdc);
        return 0;
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}
cs

 

상기 소스는 동시에 4개의 스레드가 실행되면서 파란색 사각형을 계속 그리고 있으며 주 스레드는 왼쪽 마우스 클릭 입력을 받아

원을 그리는 동작을 한다. 주 스레드를 포함한 5개의 스레드가 동시에 실행되고 있는 것이다.

 

WM_CREATE에서 4개의 스레드를 생성하는데 같은 동작을 하는 스레드이므로 시작 함수는 모두 ThreadFunc이다.

같은 프로세스 내의 Thread끼리는 주소 공간, 전역 변수, 코드를 공유하므로 같은 시작함수를 사용해도 상관없다.

시작 함수가 같더라도 전달되는 인수가 다르면 다른 동작을 한다. 그러므로 차후에 멀티 스레드를 사용하게 되면 같은 자원에 대해

Thread끼리 자원에 접근하는 순서를 제어할 필요가 있게 된다. (동기화 처리)

 

여기서는 앞에와 다르게 ThreadFunc의 인자를 CreateThread 4번째 인자로 넘겨주었다.

스레드로 전달되는 인자는 LPVOID 형이기 때문에 실제로 어떤 형태의 인수든지 전달할 수 있다.

간단한 정수형도 가능하고 크기가 큰 데이터라면 구조체를 만든 후 그 포인터를 전달하면 된다.

'Programming > Thread' 카테고리의 다른 글

[API] Thread 6 (동기화 - 크리티컬 섹션)  (0) 2017.08.30
[API] Thread 5 (스케줄링)  (0) 2017.08.30
[API] Thread 4 (UI 스레드)  (0) 2017.08.30
[API] Thread 3 (Thread 관리)  (0) 2017.08.29
[API] Thread 1 (단일 Thread)  (0) 2017.08.24

해당 내용들은 김상형 저자님의 윈도우즈 API 정복 #2 에 나오는 내용들입니다.

(API의 바이블이라고 생각하는 도서이며 옆에 껴두고 필요한 부분들을 참고하면서 공부하기 정말 좋은 책입니다.)

 

한 프로그램에서 여러 가지 작업을 동시에 수행해야 할 경우가 있다.

대체로 cpu가 하나뿐이며 폰 노이만형 컴퓨터는 한 번에 하나의 일만 할 수 있으므로 실제로 이것은 불가능하다.

그리하여 나온 것이 Thread 개념이며 여러 Thread들이 번갈아 가면서 조금씩 작업을 함으로써 동시에 실행되는 듯한

효과를 낼 수 있다.

 

Thread는 프로세스 내에 존재하는 일련의 실행 코드이다.

프로세스는 단지 존재하기만 하는 껍데기일 뿐이며 실제 작업은 스레드가 담당한다.

프로세스 생성 시 하나의 주 스레드가 생성되며 대부분의 경우 주 스레드가 모든 작업을 처리하고 주 스레드가 종료되면

프로세스도 같이 종료된다.

 

스레드가 여러개 생긴 경우라면, 주 스레드와 나머지 스레드들은 CPU 시간을 우선 순위에 따라 적절하게 분배하여 동시에

실행된다. 운영체제는 스레드별로 골고루 CPU 시간을 배분하므로 한 스레드가 시간을 지나치게 오래 끌더라도 다른 스레드가

이에 영향을 받지 않고 실행된다.

그러므로 이 방법은 타이머나 PeekMessage를 사용하는 방법보다 반응성이 훨씬 좋다.

 

하나의 운영체제에 여러 개의 프로세스가 동시에 실행되는 환경을 멀티 태스킹이라하며 하나의 프로세스에서 여러 개의 스레드가

동시에 실행되는 환경은 멀티스레드라 한다.

 

<간단한 Thread 생성 코드>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
LRESULT CALLBACK WndProc(HWND hWnd,UINT iMessage,WPARAM wParam,LPARAM lParam)
{
    DWORD ThreadID;
    HANDLE hThread;
 
    switch(iMessage) 
    {
    case WM_CREATE:
        hWndMain = hWnd;
        hThread = CreateThread(NULL,0,ThreadFunc, NULL0&ThreadID);
        CloseHandle(hThread);
        return TRUE;
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    }
    return(DefWindowProc(hWnd,iMessage,wParam,lParam));
}
 
cs

 

우선 상기 코드는 주 스레드의 WM_CREATE에서 스레드를 하나 생성한 코드이며 CreateThread 함수를 사용한다.

 

<CreateThread 함수>

1
2
3
4
5
6
7
8
9
10
HANDLE
WINAPI
CreateThread(
    __in_opt  LPSECURITY_ATTRIBUTES lpThreadAttributes,
    __in      SIZE_T dwStackSize,
    __in      LPTHREAD_START_ROUTINE lpStartAddress,
    __in_opt  LPVOID lpParameter,
    __in      DWORD dwCreationFlags,
    __out_opt LPDWORD lpThreadId
    );
cs

 

 · Parameter 1 (LPSECURITY_ATTRIBUTES lpThreadAttributes)

 

  - 스레드의 보안 속성을 지정하는데 자식 프로세스로 핸들을 상속하지 않는 한 NULL로 지정하면 된다.

 

 · Parameter 2 (SIZE_T dwStackSize)

 

  - 스레드의 스택 크기를 지정하는데 스레드끼리 상호 안정된 동작을 하기 위해 스레드별로 별도의 스택이 할당된다.

  - 스택의 크기를 0으로 지정하면 주 스레드와 같은 크기를 가진다.

 

 · Parameter 3 (LPTHREAD_START_ROUTINE lpStartAddress)

 

  - 스레드의 시작 함수(Entry Point)를 지정하며 실질적으로 가장 중요한 인자. (주 스레드의 WinMain 함수에 해당)

  - 지정된 함수로부터 스레드의 실행을 시작하며 해당 함수가 종료되면 스레드도 종료된다.

  - 시작 함수는 다음과 같은 원형을 가져야 한다. → DWORD WINAPI ThreadFunc(LPVOID lpParameter);

 

 · Parameter 4 (LPVOID lpParameter)

  

  - Parameter 3에서 지정한 스레드의 시작 함수는 LPVOID형의 인수 하나만 받아들이는데 이 인수는 CreateThread의

    네 번째 인수로 지정한다.

  - 4 번째 인수는 스레드로 전달할 작업 내용을 지정한 것이며, 전달할 내용이 없다면 NULL을 전달하면 된다.

 

 · Parameter 5 (DWORD dwCreationFlags)

 

  - 생성할 스레드의 특성을 지정하는 인자이며 아무 특성이 없는 보통 스레드를 만들고자 한다면 0으로 지정하면 된다.

  - 만약 스레드를 만들기만 하고 실행은 하지 않게 하고 싶다면, CREATE_SUSPENDED 플래그를 지정하면 된다.

 

 · Parameter 6 (LPDWORD lpThreadId)

 

  - CreateThread 함수가 스레드를 만든 후 스레드의 ID를 리턴하기 위한 출력용 인수이므로 DWORD형의 변수를 하나
    선언한 후 주소값을 넘겨준다.

 

CreateThread 함수는 스레드를 만든 후 스레드의 핸들을 리턴하며 에러가 발생했을 경우 NULL을 리턴한다.

리턴된 핸들을 이용하여 스레드를 제어하는데 상기 예제는 생성 후 스레드를 제어하지 않기 때문에 바로 핸들을 닫았다.

스레드 핸들과 스레드 자체는 다르므로 핸들을 닫는다고 해서 스레드가 종료되는 것은 아니다.

'Programming > Thread' 카테고리의 다른 글

[API] Thread 6 (동기화 - 크리티컬 섹션)  (0) 2017.08.30
[API] Thread 5 (스케줄링)  (0) 2017.08.30
[API] Thread 4 (UI 스레드)  (0) 2017.08.30
[API] Thread 3 (Thread 관리)  (0) 2017.08.29
[API] Thread 2 (MultiThread)  (0) 2017.08.29

클래스에서 암시적 타입 변환을 지원하는 것은 일반적으로 못된 생각이다. 그러나 예외가 있는데, 가장 흔한 예외 중

하나가 숫자 타입을 만들 때이다. 예를 들어 유리수를 나타내는 클래스를 만들고 있다면, 정수에서 유리수로의 암시적

변환은 허용하더라도 잘못되었다고 볼 수 없다. 즉, int를 double 형으로 변환하는 것과 동일하다고 할 수 있다.

 

1
2
3
4
5
6
7
8
class Rational
{
public:
    Rational(int numerator = 0int denominator = 1); //암시적 변환을 허용하기 위해서 explicit을 붙이지 않았다.
 
    int numerator() const;        //분자.
    int denominator() const;    //분모.
};
cs

 

유리수를 나타내는 클래스인만큼 덧셈이나 곱셉 등의 수치 연산은 기본으로 지원하게 만들고 싶을텐데, 이럴 때는 어떤 식으로 지원해야

좋을지 고민해봐야 한다.

아마 흔히 생각나는 방법은 멤버 함수로 operator 연산자를 이용하여 구현하는 방법일 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Rational
{
public:
    ...
 
    const Rational operator* (const Rational& rhs) const;
};
 
void Func()
{
    Rational oneEight(1,8);
    Rational oneHalf(1,2);
 
    Rational result = oneEight * oneHalf;    //이상 없음.
 
    result = result * oneEight;              //이상 없음.
}
cs

 

여기서 혼합형 수치 연산도 가능하길 원한다면 (Rational 객체를 int 같은 것과도 곱할수 있도록), 이것은 반쪽짜리 연산이었다는

사실을 알게 될 것이다. 하기 소스를 보자.

 

1
2
3
4
5
6
7
8
9
10
void Func()
{
    Rational oneEight(1,8);
    Rational oneHalf(1,2);
 
    Rational result = oneHalf * 2;   //이상 없음.
 
    result = 2 * oneHalf;            //컴파일 에러.
}
 
cs

 

곱셈은 기본적으로 교환법칙이 성립해야 되는데 상기에 있는 소스는 컴파일 에러가 발생한다.
상기 소스를 함수 형태로 바꾸어 써 보면 왜 컴파일 에러가 발생하는지 알 수 있다. 하기 소스 참조.

 

1
2
3
4
5
6
7
8
9
10
11
void Func()
{
    Rational oneEight(1,8);
    Rational oneHalf(1,2);
 
    Rational result = oneHalf * 2;    
    // => Rational result = oneHalf.operator*(2);
 
    result = 2 * oneHalf;
    // => result = 2.operator*(oneHalf);
}
cs

 

첫 번째 result 연산에서 oneHalf 객체는 operator* 연산자를 멤버로 갖고 있는 클래스의 인스턴스이므로,
컴파일러는 이 함수를 문제없이 호출한다. 하지만 두 번째 result 연산에서 정수 2에는 클래스 같은 것이 아니기 때문에,

operator* 연산자가 있을 리가 없다. 그럼 컴파일러는 호출할 수 있는 비멤버 operator*(네임스페이스 또는 전역 유효범위에

있는 operator*) 연산자를 찾아본다.

 

result = operator*(2, oneHalf); // 컴파일 에러.

 

그러나 상기 소스와 같이 int형과 Rational을 인자로 갖는 비멤버 버전의 operator* 연산자가 없으므로 컴파일 에러가 발생한다.

 

그럼, 상기 소스에서 Rational result = oneHalf.operator*(2);는 문제가 발생하지 않은 이유에 대해서 알아보자.
이유는 바로 암시적 타입 변환이다.

컴파일러는 함수 쪽에선 Rational 타입을 요구하지만 프로그래머가 operator* 함수에 int를 넘겼다는 사실을 알고 있으나,

이 int를 Rational 클래스의 생성자에 주어 호출하면 Ratinal로 둔갑시킬 수 있다는 사실 또한 알고 있다.

즉, 컴파일러는 하기와 같은 소스로 처리한다는 것이다.

 

1
2
3
4
5
6
    Rational result = oneHalf * 2;    
    // => Rational result = oneHalf.operator*(2);
 
    //result = oneHalf * 2; 상기 문장을 컴파일러는 하기와 같이 처리한다.
    const Rational temp(2);            //2로부터 임시 Rational 객체를 생성.
    Rational result = oneHalf * temp; // oneHalf.operator*(temp); 와 같은 문장.
cs

 

물론 컴파일러가 이렇게 동작하는 것은 명시호출(explicit)로 선언되지 않은 생성자가 있기 때문이다.

만약 Rational 생성자가 명시호출 생성자였으면 하기 코드 모두 컴파일 에러가 발생한다.

 

1
2
3
    Rational result = oneHalf * 2;   //컴파일 에러.
 
    result = 2 * oneHalf;            //컴파일 에러.
cs

 

 

그럼 이제 혼합형 수치 연산이 문제 없도록 만들어보자.

우선 여태까지 알아봤던 결론으로는 암시적 타입 변환에 대해 매개변수가 먹혀들려면 매개변수 리스트에 들어 있어야만 한다는

것이다. 그러니까 멤버 함수를 호출하는 객체에 해당하는 암시적 매개변수는 암시적 변환이 먹히지 않는다는 것이다.

(즉, 2 가 operator* 멤버 함수를 호출하는 객체이므로 이렇게 멤버 함수를 호출하는 쪽의 객체는 암시적 형변환이 되지 않는다는

 것이다.)

 

결국 이를 해결하기 위해서는 2가 멤버 함수를 호출하지 않는 방향으로 가야하므로 operator* 함수를 멤버 함수가 아닌 비멤버 함수로

선언하여 사용하면 된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Rational
{
private:
    int m_numerator;
    int m_denominator;
 
public:
    Rational(int numerator = 0int denominator = 1); //암시적 변환을 허용하기 위해서 explicit을 붙이지 않았다.
 
    int numerator() const;        //분자.
    int denominator() const;      //분모.
};
 
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
    return Rational((lhs.numerator() * rhs.numerator()), (lhs.denominator() * rhs.denominator()));
}
 
void Func()
{
    Rational oneEight(1,8);
    Rational oneHalf(1,2);
 
    Rational result = oneHalf * 2;
    // => Rational result = operator*(oneHalf, temp(2));  정상 동작.
 
    result = 2 * oneHalf;
    // => result = operator*(temp(2), oneHalf);           정상 동작.
}
cs

 

여기서 한 가지 더 고민해볼 것이 있다. Operator* 함수를 Rational 클래스의 프랜드 함수로 두어도 되는가?에 대한 질문이다.

그리고 이 예제에서의 대답은 '아니오'이다.

왜냐하면 이 예제에서 operator* 연산자는 완전히 Rational의 public 인터페이스만을 써서 구현할 수 있기 때문이다.
여기서 한 가지 중요한 결론을 뽑을 수 있는데.. 멤버 함수로 하면 안되는 함수에 대해서 프랜드로 만든다고 해결되는 것이 아니라는 점이다.
즉, "멤버 함수이면 안 되니까"라는 말이 반드시 "프랜드 함수이어야 해"라는 말이 아니라는 것이다.
멤버 함수의 반대는 프랜드 함수가 아니라 비멤버 함수이다.

 

1줄 요약

 

 - 어떤 함수에 들어가는 모든 매개변수(this 포인터가 가리키는 객체도 포함)에 대해 암시적 타입 변환을 해 줄 필요가 있다면,

   그 함수는 비멤버 함수이어야 한다.

어떤 클래스를 만들면 이런 저런 함수를 통해 제공하는 기능이 많을 것이다.

하기 내용은 클래스 설계자보다는 클래스를 사용하여 프로그래밍을 하는 사용자 입장에서 클래스를 사용하는데 있어

편하게 확장시킬 수 있는 방법에 대한 내용이라고 생각하면 조금 더 쉽게 생각할 수 있다.

 

1
2
3
4
5
6
7
8
9
class WebBrowser
{
public:
    ...
    void clearCache();
    void clearHistory();
    void removeCookies();
    ...
};
cs

 

위와 같이 동작하는 함수들을 한 번에 하고 싶을 때도 있기 때문에, 세 함수를 모아서 호출하는 함수도 준비해 둘 수 있다.

 

<멤버 함수 ver>

1
2
3
4
5
6
7
8
9
10
11
class WebBrowser
{
public:
    ...
    void clearCache();
    void clearHistory();
    void removeCookies();
 
    void clearEverything();    //상기 3개 함수 호출.
    ...
};
cs

 

<비멤버 함수 ver>

1
2
3
4
5
6
void clearBrowser(WebBrowser& wb)
{
    wb.clearCache();
    wb.clearHistory();
    wb.removeCookies();
}
cs

 

멤버 버전인 clearEverything과 비멤버 버전인 clearBrowser 중 어떤 쪽이 더 좋은지 살펴보자.
객체 지향 법칙에 관련된 이야기를 찾아보면 데이터와 그 데이터를 기반으로 동작하는 함수는 한 데 묶여 있어야하며,
멤버 함수가 더 낫다고들 한다. 그러나 이는 잘 못된 지식이다. 분명히 객체 지향 법칙은 할 수 있는 만큼 데이터를 캡슐화하라고

주장하지만 멤버 버전인 clearEverything은 비멤버 버전인 clearBrowser보다 캡슐화 정도가 오히려 형편없다.

또한 비멤버 함수를 사용하면 WebBrowser 관련 기능을 구성하는데 있어서 패키징 유연성이 높아지는 장점도 있고 컴파일

의존도도 낮추어 WebBrowser의 확장성을 높일 수 있다.

 

우선 캡슐화하는 것이 늘어나면 그만큼 밖에서 볼 수 있는 것들이 줄어들기 때문에, 내부적으로 변경될 때 필요한 유연성이 커진다.

즉, 이미 있는 코드를 바꾸더라도 제한된 사용자들밖에 영향을 주지 않는 융통성을 확보할 수 있다는 것이다.

정리하면... 앞에서도 말 했듯이 멤버 변수에 접근할 수 있는 함수의 개수가 적을수록 캡슐화가 잘 되었다고 할 수 있다.

그러나 멤버 함수가 늘어나면 늘어날 수록 멤버 변수에 접근할 수 있는 함수의 개수도 늘어나게 되는 것이므로 똑같은 기능을

제공하는데 멤버 함수를 쓰는 것보다는 비멤버 비프랜드 함수를 사용하는 것이 더 바람직하다고 볼 수 있다.

비멤버 비프른드 함수는 어떤 클래스의 private 멤버 부분을 접근할 수 있는 함수의 개수를 늘리지 않기 때문이다.

 

좋은 방법

 

C++에서는 Webrowser가 가진 private 멤버의 캡슐화에 영향을 주지 않으면서 자연스런 방법으로 구사할 수 있다.

clearBrowser를 비멤버 함수로 두되, WebBrowserstuff와 같은 네임스페이스 안에 두는 것이다.

 

1
2
3
4
5
6
7
8
namespace WebBrowserStuff
{
    class WebBrowser { ... };
 
    void clearBrowser(WebBrowser& wb);
 
    ...
}
cs

 

이 방법은 매우 진보적인 방법이라고 볼 수 있다. 네임스페이스는 클래스와 달리 여러 개의 소스 파일에 나뉘어 흩어질 수 있기 때문이다.

clearBrowser 같은 함수들은 편의상 준비한 함수들이며 멤버도 프랜드도 아니기에, WebBrowser 사용자 수준에서 아무리 애를 써도

얻어낼 수 없는 기능은 이들도 제공할 수 없다. 예를 들어, clearBrowser가 없다면 사용자는 그냥 clearCache, clearHistory, removeCookies

를 호출하면 그만이다.

 

WebBrowser처럼 응용도가 높은 클래스는 이런 종류의 편의 함수가 꽤 많이 생길 수 있다. 즐겨찾기에 관련된 함수라든지, 인쇄에 관련된

함수도 있을 것이고, 쿠키 관리용 함수 등등이 있을 것이다.

이것들을 나누어 놓는 쉽고 깔금한 방법은, 즐겨찾기 관련 편의 함수를 하나의 헤더 파일에 몰아서 선언하고, 쿠키 관련 편의 함수는 다른

헤더 파일에 몰아서 선언하고, 인쇄 관련 편의 함수는 또 다른 헤더 파일에 몰아서 선언하는 것이다. <하기 참조>

 

<WebBrowser.h>

1
2
3
4
5
6
namespace WebBrowserStuff
{
    class WebBrowser { ... };
 
    ...
}
cs

 

<WebBrowserClear.h>

1
2
3
4
namespace WebBrowserStuff
{
    void clearBrowser(WebBrowser& wb); //clear 관련 편의 함수
}
cs

 

<WebBrowserBookmark.h>

1
2
3
4
namespace WebBrowserStuff
{
    //즐겨찾기 관련 관련 편의 함수들이 여기에 들어간다.
}
cs

 

<WebBrowserCookies.h>

1
2
3
4
namespace WebBrowserStuff
{
    //쿠키 관련 관련 편의 함수들이 여기에 들어간다.
}
cs

 

표준 C++ 라이브러리가 이러한 구조로 구성되어 있다. std 네임스페이스에 속한 모든 것들이 <C++ standardLibrary> 헤더 같은

것에 모조리 들어가 있지 않고, 몇 개의 기능과 관련된 함수들이 수십 개의 헤더(<vector>, <algorithm>, <memory> 등등..)에

흩어져 선언되어 있다.

 

이렇게 사용하면 vector 기능만 필요한 사용자는 굳이 <memory>를 #include 할 필요가 없으며 사용자가 필요한 기능에 대해서만

#include를 이용하여 사용하면 된다. 결국 사용자가 실제로 사용하는 구성요소에 대해서만 컴파일 의존성을 고려할 수 있게 된다.

그러나 멤버 함수로 사용하게 되면 이런 식으로 기능을 쪼개는 것이 불가능하므로 하나의 클래스는 그 전체가 통으로 정의되어야만

한다.

 

또한 여러 개의 헤더 파일에 나누어 놓으면 편의 함수 집합의 확장도 쉽게 할 수 있다.

만약 사용하던 중에 사용자가 추가로 다운로드 이미지에 관련된 편의 함수가 필요하다고 생각된다면 사용자가 직접 헤더 파일을 하나

추가해서 WebBrowserStuff 네임스페이스를 만들고 그 안에 관련 함수들을 집어넣기만 하면 되기 때문이다.

이런 부분 역시 클래스 멤버 함수였다면 사용자가 클래스 정의 자체를 수정할 수 없기 때문에 불가능하였을 것이다.

만약 해당 클래스를 상속받아서 사용자가 사용한다고 해도 파생클래스는 기본 클래스 안에 캡슐화된 멤버에 대한 접근권한이 없기

때문에, 이런 식의 확장은 좋은 방법이 아니다.

 

1줄 요약

 

 - 멤버 함수보다는 비멤버 비프렌드 함수를 자주 쓰도록 하자. 캡슐화 정도가 높아지고, 패키징 유연성도 커지며, 기능적인

   확장도 늘어난다.

모든 멤버 변수는 private으로 하는 것이 좋다. 상속 관계에서도 동일하다.

우선 모든 멤버 변수를 private으로 선언하면 문법적 일관성을 유지하는데 좋다.

사용자는 고민없이 무조건 멤버 변수에 접근하기 위해서 괄호를 붙여 접근하면 된다.

어떤 변수는 변수명으로 접근해야 하고 어떤 변수는 함수를 통해 접근해야된다면 사용자 입장에서는 헷갈릴만한 문제이다.

 

또한 해당 private 변수에 접근 권한을 부여할 수 있기 때문에 정교하게 제어할 수 있다. (하기 코드 참조)

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class AccessLevels
{
public:
    int getReadOnly() const         { return nReadOnly; } 
 
    void setReadWrite(int value) { nReadWrite = value; }
    int getReadWrite()             { return nReadWrite; }
 
    void setWriteOnly(int value) { nWriteOnly = value; }
 
private:
    int nNoAccess;    //해당 int에는 접근 불가.
 
    int nReadOnly;  //해당 int에는 읽기 전용 접근 가능.
 
    int nReadWrite; //해당 int에는 읽기 쓰기 접근 가능.
 
    int nWriteOnly; //해당 int에는 쓰기 전용 전급 가능.
};
cs

 

이렇게 세밀한 접근 제어는 나름대로의 중요성을 갖고 있다. 어떤 식으로든 외부에 노출시키면 안되는 데이터 멤버들이

꽤 많기 때문이다. 또한 캡슐화를 보다 견고하게 할 수 있다.

 

다른 예를 보자. 하기는 자동차의 속도 값을 모아서 평균을 반환하는 클래스이다.

 

1
2
3
4
5
6
class SpeedDataCollection
{
public:
    void addValue(int speed);    //새 데이터 값 추가.
    double averageSoFar() const//평균 속도 반환.
};
cs

 

averageSoFar 멤버 함수를 구현하는 방법은 2가지가 있다.

 

첫 번째 방법.

 

 -지금까지 수집한 속도 데이터 전체의 평균값을 담는 어떤 데이터 멤버를 클래스 안에 넣어두는 방법.

  averageSofar 함수를 호출할 때마다 이 데이터 멤버의 값을 반환한다.

 

두 번째 방법.

 

 - averageSoFar 함수가 호출될 때마다 평균값을 계산해서 반환하는 방법.

 

첫 번째 방법을 사용하면 SpeedDataCollection 객체의 크가 커지지만(평균값을 유지하기 위한 메모리들이 추가 되기 때문)

averageSoFar 함수의 효율은 좋을 것이다.

두 번재 방법을 사용하면 함수 자체의 속도는 느려지지만, SpeedDataCollection 객체의 크기는 첫 번째보다 작아질 것이다.

 

상황에 따라 어떤 방법을 쓰든 상과없지만 중요한 포인트는 두 방법 모두 "평균값 접근에 멤버 함수를 통하게 한다. (다른 말로

평균값을 캡슐화한다.)"라는 점이다.

 

사용자로부터 데이터 멤버를 숨기면(캡슐화하면), 클래스의 불변속성을 항상 유지하는데 절대로 소홀해질 수 없게 된다.

불변속성을 보여줄 수 있는 통로가 멤버 함수밖에 없기 때문이다. 또한 캡슐화는 현재의 구현을 나중에 바꾸기로 결정할 수 있는

권한을 예약하는 셈이다.

만약 멤버 변수가 public으로 되어있으면 사용자는 이 멤버 변수를 직접 가져다 사용했을텐데... 이 멤버 변수를 수정해야되는 일이

발생하면, 사용자 코드도 죄다 수정을 해줘야하기 때문에 수정 작업에 어려움을 겪게 된다.

그러므로 public 이란 "캡슐화되지 않았다"는 뜻이며 또한 "바꿀 수 없다"라는 의미를 담게 되는 것이다.

 

private은 멤버 함수로 접근하기 때문에 내부적으로 멤버 변수가 바껴도 사용자 코드에서는 수정할 부분이 없다는 얘기이다.

즉, 위의 코드에서 평균을 반환하는 방법이 내부적으로 바뀌더라도 사용자는 averageSoFar 함수를 통해 얻는 Data는 동일하기 때문에

아무런 문제가 되지 않는다는 것이다.

 

protected 또한 public과 다를바가 없다. 외부에서 접근을 못하는 것을 제외하면 캡슐화 측면에서는 public과 동일하다.

상속 관계에서는 멤버 변수를 그대로 가져다 사용할 수 있기 때문에 부모 클래스의 멤버 변수가 수정되면 자식 클래스에 엮여있는

해당 변수들도 전부 수정 작업이 되어야 하기 때문이다.

 

캡슐화의 관점에서 접근 수준은 private와 private가 아닌 나머지 이렇게 둘 뿐이다.

 

2줄 요약

 

 - 데이터 멤버는 private 멤버로 선언하자. 클래스 제작자는 문법적으로 일관성 있는 데이터 접근 통로를 제공할 수 있고,

   세밀한 접근 제어도 가능하며, 클래스의 불변속성을 강화할 수도 있고, 내부 구현의 융통성도 발휘 할 수 있다.

 

 - protected는 public과 똑같다고 생각하자.

 

 

 

 

앞 장에서 배운 내용으로 모든 코드를 '참조에 의한 전달'만으로 구성하려고 할 것이다.
그러나 함수 반환의 경우에는 참조자를 넘기는 것은 좋지 않다.

 

유리수를 나타내는 클래스가 하나 있고 두 유리수를 곱하는 멤버 함수가 있다고 가정하자.

 

1
2
3
4
5
6
7
8
9
10
11
class Rational
{
public:
    Rational(int numerator = 0int denominator = 1//분자, 분모
 
private:
    int n,d; 
 
friend
    const Rational& operator*(const Rational& lhs, const Rational& rhs); //두 곱셈 결과를 반환.
};
cs

 

배운대로 '참조에 의한 전달'을 했지만 생각해보면 위의 코드는 이상한 코드이다.
참조자는 반드시 이미 존재하는 Rational 객체의 참조자여야 한다, 그렇다면 대체 반환할 때 이미 존재하는 어떤 Rational 객체를

반환해야 된다는 것인가??

 

1
2
3
4
5
6
7
void Func()
{
    Rational a(1,2);   // a = 1/2
    Rational b(3,5);   // b = 3/5
 
    Rational c = a*b;  // c = 3/10이 될 것이다. 
}
cs

 

위의 코드를 보면 a*b위의 결과를 바로 c의 객체에다가 집어넣는다.
a*b의 결과 값을 가지는 Rational 객체가 존재해야지만 참조형으로 반환을 해줄 수 있을텐데 a*b의 결과인 임시 객체에 참조를

한다는 것은 아무런 의미가 없는 행동이다.

 

잘못된 시도1. 지역 객체를 만들어서 반환해보기.

 

아래와 같은 미친짓은 하지 말자.

 

1
2
3
4
5
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
    Rational result(lhs.n*rhs.n. lhs.n*rhs.n);
    return result;    //미친짓
}
cs

 

위와 같은 경우는 지역 객체이고 operator* 함수가 끝나면 result 객체도 자연히 소멸되는데 저 소멸되는 객체의 참조자를

반환하는 것은 절대 하지 말아야 한다.

또한 애시당초 참조자를 전달하는 목적은 생성자를 호출하지 않기 위함인데 위와 같은 코드는 어차피 생성자도 호출되기 때문에

'참조에 의한 전달'이 의미가 없게 된다.

 

잘못된 시도2. new로 힙에 메모리를 생성해서 반환하기.

 

1
2
3
4
5
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
    Rational* result = new Rational(lhs.n*rhs.n. lhs.n*rhs.n);
    return result;    //이것도 미친짓
}
cs

 

다른 방법으로 힙에 메모리를 생성하여 소멸되지 않도록 해보려고도 할 것이다.

그러나 어차피 이 방법도 생성자가 호출되며 더 큰 문제는 해당 객체를 누가 delete를 해줄 것인지에 대한 문제이다.

만약 위와 같은 코드를 만든 상태에서 아래와 같이 함수가 호출되었다고 가정하자.

 

1
2
3
4
5
6
void Func2()
{
    Rational w. x. y. z;
 
    w = x * y * z; //operator*(operator*(x,y), z)와 같다.
}
cs

 

위의 x * y * z; 문장에서 operator*가 2번 호출되어 new가 2번 실행했으니 delete 또한 2번 해줘야 하는데....

프로그래머는 위의 객체에 접근할 방법이 없기 때문에 delete를 해줄수가 없다.

 

잘못된 시도2. 정적 객체를 만들어 정적 객체를 반환하기.

 

그래서 마지막으로 한 번 더 시도하게 되는 것이.... static일 것이다.

 

1
2
3
4
5
6
7
8
const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
    static Rational result;
 
    result = ...;
 
    return result;    //가장 미친짓
}
cs

 

위의 코드는 스레드 안정성 문제가 얽혀 있다.
또한 만약 아래와 같은 코드가 진행된다면 정상적으로 진행되지 않을 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
void Func2()
{
    Rational a. b. c. d;
 
    ...
 
    if((a*b) == (c*d))
    {
        //두 유리수 곱이 같으면 어떤 처리를 한다.
    }
}
cs

 

위의 operator== 부분 코드를 바꿔보면 아래와 같다.

 

1
if (operator==(operator*(a,b), operator*(c,d)));
cs

 

a,b,c,d에 적절한 값이 들어가고 2번의 operator* 연산이 진행되어 정적 객체가 각 각 반환되었을 것이다.

그리고 operator== 함수가 호출되어 두 개의 값이 같은 비교를 할 것이다.

그런데 a,b,c,d 값이 무엇이 되었든 간에 정적 Rational 객체의 값이 반환되므로 무조건 마지막에 계산된 operator*의

반환 값이 Rational 객체의 값으로 설정되어 있을 것이다.

 

즉, a,b,c,d 값에 상관없이 operator* 의 반환 값은 항상 일치하므로 상기의 조건문은 무조건 true가 된다.

 

해결 방법

 

위의 잘못된 시도들에서 봤듯이 무조건 '참조에 의한 전달'만이 옳은 것은 아니다.

그러므로 아래와 같이 새로운 객체를 반환하게 만들어 반환하도록 하자.

 

1
2
3
4
const Rational operator*(const Rational& lhs, const Rational& rhs)
{
    return Rational(lhs.n * rhs.n, lhs.d * rhs.d);    //가장 미친짓
}
cs

 

이 방법 또한 새로운 객체가 생성되는 것이므로 생성자와 소멸자가 호출된다.

그러나 위와 같이 에러를 유발하게 되는 위험 부담은 일체 없다. 컴파일러 구현자들이 가시적인 동작 변경을 가하지 않고도

기존 코드의 수행 성능을 높이는 최적화를 적용할 수 있도록 해두었다. 그러므로 상기 동작은 생각보다 빠르다.

 

참조자를 반환할지 새로운 객체를 반환할 것인가를 결정할 때, 올바른 동작이 이루어지도록 만들어야 한다는 것은 반드시

명심해야 한다.

 

1줄 요약

 

 - 지역 스택 객체에 대한 포인터나 참조자를 반환하는 일, 힙에 할당된 객체에 대한 참조자를 반환하는 일 또는

   지역 정적 객체에 대한 포인터나 참조자를 반환하는 일은 그런 객체가 두 개 이상 필요해질 가능성이 있다면

   절대로 하지 마라.

C++는 함수로부터 객체를 전달받거나 함수에 객체를 전달할 때 '값에 의한 전달' 방식을 사용한다. (C에서 물려받은 특성)

값에 의한 전달 방식을 사용하면, 함수 매개변수는 실제 인자의 '복사본'을 통해 초기화되며, 함수가 반환할 때 함수를 호출한

쪽은 함수가 반환한 값의 '복사본'을 돌려받는다.

 

복사본을 만들어내는 원천이 바로 복사 생성자이므로 '값에 의한 전달'이 고비용 연산이 되기도 한다.
왜 그런지 아래 예제로 확인해보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Person
{
public:
    Person();
    virtual ~Person();
 
private:
    std::string strName;
    std::string strAddress;
};
 
class Student : public Person
{
public:
    Student();
    ~Student();
 
private:
    std::string strSchoolName;
    std::string strSchoolAddress;
};
 
 
bool ValidateStudent(Student s); //유효한 학생 정보인지 체크하는 함수
 
void Func()
{
    Student plato;
 
    bool platoIsOK = ValidateStudent(plato); //plato 객체 체크.
}
cs

 

plato로부터 매개변수 s를 초기화시키기 위해 Student의 복사 생성자가 호출된다.
그리고 s는 ValidateStudent 함수가 종료되면 소멸된다.
즉, ValidateStudent가 호출되면 Student의 복사 생성자가 한 번 호출되고 Student의 소멸자가 한 번 호출된다.

 

그러나 Student에는 string 객체 2개가 있기 때문에, Student 객체가 생성될 때마다 멤버 객체들도 덩달아 생성되어야 한다.
게다가 부모클래스인 Person객체도 생성되어야 하므로 Person 객체의 string 객체 2개 또한 생성된다.

Student 객체를 값으로 전달하면, Student 복사 생성자 호출 1번, Person 복사 생성자 호출 1번, string 복사 생성자 호출

4번이 일어난다. 그리고 Student 객체의 사본이 소멸될 때도 앞에서 호출된 생성자 호출 횟수만큼 소멸자도 호출된다.
결과적으로 총 생성자 6번, 소멸자 6번이 호출되는 것이다.

 

이런 불필요한 과정을 거치지 않도록 하기 위해서 매개변수를 넘길 때 상수객체에 대한 참조자로 전달하면 된다.

 

1
2
3
4
5
6
7
8
bool ValidateStudent(const Student& s); //유효한 학생 정보인지 체크하는 함수
 
void Func()
{
    Student plato;
 
    bool platoIsOK = ValidateStudent(plato); //plato 객체 체크.
}
cs

 

이렇게 하면 새로 만들어지는 객체같은 것이 없기 때문에, 생성자와 소멸자가 호출되지 않는다.
그리고 원래 값에 의한 전달은 사본으로 작업이 되기 때문에 원본에는 아무런 영향이 없었지만 참조자를 전달하였으므로

작업 도중 원본에 영향을 미칠 수 있다. 이러한 문제를 방지하기 위해서 const를 붙여주는 것이다.

 

또한 참조자로 매개변수를 넘기면 파라미터로 넘어온 데이터가 짤리는 문제(복사 손실, slice problem)도 방지할 수 있다.
아래 예처럼 함수의 파라미터는 부모 클래스형이고 함수 호출부의 파라미터는 자식 클래스 형인 경우를 보자.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Window
{
public:
    ...
    std::string name() const;     //해당 윈도우 이름을 반환해주는 함수.
    virtual void display() const//일반적인 윈도우를 화면에 그려주는 함수.
};
 
class windowWithScrollBars : public Window
{
public:
    ...
    virtual void display() const//스크롤바 기능이 있는 윈도우를 그려주는 함수.
};
 
void printNameAndDisplay(Window w) //함수의 파라미터는 부모클래스 형.
                                   //값에 의한 전달이므로 자식클래스부분은 싹 짤림. (slice)
{
    std::cout << w.name();
    w.display();
}
 
void Func()
{
    windowWithScrollBars wBar;
 
    printNameAndDisplay(wBar);        //함수 호출부의 파라미터는 자식클래스 형
}
cs

 

위의 예제처럼 값에 의한 전달을 하는 경우에는 매개변수 w가 생성되기는 하는데 부모클래스에 대한(Window) 데이터만

생성된다.

즉, 자식클래스 부분은 생성되지 않고 짤리게 되며 여기서w.Display()를 호출하면 Window 클래스의 Display 함수가 호출된다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
void printNameAndDisplay(const Window& w) //함수의 파라미터는 부모클래스 형.
                                          //참조에 의한 전달이므로 자식클래스 부분도 전달됨.
{
    std::cout << w.name();
    w.display();
}
 
void Func()
{
    windowWithScrollBars wBar;
 
    printNameAndDisplay(wBar);        //함수 호출부의 파라미터는 자식클래스 형
}
cs

 

이제 w는 어떤 타입의 윈도우 객체가 넘겨지더라도 넘겨진 객체 자체의 성질을 그대로 갖게 된다.

 

참조자는 보통 포인터를 써서 구현된다. 즉, 참조자를 전달한다는 것은 결국 포인터를 전달하는 것과 같은 것이다.
그러므로 기본 제공 타입(int 등..)일 경우에는 그냥 값에 의한 전달을 해도 상관이 없다.

그리고 한 가지 주의할 점은 객체 크기가 작고 멤버 변수 얼마 없다고 해서 '값에 의한 전달'을 선택해서는 안된다.

이유는 컴파일러 중에는 기본제공 타입과 사용자 정의 타입을 아예 다르게 취급하는 것들이 있기 때문이다.
진짜 int나 double은 레지스터에 넣어주지만, double 하나로만 만들어진 사용자 정의 타입은 레지스터에 넣지 않는다.
그러므로 사용자 정의 타입은 참조에 의한 전달을 쓰는 편이 좋다. 포인터(참조자도 포인터로 구현됨)만큼은 레지스터에

들어가기 때문이다.

 

'값에 의한 전달'이 저비용이라고 가정해도 괜찮은 타입은 기본 제공 타입, STL 반복자, 함수 객체 타입 이렇게 세 가지 뿐이다.
이외에는 무조건 '참조에 의한 전달'을 사용하자.

 

2줄 요약

 

 - '값에 의한 전달'보다는 '상수객체 참조자에 의한 전달'을 사용하자. 성능뿐만 아니라 복사손실 문제까지도 막아준다.

 - 기본제공 타입, STL 반복자, 함수 객체 타입에만 값에 의한 전달을 사용하자.

+ Recent posts