public string DevelopmentDiary(Knowledge knowledge) {
if (knowledge != null) {
return "level up"
} else {
return "level down"
}
}
5주차라니 감회가 새롭다.
한 달이 지났다는 얘기니까.
시간이 순식간에 지나가는 것 같다.
한 달이 지났으니 확실하게 얘기할 수 있을 것 같다.
컴퓨터 언어나 CS를 한 번도 접해보지 않았다면
여정이 많이 힘들었을 것 같다.
다들 벨로그를 재밌게 쓴다니 나도 따라해보려고 한다.
홍대병의 반대가 뭘까? 아무튼 나는 그거다.
C의 시간이 왔다. C는 접해본 적이 있다.
아무 말이나 하는 것을 좋아하기 때문에
아무 언어나 배우는 것을 좋아한다.
다음은 내 개발 흐름이다.
C# 책을 샀다. 자마린으로 어플을 개발하고 유니티로 게임을 개발하려 했다. C# 책을 꾸역꾸역 읽고나서 깨달았다. 자마린은 잘 쓰이지 않는 것을..
플러터 책을 샀다. 친한 친구가 플러터로 어플을 만든다길래 따라 사봤다. 플러터의 다트는, 자바와 C#이랑 비슷하다고 한다. 개발하는데 타입과 Null을 고려하느라 좀 고생했던 기억이 있다. 그리고 setState와 Provider를 남발해서 돌덩이 같은 어플을 만든 기억이 있다.
돌덩이를 만들다보니 운영체제 및 컴퓨터 구조를 공부할 필요성을 느꼈다. 동시에 자료구조와 알고리즘도 알아야겠다는 생각이 들었고, 관련 책들을 샀다.
문득 AI 관련 대학원을 가자는 생각이 들었다. VR게임을 유니티로 개발하고 싶은데 CV를 공부하면 도움이 될 것 같았고 AI 석사 학위도 따고 일석이조라는 생각이 들었다. AI관련 책들을 샀다. 파이썬부터 시작해서 딥러닝, 선형대수, 머신러닝, openCV, 파이토치 등등 근데 결국 모니터 받침대가 됐다. 두꺼워서 받침대로 좋다.
자료구조와 알고리즘 관련 책이 C언어로 알려주는 책이었기 때문에 C언어 관련 책을 샀다. 이 때가 내가 처음 C 언어를 접했던 시기이다. 언리얼을 사용하기 위해 C++도 하려했으니 C가 필수이긴 했다. 당연히 C++도 샀다. (C++은 샀지만 아직 공부는 안했다.)
C 언어를 공부한 이후에 자료구조 알고리즘을 부쉈다. 리액트 네이티브로 어플을 만들려고 했는데, 얘는 또 자바스크립트라고 한다.
Html, Css, 자바스크립트 관련책을 샀다. 또 공부했다. 자바스크립트는, 개인적인 생각이지만 파이썬과 비슷하다는 느낌이 들었다.
리액트 네이티브로 어플을 만들었다. 얘는 처음에는 타입 신경 안쓰고 만들어서 편했지만, 나중에는 결국 그게 오히려 불편하게 만든다는 것을 알았다. 타입 스크립트를 쓰면서 최대한 타입 명시하고 인터페이스를 사용하려고 노력했다.
방통대에 진학하고 새로운 책들을 많이 샀다. 여태 샀던 책들을 모니터 받침대로 두면 거북목 걱정은 없다. 대신 뒷목이 좀 많이 아플 것 같다.
아무튼 취업을 해야하니 자바를 샀다. 스프링 책도 샀다. 그래서 요즘은 자바를 다 떼고 스프링을 공부 중이다.
책은 못참는다. 클린코드도 샀다. 책에 욕심이 많은 것 같다. 또 언제 갖고 싶은 책이 생길지 모른다.
아무튼 이제 C를 회고하는 시간을 가져볼 예정이다. 기초적인 내용들은 사실 C#이나 자바나 다트나 큰 차이가 없다. 그래서 C만의 독특한 문법들을 위주로 정리를 할 생각이다. 그 시절 사람들에게는 참 편했겠지만 AI 시대에 사는 나로서는 참 지독한 언어가 아닐까 싶다. 어제도 파이썬으로는 30초면 푸는 문제를 5분을 공들여 풀었다. C언어로 알고리즘을 푸는 사람을 만나면 조금 멀어지려 한다. 속으로 무슨 생각을 하고 있을지 모른다.
C의 변태적인 속성에 크게 한 몫하는 녀석인 것 같다. 입력과 출력 과정이 단짝을 만나듯 사뭇 감동적이다.
A) printf
print formatted라는 의미이며 형식화된 출력 기능을 제공하는 함수라고 한다.
기본적인 문법은 다음과 같다.
int printf(const char *format, ...);
format은 형식 지정자 및 텍스트를 포함하는 문자열이고
...은 가변 인자로 형식 지정자에 따라 출력할 값들, 즉 변수가 들어가는 자리이다.
형식 지정자는 다음과 같다.
| 형식 지정자 | 설명 | 데이터 타입 |
|---|---|---|
%d | 부호 있는 10진수 정수 출력 | int |
%i | 부호 있는 10진수 정수 출력 | int |
%u | 부호 없는 10진수 정수 출력 | unsigned int |
%f | 실수 출력 (소수점 이하) | float, double |
%lf | double형 실수 출력 | double |
%c | 단일 문자 출력 | char |
%s | 문자열 출력 | char* |
%x | 부호 없는 16진수 출력 (소문자) | unsigned int |
%X | 부호 없는 16진수 출력 (대문자) | unsigned int |
%o | 부호 없는 8진수 출력 | unsigned int |
%p | 포인터(주소) 출력 | 포인터 타입 |
%% | % 기호 자체 출력 | - |
내가 고생한 건 아니고 AI가 좀 고생했다.

나를 우롱하는 놈들도 있다.

GPT는 꽤나 유쾌한 녀석이다.
사용 예시는 다음과 같다.
#include <stdio.h>
int main() {
int age = 25;
float height = 175.5;
char name[] = "Alice";
printf("이름: %s, 나이: %d, 키: %.1fcm\n", name, age, height);
return 0;
}
각 형식지정자의 순서와 변수의 순서를 만나게 해주면 된다. 꽤나 감동적이지 않은가.
즉 %s와 name변수가 만나고, %d와 age, %.1f와 height가 각각 만난다.
타입에 따라서 변수를 찾아간다고 하기도 하는데, 유지보수나 가독성 측면에서 좋지 않으니 그냥 순서대로 쓰도록 하자.
이스케이프 시퀀스라고 하는 것도 있는데, 이는 문장의 띄어쓰기나 줄 바꿈과 관련이 있다.
| 이스케이프 시퀀스 | 의미 | 출력 예 |
|---|---|---|
\n | 줄 바꿈 (newline) | 줄을 바꿔서 출력함 |
\t | 수평 탭 (tab) | 탭 간격으로 띄움 |
\\\ | 역슬래시 (backslash) 출력 | \ 출력 |
\\" | 큰따옴표 (") 출력 | " 출력 |
\\' | 작은따옴표 (') 출력 | ' 출력 |
\a | 경고음 (alert, bell) | 삑 소리 (일부 환경에서만) |
\b | 백스페이스 (backspace) | 커서 한 칸 뒤로 이동 |
\r | 캐리지 리턴 (carriage return) | 커서를 줄 맨 앞으로 이동 |
\f | 폼 피드 (form feed) | 페이지 넘김 (프린터용 등) |
\v | 수직 탭 (vertical tab) | 수직 간격 (거의 사용 안 함) |
\? | 물음표 출력 | ? 출력 (특수 경우 대비용) |
B) scanf
printf와 비슷하다. 얘도 결국 짝을 찾아가는 친구인데, 다른 점이 하나있다면
scanf("%d%d%f%f", &i, &j, &x, &y);
변수명에 &를 다는 것이다.
&란 무엇인가? 읽을 때는 앰퍼샌드라고 한다. 해당 변수의 주소라고 생각하면 된다. 자세한 것 추후 서술하도록 하겠다.
기본형은 모든 언어에 존재한다. 각 데이터 별로 크기가 다르다는 것을 아는 것은 중요하다. 여기서는 형변환 및 형정의를 다루고 가려고 한다. 형변환은 사실 C만의 특징이 아니긴 하다.
A) 형변환
형(type)에 따라 아우가 달라진다. 즉, 일반적으로 작은 바이트를 가진 type이 변해야 한다. 그래야 데이터를 잃지 않기 때문이다. 형과 아우의 크기는 다음과 같다.
| 데이터 타입 | 크기 (바이트) | 설명 |
|---|---|---|
char | 1 | 문자 1개를 저장 (ASCII 기반) |
unsigned char | 1 | 부호 없는 문자형 (0~255) |
signed char | 1 | 부호 있는 문자형 (-128~127) |
short | 2 | 작은 범위의 정수 저장 |
unsigned short | 2 | 부호 없는 짧은 정수 (0부터 시작) |
int | 4 | 기본 정수형 (보통 4바이트) |
unsigned int | 4 | 부호 없는 기본 정수형 |
long | 4 또는 8 | 더 큰 범위의 정수 (시스템에 따라 다름) |
unsigned long | 4 또는 8 | 부호 없는 long 정수 |
long long | 8 | 아주 큰 정수 범위 저장 |
unsigned long long | 8 | 부호 없는 아주 큰 정수 |
float | 4 | 단정도 실수형 (소수점 숫자, 약 7자리 정밀도) |
double | 8 | 배정도 실수형 (더 높은 정밀도) |
long double | 10~16 | 컴파일러/플랫폼에 따라 더 정밀한 실수형 |
_Bool | 1 | 논리형 (C99 이후 도입, true/false 저장) |
즉, int와 longlong은 바이트가 다르기 때문에 바이트 수를 동일하게 해서 계산을 해야한다. 1달러와 1000원을 더해서 1001원이 될 수는 없으니 변경을 해줘야 한다.
다른 언어를 먼저 배웠기 때문에 그 언어들을 기준으로 형변환을 생각했으나 C의 형변환은 보다 널널하다.
무슨 말이냐면, 자바나 C# 같은 언어는 암시적 형변환 및 명시적 형변환의 규칙이 정해져있다. 명시적 형변환은 데이터의 형을 변환한다는 그 목적을 분명히 드러내기 때문에 데이터 타입의 크기 관계에 상관없이 항상 사용할 수 있다. 암시적 형변환은 크기가 작은 데이터 타입에서 큰 데이터 타입으로 변할 때만 사용가능하다. 또한 부호 사용 유무에 따라서도 표현할 수 있는 값이 다르니 형변환이 필요하다.
예를 들어 4비트 데이터 a = 0010 이라는 값과 b = 01000010이 있다고 생각해보자.
a의 타입을 (4bit)이라고 하고 b의 타입을 (8bit)이라고 할 것이다.
a) 명시적 형변환
데이터를 변환하겠다고 표시하고 데이터를 변환하는 것을 말한다.
a와 b를 계산하기 위해 a를 00000010로 만드는 과정에서
(8bit)a라고 하는 것이다.
혹은 b를 4비트 타입로 만들기 위해 (4bit)b라고 하는 것이다.
이때 b의 상위 4비트는 오버플로우 처리되며 비트가 잘린다.
즉, 01000010이 0010이 돼버린다.
따라서 이런 데이터의 손실을 엄격하게 생각하는 언어에서는 명시적으로 처리한다고 보면된다.
b) 암시적 형변환
데이터를 변환하겠다고 표시하지 않고 데이터를 변환하는 것을 말한다.
그냥 아무 것도 쓰지 않고 a + b를 한다. 그럼 큰 값에 맞춰서 a의 데이터 형이 변한다.
혹은 (4bit)의 a에 (8bit) b를 값을 할당하면(a = b) 4bit가 잘리게 된다.
B) 형정의
typedef int Bool;
형을 정의해주는 것으로, 정의하려는 형의 이름은 마지막에 위치한다. 두 가지 표기법이 있는데, 하나는 앞의 예시에 나와있듯 대문자로 시작하는 것이고 두 번째는 스네이크 표기법(bool_t)을 통해 뒤에 형정의 했음을 명시하는 것이다.
형정의는 프로그램의 가독성 및 유지보수성을 높여준다. 예를 들어
typedef int age_t;
age_t young, old;
와 같은 구문은 가독성을 높여주고 int를 double로 바꿀 경우, 나이와 관련된 변수들을 따로 double 선언할 필요없이 typedef 부분의 int만 double로 바꾸면 된다.
typedef double age_t;
age_t young, old;
이런 식으로 말이다.
또한 struct를 사용할 때,
struct _linked_list_node {
int data;
_linked_list_node* next_node;
}
와 같은 구문은 사용할 때 다음처럼 사용해야 한다.
struct _linked_list_node new_node;
하지만 typedef을 사용한다면
typedef struct _linked_list_node {
int data;
_linked_list_node* next_node;
} Linked_list_node
Linked_list_node new_node;
와 같이 선언할 수 있다. 즉, struct를 쓰지 않아도 된다.
형이 누군지 정의를 하면 동생은 따라가기 때문에 아주 편하다.
동생들이 자기 형 누군지 아냐고 묻는 이유가 다 있다.
A) 1차원 배열
물론 다른 언어에도 배열은 다 있지만, C 언어의 배열을 다양한 방식으로 사용한다는 것은 주소와 데이터 및 포인터의 개념을 어느 정도 이해한 것이 아닐까 생각한다.
우선 배열은 다음과 같이 타입을 적고 선언한다.
int a[3] = {1, 2, 7};
여기서 a는 포인터이다. 포인터의 개념을 추후 서술하겠지만, 간단하게 포인터는 어떤 주소를 가리키는 친구라고 생각하면 된다. 모든 데이터들은 각각의 고유한 주소를 가지고 있다. 주소가 없으면 길을 잃어버리기 때문이다.
여기서 a의 값을 프린트하면 재밌는 값이 나온다.
printf("%d", a)
OUTPUT
1800006728
근데 여기서 a에 *(asterisk) 혹은 (star)를 붙이면
printf("%d", *a)
OUTPUT
1
위와 같은 결과가 나온다. 그 이유는 a는 배열의 시작 주소, 즉 a[0]의 값을 갖고 있기 때문이다.
배열은 표와 같다.
| 인덱스 | 0 | 1 | 2 |
|---|---|---|---|
| 주소 | 1800006728 | 1800006732 | 1800006736 |
| 값 | 1 | 2 | 7 |
따라서 &a[1]의 프린트 값은 1800006732가 될 것이다.
그리고 다음의 각 연산에 대한 결과 값은 어떻게 될까?
c[1]+1
*(&c[1])+1
*(&c[1]+1)
다음과 같다.
3
3
7
왜 이런 일들이 발생할까? 바로 각 배열의 4바이트이며 타입이 int이기 때문이다. 각 주소별로 4씩 차이나는 것, 주소에 1을 더할 때 주소값이 4가 커진 것 역시 배열의 타입을 고려했기 때문이다. 예를 들어 데이터의 크기가 16바이트라면 주소값이 16바이트씩 차이날 것이다.
이를 이해하면 다차원 배열에 접근하기가 쉬워진다.
B) 다차원 배열
앞서 a[10]에서 a는 포인터의 역할을 했다. 그리고 배열에 들어가는 크기에 따라 한 칸의 크기가 달라졌다.
2차원 배열을 예시로 들어 설명하겠다.
int a[2][3]은 int형 데이터가 3개씩 2묶음이 있는 것이다.
차수가 높아져도 다름없다.
int a[2][3][4]은 int형 데이터가 4개씩 3묶음씩 2묶음 있는 것이다.
따라서 int b[3];를 타입으로 생각할 때 int a[2][3];는 b a[2]; 와 같이 생각해도 좋다.
즉 b는 4바이트 int타입의 배열이 3개 있고, a는 12바이트 b타입의 배열이 2개 있다.
이해하기 쉽게 그림으로 설명하면 b는 다음과 같고 a는 이를 두 개 가지고 있는 것이다.
b
| 인덱스 | 0 | 1 | 2 |
|---|---|---|---|
| 값 |
a
| a | --- 0 --- | --- 1 --- |
|---|
| b | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| 값 |
따라서 a는 a[0]이자 a[0][0]을 가리킨다.
그리고 a는 12바이트 타입의 b를 2개 가진 것이라고 보면 된다.
A) 포인터
C를 가치 있게 만드는 놈이다.
유명한 짤이 있다.

출처: https://defold.com/2023/09/07/Defold-x-Luden-io/
공통점은 가리키고 있다는 것이다. 기본형 변수들은 이런 느낌으로 저장된다.
ex) int a = 8
| 변수명 | 주소 | 값 |
|---|---|---|
| a | 0x1F2C4F01 | 8 |
그럼 포인터 변수들은 어떤 느낌일까?
ex) int* b = &a (포인터 변수b에 a의 주소를 저장한다는 의미이다.
| 변수명 | 주소 | 값 |
|---|---|---|
| b | 0x1D2FFC09 | 0x1F2C4F01 |
따라서 포인터 b 가르키는 값을 따라가면 a의 값이 나온다.
포인터의 개념이 중요한 것이 다른 언어에서 배우는 참조형을 이해하는 데 중요한 역할을 하기 때문이다.
특히 배열에서 깊은 복사와 얕은 복사를 이해하는 데 있어서도 참조형은 중요하다.
간단하게 설명하고 넘어가자면 클래스(붕어빵틀)와 인스턴스(붕어빵)이 있다.
내* 붕어빵A은 친구* 붕어빵B과 다르다.
따라서 내* 붕어빵A를 먹는다고 친구* 붕어빵B가 바뀌진 않는다.
근데 친구놈이 내* 붕어빵A를 자기 붕어빵이라고 가리키면 얘기가 달라진다.
그럼 친구의 붕어빵은 붕어빵A가 돼버리고
내가 붕어빵A를 먹어버리면 친구가 가리키는 붕어빵A 역시 줄어든다.
위 설명은 얕은 복사와도 관련이 있다.
이를 C언어로 풀어서 설명하면
int a = 8이고
int* b = &a, int* c = &a 라고 할 때
*b == 8이고 *c == 8이다.
이때 a = 5를 해버리면
*b == 5, *c == 5가 돼버린다.
B) 포인터 할당
int* a;
int* b;
위와 같이 선언했을 때
a = b
와
*a = *b
의 차이는 무엇일까?
a와 b에 담긴 값은 주소이다.
따라서 a = b는 b의 값(주소)를 a의 값(주소)에 대입하겠다는 의미이다.
*a와 *b는 주소가 가리키는 값이다.
따라서
*a = *b는 b가 가리키고 있는 곳의 값을 a가 가리키고 있는 곳의 값에 대입하겠다는 의미이다.
표로 이해하면 더 수월할 것이다.
int c = 5;
int d = 7;
int* a = &c;
int* b = &d;
주소를 보기 쉽게 바꿔놓겠다.
| 변수명 | 주소 | 값 |
|---|---|---|
| *a | 0x00000000 | 0x88888888 |
| *b | 0xFFFFFFFF | 0xCCCCCCCC |
| c | 0x88888888 | 5 |
| d | 0xCCCCCCCC | 7 |
각 상황에 따라 어떻게 변하는지 보겠다. (따옴표를 통해 무엇이 변했는지 확인하자)
ex1) a = b
| 변수명 | 주소 | 값 |
|---|---|---|
| *a | 0x00000000 | '0xCCCCCCCC' |
| *b | 0xFFFFFFFF | 0xCCCCCCCC |
| c | 0x88888888 | 5 |
| d | 0xCCCCCCCC | 7 |
ex2) *a = *b
| 변수명 | 주소 | 값 |
|---|---|---|
| *a | 0x00000000 | 0x88888888 |
| *b | 0xFFFFFFFF | 0xCCCCCCCC |
| c | 0x88888888 | '7' |
| d | 0xCCCCCCCC | 7 |
C) 함수와 포인터
함수는 스택 공간에 할당된다. 즉, 매개변수로 가져온 값은 아무리 바꿔도 실제 데이터는 할당된 스택 바깥 공간에 있기 때문에 영향을 주지 않는다. 하지만, 영향을 주는 방법이 있다. 바로 주소로 직접 접근하는 방법이다.
따라서, 매개변수로 포인터를 준다면 스택 바깥 공간의 원래 데이터에 접근할 수 있게 되고, 우리는 그 값을 수정할 수 있다.
이를 C언어로 표현하면 다음과 같다.
a) 매개변수에 기본형을 사용하는 경우
int a = 10;
void change_num(int input_num) {
input_num = 15;
}
change_num(a);
printf("%d", a);
------------------
OUTPUT:
10
b) 매개변수에 포인터를 사용하는 경우
int a = 10;
int* b = &a;
void change_num(int* input_pointer) {
*input_pointer = 15;
}
change_num(b);
printf("%d", a);
------------------
OUTPUT:
15
char*이나 char[]을 통해 문장을 저장할 수 있다.
char*은 문자열 리터럴이라고 부르고
char[]은 문자 배열이라고 부른다.
문자열 리터럴은 상수로 취급하며 큰 따옴표로 감싸진 텍스트이고, 읽기 전용 메모리에 저장되기 때문에 수정되지 않는다.
문자 배열은 메모리 공간에 문자를 연속적으로 저장하는 데이터 구조이며 수정 가능하고 스택, 힙에 저장된다.
두 가지 방법 모두 NULL문자에 의해 종료된다.
N 길이의 문자열 변수(문자 배열)은 널문자(\0)가 필요하기 때문에 N+1 길이를 할당하여 데이터를 저장한다.
개인적으로 C를 공부할 때 가장 클래스에 가깝다고 느낀 것이 구조체였다. 다양한 타입을 하나에 넣을 수 있는 것에서 그걸 느꼈던 것 같다. 물론 구조체 내부에 함수를 선언할 수는 없지만, 포인터를 통해 함수를 넣을 수도 있고, 구조체 포인터를 통해서 변수들을 만들면 그게 일종의 객체가 아닐까 생각했다.

유쾌한 친구의 답변이다. 맞긴하다.
아무튼 C를 C답게 만드는 3가지를 뽑으라면 포인터와 구조체를 뽑을 것이다. 한 가지는 뒤에 나온다.
구조체가 어떻게 C를 구조하는지 들여다보자
구조체는 연속된 변수의 박스이다.
좌표라는 구조체를 만들기 위해 다음과 같이 정의할 수 있다.
1.
struct {
int x;
int y;
} point;
2.
struct Point {
int x;
int y;
};
첫 번째 방식은 익명 구조체 방식으로 바로 변수를 선언하는 방식이고
두 번째 방식은 구조체 태그를 선언하는 방식이다.
다른 언어도 그렇지만 보통 익명으로 선언한 것들은 한 번 사용하고 더 이상 사용하지 않을 때 쓴다.
즉, point라는 변수만 만들고 싶을 때 위와 첫 번째 방식으로 사용하고
point 변수를 여러개 생성하고 싶을 때는 두 번째 방식으로 선언한 뒤
struct Point point1;
struct Point point2;
처럼 사용한다.
사람 구조체를 다음과 같이 만들 수 있을 것이다.
struct Human {
char* name;
int age;
double height;
}
struct Human younghi;
struct Human cheolsu;
그렇다면 위와 같은 구조체는 메모리를 어떻게 사용할까? 다음처럼 한 덩어리가 된다.
younghi
| char* | int | double |
|---|---|---|
| name | age | double |
그리고 각 타입별 크기를 더한 것 만큼 메모리를 차지한다.
포인터 변수는 레지스터의 워드에 따라 크기가 달라진다. 64bit 기준으로 8바이트이다.
int 4바이트, double 8바이트
따라서 다음 그림처럼 Human struct 하나에 20바이트의 메모리를 차지할 것이다.
| char* | - | - | - | - | - | - | - | int | - | - | - | double | - | - | - | - | - | - | - |
|---|
구조체는 접근연산자 . 을 통해서 접근할 수 있다.
만약 구조체를 포인터로 선언했다면 ->로 접근한다.
어렵게 사는 방법은 다양하다.
다시 말해
struct Human younghi;
younghi.name = younghi;
younghi.age = 21;
younghi.height = 160;
struct Human cheolsu;
struct Human* cheolsu_ptr = &cheolsu;
cheolsu_ptr -> name = cheolsu;
cheolsu_ptr -> age = 20;
cheolsu_ptr -> height = 175;
gpt 말로는 화살표가 한 번에 역참조와 점 연산을 하는 축약 문법이라고 한다.
그리고 구조체를 선언할 때 앞서 설명했던 형정의를 사용하면 더욱 편하게 사용할 수 있다.
typedef struct _human {
char* name;
int age;
double height;
} Human;
Human younghi;
컴파일 시간에 크기와 위치가 고정되는 변수(전역 변수, 정적 변수)가 있으며 이들은 메모리 공간에 할당된다.
이를 정적 메모리 할당이라고 한다.
그러나 배열을 추가하거나 새로운 데이터 처리를 위해 메모리에 새로운 공간이 필요할 때도 있다.
이를 해결하기 위해 동적 메모리 할당을 한다.
현재 더블 포인터에 대한 내용을 정리 중인데, 덕분에 동적 할당에 대해 조금 더 이해하게 되었다.
메모리 동적 할당은 메모리 내의 힙에다가 값을 저장할 수 있는 메모리 공간을 할당받는 것이다.
C에서는 사용자가 할당하기 때문에 직접 free를 통해 메모리를 해제해야 하지만
다른 언어에서는 가비지 컬렉터를 통해 메모리를 정리한다.
A) malloc
말록이라고 읽고 싶게 생겼지만, 엠얼록이라고 읽는게 더 정확한 표현 같다.
근데 아무리 생각해도 엠얼록이라고 읽히고 싶었으면 mAlloc으로 표현하는게 맞다고 생각한다.
그러니 말록으로 읽을 것이다. 모 게임에 등장하기도 해서 친근한 이름이다.
아무튼 그 의미를 명확하게 하자면 엠얼록이 맞다.
m - 메모리에
allocate - 할당하는 것이다.
말 그대로 추가적인 공간이 필요할 때 요청하는 용도이다.
B) calloc
칼록이라고 읽고 싶게 생겼지만, 아마 씨얼록이 아닐까 싶다.
읽는 건 중요하지 않기 때문에 찾아보지는 않았다.
칼록칼록하면서 기침하면 다 도망가기 때문에 메모리를 0으로 초기화시켜준다.
c가 contiguous의 약자라고 하는데 작명하다가 다소 힘들었지 않나 싶다.

C) realloc
이건 어떻게 읽어도 리얼록이기 때문에 트집잡을 수 없다.
즉, 얘는 더 이상 트집잡지 못하게 만들어준다.
이미 할당한 공간이 부족할 때 공간을 추가로 확보해주거나
너무 많은 공간을 할당했을 때 공간을 반납한다.
트집 잡고 싶은 사람이 생기면 리얼록 시켜주자.
D) free
말 그대로 자유롭게 해주는 것이다.
물건을 들고 있으면 불편하다.
물건을 놓으면 편하다.
값을 놓으면 편해진다.
사실 메모리 동적할당부터 CS에 조금 더 가깝기 때문에 어떻게 할지 고민했지만 그냥 쓰려고 한다.

비전공이라 잘 모르기 때문에 두더지처럼 파고들어야 한다. like 더지
컴퓨터 시스템 책을 보며 내가 이해한 가상화는 약간 이런 느낌이었다.
'구성 요소를 뜯어보면 조금씩 차이는 있지만 하나로 퉁쳐서 관리한다.'
예를 들어 가상메모리는 결국 뜯어보면 메모리도 있고 하드도 있다. 근데 둘을 하나로 취급하여 가상메모리라는 것 위에서 관리한다.
정확한 설명을 보면
가상화(Virtualization)는 물리적 하드웨어를 소프트웨어로 추상화(물리적 자원을 추상화하여 논리적 자원으로 변환)하여 가상의 컴퓨팅 환경을 생성하는 기술입니다. 이를 통해 하나의 물리적 시스템에서 여러 개의 독립적인 가상 시스템(가상 머신, Virtual Machine)을 실행할 수 있습니다. 가상화는 IT 인프라의 효율성을 극대화하고, 리소스 활용도를 높이며, 비용 절감을 가능하게 합니다.
큰 개념은 물리적인 하드웨어를 소프트웨어로 관리하는 것 같다.
또한 '하나의 물리적 시스템'이라고 하는데 '시스템'이라는 단어에 집중해야할 것 같다. 아마 단 하나의 하드웨어를 말하는 것이 아니라 같은 목적을 위해 서로 상호작용하는 하드웨어를 포괄적으로 표현하는 말이 아닐까 싶다. (AI가 맞다고 했다.)
a) 하이퍼바이저(가상 머신 모니터, VMM)
가상화 기술의 핵심 소프트웨어 계층. 물리적 하드웨어 위에서 여러 개의 가상 머신을 생성하고 관리함. 가상 머신과 하드웨어 사이에 있는 중간 관리자로 물리적인 자원을 가상 머신에게 적절히 분배하여 여러 가상 머신이 서로 간섭하지 않도록 한다.
(1) 하이퍼바이저의 주요 기능
(2) 하이퍼바이저의 유형
베어메탈 하이퍼바이저
물리적 하드웨어 위에서 직접 실행되는 하이퍼바이저. 운영체제를 대체하는 형태. Middle 거침
사용 장소: 데이터센터 및 클라우드 센터
특징: 높은 성능과 안정성, VM간 격리가 강력함
장점: 직접 하드웨어에 접근하여 리소스 효율성 높음. 대규모 환경에서 적합. 오버헤드 적음
단점: 초기 설정 및 관리가 복잡함
| VM | VM | VM |
|---|---|---|
| - | HV | - |
| - | HW | - |
호스트형 하이퍼바이저
기존 운영체제 위에서 애플리케이션 형태로 실행되는 하이퍼바이저
특징: 개인 사용자나 테스트 환경에 적합. 설치와 사용이 간단하나 성능이 낮음
장점: 설치 및 관리가 쉬움, 개발 및 테스트 환경에 적합.
단점: 호스트 운영 체제
| VM | VM | VM |
|---|---|---|
| - | HV | - |
| - | Host OS | - |
| - | HW | - |
b) 가상 머신
가상 머신(Virtual Machine, VM)은 물리적 컴퓨터 시스템을 소프트웨어로 에뮬레이션한 환경입니다. VM은 독립적인 컴퓨터처럼 동작하며, CPU, 메모리, 스토리지, 네트워크 인터페이스와 같은 가상의 하드웨어를 통해 운영 체제와 애플리케이션을 실행할 수 있습니다. 이를 통해 하나의 물리적 시스템에서 여러 개의 운영 체제나 애플리케이션 환경을 동시에 실행할 수 있습니다.
라고 한다.
(1) 가상 머신의 구성 요소
(2) 가상 머신의 활용 사례
컨테이너
애플리케이션과 그 실행에 ㅍ필요한 모든 의존성을 하나의 패키지로 묶은 가벼운 실행 환경
| C1 | C2 | C3 |
|---|---|---|
| - | Host OS | - |
| - | HW | - |
C1 ~ C3 각각은 파일(애플리케이션 + 라이브러리 + 설정)임.
호스트의 OS와 커널를 빌려서 사용하기 때문에 운영체제가 다른 컨테이너는 실행이 불가능 하다고 한다.
실리콘(arm 아키텍처)에서 x86-64(인텔 아키텍처)가 동작하는 이유는 Rosetta 2가 x86-64로 컴파일된 프로그램을 arm에서 실행할 수 있게 해 준다.
따라서 OS가 완전히 다른 운영체제를 사용하기 위해서는 가상머신을 사용해야 함.
B) 가상화 방법
GNU 프로젝트에서 개발한 오프소스 컴파일러 모음. 다양한 프로그래밍 언어와 하드웨어 아키텍처를 지원함.
A) GCC의 컴파일 과정
B) GCC의 장점
C) GCC의 단점
root 노드는 빈 노드이며 글자 하나당 하나의 노드로 표현하며 루트 노드에서 리프 노드까지가 하나의 문자열을 구성함.
( ) root
|
a
|
l
/ \
g o
| |
o n
| |
r g
|
i
|
t
|
h
|
m
|
s
욕심내다가 길어졌다.
딱봐도 구현이 쉽게 생겼다.
문자열 찾는 과정도 어렵지 않을 것 같고
삭제 역시 노드끝에서 타고 가다가 다른 자식 있는 노드 만나면 중지하면 될 것 같다.

맞는지 확인하려 했는데 좀 이상하다
문자열을 찾는 알고리즘 중 하나인 KMP 알고리즘이다.
KMP 알고리즘은 발상과 구현 모두 어렵지 않은 편이다.
기본적인 개념은 다음과 같다.
문자열에서 접두부와 반복되는 것을 찾고 그만큼 건너뛰는 알고리즘이다.
그림으로도 쉽게 나타낼 수 있다.
| a | b | c | a | b | b | c | a | b | c | a | b |
|---|
이게 주어진 텍스트이고
| a | b | c | a | b |
|---|
이게 문자열이면
다음과 같이 진행한다.
a)
| a | b | c | a | b | c | a | a | b | c | a | b |
|---|---|---|---|---|---|---|---|---|---|---|---|
| a | b | c | a | b |
아무렇게나 막 쓰다보니 처음부터 맞는 걸 넣어버렸다.
그 다음은 이렇게 이동한다.
b)
| a | b | c | a | b | c | a | a | b | c | a | b |
|---|---|---|---|---|---|---|---|---|---|---|---|
| a | b | c | a | b |
c)
| a | b | c | a | b | c | a | a | b | c | a | b |
|---|---|---|---|---|---|---|---|---|---|---|---|
| a | b | c | a | b |
d)
| a | b | c | a | b | c | a | a | b | c | a | b |
|---|---|---|---|---|---|---|---|---|---|---|---|
| a | b | c | a | b |
그렇다면 어떻게 이동할까?
문자열에 이동할 인덱스를 정하면 된다.
먼저 가장 앞 문자에 -1을 저장한다.
-1은 문자열의 앞 인덱스를 의미한다.
| a | b | c | a | b |
|---|---|---|---|---|
| -1 |
그 다음부터는 앞 문자와 연속되었는지를 확인한다.
만약 연속인 경우 1씩 증가시킨다.
아래는 다르기 때문에 -1이다.
| a | b | c | a | b | |
|---|---|---|---|---|---|
| -1 | -1 | ||||
| a | b | c | a | b |
c도 -1이다.
| a | b | c | a | b | ||
|---|---|---|---|---|---|---|
| -1 | -1 | -1 | ||||
| a | b | c | a | b |
a, b는 각각 0, 1이다.
| a | b | c | a | b | ||
|---|---|---|---|---|---|---|
| -1 | -1 | -1 | 0 | 1 | ||
| a | b | c | a |
얘가 완성본이다.
| a | b | c | a | b |
|---|---|---|---|---|
| -1 | -1 | -1 | 0 | 1 |
이 kmp 테이블의 숫자들은 그럼 무엇일까?
첫째, 문자열의 첫 문자부터 연속되는 문자열이 얼마나 나오는지를 알려주며
(문자열[4]의 값 '1'은 문자열['1']까지 연속된다는 것을 의미함)
둘째, 동시에 해당 문자열의 인덱스를 나타낸다.
(뒤에서 등장한 a, b는 문자열 0번, 1번 인덱스의 연속된 문자와 일치함)
생각보다 설명을 잘 못하는 것 같다. 이해를 위해 예시를 좀 더 들어보자.
| a | b | a | b | c | a | b | a |
|---|
문자열을 A라고 하겠다.
A[0], A[1]이 A[2], A[3]에서 반복되는 것을 확인할 수 있고
A[0], A[1], A[2]가 A[5], A[6], A[7]에서 반복되는 것을 확인할 수 있다.
이해하기 쉽게 그림으로 나타냈다.

그럼 이제 테이블을 만들어보자.
일치하는 인덱스까지 확인해야하는데, 첫 값에서 다르면 일치하는 게 없기 때문에 한 칸 뒤로 이동이다.
a)
| a | b | a | b | c | a | b | a |
|---|---|---|---|---|---|---|---|
| -1 |
b)
| a | b | a | b | c | a | b | a |
|---|---|---|---|---|---|---|---|
| -1 | -1 | ||||||
| a | b | a | b | c | a | b |
c)
| a | b | a | b | c | a | b | a |
|---|---|---|---|---|---|---|---|
| -1 | -1 | 0 | 1 | ||||
| a | b | a | b | c | a |
d)
| a | b | a | b | c | a | b | a |
|---|---|---|---|---|---|---|---|
| -1 | -1 | 0 | 1 | -1 | |||
| a | b | a | b |
e)
| a | b | a | b | c | a | b | a |
|---|---|---|---|---|---|---|---|
| -1 | -1 | 0 | 1 | -1 | 0 | 1 | 2 |
| a | b | a |
f)
| a | b | a | b | c | a | b | a |
|---|---|---|---|---|---|---|---|
| -1 | -1 | 0 | 1 | -1 | 0 | 1 | 2 |

사진과 비교해서 보길 바란다.
왜 인덱스를 나타내느냐?
1. 문자열 비교를 통해 마지막으로 일치하는 문자를 찾는다.
2. 해당 문자열의 kmp테이블 값을 확인한다.
3. 현재 문자열을 점프해서 문자열의 인덱스와 kmp테이블 값을 일치시킨다.
| a | b | c | a | 'b' | c | a | a | b | c | a | b |
|---|---|---|---|---|---|---|---|---|---|---|---|
| a | b | c | a | 'b' | |||||||
| -1 | -1 | -1 | 0 | '1' | |||||||
| 0 | 1 | 2 | 3 | 4 |
이 상황에서 마지막에 일치한 문자는 b이다. abcab의 마지막 b는 1의 값을 가지므로
문자열에서 1번 인덱스에 해당하는 b를 저기까지 점프시키는 것이다.
| a | b | c | a | b | c | a | a | b | c | a | b |
|---|---|---|---|---|---|---|---|---|---|---|---|
| a | b | c | a | b | |||||||
| -1 | -1 | -1 | 0 | '1' | |||||||
| -1 | 0 | '1' | 2 | 3 | 4 |
예시 두 개를 더 보여주면 확실하게 이해가 될 것이다.
예시 1)
a까지 같기 때문에 (b에서 다르기 때문에) a에 해당하는 인덱스만큼 점프한다.
점프 전)
| a | b | c | a | b | c | 'a' | a | b | c | a | b |
|---|---|---|---|---|---|---|---|---|---|---|---|
| a | b | c | 'a' | b | |||||||
| -1 | -1 | -1 | '0' | 1 | |||||||
| -1 | 0 | 1 | 2 | 3 | 4 |
점프 후)
| a | b | c | a | b | c | a | a | b | c | a | b |
|---|---|---|---|---|---|---|---|---|---|---|---|
| a | b | c | a | b | |||||||
| -1 | -1 | -1 | '0' | 1 | |||||||
| -1 | '0' | 1 | 2 | 3 | 4 |
예시 2)
b에서 다르기 때문에 a에 해당하는 인덱스만큼 점프한다.
점프 전)
| a | b | c | a | b | c | 'a' | a | b | c | a | b |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 'a' | b | c | a | b | |||||||
| '-1' | -1 | -1 | 0 | 1 | |||||||
| -1 | 0 | 1 | 2 | 3 | 4 |
점프 후)
| a | b | c | a | b | c | a | a | b | c | a | b |
|---|---|---|---|---|---|---|---|---|---|---|---|
| a | b | c | a | b | |||||||
| '-1' | -1 | -1 | 0 | 1 | |||||||
| '-1' | 0 | 1 | 2 | 3 | 4 |
구현 화이팅
열받는 놈이다.
최근에 얘 때문에 고생했다.
하지만 그만큼 성능은 확실하다.
보이어 무어는 두 가지 방법으로 이동한다.
1. Bad Character (나쁜 문자 이동)
2. Good Sufix (좋은 접미부 이동)
좋은 접미부 이동에 대해서는
고찰을 해둔 자료가 있기 때문에 이를 참고하길 바란다.
https://velog.io/@mogiyoon/보이어-무어-알고리즘-good-sufix-테이블-이해하기
참고 인내하면서 견디라는 얘기다.
따라서 여기서는 나쁜 문자 이동에 대해서만 다룰 것이다.
좋은 접미부는 그 개념부터 범상치않지만
나쁜 문자는 개념이 아주 간단하다.
문자열과 텍스트를 뒤에서 비교해가면서
다른 문자를 발견하면 그게 나쁜 문자이다.
그럼 이제 그 다른 문자를 어떻게 하느냐?
그 다른 문자가 문자열에서 가장 늦게 등장하는 인덱스를 찾는다.
다음과 같은 문자열이 있고,
| a | b | a | b | c | a | b |
|---|---|---|---|---|---|---|
| 0 | 1 | 2 | 3 | 4 | 5 | 6 |
텍스트에 등장하는 문자들만 모아둔 테이블이 있을 때
| a | b | c | d |
|---|---|---|---|
| -1 | -1 | -1 | -1 |
이 테이블을 업데이트 시킨다.
A) 업데이트
(1)
| 'a' | b | a | b | c | a | b |
|---|---|---|---|---|---|---|
| '0' | 1 | 2 | 3 | 4 | 5 | 6 |
.
| a | b | c | d |
|---|---|---|---|
| '0' | -1 | -1 | -1 |
(2)
| a | 'b' | a | b | c | a | b |
|---|---|---|---|---|---|---|
| 0 | '1' | 2 | 3 | 4 | 5 | 6 |
.
| a | b | c | d |
|---|---|---|---|
| 0 | '1' | -1 | -1 |
(3)
| a | b | 'a' | b | c | a | b |
|---|---|---|---|---|---|---|
| 0 | 1 | '2' | 3 | 4 | 5 | 6 |
.
| a | b | c | d |
|---|---|---|---|
| '2' | 1 | -1 | -1 |
(4)
| a | b | a | 'b' | c | a | b |
|---|---|---|---|---|---|---|
| 0 | 1 | 2 | '3' | 4 | 5 | 6 |
.
| a | b | c | d |
|---|---|---|---|
| 2 | '3' | -1 | -1 |
(5)
| a | b | a | b | 'c' | a | b |
|---|---|---|---|---|---|---|
| 0 | 1 | 2 | 3 | '4' | 5 | 6 |
.
| a | b | c | d |
|---|---|---|---|
| 2 | 3 | '4' | -1 |
(6)
| a | b | a | b | c | 'a' | b |
|---|---|---|---|---|---|---|
| 0 | 1 | 2 | 3 | 4 | '5' | 6 |
.
| a | b | c | d |
|---|---|---|---|
| '5' | 3 | 4 | -1 |
(7)
| a | b | a | b | c | a | 'b' |
|---|---|---|---|---|---|---|
| 0 | 1 | 2 | 3 | 4 | 5 | '6' |
.
| a | b | c | d |
|---|---|---|---|
| 5 | '6' | 4 | -1 |
(완성)
| a | b | c | d |
|---|---|---|---|
| 5 | 6 | 4 | -1 |
테스트 해보자.
다음과 같은 문자열이 있을 때
| a | b | a | a | d | a | b | a | b | c | a | c | a | b | a | c | a | b |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| a | b | a | b | c | a | b | |||||||||||
d는 -1이므로
| a | b | a | a | 'd' | a | b | a | b | c | a | c | a | b | a | c | a | b |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| a | b | a | b | 'c' | a | b | |||||||||||
| -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
위 인덱스에 맞게
| a | b | a | a | d | a | b | a | b | c | a | c | a | b | a | c | a | b |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| a | b | a | b | c | a | b | |||||||||||
| -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
이동시키면 된다.
다시
| a | b | a | a | d | a | b | a | b | c | a | 'c' | a | b | a | c | a | b |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| a | b | a | b | c | a | 'b' | |||||||||||
| -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
c가 다르고, c에 해당하는 인덱스는 4이므로
| a | b | a | a | d | a | b | a | b | c | a | c | a | b | a | c | a | b |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| a | b | a | b | c | a | b | |||||||||||
| -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 |
이렇게 이동시키면 된다.
근데 만약 다음과 같은 상황이 발생한다면
| a | a | b | a | b | a | a | a | a |
|---|---|---|---|---|---|---|---|---|
| a | a | a | a | b |
문자열이 앞으로 이동해야 될 수도 있으므로
좋은 접미부와 반드시 함께 사용해야 한다.
앞으로도 재밌게 쓰면 읽을게요ㅋ