문제에 접속하여 flag 파일을 읽으려하면 권한 문제로 읽히지 않는다.

input파일을 실행해보면 다음과 같이 출력이 나온다.

또한 input.c 소스코드를 보면 다음과 같다.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69
#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <sys/socket.h>

#include <arpa/inet.h>



int main(int argc, char\\\\\\\\\\* argv[], char\\\\\\\\\\* envp[]){

printf("Welcome to pwnable.kr\n");

printf("Let's see if you know how to give input to program\n");

printf("Just give me correct inputs then you will get the flag :)\n");



// argv

if(argc != 100) return 0; // 인자가 100이 아니면

if(strcmp(argv['A'],"\x00")) return 0;

if(strcmp(argv['B'],"\x20\x0a\x0d")) return 0;

printf("Stage 1 clear!\n");



// stdio

char buf[4];

read(0, buf, 4);

if(memcmp(buf, "\x00\x0a\x00\xff", 4)) return 0;

read(2, buf, 4);

if(memcmp(buf, "\x00\x0a\x02\xff", 4)) return 0;

printf("Stage 2 clear!\n");



// env

if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;

printf("Stage 3 clear!\n");



// file

FILE\\\\\\\\\\* fp = fopen("\x0a", "r");

if(!fp) return 0;

if( fread(buf, 4, 1, fp)!=1 ) return 0;

if( memcmp(buf, "\x00\x00\x00\x00", 4) ) return 0;

fclose(fp);

printf("Stage 4 clear!\n");



// network

int sd, cd;

// 소켓 구조체

struct sockaddr\\\\\\\\\\_in saddr, caddr;

sd = socket(AF\\\\\\\\\\_INET, SOCK\\\\\\\\\\_STREAM, 0);

if(sd == -1){

printf("socket error, tell admin\n");

return 0;

}

saddr.sin\\\\\\\\\\_family = AF\\\\\\\\\\_INET;

saddr.sin\\\\\\\\\\_addr.s\\\\\\\\\\_addr = INADDR\\\\\\\\\\_ANY;

// port 정보

saddr.sin\\\\\\\\\\_port = htons( atoi(argv['C']) );

if(bind(sd, (struct sockaddr\\\\\\\\\\*)&saddr, sizeof(saddr)) < 0){

printf("bind error, use another port\n");

return 1;

}

listen(sd, 1);

int c = sizeof(struct sockaddr\\\\\\_in);

cd = accept(sd, (struct sockaddr \\\\\\\\\\*)&caddr, (socklen\\\\\\\\\\_t\\\\\\\\\\*)&c);

if(cd < 0){

printf("accept error, tell admin\n");

return 0;

}

if( recv(cd, buf, 4, 0) != 4 ) return 0;

if(memcmp(buf, "\xde\xad\xbe\xef", 4)) return 0;

printf("Stage 5 clear!\n");



// here's your flag

system("/bin/cat flag");

return 0;

}

Colored by Color Scripter
cs

 

//argv로 주석이 되어 있는 부분의 코드를 보면 인자의 크기가 100이 아니면, 인자의 'A'인덱스 즉 65번째 인덱스가 "\x00"가 아니고, 인자의 'B' 인덱스 즉 66번째 인덱스가 "\x20\x0a\x0d"가 아니면 프로그램을 종료하고, 위 조건들을 만족시키는 경우 Stage 1 clear을 출력한다.

 

//stdio로 주석이 되어 있는 부분의 코드를 보면 read 함수로 buf를 읽어오는데, 0(stdin)이 "\x00\x0a\x00\xff"이 아닐 경우, 2(stderr)이 "\x00\x0a\x02\xff"가 아닐 경우 조건을 프로그램을 종료하고, 조건들을 만족시킬 경우 Stage 2 clear을 출력한다.

 

//env로 주석이 되어 있는 부분의 코드를 보면 getenv()함수로 환경변수 "\xde\xad\xbe\xef"의 값이 "\xca\xfe\xba\xbe"가 아닐 경우 프로그램을 종료하고, 조건을 만족하면 Stage 3 clear을 출력한다.

 

//file로 주석이 되어 있는 부분의 코드를 보면 fopen()함수로 "\x0a"라는 이름의 파일을 열고 파일을 읽어 파일의 내용이 "\x00\x00\x00\x00"가 아니면 프로그램을 종료하고, 조건을 만족하면 Stage 4 clear을 출력한다.

 

//network로 주석이 되어 있는 부분의 코드를 보면 sockaddr_in 구조체를 사용하고 있고, 소켓 생성에 실패(socket 함수 -1 반환)하면 socket error을 출력하고 프로그램을 종료한다. saddr의 포트 정보에 인자의 'C' 인덱스 즉 67번째 인덱스의 값을 넣고, bind 함수로 소켓 통신할 준비를 해준다. 이 과정에서 에러가 발생할 경우 bind error use another port를 출력하고 프로그램을 종료한다. accept()함수로 소켓을 연결하고, 에러가 발생할 경우(return -1) accept error tell admin을 출력하고 프로그램을 종료한다. 이후 소켓에서 4바이트를 받아와서 받아온 값이 "\xde\xad\xbe\xef"가 맞다면 Stage 5 clear을 출력한다.

 

위 5가지의 조건을 모두 만족시키면 플래그를 읽을 수 있다.

 

문제를 해결하기 위해 pwntools를 이용하여 파이썬 코드로 작성하였다.

첫번째 조건을 만족시키기 위해 인자값을 조건에서 주어진 대로 설정하고, 인자를 전달하려면 process 함수의 argv 옵션을 이용하여 전달할 수 있다.

 

두 번째 조건을 만족시키기 위해 stdin으로는 "\x00\x0a\x00\xff"를 전달하였고, stderr일 경우 전달하는 파일을 만들어 "\x00\x0a\x02\xff"을 저장하여 process함수의 stderr옵션을 이용하여 전달하였다.

 

세 번째 조건을 만족시키기 위해 process함수의 env 옵션을 이용하여 환경변수를 설정하였다.

 

네 번째 조건을 만족시키기 위해 조건에 주어진 이름의 파일을 만들고, 파일 내용도 조건에 주어진 대로 지정해 주었다.

 

다섯 번째 조건을 만족시키기 위해 인자의 67번째('C')인덱스에 포트를 지정하고 지정한 포트로 nc 연결을 하여 조건에서 주어진 값을 전달하였다.

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22
from pwn import \\\*



argvs = [str(i) for i in range(100)]

argvs[65] = '\x00'

argvs[66] = '\x20\x0a\x0d'

argvs[67] = '3000'



p = process(executable='/home/input2/input', argv=argvs, stderr=open('./stderr'), env={'\xde\xad\xbe\xef':'\xca\xfe\xba\xbe'})



pay = '\x00\x0a\x00\xff'

with open('./stderr', 'w') as f:

f.write('\x00\x0a\x02\xff')



with open('\x0a', 'w') as f:

f.write('\x00\x00\x00\x00')



p.sendline(pay)



r = remote('localhost',3000)

r.send('\xde\xad\xbe\xef')



p.interactive()

Colored by Color Scripter
cs

또한 flag파일은 현재 경로에 없기 때문에 ln \\-s /home/input2/flag flag 명령으로 현재 경로에 심볼릭 링크를 걸어준 후 파이썬 코드를 실행하면 플래그를 얻을 수 있다.

'Security & Hacking > Wargame' 카테고리의 다른 글

[HackCTF] RTL_World  (0) 2021.07.24
[pwnable.kr] shellshock  (0) 2021.06.16
[HackCTF] BOF_PIE  (0) 2021.06.01
[LoS] golem  (0) 2021.05.19
[HackCTF] Offset  (0) 2021.05.13

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;

 

문자 출력 함수

모니터에 하나의 문자를 출력할 때 일반적으로 사용하는 함수는 putchar() 과 fputc()가 있다.

#include <stdio.h>
int putchar(int c);
int fputc(int c, FILE * stream);
// 함수 호출 성공시 쓰여진 문자 정보, 실패시 EOF반환

putchar() 함수는 인자로 전달된 문자정보를 stdout 표준 출력 스트림으로 전송하여 모니터로 출력하는 함수이다. 

fputc()함수는 문자를 전송한다는 측면은 putchar()함수와 동일하지만 문자를 전송할 스트림을 지정할 수 있어 파일을 대상으로도 데이터를 저장할 수 있다. fputc() 함수의 두 번째 매개변수 stream은 문자를 출력할 스트림을 지정하는데 사용된다.

 

문자 입력 함수

키보드로 부터 하나의 문자을 입력 받을 때 일반적으로 사용하는 함수는 getchar() 과 fgetc()가 있다.

#include <stdio.h>
int getchar(void)
int fgetc(FILE * stream);
// 파일의 끝에 도달하거나 함수 호출 실패 시 EOF반환

getchar() 함수는 stdin으로 표현되는 표준 입력 스트림으로부터 하나의 문자를 입력받아 반환하는 함수이다. 

fgetc() 함수는 문자를 입력받을 스트림을 지정할 수 있다.

 

#include <stdio.h>

int main(void)
{
    int ch1, ch2;

    ch1 = getchar();    // 문자 입력
    ch2 = fgetc(stdin); // 엔터키 입력

    putchar(ch1);       // 문자 출력
    fputc(ch2, stdout); // 엔터키 출력
    return 0;
}

위 코드에서 문자를 int 형 변수로 선언한 이유는 getchar() 함수와 fgetc()함수의 반환형이 int 이기 때문이다. 

 

문자 입출력에서의 EOF

EOF는 End Of File의 약자로 파일의 끝을 표현하기위해 정의된 상수이다. 따라서 EOF가 반환되면 파일의 끝에 도달하여 더 이상 읽을 내용이 없음을 의미한다. 키보드를 대상으로 하느 fgetc() 와 getchar() 함수는 다음의 경우중 하나가 만족되었을 때 EOF를 반환한다.

  • 함수 호출 실패
  • Windows에서 CTRL + Z, Linux에서 CTRL + D 키가 입력되었을 경우
#include <stdio.h>

int main(void)
{
    int ch;

    while (1)
    {
        ch = getchar();
        if(ch == EOF)
            break;
        putchar(ch);
    }
    return 0;
}

위 코드를 실행하면 문자 입출력이 반복되고 EOF를 반환하는 CTRL + D 키를 입력하면 프로그램이 종료된다.

 

위 함수들의 반환형이 int 이고, int형 변수에 문자를 담는 이유는 EOF 는 -1로 정의된 상수인데, 반환형이 char 이라면 처리하는 컴파일러에 따라 char을 unsigned char로 처리하는 경우가 있어 -1을 양의 정수로 형변환 하는 경우가 발생 할 수 있다. 그래서 어떤 컴파일러라도 -1 을 유지하기 위해 반환형을 int 로 정의해둔것이고, 반환형이 int 이기 때문에 int 형 변수에 값을 저장해야 한다.

'Language > C, C++' 카테고리의 다른 글

[C++] 입출력  (0) 2021.08.21
[Project H4C] C언어 코딩도장(6)  (0) 2021.03.02
[C] 스트림  (0) 2021.01.15
[C] main 함수로의 인자 전달  (0) 2021.01.09
[C] void 포인터  (0) 2021.01.08

프로그램 상에서 키보드로 입력받아 모니터로 출력할 때 이를 연결해 주는 매개체를 스트림 이라고 한다.

스트림은 입력 장치에서 실행중인 프로그램으로 연결시켜 주는 입력 스트림과, 프로그램에서 출력장치로 연결시켜주는 출력 스트림이 있다.

 

콘솔 입출력을 위한 입력 스트림과 출력 스트림은 프로그램이 생성되면 자동으로 생성되고, 프로그램이 종료되면 자동으로 종료되기 때문에 직접 요구할 필요는 없지만, 파일과의 연결을 위한 스트림의 생성은 직접 요구해야 한다.

 

콘솔 입출력을 위한 스트림은 기본적으로 제공되는 표준 스트림(standard stream)이다. 표준 스트림은 다음과 같다.

  • stdin : 표준 입력 스트림 : 키보드 대상으로 입력
  • stdout : 표준 출력 스트림 : 모니터 대상으로 출력
  • stderr : 표준 에러 스트림 : 모니터 대상으로 출력

'Language > C, C++' 카테고리의 다른 글

[Project H4C] C언어 코딩도장(6)  (0) 2021.03.02
[C] 문자 단위 입출력 함수  (0) 2021.01.15
[C] main 함수로의 인자 전달  (0) 2021.01.09
[C] void 포인터  (0) 2021.01.08
[C] 함수 포인터  (0) 2021.01.08

main 함수는 다음과 같이 정의할 수도 있다. 

int main(int argc, char * argv[]) { ... }

프로그램 실행시 main함수로 인자를 전달할 수 있고, main 함수도 인자를 전달받을 수 있도록 한 것이다. 

#include <stdio.h>

int main(int argc, char * argv[])
{
    int i = 0;
    printf("전달된 문자열의 수 : %d\n", argc);
    
    for(i = 0; i < argc; i++)
        printf("%d 번째 문자열 : %s\n", i+1, argv[i]);
    
    return 0;
}
/* 
input : 
# ./argcargv I love you

output:
전달된 문자열의 수 : 4
1 번째 문자열 : ./argcargv
2 번째 문자열 : I
3 번째 문자열 : love
4 번째 문자열 : you
*/

위 코드와 같이 main함수를 구성하면 프로그램을 실행할 때 인자를 전달 할 수 있다. argv는 char형 더블 포인터 변수이다. 

인자 전달 과정에서 공백은 문자열을 나누는 기준이 된다. 인자로 전달된 문자열들이 배열에 묶여 main 함수의 두 번째 인자로 전달이되고, 첫 번째 인자는 문자열의 수가 전달이 된다.

#include <stdio.h>

int main(int argc, char * argv[])
{
    int i = 0;
    printf("전달된 문자열의 수 : %d\n", argc);

    while(argv[i] != NULL)
    {
        printf("%d 번째 문자열 : %s\n", i+1, argv[i]);
        i++;
    }

    return 0;
}
/* 
input : 
# ./argvNull "I Love You"

output : 
전달된 문자열의 수 : 2
1 번째 문자열 : ./argvNull
2 번째 문자열 : I Love You
*/

위 코드를 통해 전달받은 배열의 마지막에 NULL이 삽입됨을 확인할 수 있다. 또한 큰 따옴표로 묶으면 공백을 포함하는 문자열을 인자로 전달할 수 있다.

'Language > C, C++' 카테고리의 다른 글

[C] 문자 단위 입출력 함수  (0) 2021.01.15
[C] 스트림  (0) 2021.01.15
[C] void 포인터  (0) 2021.01.08
[C] 함수 포인터  (0) 2021.01.08
[C] 2차원 배열의 포인터  (0) 2020.12.30

다음과 같이 선언되는 포인터를 void형 포인터라 한다.

void * ptr;

void형 포인터 변수는 함수의 주소를 포함한 어떠한 변수의 주소값이든 담을 수 있다.

#include <stdio.h>

void SimpleFunc(void)
{
    printf("Simple function\n");
}

int main(void)
{
    int num = 20;
    void *ptr;
    
    ptr = &num;
    printf("%p\n", ptr);

    ptr = SimpleFunc;
    printf("%p\n", ptr);
    return 0;
}
/* output : 
0x7ffee8607858
0x1075fbf00
*/

위 코드처럼 어떤 형태의 변수의 주소값이든 저장할 수 있다. 그러나 void형 포인터 변수는 형(type)에 대한 정보가 없기 때문에 포인터 연산, 값의 변경, 참조가 불가능하다.

'Language > C, C++' 카테고리의 다른 글

[C] 스트림  (0) 2021.01.15
[C] main 함수로의 인자 전달  (0) 2021.01.09
[C] 함수 포인터  (0) 2021.01.08
[C] 2차원 배열의 포인터  (0) 2020.12.30
[C] 삼중 포인터  (0) 2020.12.28

프로그램 내에 정의된 다양한 함수들도 바이너리 형태로 메모리에 저장되어 호출시 실행된다. 이런 함수들의 주소도 포인터 변수에 저장할 수 있다. 배열의 이름이 배열의 시작 주소를 가리키듯, 함수의 이름도 함수가 저장된 메모리 주솟값을 나타낸다.

int SimpleFunc(int num) { ... }

위 함수의 반환형은 int 이고, 매개변수는 int num 이다. 함수의 이름 SimpleFunc는 함수의 주솟값을 의미하는 상수형태의 함수 포인터 이다. 이 함수의 포인터 형은 반환형이 int이고, 매개변수로 int형 변수가 하나 선언된 포인터형 이다.

 

포인터 변수의 선언은 다음과 같다.

int (*fptr) (int)

반환형이 int이며, 매개변수 선언이 int 하나인 함수 포인터 변수이다. 

다음과 같은 함수의 경우 아래와 같이 함수 포인터 변수를 선언한다.

int Func(int num1, int num2) { ... }
int (*fptr) (int, int);

그리고 다음과 같은 방식으로 Func 함수를 호출할 수 있다.

fptr(2,5);

다음 코드는 함수 포인터 변수를 활용한 예시이다.

#include <stdio.h>

void SimpleAdder(int n1, int n2)
{
    printf("%d + %d = %d\n", n1, n2, n1+n2);
}

void ShowString(char * str)
{
    printf("%s\n", str);
}

int main(void)
{
    char *str = "Function Pointer";
    int num1 = 10, num2 = 20;

    void(*fptr1)(int, int) = SimpleAdder;
    void(*fptr2)(char *) = ShowString;
    fptr1(num1, num2);
    fptr2(str);
    return 0;
}
/* output : 
10 + 20 = 30
Function Pointer
*/

 

#include <stdio.h>

int WhoIsFirst(int age1, int age2, int (*cmp)(int n1, int n2))
{
    return cmp(age1, age2);
}

int OlderFirst(int age1, int age2)
{
    if(age1 > age2)
        return age1;
    else if(age2 > age1)
        return age2;
    else
        return 0;
}

int YoungerFirst(int age1, int age2)
{
    if(age1<age2)
        return age1;
    else if(age2<age1)
        return age2;
    else
        return 0;
}

int main(void)
{
    int age1 = 20, age2 = 30;
    int first;

    printf("입장순서 1 \n");
    first = WhoIsFirst(age1, age2, OlderFirst);
    printf("%d세와 %d세중 %d세가 먼저 입장\n", age1, age2, first);

    printf("입장순서 2 \n");
    first = WhoIsFirst(age1, age2, YoungerFirst);
    printf("%d세와 %d세중 %d세가 먼저 입장\n", age1, age2, first);
    return 0;
}
/* output : 
입장순서 1 
20세와 30세중 30세가 먼저 입장
입장순서 2 
20세와 30세중 20세가 먼저 입장
*/

위 코드와 같이 함수의  매개변수로 함수 포인터 변수를 넣을 수 도 있다.

 

'Language > C, C++' 카테고리의 다른 글

[C] main 함수로의 인자 전달  (0) 2021.01.09
[C] void 포인터  (0) 2021.01.08
[C] 2차원 배열의 포인터  (0) 2020.12.30
[C] 삼중 포인터  (0) 2020.12.28
[C] 더블 포인터  (0) 2020.12.28
int arr2d[3][3];

위의 2차원 배열은 다음과 같은 모양을 갖는다.

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

2차원 배열의 경우 arr2d[0], arr2d[1], arr2d[2]도 각각 1행, 2행, 3행의 첫 번째 요소를 가리킨다.

또한 2차원 배열의 첫 번째 요소의 주소 값을 출력할 때는 다음 두 가지 형태로 동일하게 출력이 가능하다.

printf("%p\n", arr2d);
printf("%p\n", arr2d[0]);
#include <stdio.h>

int main(void)
{
    int arr2d[3][3];
     printf("%d\n", arr2d);
     printf("%d\n", arr2d[0]);
     printf("%d\n\n", &arr2d[0][0]);

    printf("%d\n", arr2d[1]);
    printf("%d\n\n",&arr2d[1][0]);

     printf("%d\n", arr2d[2]);
     printf("%d\n\n", &arr2d[2][0]);

     printf("sizeof(arr2d) : %d\n", sizeof(arr2d));
     printf("sizeof(arr2d[0]) : %d\n", sizeof(arr2d[0]));
     printf("sizeof(arr2d[1]) : %d\n", sizeof(arr2d[1]));
     printf("sizeof(arr2d[2]) : %d\n", sizeof(arr2d[2]));
    return 0;
}
/* output
 -272632624
 -272632624
 -272632624

 -272632612
 -272632612

 -272632600
 -272632600

 sizeof(arr2d) : 36
 sizeof(arr2d[0]) : 12
 sizeof(arr2d[1]) : 12
 sizeof(arr2d[2]) : 12
 */

 위 코드와 같이 arr2d는 첫 번째 요소를 가리키며, 배열 전체를 의미한다.  arr2d[0]은 첫 번째 요소를 가리키며 1행만을 의미한다. 그래서 sizeof 연산 결과 arr2d는 배열 전체의 크기인 36이 출력되었고, arr2d[0]은 첫 번째 행의 크기인 12가 출력되었다.

 

1차원 배열의 경우 다음 코드에서 iarr은 int형 포인터 이기 때문에 iarr+sizeof(int)의 계산 결과가 출력되고,  darr은 double형 포인터 이므로 darr+sizeof(double)이 출력된다.

int iarr[3];
double darr[7];

printf("%p\n", iarr+1);
printf("%p\n", darr+1);

또한 2차원 배열을 대상으로 증가 연산을 진행해보면 다음과 같다.

#include <stdio.h>

int main(void)
{
    int arr1[3][2];
    int arr2[2][3];

    printf("arr1 : %p\n", arr1);
    printf("arr1+1 : %p\n", arr1+1);
    printf("arr1+2 : %p\n\n", arr1+2);

    printf("arr2 : %p\n", arr2);
    printf("arr2+1 : %p\n", arr2+1);
    return 0;
}
/* output : 
@"arr1 : 0x7ffeefbff7c0\r\n"
@"arr1+1 : 0x7ffeefbff7c8\r\n"
@"arr1+2 : 0x7ffeefbff7d0\r\n"
@"\r\n"
@"arr2 : 0x7ffeefbff7a0\r\n"
@"arr2+1 : 0x7ffeefbff7ac\r\n"
*/

첫 번째 배열의 경우 1씩 증가시켰을 때 8이 증가하였고, 두 번째 배열의 경우 1을 증가시키면 12가 증가하였다. 2차원 배열 이름을 대상으로 증가 및 감소 연산을 할 경우 각 행의 첫번째 요소를 가리킨다. arr1은 1행의 첫번째 요소를 가리키며, arr1+1은 두번째 행의 첫번째 요소를 가리킨다. arr1은 가로가 한 행에 두 칸이기 때문에 8이 증가한 것이다. 2차원 배열의 포인터형은 가로의 길이에 따라서 달라진다.

2차원 배열은 포인트 연산시 sizeof(type) x 가로길이 만큼 값이 증가한다.

 

위와 같은 유형의 포인트 변수 선언은 int형이며, sizeof(int)x4의 크기 단위로 증가 및 감소하는 포인터 변수 선언은 다음과 같다.

int (*ptr)[4];

 

#include <stdio.h>

int main(void)
{
    int arr1[2][2] = {
        {1,2}, {3,4}
    };
    int arr2[3][2] = {
        {1,2}, {3,4}, {5,6}
    };
    int arr3[4][2] = {
        {1,2}, {3,4}, {5,6}, {7,8}
    };

    int (*ptr)[2];
    int i;

    ptr = arr1;
    printf("** Show 2,2 arr1 **\n");
    for(i = 0; i < 2; i++)
        printf("%d %d\n", ptr[i][0], ptr[i][1]);

    ptr = arr2;
    printf("** Show 3,2 arr2 **\n");
    for(i = 0; i < 3; i++)
        printf("%d %d\n", ptr[i][0], ptr[i][1]);
    
    ptr = arr3;
    printf("** Show 4,2 arr3 **\n");
    for(i = 0; i < 4; i++)
        printf("%d %d\n", ptr[i][0], ptr[i][1]);
      
}
/* output : 
@"1 2\r\n"
@"3 4\r\n"
@"** Show 3,2 arr2 **\r\n"
@"1 2\r\n"
@"3 4\r\n"
@"5 6\r\n"
@"** Show 4,2 arr3 **\r\n"
@"1 2\r\n"
@"3 4\r\n"
@"5 6\r\n"
@"7 8\r\n"
*/

위 코드와 같이 포인터 변수를 선언하여 2차원 배열에 접근할 수 있다.

 

2차원 배열의 주소 값을 인자로 전달 받는 함수를 정의할 때 매개 변수는 다음과 같이 선언할 수 있다.

void Function(int (*parr1)[7], double (*parr2)[5]) {...}
void Function(int parr[][7], double parr[][5]) {...}

위 두 방식의 매개변수 선언은 동일한 방식이다.

#include <stdio.h>

void ShowArr2DStyle(int (*arr)[4], int column)
{
    int i,j;
    for(i=0; i<column; i++){
        for(j=0; j<4; j++)
            printf("%d ", arr[i][j]);
        printf("\n");
    }
    printf("\n");
}

int Sum2DArr(int arr[][4], int column)
{
    int i, j, sum=0;
    for(i=0; i<column; i++)
        for(j=0; j<4; j++)
            sum+=arr[i][j];
    return sum;
}

int main(void)
{
    int arr1[2][4] = {1,2,3,4,5,6,7,8};
    int arr2[3][4] = {1,1,1,1,3,3,3,3,5,5,5,5};

    ShowArr2DStyle(arr1, sizeof(arr1)/sizeof(arr1[0]));
    ShowArr2DStyle(arr2, sizeof(arr2)/sizeof(arr2[0]));
    printf("arr1의 합 : %d\n", Sum2DArr(arr1, sizeof(arr1)/sizeof(arr1[0])));
    printf("arr2의 합 : %d\n", Sum2DArr(arr2, sizeof(arr2)/sizeof(arr2[0])));
    return 0;
}
/* output : 
1 2 3 4 
5 6 7 8 

1 1 1 1 
3 3 3 3 
5 5 5 5 

arr1의 합 : 36
arr2의 합 : 36
*/

위 코드의 다음과 같은 연산은 배열의 세로길이를 계산하는 것이다.

sizeof(arr1) / sizeof(arr1[0])
sizeof(arr2) / sizeof(arr2[0])

sizeof(arr1)은 배열의 전체 크기를 나타내고, sizeof(arr1[0])은 배열의 가로의 크기를 나타내기 때문에 위 코드는 배열의 세로 길이를 의미한다.

 

int arr[3][2] = { {1,2}, {3,4}, {5,6} };

2차원 배열이 위와 같이 선언되어 있을 때, 인덱스 [2][1]의 위치의 값을 4로 변경하려면 다음의 코드들을 이용할 수 있다.

arr[2][1] = 4;
(*(arr+2))[1] = 4;
*(arr[2]+1) = 4;
*(*(arr+2)+1) = 4;

 

'Language > C, C++' 카테고리의 다른 글

[C] void 포인터  (0) 2021.01.08
[C] 함수 포인터  (0) 2021.01.08
[C] 삼중 포인터  (0) 2020.12.28
[C] 더블 포인터  (0) 2020.12.28
[C] 3차원 배열  (0) 2020.12.26

포인터를 선언할 때 * 연산자가 둘 이상 사용되면 다중 포인터라 한다. *연산자는 얼마든지 사용될 수 있다.

 

다음과 같이 *연산자가 3개이상 삽입된 포인터 변수를 삼중 포인터 변수라고 한다. 

int ***tptr;

삼중 포인터 변수는 이중(더블) 포인터 변수를 가리키는(주소값을 저장하는) 용도로 사용된다.  

#include <stdio.h>

int main(void)
{
    int num = 100;
    int *ptr = &num;
    int **dptr = &ptr;
    int ***tptr = &dptr;

    printf("%d %d\n", **dptr, ***tptr);
    return 0;
}
// output : 100 100

위 코드는 삼중포인터의 선언과 접근에 대한 예시이다.

'Language > C, C++' 카테고리의 다른 글

[C] 함수 포인터  (0) 2021.01.08
[C] 2차원 배열의 포인터  (0) 2020.12.30
[C] 더블 포인터  (0) 2020.12.28
[C] 3차원 배열  (0) 2020.12.26
[C] 2차원 배열  (0) 2020.12.24

포인터 변수를 가리키는 또 다른 포인터 변수를 더블 포인터 또는 이중 포인터 라고 한다. 선언은 다음과 같이 * 연산자를 두 개 이어서 선언 한다.

int **dptr;

 

int main(void)
{
    double num = 3.14;
    double *ptr = &num;
    double **dptr = &ptr;
}

위와 같이 코드를 작성하게 되면 num 변수에는 3.14가 저장되어 있고, ptr변수는 num 변수의 주소를 가리키고 있고, dptr 변수는 ptr변수의 주소를 가리키고 있다. 또한 dptr 변수를 사용하여 다음과 같이 ptr변수와 num 변수에 접근할 수 있다. 

*dptr = ....;		// ptr 변수에 접근
*(*dptr) = ....;	// num 변수에 접근

*(*dptr)의 경우 괄호의 생략이 가능하여 **dptr로 접근할 수도 있다.

#include <stdio.h>

int main(void)
{
    double num = 3.14;
    double *ptr = &num;
    double **dptr = &ptr;
    double *ptr2;

    printf("%9p %9p\n", ptr, *dptr);
    printf("%9g %9g\n", num, **dptr);
    ptr2 = *dptr;   // ptr2 = ptr과 같음
    *ptr2 = 10.22;
    printf("%9g %9g\n", num, **dptr);
    return 0;
}
/* output : 
 0061FF10  0061FF10
     3.14      3.14
    10.22     10.22
*/

위와 같이 코드를 작성했을때 num 변수는 ptr변수와 ptr2변수가 가리키고 있고, ptr변수는 dptr변수가 가리키고 있다.

위와 같은 경우 변수 num에 접근하는 방법은 다음과 같다.

**dptr = 10.5;
*ptr = 10.5;
*ptr2 = 10.5;
num = 10.5;

위 4개의 경우 모두 num 변수에 10.5가 저장된다.

 

배열은 시작점의 주소를 가리키는 하나의 포인터이다. 포인터를 요소로 갖는 배열은 더블 포인터가 된다. 따라서 다음과 같이 포인터를 요소로 갖는 배열은 더블 포인터를 이용하려 접근할 수 있다.

#include <stdio.h>

int main(void)
{
    int num1 = 10, num2 = 20, num3 = 30;
    int *ptr1 = &num1;
    int *ptr2 = &num2;
    int *ptr3 = &num3;

    int *ptrArr[] = {ptr1, ptr2, ptr3};
    int **dptr = ptrArr;

    printf("%d %d %d\n", *(ptrArr[0]), *(ptrArr[1]), *(ptrArr[2]));
    printf("%d %d %d\n", *(dptr[0]), *(dptr[1]), *(dptr[2]));
    return 0;
}
/* output : 
10 20 30
10 20 30
*/

 

'Language > C, C++' 카테고리의 다른 글

[C] 2차원 배열의 포인터  (0) 2020.12.30
[C] 삼중 포인터  (0) 2020.12.28
[C] 3차원 배열  (0) 2020.12.26
[C] 2차원 배열  (0) 2020.12.24
[C] 포인터에서 const 사용  (0) 2020.12.20

+ Recent posts