스트림

스트림에는 입력 스트림, 출력 스트림 2가지가 존재한다. 이 스트림은 운영체제가 제공하는 소프트웨어로 구현되어있는 것으로 파일 스트림 혹은 모니터에 출력하는 출력 스트림 등이 있다.

먼저 문자 단위 입출력 함수를 보자.

  • int putchar(int c);
  • int fputc(int c, File * stream);
  • 함수 호출 성공 시 쓰인 문자 정보 반환, 실패 시 EOF 반환.

여기서 신경 쓸 점은 fputc함수의 경우 파일을 문자를 전송할 스트림을 지정할 수 있다. 즉 파일을 대상으로도 데이터를 전송 가능하다.

다음으로 문자 입력 함수를 보자.

  • int getchar(void);
  • int fgetc(File * stream);
  • 파일의 끝에 도달하거나 함수호출 실패 시 EOF 반환

키보드로부터 하나의 문자를 입력 받는 함수라 할 수 있다. fgetc 함수도 하나의 문자를 입력받지만 fputc함수와 마찬가지로 입력받을 스트림을 지정할 수 있다. 즉 파일을 대상으로 입력이 가능하다.

 

위의 함수들을 보면 함수호출 실패 시 EOF를 반환한다는 것을 알 수 있다. EOF란 End Of File의 약자로써 파일의 끝이라는 의미이다. 파일의 끝은 존재하지만 키보드의 끝은 어떻게 알 수 있을까? 바로 ctrl +  Z이다.(윈도우 기준) 이를 다음의 예시로 알아보자.

<소스코드>

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>
 
int main(void) {
    int ch;
    while (1) {
        ch = getchar();
        if (ch == EOF)
            break;
        putchar(ch);
    }
    return 0;
}
cs

<실행결과>

ctrl + Z를 입력하자 종료가 된것을 확인할 수 있다.

이번에는 문자열 입출력 함수에 대해서 알아보자.

  • int puts(const char *s);
  • int fputs(const char* s, FILE * stream);
  • 성공시 음수가 아닌 값, 실패 시 EOF 반환

puts 함수의 경우 호출되면 자동으로 개행이 되지만 fputs함수의 경우 자동으로 개행이 되지 않는다.

  • char * gets(char *s);
  • char * fgets(char *s, int n, FILE * stream);
  • 파일의 끝에 도달하거나 함수호출 실패 시 NULL 포인터 반환

표준 입출력 & 버퍼

버퍼링을 하는 이유는 데이터 전송의 효율성 때문이다. 1층에서 3층까지 책을 옮긴다고 했을때 책을 한권씩 나르는것 보다 가방에 20권씩 담아서 나르는것이 더 효율적이라는 예시를 생각해보면 이를 쉽게 이햐할 수 있다.

출력버퍼를 비우는 fflush함수

int fflush(FILE * stream);

이 함수는 인자로 전달된 스트림의 버퍼를 비우는 기능을 제공한다.

 

그렇다면 입력버퍼를 비워야할 경우가 있을까? 이를 다음의 예시를 통해 알아보자.

<소스코드>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
 
int main(void) {
    char perID[7];
    char name[10];
 
    fputs("주민번호 앞 6자리 입력: ", stdout);
    fgets(perID, sizeof(perID), stdin);
 
    fputs("이름 입력: ", stdout);
    fgets(name, sizeof(name), stdin);
 
    printf("주민번호: %s \n", perID);
    printf("이름: %s \n", name);
    return 0;
}
cs

<실행결과>

왜 이런 결과가 나타났을까? 우선 주민등록번호를 보면 999999 총 7글자이다.(엔터 포함) 그런데 sizeof(perID)가 7이므로 문자열을 입력받기 위해 널문자를 제외하고 총 6글자를 읽어지는 것이다. 즉 개행 이 입력 버퍼에 남으므로 fgets함수까지 남는다. fgets는 \n을 읽으면 종료하므로 입력버퍼에 남은 \n 만 읽고 종료해버리는 것이다.따라서 위와 같은 경우를 방지하기 위해 입력 버퍼를 비워야 한다. 그러면 어떻게 해야 할까?

바로 함수를 정의하는 것이다. 이를 다음의 예시로 보자.

<소스코드>

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>
 
void ClearLineFromReadBuffer(void) {
    while (getchar() != '\n');
}
 
int main(void) {
    char perID[7];
    char name[10];
 
    fputs("주민번호 앞 6자리 입력: ", stdout);
    fgets(perID, sizeof(perID), stdin);
 
    ClearLineFromReadBuffer();
 
    fputs("이름 입력: ", stdout);
    fgets(name, sizeof(name), stdin);
 
    printf("주민번호: %s \n", perID);
    printf("이름: %s \n", name);
    return 0;
}
cs

<실행결과>

입력 버퍼를 비우니 정상적으로 동작하는것을 알 수 있다.


그 외의 함수들

문자열 길이 반환하는 함수 : strlen

문자열 복사 함수 : strcpy, strncpy

문자열 덧붙이는 함수 : strcat, strncat

문자열 비교 함수 : strcmp, strncmp

문자열의 내용 int로 변환 : atoi

문자열의 내용을 long으로 변환 : atol

문자열의 내용을 double로 변환 : atof

'C' 카테고리의 다른 글

12. 구조체와 사용자 정의 자료형 2  (0) 2021.06.27
11. 구조체와 사용자 정의 자료형  (0) 2021.06.27
9. 함수 포인터 & void 포인터  (0) 2021.06.26
8. 2차원 배열과 포인터  (0) 2021.06.26
7. 이중 포인터  (0) 2021.06.25

함수 역시 함수의 주소 값을 저장할 수 있는 포인터 변수를 선언할 수 있다. 그렇다면 어떻게 선언해야 할까? 먼저 다음의 예시를 살펴보자.

<소스코드>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
int SimpleFunc(int num) {
    return num;
}
 
int main(void) {
    int num;
    scanf("%d"&num);
 
    printf("%d을 입력받았습니다.\n", SimpleFunc(num));
 
    return 0;
cs

위의 SimpleFunc 함수를 보면 다음의 사실들을 알 수 있다. 

함수의 반환형 :  int형

함수의 매개변수 : int형 1개

즉 우리는 함수의 포인터를 선언하기 위해서 함수 포인터의 변수에는 반환형 정보와, 매개변수 선언의 정보를 모두 표시해야 한다. 따라서 함수 포인터 변수는 다음과 같다. int (*fptr) (int) 여기서 fptr은 포인터, 앞의 int는 반환형, 뒤의 int는 매개변수의 형과 그 개수를 의미한다. 이를 예시로 확인해보면 int TwoSimpleFunc(int num1, int num2) 인 경우 위의 규칙을 적용하면 함수의 포인터 변수는 다음과 같다.

int (*fptr) (int, int); 

이 포인터 변수에 TwoSimpleFunc의 주소 값을 저장하는 대입연산은 다음과 같다.

fptr = TwoSimpleFunc;

또한 이 포인터 변수 fptr을 이용해서 fptr(1, 2); 의 형태로도 함수를 호출할 수 있다. -> TwoSimpleFunc(1, 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
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
 
void TwoAdderFunc(int num1, int num2) {
    int result = 0;
    result = num1 + num2;
    printf("%d 와 %d 의 합은 %d 입니다.\n", num1, num2, result);
}
 
void ShowString(const char* str) {
    printf("%s", str);
}
 
int main(void) {
    int n1, n2;
    scanf("%d %d"&n1, &n2);
 
    const char* str = "함수 포인터";
 
    void (*fptr1)(intint= TwoAdderFunc; // TwoAdderFunc 함수 포인터
    void (*fptr2) (const char*= ShowString; // ShowString 함수 포인터
 
    // 함수 포인터를 통한 호출
    fptr1(n1, n2);
    fptr2(str);
 
    return 0;
cs

<실행결과>

매개변수의 선언으로도 함수 포인터가 올 수 있다. 먼저 이를 예시로 알아보자.

<소스코드>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
 
int WhoIsFirst(int age1, int age2, int (*fptr) (int n1, int n2)) { // 매개변수로 함수 포인터가 있음
    return fptr(age1, age2); // 함수 포인터로 함수 호출
}
 
int OlderFirst(int age1, int age2) { // 나이가 많은 사람 먼저 하는 함수
    if (age1 > age2)
        return age1;
    else if (age1 < age2)
        return age2;
    else
        return 0;
}
 
int YongerFirst(int age1, int age2) { // 나이가 적은 사람 먼저 하는 함수
    if (age1 < age2)
        return age1;
    else if (age1 > age2)
        return age2;
    else
        return 0;
}
 
int main(void) {
    int age1 = 10, age2 = 20;
    int first;
 
    printf("입장방법 : OlderFirst \n");
    first = WhoIsFirst(age1, age2, OlderFirst); // WhoIsFirst 함수 호출 매개변수로 OlderFirst 함수 넘김
    printf("%d세와 %d세 중 %d세가 먼저 입장! \n\n", age1, age2, first);
 
    printf("입장방법 :  YongerFirst\n");
    first = WhoIsFirst(age1, age2, YongerFirst); // WhoIsFirst 함수 호출 매개변수로 YongerFirst 함수 넘김
    printf("%d세와 %d세 중 %d세가 먼저 입장! \n\n", age1, age2, first);
 
    return 0;
}
cs

<실행결과>


void 포인터

void * ptr; 의 경우 void형 이므로 형이 정해지지 않은 즉 어떤 형태의 주소 값이든 저장할 수 있는 포인터 변수이다.

이를 다음의 예시를 통해 알아보자.

<소스코드>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
 
void Func(void) {
    printf("그냥 함수");
}
 
int main(void) {
    void* ptr; // void형 포인터
    
    int num = 20;
    ptr = &num; // void형 포인터에 int형 주소 값 저장
    printf("%p \n", ptr);
 
    ptr = Func; // void형 포인터에 Func함수 주소 값 저장
    printf("%p \n", ptr);
 
    return 0;
}
cs

<실행결과>

이렇듯 void 형 포인터의 경우 어떤 형의 주소 값이든 담을 수 있다는 장점이 있지만 연산, 변경, 참조를 할 수 없다는 단점이 존재한다.

(위 포스팅은 윤성우 님의 열혈 C를 인용하였습니다.)

'C' 카테고리의 다른 글

11. 구조체와 사용자 정의 자료형  (0) 2021.06.27
10. 문자열 관련 함수  (0) 2021.06.27
8. 2차원 배열과 포인터  (0) 2021.06.26
7. 이중 포인터  (0) 2021.06.25
6. 다차원 배열  (0) 2021.06.25
2차원 배열

int arr[3][3];

다음과 같은 3행 3열의 2차원 배열이 있다 가정해보자. 이때 각각의 칸들은 칸 안에 쓰여있는 이름이다.

0행0열 0행1열 0행2열
1행0열 1행1열 1행2열
2행0열 2행1열 2행2열

arr[0]은 0행0열 즉 1번째 줄의 1번째, arr[1]은 1행 즉 2번째 줄의 1번째, arr[2]은 2행 즉 3번째 줄의 1번째를 의미한다.
이를 예제로 살펴보며 arr과 arr[0]은 같은 것일까 생각해보자.(출력 결과 자체는 둘다 0행 0열을 가리킨다.)

<소스코드>

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

<실행결과>

5 6 7 번 라인에서 알 수 있듯이 모두 같은 주소 값을(배열의 0행 0열) 가진다. 그러나 arr은 첫 번째 요소를 가리키며 배열 전체를 가리키지만 arr[0]은 첫번째 요소를 가리키며 배열의 첫 줄 즉 0행만을 의미한다. (3행 3열 즉 9칸이고 int 이므로 4byte. 36byte는 배열의 전체이고 12byte는 배열의 한 줄을 의미한다.)


이번에는 배열 이름에 1을 더한 결과를 알아보자. 앞서서 1차원 배열의 이름에 1을 더한 결과를 알아보았기 때문에 이해할 수 있으리라 생각한다. 예시와 함께 알아보자.

<소스코드>

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

<실행결과>

arr1, arr1+1, arr1+2를 보면 16진수라서 잘 와닿지는 않지만 12씩 차이 나는 것을 볼 수 있고

arr2, arr2+1을 보면 8씩 차이나는것을 볼 수 있다. 이를 그림으로 표현하면 다음과 같다.

즉 한 줄씩 차이 난다는 것을 확인할 수 있는 것이다. 따라서 arr1, arr2는 둘 다 int형 2차원 배열이지만 열의 정보가 다르므로 다른 것을 확인할 수 있다.


2차원 배열의 포인터 형

char (*arr1)[4]; // arr1은 char형 변수를 가리키면서, 포인터 연산 시 1(char의 크기) x 4의 크기 단위로 증가 감소

double (*arr2)[7]; // arr2은 double 변수를 가리키면서, 포인터 연산 시 8(double의 크기) x 8의 크기단위로 증가 감소

 

그렇다면 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>
 
void ShowArr(int array[][2], int column) {
    for (int i = 0; i < column; i++) {
        for (int j = 0; j < 2; j++) {
            printf("%d ", array[i][j]);
        }
        printf("\n");
    }
}
 
void ShowArr2(int (*array)[4], int column) {
    for (int i = 0; i < column; i++) {
        for (int j = 0; j < 4; j++) {
            printf("%d ", array[i][j]);
        }
        printf("\n");
    }
}
 
int main(void) {
    int arr1[3][2= {
        {12},
        {34},
        {56}
    };
 
    int arr2[2][4= {
        {5678},
        {9101112}
    };
 
    ShowArr(arr1, sizeof(arr1) / sizeof(arr1[0]));
    puts("");
 
    ShowArr2(arr2, sizeof(arr2) / sizeof(arr2[0]));
    return 0;
}
cs

<실행결과>

3번 라인과 12번 라인에서 볼 수 있듯이 두 가지 방법으로 2차원 배열의 매개변수 선언을 알 수 있다. 이 둘 중 3번 라인의 방식 int array [][3] 이 배열을 매개변수로 전달하는 것을 알기 쉬우므로 이 방법을 권장한다. 또한 두 번째 매개변수로 int column 즉 행의 정보를 넘겨주는 것을 알 수 있는데 이는 2차원 배열의 매개변수가 열 정보만 포함하고 있고 행 정보는 포함하고 있지 않기 때문이다. 따라서 열 정보가 같으면 행의 정보는 달라도 매개변수로 넘겨줄 수 있기 때문에 이를 보완하기 위해서 행 정보를 넘겨주는 것이다.

'C' 카테고리의 다른 글

10. 문자열 관련 함수  (0) 2021.06.27
9. 함수 포인터 & void 포인터  (0) 2021.06.26
7. 이중 포인터  (0) 2021.06.25
6. 다차원 배열  (0) 2021.06.25
5. 포인터와 함수  (0) 2021.06.25

이중 포인터란 포인터 변수를 가리키는 또 다른 포인터 변수를 뜻한다. 이중 포인터 혹은 더블 포인터라고 부르며 

다음과 같이 선언한다. int **dptr; // int형 이중 포인터 

만약 다음과 같은 코드가 있다고 생각해보자.

int main(void) {

    double num = 3.14;

    double * ptr = &num;

    double ** ptr = &ptr;

}

이중포인터가 포인터를 통해 num을 가르키는 모습

<소스코드>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
 
int main(void) {
    double num = 3.14;
    double* ptr = &num; // 포인터 변수 ptr로 num을 가리킴
    double** dptr = &ptr; // 이중 포인터 변수 dptr로 포인터 변수 ptr을 가리킴
 
    double* ptr2;
 
    printf("%9p %9p \n", ptr, *dptr); // ptr과 *dptr은 같으므로 동일한 출력결과를 보인다.
    printf("%9g %9g \n", num, **dptr); // **dpt과 num은 같으므로 동일한 출력결과를 보인다.
    ptr2 = *dptr; // ptr2 = ptr과 같은 의미
    *ptr2 = 10.99;
    printf("%9g %9g \n", num, **dptr); // num과 **dptr이 같으므로 동일한 출력결과를 보인다.
    return 0;
}
cs

<실행결과>

위의 코드를 그림으로 표현하면 다음과 같다.


포인터 변수의 Call by reference
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <stdio.h>
 
void SwapPtr(int **dptr1, int **dptr2) { // 1번 방법
    int temp;
    temp = **dptr1;
    **dptr1 = **dptr2;
    **dptr2 = temp;
}
 
void SwapPtr2(int** dptr1, int** dptr2) { // 2번 방법
    int* temp;
    temp = *dptr1;
    *dptr1 = *dptr2;
    *dptr2 = temp;
}
 
int main(void) {
    int num1 = 5, num2 = 10;
    int* ptr1 = &num1, * ptr2 = &num2;
    printf("*ptr1 : %d, *ptr2 : %d \n"*ptr1, *ptr2);
 
    SwapPtr(&ptr1, &ptr2);
    printf("*ptr1 : %d, *ptr2 : %d \n"*ptr1, *ptr2);
 
    SwapPtr2(&ptr1, &ptr2);
    printf("*ptr1 : %d, *ptr2 : %d \n"*ptr1, *ptr2);
 
    return 0;
}
cs

<실행결과>


포인터 배열과 포인터 배열의 포인터 형

예제로 알아보자.

<소스코드>

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
 
int main(void) {
    int num1 = 5, num2 = 10, num3 = 15;
    int* ptr1 = &num1, * ptr2 = &num2, * ptr3 = &num3;
 
    int *ptrArray[3= { ptr1, ptr2, ptr3 };
    int** dptr = ptrArray;
 
    printf("%d %d %d \n"*(ptrArray[0]), *(ptrArray[1]), *(ptrArray[2]));
    printf("%d %d %d \n"*(dptr[0]), *(dptr[1]), *(dptr[2]));
    return 0;
}
cs

<실행결과>

7번 라인을 보면 num1, num2, num3의 주소 값이 배열로 저장된 포인터 배열 ptrArray를 확인할 수 있다.

8번 라인을 보면 이중 포인터 dptr에 ptrArray가 대입된다는 것을 알 수 있다. 


'C' 카테고리의 다른 글

9. 함수 포인터 & void 포인터  (0) 2021.06.26
8. 2차원 배열과 포인터  (0) 2021.06.26
6. 다차원 배열  (0) 2021.06.25
5. 포인터와 함수  (0) 2021.06.25
4. 포인터와 배열  (0) 2021.06.22

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

int arr1[3][4];

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

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

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

<소스코드>

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

<실행결과>

 

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

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

<소스코드>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <stdio.h>
 
int main(void) {
    int arr1[3][3= {
        {123},
        {456},
        {789}
    };
 
    int arr2[3][3= {
        {1},
        {45},
        {789}
    };
 
    int arr3[3][3= { 1234567 };
 
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%d ", arr1[i][j]);
        }
        printf("\n");
    }
    printf("\n");
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%d ", arr2[i][j]);
        }
        printf("\n");
    }
    printf("\n");
    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            printf("%d ", arr3[i][j]);
        }
        printf("\n");
    }
}
cs

<실행결과>

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

'C' 카테고리의 다른 글

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

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

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

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

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

<소스코드>

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

<실행결과>


Call by value와 Call by reference

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

<소스코드>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
 
void SwapByValue(int num1, int num2) { // Call by value에 의한 함수 SwapByValue
    int temp; 
    // swap
    temp = num1;
    num1 = num2;
    num2 = temp;
}
 
void SwapByReference(int * num1, int * num2) { // Call by reference에 의한 함수 SwapByReference
    int temp;
    // swap
    temp = *num1;
    *num1 = *num2;
    *num2 = temp;
}
 
int main(void) {
    int n1 = 3, n2 = 5;
 
    SwapByValue(n1, n2);
    printf("Call by value\n");
    printf("n1 = %d, n2 = %d\n", n1, n2);
 
    puts("");
 
    printf("Call by reference\n");
    SwapByReference(&n1, &n2);
    printf("n1 = %d, n2 = %d\n", n1, n2);
    return 0;
}
cs

<실행결과>

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

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

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


'C' 카테고리의 다른 글

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

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

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

<실행결과>

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

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

 

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

 

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

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

<소스코드>

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

 

<실행결과>

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

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


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

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

int* ptr1 = 0x0010;

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


double* ptr2 = 0x0010;

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

 

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


결론

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

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


예제

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

<소스코드>

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

<실행결과>


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

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

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

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

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

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

char * str = "Hello world!";

str = "Bye~";

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

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

<실행결과>


포인터 배열

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

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

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

예제는 다음과 같다.

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

<실행결과>

 

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

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

 

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

<실행결과>


 

'C' 카테고리의 다른 글

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

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

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


포인터 변수의 선언

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

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

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

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

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


& 연산자

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

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

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

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

* 연산자

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

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

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

예시를 통해 살펴보자

<소스코드>

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

<실행결과>

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

포인터 구조


예제

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
 
int main(void) {
    int num1 = 30, num2 = 20;
    int* ptr1, * ptr2;
 
    // ptr1, ptr2가 num1과 num2를 가리키게 한다.
    ptr1 = &num1;
    ptr2 = &num2;
 
    // ptr1과 ptr2를 각각 10 증가 10 감소
    (*ptr1) += 10;
    (*ptr2) -= 10;
 
    // 두 포인터 변수 ptr1과 ptr2가 가리키는 대상 변경
    int* temp;
    temp = ptr1;
    ptr1 = ptr2;
    ptr2 = temp;
 
    // 출력
    printf("*ptr1 = %d\n"*ptr1);
    printf("*ptr2 = %d\n"*ptr2);
 
    return 0;
}
cs

<실행결과>

 

'C' 카테고리의 다른 글

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

+ Recent posts