7장 포인터

감사의 글

유튜브 동영상 모음집 (나도코딩 C)에서 설명하는 코드를 활용합니다. 강의동영상을 공개한 나도코딩님께 감사드립니다.

핵심

  • 포인터 이해

(C 언어) 물고기 키우기

게임 설명

  • 어항을 클릭해서 물 채우기

(C 언어) 친구들의 주소

  • 예제: 실제 저장된 메모리상의 주소 확인하는 방법
  • 앰퍼샌드(ampersand) 기호(&): (일반) 변수에 할당된 값이 저장된 메모리 주소를 가리킴
  • 주의: 포인터 값을 받는 출력 포맷: %p (동영상에서 사용하는 %d는 경고 발생시킴)
    • 16진법으로 표현된 메모리 주소 출력

#include <stdio.h>

int main()
{
    // 포인터

    // 한 아파트에 철수, 영희, 민수 거주
    // 각각의 문 앞에 '암호'가 걸려 있음.

    // [철수]: 101호 -> 실제 메모리 공간의 주소는 다름
    // [영희]: 201호 -> 실제 메모리 공간의 주소는 다름
    // [민수]: 301호 -> 실제 메모리 공간의 주소는 다름

    int 철수 = 1;  
    int 영희 = 2;
    int 민수 = 3;

    printf("철수네 주소: %p, 암호: %d\n", &철수, 철수);
    printf("영희네 주소: %p, 암호: %d\n", &영희, 영희);
    printf("민수네 주소: %p, 암호: %d\n", &민수, 민수);
}

Python 구현

  • 파이썬에서는 포인터 변수를 굳이 사용할 필요 없음.

  • 또한 변수의 주소 등을 확인할 일도 일밙거으로 없음.


(C 언어) 미션맨(포인터)의 등장

  • 두 종류의 변수 활용
    • (일반) 변수: 정수, 부동소수점, 문자, 문자열 등 값을 가리키는 변수
    • 포인터 변수: 메모리 주소를 가리키는 변수
      • (일반) 변수가 가리키는 값이 저장된 메모리 주소
      • 배열(어레이), 문자열 등이 저장된 메모리 주소
  • 애스터리스크(asterisk) 기호(*)의 역할
    • 포인터 변수가 가리키는 주소에 저장된 값을 가리킴

// 미션맨 등장!

    // 첫 번째 미션: 아파트의 각 집에 방문하여 문에 적힘 암호 확인

    int * 미션맨;   // 포인터 변수

    // 포인터 변수와, 포인터 변수가 가리키는 주소에 저장된 값 확인
    미션맨 = &철수;  // 철수의 주소
    printf("미션맨이 방문하는 곳의 주소: %p, 암호: %d\n", 미션맨, *미션맨);

    미션맨 = &영희;  // 영희의 주소
    printf("미션맨이 방문하는 곳의 주소: %p, 암호: %d\n", 미션맨, *미션맨);

    미션맨 = &민수;  // 민수의 주소
    printf("미션맨이 방문하는 곳의 주소: %p, 암호: %d\n", 미션맨, *미션맨);

    // 두 번째 미션: 각 집의 암호에 3 곱하기

    미션맨 = &철수;
    *미션맨 = *미션맨 * 3;
    printf("미션맨이 암호를 바꾼 곳의 주소: %p, 암호: %d\n", 미션맨, *미션맨);

    미션맨 = &영희;
    *미션맨 = *미션맨 * 3;
    printf("미션맨이 암호를 바꾼 곳의 주소: %p, 암호: %d\n", 미션맨, *미션맨);

    미션맨 = &민수;
    *미션맨 = *미션맨 * 3;
    printf("미션맨이 암호를 바꾼 곳의 주소: %p, 암호: %d\n", 미션맨, *미션맨);

(C 언어) 스파이(또다른 포인터)의 등장

  • 두 개의 포인터가 하나의 메모리 주소 공유
    • 포인터의 주요 역할임. 하지만 매우 어려운 문제 야기 가능
    • 여러 포인터 변수가 하나의 주소를 대상으로 많은 변화를 줄 수 있기 때문.
  • 포인터 변수가 가리키는 주소를 저장하는 메모리 공간도 물론 따로 존재
    • 역시 앰퍼샌트 기호(&)를 활용하여 확인 가능

// 스파이
    // - 두 개의 포인터가 동일한 메모리 주소를 가리킬 수 있음.
    // 미션맨이 바꾼 암호에서 2를 빼라!
    int * 스파이 = 미션맨;

    printf("\n ... 스파이가 미션 수행하는 중 ... \n\n");

    스파이 = &철수;
    *스파이 = *스파이 - 2;  // 철수 = 철수 - 2
    printf("스파이가 방문하는 곳 주소: %p, 암호: %d\n", 스파이, *스파이);

    스파이 = &영희;
    *스파이 = *스파이 - 2;  // 영희 = 영희 - 2
    printf("스파이가 방문하는 곳 주소: %p, 암호: %d\n", 스파이, *스파이);

    스파이 = &민수;
    *스파이 = *스파이 - 2;  // 민수 = 민수 - 2
    printf("스파이가 방문하는 곳 주소: %p, 암호: %d\n", 스파이, *스파이);

    // 철수, 영희, 민수는 집에 돌아와서 바뀐 암호를 보고 깜짝 놀람.
    // - 철수 제외!
    printf("철수네 주소: %p, 암호: %d\n", &철수, 철수);
    printf("영희네 주소: %p, 암호: %d\n", &영희, 영희);
    printf("민수네 주소: %p, 암호: %d\n", &민수, 민수);

    // 참고: 미션맨/포인터가 사는 곳의 주소 또한 &미션맨/&스파이 등으로 확인
    printf("미션맨의 주소: %p\n", &미션맨);
    printf("스파이의 주소: %p\n", &스파이);

(C 언어) 배열과 포인터의 관계

  • 배열을 가리키는 변수는 포인터 변수로 인식됨
  • 아래 예제 코드에서 arrptr은 동일한 배열을 가리킴.

#include <stdio.h>

int main()
{
    // 배열

    int arr[3] = {5, 10, 15};
    int * ptr = arr;

    // 배열의 항목 확인
    for (int i = 0; i < 3; i++)
    {
        printf("배열 arr[%d]의 값: %d\n", i, arr[i]);
    }

    // 포인터로도 배열 항목 확인 가능
    for (int i = 0; i < 3; i++)
    {
        printf("포인터 ptr[%d]의 값: %d\n", i, ptr[i]);
    }

    // 포인터를 활용하여 배열 항목 수정
    ptr[0] = 100;
    ptr[1] = 200;
    ptr[2] = 300;

    // 배열 항목 다시 확인
    for (int i = 0; i < 3; i++)
    {
        printf("배열 arr[%d]의 값: %d\n", i, arr[i]);
    }

    for (int i = 0; i < 3; i++)
    {
        printf("포인터 ptr[%d]의 값: %d\n", i, ptr[i]);
    }
}
  • 배열이 저장된 주소를 가리키는 포인터 변수는 실제로는 배열의 첫째 항목이 가리키는 메모리 주소를 가리킴.

// 포인터 값을 이용하여 아래와 같이 수정해도 동일하게 작동함
    // 포인터 변수는 어레이의 첫째 항목이 저장된 주소를 가리킴
    for (int i = 0; i < 3; i++)
    {
        printf("배열 arr[%d]의 값: %d\n", i, *(arr + i));
    }

    for (int i = 0; i < 3; i++)
    {
        printf("포인터 ptr[%d]의 값: %d\n", i, *(ptr + i));
    }

    // *(arr + i) == arr[i] 
    // arr == arr 배열의 첫번째 값의 주소와 동일, 즉, &arr[0]
    printf("arr 자체의 값: %p\n", arr);
    printf("arr[0]의 주소: %p\n", &arr[0]);

    // 배열 포인터 주소에 저장된 값 = 배열의 첫째 항목
    printf("arr 자체의 값이 가지는 주소의 실제 값: %d\n", *arr); // *(arr + 0)
    printf("arr[0]의 값: %d\n", *&arr[0]);

    // *& 는 아무 것도 없는 것과 같다. 
    // & 는 주소, * 는 그 주소에 저장된 값
    printf("arr[0]의 값: %d\n", *&*&*&arr[0]);
    printf("arr[0]의 값: %d\n", arr[0]);

Python 구현

  • 배열에 해당하는 파이썬 자료형은 리스트임.
  • 하지만 또한 일반 변수로 선언해서 사용함.
  • 리스트의 항목은 인덱스 활용하며, 주소값을 사용할 일이 일반적으로 없음.
In [1]:
arr = [5, 10, 15]

for i in range(3):
    print("배열 arr[%d]의 값: %d" % (i, arr[i]));
배열 arr[0]의 값: 5
배열 arr[1]의 값: 10
배열 arr[2]의 값: 15
  • 인덱스 대신에 항목을 직접 이용하여 for 반복문 실행 가능
In [2]:
arr = [5, 10, 15]

for item in arr:
    print(item)
5
10
15
  • 항목과 인덱스를 함께 사용하려면 enumerate() 함수 활용
In [3]:
arr = [5, 10, 15]

for i, item in enumerate(arr):
    print(f"배열 arr[{i}]의 값: {item}")
배열 arr[0]의 값: 5
배열 arr[1]의 값: 10
배열 arr[2]의 값: 15

(C 언어) Swap

두 개의 변수에 할당된 값 바꾸기 잘못된 시도

  • 아래와 같이 시도하면 변수 a와 b에 저장된 값을 바꿀 수 없음.
  • 이유는 swap 함수에 전달되는 것은 a와 b의 주소가 아니라 a와 b가 가리키는 값만 전달되기 때문임.

#include <stdio.h>

// 스왑 함수
void swap(int a, int b);

int main()
{
    // Swap

    int a = 10;
    int b = 20;

    // a와 b의 값 바꾸기
    printf("Swap 함수 적용 전 => a: %d, b:%d\n", a, b);
    swap(a, b);
    printf("Swap 함수 적용 후 => a: %d, b:%d\n", a, b);
}

void swap(int a, int b)
{
    int temp = a;
    a = b;
    b = temp;
    printf("Swap 함수 내 => a: %d, b:%d\n", a, b);
}
  • 실제로 swap 함수 본체 내에서 사용되는 변수들이 가리키는 주소가 다름.

#include <stdio.h>

// 스왑 함수
void swap(int a, int b);

int main()
{
    // Swap

    int a = 10;
    int b = 20;
    printf("a의 주소: %p\n", &a);
    printf("b의 주소: %p\n", &b);

    // a와 b의 값 바꾸기
    printf("Swap 함수 적용 전 => a: %d, b:%d\n", a, b);
    // 값에 의한 복사(Call by Value): 값만 복사한다는 의미
    swap(a, b);
    printf("Swap 함수 적용 후 => a: %d, b:%d\n", a, b);
}

void swap(int a, int b)
{
    printf("(swap 함수 본체) a의 주소: %p\n", &a);
    printf("(swap 함수 본체) b의 주소: %p\n", &b);

    int temp = a;
    a = b;
    b = temp;
    printf("Swap 함수 내 => a: %d, b:%d\n", a, b);
}
  • 위 코드 실행결과
a의 주소: 0x7ffc1b55074c
b의 주소: 0x7ffc1b550748
Swap 함수 적용  => a: 10, b:20
(swap 함수 본체) a의 주소: 0x7ffc1b55071c
(swap 함수 본체) b의 주소: 0x7ffc1b550718
Swap 함수  => a: 20, b:10
Swap 함수 적용  => a: 10, b:20

두 개의 변수에 할당된 값 바꾸기: 주소값(포인터) 활용

  • 스왑 함수에 변수들의 주소를 전달하면 됨.
  • 이를 위해 포인터 주소를 인자로 받는 스왑함수 활용

#include <stdio.h>

// 스왑 함수
void swap_addr(int * a, int * b);

int main()
{
    // Swap

    int a = 10;
    int b = 20;

    // a와 b의 값 바꾸기
    printf("Swap 함수 적용 전 => a: %d, b:%d\n", a, b);
    // 주소값 전달하는 방식으로
    swap_addr(&a, &b);
    printf("Swap 함수 적용 후 => a: %d, b:%d\n", a, b);
}

void swap_addr(int * a, int * b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

Python 구현

  • 파이썬에서 두 변수에 할당된 값의 교환은 매우 간단함.
In [4]:
a = 10
b = 20

a, b = b, a

print(f"a: {a}", f"b: {b}", sep='\n')
a: 20
b: 10
  • 주의: 아래와 같이 하면 C 언어에서와 동일한 결과를 얻음.
    • 각각의 변수들이 사는 영역이 다르기 때문임.
In [5]:
a = 10
b = 20

def swap(a, b):
    temp = a
    b = a
    a = b

swap(a, b)

print(f"a: {a}", f"b: {b}", sep='\n')
a: 10
b: 20

PythonTutor 활용하기


(C 언어) 포인터로 배열 값 변경하기

  • 배열의 항목을 포인터를 이용하여 수정 가능

#include <stdio.h>

// 스왑 함수
void changeArray(int * ptr);

int main()
{
    // 배열의 2번 인덱스 값 변경하기

    int arr2[3] = {10, 20, 30};
    changeArray(arr2);

    for (int i = 0; i < 3; i++)
    {
        printf("%d\n", arr2[i]);
    }
}

void changeArray(int * ptr)
{
    ptr[2] = 50;
}
  • changeArray(arr2);changeArray(&arr2[0]);로 변경 가능
  • 이제 scanf 함수에서 정수를 입력받을 때 &num 과 같이 앰퍼샌드 기호(&) 사용하는 이유를 이해할 수 있음.

Python 구현

  • 리스트의 항목 수정은 인덱스를 활용하면 됨.
In [6]:
arr2 = [10, 20, 30]

def changeArray(list):
    list[2] = 50
    
changeArray(arr2)

print(arr2)
[10, 20, 50]

(C 언어) 프로젝트

  • 물고기 키우기 게임
    • 물고기 6마리가 각각 다른 어항에 살고 있는데 사막이다보니 너무 건조해서 물이 아주 빠르게 증발함.
    • 물이 다 증발하기 전에 어항에 물을 채워주어야 함.
    • 시간이 지날 수록 물고기 커지며, 나주에는 .... 냠냠...

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

// 물고기 6마리가 각각 다른 어항에 살고 있는데
// 사막이다보니 너무 건조해서 물이 아주 빠르게 증발함.
// 물이 다 증발하기 전에 어항에 물을 채워주어야 함.
// 시간이 지날 수록 물고기 커지며, 나주에는 .... 냠냠...

int level;
int arrayFish[6];
int * cursor;                  // 각 어항을 가리키는 역할

void initData();
void printFishes();
void decreaseWater(long elapsedTime);
int checkFishAlive();

int main(void)
{
    long startTime = 0;        // 게임 시작시간
    long totalElapsedTime = 0; // 총 경과시간
    long prevElapsedTime = 0;  // 직전 경과시간 (최근에 물을 준 시간 간격)

    int num;                   // 물을 채울 어항 번호 (사용자 입력)

    initData();

    cursor = arrayFish;        // cursor[0], cursor[1], ...

    startTime = clock();       // 현재시각을 millisecond(ms, 1000분의 1초) 단위로 변환

    while (1)                  // 문한반복
    {
        printFishes();
        printf("몇 번 어항에 물을 주시겠어요? ");
        scanf("%d", &num);

        // 입력값 체크
        if (num < 1 || num > 6)
        {
            printf("\n입력값이 잘못되었어요.\n");
            continue;
        }

        // 게임 총 경과시간

        totalElapsedTime = (clock() - startTime) / CLOCKS_PER_SEC;
        // repl.it 사이트에서는 시간이 연속적이지 않음. 100으로 나누어 주는 것으로 모의실험 가능.
        // totalElapsedTime = (clock() - startTime) / 100;

        printf("게임 총 경과시간: %ld 초\n", totalElapsedTime);

        // 마지막으로 물을 준 이후로 흐른 시간
        // 흐른 시간동안 증발된 물의 양을 측정하기 위함.
        prevElapsedTime = totalElapsedTime - prevElapsedTime;
        printf("마지막으로 물을 준 이후로 흐른 시간: %ld 초\n", prevElapsedTime);

        // 증발된 만큼 물을 감소시키기
        decreaseWater(prevElapsedTime);

        // 사용자가 지정한 어항에 물주기
        // 1. 어항의 물이 0이면? 물고기 이미 사망. 따라서 물 주지 않음.
        if (cursor[num-1] <= 0)
        {
            printf("%d 번 물고기는 이미 사망. 물 주지 않음.\n", num);
        }
        // 2. 어항의 물의 양이 0이 아니면 물을 줌. 단, 어항 물의 양이 100 넘지 않게 해야함.
        else if (cursor[num-1] + 1 <= 100)
        {
            // 물 주기
            printf("%d 번 어항에 물주기\n\n", num);
            cursor[num-1] += 1;
        }

        // 레벨업 시행여부 확인 (20초마다 레벨업 수행)
        if (totalElapsedTime / 20 > level - 1)
        {
            level += 1;
            printf(" *** 축 레벨업! 기존 %d 레벨에서 %d 레벨로 업그레이드 ***\n\n", level-1, level);

            // 최종레벨: 5
            if (level == 5)
            {
                printf("\n\n축하합니다. 최고레벨을 당성하였음! 게임 종료!\n\n");
                exit(0);
            }
        }

        // 물고기가 죽었는지 확인하기
        if (checkFishAlive() == 0)
        {
            // 모든 물고기 모두 사망
            printf("모든 물고기 사망 ㅠㅠ\n");
            exit(0);
        }
        else
        {
            printf("물고기 아직 살아 있음!\n");
        }

        // while 문이 반복될 때마다 마지막에 물준 시간 기억하기
        // prevElapsedTime 이미 그 역할 다했기 때문에 여기서 업데이트 해도 문제 없음.
        prevElapsedTime = totalElapsedTime;
    }

    return 0;
}

void initData()
{
    level = 1;                 // 게임 시작 레벨 (1-5)

    for (int i = 0; i < 6; i++)
    {
        arrayFish[i] = 100;     // 최초 어항 물높이 
    }
}

void printFishes()
{
    printf("\n");
    printf("%3d번 %3d번 %3d번 %3d번 %3d번 %3d번\n", 1, 2, 3, 4, 5, 6);
    for (int i = 0; i < 6; i++)
    {
        printf(" %4d ", arrayFish[i]);
    }
    printf("\n\n");
}

void decreaseWater(long elapsedTime)
{
    for (int i = 0; i < 6; i++)
    {
        arrayFish[i] -= (level * 3 * (int)elapsedTime); // 3은 난이도 조절용

        // 물의 최소량은 0임.
        if (arrayFish[i] < 0)
        {
            arrayFish[i] = 0;
        }
    }
}

int checkFishAlive()
{
    for (int i = 0; i < 6; i++)
    {
        if (arrayFish[i] > 0)
            return 1; // 참 True
    }

    return 0;
}

특징

  • 포인터 변수 cursor를 굳이 사용해야 하는 이유가 확실하지 않음.
  • cursor 대신에 arrayFish 변수를 그대로 사용해도 되기 때문임.

Python 구현

  • 위 C 코드는 main() 함수의 본체가 좀 길게 작성됨.
  • 여기서는 while 반복문에 사용된 일부 코드를 함수화시켜서 코드를 보다 단순하게 작성하였음.
  • 또한 레벨 5 목표를 달성하거나 물고기가 모두 사망하면 게임을 억지로 종료시키기 위해 exit() 함수를 사용하는 것 보다 실행을 자연스럽게 멈추게 하도록 할 수 있음.
    • checkFishAlive() 함수와 levelUp() 함수 참조
  • levelUp() 함수에서 level 변수가 global 지정자를 이용하여 전역변수로 선언됨.
    • 함수 내에서 할당된 값이 수정되는 변수는 global 키워드로 지정되어야 함.
    • 단, arrayFish 처럼 리스트를 가리키는 경우는 그럴 필요 없음.
    • 주의: C 언어의 경우 함수 밖에서 선언되면 자동으로 전역변수로 취급됨.
In [17]:
import time

# 게임초기화
def initData():
    for i in range(0, 6):
        arrayFish[i] = 100

# 현재 어항 상태 보여주기
def printFishes():
    print("\n===\n")
    print("%3d  %3d  %3d  %3d  %3d  %3d" % (1,2,3,4,5,6))
    for i in range(0,6):
        print("%3d  " % (arrayFish[i]),end ='')
    print("\n")

# 매 초당 (level * 3)씩 물의 양 줄이기
def decreaseWater(elapsedTime):
    for i in range(0, 6):
        arrayFish[i] -= (level * 3 * elapsedTime)
        if (arrayFish[i] < 0):
            arrayFish[i] = 0

# 물고기가 한 마리라도 살아있는지 확인
def checkFishAlive():
    for i in range(0, 6):
        if (arrayFish[i] > 0):
            print("물고기 아직 살아 있음!")
            return True
    print("물고기 모두 사망! ㅠㅠ\n")
    return False                                           # 모든 물고기 죽었음

# 물 추가 어항 선택
def selectNum():
    printFishes()
    num = int(input("몇 번 어항에 물을 주시겠어요? "))
    if (num < 1 or num > 6):
        print("입력값이 잘못되었어요.")
        selectNum()                                        # 재귀호출
    return num

# 지정된 어항에 물 추가
def watering(num):
    if(arrayFish[num-1] <= 0):
        print(f"{num}번 어항 물고기 사망. 물 주지 않음!")
    elif (arrayFish[num-1] + 1 <= 100):
        print(f"{num}번 어항에 물주기")
        arrayFish[num-1] += 1

# 5초마다 레벌 업 시켜주기
def levelUp(totalElapsedTime):
    global level                                           # 전역변수 선언 필요!
    if totalElapsedTime/5 > level:
        level += 1
        print(f"\n축! {level} 레벨로 업그레이드!!!\n")
    if (level == 5):
        print("\n최고레벨 당성! 게임 종료!\n")
        return False                                       # 레벨 = 5 달성
    return True


##############
### 게임실행 ###
##############

if __name__ == "__main__":
    
    # 게임세팅
    arrayFish = [0, 0, 0, 0, 0, 0]
    initData()

    # 게임시작
    startTime = time.time()                                # 시작 시간 기억
    lastPouringTime = 0                                    # 마지막으로 물 추가한 시간 기억
    level = 1                                              # 레벨 1로 시작

    # 어항선택 반복
    while True:
        num = selectNum()
        
        totalElapsedTime = int(time.time() - startTime)    # 게임 총 경과시간
        print(f"게임 총 경과시간: {totalElapsedTime} 초")
        elapsedTime = totalElapsedTime - lastPouringTime   # 마지막으로 물 추가한 후 흐른 시간
        decreaseWater(elapsedTime)

        # 게임종료 조건 확인
        if not (checkFishAlive() and levelUp(totalElapsedTime)):
            break

        watering(num)
        lastPouringTime = totalElapsedTime
===

  1    2    3    4    5    6
100  100  100  100  100  100  

몇 번 어항에 물을 주시겠어요? 1
게임 총 경과시간: 1 초
물고기 아직 살아 있음!
1번 어항에 물주기

===

  1    2    3    4    5    6
 98   97   97   97   97   97  

몇 번 어항에 물을 주시겠어요? 4
게임 총 경과시간: 5 초
물고기 아직 살아 있음!
4번 어항에 물주기

===

  1    2    3    4    5    6
 86   85   85   86   85   85  

몇 번 어항에 물을 주시겠어요? 2
게임 총 경과시간: 18 초
물고기 아직 살아 있음!

축! 2 레벨로 업그레이드!!!

2번 어항에 물주기

===

  1    2    3    4    5    6
 47   47   46   47   46   46  

몇 번 어항에 물을 주시겠어요? 2
게임 총 경과시간: 26 초
물고기 모두 사망! ㅠㅠ

연습문제

위 파이썬 코드와 동일한 구조를 갖는 C 언어 코드를 작성하라.

파이썬 클래스 사용

  • 위 파이썬 코드에서 levelUp() 함수에서 level 변수가 전역변수로 사용됨.
    • 그렇지 않으면 level 변수를 사용할 수 없음.
  • 이 점을 보완하기 위해 levelUp() 함수와 level 변수가 하나의 공간에서 활동하여 서로 알 수 있도록 해야 함.
  • 이것을 구현하기 위해 하나의 클래스에서 속성과 메서드로 선언하면 됨.
    • 이후에 인스턴스 객체를 선언하면 self에 의해 서로의 존재를 인식하게 됨(아래 코드 참조)
In [21]:
import time

class FishWatering():
    def __init__(self, arrayFish=[0]*6, level=1):
        self.arrayFish = arrayFish
        self.level = level        

    # 게임초기화
    def initData(self):
        for i in range(0, 6):
            self.arrayFish[i] = 100

    # 현재 어항 상태 보여주기
    def printFishes(self):
        print("\n===\n")
        print("%3d  %3d  %3d  %3d  %3d  %3d" % (1,2,3,4,5,6))
        for i in range(0,6):
            print("%3d  " % (self.arrayFish[i]),end ='')
        print("\n")

    # 매 초당 (level * 3)씩 물의 양 줄이기
    def decreaseWater(self, elapsedTime):
        for i in range(0, 6):
            self.arrayFish[i] -= (self.level * 3 * elapsedTime)
            if (self.arrayFish[i] < 0):
                self.arrayFish[i] = 0

    # 물고기가 한 마리라도 살아있는지 확인
    def checkFishAlive(self):
        for i in range(0, 6):
            if (self.arrayFish[i] > 0):
                print("물고기 아직 살아 있음!")
                return True
        print("물고기 모두 사망! ㅠㅠ\n")
        return False                                           # 모든 물고기 죽었음

    # 물 추가 어항 선택
    def selectNum(self):
        self.printFishes()
        num = int(input("몇 번 어항에 물을 주시겠어요? "))
        if (num < 1 or num > 6):
            print("입력값이 잘못되었어요.")
            self.selectNum()                                        # 재귀호출
        return num

    # 지정된 어항에 물 추가
    def watering(self, num):
        if(self.arrayFish[num-1] <= 0):
            print(f"{num}번 어항 물고기 사망. 물 주지 않음!")
        elif (self.arrayFish[num-1] + 1 <= 100):
            print(f"{num}번 어항에 물주기")
            self.arrayFish[num-1] += 1

    # 5초마다 레벌 업 시켜주기
    def levelUp(self, totalElapsedTime):
        if totalElapsedTime/5 > self.level:
            self.level += 1
            print(f"\n축! {self.level} 레벨로 업그레이드!!!\n")
        if (self.level == 5):
            print("\n최고레벨 당성! 게임 종료!\n")
            return False                                       # 레벨 = 5 달성
        return True


##############
### 게임실행 ###
##############

if __name__ == "__main__":
    
    # 게임세팅
    Fishes = FishWatering()
    Fishes.initData()

    # 게임시작
    startTime = time.time()                                # 시작 시간 기억
    lastPouringTime = 0                                    # 마지막으로 물 추가한 시간 기억

    # 어항선택 반복
    while True:
        num = Fishes.selectNum()
        
        totalElapsedTime = int(time.time() - startTime)    # 게임 총 경과시간
        print(f"게임 총 경과시간: {totalElapsedTime} 초")
        elapsedTime = totalElapsedTime - lastPouringTime   # 마지막으로 물 추가한 후 흐른 시간
        Fishes.decreaseWater(elapsedTime)

        # 게임종료 조건 확인
        if not (Fishes.checkFishAlive() and Fishes.levelUp(totalElapsedTime)):
            break

        Fishes.watering(num)
        lastPouringTime = totalElapsedTime
===

  1    2    3    4    5    6
100  100  100  100  100  100  

몇 번 어항에 물을 주시겠어요? 2
게임 총 경과시간: 1 초
물고기 아직 살아 있음!
2번 어항에 물주기

===

  1    2    3    4    5    6
 94   95   94   94   94   94  

몇 번 어항에 물을 주시겠어요? 4
게임 총 경과시간: 8 초
물고기 아직 살아 있음!

축! 2 레벨로 업그레이드!!!

4번 어항에 물주기

===

  1    2    3    4    5    6
 52   53   52   53   52   52  

몇 번 어항에 물을 주시겠어요? 2
게임 총 경과시간: 17 초
물고기 모두 사망! ㅠㅠ


부록: (C 언어) 이중 포인터

이중 포인터 변수

  • 이중 포인터 변수: 포인터 변수의 주소를 가리키는 포인터 변수
  • 포인터 변수에 저장되는 값 또한 하나의 정수값이기에 그 값이 저장되는 메모리의 주소를 저장하는 변수가 이중 포인터 변수임.
  • 이중 포인터 작동은 일반 포인터가 작동과 동일

예제: 이중 포인터 변수


#include <stdio.h>

int main(){
    int var = 10;
    int *ptr1;
    int **ptr2;

    ptr1 = &var;
    ptr2 = &ptr1;

    printf("var : %d *ptr1 : %d **ptr1 : %d\n", var, *ptr1, **ptr2);
    printf("var 주소 : %d *ptr1 값 : %d **ptr1 값 : %d\n", &var, ptr1, *ptr2);
    printf("ptr1 주소 : %d ptr2 값 : %d", &ptr1, ptr2);

    return 0;
}

PythonTutor 활용하기

포인터 배열

  • 포인터 배열: 포인터(메모리 주소)로 구성된 배열
  • 일반 배열과 동일. 다만, 항목에 사용되는 값들이 값들이 저장된 메모리의 주소에 불과함.

예제: 포인터 배열


#include <stdio.h>

int main(){
    int num1 = 10, num2 = 20, num3 = 30;
    int *parr[3];

    parr[0] = &num1;
    parr[1] = &num2;
    parr[2] = &num3;

    for(int i=0; i<3; i++){
        printf("parr[%d] : %d\n", i, *parr[i]);
    }

    return 0;
}

PythonTutor 활용하기


이중 배열

  • 포인터 배열을 이용하여 서로 다른 길이의 배열을 항목으로 갖는 배열, 즉, 이중 배열을 정의할 수 있음.

#include <stdio.h>

int main(){
    int arr1[2] = {10, 11};
    int arr2[3] = {20, 30, 40};

    int * parr[2];

    parr[0] = arr1;
    parr[1] = arr2;

    for (int i = 0; i < 2; i++)
    {
        printf("parr[%d]: %d\n", i, *parr[i]);
    }

    return 0;
}

PythonTutor 활용하기

주의사항

  • 서로 다른 자료형의 배열을 항목으로 갖는 포인터 배열은 구조체를 활용해야 함.

    int arr1[2] = {10, 11};
      float arr2[3] = {1.0, 1.1, 1.2};
    
  • 이후 구조체를 학습한 후에 예제 살펴볼 것임.


Python 구현

  • 이중배열을 파이썬으로 구현하는 일은 매우 간단.
  • 자료형, 포인터 생각할 필요 없이 이중 배열로 선언하면 됨.
  • 주의: parr[i]는 i 번째 리스트의 첫째 항목을 가리킴.
In [8]:
arr1 = [10, 11]
arr2 = [20, 30, 40]

parr = [arr1, arr2]

for i in range(2):
    print("parr[%d]: %d" % (i, parr[i][0]))
parr[0]: 10
parr[1]: 20

부록: call-by-value vs. call-by-reference

  • 함수에 대한 인자로 일반적인 값을 받는 경우와 포인터 값을 받는 경우로 구분할 수 있음.
  • call-by-value(콜 바이 밸류) 함수: 일반적인 값을 인자로 받는 함수
  • call-by-reference(콜 바이 레퍼런스) 함수: 포인터 값을 인자로 받는 함수
  • 함수에 인자를 전달할 때 값을 전달하는 방식과 포인터를 전달하는 방식에 차이가 있음.
    • 예제: swap()swap_addr()

예제


#include <stdio.h>

void cbv(int val)
{
    val = 30;
}

void cbr(int *ref)
{
    *ref = 30;
}

int main()
{
    int val1 = 10, val2 = 10;

    printf("이전 : val1=%d, val2=%d\n", val1, val2);
    cbv(val1);
    cbr(&val2);
    printf("이후  : val1=%d, val2=%d\n", val1, val2);
    return 0;
}


연습문제

  1. 물고기 키우기 프로젝트의 C 언어 소스코드에 사용된 main() 함수의 본문 내용을 보다 간결하게 작성하라. 즉, 기능별로 보다 많은 함수를 작성하라.

    힌트: 파이썬 소스코드 참조

  • 모범답안: 아래 코드는 과제로 제출한 모범답안임. 코드에 약간의 중복이 있지만 매우 모범적인 코드임.
    • 예를 들어, checkFishAlive() 함수와 isItAlive() 함수를 병합할 수 있음.

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

int level;
int arrayFish[6];
int* cursor;

void initData();
void printFishes();
void decreaseWater(long elapsedTime);
int checkFishAlive();
int initNum();
long initTotalElapsedTime(long startTime);
long initPrevElapsedTime(long totalElapsedTime, long prevElapsedTime);
void watering(int num);
void levelUp(long totalElapsedTime);
void isItAlive();

int main(void)
{
    long startTime = 0;        // 게임 시작시간
    long totalElapsedTime = 0; // 총 경과시간
    long prevElapsedTime = 0;  // 직전 경과시간 (최근에 물을 준 시간 간격)

    int num;                   // 물을 채울 어항 번호 (사용자 입력)

    initData();

    cursor = arrayFish;

    startTime = clock();       // 현재시각을 millisecond(ms, 1000분의 1초) 단위로 변환

    while (1)                  // 문한반복
    {
        printFishes();

        num = initNum();

        totalElapsedTime = initTotalElapsedTime(startTime);
        printf("게임 총 경과시간: %ld 초\n", totalElapsedTime);

        prevElapsedTime = initPrevElapsedTime(totalElapsedTime, prevElapsedTime);
        printf("마지막으로 물을 준 이후로 흐른 시간: %ld 초\n", prevElapsedTime);

        decreaseWater(prevElapsedTime);

        watering(num);

        levelUp(totalElapsedTime);

        isItAlive();

        prevElapsedTime = totalElapsedTime;
    }
    return 0;
}

void initData()
{
    level = 1;                 // 게임 시작 레벨 (1-5)

    for (int i = 0; i < 6; i++)
    {
        arrayFish[i] = 100;     // 최초 어항 물높이 
    }
}

void printFishes()
{
    printf("\n");
    printf("%3d번 %3d번 %3d번 %3d번 %3d번 %3d번\n", 1, 2, 3, 4, 5, 6);
    for (int i = 0; i < 6; i++)
    {
        printf(" %4d ", arrayFish[i]);
    }
    printf("\n\n");
}

void decreaseWater(long elapsedTime)
{
    for (int i = 0; i < 6; i++)
    {
        arrayFish[i] -= (level * 3 * (int)elapsedTime); // 3은 난이도 조절용

        // 물의 최소량은 0임.
        if (arrayFish[i] < 0)
        {
            arrayFish[i] = 0;
        }
    }
}

int checkFishAlive()
{
    for (int i = 0; i < 6; i++)
    {
        if (arrayFish[i] > 0)
            return 1; // 참 True
    }

    return 0;
}

int initNum() {
    int num;

    while (1) {
        printf("몇 번 어항에 물을 주시겠어요? ");
        scanf("%d", &num);

        // 입력값 체크
        if (num < 1 || num > 6)
        {
            printf("\n입력값이 잘못되었어요.\n");
            continue;
        }
        else {
            return num;
        }
    }
}

long initTotalElapsedTime(long startTime) {
    long totalElapsedTime = 0;

    totalElapsedTime = (clock() - startTime) / 10;

    return totalElapsedTime;
}

long initPrevElapsedTime(long totalElapsedTime, long prevElapsedTime) {
    return totalElapsedTime - prevElapsedTime;
}

void watering(int num) {
    if (cursor[num - 1] <= 0)
    {
        printf("%d 번 물고기는 이미 사망. 물 주지 않음.\n", num);
    }
    else if (cursor[num - 1] + 1 <= 100)
    {
        printf("%d 번 어항에 물주기\n\n", num);
        cursor[num - 1] += 1;
    }
}

void levelUp(long totalElapsedTime) {
    if (totalElapsedTime / 20 > level - 1)
    {
        level += 1;
        printf(" *** 축 레벨업! 기존 %d 레벨에서 %d 레벨로 업그레이드 ***\n\n", level - 1, level);

        if (level == 5)
        {
            printf("\n\n축하합니다. 최고레벨을 달성하였음! 게임 종료!\n\n");
            exit(0);
        }
    }
}

void isItAlive() {
    if (checkFishAlive() == 0)
    {
        printf("모든 물고기 사망 ㅠㅠ\n");
        exit(0);
    }
    else
    {
        printf("물고기 아직 살아 있음!\n");
    }
}