[출처 : http://cboard.cprogramming.com/c-programming/109648-fwrite-fread-question.html]

회사 동료가 찾아준 링크에서

bigendian을 littleendian로 바꿨다.


그리고 fwrite랑 동일하게 작동하려면,

else 부분에서 반환되는 값을 size로 나눠야 한다.


static size_t littleendian_fwrite(const void* data, size_t size, size_t count, FILE* stream)

{

uint32_t i = 1;

const char* c = (char*)&i;

int32_t written = 0;

if (!(*c == 0))

return fwrite(data, size, count, stream);

else

{

c=data-1;

while(count-- > 0)

{

c+=size;

for(i=0;i<size;i++)

written+=fwrite(c--,1,1,stream);

}

return written/size;

}

}



[출처 : http://semicolon238.com/333]


 프로그래밍을 하다보면 여러가지 난관에 부딧친다. 이는 정적인 부분에서도 마찬가지인데, 그난관이란 대체적으로 논리문제 아니면, 내가 만든함정문제 아니면, 자료구조문제이다. 뭐 이 포스팅에서 그렇게까지 크게 이야기할 것은 없고, 오늘 해볼것은 바로 배열의 차원을 속이는 것이다. 뭐 속인다고해도 크게 속일 것은 없고, 1차원과 2차원을 속일 뿐이다.

 그렇다면, 왜 속여야하는가? 그냥 주어진대로 쓰고, 솔직하게 바꾸면 어디가 덧나나? 라고 생각할지 모르지만 천만의 말씀 만만의 콩떡이다. 때론 솔직히 돌아가는 것보다 거짓말을 하는것이 문제해결을 쉽게 만든다. 2차원 배열은 깔끔하고 직관적이지만 어떤 특정한 상황에선 1차원이 더 의미 있을 수도 있다. 물론 그반대경우가 될 수도 있다. 반드시 4x4타일맵을 작성하는데 있어 2차원배열을 종용하는 사람은 유도리가 없다는게 내 생각이다. 항상 특수한 경우를 생각해야되고 어떤게 더 옳을지 생각해보는것은 바로 프로그래머의 몫이다.

 그리고 조금은 다른이야기지만 2차원배열보다 1차원배열이 더 좋은 이유는 오버헤드가 없다는 것이다. 2차원배열은 아무래도 다차원이다 보니 세로의 원소를 읽을때는 메모리를 내부적인 포인터가 꽤 크게 건너뛰어야된다. 그러나 결국 메모리는 선형적이고 1차원배열과 그 쓰임이 비슷하다보니 2차원, 혹은 다차원배열보다는 1차원배열이 훨씬빠르다는 결론에이르게된다. 이는 배열의 크기가 크면클수록 더 커지게된다. 물론 이러한 기법을 이용한다고 해서 크게 성능이 향상되지는 않지만(어차피 2차원을 쓰는것처럼 1차원을 쓰므로) 그래도 순수하게 2차원배열을 쓰는것보다는 약간 빠르게된다. 물론 속도때문에 항상 이렇게 써야된다고 자신을 학대하는것 역시 옳지않은 생각이므로 적절한 상황에서 자신의 판단에 맞추어 제대로 쓰도록 하자. 그럼 지저분한 서론은 집어치우고 당장 본론으로 넘어가보겠다.


int arr[5][3];
 여기에 이렇게 2차원배열 하나가 있다. 왜 쓰는지는 필요없고, 그래봤자 접근과 데이터변경의 용의성인 이유가 대부분이니 넘어가자.


arr[3][2] = 3;
 이런식으로 데이터를 원하는위치에 쉽게 넣을 수 있다. 뭐 여기까진 좋다. 


void array_print(int* arr[]);
 함수가 조금씩 더러워지기 시작한다.


void file_save()
{
    for(int y = 0; y < 5; ++y)
        for(int x = 0; x < 3; ++x)
            fprintf("%d", arr[y][x]);
}
 저장할때도 길어진다. 만일 일반자료형이 아닌, 내부적으로 계산해주어야 되는 경우나 데이터가 여러차원으로 분리되어있을떄는 저장위치 잡기도 애매해진다. 그럼 여기서 정리. 어떻게하면 1차원배열로 2차원처럼 보일 수있을까.


int arr[x * y];
 1차원과 2차원을 곱한것이 결국엔 해당 2차원배열의 원소수이므로, 이 원소수를 한계값으로 두는 1차원배열을 생성한다. 이거라면 여기 올릴만한 문제가 아니잖아! 라고 할텐데.. 조금 더 설명을 들어보시라.


for(int i = 0; i < (x * y); ++i)
    arr[i] = 3;
 값을 넣을때는 이렇게 넣는다. 그러나 그렇게되면 어렵잖아! 라고 말할것이 분명하다. 그렇다. 1차원배열을 2차원처럼 쓰는것은 쉬우나 값을 넣을때 일일히 계산해줘야되는 문제가 있었다. 그렇다면 이를 이렇게 해결해보자.


void set_array_element(int* pArr, int x, int y, int xcount)
{
    pArr[(y * xcount) + x] = 0;
}
 결국에는 현재 2차원배열의 가로사이즈(2차원에서의 x축)을 기반으로 하여 실제적인 1차원의 원소위치를 알아낸다. 획기적인건 아니고 많이 쓰는 방법이지만 아주 깔끔하고, add_data를 사용하는 사람 입장에서는 x,y축을 삽입하여 원하는 원소에 값을집어넣으므로 아주 좋은방법이다. 여기서 xcount를 모르는 사람은 없으리라고본다. 왜냐면 대부분 2차원배열은 맵등의 구조로서 쓰이는데 맵의 가로크기를 생각안하고 짜는 사람은 없기때문이다. 이는 값을 얻을때도 분명히 나타난다.


int get_array_element(int* pArr, int x, int y, int xcount)
{
    return pArr[(y * xcount) + x];
}
 이 함수만 쓴다면 값을 얻어오려는 사람은 2차원배열의 첨자를 기입하여 1차원배열의 값을 얻어온다. 조금 돌아가는 감이 있지만 생각해보면 꽤 깔끔한 방법이다. 그렇다면 이번엔 반대로 2차원을 1차원처럼 속여보자.


int arr[10][5];
여기에 2차원 배열이 있다. 역시 왜 쓰는지는 필요없고 이를 1차원배열처럼 써보자. 


arr[y][x] = 0;
 보통은 이렇게 해서 넣어준다. 아까 1차원배열을 2차원처럼 속일때도 결국엔 원소의 x위치와 y위치를 이용하여 실제원소위치 i를 만들어내었다. 그렇다면 이번엔 반대로 하면 되는 것이다. 원소의 위치 i를 받아서 x와 y를 생성해보자.

void set_array_element(int* arr[], int i)
{
    int x = i % xcount;
    int y = i / xcount;
    arr[x][y] = 0;
}
 깔끔하다. 무엇보다 이 방법이 좋은점은 현재 위치를 입력하면 내부적으로 x와 y를 만들어버렸다는 것이다. 위의 방법과 병합하면, 2차원원소와 1차원원소의 복사가 엄청 쉬워진다.


int arr1[50];
int arr2[10][5];

// 1차원기준으로 1차원과 2차원의 데이터를 서로 옮겨보자.
void swap_array_d1tod2(int* arr1, int* arr2[], int i, int xcount)
{
    int temp = arr1[i];
    arr1[i] = arr2[i/xcount][i%xcount];
    arr2[i/xcount][i%xcount] = temp;
}

// 2차원기준으로 1차원과 2차원의 데이터를 서로 옮겨보자.
void swap_array_d1tod2(int* arr1, int* arr2[], int x, int y, int xcount)
{
    int temp = arr1[(y * xcount) + x];
    arr1[(y * xcount) + x] = arr2[y][x];
    arr2[y][x] = temp;

 매우깔끔하다. 실제로 1차원과 2차원 각각에 반대되는 형태의 원소위치를 넣어도 내부적으로 현재 원소의 위치를 구해내어 결국에는 서로의 값을 바꾸는데 아무런 지장이 없어지는 것이다. 만일 이러한 기법들을 사용하지 않는다면 소스는 훨씬 지저분해지며, 쓸때없이 변수들을 마구잡이로 생성하는 등의 안좋은 우회방법을 사용하기 일쑤일 것이다.

이 기법들은 사실 평소엔 거의 쓸일이 없다. 하지만 2차원배열을 사용하는 텍스쳐나, 게임의 배경맵 등을 짤때는 알아두면 알아둘수록 도움되지 않겠는가? 이런게 다 노하우인 것이다. 별것아닌 팁을 주의깊게 읽어주어 고맙다는 말로서 오늘의 이 포스팅을 마치도록 하겠다.
[출처 : http://bufferoverflow.tistory.com/tag/포인터%20배열%20속도]


우선 배열의 경우에는

시작주소( 배열명칭) + sizeof( 데이타형 ) * 인덱스

와 같은 수식으로 
계산됩니다.


 
여기서   '+' 연산자의 앞부분은 일정하고  뒷부분에서  인덱스만 변하여

계산을 하는데  어셈블리어나 기계어로 번역될 때의 자세한 상태를 보지 아니해도

+ 연산자를 전후로 대략 두개의 피연산자을 동반합니다.

ADD operand1 operand2  이고

operand2는 다시 곱셈에서 얻어지므로 대략  

MUL operand2 operand3  와 같습니다



그리고 포인터에 의한 주소연산에서는  
시작시에  pa를 시작주소를 설정하고나서

이 하나의 피연산자에 대하여만 연산을 하게됩니다.

 

그래서 

INC operand1

기계어로 번역될 때 명령코드도 간단하게되므로 메모리도 절약되지만

덧셈의 연산에 비하여 속도가 차이가 납니다.

굳이 어려운 포인터를 사용하는 이유가 있다고 봅니다.

 

좀더 자세히 설명을 한다면

arr[i] = ?  와 같은 배열문장은 배열의 i 인덱스 요소에 ?라는 값을 대입하라는 

의미입니다. 

값을 대입하기 위하여는 우선 그 주소로 이동하기 위하여 주소연산을 하게됩니다.

 

arr[i] = ? 에서 배열의 '[i]'연산자는  *( arr + i ) = ? 와 같은 표현식으로

간주되는 데  (arr + i )는 arr라는 배열의 이름( 상수의 주소값 )에 

정수를 더하여 그 주소를 계산하라는 의미이고 

'*' 연산자는  arr가 data라는 구조체형의 포인터이므로 그 주소에서 data라는 구조체형의 크기 공간에 있는 내용물을 ? 라는 값으로 채워라는 뜻입니다.

 

여기서 포인터 연산의 정수 덧셈은 arr라는 주소값에 sizeof( data ) * i의

바이트 수를  더하게 됩니다.

매번 i라는 배열요소의 인덱스가 변할 때 마다 내부적으로는

( 시작주소 + 데이타형의 크기 * 인덱스 )라는 주소계산을 합니다.

 

반면에 포인터를 사용하는 경우에는  

*pa = ?; 

pa++; 에서

 

*pa = ? 는 현재의 pa라는 주소에 data형의  크기공간안의 내용을 ? 라는 값으로

채워라는 의미입니다.

pa++; 는 다음  요소에 접근하기 위한 주소연산을 하는 문장입니다.

 

pa++ 연산은 배열의 '[]'연산자를 사용하는 경우와는 달리  

pa + sizeof( data );  와 같은 계산으로 주소값을 계산하게됩니다.

 

즉 현재의 요소가 위치하는 pa라는  주소에  다음 주소로 옮기기 위하여

구조체형의 크기만큼의 바이트수를 더하여 얻는 주소값으로 계산됩니다..

 

기계코드로 번역될 때   sizeof( data ) * i  와 같은 곱셈연산은 많은 시간을

소모하는 것으로 알려져있습니다.

그래서 배열을 사용하는 것은 프로그래머의 입장에서는 사용하기 편하고 쉽게

인식하지만 기계코드를 전환될 때는 후자의 포인터를 사용할 때 만큼의

속도를 얻지 못합니다.

 

물론 소수의 배열요소들에 접근하여 작업하는 경우야 눈에 띄는 

차이를 느끼기 어렵겠지만  방대한 DB에서 어떤 데이타를 검색한다던지

할 때를 생각해보시면 짐작할 수  있을 것입니다.

 

반면에 포인터는 학습하는데 대체로 많은 수고를 요구하거나 디버깅을 할 때

쉽지 아니하지만. 배열을 사용할 때보다도 실행속도에서는 빠른다는 장점이 있으므로

각각의 장단점이 있습니다.

    

보통 포인터 식이라고 할때   arr[i]를   *(arr + i )를 고치는 것을 포인터식이라고

말하기도 하는 데  이것은 내부적으로 는 같은 코드를 생성하므로 같은 표현식으로

보아야하고 순수한 포인터 연산에 의한 코드 작성은 포인터의 단항연산자를 사용하여
 
배열의 요소로 이동하면서 필요한 연산을 

수행하는 경우라고 봅니다.

[출처 : http://bufferoverflow.tistory.com/entry/2차원-배열이-더블포인터인가]


int a[4];

in *p = a;

일때 a[0] == p[0], a[1] == p[1] 이라고 하는 것을 보고

 

in a[3][4]; 일 때

int **p =a;

이고 a[0][0] == p[0][0], a[1][2]== p[1][2]로 알고 계시는 분들이 있으십니다.

혹은 왜 이게 안되는지 궁급해 하십니다.

 

그래서 이런 글을 올립니다. 참고하세요

 

배열의 등가포인터 의미와 생성 그리고 활용에 대하여 다룹니다.

 

자 이런 배열이 있습니다

int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};

 

이때 배열의 이름 a는 배열의 정의상 배열의 첫요소의 주소입니다.

 

그러면 이는 배열의 첫요소 즉, a[0][0]의 주소 즉, &a[0][0] 일까요?

아니면 &a[0] 일까요? 값만 놓고 보면 둘 다 같은 값이 나옵니다.

그러면 의미상 정확하게 맞는 표현은 무엇일까요?

 

int a[3][4] 배열은 요소 12개 짜리 1차원 배열이 아닙니다.

메모리에는 12개의 숫자가 연속해서 존재하기는 하나 이를

편의를 위하여 2차원 배열처럼 생각하는 거지요

 

고로 int a[3][4]는 요소수 3개 짜리 배열입니다. 즉, 3칸짜리 배열이지요

 

그런데 이 배열은 각 칸칸마다 뭐가 들어 있냐면 정수가 있는것이 아니라

int [4] 즉, int 4개짜리 배열이 들어있는 겁니다.

 

따라서 이 배열의 요소수는 3개이고 이 배열의 첫요소는 a[0] 가 됩니다.

고로 배열명 a == &a[0]라고 해야합니다.

 

그러면 어차피 값은 &a[0][0]나 &a[0]이나 같은데 왜 꼭 &a[0]가 맞다고 할까요?

자 다음을 보시지요

 

int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};

int *p = a;

int (*q)[4] = a;

int **r = a;

 

세개의 포인터에 배열명 a를 담았습니다. 어느것이 옳을까요?

여기서 옳다는 것은 단지 대입이 되느냐 아니냐가 아니라

컴파일리 에러, 경고 없는 경우입니다.

 

사실 대부분 컴파일러는 int *p = a; 는 경고를 내거나 엄격한 컴파일러는

에러를 냅니다. 고로 에러, 경고를 막으려면 casting을 써서

int *p = (int *)a; 라고 해야 합니다.

 

그러나 두번째 int (*q)[4] = a; 는 에러, 경고 나지 않습니다.

즉, 두개는 완벽히 일치하는 데이터 타입이라는 뜻이지요...

 

세번째 2중 포인터는 int **r = p;는 역시 첫번째와 마찬가지로

타입이 다른 주소가 대입되므로 에러 혹은 경고가 납니다.

정상이려면 int **r = (int **)a; 하셔야 합니다.

 

자 왜 두번째 포인터만 정상일까요?

위에서 얘기 했듯이 int a[3][4] 일때 배열명 a는 &a[0]의 주소인데

a[0]은 a배열의 첫요소 즉, 3칸 짜리 배열에는 int [4]

즉, int 4개 짜리 배열이 들어 있으므로

a[0]는 그 속의 int [4] 배열의 이름이 됩니다.

 

고로 &a[0]는 바꿔 말하면 int [4] 배열의 주소이므로

타입은 int (*)[4] 라고 쓸수 있습니다.

그런데 a == &a[0] 이니 결국 배열 이름은 int (*)[4] 와 같은 타입입니다.

 

그러므로 이러한 주소를 받는 포인터는 int (*q)[4] 로 선언하게되고

int (*q)[4] = a; 가 성립하는 것입니다.

 

그럼 다시 다음에서 봅시다.

 

int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};

int *p = (int *)a;

int (*q)[4] = a;

int **r = (int **)a;

 

이렇게 강제로라도 a를 대입했다면 이 포인터들로

배열에 있는 a[1][2] 즉,  7을 억세스 하려면 어떻게 할까요?

 

int *p는 현재 값이 정수 즉, 1의 주소를 갖으므로

그리고 정수(int)의 포인터 이므로

printf("%d\n", *(p+6)); 하거나 혹은 [ ] 연산자를 이용하여

printf("%d\n", p[6]); 으로 해야 합니다.

 

그러나 두번째는 int (*q)[4]의 현재값이 {1,2;3,4} 배열의 주소이므로

a[1][2]와 동일한 표현은 q[1][2] 가 됩니다.

이것이 왜 이렇게 되는지는 다차원 배열을 공부하시면 됩니다.

 

즉,

int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};

에서 a[1][2] 와 같은 값을 억세스 하기 위하여

 

int *p = (int *)a; 로 하면 p[6]  혹은 *(p+6)

int (*q)[4] = a; 로 하면 q[1][2] 로 됩니다.

두 방법중 어떤것이 이해가 쉽습니까?

 

이와같이 배열의 이름을 대입받을 때 에러, 경고, 캐스팅 없이 대입 가능한

포인터를 배열의 등가포인터라고 하고 등가포인터가 배열의 이름을 받으면

그때부터는 포인터가 배열과 표현식이 동일하게 사용가능합니다.

 

물론 그렇다고 이것이 완전히 동일해 지는것은 아닙니다.

배열은 배열이고 포인터는 포인터입니다. 단지 배열명을 갖는 포인터이지요

 

단, 억세스는 간접연산(포인터로 인하여)이므로 억세스 속도는 배열보다 느립니다.

 

그럼 1차원 함수에 넘기는 경우를 보지요...

 

void main(void)

{

   int a[4] = {1,2,3,4};

   func(a);

}

라고 할때 func 함수는 넘겨받은 배열의 이름을 이용하여

배열내의 a[2]인 3을 인쇄한다고 합시다.

 

그럼 어떻게 하나요?

void func(int *p)

{

    printf("%d\n", p[2]);

}

라고 할것입니다.

 

 이 상황을 잘보면

 

(1) main에서 func를 호출하면서 넘긴것은 배열의 이름 a입니다.

1차원 배열이므로 이 이름은 첫요소의 주소 즉, &a[0]  이고 a[0]는 정수 1이므로

int * 타입입니다.

 

(2) 고로 func에서는 이런 배열의 이름과 등가인 int *p로 받는것입니다.

즉, int *p = a; 가 가능하므로 int *로 한것이지요

고로 p는 표현식에서 a 배열과 동일하므로 a[1] ==p[1]이 됩니다.

 

즉, 배열은 전체를 넘기지 못하고 그 첫요소의 주소(배열명)를 넘기므로

 

이를 등가포인터로 받고 활용은 마치 배열인듯이 하는 겁니다.

 

이것이 등가포인터의 활용이지요...

 

그럼 2차원 배열을 넘기면 어떻게 되나요?

 

int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};

func(a);

에서 a[1][2] 와 값을 인쇄하는 함수는 어떻게 만드나요?

 

(1) int func(int *p)

(2) int func(int (*p)[4]);

(3) int func(**p); 어느것이 옳을까요?

 

만약 1처럼 하면?

 

func(a); 하면 경고가 발생하므로

억지로 캐스팅 하여 func((int *)a); 로 해야 합니다.

그리고 함수는 이렇게 만들지요

 

int func(int *p)

{

    printf("%d\n", p[6]);

    또는

    printf("%d\n", *(p+6));

}

 

그런데 2번처럼 하면 아까 얘기했듯이 int (*p)[4]는 배열명 a 와 등가이므로

int (*p)[4] = a; 가 가능합니다.

 

고로 활용이 마치 a 배열처럼 되므로 다음과 같이 됩니다.

 

int func(int (*p)[4])

{

    printf("%d\n", p[1][2]);

}

 

3번 처럼 단순히 2중포인터라고 생각한다면?

이거 골치아퍼집니다. 아마 세가지 방법에서 제일 골치아프겠네요

한번 직접 해보세요...

 

이때 p[1][2]는 절대 배열이 아닙니다. p는 엄연한 포인터입니다.

그럼 어떤것이 쉽고 정상적인 방법일까요?

 

배열을 넘기고(배열명을 넘기는 거지만) 이를 등가 포인터로 받고

그리고 이를 활용할때는 마치 원래의 배열처럼 하는것 이게 등가포인터를 활용한 겁니다.

 

다음은 2차원 배열을 넘기고  배열요소의 전체 합을 구하는 함수를 설계합니다.

역시 등가 포인터를 활용합니다.

 

결국

 

void main(void)

{

    int a[3][4] = {1,2,3,4,5,6,7,8,9,10,11,12};

    printf(""%d\n", sum(a));

}

 

int sum(int (*p)[4])

{

    int a = 0, i, j;

    for(i=0; i<3; i++)

   {

       for(j=0; j<3; j++)

      {

           a += p[i][j];

      }      

   }

  return a;

}

 

다시한번 말씀드리지만

위와 같이 배열을 다른 함수로 넘기거나 다른함수에서 리턴하거나

할때 배열명을 넘기고 받기는 포인터(등가포인터)로 받고

활용은 넘겨준 배열과 동일하게 하는것이 좋습니다.

이런 포인터를 등가포인터라고 합니다.

 

결국 등가포인터를 이용하면 배열, 함수를 함수로 넘기거나 함수 리턴으로 줄 때

매우 쉬워 집니다.

 

참고로 다음은 등가적인 포인터의 예를 보입니다.

 

(1) int a[4]  ==> int *p

(2) int *a[4] ==> int **p

(3) int a[3][4] ==> int (*p)[4]

(4) int a [2][3][4] ==> int (*p)[3][4]

(5) int *(*a[3])(int) ==> int *(**p)(int)

[출처 : http://blog.daum.net/jchern/13756798]


블로그에서 strncpy와 memcpy 중 memcpy가 더 빠르다고 본 기억이 있어서 확인차원에서 구글 codesearch를 빌려 확인

 

 

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

                                     STRNCPY

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

char *
strncpy(char *dst, const char *src, size_t n)
{
        if (n != 0) {
                char *d = dst;
                const char *s = src;

                do {
                        if ((*d++ = *s++) == 0) {
                                /* NUL pad the remaining n-1 bytes */
                                while (--n != 0)
                                        *d++ = 0;
                                break;
                        }
                } while (--n != 0);
        }
        return (dst);
}
 -------------------------------------------------------------------

strcpy는 assign 연산후에 널인지를 체크하는 비교 연산이 한번더 수행되고

널체크이후 타켓에 남은 size n만큼 0으로 패딩한다.

 

 

 

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

                                     MEMCPY

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

void *memcpy(void *_dst, const void *_src, unsigned len)
{
        unsigned char *dst = _dst;
        const unsigned char *src = _src;
        while(len-- > 0) {
                *dst++ = *src++;
        }
        return _dst;
}
 -------------------------------------------------------------------

memcpy는 심플하게 len만큼 복사하고 끝...

 

 

정확히 size를 알고 있다면 strncpy보다는 memcpy가 속도상 약간의 이득이 있는것으로 보임..

 

[출처 : http://irgroup.org/zbxe/5022]



n-gram이란 입련된 문자열을 n개의 음절단위로 절단하는 방법입니다.

예를 들어 "정보검색" 이란 문자열을 절단할 때..

1-gram : 정, 보, 검, 색 으로 분리

2-gram : 정보, 보검, 검색 으로 분리

3-gram : 정보검, 보검색 으로 분리

...

 

^_^ 간단하죠?

이 방식은 초기 검색엔진들에서 키워드 추출하는 방식으로 많이 애용되었습니다.

단순한 방식과 빠른 키워드 추출로 인기가 좋았죠.

특히나 미국의 검색엔진들이 CJK(중국,일본,한국)의 언어들을 처리하기 위해 형태소분석기를 만들수 없는 상황에서 많이 애용되었습니다.

부산물로 "사오정검색"이란 단어가 생겨나기도 했구요.. "보검"을 검색하면 정보검색이 나타나니까요..

 

서론이 길어졌습니다.

다음은 소스코드입니다.

=============================================================================

/******************************************************************************
N-Gram module

화일명: ngram.h
작성자: 
작성일: 2001.12.03
내  용:

******************************************************************************/

#ifndef __N_GRAM_H__
#define __N_GRAM_H__

 

#define MAX_N_GRAM_NUM  10
#define MAX_ONE_WORD_SIZE ((MAX_N_GRAM_NUM + 1) * 2)
#define MAX_N_GRAM_WORD  10000
#define MIN_N_GRAM_NUM 1

 

// n-gram을 사용하여 string을 분해한다.
int NGram(unsigned char *str, int min, int max, unsigned char value[][MAX_ONE_WORD_SIZE]);

 

// n-gram으로 분해된 term들에 대해 중복을 제거하고 중복 갯수를 저장한다.
// 중복갯수는 각 단어의 처음에 한바이트로 기입된다.
int AvoidDuplicationTerm4NGram(unsigned char value[][MAX_ONE_WORD_SIZE], int num);

#endif

 

/******************************************************************************
N-Gram module

화일명: ngram.c
작성자: 
작성일: 2002.12.03
내  용:

******************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "ngram.h"

 

/******************************************************************************
AvoidDuplicationTerm4NGram
 N_Gram 함수로 stem된 term들의 중복 여부를 검사하여 중복된 term들을 배제시킨다.
 이 때 value 변수의 각 단어들은 첫번째 바이트로 중복된 갯수를 가진다.

input : unsigned char value[][MAX_ONE_WORD_SIZE]
  int num  현재 item의 갯수
output: unsigned char value[][MAX_ONE_WORD_SIZE]
return: 중복 배제시킨 term의 갯수
******************************************************************************/
int AvoidDuplicationTerm4NGram(unsigned char value[][MAX_ONE_WORD_SIZE], int num)
{
 int cnt;
 int i;
 int j;
 unsigned char tmp[MAX_ONE_WORD_SIZE];

 strcpy(tmp, value[0]);
 value[0][0] = 1;
 strcpy(value[0] + 1, tmp);

 for(i = 1, cnt = 1; i < num; i ++)
 {
  for(j = 0; j < cnt; j ++)
  {
   if(strcmp(value[i], value[j] + 1) == 0)
   {
    if(value[j][0] < 255)
     value[j][0] ++;
    
    break;
   }
  }

  if(j == cnt)
  {
   strcpy(tmp, value[i]);
   value[cnt][0] = 1;
   strcpy(value[cnt] + 1, tmp);

   cnt ++;
  }
 }

 return cnt;
}

 

/******************************************************************************
NGram
 입력된 문자열을 min~max 사이의 n-gram으로 절단한다.
 ascii code 128글자와 한글(ksc5601)코드에 대한 지원만을 한다.
 stem 된 각 term들의 최대 갯수는 10000개로 제한한다.
 한글 stem시 2byte씩 한글자를 이루므로 한 단어에 대한 버퍼의 크기는 두배로
 잡아야 한다.
 한글 정보검색에서 사용할 때는 1-gram과 bi-gram을 사용하는 것이 효율상 바람직.
 필요에 따라서는 3-gram도 적용해 볼만 함
 대소문자 구별을 한다.

input : char *str
  int min  n-gram 시작값
  int max  n-gram 마지막값
output: char value[][MAX_ONE_WORD_SIZE]
return: value count
******************************************************************************/
int NGram(unsigned char *str, int min, int max, unsigned char value[][MAX_ONE_WORD_SIZE])
{
 int cnt;
 unsigned char *p;
 unsigned char *src;
 int i;
 int j;
 int start;
 int end;
 int nletter;

 start = (min > 0) ? min : 1;
 end = (max < MAX_N_GRAM_NUM) ? max : MAX_N_GRAM_NUM;
 
 cnt = 0;
 for(i = start; i <= end; i ++)
 {
  src = str;
  while(*src)
  {
   p = src;
   nletter = 0;
   for(j = 0, nletter = 0; nletter < i && *p != 0; j ++)
   {
    // 한글인지 영문인지 검사
    if(*p < 0x80)
    {
     // 제어문자일 경우 중지
     if(*p <= 0x20)
      break;

     value[cnt][j] = *p;
     p ++;
     nletter ++;
    }
    else
    {
     value[cnt][j++] = *p++;
     value[cnt][j] = *p;
     p ++;
     nletter ++;
    }
/*
    else if(*p >= 0xa1 && *p <= 0xfe) // 한글코드 영역이면
    {
     value[cnt][j++] = *p++;
     value[cnt][j] = *p;
     p ++;
     nletter ++;
    }
*/
   }

   if(nletter == i) // 정상 추출
   {
    value[cnt][j] = 0;
    cnt ++;

    // stem한 term의 갯수가 MAX_N_GRAM_WORD보다 크거나 같으면 종료
    if(cnt >= MAX_N_GRAM_WORD)
     break;
   }

   if(*src >= 0xa1 && *src <= 0xfe)
    src += 2;
   else if(*src < 0x80)
    src ++;
   else
    src ++;
  }
 }

 return cnt;
}

 

==============================================================================

Copy&Paste했더니 엉망이네요. 

 

리눅스에서 gnu c를 사용했던 소스입니다만 윈도에서도 잘 돌아가네요.

[출처 : http://sp0ngee.tistory.com/51]


어느날 strtok() 함수의 메뉴얼 페이지를 보게 되었는데 다음과 같은 내용이 있었습니다.

버그
이 함수를 사용해서는 안된다. 만일 사용해야 한다면, 다음을 주의하라:
이 함수는 처음 인자를 수정한다.
구분자의 원본은 잃게 된다.
이 함수는 상수 문자열에서는 사용해서는 안된다.
strtok() 함수는 파싱하는 동안 정적 버퍼를 사용한다. 그래서 thread safe가 아니다. 만일 이것이 문제라면 strtok_r() 를 사용해라.

여기서 말하는 thread safe란 것은 여러 thread가 동시에 사용되어도 안전하단 말입니다.
thread safe에 대해서 더 자세히 알고 싶으시면 출처의 링크를 참고하시거나, 구글에서 검색해 보세요.
[ 'thread safe' 출처: http://kldp.org/node/36904 ]

그래서 앞으로 문자열 파싱을 할 일이 생기거든 strtok_r() 함수를 사용해 보려고 합니다.
머리가 좋지 못한 저는 여기에 정리해 놓고, 필요할 때 가져다 쓸려구요...-_ㅜ
혹시 필요하신 분들도 가져다가 사용하시길 바랍니다.

또한, 코드 내용 중에 잘못된 부분이나, 수정하면 더~ 좋은 프로그램이 되겠다라는 곳이 있으면 주저하지 마시고 답변 달아주세요! 다른 분들이 잘못된 정보를 알아가면 안되니까요. ^^

사용 예제를 보기 전에 간단하게 메뉴얼 페이지 내용을 적어봤습니다.

이름
strtok, strtok_r - 문자열에서 토큰들을 뽑아낸다.

사용법
#include <string.h>
char *strtok(char *s, const char *delim);
char *strtok_r(char *s, const char *delim, char **ptrptr);

설명
`토큰`이란 문자열 delim에 속하지 않는 문자들로 이루어진 비어 있지 않은 문자열이며 \0 이나 delim에 있는 문자가 뒤따른다.

strtok() 함수는 문자열 s를 토큰으로 파싱하기 위해 사용된다. strtok()의 첫번째 인자로 s를 주면, 가장 앞에 있는 토큰을 구하고, 그 문자열안의 다음 토큰을 구하고자 할 때에는 첫번째 인자를 NULL로 설정하여야 한다. 각 호출은 다음 토큰에 대한 포인터를 반환하거나 더이상  토큰이  발견되지  않는다면 NULL을 반환한다. 토큰이 구분자로 끝난다면, 이 구분자는 \0 로 겹쳐 쓰여지며 다음 문자에 대한 포인터가 strtok()에 대한 다음 호출을 위해 저장된다. 구분 문자열 delim는 각 호출 시 다를 수 있다.

strtok_r() 함수는 strtok() 와 동일하게 작동한다. 그러나 정적 버퍼를 사용하는 대신에 이 함수는 char * 포인터로 할당된 유저에 대한 포인터를 사용한다. 이 포인터, ptrptr 파라미터는 같은 문자열을 파싱하는 동안 같아야만 한다.

다음은 예제 소스코드 입니다.
01.#include <stdio.h>
02.#include <string.h>
03.#define DELIM "_"
04. 
05.int main(void)
06.{
07.char buf[] = "My_name_is_ubuntu";
08.char tmp[4][10] = {};
09.char *token;
10.char *ptr[2];
11.int i = 0;
12. 
13.token = strtok_r(buf, DELIM, &ptr[0]);
14.while( token )
15.{
16.strcpy(tmp[i], token);
17.token = strtok_r(tmp[i], DELIM, &ptr[1]);
18.while( token )
19.{
20.strcpy(tmp[i], token);
21.token = strtok_r(NULL, DELIM, &ptr[1]);
22.}
23.token = strtok_r(NULL, DELIM, &ptr[0]);
24.i++;
25.}
26. 
27.for(i=0; i<4; i++)
28.printf("tmp[%d]: %s\n", i, tmp[i]);
29. 
30.return 0;
31.}

실행 결과 입니다.




*참고로 delim은 delimiter의 약자입니다. 단어의 뜻은 '구분 문자'입니다.
이 단어는 자기 테이프 등에서 데이터의 시작(끝)을 나타내는 문자나 기호를 말합니다.

+ Recent posts