c

입출력, 매크로, wsl

haniru 2025. 11. 17. 23:43

다양한 형태로 입출력 하는 함수

 

a.txt에는 

이름1 70 80 90

이름2 40 50 60

이름3 100 20 30

 

이런식으로 텍스트가 들어있다. 

 

a.txt 에서 이름(문자열)과 세 개의 점수(정수)를 순서대로 읽어온 뒤 세 점수의 총점과 평균을 계산한 후 b.txt 파일에 이름, 총점, 평균을 정해진 형식으로 새로 쓴다.

 

1. 서식 지정 입출력: fprintf - 각 데이터 사이에 있는 공백/줄바꿈은 자동 무시, fscanf - 데이터를 지정된 형태로 변환하여 출력

2. 줄/문자열 단위 입출력: fgets - \n을 문자열 끝에 포함하여 읽고 공백문자를 구분자로 사용하지 않고 오직 줄 끝(\n) 또는 파일의 끝(EOF) 까지 읽는다, fputs - 주어진 문자열을 파일 포인터가 가리키는 파일에 출력한

#include <stdio.h>
int main(void) {
    // FILE *ifp, *ofp;
    FILE *ifp;
    FILE *ofp;
    char name[20];
    int kor, eng, math;
    int total;
    double avg;
    int res;

    // ifp = fopen("~/work/basic3/a.txt", "r"); // 상대경로
    // ifp = fopen("./a.txt", "r");
    ifp = fopen("a.txt", "r");
    if (ifp == NULL) {
        printf("Read Error\\n");
        return 1;
    }

    ofp = fopen("b.txt", "w");
    if(ofp == NULL) {
        printf("Write Error");
        return 1;
    }

    while (1) {
        // 1. a.txt 내용을 읽어오기
        res = fscanf(ifp, "%s%d%d%d", name, &kor, &eng, &math);
        if (res == EOF) // EOF는 -1
            break;
        // 2. 총점 구하기
        total = kor + eng + math;
        avg = total / 3.0; // 3.0을 쓰므로 캐스팅 연산자 안써도 됨

        // 3. b.txt에 각 학생의 총점과 평균값을 파일에 출력
        fprintf(ofp, "%s%5d%7.lf\\n", name, total, avg);
    }

    // 4. 리소스 반환
    fclose(ifp);
    fclose(ofp);

    return 0;
}

파일 입출력 함수 비교: 서식 vs. 문자열

함수 목적 처리 단위 서식 지정 주로 사용되는 상황
fscanf 파일에서 읽기 서식(Format)에 맞는 개별 데이터 항목 O (%d, %s, %f 등) 구조화된 숫자 및 문자열 데이터를 읽을 때 (예: CSV 파일, 점수 기록)
fprintf 파일에 쓰기 서식(Format)에 맞춘 출력 문자열 O (%d, %s, %f 등) 데이터를 일정한 형식으로 파일에 기록할 때 (예: 보고서, 포맷된 로그)
fgets 파일에서 읽기 한 줄 전체 (줄바꿈 문자 포함) X (오직 문자열) 파일의 내용을 한 줄씩 읽어와 처리할 때 (예: 텍스트 파일 전체 복사, 줄 단위 파싱)
fputs 파일에 쓰기 문자열 X (오직 문자열) 가공되지 않은 문자열 데이터를 파일에 기록할 때 (예: 줄바꿈 없이 단순 텍스트 쓰기)

 

마찬가지로 a2.txt 에서 글자 읽은 후, 총합과 평균 구해서 b2.txt에 쓰는 코드이다

#include <stdio.h>
int main(void) {
    char name[20];
    int kor, eng, math;
    int total = 0;
    double avg = 0;
    int res;

    FILE *ifp, *ofp;
    
    ifp = fopen("a2.txt", "r");
    if (ifp == NULL) {
        printf("Read Error");
        return 1;
    }

    ofp = fopen("b2.txt", "w");
    if (ifp == NULL) {
        printf("Write Error");
        return 1;
    }

    while (1) {
        res = fscanf(ifp, "%s%d%d%d", name, &kor, &eng, &math);
        if (res == EOF) {
            break;
        }
        total = kor + eng + math;
        avg = (double) total / 3;
        fprintf(ofp, "%s %d %lf\\n", name, total, avg);
    }

    fclose(ifp);
    fclose(ofp);

    return 0;
}

 

 

스트림 파일 입출력 시 버퍼 공유 문제

a3.txt 파일에

25

Tom

이라는 내용이 있을 경우

 

fscanf(fp, "%d", &age) 실행 시 fscanf는 파일에서 정수(25)를 성공적으로 읽어 age에 저장한다. 하지만 fscanf는 숫자 뒤에 남아있는 줄 바꿈 문자는 읽지 않고 입력 버퍼에 남겨둔다.

fgetc(fp) != '\n' 또는 fgets(name, sizeof(name), fp); 실행 시 스트림에 남아있는 줄바꿈 문자를 첫 번째 문자로 인식하고 바로 문자열의 끝으로 간주하여 읽기를 종료한다.

결과적으로 fgets는 a3.txt 파일의 두번째 줄 Tom을 읽지 못하고 name 변수에는 줄바꿈 문자만 포함되거나 첫 번째 줄의 잔여물만 남게된다.

따라서 while(fgetc(fp) != '\n');는 이 문제를 회피하는 일반적인 방법이다

#include <stdio.h>

int main(void) {
    FILE* fp;
    int age;
    char name[20]; // char* name

    fp = fopen("a3.txt", "r");
    if (fp == NULL) {
        printf("Read Error");
        return 1;
    }

    fscanf(fp, "%d", &age);
    while(fgetc(fp) != '\\n'); // 버퍼 공유 문제 처리 팁
    fgets(name, sizeof(name), fp);

    printf("나이: %d, 이름: %s", age, name);
    fclose(fp);
    return 0;
}

 

 

fread와 fwrite: 변환 없는 메모리 입출력

fread와 fwrite는 C 라이브러리의 바이너리 입출력 함수이다.

fwrite: 메모리에 저장된 변수(num = 10이 저장된 4바이트)의 비트 패턴을 그대로 파일에 복사한다. 문자열이나 서식으로 변환하는 과정이 없다.

fread: 파일에 저장된 바이트들을 읽어와 메모리 변수 위치에 그대로 복사한다.

fwrite가 생성한 b4.txt 파일의 내용을 xxd -b b4.txt 로 확인하면 4바이트가 저장되어 있으며, 이 4바이트의 순서가 바로 엔디안을 나타낸다

 

리틀 엔디안: 인텔 x86, amd, arm 계열의 대다수 cpu가 사용한다. 가장 낮은 바이트가 가장 낮은 메모리 주소에 저장된다. (10의 16진수는 0x0000000A이고, b4.txt에 저장되는 순서는 0A 00 00 00)

 

빅 엔디안: 네트워크 프로토콜(tcp/ip, 모토로라, ibm 메인프레임, 초기 맥 컴퓨터의 poserPC cpu 등에서 사용되었다. 데이터의 가장 높은 바이트가 가장 낮은 메모리 주소에 저장된다 (정수 10의 16진수는 0x0000000A 이다. 저장되는 순서 00 00 00 0A)

#include <stdio.h>

// <https://modoocode.com/68>
int main(void) {
    FILE *afp, *bfp; // 파일포인터
    int num = 10;
    int res;

    afp = fopen("a4.txt", "wt"); // text 모드로 write
    fprintf(afp, "%d", num);
    fclose(afp);

    bfp = fopen("b4.txt", "wb"); // binary 모드로 write
    fwrite(&num, sizeof(num), 1, bfp);
    fclose(bfp);

    bfp = fopen("b4.txt", "rb"); // binary 모드로 read
    fread(&res, sizeof(res), 1, bfp); // b4.txt binary 파일을 읽어옴
    printf("%d\\n", res); // 10 정수로 출력

    // size_t: long unsigned int

    fclose(bfp);
    return 0;
}

2진수 00001010 은 16진수로 0A 이다. 따라서 데이터의 가장 낮은 바이트가 파일/메모리의 앞쪽에 저장되는 리틀 엔디안 순서로 b4.txt에 저장되었다. 리틀 엔디안 시스템에서 fread로 읽힐 때 CPU가 0A 00 00 을 정상적으로 역순으로 조합하여 다시 정수 10으로 복원함. 하지만 빅엔디안에서 이를 읽으려고 하면 바이트 순서가 뒤집혀 엉뚱한 값이 읽힌다. 바이너리 입출력 시 엔디안을 고려해야 한다.

 

 

사용자 정의 헤더파일

중복 포함을 방지하기 위해 #pragma once 또는 #ifndef, #define 전처리 지시자를 사용한다

#ifndef, #define, #endif 를 인클루드 가드라고 부른다

#pragma once 지시자도 같은 역할을 하지만 이는 컴파일러 종속적일 수 있다.

 

student.h

#ifndef MY_H
#define MY_H // 이 두줄은 pragma once 랑 같음

//#pragma once

typedef struct {
	int num;
	char name[20];
} Student;

#endif

main.c

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include "student.h" // 사용자 정의 헤더

int main() {
	Student a = { 315, "홍길동" };
	printf("학번: %d, 이름: %s\\n", a.num, a.name);
	return 0;
}

 

 

#define 전처리기 지시자 이해하기

#define은 c/c++ 언어에서 전처리 단계에 사용되는 지시자인데 컴파일이 시작되기 전에 소스코드 내에서 정의된 매크로를 찾아 그 내용으로 문자열 치환을 수행한다. 이런 특성 때문에 빠르다는 장점이 있지만, 타입 검사를 할 수 없어 디버깅이 어렵다.

#define 은 const 처럼 사용할 수 있지만 차이점은 다음과 같다

특징 #define (전처리 상수) const (타입이 있는 상수)
처리 시점 전처리 단계 (컴파일 이전) 컴파일 단계 (일반 변수와 동일)
메모리 사용 대개 메모리를 사용하지 않음 (단순 치환) 메모리를 사용하거나 레지스터에 저장될 수 있음
타입 검사 없음 (문자열 치환) 있음 (자료형 검사)
디버깅 어려움 (치환된 코드가 에러를 발생시킬 수 있음) 쉬움 (컴파일러가 오류를 잡아줌)
예시 #define PI 3.141592 const double PI = 3.141592;

 

 

매크로 함수

#define을 함수처럼 인수를 받아 처리하도록 만들 수 있고, 이를 매크로 함수라고 한다. 일반 함수와 달리 함수 호출 오버헤드 없이 코드를 인라인 하는 효과를 나타낸다. 모든 인수를 괄호로 묶지 않으면, 잘못된 결과가 나올 수 있다 (#define MUL(a, b) a*b 처럼 사용하면 안된다는 뜻)

매크로 함수는 함수 호출이 아니라 코드 치환이기 때문에 함수를 호출할 때 발생하는 스택 푸시/팝 등의 오버헤드가 전혀 없어 매우 빠르게 동작한

매크로 정의 설명
SUM #define SUM(a, b) ((a) + (b)) a와 b를 안전하게 더하기.
MUL #define MUL(a, b) ((a) * (b)) a와 b를 안전하게 곱하기.

 

 

이미 정의된 매크로

컴파일러에 의해 자동으로 값이 설정되며 디버깅이나 로그 기록에 유용하게 사용된다

#line 지시자: 컴파일러에게 다음 코드 줄부터는 지정된 행 번호와 파일 이름을 사용하라고 지시한다. 따라서 이 지시자 이후에 __FILE__과 __LINE__의 값이 변경되어 적용된다.

#include <stdio.h>

void func(void);

int main() {
    printf("컴파일 날짜와 시간 : %s, %s\\n\\n", __DATE__, __TIME__); // __DATE__: 컴파일을 시작한 날짜, __TIME__: 컴파일을 시작한 시간
    printf("파일명: %s\\n", __FILE__); // 전체 디렉터리 경로를 포함한 파일명
    printf("함수명: %s\\n", __FUNCTION__); // 매크로명이 사용된 함수 이름
    printf("행번호: %d\\n", __LINE__); // 매크로명이 사용된 행 번호

    #line 100 "macro.c"
        func();

    return 0;
}

void func() {
    printf("\\n\\n");
    printf("파일명: %s\\n", __FILE__); // macro.c (조작된 파일명)
    printf("함수명: %s\\n", __FUNCTION__); // func
    printf("행번호: %d\\n", __LINE__); // 100번 이후의 행번호 (조작된 행번호)
}
매크로 타입 설명 출력 예시
__DATE__ 문자열 (char*) 소스 파일이 컴파일된 날짜 (Mmm dd yyyy 형식) "Nov 17 2025"
__TIME__ 문자열 (char*) 소스 파일이 컴파일된 시간 (hh:mm:ss 형식) "14:30:55"
__FILE__ 문자열 (char*) 현재 파일의 전체 경로 및 파일 이름 "/home/user/Ex19-4.c"
__FUNCTION__ 문자열 (char*) 매크로가 사용된 현재 함수 이름 "main" 또는 "func"
__LINE__ 정수 (int) 매크로가 사용된 소스 코드의 행 번호 12
__STDC__ 정수 (int) 컴파일러가 표준 C를 준수하면 1로 정의됨 1

 

문자열화 연산자와 토큰연결 연산자

##: 변수 이름이나 함수 이름을 동적으로 생성할 때

#: 디버깅이나 로깅 목적으로 변수 이름이나 표현식을 출력할 

 

전처리 과정

NAME_CAT(a, 1) = 10; > (a ## 1) = 10; > a1 = 10;

NAME_CAT(a, 2) = 20; > (a ## 2) = 20; > a2 = 20;

 

PRINT_EXPR(a1+a2); > printf("a1 + a2" " = %d\n", a1 + a2);

PRINT_EXPR(a2 - a1); > printf("a2 - a1" " = %d\n", a2 - a1);

#include <stdio.h>
#define PRINT_EXPR(x) printf(#x " = %d\\n", x);
#define NAME_CAT(x, y) (x ## y)

int main() {
    int a1, a2;
    NAME_CAT(a, 1) = 10;
    NAME_CAT(a, 2) = 20;
    PRINT_EXPR(a1 + a2);
    PRINT_EXPR(a2 - a1);
    
    return 0;
}

 

 

분할 컴파일 아키텍쳐

분할 컴파일을 사용하여 전역변수를 모듈간에 공유하는 방법

 

point.h

//#pragma once

// header파일 하는 역할: 함수 정의, 구조체 정의
#ifndef _POINT_H_
#define _POINT_H_

// void myprint(); // 함수헤더

typedef struct {
	int x;
	int y;
} Point;

extern Point Gpoint;

#endif

point.c

#include "point.h"

void myprint() {
	// 함수의 구현부가 들어간다
}

Point Gpoint = { 0, 0 };

main.c

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

int main() {
	printf("첫 좌표 Gpoint = (%d, %d)\\n", Gpoint.x, Gpoint.y);
	Gpoint.x = 10;
	Gpoint.y = 20;

	printf("변환 좌표 Gpoint = (%d, %d)\\n", Gpoint.x, Gpoint.y);

	return 0;
}

 

wsl

 

power shell

더보기

bash

uname

uname -a

lsb_realase -a

exit

wsl --list --online

wsl

cd ~/

pwd

ls

ls -a

history

man history

clear

history

!402 # 이렇게 하면 라인에 해당하는 명령어 실행됨

!402:p # 라인에 해당하는 명령어가 뜸

sudo apt install vim

vi hello.c # :set nu 하면 줄이 뜸

ls # 해당 디렉터리에 있는 파일 목록 나열

cd # 디렉터리 이동

pwd # 현재 경로 출력

rm # 파일 또는 디렉터리 삭제 (-rf)

cp # 파일 또는 디렉터리 복사

touch # 크기가 0인 새 파일을 생성, 이미 존재하는 경우 수정 시간 변경

mv # 파일 또는 디렉터리 이름 변경 또는 위치 이동

mkdir # 새로운 디렉터리 생성

cd ~/work

mkdir cmdTest1

ls

cd ~/work/cmdTest1/

touch abc.txt

ls -l

rm abc.txt

echo “Hello World!” > hello.txt # 리눅스 자체가 c 프로그램

cat hello.txt

date

echo “Hello World” > abc.txt

cat abc.txt # 웹에서 restful. 24시간 365일 서버가 켜져있다.

env

env | grep LANG # grep을 사용하면 원하는 단어(LANG)가 포함되는 결과만 찾을 수 있다

env | grep LANG > abc.txt

cat abc.txt

rmdir # 디렉터리 삭제

cat # 텍스트로 작성된 파일을 화면에 출력

head # 텍스트로 작성된 파일의 앞 10행 출력

tail # 텍스트로 작성된 파일의 마지막 10행 출력

more # 텍스트로 작성된 파일을 화면에 페이지 단위로 출력

less # more과 비슷

file # 어떤 종류의 파일인지

cd /var/log

cat dpkg.log

head dpkg.log

head -3 dpkg.log # 위에서 3줄

tail -5 dpkg.log # 밑에서 5줄

 

오라클 리눅스 설치: 오라클 리눅스는 클라우드 서버, 분산 컴퓨팅에 많이 사용된다. 또 리더 역할을 하는 서버가 죽었을 때 다른 서버가 자동으로 역할을 이어받는 클러스터링 및 HA 시스템은 분산 컴퓨팅의 핵심이다. 오라클 리눅스는 Unbreakable Enterprise Kernel을 제공한다.

더보기

wsl --list --online

wsl --install -d OracleLinux_9_5

 

sudo dnf update

sudo dnf groupinstall "Development Tools" # 강력한 툴체인. 우분투에서는 sudo apt install build-essential

gcc

g++

make

sudo dnf install nano

nano hello.c

gcc -o hello hello.c

./hello

exit

wsl --list --verbose

'c' 카테고리의 다른 글

c 언어 문제  (0) 2025.11.14
파일처리  (0) 2025.11.13
구조체  (0) 2025.11.12
배열, 포인터와 구조  (0) 2025.11.11
이중포인터 N차원 배열  (1) 2025.11.10