2016. 12. 8. 13:56

안녕하세요. 게임 개발자 놀이터입니다.


이번 포스팅에선 버블 정렬에 대하여 설명해보려고 합니다.




버블 정렬 ( Bubble Sort )


버블 정렬 ( Bubble Sort )은 인접한 두 수 를 비교하여 큰 수를 뒤로 보내는 간단한 정렬 알고리즘입니다.

시간 복잡도는 O(N²)을 가지고 있습니다. 속도는 느린 편 이지만, 코드가 단순 해서 자주 언급됩니다.


원소의 이동이 꼭 거품이 수면으로 올라오는듯한 모습을 보이기 때문에 Bubble sort 라는 이름을 가지게 되었다고 합니다.



정렬 과정


간단하게 버블 정렬의 예시를 보여드리겠습니다.


아래와 같이 5개의 숫자가 주어졌다고 했을때,  Bubble Sort로 정렬 해보겠습니다.


9 5 1 3 7


먼저 첫번째 인덱스와 두번째 인덱스를 비교합니다.


9와 5, 9가 더 크므로 두 수 의 자리를 바꿉니다.


교제 전 : 9 5 1 3 7


교체 후 : 5 9 1 3 7


다음 인덱스를 하나 늘려서 두번째 인덱스와 세번째 인덱스를 비교합니다.


9와 1, 9가 더 크므로 두 수의 자리를 바꿉니다.


교체 전 : 5 9 1 3 7


교체 후 : 5 1 9 3 7


계속해서 인덱스를 늘려가며 값을 비교하고 자리를 바꿔줍니다.


교체 전 : 5 1 9 3 7

교체 후 : 5 1 3 9 7


교체 전 : 5 1 9 7

교체 후 : 5 1 3 7 9


이렇게 한번의 순회가 끝났습니다. 한바퀴의 순회가 끝나니까 가장 큰 수인 9는 자리 자리를 찾아갔습니다.


그럼 다음 순회에선 맨 마지막 원소는 방문할 필요가 없어졌네요.


맨 마지막 원소인 9는 고정하고 나머지 원소도 진행 해봅시다.


교체 전 : 5 1 3 8 9

교체 후 : 1 5 3 7 9


교체 전 : 1 5 3 7 9

교체 후 : 1 3 5 7 9


교체 전 : 1 3 5 7 9

교체 후 : 1 3 5 7 9


두번째 순회가 끝났네요. 예시로 랜덤하게 적은 숫자가 2번의 순회 만으로 정렬이 완료되어버렸네요.


그래도 어느정도 느낌을 잡힐꺼라 생각합니다.


이런게 바로 버블 정렬입니다! 하하


원소들이 큰 순서대로 뒤로 밀려가서 거품처럼 정렬되는 모습이었습니다.



예제 코드


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void BubbleSort(int arrays[], int length)
{
    for(int i=0; i<length; ++i)
    {
        forint j=0; j < length-i-1 ; ++j)
        {
            if(arrays[j] > arrays[j+1])
            {
                int swap = arrays[j];
                arrays[j] = arrays[j+1];
                arrays[j+1= swap;
            }
        }
    }
}
cs


간단하게 구현했습니다.



알고리즘 분석


정렬 알고리즘은 분석을 할 필요가 있습니다.


제가 위에 시간 복잡도가 O(N²)라고 적어두긴 했지만 어떻게 구하는지도 알아야 합니다.


비교 횟수


버블 정렬은 한번의 순회를 마칠때마다 비교 대상이 하나씩 줄어듭니다.

전체 원소 갯수를 n개 라고 하면 총 n-1번의 순회를 돌면 정렬이 끝나게 됩니다.

5개의 원소 배열을 정렬한다면 4+3+2+1 = 10번 비교 하게 됩니다.

이를 수식화 하게되면


(n-1) + (n-2) + (n-3) + ... + 3 + 2 + 1 = ( n-1 ) * n / 2 = n(n-1)/2


이렇게 됩니다.


자리 교환 횟수


최선의 경우, 그러니까 이미 정렬된 원소 배열일 경우, 정렬 과정중 자리 교환이 한번도 일어나지 않을겁니다.

최악의 경우, 원소들을 비교할때마다 자리교환이 일어나므로, 자리교환 횟수 또한 비교횟수와 같아질 것입니다.


최선의 경우 교환 횟수 : 0

최악의 경우 교환 횟수 : n(n-1)/2


비교 횟수 와 자리 교환 횟수를 더하면 n²-n 이 되는데 시간 복잡도는 상수는 생략하고 최고차항만 생각합니다.


따라서 시간 복잡도는 빅 오 표기법으로 O(N²) 이 됩니다.





밑의 자료는 제가 버블정렬에 대한 자료를 찾다가 발견한 자료들입니다.

버블 정렬의 이미지 예시 




버블 정렬의 동영상 예시

출처 ( 유투브 동영상(클릭) )





감사합니다.

Posted by 시리시안
2016. 12. 8. 11:43

안녕하세요 게임개발자 놀이터입니다.


이번 포스팅에선 순차탐색에 대하여 정리하겠습니다.


순차 탐색 (Linear Search)


사실 순차 탐색, Linear Search라고 이름이 거창하지만, 한번쯤은 누구나 다 해봤을 탐색법입니다.


맨 앞에서부터 맨 끝까지 순서대로 탐색을 하는 알고리즘입니다.


시간 복잡도는 당연히 O(n)을 가지고있습니다.


종료 조건


배열에 길이에서 숫자를 발견하거나, 끝까지 발견하지 못할 경우 종료됩니다.


예제 코드


1
2
3
4
5
6
7
8
9
10
int Algorithm(int[] arrays, int target)
{
 
    for(int i=0; i<arrays.Length; ++i)
    {
        if(arrays[i] == target)
            return i;
    }
    return -1;
}
cs


정말 간단했네요..


감사합니다.

Posted by 시리시안
2016. 12. 8. 10:46

안녕하세요. 게임개발자 놀이터입니다.


이번 포스팅에선 이진 탐색 알고리즘에 대해서 적어보려고 합니다.


이진 탐색 알고리즘 (Binary Search)


이진 탐색 알고리즘을 하기 위해선 데이터가 정렬되어있어야 합니다. 


정렬된 데이터가 아니면 적용이 불가능한 알고리즘이기 때문이죠.


코드를 살펴보기 전에 알고리즘에 진행 방식을 보겠습니다.


배열 { 1, 3, 4, 5, 7, 8, 10, 20, 60, 100 } 에서 5를 찾아보겠습니다.


 array[0]

 array[1] 

 array[2]

 array[3]

 array[4]

 array[5]

 array[6]

 array[7]

 array[8]

 array[9]

 1

10 

20 

60 

100 


첫번째 케이스


배열 시작 인덱스와 마지막 인덱스 값을 합하여 2로 나눕니다. 그 인덱스에 해당하는 값이 5인지 확인하고 아니라면 5보다 큰지, 작은지 확인합니다.


 (0 (시작 인덱스) + 9 (마지막 인덱스)) / 2 = 4.5 (나머지는 버립니다.) => 4


array[4]는 7 이므로 값이 같지 않으니 대 소 를 비교합니다.


array[4]가 찾고자 하는 값(5)보다 크니 4~마지막 인덱스 까진 탐색에서 제외합니다.



 array[0]

 array[1] 

 array[2]

 array[3]

 array[4]

 array[5]

 array[6]

 array[7]

 array[8]

 array[9]

 1

10 

20 

60 

100 


두번째 케이스


(0 (시작 인덱스) + 3 (마지막 인덱스)) / 2 = 1.5 => 1


array[1] 는 3 이므로 값이 같지 않으니 대 소 를 비교합니다.


array[1]가 찾고자 하는 값(5)보다 작으니 시작인덱스 ~ 1 까지 탐색에서 제외합니다.


 array[0]

 array[1] 

 array[2]

 array[3]

 array[4]

 array[5]

 array[6]

 array[7]

 array[8]

 array[9]

 1

10 

20 

60 

100 



세번째 케이스


(2 (시작 인덱스) + 3 (마지막 인덱스)) /2 = 2.5 => 2


array[2]는 4이므로 값이 같지 않으니 대 소 를 비교합니다.


array[2]가 찾고자 하는 값(5)보다 작으니 시작인덱스~2까지 탐색에서 제외합니다.



 array[0]

 array[1] 

 array[2]

 array[3]

 array[4]

 array[5]

 array[6]

 array[7]

 array[8]

 array[9]

 1

10 

20 

60 

100 


네번째 케이스


(3 (시작 인덱스)  + 3 (마지막 인덱스)) /2 = 3


array[3]은 5이므로 찾고자 하는 값과 같으니 반환하며 탐색을 종료합니다.



위처럼 절반 씩 줄어 들면서 값을 검색하니, 시간 복잡도는 O(Log N)을 가집니다.


값을 못찾았다면, 시작인덱스가 마지막인덱스 보다 커진 경우 탐색을 종료하며 실패를 반환합니다.



예시 코드


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int searchint(int[] intarrays, int index)
{
    int from = 0;
    int to = intarrays.Length - 1;
 
    while (from <= to)
    {
        int mid = (from + to) / 2;
 
        if (intarrays[mid] > index) 
        {
            to = mid - 1;
        }
        else if (intarrays[mid] < index)
        {
            from = mid + 1;
        }
        else
        {
            return mid;
        }
    }
    return -1;
}
cs



알고리즘의 종료 조건


위 코드처럼 시작인덱스(from)이 마지막인덱스(to)보다 커지면 종료합니다.

Posted by 시리시안
2016. 12. 7. 17:28

안녕하세요. 게임개발자 놀이터 입니다.


이번 포스팅에선 피보나치 수열에 대하여 정리 해보려고합니다.


피보나치 수열


피보나치 수열은 앞의 두 수 를 더해서 현재 위치의 수를 만드는 수열을 말합니다.


0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89 ...


시작 두 수 인 0, 1을 제외하고 3번째 수부터 바로 전 수 와 전 전 수의 합계를 나타내면 됩니다.


0, 1, (0+1) , ((0+1) + 1) , (((0+1) + 1) +(0+1)) ...


간단한 알고리즘이라서 그런지 구현 방법은 여러가지가 있습니다.


먼저 재귀함수 호출을 이용한 피보나치 수열 구현 입니다.


1
2
3
4
5
6
7
8
int Algorithm(int n){
    if (n == 1)
        return 0;
    else if (n == 2)
        return 1;
    else
        return Algorithm(n - 1+ Algorithm(n - 2);
}
cs


1번째와 2번째의 값은 0과 1로 정해져있으니, 각각 0과 1을 반환하고, n의 전 값과, n의 전전 값을 더해서 리턴하는 형식을 가지고 있습니다.


간단하게 구현을 완료했지만, Algorithm 함수를 들어갈때마다 계속 n이 1인지, 2인지를 검사 하고 시작해야 하네요.


반복전인 2회 검사를 제거 하기 위해 이번엔 반복문으로 구현해봤습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int Algorithm(int n){
    if (n == 1)
        return 0;
    else if (n == 2)
        return 1;
    else
    {
        int beforevalue = 0;
        int current = 1;
 
        for(int i=1; i<n; ++i)
        {
            int temp = current;
            current += beforevalue;
            beforevalue = temp;
        }
        return current;
    }
}
cs


간단하게 재귀 함수를 호출하는 부분을 for문으로 변경해봤습니다.


current는 현재 숫자, beforevalue는 이전 숫자입니다.

다음 숫자는 현재 숫자 + 이전숫자 이므로, current와 beforevalue를 더해서 구합니다.


이것 외에 다른 방법은.. 성능상으로는 비슷하지만 코드상으로는 다른 '꼬리 재귀 함수'를 이용한 구현법이 있습니다.


1
2
3
4
5
6
7
8
9
10
int Algorithm(int n){
    return AlgorithmTail(n, 01);
}
 
int AlgorithmTail(int n, int before, int next){
    if (n == 0)
        return before;
    else
        return AlgorithmTail(n - 1, next, before + next);
}
cs


구하려는 n번째 자리수를 이용하여 n을 한단계씩 줄여가며 before와 next를 더해가는겁니다. 


한 타임이 지나면

before는 next가 되며

next는 before + next가 됩니다.


그렇게 반복되며 꼬리재귀함수를 돌다가 n값이 0 이라면 현재 값(before)를 반환합니다.


꼬리 재귀 함수에 관해선 다른 포스팅에서 자세히 다루도록 하겠습니다.



감사합니다.



Posted by 시리시안
2016. 12. 6. 18:02

안녕하세요. 게임개발자 놀이터 입니다.


이번 포스팅에선 알고리즘 수행 시간을 분석하는 방법중 하나인 점근 표기법에 대해 포스팅 하려고 합니다.


대부분의 알고리즘은 입력된 크기가 작으면, 알고리즘의 효율과 상관 없이 빨리 끝납니다.


하지만 입력된 크기가 크면, 속도에 차이가 나게 됩니다. 그래서 알고리즘의 수행시간을 분석할 때는 항상 입력의 크기가 충분히 클때에 대해서 분석 합니다. 이를 점근적 분석 이라고 합니다.


대표적으로 5가지 표기법이 있습니다.

  • 대문자 O 표기법 ( 빅 오 표기법 )
  • 소문자 o 표기법 ( 리틀 오 표기법 )
  • 대문가 오메가(Ω) 표기법 ( 오메가 표기법 )
  • 소문자 오메가(ω) 표기법 ( 리틀 오메가 표기법 )
  • 대문자 세타(Θ) 표기법 (세타 표기법 )


하나씩 적어 보도록 하겠습니다.


대문자 O 표기법 ( 빅 오 표기법 )


빅 오 표기법은 컴퓨터 공학에서 알고리즘의 복잡도 또는 성능을 표현하기 위해 사용됩니다.

빅 오 표기법은 특히 최악의 경우를 표현하며, 알고리즘의 실행 시간이나, 사용 메모리 (메모리, 또는 디스크) 공간을 표현 하기도 합니다.


O(1)

O(1) 은 알고리즘의 실행시간 (또는 공간)이 입력되는 데이터의 크기에 상관 없이 항상 같은경우를 의미합니다.


1
2
3
4
5
6
7
8
bool IsNull(int[] intarrays)
{
    if (intarrays == null)
    {
        return true;
    }
    return false;
}
cs


위와 같은 경우 입력된 intarrays의 크기와 상관없이 if문 한번만 실행됩니다. 그래서 데이터의 크기에 상관없이 항상 같습니다.


O(N)

O(N)은 입력되는 데이터의 양에 따라 비례하여 처리 시간이 증가하는 경우를 의미합니다.

1
2
3
4
5
6
7
8
9
10
11
bool IsNull(int[] intarrays, int index)
{
    for(int i = 0; i < intarrays.Length; ++i)
    {
        if (intarrays[i] == index)
        {
            return true;
        }
    }
    return false;
}
cs


위와 같은 알고리즘의 경우 중간에 intarrays[i]와 index값이 같아 빠져나갈수도있습니다. 하지만 빅 오(Big O)는 최악의 경우에 대한 성능을 나타낸다는것을 기억해야합니다. 즉 항상 최대로 반복이 이뤄질경우를 상정한 한계값을 표시하는겁니다.


O(N²)

O(N²)는 수행 성능이 입력 데이터의 크기의 제곱승에 비례하여 증가하는 상황을 표현합니다. 


1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool IsNull(int[] intarrays, int index)
{
    for(int i = 0; i < intarrays.Length; ++i)
    {
        for(int j = 0; j < intarrays.Length; ++j)
        {
            if (intarrays[j] == index)
            {
                return true;
            }
        }
    }
    return false;
}
cs


이는 일반적으로 중첩된 반복문이 주어딘 데이터값들에 대해 수행하는 경우에 해당됩니다. 좀더 많은 반복문이 있다면 O(N³), O(N⁴)의 결과로 나타냅니다. 


O(log N)

O(log N) (로그형 빅오) 는 입력 데이터의 크기에 따라 처리 시간이 조금씩 증가하는 경우를 표현합니다.

이진 탐색 알고리즘이 대표적이죠!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int searchint(int* intarrays, int index)
{
    int from = 0;
    int to = intarrays.Length - 1;
 
    while (from <= to)
    {
        int mid = (from + to) / 2;
 
        if (intarrays[mid] > index) 
        {
            to = mid - 1;
        }
        else if (intarrays[mid] < index)
        {
            from = mid + 1;
        }
        else
        {
            return mid;
        }
    }
    return -1;
}
cs


좋은 알고리즘들이 대부분 가지고있는 형태 입니다.


O(2ⁿ)

O(2ⁿ)는 입력 데이터가 하나 증가할때마다 처리 시간이 2배씩 증가하는 경우를 표현합니다. 

(으... 예제로 무슨 코드를 짜야하는거지..)


예제 코드를 짜는데 실패했습니다... 알려주세요.. (._.


O(2ⁿ)은 수행시간이 아주 빠르게 증가하여 최악의 알고리즘이라고 합니다..



빅 오 표기들을 대소로 정리하면 다음과 같습니다.

O(1) < O(log N) < O(N) < O(N²) < O(2ⁿ)




소문자 o 표기법 ( 리틀 오 표기법 )


리틀 오 표기법은 빅 오 표기법 보다 좀 더 여유있는 상한을 표현할때 사용됩니다.

알고리즘의 복잡도 또는 성능을 표기할때는 자주 쓰이지 않는 표기법입니다.


어떤 알고리즘의 분석시간이 o(log N) 이라 한다면, 이는 해당 알고리즘의 소요시간이 입력크기 N에 대해 log N보다 완만하게 증가한다는것을 뜻합니다.

이해 하기 어려웠는데, log N처럼 증가하지 않지만, log N과 비슷하게 증가한다는 뜻입니다. 즉 O(log N)과 o(log N)은 같은 증가 속도가 될 순 없고, log N보다 완만하게 증가하는것이 분명할때 사용된답니다.


대문자 오메가(Ω) 표기법 ( 빅 오메가 표기법 )


빅 오메가 표기법은 빅 오 표기법과 마찬가지로 알고리즘의 복잡도 또는 성능을 표기할때 사용됩니다.

하지만, 빅 오 표기법은 최악의 경우 를 표시 했다면, 빅 오메가 표기법은 최고의 경우를 표현합니다.


손쉽게 말하면 f(N) = Ω(2N) 이라 한다면 f(N)은 적어도 2N에 비례하는 시간이 소요됨을 뜻합니다.


해당되는 알고리즘을 실행하는데 걸리는 최소한의 시간을 나타 내는 것입니다.


이는 맨 밑에있는 세타 표기법과 동시에 설명하는게 편하니, 밑에서 예제를 들겠습니다.


소문자 오메가(ω) 표기법 ( 리틀 오메가 표기법 )


리틀 오메가 표기법은 빅 오메가 표기법 보다 좀 더 여유있는 상한을 표현 할때 사용됩니다.


제가 위에 적은 리틀 오 표기법과 비슷하죠. 맞습니다.


어떤 알고리즘의 분석시간이 ω(log N) 이라 한다면, 이는 해당 알고리즘의 소요시간이 입력크기 N에 대해 log N보다 완만하게 증가한다는것을 뜻합니다.

이해 하기 어려웠는데, log N처럼 증가하지 않지만, log N과 비슷하게 증가한다는 뜻입니다. 즉 Ω(log N)과 ω(log N)은 같은 증가 속도가 될 순 없고, log N보다 완만하게 증가하는것이 분명할때 사용된답니다.


세타(Θ) 표기법 (세타 표기법 )


세타 표기법은 빅 오 표기법은 최악의 경우, 빅 오메가 표기법은 최고의 경우를 나타낸다면, 세타 표기법은 평균의 경우를 나타내는 표기법입니다.


빅 오 와 빅 오메가의 중간이라고 생각 하시면 편합니다.


중간이라는 표현은 좀 애매한데, 빅 오 와 빅 오메가의 교집합이라 생각하십니다. 그러니까

이 알고리즘은 아무리 좋아지거나 아무리 나빠지더라도 세타 범위 안에있다. 라는 뜻입니다.


점근적 표기법에 대해 어느정도 적어봤습니다.


적다 보니 빅 오 를 제외하고 나머지 4개 표기법에 대해서는 완벽하겐 모르겠네요.


앞으로 이 5개를 각각 1개 씩 나눠서 포스팅을 해야겠습니다.


감사합니다.


'프로그래밍 > Computer Science' 카테고리의 다른 글

[TCP/IP] TCP 3-way Handshake  (0) 2016.12.14
Posted by 시리시안
2016. 12. 5. 13:15

안녕하세요. 게임개발자 놀이터 입니다.


이번 포스팅에선 C++의 예약어중 하나인 inline에 대해 포스팅 하려고합니다.


C++ 책을 보다보면 꼭 나오는 예약어중 하나인데요.


쉽게 말하자면, 인라인 함수는 컴파일된 함수의 코드가 프로그램의 코드안에 직접 삽입이 되는겁니다.


즉, 컴파일러가 함수를 호출하는 과정이 사라져 버리는것이죠.


코드를 볼까요?


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
 
using namespace std;
 
inline void Inlinefuction()
{
    cout<<"*** Inlinefuction() ***"<<endl;
}
 
 
int main()
{
 
    Inlinefuction();

    return 0;
}
cs


이렇게 선언한 코드를 컴파일 하게되면


1
2
3
4
5
6
7
8
9
10
11
#include<iostream>
 
using namespace std;
 
int main()
{
 
    cout<<"*** Inlinefuction() ***"<<endl;
 
    return 0;
}
cs


위와 같이 바뀌게 됩니다. 어떤가요?


좀더 확실한 결과를 보기 위해 어셈블리 코드로 확인 해보겠습니다.


Visual C++에서 어셈블리 파일을 생성할떄 /Ob1 이라는 추가 명렁 옵션을 주셔야 어셈블리 코드가 정상적으로 출력됩니다.

(진짜 몰라서 엄청 고생했네요...)


먼저 인라인이 적용 되지 않은 어셈블 코드중 메인 함수를 보면


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
; 12   : {
 
    push    ebp
    mov    ebp, esp
 
; 13   :     Inlinefuction();
 
    call    ?Inlinefuction@@YAXXZ            ; Inlinefuction
 
; 14   :     return 0;
 
    xor    eax, eax
 
; 15   : }
 
    cmp    ebp, esp
    call    __RTC_CheckEsp
    pop    ebp
    ret    0
cs


위와 같이 나오게 됩니다. 그후 Inline이 적용된 함수의 어셈블 코드를 보면


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
; 12   : {
 
    push    ebp
    mov    ebp, esp
    push    esi
 
; 13   :     Inlinefuction();
 
    mov    esi, esp
    mov    eax, DWORD PTR __imp_?endl@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@1@AAV21@@Z
    push    eax
    push    OFFSET ??_C@_0BI@FMEKNHNP@?$CK?$CK?$CK?5Inlinefuction?$CI?$CJ?5?$CK?$CK?$CK?$AA@
    mov    ecx, DWORD PTR __imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A
    push    ecx
    call    ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?$char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<<std::char_traits<char> >
    add    esp, 8
    mov    ecx, eax
    call    DWORD PTR __imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QAEAAV01@P6AAAV01@AAV01@@Z@Z
    cmp    esi, esp
    call    __RTC_CheckEsp
 
; 14   :     return 0;
 
    xor    eax, eax
 
; 15   : }
 
    pop    esi
    cmp    ebp, esp
    call    __RTC_CheckEsp
    pop    ebp
    ret    0
cs


이렇게 나오게 됩니다.


13번째 줄을 보면 확연히 차이가 나는걸 보실수 있습니다.


음.. 보기 어렵군요?


함수를 좀더 보기 쉽게 바꿔보겠습니다.


소스코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include<iostream>
 
using namespace std;
 
int Inlinefuction(int a, int b)
{
    return a+b;
}
 
 
int main()
{
    int n;
    n = Inlinefuction(1,2);
    return 0;
}
cs


Inline이 적용하지 않은 어셈블리 코드

1
2
3
4
5
6
7
8
9
10
11
12
; 13   :     int n;
; 14   :     n = Inlinefuction(1,2);
 
    push    2
    push    1
    call    ?Inlinefuction@@YAHHH@Z            ; Inlinefuction
    add    esp, 8
    mov    DWORD PTR _n$[ebp], eax
 
; 15   :     return 0;
 
    xor    eax, eax
cs


Inline이 적용된 어셈블리 코드

1
2
3
4
5
6
7
8
9
10
; 13   :     int n;
; 14   :     n = Inlinefuction(1,2);
 
    mov    eax, 1
    add    eax, 2
    mov    DWORD PTR _n$[ebp], eax
 
; 15   :     return 0;
 
    xor    eax, eax
cs



14 번의 코드를 보면 확 차이가 느껴지실껍니다.

가장 크게 어셈블리 코드상에서 call이 불리지 않게됩니다.

또 지금 코드상에선 push도 호출되고 있지 않네요.



일반 함수는 호출되는 과정을 보면, 우선 매개변수를 스택이 집어넣고, 함수를 호출합니다. 그후 함수내 리턴값을 위한 스택의 push, pop 도있고, 리턴후 스택 정리를 위한 pop이 있습니다.

이러한 함수 호출은 스택에 접급 하기 때문에 즉, 메모리에 접근 하기 때문에 속도가 저하 될 수 있습니다.

함수 호출에 대한 자세한 이야기는 다른 포스팅에서 다루겠습니다.



인라인을 쓰게되면 가장 먼저 속도와 메모리 면에서 효과가있습니다.

메모리는 위에 적은 것처럼 함수를 부르기 위해 스택에 넣었다 빼는 과정이 없어지기 때문이고, 그에따라 속도 면에서 더 빨라진답니다.


하지만 단점은 너무 반복해서 사용하게 될경우 어셈블리 코드가 길어진다는 것입니다. push와 call로 끝나는 함수가 여러번 호출되면서 여러번의 내용이 다 복사되서 어셈블리 코드로 적히게 되면 그 길이는 장난 아니겠죠?



결론


음.. 제 짧은 지식으로 내린 결론은 

함수 구문이 짧고, 호출이 적은 함수는 inline화 시키자는 겁니다. 크게 영향을 주지않는 함수 호출을 줄이자는거죠..!!


잘 모르는 분야라 댓글로 지적 해주시면 감사하겠습니다.






Posted by 시리시안
2016. 11. 30. 13:51

안녕하세요. 게임개발자 놀이터입니다.


이번 포스팅에선 C++로 Swap 함수를 짜보려고합니다.


흔히 Swap 함수를 짠다고 하면 자료형 int에 한애 대부분 이렇게 짤것입니다.


1
2
3
4
5
6
void Swap(int *a, int *b)
{
    int temp = *b;
    *= *a;
    *= temp;
}
cs


int형에 한에 정말 간단하고 편한 방법이지요.


main에서는 다음과 같이 사용할것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
 
    int a =10;
    int b =50;
 
    cout<<"*** Swap 함수 실행 전 2개의 값 : "<<a<<" , "<<b<<" ***"<<endl;
    Swap(&a,&b);
    cout<<"*** Swap 함수 실행 후 2개의 값 : "<<a<<" , "<<b<<" ***"<<endl;
 
    return 0;
}
cs


실행 결과는 다음과 같겠죠??


하지만 여기서 Swap을 숨겨저 포인터 인자인지를 모른다면..??


한번쯤은 이렇게 쓰고 싶어질것입니다.


1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
 
    int a =10;
    int b =50;
 
    cout<<"*** Swap 함수 실행 전 2개의 값 : "<<a<<" , "<<b<<" ***"<<endl;
    Swap(a,b);
    cout<<"*** Swap 함수 실행 후 2개의 값 : "<<a<<" , "<<b<<" ***"<<endl;
 
    return 0;
}
cs


그럼 Swap 함수는 이렇게 고쳐야 할꺼애요.


1
2
3
4
5
6
void Swap(int &a, int &b)
{
    int temp = b;
    b = a;
    a = temp;
}
cs


어느게 더 좋아 보이나요??


뭐.. 사용자에 다를테니.. 자세한 의견댓글로 남겨주시면 감사하겠습니다..


하지만 위의 방식은 int자료형에 한정되어있다는점이 아쉽죠.


char는? float는?


그럼 그때마다 함수를 계속 새로 만들어야 할까요?


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void Swap(int &a, int &b)
{
    int temp = b;
    b = a;
    a = temp;
}
 
void Swap(float &a, float &b)
{
    float temp = b;
    b = a;
    a = temp;
}
 
void Swap(char &a, char &b)
{
    char temp = b;
    b = a;
    a = temp;
}

cs


이렇게 말이죠..? 너무 비효율적이죠. Swap이란 이름을 가진 함수는 뭐든간에 2개의 자리 또는 서로 교체 한다는 뜻일텐데 말이죠.


그래서 Template를 이용합니다.


1
2
3
4
5
6
7
template<typename T>
void Swap(T &a, T &b)
{
    T temp = b;
    b = a;
    a = temp;
}

cs


이러면 어떤가요? 이렇게 하면 어떤 자료형이라도 Swap이 가능합니다.


밑은 풀소스입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include<iostream>
 
using namespace std;
 
template<typename T>
void Swap(T &a, T &b)
{
    T temp = b;
    b = a;
    a = temp;
}
 
 
int main()
{
 
    int a =10;
    int b =50;
 
    cout<<"*** Swap 함수 실행 전 2개의 값 : "<<a<<" , "<<b<<" ***"<<endl;
    Swap(a,b);
    cout<<"*** Swap 함수 실행 후 2개의 값 : "<<a<<" , "<<b<<" ***"<<endl;
 
    return 0;
}
cs


감사합니다.


Posted by 시리시안
2016. 11. 30. 11:57

안녕하세요. 게임개발자 놀이터 입니다.

오늘은 C++에서 자주 쓰이지 않는! (개인적인 기준입니다..! 전 지금까지 써본적이 없었어요) friend에 대해서 정리 해볼까 합니다.


먼저 friend의 뜻을 생각해볼까요? 제목에도 적어놨지만 [친구]라는 뜻을 가지고 있습니다. C++에서 friend를 선언하면 친구 관계가 되는 거랍니다.  클래스끼리 말이죠. 그것도 굉장히 [친한 친구]! 베스트프렌드! 라고 생각하시면 이해가 좀 쉬울꺼애요.


모두가 그런건 아니겠지만, 여기선 이렇게 생각해봅시다. 

'나는 내 친한친구에게 나의 모든걸 보여줘도 괜찮아.'

'내 친구에게는 숨기는게 없어'

라는 뜻으로 friend를 생각하면 됩니다.


즉, A클래스가 B클래스를 friend로 지정한다면 B클래스는 A클래스의 private 멤버나 함수까지 접근이 가능합니다.

여기서 B클래스도 A클래스를 friend로 지정한다면 서로간의 private멤버나 함수까지 접근이 가능합니다.

(물론 B클래스는 A클래스를 friend로 지정 안할수도있습니다. (일방적인 사랑) )


선언 방법은 클래스 내부에 friend 로 선언해주면 되는데 이때 위치는 private, protected, public 어디든 상관 없습니다.


1
2
3
4
5
6
7
8
9
10
11
12
class A
{
    // 어디든 상관 없다! 
private:
    friend B;
 
protected:
    friend B;
 
public:
    friend B;
}
cs


위 코드처럼 firend는 어디에 선언되든 상관없습니다. ( 위 코드에선 A클래스가 선언되기전 B클래스를 알고있다고 생각합시다.)


그럼 직접적인 사용 예를 한번 보겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include<iostream>
 
using namespace std;
 
class B;
 
class A
{
 
private:
    int value;
public:
    A(int data)
    {
        value = data;
    }
    friend B;
};
 
class B
{
public:
    void fb(A a){
        cout<<"*** B클래스 public 함수 ***"<<endl;
        cout<<"*** A클래스 priavte int value에 접근 : "<<a.value<<" ***"<<endl;
    }
};
 
int main()
{
    A a(10);
 
    B b;
 
    b.fb(a);
 
    return 0;
}
cs


위 코드에서 25번째줄을 봅시다.

인자로 받아온 클래스 A의 value값에 접근하고있습니다.

value값은 private라 일반적으로는 절대 접근 할 수 없습니다.


하지만 friend가 이를 가능하게 만들어 줍니다.


실행 결과는 다음과 같습니다.


즉 friend로 선언 받은 이상 A클래스의 모든걸 접근할 수 있다는 것입니다.


이처럼 friend는 언뜻보면 아무런 이상 없고 정말 편해 보이지만, 이 friend는 객체지향의 핵심중 하나인 '정보 은닉'을 깨부수는 행위를 일으키게 됩니다.


어떤 클래스라도 private에 넣어둔 이유가 분명이 있을텐데, 이를 무시하고 접근해서 원한다면, 수정을 해버릴수도 있죠.


그래서 전 friend를 써본적이 없습니다..


이상으로 friend관련 글을 이만 마칩니다.


감사합니다.




Posted by 시리시안
2016. 11. 28. 17:45

안녕하세요. 게임 개발자 놀이터입니다. 정말 오랜만에 포스팅하는데요..

갑자기 제가 float를 C++로 소수점을 출력할일이 생겼는데.. 순간.. 기억이..! 나질않아서..!!! 이렇게 정리하려고합니다.


앞으로도 이런 이슈가 생기면 대부분 정리해두려고합니다.


먼저 cout이 가지고있는 presision을 사용하면 됩니다.


먼저 일반적인 코드와 실행결과입니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
 
using namespace std;
 
int main()
{
    float f = 3.141592f;
 
    cout<<f;
 
    return 0;
}


위의 코드를 실행하게 되면 출력결과는 다음과 같습니다.

꼭 직접 해보시기 바랍니다.



그럼 cout이 가지고있는 presision을 사용해보도록 하겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
 
using namespace std;
 
int main()
{
    cout.precision(7);
 
    float f = 3.141592f;
 
    cout<<f;
 
    return 0;
}
cs

위처럼 입력 하고 출력하면.


정상적으로 나오는걸 확인 하실수있습니다!



+ 숫자를 크게하면 소숫점은 다르수가 나오게 됩니다.


왜 그럴까요..?




Posted by 시리시안
2016. 7. 28. 10:11

const ( 상수 )


많이 들어봤으나, 사용하는 사람은 자주 사용하고, 사용하지 않는 사람들은 자주 사용하지 않을것입니다.


상수 (constant)는 값을 '절대로' 바꿀 수 없다. 라는 특징을 가지고 있습니다.

또한 정의시 무조껀 값을 지정해주어야 합니다.


const ( 자료 타입 ) ( 상수 명 ) = ( 상수 값 );


1
2
3
4
5
6
#include <iostream>
 
int main()
{
    const int c = 10
}
cs


이런식으로 말이죠.


하지만 const의 위치는 여러군데에 붙을 수 있답니다.


1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
 
int main()
{
    int a = 10;
    
    const int * c1 = &a;     
    int * const c2 = &a; 
 
 
}
cs


위와 같은 코드를 보시면, c1과 c2의 차이점을 아시겠나요?


간단하게 생각하면 어떤게 상수화가 되는지를 생각하면 됩니다.


c1의 경우 정수형 포인터가 됩니다. 즉 c1의 메모리 주소를 변경이 가능하지만, 메모리에 있는 값을 변경하지 못하게 합니다.


하지만 c2는 상수형 포인터가 됩니다. c2의 메모리주소는 상수화 되어서 변경이 불가능하지만, 메모리에 있는 값은 변경이 가능하게 됩니다.


즉 이런 대입이 가능합니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
 
int main()
{
    int a = 10;
    
    const int * c1 = &a;     
    int * const c2 = &a; 
    
    int b = 20;
    
    c1 = &b;
    c2 = 100;
 
}
cs


이 의외에 대입을 하면 에러가 나니, 직접 해보시기 바랍니다.



Complite const 와 Runtime const


C++에서 const에는 2가지의 종류가 있습니다.


바로 컴파일 상수와 런타임 상수 입니다.


어떤 차이 냐면 아래 소스를 보시면 됩니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
 
int main()
{
    int m_1 = 10;
    int array01[m_1];  // error.
 
    const int m_2 = 10;
    int array02[m_2];  // ok..
 
    const int m_3 = s1;
    int array03[m_3];   // error.
}
cs


위 소스 에서 가운데 있는 array02를 제외하곤 전부 에러가 발생합니다.


이유가 뭘까요?


먼저 array01은 배열의 크기가 변수이기때문에 에러가납니다.


반대로 array02는 상수이기때문에 가능하지요. 하지만,


array03은 상수를 넣었는데 왜 에러가 날까요?


이는 array02에 쓰이는 m_2는 컴파일 상수이며, array03에 쓰이는 m_3는 런타임 상수이기 때문입니다.


실제로 코드를 보시면 m_3는 프로그램이 실행되어 배열 생성까지 도달할때 m_3에 대입되는 s1의 값을 모릅니다.


위 코드는 간단해서 생각만으로도 값을 알아낼 수 있지만, 좀더 복잡한 코드가 된다면 눈으로도 알아낼 수 없을것입니다.


이런 컴파일 상수와 런타임 상수과 확실하게 나눠진다면 정말 편할텐데 말이죠.


(PS. C++11에서는 이를 해결 할 수 있는 constexpr 이 있습니다. 참고하세요.)



const 위치에 따른 쓰임 변화


const는 정말 여러 위치에 놓일 수 있습니다.


변수 선언 시 : [const] 자료형타입 [ const ] 포인터 [const] 변수명

함수 선언 시 : [const] 자료형타입 [ const ] 포인터 [const] 함수명(매개변수...) [const { }


함수 변수를 생각하면 총 7군데나 되네요.. 많아라..


겹치는곳을 생각하면 총 4가지입니다. 걱정마시죠!


그럼 먼저 다시 짚고 가야할 부분이 있습니다.


const(상수화)가 되면 값 변경이 불가능하다.


그럼 이제 찾아야 할 점을 '어디서' 또는 '어떤게' 값 변경이 안되는가 입니다.



그럼 몸풀기로 생각을 해봅시다.


const int * * a;

int *const * a;

int * * const a;


이렇게 3개의 변수가 각각 다른 위치에 const를 달고 있습니다. 이 3개를 구분지어 볼까요?


먼저 '어디서', '어떤게' 영향을 받았나가 중요합니다.


그럼 제일 크게 const를 기반으로 자료형과 변수명을 나눌수있습니다.


(const int) * * a;

(int *const) * a;

(int **) const a;


위 처럼 나눠서 보면 더 쉬울까요?


첫번째 (const int) * * a; 은 상수화된 int의 이중포인터 라는 것입니다.

즉 해석하면 가능과같습니다.

const int x =10;

const int * y = &x;

이때 &x 가 const int ** 라고 형입니다.

즉, (const int) * * a; 에선 알맹이 값인 a가 상수처리 되는것이죠.


다시 말하면

a (값변경가능 = 주솟값)

*a (값 변경가능 = 주솟값)

**a (값 병경 불가능 , 상수)

입니다.


2번쨰 (int *const) * a; 은 int*이 가지고 있는 주솟값 즉 *를 상수화 시킨겁니다.


가리키는 방향을 상수화 한거죠.


간단하죠?


3번쨰는 (int **) const a;

int **형 자체가 상수화가 된겁니다.


즉 처음으로 가리키는 주솟값이 상수화가 된것이지요.


함수도 비슷합니다. 하지만 마지막으로 함수 맨뒤에 붙는 const는 함수 내부 내용을 상수화 하겠다는 뜻입니다. 즉, 함수 내부에서는 값을 변화시키는 행위를 일절 하지 못하게 막는다는 소리입니다.


감사합니다



Posted by 시리시안