C, Call by value, Call by reference, Debug


이번 챕터의 목표


문자열을 복사하는 함수를 작성하기

c_tutorial12_goal

#include <stdio.h>

void MyStrcpy(char *dest, int size, char *src)
{
	// 코드 작성하기
}

int main(void)
{
	char str1[12] = { "Cute Teemo" };
	char str2[12] = { 0 };

	printf("str1을 str2에 복사하는 프로그램\n\n");
	printf("str1: %s\n", str1);
	printf("str2: %s\n\n", str2);

	MyStrcpy(str2, sizeof(str2), str1);
	
	printf("str2: %s\n\n", str2);

	return 0;
}

만약 str2의 초기 크기가 3인데,

str1의 문자열의 길이가 3보다 크다고하면

다음과 같이 복사가 진행되지 않고 끝나야합니다.

c_tutorial12_goal2




Call by value / Call by reference


Call by value와 Call by reference는

함수에 매개변수를 전달하는 방법입니다.

사실 이전 챕터의 목표로도 이미 나왔었던 사항입니다.


Call by value는 말 그대로

매개변수에 값 그 자체를 전달하는 방식입니다.

예를 들어 다음 코드를 보겠습니다.

#include <stdio.h>

void TryChangeValue(int a, int b)
{
    a = 7;
    b = 77;
}

int main(void)
{
    int x = 10, y = 20;
    TryChangeValue(x, y);
    
    printf("x: %d, y: %d\n\n", x, y);
    
    return 0;
}

/*
 x: 10, y: 20

*/

초기 x의 값은 10, y의 값은 20입니다.

그리고 TryChangeValue()함수에서

x의 값을 7, y의 값을 77로 바꾸는 걸 시도합니다.

하지만 출력결과에서도 알 수 있듯이 값은 바뀌지 않았습니다.

그 이유는 main()함수에서 TryChangeValue()함수를 호출할 때

매개변수로 넘겨준 값은 x와 y가 아니라

10, 20 이란 값 그 자체였기 때문입니다.


Call by reference는 간단히 말해

매개변수로 주소(포인터)를 넘겨주는 방식입니다.

호출된 함수에서는 해당 주소에 접근함으로써

실제 받은 주소에 해당하는 변수를 다룹니다.

다음 코드를 보겠습니다.

#include <stdio.h>

void TryChangeValue(int* a, int* b)
{
    *a = 7;
    *b = 77;
}

int main(void)
{
    int x = 10, y = 20;
    TryChangeValue(&x, &y);
    
    printf("x: %d, y: %d\n\n", x, y);
    
    return 0;
}

/*
 x: 7, y: 77
 
 */

아까 코드와 거의 비슷합니다.

다만 TryChangeValue()함수의 매개변수가

포인터로 바뀐 것을 알 수 있습니다.

main()함수에서 TryChangeValue()함수를 호출할 때

매개변수로 x, y의 주소가 넘어가고

TryChangeValue()에서는 받은 주소를 가지고

그 주소에 해당하는 변수를 직접적으로 다룹니다.


이렇게 포인터를 매개변수로 받을 경우 가장 큰 문제점은

매개변수로 받은 포인터가 가리키는 변수의 크기를 모른다는 것입니다.

sizeof(포인터)를 하면 포인터 자체의 크기인 4byte 혹은 8byte가 나옵니다.

그래서 포인터를 매개변수로 넘겨줄 때는

필요에 따라 포인터가 가리키는 변수의 크기도 같이 넘겨줍니다.

문자열을 받아 출력하는 다음의 코드를 살펴보겠습니다.

#include <stdio.h>

void GetName(char* name, int size)
{
    printf("이름을 입력하세요: ");
    
    // gets()로 문자열 받는건 보안 결함
    // 맥에선 윈도우처럼 gets_s() 없음
    // fgets()로 받으면 문자열 마지막이 줄넘김('\n')
    // 그래서 문자열의 마지막을 '\n'에서 '\0'으로 바꿔주는 작업 필요
    fgets(name, size, stdin);
    int temp = strlen(name);
    name[temp-1] = '\0';
    
    // 윈도우에서는 그냥 이 한줄이면 끝
    // gets_s(name, size);
}

int main(void)
{
    char name[30] = {0};
    
    GetName(name, sizeof(name));
    printf("당신의 이름은 %s입니다.\n\n", name);
    
    return 0;
}

/*
 이름을 입력하세요: onsil
 당신의 이름은 onsil입니다.
*/

그냥 gets()함수를 통해 문자열을 받으면,

입력받은 문자열의 크기가 변수의 크기보다 클 수 있기 때문에 보안 결함이 있습니다.

이를 방지하기 위해 fgets()나 gets_s()함수를 사용합니다.

그런데 이 함수를 이용할 때는 문자열을 저장할 배열의 크기도 넘겨줘야합니다.

그런 이유로 GetName()함수에서 name 배열의 크기도 같이 받는 것이고,

이렇게 크기를 따로 받지 않으면 GetName() 함수 내에서

name 배열의 크기를 알 방법이 없습니다.


그리고 개발을 하다보면 이런 경우도 있습니다.

피호출자 함수에서 동적으로 할당한 메모리를 호출자 함수에서 해제하는 경우도 있습니다.

다음 예제를 살펴보겠습니다.

이번 코드는 gets_s()함수를 사용했으므로

윈도우에서만 실행됩니다.

// 동적할당을 하는 함수와 반환하는 함수가 다를 수 있다.

#include <stdio.h>
#include <stdlib.h>

char* GetName(void)
{
	char *name = NULL;
	name = (char*)calloc(30, sizeof(char));

	printf("이름을 입력하세요: ");
	gets_s(name, sizeof(char) * 30);

	return name;
}

int main(int argc, char* argv[])
{
	char *name = NULL;
	name = GetName();
	printf("\n당신의 이름은 %s입니다.\n\n", name);

	free(name);
	return 0;
}

이 코드에서는 main()함수에서 GetName()함수를 호출합니다.

즉, main()이 호출자, GetName()이 피호출자 함수입니다.

그리고, 피호출자 함수에서 name에 30byte 크기의 메모리를 동적 할당하고

호출자 함수에서 그 메모리를 해제하는 걸 볼 수 있습니다.


이 코드도 GetName() 함수의 반환값이 포인터(char*)이므로,

주소를 반환하는 일종의 Return by reference라 볼 수 있습니다.

정확히는 C보다는 C++에서 Return by reference 란 용어를 사용하던데

여하튼 중요한건 값이 아니라 주소를 반환했다는 것입니다.




디버그(Debug)


이번에는 윈도우 환경의 Visual Studio 2017에서

간단한 디버깅을 해보겠습니다.

사실 저도 초짜라 디버깅에 익숙하진 않지만,

고급개발자가 되려면 디버깅은 필수라 합니다.


디버그란?


bug는 ‘벌레’를 의미하고, debug는 ‘벌레를 잡다’를 의미합니다.

여기서는 벌레는 프로그램의 오류를 의미합니다.

오류를 벌레라고 부르게 된 유래는…

아주 옛날(1947년)에 하버드 출신이 만든 컴퓨터 Mark II가

오작동 하게됩니다. 원인을 알아내려 이 거대하고 무거운(25톤)

컴퓨터를 살펴보는데, 컴퓨터 회로에 나방 한마리가 들어가 있었댔죠.

이때부터 버그가 컴퓨터 오작동의 대명사로 불리기 시작했다는데,

이보다 훨씬 전에 에디슨이 사용했다고도 하고…


여하튼 debug는 결국 ‘오류를 잡다’ 라는 뜻이 됩니다.

정확히는 오류는 잡는 행위를 디버깅(Debugging)이라하고

이 디버깅을 하는데 쓰이는 툴을 디버거(Debugger),

그리고 이 둘을 합쳐 디버그(Debug)라고 부릅니다.


디버깅을 하는 방법은 다양한데, 보통은 코드를 한꺼번에 실행하는게 아니라

코드를 한줄한줄 실행하면서 특정 변수의 값의 변화나

특정 주소의 메모리값을 검사하는 방식으로 진행됩니다.


디버깅은 대부분의 개발툴에서 제공하는 기능입니다.

여기서는 Visual Studio 2017로 C 코드를 간단하게 디버깅해보겠습니다.


중단점(break point)


중단점은 원하는 위치에서 프로그램을 멈춰주는 역할을 합니다.

아래의 간단한 코드를 디버깅 해보겠습니다.

// 중단점 의미파악

#include <stdio.h>

int main(void)
{
	printf("1\n");
	printf("2\n");
	printf("3\n");
	printf("4\n");
	printf("5\n");
	printf("6\n");
	printf("7\n");
	printf("8\n");
	printf("9\n");
	printf("10\n\n");

	return 0;
}

그냥 1부터 10까지 출력하는 코드입니다.

c_vs_breakpoint1


먼저 중단점을 아래 사진과 같이 지정합니다.

c_vs_breakpoint2

그냥 저 빨간색 점 위치를 클릭하면 됩니다.

그리고 메뉴에서 디버그 > 디버깅 시작 을 클릭합니다.

단축키가 나와있는 대로 그냥 F5를 눌러도 됩니다.

c_vs_breakpoint3


그럼 다음과 같이 중단점 직전까지만 코드가 실행되고 잠시 멈춘 것을 볼 수 있습니다.

c_vs_breakpoint4

중단점 부분에 노란 화살표가 의미하는 것은

해당 줄의 코드가 아직 실행된 상태는 아니고, 실행할 차례라는 것입니다.

이 상태에서 디버그 > 프로시저 단위 실행 을 클릭하거나

단축키인 F10을 누르면 코드가 한줄한줄씩 실행됩니다.

c_vs_breakpoint5


그러면 실행화면에는 5가 출력이 되고,

화살표는 한줄 밑으로 내려간 것을 볼 수 있습니다.

c_vs_breakpoint6

같은 방식으로 한줄한줄 계속 실행하다

모든 코드를 실행하게 되면 다음과 같은 화면이 됩니다.

c_vs_breakpoint7

노란 화살표가 가리키고 있는 코드가

invoke_main()함수인 것으로 보아,

c함수 내부적으로 main()함수를 실행시키는 코드인 것 같습니다.

다음 과정이 계속 궁금하다면 계속 F10을 눌러 실행시키면 됩니다.

도중에 디버깅을 끝내고 싶다면 위 그림에서 표시한 중지 아이콘을 클릭하면 됩니다.


메모리 값 살펴보기


이번에는 특정 변수의 메모리 값을 살펴보겠습니다.

사용할 코드는 아까 Call by reference 부분에서 사용했던 코드입니다.

// 동적할당을 하는 함수와 반환하는 함수가 다를 수 있다.

#include <stdio.h>
#include <stdlib.h>

char* GetName(void)
{
	char *name = NULL;
	name = (char*)calloc(30, sizeof(char));

	printf("이름을 입력하세요: ");
	gets_s(name, sizeof(char) * 30);

	return name;
}

int main(int argc, char* argv[])
{
	char *name = NULL;
	name = GetName();
	printf("\n당신의 이름은 %s입니다.\n\n", name);

	free(name);
	return 0;
}


여기서는 name 변수를 살펴보겠습니다.

중단점을 아래와 같이 설정합니다.

c_debug1

그리고 아까 했던 것처럼 디버깅을 시작합니다.

c_debug2

그럼 아래와 같은 화면이 될 것입니다.

c_debug3

노란 화살표를 살펴보면 c코드에서 제일 먼저 실행되는 main()함수의

중단점에서 코드가 멈춰 실행대기 중인 걸 볼 수 있습니다.

여기서 메모리의 값을 살펴볼려면

디버그 > 창 > 메모리 에서 메모리창을 켜야합니다.

여기서는 총 3개의 메모리창을 열 것입니다.

c_debug4

일단 메모리1 창을 켜면 다음과 같은 화면이 됩니다.

c_debug5


코드와 메모리창을 함께 보기위해 코드창에 우클릭을 해서 새 가로 탭 그룹을 클릭합니다.

c_debug6

그러면 다음과 같이 한 화면에 나눠서 메모리창과 코드창을 볼 수 있습니다.

c_debug7


위와 같은 방식으로 메모리2 창도 엽니다.

c_debug8

근데 메모리창과 코드창이 뒤죽박죽이니 보기 안좋습니다.

메모리2 창을 드래그&드롭으로 코드창과 같은 라인에 합칠 수 있습니다.

c_debug9

그 다음에 위에 같은 방식으로 코드창에 우클릭 후, 새 가로 그룹 탭을 클릭하면

깔끔하게 위 2개의 창은 메모리창, 맨 아래는 코드창으로 나눠집니다.

같은 방식으로 메모리3 창짜리 열고 다음 화면을 만듭니다.

c_debug10

일단 지금은 메모리창이 랜덤 주소로 설정되어있고, 아직 해당 주소의 값은 안나와있습니다.


지금 실행대기 노란 화살표를 보면 main()함수 내입니다.

이 상태에서 메모리1 창에 &name을 입력합니다.

그럼 main()함수 내의 지역변수인 name의 주소값이 입력됩니다.

c_debug11

저 주소 쓰는 곳에 직접 주소를 써야하지만, 우리는 그 주소를 모르니

&연산자를 이용해서 입력해줍니다.

그러면 자동으로 메모리 주소로 바뀌고, 아래와 같이 뜹니다.

c_debug12

쓰레기값이 들어있습니다.

이 상태에서 프로시저 단위 실행(F10)을 해봅니다.

그러면 *name 선언 라인이 실행되면서 다음과 같이 됩니다.

c_debug13

name은 포인트 변수이고 32비트 환경에서 4바이트입니다.

그리고 NULL값으로 지정을 해주니, 메모리1 창을 보면

해당 주소에 정확히 4바이트만큼 0으로 채워지는 걸 볼 수 있습니다.

그리고 노란 화살표는 다음 줄로 넘어가있습니다.

이 상태에서 또 프로시저 단위 실행을 해보면

해당 줄이 실행되면서, GetName() 함수로 넘어가게 됩니다.

그리고 *name 선언부에 노란 화살표가 멈추고 대기해있습니다.

main()함수에 *name 변수와 GetName() 함수에 *name 변수는

서로 다른 지역변수 입니다.


이 상태에서 메모리2 창에 &name을 입력하면

GetName()함수내의 name변수의 주소값을 입력하는 것입니다.

c_debug14

그럼 아래와 같이 해당 변수의 주소로 바뀌고 값이 뜹니다.

c_debug15

이 상태에서 또 F10으로 한줄을 실행시키면…

c_debug16

name 변수를 NULL로 설정했기 때문에 역시 4바이트만큼 0으로 채워집니다.

그리고 실행대기인 코드를 보면 30바이트만큼 메모리를 동적할당 하는 구문입니다.

이 상태에서 또 한줄을 실행시키면…

c_debug17

GetName()함수의 name값이 변한 걸 볼 수 있습니다.

calloc()함수가 30바이트의 메모리를 동적할당하고,

그 동적할당 한 메모리의 시작 주소를 리턴하여 name함수에 대입한 것이죠.


이 상태에서 아래 조사식 부분에 name을 입력해봅니다.

c_debug18

그럼 아래와 같이 name 변수의 값과 형식이 나옵니다.

c_debug19

값을 잘 보면 0x00d35130입니다. 포인터변수니까 주소값이겠죠.

그리고 메모리2의 4바이트를 뒤에서부터 읽으면 역시 0x00d35130입니다.

이 값을 메모리3 에 입력해봅니다. 직접 입력해도 되고, 아래 그림과 같이 드래그&드롭 해도 됩니다.

c_debug20

메모리3 의 메모리값을 살펴보면 정확히 30바이트가 0으로 채워진 걸 볼 수 있습니다.

calloc() 함수가 잘 작동한 걸 확인할 수 있습니다.

이 상태에서 또 한줄 실행하면 printf()문이 실행되어 다음과 같이 됩니다.

c_debug21

또 한줄을 실행하면 이번엔 아래와 같이 gets_s()함수가 실행됩니다.

c_debug22

‘onsil’을 입력하고 엔터를 치면…

c_debug23

메모리3 값이 변한 걸 볼 수 있습니다. 옆에 친절하게도 나와있지만

‘o’의 아스키 코드값은 6f

‘n’의 아스키 코드값은 6e

’s’의 아스키 코드값은 73

‘i’의 아스키 코드값은 69

‘l’의 아스키 코드값은 6c

입니다. 여하튼 입력한대로 메모리에 잘 저장된 것을 볼 수 있습니다.


이제 한줄한줄 계속 실행하다 GetName()함수를 빠져나가면…

c_debug24

아래 조사식을 보면 name값이 NULL로 된걸 볼 수 있습니다.

이는 이제 name이 나타내는 바가 main()함수의 name 변수이고,

아직 main()함수의 name은 NULL값이기 때문입니다.


또 한줄을 실행하면…

c_debug25

정상적으로 main()함수의 name 변수값을 의미하는

메모리1 창의 값이 아까 동적할당한 메모리의 시작주소값으로 바뀝니다.

아래 조사식에서도 onsil을 가리키고 있다고 나옵니다.


또 한줄을 실행하면…

c_debug26

printf()문이 정상적으로 실행되는데,

메모리2의 값이 쓰레기값으로 변한 걸 볼 수 있습니다.

이는 GetName()함수가 완전히 종료되어

GetName()함수의 지역변수였던 name이 소멸했다고 볼 수 있습니다.


또 한줄을 실행하여 free()함수가 실행되면…

c_debug27

동적할당한 메모리3의 값이 쓰레기값으로 전부 바뀐 걸 확인할 수 있습니다.


호출 스택 살펴보기


이번에는 디버깅 모드를 통해 호출 스택을 살펴보겠습니다.

살펴볼 코드는 팩토리얼을 계산하는 코드입니다.

팩토리얼을 간단하게 설명하자면

\[n! = n \cdot (n-1) \cdot (n-2) \cdots 3 \cdot 2 \cdot 1\]

즉, 예를 들어 3! = 3x2x1 = 6 입니다.

#include <stdio.h>

int CalcFactorial(int n)
{
	if (n == 1) return 1;
	return CalcFactorial(n - 1)*n;
}

int main(void)
{
	int input = 0, result = 0;
	printf("팩토리얼 계산기\n\n");
	printf("숫자를 입력하세요: ");
	scanf("%d", &input);

	result = CalcFactorial(input);
	putchar('\n');
	printf("%d!: %d\n\n", input, result);

	return 0;
}

c_debug28


이번에는 다음과 같이 CalcFactorial()함수의 첫줄에 중단점을 찍고

디버깅을 시작해봅니다.

c_debug29

여기에 3을 입력하고 F10으로 다음 줄을 실행하면

c_debug30

밑의 조사식에 이름에 n을 입력하면 현재 입력한 값인 3이 나옵니다.

그리고 그 옆에 호출 스택을 보면 현재 호출된 함수 스택을 볼 수 있습니다.

시작할때 main() 함수가 호출(실행)되고,

현재 CalcFactorial() 함수가 호출되어서

순서대로 나와있습니다.

여기서 스택(Stack)이라는 것은 자료구조의 일종입니다.

c_stack

출저: 위키백과


데이터를 세로로 쌓는데 Push가 데이터를 입력하는 행위이고,

Pop이 데이터가 빼는 행위입니다.

6이란 데이터를 보면 가장 늦게 입력되었지만, 가장 먼저 나가게 됩니다.

이를 LIFO(Last In Fisrt Out)이라 부릅니다.


메모리 영역 중 Stack도 이와 동일한 원리로 작동합니다.

먼저 호출된 함수나 먼저 선언된 변수 순서대로

밑에서부터 쌓여지는… 그리고 없어질 땐 위에서부터 사라지는 구조입니다.

그래서 여기서도 main()함수가 밑에 그 위에 CalcFactorial()함수가 있습니다.


F10으로 계속 실행을 해보면 다음과 같이 됩니다.

c_debug31

처음에 CalcFactorial() 함수가 받은 값은 3이므로,

if문을 지나쳐 return 구문으로 넘어갑니다.

그런데 CalcFactorial(n-1)*n 값을 리턴하므로,

먼저 CalcFactorial(n-1)값을 계산해야겠죠.

그래서 처음에 실행됐던 CalcFactorial() 함수가 끝나지 않은 상태에서

또 CalcFactorial() 함수가 실행되었고, 이는 호출 스택부분에 나타납니다.

그리고 가장 최근에 실행된 CalcFactorial()은

CalcFactorial(2)이므로, 조사식 부분에 n값은 2가 나옵니다.


또 F10으로 다음줄을 실행시키면…

c_debug32

이번엔 CalcFactorial(1)이 호출된 것이므로,

if문에 걸려 1을 반환하고 끝나게 됩니다.

그 이후로 계속 실행을 하다보면 다음과 같이

호출된 함수들이 하나씩 위에서부터 사라지게 됩니다.

(호출스택 부분과 조사식에 n값 주의해서 보기)

c_debug33

c_debug34

c_debug35


지금은 별 이상 없는 코드를 디버깅 해보았지만,

실제로 자신이 작성한 코드가 오류를 뿜을 때

이런 방식으로 메모리값이나 호출함수를 조사하여, 프로그램이

정말 자신이 의도한대로 돌아가고 있는지 확인할 수 있습니다.

한줄한줄 실행하기 때문에 어느 코드에서 잘못됐는지도 알 수 있습니다.




Back to the Goal


문자열을 복사하는 함수를 작성하기

c_tutorial12_goal

#include <stdio.h>

void MyStrcpy(char *dest, int size, char *src)
{
	// 코드 작성하기
}

int main(void)
{
	char str1[12] = { "Cute Teemo" };
	char str2[12] = { 0 };

	printf("str1을 str2에 복사하는 프로그램\n\n");
	printf("str1: %s\n", str1);
	printf("str2: %s\n\n", str2);

	MyStrcpy(str2, sizeof(str2), str1);
	
	printf("str2: %s\n\n", str2);

	return 0;
}

만약 str2의 초기 크기가 3인데,

str1의 문자열의 길이가 3보다 크다고하면

다음과 같이 복사가 진행되지 않고 끝나야합니다.

c_tutorial12_goal2


#include <stdio.h>

void MyStrcpy(char *dest, int size, char *src)
{
	int idx = 0;

	if (strlen(src) + 1 > size)
	{
		printf("dest의 크기가 작아 src를 복사할 수 없습니다.\n");
		return;
	}

	while (src[idx] != '\0')
	{
		dest[idx] = src[idx];
		idx++;
	}

	dest[idx] = '\0';
}

int main(void)
{
	char str1[12] = { "Cute Teemo" };
	char str2[12] = { 0 };

	printf("str1을 str2에 복사하는 프로그램\n\n");
	printf("str1: %s\n", str1);
	printf("str2: %s\n\n", str2);

	MyStrcpy(str2, sizeof(str2), str1);
	
	printf("str2: %s\n\n", str2);

	return 0;
}