본문 바로가기

C/기초와 문법

[C] 제어문과 반복문, 그리고 배열과 함수

반응형

🔊 해당 포스팅은 인프런의 널널한 개발자님의 독하게 시작하는 C 프로그래밍 강의를 듣고 개인적인 복습 목적 하에 작성된 글입니다. 해당 포스팅에 사용된 모든 자료는 필자가 직접 재구성하였음을 알립니다.

 

C언어를 배워보자!

 

이번 포스팅에서는 C언어로 제어문과 반복문을 작성하는 방법을 배워보고, 이후에는 C언어에서 배열과 문자열을 다루는 방법, 마지막으로는 함수를 정의하는 방법에 대해 알아보도록 하자.

1. 제어문 : if 문!

모든 언어에서 그렇듯이 제어문은 if 라는 키워드를 사용해서 정의한다. if 문을 사용하는 예시 소스코드를 보자.

 

#include <stdio.h>

int main(void) {
    int nAge = 0, threshold = 20;

    printf("나이를 입력하세요: ");
    scanf("%d", &nAge);
    if (nAge >= threshold)
        printf("성인입니다.\n");
    else
        printf("미성년자입니다.\n");
}

 

나이를 입력으로 받아서, 입력받은 나이가 20세 이상이면 성인, 미만이면 미성년자임을 출력하는 코드이다. if ~ else 조건문 정의하는 방식을 보자. if 키워드를 넣고 소괄호 안에 조건식을 정의한다. 그리고 조건문에 따라 실행되는 코드가 1개의 구문이면 위와 같은 방식으로 정의하고, 만약 2개 이상의 구문이 존재하면 중괄호를 두어 if 조건문의 body 스코프를 만든다. 아래처럼 말이다.

 

#include <stdio.h>

int main(void) {
    int nAge = 0, threshold = 20;

    printf("나이를 입력하세요: ");
    scanf("%d", &nAge);
    if (nAge >= threshold) {
        int gap = nAge - threshold;
        printf("20살과의 나이 차이는 %d살 입니다", gap);
    }
    else {
        int gap = threshold - nAge;
        printf("20살과의 나이 차이는 %d살 입니다", gap);
    }
}

 

만약 다중 if 문을 쓰려한다면 아래처럼 작성해볼 수도 있다.

 

#include <stdio.h>

int main(void) {
    int score = 0;
    printf("점수를 입력하세요: ");
    scanf("%d", &score);

    if (score > 90)
        printf("점수는 A입니다");
    else if (score > 50)
        printf("점수는 B입니다");
    else if (score > 20)
        printf("점수는 C입니다");
    else
        printf("점수는 F입니다");
}

 

참고로 중괄호로 묶여지는 일명 스코프 안에서 정의되는 변수는 기본적으로 쓰레드의 스택 메모리에 저장이 된다. 그리고 해당 스코프가 끝이나면 소멸된다. 이는 추후에 알아볼 함수에서도 마찬가지이다.

 

다음으로 알아볼 제어문은 swtich-case 문이다. Python에서도 3.10버전부터 추가된 문법 중 비슷한 match-case 문이 있다. C언어에서의 switch-case 문은 다중 if ~ else 구문과 비슷하지만 switch-case 문에서 정의하는 조건식은 반드시 상등연산임을 전제한다. switch-case 문을 사용한 예시 코드는 아래와 같다.

 

#include <stdio.h>

int main(void) {
    int day;

    printf("요일을 입력하세요 (1-7): ");
    scanf("%d", &day);

    switch(day) {
        case 1:
            printf("월요일\n");
            break;
        case 2:
            printf("화요일\n");
            break;
        case 3:
            printf("수요일\n");
            break;
        case 4:
            printf("목요일\n");
            break;
        case 5:
            printf("금요일\n");
            break;
        case 6:
            printf("토요일\n");
            break;
        case 7:
            printf("일요일\n");
            break;
        default:
            printf("유효하지 않은 입력입니다. 1에서 7 사이의 숫자를 입력하세요.\n");
            break;
    }
    return 0;
}

 

switch-case 문에서 특정 케이스에 맞다면 보통 break를 선언하여 해당 switch-case 문에서 빠져나오게 하는 것이 보통이다. 만약 break 문이 없다면 조건에 맞는 case 문이 실행되고 default 키워드에 정의된 부분도 같이 실행된다는 것을 주의하자.

 

그리고 switch 키워드 뒤에 조건식을 정의해줄 때 식을 정의해줄 수도 있다. 다만, 조건식의 결과는 반드시 정수여야 함에 주의하자.

 

#include <stdio.h>

int main(void) {
    int nData;

    printf("숫자를 입력하세요: ");
    scanf("%d", &nData);

    switch (nData % 2) {
        case 0:
            printf("짝수입니다");
            break;
        case 1:
            printf("홀수입니다");
            break;
        default:
            printf("짝수도 홀수도 아닙니다");
            break;
    }
    return 0;
}

 

다음으로 알아볼 제어문은 go to 문이다. 이름에서도 알 수 있듯이 go to 뒤에 나오는 구문은 어떤 조건이든 반드시 바로 go to 구문으로 가라는 뜻이다. 예시 코드를 살펴보자.

 

#include <stdio.h>

int main(void) {
    int num = 0;
    // go to 구문을 정의할 레이블을 선언
    start:

    printf("양의 정수를 입력하세요: ");
    scanf("%d", &num);

    if (num < 0)
        goto end;

    printf("입력한 숫자는 %d 입니다\n", num);
    // 다시 입력을 받도록 함
    goto start;

    // go to 구문을 정의할 또 다른 레이블을 선언
    end:
    printf("프로그램을 종료합니다\n");
}

 

하지만 이 goto 문은 일반적으로 코드를 읽는 순서인 위에서 아래로 흘러가는 실행 흐름을 역전시켜 코드 가독성을 해칠 수 있으며 사용이 권장되지 않는다. 여기서는 이런 문법이 있구나 하는정도만 하고 넘어가도록 하자.

2. 반복문 : while, for, do while 문!

먼저 while 문은 위에서 살펴본 if 조건문에서 키워드를 if 에서 while 로만 바꾸면 된다. 예시코드는 아래와 같다.

 

#include <stdio.h>

int main(void) {
    int num = 0;

    while (num < 10) {
        printf("현재숫자는 %d 입니다\n", num);
        ++num;
    }
}

 

다음은 for 문이다. for 문을 사용하는 방식은 while 문과 생김새가 약간 다르다. 먼저 아래 예시코드 부터 보자.

 

#include <stdio.h>

int main(void) {
    for (int i = 0; i < 10; ++i) {
        printf("현재 숫자는 %d입니다\n", i);
    }
}

 

for 키워드를 쓰고 소괄호가 시작되는 데 여기서 3가지를 넣어야 한다. 첫번째는 반복계수(소스코드에서 변수 $i$를 의미), 두 번째는 반복문이 수행될 조건을 반복계수 기반으로 정하고 마지막 세번째는 반복계수를 증가 또는 감소시킬 때 얼마나 할 것인지이다. 위 예시에서는 단항 증가연산자를 쓰긴 했지만 아래처럼 이항연산도 가능하다.

 

#include <stdio.h>

int main(void) {
    for (int i = 0; i < 10; i += 2) {
        printf("현재 숫자는 %d입니다\n", i);
    }
}

 

다음은 반복문 중첩인데, while 문이나 for 문 모두 중첩시킬 수 있다. 예시 코드는 이중 for 문을 살펴보자.

 

#include <stdio.h>

int main(void) {
    for (int i = 0; i < 5; ++i) {
        for (int j = 0; j < 5; ++j) {
            printf("i: %d, j: %d\n", i, j);
        }
    }
}

 

다음으로 알아볼 반복문 구문을 do ~ while 문이다. do ~ while 문은 while 문이 실행되기 전에 일단 무조건 do 뒤에 구문이 먼저 실행되고 난 뒤에 while 조건문을 살펴본다. 만약 while 뒤의 조건문이 충족되지 않으면 반복문이 종료된다. 예시코드는 아래와 같다.

 

#include <stdio.h>

int main(void) {
    int num = 0;

    do {
        printf("현재 숫자: %d\n", num);
        ++num;
    } while (num < 10);
}

 

일반적인 while 문과 뭐가 다른지를 보면 while 조건문이 동작하기 이전에 무조건적으로 사전에 한번 실행되어야 하는 구문이 있다면 해당 구문을 사용해볼 수 있다.

 

마지막으로 반복문에서 알아볼 내용은 반복문 중간에 사용하는 키워드인 continue 이다. 비슷한 기능으로 break 문이 있지만 두 개는 엄연히 다르다. 일단 break 문은 해당 키워드를 만나면 반복문의 스코프 밖으로 벗어나지만 continue는 스코프 밖으로 넘어가지 않고 바로 다음 구문이 실행된다. if 문에서도 마찬가지이다.

 

#include <stdio.h>

int main(void) {
    for (int i = 0; i < 10; ++i) {
        printf("반복문이 시작되었습니다\n");
        continue;
        printf("이 부분은 절대 콘솔에 나오지 않습니다");
    }
}

 

#include <stdio.h>

int main(void) {
    for (int age = 1; age < 30; ++age) {
        if (age >= 20)
            continue;
        printf("당신의 나이는 %d살입니다\n", age);
    }
}

3. 배열과 문자열

다음은 C언어에서 배열과 문자열을 정의하는 방법에 대해 알아보자. 우선 배열부터 살펴보자. 배열은 형식이 같은 자료(데이터) 여러 개를 모아 한 덩어리로 관리하는 자료구조를 의미한다. C언어에서는 아래처럼 선언하고 정의할 수 있다.

 

#include <stdio.h>

int main(void) {
    int nData[32] = { 0 };
    for (int i = 0; i < 32; ++i) {
        printf("element of nData array: %d\n", nData[i]);
    }
}

 

위 소스코드에서 볼 수 있듯이 배열에서 특정 인덱스의 요소로 접근하기 위해서 Python에서의 리스트에서 접근하는 것처럼 똑같이 수행할 수 있다. 여기서 알아두어야 할 점은 C언어에서 배열의 이름 즉, 소스코드 상에서 'nData' 라는 것은 해당 배열 자체를 식별하는 식별자이면서 메모리 주소를 의미한다. 좀 더 구체적으로, 배열의 메모리 주소는 기준 요소(인덱스가 0번째에 있는 요소)의 메모리 주소 식별자이자 상수이다.

 

잘 이해가 안갈 수 있다. 우선 break point를 찍은 아래의 소스코드를 디버그 모드로 실행해보고 메모리 주소를 살펴보자.

 

초록색으로 칠해진 부분을 보자

 

소스코드에서 길이가 5이면서 정수로 이루어진 배열을 선언했다. 그리고 nData에 메모리 주소 연산(&)을 적용한 뒤 nData 배열의 메모리 주소를 보면 초록색으로 칠해진 부분임을 알 수 있다. nData에는 1부터 5까지가 선언이 되어 있고, 메모리 주소는 위 사진 처럼 1, 2, 3, 4, 5 원소가 각각 메모리 주소를 차지하고 있는 것을 볼 수 있다. 여기서 요소 하나 당 총 4바이트(2자리씩 4개) 메모리를 차지하는 것을 볼 수 있는데, 이는 정수(int)가 하나 당 기본적으로 32bit(4바이트) 크기이기 때문에 그렇다.

 

방금 위에서 배열의 이름은 해당 배열의 메모리 주소이고, 그 메모리 주소는 배열의 첫번째 요소의 메모리 주소 값이라고 했다. 위 그림을 보면 메모리 주소 창 화면에 0x000000016db6d0b3 라는 값이 있는데, 이것이 바로 해당 배열의 첫번째 요소의 메모리 주소 값이자 배열의 메모리 주소가 된다. 

 

따라서 메모리의 주소인 배열의 이름은 위처럼 0x000000016db6d0b3 이라는 상수값이기 때문에 변수가 될 수 있는 L-value가 아닌 R-value 밖에 될 수 없다.

 

하지만 아래처럼 배열 연산자를 쓰는 경우는 배열의 요소 하나를 식별하는 것이기 때문에 변수가 될 수 있는 L-value가 가능하다. 아래 소스코드처럼 말이다.

 

#include <stdio.h>

int main(void) {
    int nData[5] = { 1, 2, 3, 4, 5 };
    for (int i = 0; i < 5; ++i) {
        nData[i] = 99;
        printf("%d 번째 요소의 값이 %d로 바뀌었습니다\n", i, nData[i]);
    }
    return 0;
}

 

다음으로 문자열에 대해 알아보자. 문자열은 문자 배열의 줄임말이기도 하다. 즉, 문자(char)로 이루어진 배열을 의미한다. 문자열의 특성으로는 일단 반드시 문자열의 끝에 null(\0)이 들어간다는 것이다. 이는 이전 입/출력 포스팅에서 살펴본 적이 있다. 문자열은 상수이다. 그래서 문자열은 보통 포인터를 이용해 관리하는데, 포인터에 대한 내용은 추후에 메모리를 다룰 때 더 자세히 알아보도록 하자. 여기서는 포인터란 변수의 종류 중 하나로, 메모리 주소값이 담긴 변수라고 생각하면 된다. 방금 위에서 정수 배열을 살펴볼 때 0x000000016db6d0b3 이라는 주소 값이 있었는데 말 그대로 이러한 긴 숫자값이 담겨있는 변수라고 생각하면 된다.

 

문자열도 정수일 때의 배열때와 마찬가지로 문자열의 메모리 주소는 문자열의 가장 첫 번째 요소의 메모리 주소 값이 된다. 아래 그림을 보자.

 

Hello 라는 문자열을 할당하고 출력하는 것을 디버그 모드로 실행

 

보면 Hello 라는 문자열이 담긴 메모리 주소가 초록색 배경화면으로 칠해져 있는 것을 볼 수 있다. 그림 속에서 정의한 변수인 szName에는 위 화면에서 가장 첫번째 문자인 'H'의 메모리 주소인 0x000000016d5b30d8가 된다.

 

다음은 다차원 배열 여기서는 2차원 배열을 정의하는 방법에 대해서 알아보도록 하자. C언어에서 2차원 배열은 아래처럼 정의할 수 있다.

 

#include <stdio.h>

int main(void) {
    int aList[2][2] = { 0 };

    for (int i = 0; i < 2; ++i) {
        for (int j = 0; j < 2; ++j) {
            printf("array의 %d행 %d열 요소값은 %d 입니다\n", i, j, aList[i][j]);
        }
    }
    return 0;
}

 

그런데 여기서 주의할 점은 2차원 배열에서 위 소스코드 처럼 aList[i][j] 로 배열 연산자를 적용하면 2차원 배열의 특정 요소에 대한 식별자이므로 L-value 즉, 변수가 될 수 있다. 하지만 aList[i] 처럼 배열 연산자를 적용하면 배열 내의 배열에 대한 식별자이기 때문에 메모리 주소가 되므로 R-value 밖에 될 수가 없다. 예를 들어, 아래의 소스코드는 에러가 발생한다.

 

#include <stdio.h>

int main(void) {
    int aList[2][2] = { 0 };

    // 배열에 대한 식별자는 L-value가 될 수 없음
    // aList[0] = { 1, 2 };

    // 배열 요소에 대한 식별자는 L-value가 될 수 있음
    aList[0][0] = 99;
    for (int i = 0; i < 2; ++i) {
        for (int j = 0; j < 2; ++j) {
            printf("array의 %d행 %d열 요소값은 %d 입니다\n", i, j, aList[i][j]);
        }
    }
    return 0;
}

4. 함수

C언어에서 함수를 정의하는 방법에 대해 알아보기 전에 용어에 대해서 간단히 짚고 넘어가자. 만약 아래와 같이 main() 이라는 함수 안에서 add() 라는 함수를 호출하는 소스코드가 있다고 가정해보자.(add 함수에 대한 정의부분은 일단 생략)

 

#include <stdio.h>

(.. add 함수 정의 부분 생략..)

int main(void) {
    int a = 10, b = 10, res = 0;
    
    res = add(a, b);
    printf("result: %d\n", res);
    return 0;
}

 

여기서 add 함수와 main 함수 간의 관계에 대해서 살펴보자. 함수 간의 관계를 정의하는 용어 중에서는 호출자라고 불리는 Caller, 피호출자라고 불리는 Callee가 있다. add 와 main 에서는 누가 Caller 고 누가 Callee 일까?

 

main 과 add 간의 관계

 

당연히 main 함수가 add 함수를 호출한 주체이므로 '호출자'라고 부르며, add 함수는 호출을 '당했으므로' 피호출자가 된다. 이렇게 호출자, 피호출자 관계를 이루고 있는 것을 "두 함수가 binding 되었다" 라고도 부른다. 물론 좀 더 세부적으로 binding 되었다라는 것은 두 개로 또 분류되는데, 하나는 빌드 타임 즉, 컴파일 타임 또는 링크 타임 중에 바인딩 되었다면 Static Binding 이라고 부르며, 런타임 중에 바인딩 되었다면 Dynamic 바인딩이라고 나누어 부른다. 참고해서 알아두자.

 

그리고 이전 목차에서 문자열, 배열을 선언하고 정의한 변수에는 첫 요소의 메모리 주소가 담겨있다고 했다. 함수도 비슷하게 함수 이름에는 메모리 주소가 담겨 있다. 일단 이를 확인하기 위해서 C언어에서 함수를 선언하는 방법에 대해 알아보자.

 

방법은 간단하다. main 함수와 똑같이 선언하면 된다. return type, 함수 이름, parameter 타입을 정의해주고 scope를 중괄호로 열어주어 로직을 작성하고 반환하면 된다. 예시 코드는 아래와 같다.

 

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int main(void) {
    int a = 10, b = 10, res = 0;

    res = add(a, b);
    printf("result: %d\n", res);
    return 0;
}

 

그러면 이제 함수의 이름에 메모리 주소가 정말 담겨있는지, 담겨있는 값은 무엇인지 디버그 모드로 아래 사진처럼 실행시켜보자.

 

디버그 모드로 실행해 함수 이름에 담겨있는 메모리 주소를 살펴보자

 

위 메모리 주소 화면을 보면 초록색 부분으로 칠해져있는 부분을 볼 수 있다. 0x00000001049cbf14 라고 되어 있는 부분이 바로 함수의 메모리 주소이다. 

 

우리가 아까 위에서 배열을 할당한 변수 이름이 nData 라고 한다면 nData 에는 메모리 주소가 있고 nData[1] 이렇게 작성하면 배열 연산을 수행해서 특정 요소에 접근할 수 있게 된다고 했다. 함수에서도 마찬가지로 함수 이름은 메모리 주소이고 함수를 호출할 때 하는 소괄호 () 연산이 바로 함수의 연산자가 된다.

5. 함수의 응용

다음은 함수를 응용하는 몇 가지 방법에 대해서 알아보자.

5-1. 함수의 원형 선언

먼저 함수 원형 선언이다. 그동안 배웠던 정수, 실수, 문자를 선언할때 정의도 한꺼번에 해줄 수 있지만, 선언만 해주고 다른 곳에서 정의를 해줘도 무방했었다. 예를 들어, 아래처럼 말이다.

 

#include <stdio.h>

int main(void) {
    char ch;  // 선언만 수행
    ch = 65;  // 정의 수행
    printf("%c\n", ch);
}

 

함수도 위처럼 선언과 정의를 분리할 수도 있다. 일단 선언과 정의를 함께한 코드부터 살펴보자. 위에서 봤던 add 함수를 정의했던 코드와 똑같다.

 

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int main(void) {
    int a = 10, b = 10, res = 0;

    res = add(a, b);
    printf("result: %d\n", res);
    return 0;
}

 

그런데 만약에 위 소스코드에서 add 함수를 정의한 부분은 main 아래로 옮기면 어떻게 될까?

 

#include <stdio.h>

int main(void) {
    int a = 10, b = 10, res = 0;

    res = add(a, b);
    printf("result: %d\n", res);
    return 0;
}

int add(int a, int b) {
    return a + b;
}

 

위 코드를 실행하면 아래와 같은 빌드 에러가 발생한다.

error: call to undeclared function 'add'; ISO C99 and later do not support implicit function declarations [-Wimplicit-function-declaration]

 

메세지를 잘 읽어보면 add 함수가 선언이 되지 않았는데 호출한다고 에러가 나고 있다. 즉, 컴파일러가 add 함수를 인식하지 못하고 있는 것이다. 왜냐하면 컴파일러는 소스코드를 위에서 아래로 쭉 읽어보기 때문이다. 컴파일러는 main 함수에서 add 함수를 호출하는 부분을 먼저 보았는데, 이게 웬걸? main은 add 라는 함수를 아직까지는 이전에 본적이 없다는 것이다. 

 

따라서 위와 같은 코드를 정상동작하도록 만들어주기 위해서 함수도 선언과 정의를 분리하는 함수의 원형 선언을 할 수 있다. 아래처럼 main 함수 위에 add 함수의 원형을 정의해줄 수 있다. 해당 함수의 return type, 함수 이름, parameter를 정의해주면 된다.

 

#include <stdio.h>

int add(int, int);

int main(void) {
    int a = 10, b = 10, res = 0;

    res = add(a, b);
    printf("result: %d\n", res);
    return 0;
}

int add(int a, int b) {
    return a + b;
}

 

5-2. 분할 컴파일

다음으로 알아볼 개념은 분할 컴파일이다. 지금까지 우리는 main.c 라는 하나의 소스코드에서 모두 수행해왔다. 하지만 항상 이럴 수는 없을 것이다. 코드가 길어지고 로직이 복잡해지면 반드시 한 프로젝트에 여러개의 C 소스코드를 작성할 수 밖에 없을 것이다. 이번 목차에서는 그것을 가능하게 해주는 분할 컴파일에 대해 배워보도록 하자.

 

먼저 분할 컴파일의 개념을 도식화해서 살펴보자. 예를 들어, 나에게 C 소스코드 파일이 2개가 있고, 이를 빌드한다고 해보자. 그러면 아래처럼 빌드가 이루어진다.

 

두 개의 C언어 소스코드를 빌드할 경우

 

 

그러면 이제 실제 분할 컴파일에 대한 예제 코드를 만들어보자. 우선 main.c 라는 소스코드에서 func.c 에서 선언되어 있는 add 함수를 호출한다고 해보자. 

 

가장 먼저 해주어야할 것은 func.c 라는 파일과 func.h 라는 헤더 파일을 생성해주어야 한다. 먼저 func.c 파일을 생성한 뒤, add 함수를 정의해주자

 

// content of `func.c` src file

int add(int a, int b) {
    return a + b;
}

 

그리고 이제 main.c 소스코드에서 fun.c 파일에 정의되어 있는 add 함수를 참조할 수 있도록 하기 위해서는 두 단계가 필요하다. 먼저 func.h 파일을 생성하면 아래처럼 템플릿이 생성된다. 그리고 fun.c 파일에 정의되어 있는 함수를 아래처럼 추가해주는데, 위에서 배웠던 함수 원형 선언할 때 처럼 정의해준다.

 

#ifndef func_h
#define func_h

int add(int, int);

#endif //FUNC_H

 

그리고 이제 main.c 소스코드에서 add 함수를 호출하도록 하기 위해 main.c 소스코드 상단에 전처리기로 func.h 를 추가해준다. main.c 소스코드는 아래와 같다.

 

#include <stdio.h>
#include "func.h"

int main(void) {
    int a = 10, b = 10, res = 0;

    res = add(a, b);
    printf("result: %d\n", res);
    return 0;
}

 

주의할 점은 사용자가 직접 만든 분할 컴파일용 헤더는 sdtio.h 처럼 <> 로 선언하는게 아닌 쌍따옴표인 " 를 이용해서 선언해주어야 한다.

6. 전역변수와 지역변수

마지막으로 알아볼 내용은 전역변수와 지역변수이다. C언어에서 전역변수는 함수 scope 밖에서 정의되고, 정의된 전역변수는 접근 범위가 파일(.c 확장자 파일) 수준까지 격상이 된다. 그래서 전역변수의 식별자 검색 순서가 [지역 scope → 함수의 body scope → 로컬 파일 → 외부 파일] 로 진행된다. 심지어 외부 파일까지 찾는다는 점이 좀 놀랐다..

 

#include <stdio.h>

char ch = 65;

int main(void) {
    printf("global variable: %c\n", ch);
    return 0;
}

 

 

반면에 지역변수는 함수 scope 내에서 정의 및 선언하는 변수이며 함수 scope가 종료되면 지역변수도 자동소멸된다. 

 

전역변수는 프로세스 내에 모든 쓰레드들이 공유하는 공용 영역인 정적 영역에 저장이 되지만 반대로 지역변수는 쓰레드 내의 Stack(자료 구조를 사용하는) 메모리에 저장이 된다. 실제로 살펴보기 위해서 두 개의 변수를 정의하는데, 1번 Case는 전역변수와 하나는 지역변수로 정의하고, 2번 Case에서는 두 개의 변수 모두 지역변수로 정의했을 때를 비교해보자.

 

먼저 1번 Case 즉, 두 개의 변수 중 하나는 전역변수, 나머지 하나는 지역변수로 정의했을 때의 메모리 화면이다.

 

1번 Case

 

사진 속 빨간색 칸을 보면 쓰레드라는 것에 tmp 라는 지역변수만 현재 존재하는 것을 볼 수 있다. ch 는 전역변수이기 때문에 쓰레드에서 보이지 않는 것이고, 쓰레드에서 보이지 않는 것은 곧 정적 영역 메모리를 사용한다는 것이다.

 

반대로 2번 Case일 때의 사진을 보자.

 

2번 Case

 

이번엔 ch 도 지역변수로 선언했다. 그러니까 ch 와 tmp 변수 모두 쓰레드 안에 있는 것을 볼 수 있다. 결국 ch, tmp 변수 모두 쓰레드 내의 스택 메모리에 저장되어 있음을 알 수 있다.

 

여담으로 강의 속에서는 전역 변수를 웬만하면 사용하지 말라고 권장한다. 이유는 추후에 멀티 쓰레딩을 통한 동기화 이슈가 발생할 가능성이 높기 때문이라고 한다. 그래서 보통은 전역 변수를 사용한다고 하면 변수명 앞에 prefix 로 g_ 를 붙인다고 한다. 전역변수에 대해서는 참고해서만 알아두도록 하자.

 

 

반응형