데이터가 담긴 노드(메모리 공간)를 일렬로 연결해 놓은 것
- 리스트의 중간 지점에 노드를 손쉽게 추가하거나 삭제할 수 있다.
- 특정 노드를 찾으려면 노드를 모두 검색해야 한다.
- 크기가 고정되어 있지 않다.
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 함수
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으로 초기화 된다.
지역 변수와 전역 변수의 이름이 같을 때는 현재 블록의 변수를 우선으로 접근한다.
전역변수를 무분별하게 사용하면 발생하는 문제점
- 프로그램이 커지다 보면 어떤 함수가 전역 변수의 값을 바꾸는지 알기 어려워져, 유지 보수도 힘들고 눈에 잘 띄지 않는 버그가 생기기 쉽다.
- 지역 변수와 전역 변수의 이름이 겹칠 가능성이 커지고 의도하지 않은 결과가 나올 수 있다.
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();
키워드 | 저장 장소 | 범위 | 초깃값 | 수명 주기 |
---|---|---|---|---|
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);
레지스터 변수는 반복 횟수가 많을 때 유용하다.
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.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);// 문자열 복사
}