전체 글

혹시나 문제가 된다면 바로 비공개 처리하겠습니다. 지적이나 댓글 환영합니다!

 

이번 포스팅에서는 보너스 파트의 함수를 구현해보았다. 보너스 파트는 연결리스트(랑크드리스트)를 이용한 기본적인 기능을 구현한 함수들을 구현하였다. 이후에도 많이 사용하게 될 것 같다.

 

참고로, 내가 정의한 libft.h 헤더에는 <unistd.h>와 <stdlib.h>가 include 되어있다. 따라서 libft.h를 호출하면, 따로 정의하지 않고도 <unistd.h> 에 정의된 size_t 타입과 <stdlib.h>의 malloc/free를 사용할 수 있다. 또한 <unistd.h>에 있는 write 함수 또한 사용할 수 있다. 또한 t_list 구조체가 정의되어 있어 보너스 함수 구현에 이를 사용하였다.

 

 

 

 

 

연결리스트란 무엇인가?

 

출처 : geeksforgeeks

 

위키 백과의 정의 :

연결 리스트, 링크드 리스트(linked list)는 각 노드가 데이터와 포인터를 가지고 한 줄로 연결되어 있는 방식으로 데이터를 저장하는 자료 구조이다. 이름에서 말하듯이 데이터를 담고 있는 노드들이 연결되어 있는데, 노드의 포인터가 다음이나 이전의 노드와의 연결을 담당하게 된다

 

1
2
3
4
5
6
typedef struct        s_list
{
    void            *content;
    struct s_list    *next;
}                    t_list;
 
cs

 

위의 코드는 libft 에서 사용된 단일 연결리스트 구조체의 선언 코드이다. 1개의 연결리스트 구조체를 1개의 노드라고도 한다.

기본적인 노드의 구조는 (단일 연결리스트), 현재 노드의 내용물을 담는 변수(content)다음 노드의 주솟값을 갖는 포인터 변수(next) 총 2개로 이루어져있다.

한개의 노드에 다음 노드의 주소를 저장하고, 그 다음 노드에는 그 다음 노드의 주소를 저장하는 식으로 줄줄이 비엔나 소세지처럼 이어진다. 따라서 한개의 노드로부터 다음 노드의 주솟값들을 계속 참조하여 다음 노드로 이동할 수 있다.

 

 

그렇다면, 연결리스트를 쓰는 이유는 무엇일까?

 

기본적인 배열은 정적 할당과 동적 할당으로 나누어진다. 어느 쪽이든, 초기에 할당된 이후에, 연속된 공간의 메모리를 늘려 내용물을 더 저장하거나 원하는 위치의 내용물을 삭제하는 과정들이 자유롭지 않다. 정적 할당의 경우는 거의 불가능하다고 보면 된다. 동적할당에서도 저런 처리를 하려면 복잡한 과정을 거쳐야한다. (예를 들어, "abcd"에서 "bc"를 삭제하고 "ad"라는 문자열을 만드려면, 새로 메모리를 할당하고, 거기에 a와 d만 복사하여 새로운 문자열을 생성해야한다. 그리고 기존 메모리는 해제(free)해줘야한다)

 

그러나 연결리스트는 다르다. 연결리스트의 어느 곳에나 자유롭게 새로운 노드를 끼워넣거나 기존의 노드를 삭제할 수 있다. 따라서 내용의 수정과 생성/삭제가 유연하게 이루어질 수 있다. 필요시에만 메모리를 할당하기 때문에, 메모리 누수가 발생하지 않는 장점도 있다.

그러한 과정이 어떻게 가능한 지는 다음의 구현 예시 코드들을 통해 더욱 잘 이해할 수 있을 것이다.

 

 

 

 

 

(1) ft_lstnew : 새로운 노드를 생성


- 구현 코드 예시 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "libft.h"
 
t_list    *ft_lstnew(void *content)
{
    t_list    *lst;
 
    lst = malloc(sizeof(t_list));
    if (!(lst))
        return (0);
    lst->next = 0;
    lst->content = content;
    return (lst);
}
 
cs

 - malloc을 통해 t_list의 사이즈만큼 메모리를 할당해준다 (malloc 실패 시 0 리턴)

- 할당해준 노드의 next를 0으로 초기화하고, 현재 노드의 content 포인터 변수에는 매개변수로 받은 content의 포인터를 할당한다.

 

 

 

 

 

(2) ft_lstsize : 연결리스트의 사이즈 구함


- 구현 코드 예시 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "libft.h"
 
int        ft_lstsize(t_list *lst)
{
    int    len;
 
    len = 0;
    while (lst)
    {
        len++;
        lst = lst->next;
    }
    return (len);
}
 
cs

- lst = lst -> next를 통해, 다음 노드의 주소를 현재 노드에 저장하여, 다음 노드로 이동한다.

- lst가 마지막 노드에 도달하면 마지막 노드의 next 값은 0으로 초기화 된 채로 남아있으므로, 다음 loop로 넘어가면서 while문에서 빠져나오게 된다 (lst == 0이 되므로)

- 다음 노드로 이동할 수 있는 총 횟수가 연결리스트의 총 길이가 된다.

 

 

 

 

 

(3) ft_lstlast : 연결리스트의 마지막 노드 구함


- 구현 코드 예시 :

1
2
3
4
5
6
7
8
9
10
#include "libft.h"
 
t_list    *ft_lstlast(t_list *lst)
{
    if (!(lst))
        return (0);
    while (lst->next)
        lst = lst->next;
    return (lst);
}
cs

- lst 가 NULL일 경우, 마지막 노드가 없으므로 0을 리턴한다.

- lst = lst -> next를 통해, 다음 노드의 주소를 현재 노드에 저장하여, lst -> next 가 NULL이 될 때까지 다음 노드로 이동한다.

  마지막 노드에 저장된 다음 노드의 주소값은 0이기 때문이다.

- 마지막 노드를 찾았다면 해당 노드를 리턴한다.

 

 

 

 

 

(4) ft_lstadd_front : 연결리스트의 앞부분에 새로운 노드 추가


- 구현 코드 예시 :

1
2
3
4
5
6
7
8
#include "libft.h"
 
void    ft_lstadd_front(t_list **lst, t_list *new)
{
    new->next = (*lst);
    (*lst) = new;
}
 
cs

- new의 다음 노드 주소로, 주어진 연결리스트(lst)의 첫 노드의 주소를 저장한다.

- 주어진 연결리스트의 첫 노드의 시작 주소를 new의 주소로 갱신한다.

 

 

 

 

 

(5) ft_lstadd_back : 연결리스트의 뒷부분에 새로운 노드 추가


- 구현 코드 예시 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "libft.h"
 
void    ft_lstadd_back(t_list **lst, t_list *new)
{
    t_list    *lst_ptr;
 
    if (*lst == NULL)
    {
        (*lst) = new;
        return ;
    }
    lst_ptr = (*lst);
    while (lst_ptr->next)
        lst_ptr = lst_ptr->next;
    lst_ptr->next = new;
}
cs

- 주어진 노드가 NULL이면 해당 노드에 new의 주소를 할당해 준 뒤 함수를 종료한다. 

- 그렇지 않으면, 마지막 노드가 발견될 때까지 노드의 위치를 이동한 후, 마지막 노드의 next에 new의 주소를 할당해준다.

 

 

 

 

참고 자료 출처 :

'IT > 42Seoul' 카테고리의 다른 글

malloc/calloc 관련 정리  (0) 2021.05.30
[Libft] C 언어 라이브러리 구현_BONUS_보너스 함수 구현2  (0) 2021.05.30
Makefile 정리2  (0) 2021.05.25
Makefile 정리1  (0) 2021.05.25
TIP : 코드 리뷰 잘 하는 법  (0) 2021.05.24

Makefile 정리2

2021. 5. 25. 13:04

 

참고 링크 : https://www.youtube.com/watch?v=jnJL6ppn26Q  의 내용을 요약해서 정리한 내용입니다.

 


* makefile이 필요한 이유

: 파일이 많아질수록 컴파일이 복잡해지기 때문에

 

1) 반복되는 컴파일 작업이 지겹고 시간이 오래 걸려서

2) 수정된 파일만 컴파일 할 수 있어서

3) 대규모 프로젝트, 공동 프로젝트에서 반드시 필요

 

 


* 컴파일 과정

: 소스파일(*.c) -> 목적파일(*.o) -> 실행파일(a.out) (바이너리)

 

- 소스파일: vi, vscode 등의 편집기로 편집. 인간이 이해할 수 있는 프로그래밍 언어

- 목적파일: object 파일, 컴파일러를 통해 번역된 기계어

- 실행파일: 기계어 + 라이브러리를 묶는 "링킹"과정을 거쳐 만들어진 파일

 

cf) main함수에서 쓰이는 함수들의 헤더파일을 만드는 이유: main에서 호출하는 함수가 어딨는지 알아야하기 때문에

 

 


* 컴파일 옵션

- -c 옵션 : 소스파일(.c)로 목적파일(.o)을 생성

 

 

ex) gcc -c main.c kor.c usa.c

-> kor.o, main.o usa.o 가 생성됨

- 생성된 목적파일(.o)을 링커 과정을 통해 실행파일로 생성

 

ex) gcc -o app.out kor.o main.o usa.o

-> 실행파일 app.out이 생성됨

- 소스파일(.c)를 한번에 실행파일로 바꾸는 법

 

ex) gcc -o app.out kor.c main.c usa.c

-> 컴파일러가 알아서 .c(소스파일)를 .o(목적파일)로 만들고, 링커를 걸어서 실행파일로 바로 바꿔줌

-> 이 경우, 디렉토리에 .o 파일이 안남고, 실행파일 app.out만 추가됨 (.o 파일을 생성하지 X)

 

 


 

* Makefile의 구조

 

TARGET : DEPENDENCY

(tab넣음) command

 

-> TARGET(만들고자 하는 파일)을 만들기 위해서는 DEPENDENCY 파일들이 필요. 이 둘을 : 으로 구분

-> DEPENDENCY에 command를 사용하여 명령을 실행시키고, 이를 통해 TARGET 파일을 생성함

 

cf) Makefile은 make라는 명령어로 실행시킬 수 있다!

* 코드 예시 (개념의 적용 순서대로 하나하나 예시가 있음)

 

 


 

ex01) 기본적인(명시적인) 코드

// main.c kor.c usa.c

// -> main.o kor.o usa.o

// --> binary (app.out)

 

app.out : main.o kor.o usa.o

          gcc -o app.out main.o kor.o usa.o

main.o :

          gcc -c main.c

kor.o :

          gcc -c kor.c

usa.o :

          gcc -c usa.c

 

 

 

ex02) all 추가

// all이란? 최종적으로 니가 만들고 싶은게 뭐냐를 명시하는 것

// all 옵션이 없는 경우, 제일 첫번째 Target만 실행시키고 종료 (즉 main.o가 위에 있으면 app.out 생성 없이 main.o만 실행시키고 끝남)

// all 옵션을 주면, 순서가 어딜가나 상관없이, 실행시키고자 하는 타겟을 찾아 실행 (타겟의 dependency를 만들어 결과물을 만드는 순서대로 잘 실행됨)

 

all : app.out

main.o :

          gcc -c main.c

kor.o :

          gcc -c kor.c

usa.o :

          gcc -c usa.c

app.out : main.o kor.o usa.o

          gcc -o app.out main.o kor.o usa.o

 

 

 

ex03) 컴파일러, 실행파일을 변수로 설정

// 컴파일러를 원하는 걸로 바꿀 수 있게, 환경변수로 빼주면 좋다

// 그렇게 되면, g++로 컴파일러를 바꿔도, 명령어 전체에서 gcc를 찾아가며 일일이 바꿔줄 필요가 없음

// 이렇게 변수를 추가하면, 다른 프로그램에서 다시 활용하기도, 수정하기도 쉬워짐 (유연해짐)

 

CC = gcc

TARGET = app.out

all : ($TARGET)

main.o :

          $(CC) -c main.c

kor.o :

          $(CC) -c kor.c

usa.o :

          $(CC) -c usa.c

($TARGET) : main.o kor.o usa.o

          $(CC) -o ($TARGET) main.o kor.o usa.o

 

 

 

ex04) 의존 파일 변수 추가

// 실행에 필요한 파일들이 추가되더라도, 일일이 코드에 추가하지 않고 한번에 바꿀 수 있게 함

// 환경파일에서 추가된 소스만 넣으면, 깔끔하게 빌드될 수 있게, OBJS 변수 사용

 

CC = gcc

TARGET = app.out

OBJS = main.o kor.o usa.o

all : ($TARGET)

          ($TARGET) : ($OBJS)

$(CC) -o ($TARGET) ($OBJS)

main.o :

          $(CC) -c main.c

kor.o :

          $(CC) -c kor.c

usa.o :

          $(CC) -c usa.c

 

 

 

ex05) $@, $^, .c.o, $<

// 타겟이라는 변수를 command에서 다시 언급하는 대신 $@ 씀

// Object 파일들을 $^를 통해 dependency 파일로 치환함

// .c.o를 통해, 파일 추가 시 새로 또 코드를 적는 것을 방지함

// -> makefile이 위치한 공간의 .c 파일을 모두 읽어 .o 파일로 바꿔준다는 뜻

 

CC = gcc

TARGET = app.out

OBJS = main.o kor.o usa.o

all : ($TARGET)

          ($TARGET) : ($OBJS)

$(CC) -o ($TARGET) ($OBJS)

.c.o:

          $(CC) -c -o $@ $<

 

// $@를 통해 .o 파일을 타겟으로 잡아줌

// $<를 통해 .c 라는 소스 파일을 잡아줌

 

 

 

ex06) CFLAGS, LDFLAGS

// CFLAGS - 컴파일 옵션

// ex) -Wall (warning이나 에러 등을 상세하게 출력), -g(디버깅 모드)

// LDFLAGS - 링크 옵션, 바이너리를 만들 때 참조할 옵션. 보통 라이브러리 관련 된 것들이 들어감

// ex) -lopenssl(openssl라이브러리), -lc(c라이브러리)

 

CC = gcc

TARGET = app.out

OBJS = main.o kor.o usa.o

CFLAGS = -Wall -g

LDFLAGS = -lc

all : ($TARGET)

($TARGET) : ($OBJS)

          $(CC) ($LDFLAGS) -o ($TARGET) ($OBJS)

.c.o:

          $(CC) ($CFLAGS) -c -o $@ $<

 

 

 

ex07) clean 추가

// 실행 후 생성된 .o (object)파일과 실행파일(binary)를 지워주는 명령어 추가

// make clean 실행 시 지워짐

 

CC = gcc

TARGET = app.out

OBJS = main.o kor.o usa.o

CFLAGS = -Wall -g

LDFLAGS = -lc

all : ($TARGET)

($TARGET) : ($OBJS)

          $(CC) ($LDFLAGS) -o ($TARGET) ($OBJS)

.c.o:

          $(CC) ($CFLAGS) -c -o $@ $<

clean :

          rm -f $(OBJS) $(TARGET)

 

 

 

ex08) main.c 파일을 수정 후, 다시 메인 함수 실행

타임 스탬프를 읽어, 수정된 파일만 읽어들여 다시 컴파일함 (효율성 up)

cf) 한번 makefile 잘 만들어두면, 소스파일 변수만 수정해서 잘 재탕해서 평생 쓸 수 있음

 

 

Makefile 정리1

2021. 5. 25. 12:49

참고 링크 : https://modoocode.com/311 의 내용을 요약 정리해보았습니다.

 

 


* 컴파일 (compile)

: 소스 코드 -> 어셈블리어(컴퓨터가 이해할 수 있는 언어)

성공적으로 컴파일 시, main.o라는 목적 파일(object file)이 생성됨

 

예제 코드)

#include "bar.h"

#include "foo.h"

int main() {

foo();

bar();

}

 


* 링킹 (linking)

: 서로 다른 파일에 흩어져 있는 함수나 클래스들을 한데 묶어서 링크해줌, 컴파일러에 object 파일을 전달

위 코드를 컴파일 해도, main.o 외에 foo.o bar.o가 있어야 실제 함수가 작동함 (main.o 에는 foo와 bar의 코드가 없고 함수의 호출만 정의되어 있음)

따라서 main.o foo.o bar.o를 링킹하여 이 파일들이 모두 하나로 합쳐져야 프로그렘이 제작됨

 

 

 


* make 를 쓰는 이유?

: 일부 파일을 수정할 경우 필요한 명령만 빠르게 컴파일 할 수 있음

* make

: 주어진 쉘 명령어들을 조건에 맞게 실행하는 프로그램

 

Makefile : 이때 어떠한 조건으로 명령어를 실행할 지 담은 파일, make 실행 시 Makefile을 읽어들임

 

 


* Makefile의 3요소

1) target (make할 파일)

2) recipes (실행할 명령어) :

주어진 타겟을 make 할 때 실행할 명령어들의 나열. (주의사항: 명령어 쓸 때, tab 1번의 들여쓰기 필수)

3) prerequisites (필요 조건들) :

주어진 타겟을 make할 때 사용될 파일들의 목록. 의존 파일 (dependency)이라고도 함 (해당 타겟 처리하기 위해 건드려야 할 파일들을 써놓음)

 

만일 주어진 파일들의 수정 시간 보다 타겟이 더 나중에 수정되었다면 해당 타겟의 명령어를 실행하지 않음 (이미 이전에 타겟이 만들어져있다고 간주)

ex) 이를 통해, 수정되지 않은 타겟들은 굳이 다시 컴파일 하지 않고 놔둘 수 있음.

ex) Makefile 형식 !!

target … : prerequisites …

(탭)recipe

 

 

 


* 변수

: 변수 사용 시 $()안에 사용하고자 하는 변수의 이름을 지정하면 됨

 

ex01)

CC = g++

foo.o : foo.h foo.cc

         ($)CC -c foo.cc

 

ex02)

CC = g++

foo.o : foo.h foo.cc

          g++ -c foo.cc

-> ex01과 ex02의 의미가 같다

 

 

TMI) 변수를 정의하는 또 다른 방법 := 과 기존 = 의 차이

:= 로 변수를 정의할 경우, 해당 시저의 변수의 값만 확인함

반면에, = 로 변수를 정의할 경우, 참조할 값이 정의될 때까지 변수의 값이 결정되지 않음

대부분의 상황에서는 = 나 := 중 아무거나 사용해도 상관 없음

 

ex)

B = $(A)

C = $(B)

A = a

-> A가 실제로 정의될 때까지 B와 C가 결정되지 않는다. 마지막에 A = a 를 통해 A가 a로 대응되어야, C가 a로 결정됨

 

ex)

B := $(A)

A = a

-> := 를 통해 해당 시점에의 변수 값만 확인하므로, B는 그냥 빈 문자열이 됨

- 변수들의 정의 순서에 크게 구애받고 싶지 않다면 = 를 사용하는 것이 편함

- A = 와 같이 자기 자신을 수정하고 싶다면, := 를 사용해야 무한 루프를 피할 수 있음

 

 

TMI)

CC, CXXFLAGS 는 Makefile에서 자주 사용되는 변수

CC : 컴파일러 이름

CXXFLAGS : 컴파일러 옵션

 

 


* PHONY :

Makefile에 흔히 추가하는 기능으로 빌드 관련된 파일들 (.o 파일들)을 모두 제거하는 명령 넣음

 

주의)

실제로 clean이라는 파일이 디렉토리에 생성되면, make clean시 문제가 생김

make는 clean의 필요 파일들이 없는데, clean 파일이 있으니까 clean 파일은 항상 최신이므로, recipe를 실행하지 않겠다며 make clean 명령을 무시함

 

-> 이를 방지하기 위해서, clean을 PHONY라고 등록함 (Phony : 가짜의, 허위의)

그리고 make clean을 실행하면, clean 파일의 유무와 상관 없이 언제나 해당 타겟의 명령을 실행하게 됨

 


* 패턴 사용하기 :

실제 프로젝트 시, 엄청나게 많은 파일들을 다루게 됨. 이런 경우 한꺼번에 빌드 방식을 명시햐야함.

이때, "패턴 매칭"을 통해서 특정 조건에 부합하는 파일들에 대해, 간단하게 recipe를 작성할 수 있음

 

1) %.o : 와일드 카드. *.o와도 같음 (.o로 끝나는 파일 이름들을 타겟으로 함)

2) $< : prerequisite 에서 첫 번째 파일의 이름에 대응됨

3) 그 외의 Makefile에서 제공하는 자동변수

            (1) $@ : 타겟 이름에 대응됨

            (2) $< : prerequisite 에서 첫 번째 파일의 이름에 대응됨

            (3) $^ : 의존 파일 목록 전체에 대응됨

            (4) $?: 타겟보다 최신인 의존 파일들에 대응됨

            (5) $+: $^와 비슷하지만, 중복된 파일 이름들까지 모두 포함

 

ex01)

foo.o : foo.h foo.cc

          $(CC) $(CXXFLAGS) -c foo.cc

bar.o : bar.h bar.cc

          $(CC) $(CXXFLAGS) -c bar.cc

 

ex02)

foo.o: foo.cc foo.h

           $(CC) $(CXXFLAGS) -c $<

 

-> ex01의 명령어들에 패턴을 사용하면, ex02와 같이 나타낼 수 있음

 

cf) 패턴은 타겟과 prerequisite 부분에만 사용할 수 있음. recipe 부분에서는 패턴을 사용할 수 없음

따라서 컴파일러에 foo.cc를 전달하기 위해서는 Makefile의 자동 변수를 사용해야함

 

 


 

* 자동으로 prerequisite 만들기:

컴파일 시에 -MD 옵션을 추가해서 컴파일

이를 통해, 목적 파일 말고도 컴파일 한 소스파일을 타겟으로 하는 의존파일 목록을 담은 파일을 생성해줌 (main.d라는 파일을 생성함)

 

cf)

$ cat main.d

main.o: main.cc /usr/include/stdc-predef.h foo.h bar.h

-> main.d를 살펴보면, target : prerequisite 인 것 같은 부분이 생성된다.

이때, /usr/include/stdc-predef.h 파일은 컴파일러가 컴파일 할 때 암묵적으로 참조하는 헤더파일이다.

(-MD를 통해 컴파일러가 생성한 의존파일 목록이므로 포함됨)

 

ex)

CC = g++

CXXFLAGS = -Wall -O2

OBJS = foo.o bar.o main.o

%.o: %.cc %.h

        $(CC) $(CXXFLAGS) -c $<

main : $(OBJS)

        $(CC) $(CXXFLAGS) $(OBJS) -o main

.PHONY: clean

clean:

rm -f $(OBJS) main

include main.d

 

-> 마지막에 include main.d를 넣어서 Makefile에 main.d를 포함시킴

 

ex)

CC = g++

CXXFLAGS = -Wall -O2

OBJS = foo.o bar.o main.o

%.o: %.cc

         $(CC) $(CXXFLAGS) -c $<

main : $(OBJS)

         $(CC) $(CXXFLAGS) $(OBJS) -o main

.PHONY: clean

clean:

rm -f $(OBJS) main

-include $(OBJS:.o=.d)

 

-> %.o: %.cc %.h

 

$(CC) $(CXXFLAGS) -c $< 이 부분을 컴파일러가 생성한 .d 파일로 대체함

-> $(OBJS:.o=.d) 부분은 OBJS 에서 .o 로 끝나는 부분을 .d 로 모두 대체하라는 의미임

즉, 해당 부분은 -include foo.d bar.d main.d 가 됨

 

-> include 될 때 이미 있는 %.o : %.cc 는 어떻게 되느냐? :

같은 타겟에 대해서 여러 의존 파일 목록들이 정해져 있다면 이는 make에 의해 모두 하나로 합쳐짐

-> include 가 아닌 -include의 경우, 포함하고자 하는 파일이 존재하지 않아도 make 메세지를 출력하지 않음

 

cf)

맨 처음에 make를 할 때에는 .d 파일들이 제대로 생성되지 않은 상태이기 때문에, include 가 아무런 .d 파일들을 포함하지 않음

물론 큰 문제가 없는게, 어차피 .o 파일들도 make가 %.o:%.cc 부분의 명령어들을 실행하면서 컴파일 하기 때문에, make를 하게 될 때에는 제대로 .d 파일들을 로드할 수 있음

 

 


* 최종 정리

 

1) 헤더파일들을 분리하지 않는 경우

Makefile, obj, src/bar.cc + src/bar.h + src/foo.cc + src/foo.h + src/main.cc 의 프로젝트 구조를 가정함

이와 같은 구조에서 항상 사용할 수 있는 만능 Makefile은 다음과 같음

 

(중요)

CC = g++

# C++ 컴파일러 옵션

CXXFLAGS = -Wall -02

# 링커 옵션

LDFLAGS =

# 소스 파일 디렉토리

SRC_DIR = ./src

# 오브젝트 파일 디렉토리

OBJ_DIR = ./obj

# 생성하고자 하는 파일 이름

TARGET = main

# Make 할 소스 파일들

# wildcard 로 SRC_DIR 에서 *.cc로 된 파일 목록을 뽑아낸 뒤에

# notdir로 파일 이름만 뽑아낸다.

# (e.g SRCS는 foo.cc bar..cc main.cc가 된다.)

SRCS = $(notdir $(wildcard $(SRC_DIR)/*.cc))

OBJS = $(SRCS:.cc=.o)

# OBJS 안의 object 파일들 이름 앞에 $(OBJS_DIR)/ 을 붙인다

# patsubjst 함수 사용, $(patsubst 패턴,치환 후 형태,변수)와 같은 꼴로 사용

# $(OBJS) 안에 있는 모든 %.o 패턴을 $(OBJ_DIR)/%.o 로 치환해라

OBJECTS = $(patsubjst %.o, $(OBJ_DIR)/%.o, $(OBJS))

DEPS = $(OBJECTS: .o=.d)

all: main

$(OBJ_DIR)/%.o : $(SRC_DIR)/%.cc

          $(CC) $(CXXFLAGS) -c $< -o $@ -MD $(LDFLAGS)

$(TARGET) : $(OBJECTS)

           $(CC) $(CXXFLAGS) $(OBJECTS) -o $(TARGET) $(LDFLAGS)

.PHONY: clean all

clean:

rm -f $(OBJECTS) $(DEPS) $(TARGET)

-include $(DEPS)

 

 

 

2) 헤더 파일들을 따로 뽑는 경우

파일 구조: include (.h파일들 모아둔 폴더), Makefile, obj, src(.cc 모아둔 폴더)

 

 

# 헤더파일 경로

INCLUDE = -Iinclude/

컴파일러 옵션에 -Iinclude/ 를 추가해주면 됨. 여기서 include 는 헤더파일 경로를 의미함

 

 


 

* 멀티 코어를 활용해서 Make 속도를 올리자 :

그냥 make를 실행하게 되면 1개의 쓰레드만 실행되어서 속도가 꽤 느림. 멀티 코어 cpu를 사용하는 컴퓨터에서는 make를 여러 개의 쓰레드에서 돌릴 수 있음

이를 위해선 인자로 -j 뒤에 몇 개의 쓰레드를 사용할 지 숫자를 적어서 전달하면 됨

 

ex) make -j8

-> make가 8개의 쓰레드에 나뉘어 실행. make 속도가 월등히 향상됨

코어 개수 + 1 만큼의 쓰레드를 생성해서 돌리는 것이 가장 속도가 빠름

만약 내 컴퓨터의 코어 개수를 모른다면? (리눅스의 경우)

 

ex) make -j$(nproc

-> $(proc) 알아서 컴퓨터의 현재 코어 개수로 치환됨

 


 

유튜브에서 좋은 영상을 발견하여 핵심 내용을 메모해 보았습니다.

 

42서울에서는 개발자들간의 커뮤니케이션과 피드백 동료학습 등을 권장하는 만큼, 코드 리뷰를 할 기회가 굉장히많다.

라피신 초반에는 낯선 환경에서 거의 한 적이 없던 코드리뷰라는 것을 진행하자니 난감했고, 제대로 된 양질의 피드백을 남기지 못한 듯한 기분이 들었다. 점점 갈수록 코드 리뷰를 하는 일에는 익숙해졌지만 스스로도 '제대로 코드 리뷰를 해주는 법'에 대해 잘 몰랐던 것 같다.

그래서 유튜브를 보는 중에 좋은 영상을 발견했고, 내용을 요약해보았다. 나도 당장 내일부터 동료평가할 떄 이 사항을 적용해보아야겠다.

 

 


코드 리뷰 잘 하는 법

 

리뷰받는 사람

  1. 좋은 마음가짐 : 니가뭔데내코드를 악평해? 라거나 니가무조건맞아!라는 태도는 지양 (얼린마음 + 비판적 사고 유지)
  2. 리뷰하기 좋은 코드 짜기 (코드가 짧고 섹션 별로 잘 나누어져있어야)
  3. 테스트 플랜을 쓰자 : 코드에 대한 신뢰성을 높이고 퀄리티에 대한 피드백을 잘 받기 위해서 좋다

 

리뷰 주는 사람

  1. 피드백을 “유하게” 잘 주자 : 말을 덜 공격적이고 긍정적인 방식으로 (제 생각에는…) 효과적으로 피드백이 잘 전달될수록 팀의 생산성이 좋아짐
  2. 피드백은 최대한 정확하고 자세하고 뭘해야되는지 명확하게 주자
  3. 리뷰를 할 땐 변경사항의 트레이드 오프를 생각하며 하자.

 

  • 협업치트키 : 직장 동료를 넘어서는 친구가 되는 것

(좀더 관계가 좋으면 쓸데 없는 오해나 커뮤니케이션 부족에서 일어나는 비효율성을 줄일 수가 있다)

 


 

유튜브 링크 :

https://www.youtube.com/watch?v=VaaRvs8YU1M

혹시나 문제가 된다면 바로 비공개 처리하겠습니다. 지적이나 댓글 환영합니다!

 

이번 포스팅에서는 이어서 part2의 함수를 구현해 보겠다. file descripter를 매개변수로 받아 write()를 사용하는 함수 4가지를 구현하였다.

주의사항이라고 적은 사항들이 대부분 내가 실수했으나 고쳐서 어떻게 해결했는 지에 대한 내용들이다.

 

참고로, 내가 정의한 libft.h 헤더에는 <unistd.h>와 <stdlib.h>가 include 되어있다. 따라서 libft.h를 호출하면, 따로 정의하지 않고도 <unistd.h> 에 정의된 size_t 타입과 <stdlib.h>의 malloc/free를 사용할 수 있다. 또한 <unistd.h>에 있는 write 함수 또한 사용할 수 있다.

 

 

 

* fd (File descripter)란 무엇인가?

1) 시스템으로부터 할당 받은 파일을 대표하는 0이 아닌 정수 값

2) 프로세스에서 특정 파일에 접근할 때 사용하는 추상적인 값

3) 프로세스에서 열린 파일의 목록을 관리하는 테이블의 인덱스

 

- 유닉스 시스템에서 모든 것은 "파일"이다. 모든 객체들은 파일로써 관리된다.

- 유닉스 시스템에서 프로세스가 이 파일들을 접근할 때에 파일 디스크립터라는 개념을 이용한다.

 

- 파일 디스크립터는 '0이 아닌 정수' 값이다. 즉, 음수가 아닌 0과 양수인 정수 값을 갖는다.

프로세스가 실행 중에 파일을 Open 하면 커널은 해당 프로세스의 파일 디스크립터 숫자 중에 사용하지 않는 가장 작은 값을 할당해 준다.

그 다음 프로세스가 열려있는 파일에 시스템 콜을 이용해서 접근할 때, FD 값을 이용해 값을 지칭할 수 있다.

 

프로그램이 프로세스로 메모리에서 실행을 시작할 때, 기본적으로 할당되는 파일 디스크립터들이 있다. 바로 표준 입력, 표준 출력, 표준 에러이다. 이들에게 각각 0, 1, 2 라는 정수가 할당된다.

0이 아닌 정수로 표현된느 파일 디스크립터는 0 ~ OPEN_MAX까지의 값을 가질 수 있으며, OPEN_MAX 값은 플랫폼에 따라 다르다.

 

이미지 출처 : http://itnovice1.blogspot.com/2019/08/linux-file-descriptor.html

파일 디스크립터는 위에서 볼 수 있듯이, 단순히 숫자인 이유는 프로세스가 유지하고 있는 FD 테이블의 인덱스이기 때문이다. FD 3번이라는 의미는 FD 테이블의 3번 항목이 가리키는 파일이라는 의미이다.

프로세스는 이런 FD 테이블과 파일 테이블의 정보를 직접 고칠 수 없으며, 반드시 커널을 통해서 수정해야 한다.

 

 

 

 

(1) ft_putchar_fd

: 주어진 fd에 대하여 주어진 문자를 write한다.

 


- 구현 코드 예시 :

1
2
3
4
5
6
#include "libft.h"
 
void    ft_putchar_fd(char c, int fd)
{
    write(fd, &c, 1);
}
cs

 

 

 

(2) ft_putstr_fd

: 주어진 fd에 대하여 주어진 문자열을 write한다.

 


- 구현 코드 예시 :

1
2
3
4
5
6
7
#include "libft.h"
 
void    ft_putstr_fd(char *s, int fd)
{
    write(fd, s, ft_strlen(s));
}
 
cs

- ft_strlen()을 통해 문자열의 길이를 구해서,  s의 시작주소로부터 그만큼의 바이트를 주어진 fd로 write한다.

 

 

 

(3) ft_putendl_fd

: 주어진 fd에 대하여 주어진 문자열을 개행문자와 함께 write한다.

 


- 구현 코드 예시 :

1
2
3
4
5
6
7
#include "libft.h"
 
void    ft_putendl_fd(char *s, int fd)
{
    ft_putstr_fd(s, fd);
    write(fd, "\n"1);
}
cs

- 주의사항 : 따로 개행을 write할 때, write(fd, "\n", 1)이 아니라 write(1, "\n", 1)라고, 습관적으로 fd 대신 1을 쓰는 실수를 할 수 있음

특히 이런 실수의 경우는 잘 눈에 띄지 않아 찾아내기가 어렵다

 

- 앞서 구현한 ft_putstr_fd()를 활용하여 문자열을  write한 다음, write함수를 통해 주어진 fd로 개행문자를 write해준다.

 

 

(4) ft_putnbr_fd

: 주어진 fd에 대하여 주어진 숫자를 write한다.

 


- 구현 코드 예시 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include "libft.h"
 
static void    write_nbr_fd(int n, int fd)
{
    char    c;
 
    c = (n % 10+ '0';
    if (n >= 10)
        write_nbr_fd(n / 10, fd);
    write(fd, &c, 1);
}
 
void        ft_putnbr_fd(int n, int fd)
{
    if (n == -2147483648)
    {
        write(fd, "-2147483648"11);
        return ;
    }
    else if (n < 0)
    {
        write(fd, "-"1);
        n *= -1;
    }
    write_nbr_fd(n, fd);
}
cs

- 주의사항 : 습관적으로 write()의 fd 인자 위치에 1을 써버리는 실수를 할 수 있다. 특히 "-"를 write() 해주는 부분을 주의해야한다

 

- 숫자 n을 양수로 만들어, 재귀를 통해 한 글자씩 write해주는 방식으로 구현하였다.

- 이때 int 최솟값인 -2^31에 -1을 곱해 양수로 만들면 int 범위 (최대값 =  2^31 - 1)를 벗어나므로, 이는 따로 앞부분에서 예외처리 해주었다.

- 음수에 (-1)을 곱해 양수로 만들 때, "-" 부호를 주어진 fd로 write() 해주었다.

 

 

 

 

 

참고 자료 출처 : 

혹시나 문제가 된다면 바로 비공개 처리하겠습니다. 지적이나 댓글 환영합니다!

 

이번 포스팅에서는 이어서 문자열 관련 함수들을 구현해보겠다.

소개할 함수 3개의 공통점은 모두 동적할당(malloc)을 사용하여 편집된 문자열을 새로 만드는 것이다.

이번 함수들은 메뉴얼이 이미 과제 페이지에 영어로 상세히 적혀있는 대신, 전보다 구현이 복잡한 특징을 가졌다.

따라서 이번 시간에는 함수의 역할 대한 정리보다는 함수를 어떤 방식으로 구현했는지, 구현에서 어떤 문제들을 마주쳤고 이를 어떻게 해결했는 지 위주로 설명하겠다. (면접용 연습)

주의사항이라고 적은 사항들이 대부분 내가 실수했으나 고쳐서 어떻게 해결했는 지에 대한 내용들이다.

 

참고로, 내가 정의한 libft.h 헤더에는 <unistd.h>와 <stdlib.h>가 include 되어있다. 따라서 libft.h를 호출하면, 따로 정의하지 않고도 <unistd.h> 에 정의된 size_t 타입과 <stdlib.h>의 malloc/free를 사용할 수 있다.

 

 

 

 

(1) ft_split : 주어진 문자를 기준으로 문자열 쪼개기

: 주어진 문자열(1차원 배열)을 주어진 문자를 기준으로 쪼개서, 쪼개진 단어들을 동적할당한 2차원 배열에 넣는다.

 

 


- 구현 코드 예시 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#include "libft.h"
 
static int    ft_get_cnt(char const *s, char c)
{
    int i;
    int flag;
    int cnt;
 
    i = 0;
    cnt = 0;
    flag = 0;
    while (s[i])
    {
        flag = 0;
        while (s[i] && s[i] != c)
        {
            i++;
            flag = 1;
        }
        if (flag == 1)
            cnt++;
        if (s[i])
            i++;
    }
    return (cnt);
}
 
static char    *ft_strndup(char const *s, int size)
{
    char    *p;
    int        i;
 
    i = 0;
    if (!(p = malloc(sizeof(char* (size + 1))))
        return (0);
    while (i < size)
    {
        p[i] = s[i];
        i++;
    }
    p[i] = '\0';
    return (p);
}
 
static char    **free_all(char **ptr)
{
    int i;
 
    i = 0;
    while (ptr[i])
    {
        free(ptr[i]);
        i++;
    }
    free(ptr);
    return (0);
}
 
static char    **ft_word_split(char **p, char const *s, char c)
{
    int    i;
    int    size;
    int    index;
 
    i = 0;
    index = 0;
    while (s[i] == c)
        i++;
    while (s[i])
    {
        size = 0;
        while (s[i + size!= c && s[i + size])
            size++;
        if (size == 0)
            i++;
        else
        {
            if (!(p[index++= ft_strndup((s + i), size)))
                return (0);
            i += size;
        }
    }
    p[index] = 0;
    return (p);
}
 
char        **ft_split(char const *s, char c)
{
    char    **p;
    int        cnt;
 
    if (*== 0)
    {
        if (!(p = malloc(sizeof(char *))))
            return (0);
        p[0= 0;
        return (p);
    }
    cnt = ft_get_cnt(s, c) + 1;
    if (!(p = malloc(sizeof(char ** cnt)))
        return (0);
    if (!(p = ft_word_split(p, s, c)))
        return (free_all(p));
    return (p);
}
 
cs

- ft_get_cnt() 함수를 통해 문자 c로 구분되는 단어의 개수를 구한다.

- 단어의 개수 + 끝부분 null을 넣을 1칸을 추가하여 메모리를 할당한다.

 

- 메모리 할당이 성공했을 경우, ft_word_split() 함수를 통해 각각의 단어를 2차원 배열에 할당해준다.

- ft_strndup() 함수정해진 사이즈만큼 1차원 배열을 할당하고 복사하여 포인터를 리턴하는 함수이다. ft_word_split() 실행 중에, 구분자로 나눠진 단어를 복사하는 데 쓰였다. (char *) 배열의 1칸에 (char) 배열인 문자열 포인터를 받아와 붙여주는 식이다.

 

메모리 할당이 실패할 경우 남아있는 메모리들을 모두 free()해주기 위해서, free_all() 함수를 사용하였다.

 

 

- 주의할 점 : ft_word_split()이나 ft_strndup()에서는 malloc을 통한 메모리 할당을 사용한다. 이때 커널에 할당할 메모리가 부족할 경우, 리턴되는 포인터에 0을 받을 수 있다. (char) 1차 배열을 할당하고 복사하는 상황에서, 메모리가 다 할당되는데는 성공하지 못했어도, 이미 할당된 메모리가 남아있을 수 있다. 메모리 누수를 예방하려면 이러한 메모리들도 모두 free 해주어야 한다.

(안하면 "메모리 누수"로 판단되어 동료평가에서 0점 맞을 수 있음)

여기서 주의할 점은, (char *)을 할당하는 부분에서는 free()를 하지 않는다. 왜냐면 p에는 이미 메모리 할당 실패로 null이 들어가 있는 상태기 때문이다. 또한 이 경우는 할당한 메모리가 존재하지 않는다. 그러므로 free(p)가 free(null)과 다름 없이 해석되어 segfault 오류가 날 수 있다. (char *) 배열 할당 후, char 배열을 할당하는 시점에서 메모리 할당이 실패할 경우에만, 이미 할당되고 남아있는 메모리들을 free()로 정리해주는 것을 유의하자.

 

- 주의할 점 : 주어진 문자열 s가 빈 문자열일 경우, 할당된 p를 p[0] = 0 으로 처리하여 포인터를 리턴해야한다. 나는 이 예외처리 부분을 잡지 못해서 첫 트라이에서 segfault를 받은 것 같다. 

 

 

- 참고) 커널이란?

더보기

Linux 커널은 Linux 운영 체제(OS)의 주요 구성 요소이며, 컴퓨터 하드웨어와 프로세스를 연결하는 핵심 인터페이스이다.

자원 관리, 한정된 시스템 자원을 효율적으로 관리하여 프로그램의 실행을 원활하게 한다

 

 

 

 

(2) ft_itoa : 숫자를 문자열로 바꾸기

- Alphabet to Integer

- 정수를 문자열로 변환한 뒤, 그 문자열의 포인터를 리턴한다.

 

 


- 구현 코드 예시 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include "libft.h"
 
static int    ft_nbr_len(int n, int cnt)
{
    if (n == 0 && cnt != 0)
        return (cnt);
    return (ft_nbr_len(n / 10, cnt + 1));
}
 
static int    ft_power_len(int n)
{
    int    gop;
 
    gop = 1;
    while (n--)
        gop *= 10;
    return (gop);
}
 
static char    *ft_strcpy(char *dst, char *src)
{
    int    i;
 
    i = 0;
    while (src[i])
    {
        dst[i] = src[i];
        i++;
    }
    dst[i] = '\0';
    return (dst);
}
 
char        *ft_itoa(int n)
{
    char    *p;
    int        len;
    int        i;
 
    len = ft_nbr_len(n, 0);
    if (n < 0)
        len++;
    p = malloc(sizeof(char* (len + 1));
    if (!(p))
        return (0);
    if (n == -2147483648)
        return (ft_strcpy(p, "-2147483648"));
    i = 0;
    if (n < 0)
    {
        p[i++= '-';
        n *= -1;
    }
    while (i < len)
    {
        p[i] = ((n / ft_power_len(len - 1 - i)) % 10+ '0';
        i++;
    }
    p[i] = '\0';
    return (p);
}
 
cs

- 주의할 점 : 숫자를 양수로 바꾼 뒤 문자열로 변환하였기 때문에, int형 최솟값인 -2^31은 따로 예외처리 해주었다. 이 경우는 ft_strcpy를 통해, 이미 할당된 공간에 문자열 "-2147483648"를 복사하고, 이를 바로 리턴하였다.

 

 

- 재귀를 사용하는 ft_nbr_len() 함수를 통해, 숫자의 길이를 구하여 len에 저장한다. 만일 숫자가 음수인 경우, 부호 부분의 길이인 1을 더해준다.

- 구한 숫자의 길이 + 1 만큼 malloc으로 메모리를 할당해준다.

- 앞에서부터 숫자를 하나씩 떼서 그 한자리 숫자에 '0'을 더해 문자로 변환한다. 그리고 이 문자를 할당된 공간의 인덱스에 하나씩 넣어준다.

- 변환이 완료되면, 문자열의 마지막에 '\0'을 넣어서 문자열 종료를 알린다. 그다음 문자열의 포인터인 p를 리턴한다.

 

 

 

 

 

(3) ft_strmapi : 기존의 문자열에 함수를 적용한 새로운 문자열 생성

 

 


- 구현 코드 예시 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include "libft.h"
 
char        *ft_strmapi(char const *s, char (*f)(unsigned intchar))
{
    char            *str;
    unsigned int    i;
    size_t            len;
 
    len = ft_strlen(s);
    str = malloc(sizeof(char* (len + 1));
    if (!(str))
        return (0);
    i = 0;
    while (s[i])
    {
        str[i] = (*f)(i, s[i]);
        i++;
    }
    str[i] = '\0';
    return (str);
}
 
cs

- 매개변수로 함수의 주소를 받는다.

- malloc()을 통해 매개변수로 받은 문자만큼의 공간을 새로 할당한다.

- 문자열의 모든 문자에 각각 그 함수를 적용하여, 그 결과값으로 나오는 문자들을 새로 할당한 공간에 저장한다.

- 마지막으로, 할당된 공간에 '\0'를 넣어 문자열을 종료시킨다.

- 만들어진 새로운 문자열의 포인터를 리턴한다. 

 

- 주의사항 : malloc() 실패 시 널가드

 

 

+ Recent posts