이 포스팅은 윤성우 님의 열혈 자료구조를 기반으로 합니다.

추상 자료형: Abstract Data Type

추상 자료형은 간단히 ADT라고 불리며 기능만 언급하며 기능의 과정은 포함하지 않는다.
이번에 배울 배열기반 리스트 자료구조의 ADT는 다음과 같다.

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
void ListInit(List * plist); 
// 초기화할 리스트의 주소 값을 인자로 전달한다.
// 리스트 생성 후 제일 먼저 호출되어야 하는 함수이다.
 
void LInsert(List * plist, LData data);
// 리스트에 데이터를 저장한다. 매개변수 data에 전달된 값을 저장한다.
 
int LFirst(List * plist, LData * pdata);
// 첫 번째 데이터가 pdata가 가리키는 메모리에 저장된다.
// 데이터의 참조를 위한 초기화가 진행된다.
// 참조 성공 시 TRUE(1), 실패 시 FALSE(0) 반환
 
int LNext(List * plist, LData * pdata);
// 참조된 데이터의 다음 데이터가 pdata가 가리키는 메모리에 저장된다.
// 순차적인 참조를 위해서 반복 호출이 가능하다.
// 참조를 새로 시작하려면 먼저 LFirst함수를 호출해야 한다.
// 참조 성공 시 TRUE(1), 실패 시 FALSE(0) 반환
 
LData LRemove(List * plist);
// LFirst 또는 LNext 함수의 마지막 반환 데이터를 삭제한다.
// 삭제된 데이터는 반환된다.
// 마지막 반환 데이터를 삭제하므로 연이은 반복 호출을 허용하지 않는다.
 
int LCount(List * plist);
// 리스트에 저장되어 있는 데이터의 수를 반환한다.
cs

배열을 이용한 리스트의 구현

리스트에는 크게 두가지의 종류가 있고 이는 다음과 같다.

  • 순차 리스트
  • 연결 리스트

리스트 자료구조는 데이터를 나란히 저장한다는 점과 중복 데이터의 저장을 허용한다는 큰 특성이 있다.
이제 위의 ADT를 기반으로 main함수를 작성하면 다음과 같다.

<ListMain.c>

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <stdio.h>
#include "ArrayList.h"
 
int main(void)
{
    /*** ArrayList의 생성 및 초기화 ***/
    List list;
    int data;
    ListInit(&list);
 
    /*** 5개의 데이터 저장 ***/
    LInsert(&list, 11);  LInsert(&list, 11);
    LInsert(&list, 22);  LInsert(&list, 22);
    LInsert(&list, 33);
 
    /*** 저장된 데이터의 전체 출력 ***/
    printf("현재 데이터의 수: %d \n", LCount(&list));
 
    if (LFirst(&list, &data))    // 첫 번째 데이터 조회
    {
        printf("%d ", data);
 
        while (LNext(&list, &data))    // 두 번째 이후의 데이터 조회
            printf("%d ", data);
    }
    printf("\n\n");
 
    /*** 숫자 22을 탐색하여 모두 삭제 ***/
    if (LFirst(&list, &data))
    {
        if (data == 22)
            LRemove(&list);
 
        while (LNext(&list, &data))
        {
            if (data == 22)
                LRemove(&list);
        }
    }
 
    /*** 삭제 후 저장된 데이터 전체 출력 ***/
    printf("현재 데이터의 수: %d \n", LCount(&list));
 
    if (LFirst(&list, &data))
    {
        printf("%d ", data);
 
        while (LNext(&list, &data))
            printf("%d ", data);
    }
    printf("\n\n");
    return 0;
}
cs

위의 ListMain.c를 기반으로 하는 ArrayList.h, ArrayList.c도 다음과 같다.
<ArrayList.h>

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
#ifndef __ARRAY_LIST_H__
#define __ARRAY_LIST_H__
 
#define TRUE    1
#define FALSE   0
 
/*** ArrayList의 정의 ****/
#define LIST_LEN        100
typedef int LData;
 
typedef struct __ArrayList
{
        LData arr[LIST_LEN];
        int numOfData;
        int curPosition;
} ArrayList;
 
 
/*** ArrayList와 관련된 연산들 ****/
typedef ArrayList List;
 
void ListInit(List* plist);
void LInsert(List* plist, LData data);
 
int LFirst(List* plist, LData* pdata);
int LNext(List* plist, LData* pdata);
 
LData LRemove(List* plist);
int LCount(List* plist);
 
#endif
cs

<ArrayList.c>

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <stdio.h>
#include "ArrayList.h"
 
void ListInit(List* plist)
{
        (plist->numOfData) = 0;
        (plist->curPosition) = -1;
}
 
void LInsert(List* plist, LData data)
{
        if (plist->numOfData > LIST_LEN)
        {
                puts("저장이 불가능합니다.");
                return;
        }
 
        plist->arr[plist->numOfData] = data;
        (plist->numOfData)++;
}
 
int LFirst(List* plist, LData* pdata)
{
        if (plist->numOfData == 0)
                return FALSE;
 
        (plist->curPosition) = 0;
        *pdata = plist->arr[0];
        return TRUE;
}
 
int LNext(List* plist, LData* pdata)
{
        if (plist->curPosition >= (plist->numOfData) - 1)
                return FALSE;
 
        (plist->curPosition)++;
        *pdata = plist->arr[plist->curPosition];
        return TRUE;
}
 
LData LRemove(List* plist)
{
        int rpos = plist->curPosition;
        int num = plist->numOfData;
        int i;
        LData rdata = plist->arr[rpos];
 
        for (i = rpos; i < num - 1; i++)
                plist->arr[i] = plist->arr[i + 1];
 
        (plist->numOfData)--;
        (plist->curPosition)--;
        return rdata;
}
 
int LCount(List* plist)
{
        return plist->numOfData;
}
cs

위의 코드들을 자세히 살펴보면 배열 기반 리스트의 삭제가 복잡한 것을 알 수 있다.


먼저 배열 리스트의 데이터 삭제를 살펴보자
배열의 특성상, 그리고 리스트의 특성상 데이터가 나란히 존재해야 하므로 다음의 그림처럼 되는 것을 확인할 수 있다.

또한 가장 최근에 참조가 이루어진 데이터의 인덱스 정보를 담는 변수 curPosition 역시 참조하던 데이터가 삭제되면
앞의 데이터를 참조해야 한다. 이는 다음의 그림과 같다.


실제로 리스트에는 예시의 정수 이외에 다른 자료들도 들어간다. 이번에는 그렇다면 구조체 변수의 주소 값을 저장하여 보자 구조체는 다음과 같다.

1
2
3
4
typedef struct _point {
    int xpos; // x좌표
    int ypos; // y좌표
} Point;
cs

<Point.h>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef __POINT_H__
#define __POINT_H__
 
typedef struct _point {
    int xpos;
    int ypos;
} Point;
 
// Point 변수의 xpos, ypos 값 설장
void SetPointPos(Point* ppos, int xpos, int ypos);
 
// Point 변수의 xpos, ypos정보 출력
void ShowPosition(Point* ppos);
 
// 두 Point 변수의 비교
int PointComp(Point* pos1, Point* pos2);
 
#endif
cs

<Point.c>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>
#include "Point.h"
 
void SetPointPos(Point* ppos, int xpos, int ypos) {
    ppos->xpos = xpos;
    ppos->ypos = ypos;
}
 
void ShowPosition(Point* ppos) {
    printf("[%d, %d] \n", ppos->xpos, ppos->ypos);
}
 
int PointComp(Point* pos1, Point* pos2) {
    if (pos1->xpos == pos2->xpos && pos1->ypos == pos2->ypos)
        return 0;
    else if (pos1->xpos == pos2->xpos)
        return 1;
    else if (pos1->ypos == pos2->ypos)
        return 2;
    else
        return -1;
}
cs

이 후 ArrayList.h, ArrayList.c를 기반으로 위의 Point 구조체를 저장할 수 있도록 하면 이 과정에서
헤더 파일은 변경이 되어도 되지만 소스파일은 변경이 되면 안 된다.
헤더 파일에서 달라진 점은 typedef int LData; 에서 typedef Point * LData; 으로 변경되었다.
또한 ArrayList.h의 선언문에 #include "Point.h" 를 추가한다. 이제 main함수는 다음과 같다.
<PointListMain.c>

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
#include <stdio.h>
#include <stdlib.h>
#include "ArrayList.h"
#include "Point.h"
 
int main(void) {
    List list;
    Point comPos;
    Point* ppos;
 
    ListInit(&list);
 
    // 4개의 데이터 저장
    ppos = (Point*)malloc(sizeof(Point));
    SetPointPos(ppos, 21);
    LInsert(&list, ppos);
 
    ppos = (Point*)malloc(sizeof(Point));
    SetPointPos(ppos, 22);
    LInsert(&list, ppos);
 
    ppos = (Point*)malloc(sizeof(Point));
    SetPointPos(ppos, 31);
    LInsert(&list, ppos);
 
    ppos = (Point*)malloc(sizeof(Point));
    SetPointPos(ppos, 32);
    LInsert(&list, ppos);
 
    // 저장된 데이터의 출력
    printf("현재 데이터의 수는 : %d \n", LCount(&list));
 
    if (LFirst(&list, &ppos)) {
        ShowPosition(ppos);
 
        while (LNext(&list, &ppos)) {
            ShowPosition(ppos);
        }
    }
    printf("\n");
 
    // xpos가 2인 모든 데이터 삭제
    comPos.xpos = 2;
    comPos.ypos = 0;
 
    if (LFirst(&list, &ppos)) {
        if (PointComp(ppos, &comPos) == 1) {
            ppos = LRemove(&list);
            free(ppos);
        }
 
        while (LNext(&list, &ppos)) {
            if (PointComp(ppos, &comPos) == 1) {
                ppos = LRemove(&list);
                free(ppos);
            }
        }
    }
 
    // 삭제 후 남은 데이터 전체 출력
    printf("현재 데이터의 수는 : %d \n", LCount(&list));
 
    if (LFirst(&list, &ppos)) {
        ShowPosition(ppos);
 
        while (LNext(&list, &ppos)) {
            ShowPosition(ppos);
        }
    }
    printf("\n");
 
    return 0;
}
cs

이제 정리해보자.

  • 배열 기반 리스트의 단점
    • 배열의 길이가 초기에 결정되어야 한다. 또한 변경이 불가능하다.
    • 삭제의 과정에서 데이터의 이동이 일어난다. (마지막 경우가 아닌 경우)
  • 배열 기반 리스트의 장점
    • 데이터의 참조가 쉽다. 인덱스를 기준으로 한 번에 참조가 가능하다.

'자료구조' 카테고리의 다른 글

5. 스택  (0) 2022.01.10
4. 연결 리스트  (0) 2021.07.19
2. 재귀 (Recursion)  (0) 2021.06.27
1. 자료구조와 알고리즘의 이해  (0) 2021.06.27

이 포스팅은 윤성우님의 열혈 자료구조를 기반으로 합니다.

2-1 함수의 재귀적 호출의 이해

재귀 함수란 함수 내에서 자기 자신을 다시 호출하는 함수를 말하며 동작 과정은 다음과 같다.
먼저 다음의 재귀함수 코드를 보자.

1
2
3
4
void Recursive(void) { 
    printf("Recursive call! \n"); 
    Recursive(); 
}
cs

이를 그림으로 표현하면 다음과 같다.

Recursive 함수의 동작


Recursive 함수를 실행하는 중간에 다시 Recursive 함수가 호출되면, Recursive 함수의 복사본을 하나 더 만들어서 복사본을 실행하게 된다. 재귀 함수의 중요한 점은 바로 탈출 조건을 설정해야 한다는 것이다.
위의 코드의 경우 탈출 조건이 없으므로 무한 호출이 된다. 다음의 코드에서는 탈출 조건을 설정하여 3번 출력이 되는것을 확인할 수 있다.

<RecursiveFunc.c>

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
 
void Recursive(int num) {
    if (num <= 0// 탈출 조건
        return;
    printf("Recursive call! %d \n", num);
    Recursive(num - 1);
}
 
int main(void) {
    Recursive(3);
    return 0;
}
cs

<실행결과>

재귀함수의 예시로 팩토리얼 함수를 들 수 있다. 먼저 팩토리얼의 수식을 쓰면 다음과 같다.

이를 코드적인 관점에서 생각해보면 n이 1인 경우는 1을 반환하고 그 이외의 경우는 n-1을 반환하면 되는 것이다. 

코드로 작성하면 다음과 같다.

<RecursiveFactorial.c>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
 
int Factorial(int num) {
    if (num == 0// 탈출 조건
        return 1;
    return num * Factorial(num - 1);
}
 
int main(void) {
    printf("1! = %d\n", Factorial(1));
    printf("2! = %d\n", Factorial(2));
    printf("3! = %d\n", Factorial(3));
    printf("4! = %d\n", Factorial(4));
    printf("9! = %d\n", Factorial(9));
    return 0;
}
cs

<실행결과>


2-2 재귀의 활용

재귀의 또다른 예로 피보나치 함수가 존재한다. 피보나치의 경우 0 1 1 2 3 5 8 13 21....로 1번째와 2번째를 제외하고 전 수와 전전 수를 합하여 현재의 수를 만드는 것이다. 이를 코드 적인 관점에서 생각하면 n이 1인 경우는 0, 2인 경우는 1로 설정하고 그 외의 경우는 n-1번째 수+ n-2번째 수로 하면 된다. 코드로 작성하면 다음과 같다.

<FibonacciFunc.c>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
 
int Fibo(int num) {
    if (num == 1// 1번째인 경우 0 반환
        return 0;
    else if (num == 2// 2번째인 경우 1 반환
        return 1;
    else // 그 외의 경우
        return Fibo(num-1+ Fibo(num-2);
}
 
int main(void) {
    for (int i = 1; i <= 15; i++) { // 피보나치 수 15개 출력
        printf("%d ", Fibo(i));
    }
    return 0;
}
cs

<실행결과>

앞선 포스팅에서 다루었던 이진 탐색의 경우 반복문을 통해 작성하였다. 이 또한 방금 배운 재귀를 이용하여 작성할 수 있다. 반복문에서는 while(first <= last) 의 조건을 통해 구현하였으니 이를 탈출 조건으로 작성하면 된다.

소스코드로 나타내면 다음과 같다.

<RecursiveBinarySearch.c>

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
#include <stdio.h>
 
int BSearchRecur(int arr[], int first, int last, int target) {
    int mid;
    if (first > last) // 탈출조건
        return -1;
    
    mid = (first + last) / 2;
    if (arr[mid] == target) // 찾은 경우 해당 위치의 인덱스 반환
        return mid;
    else if (arr[mid] > target) // mid의 값이 target보다 큰 경우 좌측에서 탐색해야함
        return BSearchRecur(arr, first, mid - 1, target);
    else // mid의 값이 target보다 작은 경우 우측에서 탐색해야함
        return BSearchRecur(arr, mid + 1, last, target);
}
 
int main(void) {
    int arr[] = { 13579 };
    int idx;
 
    idx = BSearchRecur(arr, 0sizeof(arr) / sizeof(int- 15);
    if (idx == -1)
        printf("탐색 실패 \n");
    else
        printf("타겟의 인덱스는 %d 입니다.\n", idx);
 
    idx = BSearchRecur(arr, 0sizeof(arr) / sizeof(int- 18);
    if (idx == -1)
        printf("탐색 실패 \n");
    else
        printf("타겟의 위치는 %d 입니다.\n", idx);
 
    return 0;
}
cs

<실행결과>

다만 이진탐색의 경우 중복으로 호출하는 구간이 존재하므로 코드의 시간 복잡도의 측면에서는 비효율적이다.


하노이 타워: The Tower of Hanoi

재귀함수의 또 다른 예시로 하노이 타워가 있다. 하노이 타워란 다음의 사이트에서 확인하길 바란다.

 

하노이의 탑 - 위키백과, 우리 모두의 백과사전

하노이의 탑(Tower of Hanoi)은 퍼즐의 일종이다. 세 개의 기둥과 이 기둥에 꽂을 수 있는 크기가 다양한 원판들이 있고, 퍼즐을 시작하기 전에는 한 기둥에 원판들이 작은 것이 위에 있도록 순서대

ko.wikipedia.org

어쨌든 우리가 알아야 할 것은 원반은 한 번에 하나만 옮길 수 있고 작은 원반 위에 큰 원반이 놓여서는 안 된다는 것이다. 이를 자세하게 살펴보면 하노이 역시 일련의 과정을 반복하므로 재귀를 통해 구현할 수 있다는 것을 알 수 있다.

간단하게만 알아보아도 될 예시이므로 바로 소스코드를 보인다.

<HanoiTowerSolu.c>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
 
void HanoiTowerMove(int num, char from, char by, char to) {
    if (num == 1)
        printf("원반 1을 %c에서 %c로 이동\n", from, to);
    else {
        HanoiTowerMove(num - 1, from, to, by); 
        printf("원반 %d를 %c에서 %c로 이동\n", num, from, to); 
        HanoiTowerMove(num - 1, by, from, to);
    }
}
 
int main(void) {
    HanoiTowerMove(3'A''B''C');
    return 0;
}
cs

<실행결과>

'자료구조' 카테고리의 다른 글

5. 스택  (0) 2022.01.10
4. 연결 리스트  (0) 2021.07.19
3. 배열 기반 리스트  (0) 2021.07.19
1. 자료구조와 알고리즘의 이해  (0) 2021.06.27
typedef

typedef를 배우기 전에 다음의 코드를 살펴보자. 

<소스코드>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
 
struct point {
    int xpos;
    int ypos;
};
 
int main(void) {
    struct point pnt = { 23 };
    printf("[%d, %d]", pnt.xpos, pnt.ypos);
 
    return 0;
}
 
cs

9번 라인을 보면 struct point형 변수를 선언하기 위해 struct point pnt = {2, 3}; 으로 선언한 것을 볼 수 있다. 매번 struct를 사용하는 것은 불편하다. 그렇다면 int num = 10; 처럼 간단하게 할 수 있는 방법이 없을까? 있다. 바로 typedef를 사용하면 된다. 이를 다음의 소스코드와 이전의 소스코드를 비교하며 이해해보자.

<소스코드>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
 
typedef struct {
    int xpos;
    int ypos;
} Point;
 
int main(void) {
    Point pnt = { 23 };
    printf("[%d, %d]", pnt.xpos, pnt.ypos);
 
    return 0;
}
 
cs

두 코드의 실행결과는 모두 동일하며 3번 라인을 보면 struct 앞에 typedef 키워드가 있고 point라는 구조체 이름이 빠지고 구조체의 마지막에 Point라고 붙여진것을 볼 수 있다. 이를 통해 9번 라인에서 볼 수 있듯이 단순하게 Point pnt = {2, 3}; 으로 구조체 변수를 간단하게 생성하는 것을 볼 수 있다.


함수로의 구조체 변수 전달과 반환

기본 자료형과 마찬가지로 구조체 변수 역시 매개변수에 복사가 되거나 반환될 수 있다. 다음의 코드를 살펴보자.

<소스코드>

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
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
typedef struct {
    int xpos;
    int ypos;
} Point;
 
void showPosition(Point pnt) {
    printf("[%d, %d] \n", pnt.xpos, pnt.ypos);
}
 
Point GetCurrentPosition(void) {
    Point cen;
    printf("pos를 입력하시오 : ");
    scanf("%d %d"&cen.xpos, &cen.ypos);
    return cen;
}
 
int main(void) {
    Point curPos = GetCurrentPosition();
    showPosition(curPos);
 
    return 0;
}
 
cs

<실행결과>

라인 9번에서 볼 수 있듯이 매개변수로 구조체 변수가 들어갔고 21번 라인에서 볼 수 있듯 반환값을 Point 변수 curpos에 대입하는것을 볼 수 있다. 또한 매개변수로 구조체의 포인터 변수도 들어갈 수 있다. 이를 통해 Call by reference도 할 수 있다. 이를 예시 코드를 통해 알아보자.

<소스코드>

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
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
typedef struct {
    int xpos;
    int ypos;
} Point;
 
void OrgSynTrans(Point* pnt) {
    pnt->xpos = pnt->xpos * -1;
    pnt->ypos = pnt->ypos * -1;
}
 
void showPosition(Point pnt) {
    printf("[%d, %d] \n", pnt.xpos, pnt.ypos);
}
 
int main(void) {
    Point pos = { 7-5 };
    OrgSynTrans(&pos);
    showPosition(pos);
    OrgSynTrans(&pos);
    showPosition(pos);
    return 0;
}
 
cs

<실행결과>

9번 라인에서 볼 수 있듯이 함수의 매개변수로 구조체의 주소값을 넘겨서 원점 대칭하는것을 알 수 있다. 따라서 메인함수의 좌표값을 메인함수 외부에서 변경하는것을 볼 수 있다.

또한 구조체 변수 역시 구조체의 멤버로 포함될 수 있다. 이를 구조체의 중첩이라 한다. 다음의 소스코드를 확인하자.

<소스코드>

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
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
typedef struct {
    int xpos;
    int ypos;
} Point;
 
typedef struct {
    Point cen;
    double rad;
} Circle;
 
void showCircleInfo(Circle * c) {
    printf("[%d, %d] \n", (c->cen).xpos, (c->cen).ypos);
    printf("radius: %g \n\n", c->rad);
}
 
int main(void) {
    Circle c1 = { {12}, 3.5 }; // 구조체가 중첩 됨
    showCircleInfo(&c1);
 
    return 0;
}
 
cs

<실행결과>

20번 라인에서 볼 수 있듯이 구조체가 중첩된 것을 알 수 있다.


UNION

구조체와 공용체(union)의 차이

typedef struct {

    int mem1;

    int mem2;

    double mem3;

} Sbox;

 

typedef union {

    int mem1;

    int mem2;

    double mem3;

} Ubox;

위의 Sbox, Ubox의 sizeof 연산을 해보면 각각 16 byte, 8  byte 인것을 확인할 수 있다.  이를 그림으로 표현하면 다음과 같다.

이 union은 하나의 메모리 공간에 둘 이상의 방식으로 접근할 수 있다는 특성이 있다.

 


Enum 열거형

enum을 설명하기 전에 먼저 다음의 소스코드를 보자.

<소스코드>

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
39
#include <stdio.h>
 
typedef enum {
    Do = 1, Re = 2, Mi = 3, Fa = 4, Sol = 5, La = 6, Ti = 7
} Syllable;
 
void Sound(Syllable sy) {
    switch (sy) {
    case Do:
        puts("도 입니다.");
        return;
    case Re:
        puts("레 입니다.");
        return;
    case Mi:
        puts("미 입니다.");
        return;
    case Fa:
        puts("파 입니다");
        return;
    case Sol:
        puts("솔 입니다.");
        return;
    case La:
        puts("라 입니다.");
        return;
    case Ti:
        puts("시 입니다.");
        return;
    }
}
 
int main(void) {
    Syllable tone;
    for (tone = Do; tone <= Ti; tone++)
        Sound(tone);
    return 0;
}
 
cs

<실행결과>

열거형은 연관이 있는 이름을 동시에 상수로 선언할 수 있다는 장점이 존재한다.

 

이 포스팅은 윤성우님의 열혈 C를 기반으로 작성되었습니다.

'C' 카테고리의 다른 글

14. 메모리 및 동적할당  (0) 2021.06.28
11. 구조체와 사용자 정의 자료형  (0) 2021.06.27
10. 문자열 관련 함수  (0) 2021.06.27
9. 함수 포인터 & void 포인터  (0) 2021.06.26
8. 2차원 배열과 포인터  (0) 2021.06.26

2차원 배열의 선언은 다음과 같다.

int arr1[3][4];

이는 3행 4열을 가진 이차원 배열 arr1을 의미하며 그림으로 표현하면 다음과 같다.

[0][0] [0][1] [0][2] [0][3]
[1][0] [1][1] [1][2] [1][3]
[2][0] [2][1] [2][2] [2][3]

그렇다면 이 배열의 sizeof 연산을 한 결과를 예시를 통해 확인해보자.

<소스코드>

1
2
3
4
5
6
7
8
9
#include <stdio.h>
 
int main(void) {
    int arr1[3][4];
 
    printf("3행 4열인 배열 arr1의 크기는 %d 입니다.\n"sizeof(arr1));
 
    return 0;
}
cs

<실행결과>

 

위의 표를 보면 3행 4열 이므로 총 12칸이 존재하고 int 형 배열이므로 12 x 4이므로 48바이트 인 것을 확인할 수 있다.

이차원 배열은 다음의 형태로 선언과 동시에 초기화할 수 있는데 총 3가지의 형태가 존재한다. 이를 예시를 통해 확인해보자.

<소스코드>

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 <stdio.h>
 
int main(void) {
    int arr1[3][3= {
        {123},
        {456},
        {789}
    };
 
    int arr2[3][3= {
        {1},
        {45},
        {789}
    };
 
    int arr3[3][3= { 1234567 };
 
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%d ", arr1[i][j]);
        }
        printf("\n");
    }
    printf("\n");
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%d ", arr2[i][j]);
        }
        printf("\n");
    }
    printf("\n");
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%d ", arr3[i][j]);
        }
        printf("\n");
    }
}
cs

<실행결과>

3차원 배열도 위의 2차원 배열과 비슷하므로 이 포스팅에서는 생략 하겠다.

'C' 카테고리의 다른 글

8. 2차원 배열과 포인터  (0) 2021.06.26
7. 이중 포인터  (0) 2021.06.25
5. 포인터와 함수  (0) 2021.06.25
4. 포인터와 배열  (0) 2021.06.22
3. 포인터  (0) 2021.06.21
함수의 인자로 배열 전달

함수 호출시 전달되는 인자의 값은 매개변수에 복사가 되는 형태이다. 즉 int Func(int number); Func(num); 이라는 문장이 있다면 실제의 num값이 아닌 num에 저장된 값이 number에 복사가 되는 것이다. 그렇다면 Func 함수 내부에서 number값을 하나 증가 혹은 감소시키면 num은 어떻게 될까?

정답은 아무 상관이 없다 이다. 이유는 두 변수는 서로 다른 변수이기 때문이다. 

그렇다면 배열을 인자로 전달하려면 어떻게 해야 할까? 바로 주소값을 알려주면 되는 것이다.

int arr[3] = {1, 2, 3}; 다음의 형태로 저장된 배열이 있다면 Func(arr); 의 형태로 배열의 주소 값을 전달하면 되는 것이다. 앞에서 배열의 이름은 주소 값이라는 것을 배웠기 때문에 가능한 일이다. 다음의 예시를 통해 함수의 매개변수로 배열을 인자로 전달하는 형태를 살펴보자.

<소스코드>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
 
void showArray(int array[], int len) { // 배열을 함수의 인자로 전달받는 함수의 선언
    for (int i = 0; i < len; i++) {
        printf("%d ", array[i]);
    }
}
 
int main(void) {
    int arr[5= { 12345 };
    int length = sizeof(arr) / sizeof(int);
 
    showArray(arr, length); // 함수의 호출
 
    return 0;
}
cs

<실행결과>


Call by value와 Call by reference

함수의 호출에는 두가지 방식이 존재한다. 바로 값에 의한 호출, 참조에 의한 호출이 그것이다. 기존에 배웠던 함수들의 대부분은 Call by value 였다. 그렇다면 Call by reference는 왜 존재하는 것일까? 다음의 예시를 통해 알아보자.

<소스코드>

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
#include <stdio.h>
 
void SwapByValue(int num1, int num2) { // Call by value에 의한 함수 SwapByValue
    int temp; 
    // swap
    temp = num1;
    num1 = num2;
    num2 = temp;
}
 
void SwapByReference(int * num1, int * num2) { // Call by reference에 의한 함수 SwapByReference
    int temp;
    // swap
    temp = *num1;
    *num1 = *num2;
    *num2 = temp;
}
 
int main(void) {
    int n1 = 3, n2 = 5;
 
    SwapByValue(n1, n2);
    printf("Call by value\n");
    printf("n1 = %d, n2 = %d\n", n1, n2);
 
    puts("");
 
    printf("Call by reference\n");
    SwapByReference(&n1, &n2);
    printf("n1 = %d, n2 = %d\n", n1, n2);
    return 0;
}
cs

<실행결과>

먼저 SwapByValue 함수를 호출한 경우 main함수의 n1, n2가 그대로인 것을 확인할 수 있다. 이는 이 포스팅의 맨 처음 부분에 나왔던 number값을 증가시켜도 num값은 변화하지 않는 것과 동일한 이유 때문이다. 따라서 SwapByReference 방식을 이용한 즉 Call By Reference 방식을 이용하여 주소 값을 통해 접근하게 해야 main 함수에서도 변경이 될 수 있는 것이다. 이를 그림으로 표현하면 다음과 같다.

call by value의 경우
Call by reference의 경우

위의 그림에서 볼 수 있듯이 call by value와 다르게 call by reference의 경우 주소값을 통해 함수 내부에서 main영역에 있는 변수에 접근할 수 있다.


'C' 카테고리의 다른 글

7. 이중 포인터  (0) 2021.06.25
6. 다차원 배열  (0) 2021.06.25
4. 포인터와 배열  (0) 2021.06.22
3. 포인터  (0) 2021.06.21
2. 배열을 이용하여 문자열의 표현  (0) 2021.06.21
배열의 이름은 포인터 

배열의 이름은 포인터이며 값을 변경할 수 없는 상수 형태의 포인터이다. 다음 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main(void) {
    int arr[3= { 123 };    
    printf("배열의 이름 : %p \n", arr);
    printf("배열의 첫번째 원소 : %p \n"&arr[0]);
    printf("배열의 두번째 원소 : %p \n"&arr[1]);
    printf("배열의 세번째 원소 : %p \n"&arr[2]);
 
    return 0;
}
cs

<실행결과>

위의 코드와 실행결과를 통해 배열 arr가 int형 이므로 각각 4바이트씩 주소 값의 차이가 나는 것을 볼 수 있으며 모든 배열 요소가 메모리 공간에 나란히 할당된다는 것을 알 수 있다. 또한 배열의 이름의 주소 값이 010FFB88이고 배열의 첫 번째 원소의 주소 값이 010FFB88로 동일한 것을 알 수 있다.

또한 배열의 이름은 대입 연산자의 피연산자가 될 수 없으므로 다음과 같이 정리할 수 있다.

 

배열의 이름은 배열의 시작 주소 값을 의미하며, 그 형태는 값의 저장이 불가능한 상수이다.

 

즉 배열의 이름은 상수형태의 포인터이다. 따라서 배열의 이름을 "포인터 상수"라고 부른다.

결국 배열의 이름은 포인터 이므로 * 연산자를 사용할 수 있다.

<소스코드>

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main(void) {
    int arr[3= { 123 };    
    
    printf("배열의 첫번째 원소 : %d \n"*arr);
    *arr += 100;
 
    printf("%d %d %d", arr[0], arr[1], arr[2]);
    return 0;
}
cs

 

<실행결과>

*연산자를 통해 6번 라인에서 *arr을 출력하는 것을 알 수 있다. 이 경우 배열의 첫 번째 원소가 출력이 된다.

또한 7번 라인을 통해 배열의 첫 번째 원소에 접근하여 100을 더하여 101로 변경이 된 것을 확인할 수 있다.


포인터를 대상으로 하는 증가 감소 연산

먼저 다음의 예제를 먼저 살펴보자.

int* ptr1 = 0x0010;

이 경우 ptr1++;를 하게 되면 0x0011이 아니라 int형이므로 4byte가 증가하여 0x0014가 된다.


double* ptr2 = 0x0010;

이 경우 ptr2++;를 하게 되면 0x0011이 아니라 double형이므로 8byte가 증가하여 0x0018이 된다.

 

즉 이를 일반화하면 type형 포인터를 대상으로 n의 크기만큼 증가 혹은 감소 시 n*sizeof(type)의 크기만큼 증가 혹은 감소한다는 것을 알 수 있다.


결론

그래서 우리는 중요한 사실을 유도할 수 있는데 바로 arr[i] == *(arr + i) 라는 것이다.

arr + i 는 arr의 type만큼 주소 값이 증가하기 때문에 arr [i]라는 것이다. 이것은 매우 중요하므로 잘 기억해야 한다.


예제

길이가 5인 int형 배열 arr을 선언하고 이를 1, 2, 3, 4, 5로 초기화 한 다음 이 배열의 첫 번째 요소를 가리키는 포인터 변수 ptr을 선언한다. 그다음 포인터 변수 ptr에 저장된 값을 증가시키는 형태의 연산을 기반으로 포인터 변수 ptr에 저장된 값을 변경시키지 않고 값을 3씩 증가시키고, 정상적으로 증가가 이루어졌는지 확인하는 예제를 작성해보자. (출처 윤성우 님의 열혈 C 프로그래밍 chapter 13 p.299 문제 1)

<소스코드>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
 
int main(void) {
    int arr[5= { 12345 };    
    int* ptr = arr; 
 
    for (int i = 0; i < 5; i++) { // ptr로 모든 원소 2씩 증가
        *(ptr + i) += 2;
    }
 
    for (int i = 0; i < 5; i++) { // 확인하기 위해 출력하는 
        printf("%d ", arr[i]);
    }
    
    return 0;
}
cs

<실행결과>


상수 형태의 문자열을 가리키는 포인터

문자열을 표현하는 방식에는 2가지가 있다.

  1. 배열을 기반으로 하는 문자열 - char str[] = "Hello world!";
  2. 포인터를 이용하는 문자열 - char * str = "Hello world!";

이를 그림으로 표현하면 다음과 같다.

배열을 기반으로 하는 문자열
포인터 변수 str

배열을 기반으로 하는 문자열은 변경이 가능하다. 그렇지만 포인터를 이용하는 문자열은 변경이 불가능하다.  사실상 배열 형태의 str, 포인터 변수 str 모두 첫 문자 H의 주소 값을 가리키긴 하지만 배열 str은 다른 것을 가리킬 수 없는 반면에 포인터 변수 str은 다른 위치를 가리킬 수 있다. 다시 말하면

char * str = "Hello world!";

str = "Bye~";

로 변경이 가능하다는 뜻 이다. 예시를 통해 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
 
int main(void) {
    char str1[] = "This is Array";
    const char* str2 = "This is Pointer";
    printf("str1 : %s \nstr2 : %s", str1, str2);
 
    //str1 = "This is Array and can't change"; // 배열 형태는 가르키는 대상 변경 불가능
    str2 = "This is Pointer and can change!"// 포인터 형태는 가르키는 대상 변경 가능
    printf("str1 : %s \nstr2 : %s", str1, str2);
 
    str1[0= 't'// 배열 형태는 문자열 변경 가능
    //str2[0] = 't'; // 포인터 형태는 문자열 변경 불가능
 
    return 0;
}
cs

<실행결과>


포인터 배열

포인터 변수로 이루어져 주소 값의 저장이 가능한 배열이 포인터 배열이고 선언방식은 다음과 같다.

int * arr1[20]; // 길이가 20인 int형 포인터 배열 arr1

dounle * arr2[10]; // 길이가 10인 double형 포인터 배열 arr2

예제는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main(void) {
    int num1 = 10, num2 = 20, num3 = 30;
    int* arr[3= { &num1, &num2, &num3 };
 
    for (int i = 0; i < 3; i++) {
        printf("%d\n"*arr[i]);
    }
    return 0;
}
cs

<실행결과>

 

이를 그림으로 표현하면 다음과 같다.

이는 다음의 형태로 문자열을 저장할 수도 있다. 위와 비슷하게 예시 코드를 보자.

 

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
 
int main(void) {
    const char* strArr[3= { "Simple""String""Array" };
 
    for (int i = 0; i < 3; i++) {
        printf("%s\n", strArr[i]);
    }
    return 0;
}
cs

<실행결과>


 

'C' 카테고리의 다른 글

6. 다차원 배열  (0) 2021.06.25
5. 포인터와 함수  (0) 2021.06.25
3. 포인터  (0) 2021.06.21
2. 배열을 이용하여 문자열의 표현  (0) 2021.06.21
1-1 배열 예제  (0) 2021.06.21

C에서 어려운 포인터입니다. C는 low-level 언어라고 하는데 바로 이 포인터를 이용해 메모리에 직접 접근이 가능하기 때문에 low-level 언어라고 불립니다. 그렇다면 이 포인터는 과연 어떤 것일까요?

int num = 10; 즉 변수 num의 경우 int형이므로 4바이트가 메모리에 저장이 된다. 이 메모리의 주소 값을 임의로 0x123456부터 시작한다고 가정해보자. 이때 이 0x123456이라는 번지를 주소 값이라고 한다. 이 주소 값 역시 저장할 수 있는 값이며 포인터 변수는 메모리의 주소 값을 저장하기 위한 변수이다.


포인터 변수의 선언

이 포인터 변수의 선언은 매우 간단하다. 다음을 보자

int * pnum; // int 형 변수를 가리키는 포인터 변수 pnum

double * pnum2; // double 형 변수를 가르키는 포인터 변수 pnum2

float * pnum3; // float 형 변수를 가르키는 포인터 변수 pnum3

즉 저장하고자 하는 변수의 형에 따라 포인터 변수의 형 역시 정해진다.


& 연산자

& 연산자는 피연산자의 주소 값을 반환하는 연산자이다. 이를 다음의 예시를 통해 살펴보면 다음과 같다.

1
2
3
4
5
6
7
8
9
#include <stdio.h>
 
int main(void) {
    int num = 7;
    int* pnum;
    pnum = &num; // num의 주소값을 &연산자를 통해 반환하여 포인터 변수 pnum을 초기화 한다.
 
    return 0;
}
cs

이때 변수의 형과 포인터 변수의 형은 일치해야 한다. 즉 다음의 경우는 오류가 발생할 수 있다.

1
2
3
4
5
6
7
8
9
#include <stdio.h>
 
int main(void) {
    int num = 7;
    double* pnum; 
    pnum = &num; // double 형 포인터 변수에 int형 변수의 주소값을 넣으려 하므로 오류 발생
 
    return 0;
}
cs

* 연산자

* 연산자는 포인터가 가리키는 메모리 공간에 접근할 때 사용하는 연산자이다. 다음을 보자. 주석이 그 의미이다.

*pnum = 20; // 포인터 변수 pnum이 가리키는 메모리 공간인 변수 num에 int 값 20을 저장하라

printf("%d", *pnum); // 포인터 변수 pnum이 가리키는 메모리 공간인 변수 num에 저장된 값을 출력하라

예시를 통해 살펴보자

<소스코드>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
 
int main(void) {
    int num1 = 50, num2 = 100;
    int* pnum;
 
    pnum = &num1; // 포인터 변수 pnum이 int형 변수 num1을 가리킴
    (*pnum) += 30// num1 += 30과 동일하다
 
    pnum = &num2; // 포인터 변수 pnum이 int형 변수 num2를 가리킴
    (*pnum) -= 30;
 
    printf("num1 : %d\n", num1); // 50 + 30 
    printf("num2 : %d\n", num2); // 100 - 30
 
    return 0;
}
cs

<실행결과>

위의 결과를 그림으로 그려보면 다음과 같다.

포인터 구조


예제

int형 변수 num1, num2를 선언과 동시에 각각 30, 20으로 초기화하고 int형 포인터 변수 ptr1, ptr2를 선언하여 각각 num1, num2를 가리키게 하자. 그 후 ptr1과 ptr2를 이용하여 num1의 값을 10 증가시키고, num2의 값을 10 감소시키자. 이제 두 포인터 변수 ptr1과 ptr2가 가리키는 대상을 서로 바꾸고 ptr1과 ptr2가 가리키는 변수에 저장된 값을 출력하라

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
#include <stdio.h>
 
int main(void) {
    int num1 = 30, num2 = 20;
    int* ptr1, * ptr2;
 
    // ptr1, ptr2가 num1과 num2를 가리키게 한다.
    ptr1 = &num1;
    ptr2 = &num2;
 
    // ptr1과 ptr2를 각각 10 증가 10 감소
    (*ptr1) += 10;
    (*ptr2) -= 10;
 
    // 두 포인터 변수 ptr1과 ptr2가 가리키는 대상 변경
    int* temp;
    temp = ptr1;
    ptr1 = ptr2;
    ptr2 = temp;
 
    // 출력
    printf("*ptr1 = %d\n"*ptr1);
    printf("*ptr2 = %d\n"*ptr2);
 
    return 0;
}
cs

<실행결과>

 

'C' 카테고리의 다른 글

5. 포인터와 함수  (0) 2021.06.25
4. 포인터와 배열  (0) 2021.06.22
2. 배열을 이용하여 문자열의 표현  (0) 2021.06.21
1-1 배열 예제  (0) 2021.06.21
1. 배열 - 배열과 초기화에 관하여  (0) 2021.06.21

배열을 이용하여 문자열을 표현할 수 도 있다. 다음을 보자.

char str[ ] = "Hello world!"; 

위의 코드를 통해 배열 str에 char형 배열이 할당된 것을 확인할 수 있다. 그렇다면 위의 배열의 앞에서 배웠던 sizeof함수를 이용하여 길이를 알아보자.

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
 
int main(void) {
    char str[] = "Hello world!"// 글자는 공백을 포함하여 12개 이다. 
 
    int len = sizeof(str) / sizeof(char);
    printf("배열의 길이 : %d\n", len);
    
    return 0;
}
cs

<실행결과>

공백을 포함하여 Hello world! 는 12글자인데 왜 13글자가 나올까? 이는 널 문자 때문이다. 다시 말하면 배열 str은 다음의 구조와 같다.

널 문자 \0가 포함된 모습

그렇다면 이 널 문자는 왜 포함되는 것 일까? 바로 문자열을 표현하기 위해서 이다.

 

char arr[ ] = {'H', 'i', '~'}; // 널 문자(\0)가 없으므로 단순한 문자 배열

char arr2[ ] = {'H', 'i', '~', '\0'}; // 널 문자(\0)가 존재하므로 문자열  

 

즉 C언어에서는 문자열에서 널 문자를 인식하고 이를 문자열의 마지막으로 인식한다.


예제

프로그램 사용자로부터 하나의 영단어를 입력받아서 입력받은 영단어를 뒤집어서 출력하도록 예제를 작성하라 단 널 문자의 위치를 변경해서는 안된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
int main(void) {
    char str[100];
    scanf("%s", str);
 
    int len = 0;
    while (str[len] != '\0')
        len++;
 
    char temp;
 
    for (int i = 0; i < len / 2; i++) {
        temp = str[i];
        str[i] = str[len - 1 - i];
        str[len - 1 - i] = temp;
    }
 
    printf("%s", str);
    return 0;
}
cs

<실행결과>

 

'C' 카테고리의 다른 글

5. 포인터와 함수  (0) 2021.06.25
4. 포인터와 배열  (0) 2021.06.22
3. 포인터  (0) 2021.06.21
1-1 배열 예제  (0) 2021.06.21
1. 배열 - 배열과 초기화에 관하여  (0) 2021.06.21

+ Recent posts