블로그 이미지
Every unexpected event is a path to learning for you.

카테고리

분류 전체보기 (2737)
Unity3D (817)
Programming (474)
Server (33)
Unreal (4)
Gamebryo (56)
Tip & Tech (228)
협업 (58)
3DS Max (3)
Game (12)
Utility (136)
Etc (96)
Link (32)
Portfolio (19)
Subject (90)
iOS,OSX (53)
Android (14)
Linux (5)
잉여 프로젝트 (2)
게임이야기 (3)
Memories (20)
Interest (38)
Thinking (38)
한글 (30)
PaperCraft (5)
Animation (408)
Wallpaper (2)
재테크 (18)
Exercise (3)
나만의 맛집 (3)
냥이 (10)
육아 (16)
Total
Today
Yesterday
04-25 09:56

String

Programming/C/C++ / 2010. 3. 22. 22:05
반응형
Posted by blueasa
, |

STL

Programming/STL / 2010. 3. 22. 22:04
반응형

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

C++ STL int -> string, string -> int 로 변환하기  (1) 2010.04.27
괜찮은 참고 사이트  (0) 2010.04.20
effective_stl-hermet  (0) 2010.04.08
STL Container 조합하기  (0) 2010.03.21
About STL : C++ STL 프로그래밍  (0) 2010.03.21
Posted by blueasa
, |

STL 에서 하나의 Container를 선택하는 방법은 간단합니다.

 

  • vector – 맨 뒤에만 추가할 경우 순차 검색에 유리
  • dequeue – 앞 뒤로 추가할 경우 및 순차 검색에 유리
  • map – 검색이 필요할 경우 유리
  • list – 데이터의 삽입과 삭제가 빈번할 경우 유리

와 같은 식으로 간단하게 선택할 수 있습니다.

하지만가끔가다 보면 위에 Container 의 특징을 하나 이상 만족해야 할 경우가 있습니다특히 검색도 빨라야 하면서초기에 주어진 순서를 그대로 유지해야 하는 경우가 그렇습니다.

간단하게 생각하면 map 와 vector 를 같이 사용하면 되지 않나 싶은데요

몇 가지 고려해야 할 경우가 있습니다.

우선 설명을 더 진행하기 전에 예제에서 사용할 간단한 더미 구조체를 하나 선언하겠습니다단순 설명을 위한 자료구조이니 구조체는 신경 쓰지 말아 주세요. ~


struct DATA

{

           string sKey;

           char szDummy[20];

           int nDummy;

};

 

1.     vector  map 의 조합

먼저 생각할 수 있는 가장 흔한 방법입니다순차 검색을 위해 데이터는 vector 에 보관하고, vector 내의 데이터 인덱스를map으로 관리하는 방법인데요

코드를 나타내면 아래처럼 생각할 수 있습니다.


vector<DATA> m_vtData;

map<string, vector<DATA>::iterator> m_mapVtData;


데이터를 넣어 줘야겠지요아래처럼 vector  DATA 를 넣으면서 map 에는 vector 의 iterator 값을 넣어줘 봤습니다.

bool CstlcontainerDlg::InitData()

{

           DATA data;

           char szBuf[10];

           for (int i = 0 ; i < 1000; i++)

           {

                     sprintf_s(szBuf, sizeof(szBuf), "%03d", i);

                     data.sKey = szBuf;

                     data.nDummy  = i;

                     strcpy_s(data.szDummy, sizeof(data.szDummy), szBuf);

                     m_vtData.push_back(data);

                     vector<DATA>::iterator it = m_vtData.end();

                     it--;

                     m_mapVtData.insert(make_pair(data.sKey, it));

           }

 

           return true;

}

 


이 코드가 잘 작동하는지 아래처럼 검증해 볼 수 있습니다.


void CstlcontainerDlg::OnBnClickedButton2()

{

           // TODO: Add your control notification handler code here

           map<string, vector<DATA>::iterator>::iterator it = m_mapVtData.begin();

           map<string, vector<DATA>::iterator>::iterator itEnd = m_mapVtData.end();

           for (; it != itEnd; it++)

           {

                     TRACE("key Value -> %s\n", it->second->sKey.c_str());

           }

}


잘 작동할까요이미 눈치 채셨겠지만 이 방법은 프로그램이 바로 죽습니다. vector 에서는 iterator 값이 container 의 사이즈가 증가할 때 완전히 달라질 수가 있기 때문에 위 예제와 같이 vector  push_back 을 통해 계속 데이터를 추가하면서 그때 마다 iterator를 저장해서 사용할 경우 아무 의미 없는 데이터를 가리키게 되어 결국 프로그램이 죽게 됩니다.

프로그램을 죽지 않게 하기 위해서는 vector 에 모든 데이터를 넣은 후에, map  iterator 를 넣어주는 방법으로 하면 됩니다.


bool CstlcontainerDlg::InitData()

{

           DATA data;

           char szBuf[10];

           // 우선vector 에아무값이나넣습니다.

           for (int i = 0 ; i < 1000; i++)

           {

                     sprintf_s(szBuf, sizeof(szBuf), "%03d", i);

                     data.sKey = szBuf;

                     data.nDummy  = i;

                     strcpy_s(data.szDummy, sizeof(data.szDummy), szBuf);

                     m_vtData.push_back(data);

           }

          

           // 채워진vector iterator map 에저장합니다.

           vector<DATA>::iterator it = m_vtData.begin();

           vector<DATA>::iterator itEnd = m_vtData.end();

           for (; it != itEnd; ++it)

           {

                     m_mapVtData.insert(make_pair(it->sKey, it));

           }

           return true;

}


만약데이터가 초기에 로딩된 후에 더 이상 삽입이나 삭제가 되지 않을 경우는 이 방법으로 충분합니다하지만 데이터를 더 추가해야 하거나삭제할 경우 모든 map  iterator 를 다시 갱신해야 하는 오버헤더가 따릅니다.


2.     list  map 의 조합

제가 생각하는 가장 좋은 조합입니다.

list  vector 와 달리 모든 iterator 가 삽입삭제에 무관하게 유지 되기 때문에 list::iterator 를 이용하면 훌륭한 데이터 위치 추적이 가능합니다.

이번에는 list  list 의 iterator 를 이용하는 샘플을 만들어 보겠습니다.


list<DATA> m_lstData;

map<string, list<DATA>::iterator> m_mapLstData;


초기 데이터를 넣는 방법은 vector  map 에서와 같습니다아래처럼 짝퉁 키를 만들어서 리스트에 넣고리스트의 iterator를 구해서 map 에 추가했습니다.


bool CstlcontainerDlg::InitData()

{

           DATA data;

           char szBuf[10];

           for (int i = 0 ; i < 1000; i++)

           {

                     sprintf_s(szBuf, sizeof(szBuf), "%03d", i);

                     data.sKey = szBuf;

                     data.nDummy  = i;

                     strcpy_s(data.szDummy, sizeof(data.szDummy), szBuf);

 

                     m_lstData.push_back(data);

                     list<DATA>::iterator itList = m_lstData.end();

                     itList--;

                     m_mapLstData.insert(make_pair(data.sKey, itList));

           }

           return true;

}


이제 map 으로 순차 검색했을 때 list 의 데이터가 제대로 들어 있는지 테스트 코드를 돌려 봤습니다.


void CstlcontainerDlg::OnBnClickedButton3()

{

           // TODO: Add your control notification handler code here

           map<string, list<DATA>::iterator>::iterator it = m_mapLstData.begin();

           map<string, list<DATA>::iterator>::iterator itEnd = m_mapLstData.end();

           for (; it != itEnd; it++)

           {

                     TRACE("key Value -> %s\n", it->second->sKey.c_str());

           }

}


문제 없이 잘 작동합니다 ^^;

이제 리스트에서 키 값이 “887” 인 항목을 지우려고 합니다키 값 검색을 빠르게 하기 위해 당연히 map 으로 검색하고검색된 데이터는 map 과 리스트에서 모두 제거했습니다.


void CstlcontainerDlg::OnBnClickedButton4()

{

           // TODO: Add your control notification handler code here

           string sKey = "887";

           map<string, list<DATA>::iterator>::iterator it = m_mapLstData.find(sKey);

           if (it == m_mapLstData.end())

                     return;

           TRACE("key Value -> %s\n", it->second->sKey.c_str());

           m_lstData.erase(it->second);

           m_mapLstData.erase(it);

 

           it = m_mapLstData.begin();

           map<string, list<DATA>::iterator>::iterator itEnd = m_mapLstData.end();

           for (; it != itEnd; it++)

           {

                     TRACE("key Value -> %s\n", it->second->sKey.c_str());

           }

           TRACE("\nLIST SIZE->%d, MAP<LIST> SIZE -> %d\n"       m_lstData.size(), m_mapLstData.size());

 

}


역시 잘 작동합니다리스트 순차조회의 경우 아시는 것처럼 vector<> 의 순차 검색보다 분명 느린 단점이 있습니다하지만 깨지지 않는 iterator 를 통해 map 과 함께 사용될 경우 안정성을 보장받을 수 있는 장점이 있습니다.

 

3.     기타 조합

그 외에도 데이터를 new 한 다음 그 포인트 값을 vector  map 에 보관시켜 검색과 순차조회를 하는 방법도 물론 있습니다.


vector<DATA*> m_vtData;

map<string, DATA*> m_mapVtData;


이 경우 단점은 new 와 delete 의 책임이 프로그래머가 가지게 된다는 단점이 있고데이터를 삭제해야 할 경우에는 vector의 모든 데이터를 뒤져야 하기 때문에 삭제가 필요한 경우에는 적합하지 않습니다.

 

이상 간단하게 STL 의 컨테이너 선택에 대해 조금 생각해 본 내용인데요또 다른 좋은 방법이 있을지도 모르겠습니다고수님들 혹시 이 글을 읽으신다면 조언 부탁 드리겠습니다. ^^;



반응형

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

C++ STL int -> string, string -> int 로 변환하기  (1) 2010.04.27
괜찮은 참고 사이트  (0) 2010.04.20
effective_stl-hermet  (0) 2010.04.08
STL  (0) 2010.03.22
About STL : C++ STL 프로그래밍  (0) 2010.03.21
Posted by blueasa
, |
반응형

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

C++ STL int -> string, string -> int 로 변환하기  (1) 2010.04.27
괜찮은 참고 사이트  (0) 2010.04.20
effective_stl-hermet  (0) 2010.04.08
STL  (0) 2010.03.22
STL Container 조합하기  (0) 2010.03.21
Posted by blueasa
, |
반응형
Posted by blueasa
, |
반응형

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

투영 벡터 ( Projection Vector )  (0) 2012.05.16
동차좌표 ( 同次座標, Homogeneous coordinate )  (0) 2012.05.16
[펌] 3D공간구조 기본충돌  (0) 2011.09.08
좌표계  (0) 2010.07.01
행렬식(Determinant)  (0) 2010.03.17
Posted by blueasa
, |

C++ 컴파일러는 가상 함수를 호출할때 자료형을 참조하는 것이 아니라, 객체 포인터를 이용해 함수를 호출하게 되는데, 이를 구현하기 위해 C++ 컴파일러는 컴파일 단계에서 가상 함수를 가지는 모든 클래스에 대해 가상 함수 테이블을 생성한다. 이 가상 함수 테이블 내의 각 항목은 정의된 가상 함수를 가리킨다. C++ 컴파일러는 가상 함수를 가지는 클래스의 객체에 대해 객체가 할당된 메모리의 처음 4바이트에 가상 함수 테이블의 주소를 저장한다..

 가상함수 테이블 포인터(vptr)을 이용해 가상함수를 호출하기 때문에 객체의 처음 4바이트가 어떤 클래스의 가상 함수 테이블을 가리키는지에 따라 다른 가상 함수를 테이블이 구체적으로 어떻게 생성되고 객체는 자신의 처음 4바이트를 어떻게 초기화시키는지 알아보자

 가상 함수 테이블이 클래스 단위로 생성된다고 했는데, C++ 컴파일러가 가상함수 테이블을 클래스 단위로 생성하는 것은 객체의 메모리 크기를 줄이기 위해서 이다. 만약 객체 메모리 영역에 직접 가상 함수 포인터를 생성한다면 가상 함수 개수에 비례하여 객체의 크기는 늘어날 것이다.

어차피 동일한 클래스로 생성된 객체들은 동일 가상 함수를 호출해야 하기 때문에 이것은 메모리 낭비가 아닐수 없다.

 그래서 C++ 컴파일러는 가상함수를 가리키는 포인터를 테이블로 구성하고 이것을 클래스 단위로 생성한 다음 이 테이블에 대한 포인터를 객체 맨 앞에 4바이트에 설정함으로서 간단하게 객체에 가상함수 테이블을 포함하는 효과를 가질수 있도록 한다. 이렇게 C++ 컴파일러가 가상함수 테이블을 관리하기 때문에 가상함수를 가지는 클래스는 가상함수의 개수에 상관없이 객체의 크기는 항상 멤버 변수의 총 크기에 4바이트를 더한 크기로 생성될수 있게 된다.

 가상 함수에 대한 모든 원리는 테이블과 관련이 있기 때문에 이것에 대해 좀더 알아보자

가상 함수 테이블의 크기는 해당 클래스에 정의된 가상함수 개수에 비례한다.

여기서 정의된 가상함수란 상위 클래스에 정의된 가상함수도 포함한다.

그래서 상위 클래스에 두개의 가상함수가 정의되고, 자신의 클래스에 하나의 가상 함수가 정의 된다면 가상 함수 테이블 내의 항목은 상위 클래스의 가상 함수를 가리킨다. 너무나 당연한 이야기지만 가상 함수 테이블 내의 항목은 재정의 된 가상 함수를 가리킨다. 가상 함수 테이블의 이런 특징 때문에 어떤 클래스가 가상 함수를 상속 받아 가상 함수를 재정의 하지 않고 객체를 생성하여 해당 가상 함수를 호출하면 상위 클래스에 정의된 가상 함수가 호출된다. 


반응형

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

String  (0) 2010.03.22
enum 보다 나은 enum  (0) 2010.03.21
[펌] C++, 가상함수,순수가상함수  (2) 2010.03.14
[펌] 프로그램, 프로세스, 스레드  (0) 2010.03.13
[펌] 메모리 할당자의 단편화 문제점  (0) 2010.03.13
Posted by blueasa
, |
반응형

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

투영 벡터 ( Projection Vector )  (0) 2012.05.16
동차좌표 ( 同次座標, Homogeneous coordinate )  (0) 2012.05.16
[펌] 3D공간구조 기본충돌  (0) 2011.09.08
좌표계  (0) 2010.07.01
역행렬(Inverse Matrix)  (0) 2010.03.17
Posted by blueasa
, |

[ 가상함수(Virtual Function) ]

1. 가상함수 개요

■ 가상함수(Virtual Function)란?

- 실행시 사용된 객체에 의해 실행 코드가 결정되는 함수

- 지정자 virtual로 선언됨 지정자 virtual은 선언문에 존재함.

class Animal {

  public :

    virtual void breathe();

};

 

class Fish : public Animal {

   public :

    virtual void breathe();

};

 

■ 가상 함수의 호출

- 기반클래스의 포인터가 파생클래스의 객체를 가리킬 때, 그 포인터를 통해 가상함수를 호출하면 파생 클래스에서 재정의한 함수가 호출됨.

Animal* a = new Fish;

a -> breathe();    // Fish::breathe 호출

→ a->breathe(); 는 Fish::breathe 를 호출함. 즉, a->breathe(); 는 함수명으로는 Animal::breathe를 호출하지만, 내용으로는 Fish::breathe 가 호출되는 것임.

 

■ 기반클래스의 멤버함수가 virtual 을 갖고 있을 때

- 파생클래스에서 재정의한 함수는 virtual 을 생략해도 저절로 가상함수가 됨.

- 그러나 기반클래스의 가상 함수를 파생클래스에서 재정의할  때도 virtual 을 명시해주는 것이 관례임. (소스코드 이해가 수월하기 위해서...)

 

2. 가상함수의 특징

■ 동적바인딩(Dynamic Binding)

- 가상함수는 동적결합(Dynamic Binding)을 함. (코드만 봐서는 어떤 코드가 실행될지 정해지지 않은 상태. 즉, 객체를 생성하는 시점에 생성조건에 따라 실행코드가 결정된다.)

- 일반함수는 정적 결합을 하는 반면, 가상 함수는 동적 결합을 함.

 

■ 가상함수(동적 바인딩)의 장단점

- 장점 : 실행시 다형성(Runtime Polymorphism) 이라는 융통성을 갖게 됨. (상황에 생성하는 조건에 따라 실행 코드를 변경할 수 있다.)

- 단점 : 가상함수를 사용하면 실행속도에서는 손해

  

3. 가상 함수 테이블 (Virtual Function Table)

■ vtable (Virtual Function Table) 이란?

- 가상 함수에 대한 포인터 배열

- 가상 함수를 사용하는 클래스의 각 객체는 vtable을 가리키는 vptr이라는 숨겨진 포인터를 갖고 있음.

- 가상함수를 포함한 클래스의 객체를 생성할 때 vtable이 만들어짐.

- 객체에서 가상함수를 호출하면 vptr를 통해 vtable에 있는 해당함수를 찾아 수행시킴.

 

■ 파생클래스에서의 vtable

- 기반클래스의 객체로부터 vtable을 물려 받아 수정 및 확장함.

- 파생클래스에서 재정의한 가상함수가 있다면 기반클래스의 가상 함수에 대한 포인터가 들어 있던 곳을 파생클래스에서 재정의한 가상함수에 대한 포인터로 변경함.

- 기반클래스의 포인터가 파생클래스의 오브젝트를 가리킬 때 그 포인터를 통해 가상 함수를 호출하면 파생클래스에서 수정한 vtable 을 사용하게 되므로 파생 클래스에서 재정의한 함수가 호출됨.

 

4. 가상 소멸자

■ 상속 관계에서 기반클래스의 포인터가 파생클래스의 동적 객체를 가리킬 때 그 포인터를 통해 동적 객체를 제거하면...?

기본적으로 기반 클래스의 소멸자만 호출되고 파생 클래스의 소멸자는 호출되지 않음.

class Animal {

};

 

class Fish : public Animal {

};

 

Animal* a = new Fish;

delete a;

 

■ 기반클래스의 포인터가 파생클래스의 동적 객체를 가리키고, 그 포인터를 통해 동적 객체를 제거했을 때 파생클래스의 소멸자까지 호출되도록 하려면...?

기반 클래스의 소멸자를 다음과 같이 가상 소멸자(Virtual Destructor)로 만들어야 함.

class Animal {

  public :

    Animal();

    virtual ~Animal();

};

 

class Fish : public Animal {

  public :

    Fish();

    virtual ~Fish();

};

 

Animal* a = new Fish;

delete a;

→ delete a;는 Animal::~Animal 뿐만 아니라 Fish::~Fish 도 호출함.

 

[ 순수가상함수(Pure Virtual Function) ]

1. 순수 가상 함수

■ 순수 가상 함수 (Pure Virtual Function) 란?

- 선언만 있고 정의가 없는 가상 함수

- 파생 클래스에서 재정의할 것으로 예상되는 함숭에 대해 미리 호출 계획을 세워 두기 위해 정의.

 

■ 가상함수를 순수 가상함수로 만드는 방법

- 다음의 예와 같이 선언시 0을 지정하면 됨.

- 여기서 '=0'은 Pure Specifier임.

class Animal {

  public :

    virtual void breathe() = 0;

};

 

class Fish : public Animal {

  public :

    Fish();

    virtual void breathe();

};

→ Animal::breathe 는 순수 가상함수임. Animal::breathe 는 함수명만 제공하고, 내용은 Fish::breathe 에서 제공하게 됨.

 

2. 추상클래스 (Abstract Class)

■ 추상클래스란?

- 순수 가상함수를 포함하고 있는 클래스.

- 파생클래스의 행동양식을 미리 설계하기 위한 기반클래스임.

- 일반적으로 순수 가상함수를 통해 인터페이스를 제공하고 파생클래스에서 그 인터페이스를 구현하게 됨.

class Animal { // 추상클래스 Animal

  public :

    virtual void breathe() = 0;

};

 

■ 추상클래스의 특징

- 추상클래스의 객체를 생성하는 것은 불가능. ∵ 정의되지 않은 함수를 가지고 있기 때문...

- 추상클래스는 파생클래스를 정의하여 순수가상함수를 구현하였을 때 객체를 생성할 수 있다.

- 추상클래스의 포인터를 선언하는 것은 가능함.

- 추상클래스의 포인터는 흔히 파생 클래스의 객체를 가리키게 되는데, 추상클래스의 포인터가 파생클래스의 객체를 가리킬 때 그 포인터를 통해 가상함수를 호출하면 파생클래스에서 재정의한 함수가 호출됨.

Animal* a = new Fish;

a -> breathe(); // 추상클래스 Animal

delete a;

 

2. 순수 가상함수의 재정의

■ 순수 가상함수의 재정의

- 순수 가상함수는 사용하기 전에 반드시 재정의해야 함.

- 만일 기반 클래스가 순수 가상 함수를 포함하고 있는데 파생 클래스에서 그것을 재정의하지 않는다면 그 파생 클래스도 추상 클래스가 됨.

→ 기반클래스 뿐만 아니라, 그 파생클래스의 객체를 생성하는 것도 불가능 함.

class Animal { // 추상클래스 Animal

  public :

    virtual void breathe() = 0;

};

 

class Fish : public Animal {

  public :

    void swim();

};

 

Animal* a = new Animal; // error

Animal* f = new Fish;  // error

 

출처 : http://blog.daum.net/embedded-seo/449971


반응형
Posted by blueasa
, |

프로그램, 프로세스, 스레드의 차이를 알아보자.

일반적으로 프로그램(Program)은 메모장과 같은 하나의 실행 가능한 단위를 의미한다.

그러나 아시다시피 메모장은 여러개를 동시에 실행할 수 있다. 이때 각각의 메모장은 서로 다른 프로세스(Process)에 의해서 실행이 된다.

즉, 프로세스는 프로그램을 객체화 시킨 것.

각 프로세스는 4GB의 개별 주소 공간과 파일, 메모리, 스레드를 소유하게 된다. 이 말에서도 나온것 처럼 스레드는 다시 프로세스에 속한다.

하나의 프로세스는 여러개의 스레드(Thread)를 소유할 수 있으며 프로세스 자체는 껍데기일 뿐이고 사실은 스레드가 모든 명령을 수행한다.

프로세스가 최초로 생성될때 각종 관련 변수와 메모리 등을 생성하면서 메인 스레드를 생성하게 되는데 이 메인 스레드가 사실 모든 명령들을 처리한다.

그러나 사용자가 원하면 멀티 스레드 (Multi-Thread)가 가능한데 즉, 하나의 프로세스가 여러개의 스레드를 가지고서 다양한 작업을 분산 시켜 처리하도록 하는 것이다.

사용자가 임의로 타이머를 만들어서 규칙적인 시간에 일정한 작업을 처리하는 것보다 (또는 PeekMessage를 이용한 방법보다) 운영체제가 알아서 시간을 쪼개에 스레드에게 작업을 나누어 주는 것이 훠어어얼씬 효율적이고 빠르단다.

다만, 멀티 스레드가 가지는 문제점은 하나의 프로세스안에 존재하는 스레드들은 같은 코드와 주소공간, 그리고 전역 변수를 '공유'하게 됨으로써 생기는 '자원 경쟁' 혹은 '비동기 문제'이다.

하나의 프로세스 안에 있는 1번 스레드가 전역 변수를 변경하고 있는 동안 다른 2번 스레드가 그 값을 참조하거나 다시 변경하게되면 사용자가 예상치 못한 이상한 값들로 바뀌어 버리는 문제가 생기게 된다.

그래서 이것을 해결하기 위해서는 가장 좋은 것이 스레드들이 공유하는 전역 변수 자체를 안만들거나, 혹은 스레드 지역 기억장소 (Thread Local Storage) 공간을 사용해야 한다.

TLS는 다른 말로 쓰자면 '하나의 스레드에 대해서만 전역 변수인 공간'이다.

이렇게 복잡하게 설정을 해야 하는 이유는, C의 런타임 라이브러리가 애당초 Multi-Thread개념이 생기기 전에 만들어 졌기 때문이다.

기존 C 런타임 라이브러리는 함수들이 주로 Static 을 많이 사용하고 있는데 (특정 함수에서) 이 Static 변수들이 스레드 간에 '자원 경쟁' 혹은 '비동기' 때문에 엉뚱한 결과를 내는 현상을 가져왔다.

그래서 이걸 뜯어고치기되 기존 C 런타임 라이브러리를 동시에 유지하기 위해서 MLS가 도입된 것이다.

출처 : http://diehard98.tistory.com/entry/프로그램-프로세스-스레드

반응형
Posted by blueasa
, |