Unit 56. 구조체 비트 필드 사용하기

구조체 비트 필드를 사용하면 구조체 멤버를 비트 단위로 저장할 수 있다. CPU나 기타 칩의 플래그를 다루는 저수준 프로그래밍에서 비트 단위로 값을 가져오거나 저장하는 경우가 많아 구조체 비트 필드가 유용하게 사용된다.

56.1 구조체 비트 필드를 만들고 사용하기

C99를 제외한 대부분의 컴파일러에서는 모든 정수 자료형을 사용할 수 있으며 부호 없는(unsigned)자료형을 주로 사용한다. 실수 자료형은 비트 필드로 사용할 수 없다.

비트 필드는 멤버를 선언할 때 클론 뒤에 비트 수를 지정해주면 된다.

다음은 구조체를 7비트, 3비트, 1비트로 나눠 비트 필드를 정의한 것이다.

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>
 
struct Flags{
    unsigned int a : 1;
    unsigned int b : 3;
    unsigned int c : 7;
};
 
int main()
{
    struct Flags f1;
 
    f1.a = 1;   // 0000 0001
    f1.b = 15;  // 0000 1111
    f1.c = 255// 1111 1111
 
    printf("%u\n", f1.a);
    printf("%u\n", f1.b);
    printf("%u\n", f1.c);
 
    return 0;
}
cs

구조체를 정의할 때 멤버 뒤에 클론을 붙여서 비트수를 지정한다.

비트 필드에 각각 1비트, 4비트, 8비트로 지정한 비트수보다 많은 비트수를 할당하였다.

각 멤버를 출력해보면 할당한 값과 다른 값이 나오는 것을 확인할 수 있다. 비트 필드에는 지정한 비트 수만큼 저장되고 나머지 비트는 버려진다. 따라서 8비트인 255는 비트 하나가 버려져서 111 1111이 되서 127이 출력됨을 확인할 수 있다.

비트 필드의 각 멤버는 최하위 비트부터 차례대로 배치된다. 따라서 a가 최하위 비트에 오고 나머지 멤버들은 상위 비트에 배치된다.

sizeof 연산자로 Flags 비트 필드 구조체의 크기를 구해보면 멤버를 unsigned int로 선언했기 때문에 4가 나온다.

56.2 비트 필드와 공용체를 함께 사용하기

CPU나 칩에 값을 설정할 대는 모든 비트를 묶어서 한꺼번에 저장한다. 비트필드와 공용체를 함께 사용하여 비트 필드의 값을 한꺼번에 사용할 수 있다.

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
#include <stdio.h>
 
struct Flags{
    union{
        struct{
            unsigned short a : 3;
            unsigned short b : 2;
            unsigned short c : 7;
            unsigned short d : 4;
        };
        unsigned short e;
    };
};
 
int main()
{
    struct Flags f1 = {0, };
 
    f1.a = 4;   // 0000 0100
    f1.b = 2;   // 0000 0010
    f1.c = 80;  // 0101 0000
    f1.d = 15;  // 0000 1111
 
    printf("%u\n", f1.e);
 
    return 0;
}
cs

비트 필도로 사용할 멤버는 익명 구조체로 감싸주고, 비트 필드의 값을 한번에 접근할 수 있는 unsigned short형 멤버(e)를 선언하여 익명 공용체로 감싸준다. 각 비트 필드에 값을 할당하고, e를 출력하면 64020이 나온다. 각 비트 필드에 할당한 비트들을 차례로 연결하면 1111 1010000 10 100이 되어 10진수로 64020이 된다.

56.3 퀴즈

정답은 d 이다.

정답은 4이다.

정답은 e 이다.

56.4 연습문제 : 구조체로 플래그 비트 필드 만들기

비트 수를 제한해야 하므로 정답은 다음 코드와 같다.

unsigned int a : 2;
unsigned int b : 1;
unsigned int c : 6;

56.5 연습문제 : 구조체와 공용체로 플래그 비트 필드 만들기

32936을 2진수로 표현하면 1000 0000 1010 1000이다. 할당할 값들을 2진수로 표현하면 각각 1000, 10, 10, 1000 0000 이므로 비트를 맞춰주면 다음과 같다.

unsigned short a : 4;
unsigned short b : 2;
unsigned short c : 2;
unsigned short d : 8;

56.6 심사문제 : 구조체로 플래그 비트 필드 만들기

정답은 다음 코드와 같다.

struct Flags{
  unsigned int a : 4;
  unsigned int b : 7;
  unsigned int c : 3;
};

56.7 심사문제 : 구조체와 공용체로 플래그 비트 필드 만들기

57412를 2진수로 변환하면 1110 0000 0100 0100이므로 비트수를 맞춰주면 다음과 같다.

unsigned int a : 3;
unsigned int b : 4;
unsigned int c : 7;
unsigned int d : 2;

Unit 57. 열거형 사용하기

열거형은 정수형 상수에 이름을 붙여서 코드를 이해하기 쉽도록 해준다.

57.1 열거형 정의하기

열거형은 eum키워드로 정의하고, enum 열거형이름 변수이름; 의 형태로 정의하여 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
 
enum DayOfWeek{
    Sunday = 0,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
};
 
int main()
{
    enum DayOfWeek week;
    week = Tuesday;
 
    printf("%d\n", week);
    return 0;
}
cs

enum 키워드 뒤에 열거형 이름을 지정하고, 중괄호안에 값을 나열한다. 각 값들은 콤마로 구분하고, 할당연산자를 사용하여 값을 할당할 수 있다. 열거형의 값은 처음에만 할당해주면 그 아래에 오는 값들은 1씩 증가하며 자동으로 할당된다.

정의한 열거형을 사용하려면 열거형 변수를 선언해야 한다. 열거형 이름 앞에 enum 키워드를 붙여준다. 보통 열거형 변수에는 미리 정의한 열거형 값을 넣는다.

연속되지 않은 불규칙한 값을 사용하려면 모든 열거형 값에 정수를 할당하면 된다.

57.2 열거형을 switch에 활용하기

열거형은 다음과 같이 switch 분기문을 사용할 때 유용하게 사용할 수 있다.

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>
 
enum LuxSkill{
    LightBinding = 1,
    PrismaticBarrier,
    LucentSigularity,
    FinalSpark
};
 
int main()
{
    enum LuxSkill skill;
    skill = LightBinding;
 
    switch (skill)
    {
    case LightBinding:
        printf("LightBinding\n");
        break;
    case PrismaticBarrier:
        printf("PrismaticBarrier\n");
        break;
    case LucentSigularity:
        printf("LucentSigurlarty\n");
        break;
    case FinalSpark:
        printf("FinalSpark\n");
        break;
    default:
        break;
    }
 
    return 0;
}
cs

열거형 변수를 switch문에 사용하면 열거형 값에 따라 코드를 실행할 수 있다. 위 코드에서는 skill 에 LightBinding를 할당했으므로 이에 해당하는 case가 실행된다.

57.3 열거형을 for에 활용하기

다음과 같이 열거형을 정의한 뒤 for 반복문에 활용할 수 도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
 
typedef enum _DayOfWeek { 
    Sunday = 0,                  
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    DayOfWeekCount      // 열거형 값의 개수
} DayOfWeek;                 
 
int main()
{
    
    for (DayOfWeek i = Sunday; i < DayOfWeekCount; i++
    {
        printf("%d\n", i);
    }
 
    return 0;
}
cs

열거형을 정의할 때 초기값을 0으로 할당하였고, 열거형 값을 나열하다 마지막에 열거형 값의 개수를 나타내는 항목을 넣어주었다. typedef를 사용하여 열거형 별칭을 DayOfWeek로 정의하였다.

for 반복문에서 열거형 별칭으로 변수 i를 선언한 뒤 초기값으로 Sunday를 넣고 i가 DayOfWeekCount보다 작을 때 까지 반복하여 0(Sunday) 부터 6(Saturday)까지 반복할 수 있다.

57.4 퀴즈

정답은 d이다.

정답은 4이다.

정답은 e이다.

정답은 c이다.

57.5 연습문제 : 장치 종류 정의하기

두번째 열거형을 출력했는데 3이 나왔으므로 정답은 2이다.

57.6 연습문제 : 게임 캐릭터 스킬 처리하기

정답은 다음과 같다.

1. switch(skill)

2. Tumble

3. SilverBolts

4. Condemn

5. FinalHour

57.7 연습문제 : 월 출력하기

정답은 다음 코드와 같다.

Jan = 1,
Feb,
Mar,
Apr,
May,
Jun,
Jul,
Aug,
Sep,
Oct,
Nov,
Dec,
MonthCount

값을 1부터 할당하고 마지막 항목은 값의 개수를 나타내는 항목을 만들면 된다.

57.8 심사문제 : 프로토콜 종류 정의하기

정답은 다음 코드와 같다.

enum PROTOCOL_TYPE{
    PROTOCOL_IP = 4,
    PROTOCOL_UDP,
    PROTOCOL_TCP
};

57.9 심사문제 : 게임 캐릭터 스킬 처리하기

정답은 다음 코드와 같다.

enum MasterYiSkill skill;
skill = Meditation;

57.10 심사문제 : 인터페이스 타입 출력하기

정답은 다음 코드와 같다.

for(INTERFACE_TYPE i = Internal; i < MaximumInterfaceType; i++)

Unit 58. 자료형 변환하기

C에서는 자료형이 같거나 크기가 큰 쪽, 표현 범위가 넓은 쪽으로 저장하면 자동으로 형 변환이 된다. 그러나 자료형이 다르면서 크기가 작은쪽, 표현 범위가 좁은 쪽으로 저장하면 컴파일 경고가 발생할 수 있다. 자료형의 크기가 큰 쪽, 표현 범위가 넓은 쪽으로 자동 형 변환 되는 것을 형 확장 이라 하며 암시적 형 변환이라 한다. 반대로 자료형 크기가 작은 쪽, 표현 범위가 좁은 쪽으로 변환되는것을 형 축소라 한다. 형 축소에서 컴파일 경고가 나오지 않도록 만드는 것을 형 변환 이라 한다. 프로그래머가 강제적으로 자료현을 변환한다고 하여 명시적 형 변환이라 부르기도 한다. 형 확장은 값의 손실이 없기 때문에 컴파일러가 알아서 처리할 수 있지만 형 축소는 값의 손실이 발생하여 컴파일러가 알아서 처리할 수 없기 때문에 경고가 발생한다. 형 변환은 컴파일러에게 자료형을 변환한다는 의도를 명확하게 알려주는 것이다.

58.1 기본 자료형 변환하기

자료형을 지정하여 변환하는 것을 명시적 자료 변환이라고 하며 변수나 값 앞에 변환할 자료형을 붙인 뒤 괄호로 묶어준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
 
int main()
{
    int num1 = 32;
    int num2 = 7;
    float num3;
 
    num3 = num1 / num2;     
    printf("%f\n", num3);    
 
    num3 = (float)num1 / num2;    
    printf("%f\n", num3);         
 
    return 0;
}
cs

정수 / 정수를 계산하게 되면 정수 4가 나와서 num3에는 4.000000이 저장된다. num1을 float로 강제 변환해주면 실수 / 정수가 되어 결과도 실수가 되어 num3에는 4.571429가 저장되며 컴파일 경고도 발생하지 않는다.

58.2 포인터 변환하기

다음 코드는 포인터끼리 변환하는 예시이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdlib.h>   
 
int main()
{
    int *numPtr = malloc(sizeof(int));  
    char *cPtr;
 
    *numPtr = 0x12345678;
    cPtr = (char *)numPtr;
 
    printf("0x%x\n"*cPtr);   
 
    free(numPtr);   
    return 0;
}
cs

numPtr에 메모리를 할당하고 역참조 하여 0x12345678을 저장하였다. cPtr에 int 포인터를 char 포인터로 저장하여 메모리 주소를 저장하면 cPtr은 char 포인터이므로 1바이트만 값을 가져오게 된다. 메모리 공간에는 리틀 엔디언 형식으로 값이 저장되므로 출력값은 0x78이 된다.

위와 반대로 크기가 작은 메모리 공간을 할당한 뒤 큰 자료형의 포인터로 역참조하면 의도치 않은 값을 가져올 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>   
 
int main()
{
    short *numPtr1 = malloc(sizeof(short));    
    int *numPtr2;
 
    *numPtr1 = 0x1234;
    numPtr2 = (int *)numPtr1;    
    
    printf("0x%x\n"*numPtr2);    
    free(numPtr1);  
    return 0;
}
cs

크기가 작은 메모리를 할당한 뒤 큰 자료형의 포인터로 역참조하면 컴파일러에 따라 옆의 메모리 공간을 침범하여 값을 가져올 수 도 있다. 위 코드는 2바이트 만큼 메모리를 할당했으므로 0x1234만 저장되어 있지만 4바이트 크기만큼 값을 가져오면 2바이트 크기를 벗어나서 malloc함수로 할당하지 않은 공간까지 함께 가져올 수도 있다. 할당하지 않은 공간에는 쓰레기값이 들어있다.

포인터를 다른 자료형으로 변환하면서 역참조 하려면 다음과 같이 괄호 앞에 역참조 연산자를 붙여주면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h> 
 
int main()
{
    int *numPtr = malloc(sizeof(int));   
    char *cPtr;
 
    *numPtr = 0x12345678;
 
    printf("0x%x\n"*(char *)numPtr);    
    free(numPtr);  
    return 0;
}
cs

*(char *)numPtr같이 char 포인터로 변환한 뒤 역참조 하여 1바이트 크기만큼 0x78이 출력되었다.

58.3 void포인터 변환하기

void포인터는 자료형이 정해져 있지 않으므로 역참조 연산을 할 수 없다. 하지만 void포인터를 다른 자료형으로 변환하면 역참조를 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
 
int main()
{
    int num1 = 10;
    float num2 = 3.5f;
    char c1 = 'a';
    void *ptr;
 
    ptr = &num1;   
    printf("%d\n"*(int *)ptr);    
 
    ptr = &num2;   
    printf("%f\n"*(float *)ptr);  
 
    ptr = &c1;    
    printf("%c\n"*(char *)ptr);   
 
    return 0;
}
cs

ptr은 void 포인터이기 때문에 역참조를 하면 컴파일 에러가 발생하지만 void 포인터를 다른 자료형으로 변환하여 역탐조를 하게 되면 컴파일 에러가 발생하지 않는다.

58.4 구조체 포인터 변환하기

자료형 변환을 주로 사용하는 상황은 구조체 포인터를 변환할 때 이다. struct 구조체 이름 뒤에 *을 붙여주고 괄호로 묶으면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdlib.h>    
struct Data {
    char c1;
    int num1;
};
 
int main()
{
    struct Data *d1 = malloc(sizeof(struct Data));    
    void *ptr;   
 
    d1->c1 = 'a';
    d1->num1 = 10;
 
    ptr = d1;  
 
    printf("%c\n", ((struct Data *)ptr)->c1);      
    printf("%d\n", ((struct Data *)ptr)->num1);    
 
    free(d1);    
    return 0;
}
cs

구조체 포인터 d1을 선언하고 구조체 크기만큼 메모리를 할당하여 각 멤버에 'a'와 10을 저장하였다. 이후 void ptr에 d1을 할당하였다. void포인터이기 때문에 Data 구조체의 형태를 모르는 상태 이므로 멤버에 바로 접근할 수 없기 때문에 위와 같이 ptr을 Data 구조체로 변환한 뒤 멤버에 접근해야 한다. ptr을 구조체 포인터로 변환한 뒤 멤버에 접근할 때는 자료형 변환과 포인터 전체를 다시 한 번 괄호로 묶어야 -> 연산자를 사용하여 구조체 멤버에 접근할 수 있다.

58.5 퀴즈

정답은 d 이다.

정답은 e이다.

정답은 c이다.

58.6 연습문제 : 삼각형의 넓이 구하기

base와 height가 모두 정수이므로 하나를 실수로 변환해야 한다. 정답은 다음 코드와 같다.

area = (float)base * height / 2;

58.7 포인터 변환하기

출력값이 2바이트 이므로 short 포인터로 형변환 해야 한다.

numPtr2 = (short *)nupPtr1;

58.8 연습문제 : void 포인터 변환하기

numPtr1의 크기가 uint64_t이므로 uint64_t 포인터로 변환한 뒤 역참조하여야 한다.

*(uint64_t *)ptr

58.9 연습문제 : 구조체 포인터 변환하기

정답은 다음과 같다.

((struct Person *)ptr)->name, ((struct Person *)ptr)->age

58.10 심사문제 : 소수점 이하 버리기

num1이 실수이고, 출력하는 것은 num2이고, 정수이므로 num2에 num1을 넣으면 된다.

num2 = num1;

58.11 심사문제 : 포인터 변환하기

입력받은 값을 형변환 하여 numPtr2에 넣으면 된다.

numPtr2 = (unsigned int *)numPtr1;

58.12 심사문제 : void 포인터 변환하기

void 포인터를 numPtr의 포인터형과 동일하게 변환하면 된다.

*(long double *)

58.13 심사문제 : 구조체 포인터 변환하기

void 포인터이므로 Stats 구조체 포인터로 변환하여 각 멤버에 접근해야 한다.

printf("%u\n", ((struct Stats *)ptr)->mana);
printf("%u\n", ((struct Stats *)ptr)->movementSpeed);

Unit 59. 포인터 연산 사용하기

포인터 변수에는 메모리 주소가 들어있다. 메모리 주소에 일정 숫자를 더하거나 빼면 메모리 주소가 증가, 감소한다. 포인터 연산을 통해 다른 메모리 주소에 접근할 수 있으며 메모리 주소를 손쉽게 옮겨 다닐 수 있다. 메모리 주소가 커지는 상황을 순방향으로 이동, 메모리 주소가 작아지는 상황을 역방향으로 이동이라고 한다.

59.1 포인터 연산으로 메모리 주소 조작하기

포인터 변수에 +, - 사용하여 값을 더하거나 빼고, ++, -- 연산자로 값을 증가, 감소시킬 수 있다. *, / 연산자와 실수값은 사용할 수 없다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
 
int main()
{
    int numArr[5= { 1122334455 };
    int *numPtrA;
    int *numPtrB;
    int *numPtrC;
 
    numPtrA = numArr;   
 
    numPtrB = numPtrA + 1;   
    numPtrC = numPtrA + 2;    
    
    printf("%p\n", numPtrA);   
    printf("%p\n", numPtrB);  
    printf("%p\n", numPtrC);    
 
    return 0;
}
cs

정수형 배열을 선언하고, 배열을 포인터에 할당하였다. 메모리 주소에 1씩 더하면 포인터 자료형의 크기만큼 더하거나 빼져서 위의 경우 int형 배열이였기 때문에 4씩 더해졌다.

뺄셈에서도 int형 배열이기 때문에 1을 뺄 때 마다 int형 크기인 4바이트만큼 역방향으로 이동한다.

증가 감소 연산도 1씩 더하거나 빼는 것이기 때문에 연산을 수행할 때 마다 자료형의 크기만큼 더하거나 빼진다.

59.2 포인터 연산과 역참조 사용하기

포인터 연산으로 조작한 메모리 주소도 역참조 연산으로 메모리에 접근할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
 
int main()
{
    int numArr[5= { 1122334455 };
    int *numPtrA;
    int *numPtrB;
    int *numPtrC;
 
    numPtrA = numArr;    
    numPtrB = numPtrA + 1;    
    numPtrC = numPtrA + 2;   
 
    printf("%d\n"*numPtrB);   
    printf("%d\n"*numPtrC);   
    return 0;
}
cs

numPtrB는 numPtrA에 1을 더해 4바이트 만큼 순방향 이동했고, numPtrC는 numPtrA에 2를 더해서 8바이트만큼 순방향으로 이동했다. numPtrB, numPtrC도 일반 포인터이므로 역참조 연산이 가능하고, 두 값은 결국 numArr[1], numArr[2]와 같다.

다음과 같이 포인터 연산을 한 부분을 괄호로 묶어 맨 앞에 역참조 연산자를 붙여 포인터 연산과 역참조 연산을 동시에 사용할 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
 
int main()
{
    int numArr[5= { 1122334455 };
    int *numPtrA;
 
    numPtrA = numArr;  
 
    printf("%d\n"*(numPtrA + 1));    
    printf("%d\n"*(numPtrA + 2));   
    return 0;
}
cs

만약 포인터 연산을 괄호로 묶어주지 않으면 역참조 연산자가 먼저 실행되어 값을 가져온 뒤 연산을 하게 된다.

증가연산자와 감소연산자를 사용하고 역참조 연산자를 사용할 수 도 있다. 이때 증가, 감소 연산자를 변수 뒤에 붙이고 포인터 연산을 하면 현재 메모리의 값을 가져온뒤 포인터 연산을 하기 때문에 주의해야 한다.

59.3 void 포인터로 포인터 연산하기

void 포인터는 자료형의 크기가 정해져있지 않아서 +, - 로 연산을 해도 얼마만큼 이동할지 알 수 없기에 포인터 연산을 할 수없다.

만약 void 포인터로 포인터 연산을 하고 싶다면 다른 포인터로 변환한 뒤 연산을 하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h> 
 
int main()
{
    void *ptr = malloc(100);   
 
    printf("%p\n", ptr);               
    printf("%p\n", (int *)ptr + 1);    
    printf("%p\n", (int *)ptr - 1);   
 
    free(ptr);
    return 0;
}
 
cs

void 포인터를 int 포인터로 변환하여 포인터 연산을 하였다.

void 포인터를 포인터 연산한 뒤 역참조 하려면 먼저 다른 포인터로 변환하여 포인터 연산을 하고, 연산 부분을 괄호로 묶어 맨 앞에 역참조 연산자를 붙이면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
 
int main()
{
    int numArr[5= { 1122334455 };
    void *ptr = &numArr[2];  
 
    printf("%d\n"*(int *)ptr);
   
 
    printf("%d\n"*((int *)ptr + 1));   
    printf("%d\n"*((int *)ptr - 1));    
    return 0;
}
 
cs

void 포인터 ptr을 int 포인터로 변환하여 포인터 연산 후 역참조를 하였다.

59.4 구조체 포인터로 포인터 연산하기

구조체 포인터도 포인터 연산을 할 수 있다. 포인터 연산을 한 부분을 괄호로 묶고, 화살표 연산자를 사용하여 멤버에 접근하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
 
struct Data {
    int num1;
    int num2;
};
 
int main()
{
    struct Data d[3= { { 1020 }, { 3040 }, { 5060 } };    
    struct Data *ptr;   
    ptr = d;  
 
    printf("%d %d\n", (ptr + 1)->num1, (ptr + 1)->num2);  
    printf("%d %d\n", (ptr + 2)->num1, (ptr + 2)->num2);   
    return 0;
}
cs

구조체 배열 d를 선언한 뒤 첫번째 요소의 메모리 주소를 ptr에 저장하였다. (ptr+1)->num1 과 같이 포인터 연산을 한뒤 괄호로 묶은 후 화살표 련산자를 사용하여 멤버에 접근할 수 있다. 이는 구조체 배열의 인덱스로 접근하는 d[1].num1과 같다. 구조체의 크기는 4바이트짜리 int형 멤버가 두 개 있으므로 8바이트이다. 따라서 포인터 연산을 하게 되면 8바이트씩 메모리 주소에서 더하거나 뺀다.

다음은 동적 메모리를 할당하여 포인터 연산을 하는 것이다.

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
#include <stdio.h>
#include <stdlib.h>   
#include <string.h>    
 
struct Data {
    int num1;
    int num2;
};
 
int main()
{
    void *ptr = malloc(sizeof(struct Data) * 3);   
    struct Data d[3];
 
    ((struct Data *)ptr)->num1 = 10;        
    ((struct Data *)ptr)->num2 = 20;       
 
    ((struct Data *)ptr + 1)->num1 = 30;    
    ((struct Data *)ptr + 1)->num2 = 40;    
 
    ((struct Data *)ptr + 2)->num1 = 50;    
    ((struct Data *)ptr + 2)->num2 = 60
     
    memcpy(d, ptr, sizeof(struct Data) * 3);   
    
    printf("%d %d\n", d[1].num1, d[1].num2);    
    printf("%d %d\n", ((struct Data *)ptr + 2)->num1, ((struct Data *)ptr + 2)->num2);
                                               
    free(ptr);   
    return 0;
}
cs

구조체를 포인터로 변환한 뒤 값을 더해주면 된다. 이후 포인터 연산을 하여 메모리에 값을 저장하는데, 구조체 포인터로 변환하지 않는다면 void 포인터이기 때문에 구조체 형태를 몰라서 컴파일 에러를 발생한다. 

구조체 배열의 멤버를 출력해보면 포인터 연산을 통해 저장한 값이 출력됨을 알 수 있다. 동적 메모리에 저장한 값의 위치가 구조체 배열의 형태와 같고, 동적 메모리의 내용을 그대로 복사했기 때문이다.

 

59.5 퀴즈

정답은 b,c,e,f이다.

정답은 d이다.

정답은 d, g 이다.

정답은 e 이다.

 

59.6 연습문제 : 포인터 연산으로 메모리 주소 출력하기

int 형 포인터이므로 4바이트씩 포인터 연산을 하므로 각 2와 5씩 더하면 된다. 답은 +2, +5이다.

 

59.7 연습문제 : 포인터 연산과 역참조 사용하기

정답은 다음 코드와 같다.

*(numPtrA + 2)
((int *)ptr + 1)

 

59.8 연습문제 : 구조체 포인터와 포인터 연산

정답은 다음과 같다.

(ptr + 1)->x, (ptr + 2)->y

 

59.9 심사문제 : 포인터 연산으로 메모리 주소 조작하기

short 자료형으로 2바이트 이기 때문에 각 3과 5를 더해야 한다.

numPtrB = numPtrA + 3;
numPtrC = numPtrA + 5;

 

59.10 심사문제 : 포인터 연산과 역참조 사용하기

정답은 다음 코드와 같다.

num1 = *(numPtr + 2);
num2 = *((long long *)ptr + 1);

 

59.11 심사문제 : 구조체 포인터로 포인터 연산하기

정답은 다음 코드와 같다.

result1 = ((struct Point3D *)ptr + 1) -> x;
result2 = ((struct Point3D *)ptr + 2) -> z;

 

이 페이지 요약 

1. 공용체 

2. 리틀 엔디언

3. 구조체, 공용체 함께 사용

 

Unit 54. 공용체 사용하기

공용체는 다음 그림과 같이 모든 멤버가 공간을 공유한다.

공용체는 멤버중에 가장 큰 자료형의 공간을 공유한다.

54.1 공용체를 만들고 사용하기

공용체는 union 키워드를 사용하여 정의한다. 공용체는 다음과 같이 정의하고, 변수로 선언해서 사용한다.

union 공용체이름 {
    자료형 멤버이름;
};

union 공용체이름 변수이름;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <string.h>
 
union Box{
    short candy;
    float snack;
    char doll[8];
};
 
int main()
{
    union Box b1;
    printf("%d\n"sizeof(b1));
 
    strcpy(b1.doll, "bear");
    
    printf("%d\n", b1.candy);
    printf("%f\n", b1.snack);
    printf("%s\n", b1.doll);
    return 0;
}
cs

union 키워드 뒤에 공용체 이름을 지정하고, 중괄호 안에 변수를 선언한다. 닫는 중괄호 뒤에는 반드시 세미클론을 붙여줘야 한다. 공용체는 보통 main함수 밖에 정의한다.

정의한 공용체를 사용하려면 공용체 변수를 선언해야 한다. 공용체 이름 앞에는 union 키워드를 붙이고, 공용체 이름과 공용체 변수 이름을 지정하여 선언한다. sizeof 연산자로 공용체 크기를 구하면 가장 큰 자료형의 크기가 공용체의 전체 크기로 나온다. 위 코드의 경우 char 8개 크기인 배열로 8바이트 이다.

공용체의 멤버에 접근할 때는 점을 사용한다. 공용체는 멤버중 가장 큰 자료형의 공간을 공유하기 때문에 어느 한 멤버에 값을 저장하면 나머지 멤버의 값은 사용할 수 없는 상태가 된다. 공용체의 멤버는 한 번에 하나씩 값을 쓰면 정상적으로 사용할 수 있다.

실무에서는 공용체에 값을 저장할 때 어떤 멤버를 사용할 지 미리 정해두고 꺼낼때도 정해둔 멤버에서 값을 꺼내는 식으로 사용한다. 여러 멤버에 동시에 접근하지 않는 경우 메모리 레이아웃에 멤버를 모아둘 때 사용한다. 임베디드 시스템이나 커널 모드 디바이스 드라이버 등에서 주로 사용하며 보통은 거의 쓰지 않는다.

54.2 공용체와 엔디언

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
 
union Data {
    char c1;
    short num1;
    int num2;
};
 
int main()
{
    union Data d1;
 
    d1.num2 = 0x12345678;
 
    printf("0x%x\n", d1.num2);
    printf("0x%x\n", d1.num1);
    printf("0x%x\n", d1.c1);
    printf("%d\n"sizeof(d1));
    return 0;
}
cs

d1의 멤버중 가장 큰 자료형인 num2에 0x12345678을 할당하고 다른 멤버들을 출력해보면 d1.num2는 저장한 숫자가 그대로 나오지만, 다른 멤버는 숫자중 일부만 나온다. 공용체는 값을 저장하는 공간은 공유하지만 가져올 때는 해당 자료형의 크기만큼만 가져오기 때문이다. 우리가 사용하는 CPU는 리틀 엔디언 방식으로 값을 메모리에 저장한다. 리틀 엔디언은 숫자를 1바이트씩 쪼개서 낮은 자릿수가 앞에 오도록 한다. 따라서 0x12345678을 리틀 엔디언 방식으로 메모리에 저장하면 78 56 34 12가 된다. 공용체는 앞에서부터 자료형의 크기만큼 값을 가져오므로 d1.num1은 앞의 2바이트 56 78을 가져오고, d1.c1은 앞의 1바이트인 78만 가져온다.

공용체도 구조체처럼 typedef로 별칭을 지정하고 별칭으로 선언하거나, 익명 공용체를 정의할 수 있다. 또한 다음과 같이 정의하는 동시에 변수를 선언할 수 있다.

union 공용체이름 {
    자료형 멤버이름;
} 변수;

54.3 공용체 포인터를 선언하고 메모리 할당하기

공용체도 포인터를 선언하고, malloc함수를 사용하여 동적 메모리를 할당할 수 있다.

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 <stdio.h>
#include <stdlib.h>
#include <string.h>
 
union Box{
    short candy;
    float snack;
    char doll[8];
};
 
int main()
{
    union Box *b1 = malloc(sizeof(union Box));
 
    printf("%d\n"sizeof(union Box));
 
    strcpy(b1->doll, "bear");
 
    printf("%d\n", b1->candy);
    printf("%f\n", b1->snack);
    printf("%s\n", b1->doll);
 
    free(b1);
    return 0;
}
cs

malloc함수로 메모리를 할당할 때 크기를 알아야 하므로 sizeof(union Box)처럼 공용체의 크기를 구할 수 있다. 공용체 포인터도 멤버에 접근할 때는 화살표 연산자를 사용한다. strcpy함수로 공용체 포인터의 멤버에 접근한 뒤 문자열을 복사하고, 공용체 포인터도 멤버중 가장 큰 자료형의 공간을 공유함을 출력값을 통해 확인할 수 있다. 할당한 메모리는 사용이 끝났으면 꼭 free로 해제해줘야 한다.

54.4 퀴즈

정답은 long long int 자료형의 크기인 8 이다.

정답은 d이다.

정답은 0x1111 이다.

54.5 연습문제 : 정수 데이터 공용체 정의하기

정답은 다음 코드와 같다.

// 1.
union Data{
    char c1;
    short num1;
};

// 2. 
union Data

// 3.
d1.num1 = 0x5678;

0x5678 은 2바이트 이므로 short, 0x78은 1바이트이므로 char로 선언하고, 리틀 엔디언으로 값이 저장되기 때문에 0x5678을 num1멤버에 넣으면 위와 같은 결과가 나온다.

54.6 연습문제 : 공용체 포인터 사용하기

화살표 연산자를 사용했으므로 포인터를 선언하여 메모리를 할당해야 하기 때문에 정답은 union Data *d1 = malloc(sizeof(union Data)); 이다.

54.7 심사문제 : 정수 데이터 공용체 정의하기

정답은 다음 코드와 같다.

union Data{
    char c1;
    int num1;
};

공용체의 크기가 4가 나오므로 4바이트 크기를 갖는 자료형이 사용되야 한다.

54.8 심사문제 : 공용체 포인터 사용하기

정답은 다음 코드와 같다.

union Data *d1 = malloc(sizeof(union Data));
d1->num2 = 0x11111111;

화살표 연산자가 사용됬기 때문에 포인터를 선언한 후 메모리를 할당해야 한다. 공용체는 가장 큰 자료형의 공간을 공유하므로 가장 큰 멤버에만 0x111111111을 넣으면 된다.

Unit 55. 구조체와 공용체 활용하기

구조체 안에 구조체와 공용체가 들어갈 수 있고, 반대로 공용체 안에 구조체와 공용체가 들어갈 수도 있다.

55.1 구조체 안에서 구조체 멤버 사용하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
 
struct Phone {
    int areacode;
    unsigned long long num;
};
 
struct Person {
    char name[20];
    int age;
    struct Phone phone;
};
 
int main()
{
    struct Person p1;
 
    p1.phone.areacode = 82;
    p1.phone.num = 12345678;
 
    printf("%d %llu\n", p1.phone.areacode, p1.phone.num);
    return 0;
}
cs

구조체를 멤버로 가지려면 구조체 안에서 구조체 변수를 선언하면 된다. 위 코드에서는 Person 구조체가 Phone 구조체를 멤버로 가지고 있다. 멤버에 접근할 때는 점을 사용하여 가지고 있는 구조체에 계층적으로 접근하면 된다. 위 코드와 같이 점을 두 번 사용하면 구조체의 areacode와 num 멤버에 접근할 수 있다.

구조체 안에 구조체를 정의할 때는 무조건 안에 들어가는 구조체를 먼저 선언해줘야 한다.

만약 Phone구조체를 다른 곳에서는 쓰지 않고 특정 구조체 안에서만 쓴다면 다음과 같이 구조체 안에 구조체를 정의하는게 더 편리하다. 이때 안에 정의하는 구조체는 정의하고 반드시 변수를 선언해 줘야 한다.

struct Person {   
    char name[20];    
    int age;          
    struct Phone {   
        int areacode;                 
        unsigned long long num;    
    } phone;                       
};

구조체 변수를 선언하며 안에 있는 구조체까지 초기화 하려면 중괄호 안에 중괄호를 사용하여 다음과 같이 초기화 하면 된다.

struct 구조체이름 변수이름 = { 값1, 값2, { 값3, 값4 } };

55.2 구조체 안의 구조체 멤버에 메모리 할당하기

다음은 구조체 안에 구조체 멤버가 변수로 있는 상태에서 메모리를 할당하여 사용하는 방법이다.

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>
#include <stdlib.h>
 
struct Phone {
    int areacode;
    unsigned long long num;
};
 
struct Person {
    char name[20];
    int age;
    struct Phone phone;
};
 
int main()
{
    struct Person *p1 = malloc(sizeof(struct Person));
 
    p1->phone.areacode = 82;
    p1->phone.num = 12345678;
 
    printf("%d %llu\n", p1->phone.areacode, p1->phone.num);
 
    free(p1);
    return 0;
}
cs

Person 구조체에 메모리를 할당하면 각 구조체의 멤버에 접근하려면 p1은 포인터이기 때문에 화살표 연산자를 사용하여 멤버에 접근하고, phone은 포인터가 아닌 일반 변수이므로 점을 사용하여 멤버에 접근한다. 구조체 포인터 사용이 끝나면 free함수로 할당된 메모리를 해제해야 한다.

다음은 구조체의 포인터를 멤버로 갖는 상황이다.

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
#include <stdio.h>
#include <stdlib.h>
 
struct Phone {
    int areacode;
    unsigned long long num;
};
 
struct Person {
    char name[20];
    int age;
    struct Phone *phone;
};
 
int main()
{
    struct Person *p1 = malloc(sizeof(struct Person));
    p1->phone = malloc(sizeof(struct Phone));
 
    p1->phone->areacode = 82;
    p1->phone->num = 12345678;
 
    printf("%d %llu\n", p1->phone->areacode, p1->phone->num);
 
    free(p1->phone);
    free(p1);
    return 0;
}
cs

Person 구조체 안에는 구조체 포인터를 멤버로 갖고 있다. 먼저 바깥 구조체의 포인터에 메모리를 할당하고 멤버로 있는 구조체 포인터에 메모리를 할당해야 한다. 각 멤버에 접근하려면 p1은 포인터이고, 구조체 멤버 phone도 포인터이므로 화살표 연산자로 접근하면 된다. 구조체 포인터 사용이 끝나면 메모리를 해제해야 하는데, 안쪽에 있는 멤버부터 메모리를 해제해야 한다. 만약 바깥에 있는 구조체를 먼제 해제하게 되면 데이터가 사라져서 멤버에 더이상 접근할 수 없다. 멤버 포인터에 저장된 주소도 알 수 없기 때문에 해제도 할 수 없다. 그래서 ㅇㄴ쪽의 구조체 먼제 해제해야 한다.

55.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
#include <stdio.h>
 
struct Vector3 {
    union {
        struct {
            float x;
            float y;
            float z;
        };
        float v[3];
    };
};
 
int main()
{
    struct Vector3 pos;
 
    for(int i = 0; i < 3; i++)
    {
        pos.v[i] = 1.0f;
    }
 
    printf("%f %f %f\n", pos.x, pos.y, pos.z);
    return 0;
}
cs

제일 안에 있는 x, y, z는 float로 선언되어 있는 익명 구조체 이다. 익명 공용체가 x, y, z익명 구조체오 배열 v를 감싸고 있다. float x, y, z는 변수 3개이고, float v[3]도 배열의 요소가 3개이므로 자료형도 같고, 개수도 같으므로 같은 공간을 차지한다. 공용체로 묶어주면 x, y, z와 v[3]은 같은 공간을 공유하게 된다.

v는 배열이기 때문에 인덱스로 접근하여 값을 할당할 수 있다. 공용체에 넣었으므로 값은 같은 공간에 있기 때문에 x,y,z 멤버로도 접근할 수 있다.

 

55.4 퀴즈

정답은 d 이다.

정답은 c 이다.

정답은 b이다.

정답은 d 이다.

 

55.5 연습문제 : 게임 캐릭터 구조체 만들기

정답은 struct Stats stats 이다.

 

55.6 연습문제 : 게임 캐릭터 구조체 사용하기

화살표 연산자로 접근하고 있기 때문에 포인터로 만들어서 메모리를 할당시켜줘야 하므로 정답은 struct Champion *lux = malloc(sizeof(struct Champion)); 이다.

 

55.7 연습문제 : 장치 옵션 구조체 만들기

정답은 다음 코드와 같다.

union {
    unsigned long long option;
    struct {
        unsigned char boot[4];
        unsinged char interrupt[2];
        unsigned char bus[2];
    };
};

사용하는 공간이 총 8 바이트이기 때문에 공간을 공유하는 공용체를 8바이트로 만들면 코드에서 저장한 값들을 다 저장할 수 있다.

 

55.8 심사문제 : 게임 캐릭터 구조체 만들기

 

정답은 다음 코드와 같다.

struct Champion swain;

strcpy(swain.name, "Swain");
swain.stats.health = 463;

 

55.9 심사문제 : 게임 캐릭터 구조체 포인터 사용하기

정답은 다음 코드와 같다.

struct Champion *swain = malloc(sizeof(struct Champion));
swain->stats = malloc(sizeof(struct Stats));

strcpy(swain->name, "Swain");
swain->stats->health = 463;

 

55.10 심사문제 : 장치 옵션 구조체 만들기

정답은 다음 코드와 같다.

struct DeviceOption {
    union{
        unsigned short option;
        struct{
            unsigned char boot;
            unsigned char interrupt;
        };
    };
};

공용체는 모든 멤버의 공간을 공유하기 때문에 unsigned char이 두개 쓰였으므로 이 두개 모두 들어갈 수 있는 unsigned short 자료형을 사용하였다.

'Project H4C Study Group' 카테고리의 다른 글

[Project H4C] FTZ (8 ~11)  (0) 2021.03.27
[Project H4C] FTZ (1 ~ 7(문제오류))  (0) 2021.03.24
[Project H4C] C언어 코딩도장(13)  (0) 2021.03.23
[Project H4C] PLT, GOT  (0) 2021.03.21
[Project H4C] SFP, RET  (0) 2021.03.21

이 페이지 요약

1. 구조체 멤버 정렬

2. 구조체 배열

3. 구조체 포인터 배열

 

Unit 51. 구조체 멤버 정렬 사용하기

구조체가 메모리에 올라갔을 때 멤버를 정렬하는 기능이다.

컴퓨터는 CPU가 메모리에 접근할 때 32비트는 4바이트 단위, 64비트는 8바이트 단위로 접근한다. 만약 32비트 CPU에서 4바이트보다 작은 데이터에 접근하게 되면 내부적으로 시프트 연산이 발생하여 효율이 떨어지기 때문에 C컴파일러에서 CPU가 메모리의 데이터에 효율적으로 접근할 수 있도록 구조체를 일정한 크기로 정렬한다. 구조체 크기가 15나 17바이트가 되면 접근 효율이 떨어지기 떼문에 2, 4, 8, 16바이트 단위로 정렬한다.

구조체의 멤버를 정렬하면 안되는 경우도 있다. 사진파일의 경우 정렬하게 되면 사진을 저장할 때 마다 조금씩 깨질 수 있다. 또한 네트워크로 데이터를 전송할 경우 몇 바이트씩 어떤 순서로 보낼지 규약을 정해놓았는데, 이때 정렬이 발생하면 정해놓은 규약에서 벗어나 받는 쪽에서 데이터를 알아볼 수 없게 된다.

51.1 구조체 크기 알아보기

구조체 전체 크기는 sizeof 연산자를 사용하여 알 수 있다. sizeof 연산자는 다음과 같이 사용할 수 있다.

  • sizeof(struct 구조체)
  • sizeof(구조체별칭)
  • sizeof(구조체변수)
  • sizeof 구조체변수

다음은 가상의 네트워크 구조체 PacketHeader를 정의하여 멤버의 크기와 구조체의 크기를 구한 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
 
struct PacketHeader {
    char flags;
    int seq;
};
 
int main()
{
    struct PacketHeader header;
 
    printf("%d\n"sizeof(header.flags));
    printf("%d\n"sizeof(header.seq));
    printf("%d\n"sizeof(header));
    printf("%d\n"sizeof(struct PacketHeader));
 
    return 0;
}
cs

구조체의 크기를 구하려면 sizeof 연산자 안에 변수나 struct 키워드와 구조체 이름을 넣어주면 된다. PacketHeader 구조체 안에는 1바이트 크기의 char 변수와 4바이트 크기의 int 변수가 들어있어 전체 크기는 5가 나와야 할 것 같지만 8이 나왔다. C에서 구조체들을 정렬할 때는 멤버중에서 가장 큰 자료형 크기의 배수로 정렬한다. 위 구조체에서는 int가 가장 큰 자료형이므로 느끼는 4바이트를 기준으로 정렬하여 구조체 전체 크기는 8바이트가 되었다. 1바이트 크기인 char flags 뒤에는 4바이트를 맞추기 위해 남는 공간에 3바이트가 더 들어가게 되는데 이렇게 구조체를 정렬할 때 남는 공간을 채우는 것을 패딩이라고 한다.

다음 코드를 이용하여 구조체를 정렬한 뒤 멤버의 위치가 위 그림처럼 되는지 확인할 수 있다. 구조체에서 멤버의 위치(offset)를 구할 때는 offsetof 매크로를 사용하여 구할 수 있다. offsetof매크로는 stddef.h 헤더파일에 정의되어 있다.

  • offsetof(struct 구조체, 멤버)
  • offsetof(구조체별칭, 멤버)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include <stdio.h>
    #include <stddef.h>
     
    struct PacketHeader {
        char flags;
        int seq;
    };
     
    int main()
    {  
        printf("%d\n", offsetof(struct PacketHeader, flags));
        printf("%d\n", offsetof(struct PacketHeader, seq));
        
        return 0;
    }
    c

offsetof 매크로에 구조체와 멤버를 지정하면 구조체에 해당 멤버의 상대 위치가 반환된다. 첫 멤버의 위치는 0이고 4바이트 단위로 정렬하므로 seq의 위치는 4가 나온다.

51.2 구조체 정렬 크기 조절하기

각 컴파일러에서 제공하는 특별한 지시자를 사용하여 구조체 정렬 크기를 조절할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
 
#pragma pack(push, 1)   // 1바이트 크기로 정렬
struct PacketHeader {
    char flags;
    int seq;
};
#pragma pack(pop)       // 정렬 상태를 이전 상태(기본값)로 되돌림
 
int main()
{
    struct PacketHeader header;
 
    printf("%d\n"sizeof(header.flags));
    printf("%d\n"sizeof(header.seq));
    printf("%d\n"sizeof(header));
 
    return 0;
}
cs

구조체를 정의할 때 위 아래로 #pragma pack(push, 1)과 #pragma pack(pop)을 넣었다. pack을 1로 설정하면 1바이트 단위로 정렬하여 남는 공간 없이 자료형 크기 그대로 메모리에 올라간다. #pragma pack(push, 1)을 한 번 사용하면 이후 모든 구조체에 영향을 주므로 정렬 한 뒤 #pragma pack(pop)를 사용하여 설정을 이전 상태로 되돌린다. 만약gcc버전이 4.0미만이면 __attribute__((aligned(1), packed))를 사용한다.
offsetof로 멤버의 위치를 확인하면 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stddef.h>
 
#pragma pack(push, 1)  
struct PacketHeader {
    char flags;
    int seq;
};
#pragma pack(pop
 
int main()
{
    printf("%d\n", offsetof(struct PacketHeader, flags));
    printf("%d\n", offsetof(struct PacketHeader, seq));
 
    return 0;
}
cs

구조체를 1바이트 단위로 정렬해서 seq의 상대 위치는 1이 나온다. char flags 바로 뒤에 int seq가 오기 때문이다. 구조체의 바이트 크기는 1 이외에도 2, 4, 8, 16등으로 지정할 수 있다.

51.3 퀴즈

정답은 b,d,e 이다.

정답은 e 이다.

정답은 d이다.

정답은 e이다.

51.4 연습문제 : 압축 헤더 크기 구하기

8이 나오려면 구조체의 전체 크기를 구해야 하므로 정답은 sizeof(header)이다.

51.5 연습문제 : 패킷 크기 조절하기

6이 출력되려면 legth의 자료형 크기가 2가 되어야 하므로 정답은 short이다.

51.5 심사문제 : 암호화 헤더 크기 구하기

12가 출력되야 하므로 가장 큰 자료형의 크기는 4가 되도록 하고, 구조체 멤버를 두개 만들면 된다.

int num1;
int num2;

51.7 심사문제 : 패킷 크기 조절하기

3이 출력되야 하므로 구조체 정렬 크기를 자료형에 맞게 조절하고, 1바이트인 char 자료형, 2 바이트인 short 자료형으로 멤버를 하나씩 만들면 된다.

#pragma pack(push,1)
struct Packet{
    char c;
    short s;
};
#pragma pack(pop)

Unit 52. 구조체와 메모리 활용하기

구조체도 변수를 선언하거나 메모리를 할당하게 되면 메모리 공간을 차지하므로 메모리 관련 함수를 사용할 수 있다.

52.1 구조체와 메모리를 간단하게 0으로 설정하기

구조체의 모든 멤버를 0으로 만들기 위해 각 멤버에 접근하여 0을 저장하는 것은 매우 번거로운 작업이다. 따라서 {0, }과 같이 중괄호를 사용하여 모두 0으로 초기화 할 수 있다. 이 방법은 변수에만 사용할 수 있고, malloc 함수로 할당한 메모리에는 사용할 수 없다.

struct 구조체이름 변수이름 = { 0, };

구조체 변수나 메모리의 내용에 한꺼번에 값을 설정하려면 다음과 같이 memset함수를 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <string.h>
 
struct Point2D{
    int x;
    int y;
};
 
int main()
{
    struct Point2D p1;
    
    memset(&p1, 0sizeof(struct Point2D));
    
    printf("%d %d\n", p1.x, p1.y);
    return 0;
}
cs

memset함수로 구조체 변수의 값을 설정할 때는 &p1과 같이 주소연산자 &을 사용하야 메모리 주소를 구해서 넣어줘야 한다. 그리고 설정할 값과 크기를 넣어준다. 위 코드에서는 구조체의 내용을 0으로 설정하고 Point2D구조체 크기만큼 설정했다. 그리고 각 멤버를 출력해보면 모두 설정한 0이 나온다.
다음과 같이 malloc함수로 설정한 동적 메모리에도 값을 설정할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
 
struct Point2D{
    int x;
    int y;
};
 
int main()
{
    struct Point2D *p1 = malloc(sizeof(struct Point2D));
    
    memset(p1, 0sizeof(struct Point2D));
 
    printf("%d %d\n", p1->x, p1->y);
 
    free(p1);
    return 0;
}
cs

memset 함수로 메모리를 설정할 때 구조체가 포인터 변수이기 때문에 &을 사용하지 않고 그대로 넣어준다. 설정할 값과 크기를 넣어주고 각 멤버를 출력해보면 모두 0이 나온다.

52.2 구조체와 메모리 복사하기

내용이 같은 구조체를 만들거나 이미 생성하여 값을 저장한 구조체나 메모리를 다른 곳에 복사할 경우 memcpy 함수를 사용하여 메모리의 내용을 다른 곳에 복사할 수 있다. string.h 헤더파일에 선언되어 있다. memcpy(목적지포인터, 원본포인터, 크기); 의 형태로 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <string.h>
 
struct Point2D{
    int x;
    int y;
};
 
int main()
{
    struct Point2D p1;
    struct Point2D p2;
 
    p1.x = 10;
    p1.y = 20;
 
    memcpy(&p2, &p1, sizeof(struct Point2D));
 
    printf("%d %d\n", p2.x, p2.y);
    return 0;
}
cs

구조체 변수 p1, p2를 선언하고 p1의 멤버에만 값을 저장한 상태에서 memcpy 함수를 사용하여 p1의 내용을 p2에 복사하였다. memcpy함수를 사용할 때는 구조체 변수에 주소 연산자를 사용하여 변수의 메모리 주소를 넣고, 크기는 구조체의 크기를 넣어준다. 목적지 포인터와 원본포인터는 앞쪽이 목적지 포인터이고, 뒷쪽이 원본 포인터이다. p2의 각 멤버를 출력해보면 p1의 멤버에 저장했던 값이 나온다.
다음은 malloc함수로 할당한 동적 메모리끼리 복사하는 방법이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
 
struct Point2D{
    int x;
    int y;
};
 
int main()
{
    struct Point2D *p1 = malloc(sizeof(struct Point2D));
    struct Point2D *p2 = malloc(sizeof(struct Point2D));
 
    p1->= 10;
    p1->= 20;
    memcpy(p2, p1, sizeof(struct Point2D));
 
    printf("%d %d\n", p2->x, p2->y);
 
    free(p2);
    free(p1);
    return 0;
}
cs

구조체 포인터 p1, p2를 선언한 뒤 메모리를 할당하여 p1의 멤버에 값을 저장하고 , p2에는 memcpy함수를 사용하여 p1의 내용을 p2에 복사했다. p1과 p2는 메모리를 담고 있는 포인터이므로 주소연산자는 사용하지 않는다.

또한 구조체 변수에서 동적메모리로, 동적메모리에서 구조체 변수로도 내용을 복사할 수 있다.

52.3 퀴즈

정답은 b이다.

정답은 a이다.

52.4 연습문제 : 2차원 좌표 초기화하기

정답은 다음과 같다.

1. #include <string.h>

2. &p, 0, sizeof(struct Point2D)

3. ptr, 0, sizeof(struct Point2D)

52.5 연습문제 : 2차원 좌표 복제하기

p1의 멤버에만 값을 저장했기 때문에 p1의 값을 p2복제해야 하는데 p2는 메모리 주소이므로 정답은 p2, &p1, sizeof(struct Point2D) 이다.

52.6 심사문제 : 인적 정보 삭제하기

출력했을 때 아무 정보도 나오지 않도록 하려면 모든 멤버를 0으로 초기화해야 하므로 정답은 &p1, 0, sizeof(struct Person)이다.

52.7 심사문제 : 인적 정보 복제하기

p2는 구조체 변수이므로 주소 연산자를 사용해서 p1의 값을 복사해와야 한다. 정답은 &p2, p1, sizeof(struct Person) 이다.

Unit 53. 구조체 배열 사용하기

구조체 변수를 여러개 만들어야 할 경우 구조체 배열을 선언하여 여러개의 구조체 변수를 한 번에 만들 수 있다.

53.1 구조체 배열 선언하기

구조체 배열은 변수 이름 뒤에 [](대괄호)를 붙인 뒤 크기를 설정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
 
struct Point2D{
    int x;
    int y;
};
 
int main()
{
    struct Point2D p[3];
 
    p[0].x = 10;
    p[0].y = 20;
    p[1].x = 30;
    p[1].y = 40;
    p[2].x = 50;
    p[2].y = 60;
 
    printf("%d %d\n", p[0].x, p[0].y);
    printf("%d %d\n", p[1].x, p[1].y);
    printf("%d %d\n", p[2].x, p[2].y);
    return 0;
}
cs

구조체 변수를 선언할 때 대괄호 안에 크기를 넣어주면 배열로 선언할 수 있다. 배열에서 각 요소에 접근하려면 배열 뒤에 대괄호를 사용하며 대괄호 안에 인덱스를 지정해주면 된다. 이 상태에서 멤버에 접근하려면 점을 사용한다. p[0].x 구조체 배열의 첫 번째 요소의 멤버 x에 접근한다는 뜻이다.
구조체 배열을 선언하는 동시에 초기화하려면 다음과 같이 중괄호 안에 중괄호를 사용하면 된다.

  • struct구조체이름 변수이름[크기] = { { .멤버이름1 = 값1, .멤버이름2 = 값2 }, { .멤버이름1 = 값3, .멤버이름2 = 값4 } };
  • struct구조체이름 변수이름[크기] = { { 값1, 값2 }, { 값3, 값4 } };

모든 요소의 멤버를 0으로 초기화 하여면 구조체 배열을 선언하며 {0, }을 할당하면 된다.

53.2 구조체 포인터 배열 선언하기

구조체 포인터 배열을 만들고 malloc함수로 각 요소에 메모리를 할당하면 요소마다 메모리를 할당할 수 있다. 구조체 포인터 배열은 포인터 변수 이름 뒤에 대괄호를 붙인 뒤 크기를 설정한다.

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>
#include <stdlib.h>
 
struct Point2D{
    int x;
    int y;
};
 
int main()
{
    struct Point2D *p[3];
 
    for(int i  = 0; i < sizeof(p) / sizeof(struct Point2d *); i++)
    {
        p[i] = malloc(sizeof(struct Point2D));
    }
 
    p[0]->= 10;
    p[0]->= 20;
    p[1]->= 30;
    p[1]->= 40;
    p[2]->= 50;
    p[2]->= 60;
 
    printf("%d %d\n", p[0]->x, p[0]->y);
    printf("%d %d\n", p[1]->x, p[1]->y);
    printf("%d %d\n", p[2]->x, p[2]->y);
 
    for(int i  = 0; i < sizeof(p) / sizeof(struct Point2d *); i++)
    {
        free(p[i]);
    }
    return 0;
}
cs

구조체 포인터를 선언하고 사용하려면 메모리를 할당해야 한다. 배열의 요소 개수 만큼 반복하면서 각 요소에 구조체 크기 만큼 메모리를 할당해야 한다. 구조체 포인터 배열에는 포인터가 들어있으므로 요소 개수를 구하려면 구조체 포인터 배열의 전체 크기에서 구조체 포인터의 크기로 나눠주면 된다. sizeof(struct Point2D *)는 구조체 포인터의 크기이다. 멤버에 접근할 때는 배열안에 들어있는 요소가 포인터이므로 화살표 연산자를 사용하여 멤버에 접근해야 한다. 구조체 배열의 사용이 끝나면 배열 크기만큼 반복하며 각 요소에 할당된 동적 메모리를 해제해야 한다.

53.3 퀴즈

정답은 b이다.

정답은 b이다.

정답은 d이다.

정답은 f이다.

53.4 연습문제 : 2차원 좌표 출력하기

정답은 다음 코드와 같다.

// 1
struct Point2D p[3];

// 2
for(int i = 0; i < sizeof(p) / sizeof(struct Point2D); i++)
{
	printf("%d %d\n", p[i].x, p[i].y);
}

구조체 배열의 모든 요소를 출력할 때 0번 인덱스 부터 마지막 인덱스까지 for 문을 이용하여  반복하여 출력할 수 있다.

 

53.5 연습문제 : 인적 정보를 초기화하기

정답은 다음 코드와 같다.

// 1.
struct Person *p[3000];

// 2.
for(int i = 0; i < sizeof(p) / sizeof(struct Person *); i++)
{
	p[i] = malloc(sizeof(struct Person));
    memset(p[i], 0, sizeof(struct Person));
}

구조체 포인터 배열이므로 첫번째 인덱스 부터 마지막 인덱스까지 메모리를 할당해주고, 메모리를 0으로 초기화 해야한다.

 

 53.6 심사문제 : 선의 길이 구하기

정답은 다음 코드와 같다.

for(int i=0; i<(sizeof(p) / sizeof(struct Point2D))-1; i++)
{
    double len_x = p[i+1].x - p[i].x;
    double len_y = p[i+1].y - p[i].y;
    length += sqrt((len_x)*(len_x) + (len_y)*(len_y));
}

두 점 사이의 거리를 구하는 공식을 이용해서 거리를 구했고, math.h 헤더의 sqrt 함수를 이용하여 제곱근을 구했다.

 

53.7 심사문제 : 나이가 가장 많은 사람 찾기

정답은 다음 코드와 같다.

int big_age = 0;

for (int i = 0; i < sizeof(p) / sizeof(struct Person *); i++)
{
    p[i] = malloc(sizeof(struct Person));
    scanf("%s %d", p[i]->name, &p[i]->age);
    if(p[i]->age >= p[big_age]->age)
    {
        big_age = i;
    }
}

printf("%s\n", p[big_age]->name);

가장 큰 나이의 인덱스를 저장할 변수를 만들고, 배열 길이만큼 반복하면서 메모리를 할당하고, 할당한 메모리에 값을 입력받는다. 입력받은 나이가 저장되어있던 인덱스의 나이보다 크면 현재 인덱스를 저장한다. 반복문이 끝나면 저장된 인덱스에 해당한는 이름을 출력한다.

'Project H4C Study Group' 카테고리의 다른 글

[Project H4C] FTZ (1 ~ 7(문제오류))  (0) 2021.03.24
[Project H4C] C언어 코딩도장(14)  (0) 2021.03.23
[Project H4C] PLT, GOT  (0) 2021.03.21
[Project H4C] SFP, RET  (0) 2021.03.21
[Project H4C] Buffer Overflow  (0) 2021.03.21

이 페이지 요약 

코딩도장 Unit 48 ~ 50

1. 구조체, 구조체에서 포인터 사용

2. 두 점 사이의 거리 구하기(math.h 헤더 파일의 함수들)

 

Unit 48. 구조체 사용하기

인적 정보를 처리할 경우 이름, 나이 , 주소 등을 저장할 변수가 필요하다. 각각 변수를 만들어 저장할 경우 한 사람의 정보만 저장할 수 있고, 여러명의 정보를 저장하려면 변수를 계속 만들어야 하므로 비효율 적이다.

구조체는 struct 키워드로 정의 하며 다음과 같이 사용할 수 있다.

struct Person {
    char name[20];        // 이름
    int age;              // 나이
    char address[100];    // 주소
};

이름, 나이, 주소 정보가 Person 이라는 구조체에 들어가 사람 단위로 정보를 처리할 수 있다.

구조체는 관련 정보를 하나의 의미로 묶을 때 사용한다. 목적에 맞는 자료형을 만들어서 사용하는데 기본 자료형을 조합하여 만든 자료형을 파생형이라 한다.

48.1 구조체를 만들고 사용하기

구조체는 struct 키워드로 정의한다. 정의 한 후 변수로 선언하여 사용한다.

다음은 인적 정보를 표현하는 구조체를 만들고 사용한 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <string.h>
 
struct Person {
    char name[20];
    int age;
    int address[100];
};
 
int main()
{
    struct Person p1;
    
    strcpy(p1.name, "홍길동");
    p1.age = 30;
    strcpy(p1.address, "서울시 용산구");
 
    printf("이름 : %s\n", p1.name);
    printf("나이 : %d\n", p1.age);
    printf("주소 : %s\n", p1.address);
 
    return 0;
}
cs

struct 키워드 뒤에 구조체 이름을 지정하고 중괄호 안에 변수를 선언한다. 구조체 안에 들어있는 변수를 멤버라고 부른다. 구조체를 정의할 때는 }(닫는 중괄호) 뒤에는 반드시 ; (세미클론)을 붙여줘야 한다. 구조체는 보통 main 함수 바깥에 정의한다. 함수 안에 구조체를 정의하면 해당 함수 안에서만 구조체를 사용할 수 있다.

정의한 구조체를 사용하려면 구조체 변수를 선언해야 하며 구조체 이름 앞에 struct 키워드를 붙여줘야 한다.

구조체 멤버에 접근할 때는 .(점)을 사용한다. p1.age = 30; 과 같이 구조체 멤버에 접근한 뒤 값을 할당하고 값을 가져온다. 문자열 멤버는 할당연산자로 저장할 수 없으므로 strcpy함수를 사용하면 된다.

다음과 같이 중괄호와 세미클론 사이에 변수를 지정하면 구조체를 정의하는 동시에 변수를 선언할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <string.h>
 
struct Person {
    char name[20];
    int age;
    int address[100];
} p1;
 
int main()
{   
    strcpy(p1.name, "홍길동");
    p1.age = 30;
    strcpy(p1.address, "서울시 용산구");
 
    printf("이름 : %s\n", p1.name);
    printf("나이 : %d\n", p1.age);
    printf("주소 : %s\n", p1.address);
 
    return 0;
}
cs

구조체를 정의하면서 닫는 중괄호와 세미클론 사이에 변수를 지정하면 구조체를 정의하는 동시에 변수가 선언된다. 이와 같이 선언된 변수 p1은 main함수 바깥에 선언되어 있으며 전역변수이다.

구조체 변수를 선언하는 동시에 값을 초기화 하려면 중괄호 안에 .(점)과 멤버 이름을 적고 값을 할당한다. 또한 멤버 이름과 할당 연산자 없이 값만 콤마로 구분하여 나열해도 되는데, 이러한 경우 처음부터 순서대로 값을 채워야 하며 중간에 있는 멤버만 값을 할당하거나 생략할 수 는 없다.

  • struct 구조체이름 변수이름 = { .멤버이름1 = 값1, .멤버이름2 = 값2 };
  • struct 구조체이름 변수이름 = {값1, 값2};

48.2 typedef로 struct 키워드 없이 구조체 선언하기

typedef로 구조체를 정의하며 별칭(alias)을 지정할 수 있다.

구조체 이름과 구조체 별칭은 겹쳐도 되지만 구분하기 위해 구조체 이름은 앞에 _을 붙이겠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <string.h>
 
typedef struct Person_ {
    char name[20];
    int age;
    int address[100];
} Person;
 
int main()
{
    Person p1;
    
    strcpy(p1.name, "홍길동");
    p1.age = 30;
    strcpy(p1.address, "서울시 용산구");
 
    printf("이름 : %s\n", p1.name);
    printf("나이 : %d\n", p1.age);
    printf("주소 : %s\n", p1.address);
 
    return 0;
}
cs

구조체를 정의할 땐 맨 앞에 typedef를 붙이고, 구조체를 정의한다. 그리고 닫는 중괄호와 세미클론 사이에 구조체 별칭을 지정하면 된다.

구조체 변수를 선언할 때는 struct 키워드를 생략하고 구조체 별칭으로 바로 변수를 선언할 수 있다. 구조체 별칭으로 선언한 변수도 멤버에 접근할 때는 점을 사용한다.

struct 뒤에 붙는 구조체 이름은 원래 태그(tag)라 부른다.

48.3 익명 구조체 사용하기

typedef로 구조체를 정의하면서 이름을 생략할 수 있다. 변수는 구조체 별칭으로 선언하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <string.h>
 
typedef struct{
    char name[20];
    int age;
    int address[100];
} Person;
 
int main()
{
    Person p1;
    
    strcpy(p1.name, "홍길동");
    p1.age = 30;
    strcpy(p1.address, "서울시 용산구");
 
    printf("이름 : %s\n", p1.name);
    printf("나이 : %d\n", p1.age);
    printf("주소 : %s\n", p1.address);
 
    return 0;
}
cs

typedef struct 뒤에 이름을 지정하지 않고 바로 여는 중괄호로 시작하면 된다.

이때는 반드시 구조체 별칭을 지정해야 한다.

구조체 변수는 구조체 별칭으로 선언하고, 멤버에 접근할 때는 점으로 접근한다.

48.4 퀴즈

정답은 c이다.

정답은 c이다.

정답은 d 이다.

정답은 익명 구조체 이다.

48.5 연습문제 : 좌표 구조체 정의하기

정답은 다음 코드와 같다.

// 1.
 Point2D{
    int x;
    int y;
};

// 2.
struct point2D

// 3.
p1.x = 10;

48.6 연습문제 : typedef로 좌표 구조체 정의하기

정답은 다음 코드와 같다.

// 1. 
struct _Point2D{
    int x;
    int y;
} Point2D;

// 2.
p1

// 3.
p1.y = 20;

48.7 연습문제 : 익명 구조체로 좌표 구조체 정의하기

정답은 } Point2D; 이다.

48.8 심사문제 : 자동차 계기판 구조체 선언하기

정답은 다음코드와 같다.

struct Dashboard d1;

d1.speed = 80;
d1.fuel = 'F';
d1.mileage = 5821.442871f;
d1.engineTemp = 200;
d1.rpm = 1830;

48.9 심사문제 : 자동차 계기판 구조체 정의하기

정답은 다음 코드와 같다.

typedef struct _Dashboard{
    int speed;
    char fuel;
    float mileage;
    int engineTemp;
    int rpm;
}Dashboard;

Unit 49. 구조체 포인터 사용하기

구조체는 멤버 변수가 여러 개 들어 있어 크기가 큰 편이기 때문에 구조체 변수를 일일이 선언해서 사용하는 것 보다 포인터에 메모리를 할당하여 사용하는 것이 효율적이다.

49.1 구조체 포인터를 선언하고 메모리 할당하기

다른 자료형 처럼 구조체도 포인터를 선언할 수 있으며 구조체 포인터에는 malloc함수를 사용하여 동적 메모리를 할당할 수 있다.

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 <stdio.h>
#include <string.h>
#include <stdlib.h>
 
struct Person {
    char name[20];
    int age;
    char address[100];
};
 
int main()
{
    struct Person *p1 = malloc(sizeof(struct Person));
 
    strcpy(p1->name, "홍길동");
    p1->age = 30;
    strcpy(p1->address, "서울시 용산구");
 
    printf("이름 : %s\n", p1->name);
    printf("나이 : %d\n", p1->age);
    printf("주소 : %s\n", p1->address);
 
    free(p1);
    return 0;
}
cs

struct 키워드와 구조체 이름을 사용하여 구조체 포인터를 선언한다. 포인터 변수 이므로 반드시 *을 붙인다. malloc함수로 메모리를 할당 할 때 sizeof(struct Person)과 같이 구조체 크기를 구하여 넣어준다.

구조체의 멤버에 접근할 때는 점이 아닌 -> (화살표 연산자)를 사용한다.

p1->name 같은 문자열 멤버는 할당연산자로 저장할 수 없기 때문에 strcpy 함수를 사용하면 된다.

사용이 끝났으면 free 함수를 사용하여 할당한 메모리를 해제한다.

(*p1).age 처럼 역참조를 사용하면 점으로 멤버에 접근할 수 있다.

49.2 구조체 별칭으로 포인터를 선언하고 메모리 할당하기

구조체별칭 *포인터이름 = malloc(sizeof(구조체별칭)); 의 형태로 포인터를 선언하고 메모리를 할당할 수 있다.

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 <stdio.h>
#include <string.h>
#include <stdlib.h>
 
typedef struct _Person {
    char name[20];
    int age;
    char address[100];
} Person;
 
int main()
{
    Person *p1 = malloc(sizeof(Person));
 
    strcpy(p1->name, "홍길동");
    p1->age = 30;
    strcpy(p1->address, "서울시 용산구");
 
    printf("이름 : %s\n", p1->name);
    printf("나이 : %d\n", p1->age);
    printf("주소 : %s\n", p1->address);
 
    free(p1);
    return 0;
}
cs

구조체 별칭을 사용하여 포인터를 바로 선언한 뒤 malloc 함수로 메모리를 할당하면 된다. 할당하는 메모리 크기도 구조체 별칭으로 바로 구할 수 있다. 구조페를 다 사용했으면 free 함수로 메모리를 해제해주면 된다.

익명 구조체도 사용하려면 구조체 별칭을 지정해줘야 하므로 메모리 할당 방법은 위와 동일하다.

49.3 구조체 포인터에 구조체 변수의 주소 할당하기

동적 메모리를 할당하지 않고 구조체 변수에 &(주소 연산자)를 사용하여 구조체 포인터를 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
 
struct Person {
    char name[20];
    int age;
    char address[100];
};
 
int main()
{
    struct Person p1;
    struct Person *ptr;
 
    ptr = &p1;
    
    ptr->age = 30;
 
    printf("나이 : %d\n", p1.age);
    printf("나이 : %d\n", ptr->age);
    return 0;
}
cs

먼저 구조체 변수를 선언하고, 구조체 포인터를 선언한다.

구조체 변수는 주소 연산자 &를 사용하여 메모리 주소를 구할 수 있으며 메모리 주소는 구조체 포인터에 할당할 수 있다.

ptr은 구조체 포인터이므로 -> 으로 멤버에 접근하여 값을 할당하였다.

ptr은 p1의 메모리 주소 이므로 ptr의 멤버를 수정하면 결국 p1의 멤버도 바뀐다.

49.4 퀴즈

정답은 4 이다.

정답은 d 이다.

정답은 d 이다.

49.5 연습문제 : 학생 구조체 포인터에 메모리 할당하기

정답은 다음코드와 같다.

// 1.
malloc(sizeof(struct Student))

// 2.
strcpy(s1->name, "고길동")
s1->grade = 1;
s1->class = 3;
s1->average = 65.389999f;

49.6 연습문제 : 3차원 좌표 구조체 포인터에 메모리 할당하기

정답은 다음 코드와 같다.

// 1. 
malloc(sizeof(Point3D));

// 2.
p1->x = 10.0f;
p1->y = 20.0f;
p1->z = 30.0f;

49.7 연습문제 : 구조체 포인터에 구조체 주소 할당하기

정답은 다음 코드와 같다.

// 1. 
struct Item *ptr;

// 2.
ptr = &item1;

49.8 심사문제 : 사람과 자동차 구조체 포인터에 메모리 할당하기

정답은 다음 코드와 같다.

struct Person *p1 = malloc(sizeof(struct Person));
Car *c1 = malloc(sizeof(Car));

strcpy(p1->name, "고길동");
p1->age = 40;
strcpy(p1->address, "서울시 서초구 반포동");

strcpy(c1->name, "스텔라");
c1->number = 3421;
c1->displacement = 2000;

49.9 심사문제 : 구조체 포인터에 구조체 변수의 주소 할당하기

정답은 다음 코드와 같다.

ptr = &p1;

Unit 50. 두 점 사이의 거이 구하기

50.1 두 점 사이의 거리 구하기

2차원 평면에서 위치를 표현하려면 x와 y값이 필요하다. 구조체를 이용하여 한 점에 x와 y값을 선언하여 점을 표현할 수 있다.

두 점 사이의 거리를 구하려면 피카고라스의 정리를 이용하면 된다.

점 두 개가 있을 때 직각 삼각형을 그리면 다음과 같다.

피타고라스의 정리를 이용하여면 먼저 선 a 와 b의 길이를 구해야 하는데 구조체 변수에 두 점의 좌표가 들어있다고 가정하면 다음과 같이 구할 수 있다.

int a = p2.x - p1.x;    // 선 a의 길이
int b = p2.y - p1.y;    // 선 b의 길이

피타고라스 정리에서 c의 길이를 구하려면 제곱근을 구해야 한다.

C에서 루트는 math.h 헤더파일에 선언된 sqrt 함수를 사용하여 구할 수 있다.

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
#include <stdio.h>
#include <math.h>
 
struct Point2D {
    int x;
    int y;
};
 
int main()
{
    struct Point2D p1;
    struct Point2D p2;
 
    p1.x = 30;
    p1.y = 20;
 
    p2.x = 60;
    p2.y = 50;
 
    int a = p2.x - p1.x;
    int b = p2.y - p1.y;
 
    double c = sqrt((a*a) + (b*b));
    
    printf("%f\n", c);
    return 0;
}
cs

sqrt 함수를 사용하면 넣은 값의 제곱근을 구할 수 있다. 제곱근은 소수로 나오기 때문에 double형 변수에 저장했다.

제곱을 구할 때는 math.h 헤더파일에 선언되있는 pow 함수를 사용해도 된다. a^2를 구하고 싶으면 pow(a,2)와 같이 사용한다.

abs, fabs, fabsf 함수를 사용하면 각각 정수, double형 실수, float형 실수에 대한 절댓값을 구할 수 있다.

 

50.2 연습문제 : 사각형의 넓이 구하기

정답은 다음 코드와 같다.

int w = abs(rect.x2 - rect.x1);
int h = abs(rect.y2 - rect.y1);
area = w * h;

밑변과 높이를 구하면서 음수가 나올수 있기 때문에 절댓값을 구하는 abs 함수를 사용하였다.

 

50.3 심사문제 : 두 점 사이의 거리 구하기

정답은 다음 코드와 같다.

int a = p2.x - p1.x;
int b = p2.y - p1.y;
distance = sqrt((a*a) + (b*b));

이 페이지 내용 요약

1. 문자열 관련 함수( 문자열 자르기, 문자열 <-> 숫자 변환)

2. 회문, N-gram 구현 

 

Unit 45. 문자열 자르기

45.1 문자를 기준으로 문자열 자르기

strtok 함수로 특정 문자를 기준으로 문자열을 자를 수 있다. string.h 헤더 파일에 선언되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define _CRT_SECURE_NO_WARNINGS    // strtok 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s1[30= "The Littel Prince";
    char *ptr = strtok(s1, " ");
 
    while(ptr != NULL)
    {
        printf("%s\n", ptr);
        ptr = strtok(NULL" ");
    }
 
    return 0;
}
cs

strtok함수는 지정된 문자를 기준으로 문자열을 자른 다. 기준 문자는 큰 따옴표로 묶어야 한다. 위의 경우 공백 문자를 넣어 공백을 기준으로 잘랐다. 잘린 문자열을 한 번에 얻을 수 없어서 while 반복문으로 문자열을 계속 자르다가 문자열이 나오지 않으면 반복문을 끝내는 방식을 사용한다.

while 반복문 안의 strtok 함수는 자를 문자열 부분에 NULL을 넣어 주었다. NULL은 직전 strtok 함수에서 처리했던 문자열에서 잘린 문자열 만큼 다음 문자료 이동한 뒤 다음 문자열을 자른다. ptr = strtok(ptr," "); 처럼 잘린 문자열의 포인터를 다시 넣으면 다음 문자로 이동하지 못하고 처음에 나오는 문자열만 자르게 된다. strtok 함수를 사용할 때는 처음에만 자를 문자열을 넣어주고 그 다음부터는 NULL을 넣어줘야 한다.

처음 호출되는 strtok는 공백문자를 찾아서 NULL로 채운 뒤 문자열의 첫 부분인 The를 자른다.

반복문 안의 strtok에 NULL을 넣어주면 앞에서 잘린 문자열 만큼 다음 문자로 이동한 뒤 NULL로 채운뒤 Little를 자른다. 이런 식으로 반복하여 문자열 맨 끝에 있는 NULL문자를 만나면 반복문을 종료한다.

strtok 함수는 문자열을 새로 생성해서 반환하는 것이 아니라 자르는 부분으로 널문자로 채운 뒤 잘린 문자열의 포인터를 반환하는 것이다. 원본 문자열의 내용을 바꾸므로 사용에 주의해야 한다.

45.2 문자열 포인터 자르기

문자열 포인터에 문자열 리터럴이 들어 있어서 읽기 전용인 상태이면 strtok 함수는 사용할 수 없다.

문자열 포인터를 자를때는 동적 메모리를 할당하고, 문자열을 복사하여 이 문제를 해결할 수 있다.

1
2
3
4
5
6
7
8
9
char *s1 = malloc(sizeof(char* 30);    
strcpy(s1, "The Little Prince");  
char *ptr = strtok(s1, " ");   
while (ptr != NULL)
{
   printf("%s\n", ptr);
   ptr = strtok(NULL" ");
}
free(s1);    
cs

45.3 날짜와 시간값 자르기

strtok함수는 다양한 특수문자와 알파벳 영문자를 기준으로 문자열을 자를 수 있다. 또한 기준 문자는 한 번에 여러개를 지정할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define _CRT_SECURE_NO_WARNINGS    // strtok 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s1[30= "2015-06-10T15:32:19";
    char *ptr = strtok(s1, "-T:");
 
    while(ptr != NULL)
    {
        printf("%s\n", ptr);
        ptr = strtok(NULL"-T:");
    }
 
    return 0;
}
cs

-, T, : 기준으로 문자열을 자르므로 기준 문자를 여러 개 넣을 수 있다.

45.4 자른 문자열 보관하기

문자열을 자르는 while 반복문 안에서 모든 처리를 할 수 없는 상황이 있을 수 있기 때문에 자른 문자열을 보관해야 한다. 다음 코드와 같이 문자열 포인터 배열에 자른 문자열을 보관할 수 있다.

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    // strtok 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>
#include <string.h>
int main()
{
    char s1[30= "The Little Prince";
    char *sArr[10= {NULL, };
    int i = 0;
    char *ptr = strtok(s1, " ");
    while (ptr != NULL)
    {
        sArr[i] = ptr;
        i++;
        ptr = strtok(NULL" ");
    }
    for(int i = 0; i < 10; i++)
    {
        if(sArr[i] != NULL)
            printf("%s\n", sArr[i]);
    }
    return 0;   
}
cs

while 반복문 안에서는 자른 문자열의 메모리 주소를 배열에 저장하고, 배열의 인덱스를 증가시킨다.

ptr에 저장된 메모리 주소가 바뀌기 전에 다른 곳에 보관하면 자른 문자열을 나중에도 계속 사용할 수 있다. 이후 for 반복문에서 배열을 출력하였다.

*45.5 퀴즈 *

정답은 d 이다.

정답은 a, c이다. 기준 문자열은 여러 개 지정할 수 있으며 잘린 문자열의 포인터를 반환한다.

정답은 b 이다.

45.6 연습문제 : 문자열 자르기

정답은 다음 코드와 같다.

1. char *ptr = strtok(s1. " ");

2. while(ptr != NULL)

3. ptr = strtok(NULL, " ");

45.7 심사문제 : 문자열 자르기

정답은 다음 코드와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s[61];
    scanf("%s", s);
    
    char *ptr = strtok(s, ".");
    
    while(ptr != NULL)
    {
        printf("%s\n", ptr);
        ptr = strtok(NULL".");
    }
    
    return 0;
}
cs

45.8 심사문제 : 특정 단어 개수 세기

정답은 다음 코드와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <string.h>
 
int main()
{
    int cnt = 0;
    char s[1001];
    scanf("%[^\n]s", s);
    
    char *ptr = strtok(s, " .,");
    
    while(ptr != NULL)
    {
        if(strcmp(ptr, "the"== 0)
        {
            cnt++;
        }
        ptr = strtok(NULL" .,");
    }
    
    printf("%d\n", cnt);
    return 0;
}
cs

공백, 점, 쉼표를 기준으로 문자열을 잘랐고 strcmp 함수를 이용해 자른 문자열이 the 이면 cnt 변수에 1씩 누적하고, 마지막에 cnt 변수를 출력하였다.

Unit 46. 문자열과 숫자를 서로 변환하기

46.1 문자열을 정수로 변환하기

atoi 함수를 사용하면 10진법으로 표기된 문자열을 정수로 바꿀 수 있다. stdlib.h 헤더파일에 선언되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    char *s1 = "283";
    int num1;
    
    num1 = atoi(s1);
    printf("%d\n", num1);
    return 0;
}
cs

atoi 함수에 문자열을 넣으면 정수가 반환된다. 문자열 정수로 되어 있어야 하며 알파벳, 특수문자 등이 포함되면 해당 문자부터는 변환하지 않는다. 또한 처음부터 숫자가 아니면 0으로 반환된다. 다음은 정수에 영문자, 특수문자가 섞여 있을 때 반환 예 이다.

46.2 특정 진법으로 표기된 문자열을 정수로 변환하기

strtol 함수를 사용하여 16진법으로 표기된 문자열을 정수로 바꿀 수 있다. strtol(문자열, 끝포인터, 진법)의 형태로 사용한다. stdlib.h 헤더파일에 선언되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    char *s1 = "0xaf10";
    int num1;
 
    num1 = strtol(s1, NULL16);
    printf("%x %d\n", num1, num1);
    return 0;
}
cs

변환할 문자열을 저장한 배열을 넣어주고 16을 지정하면 16진법으로 표기된 문자열을 정수로 변환할 수 있다. 10을 지정하면 10진법으로 표기된 문자열을 정수로 변환할 수 있다.

두 번째 인수는 여러개의 정수로 된 문자열을 변환할 때 사용한다. 여러개의 정수로된 문자열을 각각 변환하는 것은 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    char *s1 = "0xaf10 42 0x27C 9952";
    int num1;
    int num2;
    int num3;
    int num4;
    char *end;
 
    num1 = strtol(s1, &end16);
    num2 = strtol(end&end10);
    num3 = strtol(end&end16);
    num4 = strtol(endNULL10);
 
    printf("%x\n%d\n%X\n%d\n", num1,num2,num3,num4);
    return 0;
}
cs

s1에는 16진법으로 표기된 숫자 2개와 10진법으로 표기된 숫자 2개사 있다. 두 번째 인수로 끝 포인터를 선언하여 메모리 주소를 넣으면 strtol 함수가 실행된 뒤에는 끝 포인터가 " 42 0x27C 9952" 처럼 이전 숫자의 끝 부분부터 시작하게 된다.

두번째 부터는 문자열에 end를 넣어 이전 숫자의 끝 부분부터 변환하면 된다. 이 과정을 그림으로 확인하면 다음과 같다.

46.3 문자열을 실수로 변환하기

atof 함수를 사용하여 문자열을 실수로 바꿀 수 있다. stdlib.h 헤더 파일에 선언되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    char *s1 = "35.212345";
    float num1;
 
    num1 = atof(s1);
 
    printf("%f\n", num1);
    return 0;
}
cs

atof 함수에 문자열을 넣으면 실수가 반환된다. 문자열은 실수로 되어있어야 하며 알파벳 영문자, 특수문자가 포함되면 해당 문자부터는 변환을 하지 않는다. 또한 처음부터 숫자가 아니면 0으로 변환된다.

다음과 같이 알파벳 e를 사용한 지수 표기법으로 된 문자열도 실수로 바꿀 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    char *s1 = "3.e5";
    float num1;
 
    num1 = atof(s1);
 
    printf("%e %f\n", num1, num1);
    return 0;
}
cs

strtof 함수를 사용하여 여러개의 실수로 된 문자열을 실수로 바꿀수 있다. stdlib.h 헤더파일에 선언되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdlib.h>   
 
int main()
{
    char *s1 = "35.283672 3.e5 9.281772 7.e-5";    
    float num1;
    float num2;
    float num3;
    float num4;
    char *end;   
 
    num1 = strtof(s1, &end);     
    num2 = strtof(end&end);    
    num3 = strtof(end&end);   
    num4 = strtof(endNULL);    
 
    printf("%f\n", num1);   
    printf("%e\n", num2);    
    printf("%f\n", num3);  
    printf("%e\n", num4);    
    return 0;
}
cs

처음에는 s1을 넣어 문자열을 실수로 변환하고, 끝 포인터는 &end처럼 메모리 주소를 넣어 strtof함수가 실행된 후 끝 포인터가 이전 숫자의 끝 부분부터 시작하게 했다. 두 번째 부터는 end를 넣어 이전 숫자의 끝 부분버터 변환한다. double 형 실수로 변환하는 strtod 함수도 있다.

46.4 정수를 문자열로 변환하기

sprintf 함수를 사용하여 정수를 문자열로 변환할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main()
{
    char s1[10];
    int num1 = 283;
 
    sprintf(s1, "%d", num1);
    printf("%s", s1);
    return 0;
}
cs

변환한 문자열을 저장할 배열을 선언하고, sprintf 함수에 서식지정자로 %d를 설정한 뒤 정수를 문자열로 저장하였다. atoi를 반대로 변환하는 itoa 같은 함수도 있지만 C 표준 함수는 아니다.

다음과 같이 16진법으로 표기된 문자열로 변환하려면 서식지정자 %x를 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main()
{
    char s1[10];
    int num1 = 283;
 
    sprintf(s1, "0x%x", num1);
    printf("%s\n", s1);
    return 0;
}
cs

sprintf 함수에서 서식지정자로 %x를 사용하면 16진법으로 표기된 문자열로 변환할 수 있다. 16진수라는 것을 명확하게 나타내기 위해 앞에 0x를 붙여준 것이다. 서식지정자를 %X로 지정하면 16진수 알파벳 부분이 대문자로 저장된다.

46.5 실수를 문자열로 변환하기

실수를 문자열로 변환할 때도 sprintf 함수를 사용한다. 다음과 같이 서식지정자로 %f를 지정하면 된다. 또한 %e를 지정하여 지수 표기법으로 된 문자열로 변환할 수도 있다.

sprintf(s1, "%f", num1);
sprintf(s1, "%e", num1);

변환된 문자열이 길어질 수 있기 때문에 배열의 크기 또는 동적메모리로 할당한 크기를 잘 확인해야 한다.

46.6 퀴즈

정답은 b이다.

정답은 b이다.

정답은 c 이다.

정답은 e 이다.

정답은 c, g 이다.

정답은 b이다.

46.7 연습문제: 문자열을 10진 정수로 변환하기

정답은 다음 코드와 같다.

1. #include <stdlib.h>

2. num1 = atoi(s1);

46.8 연습문제 : 문자열을 16진 정수로 변환하기

정답은 다음 코드와 같다.

1. num1 = strtol(s1, NULL, 16)

2. "0x%X\n"

대문자로 출력하므로 서식지정자 %X를 사용한다.

46.9 연습문제 : 문자열을 실수로 변환하기

정답은 다음과 같다.

1. "97.527824"

2. num1

46.10 연습문제: 여러개의 실수로 된 문자열을 실수로 변환하기

정답은 다음 코드와 같다.

1. num1 = strtof(s1, &end);

2. num2 = strtof(end, NULL);

46.11 연습문제 : 숫자를 문자열로 변환하기

정답은 다음 코드와 같다.

1. char s1[30];

2. sprintf(s1, "%f 0x%x", num1, num2);

46.12 심사문제 : 문자열을 정수와 실수로 변환하기

정답은 다음 코드와 같다.

num1 = strtol(s1, &end, 16);
num2 = strtol(end, &end, 10);
num3 = strtof(end, NULL);

46.3 심사문제 : 정수와 실수를 문자열로 변환하기

정답은 다음 코드와 같다.

sprintf(s1, "%d", num1);
sprintf(s2, "%f", num2);

Unit 47. 회문 판별과 N-gram 만들기

회문은 유전자 염기 서열 분석에서 많이 쓰이고, N-gram은 빅데이터 분석, 검색 엔진에서 많이 쓰인다.

47.1 회문 판별

회문은 level, SOS 등과 같이 순서를 거꾸로 읽어도 제대로 읽은 것과 같은 단어와 문장을 말한다.

회문인지 판별하려면 첫번째 글자와 마지막 글자가 같고, 안쪽으로 한 글자씩 좁혔을 때 글자가 서로 같으면 회문이다.

다음은 문자열을 배열에 넣은뒤 반복문으로 각 글자를 검사한 것이다.

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
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
 
int main()
{
    char word[30];
    int length;
    bool isPalindrome = true;
 
    printf("단어를 입력하세요 : ");
    scanf("%s", word);
 
    length = strlen(word);
 
    for(int i = 0; i < length / 2; i++)
    {
        if(word[i] != word[length - 1 -i])
        {
            isPalindrome = false;
            break;
        }
    }
 
    printf("%d\n", isPalindrome);
    return 0;
}
cs

회문일 경우 1이 출력되고, 회문이 아닐 경우 0이 출력된다.

회문 판별에서 가장 중요한 것은 문자열의 길이이다.

문자열 길이의 절반만큼 반복하며 왼쪽 문자와 오른쪽 문자들을 검색한다. 반복문 안에서 왼쪽문자와 오른쪽 문자를 비교하며 문자가 다르면 isPalindrome 에 false를 넣고 반복문을 끝낸다. 문자열의 마지막 문자는 word[length-1] 이므로 인덱스를 i 만큼 빼주면 오른쪽에서 왼쪽으로 진행할 수 있다.

47.2 N-gram 만들기

N-gram은 문자열에서 N개의 연속된 요소를 추출하는 방법이다. Hello의 경우 2-gram으로 추출하면 다음과 같다.

문자열의 처음부터 끝까지 한 글자씩 이동하며 2 글자씩 추출한다.

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

int main()
{
    char text[30] = "Hello";
    int length;

    length = strlen(text);

    for(int i = 0; i < length - 1; i++)
    {
        printf("%c%c\n", text[i], text[i+1]);
    }

    return 0;
}

2-gram이므로 문자열의 끝에서 한 글자 앞까지만 반복하며 현재 문자와 그 다음문자 두 글자씩 출력한다.

만약 3-gram이라면 조건식은 i < length -2가 될 것이고, 문자열 끝에서 두 글자 앞까지 반복하면 된다.

단어 단위 N-gram도 있다. 다음은 공백을 기준으로 구분하여 단어단위 2-gram을 출력하는 코드이다.

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 <stdio.h>
#include <string.h>
 
int main()
{
    char text[100= "this is c language";
    char *tokens[30= {NULL, };
 
    int cnt = 0;
    char *ptr = strtok(text, " ");
 
    while (ptr != NULL)
    {
        tokens[cnt] = ptr;
        cnt++;
        ptr = strtok(NULL" ");
    }
 
    for(int i = 0; i < cnt -1; i++)
    {
        printf("%s %s\n", tokens[i], tokens[i+1]);
    }
 
    return 0;
}
cs

strtok 함수로 문자열을 자른 뒤 각 단어들을 배열에 넣고, 배열의 마지막에서 요소 한 개 앞까지만 반복하면서 현재 문자열과 그 다음 문자열을 출력하면 된다.

47.3 연습문제 : 정수 회문 판별하기

정답은 다음 코드와 같다.

1. sprintf(text, "%lld", num1)

2. begin++;

3. end--;

입력값이 정수 이므로 sprintf 함수로 문자열로 변환하여 회문을 판별한다.

47.4 연습문제 : 4-gram 만들기

정답은 다음 코드와 같다.

1. length < n

2. i < length - (n-1)

3. j < n

문자열을 반복할 때는 배열의 범위를 벗어나지 않도록 i < length(n-1)로 조건식을 사용했다.

47.5 심사문제 : 공백이 포함된 회문 판별

정답은 다음 코드와 같다.

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
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
 
int main()
{
    char str[31];
    int length;
    bool isP = true;
    
    scanf("%[^\n]s", str);
    length = strlen(str);
    
    for(int i = 0, j = length-1; i < j; i++, j--)
    {
        while(str[i] == ' ')
            i++;
        while(str[j] == ' ')
            j--;
        if(str[i] != str[j])
        {
            isP = false;
            break;
        }
    }
    printf("%d\n", isP);
    return 0;
}
cs

반복문에서 문자열의 처음부터, 끝에서부터 반복시키며 공백이 있을 경우 한 칸씩 증가, 감소 시켜서 공백을 지우고, 문자열 앞에서 부터 온 문자와 뒤에서 부터 온 문자가 다르면 회문을 거짓으로 하고, 반복문을 끝낸다.

47.6 심사문제 : N-gram 만들기

정답은 다음 코드와 같다.

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
#include <stdio.h>
#include <string.h>
 
int main()
{
    char text[11];
    int n;
    scanf("%d %s"&n, text);
    int length = strlen(text);
    
    if(length < n)
    {
        printf("wrong\n");
    }
    else
    {
        for(int i = 0; i < length - (n-1); i++ )
        {
            for(int j = 0; j < n; j++)
            {
                printf("%c", text[i+j]);
            }
            printf("\n");
        }
    } 
    
    return 0;
}
cs

2중 for문의 안쪽 반복문은 입력한 숫자만큼 반복하므로 입력한 숫자만큼의 문자를 출력할 수 있고, 바깥쪽 반복문으로 1씩 증가시키면서 문자열에서 문자를 한 칸씩 앞으로 가도록 했다.

 

 

이번 페이지 요약

코딩도장 C언어 unit 41~44

1. 문자열 관련 함수(길이, 복사, 문자열 만들기, 검색)

 

Unit 41. 문자열의 길이를 구하고 비교하기

41.1 문자열의 길이 구하기

문자열의 길이는 strlen 함수로 구할 수 있으며 string.h 헤더 파일에 선언되어 있다. 문자열 포인터와 문자 배열의 길이 모두 구할 수 있으며 사용법은 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <string.h>
 
int main()
{
    char *s1 = "Hello";
    char s2[10= "Hello";
 
    printf("%d\n", strlen(s1));
    printf("%d\n", strlen(s2));
    return 0;
}
cs

strlen에 문자열 포인터나 문자 배열을 넣으면 문자열의 길이가 반환된다 Hello는 5글자 이기 때문에 5가 출력되었으며 NULL 부분은 포함하지 않는다. 배열의 경우 배열의 크기는 관계없이 문자열의 길이만 구한다. 위 코드의 경우도 배열의 크기는 10이지만 출력값은 문자열의 길이인 5가 나왔다.

41.2 문자열 비교하기

strcmp 함수를 사용하면 두 문자열이 같은 지 비교할 수 있다. string.h 헤더 파일에 선언되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
 
int main()
{
    char *s1 = "Hello";
    char s2[10= "Hello";
    
    int ret = strcmp(s1, s2);
 
    printf("%d\n", ret);
    return 0;
}
cs

strcmp 함수에 비교할 문자열을 넣으면 결과를 정수로 반환하며 반환 되는 값의 종류는 다음과 같다.

  • -1: ASCII 코드 기준으로 문자열2(s2)가 클 때
  • 0: ASCII 코드 기준으로 두 문자열이 같을 때
  • 1: ASCII 코드 기준으로 문자열1(s1)이 클 때

배열 문자열, 문자열 포인터 등 문자열의 저장 방식은 문자열 비교에 영향을 주지 않는다.

strcmp 함수는 문자열에서 첫 번째 문자부터 차례대로 비교하며 비교 기준은 각 문자의 ASCII 코드이다.

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <string.h>
 
int main()
{
    printf("%d\n", strcmp("aaa""aaa"));
    printf("%d\n", strcmp("aab""aaa"));
    printf("%d\n", strcmp("aab""aac"));
    return 0;
}
cs

aaa는 아스키 코드로 97 97 97 이고, aab 는 97 97 98 이기 때문에 aab 가 더 크다. 또한 aac는 97 97 99 이므로 aac가 aab 보다 크다. 앞의 것이 크면 1, 뒤의 것이 크면 -1을 반환한다. 이것은 윈도우 기준이고, 리눅스나 OS X에서는 코드값의 차이를 반환한다. 따라서 입력받는 두 문자열의 크기를 비교할 땐 윈도우에서는 switch문을 사용하면 되고, 리눅스나 OS X 에서는 if 문을 사용해야 한다.

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
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s1[20];
    char s2[20];
 
    printf("문자열 두 개를 입력하세요 : ");
    scanf("%s %s", s1, s2);
 
    int ret = strcmp(s1, s2);
    printf("반환값 : %d\n", ret);
 
    if(ret == 0)
    {
        printf("같음\n");
    }
    else if(ret > 0)
    {
        printf("%s가 %s보다 큼\n", s2, s1);
    }
    else if(ret < 0)
    {
        printf("%s가 %s보다 큼\n", s1, s2);
    }
 
    return 0;
}
cs

리눅스나 OS X에서 strcmp는 문자열1에서 문자열 2의 아스키 코드값을 뺏을 때 양수가 나오면 1이 크고, 음수가 나오면 2가 크다. 두 문자열이 같으면 0이 나온다.

41.3 퀴즈

정답은 c 이다.

정답은 c,d,f 이다.

41.4 연습문제 : 문자열 길이 구하기

정답은 strlen(s1) 이다.

41.5 연습문제 : 문자열 비교하기

정답은 char *s1 = "Pachelbel Canon"; 이다.

41.6 심사문제 : 문자열 길이 구하기

정답은 다음 코드와 같다.

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s[30];
    scanf("%s", s);
    printf("%d", strlen(s));
    return 0;
}
cs

41.7 심사문제 : 문자열 비교하기

정답은 다음 코드와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s1[30];
    char s2[30];
    
    scanf("%s %s", s1, s2);
    
    printf("%d", strcmp(s1, s2));
    return 0;
}
cs

Unit 42. 문자열을 복사하고 붙이기

42.1 문자열 복사하기

strcpy 함수를 이용하여 문자열을 다른 배열이나 포인터(메모리)로 복사할 수 있다. string.h 헤더 파일에 선언되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define _CRT_SECURE_NO_WARNINGS    // strcpy 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s1[10= "Hello";
    char s2[10];
 
    strcpy(s2, s1);
 
    printf("%s\n", s2);
    return 0;
}
cs

배열 s2에는 아무것도 들어있지 않았지만 strcpy 함수로 s1의 문자열이 복사되어 s2에도 저장되었다.

복사된 결과가 저장된 배열의 크기는 반드시 NULL까지 들어갈 수 있는 크기여야 한다. 따라서 Hello 라는 문자열이 복사되려면 NULL문자까지 크기는 최소 6 이상이 되어야 한다.

문자열 포인터는 복사할 공간도 없으며 읽기만 가능하기 때문에 문자열을 복사할 수 없다. 문자열 포인터에 문자열을 복사하려면 다음과 같이 메모리를 할당한 뒤 복사해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define _CRT_SECURE_NO_WARNINGS    // strcpy 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
 
int main()
{
    char *s1 = "Hello";
    char *s2 = malloc(sizeof(char* 10);
 
    strcpy(s2, s1);
 
    printf("%s\n", s2);
    free(s2);
    return 0;
}
cs

s2에 char 10개 크기만큼 동적 메모리를 할당하여 문자열을 복사하고, free로 할당한 메모리를 해제하였다.

42.2 문자열 붙이기

문자열은 strcat 함수를 이용하여 서로 붙일 수 있다. string.h 헤더파일에 선언되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define _CRT_SECURE_NO_WARNINGS    // strcat 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s1[10= "world";
    char s2[20= "Hello";
 
    strcat(s2, s1);
 
    printf("%s\n", s2);
    return 0;
}
cs

strcat함수에 최종 결과가 나올 문자열과 붙일 문자열을 넣는다. 위의 경우 s2 뒤에 s1 이 붙어서 Helloworld가 나온다.

문자열을 붙이더라도 배열이 모자라지 않도록 최종 결과가 나올 문자열의 배열 크기를 넉넉하게 만들어야 한다.

문자열 포인터에 사용하려면 붙일 문자열 포인터에 동적 메모리를 할당해 줘야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define _CRT_SECURE_NO_WARNINGS  
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
 
int main()
{
    char *s1 = "world";
    char *s2 = malloc(sizeof(char* 20);
 
    strcpy(s2, "Hello");
    strcat(s2, s1);
 
    printf("%s\n", s2);
    free(s2);
    return 0;
}
cs

최종 결과가 나올 문자열 포인터에 char 20개 크기만큼 동적 메모리를 할당하고, 메모리가 할당된 문자열 포인터는 문자열을 직접 할당할 수 없으므로 strcpy 함수를 이용하여 "Hello"를 복사하고, s2 뒤에 s1을 붙인다. 문자열 사용이 끝났으면 free로 동적 메모리를 해제한다.

42.3 배열 형태의 문자열을 문자열 포인터에 복사하기

다음과 같이 배열 형태의 문자열을 문자열 포인터로 복사할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define _CRT_SECURE_NO_WARNINGS  
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
 
int main()
{
    char s1[10= "Hello";
    char *s2 = malloc(sizeof(char* 10);
 
    strcpy(s2, s1);
 
    printf("%s\n", s2);
    free(s2);
    return 0;
}
cs

malloc 함수로 s2에 메모리를 복사할 문자열이 충분히 들어갈 수 있을 정도로 할당하고, strcpy로 복사하면 된다.

42.4 배열 형태의 문자열을 문자열 포인터에 붙이기

다음과 같이 배열 문자열을 문자열 포인터에 붙일 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#define _CRT_SECURE_NO_WARNINGS   
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
 
int main()
{
    char s1[10= "world";
    char *s2 = malloc(sizeof(char* 20);
 
    strcpy(s2, "Hello");
    strcat(s2, s1);
 
    printf("%s\n", s2);
    free(s2);
    return 0;
}
cs

문자열 포인터 s2 뒤에 문자열을 붙여야 하므로 메모리가 부족하지 않도록 넉넉히 할당하고, strcpy함수로 s2에 "Hello"를 복사한다. 이후 s2 뒤에 strcat 함수로 문자열 배열 s1을 붙이면 된다. 문자열 사용이 끝나면 free로 메모리를 해제해줘야 한다.

42.5 퀴즈

정답은 NULL문자까지 6이다.

정답은 NULL문자까지 11바이트 이다.

정답은 c 이다.

42.6 연습문제 : 문자열 포인터를 배열에 복사하기

문자열을 복사하는 것이므로 strcpy 함수를 사용하여 정답은 strcpy(s2, s1); 이다.

42.7 연습문제 : 문자열 포인터를 동적 메모리에 복사하기

정답은 다음 코드와 같다.

1. malloc(sizeof(char) * 20);

2. strcpy(s2, s1);

42.8 연습문제 : 문자 배열을 붙이기

정답은 strcat(s2, s1); 이다.

42.9 연습문제 : 문자열 리터럴과 동적 메모리 붙이기

정답은 다음 코드와 같다.

1. strcpy(s2, "Alice in");

2. strcat(s2, s1);

42.10 심사문제 : 문자 배열 복사하기

정답은 다음 코드와 같다.

scanf("%s", s1);
strcpy(s2, s1);

s1에는 값을 입력받고, s2에는 s1의 값을 복사해서 가져왔다.

42.11 심사문제 : 두 문자열 붙이기

scanf("%s", s1);
strcat(s1, "th");

문자열을 입력받아 strcat 함수로 문자열 끝에 th를 붙였다.

Unit 43. 문자열 만들기

43.1 서식을 지정하여 배열 형태로 문자열 만들기

sprintf 함수를 사용하면 서식을 지정하여 문자열을 만들 수 있다. sprintf(배열, 서식, 값);의 형태로 사용하며 서식에 값이 들어간 것을 배열에 저장할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
#define _CRT_SECURE_NO_WARNINGS    // sprintf 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>
 
int main()
{
    char s1[20];
 
    sprintf(s1, "Hello %s""world!");
 
    printf("%s\n", s1);
    return 0;
}
cs

sprintf 함수에 문자열을 저장할 배열과 문자열을 만들 서식, 문자열을 만들 값(문자열)을 순서대로 넣어 %s부분이 "word!"로 바뀌게 된다. 위 코드에서 s1과 같이 문자열을 저장할 빈 배열을 버퍼(buffer)라고 부른다. 서식지정자로 C언어의 다양한 자료형들을 문자열로 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
 
int main()
{
    char s1[30];
 
    sprintf(s1, "%c %d %f %e"'a'103.2f, 1.123456e-21f);
    printf("%s\n", s1);
    return 0;
}
cs

sprintf()함수에 %c, %d, %f, %e를 지정하여 각 서식지정자에 맞는 문자, 정수, 소수점 표기 실수, 지수 표기법 실수를 넣었다. 위 코드에서는 함수에 값을 바로 넣었지만, 변수를 넣어도 된다.

43.2 서식을 지정하여 문자열 포인터에 문자열 만들기

문자열 포인터를 사용하려면 malloc 함수로 메모리를 할당한 뒤 sprintf함수로 문자열을 만들면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    char *s1 = malloc(sizeof(char* 20);
    sprintf(s1, "Hello, %s""world!");
    
    printf("%s\n", s1);
    free(s1);
    return 0;
}
cs

char 20개 크기의 동적 메모리를 할당하고 sprintf함수에 서식을 지정하여 문자열을 만들면 된다. 배열과 마찬가지로 문자열을 생성할 메모리 공간은 버퍼이다. 문자열 사용이 끝나면 free 함수 동적 할당한 메모리를 해제 해야 한다.

다음과 같이 다양한 종류의 서식지정자를 사용하여 다양한 자료형도 문자열로 만들 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    char *s1 = malloc(sizeof(char* 30);
    sprintf(s1, "%c %d %f %e"'a'103.1f, 1.23e-23f);
    
    printf("%s\n", s1);
    free(s1);
    return 0;
}
cs

char 30개 크기만큼 동적으로 메모리를 할당하고, sprintf함수에 서식을 지정하여 문자, 정수, 소수점 표기 실수, 지수 표기 실수를 문자열로 만들었다. 문자열 사용이 끝나면 free함수로 할당한 메모리를 해제해야 한다.

43.3 퀴즈

정답은 d이다.

정답은 "%c %d" 이다.

정답은 버퍼 이다.

43.4 연습문제 : 숫자와 문자열을 조합하여 문자열 만들기

숫자와 문자열이 순서대로 들어가므로 정답은 9, "Symphony" 이다.

43.5 연습문제 : 서식에 맞게 문자열 만들기

정수 3개 ,문자 1개 정수 1개가 순서대로 들어가므로 정답은 "%d %d %d %c %d" 이다.

43.6 심사문제 : 서수 줄임말 문자열 만들기

정답은 다음코드와 같다.

scanf("%d %s", &number, name);

if(number == 1)
    sprintf(result, "%dst %s", number, name);
else if(number == 2)
    sprintf(result, "%dnd %s", number, name);
else if(number == 3)
    sprintf(result, "%drd %s", number, name);
else
    sprintf(result, "%dth %s", number, name);

숫자에 따라 숫자 뒤에 붙는것이 다르기 때문에 if 조건문을 사용하여 조건에 맞게 출력하도록 하였다.

Unit 44. 문자열 검색하기

44.1 문자열 안에서 문자로 검색하기

strchr 함수를 사용하여 문자열에서 특정 문자로 검색할 수 있다. string.h 헤더 파일에 선언되어 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s1[30= "A Garden Diary";
    char *ptr = strchr(s1, 'a');
 
    while(ptr != NULL)
    {
        printf("%s\n", ptr);
        ptr = strchr(ptr + 1'a');
    }
    return 0;
}
cs

strchr 함수에 문자열과 검색할 문자를 넣어주면 해당 문자로 시작하는 문자열의 위치(포인터)를 반환한다.

'a' 로 시작하는 부분을 더 찾기 위해 while 반복문을 사용하여 검색된 문자여릐 포인터에 1을 더해 다음부터 또 검색해서 찾는다. NULL이 나오면 검색할 문자열이 없는 것이므로 반복을 끝낸다.

'a' 가 들어간 문자를 처음 찾으면 arden Diary를 찾을 수 있다. 포인터에 1을 더하면 rden Diary 가 되므로 다음번 a를 찾을 수 있게 된다.

strchr 함수는 대소문자를 구분하므로 A는 찾지 않고 넘어간다.

44.2 문자열의 오른쪽 끝부터 문자로 검색하기

strrchr 함수는 문자열의 끝에서 부터 문자를 검색한다. string.h 헤더 파일에 선언되어 있다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s1[30= "A Garden Diary";
    char *ptr = strrchr(s1, 'a');
 
    printf("%s\n", ptr);
    return 0;
}
cs

strrchr 함수에 문자열과 검색할 문자를 넣으면 문자열 끝에서 부터 검색하여 'a'로 시작하는 문자열의 포인터를 반환한다. strrchr함수도 대소문자를 구분한다.

44.3 문자열 안에서 문자열로 검색하기

strstr 함수는 문자열 안에서 문자열을 검색할 수 있는 함수이며 string.h 헤더 파일에 선언되어 있다.

워드나 메모장에서 검색할 때 단어로 검색하는 경우가 많은것처럼 프로그램을 만들때도 문자열 찾는 함수를 더 많이 사용한다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s1[30= "A Garden Diary";
    char *ptr = strstr(s1, "den");
 
    printf("%s\n", ptr);
    return 0;
}
cs

strstr 함수에 검색할 문자열을 넣으면 해당 문자열로 시작하는 문자열의 위치(포인터)를 반환한다. 검색한 문자만 나오지 않고, 검색한 문자를 포함하여 뒤에 오는 모든 문자열이 나온다. strstr 함수도 대소문자를 구분하며 다음과 같이 반복문을 사용하여 문자열을 계속 검색할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <string.h>
 
int main()
{
    char s1[100= "A Garden Diary A Garden Diary A Garden Diary";
    char *ptr = strstr(s1, "den");
 
    while(ptr != NULL)
    {
        printf("%s\n", ptr);
        ptr = strstr(ptr+1"den");
    }
    return 0;
}
cs

44.4 퀴즈

정답은 c, e 이다.

정답은 b 이다.

44.5 연습문제 : 문자열 안에서 문자로 검색하기

정답은 다음 코드와 같다.

1. char *ptr = strchr(s1, 'n');

2. while(ptr != NULL)

3. ptr = strchr(ptr + 1, 'n');

44.6 연습문제 : 문자열의 오른쪽 끝 부터 문자로 검색하기

정답은 char *ptr = strrchr(s1, 'i'); 이다. 마지막의 ince만 출력하려면 오른쪽 부터 검색하는 strrchr 함수를 사용해야 한다.

44.7 심사문제 : 공백 개수 세기

정답은 다음 코드와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <string.h>
 
int main()
{
    char str[1001];
    scanf("%[^\n]s", str);
    char *ptr = strchr(str, ' ');
    int cnt = 0;
    
    while(ptr != NULL)
    {
        cnt++;
        ptr = strchr(ptr + 1' ');
    }
    printf("%d\n", cnt);
    return 0;
}
cs

정수형 변수를 선언한 후 공백을 찾을 때 마다 1씩 누적하고 반복문이 끝난 뒤 누적한 값을 출력하였다.

 

이번 페이지 요약 : 

코딩도장 C언어 Unit 39, 40

1. 문자열 사용하기

2. 입력값 문자열에 저장 

 

Unit 39. 문자열 사용하기

c언어 문자를 저장하는 자료형은 있지만 문자열을 저장하는 자료형은 없다. 그래서 char 자료형에 문자열을 저장하면 컴파일은 되지만 실행은 되지 않는다. 컴파일을 한 뒤 디버깅 모드로 보면 다음과 같은 에러가 발생한다.

39.1 문자와 문자열 포인터 알아보기

문자열은 다음과 같이 char 포인터 형식으로 사용한다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main()
{
    char c1 = 'a';
    char *s1 = "Hello";
 
    printf("%c\n", c1);
    printf("%s\n", s1);
    return 0;
}
cs

문자는 글자가 하나만 있는 상태이고, 문자열은 글자 여러 개가 계속 이어진 상태를 의미한다. 문자는 1바이트 크기라 char에 저장할 수있지만, 문자열은 1바이트가 넘어서므로 char에 저장할 수 없다. 따라서 문자열은 변수에 직접 저장하지 않고 포인터를 이용하여 저장한다. 포인터는 메모리 주소를 저장하기 때문에 메모리 주소를 보고 문자열이 저장된 위치로 이동한다. 문자열 리터럴이 있는 메모리 주소는 읽기 전용이기 때문에 다른 문자열을 덮어 쓸 수 없다. 문자열 리터럴이 저장되는 위치는 컴파일러가 결정한다.

문자열은 ""(큰따옴표)로 묶어야 하며 문자열을 출력할 때는 서식지정자 %s를 사용해야 한다.

C에서 문자열은 마지막 끝에 항상 널문자가 붙는다. NULL은 0으로 표현할 수 있으며 문자열의 끝을 나타낸다.

문자열을 출력할때는 문자들을 순서대로 계속 출력하다가 NULL을 만나면 출력을 끝낸다.

39.2 문자열 포인터에서 인덱스로 문자에 접근하기

문자열도 다음과 같이 대괄호를 사용하여 인덱스로 접근할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main()
{
    char *s1 = "Hello";
 
    printf("%c\n", s1[1]);
    printf("%c\n", s1[4]);
    printf("%c\n", s1[5]);
    return 0;
}
cs

문자 하나를 출력할 때는 %c 서식지정자를 사용하며 맨 뒤 문자열인 5번째 인덱스를 가져오면 NULL 이기 때문에 출력해도 화면에 표시되지 않는다.

문자열 포인터에서 인덱스로 문자를 가져오는 것은 가능하지만 인덱스로 문자를 할당하는것은 불가능하다. 문자를 할당하려고 에러가 발생한다. 문자열 리터럴이 있는 메모리 주소는 읽기 전용이기 때문이다.

38.3 배열 형태로 문자열 선언하기

문자열은 다음과 같이 문자 배열에 저장할 수 도 있다.

1
2
3
4
5
6
7
8
9
#include <stdio.h>
 
int main()
{
    char s1[10= "Hello";
    
    printf("%s\n", s1);
    return 0;
}
cs

위 코드는 크기가 10인 배열을 선언한 뒤 문자열을 할당한 것이다.

문자열에 배열을 저장하는 방식은 배열 요소 하나하나에 문자가 저장된다. 배열이기 때문에 인덱스는 0부터 시작하고, 문자열의 맨 뒤에 NULL이 들어간다. 남는 공간에도 모두 NULL이 들어간다.

배열로 문자열을 사용할 때는 무조건 배열을 선언한 즉시 문자열로 초기화해야 한다. 이미 선언한 배열에는 문자열을 할당할 수 없으며 정 할당하고 싶으면 요소에 문자 하나 하나를 넣으면 된다.

배열을 선언할 때는 배열의 크기를 할당할 문자열보다 크게 지정해야 한다. 문자열 문자 개수보다 배열의 크기가 작다면 컴파일은 되지만 문자열이 제대로 출력되지 않는다. 문자열은 마지막에 NULL이 꼭 들어가야 하므로 문자 개수에 하나 더한 크기가 문자열을 저장할 수 있는 배열의 최소 크기 이다.

다음과 같이 문자열을 바로 할당할 때는 배열의 크기를 생략할 수 있다.

1
2
3
4
5
6
7
8
9
#include <stdio.h>
 
int main()
{
    char s1[] = "Hello";
    
    printf("%s\n", s1);
    return 0;
}
cs

이와 같은 경우 배열의 크기는 문자열의 문자 개수에 맞게 자동으로 지정된다. 위 경우 문자 5개에 NULL을 더해 6이 된다.

39.4 배열 형태의 문자열에서 인덱스로 문자에 접근하기

다음과 같이 인덱스로 접근할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main()
{
    char s1[] = "Hello";
    
    printf("%c\n", s1[1]);
    printf("%c\n", s1[4]);
    printf("%c\n", s1[5]);
    return 0;
}
cs

배열을 인덱스로 접근하면 char과 같기 때문에 %c로 출력할 수 있다. 배열로 저장된 문자열 맨 뒤에는 NULL이 있어 출력값에는 보이지 않는다.

배열 형태의 문자열은 다음과 같이 인덱스로 접근하여 문자를 할당할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main()
{
    char s1[] = "Hello";
    
    s1[0= 'A';
    
    printf("%s\n", s1);
    return 0;
}
cs

Hello 가 들어있는 문자 배열의 0번 인덱스에 A를 할당해서 Aello가 되었다. 배열 형태의 문자열은 배열에 문자열이 모두 복사되었기 때문에 인덱스로 접근하여 내용을 변경할 수 있다.

39.5 퀴즈

정답은 b,e,g,h이다.

정답은 d 이다.

정답은 b 이다.

정답은 a, d 이다.

39.6 연습문제 : 문자열 만들기

정답은 char s1[] 이다.

39.7 연습문제 : 문자열 요소 출력

정답은 s1[10] 이다.

39.8 심사문제 : 문자열 만들기

정답은 다음 코드와 같다.

char s1[] = "Beethoven\n9th\nSymphony";

출력에 줄바꿈이 있으므로 개행문자 \n을 사용했다.

Unit 40. 입력 값을 문자열에 저장하기

40.1 입력 값을 배열 형태의 문자열에 저장하기

scanf 함수에서 서식지정자 %s를 사용하면 입력값을 배열 형태의 문자열에 저장할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
 
int main()
{
    char s1[10];
 
    printf("문자열을 입력하세요 : ");
    scanf("%s", s1);
 
    printf("%s\n", s1);
    return 0;
}
cs

scanf 함수에 서식지정자로 %s를 넣어서 문자열을 입력받을 수 있다. 매개변수는 입력값을 저장할 배열을 넣는데, 배열 앞에는 일반 변수와 달리 &를 붙이지 않는다. 위 코드에서는 크기가 10인 배열을 선언했으므로 문자열 맨 뒤에 붙는 널문자 까지 포함하여 총 10개의 문자를 저장할 수 있다. 따라서 실제 저장할 수 있는 문자는 9개이다.

scanf에서 %s로 문자열을 저장할 때 입력된 문자열에 공백이 있다면 배열에는 공백 직전까지만 저장된다. 공백을 포함하여 저장하고 싶다면 서식지정자로 %[^\n]s를 사용하면 된다.

40.2 입력 값을 문자열 포인터에 저장하기

문자열 포인터를 선언한 뒤 scanf 함수로 입력 값을 문자열 포인터에 저장하고 실행하면 에러가 발생한다. 이미 할당된 문자열 포인터에 저장된 메모리 주소는 읽기만 할 수 있고, 쓰기가 막혀있기 때문이다.

입력값을 문자열 포인터에 저장하려면 문자열이 들어갈 공간을 따로 만들어야 한다. malloc 함수로 메모리를 할당한 뒤 문자열을 저장하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    char *s1 = malloc(sizeof(char* 10);
    
    printf("문자열을 입력하세요 : ");
    scanf("%s", s1);
 
    printf("%s\n", s1);
    free(s1);
    return 0;
}
cs

char 10개 크기 만큼 동적 메모리를 할당하여 입력값을 문자열 포인터 s1에 저장할 수 있다. 입력 받을 때 s1은 포인터이므로 &를 붙이지 않는다. 문자열 사용이 끝나면 free 함수로 메모리를 해제해야 한다. 문자열 중간에 공백이 있다면 문자열 포인터에는 공백 직전까지만 저장된다.

40.3 문자열을 여러 개 입력받기

다음과 같이 공백으로 구분된 문자열 여러개를 입력받을 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
 
int main()
{
    char s1[10];
    char s2[10];
 
    printf("문자열을 두 개 입력하세요 : ");
    scanf("%s %s", s1, s2);
 
    printf("%s\n", s1);
    printf("%s\n", s2);
    return 0;
}
cs

문자열 두 개를 저장하기 위한 공간을 선언하고, scanf 함수에서 서식지정자로 %s를 두 개 넣어준다. 문자열이 저장될 배열을 두 개 넣어주면 공백으로 구분된 문자열을 입력받을 수 있다. 문자열을 더 입력받고 싶으면 %s와 입력받을 배열(또는 문자열 포인터)의 개수를 늘려주면 된다.

40.4 퀴즈

정답은 c, e, g 이다.

정답은 9 이다.

40.5 연습문제 : 입력받은 문자열을 배열에 저장하기

정답은 scanf("%s", s1); 이다.

40.6 연습문제 : 입력받은 문자열을 동적 메모리에 저장하기

정답은 다음 코드와 같다.

1. char *s1 = malloc(sizeof(char) * 10);

2. scanf("%s", s1);

40.7 연습문제 : 문자열 세 개 입력받기

정답은 scanf("%s %s %s", s1, s2, s3); 이다.

40.8 심사문제 : 문자열 4 개 입력받기

정답은 다음 코드와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
 
int main()
{
    char s1[30];
    char s2[30];
    char s3[30];
    char s4[30];
    
    scanf("%s %s %s %s", s1, s2, s3, s4);
    
    printf("%s\n%s\n%s\n%s\n", s1, s2, s3, s4);
    return 0;
}
cs

Unit 37. 2차원 배열 사용하기

2차원 배열은 다음과 같이 가로 x 세로 의 형태로 되어 있는 평면 구조이며 행과 열 모두 0부터 시작한다.

37.1 2차원 배열을 선언하고 요소에 접근하기

2차원 배열은 [](대괄호)를 두번 사용하여 선언하며 첫 번째 대괄호는 세로크기, 두 번째 대괄호는 가로 크기를 지정한다.

2차원 배열을 선언하면서 초기화 하려면 { }(중괄호)를 사용하는데, 다음과 같이 가로 요소들을 먼저 묶고, 가로 줄을 세로 크기만큼 다시 묶는다.

int numArr[3][4] = {
    { 가로 요소 4개 },
    { 가로 요소 4개 },
    { 가로 요소 4개 }
};

{ }를 사용라여 배열에 값을 할당하는 방법은 배열을 선언할 때만 사용할 수 있으며 이미 선언한 배열에는 사용할 수 없다.

2차원 배열의 요소에 접근하려면 배열 뒤에 [ ](대괄호)를 두 번 사용하여 안에 세로와 가로 인덱스를 지정하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
 
int main()
{
    int numArr[3][4= {
        {1,2,3,4},
        {5,6,7,8},
        {9,10,11,12}
    };
 
    printf("%d\n", numArr[0][0]);
    printf("%d\n", numArr[1][2]);
    printf("%d\n", numArr[2][0]);
    printf("%d\n", numArr[2][3]);
    return 0;
}
cs

세로크기가 3, 가로크기가 4인 int형 2차원 배열을 선언하고, 값을 초기화 했다.

2차원 배열도 가로, 세로 인덱스 모두 0부터 시작한다.

배열을 초기화 할때 가로 요소를 중괄호로 묶지 않아도 되지만 알아보기가 힘들어 잘 사용하진 않는다.

37.2 2차원 배열을 초기화하기

2차원 배열은 다음 코드로 모든 요소를 0으로 초기화 할 수 있다.

int numArr[3][4] = { 0, };

37.3 2차원 배열의 요소에 값 할당하기

다음 코드와 같이 값을 초기화 하지 않은 배열의 요소에 값을 할당할 수 있다.

int numArr[3][4];

numArr[0][1] = 5;
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
#include <stdio.h>
 
int main()
{
    int numArr[3][4];
 
    numArr[0][0= 11;
    numArr[0][1= 22;  
    numArr[0][2= 33;    
    numArr[0][3= 44;  
 
    numArr[1][0= 55;   
    numArr[1][1= 66;  
    numArr[1][2= 77;   
    numArr[1][3= 88;   
 
    numArr[2][0= 99;   
    numArr[2][1= 110;  
    numArr[2][2= 121;   
    numArr[2][3= 132;  
 
    printf("%d\n", numArr[-1][-1]);   
    printf("%d\n", numArr[0][4]);     
    printf("%d\n", numArr[4][0]);  
    printf("%d\n", numArr[5][5]);     
 
    return 0;
}
cs

2차원 배열도 범위를 벗어난 인덱스에 접근하여 출력하면 쓰레기값이 출력된다. 배열의 범위를 벗어난 인덱스에 접근하면 배열이 아닌 다른 메모리 공간에 접근하게 된다. 또한 [0][4]처럼 가로 인덱스만 범위를 벗어나도록 지정하면 그 다음 세로 인덱스 요소인 [1][0]에 접근된다.

37.4 2차원 배열의 크기 구하기

2차원 배열의 전체공간, 가로, 세로 요소의 개수는 다음과 같이 sizeof 연산자를 이용하여 구할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
 
int main()
{
    int numArr[3][4= {    
        { 11223344 },
        { 55667788 },
        { 99110121132 }
    };
 
    printf("%d\n"sizeof(numArr));  
 
    int col = sizeof(numArr[0]) / sizeof(int);                                    
    int row = sizeof(numArr) / sizeof(numArr[0]); 
 
    printf("%d\n", col);   
    printf("%d\n", row);   
    return 0;
}
cs

sizeof로 2차원 배열의 크기를 구하면 전체 공간을 구할 수 있다. 위에서는 4바이트 요소가 8개 있으므로 48이 나온다.

가로의 요소 개수는 가로 한 줄의 크기를 요소 자료형의 크기로 나누면 된다.

세로의 요소 개수는 배열의 전체 공간을 가로 한 줄의 크기로 나누면 된다.

37.5 반복문으로 2차원 배열의 요소를 모두 출력하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
 
int main()
{
    int numArr[3][4= {    
        { 11223344 },
        { 55667788 },
        { 99110121132 }
    };
 
    int col = sizeof(numArr[0]) / sizeof(int);  
    int row = sizeof(numArr) / sizeof(numArr[0]);
 
    for (int i = 0; i < row; i++)    
    {
        for (int j = 0; j < col; j++)    
        {
            printf("%d ", numArr[i][j]); 
        }
        printf("\n");                
    }
 
    return 0;
}
cs

배열의 세로와 가로 요소의 개수를 구하고, 2중 for문으로 세로부터 반복하고, 가로를 반복한다. 2차원 배열의 인덱스로는 세로에는 바깥쪽 반복문의 i를, 가로에는 안쪽 반복문의 j를 넣어 배열의 요소를 순서대로 접근하여 출력할 수 있다. 안쪽 반복문이 끝나면 개행문자를 출력함으로 세로줄이 바뀔때 줄을 바꿔 출력한다.

다음 코드와 같이 역순으로도 출력할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
 
int main()
{
    int numArr[3][4= {    
        { 11223344 },
        { 55667788 },
        { 99110121132 }
    };
 
    int col = sizeof(numArr[0]) / sizeof(int);  
    int row = sizeof(numArr) / sizeof(numArr[0]);
 
    for (int i = row - 1; i >= 0; i--)    
    {
        for (int j = col-1; j >= 0; j--)    
        {
            printf("%d ", numArr[i][j]); 
        }
        printf("\n");                
    }
 
    return 0;
}
cs

세로와 가로의 마지막 인덱스부터 0까지 인덱스를 1씩 감소시키면서 출력하면 된다.

37.6 2차원 배열을 포인터에넣기

2차원 배열을 포인터에 담으려면 가로의 크기를 알아야 한다. 다음과 같이 포인터를 선언할 때 *과 포인터 이름을 괄호로 묶은 뒤 []에 가로 크기를 지정하면 된다.

int (*numPtr)[4];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
 
int main()
{
    int numArr[3][4= {    
        { 11223344 },
        { 55667788 },
        { 99110121132 }
    };
    int (*numPtr)[4= numArr;
 
    printf("%p\n"*numPtr);
    printf("%p\n"*numArr);
    printf("%d\n", numPtr[1][2]);
    printf("%p\n"sizeof(numArr));
    printf("%p\n"sizeof(numPtr));
    return 0;
}
cs

위 코드와 같이 2차원 배열을 포인터에 할당할 수 있다.

2차원 배열을 포인터에 할당한 뒤 포인터를 역참조 해보면 세로 첫번째 요소의 주솟값이 나온다. 배열을 역참조해도 같은 값이나온다.

2차원 배열 포인터도 대괄호를 두 번 사용하여 배열의 요소에 접근할 수 있다.

sizeof로 크기를 계산하면 배열은 메모리가 차지하는 전체 공간이 출력되지만, 포인터는 포인터의 크기만 출력된다.

37.7 퀴즈

정답은 c이다.

정답은 d이다.

정답은 c이다.

정답은 b,d,e이다.

37.8 연습문제 : 행렬의 주대각선 성분 구하기

정답은 다음 코드와 같다.

for (int i = 0; i < sizeof(matrix) / sizeof(matrix[0]); i++)
{
    printf("%d ", matrix[i][i]);
}

인덱스 값이 (0,0), (1,1) ... 처럼 가로 세로 모두 1씩 증가하기 때문에 0~7까지 반복한 값을 가로, 세로 인덱스에 넣으면 된다.

37.9 심사문제 : 전치 행렬 구하기

정답은 다음 코드와 같다.

1
2
3
4
5
6
7
8
9
10
11
int col = sizeof(matrix[0]) / sizeof(int);  
int row = sizeof(matrix) / sizeof(matrix[0]);
 
for (int i = 0; i < row; i++)    
{
    for (int j = 0; j < col; j++)    
    {
        printf("%d ", matrix[j][i]); 
    }
        printf("\n");                
}
cs

행과 열이 서로 바뀌어있기 때문에 2차원 배열을 출력할 때 세로에 i를 넣고, 가로에 j를 넣으면 된다.

Unit 38. 포인터와 배열 응용하기

다음과 같이 프로그램 실행 중 원하는 크기 만큼 배열을 생성하는 기능은 gcc에서는 지원하지만 Viual Studio에서는 지원하지 않아 컴파일 에러가 발생한다.

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
 
int main()
{
    int size;
    scanf("%d"&size);  
 
    int numArr[size];    // 입력값을 배열의 크기로 사용
    return 0;
}
cs

컴파일 에러 없이 배열의 크기를 동적으로 지정하려면 포인터를 선언하고 메모리를 할당한 뒤 메모리를 배열처럼 사용해야 한다.

38.1 포인터에 할당된 메모리를 배열처럼 사용하기

포인터에 malloc 함수로 메모리를 할당하여 포인터를 배열처럼 사용할 수 있다.

자료형 *포인터이름 = malloc(sizeof(자료형) * 크기);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    int *numPtr = malloc(sizeof(int* 10);
    numPtr[0= 10;
    numPtr[9= 20;
 
    printf("%d\n", numPtr[0]);
    printf("%d\n", numPtr[9]);
    free(numPtr);
    return 0;
}
cs

위 코드와 같이 int 크기에 10을 곱하여 동적으로 메모리를 할당하여 배열처럼 사용할 수 있다. 배열 처럼 대괄호 안데 인덱스를 지정하여 값을 저장하거나 가져올 수 있다.

배열은 한번 선언하면 끝이지만 위와 같은 포인터는 malloc함수로 메모리를 할당했기 때문에 free함수로 해제해야 한다.

*numPtr처럼 역참조 한것과 numPtr[0]으로 0번 인덱스를 가져오는 것은 같은 값을 가져오고, *(numPtr + 1)은 numPtr[1]과 같은 값을 가져오게 된다. 포인터에 값을 더하는 방식을 포인터 연산 이라고 한다.

38.2 입력한 크기만큼 메모리를 할당하여 배열처럼 사용하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    int size;
    scanf("%d"&size);
 
    int *numPtr = malloc(sizeof(int* size);
 
    for(int i = 0; i < size; i++)
    {
        numPtr[i] = i;
    }
    for(int i = 0; i < size; i++)
    {
        printf("%d\n", numPtr[i]);
    }
    free(numPtr);
    return 0;
}
cs

scanf로 크기를 입력받고, int 크기에 입력받은 크기를 곱하여 메모리를 할당했다.

입력받은 크기 만큼 반복하며 값을 할당 하고, 입력받은 크기만큼 반복하며 값을 출력하였다.

사용이 끝나면 free함수로 할당한 메모리를 해제해야 한다.

38.3 포인터에 할당된 메모리를 2차원 배열처럼 사용하기

2차원 배열처럼 사용하기 위해 포인터에 할당하는 방법은 다음과 같다.

1. 자료형 **포인터이름 = malloc(sizeof(자료형 *) * 세로크기); 와 같이 세로 공간 메모리 할당

2. 반복문으로 반복하며 포인터[i] = malloc(sizeof(자료형) * 가로크기); 와 같이 가로 공간 메모리 할당

3. 반복문으로 반복하면서 free(포인터[i]); 와 같이 가로 공간 메모리 해제

4. 반복문으로 반복하면서 free(포인터); 와 같이 세로 공간 메모리 해제

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
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    int **= malloc(sizeof(int ** 3);
 
    for(int i = 0; i < 3; i++)
    {
        m[i] = malloc(sizeof(int* 4);
    }
 
    m[0][0= 1;
    m[2][0= 5;
    m[2][3= 2;
 
    printf("%d\n", m[0][0]);
    printf("%d\n", m[2][0]);
    printf("%d\n", m[2][3]);
 
    for(int i = 0; i < 3; i++)
    {
        free(m[i]);
    }
    free(m);
    return 0;
}
cs

2중 포인터에 2차원 배열의 세로 공간에 해당하는 메모리를 할당한다. 이때 세로 공간에는 값이 들어가지 않고 가로 공간의 메모리 주소가 들어간다. 그래서 sizeof(int *)처럼 포인터의 크기를 구한 후 세로 크기 3을 곱하는 것이다.

이후 세로 크기 만큼 반복하며 2차원 배열의 가로 공간에 해당하는 메모리를 할당한다.

가로 공간은 int 자료형의 크기에 가로크기 4를 곱해주었다.

이중 포인터를 2차원 배열처럼 사용하도록 메모리를 할당하는 모습을 그림으로 표현하면 다음과 같다.

m은 pointer to pointer to int이므로 int **m 처럼 선언한다.

2차원 배열을 사용하는 것처럼 [ ] [ ]에 세로인덱스, 가로인덱스를 지정하여 값을 할당하거나 가져올 수 있다.

포인터를 다 사용하면 가로 공간에 해당하는 메모리부터 해제한다. 이후 세로 공간에 해당하는 메모리를 해제한다.

메모리를 할당할 때는 세로 - > 가로의 순서로 할당했으므로 해제할 때는 가로 -> 세로의 순서로 해제하는 것이다.

38.4 입력한 크기만큼 메모리를 할당하여 포인터를 2차원 배열처럼 사용하기

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>
#include <stdlib.h>
 
int main()
{
    int row, col;
    scanf("%d %d"&row, &col);
 
    int **= malloc(sizeof(int ** row);
    for(int i = 0; i < row; i++)
    {
        m[i] = malloc(sizeof(int* col);
    }
 
    for(int i = 0; i < row; i++)
    {
        for(int j = 0; j < col; j++)
        {
            m[i][j] = i + j;
        }
    }
 
    for(int i = 0; i < row; i++)
    {
        for(int j = 0; j < col; j++)
        {
            printf("%d ", m[i][j]);
        }
        printf("\n");
    }
 
    for(int i = 0; i < row; i++)
    {
        free(m[i]);
    }
    free(m);
    return 0;
}
cs

세로 크기와 가로 크기를 입력 받고, 입력받은 값을 활용하여 세로공간과 가로공간 메모리를 할당하였다.

세로, 가로 크기가 고정되어 있지 않으므로 입력값 row, col을 활용하여 반복하면서 값을 할당하고, 값 할당이 끝나면 다시 반복하면서 2차원 배열의 값을 출력한다.

사용이 모두 끝나면 입력받은 세로 크기만큼 반복하며 가로 공간 메모리를 해제하고, 이후 세로 공간 메모리를 해제한다.

38.5 퀴즈

정답은 d이다.

정답은 b이다.

38.6 연습문제 : 포인터에 할당된 메모리를 3차원 배열처럼 사용하기

정답은 다음 코드와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1.
for(int depth = 0; depth < 2; depth++)
{
    m[depth] = malloc(sizeof(long long ** 3);
    
    for(int row = 0; row < 3; row++)
    {
        m[depth][row] = malloc(sizeof(long long* 5);
    }
}
 
// 2.
for(int depth = 0; depth < 2; depth++)
{
    for(int row = 0; row < 3; row++)
    {
        free(m[depth][row]);
    }
    free(m[depth]);
}
cs

3차원의 면에 해당하는 공간은 할당되어 있기 때문에 2중 for문으로 면의 개수만큼 반복하면서 세로공간에 해당하는 메모리를 할당하고, 안쪽 반복문에서 세로 크기만큼 반복하며 가로 공간에 해당하는 메모리를 할당하였다.

메모리를 해제할 때도 2차원 배열과 마찬가지로 할당의 역순으로 가로 -> 세로 -> 면 순서로 해제하면 된다.

38.7 심사문제 : 단위 행렬 만들기

정답은 다음 코드와 같다.

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
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    int size;
    scanf("%d"&size);
 
    int **= malloc(sizeof(int ** size);
    for(int i = 0; i < size; i++)
    {
        m[i] = malloc(sizeof(int* size);
    }
 
    for(int i = 0; i < size; i++)
    {
        for(int j = 0; j < size; j++)
        {
            if(i == j)
            {
                m[i][j] = 1;
            }
            else
            {
                m[i][j] = 0;
            }
        }
    }
 
    for(int i = 0; i < size; i++)
    {
        for(int j = 0; j < size; j++)
        {
            printf("%d ", m[i][j]);
        }
        printf("\n");
    }
 
    for(int i = 0; i < size; i++)
    {
        free(m[i]);
    }
    free(m);
    return 0;
}
cs

가로 인덱스와 세로 인덱스가 같은 위치는 1이고, 다른 위치는 모두 0이므로 2차원 배열에 값을 할당하는 부분에서 if 조건문으로 해당 조건에 맞게 값을 저장하였다.

38.8 심사문제 : 지뢰찾기

정답은 다음 코드와 같다.

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
#define _CRT_SECURE_NO_WARNINGS 
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    int m, n;
    int cnt = 0;
    scanf("%d %d"&m, &n);
 
    // 메모리 할당
    char **matrix = malloc(sizeof(char ** m);
    for(int i = 0; i < m; i++)
    {
        matrix[i] = malloc(sizeof(char ** (n+1));
    }
 
    // 입력
    for(int i = 0; i < m; i++)
    {
        scanf("%s", matrix[i]);
    }
    
    for (int i = 0; i < m; i++
    {
        for (int j = 0; j < n; j++
        {
            if (matrix[i][j] == '.'
            {
                matrix[i][j] = '0';
            }
        }
    }
    for(int i = 0; i < m; i++)
    {
        for(int j = 0; j < n; j++)
        {
            // 지뢰일 경우
            if(matrix[i][j] == '*')
            {
                for(int y = i - 1; y < i + 2; y++)
                {
                    for(int x = j - 1; x < j + 2; x++)
                    {
                        if(y < 0 || x < 0 || y >= m || x >= n || matrix[i][j] == matrix[y][x] || matrix[y][x] == '*')
                        {
                            continue;
                        }
                        matrix[y][x] += 1;
                    }
                }
            }
        }
        
    }
    
    // 결과 출력
    for (int i = 0; i < m; i++) {
        for(int j = 0; j < n; j++)
        {
            printf("%c", matrix[i][j]);
        }
        printf("\n");
    }
    // 메모리 해제
    for (int i = 0; i < m; i++
    {
        free(matrix[i]);
    }
    free(matrix);
    return 0;
}
cs

행렬 크기와 행렬을 입력받고, 각 요소별로 반복하면서 '.' 인 경우 '0' 으로 바꿔 주었다.

각 요소별로 반복하면서 요소가 지뢰일 경우 그 요소를 둘러싸고 있는 요소들에 1씩 추가해 줬는데, 조건문을 사용하여 행렬을 벗어나지 않고, 둘러싸고 있는 요소가 지뢰가 아닐 경우에만 1씩 누적하였다.

반복문의 실행이 끝나면 다시 반복문으로 수정된 행렬의 결과를 출력하고 메모리를 해제하였다.

Unit 35. 메모리 사용하기

포인터에 원하는 만큼 메모리 공간을 할당받아 사용할 수 있다.

메모리는 malloc -> 사용 -> free의 패턴으로 사용한다.

35.1 메모리 할당하기

메모리를 사용하려면 malloc 함수로 사용할 메모리 공간을 확보해야 한다. 할당할 메모리 크기는 바이트 단위로 지정한다. malloc함수는 stdlib.h 헤더파일에 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    int num1 = 20;
    int *numPtr1;
 
    numPtr1 = &num1;
 
    int *numPtr2;
    numPtr2 = malloc(sizeof(int));
 
    printf("%p\n", numPtr1);
    printf("%p\n", numPtr2);
    
    free(numPtr2);
    return 0;
}
cs

메모리를 할당할 때는 malloc 함수를 사용하여 할당할 메모리 공간의 크기를 넣어주는데, 위 코드에서는 sizeof 연산자를 이용하여 int크기 만큼의 즉 4바이트 크기의 메모리를 할당했다.

이처럼 원하는 시점에 원하는 메모리를 할당할 수 있어 동적 메모리 할당이라 부른다.

출력값은 같은 메모리 주소를 출력하는것 같지만 메모리 주소에는 내부적인 차이가 있다. 변수는 스택에 생성되며 malloc함수는 힙부분의 메모리를 사용한다.

스택에 생성된 변수는 사용한뒤 따로 처리를 하지 않아도 되지만 malloc함수를 사용하여 힙에 할당한 메모리는 반드시 해제를 해야 한다. free 함수를 이용하여 메모리를 해제할 수 있다.

메모리를 해제하지 않으면 시스템의 메모리가 부족해져 운영체제가 프로그램을 강제로 종료시키거나 메모리 할당에 실패할 수 있다. 메모리를 해제하지 않아 메모리 사용량이 계속 증가하는 현상을 메모리 누수(memory leak)라 한다.

35.2 메모리에 값 저장하기

다음은 할당한 메모리에 값을 저장하는 코드이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
 
int main()
{
    int *numPtr;
    numPtr = malloc(sizeof(int));
    *numPtr = 10;
 
    printf("%d\n"*numPtr);
    free(numPtr);
    return 0;
}
cs

malloc 함수로 할당한 메모리에 값을 저장할 때는 포인터를 역참조한 뒤 값을 저장하면 된다. 값을 출력할 때도 역참조를 사용하면 된다. 메모리를 할당하고 사용한 뒤에는 반드시 free함수로 해제를 해 줘야 한다.

35.3 메모리 내용을 한꺼번에 설정하기

memset 함수를 사용하면 메모리의 내용을 원하는 크기 만큼 특정 값으로 설정할 수 있다. 설정하는 크기는 바이트 단위이며 이 함수는 string.h 헤더파일에 선언되어 있다. memset 함수는 다음과 같이 사용한다.

memset(포인터, 설정할값, 크기);

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main()
{
    long long *numPtr = malloc(sizeof(long long));
    memset(numPtr, 0x278);
 
    printf("0x%llx"*numPtr);
    free(numPtr);
    return 0;
}
cs

long long 자료형의 크기인 8바이트 만큼 동적 메모리를 할당하고, memset 함수로 numPtr이 가리키는 메모리에 16진수 27이 8개 들어가도록 한다.

memset함수는 다음과 같이 설정할 값을 모두 0으로 지정하여 메모리의 내용을 모두 0으로 만들 때 주로 사용한다.

memset(numPtr, 0, 8); 

35.4 널 포인터 사용하기

1
2
3
4
5
6
7
8
9
#include <stdio.h>
 
int main()
{
    int *numPtr1 = NULL;  
 
    printf("%p\n", numPtr1); 
    return 0;
}
cs

위 코드와 같이 NULL이 들어있는 포인터를 널 포인터라고 하며 아무것도 가리키지 않는 상태이다. 역참조도 할 수 없다.

실무에서는 다음과 같이 포인터가 NULL인지 확인하고, NULL이면 메모리를 할당하는 방식을 주로 사용한다.

if (ptr == NULL)      
{
    ptr = malloc(1024);  
}

35.5 퀴즈

정답은 d이다.

정답은 c이다.

정답은 b이다.

정답은 a이다.

정답은 d이다.

정답은 널 포인터 이다.

35.6 연습문제 : 메모리 할당하기

정답은 다음과 같다.

1. malloc(sizeof(int));

2. malloc(sizeof(long long));

각각 4바이트와 8바이트 만큼의 메모리를 할당해 줘야 하기 때문이다.

35.7 심사문제 : 두 정수의 합 구하기

정답은 다음 코드와 같다.

1
2
int *numPtr1 = malloc(sizeof(int));
int *numPtr2 = malloc(sizeof(int));
cs

Unit 36. 배열 사용하기

배열은 같은 자료형의 변수를 일렬로 늘어놓은 형태이다.

반복문과 결합하면 연속적이고 반복되는 값을 손쉽게 처리할 수 있다.

36.1 배열을 선언하고 요소에 접근하기

배열은 변수 이름 뒤에 [](대괄호)를 붙인 뒤 크기를 설정한다. 배열을 선언하며 값을 초기화 할 때는 {}(중괄호)를 사용한다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main()
{
    int numArr[10= { 112233445566778899110 };    
 
    printf("%d\n", numArr[0]);    
    printf("%d\n", numArr[5]);    
    printf("%d\n", numArr[9]);    
    return 0;
}
cs

크기가 10인 int형 배열을 선언하며 값을 할당하였다. 값을 할당할 때 {} 안의 값 개수는 배열의 크기보다 작아도 되만 크면 안된다. 또한 {}를 사용하여 값을 할당하는 것은 배열을 선언할 때만 사용할 수 있으며 이미 선언된 배열에서는 사용할 수 없다.

배열 값이 저장된 각각의 공간을 요소라고 하며 요소에 접근할 때는 배열 뒤에 [](대괄호)를 사용하고, 대괄호 안에 각 요소의 인덱스를 지정한다. 배열의 인덱스는 항상 0부터 시작한다.

배열을 선언하며 값을 초기화하면 다음과 같이 배열의 크기를 생략할 수 있다. 초기화를 하지 않고 생략하면 컴파일 에러가 발생한다.

int numArr1[] = { 11, 22, 33, 44, 55, 66, 77, 88, 99, 110 }; 

36.2 배열을 0으로 초기화하기

다음과 같이 한번에 배열의 모든 요소를 0으로 초기화 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main()
{
    int numArr[10= {0, };    
 
    printf("%d\n", numArr[0]);    
    printf("%d\n", numArr[5]);    
    printf("%d\n", numArr[9]);    
    return 0;
}
cs

배열의 모든 요소를 한 번에 0으로 초기화하여 0을 일일이 나열하지 않아도 된다.

36.3 배열의 요소에 값 할당하기

배열은 []로 요소에 접근한 뒤 값을 할당할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
 
int main()
{
    int numArr[3];
 
    numArr[0= 1;
    numArr[1= 22;
    numArr[2= 8;
 
    printf("%d\n", numArr[0]); 
    printf("%d\n", numArr[1]); 
    printf("%d\n", numArr[2]);  
    return 0;
}
cs

인덱스는 0부터 시작하며 []에 인덱스를 지정한 뒤 값을 할당하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
 
int main()
{
    int numArr[3];
 
    numArr[0= 1;
    numArr[1= 22;
    numArr[2= 8;
 
    printf("%d\n", numArr[-1]); 
    printf("%d\n", numArr[5]); 
    printf("%d\n", numArr[10]);  
    return 0;
}
cs

위 코드와 같이 배열의 요소에 접근할 때 인덱스로 음수를 지정하거나, 배열의 크기를 벗어난 인덱스를 지정해도 컴파일에러는 발생하지 않지만, 쓰레기값을 출력한다. 배열의 범위를 벗어난 인덱스에 접근하면 배열이 아닌 다른 메모리 공간에 접근하여 엉뚱한 값을 출력하게 된다.

36.4 배열의 크기 구하기

반복문을 이용해서 배열의 요소를 모두 출력할 때 선언된 크기로 사용하면 나중에 배열의 크기를 늘려야 해서 코드를 수정해야 할 경우 반복문의 조건식도 수정해야 모든 배열이 출력될 수 있다. 이러한 불편함을 해결하려면 sizeof연산자를 사용하여 배열의 전체 요소의 개수를 구해 배열의 크기가 수정되면 반복문의 조건식에서도 알아서 계산되도록 할 수 있다.

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
 
int main()
{
    int numArr[10= { 112233445566778899110 };    
 
    printf("%d\n"sizeof(numArr));                  
    printf("%d\n"sizeof(numArr) / sizeof(int));                                                     
    return 0;
}
cs

sizeof연산자로 numArr의 크기를 구하면 4바이트인 int형 요소가 10개 있으므로 40이 나온다. 요소의 개수를 구하려면 배열의 크기에서 요소의 크기(sizeof(int))로 나눠주면 된다.

36.5 반복문으로 배열의 요소를 모두 출력하기

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
 
int main()
{
    int numArr[10= { 112233445566778899110 };    
 
    for (int i = 0; i < sizeof(numArr) / sizeof(int); i++)   
    {
        printf("%d\n", numArr[i]);   
    }
    return 0;
}
cs

반복문의 조건식에서 sizeof 연산자를 사용하여 배열의 요소 개수만큼 반복하였다.

변수 i는 변화식을 통해 1씩 증가하므로 인덱스에 i를 넣어서 모든 배열의 요소를 순서대로 출력할 수 있다.

다음과 같이 배열의 요소를 역순으로 출력할 수 도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
 
int main()
{
    int numArr[10= { 112233445566778899110 };    
 
    for (int i = sizeof(numArr) / sizeof(int- 1; i >= 0; i--)  
    {
        printf("%d\n", numArr[i]);   
    }
    return 0;
}
cs

배열을 역순으로 출력할 때는 요소의 개수를 조건식에 바로 넣으면 인덱스는 0부터 시작하기 때문에 배열의 마지막 인덱스는 배열의 개수보다 1 작아서 반복문의 조건식에서도 요소의 개수에서 1 뺀 값에서 시작해야 한다. 조건식에서 0보다 크거나 같을 때 까지 반복하여 역순으로 출력할 수 있다.

36.6 배열의 요소 합계 구하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
 
int main()
{
    int numArr[10= { 112233445566778899110 };
    int sum = 0;    
 
    for (int i = 0; i >= sizeof(numArr) / sizeof(int); i++)  
    {
        sum += numArr[i];
    }
    printf("%d\n", sum);
    return 0;
}
cs

위 코드와 같이 합을 저장할 변수를 만들고 배열의 요소만큼 반복하면서 배열의 각 요소의 값을 만든 변수에 누적시킨다. 저장할 변수를 만들때는 0으로 초기화 하지 않으면 쓰레기값이 들어간다.

36.7 배열의 요소에 저장된 값을 두 배로 만들기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
 
int main()
{
    int numArr[10= { 112233445566778899110 }; 
 
    for (int i = 0; i < sizeof(numArr) / sizeof(int); i++
    {
        numArr[i] *= 2;
    }
 
    for (int i = 0; i < sizeof(numArr) / sizeof(int); i++
    {
        printf("%d\n", numArr[i]);
    }
    
    return 0;
}
cs

배열의 요소에 저장된 값을 두 배로 만드려면 배열의 요소에 접근하여 2로 곱한 뒤 다시 요소에 저장하면 된다. *= 연산자를 사용하고, 반복문으로 모든 요소에 접근하여 값을 두배로 변경하고 출력했다.

36.8 배열을 초인터에 넣기

배열은 첫 번째 요소의 주솟값만 담고 있다. 배열은 주솟값이기 때문에 포인터에 넣을 수 있다. 다음과 같이 포인터에 배열을 넣고 포인터에서 인덱스로 요소에 접근할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
 
int main()
{
    int numArr[10= { 112233445566778899110 };
    int *numPtr = numArr;
 
    printf("%d\n"*numPtr);
    printf("%d\n"*numArr);
    printf("%d\n", numPtr[5]);
    printf("%d\n"sizeof(numArr));
    printf("%d\n"sizeof(numPtr));
    return 0;
}
cs

위 코드처럼 배열을 포인터에 바로 할당할 수 있다. 자료형이 같아야 하며 1차원 배열이면 *이 한개인 단일 포인터여야 한다. 배열을 포인터에 할당한뒤 포인터를 역참조하면 배열의 첫 번째 요소가 나온다. 배열도 주솟값을 가진 포인터이기 때문에 역참조 할 수 있다.

배열의 주소가 들어있는 포인터는 인덱스를 통해 요소에 접근할 수 있다.

sizeof 연산자를 사용하면 배열은 전체 공간의 크기가 출력되고, 포인터는 배열의 주소가 들어있는 포인터의 크기만 나온다. 운영체제가 32비트면 4, 64비트면 8이 나온다.

36.9 배열을 활용하여 10진수를 2진수로 변환하기

10진수를 2진수로 변환하는 방법은 다음과 같이 10진수를 0이 될 때 까지 2로 계속 나눈 뒤 나머지를 역순으로 읽으면 2진수가 된다.

이 식을 코드로 변환하면 다음과 같다.

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 <stdio.h>
 
int main()
{
    int dec = 13;         
    int bin[20= {0, };
    int pos = 0;
 
    while(1)
    {
        bin[pos] = dec % 2;
        dec = dec / 2;
        pos++;
 
        if(dec == 0)
            break;
    }
 
    for(int i = pos-1; i >= 0; i--)
    {
        printf("%d", bin[i]);    
    }
 
    return 0;
}
cs

변환할 10진수 13을 변수 dec에 저장하고, 2진수를 저장할 배열 bin을 선언한다.

배열에 나머지를 구해서 저장하고 10진수를 2로 나눈 것을 dec가 0이 될 때까지 무한 반복한다.

가장 아래의 나머지부터 읽어야 하므로 배열을 출력할 때는 요소를 역순으로 출력해야 한다.

36.10 퀴즈

정답은 c이다.

정답은 d이다.

정답은 e 이다.

정답은 a, e이다. 배열의 범위는 0~99인데 범위를 벗어난 인덱스를 접근했다.

36.11 연습문제 : 점수 평균 구하기

정답은 다음 코드와 같다.

1. sum += scores[i];

2. average = sum / ( sizeof(scores) / sizeof(float) );

점수 합계를 구할 때는 배열의 모든 요소를 더해주면 되고 평균을 구할 때는 합계에서 요소의 개수로 나누면 된다.

36.12 연습문제 : 2진수를 10진수로 변환하기

정답은 다음 코드와 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
int pos = 0;
 
for(int i = sizeof(binary) / sizeof(int) - 1; i >= 0; i--)
 
{
 
    if(binary[i] == 1)
 
        decimal += 1 << pos;
 
    pos++;
 
}
cs

2진수를 10진수로 변환하는 방법은 2진수의 각 자리수 1이면 비트 연산자로 해당 자리에 1을 위치시키면 된다.

36.13 심사문제 : 2진수를 10진수로 변환하기

정답은 다음 코드와 같다.

1
2
3
4
5
6
smallestNumber = numArr[0];
for(int i = 0; i <= 4; i++)
{
    if(smallestNumber > numArr[i])
        smallestNumber = numArr[i];
}
cs

Unit 34. 포인터 사용하기

값을 저장할 때 사용하는 변수는 컴퓨터의 메모리에 생성된다. 메모리에 일정한 공간을 확보해 두고 원하는 값을 저장하거나 가져오는 방식이다.

변수는 num과 같이 지정된 이름으로 사용되지만, 메모리의 특정 장소에 위치하므로 메모리 주로로도 표현할 수 있다.

메모리 주소는 다음과 같이 출력할 때변수 앞에 &을 붙이면 된다.

1
2
3
4
5
6
7
8
#include <stdio.h>
 
int main()
{
    int num1 = 10;
    printf("%p\n", &num1);
    return 0;
}
cs

메모리주소는 위와 같이 16진수 형태이며 서식지정자 %p를 사용하여 출력한다. 16진수를 의미하는 %x나 %X를 이용해도 된다. 메모리주소는 고정된 값이 아니기 때문에 컴퓨터마다, 실행할 때 마다 달라진다.

시스템이 32비트인지, 64비트인지에 따라 메모리 주소의 범위도 달라진다. 각각의 범위는 다음과 같다.

  • 32비트: 16진수 8자리

  • 0x00000000 ~ 0xFFFFFFFF

  • 예) 0x008AF7FC

  • 64비트: 16진수 16자리

  • 0x0000000000000000 ~ 0xFFFFFFFFFFFFFFFF

  • 예) 0x00000000008AF7FC

  • 64비트 메모리 주소는 0x00000000`00000000처럼 8자리씩 끊어서 `를 붙이기도 한다

리눅스나 맥의 osX에서는 서식지정자 %p를 사용하며 주소 앞에 0x를 붙이고, a~f는 소문자로 출력되며 높은 자리수의 0은 생략된다.

 

34.1 포인터 변수 선언하기

C에서 메모리 주소는 포인터 변수에 저장한다.

포인터 변수는 다음과 같이 *을 이용하여 선언한다.

자료형 *포인터이름;
포인터 = &변수;
1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
 
int main()
{
    int *numPtr;      
    int num1 = 10;   
    numPtr = &num1;  
 
    printf("%p\n", numPtr);   
    printf("%p\n"&num1);     
    return 0;
}
cs

포인터 변수를 선언할 때는 자료형 뒤에 *(Asterisk, 애스터리스크)를 붙인다. *위치는 자료형과 변수 사이에 있다면 어디에 있던 상관 없다.

&로 변수의 주소를 구해서 포인터 변수에 저장할 수도 있다.

printf로 포인터 변수 numPtr을 출력해보면 num1의 메모리 주소와 포인터 변수는 같은 값이 나온다. 포인터와 메모리 주소는 같은 의미이다.

포인터 변수를 선언할 때는 자료형을 알려주고 *을 붙여야 한다. 만약 변수가 int형이면 이 변수를 저장하는 포인터는 int *로 지정해야 한다.

포인터는 메모리의 특정 위치를 가리킬 때 사용한다. 포인터가 메모리를 가리키는것을 그림으로 확인하면 다음과 같다.

 

34.2 역참조 연산자 사용하기

포인터 변수에는 메모리 주소가 저장되어 있는데, 이 포인터 주소를 이용하여 값을 가져오고 싶으면 역참조 연산자 *을 사용하면 된다.

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
 
int main()
{
    int *numPtr;      
    int num1 = 10;  
    numPtr = &num1; 
    
    printf("%d\n"*numPtr);    // 역참조 연산자 사용
    return 0;
}
cs

역참조 연산자 *는 포인터 앞에 붙인다. 위와 같이 포인터 변수 앞에 *을 붙이면 그 변수에 저장된 메모리 주소로 가서 값을 가져온다.
다음은 포인터 변수에 역참조 연산자를 사용하여 값을 할당한 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
 
int main()
{
    int *numPtr;      
    int num1 = 10;   
    numPtr = &num1;  
    *numPtr = 20;    
 
    printf("%d\n"*numPtr);    
    printf("%d\n", num1);       
    return 0;
}
cs

역참조 연산자로 메모리에 접근하여 값을 저장할 수도 있다. 위 num1 변수에는 10이 들어있었는데 num1을 가리키는 포인터변수 numPtr에 역참조 연산자로 20을 저장했기 때문에 num1 과 numPtr을 출력하면 둘 다 20이 나온다.
만약 포인터 변수 numPtr에 num1을 할당하면 간접 참조 수준이 다르다는 경고가 발생한다. numPtr과 num1이 각각 포인터와 int로 자료형이 다르기 때문이다. 컴파일 경고가 발생하지 않도록 하려면 numPtr앞에 * 을 붙여 값을 가져오게 하여 int와 동일한 자료형으로 만들어야 한다. 값을 가져오는것은 자료형을 동일하게 한다는 것이다. 주소연산자 &도 마찬가지로 자료형을 맞춰주는 역할을 한다.
변수, 주소 연산자, 역참조 연산자, 포인터의 차이는 다음과 같다.

 

34.3 디버거에서 포인터 확인하기

디버거를 사용하면 변수의 메모리 주소, 포인터, 역참조를 쉽게 확인할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
 
int main()
{
    int *numPtr;     
    int num1 = 10;    
 
    numPtr = &num1;   
    *numPtr = 20;    
 
    printf("%d\n"*numPtr);    
    printf("%d\n", num1);      
    return 0;
}
cs

9번째 줄에 중단점을 설정하고 실행하면 다음과 같다.

위 코드에서 8번째 줄까지 실행한 상태이다. 포인터 변수 numPtr에 num1의 주소가 저장되있다. Locals 창에서 > 을 클릭하면 현재 메모리 주소에 저장된 값을 확인할 수 있다.

맥 OS에서 메모리 주소의 내용을 확인하려면 Xcode에서 디버깅 하면 된다. 

Xcode에서 9번째 줄과 10번째 줄에 중단점을 지정하고 한번 디버깅 버튼을 클릭하면 8번째 줄까지 실행되어 numPtr은 num1의 주소를 가리킨다.

이때 메모리 주소의 내용을 확인하려면 numPtr을 클릭하고 우클릭을 눌러 view memory of *numPtr을 클릭하면 된다.

num1은 int 자료형이기 때문에 0A 00 00 00 처럼 숫자 4개를 차지한다. 리틀 엔디언 방식으로 저장되기 때문에 값이 거꾸로 저장되어 원래는 00 00 00 0A 이며 0A는 10진수로 10이다. 

다음줄을 실행시키면 메모리 주소의 내용이 다음과 같이 바뀐다.

0A가 14로 바뀌었다. 16진수 14는 10진수로 20이다. 역참조후 20을 할당하여 메모리의 내용이 바뀐 것을 확인할 수 있다.

 

 

34.4 다양한 자료형의 포인터 선언하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
 
int main()
{
    long long *numPtr1;    
    float *numPtr2;        
    char *cPtr1;  
 
    long long num1 = 10;
    float num2 = 3.5f;
    char c1 = 'a';
 
    numPtr1 = &num1;    
    numPtr2 = &num2;    
    cPtr1 = &c1;       
 
    printf("%lld\n"*numPtr1);    
    printf("%f\n"*numPtr2);    
    printf("%c\n"*cPtr1);       
    return 0;
}
cs

C에서 사용할 수 있는 모든 자료형은 포인터로 만들 수 있다.

포인터에서 저장되는 메모리 주솟값은 정수형으로 동일하지만 선언하는 자료형에 따라 메모리에 접근하는 방법이 달라진다. 다음과 같이 포인터를 역참조 할 때 자료형의 크기에 맞게 값을 가져오거나 저장한다.

상수와 포인터

포인터에도 const 키워드를 붙일 수 있는데 const의 위치에 따라 특성이 달라진다.

1
2
3
4
5
const int num1 = 10;    
const int *numPtr;      
 
numPtr = &num1;
*numPtr = 20; //컴파일에러
cs

num1 이 const int 이므로 num1의 주소를 넣을 포인터도 const int로 선언해야 한다. 역참조 연산자로 값을 변경하려 해도 num1은 상수이기 때문에 컴파일 에러가 발생한다.

다음은 포인터 자체가 상수인 상황이다.

1
2
3
4
5
int num1 = 10;  
int num2 = 20;    
int * const numPtr = &num1;    
 
numPtr = &num2;    // 컴파일 에러
cs

numPtr은 포인터 자체가 상수이기 때문에 상수 포인터에는 다른 변수의 메모리 주소를 할당할 수 없다.

다음은 상수 포인터가 상수를 가리키는 상황이다.

1
2
3
4
5
6
const int num1 = 10;    
const int num2 = 20;   
const int * const numPtr = &num1;    
 
*numPtr = 30;      // 컴파일 에러
numPtr = &num2;    // 컴파일 에러
cs

포인터를 역참조한 값을 변경하려해도 해당 주소가 가리키는 것은 상수이기 때문에 컴파일 에러가 불가능하며 포인터도 상수 이므로 메모리 주소도 변경할 수 없다.

 

34.5 void포인터 선언하기

void 포인터는 자료형이 정해져있지 않은 포인터이다. 다음과 같이 사용한다.

void *포인터이름;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
 
int main()
{
    int num1 = 10;
    char c1 = 'a';
    int *numPtr1 = &num1;
    char *cPtr1 = &c1;
 
    void *ptr;        
 
    
    ptr = numPtr1;    
    ptr = cPtr1;     
 
   
    numPtr1 = ptr;   
    cPtr1 = ptr;      
 
    return 0;
}
cs

기본적으로 C는 자료형이 다른 포인터끼리 메모리 주소를 저장하면 컴파일 경고를 발생하지만 void 포인터는 어떤 자료형의 포인터든 모두 저장할 수 있다. 이런 특성 때문에 void 포인터를 범용 포인터라고 부르기도 한다.

void 포인터는 자료형이 정해지지 않았으므로 값을 가져오거나 저장할 크기도 정해지지 않았기 때문에 역참조를 하게 되면 컴파일 에러가 발생한다.

또한 void키워드로 변수를 선언할 수도 없다.

void포인터는 함수에서 다양한 자료형을 받아들일 때, 함수의 반환 포인터를 다양한 자료형으로 된 포인터를 저장할 때, 자료형을 숨기고 싶을 때 사용한다.

 

34.6 이중 포인터 사용하기

포인터의 메모리주소를 저장하는 포인터의 포인터도 선언할 수 있다. 이것을 이중 포인터 라고 하며 *을 두 번 사용한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
 
int main()
{
    int *numPtr1;    
    int **numPtr2;    
    int num1 = 10;
 
    numPtr1 = &num1;  
    numPtr2 = &numPtr1; 
 
    printf("%d\n"**numPtr2);  
    return 0;
}
cs

포인터도 변수이기 때문에 포인터를 저장하고 있는 메모리 주소도 구할 수 있다. 포인터의 메모리 주소는 일반 포인터에는 저장할 수 없고 **을 사용한 이중 포인터에 저장해야 한다.

이중포인터를 끝까지 따라가서 실제 값을 가져오려면 변수 앞에 역참조 연산자를 두 번 사용하면 된다.

포인터를 선언할 때 *의 개수에 따라서 삼중포인터, 사중포인터 등도 만들 수 있다.

 

34.7 잘못된 포인터 사용

포인터는 메모리 주소를 저장하는 용도이므로 다음과 같이 값을 직접 저장하면 안된다.

 int *numPtr = 0x100; 

메모리에서 0x100은 잘못된 주솟값이기 때문이다.

위와 같이 할당할 때 까지는 에러는 발생하지 않지만 역참조로 메모리 주소를 접근할 때 에러가 발생한다. 운영체제는 프로그램이 잘못된 메모리 주소에 접근했을 때 에러를 발생시킨다.

만약 실제 존재하는 메모리 주소라면 포인터에 직접 저장할 수 있다.


34.8 퀴즈

정답은 c이다.

정답은 d이다.

정답은 a이다.

정답은 e이다.

정답은 d이다.

 

34.9 연습문제 : 포인터와 주소 연산자 사용하기

정답은 다음과 같다.

1. numPtr = &num1;

2. numPtr = &num2;

 

34.10 심사문제 : 포인터와 주소 연산자 사용하기

정답은 다음 코드와 같다.

1
2
numPtr1 = &num1;
numPtr2 = &numPtr1;
cs

 

'Language > C, C++' 카테고리의 다른 글

[C++] Reference(참조자)  (0) 2021.08.21
[C++] 입출력  (0) 2021.08.21
[C] 문자 단위 입출력 함수  (0) 2021.01.15
[C] 스트림  (0) 2021.01.15
[C] main 함수로의 인자 전달  (0) 2021.01.09

+ Recent posts