Skip to content

Latest commit

 

History

History
636 lines (520 loc) · 19.4 KB

211101.md

File metadata and controls

636 lines (520 loc) · 19.4 KB

Day 31

Review C

연결 리스트

데이터가 담긴 노드(메모리 공간)를 일렬로 연결해 놓은 것

  • 리스트의 중간 지점에 노드를 손쉽게 추가하거나 삭제할 수 있다.
  • 특정 노드를 찾으려면 노드를 모두 검색해야 한다.
  • 크기가 고정되어 있지 않다.

연결 리스트 구조체 만들고 사용

struct NODE { // 연결 리스트의 노드 구조체
    struct NODE *next; // 다음 노드의 주소를 저장할 포인터
    int data; // 데이터를 저장할 멤버
};

노드의 종류

  • head node : 단일 연결 리스트의 기준점이며 head라고도 불린다. head node는 첫 번째 노드를 가리키는 용도이므로 데이터를 저장하지 않는다.
  • node : 단일 연결 리스트에서 데이터가 저장되는 실제 노드다.

head -> node1 -> nod2 -> NULL
같은 모양이 된다.

struct NODE *head = malloc(sizeof(struct NODE)); // head node 생성, 데이터 저장하지 않음

struct NODE *node1 = malloc(sizeof(struct NODE)); // 첫 번째 노드 생성
head->next = node1; // head node 다음은 첫 번째 노드
node1->data = 10; // 첫 번째 노드에 data 저장

struct NODE *node2 = malloc(sizeof(struct NODE));
node->next = node2;
node2->data = 20;

node2->next = NULL; // 다음 노드가 없음

struct NODE *cur = head->next; // 연결 리스트 순회용 포인터에 첫 번째 노드의 주소 저장
while (cur != NULL) // 포인터가 NULL이 아닐 때 계속 반복
{
    printf("%d", cur->data);
    cur = cur->next;
}

free(node2);
free(node1);
free(head);

연결 리스트에서 노드를 추가하는 규칙

  • 노드에 메모리 할당
  • next 멤버에 다음 노드의 메모리 주소 저장
  • data 멤버에 데이터 저장
  • 마지막 노드라면 next 멤버에 NULL 저장

노드 추가 함수

void addFirst(struct NODE *target, int data) // 기준 노드 뒤에 노드를 추가하는 함수
{
    struct NODE *newNode = malloc(sizeof(struct NODE)); // 새 노드 생성
    newNode->next = target->next; // 새 노드의 다음 노드에 기준 노드의 다음 노드를 지정
    newNode->data = data;

    target->next = newNode; // 기준 노드의 다음 노드에 새 노드를 지정
}

struct NODE *head = malloc(sizeof(struct NODE));

head->next = NULL;

addFirst(head, 10); // 머리 노드 뒤에 새 노드 추가
addFirst(head, 20);

struct NODE *cur = head->next;
while (cur != NULL) // node 메모리 해제
{
    struct NODE *next = cur->next;
    free(cur);
    cur = next;
}

free(head);

노드 삭제 함수

void removeFirst(struct NODE *target) // 기준 노드의 다음 노드를 삭제
{
    struct NODE *removeNode = target->next; // 기준 노드의 다음 노드 주소를 저장
    target->next = removeNode->next; // 기준 노드의 다음 노드에 삭제할 노드의 다음 노드를 지정

    free(removeNode);
}

연결 리스트 함수를 구현할 때는 노드가 NULL인지 검사할 것

void addFirst(struct NODE *target, int data)    // 기준 노드 뒤에 노드를 추가하는 함수
{
    if (target == NULL)     // 기준 노드가 NULL이면
        return;             // 함수를 끝냄

    struct NODE *newNode = malloc(sizeof(struct NODE));    // 새 노드 생성
    if (newNode == NULL)    // 메모리 할당에 실패하면
        return;             // 함수를 끝냄

    newNode->next = target->next;    // 새 노드의 다음 노드에 기준 노드의 다음 노드를 지정
    newNode->data = data;            // 데이터 저장

    target->next = newNode;    // 기준 노드의 다음 노드에 새 노드를 지정
}

void removeFirst(struct NODE *target)    // 기준 노드의 다음 노드를 삭제하는 함수
{
    if (target == NULL)    // 기준 노드가 NULL이면
        return;            // 함수를 끝냄

    struct NODE *removeNode = target->next;    // 기준 노드의 다음 노드 주소를 저장
    if (removeNode == NULL)    // 삭제할 노드가 NULL이면
        return;                // 함수를 끝냄

    target->next = removeNode->next;    // 기준 노드의 다음 노드에 삭제할 노드의 다음 노드를 지정

    free(removeNode);    // 노드 메모리 해제
}

매크로 정의 및 해제

매크로 정의

#define 매크로이름/*-------------------*/

#define ARRAY_SIZE 10
#define DEFAULT_ARRAY_SIZE ARRAY_SIZE // 매크로를 매크로로 정의

char s1[ARRAY_SIZE]; // char s1[10]과 같음

매크로 해제

#undef 매크로이름
/*-----------------*/

#define COUNT 10

printf("%d", COUNT); // 10 출력

#undef COUNT
#define COUNT 20

printf("%d", COUNT); // 20 출력

함수 모양의 매크로 정의

#define 매크로이름(x) 함수(x)
#define 매크로이름(x) 코드조합
/*-------------------------*/

#define PRINT_NUM(x) printf("%d", x)

PRINT_NUM(10); // 10 출력, printf("%d", 10)과 같음

함수를 사용하지 못하게 만들 수도 있음

#define printf // printf를 빈 매크로로 정의

printf("Hi"); // 출력되지 않음

여러 줄을 묶어서 매크로로 정의

#define 매크로이름 코드1 \
                  코드2 \
                  코드3
/*--------------------------*/

#define PRINT_NUM3(x) printf("%d", x); \
                      printf("%d", x + 1); \
                      printf("%d", x + 2);

PRINT_NUM3(10); // 101112가 출력, printf("%d", 10); printf("%d", 10 + 1); printf("%d", 10 + 2)를 실행한 것과 같음

swap 매크로 정의

swap 함수

void (swap(int *a, int *b))
{
    int temp;
    temp = *a;
    *a = *b;
    *b = temp;
}

바꿀 수 있는 변수의 자료형이 int로 고정되어 있음.
매크로로 만들면 모든 자료형을 만족하면서 두 변수의 값을 바꾸는 것이 가능함

#define SWAP(a, b, type) do { \
    type temp; \
    temp = a; \
    a = b; \
    b = temp; \
} while (0)

float num1 = 1.5f;
float num2 = 3.4f;

SWAP(num1, num2, float);
printf("%f %f", num1, num2); // 3.400000 1.500000 출력

매크로와 연산자 우선순위

#define MUL(a, b) a * b
#define ADD(a, b) a + b

printf("%d", MUL(1 + 2, 3 + 4)); // 1 + 2 * 3 + 4로 변환되어 11이 출력
printf("%d", ADD(1, 2) * 3); // 1 + 2 * 3으로 변환되어 7이 출력

괄호로 묶어서 define해주면 해결된다.

#define MUL(a, b) ((a) * (b))
#define ADD(a, b) ((a) + (b))

매크로 연결 사용하기

define에서 ##를 사용하면 여러 코드나 값을 붙일 수 있다.

#define 매크로이름(a, b) a##b
/*------------------------*/

#define CONCAT(a, b) a##b

printf("%d", CONCAT(1, 2)); // 12 출력

함수 호출에 응용할 수 있다.

#define EXECUTER(x) func##x()

void func1()
{
    printf("function 1");
}

void func2()
{
    printf("function 2");
}

EXECUTER(1); // func1 함수 호출

조건부 컴파일 사용

#ifdef와 #endif를 사용
#ifdef에 매크로를 지정하면 해당 매크로가 정의되어 있을 때만 코드를 컴파일한다.

#ifdef 매크로
코드
#endif
/*----------*/

#define DEBUG

#ifdef DEBUG
    printf("%s %s %s %d", __DATE__, __TIME__, __FILE__, __LINE__);
/*
__DATE__ : 컴파일한 날짜
__TIME__ : 컴파일한 시간
__FILE__ : 매크로가 사용된 헤더, 소스 파일
__LINE__ : 매크로가 사용된 줄 번호
*/
#endif

전처리기 과정을 거지면 조건에 맞는 코드만 남긴다.

값 또는 식으로 조건부 컴파일

#if  또는 
코드
#endif
/*-------------------------*/

#define DEBUG_LEVEL 2

#if DEBUG_LEVEL >= 2 // DEBUG_LEVEL이 2보다 크거나 같으면 사이의 코드를 컴파일
    printf("Level 2");
#endif

#if 1 // 조건이 항상 참이라서 사이의 코드를 컴파일
    printf("1");
#endif

#if 0 // 조건이 항상 거짓이라서 사이의 코드를 컴파일하지 않음
    printf("0");
#endif

#if defined에는 논리 연산자를 사용할 수 있다.

#define DEBUG
#define TEST

// DEBUG 또는 TEST가 정의되어 있으면서 VERSION10이 정의되어 있지 않을 때 사이의 코드를 컴파일
#if (defined DEBUG || defined TEST) && !defined(VERSION_10)
    printf("Debug");
#endif

#elif와 #else 사용

#ifdef 매크로
코드
#elif defined 매크로
코드
#else
코드
#endif

#if 조건식
코드
#elif 조건식
코드
#else
코드
#endif
/*-----------------*/

#define USB

#ifdef PS2 // PS2가 정의되어 있을 때 코드를 컴파일
    printf("PS2");
#elif defined USB // PS2가 정의되어 있지 않고, USB가 정의되어 있을 때 코드를 컴파일
    printf("USB");
#else // PS2와 USB가 정의되어 있지 않을 때 코드를 컴파일
    printf("X");
#endif

#ifndef는 매크로가 정의되어 있지 않을 때 코드를 컴파일

#ifndef 매크로
코드
#endif
/*----------------*/

#define NDEBUG

#ifndef DEBUG // DEBUG가 정의되어 있지 않을 때 코드를 컴파일
    printf("X");
#endif

ifndef도 elif와 else와 사용 가능

파일 포함

#include <파일>
#include "파일"

<>와 ""의 차이

  • <> : 보통 C 언어 표준 라이브러리의 헤더 파일을 포함할 때 사용, 컴파일 옵션에서 지정한 헤더 파일 경로를 기준으로 파일을 포함한다.
  • "" : 현재 소스 파일을 기준으로 헤더 파일을 포함하고, 헤더 파일을 찾지 못할 경우 컴파일 옵션에서 지정한 헤더 파일 경로를 따른다.

전역 변수 사용

변수는 선언된 블록 안에서만 사용할 수 있고 이를 변수의 범위라고 한다.
따라서 함수 밖에 선언을 하게 되면 파일 전체가 변수의 범위가 된다.
전역 변수는 모든 함수에서 접근할 수 있고, 한 번 저장한 값이 계속 유지된다.
전역 변수는 초깃값을 지정하지 않으면 0으로 초기화 된다.
지역 변수와 전역 변수의 이름이 같을 때는 현재 블록의 변수를 우선으로 접근한다.

전역변수를 무분별하게 사용하면 발생하는 문제점

  • 프로그램이 커지다 보면 어떤 함수가 전역 변수의 값을 바꾸는지 알기 어려워져, 유지 보수도 힘들고 눈에 잘 띄지 않는 버그가 생기기 쉽다.
  • 지역 변수와 전역 변수의 이름이 겹칠 가능성이 커지고 의도하지 않은 결과가 나올 수 있다.

extern으로 다른 소스 파일의 전역 변수 사용

print.c 파일

#include <stdio.h>

int num1 = 10;

void printNumber()
{
    printf("%d", num1);
}

main.c 파일

#include <stdio.h>

extern int num1; // 다른 소스 파일에 있는 전역 변수 num1을 사용

int main()
{
    printf("%d", num1);

    return 0;
}

extern은 함수에도 사용할 수 있다.

extern 반환값자료형 함수이름(매개변수자료형);

extern void printNumber();

기억 부류 지정자 (storage class specifier)

키워드 저장 장소 범위 초깃값 수명 주기
extern 데이터 섹션(초기화)
BSS 섹션(비초기화)
프로그램 0 프로그램 시작부터 종료까지
auto 스택 블록 초기화되지 않음 블록 시작부터 종료까지
static 데이터 섹션(초기화)
BSS 섹션(비초기화)
블록 또는 파일 0 프로그램 시작부터 종료까지
register CPU 레지스터 블록 초기화되지 않음 블록 시작부터 종료까지

자동 변수 사용

auto 자료형 변수이름;
/*-----------------*/

auto int num1 = 10; // 자동 변수 num1 선언, 현재 블록이 끝나면 사라짐
int num2 = 10; // auto 키워드를 생략한 자동 변수 선언

전역 변수에는 사용할 수 없다.

지역 변수는 자동 변수로 선언하는 것이 보통이기 때문에 자동 변수라는 말은 생략하고 지역 변수라고 말한다.

정적 변수 선언

static 자료형 변수이름;
/*-------------------*/

void increase()
{
    static int num1 = 0;

    printf("%d", num1);

    num1++;
}

increase(); // 0 출력
increase(); // 1 출력
increase(); // 2 출력, 정적 변수가 사라지지 않고 유지되어 값이 계속 증가한다.

정적 변수는 함수를 벗어나더라도 변수가 사라지지 않고 계속 유지된다.
정적 전역 변수는 자신이 선언된 소스파일 안에서만 사용할 수 있고, 외부에서는 가져다 쓸 수 없다.
전역 변수에 static을 붙이면 변수의 범위를 파일 범위로 제한하는 효과가 있다. 정적 변수는 매개변수로 사용할 수 없다.

정적 함수 사용

print.c

#include <stdio.h>

void print()
{
    printf("print.c");
}

main.c

static void print() // static을 붙이지 않으면 에러 발생
{
    printf("main.c");
}

int main()
{
    print(); // main.c 출력

    return 0;
}

한 프로젝트 안에서 함수 이름이 중복될 수 없다.
정적 함수는 해당 파일 안에서만 호출할 수 있다.

레지스터 변수 사용

register를 붙인 변수는 메모리 대신 CPU 레지스터를 사용한다.
일반 변수보다 속도가 빠르다.
레지스터 개수가 한정되어 있어 register를 붙인다고 모두 레지스터를 사용하지 않는다.

register 자료형 변수이름;
/*-----------------------*/

register int num1 = 01;

printf("%p", &num1); // 에러 발생, 메모리에 없어서 메모리 주소를 구할 수 없음

register int *numPtr = malloc(sizeof(int));

// 레지스터 변수에 메모리 주소는 저장할 수 있으므로 역참조 연산자를 사용할 수 있다.
*numPtr = 20;
printf("%d", *numPtr);

free(numPtr);

레지스터 변수는 반복 횟수가 많을 때 유용하다.

main 함수에서 실행 파일 옵션 받기

int main(int argc, char *argv[]);
/*-------------------------------*/

int main(int argc, char *argv[]) // 옵션의 개수와 옵션 문자열을 배열로 받는다.
{
    for (int i = 0; i < argc; i++)
    {
        printf("%s", argv[i]);
    }

    return 0;
}

argv에 저장되는 옵션의 내용

  • 0 : 실행 파일 이름으로 실행했으면 실행 파일, 경로로 실행했으면 실행 파일 경로
  • 1 : 첫 번째 옵션
  • 2 : 두 번째 옵션
  • n : n 번째 옵션

서식 지정자

서식 지정자 설명
d, i 부호 있는 10진 정수
u 부호 없는 10진 정수
o 부호 없는 8진 정수
x, X 부호 없는 16진 정수(소문자, 대문자)
f, F 실수를 소수점으로 표기(소문자, 대문자)
e, E 실수 지수 표기법 사용(소문자, 대문자)
g, G %f(%F)와 %e(%E) 중에서 짧은 것을 사용(소문자, 대문자)
a, A 실수를 16진법으로 표기(소문자, 대문자)
c 문자
s 문자열
p 포인터의 메모리 주소
n int 포인터를 넣으면 지금까지 출력한 문자 개수를 변수에 넣어준다.
예시 : printf("abcde%n", &num1); 지금까지 출력한 문자 개수인 5을 num1에 넣어준다.
% % 기호 출력
플래그 설명
- 왼쪽 정렬
+ 양수일 떄는 + 부호, 음수일 때는 - 부호 출력
공백 양수일 때는 부호를 출력하지 않고 공백으로 표시, 음수일 때는 - 부호 출력
# 진법에 맞게 숫자 앞에 0, 0x, 0X를 붙임
0 출력하는 폭의 남는 공간을 0으로 채움

인라인 함수

inline 반환값자료형 함수이름(매개변수자료형 매개변수이름)
{
}
/*--------------------------------------------------*/

inline int add(int a, int b)
{
    return a + b;
}

int num1;
num1 = add(10, 20);

인라인 함수는 호출을 하지 않고 함수의 코드를 그 자리에서 그대로 실행한다.
컴파일러가 함수를 사용하는 부분에 함수의 코드를 복제해서 넣어준다는 뜻이다.
호출 과정이 없어서 속도가 좀 더 빨라, 자주 호출되어 속도가 중요한 부분에서 사용한다.
단, 함수의 코드가 복제되는 것이라서 함수를 많이 사용하면 실행 파일의 크기가 커진다.

문자열 입출력 함수

gets_s(버퍼, 버퍼크기);
- 성공하면 입력된 문자열을 반환, 실패하면 NULL 반환

puts(문자열)
- 성공하면 음수가 아닌 값을 반환, 실패하면 EOF(-1) 반환
/*----------------------------------------------*/

char buffer[100];
gets_s(buffer, sizeof(buffer)); // 표준 입력에서 문자열을 입력받는다
puts(buffer); // 문자열을 화면에 출력한다.

gets_s가 아니라 gets 함수는 버퍼만 넣어주면 끝이라서 버퍼의 크기를 넘어서는 입력도 받을 수 있어서 버퍼 오버플로우 공격에 취약하다.

입출력 버퍼 활용

setvbuf(파일포인터, 사용자지정입출력버퍼, 모드, 크기);
- 설정 변경에 성공하면 0 반환, 실패하면 0 아닌 값을 반환
/*---------------------------------------------------*/

#include <time.h>

void delay(unsigned int sec) // 특정 시간만큼 기다리는 함수
{
    clock_t ticks1 = clock();
    clock_t ticks2 = ticks1;
    while ((ticks2 / CLOCKS_PER_SEC - ticks1 / CLOCKS_PER_SEC) < (clock_t)sec)
        ticks2 = clock();
}

setvbuf(stdout, NULL, _IOFBF, 10); // 출력 버퍼의 크기를 10으로 설정
printf("Hello, world");
delay(3); // 3초간 기다림

Hello, wor까지 출력되고 3초 기다렸다가 ld가 출력된다.
setvbuf로 stdout의 출력 버퍼를 10으로 설정했기 때문에 버퍼 크기만큼 출력되고 3초 후 버퍼가 비워져서 나머지가 출력되는 것

setvbuf 함수의 첫 번째 인수에는 입출력 버퍼의 설정을 변경할 파일 포인터를 넣어준다. 이때 stdin, stdout, stderr도 파일 포인터이기 때문에 그냥 넣어도 된다.
두 번째 인수에는 입출력 버퍼로 사용할 배열이나 메모리를 넣는데 NULL을 지정하면 내부적으로 버퍼 공간을 생성한다.
세 번째 인수는 버퍼링 모드, 네 번째는 입출력 버퍼 크기이다.

버퍼링 모드

  • _IOFBF : Full buffering, 버퍼가 가득 차면 버퍼를 비운다.
  • _IOLBF : Line buffering, \n을 만나거나 버퍼가 가득 차면 버퍼를 비운다.
  • _IONOBUG : No buffering, 버퍼를 사용하지 않는다. 입출력 버퍼로 사용할 배열(메모리)와 크기는 무시된다.

출력버퍼를 강제로 비우려면 fflush를 사용하면 된다.

fflush(파일포인터);
- 성공하면 0 반환, 실패하면 EOF(-1) 반환
/*----------------------------------*/

setvbuf(stdout, NULL, _IOFBF, 10);
printf("Hello, world");
fflush(stdou); // 표준 출력의 출력 버퍼를 강제로 비움

delay(3);

이번에는 3초후에 모두 출력되는 것이 아니라 끊기지 않고 바로 출력된다.

scanf 함수는 문자열 부분만 골라서 배열에 저장하기 때문에 입력 버퍼에 \n이 남아 있다. 그래서 fgets 함수를 scanf 함수 다음에 사용하면 입력이 그냥 끝나버리는데 이때 입력 버퍼를 비우는 함수를 사용하면 된다.

assert

assert.h에 정의되어 있고, 정해진 조건에 맞지 않을 때 프로그램을 중단한다.
즉, asser에 지정한 조건식이 false일 때 프로그램을 중단, true일 때 프로그램이 계속 실행
NDEBUG 매크로가 정의되어 있으면 asser는 무시된다.

assert(표현식)
/*----------*/

void copy(char *dest, char *src)
{
    assert(dest != NULL); // dest이 NULL이면 프로그램 중단
    assert(src != NULL); // src가 NULL이면 프로그램 중단

    strcpy(dest, src);// 문자열 복사
}