2025 KnockOn 부트캠프의 예시 코드를 토대로 작성 되었습니다
stdio.h, stdlib.h 등 여러 함수들이 담겨있는 헤더파일은 어떤 방식으로 만들어 질까?
표준 헤더파일 (stdio.h, string.h, stdlib.h 등)은 컴파일러나 OS의 표준 라이브러리에 포함된다.
내부에는 보통 함수 원형 선언 + 매크로 + 구조체 등이 있다.
헤더파일에선 함수를 선언할 뿐, 구현(정의)은 보통 .c 또는 .lib 파일에 따로 존재한다.
예를 들어, int printf(const char *format, ...); 로 선언만 할 뿐 printf() 함수의 실제 구현은 libc 내부에 있다.
표준 헤더파일은 아니지만 하나의 예시 헤더파일을 분석해보자.
다음은 hacker.h의 코드이다. 표준 헤더파일과 다르게 내부에 함수를 정의하였다.
//hacker.h
#pragma once //헤더파일이 한 번만 include되게 보장해주는 지시어
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
typedef struct hacker{
char name[50];
unsigned int age;
char** skill_list;
int skill_num;
int level;
}hacker;
int input_name(hacker*);
int input_age(hacker*);
int input_new_skill(hacker*);
hacker* new_hacker() {
hacker* new_ptr = (hacker*)malloc(sizeof(hacker));
memset(new_ptr->name, '\0', sizeof(new_ptr->name));
new_ptr->skill_num = 0;
new_ptr->level = 1;
new_ptr->skill_list = (char**)malloc(sizeof(char*));
input_name(new_ptr);
input_age(new_ptr);
input_new_skill(new_ptr);
return new_ptr;
}
int input_name(hacker* ptr){
write(1,"Input the name : ",17);
return scanf("%s",ptr->name);
}
int input_age(hacker* ptr){
printf("Input the age : ");
return scanf("%u",&ptr->age);
}
void increase_size(char***);
int input_new_skill(hacker* ptr){
write(1, "Input new Skill : ", 18);
char buf[50] = {0,};
scanf("%s",buf);
char* tmp = (char*)malloc(strlen(buf)+1);
strcpy(tmp,buf);
ptr->skill_num++;
increase_size(&(ptr->skill_list));
ptr->skill_list[ptr->skill_num-1] = tmp;
return ptr->skill_num;
}
void increase_size(char*** skill_list) {
if (*skill_list == NULL) {
*skill_list = (char**)malloc(sizeof(char*));
(*skill_list)[0] = NULL;
return;
}
int org_size = sizeof(*skill_list) / sizeof(char*);
char** new_ptr = (char**)malloc(sizeof(char*) * (org_size + 1));
for (int i = 0; i < org_size; i++) {
new_ptr[i] = (*skill_list)[i];
}
free(*skill_list);
*skill_list = new_ptr;
}
이 코드는 [hacker]라는 구조체를 만들어서 name, age, level, skill_list 를 사용자로부터 입력받아 구조체에 저장하고, 나중에 출력하는 프로그램이다.
구조체 [hacker]
typedef struct hacker {
char name[50];
unsigned int age;
char** skill_list;
int skill_num;
int level;
} hacker;
- char* 와 char**의 차이
- char* : 1개의 문자열을 가리키는 포인터 : "hello"
- char** : 문자열 포인터들의 배열을 가리키는 이중 포인터 : {"hello", "world"}
- char* str = "hello"; // str ----> "hello"
char** list = { "A", "B" }; // list ----> ["A" ----> "A", "B" ----> "B"]
skill_list는 여러 개의 스킬들을 저장하기 위한 문자열 리스트인 것이다.
함수 hacker* new_hacker()
hacker* new_ptr = (hacker*)malloc(sizeof(hacker));
동적으로 구조체를 하나 생성하여 hacker 구조체의 크기만큼 초기화한다. 동적 메모리이므로 Heap에 할당된다.
memset(new_ptr->name, '\0', sizeof(new_ptr->name));
new_ptr->skill_num = 0;
new_ptr->level = 1;
new_ptr->skill_list = (char**)malloc(sizeof(char*));
name 배열을 \0 으로 초기화한 뒤, skill_list를 위한 공간을 확보한다. skill_num과 level도 초기값을 설정해준다.
함수 input_
1. input_name
int input_name(hacker* ptr){
write(1,"Input the name : ",17);
return scanf("%s",ptr->name);
}
scanf로 문자열을 받고 name[50]에 저장한다.
2. input_age
int input_age(hacker* ptr){
printf("Input the age : ");
return scanf("%u",&ptr->age);
}
나이를 입력받아 age에 저장한다.
왜 name함수에는 ptr로 받고 age함수에는 &ptr로 받을까?
- scanf는 항상 '주소'가 필요하다.
- name은 배열이므로 이미 '자기 자신의 주소'를 의미한다. 그래서 &ptr->name으로 받으면 오류가 뜬다.
- age는 int형 변수이므로 &(주소)로 받아야 한다.
- 배열의 이름은 '변하지 않는 포인터'와 같다.
포인터 배열과 2차원 배열의 차이
- 포인터 배열 : 문자열 포인터 10개를 저장하는 배열
- 각각의 arr[i]는 문자열의 시작 주소를 가진다. 그 문자열은 어디든지 존재할 수 있다.(동적 할당, 상수 문자열 등)
- 문자열은 따로따로 저장되므로 메모리가 불연속이다.(각 포인터가 따로 메모리를 가리킴)
malloc() 등을 신경 써야한다. 하지만 문자열 길이가 자유롭다.
- 2차원 배열 : 문자열 n개를 저장할 수 있는 배열
- 연속적으로 저장되어 빠르지만 모든 줄이 고정된 길이를 가지므로 유연하지 않다.
3. input_new_skill
int input_new_skill(hacker* ptr){
write(1, "Input new Skill : ", 18);
char buf[50] = {0,};
scanf("%s",buf);
char* tmp = (char*)malloc(strlen(buf)+1);
strcpy(tmp,buf);
ptr->skill_num++;
increase_size(&(ptr->skill_list));
ptr->skill_list[ptr->skill_num-1] = tmp;
return ptr->skill_num;
}
buf의 내용을 동적 메모리에 복사한 뒤에 skill_list에 추가하는 코드이다.
왜 굳이 tmp라는 메모리를 만들까?
동적 메모리를 할당하는 이유
- heap 메모리 (동적할당된 메모리 tmp) 는 프로그램 종료까지 유지가 된다.
- 하지만 stack 메모리(buf)는 함수가 끝나면 사라진다.
increase_size(리스트 크기를 동적으로 늘려주는 함수)를 이용해 skill_list의 크기를 증가시키고 입력받은 새 스킬을 추가해준다. 새로 추가된 공간의 맨 뒤에 tmp를 넣는다.
skill_num은 현재 저장된 스킬의 개수이고 그 인덱스는 skill_num - 1 이라고 할 수 있겠다.
- 문자열은 항상 맨 뒤에 \0 (null)이 있다는 것을 잊지 말자.
- strlen(buf) + 1 을 하는 이유
함수 increase_size()
void increase_size(char*** skill_list) {
if (*skill_list == NULL) {
*skill_list = (char**)malloc(sizeof(char*));
(*skill_list)[0] = NULL;
return;
}
int org_size = sizeof(*skill_list) / sizeof(char*);
char** new_ptr = (char**)malloc(sizeof(char*) * (org_size + 1));
for (int i = 0; i < org_size; i++) {
new_ptr[i] = (*skill_list)[i];
}
free(*skill_list);
*skill_list = new_ptr;
}
char** 타입 문자열 배열을 늘려서 저장 공간을 확장하는 함수이다.
입력 포인터가 NULL인 경우에는 새롭게 크기가 1인 char* 배열을 만들고,
그 값도 NULL로 초기화하여 빈 배열의 시작을 만든다.
- sizeof(skill_list)/sizeof(char)를 하면 둘 다 포인터니까 size가 같아서 org_size 값이 항상 1이 될 것 같은데?
- skill_list는 malloc을 통한 메모리 할당으로 고정적인 char와 달리 크기가 가변적이다.
따라서 두 요소의 크기가 달라질 수 있다.
- skill_list는 malloc을 통한 메모리 할당으로 고정적인 char와 달리 크기가 가변적이다.
main.c
//main.c
#include "hacker.h"
int main(){
hacker* qwertyou = new_hacker();
printf("%s's age is %d\n",qwertyou->name, qwertyou->age);
printf("%s's level is %d\n",qwertyou->name, qwertyou->level);
printf("\n\n-----qwertyou's skill list-----\n");
for(int i=0;i<qwertyou->skill_num;i++){
printf("%s's skill%d : %s\n",qwertyou->name, i, qwertyou->skill_list[i]);
}
}
출력
Input the name : qwertyou
Input the age : 12
Input new Skill : SQLinjection
qwertyou's age is 12
qwertyou's level is 1
-----qwertyou's skill list-----
qwertyou's skill0 : SQLinjection
위 헤더파일은 표준 헤더와 다르게 함수 구현까지 포함한다고 하였다.
그러면 왜 표준 헤더파일은 다른 라이브러리에 따로 구현을 할까?
헤더파일은 여러 파일에서 include될 수 있기 때문에 중복으로 정의될 오류를 방지하기 위함이다.
또 컴파일 속도라던지 유지 보수를 위해 .h와 .c를 분리하기도 한다.
'언어 > C언어' 카테고리의 다른 글
[자료구조] 배열과 연결리스트로 스택과 큐를 구현해보자 (0) | 2025.04.06 |
---|---|
[자료구조] 연결리스트 (0) | 2025.04.05 |
[C++] 생성자와 소멸자, 함수 오버로딩 (2) | 2025.02.10 |
C와 C++의 차이: 클래스와 구조체, 객체, 멤버변수 (0) | 2025.01.31 |
[C] string.h 내장함수로 문자열 이항 계산기 만들기 (2) | 2025.01.19 |