728x90

stack 개념


밑이 막힌 긴 통에 비유

: 먼저 들어간(Push) 것이 아래에 있게 되고 나중에 들어간 것이 위에 있게 된다. 따라서 제일 나중에 들어간 것이 제일 먼저 나온다(POP). 

이 때문에 stack을 LIFO(Last In Firsto Out) 구조라고 한다.


위의 그림과 같이 stack에 값을 집어넣는 것을 Push, 값을 빼내는 것을 Pop이라고 한다.


자료구조에서의 Stack


배열로 구현


stack이 완전히 빈 경우는 top Index가 -1의 값을 가진다. 

1
2
3
4
5
6
7
8
9
10
#define MAX    3
 
int stack[MAX];
int stackTop;        
 
void init_stack(void)
{
    printf("\n -> Initialize stack.\n");
    stackTop = -1;
}
cs



아래는 데이터를 추가(Push)하는 과정이다.


stack의 바닥은 배열의 index 0이 된다. 마지막에 저장된 데이터 위치가 top이 된다.


push 연산은 아래와 같이 정리된다.

 - push : Top을 위로 한칸 올리고, Top이 가리키는 위치에 데이터 저장


1
2
3
4
5
6
7
8
9
10
int push(int top)
{
    if (stackTop >= MAX - 1// 스택이 꽉 찬 상태는 MAX -1 (배열의 마지막 index)
    {
        printf("\n -> Stack overflow.");
        return -1;
    }
    stack[++stackTop] = top;     // stackTop 한칸 올린 위치에 값(top)을 저장
    return top;
}
cs



pop 연산은 아래와 같이 정리된다.

- pop : Top이 가리키는 데이터 반환 후, Top 위치를 한칸 내림


pop 연산시에는 stack이 텅 비어있는 경우를 가정해야 한다. 텅 빈 stack을 pop하면 Stack Underflow가 일어나게 된다.


1
2
3
4
5
6
7
8
9
int pop(void)
{
    if (stackTop < 0)    // stack이 비어있다면
    {
        printf("\n -> Stack underflow.");
        return -1;
    }
    return stack[stackTop--];    // top 한칸 내림
}
cs



마지막으로 stack에 저장된 값을 출력하는 함수를 정의한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void print_stack(void)
{
    int i;
 
    if (stackTop < 0)    // stack이 비어있다면
    {
        printf("\n -> Stack is Empty.\n");
        return -1;
    }
    printf("\n===== Stack에 저장된 값 ===== \n");    
    for (i = stackTop; i >= 0; i--)
    { 
        printf("%d "stack[i]);
    }
    printf("\n\n");
}
cs


위 함수를 토대로 main함수를 정의하고 출력결과를 살펴 본다.

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
#include <stdio.h>
 
void main(void)
{
    int i;
    init_stack();
 
    printf("push 1, 2, 3");
    push(1); push(2); push(3);
    print_stack();
 
    i = pop();
    printf("pop 3");
    print_stack();
    
    printf("push 4, 5");
    push(4); push(5);    // stack overflow
    print_stack();
 
    init_stack();
    print_stack();
 
    printf("pop");
    pop();
    return 0;
}
cs


Output :

 -> Initialize stack.

push 1, 2, 3

===== Stack에 저장된 값 =====

3 2 1


pop 3

===== Stack에 저장된 값 =====

2 1


push 4, 5

 -> Stack Overflow.


===== Stack에 저장된 값 =====

4 2 1



 -> Initialize stack.


 -> Stack is Empty.

pop

 -> Stack underflow.


stack 구조에 맞게 push와 pop이 진행되고 overflow와 underflow가 일어나는 모습도 볼 수 있다.




연결리스트를 이용한 stack


연결리스트 초기화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct node
{
    int Ldata;
    struct node *next;
}Node;
 
Node *head, *tail;
 
void init_stack(void)
{
    head = (Node*)malloc(sizeof(Node));
    tail = (Node*)malloc(sizeof(Node));
    head->next = tail;
    tail->next = tail;
}
cs


push 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int push(int data)
{
    Node *nN;
    if ((nN = (Node*)malloc(sizeof(Node))) == NULL)
    {
        printf("\n -> Out of memory.");    // 동적할당 실패(NULL)할시 출력
        return -1;
    }
    nN->Ldata = data;
 
    nN->next = head->next;    // 새 노드를 머리와 연결
    head->next = nN;
 
    return data;
}
cs

head->next가 stack의 상단이 된다.



pop 함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int pop(void)
{
    Node *delN;
    int ret;
    if (head->next == tail) // stack이 비어있다면
    {
        printf("\n -> Stack underflow.\n");
        return -1;
    }
    delN = head->next;
    ret = delN->Ldata;
 
    head->next = delN->next;
    free(delN);
    return ret;
}
cs


pop의 과정은 다음과 같다. delN가 가리키는 노드의 data를 ret에 저장해두었다 노드의 해제와 함께 return한다.

stack이 비어있을 때는 underflow 에러를 띄우고 -1을 return하도록 한다.



clear_stack 함수

배열을 이용한 stack은 top을 -1로 초기화해줬지만 연결리스트는 모든 노드를 메모리에서 해제할 필요가 있다.


dN과 d가 처음 head->next를 가리키게 된다.

dN이 다음 노드로 이동하고 d가 가리키는 노드가 해제된다. 이 과정을 dN이 tail을 가리킬 때까지 반복하게 된다.


그리고 head->next가 tail을 가리키면서 초기화 형태로 돌아가게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
void clear_stack(void)
{
    Node *dN, *d;
    dN = head->next;
    while (dN != tail)
    {
        d = dN;
        dN = dN->next;
        free(d);
    }
    head->next = tail;
}
cs


마지막으로 stack의 데이터들을 보여주기 위한 함수이다.

1
2
3
4
5
6
7
8
9
10
11
12
void print_stack(void)
{
    Node *prt;
    prt = head->next;
    printf("\n 스택의 데이터 : \n");
    while(prt != tail)
    {
        printf("%d ", prt->Ldata);
        prt = prt->next;
    }
    printf("\n");
}
cs


위 함수를 토대로 main함수를 정의하고 출력결과를 살펴 본다.

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
#include <stdio.h> 
#include <stdlib.h>
 
int main(void)
{
    int i;
    init_stack();
    
    printf("\n push 1, 2, 3, 4, 5");
    push(1); push(2); push(3); push(4); push(5);
    print_stack();
 
    i = pop();
    printf("pop");
    print_stack();
 
    printf("push 6, 7, 8");
    push(6); push(7); push(8);
    print_stack();
    
    printf("\n 스택 비움");
    clear_stack();    // stack 모두 비움
    print_stack();
 
    printf("pop");
    pop();    
    return 0;
}
cs


Output :

 push 1, 2, 3, 4, 5

 스택의 데이터 :

5 4 3 2 1

pop

 스택의 데이터 :

4 3 2 1

push 6, 7, 8

 스택의 데이터 :

8 7 6 4 3 2 1


 스택 비움

 -> Stack is Empty.

pop

 -> Stack underflow.



stack의 개념에는 peek이라고 해서 맨 위의 개체를 반환하되 제거는 하지 않는 것도 있지만 따로 함수로 구현하진 않았다.

그리고 배열의 경우에도 동적할당을 사용해볼 수 있으나 연결리스트에 비해 큰 이점이 없으므로 필요에 따라 선택하는게 좋을 듯 하다.

'Data Structure' 카테고리의 다른 글

stack으로 구현하는 계산기 프로그램  (0) 2016.11.07
연결리스트와 파일 입출력  (0) 2016.11.02
이진 트리의 구현과 순회(Traversal)  (0) 2016.09.12
Binary Tree  (0) 2016.09.12
Doubly Linked List  (0) 2016.09.09
728x90
공용체 union 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
 
union smart
{
    int A;    short B;
    int C;    char D;
}obj;
 
int main()
{    
    obj.A = 0x12345678;    obj.B = 0x12345678;
    obj.C = 0x12345678;    obj.D = 0x12345678;
        
    printf("Size of Smart: %d\n"sizeof(union smart));
    printf("%x\n", obj.A);    printf("%x\n", obj.B);
    printf("%x\n", obj.C);    printf("%x\n", obj.D);
    return 0;
}
cs


위 공용체의 size는 4byte


Little Endian

78 

 56

 34

 12


obj.A=12345678   // int A

obj.B=5678        // short B

obj.C=12345678  // int C

obj.D=78           // char D



함수 포인터


: 함수를 가리키는 포인터 


int SoSimple(int num1, int num2) { ....}


위 함수에 대한 함수 포인터 변수 선언

int (*fptr) (int, int);



함수 포인터 변수에 SoSimple 주소값 저장

fptr = SoSimple



이는 fptr(3, 4);    // SoSimple(3, 4)와 동일한 결과


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
 
void Add(int n1, int n2)
{
    printf("%d + %d = %d \n", n1, n2, n1 + n2);
}
 
int main()
{    
    int num1 = 10, num2 = 20;
 
    void(*fptr)(intint= Add;
    fptr(num1, num2);
    return 0;
}
cs

Output :

10 + 20 = 30



void형 포인터 변수는 어떠한 변수의 주소 값이든 담을 수 있지만(함수의 주소값도)

값의 변경이나 참조를 비롯한 어떤 포인터 연산도 불가

: type 정보가 없기 때문에 당연


1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
 
void voidFunc(void)
{
    printf("void ptr test\n");
}
 
int main()
{
    void(*fptr)(void= voidFunc;
    fptr();
    return 0;
}
cs

Output :

void ptr test


함수 포인터 사용 이유 (참고 : http://norux.me/8)  


①프로그램 코드의 간결

②중복 코드를 줄이기 위해서

③상황에 따라 해당되는 함수를 호출할 수 있음


그외에 참고 https://kldp.org/node/133026




가변 인자 함수


num = Sum(3, 1, 2, 3);    // 3은 이후에 전달되는 인자의 갯수 (1,2,3)


1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
 
int Sum(int n, ...)    // 가변 인자에 대한 선언
{
    printf("n=%d \n", n);    
    return 0;
}
 
int main()
{    
    Sum(3123);
    return 0;
}
cs

Output :

n=3


가변 인자 함수의 호출을 통해 전달되는 인자 정보 추출


#include <stdarg.h>

1. 가변인자를 가리킬 수 있는 참조자 선언     : va_list

2. 참조자가 가변인자를 실제 참조                : va_start

3. 참조자를 통해 전달된 정보 추출               : va_arg

4. 참조자가 가변인자를 가리키지 않도록 해제 : va_end


Output :

3+5=2=10



22행 : (vlist, int) 첫번째 인자는 참조자 이름, 두번째 인자는 참조대상의 type



'Study > C' 카테고리의 다른 글

enum, auto, 구조체  (0) 2017.03.24
C 복습-콘솔 입출력  (0) 2017.02.11
파일의 분할과 헤더파일의 디자인  (0) 2016.08.30
선행처리기와 매크로2  (0) 2016.08.30
선행처리기와 매크로  (0) 2016.08.29
728x90

단순 연결리스트에 파일 입출력을 응용



구조체 정의

1
2
3
4
5
6
7
8
#define LEN    20
typedef char LData[LEN];
 
typedef struct _node
{
    LData data;
    struct _node * next;
} Node
cs


연결 리스트의 초기화

1
2
3
4
5
6
7
8
9
10
Node * head, * tail;    // 전역 변수 
 
void list_init(void)
{
    head = (Node*)calloc(1sizeof(Node)); 
    tail = (Node*)calloc(1sizeof(Node)); 
    head->next = tail;
    tail->next = tail;
}
 
cs


데이터 입력받음

1
2
3
4
5
6
7
8
9
10
static char data[LEN];
 
void Data_insert()
{
    printf("이름 입력:");
    fgets(data, sizeof(data), stdin);
    while (getchar() != '\n');
 
    Node_insert(data);
}
cs


노드의 삽입

1
2
3
4
5
6
7
8
void Node_insert(char *data)
{
    Node *t;
    t = (Node * )calloc(1sizeof(Node)); 
    strcpy(t->data, data);
    t->next = head->next;
    head->next = t;
}
cs

초기화하고 노드를 삽입하는 과정은 다음 그림과 같다.



node L을 추가한 다음 node I를 추가하면 head - I - L - tail 순으로 연결이 된다. tail->next는 NULL로 초기화하지 않고 tail 자신을 가리키도록 했다.

조회하고 출력할 때, 마지막에 들어간 데이터가 가장 먼저 출력된다.



연결리스트를 파일로 저장

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void save_File()
{
    FILE *fp;
    fp = fopen("DATA.DBF""wb");
 
    Book *t;
    
    if (fp == NULL)
    {
        printf(" 오류 \n");
        return;
    }
    t = head->next;
    while (t != tail)
    {
        fwrite(t, sizeof(LEN), 1, fp);    
        t = t->next;
    }
    fclose(fp);
}
cs


*t가 tail을 가리킬 때까지 (추가할 필요가 노드가 없게 될때까지) 각 노드를 추가한다.



파일을 읽어들여 연결리스트로 구성

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
void load_File()
{
    FILE *fp;
    fp = fopen("BOOK.DBF""rb");
 
    Book *p;
    Book *s;
    
    if (fp == NULL)
    {
        printf(" 오류 \n");
        return;
    }
    
    p = head->next;    /* 파일을 읽기 전 모든 노드 삭제하는 과정 시작 */
    while (p != tail)
    {
        s = p;
        p = p->next;
        free(s);
    }                /* 파일을 읽기 전 모든 노드 삭제하는 과정 끝 */
 
    head->next = tail; // head를 tail과 연결 시켜 준다.
    while (1)
    {
        p = (Book*)calloc(1sizeof(Book));
        if (!fread(p, sizeof(LEN), 1, fp))    // EOF까지 구조체를 읽어들이면서 node를 추가한다.
        {
            free(p);
            break;
        }
        p->next = head->next;
        head->next = p;
    }
    fclose(fp);
}
cs


malloc() 함수는 메모리를 할당후 초기화하지 않기 때문에 출력된 파일을 메모장으로 열어보면 데이터를 알아보기 힘들게 된다.

하지만 문자열의 끝은 NULL로 구분하게 되므로 콘솔환경의 파일 입출력시에는 아무 문제없다.


만약 식별하기 쉬운 파일 데이터를 원하면, calloc함수를 통해 동적할당하면 된다. (calloc함수는 모든 비트를 0으로 초기화)


calloc

#include <stdlib.h>

void * calloc(size_t elt_count, size_t elt_size);


malloc 함수와의 차이점은 elt_count x elt_size 크기만큼의 바이트를 동적할당한다는 것이다.

그리고 malloc 함수는 쓰레기값으로 초기화되는것과 달리 calloc는 모든 비트를 0으로 초기화한다.



도서관리 프로그램에 구현된 파일 입출력 기능


종료 메뉴 선택시 save_File() 함수 호출 후, 프로그램을 종료한다.



BOOK.DBF 파일의 내용



프로그램 재실행 후 출력 모습 

저장된 파일을 토대로 연결리스트를 구성하면서 처음 입력된 자료가 처음 출력되게 된다.


1
2
3
4
5
6
7
8
9
10
#include "book.h"
 
int main()
{
    list_init();    // 연결리스트 초기화
    load_File();    // 파일을 읽어들임 
 
    unsigned int uiMenu;
    MenuMap stMenu[END] = {
( ...)
cs

main함수에서는 연결리스트 초기화 후, 파일을 읽어들여 연결리스트로 재구성한다.

'Data Structure' 카테고리의 다른 글

stack으로 구현하는 계산기 프로그램  (0) 2016.11.07
자료 구조 - Stack  (0) 2016.11.05
이진 트리의 구현과 순회(Traversal)  (0) 2016.09.12
Binary Tree  (0) 2016.09.12
Doubly Linked List  (0) 2016.09.09
728x90

유니티 로드쇼에서는 vr과 영화, 광고를 통한 수익, 모바일 지원환경 등 많은 것을 듣고 보고 온 시간이었습니다.

아직 유니티는 막 배우는 단계이지만 이를 잘 활용해서 좋은 창작물을 만들 수 있었으면 좋겠습니다.


강연도 집중해서 잘 들었고 강의 중간중간 질문하시는 분들의 열기도 뜨거웠습니다. 

날카로운 질문을 던지는 분들 외에도 중1 개발자분도 기억에 남네요~


유니티가 가지고 있는 장점들을 앞으로도 더 발전시키고 부족한 점들을 개선시키면 좋겠습니다.
특히 엔진 메뉴얼과 멀티스레딩 지원 부분을 강화할 필요가 있어 보입니다.




@ Unity 3D VR

1 unit = 1 meter

camera - vr both 를 통해 쉽게 설정 가능 


sdk - oculus, gear vr... etc...지원


vr - ui and ux 지원

3d ui- reference들 참고하면 좋다.  


Motion sicknesss

-over stimulation theory 

-sensory conflict theory


@ 고려할 사항

과하지 않은 움직임(초당 7도), 카메라 회전 고정 금지, 과한 가속 운동 금지


@ 수렴초점 불일치

망막 경합(retinal rivalry)

양안 경합(binocular ri-)


Convergence Insufficiency

(ex- 카메라가 object를 너무 가까이 둘 때 양눈이 보는 이미지가 갈라짐, fps에서 조준시점)

Fps hand scaling- 총이 실제로 작아짐을 확연히 느끼게 됨. 벽에 파묻히는 문제는 벽에 가까이 다가섬을 지양하도록


성인 기준으로 vr이 만들어져있고 여러 문제 등으로 13세이하에는 권장되지 않음



@ Multi threaded Rendering 개선 및 강화중

5.4 버전이 ms 레이턴시를 많이 줄였고 지속적으로 개선중이다.



@ vr환경에서 개발가능한 편집기 개발중

carte blanche를 개발하는중. (유니티 내부 유니티 랩스에서 개발중)  



unity 3d 관련 forums- unity hub, facebook unity korea 



'Study > Unity Engine' 카테고리의 다른 글

스크립트의 실시간 반영 - ExecuteInEditMode  (0) 2016.12.08
728x90

객체 포인터의 참조 관계


근로학생은 학생이자 사람이다.

따라서 상속관계는 다음과 같다.


class Student : public Person

{  };


class PartTimeStudent : public Student

{  };


따라서 아래와 같이 포인터 변수를 통해 참조가 가능하다.


Person * ptr = new Student();

Person * ptr = new PartTimeStudent();

Student * ptr = new PartTimeStudent();


다음과 같은 참조는 불가능하다. 모든 학생이 근로학생이란건 논리적으로도 맞지 않다.

PartTimeStudent * ptr = new Student();




Person * ptr;

따라서, 위와 같은 포인터 변수는, Person 클래스의 객체 또는 Person을 직간접적으로 상속하는 모든 클래스의 객체를 가리킬 수 있다. (객체 주소 값 저장 가능)



'Study > C++' 카테고리의 다른 글

상속의 이해  (0) 2016.10.13
상속  (0) 2016.09.30
const, friend, static, mutable 선언  (0) 2016.09.29
복사 생성자, 얕은 복사와 깊은 복사, 임시 객체  (0) 2016.09.28
객체 배열과 this포인터  (0) 2016.09.27
728x90

유도 클래스의 객체 생성 과정


유도 클래스의 객체 과정에서 기초 클래스의 생성자는 100% 호출된다.


유도클래스의 생성자에서 기초 클래스의 생성자 호출을 명시하지 않으면, 기초 클래스의 void 생성자가 호출된다.

(기초 클래스의 void생성자가 정의되있어야 함)



#include <iostream>
using namespace std;

class SoBase
{
private:
	int baseNum;
public:
	SoBase() : baseNum(20)
	{
		cout << "SoBase()" << endl;
	}
	SoBase(int n) : baseNum(n)
	{
		cout << "SoBase(int n)" << endl;
	}
	void ShowBaseData()
	{
		cout << baseNum << endl;
	}
};

class SoDerived : public SoBase	// public 상속
{
private:
	int derivNum;
public:
	SoDerived() : derivNum(30)	
	{ 
		// 생성자, 기초 클래스의 생성자 호출을 명시하지 않음
		cout << "SoDerived()" << endl;
	}
	SoDerived(int n) : derivNum(n)	
		// 역시 기초 클래스 생성자 호출 명시하지 않음
	{
		cout << "SoDerived(int n)" << endl;
	}
	SoDerived(int n1, int n2) : SoBase(n1), derivNum(n2) 
		// n1을 인자로 받는 기초 클래스 생성자 호출 명시
	{
		cout << "SoDerived(int n1, int n2)" << endl;
	}
	void ShowDerivData()
	{
		ShowBaseData();
		cout << derivNum << endl;
	}
};

int main(void)
{
	cout << "case1..... " << endl;
	SoDerived dr1;
	dr1.ShowDerivData();
	cout << "-------------------" << endl;
	cout << "case2..... " << endl;
	SoDerived dr2(12);
	dr2.ShowDerivData();
	cout << "-------------------" << endl;
	cout << "case3..... " << endl;
	SoDerived dr3(23, 24);
	dr3.ShowDerivData();
	return 0;
};


Output : 

case1.....

SoBase()

SoDerived()

20

30

-------------------

case2.....

SoBase()

SoDerived(int n)

20

12

-------------------

case3.....

SoBase(int n)

SoDerived(int n1, int n2)

23

24



출력결과를 보면 유도 클래스의 객체를 생성할 때 기초 클래스의 생성자가 먼저 호출됨을 알 수 있다.



case 3의 객체 생성 과정은 다음과 같다.


유도 클래스의 객체를 생성할 때 생성자를 호출하고 이니셜라이저에 의해 n1, n2 인자가 전달되면서 유도 클래스의 생성자가 호출된다.

이 때 생성자가 호출되었다고 해서 바로 실행이 되는 것이 아니다.

기초 클래스의 생성자 호출이 우선되어야 한다.  


이니셜라이저의 SoBase(n1)에 의해 매개 변수 n1으로 전달된 값을 인자로 전달받을 수 있는 기초 클래스의 생성자를 호출한다. 호출이 완료되면, 

기초 클래스의 멤버 변수가 초기화되고 이어 유도 클래스의 생성자 실행이 완료되면서 유도 클래스 멤버 변수도 초기화가 이루어진다.



이렇게 객체가 생성되며, 앞서 이야기했듯이 이니셜라이저에서 기초 클래스 생성자 호출이 명시되지 않으면 void 생성자를 호출한다.


유도 클래스에서도 다음의 원칙은 적용된다.

"클래스의 멤버는 해당 클래스의 생성자를 통해 초기화해야한다."




유도 클래스 객체의 소멸 과정


소멸자는 명시적 선언이 필요없다. 다음 예제를 통해, 유도 클래스의 객체가 소멸될 때, 어느 시점에 소멸자가 호출되고 실행되는지 알 수 있다.


#include <iostream>
using namespace std;

class SoBase
{
private:
	int baseNum;
public:
	SoBase(int n) : baseNum(n)
	{
		cout << "SoBase() : " << baseNum << endl;
	}
	~SoBase()
	{
		cout << "~SoBase() : " << baseNum << endl;
	}
};

class SoDerived : public SoBase
{
private:
	int derivNum;
public:
	SoDerived(int n) : SoBase(n), derivNum(n)
	{
		cout << "SoDerived() : " << derivNum << endl;
	}
	~SoDerived()
	{
		cout << "~SoDerived() : " << derivNum << endl;
	}
};

int main(void)
{
	SoDerived drv1(15);
	SoDerived drv2(27);
	return 0;
};


Output :

SoBase() : 15

SoDerived() : 15

SoBase() : 27

SoDerived() : 27

~SoDerived() : 27

~SoBase() : 27

~SoDerived() : 15

~SoBase() : 15


출력결과를 보면 유도 클래스의 객체가 소멸될 때, 유도 클래스의 소멸자가 실행되고 기초 클래스의 소멸자가 실행된다. 

(즉 객체에 소멸순서는 생성순서와 반대)



유도 클래스의 생성자 및 소멸자 정의 모델

생성자에서 동적 할당한 메모리 공간은 소멸자에서 해제한다. 상속과 연관된 클래스의 소멸자도 이러한 원칙을 지켜 정의해야 한다.


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
class Person
{
private:
    char * name;
public:
    Person(char * myname)
    {
        name=new char[strlen(myname)+1];
        strcpy(name, myname);
    }
    ~Person()
    {
        delete []name;
    }
    void WhatYourName() const
    {
        cout<<"My name is "<<name<<endl;
    }
};
 
class UnivStudent : public Person
{
private:
    char * major;
public:
    UnivStudent(char * myname, char * mymajor)
        :Person(myname)
    {
        major=new char[strlen(mymajor)+1];
        strcpy(major, mymajor);
    }
    ~UnivStudent()
    {
        delete []major;
    }
    void WhoAreYou() const
    {
        WhatYourName();
        cout<<"My major is "<<major<<endl<<endl;
    }
};
cs


상속과 생성자의 호출 예제


https://goo.gl/4Ibgjm



생성자와 소멸자 정의 예제

#include <iostream>
using namespace std;

class MyFriendInfo
{
private:
	char * name;
	int age;
public:
	MyFriendInfo(char * fname, int fage) : age(fage)
	{
		name = new char[strlen(fname) + 1];
		strcpy(name, fname);
	}
	void ShowMyFriendInfo()
	{
		cout << "이름: " << name << endl;
		cout << "나이: " << age << endl;
	}
	~MyFriendInfo()
	{
		delete[]name;
	}
};

class MyFriendDetailInfo : public MyFriendInfo
{
private:
	char * addr;
	char * phone;
public:
	MyFriendDetailInfo(char *fname, int fage, char *faddr, char *fphone)
		:MyFriendInfo(fname, fage)
	{
		addr = new char[strlen(faddr) + 1];
		phone = new char[strlen(fphone) + 1];
		strcpy(addr, faddr);
		strcpy(phone, fphone);
	}
	void ShowMyFriendDetailInfo()
	{
		ShowMyFriendInfo();
		cout << "주소: " << addr << endl;
		cout << "전화: " << phone << endl << endl;
	}
	~MyFriendDetailInfo()
	{
		delete[]addr;
		delete[]phone;
	}
};

int main(void)
{
	MyFriendDetailInfo myfren("철수", 23, "서울종로구 사직동", "010-0000-0000");
	myfren.ShowMyFriendDetailInfo();
	return 0;
};

Output:
이름: 철수
나이: 23
주소: 서울종로구 사직동
전화: 010-0000-0000


protected로 선언된 멤버가 허용하는 접근 범위

세 가지 형태의 상속
private < protected < public



상속을 할 경우, 유도클래스에서는 private와 달리 protected로 선언된 멤버 변수에는 접근 가능

private멤버는 클래스내에서만 접근 가능하다.

주의할 것은 어떠한 형태로 상속하던지, 유도 클래스에서 상속한 private멤버는 접근이 불가능하단 것이다.


실제로는 C++에서 public 이외의 상속은 다중 상속과 같이 특별한 경우가 아니면 잘 사용하지 않는다.




상속의 기본 조건


is a 관계의 성립


전화 -> 무선 전화기            

컴퓨터 -> 노트북 컴퓨터    (Notebook Computer is a Computer)


컴퓨터를 두고 이동성이 있다는 이야기를 하진 않는다.

노트북 컴퓨터는 컴퓨터의 특성에 이동성이라는 특성이 추가된 것이다.


전화기가 더욱 넓은 범주지만 기능은 무선 전화가 더욱 많다고 이해하면 된다.

따라서 전화기가 기초 클래스가 되고, 무선전화기가 유도 클래스가 된다.



has a 관계의 성립


경찰은 총을 소유한다. (has a)


기초 클래스가 Gun, 유도 클래스가 Police


이 경우 의존도(커플링)가 높다.

Police와 Gun이 강한 연관성을 띠게되어, 총을 소유하지 않은 경찰이나 다른 무기를 소유하는 경찰을 표현하기가 쉽지 않아진다.


따라서 이런 has a관계의 상속은 좋지못하며, has a 관계는 포함으로 표현한다. 

따라서 아래와 같이 gun을 police의 멤버로 선언하는 것이 일반적이고 단순하며, 확장성 높은 방법이다.


위와 같은 클래스 정의는 권총을 소유하지 않거나(NULL), 다른 무기를 가진 경찰을 표현하기 쉬워진다.


커플링이 높은 클래스는 유연성이 낮기 때문에 주의해야한다.



728x90

상속(Inheritance)


상속에 대한 과거의 관점

: 기존에 정의해놓은 클래스의 재활용을 만들어진 문법적 요소



하지만 상속은 위와 같이 한 문장으로 정의할 수 있는 것이 아니며, 상속의 이점이 재활용에만 있는 것은 아니다.



데이터의 성격을 지닌 클래스 정의


급여관리 시스템에서 직원의 이름과 급여정보를 저장할 수 있는 클래스 정의


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class PermanentWorker
{
private:
    char name[100];    
    int salary;
public:
    PermanentWorker(char* name, int money)
        : salary(money)
    {
        strcpy(this->name, name);
    }
    int GetPay() const
    {
        return salary;
    }
    void ShowSalaryInfo() const
    {
        cout<<"name: "<<name<<endl;
        cout<<"salary: "<<GetPay()<<endl<<endl;
    }
};
cs


control 클래스(handler 클래스) 정의

: 기능의 처리를 실제로 담당하는 클래스

  • 새로운 직원 정보의 등록                 AddEmployee
  • 모든 직원의 이번 달 급여정보 출력    ShowAllSalaryInfo
  • 이번 달 급여의 총액 출력                ShowTotalSalary

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
class EmployeeHandler
{
private:
    PermanentWorker* empList[50];
    int empNum;
public:
    EmployeeHandler() : empNum(0)
    { }
    void AddEmployee(PermanentWorker* emp)
    {
        empList[empNum++= emp;
    }
    void ShowAllSalaryInfo() const
    {
        for (int i = 0; i<empNum; i++)
            empList[i]->ShowSalaryInfo();
    }
    void ShowTotalSalary() const
    {
        int sum = 0;
        for (int i = 0; i<empNum; i++)
            sum += empList[i]->GetPay();
        cout << "salary sum: " << sum << endl;
    }
    ~EmployeeHandler()
    {
        for (int i = 0; i<empNum; i++)
            delete empList[i];
    }
};
cs

이러한 컨트롤 클래스는 기능 제공의 핵심이기 때문에 OOP에서 반드시 존재하는 클래스이다.



위의 두 클래스를 기반으로 하는 main함수

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int main(void)
{
    // 직원관리를 목적으로 설계된 컨트롤 클래스의 객체생성
    EmployeeHandler handler;
 
    // 직원 등록
    handler.AddEmployee(new PermanentWorker("KIM"1000));
    handler.AddEmployee(new PermanentWorker("LEE"1500));
    handler.AddEmployee(new PermanentWorker("JUN"2000));
 
    // 이번 달에 지불해야 할 급여의 정보
    handler.ShowAllSalaryInfo();
 
    // 이번 달에 지불해야 할 급여의 총합
    handler.ShowTotalSalary();
    return 0;
}
cs


Output :

name: KIM

salary: 1000


name: LEE

salary: 1500


name: JUN

salary: 2000


salary sum: 4500



위의 프로그램은 큰 문제가 없어보이지만, 객체지향뿐만 아니라 소프트웨어 설계에 있어 중요시하는 다음의 요소를 만족하지 않는다.

  • 요구 사항의 변경에 대응하는 프로그램의 '유연성'
  • 기능의 추가에 따른 프로그램의 '확장성'


 

만약 직원의 고용형태가 다양해지고, 급여 계산 방식도 달라진다면 클래스의 추가 및 컨트롤 클래스안에 여러 소스의 추가수정이 불가피해진다.

따라서 이러한 문제는 '상속'을 통해 해결이 가능하다.



상속의 문법적 이해


 현실에서 상속(물려받음)하면 재산만을 떠올리기 쉽지만, 물려받는 것은 재산이 아닌 여러 특성일 수도 있다.


"지수는 아버지로부터 좋은 목소리와 큰 키, 재산을 물려받았다"


class Jisu : public Father    // Father클래스의 상속을 의미 (Jisu 클래스가 Father클래스로부터 물려받음을 의미)



상속의 방법과 그 결과


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Person
{
private:
    int age;        // 나이
    char name[50];     // 이름
public:
    Person(int myage, char * myname) : age(myage)
    {
        strcpy(name, myname);
    }
    void WhatYourName() const
    {
        cout << "My name is " << name << endl;
    }
    void HowOldAreYou() const
    {
        cout << "I'm " << age << " years old" << endl;
    }
};
cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class UnivStudent : public Person    // Person 클래스를 상속 
{
private:
    char major[50];     // 전공과목
public:
    UnivStudent(char * myname, int myage, char * mymajor)
        : Person(myage, myname)
    {
        strcpy(major, mymajor);
    }
    void WhoAreYou() const 
    {
        WhatYourName();
        HowOldAreYou();
        cout<<"My major is "<<major<<endl<<endl;
    }
};
cs



// 다음 선언은 public 상속을 의미

class UnivStudent : public Person // Person 클래스의 상속을 의미

{

. . . .

}


이 때 상속받은 클래스의 객체 UnivStudent 객체의 구성은 다음과 같다.


상속을 하게 되면, 상속의 대상이 되는 클래스의 멤버가 객체내에 포함,

따라서 UnivStudent 클래스내에서 Person 클래스의 멤버 함수까지도 호출이 가능해진다.


상속과 관련된 용어

: Person과 같이 상속의 대상이 되는 클래스와 UnivStudent같이 상속을 하는 클래스를 다음과 같이 여러가지로 표현한다.


Person            ->    UnivStudent

상위 클래스        ->    하위 클래스

기초(base)클래스   ->    유도(derived) 클래스

슈퍼(super)클래스  ->    서브(sub) 클래스

부모 클래스        ->    자식 클래스  



유도 클래스(derived)의 생성자 정의


유도 클래스는 기초(base) 클래스의 초기할 데이터를 인자로 전달받을 책임과,

그 데이터를 기초 클래스의 생성자 호출을 통해 전달할 책임을 진다.


1
2
3
4
5
UnivStudent(char * myname, int myage, char * mymajor)
        : Person(myage, myname)
    {
        strcpy(major, mymajor);
    }
cs

유도 클래스의 생성자는 기초 클래스의 생성자를 호출하는 형태로 멤버 초기화가 이뤄진다.


유도클래스의 생성자에서 이니셜라이저가 의미하는 바는 생성자의 호출이다.

Person클래스의 생성자를 호출하면서 인자로 myage와 myname에 저장된 값을 전달하는 것이다.



상속에 관련된 예제


#include <iostream>
#include <cstring>
using namespace std;

class Person
{
private:
	char name[50];	// 이름
	int age;		// 나이	
public:
	Person(char * myname, int myage)
	{
		strcpy(name, myname);
		age = myage;		
	}
	void WhatYourName() const
	{
		cout << "My name is " << name << endl;
	}
	void HowOldAreYou() const
	{
		cout << "I'm " << age << " years old" << endl;
	}
};

class UnivStudent : public Person
{
private:
	char major[50];     // 전공과목
public:
	UnivStudent(char * myname, int myage, char * mymajor)
		: Person(myname, myage)
	{
		strcpy(major, mymajor);
	}
	void WhoAreYou() const
	{
		WhatYourName();
		HowOldAreYou();
		cout << "My major is " << major << endl << endl;
	}
};

int main(void)
{
	UnivStudent ustd1("Lee", 22, "Computer eng.");
	ustd1.WhoAreYou();

	UnivStudent ustd2("Yoon", 21, "Electronic eng.");
	ustd2.WhoAreYou();
	return 0;
};


Output :

My name is Lee

I'm 22 years old

My major is Computer eng.


My name is Yoon

I'm 21 years old

My major is Electronic eng.



위의 예제에서 보듯 Person의 멤버 name과 age는 private 선언이 되어 있지만
Person 클래스에 정의된 public 함수를 통해 간접적으로 접근이 가능하다.

유도 클래스의 생성자에서 기초 클래스의 생성자를 호출한다.
기초 클래스의 생성자에서는 클래스내 private의 멤버에 접근이 가능하다.

이렇게 생성자의 호출을 통해 기초 클래스의 멤버를 초기화할 수 있는 것이다.



728x90

const 객체와 const 객체의 특성들

 

변수를 상수화 하듯이, 객체도 상수화 가능하다.

const SoSimple sim(20);

 

객체에 const 선언이 붙으면, 이 객체를 대상으로는 const 멤버함수만 호출이 가능하다.

(객체의 데이터 변경을 허용하지 않음)

 

아래 예제에서 const객체 sim의 멤버 함수 AddNum을 호출하려고 할 때, 호출이 불가능함을 확인할 수 있다


 


 

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

#include <iostream>

using namespace std;

 

class SoSimple

{

private:

        int num;

 

public:

        SoSimple(int n) : num(n)

        { }

        SoSimple AddNum(int n)

        {

               num += n;

               return *this;

        }

        void ShowData() const

        {

               cout << "num: " << num << endl;

        }

};

 

 

int main(void)

{

        const SoSimple sim(20);        // const 객체 생성

        // sim.AddNum(30);             // 멤버함수 AddNum const함수가 아니라서 호출 불가

        sim.ShowData();                // 멤버함수 ShowData const함수이니 호출 가능

        return 0;

}

 

Output:

1

num: 20

 

 

const와 함수 오버로딩

함수의 오버로딩이 성립하려면 매개변수 수나 자료형이 달라야 한다.

그 외에 const 선언 유무도 함수 오버로딩의 조건에 해당된다.

 

void SimpleFunc() { . . . . }

void SimpleFunc() const { . . . . }

 

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

#include <iostream>

using namespace std;

 

class SoSimple

{

private:

        int num;

public:

        SoSimple(int n) : num(n)

        { }

        SoSimple& AddNum(int n)

        {

               num += n;

               return *this;

        }

        void SimpleFunc()

        {

               cout << "SimpleFunc: " << num << endl;

        }

        void SimpleFunc() const       // Const Overloading

        {

               cout << "const SimpleFunc: " << num << endl;

        }

};

 

void YourFunc(const SoSimple &obj)    // 전달되는 인자가 const 참조자

{

        obj.SimpleFunc();              // 참조자를 이용한 함수 호출결과로 const멤버함수 호출

}

 

int main(void)

{

        SoSimple obj1(2);

        const SoSimple obj2(7);        // const 객체 생성

 

        obj1.SimpleFunc();    

        obj2.SimpleFunc();     // const 객체를 대상으로 함수를 호출했기에 const 멤버 함수가 호출됨

 

        YourFunc(obj1); // const 멤버 함수 호출

        YourFunc(obj2); // const 멤버 함수 호출

        return 0;

}


Output:

1

2

3

4

SimpleFunc: 2

const SimpleFunc: 7

const SimpleFunc: 2

const SimpleFunc: 7

 

 

friend 선언

: private 멤버의 접근을 허용하는 선언

 

  • A 클래스가 B 클래스를 대상으로 friend 선언을 하면, B 클래스는 A 클래스의 private 멤버에 직접 접근이 가능하다.
  • , A 클래스도 B 클래스의 private 멤버에 직접 접근 가능하려면, B 클래스가 A클래스를 대상으로 friend 선언을 해줘야 한다.

1

2

3

4

5

6

7

8

9

10

11

12

13

class Boy

{

private:

    int height;

    friend class Girl;    // Girl 클래스를 friend 

public:

    Boy(int len) : height(len)

    { }

    void ShowYourFriendInfo(Girl &frn);

};

Colored by Color Scripter

cs

 

Colored by Color Scripter

cs



5행의 friend선언으로 Girl 클래스 내에서는 Boy 클래스의 모든 private 멤버에 직접 접근이 가능해진다.

(물론 Girl 클래스에서 따로 friend 선언을 하지 않으면, Boy 클래스에선 Girl 클래스의 private 멤버 접근이 불가)

 

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

#include <iostream>

#include <cstring>

using namespace std;

 

class Girl;    // Girl이 클래스의 이름임을 알리는 선언

 

class Boy

{

private:

        int height;

        friend class Girl;     // Girl 클래스에 대한 friend 선언 (private 영역에서도 friend선언 가능)

public:

        Boy(int len) : height(len)

        { }

        void ShowYourFriendInfo(Girl &frn);  

};

 

class Girl

{

private:

        char phNum[20];

public:

        Girl(char * num)

        {

               strcpy(phNum, num);

        }

        void ShowYourFriendInfo(Boy &frn);

        friend class Boy;

};

 

void Boy::ShowYourFriendInfo(Girl &frn)

{

        cout << "Her phone number: " << frn.phNum << endl;

}

 

void Girl::ShowYourFriendInfo(Boy &frn)

{

        cout << "His height: " << frn.height << endl;

}

 

int main(void)

{

        Boy boy(170);

        Girl girl("010-1234-5678");

 

        boy.ShowYourFriendInfo(girl);

        girl.ShowYourFriendInfo(boy);

        return 0;

}

 

Output :

Her phone number: 010-1234-5678

His height: 170

 

 

위에서 5행의 클래스 선언이 없이도 컴파일이 되는데 11행의 선언이 Girl이 클래스의 이름임을 알리는 역할도 있기 때문이다.

 

       

friend class Girl;    // Girl은 클래스의 이름이며, Girl 클래스를 friend 선언한다!

 

 

 

friend 선언은 신중해야한다

: friend 선언은 객체지향의 '정보 은닉'에 반하는 선언이므로  신중히 선언해야 한다.

 

friend 선언은 연산자 오버로딩에서 유용하게 쓰이게 된다.

 

 

함수의 friend 선언

전역변수를 대상으로도, 클래스의 멤버 함수를 대상으로도 friend 선언이 가능하다.

friend로 선언된 함수는 자신이 선언된 클래스의 private 영역에 접근 가능하다.


#include <iostream>
using namespace std;
 
class Point;   // Point가 클래스의 이름임을 알림
 
class PointOP
{
private:
        int opcnt;
public:
        PointOP() : opcnt(0)
        {  }
 
        Point PointAdd(const Point&, const Point&);  
        Point PointSub(const Point&, const Point&);  
        ~PointOP()
        {
               cout << "Operation times: " << opcnt << endl;
        }
};
 
class Point
{
private:
        int x;
        int y;
public:
        Point(const int &xpos, const int &ypos) : x(xpos), y(ypos)
        {  }
        friend Point PointOP::PointAdd(const Point&, const Point&);       // PointOp클래스의 멤버함수 PointAdd에 대한 friend 선언
        friend Point PointOP::PointSub(const Point&, const Point&);       // PointOp클래스의 멤버함수 PointSub에 대한 friend 선언
        friend void ShowPointPos(const Point&);       // 58행에 정의된 함수 ShowPointPos에 대한 friend선언
};
 
Point PointOP::PointAdd(const Point& pnt1, const Point& pnt2)
{
        opcnt++;
        return Point(pnt1.x + pnt2.x, pnt1.y + pnt2.y);      // 30행 friend선언으로 인해 private 멤버 접근 가능
}
 
Point PointOP::PointSub(const Point& pnt1, const Point& pnt2)
{
        opcnt++;
        return Point(pnt1.x - pnt2.x, pnt1.y - pnt2.y);      // 31행 friend선언으로 인해 private 멤버 접근 가능
}
 
int main(void)
{
        Point pos1(1, 2);
        Point pos2(2, 4);
        PointOP op;
 
        ShowPointPos(op.PointAdd(pos1, pos2));
        ShowPointPos(op.PointSub(pos2, pos1));
        return 0;
}
 
void ShowPointPos(const Point& pos)
{
        cout << "x: " << pos.x << ", ";       // 32행 friend선언으로 인해 private 멤버 접근 가능
        cout << "y: " << pos.y << endl;       // 32행 friend선언으로 인해 private 멤버 접근 가능
}


Output:

1
2
3
x: 3, y: 6
x: 1, y: 2
Operation times: 2




1

friend void ShowPointPos(const Point&);

cs

이는 friend 선언이외에 함수 원형 선언이 포함되어 있다.

 

void ShowPointPos(const Point&);



C++에서의 static


C에서와 같이 전역변수와 지역변수에 내릴수 있는 static 선언의 의미는 C++에서도 통용된다.


전역변수에 선언된 static

: 선언된 파일내에서만 참조를 허용 

분할 컴파일시, 파일간에 접근이 불가능해진다. (개별화 됨)


지역변수에 선언된 static

: 전역변수의 성격을 지니게 됨, 지역변수와 달리 함수를 빠져나가도 소멸되지 않음

처음 1회만 초기화되고, 프로그램 종료시까지 메모리 상주(전역변수의 특성)

선언된 함수내에서만 접근 가능(지역변수의 특성)



static선언된 변수는 전역변수와 같이 따로 초기화하지 않을 시, 0으로 자동 초기화된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

void Counter()
{		
	static int cnt;	
	cout << "Current cnt: " << cnt << endl;
	cnt++;
}

int main()
{
	for (int i = 0; i<10; i++)
	{
		Counter();
	}	
	return 0;
}


Output:

1
2
3
4
5
6
7
8
9
10
Current cnt: 0
Current cnt: 1
Current cnt: 2
Current cnt: 3
Current cnt: 4
Current cnt: 5
Current cnt: 6
Current cnt: 7
Current cnt: 8
Current cnt: 9

 


 static 멤버 변수 (클래스 변수)

: static 변수는 객체 별로 존재하는 변수가 아니라, 프로그램 전체 영역에서 하나만 존재하는 변수로 

프로그램 실행과 동시에 초기화되어 메모리 공간에 할당된다.



아래 예제의 14행과 같이, 함수밖에서 static 멤버변수의 초기화를 해야한다.

생성자로는 매번 값의 변경이 일어나기 때문에, 별도의 초기화 문법이 제공된 것이다.


#include <iostream>
using namespace std;

class SoSimple
{
public:
	static int simObjCnt;
public:
	SoSimple()
	{
		simObjCnt++;
	}
};
int SoSimple::simObjCnt = 0;

int main(void)
{
	cout << SoSimple::simObjCnt << "번째 SoSimple 객체" << endl;
	SoSimple sim1;
	SoSimple sim2;

	cout << SoSimple::simObjCnt << "번째 SoSimple 객체" << endl;
	cout << sim1.simObjCnt << "번째 SoSimple 객체" << endl;
	cout << sim2.simObjCnt << "번째 SoSimple 객체" << endl;
	return 0;
}

Output:
1
2
3
4
0번째 SoSimple 객체
2번째 SoSimple 객체
2번째 SoSimple 객체
2번째 SoSimple 객체

 

위 예제에서 23,24행과 같이 객체의 이름을 이용해 접근하는 것은 좋지 않다.

멤버 변수에 접근하는 건지 static 변수에 접근하는 건지 혼동될 수 있어 잘 쓰지 않는다.


따라서 22행과 같이 class를 지정해 접근하는 것이 일반적이고 좋은 방법이다. 


또한 6행과 같이 public 선언되어있지않다면 class 외부에서는 static 멤버 변수에 접근이 불가능하다.



static 멤버 함수


- 특징 (static 멤버 변수의 특징과 일치)

  • 선언된 클래스의 모든 객체가 공유한다.
  • public 선언되면, 클래스를 지정해 호출 가능
  • 객체의 멤버로 존재하는 것이 아니다!

위의 그림을 다시 상기해보면 static 변수와 static 함수는 객체의 생성과 아무런 연관이 없으며, 

객체 내부에 존재하는 변수가 아님을 알 수 있다.


객체와 동시에 생성되는 것이 아닌 전역 변수와 같이 앞서 메모리 공간에 할당된 상태이다.


아래 예제에서 14행의 주석을 해제하면 

"비정적 멤버 'SoSimple::num1'에 대한 참조가 잘못되었습니다. " 라는 에러 메시지가 뜨게 된다.


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
#include <iostream>
using namespace std;
 
class SoSimple
{
private:
    int num1;
    static int num2;
public:
    SoSimple(int n) : num1(n)
    { }
    static void Adder(int n)
    {
        // num1+=n;
        num2 += n;
    }
    void ShowData() const
    {
        cout << "num: " << num1 << num2 << endl;
    }
};
int SoSimple::num2 = 0;
 
int main(void)
{    
    return 0;
}
cs

static 함수는 객체내에 존재하는 함수가 아니기 때문에, 멤버 변수나 멤버함수에 접근이 불가능하다.

당연히 static 변수에만 접근 가능하고, static 함수만 호출 가능하다.



const static 멤버


상수 정보를 담기위한 클래스 CountryArea


const static 멤버변수는, 클래스가 정의될때 지정된 값이 유지되는 상수이다. 따라서 이 경우 아래와 같이 선언과 동시에 초기화가 가능하다.


#include <iostream>
using namespace std;

class CountryArea
{
public:
	const static int RUSSIA			=1707540;
	const static int CANADA			=998467;
	const static int CHINA			=957290;
	const static int SOUTH_KOREA	        =9922;
};

int main(void)
{
	cout<<"러시아 면적: "<<CountryArea::RUSSIA<<"㎢"<<endl;
	cout<<"캐나다 면적: "<<CountryArea::CANADA<<"㎢"<<endl;
	cout<<"중국 면적: "<<CountryArea::CHINA<<"㎢"<<endl;
	cout<<"한국 면적: "<<CountryArea::SOUTH_KOREA<<"㎢"<<endl;
	return 0;
}


Output:

1
2
3
4
러시아 면적: 1707540㎢
캐나다 면적: 998467㎢
중국 면적: 957290㎢
한국 면적: 9922㎢



mutable 선언

: mutable로 선언된 멤버 변수는 const 함수내에서도 값의 변경이 가능


#include <iostream>
using namespace std;

class SoSimple
{
private:
	int num1;
	mutable int num2;
public:
	SoSimple(int n1, int n2)
		: num1(n1), num2(n2)
	{  }
	void ShowSimpleData() const
	{
		cout<<num1<<", "<<num2<<endl;
	}
	void CopyToNum2() const
	{
		num2=num1;
	}
};

int main(void)
{
	SoSimple sm(1, 2);
	sm.ShowSimpleData();
	sm.CopyToNum2();
	sm.ShowSimpleData();
	return 0;
}


Output:

1
2
1, 2
1, 1

mutable의 선언은 신중해야 한다!
: 이와 같은 선언은 const 선언에 반하기 때문에, 타당한 이유를 가지고 가급적 제한적으로 선언하는 것이 좋다.


'Study > C++' 카테고리의 다른 글

상속의 이해  (0) 2016.10.13
상속  (0) 2016.09.30
복사 생성자, 얕은 복사와 깊은 복사, 임시 객체  (0) 2016.09.28
객체 배열과 this포인터  (0) 2016.09.27
생성자와 소멸자, 클래스  (0) 2016.09.26
728x90

C++ 스타일의 초기화

int num(20); // num=20;

int &ref(num); // &ref=num;


SoSimple sim2=sim1;

바로 위 문장은,

다음 문장과도 동일하게 해석된다.

SoSimple sim2(sim1);


객체 단위의 대입은 멤버 대 멤버의 복사가 일어난다. 이는 복사 생성자의 정의에 따라 달라질 수 있다.


 복사 생성자가 정의되지 않은 SoSimple 클래스
1
2
3
4
5
6
7
8
9
class SoSimple
{
private:
    int num;
public:
    SoSimple(int n) : num(n)
    {  }
    . . . .
};
cs

복사 생성자를 정의하지 않으면, 멤버 대 멤버 복사가 이뤄지는
default 복사 생성자가 아래와 같이 자동 삽입된다. (8-9행)
1
2
3
4
5
6
7
8
9
10
class SoSimple
{
private:
    int num;
public:
    SoSimple(int n) : num(n)
    {  }
    SoSimple(const SoSimple& copy) : num(copy.num)
    {  }
};
cs

복사 생성자를 활용한, 객체 단위의 대입 예제
#include <iostream>
using namespace std;

class SoSimple
{
private:
	int num1;
	int num2;
public:
	SoSimple(int n1, int n2)
		: num1(n1), num2(n2)
	{
		// empty
	}

	SoSimple(const SoSimple &copy)	// 복사 생성자의 정의
		: num1(copy.num2), num2(copy.num1)	// num1, num2값을 swap하도록 이니셜라이저
	{
		cout << "Called SoSimple(SoSimple &copy)" << endl;
	}

	void ShowSimpleData()
	{
		cout << num1 << endl;
		cout << num2 << endl;
	}
};

int main(void)
{
	SoSimple sim1(15, 30);
	sim1.ShowSimpleData();
	cout << "생성 및 초기화 직전" << endl;	
	SoSimple sim2(sim1);	// SoSimple sim2=sim;
	cout << "생성 및 초기화 직후" << endl;
	sim2.ShowSimpleData();
	return 0;
}


Output:

1
2
3
4
5
6
7
15
30
생성 및 초기화 직전
Called SoSimple(SoSimple &copy)
생성 및 초기화 직후
30
15



키워드 explicit

: 묵시적 형 변환을 막는 키워드


SoSimple sim2=sim1;

위의 문장은, 

아래와 같이 묵시적 형 변환이 일어난다.

SoSimple sim2(sim1); 


위와 같은 묵시적 형 변환은 복사 생성자를 explicit으로 선언하면 막을 수 있다.

SoSimple sim2=sim1; 과 같은 형태의 객체 생성과 초기화를 막아줌


1
2
3
explicit SoSimple(const SoSimple &copy)
    : num1(copy.num1), num2(copy.num2)
{ }
cs


이는 복사 생성자 뿐만 아닌 생성자도 마찬가지이다.

1
2
3
4
5
6
7
8
class AAA
{
private:
    int num;
public:
    explicit AAA(int n) : num(n) { }
    . . . .
};
cs

AAA 생성자를 explicit 선언하면

다음과 같은 형태의 객체 생성은 불가능해진다.


AAA obj=3;

이는 아래와 같이 묵시적 형 변환이 이뤄지기 때문이다.

AAA obj(3);


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
#include <iostream>
using namespace std;
 
class SoSimple
{
private:
    int num1;
public:
    explicit SoSimple(int n1) // 생성자를 explicit 선언
        : num1(n1)
    {
        // empty
    }
    void ShowSimpleData()
    {
        cout << "num1 : " << num1 << endl;
    }
};
 
int main(void)
{
    SoSimple sim1(15);
    //SoSimple sim1=15;
    sim1.ShowSimpleData();    
    return 0;
}
cs

위의 소스에서 22행을 주석처리 후 23행의 주석 부분을 해제해  컴파일해보면, SoSimple로 변환하기 위한 적절한 생성자가 없으며,

초기화 중': 'int'에서 'SoSimple'(으)로 변환할 수 없다는 오류를 보게 된다.


c++의 대입의 의미가 다른 부분인데, 묵시적 호출을 원하지 않는다면 이렇게 explicit 키워드를 활용할 수 있다.




얕은 복사 (default 복사 생성자의 문제점)

#include <iostream>
#include <cstring>
using namespace std;

class Person
{
	char * name;
	int age;
public:
	Person(char * myname, int myage)	// 생성자 정의
	{
		int len = strlen(myname) + 1;
		name = new char[len];	// new 키워드를 통한 동적 할당
		strcpy(name, myname);
		age = myage;
	}
	void ShowPersonInfo() const
	{
		cout << "이름: " << name << endl;
		cout << "나이: " << age << endl;
	}
	~Person()
	{
		delete[]name;	// delete 키워드를 이용한 메모리 공간 해제
		cout << "called destructor!" << endl;
	}
};

int main(void)
{
	Person man1("Lee dong woo", 29);
	Person man2 = man1;		// default 복사 생성자에 의한 멤버대 멤버 복사가 이뤄짐
	man1.ShowPersonInfo();	
	man2.ShowPersonInfo();
	return 0;
}


Output :

이름: Lee dong woo

나이: 29

이름: Lee dong woo

나이: 29

called destructor!


위 예제 실행시 에러가 나며, 실행결과를 보면 문제점을 볼 수 있다.

25행에는 소멸자의 호출할 확인할 문장을 출력하도록 한 것을 볼 수 있는데, 실행결과에서는 소멸자가 딱 한번 호출된다.


이는 default 복사 생성자 호출에 의한 당연한 결과이다.


man1 객체의 name은 문자열이 저장된 메모리 공간은 가리키는 형태가 되며, 복사된 man2 객체의 name도 마찬가지로 이 메모리 공간을 가리킬 뿐이다. 

참조만 하는 멤버 name을 단순 복사했기 때문에 하나의 문자열을 두 객체가 가리키게 된 것이다.


또한 man1 객체가 소멸될 때 man1 객체의 멤버 name이 참조하는 문자열이 사라져버린다. ( delete [ ]name; )

이후 man2 객체가 소멸될 때 다음의 문장은 문제가 된다.


 delete [ ]name; 


이미 지워진 문자열을 대상으로 delete 연산을 하려 하는 것이다. 따라서 복사 생성자를 새롭게 정의해 이러한 문제를 해결 할 필요가 있다.



'깊은 복사'를 위한 복사 생성자의 정의

동일한 문자열을 포인터 참조하는 것이 아닌 객체의 문자열 자체를 복사해 참조하도록 해야한다.

따라서 이 경우 인자는 &ref 가 되게 된다.

1
2
3
4
5
6
7
Person(const Person &ref)     
        : age(ref.age)  // 이니셜라이저를 통한 초기화  
    {
        int len = strlen(ref.name) + 1;
        name = new char[len];    // new 키워드를 통한 동적 할당
        strcpy(name, ref.name);        
    }
cs


아래에서 깊은 복사를 위한 복사 생성자를 정의한 실행결과를 확인해볼 수 있다.

#include <iostream>
#include <cstring>
using namespace std;

class Person
{
	char * name;
	int age;
public:
	Person(char * myname, int myage)	// 생성자 정의
	{
		int len = strlen(myname) + 1;
		name = new char[len];	// new 키워드를 통한 동적 할당
		strcpy(name, myname);
		age = myage;
	}
	Person(const Person &copy)	// 깊은 복사를 위한 복사 생성자의 정의
		: age(copy.age)	
	{
		int len = strlen(copy.name) + 1;
		name = new char[len];	// new 키워드를 통한 동적 할당
		strcpy(name, copy.name);		
	}
	void ShowPersonInfo() const
	{
		cout << "이름: " << name << endl;
		cout << "나이: " << age << endl;
	}
	~Person()
	{
		delete[]name;	// delete 키워드를 이용한 메모리 공간 해제
		cout << "called destructor!" << endl;
	}
};

int main(void)
{
	Person man1("Lee dong woo", 29);
	Person man2 = man1;		// 깊은 복사를 위한 복사 생성자 추가로 깊은 복사가 이뤄짐
	man1.ShowPersonInfo();
	man2.ShowPersonInfo();
	return 0;
}

Output :

이름: Lee Chul Soo

나이: 30

이름: Lee Chul Soo

나이: 30

called destructor!

called destructor!



복사 생성자의 호출 시점


1. 기존에 생성된 객체를 이용해 새로운 객체를 초기화는 경우


Person man1("Lee Chul Soo");

Person man2(man1); // 복사 생성자 호출


이 경우 man1과 man2의 멤버대 멤버 복사가 일어남은 앞의 예제에서 보았다.



2. Call-by-value 방식의 함수 호출 과정에서 객체를 인자로 전달하는 경우


#include <iostream>
using namespace std;

class SoSimple
{
private:
	int num;
public:
	SoSimple(int n) : num(n)
	{  }
	SoSimple(const SoSimple& copy) : num(copy.num)
	{
		cout << "Called SoSimple(const SoSimple& copy)" << endl;
	}
	void ShowData()
	{
		cout << "num: " << num << endl;
	}
};

void SimpleFuncObj(SoSimple ob)
{
	ob.ShowData();
}

int main(void)
{
	SoSimple obj(7);
	cout << "함수호출 전" << endl;
	SimpleFuncObj(obj);
	cout << "함수호출 후" << endl;
	return 0;
}


Output:

1
2
3
4
함수호출 전
Called SoSimple(const SoSimple& copy)
num: 7
함수호출 후


위의 예제 30행에서 아래와 같이 obj 객체가 생성 후 SimpleFuncObj 함수가 호출 될 때, 

obj의 복사 생성자가 호출되고 obj와 ob의 멤버대 멤버 복사가 일어나게 된다.


함수 호출과 함께 복사가 이뤄지는 형태는 다음 그림과 같다.





3. 객체를 반환하되, 참조형으로 반환하지 않는 경우



아래 예제에서는 obj객체 생성후 SimpleFuncObj 함수 호출과정에서 obj 객체를 인자로 전달하고 있다.


또한 함수 호출과정에서 ob 객체가 반환된다.

객체의 반환이란건 객체의 생성과 함께, 함수가 호출된 영역으로 복사해 넘긴다는 뜻과 같다.


객체가 반환이 될 때는 임시 객체 (Temporary Object)가 생성되게 된다.


이러한 임시객체의 특성은 리터럴 상수와도 비슷하다. (int num=3+4; 에서 3과 4)

메모리 공간에 할당이 되지만 금방 사라지게 될 객체이다.


AddNum 함수의 반환형이 참조형(SelfRef &)이므로 객체 자신을 참조할 수 있는 참조자를 반환함에 유의하자. (반환된 참조값을 가지고 함수를 연이어 호출)


#include <iostream>
using namespace std;

class SoSimple
{
private:
	int num;
public:
	SoSimple(int n) : num(n)
	{ }
	SoSimple(const SoSimple& copy) : num(copy.num)
	{
		cout << "Called SoSimple(const SoSimple& copy)" << endl;
	}
	SoSimple& AddNum(int n)
	{
		num += n;
		return *this;
	}
	void ShowData()
	{
		cout << "num: " << num << endl;
	}
};

SoSimple SimpleFuncObj(SoSimple ob)
{
	cout << "return 이전" << endl;
	return ob;
}

int main(void)
{
	SoSimple obj(7);
	SimpleFuncObj(obj).AddNum(30).ShowData();
	obj.ShowData();
	return 0;
}


Output:

1
2
3
4
5
Called SoSimple(const SoSimple& copy)
return 이전
Called SoSimple(const SoSimple& copy)
num: 37
num: 7



반환할 때 만들어진 객체의 소멸시점

#include <iostream>
using namespace std;

class Temporary
{
private:
	int num;
public:
	Temporary(int n) : num(n)
	{
		cout << "create obj: " << num << endl;
	}
	~Temporary()
	{
		cout << "destroy obj: " << num << endl;
	}
	void ShowTempInfo()
	{
		cout << "My num is " << num << endl;
	}
};

int main(void)
{
	Temporary(100);
	cout << "********** after make!" << endl << endl;

	Temporary(200).ShowTempInfo();
	cout << "********** after make!" << endl << endl;

	const Temporary &ref = Temporary(300);	// 참조값이 반환되므로 참조자로 참조 가능하다.
	cout << "********** end of main!" << endl << endl;
	return 0;
}


Output:

1
2
3
4
5
6
7
8
9
10
11
12
13
create obj: 100
destroy obj: 100
********** after make!

create obj: 200
My num is 200
destroy obj: 200
********** after make!

create obj: 300
********** end of main!

destroy obj: 300


위의 예제에서 객체 생성은 하지만 객체의 이름이 없으며, 이는 임시 객체의 생성을 뜻한다.


하지만 참조자를 이용하면 반환된 참조값을 이용해 참조 가능하다.


실행 결과를 보면 참조자에 참조되는 임시객체는 메모리 공간이 바로 소멸되지 않음을 알 수 있다.

return 전에 소멸자의 호출됨을 출력된 문장을 통해 확인이 가능하다.

'Study > C++' 카테고리의 다른 글

상속  (0) 2016.09.30
const, friend, static, mutable 선언  (0) 2016.09.29
객체 배열과 this포인터  (0) 2016.09.27
생성자와 소멸자, 클래스  (0) 2016.09.26
class  (0) 2016.09.25

+ Recent posts