'2014/01'에 해당되는 글 185건

  1. 2014.01.12 [런게임] 유니티 3D로 러너 게임 만들기 - Runner, Skyline 만들기
  2. 2014.01.12 [런게임] 유니티 3D로 러너 게임 만들기 - 씬 설정하기
  3. 2014.01.12 [함수] 유니티 함수 Vector3.Lerp에 대해서 알아보자.
  4. 2014.01.12 [잡담] 어둠속을 헤매는 초보분들을 위해 ~
  5. 2014.01.10 추상 클래스 (C++)
  6. 2014.01.10 [펌] About STL : C++ STL 프로그래밍(10)-알고리즘2
  7. 2014.01.10 [펌] About STL : C++ STL 프로그래밍(9)-알고리즘1
  8. 2014.01.10 [펌] About STL : C++ STL 프로그래밍(8)-셋(Set)
  9. 2014.01.10 [펌] About STL : C++ STL 프로그래밍(7)-맵(Map)
  10. 2014.01.10 [펌] About STL : C++ STL 프로그래밍(6)-해시 맵(Hash Map)

[런게임] 유니티 3D로 러너 게임 만들기 - Runner, Skyline 만들기

|

Running

지금까지 아무것도 변한게 없어보입니다. 이제 본격적으로 게임을 만들어 봅시다. 이제 Runner을 오른쪽으로 이동시켜 봅시다.

Runner이름을 같는 C# 스크립트를 생성합니다. Runner 클래스는 Runner 폴더에 집어넣고 스크립트를 Runner 큐브에 붙입니다.. 아래코드를 작성하자.

using UnityEngine;

public class Runner : MonoBehaviour {

	void Update () {
		transform.Translate(5f * Time.deltaTime, 0f, 0f);
	}
}

play 버튼을 눌렀을때 약간의 문제가 있다. 이 camera는 cube를 따라가지 않는다. 이것을 고치려면 camera를 cube의 자식으로 놓는다.

지금은 Runner는 가만히 있고  skyline이 왼쪽으로 이동하는것 처럼보인다. 

Setup for moving camera.

Skyline을 생성하는 방법

우리는 기초적인 움직임을 배웠고, 이제는 skyline이 무한으로 생성되는 법을 배울것 입니다.  우리가 해야되는 첫번째 일은 skyline의 보이는 부분이 존재해야된다는 것입니다. 큐브가 왼쪽스크린으로 안보이게 되면 이것은 파괴하거나, 이것을 다시 skyline의 일부분으로 재사용할수도 있다.  우리는 큐브를 생성하는 방법과 뒤로 가서 더이상 보이지 않는 큐브를 앞으로 이동시키는 스크립트를 만들수 있습니다.

SkylineManager이름을 가지는 C# script를 Skyline 폴더에 만듭니다. 우리는 2개의 매니저 클래스를 만드는데,skyline 레이어에 각각 집어넣는다. 최소한, 프리팹을 사용할수 있게 skyline을 생성할수 있게 변수를 집어넣는다.

using UnityEngine;

public class SkylineManager : MonoBehaviour {

	public Transform prefab;
}

empty object를 생성한다 (GameObject / Create Empty) namedManagers의 이름으로 지정합니다. 또다른 empty object 를 생성합니다. Skyline Close Manager라고 , Managers의 하위 오브젝트로 설정하고SkylineManager 스크립트를 추가한다.

Now turn both skyline 큐브를  Skyline 폴더에 끌어당겨 프리팹을 만든다. 그후, Hierarchy에 있는 두개의 큐브를 삭제합니다. Skyline Close 프리팹을 Skyline Close Manager

에 집어넣습니다. 

Manager and prefabs.
우리는 큐브의 spawn 포인트(생성지점)를 만들어야 됩니다. startPosition변수를 만듭니다. 우리는 또한 화면에서 많은 큐브를 만들어야 됩니다. 간단하게 numberOfCubes의 변수를 만듭니다. nextPosition이라는 private 변수도 만듭니다.
	public Transform prefab;
	public int numberOfObjects;
	public Vector3 startPosition;

	private Vector3 nextPosition;

	void Start () {
		nextPosition = startPosition;
	}
다음 단계는 큐브를 초기화 하는 것이다. 우리는 간단하게 이것을 루프를 돌릴것이고, 왜냐하면 우리는 각각의 큐브가 unit으로 구성되기 때문에 거리느 그들의 localScale.x와 같다.
	void Start () {
		nextPosition = startPosition;
		for (int i = 0; i < numberOfObjects; i++) {
			Transform o = (Transform)Instantiate(prefab);
			o.localPosition = nextPosition;
			nextPosition.x += o.localScale.x;
		}
	}
현재 start 위치는 (0, -1, 0)으로 설정하고 Number of Objects 는 10으로 설정한다. 플레이 버튼을 누르면 runner밑에 간단한게 막대가 생성됩니다. 하지만 큐브는 화면밖으로 나가면 다시는 볼수 없습니다.
manager skyline
Instantiating a skyline.
Runner가 움직인 거리 만큼 recyle 시키는게 아이디어입니다. 이작업은, manager는 반드시 Runner의 움직인 거리를 알아야 되며   distanceTraveled 의 변수를  Runner 스크립트에 집어넣습니다.
using UnityEngine;

public class Runner : MonoBehaviour {

	public static float distanceTraveled;

	void Update () {
		transform.Translate(5f * Time.deltaTime, 0f, 0f);
		distanceTraveled = transform.localPosition.x;
	}
}
skyline 오브젝트에 queue를 집어넣고 재활용 해줍니다. recycleOffsetvariable 을 설정하고 10으로 설정한다.

큐란 무엇일까요?(자료구조) 바로가기

using UnityEngine;
using System.Collections.Generic;

public class SkylineManager : MonoBehaviour {

	public Transform prefab;
	public int numberOfObjects;
	public float recycleOffset;
	public Vector3 startPosition;

	private Vector3 nextPosition;
	private Queue<Transform> objectQueue;

	void Start () {
		objectQueue = new Queue<Transform>(numberOfObjects);
		nextPosition = startPosition;
		for (int i = 0; i < numberOfObjects; i++) {
			Transform o = (Transform)Instantiate(prefab);
			o.localPosition = nextPosition;
			nextPosition.x += o.localScale.x;
			objectQueue.Enqueue(o);
		}
	}

	void Update () {
		if (objectQueue.Peek().localPosition.x + recycleOffset < Runner.distanceTraveled) {
			Transform o = objectQueue.Dequeue();
			o.localPosition = nextPosition;
			nextPosition.x += o.localScale.x;
			objectQueue.Enqueue(o);
		}
	}
}
Recycling skyline configuration.

작업이 끝났다! 플레이 모드를 누르면 큐브는 다시 설정된다. 하지만 이것은 스카이라인처럼 보이지 않는다 좀더 스카이라인을 , 하지만 아직은 스카이 라인 처럼 보이지않는다. 좀더 스카인 라인처럼 보일려면, 이것을 재활용 시킬때 좀더 다양한 모양으로 만들어보자.

첫번째, 처음에 배치하는 큐브든 나중에 배치하는 규브든 모두 같은 것을 기본적으로 알자.아래 코드를 보자.

	void Start () {
		objectQueue = new Queue<Transform>(numberOfObjects);
		for (int i = 0; i < numberOfObjects; i++) {
			objectQueue.Enqueue((Transform)Instantiate(prefab));
		}
		nextPosition = startPosition;
		for (int i = 0; i < numberOfObjects; i++) {
			Recycle();
		}
	}

	void Update () {
		if (objectQueue.Peek().localPosition.x + recycleOffset < Runner.distanceTraveled) {
			Recycle();
		}
	}

	private void Recycle () {
		Transform o = objectQueue.Dequeue();
		o.localPosition = nextPosition;
		nextPosition.x += o.localScale.x;
		objectQueue.Enqueue(o);
	}
maximum 와 minimum 변수를 설정하고 랜덤으로 크기를 조정합니다.
	public Vector3 minSize, maxSize;

	private void Recycle () {
		Vector3 scale = new Vector3(
			Random.Range(minSize.x, maxSize.x),
			Random.Range(minSize.y, maxSize.y),
			Random.Range(minSize.z, maxSize.z));

		Vector3 position = nextPosition;
		position.x += scale.x * 0.5f;
		position.y += scale.y * 0.5f;

		Transform o = objectQueue.Dequeue();
		o.localScale = scale;
		o.localPosition = position;
		nextPosition.x += scale.x;
		objectQueue.Enqueue(o);
	}

Start Position (-60, -60, 50) 설정하고, Min Size 는 (10, 20, 10), Max Size는 (30, 60, 10)으로설정하고 , Recycle Offset to 60로 설정한다.

위와 마찬가지로 두번째 스카이라인을 만든다. Skyline Close Manager를 복사하고 이름을 Skyline Far Away Manager로 변경한다

Skyline Far Away prefab으로 변경한다. Start Position 을 (-100, -100, 100)으로 설정하고 Recycle Offset을 75로 설정한다, Min Size를 (10, 50, 10) 설정하고 Max Size를 (30, 100, 10)로 설정한다. 물론 너가 원하는 값으로 변경할수도있다.

managers skyline close manager skyline far away managerskylines
The complete skyline.


Trackback 0 And Comment 0

[런게임] 유니티 3D로 러너 게임 만들기 - 씬 설정하기

|

http://catlikecoding.com/unity/tutorials/runner/


개인 공부겸 번역 글입니다. 영어는 못하며 구글 번역기의 힘을 빌렸습니다.

Runner 최소한의 사이드 스크롤로 완성하기.





소개

이 튜토리얼에서는 간단한 무한 러너 게임을 만듭니다.

  • 배경 생성 방법
  • 오브젝트의 재사용
  • 간단한 물리 구현
  • 플레이어 점프 구현
  • 파워 업 구현
  • 간단한 매니저 클래스 만들기(게임을 관리할수있는 클래스)
  • 중력 조절
  • 간단한 GUI.

이 튜토리얼은 C# 과 유니티 에디터를 기본적으로 알고 있다는 가정하에 작성되었습니다.

 만약에 당신이 Clock 튜토리얼을 완성했다면 바로 시작해라. 그리고 Graphs 튜토리얼은 필수적이지는 않지만 봐두면 유용할 것입니다.

종종 기존의 코드는 생략하고 새로운 코드만 입력하게 되므로 문맥의 전후관계를 잘 보고

코드를 삽입하시기 바랍니다.

게임 

게임을 만들기 전 우리가 어떤 요소들을 게임에 집어넣을지 결정해야 됩니다. 우리는 간단한 2디 사이드 스크롤 게임을 만들기로 했지만 이것은 조금은 막연한 얘기입니다. 조금 더 구체적으로 얘기를 해볼까요?

게임 플레이어를 하는동안 플레이어는 오른쪽으로 계속 이동할 것입니다. 플레이어는 장애물과 장애물 사이를 점프 할수 있어야 되며. 이 장애물들은 각각 높이가 다를수 있으며 플레이어의 스피드를 느리게 할수도 있씁니다. 우리는 또한 파워업 아이템을 추가해서 속도를 빠르게 하거나 점프를 높게 할수 도 있습니다.

그래픽은 간단한게 큐브를 사용할것이며 간단한 파티클도 들어갑니다. 이 큐브는 runner와 파워업 아이템 그리고 장애물 하늘 배경에도 사용됩니다. 파티클 시스템은 주인공이 지나간 흔적을 나타네거나 물체가 떨어지거나 올라갈때 효과를 넣어줍니다. 그리고 우리는 음악을 사용하지 않습니다.

Scene 설정하기

새로운 프로젝트를 생성합니다(file-new project). 기본적을 2 by 3 에디터 레이아웃이지만 당신이 원한다면 당신이 편한 레이아웃으로 설정해도 됩니다. 게임뷰에 비율을 16:10 으로 맞춥니다.

우리의 게임은 기본적으로 2D이지만 , 약간의 3D 적인 느낌도 표현하려고 합니다.

 orthographic 카메라는 3D를 표현할수 없으므로, perspective 카메라를 . 이 방법을 사용하여 다양한 거리에서 다층 스크롤을 사용할 수 있습니다.  foreground 의 깊이는 0, background 깊이는 50, 그리고 다른것들은 100으로 설정합니다. 3개의 큐브를 사용하고 배치하는 가이드를 작성해 보겠습니다. 저는 제가 원하는 각도및 색 설정을 사용했지만 당신은 자유롭게 다른것의로 바꿔서 할수도 있습니다.

 directional light (GameObject / Create Other / Directional Light) 를 더하고 rotation (20, 330, 0)을 설정합니다. directional light 오른쪽 어깨너머로 광원이 빛추는 효과를 나타낼수 있습니다. directional light은 위치는 중요하지 않습니다.(영향없음)

Main Camera  의 Field of Viewf를 30으로 맞추고, position (5, 15, -40) 설정, 그리고 rotate(20, 0, 0) 설정합니다. 또한 백그라운드 색상을 (120, 180, 250)로 설정합니다.
























light camera
Light and camera.

3개의 cube를 만들자 (GameObject / Create Other / Cube) Z 값들은 각각 0, 50,100이다. RunnerSkyline Close, Skyline Far Away 라고 이름을 지정해주자. skyline 큐브들은 콜라이더를 삭제하자.

material을 생성하자. 이름은 Runner Mat 그리고 마찬가지로 다른것들도 Mat이라고 생성한다. 그리고 이것들을 각각의 큐브에 드래그 한다. 나는 기본적인 칼라인 하얀색과 (100, 120, 220), 그리고 (110, 140, 220) 이것을 사용했다..

runner
skyline close skyline far away
The three cube configurations.
Runner 와 Skyline 폴더를 만들고 각각의 materials을 폴더에 집어넣자.
projectgame scene
Hierarchy, project, and game views.








Trackback 0 And Comment 0

[함수] 유니티 함수 Vector3.Lerp에 대해서 알아보자.

|

using UnityEngine;

using System.Collections;

 

public class LerpTest : MonoBehaviour {

    //시작위치 위치

    public Transform startMarker;

    public Transform endMarker;

 

    public float speed = 10.0F;

    private float startTime;

    private float journeyLength;

    void Start()

    {

        startTime = Time.time;

        journeyLength = Vector3.Distance(startMarker.position, endMarker.position);//시작과끝 위치 거리

    }

    void Update()

    {

        //두점 사이의 거리가 10일때

        float distCovered = (Time.time - startTime) * speed; //속력 v = m/s 1초에 10움직임 한프레임당 1움직인다고하면

        float fracJourney = distCovered / journeyLength;// 속력 / 길이 = m/s / m = 1/s 시간 fracJourney = 0.1f

        transform.position = Vector3.Lerp(startMarker.position, endMarker.position, fracJourney); //

    }

}


유니티 함수 Lerp에 대해서 알아봅시다.


Lerp는 두 벡터 사이에 시간에 따른 위치를 구하는 함수입니다.

만약 startMarker position 은 (-5,0,0 )이고 endMarker 은 (5,0,0) 일때

위의 fracJourney 가 0.5초 일때 정확히 0,0,0 을 반환하게 되며, 0.1초 일때는 두점 사이의 거리가 10이므로 1,0,0 값을 반환 하게 됩니다.

fracJourncey 가 1.0f 일때나 그 이상일때는 endMarker 의 위치(5,0,0)를 반환합니다.

만약 0.0f이거나 그 이하일때는 startMarker의 위치(-5,0,0)를 반환하게 됩니다.



Trackback 0 And Comment 0

[잡담] 어둠속을 헤매는 초보분들을 위해 ~

|

출처 : http://cafe.naver.com/unityhub


프로그래밍을 처음부터 어떻게 공부해야 하는지 난감하기만하신 분들에게

조금이나마 도움이 될까해서 저의 경험들을 떠올리면서 몇 자 적어봅니다.

 

국민학교를 다닐 때 베이직을 수박겉핥기 수준으로 배우고, 컴퓨터는 오로지

오락기쯤으로 여겼던 제가 컴퓨터 학원을 다녔던 이유도 게임을 하기 위해서 였습니다.

 

그렇게 컴퓨터를 뜯었다 고쳤다 컴퓨터를 가지고 노는 데에만 미쳐있던 제가

17살 때 부터 본격적으로 C언어를 공부하기 시작한 이유는 조금은 황당합니다.

 

그날도 마찬가지로 컴퓨터를 가지고 놀다 어디선가 복사해 온 테트리스를 실행시켰는데

"만든이> 포항공대 김개똥!!" 이라고 적혀있는 겁니다.

저는 그런 대단한 프로그램은 특별한 기업에서만 만들어 내는 줄 알았지 사람?이

만들었다는건 생각도 못했습니다.

 

게임을 만드는 회사도 결국 사람이 일하는것이 분명했을 터인데 철없고, 무식했던 저에게

대학생이 게임을 만들었다는것이 엄청난 충격이 아닐 수 없었습니다.

 

그 때 친누나가 대학을 다니고 있었기 때문에 그런 누나(컴맹)와 친구?같은 사람이 이렇게 깔끔하고

멋진 테트리스를 만들었다는 말이야?하고 머리를 큰 헤머로 맞은듯 나도 맨날 납땜질만 할게 아니라

소프트웨어를 만들어야 겠구나 하고 마음을 먹었습니다.

 

그러고는 자취방 주변에 있던 컴퓨터 학원을 다짜고짜 찾아갔습니다.

학원을 두리번 거리던 저에게 수업을 하시던 선생님들이 나오셔서 무슨 용무냐고 물으시길래

제 상황을 설명드리고, "어떤 언어가 가장 좋은지? 개발툴이 있으면 복사해주시고, 책도 빌려주세요!"라고

다소 황당하게 말했습니다.

 

그 학원 선생님들은 참으로 고맙게도 "그럼 학원을 다니세요"라 하지는 않고, C언어가 지금은 가장 좋은것

같다. 그리고 Turbo-C 컴파일러와 "C언어 500예제"라는 책을 누군지도 모르는 저에게 빌려주셨습니다.

 

[Turbo-C 컴파일러]

 

집에 가서 위에 보이는 터보씨를 실행해보니 오류만 뜨고, 아무런 동작을 하지 않길래 또 찾아갔더니 

이번에는 그 여 선생님의 남편이 오셔서 문제를 해결해 주셨습니다.

(그 여 선생님은 신혼이셨는지 다른 회사에 근무하던 남편이 땀을 뻘뻘 흘리면서 학원에 도착했을 때

그 남편을 바라보던 눈빛이 아직도 기억납니다. 뭐라고 말하진 않아도 사랑이 철철 넘쳐 흐른다고 해야하나요. ^^

지금은 40~50정도 되셨을 그분들이 혹시나 이글을 보신다면 정말 다시 만나뵙고 싶습니다. ㅠㅠ

부산 동래구 온천장 부근의 금정 컴퓨터 학원이었던가? 네티즌 수사대님들 제발 찾아주셔요.)

 

어쨌거나 문제는 해결되었고, 집으로 돌아가 "Hello world" 예제를 실행했더니

내 눈 앞에 "Hello world.exe"라는 파일이 만들어 지고야 말았습니다.

그 날 저는 잠을 잘 수 없을 정도로 큰 기쁨을 느꼈고, 빌려주신 프로그래밍 500예제는 제가 만져 본 책 중에서는

가장 두꺼웠는데도 일주일만에 모두 다 읽었습니다.

 

처음에는 이해고 뭐고 없었습니다. 그냥 책을 읽고, 예제를 그대로 따라 타이핑 해보고, 실행되는 모습에 감탄하고...

지금 생각해보면 정말 별것 아닌 그런것이 저에게는 판타지 소설책을 읽는 기분이었습니다.

 

이 처럼 프로그래밍에 재미만 붙일 수 있다면 프로그래머가 되는것은 시간문제일 뿐입니다.

지금 부터는 제가 프로그래밍을 처음 하면서 고민했던것에 대한 효과적인 학습방법들을 설명하도록 하겠습니다.

 

 

 

 

 

첫째. 처음 이해가 안될때는 무조건 보고해라 (Copy and paste)

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

코딩을 하면서 책을 보거나 인터넷을 뒤져보는것을 초보라 생각하는 사람들이 있습니다.

절대 그렇지 않습니다. 제가 배울때는 딱히 적어둘곳이 없어서 수첩에다 일일이 적었습니다.

 

그 수첩을 재산처럼 가지고 다니면서 단순하게는 for문 사용법 부터 해서 그래픽 모드로 전환하는

방법까지 모두 적어 두었다가 필요할때가 되면 그 부분을 찾아서 그대로 응용했습니다.

요즘은 인터넷 세상이니 네이버의 개인 블로그에 참조할만한 예제나 문법들을 계속 적어두고 참조하세요.

 

(클래스나 함수 이름을 모두 기억해야 한다 생각하지 마세요. 프로그래밍은 수능이 아니니 절대로 컨닝을 하세요.)

 

 

둘째. 남들이 만든 프로그램이나 소스코드를 분석해라 (Bench Marking)

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

이론적인 공부만 하고도 프로그램을 뚝딱 만들 수 있다면, 엄청난 직관력을 가진 분이 아닌가 싶습니다.

대부분은 그러지 못하니 작은 프로그램들을 조금씩 분석하세요. 분석이 싫다면 남이 만든 프로그램들을

감상부터 하세요. (책을 사면 예제 위주로 만들어진 책의 소스코드나 인터넷 상에서 돌아다니는게 많습니다.)

 

그러면서 이런 프로그램이 있구나! 저런 프로그램이 있구나! 이렇게 구현했구나! 저렇게 구현했구나!

아니면 ~ 분석할려니 너무 머리가 아프게 복잡하게 만들어 졌구나! 정말 이해하기 편하게 만들어 졌구나!

하면서 많은것들을 느끼고, 남의것을 분석하는것이 쉽지는 않구나 라는것도 느낄겁니다.

 

이렇게 남의것을 보기도하고, 조금씩 자신의것을 구현하다 보면 어느 새 프로그래밍이라는게 편하게 다가 올 시기가 옵니다.

(처음 낯선곳에 갔을 때는 어색하다가 한달 두달 지나면 고향보다 더 익숙한 곳으로 변하는것 처럼 만드세요)

 

 

 

셋째. 프로그래밍 용어와 기능을 이해해라 (Technical Understanding)

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

대부분의 프로그래밍은 어떤 언어를 막론하고, 필요로하는 기술들은 다들 비슷비슷 합니다.

다만, 프로그래밍의 역사가 길어진 요즘에는 그 용어와 기술들이 엄청나게 많습니다.

그렇다고 하더라도 그런 프로그래밍 세계에서의 용어들을 마이동풍과 같이 흘려보내기만 한다면

고급 프로그래머가 되는데에는 시간이 더 많이 걸릴겁니다.

 

예를들어서 서버, 클라이언트, 네트워킹, 데이터베이스, 멀티스레드와 같은 큰 개념에서 부터

게임 그래픽 관련 용어나 리스트, 해쉬테이블, 트리와 같이 상세한 개념까지 처음듣는용어가 있다면

적어도 수박 겉핥기 정도는 해두셔야 합니다.

 

물리학자들이 어떤 이론을 설명할 때 수학식으로 설명하듯 수학에 대한 깊은 이해가 없다면

아무런 의사전달이 되지 않겠지요. 프로그래머들도 대부분의 이야기들이 기술적인 용어들이 많기 때문에

관련 용어들을 이해하지 못한다면 난감합니다.

 

(프로그래밍이라는 것이 단순한 개념 하나만으로 만들 수 있는것이 아니기 때문에 이 과정이 가장 오래 걸립니다)

 

 

넷째. 남들이 하는 이야기에 귀를 기울여라 (Communication)

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

위의 세번째에 익숙해 졌다면 이제부터는 프로그래머들의 이야기가 귀에 들리기 시작할겁니다.

책 중에는 당장 코딩에 도움이 되는 예제나 메뉴얼 형식의 책도 있지만, 특정분야나 개발툴에 대한 전반적인

이야기를 들려주는 책들이 있지요.

 

그런것 처럼 커뮤니티 사이트에서 많은 사람들이 하는 이야기와 문제점에 대해서

당장 필요하지는 않더라도 관심을 가질 필요가 있습니다.

 

제 아무리 뛰어난 프로그래머라도 혼자서 모든 상황을 겪어보기는 힘들것이고,

그런 문제에 봉착했을 때 ~ 아 옛날에 그런 이야기를 들었었는데, 문제가 거기에 있는걸까? 하고

문제해결의 실마리를 찾을수도 있습니다.

 

프로그래밍은 혼자 산속에 들어가 10년을 수양을 한다해서 고수가 될 수 있는것이 아니고,

많은 사람들과 소통하고 함께 이루어 가야한다는것을 알아야 합니다.

 

 

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

 

너무 조바심 내지말고, 포기하지 마세요.

 

 

Trackback 0 And Comment 0

추상 클래스 (C++)

|

 c# 자바 등을 하다보니까 c++ 이랑 헷갈리는개념.. abstract 란 키워드가 없음


추상 클래스 (C++)

Visual Studio 2013
1명 중 1명이 도움이 되는 것으로 평가 이 항목 평가

추상 클래스는 보다 구체적인 클래스가 파생될 수 있는 일반 개념의 식으로 작동합니다. 추상 클래스 형식의 개체를 만들 수 없습니다. 그러나 추상 클래스 형식에 대해 포인터와 참조를 사용할 수 있습니다.

최소한 하나의 순수 가상 함수가 포함된 클래스는 추상 클래스로 간주됩니다. 추상 클래스에서 파생된 클래스는 순수 가상 함수를 구현해야 합니다. 그렇지 않은 경우 이 클래스도 역시 추상 클래스가 됩니다.

가상 함수는 pure-specifier 구문을 사용하여 "pure"로 선언됩니다(클래스 프로토콜 구현에서 설명). 가상 함수에 소개된 예를 살펴보십시오. Account 클래스의 목적은 유용하게 사용되는 일반적 기능을 가진 Account 형식의 개체를 제공 하는 것입니다. 따라서 Account는 좋은 추상 클래스 후보입니다.

// deriv_AbstractClasses.cpp
// compile with: /LD
class Account {
public:
   Account( double d );   // Constructor.
   virtual double GetBalance();   // Obtain balance.
   virtual void PrintBalance() = 0;   // Pure virtual function.
private:
    double _balance;
};

이 선언과 이전 선언은 순수 지정자 = 0 선언없이 PrintBalance가 선언된 점이 다릅니다.


'잡다한것들전부 > C, C++, C#' 카테고리의 다른 글

c++에서 bool 형 값 true false 실수할 수 있는 부분  (0) 2014.01.13
동기화 비동기화 동기식 비동기식 이란?  (0) 2014.01.13
추상 클래스 (C++)  (0) 2014.01.10
Duff's Device  (0) 2014.01.10
c++ 11 이란??  (0) 2014.01.09
strncpy 로 메모리 복사  (0) 2014.01.07
Trackback 0 And Comment 0

[펌] About STL : C++ STL 프로그래밍(10)-알고리즘2

|
제공 : 한빛 네트워크
저자 : 최흥배
이전기사 :이전 회에는 STL 알고리즘들 중 비슷한 성격의 알고리즘을 모아서 '변경 불가 시퀀스 알고리즘', '변경 가능 시퀀스 알고리즘', '정렬 관련 알고리즘', '범용 수치 알고리즘'으로 크게 네 개로 나누고 이중 '변경 불가 시퀀스 알고리즘' 과 '변경 가능 시퀀스 알고리즘'에 있는 주요 알고리즘의 특징과 사용 방법에 대해서 설명하였습니다. 

이번 회는 이전 회에 설명하지 못한 '정렬 관련 알고리즘' 과 '범용 수치 알고리즘'의 주요 알고리즘의 특징과 사용 방법에 대해서 설명하겠습니다. 

10.1. 정렬 관련 알고리즘 

10.1.1 sort 

sort는 컨테이너에 있는 데이터들을 내림차순 또는 오름차순으로 정렬 할 때 가장 자주 사용하는 알고리즘입니다. 컨테이너에 저장하는 데이터의 자료 형이 기본형이라면 STL에 있는 greate나 less 비교 조건자를 사용합니다(STL의 string의 정렬에도 사용할 수 있습니다. 다만 이 때는 알파벳 순서로 정렬합니다). 기본형이 아닌 경우에는 직접 비교 조건자를 만들어서 사용해야 합니다. 

sort의 원형
template<class RandomAccessIterator>
   void sort( RandomAccessIterator _First, RandomAccessIterator _Last );
첫 번째와 두 번째 파라미터는 정렬하려는 구간의 시작과 마지막을 가리키는 반복자입니다. 비교 조건자를 필요로 하지 않고 기본형을 저장한 컨테이너를 오름차순으로 정렬합니다.
template<class RandomAccessIterator, class Pr>
   void sort( RandomAccessIterator _First, RandomAccessIterator _Last, BinaryPredicate _Comp );
첫 번째와 두 번째 파라미터는 정렬하려는 구간의 시작과 마지막을 가리키는 반복자입니다. 세 번째 파라미터는 정렬 방법을 기술한 비교 조건자입니다. 

sort 알고리즘의 원형을 보면 알 수 있듯이 램덤 접근 반복자를 지원하는 컨테이너만 sort 알고리즘을 사용할 수 있습니다. 

sort 사용 방법
vector<int> vec1;
…..
// 오름차순 정렬
sort( vec1.begin(), vec1.end() );

// 내림차순 정렬
Sort( vec1.begin(), vec1.end(), greater<int>() );
< 리스트 1. less와 greater 비교 조건자를 사용한 sort >
#include <vector>
#include <iostream>
#include <algorithm>
#include <functional>

using namespace std;

int main()
{
  vector<int> vec1(10);
  vector<int> vec2(10);
  vector<int> vec3(10);
  vector <int>::iterator Iter1;

  generate( vec1.begin(), vec1.end(), rand );
  generate( vec2.begin(), vec2.end(), rand );
  generate( vec3.begin(), vec3.end(), rand );

  // 오름차순 정렬
  cout << "vec1 정렬 하기 전" << endl;
  for( Iter1 = vec1.begin(); Iter1 != vec1.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;

  sort( vec1.begin(), vec1.end() );
  
  cout << "vec1 오름차순 정렬" << endl;
    for( Iter1 = vec1.begin(); Iter1 != vec1.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;
  cout << endl;

  // 내림차순 정렬
  cout << "vec2 정렬 하기 전" << endl;
  for( Iter1 = vec2.begin(); Iter1 != vec2.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;
  
  sort( vec2.begin(), vec2.end(), greater<int>() );

  cout << "vec2 내림차순 정렬" << endl;
    for( Iter1 = vec2.begin(); Iter1 != vec2.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;
  cout << endl;

  // 일부만 내림차순 정렬
  cout << "vec3 정렬 하기 전" << endl;
  for( Iter1 = vec3.begin(); Iter1 != vec3.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;

  sort( vec3.begin() + 5, vec3.end(), greater<int>() );

  cout << "vec3 일부만 내림차순 정렬" << endl;
    for( Iter1 = vec3.begin(); Iter1 != vec3.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;

  return 0;
}
< 결과 > 

그림1 

<리스트 1>은 기본 형 데이터를 정렬하는 예제로 이번에는 유저 정의 형 데이터를 정렬하는 예제를 보여 드리겠습니다. 

< 리스트 2. USER 구조체의 Money를 기준으로 정렬 >
#include <vector>
#include <iostream>
#include <algorithm>
#include <functional>

using namespace std;

struct USER
{
  int UID;
  int Level;
  int Money;
};

struct USER_MONEY_COMP
{
  bool operator()(onst USER& user1, const USER& user2)
  {
    return user1.Money > user2.Money;
  }
};

int main()
{
  USER User1; User1.UID = 1;  User1.Money = 2000;
  USER User2; User2.UID = 2;  User2.Money = 2050;
  USER User3; User3.UID = 3;  User3.Money = 2200;
  USER User4; User4.UID = 4;  User4.Money = 1000;
  USER User5; User5.UID = 5;  User5.Money = 2030;

  vector<USER> Users;
  Users.push_back( User1 );  Users.push_back( User2 );
  Users.push_back( User3 );  Users.push_back( User4 );
  Users.push_back( User5 );

  vector <USER>::iterator Iter1;

  cout << "돈을 기준으로 정렬 하기 전" << endl;
  for( Iter1 = Users.begin(); Iter1 != Users.end(); ++Iter1 ) {
    cout << Iter1->UID << " : " << Iter1->Money << ", ";
  }
  cout << endl << endl;

  sort( Users.begin(), Users.end(), USER_MONEY_COMP() );

  cout << "돈을 기준으로 내림차순으로 정렬" << endl;
  for( Iter1 = Users.begin(); Iter1 != Users.end(); ++Iter1 ) {
    cout << Iter1->UID << " : " << Iter1->Money << ", ";
  }
  cout << endl << endl;

  return 0;
}
< 결과 > 

그림2 

10.1.2. binary_search 

이미 정렬 되어 있는 것 중에서 특정 데이터가 지정한 구간에 있는지 조사하는 알고리즘입니다. 이것도 sort와 같이 비교 조건자가 필요 없는 버전과 필요한 버전 두 개가 있습니다(단 sort와 다르게 랜덤 접근 반복자가 없는 컨테이너도 사용할 수 있습니다). 

binary_search 원형
template<class ForwardIterator, class Type>
   bool binary_search( ForwardIterator _First, ForwardIterator _Last, const Type& _Val );

template<class ForwardIterator, class Type, class BinaryPredicate>
   bool binary_search( ForwardIterator _First, ForwardIterator _Last, const Type& _Val, 
                        BinaryPredicate _Comp );
binary_search 사용 방법
vector<int> vec1;
…..
sort( vec1.beign(), vec1.end() );
…...
bool bFind = binary_search( vec1.begin(), vec1.end(), 10 );
binary_search는 정렬한 이후에 사용해야 한다고 앞서 이야기 했습니다. 만약 정렬하지 않고 사용하면 어떻게 될까요? 

< 리스트 3. 정렬하지 않고 binary_search 사용 >
int main()
{
  vector<int> vec1;
  vec1.push_back(10);  vec1.push_back(20);  vec1.push_back(15);
  vec1.push_back(7);  vec1.push_back(100); vec1.push_back(40);
  vec1.push_back(11);  vec1.push_back(60);  vec1.push_back(140);

  bool bFind = binary_search( vec1.begin(), vec1.begin() + 5, 15 );
  if( false == bFind ) {
    cout << "15를 찾지 못했습니다." << endl;
  } else {
    cout << "15를 찾았습니다." << endl;
  }

  return 0;
}
디버그 모드로 빌드 후 실행 하면 아래와 같은 에러 창이 나옵니다. 

그림3 

에러 내용은 시퀀스가 정렬되지 않았다고 나옵니다. 릴리즈 모드 빌드 후 실행하면 위와 같은 에러는 나오지 않지만 결과는 false가 나옵니다. 

binary_search를 사용할 때는 꼭 먼저 정렬해야 한다는 것을 잊지 말기를 바랍니다. 

< 리스트 4. 정렬 후 binary_search 사용 >
#include <vector>
#include <iostream>
#include <algorithm>
#include <functional>

using namespace std;

int main()
{
  vector<int> vec1;
  vec1.push_back(10);  vec1.push_back(20);  vec1.push_back(15);
  vec1.push_back(7);  vec1.push_back(100); vec1.push_back(40);
  vec1.push_back(11);  vec1.push_back(60);  vec1.push_back(140);

  sort( vec1.begin(), vec1.end() );

  bool bFind = binary_search( vec1.begin(), vec1.begin() + 5, 15 );
  if( false == bFind ) {
    cout << "15를 찾지 못했습니다." << endl;
  } else {
    cout << "15를 찾았습니다." << endl;
  }

  return 0;
}
< 결과 > 

그림4 

비교 조건자를 사용하는 경우는 위의 <리스트 2> 코드를 예를들면 <리스트 2>에서 사용했던 USER_MONEY_COMP 조건자를 사용합니다. 

< 리스트 4. 리스트 2의 Users를 binary_search에 사용 >
…….
sort( Users.begin(), Users.end(), USER_MONY_COMP() );
bool bFind = binary_search( Users.begin(), Users.begin() + 3, User5, USER_MONY_COMP() );
…….
10.1.3 merge 

두 개의 정렬된 구간을 합칠 때 사용하는 것으로 두 구간과 겹치지 않은 곳에 합친 결과를 넣어야 합니다. 주의해야 할 점은 합치기 전에 이미 정렬이 되어 있어야 하며 합친 결과를 넣는 것은 합치는 것들과 겹치면 안되며, 또한 합친 결과를 넣을 수 있는 공간을 확보하고 있어야 합니다. 

merge의 원형
template<class InputIterator1, class InputIterator2, class OutputIterator>
   OutputIterator merge( InputIterator1 _First1, InputIterator1 _Last1,
      InputIterator2 _First2, InputIterator2 _Last2, OutputIterator _Result
   );

template<class InputIterator1, class InputIterator2, class OutputIterator, class BinaryPredicate>
   OutputIterator merge( InputIterator1 _First1, InputIterator1 _Last1,
    InputIterator2 _First2, InputIterator2 _Last2, OutputIterator _Result, BinaryPredicate _Comp );
merge 사용 방법
vector<int> vec1, vec2, vec3;
…..
merge( vec1.begin(), vec1.end(), vec2.begin(), vec2.end(), vec3.begin() );
아래의 <리스트 5>는 vec1과 vec2를 합쳐서 vec3에 넣는 것으로 vec1과 vec2는 이미 정렬되어 있고 vec3는 이 vec1과 vec2의 크기만큼의 공간을 미리 확보해 놓고 있습니다. 

< 리스트 5. 두 개의 vector의 merge >
#include <vector>
#include <iostream>
#include <algorithm>
#include <functional>

using namespace std;

int main()
{
  vector <int>::iterator Iter1;
  vector<int> vec1,vec2,vec3(12);
  
  for( int i = 0; i < 6; ++i )
    vec1.push_back( i );

  for( int i = 4; i < 10; ++i )
    vec2.push_back( i );
  
  cout << "vec1에 있는 값" << endl;
  for( Iter1 = vec1.begin(); Iter1 != vec1.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;

  cout << "vec2에 있는 값" << endl;
  for( Iter1 = vec2.begin(); Iter1 != vec2.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;


  merge( vec1.begin(), vec1.end(), 
      vec2.begin(), vec2.end(), 
      vec3.begin() );

  cout << "vec1과 vec2를 merge한 vec3에 있는 값" << endl;
  for( Iter1 = vec3.begin(); Iter1 != vec3.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;

  return 0;
}
< 결과 > 

그림5 

정렬 관련 알고리즘은 위에 소개한 세 개 이외에도 더 있지만 지면 관계상 보통 자주 사용하는 sort, binary_search, merge 세 개를 소개하는 것으로 마칩니다. 제가 소개하지 않은 정렬 관련 알고리즘을 더 공부하고 싶은 분들은 MSDN이나 C++ 책을 통해서 공부하시기를 바랍니다. 

10.2. 범용 수치 알고리즘 

10.2.1 accumulate 

지정한 구간에 속한 값들을 모든 더한 값을 계산합니다. 기본적으로 더하기 연산만 하지만 조건자를 사용하면 더하기 이외의 연산도 할 수 있습니다. accumulate를 사용하기 위해서는 앞서 소개한 알고리즘과 다르게 <numeric> 헤더 파일을 포함해야 합니다. 

accumulate의 원형
template<class InputIterator, class Type>
   Type accumulate( InputIterator _First, InputIterator _Last, Type _Val );
첫 번째와 두 번째 파라미터는 구간이며, 세 번째 파라미터는 구간에 있는 값에 더할 값입니다.
template<class InputIterator, class Type, class BinaryOperation>
 Type accumulate( InputIterator _First, InputIterator _Last, Type _Val, BinaryOperation _Binary_op );
네 번째 파라미터는 조건자로 조건자를 사용하여 기본 자료 형 이외의 데이터를 더할 수 있고, 더하기 연산이 아닌 다른 연산을 할 수도 있습니다. 

accumulate 사용 방법
vector<int> vec1;
…..
// vec1에 있는 값들만 더 한다.
int Result = accmulate( vec1.begin(), vec1.end(), 0, );
아래의 <리스트 6>은 int를 저장하는 vector를 대상으로 accmurate를 사용하는 가장 일반적인 예입니다. 

< 리스트 6. vector에 있는 값들을 계산 >
#include <vector>
#include <iostream>
#include <numeric>

using namespace std;

int main()
{
  vector <int>::iterator Iter1;
  vector<int> vec1;

  for( int i = 1; i < 5; ++i )
    vec1.push_back( i );

  // vec1에 있는 값
  for( Iter1 = vec1.begin(); Iter1 != vec1.end(); ++Iter1 ) {
    cout << *Iter1 << ", ";
  }
  cout << endl;


  // vec1에 있는 값들을 더한다.
  int Result1 = accumulate( vec1.begin(), vec1.end(), 0 );
  // vec1에 있는 값들을 더한 후 10을 더 한다.
  int Result2 = accumulate( vec1.begin(), vec1.end(), 10 );

  cout << Result1 << ", " << Result2 << endl;
}
< 결과 > 

그림6 

이번에는 조건자를 사용하여 유저 정의형을 저장한 vector를 accmulate에서 사용해 보겠습니다. 이번에도 더하기 연산만을 했지만 조건자를 사용하면 곱하기 연산 등도 할 수 있습니다. 

< 리스트 7. 조건자를 사용한 accumulate >
#include <vector>
#include <iostream>
#include <numeric>

using namespace std;

struct USER
{
  int UID;
  int Level;
  int Money;
};

struct USER_MONY_ADD
{
  USER operator()(const USER& user1, const USER& user2)
  {
    USER User;
    User.Money = user1.Money + user2.Money;
    return User;
  }
};

int main()
{
  USER User1; User1.UID = 1;  User1.Money = 2000;
  USER User2; User2.UID = 2;  User2.Money = 2050;
  USER User3; User3.UID = 3;  User3.Money = 2200;
  USER User4; User4.UID = 4;  User4.Money = 1000;
  USER User5; User5.UID = 5;  User5.Money = 2030;

  vector<USER> Users;
  Users.push_back( User1 );  Users.push_back( User2 );
  Users.push_back( User3 );  Users.push_back( User4 );
  Users.push_back( User5 );

  vector <USER>::iterator Iter1;

  for( Iter1 = Users.begin(); Iter1 != Users.end(); ++Iter1 ) {
    cout << Iter1->UID << " : " << Iter1->Money << ", ";
  }
  cout << endl << endl;

  // Users에 있는 Money 값만 더하기 위해 Money가 0인 InitUser를 세 번째 파라미터에
  // 조건자를 네 번째 파라미터로 넘겼습니다.
  USER InitUser; InitUser.Money = 0;
  USER Result = accumulate( Users.begin(), Users.end(), InitUser, USER_MONY_ADD() );
  cout << Result.Money << endl;
}
< 결과 > 

그림7 

10.2.2 inner_product 

두 입력 시퀀스의 내적을 계산하는 알고리즘으로 기본적으로는 +와 *을 사용합니다. 두 입력 시퀀스의 값은 위치의 값을 서로 곱한 값을 모두 더 한 것이 최종 계산 값이 됩니다. 주의 해야 할 것은 두 입력 시퀀스의 구간 중 두 번째 시퀀스는 첫 번째 시퀀스 구간 보다 크거나 같아야 합니다. 즉 첫 번째 시퀀스 구간의 데이터는 5개가 있는데 두 번째 시퀀스에 있는 데이터가 5개 보다 작으면 안됩니다. 

inner_product의 원형
template<class InputIterator1, class InputIterator2, class Type>
   Type inner_product( InputIterator1 _First1, InputIterator1 _Last1, InputIterator2 _First2, 
      Type _Val );
조건자를 사용하는 버전으로 조건자를 사용하면 유저 정의형을 사용할 수 있는 내적 연산 방법을 바꿀 수 있습니다.
template<class InputIterator1, class InputIterator2, class Type,
   class BinaryOperation1, class BinaryOperation2>
   Type inner_product( InputIterator1 _First1, InputIterator1 _Last1, InputIterator2 _First2, 
      Type _Val, BinaryOperation1 _Binary_op1, BinaryOperation2 _Binary_op2 );
아래의 <리스트 8>은 조건자를 사용하지 않는 inner_product를 사용하는 것으로 vec1과 vec2의 내적을 계산합니다. 

< 리스트 8. inner_product를 사용하여 내적 계산 >
#include <vector>
#include <iostream>
#include <numeric>

using namespace std;

int main()
{
  vector<int> vec1;
  for( int i = 1; i < 4; ++i )
    vec1.push_back(i);

  vector<int> vec2;
  for( int i = 1; i < 4; ++i )
    vec2.push_back(i);

  int Result = inner_product( vec1.begin(), vec1.end(), vec2.begin(), 0 );
  cout << Result << endl;

  return 0;
}
< 결과 > 

그림8 

<리스트 8>의 vec1과 vec2에는 각각 1, 2, 3 의 값이 들어가 있습니다. 이것을 네 번째 파라미터의 추가 값을 0을 넘긴 inner_droduct로 계산하면 

14 = 0 + (1 * 1) + (2 * 2) + (3 * 3); 

가 됩니다. 

<리스트 8>의 코드를 보기 전에는 어떻게 계산 되는지 잘 이해가 되지 않는 분들은 <리스트 8> 코드를 보면 inner_product가 어떻게 계산 되는지 쉽게 이해할 수 있을 것입니다. 

inner_product도 다른 알고리즘처럼 조건자를 사용할 수 있습니다. 제가 앞서 다른 알고리즘에서 조건자를 사용한 예를 보여 드렸으니 inner_product에서 조건자를 사용하는 방법은 숙제로 남겨 놓겠습니다.^^ 

범용 수치 알고리즘에는 위에 설명한 accmulate와 inner_product 이외에도 더 있지만 다른 것들은 보통 사용 빈도가 높지 않고 그것들을 다 소개하기에는 많은 지면이 필요로 하므로 이것으로 끝내겠습니다.^^; 

범용 수치 알고리즘을 끝으로 STL의 알고리즘을 설명하는 것을 마치겠습니다. 이전 회와 이번에 걸쳐서 소개한 알고리즘은 STL에 있는 알고리즘 중 사용 빈도가 높은 알고리즘들로 이 것 이외에도 많은 알고리즘이 있으니 제가 설명한 알고리즘을 공부한 후에는 제가 설명하지 않은 알고리즘들도 공부하시기를 바랍니다. 

지금까지 제가 설명한 글들을 보시면 STL은 사용할 때 일관성이 높다라는 것을 알 수 있을 것입니다. 높은 일관성 덕분에 하나를 알면 그 다음은 더 쉽게 알 수 있습니다. 

C++의 STL은 결코 사용하기 어려운 것이 아닙니다. C++을 알고 있다면 아주 쉽게 공부할 수 있으며 STL을 사용함으로 더 쉽고 견고하게 프로그래밍할 수 있습니다. 그러나 STL의 컨테이너나 알고리즘에 대해서 잘 모르면서 다른 사람들이 사용한 코드를 보고 그냥 사용하면 STL이 독이 될 수도 있음을 조심해야 합니다. 

저는 몇 달 전부터 C++0x을 공부하고 있습니다. C++0x는 현재 개발중인 새로운 C++ 표준입니다. C++0x에는 지금의 C++을 더 강력하고 쉽게 사용할 수 있게 해 주는 다양한 것들이 있습니다. 이 중 lambda라는 것이 있는데 이것을 사용하면 알고리즘에서 조건자를 사용할 때 지금보다 훨씬 더 편하게 기술할 수 있습니다. STL을 공부한 이 후에는 C++0x을 공부하시기를 바랍니다.
출처 : http://www.hanb.co.kr/


Trackback 0 And Comment 0

[펌] About STL : C++ STL 프로그래밍(9)-알고리즘1

|

제공 : 한빛 네트워크
저자 : 최흥배
이전기사 :

제가 한빛 네트워크에 C++ STL 글을 연재한지 벌써 1년이 넘었습니다. template부터 시작하여 이전 회까지는 STL의 컨테이너들에 대해 설명했습니다. 이제 알고리즘에 대한 것만 남았습니다. 제가 쓴 글들을 보고 C++ STL을 공부하는데 얼마나 도움이 되는지 알 수 없지만 조금이나마 도움이 되었기를 바랍니다.
앞으로 2회에 걸쳐서 STL의 알고리즘 중 많이 사용하고 있는 것을 중심으로 설명하겠습니다.
STL의 컨테이너는 그 자체만으로도 대단히 유용한 것이지만 알고리즘과 결합되면 유용성은 더욱 더 커집니다.
STL의 알고리즘은 컨테이너처럼 제네릭(총칭적)합니다. 제네릭 하다고 하는 이유는 STL에 있는 다양한 알고리즘을 한 종류의 컨테이너에만 사용할 수 있는 것이 아닌 vector, list, deque, map 등 여러 가지 다양한 컨테이너에 사용할 수 있기 때문입니다(참고로 STL 컨테이너만이 아닌 일반 배열 구조도 알고리즘에 사용할 수 있습니다). 

9.1 STL 알고리즘 분류 

STL 알고리즘은 크게 네 가지로 분류 할 수 있습니다.

  • 변경 불가 시퀀스 알고리즘
  • 변경 가능 시퀀스 알고리즘
  • 정렬 관련 알고리즘
  • 범용 수치 알고리즘

변경 불가 시퀀스 알고리즘에는 find와 for_each 등이 있으며 대상 컨테이너의 내용을 변경하지 못합니다. 변경 가능 시퀀스 알고리즘으로는 copy, generate 등이 있으며 대상 컨테이너 내용을 변경합니다.
정렬 관련 알고리즘은 정렬이나 머지를 하는 알고리즘으로 sort와 merge 등이 있습니다.
범용 수치 알고리즘은 값을 계산하는 알고리즘으로 accumulate 등이 있습니다. 

STL의 모든 알고리즘을 설명하기에는 많은 지면이 필요하므로 위에 소개한 알고리즘 카테고리 별로 자주 사용하는 알고리즘 중심으로 어떤 기능이 있으며 어떻게 사용하는지 설명하겠습니다. 

9.2 조건자 

알고리즘 중에는 함수를 파라미터로 받아들이는 것이 많습니다. 알고리즘에서 함수를 파라미터로 받아들이지 않거나 기본 함수 인자를 사용하며 알고리즘을 사용하는 컨테이너의 데이터는 기본 자료 형만을 사용할 수 있습니다. 유저 정의형 자료 형(struct, class)을 담는 컨테이너를 알고리즘에서 사용하기 위해서는 관련 함수를 만들어서 파라미터로 넘겨줘야 합니다.
알고리즘에 파라미터로 넘기는 함수는 보통 함수보다는 함수객체를 사용하는 편입니다. 

9.3 변경 불가 시퀀스 알고리즘 

9.3.1. find 

컨테이너에 있는 데이터 중 원하는 것을 찾기 위해서는 find 알고리즘을 사용하면 됩니다.
참고로 알고리즘을 사용하려면 algorithm 헤더 파일을 추가해야 합니다.

#include < algorithm >

find의 원형

template<class InputIterator, class Type>
InputIterator find( InputIterator _First, InputIterator _Last, const Type& _Val );

첫 번째 파라미터에 찾기를 시작할 반복자 위치, 두 번째 파라미터에 마지막 위치(반복자의 end()와 같이 이 위치의 바로 앞까지 검색함), 세 번째 파라미터에는 찾을 값을 넘깁니다.
find가 성공하면 데이터가 있는 위치를 가리키는 반복자를 반환하고, 실패하면 반복자의 end()를 반환합니다. 

find 사용 방법

vector< int > ItemCodes;
……..
// ItemCodes 컨테이너의 시작과 끝 사이에서 15를 찾는다.
find( ItemCodes.begin(), ItemCodes.end(), 15 );

find를 사용하여 캐릭터의 아이템을 검색하는 예제 코드 입니다. 

< 리스트 1. 캐릭터 아이템 검색 >

#include <algorithm>
#include <vector>
#include <iostream>

using namespace std;


int main()
{
  vector< int > CharItems;
  CharItems.push_back( 12 );
  CharItems.push_back( 100 );
  CharItems.push_back( 77 );

  vector< int >::iterator FindIter;

  // CharItems의 처음과 끝에서 12를 찾는다.
  FindIter = find( CharItems.begin(), CharItems.end(), 12 );
  if( FindIter != CharItems.end() )
  {
    cout << "CharItem 12를 찾았습니다." << endl;
  }
  else
  {
    cout << "CharItem 12는 없습니다" << endl;
  }

  // CharItems 두 번째와 끝에서12를 찾는다
  // ++CharItems.begin()로 반복자를 한칸 이동 시켰습니다.
        FindIter = find( ++CharItems.begin(), CharItems.end(), 12 );
  if( FindIter != CharItems.end() )
  {
    cout << "CharItem 12를 찾았습니다." << endl;
  }
  else
  {
    cout << "CharItem 12는 없습니다" << endl;
  }

  return 0;
}

< 결과 > 

1 

<리스트 1>에서는 vector 컨테이너를 대상으로 했지만 vector 이외의 list, deque도 같은 방식으로 사용합니다.
다만 map, set, hash_map의 연관 컨테이너는 해당 컨테이너의 멤버로 find()를 가지고 있으므로 알고리즘에 있는 find가 아닌 해당 멤버 find를 사용합니다. 

9.3.2 find_if 

find를 사용하면 컨테이너에 있는 데이터 중 찾기 원하는 것을 쉽게 찾을 수 있습니다. 그런데 find만으로는 많이 부족합니다. 이유는 find는 기본형만을 컨테이너에 저장하고 있을 때만 사용할 수 있습니다. 만약 유저 정의형을 저장하는 컨테이너는 find를 사용할 수 없습니다. 이럴 때는 9.2에서 짧게 설명한 조건자를 사용해야 합니다. 즉 조건자에 어떤 방식으로 찾을지 정의하고 알고리즘에서 이 조건자를 사용합니다.
find_if는 기본적으로 find와 같습니다. 다른 점은 조건자를 받아들인다는 것입니다. 

find_if 원형

template<class InputIterator, class Predicate>
InputIterator find_if( InputIterator _First, InputIterator _Last, Predicate _Pred );

find와 비교하면 첫 번째와 두 번째 파라미터는 동일합니다. 세 번째 파라미터가 다릅니다. find는 세 번째 파라미터에 찾기 원하는 값을 넘기지만 find_if는 조건자를 넘깁니다. 

find_if 사용법

// 컨테이너에 저장할 유저 정의형 
struct User
{
    …….
    int Money;
    int MobKillCount;
    ……
};

// 조건자
struct FindBestUser()
{
   …………………….
};

vector< User > Users;
……..

find_if( Users.begin(), Users.end(), FindBestUser() );

조건자가 있다는 것 이외에는 find_if는 find와 같으므로 조건자 부분만 잘 보시면 됩니다. 

< 리스트 2. 특정 돈을 가지고 있는 유저 찾기 >

#include <algorithm>
#include <vector>
#include <iostream>

using namespace std;

struct User
{
  int Money;
  int Level;
};

struct FindMoneyUser
{
  bool operator() ( User& user ) const { return user.Money == ComparMoney; }
  int ComparMoney;
};

int main()
{
  vector< User > Users;

  User user1; user1.Level = 10; user1.Money = 2000;
  User user2; user2.Level = 5;  user2.Money = -10;
  User user3; user3.Level = 20; user3.Money = 35000;

  Users.push_back( user1 );
  Users.push_back( user2 );
  Users.push_back( user3 );

  vector< User >::iterator FindUser;

  FindMoneyUser tFindMoneyUser; 
  tFindMoneyUser.ComparMoney = 2000;
  FindUser = find_if( Users.begin(), Users.end(), tFindMoneyUser );
  if( FindUser != Users.end() )
  {
    cout << "소지하고 있는 돈은: " << FindUser->Money << "입니다" << endl;
  }
  else
  {
    cout << " 유저가 없습니다. " << endl;
  }

  return 0;
}

< 결과 > 

2 

<리스트 2>에서는 조건자 함수를 함수 포인터가 아닌 함수 객체를 사용하였습니다. 함수 객체를 사용한 이유는 함수 객체는 inline화 되므로 함수 포인터와 비교하면 함수 포인터 참조와 함수 호출에 따른 작업이 필요 없으며, 함수 객체를 사용하면 객체에 특정 데이터를 저장할 수 있어서 조건자가 유연해집니다. 

<리스트 2>의 FindMoneyUser를 일반 함수로 만들었다면 비교로 사용할 ComparMoney의 값은 함수를 정의할 때 고정됩니다. 그러나 함수 객체로 만들면 객체를 생성할 때 ComparMoney의 값을 설정할 수 있어서 유연해집니다. 

<참고>
조건자를 사용하는 알고리즘 : STL의 알고리즘은 조건자를 사용하지 않는 것과 조건자를 사용하는 알고리즘 두 가지 버전을 가지고 있는 알고리즘이 있습니다. 이중 조건자를 사용하는 알고리즘은 보통 조건자를 사용하지 않는 알고리즘의 이름에서 뒤에 '_if'를 붙인 이름으로 되어 있습니다. 

9.3.3. for_each 

for_each는 순차적으로 컨테이너들에 담긴 데이터를 함수의 파라미터로 넘겨서 함수를 실행시키는 알고리즘입니다.
온라인 게임에서 예를 들면 플레이하고 있는 유저 객체를 vector 컨테이너인 Users에 저장하고 모든 유저들의 현재 플레이 시간을 갱신하는 기능을 구현한다면 플레이 시간을 계산하는 함수 객체 UpdatePlayTime을 만든 후 for_each에 Users의 시작과 끝 반복자와 UpdatePlayTime 함수 객체를 넘깁니다. 

for_each 원형

template<class InputIterator, class Function>
Function for_each( InputIterator _First, InputIterator _Last, Function _Func );

첫 번째 파라미터는 함수의 파라미터로 넘길 시작 위치, 두 번째 파라미터는 함수의 인자로 넘길마지막 위치, 세 번째는 컨테이너의 데이터를 파라미터로 받을 함수입니다. 

for_each 사용 방법

// 함수 객체
struct UpdatePlayTime
{
……….
};

vector< User > Users;

for_each( Users.begin(), Users.end(), UpdatePlayTime );

아래는 위에서 예를 들었던 것을 완전한 코드로 구현한 예제 코드입니다. 

< 리스트 3. for_each를 사용하여 유저들의 플레이 시간 갱신 >

#include <algorithm>
#include <vector>
#include <iostream>

using namespace std;

struct User
{
  int UID;
  int PlayTime;
};

struct UpdatePlayTime
{
  void operator() ( User& user )
  {
    user.PlayTime += PlayTime;
  }

  int PlayTime;
};

int main()
{
  vector< User > Users;

  User user1; user1.UID = 1; user1.PlayTime = 40000;
  User user2; user2.UID = 2; user2.PlayTime = 0;
  User user3; user3.UID = 3; user3.PlayTime = 25000;

  Users.push_back( user1 );
  Users.push_back( user2 );
  Users.push_back( user3 );

  // 현재 플레이 시간
  vector< User >::iterator IterUser;
  for( IterUser = Users.begin(); IterUser != Users.end(); ++IterUser )
  {
    cout << "UID : " << cout << IterUser->UID << "의 총 플레이 시간: " << IterUser->PlayTime << endl;
  }
  cout << endl;

  UpdatePlayTime updatePlayTime;
  updatePlayTime.PlayTime = 200;

  // 두 번째 유저부터 갱신
  for_each( Users.begin() + 1, Users.end(), updatePlayTime );
  
  for( IterUser = Users.begin(); IterUser != Users.end(); ++IterUser )
  {
    cout << "UID : " << cout << IterUser->UID << "의 총 플레이 시간: " << IterUser->PlayTime << endl;
  }

  return 0;
}

< 결과 > 

3 

변경 불가 시킨스 알고리즘에는 위에 설명한 find, find_if, for_each 이외에 count, search, equal, adjacent_find, equal 등이 있습니다. 

9.4. 변경 가능 시퀀스 알고리즘 

9.3의 알고리즘들이 대상이 되는 컨테이너에 저장한 데이터를 변경하지 않는 것들이라면 이번에 설명한 알고리즘들은 제목대로 대상이 되는 컨테이너의 내용을 변경하는 알고리즘들입니다. 컨테이너에 복사, 삭제, 대체 등을 할 때는 이번에 소개할 알고리즘들을 사용합니다. 

9.4.1. generate 

컨테이너의 특정 구간을 특정 값으로 채우고 싶을 때가 있습니다. 이 값이 동일한 것이라면 컨테이너의 assign() 멤버를 사용하면 되지만 동일한 값이 아니라면 assign()을 사용할 수 있습니다.
이 때 사용하는 알고리즘이 generate입니다. generate 알고리즘에 값을 채울 컨테이너의 시작과 끝, 값을 생성할 함수를 파라미터로 넘깁니다. 

generate 원형

template<class ForwardIterator, class Generator>
void generate( ForwardIterator _First, ForwardIterator _Last, Generator _Gen );

첫 번째 파라미터는 값을 채울 컨테이너의 시작 위치의 반복자, 두 번째는 컨테이너의 마지막 위치의 반복자, 세 번째는 값을 생성할 함수 입니다. 

generate 사용 방법

// 값 생성 함수
struct SetUserInfo
{
  …….
};

vector< User > Users(5);

generate( Users.begin(), Users.end(), SetUserInfo() );

generate 알고리즘의 대상이 되는 컨테이너는 값을 채울 공간이 미리 만들어져 있어야 합니다. 즉 generate는 컨테이너에 데이터를 추가하는 것이 아니고 기존의 데이터를 다른 값으로 변경하는 것입니다. 

< 리스트 4. generate를 사용하여 유저의 기초 데이터 설정>

struct User
{
  int UID;
  int RaceType;
  int Sex;
  int Money;
};

struct SetUserInfo
{
  SetUserInfo() { UserCount = 0; }

  User operator() ()
  {
    User user;

    ++UserCount;
    
    user.UID = UserCount;
    user.Money = 2000;

    if( 0 == (UserCount%2) )
    {
      user.RaceType = 1;
      user.Sex = 1;
      user.Money += 1000;
    }
    else
    {
      user.RaceType = 0;
      user.Sex = 0;
    }

    return user;
  }

  int UserCount;
};

int main()
{
  vector< User > Users(5);

  generate( Users.begin(), Users.end(), SetUserInfo() );
  
  char szUserInfo[256] = {0,};

  vector< User >::iterator IterUser;
  for( IterUser = Users.begin(); IterUser != Users.end(); ++IterUser )
  {
    sprintf( szUserInfo, "UID %d, RaceType : %d, Sex : %d, Money : %d",
    IterUser->UID, IterUser->RaceType, IterUser->Sex, IterUser->Money );

    cout << szUserInfo << endl;
  }
  
  return 0;
}

< 결과 > 

4 

9.4.2. copy 

copy 알고리즘은 컨테이너에 저장한 것과 같은 자료 형을 저장하는 다른 컨테이너에 복사하고 싶을 때 사용합니다. 

컨테이너 A의 데이터를 컨테이너 B에 copy 하는 경우 컨테이너 B에 데이터를 추가하는 것이 아니고 덧쓰는 것이므로 A에서 10개를 복사하는 경우 B에는 10개만큼의 공간이 없다면 버그가 발생합니다. 또 A와 B 컨테이너는 같은 컨테이너 필요는 없습니다만 당연히 컨테이너에 저장하는 자료 형은 같아야 합니다. 

copy 원형

template<class InputIterator, class OutputIterator>
   OutputIterator copy(
      InputIterator _First, 
      InputIterator _Last, 
      OutputIterator _DestBeg
   );

copy 사용 방법

vector<int> vec1;
……..
vector<int> vec2;

copy( vec1.begin(), vec1.end(), vec2.begin() );

< 리스트 5. copy 알골리즘 사용 예>

#include <algorithm>
#include <vector>
#include <list>
#include <iostream>

using namespace std;

int main()
{
  vector<int> vec1(10);
  generate( vec1.begin(), vec1.end(), rand );

  cout << "vec1의 모든 데이터를 vec2에 copy" << endl;

  vector<int> vec2(10);
  copy( vec1.begin(), vec1.end(), vec2.begin() );
  for( vector<int>::iterator IterPos = vec2.begin();
    IterPos != vec2.end();
    ++IterPos )
  {
    cout << *IterPos << endl;
  }
  cout << endl;


  cout << "vec1의 모든 데이터를 list1에 copy" << endl;
  list<int> list1(10);
  copy( vec1.begin(), vec1.end(), list1.begin() );
  
  for( list<int>::iterator IterPos2 = list1.begin();
    IterPos2 != list1.end();
    ++IterPos2 )
  {
    cout << *IterPos2 << endl;
  }

  return 0;
}

< 결과 > 

5 

9.4.4. remove 

remove 알고리즘은 컨테이너에 있는 특정 값들을 삭제하고 싶은 때 사용합니다.
주의해야 될 점은 삭제 후 크기가 변하지 않는다는 것입니다. 삭제가 성공하면 삭제 대상이 아닌 데이터들을 앞으로 몰아 넣고 마지막 위치의(컨테이너의 end()가 아닌 삭제 후 빈 공간에 다른 데이터를 쓰기 시작한 위치) 반복자를 반환합니다. 리턴 값이 가리키는 부분부터 끝(end()) 사이의 데이터 순서는 정의되어 있지 않으면 진짜 삭제를 하기 위해서는 erase()를 사용해야 합니다. 

remove 원형

template<class ForwardIterator, class Type>
ForwardIterator remove( ForwardIterator _First, ForwardIterator _Last, const Type& _Val );

첫 번째 파라미터는 삭제 대상을 찾기 시작할 위치의 반복자, 두 번째 파라미터는 삭제 대상을 찾을 마지막 위치의 반복자, 세 번째 파라미터는 삭제를 할 값입니다. 

remove 사용 방법

vector<int> vec1;
…..
remove( vec1.begin(), vec1.end(), 20 );

< 리스트 6. remove 사용 예>

#include <algorithm>
#include <vector>
#include <list>
#include <iostream>

using namespace std;

int main()
{
  vector<int> vec1;
  vec1.push_back(10);  vec1.push_back(20);  vec1.push_back(20);
  vec1.push_back(40); vec1.push_back(50);  vec1.push_back(30);  

  vector<int>::iterator iterPos;

  cout << "vec1에 있는 모든 데이터 출력" << endl;
  for( iterPos = vec1.begin(); iterPos != vec1.end(); ++iterPos )
  {
    cout << *iterPos << "  " << endl;
  }
  cout << endl;

  cout << "vec1에서 20 remove" << endl;
  vector<int>::iterator RemovePos = remove( vec1.begin(), vec1.end(), 20 );

  for( iterPos = vec1.begin(); iterPos != vec1.end(); ++iterPos )
  {
    cout << *iterPos << "  " << endl;
  }
  cout << endl;

  cout << "vec1에서 20 remove 이후 사용 하지않는 영역 완전 제거" << endl;
  while( RemovePos != vec1.end() )
  {
    RemovePos = vec1.erase( RemovePos );
  }

  for( iterPos = vec1.begin(); iterPos != vec1.end(); ++iterPos )
  {
    cout << *iterPos << "  " << endl;
  }
}

< 결과 > 

6 

<리스트 6>에서는 remove 후 erase()로 완전하게 제거하는 예를 나타내고 있습니다.

vector<int>::iterator RemovePos = remove( vec1.begin(), vec1.end(), 20 );

로 삭제 후 남은 영역의 반복자 위치를 받은 후

while( RemovePos != vec1.end() )
{
  RemovePos = vec1.erase( RemovePos );
}

로 완전하게 제거하고 있습니다. 

9.4.5. replace 

컨테이너의 특정 값을 다른 값으로 바꾸고 싶을 때는 replace 알고리즘을 사용합니다. 

replace 원형

template<class ForwardIterator, class Type>
   void replace(
      ForwardIterator _First, 
      ForwardIterator _Last,
      const Type& _OldVal, 
      const Type& _NewVal
   );

replace 사용 방법

vector<int> vec1;
……
replace( vec1.begin(), vec1.end(), 20, 30 );

< 리스트 7. replace 사용 예>

#include <algorithm>
#include <vector>
#include <list>
#include <iostream>

using namespace std;

int main()
{
  vector<int> vec1;
  vec1.push_back(10);  vec1.push_back(20);  vec1.push_back(20);
  vec1.push_back(40); vec1.push_back(50);  vec1.push_back(30);

  vector<int>::iterator iterPos;

  cout << "vec1에 있는 모든 데이터 출력" << endl;
  for( iterPos = vec1.begin(); iterPos != vec1.end(); ++iterPos )
  {
    cout << *iterPos << "  " << endl;
  }
  cout << endl;

  cout << "vec1의 세 번째 요소부터 20을 200으로 변경" << endl;
  replace( vec1.begin() + 2, vec1.end(), 20, 200 );

  for( iterPos = vec1.begin(); iterPos != vec1.end(); ++iterPos )
  {
    cout << *iterPos << "  " << endl;
  }

  return 0;
}

< 결과 > 

7 

변경 가능 시퀀스 알고리즘에는 generate, copy, remove, replace 이외에도 fill, swap, reverse, transform 등이 있으니 제가 설명하지 않은 알고리즘들은 다음에 여유가 있을 때 공부하시기를 바랍니다. 

지금까지 설명한 알고리즘을 보면 어떤 규칙이 있다는 것을 알 수 있을 것입니다. 알고리즘에 넘기는 파라미터는 알고리즘을 적용할 컨테이너의 위치를 가리키는 반복자와 특정 값 or 조건자를 파라미터로 넘기고 있습니다. 그래서 각 알고리즘은 이름과 기능이 다를 뿐 사용 방법은 대체로 비슷합니다. 즉 사용 방법에 일괄성이 있습니다. 그래서 하나의 알고리즘만 사용할 줄 알게 되면 나머지 알고리즘은 쉽게 사용하는 방법을 배웁니다. 이런 것이 바로 STL의 장점이겠죠 

이번 회는 이것으로 설명을 마치고 남은 알고리즘은 다음 회에 이어서 계속 설명하겠습니다.


출처 : http://www.hanb.co.kr/

Trackback 0 And Comment 0

[펌] About STL : C++ STL 프로그래밍(8)-셋(Set)

|

제공 : 한빛 네트워크
저자 : 최흥배
이전기사 :

앞서 설명했던 map과 비슷하면서도 다른 set을 이번에 이야기 하려고 합니다. map 이전에는 hash_map을 설명했는데 이번에 이야기할 set과 여러 부분에서 중복되는 부분이 있고, 저는 현업에서 set을 사용하는 경우가 거의 없어서 이번에는 내용이 길지 않을 것 같습니다. 

8.1 set 이란 

set은 원하는 key를 신속하게 찾고, 또 이 key가 정렬되기를 원할 때 사용합니다.(여기서 말하는 key라는 것은 저장할 자료를 말합니다). map과 비슷하지만 다른 점은 map은 key와 값이 한 쌍으로 저장하지만 set은 key만 저장합니다. set도 map과 같이 key를 중복으로 저장할 수 없습니다. 만약 key를 중복으로 사용하고 싶다면 multiset을 사용해야 합니다. 사용방법은 set과 거의 같습니다. set은 map과 같이 이진 탐색 트리 자료구조를 사용합니다. 

8.2 set을 사용할 때 

앞서 이야기 했듯이 set은 자료를 저장할 때 내부에서 자동으로 정렬하고, map과 다르게 key만 저장합니다. 

set은 아래 조건일 때 사용하면 좋습니다.

  1. 정렬해야 한다.
  2. key가 있는지 없는지 알아야 할 때
  3. 많은 자료를 저장하고, 검색 속도가 빨라야 할 때

다른 컨테이너를 설명 할 때 꽤 많은 이야기를 했는데 그동안 했던 이야기와 중복되는 것이 많고 특히 앞의 map과 비슷한 부분이 많아서 이번에는 바로 사용 방법으로 들어가겠습니다. 

8.3 set 사용 방법 

set 컨테이너를 쓰려면 먼저 헤더 파일을 포함해야 합니다.

#include <set>

보통 set을 사용하는 방법은 아래와 같습니다.

set< key 자료 type > 변수 이름

set< int > set1;

map과 사용 방법이 비슷하죠? 다만, set은 위와 같이 key만 저장합니다. 위에서는 key로 int 타입을 사용했습니다. 

set은 map과 같이 기본적으로 오름차순으로 정렬을 합니다. 만약 이것을 내림 차순으로 바꾸고 싶거나 key 자료형이 기본형이 아니란 유저 정의형이라면 함수 객체로 정렬 방법을 제공해야 합니다. 

set의 key가 기본형이고 내림 차순으로 정렬하고 싶다면 STL의 greater 알고리즘을 사용하면 됩니다.

set< key 자료 type, 비교 함수 > 변수 이름

set< int, greater<int> > set1;

만약 key가 기본형이 아니고 Player 이라는 클래스를 사용하고 Player의 멤버 중 HP를 비교하여 정렬하고 싶다면 아래와 같이 하면 됩니다.

class Player
{
public:
   Player() {}
   ~Player() {}

   int m_HP;
};

template< typename T >
struct HP_COMPARE : public binary_function< T, T, bool >
{
   bool operator() (T& player1, T& player2) const 
   { 
      return player1.m_HP > player2.m_HP;
   }
};

int main()
{
   set< Player, HP_COMPARE<Player> > set1;
   return 0;
}

8.3.1 set의 주요 멤버들 

멤버설명
begin첫 번째 원소의 랜덤 접근 반복자를 반환
clear저장하고 있는 모든 원소를 삭제
empty저장 하고 있는 요소가 없으면 true 반환
end마지막 원소 다음의(미 사용 영역) 반복자를 반환
erase특정 위치의 원소나 지정 범위의 원소들을 삭제
findkey와 연관된 원소의 반복자 반환
insert원소 추가
lower_bound지정한 key의 요소를 가지고 있다면 해당 위치의 반복자를 반환
operator[]지정한 key 값으로 원소 추가 및 접근
rbegin역방향으로 첫 번째 원소의 반복자를 반환
rend역방향으로 마지막 원소 다음의 반복자를 반환
size원소의 개수를 반환
upper_bound지정한 key 요소를 가지고 있다면 해당 위치 다음 위치의 반복자 반환

[표 1]. map의 주요 멤버들 

8.3.2. 추가 

set 에서는 자료를 추가 할 때 insert를 사용합니다.

원형 : 
  pair <iterator, bool> insert( const value_type& _Val );
  iterator insert( iterator _Where, const value_type& _Val );
  template<class InputIterator> void insert( InputIterator _First, InputIterator _Last );

첫 번째가 자주 사용하는 방식입니다.

set< int > set1;

// key 1을 추가.
set1.insert( 1 );

// 추가했는지 조사 하고 싶을 때는
pair< set<int>::iterator, bool > Result;
Result = set1.insert( 1 );
if( Result.second )
{
   // 추가 성공
}
else
{
  // 추가 실패
}

두 번째 방식은 특정 위치에 추가할 수 있습니다.

// 첫 번째 위치에 key 1, value 35를 추가
set1.insert( set1.begin(), 10 );

세 번째 방식은 지정한 반복자 구간에 있는 것들을 추가합니다.

set< int > set2;
// set1의 모든 요소를 set2에 추가.
set2.insert( set1.begin(), set1.end() );

set은 이미 있는 key 값을 추가할 수 없습니다(복수의 key 값을 사용하기 위해서는 multiset을 사용해야 합니다). 

참고로 특정 위치를 지정하여 추가를 하여도 정렬되어 저장합니다. 

<리스트 1. 특정 위치에 추가했을 때의 정렬 여부>

#include <iostream>
#include <functional>
#include <set>

using namespace std;

int main()
{
   set< int > set1;
   set1.insert( 10 );
   set1.insert( 15 );
   set1.insert( 12 );
   set1.insert( 2 );
   set1.insert( 100 );

   for( set<int>::iterator IterPos = set1.begin();
      IterPos != set1.end(); ++IterPos )
   {
      cout << *IterPos << endl;
   }

   set<int>::iterator IterPos = set1.begin();
   ++IterPos;
   set1.insert( IterPos, 90 );
   
   cout << endl;
   cout << "90을추가후set1의모든요소출력" << endl;
   for( set<int>::iterator IterPos = set1.begin();
      IterPos != set1.end(); ++IterPos )
   {
      cout << *IterPos << endl;
   }
   
   return 0;
}

<결과> 

그림1 

8.3.3. 반복자 사용 

다른 컨테이너와 같이 정 방향 반복자 begin(), end()와 역 방향 반복자 rbegin(), rend()를 지원합니다. 사용 방법은 다음과 같습니다.

// 정 방향으로 set1의 모든 Key 출력
set< int >::iterator Iter_Pos;
for( Iter_Pos = set1.begin(); Iter_Pos != set1.end(); ++Iter_Pos)
{
   cout << *Iter_Pos << endl;
}

// 역 방향으로 set1의 모든 요소 Key 출력
set< int >::reverse_iterator Iter_rPos;
for( Iter_rPos = set1.rbegin(); Iter_rPos != set1.rend(); ++Iter_rPos)
{
   cout << *Iter_rPos << endl;
}

8.3.4. 검색 

set에서 검색은 key를 대상으로 합니다.
key와 같은 요소를 찾으면 그 요소의 반복자를 반환하고, 찾지 못한 경우에는 end()를 가리키는 반복자를 반환합니다.

원형 : 
  iterator find( const Key& _Key );
  const_iterator find( const Key& _Key ) const;

두 방식의 차이는 반환된 반복자가 const 여부입니다. 첫 번째 방식은 const가 아니므로 찾은 요소의 Key를 변경할 수 있습니다. 그러나 두 번째 방식은 Key를 변경할 수 없습니다.

// key가 10인 요소 찾기.
set< int >::Iterator FindIter = set1.find( 10 );

// 찾았다면 value를 1000으로 변경
if( FindIter != set1.end() )
{
   // Key를 찾았다!!!
}

set은 map과 다르게 Key만 저장하기에 Key의 변경이 가능하지만 find로 찾은 Key를 변경하면 정렬되지 않습니다. 

<리스트 2. find로 찾은 Key 변경>

int main()
{
   set< int > set1;
   set1.insert( 10 );
   set1.insert( 15 );
   set1.insert( 12 );

   for( set<int>::iterator IterPos = set1.begin();
      IterPos != set1.end(); ++IterPos )
   {
      cout << *IterPos << endl;
   }

   set<int>::iterator FindIter = set1.find( 15 );
   if( FindIter != set1.end() )
   {
      *FindIter = 11;
   }

   cout << endl;
   cout << "15를 검색 후11로 변경한 후 set1의 모든 요소 출력" << endl;
   for( set<int>::iterator IterPos = set1.begin();
      IterPos != set1.end(); ++IterPos )
   {
      cout << *IterPos << endl;
   }

   return 0;
}

<결과> 

그림2 

8.3.5. 삭제 

저장하고 있는 요소를 삭제할 때는 erase와 clear를 사용합니다.
erase는 특정 요소를 삭제할 때 사용하고, clear는 모든 요소를 삭제할 때 사용합니다. 

erase

원형 : 
  iterator erase( iterator _Where );
  iterator erase( iterator _First, iterator _Last );
  size_type erase( const key_type& _Key );

첫 번째 방식은 특정 위치에 있는 요소를 삭제합니다.

// 두 번째 위치의 요소 삭제.
set1.erase( ++set1.begin() );

두 번째 방식은 지정한 구역 에 있는 요소들을 삭제합니다.

// set1의 처음과 마지막에 있는 모든 요소 삭제
set1.erase( set1.begin(), set1.end() );

세 번째 방식은 지정한 Key와 같은 요소를 삭제합니다.

// key가 10인 요소 삭제.
set1.erase( 10 );

첫 번째와 두 번째 방식으로 삭제를 하면 삭제되는 요소의 다음을 가리키는 반복자를 반환합니다.
세 번째 방식은 삭제된 개수를 반환하는데 정말 삭제가 되었다면 1이 반환됩니다. 그러나 multiset에서는 1이 아닌 삭제된 개수를 반환합니다. 

clear 

set의 모든 요소를 삭제할 때는 clear를 사용합니다.

set1.clear();

이것으로 set에서 자주 사용하는 멤버들에 대한 설명은 끝났습니다. 
아래의 <리스트 3>은 set을 전반적으로 사용하는 예를 나타내고 있습니다. 

<리스트 3. set 사용 예>

#include <iostream>
#include <functional>
#include <set>

using namespace std;

class Player
{
public:
   Player() {}
   ~Player() {}

   int m_Level;
};

// 레벨이 높은 순으로 정렬
template< typename T > 
struct LEVEL_COMPARE : public binary_function< T, T, bool >
{
   bool operator() (const T& player1, const T& player2) const 
   { 
      return player1->m_Level > player2->m_Level;
   }
};

int main()
{
   set< Player*, LEVEL_COMPARE<Player*> > PlayerList;

   Player* pPlayer1 = new Player; pPlayer1->m_Level = 10;
   PlayerList.insert( pPlayer1 );
   Player* pPlayer2 = new Player; pPlayer2->m_Level = 45;
   PlayerList.insert( pPlayer2 );
   Player* pPlayer3 = new Player; pPlayer3->m_Level = 5;
   PlayerList.insert( pPlayer3 );
   Player* pPlayer4 = new Player; pPlayer4->m_Level = 15;
   PlayerList.insert( pPlayer4 );

   // 정 방향으로 출력( 레벨이 높은 순으로)
   for( set< Player*, LEVEL_COMPARE<Player*> >::iterator IterPos = PlayerList.begin();
      IterPos != PlayerList.end(); ++IterPos )
   {
      cout << (*IterPos)->m_Level << endl;
   }

   cout << endl;

   // 역 방향으로 출력( 레벨이 낮은 순으로)
   for( set< Player*, LEVEL_COMPARE<Player*> >::reverse_iterator IterPos = PlayerList.rbegin();
      IterPos != PlayerList.rend(); ++IterPos )
   {
      cout << (*IterPos)->m_Level << endl;
   }

   cout << endl;

   // pPlayer4를검색
   set< Player*, LEVEL_COMPARE<Player*> >::iterator FindPlayer = PlayerList.find( pPlayer4 );
   if( FindPlayer != PlayerList.end() )
   {
      cout << "pPlayer4를 찾았습니다" << endl;

      cout << "pPlayer4 삭제" << endl;
      PlayerList.erase( FindPlayer );
   }
   else
   {
      cout << "pPlayer4를 못찾았습니다" << endl;
   }

   cout << endl;
   cout << "Total Player Count : " << PlayerList.size() << endl;

   cout << endl;
   PlayerList.clear();
   if( PlayerList.empty() )
   {
      cout << "Player가 없습니다." << endl;
   }
   
   delete pPlayer1;
   delete pPlayer2;
   delete pPlayer3;
   delete pPlayer4;
   return 0;
}

<결과> 

그림3 

이전 회의 map과 거의 대부분 비슷하기 때문에 map을 아시는 분들은 아주 쉬웠을 것이라고 생각합니다. 

이전과 같이 set의 멤버 중 설명하지 않은 것들은 MSDN에 있는 set 설명을 참조하시기를 바랍니다. 

과제 

Sset은 Key를 중복으로 저장할 수 없습니다. 중복 Key를 저장하기 위해서는 multiset을 사용해야 합니다. multiset을 공부해 보세요 

참고 url : http://msdn.microsoft.com/ko-kr/library/w5txk7zc.aspx


출처 : http://www.hanb.co.kr/

Trackback 0 And Comment 0

[펌] About STL : C++ STL 프로그래밍(7)-맵(Map)

|

제공 : 한빛 네트워크
저자 : 최흥배
이전기사 :

이번에는 이전 회에 설명한 hash_map과 같은 연관 컨테이너 중의 하나인 map에 대해서 설명합니다. 사용법이 hash_map과 대부분 비슷해서 앞으로 할 이야기가 별로 어렵지 않을 것입니다. 

이전 회에서 설명했던 것은 가급적 또 설명하지 않을 테니 앞의 글들을 보지 않은 분들은 꼭 봐 주세요. 그럼 map에 대한 이야기를 시작 하겠습니다. 

7.1 map의 자료구조 

map의 자료구조는 '트리(tree)'입니다(정확하게 말하면 트리 자료구조 중의 하나인 '레드-블랙 트리(Red-Black tree)'입니다). 

트리는 한글로 '나무'입니다. 나무는 뿌리에서 시작하여 여러 갈래의 가지가 있고, 가지의 끝에는 나무 잎이 있습니다. 트리 자료구조도 이와 같은 형태를 가지고 있어서 루트(root), 리프(leaf)라는 용어를 사용합니다. 

그림1 
[그림 1] 실제 나무(왼쪽)와 트리 자료구조의 모습(오른쪽) 

오른쪽의 트리 자료구조에서 제일 최상 위의 '5'는 루트 노드(root node)라고 하며, 노드'5'와 노드'7'의 관계에서 노드'5'는 부모 노드(parent node), 노드'7'은 자식 노드(child node)라고 합니다. 또한 노드 '12'와 노드 '30'의 관계에서는 부모 노드는 노드'12'입니다. 자식이 없는 노드는 리프 노드(leaf node)라고 합니다. 그림 1에서는 '9', '30', '35', '20'이 리프 노드입니다. 

7.2 트리 자료구조의 특징 

트리는 노드를 균형 있게 가지는 것이 성능에 유리하기 때문에 기본 트리에서 변형된 B-트리, B+ 트리, R-트리, 레드 블랙 트리, AVL 트리 등 다양한 종류의 트리 자료구조가 있습니다. 

균형을 이룬 트리는 자료를 정해진 방식에 따라서 분류하여 저장하기 때문에 시퀸스(일렬로)하게 자료를 저장하는 연결 리스트에 비해서 검색이 빠릅니다. 그렇지만 정해진 규칙에 따라서 자료를 삽입, 삭제 해야 되기 때문에 삽입과 삭제가 간단하지 않으며 구현이 복잡합니다. 

7.3 map을 언제 사용해야 될까? 

map은 많은 자료를 정렬하여 저장하고 있고 빠른 검색을 필요로 할 때 자주 사용합니다. 많은 자료를 빠르게 검색한다고 하는 부분은 앞 회에서 설명한 hash_map과 비슷합니다. 그러나 hash_map과 크게 다른 부분이 있습니다. map은 자료를 저장할 때 내부에서 자동으로 정렬을 하고, hash_map은 정렬하지 않는다라는 것입니다. 정렬이 필요하지 않는 곳에서 map을 사용하는 것은 불 필요한 낭비입니다. 

map은 아래 조건일 때 사용하면 좋습니다. 

1. 정렬해야 한다.
2. 많은 자료를 저장하고, 검색이 빨라야 한다
3. 빈번하게 삽입, 삭제하지 않는다. 

7.4 map 사용 방법 

가장 먼저 map의 헤더파일을 포함합니다.

#include 

보통 map을 사용하는 방법은 아래와 같습니다.

map< key 자료 type, value 자료 type > 변수 이름

map< int, int > map1;

value는 저장할 자료이고, key는 value를 가리키는 것입니다. 위에서는 key의 자료형 int, value 자료형 int인 map을 생성합니다. 

앞에서 map은 자료를 저장할 때 정렬을 한다고 말했습니다. 정렬의 대상은 key를 대상으로 하며오름차순으로 정렬합니다. 그래서 내림차순으로 정렬하고 싶거나 key의 자료형이 기본형이 아닌 유저 정의형(class나 struct로 정의한 것)인 경우는 정렬 방법을 제공해야 합니다. 

위에 생성한 map1은 오름차순으로 정렬하는데 이것을 내림차순으로 정렬하고 싶다면 아래와 같이 하면 됩니다.

map< key 자료 type, value 자료 type, 비교 함수 > 변수 이름

map< int, int, greater<int> > map1;

위에서 사용한 비교 함수 greater는 제가 따로 만든 것이 아니고 STL에 있는 템플릿입니다. 

greater와 같은 것을 STL 알고리즘 이라고 하는데 이것들은 다음 시간에 자세하게 설명할 예정이니 여기서는 이런 것이 있다는 것만 아시면 됩니다. 

다른 컨테이너와 같이 map도 동적 할당을 할 수 있습니다. 사용 방법은 앞서 소개한 컨테이너들과 비슷합니다. 

앞 회의 hash_map과 비교를 하면 사용 방법이 거의 같다라는 것을 알 수 있습니다. 이후 소개하는 map의 멤버함수도 일부분만 제외하고는 hash_map과 같습니다. 이전에도 이야기 했지만 서로 다른 컨테이너가 사용방법이 서로 비슷하여 하나만 제대로 배우 나머지 것들도 배우기 쉽다라는 것이 STL의 장점 중의 하나입니다. 

7.4.1 map의 주요 멤버들 

멤버설명
begin첫 번째 원소의 랜덤 접근 반복자를 반환
clear저장하고 있는 모든 원소를 삭제
empty저장 하고 있는 요소가 없으면 true 반환
End마지막 원소 다음의(미 사용 영역) 반복자를 반환
erase특정 위치의 원소나 지정 범위의 원소들을 삭제
Findkey와 연관된 원소의 반복자 반환
insert원소 추가
lower_bound지정한 key의 요소를 가지고 있다면 해당 위치의 반복자를 반환
operator[]지정한 key 값으로 원소 추가 및 접근
rbegin역방향으로 첫 번째 원소의 반복자를 반환
rend역방향으로 마지막 원소 다음의 반복자를 반환
size원소의 개수를 반환
upper_bound지정한 key 요소를 가지고 있다면 해당 위치 다음 위치의 반복자 반환


[표 1] map의 주요 멤버들 

7.4.2. 추가 

map 에서는 자료를 추가 할 때 insert를 사용합니다.

원형 : 
pair <iterator, bool> insert( const value_type& _Val );
iterator insert( iterator _Where, const value_type& _Val );
template<class InputIterator> void insert( InputIterator _First, InputIterator _Last );

첫 번째 방식이 보통 가장 자주 사용하는 방식입니다.

map< int, int > map1;

// key는 1, value는 35를 추가.
map1.insert( map< int, int >::value_type(1, 35));

// 또는 STL의 pair를 사용하기도 합니다.
typedef pair < int, int > Itn_Pair;
map1.insert(  Int_Pair(2, 45) );

두 번째 방식으로는 특정 위치에 추가할 수 있습니다.

// 첫 번째 위치에 key 1, value 35를 추가
map1.insert( map1.begin(), map< int, int >::value_type(1, 35) );

// 또는
map1.insert( map1.begin(),  Int_Pair(2, 45) );

세 번째 방식으로는 지정한 반복자 구간에 있는 것들을 추가합니다.

map< int, int > map2;
// map1의 모든 요소를 map2에 추가.
map2.insert( map1.begin(), map1.end() );

map은 이미 있는 key 값을 추가할 수 없습니다(복수의 key 값을 사용하기 위해서는 multi_map을 사용해야 합니다). 가장 자주 사용하는 첫 번째 방식으로 추가하는 경우는 아래와 같은 방법으로 결과를 알 수 있습니다.

pair< map<int, int>::iterator, bool > Result;
Result = map1.insert( Int_Pair(1, 35));

만약 이미 key 값 1이 추가 되어 있었다면 insert 실패로 Result.second 는 false이며, 반대로 성공하였다면 true 입니다. 

operator[]를 사용하여 추가하기 

insert가 아닌 operator[]를 사용하여 추가할 수도 있습니다.

// key 10, value 80을 추가
map1[10] = 80;

7.4.3. 반복자 사용 

다른 컨테이너와 같이 정 방향 반복자 begin(), end()와 역 방향 반복자 rbegin(), rend()를 지원합니다. 

사용 방법은 다음과 같습니다.

// 정 방향으로 map1의 모든 요소의 value 출력
map< int, int >::iterator Iter_Pos;
for( Iter_Pos = map1.begin(); Iter_Pos != map1.end(); ++Iter_Pos)
{
   cout << Iter_Pos.second << endl;
}

// 역 방향으로 map1의 모든 요소의 value 출력
map< int, int >::reverse_iterator Iter_rPos;
for( Iter_rPos = map1.rbegin(); Iter_rPos != map1.rend(); ++Iter_rPos)
{
   cout << Iter_rPos.second << endl;
}

위에서 map을 정의할 때 비교함수를 사용할 수 있다고 했습니다. 만약 비교함수를 사용한 경우는 반복자를 정의할 때도 같은 비교함수를 사용해야 합니다.

map< int, int, greater<int> > map1;
map< int, int, greater<int> >::iterator Iter_Pos;

7.4.4. 검색 

map에서 검색은 key 값을 대상으로 합니다. key와 같은 요소를 찾으면 그 요소의 반복자를 반환하고, 찾지 못한 경우에는 end()를 가리키는 반복자를 반환합니다.

원형 : 
iterator find( const Key& _Key );
const_iterator find( const Key& _Key ) const;

두 방식의 차이는 반환된 반복자가 const냐 아니냐는 차이입니다. 첫 번째 방식은 const가 아니므로 찾은 요소의 value를 변경할 수 있습니다(참고로 절대 key는 변경 불가입니다). 그러나 두 번째 방식은 value를 변경할 수 없습니다.

// key가 10인 요소 찾기.
map< int, int >::Iterator FindIter = map1.find( 10 );

// 찾았다면 value를 1000으로 변경
if( FindIter != map1.end() )
{
   FindIter->second = 1000;
}

7.4.5. 삭제 

저장하고 있는 요소를 삭제할 때는 erase와 clear를 사용합니다. erase는 특정 요소를 삭제할 때 사용하고, clear는 모든 요소를 삭제할 때 사용합니다. 

erase

원형 : 
iterator erase( iterator _Where );
iterator erase( iterator _First, iterator _Last );
size_type erase( const key_type& _Key );

첫 번째 방식은 특정 위치에 있는 요소를 삭제합니다.

// 두 번째 위치의 요소 삭제.
map1.erase( ++map1.begin() );

두 번째 방식은 지정한 구역에 있는 요소들을 삭제합니다.

// map1의 처음과 마지막에 있는 모든 요소 삭제
map1.erase( map1.begin(), map1.end() );

세 번째 방식은 지정한 키와 같은 요소를 삭제합니다.

// key가 10인 요소 삭제.
map1.erase( 10 );

첫 번째와 두 번째 방식에서는 삭제하는 요소의 다음을 가리키는 반복자를 반환하고(C++ 표준에서는 반환하지 않습니다만 Microsoft의 Visual C++에서는 반환합니다), 세 번째 방식은 삭제된 개수를 반환합니다. map에서는 세 번째 방식으로 삭제를 하는 경우 정말 삭제가 되었다면 무조건 1이지만, multi_map에서는 삭제한 개수만큼의 숫자가 나옵니다. 

clear 

map의 모든 요소를 삭제할 때는 clear를 사용합니다.

map1.clear();

이것으로 map에서 자주 사용하는 멤버들에 대한 설명은 끝났습니다. [표 1]에 나와 있는 멤버들 중 사용 방법이 간단한 것은 따로 설명하지 않으니 [리스트 1]의 코드를 봐 주세요. 

[리스트 1] 정렬된 아이템 리스트 출력

#include <map>
#include <string>
#include <iostream>

using namespace std;

struct Item
{
  char Name[32];  // 이름
  char Kind;  // 종류
  int BuyMoney;  // 구입 가격
  int SkillCd;  // 스킬 코드
};

int main()
{
  map< char*, Item > Items;
  map< char*, Item >::iterator IterPos;
  typedef pair< char*, Item > ItemPair;

  Item Item1;
  strncpy( Item1.Name, "긴칼", 32 );
  Item1.Kind = 1;    Item1.BuyMoney = 200;  Item1.SkillCd = 0;

  Item Item2;
  strncpy( Item2.Name, "성스러운 방패", 32 );
  Item2.Kind = 2;    Item2.BuyMoney = 1000;  Item2.SkillCd = 4;

  Item Item3;
  strncpy( Item3.Name, "해머", 32 );
  Item3.Kind = 1;    Item3.BuyMoney = 500;  Item3.SkillCd = 0;

  // Items에 아이템 추가
  Items.insert( map< char*, Item >::value_type(Item2.Name, Item2) );
  Items.insert( ItemPair(Item1.Name, Item1) );

  // Items가 비어 있지않다면
  if( false == Items.empty() )
  {
    cout << "저장된 아이템 개수- " << Items.size() << endl;
  }

  for( IterPos = Items.begin(); IterPos != Items.end(); ++IterPos )
  {
    cout << "이름: " << IterPos->first << ", 가격: " << IterPos->second.BuyMoney << endl;
  }

  IterPos = Items.find("긴칼");
  if( IterPos == Items.end() )   {
    cout << "아이템'긴칼'이 없습니다." << endl;
  }
  cout << endl;

  cout << "올림차순으로 정렬되어있는 map(Key 자료형으로string 사용)" << endl;
  
  map< string, Item, less<string> > Items2;
  map< string, Item, less<string> >::iterator IterPos2;
  
  Items2.insert( map< string, Item >::value_type(Item2.Name, Item2) );
  Items2.insert( ItemPair(Item1.Name, Item1) );
  // operator[]를 사용하여 저장
  Items2[Item3.Name] = Item3;

   for( IterPos2 = Items2.begin(); IterPos2 != Items2.end(); ++IterPos2 )
  {
    cout << "이름: " << IterPos2->first << ", 가격: " << IterPos2->second.BuyMoney << endl;
  }
  cout << endl;

  cout << "해머의 가격은 얼마? ";
  IterPos2 = Items2.find("해머");
  if( IterPos2 != Items2.end() )   {
    cout << IterPos2->second.BuyMoney << endl;
  }
  else {
    cout << "해머는 없습니다" << endl;
  }
  cout << endl;

  // 아이템 "긴칼"을 삭제한다.
  IterPos2 = Items2.find("긴칼");
  if( IterPos2 != Items2.end() )   {
    Items2.erase( IterPos2 );
  }

  cout << "Items2에 있는 아이템 개수: " << Items2.size() << endl;

  return 0;
}

결과 

그림2 

[리스트 1]의 Items에서 '긴칼'을 검색을 하면 찾을 수가 없습니다. 이유는 key의 자료형으로 char*을 사용했기 때문입니다. 그래서 Items2에서는 STL의 문자열 라이브러리인 string을 사용하였습니다. String에 대해서는 다음 기회에 설명할 예정이니 문자열을 처리하는 라이브러리라고 알고 계시면 됩니다. 

이것으로 map에 대한 설명이 끝났습니다. 이전 회의 hash_map과 비슷한 부분이 많아서 hash_map에 대한 글을 보셨던 분들은 쉽게 따라왔으리라 생각합니다. 그리고 map과 hash_map에 대하여 잘못 알고 있어서 정렬이 필요하지 않은 곳에서 map을 사용하는 경우가 있는데 조심하시기 바랍니다. 제가 미쳐 설명하지 않은 부분에 대해서는 MSDN에 있는 map 설명을 참조하시기를 바랍니다(http://msdn.microsoft.com/ko-kr/library/xdayte4c.aspx). 

과제 

1. [리스트 1]은 아이템 이름을 key 값으로 사용하였습니다. 이번에는 아이템 가격을 Key 값으로 사용하여 아이템을 저장하고 내림차순으로 출력해 보세요. 

앞서 중복된 key를 사용할 수 있는 multi_map 이라는 것이 있다고 이야기 했습니다. 이번 과제는 multi_map을 사용해야 합니다. multi_map에 관한 설명은 아래 링크의 글을 참조하세요. http://msdn.microsoft.com/ko-kr/library/1y9w8dz4.aspx



출처 : http://www.hanb.co.kr/

Trackback 0 And Comment 0

[펌] About STL : C++ STL 프로그래밍(6)-해시 맵(Hash Map)

|

제공 : 한빛 네트워크
저자 : 최흥배
이전기사 :

About STL을 보시는 분은 대부분 아직 STL을 잘 모르는 분들이라고 생각합니다. 제가 일하고 있는 게임업계는 주력 언어가 C++입니다. 그래서 취업 사이트에 올라온 프로그래머 채용 공고를 보면 필수 조건에 거의 대부분이 C++와 STL 사용 가능이 들어가 있습니다. 게임 업계뿐 아니라 C++을 사용하여 프로그래밍하는 곳이라면 대부분 C++과 STL을 사용하여 프로그램을 만들 수 있는 실력을 필요로 합니다.
C++ 언어를 배우고 사용하는 프로그래머라면 STL을 배우면 좋고, 특히 게임 프로그래머가 되실 분들은 STL을 꼭 사용할 줄 알아야 됩니다.
작년 여름부터 About STL을 쓰기 시작하여 지금은 2009년이 되었습니다. About STL 집필 계획으로는 이제 반 정도 도달한 것 같습니다. 앞으로 남은 반도 STL을 습득하는데 도움이 되도록 저도 최대한 노력할 테니 2009년에는 STL을 꼭 마스터하기를 바랍니다. 

6.1 시퀸스 컨테이너와 연관 컨테이너 

이전 회까지는 STL의 컨테이너에 대해서 설명했었습니다. STL 컨테이너는 크게 시퀸스 컨테이너와 연관 컨테이너로 나눕니다.
시퀸스 컨테이너는 vector, list, deque와 같이 순서 있게 자료를 보관합니다.
연관 컨테이너는 어떠한 Key와 짝을 이루어 자료를 보관합니다. 그래서 자료를 넣고, 빼고, 찾을 때는 Key가 필요합니다. 

그림1
[그림 1] 시퀸스 컨테이너와 연관 컨테이너 

시퀸스 컨테이너는 많지 않은 자료를 보관하고 검색 속도가 중요한 경우에 사용하고, 연관 컨테이너는 대량의 자료를 보관하고 검색을 빠르게 하고 싶을 때 사용합니다. 제가 만드는 온라인 게임 서버에서는 보통 접속한 유저들의 정보를 보관할 때 가장 많이 사용합니다. 

6.2 연관 컨테이너로는 무엇이 있을까요? 

연관 컨테이너로 map, set, hash_map, hash_set이 있습니다. 이것들은 Key로 사용하는 값이 중복되지 않은 때 사용합니다. 만약 중복되는 key를 사용할 때는 컨테이너의 앞에 'multi'를 붙인 multi_map, multi_set, hash_multimap, hash_multiset을 사용합니다. Key의 중복 허용 여부만 다를 뿐 사용방법은 같습니다. 

6.2.1 map, set 과 hash_map, hash_set의 차이는? 

가장 쉽게 알 수 있는 큰 차이는 이름 앞에 'hash'라는 단어가 있냐 없냐의 차이겠죠.^^
네, 'hash'라는 단어가 정말 큰 차이입니다. 
map과 set 컨테이너는 자료를 정렬하여 저장합니다. 그래서 반복자로 저장된 데이터를 순회할 때 넣은 순서로 순회하지 않고 정렬된 순서대로 순회합니다. hash_map, hash_set은 정렬 하지 않으며 자료를 저장합니다. 또 hash라는 자료구조를 사용함으로 검색 속도가 map, set에 비해 빠릅니다. 

map, set과 hash_map, hash_set 중 어느 것을 사용할지 생각할 때는
map, set의 사용하는 경우 : 정렬된 상태로 자료 저장을 하고 싶을 때.
hash_map, hash_set : 정렬이 필요 없으며 빠른 검색을 원할 때. 

를 가장 큰 조건으로 보면 좋습니다. 

6.2.2 hash_map, hash_set은 표준은 아닙니다. 

위에 열거한 연관 컨테이너 중 map과 set은 STL 표준 컨테이너지만 hash_map, hash_set은 표준이 아닙니다. 그래서 보통 STL 관련 책을 보시면 hash_map과 hash_set에 대한 설명은 없습니다. hash_map, hash_set을 쓰려면 라이브러리를 설치해야 할까요? 그럴 필요는 없습니다. STL 표준은 아니지만 오래되지 않은 C++ 컴파일러에서는 대부분 지원합니다. 윈도우에서는 Visual Studio.NET의 모든 버전에서 지원합니다.
STL 표준도 아닌 hash_map을 설명하려는 이유는 대부분 C++ 컴파일러에서 지원하고, 새로운 C++ 표준에서는 정식으로 STL에 들어갈 예정이며 현업에서 프로그래밍할 때 아주 유용하게 사용하는 컨테이너이기 때문입니다. 

[참고]
2010년 이내에 새로운 C++ 표준이 만들어질 예정인데 표준이 공표되기 전에 TR1으로 일부 공개하고 있습니다. TR1에서는 hash_map, hash_set과 거의 같은 컨테이너인 unordered_map, unordered_set이 준비되어 있습니다. hash_map, hash_set과 이름만 다를 뿐 컨테이너의 자료구조나 사용방법이 거의 같습니다.
그래서 hash_map, hash_set 사용법을 익히며 자동으로 unordered_map, unordered_set도 익히게 됩니다. 

6.3 hash_map의 자료구조 

hash_map의 자료구조는 '해시 테이블'입니다. 
아래의 [그림 2]에 나와 있듯이 해시 테이블에 자료를 저장할 때는 Key 값을 해시함수에 대입하여 버킷 번호가 나오면 그 버킷의 빈 슬롯에 자료를 저장합니다.
Key 값을 해시 함수에 입력하여 나오는 버킷 번호에 자료를 넣으므로 많은 자료를 저장해도 삽입, 삭제, 검색 속도가 거의 일정합니다. 

그림2
[그림 2] 해시 테이블에 자료 넣기 

해시 테이블에 대한 설명은 간단하지 않고 이 글에서는 hash_map 사용법을 간단하게 설명하고 마치려 합니다. 좀 더 자세하게 알고 싶은 분들은 아래의 참고 항목을 꼭 봐 주세요. 

[참고] 해시 테이블 설명
1. http://internet512.chonbuk.ac.kr/datastructure/hash/hash3.htm
2. 좋은 프로그램을 만드는 핵심 원리 25가지(한빛미디어) 

6.4 hash_map을 사용할 때와 사용하지 않을 때 

이전 연재에서 설명한 STL 컨테이너의 장단점은 컨테이너의 자료구조를 보면 알 수 있습니다. hash_map은 해시 테이블을 자료구조로 사용하므로 해시 테이블에 대해 알면 장단점을 파악할 수 있습니다. 해시 테이블은 많은 자료를 저장하고 있어도 검색이 빠릅니다. 그러나 저장한 자료가 적을 때는 메모리 낭비와 검색 시 오버헤드가 생깁니다.
Key 값을 해시 함수에 넣어 알맞은 버킷 번호를 알아 내는 것은 마법 같은 것이 아닙니다. 그러니 hash_map은 해시 테이블을 사용하므로 검색이 빠르다라는 것만 생각하고 무분별하게 hash_map을 사용하면 안됩니다. 컨테이너에 추가나 삭제를 하는 것은 list나 vector, deque가 hash_map보다 빠릅니다. 또 적은 요소를 저장하고 검색할 때는 vector나 list가 훨씬 빠를 수 있습니다. 수천의 자료를 저장하여 검색을 하는 경우에 hash_map을 사용하는 것이 좋습니다. 

hash_map을 사용하는 경우
1. 많은 자료를 저장하고, 검색 속도가 빨라야 한다.
2. 너무 빈번하게 자료를 삽입, 삭제 하지 않는다. 

6.5 Hash_map 사용방법 

STL의 다른 컨테이너와 같이 사용하려면 먼저 헤더 파일과 namespace를 선언해야 합니다. 그러나 여기서 주의할 점은 앞서 이야기 했듯이 hash_map은 표준이 아니므로 표준 STL의 namespace와 다른 이름을 사용하므로 namespace 선언할 때 실수하지 않게 조심하세요.
hash_map 헤더파일을 포함합니다.

#include <hash_map>

hash_map이 속한 namespace는 표준 STL과 다른 'stdext'입니다.

using namespace stdext;

hash_map 선언은 아래와 같습니다.

hash_map< Key 자료 type, Value 자료 type > 변수 이름

위에서는 Value는 저장할 데이터이고, Key는 Value와 가리키는 데이터입니다. 

Key는 int, Value는 float를 사용한다면 아래와 같습니다.

hash_map< int, float > hash1;

다른 컨테이너와 같이 동적 할당을 할 수 있습니다.

hash_map< key 자료 type, Value 자료 type >* 변수 이름 = new hash_map< key 자료 type, Value 자료 type >;
hash_map< int, float >* hash1 = new hash_map< int, float >;

hash_map은 Key와 Value가 짝을 이뤄야 하므로 hash_map을 처음 보는 분들은 이전의 시퀸스 컨테이너와 다르게 좀 복잡하게 보일 것입니다. 그러나 사용이 어려운 것은 아니니 잘 따라와 주세요. 

6.5.1 hash_map의 주요 멤버들 

멤버설명
begin첫 번째 원소의 랜덤 접근 반복자를 반환
clear저장한 모든 원소를 삭제
empty저장한 요소가 없으면 true 반환
end마지막 원소 다음의(미 사용 영역) 반복자를 반환
erase특정 위치의 원소나 지정 범위의 원소들을 삭제
findKey와 연관된 원소의 반복자 반환
insert원소 추가
lower_bound지정한 Key의 요소가 있다면 해당 위치의 반복자를 반환
rbegin역방향으로 첫 번째 원소의 반복자를 반환
rend역방향으로 마지막 원소 다음의 반복자를 반환
size원소의 개수를 반환
upper_bound지정한 Key 요소가 있다면 해당 위치 다음 위치의 반복자 반환



hash_map 컨테이너를 사용할 때는 거의 대부분 추가, 삭제, 검색 이렇게 3가지를 사용합니다. 핵심 기능인 만큼 아래에 좀 더 자세하게 설명하고, 다른 컨테이너는 앞서 설명한 것과 사용방법이 같으므로 예제 코드로 보여 드리겠습니다. 

6.5.2 추가 

insert 

hash_map 에서는 자료를 추가 할 때 insert를 사용합니다.

원형 : 
pair <iterator, bool> insert( const value_type& _Val );
iterator insert( iterator _Where, const value_type& _Val );
template<class InputIterator> void insert( InputIterator _First, InputIterator _Last );

insert를 사용하는 세 가지 방법 중 첫 번째 방식으로 Key 타입은 int, Value 타입은 float를 추가한다면

hash_map<int, float> hashmap1, hashmap2;

// Key는 10, Value는 45.6f를 추가.
hashmap1.insert(hash_map<int, float>::value_type(10, 45.6f));

두 번째 방식으로는 특정 위치에 추가할 수 있습니다.

// 첫 번째 위치에 key 11, Value 50.2f를 추가
hashmap1.insert(hashmap1.begin(), hash_map<int, float>::value_type(11, 50.2f));

세 번째 방식으로는 지정한 반복자 구간에 있는 것들을 추가합니다.

// hashmap1의 모든 요소를 hashmap2에 추가.
hashmap2.insert( hashmap1.begin(), hashmap1.end() );

6.5.3 삭제 

erase

원형 : 
iterator erase( iterator _Where );
iterator erase( iterator _First, iterator _Last );
size_type erase( const key_type& _Key );

첫 번째 방식은 특정 위치에 있는 요소를 삭제합니다.

// 첫 번째 위치의 요소 삭제.
hashmap1.erase( hashmap1.begin() );

두 번째 방식은 지정한 구역에 있는 요소들을 삭제합니다.

// hashmap1의 처음과 마지막에 있는 모든 요소 삭제
hashmap1.erase( hashmap1.begin(), hashmap1.end() );

세 번째 방식은 지정한 키와 같은 요소를 삭제합니다.

// Key가 11인 요소 삭제.
hashmap1.erase( 11 );

첫 번째와 두 번째 방식의 반환 값으로는 삭제된 요소의 다음의 것을 가리키는 반복자이며 세 번째 방식은 삭제된 개수를 반환합니다. 

6.5.4 검색 

hahs_map에서 검색은 Key를 사용하여 같은 Key를 가지고 있는 요소를 찾습니다.
Key와 같은 요소를 찾으면 그 요소의 반복자를 반환하고, 찾지 못한 경우에는 end()를 가리키는 반복자를 반환합니다.

원형 : 
iterator find( const Key& _Key );
const_iterator find( const Key& _Key ) const;

방식은 두 가지지만 사용법은 같습니다. 차이는 반환된 반복자가 const냐 아니냐의 차이입니다. 참고로 첫 번째 방식은 const가 아니므로 찾은 요소의 Value를 변경할 수 있습니다(참고로 Key는 변경 불가입니다). 두 번째 방식은 Value도 변경할 수 없습니다.

// Key가 10인 요소 찾기.
hash_map<int, float>::Iterator FindIter = hashmap1.find( 10 );

// 찾았다면 Value를 290.44로 변경
If( FindIter != hashmap1.end() )
{
   FindIter->second = 290.44f;
}

begin, clear, count, empty, end, rbegin, rend, size는 앞서 말 했듯이 다른 컨테이너와 사용방법이 비슷하므로 아래 예제 코드를 통해서 사용법을 보여 드리겠습니다. 

[리스트 1] hash_map을 사용한 유저 관리

#include <iostream>
#include <hash_map>
using namespace std;
using namespace stdext;

// 게임 캐릭터
struct GameCharacter
{
  // 아래의 인자를 가지는 생성자를 정의한 경우는
  // 꼭 기본 생성자를 정의해야 컨테이너에서 사용할 수 있다.
  GameCharacter() { }

  GameCharacter( int CharCd, int Level, int Money )
  {
    _CharCd = CharCd;
    _Level = Level;
    _Money = Money;
  }
  int _CharCd;    //  캐릭터 코드
  int _Level;    // 레벨
  int _Money;    // 돈
};

void main()
{
  hash_map<int, GameCharacter> Characters;

  GameCharacter Character1(12, 7, 1000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(12, Character1));

  GameCharacter Character2(15, 20, 111000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(15, Character2));

  GameCharacter Character3(200, 34, 3345000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(200, Character3));
  
  // iterator와 begin, end 사용
  // 저장한 요소를 정방향으로 순회
  hash_map<int, GameCharacter>::iterator Iter1;
  for( Iter1 = Characters.begin(); Iter1 != Characters.end(); ++Iter1 )
  {
    cout << "캐릭터 코드 : " << Iter1->second._CharCd << " |   레벨 : " << Iter1->second._Level 
      << "|  가진 돈 : " <<  Iter1->second._Money << endl; 
  }
  cout << endl; 

  // rbegin, rend 사용
  // 저장한 요소의 역방향으로순회
  hash_map<int, GameCharacter>::reverse_iterator RIter;
  for( RIter = Characters.rbegin(); RIter != Characters.rend(); ++RIter )
  {
    cout << "캐릭터 코드 : " << RIter->second._CharCd << " |   레벨 : " << RIter->second._Level 
      << "|  가진 돈 : " <<  RIter->second._Money << endl; 
  }
  cout << endl << endl;
 
  // Characters에 저장한 요소 수
  int CharacterCount = Characters.size();

  // 검색. 
  // 캐릭터 코드 15인 캐릭터를 찾는다.
  hash_map<int, GameCharacter>::iterator FindIter = Characters.find(15);
  // 찾지 못했다면 FindIter은 end 위치의 반복자가 반환된다.
  if( Characters.end() == FindIter )
  {
    cout << "캐릭터 코드가 20인 캐릭터가 없습니다" << endl;
  }
  else
  {
    cout << "캐릭터 코드가 15인 캐릭터를 찾았습니다." << endl;
    cout << "캐릭터 코드 : " << FindIter->second._CharCd << " |   레벨 : " << FindIter->second._Level 
      << "|  가진 돈 : " <<  FindIter->second._Money << endl;
  }
  cout << endl;

  for( Iter1 = Characters.begin(); Iter1 != Characters.end(); ++Iter1 )
  {
    cout << "캐릭터 코드 : " << Iter1->second._CharCd << " |   레벨 : " << Iter1->second._Level 
      << "|  가진 돈 : " <<  Iter1->second._Money << endl; 
  }
  cout << endl << endl;
  
  // 삭제
  // 캐릭터 코드가 15인 캐릭터를 삭제한다.
  Characters.erase( 15 );
  for( Iter1 = Characters.begin(); Iter1 != Characters.end(); ++Iter1 )
  {
    cout << "캐릭터 코드 : " << Iter1->second._CharCd << " |   레벨 : " << Iter1->second._Level 
      << "|  가진 돈 : " <<  Iter1->second._Money << endl; 
  }
  cout << endl << endl;

  // 모든 캐릭터를 삭제한다.
  Characters.erase( Characters.begin(), Characters.end() );

  // Characters 공백 조사
  if( Characters.empty() )
  {
    cout << "Characters는 비어 있습니다." << endl;
  }
}

결과 

결과1 

6.5.5 lower_bound와 upper_bound 

hash_map에 저장한 요소 중에서 Key 값으로 해당 요소의 시작 위치를 얻을 때 사용하는 멤버들입니다. Key 값의 비교는 크기가 아닌 저장 되어 있는 요소의 순서입니다. 23, 4, 5, 18, 14, 30 이라는 순서로 Key 값을 가진 요소가 저장되어 있으며 Key 값 18과 같거나 큰 것을 찾으면 18, 14, 30이 됩니다. 

lower_bound 

Key가 있다면 해당 위치의 반복자를 반환합니다.

원형 : 
iterator lower_bound( const Key& _Key );
const_iterator lower_bound( const Key& _Key ) const;

upper_bound 

Key가 있다면 그 요소 다음 위치의 반복자를 반환합니다.

원형 : 
iterator lower_bound( const Key& _Key );
const_iterator lower_bound( const Key& _Key ) const;

lower_bound와 upper_bound는 hahs_map에 저장된 요소를 일부분씩 나누어 처리를 할 때 유용합니다. 예를 들면 hash_map에 3,000개의 게임 캐릭터 정보를 저장되어 있으며 이것을 100개씩 나누어서 처리하고 싶을 때 사용하면 좋습니다. 

[리스트 2] lower_bound와 upper_bound 사용 예

#include <iostream>
#include <hash_map>
using namespace std;
using namespace stdext;

// 게임 캐릭터
struct GameCharacter
{
  GameCharacter() { }

  GameCharacter( int CharCd, int Level, int Money )
  {
    _CharCd = CharCd;
    _Level = Level;
    _Money = Money;
  }
  int _CharCd;    //  캐릭터코드
  int _Level;    // 레벨
  int _Money;    // 돈
};

void main()
{
  hash_map<int, GameCharacter> Characters;

  GameCharacter Character1(12, 7, 1000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(12, Character1));

  GameCharacter Character2(15, 20, 111000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(15, Character2));

  GameCharacter Character3(7, 34, 3345000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(7, Character3));

  GameCharacter Character4(14, 12, 112200 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(14, Character4));

  GameCharacter Character5(25, 3, 5000 );
  Characters.insert(hash_map<int, GameCharacter>::value_type(25, Character5));

  hash_map<int, GameCharacter>::iterator Iter1;
  cout << "저장한 캐릭터 리스트" << endl;
  for( Iter1 = Characters.begin(); Iter1 != Characters.end(); ++Iter1 )
  {
    cout << "캐릭터 코드 : " << Iter1->second._CharCd << " |   레벨 : " << Iter1->second._Level 
      << "|  가진 돈 : " <<  Iter1->second._Money << endl; 
  }
  cout << endl;

  cout << "lower_bound(14)" <<endl;
  hash_map<int, GameCharacter>::iterator Iter = Characters.lower_bound(14);
  while( Iter != Characters.end() )
  {
    cout << "캐릭터 코드 : " << Iter->second._CharCd << " |   레벨 : " << Iter->second._Level 
      << "|  가진 돈 : " <<  Iter->second._Money << endl;

    ++Iter;
  }
  cout << endl;

  cout << "upper_bound(7)" <<endl;
  Iter = Characters.upper_bound(7);
  while( Iter != Characters.end() )
  {
    cout << "캐릭터 코드 : " << Iter->second._CharCd << " |   레벨 : " << Iter->second._Level 
      << "|  가진 돈 : " <<  Iter->second._Money << endl;

    ++Iter;
  }
}

결과 

결과2 

이것으로 hash_map의 주요 사용법에 대한 설명이 끝났습니다.
위에 설명한 것들만 알고 있으면 hash_map을 사용하는데 문제가 없을 것입니다.
위에 설명한 것들 이외의 hash_map의 멤버들에 대해서 알고 싶으며 마이크로소프트의 MSDN 사이트에 있는 것을 참고하세요
http://msdn.microsoft.com/en-us/library/h80zf4bx(VS.80).aspx 

[참고]
Visual C++의 hash_map의 성능에 대해서 Visual C++에 있는 hash_map은 다른 컴파일에서 구현한 것보다 꽤 느리다라는 말이 있습니다.
(관련 글은 여기서 참고하세요. http://minjang.egloos.com/1983788 http://junyoung.tistory.com/1

얼마나 느린지 테스트했습니다.
http://blog.naver.com/jacking75/140062720030
제가 조사한 것은 Windows 플랫폼에서 VC++에서 제공한 라이브러리로 테스트한 것입니다.
결과를 보면 hash_map이 map보다 빠르지도 않고 특히 hash_map과 같은 자료구조를 사용하는 컨테이너로 마이크로소프트사에서 만든 CAtlMap에 비해 속도가 아주 느립니다.
성능이 중요한 곳에 hash_map을 사용한다면 VC++에 있는 것을 사용하지 말고 자체적으로 잘 만들어진 hash 함수를 사용하거나 C++ 오픈 소스 라이브러리인 boost에 있는 unordered_map을 사용하는 것이 좋을 것 같습니다. Windows 플랫폼에서만 사용한다면 CAtlMap을 사용하는 것도 좋습니다.


출처 : http://www.hanb.co.kr/

Trackback 0 And Comment 0