본문 바로가기
IT & AI/AI 지식

C 언어 메모리 동적 할당, 구조체, 파일 입출력

by 빛나는해커 2024. 11. 20.

1.  메모리 동적 할당

 

동적 할당 함수

  • 동적 메모리 할당은 프로그램 실행 중에 메모리를 필요할 때 할당하고, 사용이 끝나면 해제하는 것을 말한다.
  • malloc 함수는 지정된 크기의 메모리를 힙 영역에 할당하며, 성공하면 해당 메모리의 시작 주소를 반환한다.
  • 할당된 메모리를 다 사용하면 free 함수를 사용하여 반드시 해제해야 한다.
#include <stdlib.h>

int main() {
    int *p = (int *)malloc(sizeof(int) * 5);  // 정수 5개에 해당하는 메모리 할당
    if (p == NULL) {
        printf("메모리 할당 실패\n");
        return 1;
    }

    for (int i = 0; i < 5; i++) {
        p[i] = i + 1;
        printf("p[%d] = %d\n", i, p[i]);
    }

    free(p);  // 동적 메모리 해제
    return 0;
}
  • malloc을 통해 할당받은 메모리는 배열처럼 인덱스를 통해 접근할 수 있다.
  • 위 예시에서 p [i]와 같은 방식으로 메모리를 배열처럼 활용할 수 있다.
  • 동적 메모리를 할당하는 함수로는 calloc과 realloc도 있다.
  • calloc은 할당된 메모리를 초기화하며, realloc은 이미 할당된 메모리의 크기를 조절한다.
int *arr = (int *)calloc(5, sizeof(int));  // 정수 5개 크기의 메모리 할당 및 0으로 초기화
arr = (int *)realloc(arr, sizeof(int) * 10);  // 메모리 크기 재조정
free(arr);

 

동적 할당 저장 공간의 활용

  • 동적 할당을 사용하여 문자열을 저장할 수 있다.
  • 문자열의 길이가 실행 중에 결정되는 경우 malloc을 사용하여 메모리를 할당하고 문자열을 저장할 수 있다.
char *str = (char *)malloc(20 * sizeof(char));  // 길이 20의 문자열 저장 공간 할당
strcpy(str, "Hello, World!");
printf("문자열: %s\n", str);
free(str);  // 동적 메모리 해제
  • 동적 할당된 문자열을 함수로 전달하여 처리할 수 있다.
void printString(char *s) {
    printf("문자열: %s\n", s);
}

int main() {
    char *str = (char *)malloc(50 * sizeof(char));
    strcpy(str, "동적 할당된 문자열");
    printString(str);
    free(str);
    return 0;
}
  • 명령행 인수를 사용하는 방법으로 프로그램 실행 시 외부에서 입력된 값을 받아 동적으로 메모리를 활용할 수 있다.
int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("사용법: %s <문자열>\n", argv[0]);
        return 1;
    }
    printf("입력된 인수: %s\n", argv[1]);
    return 0;
}

2.  사용자 정의 자료형

 

구조체

  • 구조체는 여러 개의 데이터를 하나의 그룹으로 묶어서 관리할 수 있는 사용자 정의 자료형이다.
  • 구조체는 struct 키워드를 사용하여 선언한다.
typedef struct {
    char name[20];
    int age;
    float height;
} Person;

int main() {
    Person person = {"Alice", 25, 170.5};
    printf("이름: %s, 나이: %d, 키: %.1f\n", person.name, person.age, person.height);
    return 0;
}
  • 구조체 멤버로 배열, 포인터, 다른 구조체 등을 포함할 수 있다.
  • 이를 통해 복잡한 데이터를 간단히 표현할 수 있다.
typedef struct {
    char name[30];
    int scores[5];
    float *grades;
} Student;
  • 구조체 변수를 선언하면서 초기화할 수 있으며, 구조체 변수 간의 대입 연산도 가능하다.
Person p1 = {"Bob", 30, 180.0};
Person p2;
p2 = p1;  // p2에 p1의 값 대입
  • 구조체 변수는 함수의 매개변수로 전달할 수 있으며, 주소를 전달하여 효율성을 높일 수 있다.
void printPerson(Person *p) {
    printf("이름: %s, 나이: %d, 키: %.1f\n", p->name, p->age, p->height);
}

int main() {
    Person person = {"Charlie", 22, 175.0};
    printPerson(&person);
    return 0;
}

 

구조체 활용, 공용체, 열거형

  • 구조체 포인터를 사용하면 -> 연산자를 통해 구조체 멤버에 접근할 수 있다.
Person person = {"Dave", 28, 165.0};
Person *p = &person;
printf("이름: %s, 나이: %d\n", p->name, p->age);
  • 구조체 배열을 사용하면 여러 개의 구조체를 효율적으로 관리할 수 있다.
Person people[3] = {
    {"Eve", 20, 160.0},
    {"Frank", 27, 175.5},
    {"Grace", 22, 168.0}
};
for (int i = 0; i < 3; i++) {
    printf("이름: %s, 나이: %d\n", people[i].name, people[i].age);
}
  • 구조체 배열을 함수로 전달하여 처리할 수 있다.
void printPeople(Person people[], int size) {
    for (int i = 0; i < size; i++) {
        printf("이름: %s, 나이: %d, 키: %.1f\n", people[i].name, people[i].age, people[i].height);
    }
}

int main() {
    Person people[2] = { {"Hank", 24, 172.0}, {"Ivy", 29, 158.5} };
    printPeople(people, 2);
    return 0;
}
  • 자기 참조 구조체는 구조체 안에 자기 자신을 가리키는 포인터를 포함할 수 있다.
  • 이를 통해 연결 리스트 같은 자료구조를 구현할 수 있다.
typedef struct Node {
    int data;
    struct Node *next;
} Node;
  • 공용체는 union 키워드를 사용하며, 여러 멤버를 공유하여 하나의 메모리 공간을 절약하는 데 유용하다.
typedef union {
    int i;
    float f;
    char str[20];
} Data;

int main() {
    Data data;
    data.i = 10;
    printf("정수: %d\n", data.i);
    data.f = 220.5;
    printf("실수: %.2f\n", data.f);
    strcpy(data.str, "Hello");
    printf("문자열: %s\n", data.str);
    return 0;
}
  • 열거형은 enum 키워드를 사용하여 관련된 상수들의 집합을 정의한다.
typedef enum {
    RED,
    GREEN,
    BLUE
} Color;

int main() {
    Color color = GREEN;
    printf("색상: %d\n", color);  // 출력: 1
    return 0;
}
  • typedef를 사용하면 기존 자료형에 새로운 이름을 정의할 수 있다.
typedef unsigned long ulong;
ulong num = 1000;
printf("num: %lu\n", num);

3.  파일 입출력

 

파일 개방과 입출력

  • 파일을 사용하기 위해서는 먼저 fopen 함수를 사용하여 파일을 개방하고, 사용 후에는 fclose 함수를 사용해 파일을 닫아야 한다.
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
    printf("파일 열기 실패\n");
    return 1;
}
// 파일 작업 수행
fclose(fp);
  • 파일 입출력은 파일 포인터를 사용하며, 스트림을 통해 데이터를 읽고 쓸 수 있다.
  • 파일 포인터는 FILE 형식이며, 파일을 읽고 쓰는 데 중요한 역할을 한다.
  • fgetc 함수는 파일에서 한 문자를 읽어온다.
  • 파일이 끝에 도달하면 EOF를 반환한다.
char ch;
while ((ch = fgetc(fp)) != EOF) {
    printf("%c", ch);
}
  • fputc 함수는 파일에 한 문자를 쓴다.
FILE *fp = fopen("output.txt", "w");
if (fp != NULL) {
    fputc('A', fp);
    fclose(fp);
}
  • C 언어에서는 프로그램이 시작될 때 자동으로 세 가지 표준 스트림 파일이 개방된다:
    • 1. stdin (표준 입력)
    • 2. stdout (표준 출력)
    • 3. stderr (표준 오류)
  • 이들은 각각 사용자로부터 데이터를 입력받거나 출력하기 위해 사용된다.
  • 파일은 텍스트 파일과 바이너리 파일로 구분되며, 파일을 개방할 때 모드를 설정하여 구분할 수 있다.
  • 텍스트 파일은 사람이 읽을 수 있는 형식이고, 바이너리 파일은 컴퓨터가 이해하는 형식으로 저장된다.
FILE *fp1 = fopen("textfile.txt", "r");  // 텍스트 파일 개방
FILE *fp2 = fopen("binaryfile.bin", "rb");  // 바이너리 파일 개방
  • + 모드: r+, w+, a+ 등과 같이 사용하여 읽기와 쓰기를 모두 가능하게 한다.
  • fseek: 파일 내의 특정 위치로 이동할 때 사용한다.
fseek(fp, 0, SEEK_SET);  // 파일의 처음으로 이동
  • rewind: 파일 포인터를 파일의 처음으로 이동시킨다.
rewind(fp);
  • feof: 파일의 끝에 도달했는지 확인할 때 사용한다.
if (feof(fp)) {
    printf("파일의 끝에 도달했습니다.\n");
}

 

다양한 파일 입출력 함수

  • fgets 함수는 파일에서 한 줄씩 읽어올 때 사용한다.
  • fputs 함수는 문자열을 파일에 쓸 때 사용된다.
FILE *fp = fopen("data.txt", "r");
char buffer[100];
if (fp != NULL) {
    while (fgets(buffer, sizeof(buffer), fp) != NULL) {
        printf("%s", buffer);
    }
    fclose(fp);
}

FILE *fp_out = fopen("output.txt", "w");
if (fp_out != NULL) {
    fputs("Hello, World!\n", fp_out);
    fclose(fp_out);
}
  • fscanf와 fprintf는 파일에서 다양한 형태의 데이터를 읽고 쓸 수 있게 해 준다.
FILE *fp = fopen("data.txt", "r");
int num;
char str[20];
if (fp != NULL) {
    fscanf(fp, "%d %s", &num, str);
    printf("읽은 값: %d, %s\n", num, str);
    fclose(fp);
}

FILE *fp_out = fopen("output.txt", "w");
if (fp_out != NULL) {
    fprintf(fp_out, "정수: %d, 문자열: %s\n", 100, "Hello");
    fclose(fp_out);
}
  • 파일의 출력 버퍼는 즉시 반영되지 않고 버퍼에 저장되었다가 나중에 쓰일 수 있다.
  • 이를 해결하기 위해 fflush 함수를 사용하여 버퍼에 있는 데이터를 즉시 파일에 반영할 수 있다.
FILE *fp = fopen("output.txt", "w");
if (fp != NULL) {
    fputs("버퍼에 저장된 데이터\n", fp);
    fflush(fp);  // 버퍼를 비우고 파일에 반영
    fclose(fp);
}
  • fread와 fwrite 함수는 주로 바이너리 파일을 처리할 때 사용되며, 특정 크기만큼 데이터를 읽거나 쓴다.
FILE *fp = fopen("binaryfile.bin", "wb");
int data[5] = {1, 2, 3, 4, 5};
if (fp != NULL) {
    fwrite(data, sizeof(int), 5, fp);  // 데이터 배열을 파일에 씀
    fclose(fp);
}

fp = fopen("binaryfile.bin", "rb");
int read_data[5];
if (fp != NULL) {
    fread(read_data, sizeof(int), 5, fp);  // 파일에서 데이터를 읽어옴
    for (int i = 0; i < 5; i++) {
        printf("%d ", read_data[i]);
    }
    fclose(fp);
}

4.  전처리와 분할 컴파일

 

전처리 지시자

  • #include 지시자는 외부 파일을 포함할 때 사용한다.
  • 주로 표준 라이브러리나 사용자 정의 헤더 파일을 포함하기 위해 사용된다.
#include <stdio.h>
#include "myheader.h"
  • #define을 사용하여 상수나 코드를 매크로로 정의할 수 있다.
#define PI 3.14159
#define SQUARE(x) ((x) * (x))
  • 매크로 함수는 간단한 연산을 정의할 때 유용하며, 인라인 치환으로 실행 속도를 높일 수 있다.
  • C 언어에서는 이미 정의된 여러 매크로가 있으며, 예를 들어 __FILE__, __LINE__ 등이 있다.
printf("파일 이름: %s, 라인 번호: %d\n", __FILE__, __LINE__);
  • # 연산자는 매크로의 인수를 문자열로 변환한다.
  • ## 연산자는 매크로 인수를 결합한다.
#define STRINGIFY(x) #x
#define CONCAT(a, b) a##b
  • 조건부 컴파일은 특정 조건에 따라 코드의 일부를 컴파일할지 결정하는 기능이다.
#ifdef DEBUG
    printf("디버그 모드\n");
#endif

 

분할 컴파일

  • 프로그램을 여러 파일로 나누어 관리하고 컴파일하는 것을 분할 컴파일이라고 한다.
  • 이를 통해 코드의 재사용성과 관리 효율성을 높일 수 있다.
  • extern: 전역 변수를 다른 파일에서 사용할 수 있도록 선언할 때 사용된다.
  • static: 파일 내에서만 접근 가능한 변수를 정의할 때 사용한다.
// a.c 파일
int globalVar = 10;

// b.c 파일
extern int globalVar;
  • 헤더 파일은 함수 선언과 매크로 정의를 포함하여 여러 파일에서 공통으로 사용할 수 있다.
  • 중복 포함 문제를 해결하기 위해 #ifndef, #define, #endif 가드가 사용된다.
#ifndef MYHEADER_H
#define MYHEADER_H

void myFunction();

#endif

C 언어 기본 메모리 동적 할당, 구조체, 파일 입출력 소개 이미지