글 목록 보기

Lyn
조회 수 15485 추천 수 0 댓글 2

"비밀글입니다."


Lyn
조회 수 36309 추천 수 0 댓글 0
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄
아래코드는 C++ Builder 2009에서 테스트되었습니다.

//---------------------------------------------------------------------------

#include <vcl.h>
#include <vector>
#include <memory>
#include <iostream>
#include <cstdlib>
#pragma hdrstop

#include <tchar.h>

using namespace std;
using namespace std::tr1;

class Test
{
public:
    int i;
    Test()
    {
        cout << "Test is Create!" << endl;
    }
    Test(const Test& )
    {
        cout << "Test's Copy Constructor!" << endl;
    }
    ~Test()
    {
        cout << "Test is Destroy!" << endl;
    }
};
using namespace std;

//---------------------------------------------------------------------------

#pragma argsused
int _tmain(int argc, _TCHAR* argv[])
{
    cout << "스택에 1개 생성" <<endl;
    Test t1; // 생성 되고, 함수가 끝날때 파괴 될 것

    cout << "힙에 1개 생성" <<endl;
    Test* t2 = new Test; //생성
    cout << "힙에서 1개 파괴" <<endl;
    delete t2; //파괴

    cout << "스택에 10개 생성" <<endl;
    Test t3[10];
    vector<Test> v1;

    cout << "복사생성자가 작동한다" <<endl;
    for(int i=0;i<10;++i)
    {
        v1.push_back(t3[i]);
        //이렇게 하면 Copy Contructor 가 작동하므로 오버헤드가 큼
        //게다가 반드시 동적으로 할당해야 하는 VCL 객체에는 사용이 불가능
    }

    cout << "힙에 10개 생성 후 Vector에 넣는다" << endl;
    vector<Test*> v2;
    for(int i=0;i<10;++i)
    {
        Test* tt = new Test;
        v2.push_back(tt);
        //이렇게 하면 동적으로 할당할 수 있음.
        //그러나 vector 를 파괴하기 전에 일일히 루프를 돌면서 delete 해줘야함
        //vetror가 파괴될때 내장하고 있는 모든 객체를 파괴할 수는 없을까?
        //VCL 에서는 TObjectList 클래스를 제공한다! 그러나 이는 C++ 표준이 아니다.
        //게다가 Templete Class 가 아니라 Casting 을 추가로 해줘야 한다는 부담도있다
    }
    cout << "Vector 에 있는 10개를 파괴" << endl;
    for(int i = 10 - 1;i >= 0;--i)
    {
        delete v2[i];
    }

    cout << "shared_ptr 을 이용하여 넣는다" << endl;
    vector<shared_ptr<Test> > v3;
    for(int i=0;i<10;++i)
    {
        shared_ptr<Test> spt(new Test);
        v3.push_back(spt);
    //vector 인 v3 역시 Main이 끝나야 파괴됨.
    //v3이  파괴되기 전 shared_ptr 의 효과에 의해 v3 이 가지고있던 Test가 모두 파괴
    }

    cout << "벡터 자체를 동적으로 생성해서 테스트 해 보자" << endl;
    vector<shared_ptr<Test> > *v4 = new vector<shared_ptr<Test> >;
    for(int i=0;i<10;++i)
    {
        shared_ptr<Test> spt(new Test);
        v4->push_back(spt);
    }
    cout << "벡터를 박살내면 가지고 있던 Test 10개도 다 박살난다" << endl;
    delete v4;

    system("pause"); //이게 끝나야 Main 이 끝나므로 t1은 이 뒤에 파괴
    //vector 인 v3 역시 Main이 끝나야 파괴됨.
    //v3이  파괴되기 전 shared_ptr 의 효과에 의해 v3 이 가지고있던 Test가 모두 파괴
}
//---------------------------------------------------------------------------

/*
Ps. CopyConstructor 가 10번이 아닌 그 이상 실행 되는데.. 그 이유는 Vector 가 공간이 부족해서 재할당을 하면서 다시 복사생성자가 호출되기 때문입니다. 생각보다 오버헤드가 많이큽니다.
*/
?

Lyn
조회 수 35251 추천 수 0 댓글 0
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄
STL 에서는 B-Tree 기반의 Collection 이 준비되어 있는데, set ,multiset ,map ,multimap 의 4 종류이다.

그에 반헤 TR1에서는 위의 4종류의 Collection 에 대응되는 Hash 기반의 Collection 을 제공한다.
이름하여 unordered 시리즈(unordered_set, unordered_multiset, unordered_map, unordered_multimap) 이다. (이름너무 길다)

기본적인 사용법은 STL 의 콜렉션과 완전히 같으니 그냥 무시하도록 하겠다.

제일 기본적인 unordered_set 에 대해서만 예제를 보자

#include <iostream>
#include <string>
#include <unordered_set>

using namespace std;
using namespace std::tr1;

int main()
{
    unordered_set<string> UnOrderSet;

    UnOrderSet.rehash(10); //버켓의 갯수를 정의한다.
    //UnOrderSet.insert("김호광");
    UnOrderSet.insert("남병철");
    UnOrderSet.insert("류종택");
    UnOrderSet.insert("린");
    UnOrderSet.insert("박지훈");

    if(UnOrderSet.find("김호광") != UnOrderSet.end())
    {
        cout << "김호광 님은 볼랜드 포럼 회원입니다." << endl;
    }
    else
    {
        cout << "김호광 님은 볼랜드 포럼 회원이 아닙니다." << endl;
    }
}

위에말한대로 기본적인 사용법은 set 과 완전히 동일하다.
중요한 것은 rehash 매소드.  버켓의 갯수가 너무 많으면 메모리 낭비가 극심해지고, 너무 작으면 성능이 나빠집니다. 이래저래 귀찮은 Collection 이라 할 수 있겠군요





기본적으로 unordered_set 은 4개의 템플릿 인수를 받아드립니다.

Value(사용할 Type),  Hash(Value 를 Hash화 하는 함수객체), Pred(비교함수객체), Alloc(할당자)의 순서인데
Hash, Pred, Alloc 은 디폴트 파라미터가 있으므로 사용 하지 않아도 무방하다.

만약 기본적으로 C++에서 제공하는 놈(int, string, double 등등...) 들이 아닌 다른놈들을 사용하려면?
2가지 구현이 필요하다.

첫째로 == 연산자의 오버로딩, 두번째로 해시함수객체의 제공이다.  unordered_set의 2번째 파라메터인 Hash를 내가 사용하기 원하는 타잎을 Hash 할 수 있도록 제공 해 주는것이 필수적이다.

#include <iostream>
#include <string>
#include <unordered_set>

using namespace std;
using namespace std::tr1;

struct TTest
{
    int i;
    double d;

    bool operator == (const TTest &T) const
    {
      return ((i == T.i) && (d == T.d));
    }


};

struct TTestHash
{
    size_t operator () (const TTest &T) const
    {
        return T.i;  //그냥 간단하게 i값을 해시값 취급 해버렷다
    }
};

int main()
{
    TTest test[2];


    unordered_set<TTest, TTestHash> UnOrderSet;

    test[0].i = 5;
    test[0].d = 3.14;

    test[1].i = 9;
    test[1].d = 180.36;

    UnOrderSet.insert(test[0]);
    UnOrderSet.insert(test[1]);

    TTest FindValue;
    FindValue.i = 5;
    FindValue.d = 3.15;

    if(UnOrderSet.find(FindValue) != UnOrderSet.end())
    {
        cout << "찾는 객체가 있습니다" << endl;
    }
    else
    {
        cout << "찾는 객체가 없습니다" << endl;
    }
}

위처럼 사용자 정의타잎 TTest 를 정의하고, 그에대한 해시함수객체 TTestHash 를 정의한 후, unordered_set 을 생성할 시에, Hash함수 객체 타잎을 제공하엿다.



결론을 내자면...

1. 기본타잎을 쓰더라도 어느정도 데이터의 양을 예측 해야 만족스러운 성능이 나온다.
2. 사용자 정의타잎을 쓰고 싶으면 Hash 를 재정의 해야 하기때문에 얼마나 Hash 를 잘 시키느냐에 따라 성능이 크게 달라진다.
3. 위의 조건을 다 만족시킨다면 상황에따라 다르겠지만 대충 set 의 2배정도의 성능을 보여주는 것 같다(속도측면, 메모리는 아무래도 소비가 크다)
4. C++ Collection 너무 많다 =_=;;; 상황에 따라 최적의 Collection 을 찾는 것 만도 일이다.
5. STL 의 Collection 들은 별 존재가치가 없어졋단 생각도 든다... 메모리 아껴야되는 상황이 아니라면(오해의 소지 매우 많음!)

Ps. C++ 에서는 보통 Containers 라 하는 것 같은데... 익숙하지 않아 그냥 일반적인 Collection 이란 용어를 사용했다.
?

2008.10.05 15:08

[TR1 살펴보기] 2. Array

Lyn
조회 수 37130 추천 수 0 댓글 0
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄
예제코드

#include <iostream>
#include <array>

using namespace std;
using namespace std::tr1;

void main()
{
    array<int, 100> intarr;

    //초기화
    intarr.assign(0);  //모든 요소를 0으로 초기화한다
    for (int i = 0; i < 100; ++i)
    {
        cout << intarr[i] << " ";
    }
    //값을 대입해본다
    for (int i = 0; i < 100; ++i)
    {
        intarr[i] = i;
    }

    cout << endl << "Size : " << intarr.size() << endl;

    cout << "이터레이터를 사용한 루프" << endl;
    array<int, 100>::iterator ia;
    for (ia = intarr.begin() ; ia != intarr.end(); ++ia)
    {
        //이터레이터를 사용한 루프
        cout << *ia << " ";
    }
    cout << endl << "일반 배열처럼 접근하는 루프" << endl;
    for (int i = 0; i < 100; ++i)
    {
        cout << intarr[i] << " ";
    }

    //만약 범위를 벗어난다면 std::range_error 예외를 생성한다
//    intarr[100] = 1;
    system("pause");
}

TR1에서 추가된 array는 크기가 고정된 배열입니다.
즉 위에서의 array<int, 100> intarr 은 int intarr[100] 과 본질적으론 차이가 없습니다.

단지 관리를 편리하게 하기 위해 객체로 제공된다는것(C#, Java 와 같다고 보심 편합니다) 그리고 범위를 넘어설 시 예외를 발생한다는점이 특징입니다. (C의 배열은 범위를 넘어서도 치명적인 부분에 접근하기전까지는 오류발생을 모르는 경우도 있고.. 이경우 오류의 추적이 매우 어렵습니다)

C++ 표준 라이브러리의 일부가 될 TR1이므로 STL에서 사용하는 이터레이터 문법을 그대로 사용 할 수 있으며,
일반 배열과 본질적으로 같으므로 [] 연산자로 값을 읽고 쓸 수도 있습니다.

또한 swap, assign, size 등의 멤버함수를 제공하여, 초기화, 교체 등의 작업을 쉽게 할 수 있으며, 배열의 크기를 얻어 올 수도 있습니다.

배열에 비해 사용시의 오버헤드가 좀 있긴 하지만... 안정적인 프로그램의 메모리 관리를 위해서는 사용 해 볼만 합니다.
?

2008.10.05 15:07

[TR1 살펴보기] 1. Random

Lyn
조회 수 37872 추천 수 0 댓글 0
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄
참고사이트
http://www.boost.org/doc/libs/1_36_0/libs/random/index.html

예제소스
#include <iostream>
#include <random>
#include <windows.h>

using namespace std;
using namespace std::tr1;

void main()
{
    mt19937 Generator; //난수엔진
    uniform_int<> dst(100, 500); //범위지정자
    Generator.seed(GetTickCount());  //seed 초기화
    variate_generator<mt19937, uniform_int<> > rand(Generator, dst); //난수생성기

    for( int i = 0;i < 1000; ++i )
    {
       cout << rand() << endl;   //난수생성기는 () 연산자를 난수생성 연산자로 오버로딩 해 놓았습니다.
    }
}

TR1의 난수생성기는 3개의 요소로 이루어집니다.
1. 난수엔진
2. 범위지정자
3. 난수생성기

난수 엔진은 여러가지가 있으며,  대충 아래와같습니다.

정수난수엔진

minstd_rand
rand48
lrand48 (C 기본 라이브러리와 같음)
ecuyer1988
kreutzer1986
hellekalek1995
mt11213b
mt19937(C++0X 기본  난수엔진)

실수난수엔진 (전부 lagged_fibonacci 시리즈군요)
lagged_fibonacci607
lagged_fibonacci1279
lagged_fibonacci2281
lagged_fibonacci3217
lagged_fibonacci4423
lagged_fibonacci9689
lagged_fibonacci19937
lagged_fibonacci23209
lagged_fibonacci44497

아래로 갈 수록 랜덤의 성능이 좋아지지만, 메모리소모가 커지고 속도가 느려집니다.

만약 _HAS_CPP0X_ 매크로가 정의되어있다면,
mt19937엔진은 default_random_engine 라는 이름으로 typedef 됩니다.

Ps1. C++0X 표준에서는 variate_generator<mt19937, uniform_int<>> 처럼 템플릿의 >>를 붙여쓰는것을 허용합니다.
하지만 C++0X이전의 컴파일러에서는 >>를 시프트 연산자로 인식하여 오류를 내니 주의.
VS2008 Sp1은 >>를 정상적으로 템플릿으로 인식하며 CB2009는 오류를 냅니다.

Ps2. 위의 코드는 C++Builder 2009에서 제대로 실행되지 않습니다.
코드엔 이상이 없어 보이는데... 이상하게 Devide by Zero 익셉션을 뱉네요 ㅡ.ㅡ; 구현상의 버그인듯

Ps3. 위의 코드는 VS2008 SP1에서는 치명적인 오류를 가집니다. mt19937 엔진이 범위 밖의 숫자를 뱉어냅니다. 고쳐지기전까지 쓰지마세요(보이드소프트의 김호광님 감사합니다)
?

Lyn
조회 수 38432 추천 수 0 댓글 1
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄
?

단축키

Prev이전 문서

Next다음 문서

크게 작게 위로 아래로 댓글로 가기 인쇄

Delphi 2009(Win32) 에서 추가된 Generics 에 관해서 간략하게 써 보겠습니다.

 

Generic 이란 임의의 타입에 대해 동작하는 매소드(클래스) 를 만드는 방법으로, C++에서는 Template이라 불리며 C#, Java 등에서는 Generics 라 불리는 기능이며, 일반화 프로그래밍을 가능하게 해주는 방법입니다.

 

간단한 예제를 하나 만들어 보겠습니다

변수의 크기를 출력하는 간단한 예제입니다.

 

 

program Project1;

 

{$APPTYPE CONSOLE}

 

uses

  SysUtils, VarUtils;

 

type

  TempClass = Class

  public

    procedure GetSize<T>(Item : T);

  end;

 

  TempRecord = packed Record

    A : Integer;

    B : Double;

    C : Double;

  End;

 

procedure TempClass.GetSize<T>(Item : T);

begin

  WriteLn('Size : ', Sizeof(Item));

End;

 

var

  A : TempClass;

  B : Integer;

  C : TempRecord;

begin

  A.GetSize(3000);

  A.GetSize(4.0);

 

  B := 30;

  A.GetSize(B);

 

  A.GetSize(C);

  ReadLn;

end.

 

 

결과는 각각

2, 10, 4, 20 이 나옵니다.

 

여기서 즉 T 이라는 임의의 Type 에 관해서 크기를 알아 올 수 있는겁니다.

 

여기서 어떻게 Type 이 지정 되냐를 알 수 있는데.

3000을 넘었을 경우는 2byte 정수형 으로 인정한 것을 알 수 있습니다.

 

정수형의 경우는 입력한 숫자의 크기에 따라 1~8 byte 정수형으로 함수가 생성 됩니다.

4.0을 넣었을 경우는 크기가 10이 나왔는데, 모든 실수형은 Extended 형으로 생성합니다.

 

Integer형의 변수 B를 넣었을 경우는 이미 타입이 Integer  라는 것을 명시적으로 선언 하였으므로 크기는 4Byte 가 됩니다.

 

TempRecord 형의 변수는 당연히 20Byte가 나옵니다(Integer + Double + Double)

 

이처럼 하나의 함수로 다양한 Type 에 대한 계산이 가능해 집니다.

 

이것은 컴파일 시점에 GetSize 라는 함수를 프로그래머가 사용한 Type 에 따라 여러벌 만들기에 가능한 것입니다.

 

디스어셈블한 코드를 구경 해보도록 하겠습니다

Project1.dpr.22: WriteLn('Size : ', Sizeof(Item));

0040E1BB A1980A4100       mov eax,[$00410a98]

0040E1C0 BAF0E14000       mov edx,$0040e1f0

0040E1C5 E86A73FFFF       call @Write0UString

0040E1CA BA0A000000       mov edx,$0000000a

0040E1CF E89856FFFF       call @Write0Long

0040E1D4 E8BF56FFFF       call @WriteLn

0040E1D9 E8724DFFFF       call @_IOTest
Project1.dpr.22: WriteLn('Size : ', Sizeof(Item));

0040E200 A1980A4100       mov eax,[$00410a98]

0040E205 BA30E24000       mov edx,$0040e230

0040E20A E82573FFFF       call @Write0UString

0040E20F BA04000000       mov edx,$00000004

0040E214 E85356FFFF       call @Write0Long

0040E219 E87A56FFFF       call @WriteLn

0040E21E E82D4DFFFF       call @_IOTest

Project1.dpr.22: WriteLn('Size : ', Sizeof(Item));

0040E240 A1980A4100       mov eax,[$00410a98]

0040E245 BA70E24000       mov edx,$0040e270

0040E24A E8E572FFFF       call @Write0UString

0040E24F BA14000000       mov edx,$00000014

0040E254 E81356FFFF       call @Write0Long

0040E259 E83A56FFFF       call @WriteLn

0040E25E E8ED4CFFFF       call @_IOTest

 

 

이와 같이 같은 이름의 함수가 비슷비슷한 코드로 여러벌 생성 되어 메모리에 들어있는 것을 확인 할 수 잇습니다.

 

 

이번엔 Type 을 명시적으로 알려주는 코드를 보도록 하겠습니다.

A.GetSize(3); 의 호출 결과는 1이었습니다.

숫자가 작기떄문에 최대한 작은 크기의 메모리를 소모 하기 위하여 Byte 형으로 선언 된 결과입니다.

 

그런데 간혹 정해진 Type 이 필요 할 수도 있습니다.

이럴경우 Type 을 명시적으로 지정해서 호출 해 보겠습니다.

 

A.GetSize<Integer>(3);

로 코드를 변경 한 후 호출하면 4가 출력 되는 것을 알 수 있습니다.

 

무조건 Integer 형의 함수를 호출 하라는 일종의 협박(?) 비슷한 코드를 만들어 낸 셈이 되는겁니다.

 

 

 

다음으로 알아 볼 것은 모든 알고리즘이 임의의 타입에 대해 동작 될 수 없는 경우에 관한 해결책입니다.

 

위의 코드에서 Integer, Double, Record 형의 크기는 아무 문제 없이 구할 수 있었지만, String 형의 크기는 sizeof() 연산자로 구할 수 없습니다.

 

이럴경우 String에 관해서만 특별한 처리를 해 주어야 하는데이는 익히 잘 아시는 오버로딩을 응용하면 됩니다.

 

예제코드를 보겠습니다.

program Project1;

 

{$APPTYPE CONSOLE}

 

uses

  SysUtils, VarUtils;

 

type

  TempClass = Class

  public

    procedure GetSize(Item : String); overload;

    procedure GetSize<T>(Item : T); overload;

  end;

 

  TempRecord = packed Record

    A : Integer;

    B : Double;

    C : Double;

  End;

 

procedure TempClass.GetSize<T>(Item : T);

begin

  WriteLn('Size : ', Sizeof(Item));

End;

 

procedure TempClass.GetSize(Item : String);

begin

  WriteLn('Size : ', Length(Item));

End;

 

var

  A : TempClass;

  B : Integer;

  C : TempRecord;

begin

  A.GetSize<Integer>(3);

  A.GetSize(4.0);

 

  B := 30;

  A.GetSize(B);

 

  A.GetSize(C);

  A.GetSize('Hello Delphi!');

  ReadLn;

end.

 

GetSize 매소드를 T 형과 String 형에 관해 각각 오버로딩 하엿습니다.

String 형으로 GetSize 를 호출하면 Length 를 호출해서 계산하는 함수가, 다른 형으로 GetSize를 호출하면 Sizeof 를 사용하는 함수가 호출 됩니다.

 

이처럼 특정한 Type 에 관해 매소드를 다른 형태로 사용 할 수 있습니다.

 

 

지금까지는 함수의 Generic 을 살펴 보았습니다. 이제 클래스에 관한 Generic 을 살펴 보도록 하겠습니다.

 

program Project1;

 

{$APPTYPE CONSOLE}

 

uses

  SysUtils, VarUtils;

 

type

  TempClass<T> = Class

  public

    Item : T;

  end;

 

  TempRecord = packed Record

    A : Integer;

    B : Double;

    C : Double;

  End;

 

var

  A : TempClass<String>;

  B : TempClass<Integer>;

  C : TempClass<TempRecord>;

begin

  A := TempClass<String>.Create;

  A.Item := 'Hello RAD Studio!';

 

  B := TempClass<Integer>.Create;

  B.Item := 512;

 

  C := TempClass<TempRecord>.Create;

  C.Item.A := 55805096;

  C.Item.B := 3.14159;

  C.Item.C := 0.0;

  ReadLn;

end.

 

위 코드에서는 T 라는 임의의 타입을 가지는 클래스 TempClass<T> 를 선언하엿습니다.

그리고 각각 : String, Integer, TempRecord 형을 가지는 클래스 TempClass<String>. TempClass<Integer>, TempClass<TempRecord> 를 선언 하엿습니다 (TempClass<타입명> 까지가 클래스 이름이라고 생각하시면 편합니다)

 

그리고 각각의 클래스를 생성하고 Item 변수에 접근하여 값을 넣었습니다.

선언한 형태에 따라 TempClass 는 여러 타입의 Item 을 가지는 클래스로 탄생합니다.

 

TIntegerList, TDoubleList 등등을 따로 제작 할 필요 없이 임의의 타입을 가지는 콜렉션을 만들어 쓸 수 있습니다. 사실 이게 Generic을 쓰는 가장 큰 이유라고도 할 수 있습니다. Generic 으로 콜렉션을 만들면 캐스팅이 필요 없고, 타입이 명확하기 때문에 편리하게 사용 할 수 있습니다.

(.net Dictionary<> , STL Vactor<>, List<>, MFC CArray<,> 등이 그에 해당합니다)

마지막으로 한가지만 더 보도록 하겠습니다.

program Project1;

 

{$APPTYPE CONSOLE}

 

uses

  SysUtils, VarUtils;

 

type

  TempClass<T1, T2> = Class

  public

    Item1 : T1;

    Item2 : T2

  end;

 

var

  A : TempClass<String, Integer>;

begin

  A := TempClass<String, Integer>.Create;

  A.Item1 := 'Hello RAD Studio!';

  A.Item2 := 3;

 

  ReadLn;

end.

 

임의의 Type 은 반드시 한 개여야 한다는 제약은 없습니다.

여러 개의 임의의 Type 을 가지는 클래스도 얼마든지 선언 가능합니다.

 

위 코드는 2개의 임의의 Type을 가지는 TempClass<,> 를 선언, 사용 한 모습입니다.

 

 

PS. 간단한 함수도 매소드로 만들어서 예제를 구성했는데... 제가 잘 못해서 그런건진 모르겠는데, 델파이의 제네릭은 전역함수의 제네릭화를 허용 하지 않는 것 같습니다.

 

----

?
  • ?
    GomSun2 2010.06.17 11:24

    와우 좋은 강좌 잘봣음. :)


Board Pagination Prev 1 ... 4 5 6 7 8 9 10 11 12 13 Next
/ 13