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
'IT & AI > AI 지식' 카테고리의 다른 글
JAVA 조건문, 반복문, 참조 타입(배열) 정리 (2) | 2024.11.21 |
---|---|
JAVA 초보자를 위한 변수, 연산자, 데이터 입출력 이해 (1) | 2024.11.20 |
C 언어 변수, 다차원 배열, 응용 포인터 정리 (1) | 2024.11.19 |
C 언어 배열, 포인터, 문자열 활용 심화 (0) | 2024.11.19 |
C 언어 함수, 배열, 포인터 사용법과 예제 (0) | 2024.11.18 |