드디어! 프논사 수업의 마지막 포스팅입니다!

오늘은 꼭 필요하다고 볼 수 있는 동적 할당과,

자주 쓸 것 같으면서도 수업에서는 딱히 쓸 일이 없는 파일 입출력,

마지막으로 안해도 상관은 없지만 하는게 국룰인 분할 컴파일을 배워보도록 하겠습니다.

 

목차

     


    0. 동적 할당

    우리는 지금까지 메모리를 사용할 때 이만큼 사용하겠다고 컴퓨터님께 안내드리고 사용했습니다.

    그런데 가끔은 갑자기 많이 쓰고싶기도 하고, 가끔은 쓸데없이 많이 잡아놔서 줄이고 싶을 때가 있을 수 있잖아요?

    그럴 때 꼭 필요한 것이 동적 메모리 할당입니다.

     

    정적 메모리와 동적 메모리의 차이는 아래 표를 참고하시면 됩니다.

    사실 이런 표는 동적 메모리를 몇 번 사용해보면 자동으로 체득하는 내용이기 때문에 한번 훑어보기만 해도 충분합니다!

    정적 메모리와 동적 메모리의 차이

     

    ※ 주의! 다소 어려울 수 있음! (하지만 읽다보면 충분히 이해됨)

     

    배열의 크기를 미리 알 수 없을 때, 배열의 크기는 임의의 변수로 지정할 수 없습니다.

    상수가 아닌 변수를 [ ] 안에 넣는 순간, 바로 오류를 뿜어내죠. (처음 C/C++을 만났을 때 마주치는 가장 빈번한 오류입니다)

    이 때, 배열의 최대 크기를 가정해서 배열을 할당하면 메모리 낭비가 극심할 수 있습니다.

    만약 이럴 때 동적 메모리를 사용해서 프로그래머가 원하는 만큼 메모리를 할당할 수 있다면?

    메모리의 할당과 해제 시점을 프로그래머가 마음대로 선택할 수 있다면?

    바로 이 동적 메모리가 메모리 사용에 있어서 프로그래머에게 최대한의 자유를 보장해주게 됩니다.

     

    C언어는 동적 메모리를 할당할 때 malloc이라는 함수를 사용합니다.

    이 함수는 stdlib.h라는 라이브러리를 먼저 불러와야 사용할 수 있습니다. (#include <stdlib.h> 필요!!)

    매개변수 size는 할당할 메모리의 바이트 크기이고, malloc의 리턴값은 할당된 메모리의 주소입니다.

    void 포인터를 반환하는 이유는 어떤 자료형의 주소값을 참조할 지 모르기 때문입니다.

    그리고 이 void 포인터가 만능인 이유가 바로 어떤 자료형이든지 형변환만하면 바로 참조할 수 있다는 것입니다.

    아래는 int만큼 동적할당을 했기 때문에 (sizeof(int)에다가 size만큼의 정수를 받을 예정이라) int*형으로 형변환을 했고,

    만약 char*형으로 동적메모리를 할당한다면 void*형을 char*형으로 형변환 해주면 되는 것입니다. 생각보다 간단하죠? 

    이렇게 받아온 값을 따로 저장만 해 두면 됩니다. 이 주소 잃어버리면 나중에 필요없어서 지우고 싶을때 못지우고 메모리 낭비되고 난리납니다!! (들어올 때는 내 맘대로지만 주소 잃어버리면 못나감 ;;)

     

    즉 malloc을 통해 동적으로 메모리를 할당하고 주소값을 받아와서 어딘가에 저장해두면 되겠구나! 라고 생각하시면 되겠습니다.

    만약 동적 메모리를 할당할 수 없다면 malloc 함수는 NULL을 반환합니다.

    malloc 함수의 원형
    void*형은 어떤 형태이든 가리킬 수 있는 대신 반대로 참조할 수는 없기 때문에 (int*)형으로 바꿔서 참조합니다.
    void*로 리턴된 친구를 int*로 바꿔서 arr에 저장!! arr은 동적으로 잡은 메모리 주소 맨 앞을 가리킴!!

     

    이렇게 동적으로 메모리를 사용하기 위해 할당받은 주소를 담은 포인터 변수는 배열의 원소를 가리키는 포인터처럼 사용할 수 있습니다.

    arr[i]에다가 원하는 수 넣어버리겠다는 소리

     

    다만 이렇게 동적으로 할당을 받았다면, 할당을 해제하는 것도 프로그래머의 몫입니다.

    해제를 안하면 바로 Memory leak(메모리 누수)가 발생합니다. 이거 계속 말하지만 해제 진짜 중요합니다.

     

    해제하는 방법은 free 함수를 사용하면 됩니다. memblock 위치에 아까 주소값을 넣어둔 포인터 변수를 넣어주면 됩니다.

     

    그리고 동적 메모리를 해제한 후에는 적 메모리를 가리키던 포인터 변수에 NULL을 대입하는 것이 안전합니다.

    어짜피 그 주소에 있는 메모리 못쓰게 만들었는데 혹시나 접근할 수도 있으니까 미연에 방지하는거죠!

     

    아래는 동적 메모리의 이용 예입니다.

    원리만 이해하면 코드 자체는 별로 어렵지 않아요!

    /* Ex11_10.c */
    #include <stdio.h>
    #include <stdlib.h>
    
    int main()
    {
    	int size;
    	int* arr = NULL;
    	int sum = 0;
    	double average = 0.0;
    	int i;
    
    	printf("몇개의 정수를 입력하시겠습니까? : ");
    	scanf_s("%d", &size);
    
    	arr = (int*)malloc(sizeof(int) * size); // 동적 메모리 할당
    	if (arr == NULL)
    	{
    		printf("동적 메모리 할당 실패\n");
    		return -1;
    	}
    
    	printf("%d개의 정수를 입력하세요: ", size);
    	for (i = 0; i < size; i++) // 동적 메모리 사용
    		scanf_s("%d", &arr[i]);
    
    	for (i = 0; i < size; i++)
    		sum += arr[i];
    
    	average = (double)sum / (double)size;
    
    	printf("합계: %d, 평균: %f\n", sum, average);
    
    	free(arr); // 동적 메모리 해제
    	arr = NULL;
    
    	return 0;
    }

     


    1. 파일 입출력

    파일 입출력을 할 때는 먼저 스트림을 생성하고 그 후에 입출력을 수행해야 합니다.

    다행히도 파일 입출력 함수는 stdio.h 라이브러리에 있기 때문에 따로 라이브러리를 include 할 필요는 없습니다.

     

    fopen(file open의 줄임말!!) 함수로 파일을 열면 파일 포인터가 리턴됩니다.

    파일에 접근하려면 이 파일 포인터가 반드시 필요합니다!

     

    파일을 열고 나서는 여러 가지 파일 입출력 함수를 이용해서 입출력 작업을 수행하면 됩니다.

    텍스트 파일 입출력 함수에는 fgetc(글자 하나 읽기), fputc(글자 하나 넣기), fgets(문자열 하나 읽기), fputs(문자열 하나 넣기), fscanf(파일 내용 읽기), fprintf(파일 내용 쓰기) 등이 있고,

    바이너리 파일 입출력 함수에는 fread(바이너리 파일에서 데이터 읽기), fwrite(바이너리 파일에서 데이터 쓰기) 등이 있습니다.

    바이너리 파일은 데이터를 NULL 문자가 아닌 숫자로만 판단합니다.

     

    이것저것 작업을 끝내고 나면 파일을 닫아야 하는데, 파일을 닫을 때는 fclose 함수를 사용합니다.

     

    fopen 함수를 살펴보면, 첫 매개변수로 파일 이름을 넣고, 두 번째에는 파일 열기 모드를 넣습니다.

    반환값은 생성된 파일 스트림의 파일 포인터를 반환하고, 만약 파일을 열 수 없다면 NULL이 반환됩니다.

    그리고 기본적으로 파일을 텍스트 파일로 간주합니다.

     

    파일 열기 모드에는 다양한 것들이 있는데, 아래 표를 보시고 필요에 따라 사용하시면 됩니다.

     

    이번엔 fclose 함수를 살펴보면, 보다 간단합니다. 매개변수로 아까 열었던 파일 포인터만 넣으면 됩니다.

    만약 정상적으로 파일을 닫았으면 0을 반환하고, 실패하면 EOP(-1)을 반환합니다.

     

    몇 가지 파일 입출력 함수를 살펴보면 ferror 함수는 파일 입출력시 발생하는 에러를 확인합니다.

    매개변수로는 파일 포인터를 받습니다.

    만약 에러가 발생하지 않았으면 0을 반환하고, 에러가 나면 0이 아닌 값을 반환합니다.

     

    feof 함수는 파일의 끝인지를 검사합니다. eof는 End Of File의 준말이며, 파일의 끝을 표현하기 위해 정의해 놓은 상수입니다. 이 친구도 파일 포인터를 매개변수로 받습니다.

    읽은 위치가 파일의 끝이 아니면 0을 반환하고, 끝이면 0이 아닌 값을 반환합니다.

     

    그 외에도 다양한 파일 입출력 함수가 있습니다. 아래 표를 보고 필요에 맞게 사용하시면 됩니다!

    (fscanf와 fprintf는 맨 앞에 파일 포인터를 매개변수로 적고, 그 뒤 매개변수들은 scanf와 printf와 동일합니다!)

     


    2. 전처리기 - 분할 컴파일

    2.0. 분할 컴파일의 기초

    만약 작성해야 할 코드의 양이 많거나 여러 사람이 공동 개발을 해야 하는 경우에는 소스 파일을 여러 개로 나누어서 프로그램을 개발해야 합니다.

    이 때 관련 함수나 변수끼리 한 파일로 모아서 작성하면 유지 보수하기가 굉장히 쉬워집니다.

     

    + 프논사 맨 첫 번째 글에서 VS가 분할 컴파일에 특화되었기 때문에 유지보수 하기 유리하다고 이야기한 적이 있습니다.

    다시 보고 싶으시다면 아래 링크 클릭!!

     

     

    [프논사] 0. C언어의 개요

    이번 카테고리에서는 학교 교양수업으로 듣게 된 '프로그래밍을통한논리적사유연습' 과목에 대한 수업 내용 정리를 할 예정입니다. 아무래도 수업을 듣는 학생들이 대부분 문과생이다보니 (이

    ssocoit.tistory.com

     

     

    그리고 서로 다른 함수들이 수두룩빽빽하게 쌓여있다면, 이 함수가 도대체 어디 있는 것인지 알아보기 힘들 것입니다.

    이럴때 메뉴판처럼 간략하게 사용법만 나와있는 헤더파일이 있다면 보다 편리하게 함수를 사용할 수 있습니다.

    또한 기능별로 파일을 나누고 헤더파일까지 나눠놓으면 다른 .c 파일이나 .h파일과의 종속성을 최대한으로 피하면서 줄일 수 있습니다. 그리고 헤더파일을 나누어놓으면 헤더의 전처리부분에서 중복을 제거한 후 main에서 include 하기도 편합니다. 이렇게 다양한 메리트가 있기 때문에 큰 작업에서는 .c와 .h로 나누어서 모듈화를 합니다.

     

    이와 비슷한 내용을 두 번째 포스팅에서 간단하게 다룬 적이 있어서 링크를 올립니다!!

     

     

    [프논사] 1. 주석과 함수

    이번 포스팅부터는 실제로 코드작성을 할 때 사용하는 여러 문법에 대해서 공부할 예정입니다. 만약 프로그래밍 언어에 대해 아무 지식도 없다면 하나하나 꼼꼼히 따져가면서 이해하면 나중에

    ssocoit.tistory.com

     

    아무튼 헤더 파일은 서로 다른 소스 파일 사이에서 필요한 정보를 공유할 수 있게 만들어줍니다.

    파일 확장자로는 .h를 사용하고, 소스 파일에 정의된 함수나 전역 변수를 사용하는 데 필요한 정보를 제공합니다.

    만약 소스 파일에서 헤더 파일을 포함하려면 #include를 이용하면 됩니다.

     

    전처리기 문장인 #include문을 통해 헤더파일은 소스파일 안에 포함되고 컴파일됩니다.

    전처리기는 #include가 지정한 헤더 파일의 내용을 #include가 쓰인 위치로 복사해서 넣어줍니다.

    라이브러리 헤더인 stdio.h를 넣는 것과 똑같이 해주면 됩니다. 사용자 정의 헤더의 경우 " "로 넣어줘야 한다는 차이만 빼구요!

     

    #include가 라이브러리 헤더를 가져올 때는 전처리기가 헤더 파일을 C 컴파일러의 포함 경로에서 찾습니다. 그 곳에 컴파일러가 제공하는 표준 라이브러리 헤더 파일이 모여있거든요.

     

    #innclude가 사용자 정의 헤더를 가져올 때는 전처리기가 헤더 파일을 소스 파일이 있는 디렉토리에서 찾습니다.

     

    많이 쓰는 위와 같은 방식이 아니라 완전 경로 혹은 상대 경로를 작성해도 됩니다.

    (저는 아직 경험이 부족해서 그런지 저렇게 써본 적이 없습니다..)


    2.1. 다른 파일에 정의된 함수의 호출

    C 컴파일러는 각각의 소스파일마다 독립적으로 컴파일을 수행합니다.

    만약 함수를 호출할 때 함수의 선언이나 정의가 없다면 컴파일 경고가 발생합니다.

     

    다른 파일에 정의된 함수를 호출하기 위해서는 함수의 선언이 필요합니다.

     

    소스 파일이 여러개라면 소스 파일마다 함수의 선언이 필요합니다.

     

    이 때 헤더파일을 이용하면 함수 선언을 한 곳에 모아둘 수 있습니다!!

    굳이 Main.c, A.c, B.c, C.c에서 4번이나 함수를 찍어낼 필요가 없는 것이죠.

     

    먼저 소스 파일의 이름을 따서 헤더 파일을 만듭니다. (Array.c에 함수를 정의했다면 Array.h에 함수를 선언하는 겁니다!)

    이제 만든 함수를 호출하기 위해서는 소스 파일마다 헤더 파일을 포함시켜줍니다. (#include "Array.h"를 넣어줍니다!)

    + 함수를 정의한 소스 파일에도 헤더파일을 포함해줍시다!

    소스 파일은 각각 독립적으로 컴파일되기 때문에 라이브러리 헤더는 라이브러리를 사용하는 소스 파일마다 각각 포함해줘야 합니다. (라이브러리 헤더가 필요한 곳이라면 라이브러리 헤더를 필요한 곳에 모두 불러와야 합니다!)

     

    순서대로 정리하면 아래와 같습니다.

     

    0) 먼저 호출될 함수의 정의가 들어있는 소스 파일의 이름을 따서 헤더 파일을 생성합니다.

    1) 헤더 파일에는 다른 소스 파일에서 호출될 함수의 선언을 넣어줍니다.

    2) 다른 소스 파일에서 해당 함수를 호출하려면 함수 선언이 들어있는 헤더 파일을 포함시킨 뒤에 사용합니다.


    2.2. 분할 컴파일 - 기타

    매크로, 구조체, typedef는 소스 파일마다 정의가 필요하기 때문에 매크로, 구조체, typedef의 정의를 헤더 파일에 넣어줍니다.

    만약 헤더 파일에 구조체나 typedef를 사용하는 함수 선언이 들어갈 때는 구조체나 typedef 정의 다음에 함수 선언을 넣어줍니다. (C 컴파일러는 위에서부터 순차적으로 읽기 때문에 무언가 사용할 때 아직 읽지 못했다면 사용할 수 없습니다)

    한 소스 파일에 같은 헤더 파일이 여러번 포함될 수 없습니다!
    point 구조체가 뒤에 있으면 선언문이 POINT가 뭔지 몰라서 못씁니다 ㅠ

     

    만약 헤더 파일의 중복을 피하고 싶으면 헤더 파일의 시작과 끝에 #ifndef, #define, #endif를 넣어줍니다.

    #infdef나 #define에 헤더 파일의 포함 여부를 알려주는 매크로 심볼을 지정합니다.

    #ifndef는 if not define의 줄임말로, 만약 정의되어있지 않다면 수행하는 것입니다.

    아래에서는 POINT_H가 처음에는 정의되어있지 않기 때문에 컴파일 됐다가, 두 번째 ifndef~#endif 블록에서는 이미 정의가 되어있는 상태기 때문에 #endif까지 문장이 무시됩니다.

    이미 선언 됐으면 더이상 실행 ㄴㄴ

     

    보통 #ifndef와 #define 다음에 사용되는 매크로 심볼은 헤더 파일의 이름을 따서 만듭니다. (위에서도 POINT_H!!)

    이 매크로 심볼의 정의 여부로 헤더 파일이 포함되었는지를 확인합니다.

    가능한 모든 헤더 파일에 #ifndef를 넣어주는 것이 안전합니다.

    실제로 표준 C 라이브러리 헤더에도 이미 이 기능이 사용되고 있습니다.

    (추가로, 저는 C++을 처음 배웠을 때 #pragma once를 사용했습니다. 하지만 이건 특정 컴파일러에 맞는 전처리문이기 때문에 보다 확실한 중복처리가 필요하다면 #ifndef~를 사용합시다!!)

     

    마지막으로 두 번째 포스팅에서 올렸던 헤더 파일과 소스 파일의 구성 사진으로 마칩니다.


    확실히 C++를 배우고 C를 배우는 거랑 썡으로 배우는 것에는 큰 차이가 있습니다. 그만큼 첫 언어로 C를 접하는 것은 요즘 시대에는 굉장한 불편함을 안고 가는 것이라고 생각합니다.

    하지만 여기까지 모두 읽으신 분들은 그런 불편함을 감수하고도 공부 혹은 복습을 하셨다는 것이니 정말 존경스럽습니다. 과연 몇 명이나 읽으려나 싶긴 하지만..!

     

    아무튼 꽤 긴 시간 끝에 프논사 수업을 마무리 지었습니다. 아직 16주차까지 완강한 것은 아니지만, 시험도 코앞이다보니 한번 싹 정리할 목적으로 이렇게 서둘러 마무리를 지었습니다. 혹시나 부족한 부분, 틀린 부분이 있다면 주저없이 댓글 남겨주시면 최대한 빠르게 피드백하겠습니다. 질문도 언제든지 환영합니다 :)

     

    그렇다면 여러분들 즐거운 C/C++ 라이프 되시길 바랍니다!! 고생 많으셨어요 ㅎㅎ

    반응형
    • 네이버 블로그 공유하기
    • 네이버 밴드에 공유하기
    • 페이스북 공유하기
    • 카카오스토리 공유하기