TIL - 2024/04/12

박상우·2024년 4월 12일
0

📝 TIL

목록 보기
17/21
post-thumbnail

C

선언과 정의

선언은 어떤 변수나 함수의 형식을 정의하는 단계.

정의는 어떤 변수나 함수를 선언함과 동시에 값까지 부여하는 것.

선언만하는 경우, 해당 값이나 함수에 대해서 알 수 없다.

변수의 경우 선언만 한 경우 C 언어 컴파일러가 자동으로 변수의 기본값을 0으로 초기화 하기 때문에 문제가 생기진 않는다.

int a;

int main() {
	int b = 1;
	return a + b; // 1
}

하지만 함수의 경우 선언 후 정의를 하지 않으면 함수의 내용을 알 수가 없기 때문에 에러가 발생한다.

void func1();

int main() {
	fun1() // ERROR
}

선언 단계는 컴파일 단계에서만 영향을 끼치기 때문에 이후 어셈블러를 통해 만들어질 바이너리 파일에는 정의되지 않고 선언만된 변수, 함수에 대해서는 포함을 시키지 않는다.

정의는 반드시 선언이 선행되어야 한다. 선언 없이 정의하는 것은 불가능 하다.

함수가 정의되어 있지만 함수의 순서에 따라 선언된 사실을 모를 수 있다.

void func1() {
	func2();
}

void func2() { // code }

func2는 정의되어 있지만 func1이 실행될 때는 func2가 없다는 에러가 나타나게 된다.컴파일러가 위에서 아래로 코드를 읽기 때문에 func1을 실행하는 순간에는 func2가 있는지 몰랐기 때문에 에러가 나타나는 것이다. 그래서 함수의 위치를 바꾸거나 다음처럼 func2를 코드 상단에 선언해주어서 컴파일러에게 func2의 존재를 알림으로서 에러를 해결할 수 있다. ( 전방 선언 / forward declaration )

void func2();

void func1() {
	func2()
}

void func2() { // code }

#include

#include 전처리 구문은 다른 언어의 import 구문과 달리 단순히 다른 파일의 내용을 그대로 가져와 합치는 역할을 한다. 그래서 아래와 같이 #include 문을 사용할 수 있다.

#include <stdio.h>

char* string =
#include "test.txt"
;

int main(){
    pritnf("test.txt = %s\n",string);
    return
}

extern / static

extern은 다른 파일에 존재하는 전역변수를 참조할 때 사용된다. extern 변수는 선언만 하고 실제 매모리 공간은 변수가 저장된 파일에서 할당된다.

static은 특정 파일 내에서만 사용가능한 지역변수이다. extern 변수처럼 프로그램이 실행될 때 생성되지만, 다른 파일에서 접근 불가능하다.


enum / union / struct

  • enum

    변수가 가질 수 있는 값을 미리 열거해 놓은 자료형

    enum dice { 1, 2, 3, 4, 5, 6 }
  • struct

    기본 타입을 모아 새로운 타입을 만들 수 있는 자료형, (구조체 이름).(멤버 변수) 로 접근해서 사용할 수 있다.

    struct User {
    	char[] name;
    	int age;
    } user1;
    
    user1.name = 'park';
    user1.age = 10;
    
    // user1 = {.name = 'park', .age=10} 로도 초기화 가능
    // user1 = {'park', 10} 로도 초기화 가능
    
    printf('name: %s, age: %d', user1.name, user1.age);

    typedef - 이미 존재하는 타입에 새로운 이름을 부여한다.

    typedef struct User Person;
    
    // 구조체 선언시 아래와 같이 typedef를 사용해서 선언할 수 있다.
    typedef struct {
    	int price;
    	int order_count;
    } Menu;
  • union

    struct와 달리 union의 모든 멤버 변수가 하나의 메모리 공간을 공유한다.

    union이 선언되면 크기가 가장 큰 공용변수의 메모리 공간을 공유하게 된다.

    모든 멤버 변수가 같은 메모리를 공유하기 때문에 union의 멤버 변수 중 1개만 사용할 수 있다.

    #include <stdio.h>
    
    // 두 가지 다른 자료형을 가진 변수를 저장하기 위한 union 정의
    union Data {
        int i;
        float f;
        char str[20];
    };
    
    int main() {
        union Data data; // union 변수 선언
    
        printf("Memory size occupied by data : %d\n", sizeof(data));
    
        data.i = 10; // 정수형 변수 i에 값을 할당
        printf("data.i : %d\n", data.i);
    
        data.f = 220.5; // 실수형 변수 f에 값을 할당
        printf("data.f : %f\n", data.f);
    
        strcpy(data.str, "C Programming"); // 문자열 변수 str에 값을 할당
        printf("data.str : %s\n", data.str);
        
        // printf("data.i : %d\n data.f: %d", data.i, data.f) 이거는 안됨
    
        return 0;
    }

    union을 왜 쓰는걸까?

    특정 값을 다양한 타입으로 표현하는 것이 필요할 때, 필요한 모든 타입으로 선언하는 것 보다 union으로 선언하여 가장 큰 데이터 타입의 메모리에 한해서 값을 바꾸어가며 씀으로써 비교적 메모리를 절약할 수 있기 때문에 쓰는것 같다.

    ChatGPT의 답변: C 언어에서 union은 다양한 데이터 유형을 하나의 메모리 위치에 저장하기 위해 사용됩니다. 이는 메모리 공간을 절약하거나 데이터를 다양한 형식으로 해석할 때 유용합니다.

    예를 들어, 여러 가지 유형의 데이터를 저장하고 싶을 때 사용할 수 있습니다. 예를 들어, 한 메모리 공간에 정수, 부동 소수점 숫자, 문자열 등을 모두 저장할 수 있습니다. 이것은 구조체와 비슷해 보일 수 있지만, union은 한 번에 하나의 멤버만 사용할 수 있다는 점에서 다릅니다. 구조체는 모든 멤버를 동시에 사용할 수 있지만, union은 각각의 멤버 중 하나만 사용할 수 있습니다.

    일반적인 사용 사례로는 다양한 데이터 유형을 포함하는 변수를 표현하는 경우나, 메모리를 절약하려는 경우 등이 있습니다. 그러나 주의해야 할 점은 union을 사용할 때 데이터를 사용하는 방식을 명확히 알고 있어야 하며, 잘못된 유형으로 접근할 경우 예기치 않은 결과가 발생할 수 있습니다.


Pointer

포인터는 메모리의 주소값을 저장하는 변수.

int n = 1; // 변수
int *ptr = &n; // 포인터 변수
  • & 주소 연산자 - 변수 이름 앞에 사용하며, 해당 변수의 주소값을 반환

    • 참조 연산자 - 포인터 이름 앞에 사용되며, 포인터가 가리키는 값을 반환한다.
  • 포인터 연산

    • 포인터끼리 덧셈, 곱셈, 나눗셈은 의미가 없다.
    • 포인터끼리 뺄셈은 두 포인터 사이의 거리를 의미한다.
    • 포인터와 정수 연산은 유효하지만, 포인터와 실수 연산은 유효하지 않다.
    • 포인터끼리 비교, 대입이 가능하다.
  • 포인터의 종류

    • void 포인터 저장 대상의 데이터 타입을 표시하지 않은 포인터. 일반적인 포인터와 달리 연산이나 메모리 참조가 불가능하다. 주소 저장만 가능하다.
    • 함수 포인터 함수는 실행시 메인 메모리에 올라간다. 함수의 이름은 메모리에 올라간 함수의 시작 주소를 가리키는 포인터 상수가 된다. 함수의 시작 주소를 가리키는 포인터 상수를 함수 포인터라고 한다. 💡 **포인터 상수:** 포인터가 가리키는 주소 값을 변경할 수 없는 포인터
    • Null 포인터 아무것도 가리키지 않는 포인터
  • 포인터와 배열 배열의 이름은 값을 변경할 수 없는 상수라는 점을 제외하면 포인터와 같다. ( 배열의 이름은 포인터 상수이다. )
    int arr[3] = { 10, 20, 30 };
    int * ptr_arr = arr;
    
    printf(sizeof(arr)); // 12
    printf(sizeof(ptr_arr)); // 8
    포인터에 배열의 이름을 대입해서 포인터를 배열처럼 사용할 수 있다. 하지만 배열의 크기를 계산할 때 차이가 있다. 배열에서 메모리 크기를 계산할 때는 int형 3개로 12바이트를 차지하지만 포인터로 크기를 계산하면 포인터 변수 자체의 크기가 출력된다.
  • 배열의 포인터 연산 배열의 이름을 포인터처럼 사용할 수 있다.
    int arr[3] = { 10, 20, 30 }
    
    print('%d, %d, %d', arr[0], arr[1], arr[2])
    print('%d, %d, %d', *(arr + 0), *(arr + 1), *(arr + 2))
    
    // arr[i] == *(arr + i)
  • 포인터 배열 포인터를 배열의 요소로 가지는 배열을 포인터 배열이라고 한다.
    int i, arr_len;
    int num1 = 10, num2 = 20, num3 = 30;
    int* arr[3] = { &num1, &num2, &num3 }

메모리 동적 할당(Dynamic Allocation)

데이터와 스택에 할당되는 메모리의 크기는 컴파일 타임에 결정된다.

하지만 힙 영역에서의 메모리 크기는 런타임과정에서 사용자가 직접 결정한다.

이렇게 런타임동안 메모리를 할당받는 것을 메모리의 동적할당이라고 한다.

  • malloc()

    #include <stdlib.h>
    void *malloc(size_t size);

    할당할 메모리의 크기를 바이트 단위로 받는다. malloc()함수는 전달받은 메모리에 크기에 맞고, 아직 할당되지 않은 블록을 찾는다. 찾은 블록의 첫번째 주소값을 반환한다. 만약 블록이 없다면 null 포인터를 반환한다.

  • free()

    힙 영역에 할당받은 메모리 공간을 다시 운영체제에 돌려주는 함수. 데이터와 스택 영역에 할당되는 메모리, 즉 컴파일 타임에 할당되는 데이터의 영역은 실행내내 고정되지만, 런타임 동안 힙 영역에 생성되는 메모리의 크기는 변환가능하다. 쓰지 않는 메모리 영역은 free()를 통해 할당을 해제하여 메모리가 부족해지는 현상을 방지할 수 있다. 이렇게 해지 하지 않은 힙 영역의 메모리 공간으로 인해 메모리가 부족해지는 현상을 메모리 누수(Memory Leak)이라고 한다.

    #include <stdlib.h>
    void free(void *ptr);

    free()는 해지하려는 메모리 공간을 가리키는 포인터를 변수로 받아 해당 메모리 영역을 해제한다.

  • calloc()

    malloc()과 동일하게 힙 영역에 동적으로 메모리를 할당해주는 함수. malloc과 달리 인자 두개를 받는다. 그리고 메모리를 할당 받은 후 해당 메모리의 비트값을 모두 0으로 초기화한다. 그리고 마찬가지로 사용이 끝난 메모리 공간에 대해서는 free()를 통해 할당 해제해주어야한다.

    #include <stdlib.h>
    void *calloc(size_t nmemb, size_t size);

    calloc의 첫번째 인자로는 메모리의 블록 개수를 받고, 두번째 인자로는 각 블록의 바이트 수를 받는다.

    힙영역에 size 크기의 메모리 블록을 nmemb개 받을 수 있도록 요청한다.

  • realloc()

    이미 할당된 메모리의 크기를 바꾸어 재할당할 때 사용하는 함수

    #inlcude <stlib.h>
    void *realloc(void *ptc, size_t size);

    메모리의 크기를 바꾸려는 공간을 가리키는 포인터를 첫번째 인자로 받고, 재할당할 메모리의 크기를 두번째 인자로 받는다.


call by reference / call by value

  • call by value

    변수가 가지고 있는 을 매개변수에 복사하는 방법.

    이때 전달된 값은 전달된 변수와 완전 별개의 값이 된다. 변수 내부에서 값을 조작해도 외부 변수에 영향을 주지 않는다.

  • call by reference

    값을 저장하고 있는 변수의 주소값을 전달한다.

    함수 내부에서 값을 사용할 때 변수가 저장하고 있는 값의 주소에 직접 접근하여 사용하기 때문에 조작이 가능하고, 외부 변수도 영향을 받는다.


git clone — bare / —mirror

  • -bare

    Make a bare Git repository. That is, instead of creating <directory> and placing the administrative files in <directory>/.git, make the <directory> itself the $GIT_DIR. This obviously implies the --no-checkout because there is nowhere to check out the working tree. Also the branch heads at the remote are copied directly to corresponding local branch heads, without mapping them to refs/remotes/origin/. When this option is used, neither remote-tracking branches nor the related configuration variables are created.

    → bare 키워드를 사용하면 작업디렉토리를 만들어주지 않고, 레포지토리 이름과 같은 ‘.git’파일만 만들어진다. 해당 파일은 작업 디렉토리가 없기 때문에 작업이 불가능하고, 현재까지 진행된 작업에 대한 결과물만 들어있는 버전 파일이다. 백업용이나 코드자체, 파일 자체를 공유하기 위한 방식으로 사용한다.


  • -mirror

    Set up a mirror of the source repository. This implies --bare. Compared to --bare--mirror not only maps local branches of the source to local branches of the target, it maps all refs (including remote-tracking branches, notes etc.) and sets up a refspec configuration such that all these refs are overwritten by a git remote update in the target repository.

    → mirror는 특정 작업 브랜치에 대한 로컬 디렉토리를 만드는 Non-bare방식과는 다르게 레포지토리의 모든 브랜치, 커밋을 복사하는 것을 말한다. —bare와 동일하게 중앙 집중식 저장소로 활용하거나 백업을 위해 사용한다고 한다.

profile
나도 잘하고 싶다..!

0개의 댓글