혼자 공부하는 C언어 (4)

Erdos·2024년 10월 5일
0

감상

목록 보기
41/43
post-thumbnail

chapter13 변수의 영역과 데이터 공유


13-1 변수 사용 영역

지역 변수(local variable)

  • 범위가 함수 내, 즉 일정 지역에서만 사용하는 변수
  • 함수의 매개변수는 지역변수다.
  • 특징
    • 지역 변수는 사용 범위가 블록 내부로 제한되므로 다른 함수에서 사용할 수 없다.
    • 지역 변수는 이름이 같아도 선언된 함수가 다르면 각각 독립된 저장 공간을 갖는다.
  • 장점
    • 메모리를 효율적으로 사용합니다. 선언된 함수가 반환되면 할당된 저장 공간을 자동으로 회수해 재활용
    • 디버깅에 유리합니다. 문제가 있을 때 수정하기 쉽다.

😺 주의!
지역 변수가 할당된 저장 공간은 자동으로 초기화되지 않으므로 쓰레기 값이 사용되지 않도록 조심하기.

전역 변수(global variable)

  • 함수 밖에 변수를 선언
  • 사용범위가 함수나 블록으로 제한되지 않는다
  • 전역 변수의 문제점
    • 전역 변수의 이름을 바꾸면 그 변수를 사용하는 모든 함수를 찾아 수정해야 한다.
    • 전역 변수의 값이 잘못된 경우 접근 가능한 모든 함수를 의심해야 한다.
    • 코드 블록 내에 같은 이름의 지역 변수를 선언하면 거기서는 전역 변수를 사용할 수 없다.

결론

사용 범위가 명확하고 통제 가능한 지역 변수를 우선적으로 사용하고, 많은 함수에서 수시로 데이터를 공유하는 경우에만 제한적으로 전역 변수를 사용하는 것이 좋다.

정적 지역 변수(static variable)

  • 선언된 함수가 반환되더라도 그 저장 공간을 계속 유지
  • 함수에 필요한 값을 보관해 호출할 때마다 계속 사용
  • 예약어 static

어라 이친구는 왜 있는거지...?
아무튼 문자열 분리 코드! ⬇️

#include <stdio.h>

int main(void) {
    char str[80];
    printf("문자열을 입력하세요: ");
    fgets(str, sizeof(str), stdin);

    char *sp, *tp;
    sp = str;

    while (*sp != '\0') {
        tp = sp;

        while ((*tp != '\0') && (*tp != ' ')) 
            tp++;
        if (*tp != '\0') { 
            *tp = '\0'; 
            tp++;
        }
        if (*sp != '\0') { 
            printf("%s\n", sp);
        }
        sp = tp; 
    }
    return 0;
}

레지스터 변수(register variable)

  • 저장 공간이 할당되는 위치에 있음(CPU 안에)
  • 데이터 처리 속도가 가장 빠른 공간이다.
  • 예약어 register
  • 현재 최적화를 자동으로 처리할 수 있는 경우가 많아서 실제로 많이 사용 x
  • 주의할 점
    • 전역 변수는 레지스터 변수로 선언할 수 없다
    • 레지스터 변수는 주소를 구할 수 없다(주소연산자 x)
    • 레지스터 사용 여부는 컴파일러가 결정한다. 레지스터 변수를 선언했다고 모두 생성되는 것은 아니다. 컴파일러가 어디에 할당하는 것이 더 이득인지 판단해서 적당한 저장 공간을 선택한다.

13-2 함수의 데이터 공유 방법

값을 복사해서 전달하는 방법(call by value)

  • 가장 일반적인 방법
  • 원본 데이터를 보존할 수 있다. 안정성을 담보해야 하는 상황에 유용
  • 단, 원본의 데이터 수정이 목적일 때는 사용법이 제한된다.
#include <stdio.h>
void add_ten(int a);

int main(void) 
{
    int a = 10;
    
    add_ten(a);
    printf("a: %d\n", a);
    return 0;
}

void add_ten(int a)
{
    a = a + 10;
}

주소를 전달하는 방법(call by reference)

  • 포인터 연산으로 값 처리, 값을 전달하는 방법보다 불편.
  • 하지만, 호출된 함수에서 함수에 있는 변수의 값을 바꿀 수 있다.
#include <stdio.h>
void add_ten(int *ptr);

int main(void) 
{
    int a = 10;
    
    add_ten(&a);
    printf("a: %d\n", a);
    return 0;
}

void add_ten(int *ptr)
{
    *ptr = *ptr + 10;
}

주소를 반환하는 함수

#include <stdio.h>

void *sum(int a, int b);

int main(void) 
{
    int *res_ptr;
    res_ptr = sum(10,20);
    printf("두 정수의 합: %d\n", *res_ptr);
    return 0;
}

void *sum(int a, int b)
{
    static int res;
    
    res = a + b;
    
    return (&res);
}

  • 주소를 반환하는 함수를 만들 때 주의할 점
    • 반환값의 자료형은 반환값을 저장할 포인터의 자료형과 같아야 한다.
    • 지역 변수의 주소를 반환해서는 안된다.(정적 지역 변수나 전역 변수, 동적 할당한 메모리 주소 가능)

chapter14 다차원 배열과 포인터 배열


14-1 다차원 배열 🎆

제대로 활용한 적이 없어서 지금은 간단하게 정리

  • 다차원 배열을 쓰면 데이터를 논리적으로 분류해 이해하기 쉬운 코드를 만들 수 있다. -> 예시 찾기
  • 특히, 2차원 char배열은 여러 개의 문자열을 다루는 데 효과적

2차원 배열 초기화

14-2 포인터 배열

포인터 배열 선언과 사용

#include <stdio.h>

void *sum(int a, int b);

int main(void) 
{
    char *ptr_arr[5];
    int i;
    
    ptr_arr[0] = "dog";
    ptr_arr[1] = "elephant";
    ptr_arr[2] = "horse";
    ptr_arr[3] = "tiger";
    ptr_arr[4] = "lion";
    
    for (i = 0; i < 5; i++)
        printf("%s\n", ptr_arr[i]);
    return 0;
}

2차원 배열처럼 활용하는 포인터 배열

#include <stdio.h>

int main(void) 
{
    int ary1[4] = {1,2,3,4};
    int ary2[4] = {11,12,13,14};
    int ary3[4] = {21,22,23,24};
    int *pary[3] = {ary1, ary2, ary3};
    int i,j;
    
    for (i = 0; i < 3; i++)
    {
        for (j = 0; j < 4; j++)
            printf("%5d", pary[i][j]);
        printf("\n");
    }
}

chapter15 응용포인터


15-1 이중 포인터와 배열 포인터

이중 포인터 활용 1: 포인터 값을 바꾸는 함수의 매개변수

#include <stdio.h>

void swap_ptr(char **ppa, char **ppb);
int main(void) 
{
    char *pa = "success";
    char *pb = "failure";
    
    printf("pa -> %s, pb -> %s\n", pa, pb);
    swap_ptr(&pa, &pb);
    printf("pa -> %s, pb -> %s\n", pa, pb);
    
    return 0;
}

void swap_ptr(char **ppa, char **ppb)
{
    char *pt;
    pt = *ppa;
    *ppa = *ppb;
    *ppb = pt;
}


그림 하나 참고 ㅎㅎ

이중 포인터 활용2: 포인터 배열을 매개변수로 받는 함수

#include <stdio.h>

void print_str(char **pps, int cnt);
int main(void) 
{
    char *ptr_ary[] = {"eagle", "tiger", "lion", "squirrel"};
    int count;
    
    count = sizeof(ptr_ary) / sizeof(ptr_ary[0]);
    print_str(ptr_ary, count);
    return 0;
}

void print_str(char **pps, int cnt)
{
    int i;
    
    for (i = 0; i < cnt; i++)
        printf("%s\n", pps[i]);
}

15-2 함수 포인터와 void 포인터

함수 포인터의 개념

  • 함수 포인터는 언제 사용할까? (아직은 익숙하지 않아서 더 봐야 할 듯...🥴)
  1. 콜백 함수를 구현할 때 자주 사용.

    • 콜백 함수: 하나의 함수가 다른 함수를 인자로 전달받아 특정 시점에 그 함수를 호출하는 함수
  2. 함수 배열: 다양한 함수를 저장

    #include <stdio.h>
    
    void add(int a, int b) { printf("Sum: %d\n", a + b); }
    void subtract(int a, int b) { printf("Difference: %d\n", a - b); }
    
    int main() {
        void (*operation[2])(int, int);  // 함수 포인터 배열
        operation[0] = add;
        operation[1] = subtract;
    
        operation[0](5, 3);  // add 함수 호출
        operation[1](5, 3);  // subtract 함수 호출
    
        return 0;
    }
    
  3. 동적 함수 호출: 실행 중에 어떤 함수를 호출할지 결정해야 하는 상황

  4. 추상화와 유연성: 특정 조건에 따라 다른 동작을 수행해야 할 때...??

    #include <stdio.h>
    #include <stdlib.h>
    
    int compare_asc(const void *a, const void *b) {
        return (*(int*)a - *(int*)b);
    }
    
    int compare_desc(const void *a, const void *b) {
        return (*(int*)b - *(int*)a);
    }
    
    int main() {
        int arr[] = {3, 1, 4, 1, 5, 9};
        int n = sizeof(arr) / sizeof(arr[0]);
    
        // 오름차순 정렬
        qsort(arr, n, sizeof(int), compare_asc);
        for (int i = 0; i < n; i++) {
            printf("%d ", arr[i]);
        }
        printf("\n");
    
        // 내림차순 정렬
        qsort(arr, n, sizeof(int), compare_desc);
        for (int i = 0; i < n; i++) {
            printf("%d ", arr[i]);
        }
        printf("\n");
    
        return 0;
    }

void 포인터

#include <stdio.h>

int sum(int, int);
int main(void) 
{
    int a = 10;
    double b = 3.5;
    void *vp;
    
    vp = &a;
    printf("a : %d\n", *(int *)vp);
    
    vp = &b;
    printf("b : %.1lf\n", *(double *)vp);
    return 0;
}
  • void 포인터의 형 변환
    - 대입 연산을 할 때는 형 변환 없이 void 포인터를 다른 포인터에 대입할 수 있다.
    • 그래도 명시적으로 형 변환해서 사용하자. (컴파일러 경고 메시지)

도전 실전 예제

  • void 포인터 사용
  • 다양한 형태를 바꾸고 싶을 때
#include <stdio.h>
#include <string.h>

void swap(char *type, void *pa, void *pb);
int main(void)
{
    int a = 10, b = 20;
    double da = 1.5, db = 3.4;
    
    swap("int", &a, &b);
    printf("a: %d, b: %d\n", a, b);
    swap("double", &da, &db);
    printf("da: %.1lf, db: %.1lf\n", da, db);
    return 0;
}

void swap(char *type, void *pa, void *pb)
{
    int temp;
    double dtemp;
    
    if(strcmp(type, "int") == 0)
    {
        temp = *(int *)pa;
        *(int *)pa = *(int *)pb;
        *(int *)pb = temp;
    }
    else if(strcmp(type, "double") == 0)
    {
        dtemp = *(double *)pa;
        *(double *)pa = *(double *)pb;
        *(double *)pb = dtemp;
    }
}

chapter16 메모리 동적 할당


16-1 동적 할당 함수

malloc, free 함수

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int *pi;
    double *pd;
    
    pi = (int *)malloc(sizeof(int));
    if (pi == NULL)
    {
        printf("#메모리가 부족합니다. \n");
        exit(1);
    }
    pd = (double *)malloc(sizeof(double));
    
    *pi = 10;
    *pd = 3.4;
    
    printf("정수형으로 사용: %d\n", pi);
    printf("실수형으로 사용: %.1lf\n", *pd);
    
    free(pi);
    free(pd);
    
    return 0;
}
  • 동적 할당 시 주의할 점
    • malloc 함수의 반환값이 널 포인터인지 반드시 확인하고 사용해야 한다.
    • 사용이 끝난 저장 공간은 재활용할 수 있도록 반환해야 한다.

동적 할당 영역을 배열처럼 쓰기

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int *pi;
    int i, sum = 0;
    
    pi = (int *)malloc(5*sizeof(int));
    if (pi == NULL)
    {
        printf("메모리가 부족합니다!\n");
        exit(1);
    }
    printf("다섯 명의 나이를 입력하세요 : ");
    for (i = 0; i < 5; i++)
    {
        scanf("%d", &pi[i]);
        sum += pi[i];
    }
    printf("다섯 명의 평균 나이: %.lf\n", (sum / 5.0));
    free(pi);
    return (0);
}

기타 동적 할당 함수

  • calloc: 할당한 저장 공간을 0으로 초기화
#include <stdlib.h>
#include <string.h>  // memset 함수 사용

void *ft_calloc(size_t num, size_t size) 
{
    size_t total_size = num * size;  // 총 할당할 메모리 크기 계산
    void *ptr = malloc(total_size);  // 메모리 할당
    if (ptr == NULL)  // 할당 실패 시 NULL 반환
        return NULL;
    memset(ptr, 0, total_size);  // memset을 사용하여 메모리 0으로 초기화
    return ptr;  // 초기화된 메모리 반환
}
  • relloc: 크기 조절

메모리 누수

Valgrind 찾아보고 실행해보기

프로그램이 사용하는 메모리 영역과 그 특징

  • 메모리 저장 공간이 넉넉히 남아 있어도 메모리 할당 함수들이 널 포인터를 반환할 수 있다. 특히, 힙 영역은 메모리 사용과 반환이 불규칙하기 때문에 사용 가능한 영역이 조각나서 더 그러할 수 있음.

16-2 동적 할당 저장 공간의 활용

문자열 처리

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
    char temp[80];
    char *str[3];
    int i;
    
    for (i = 0; i<3; i++)
    {
        printf("문자열을 입력하세요 : ");
        gets(temp);
        str[i] = (char *)malloc(strlen(temp)+1);
        strcpy(str[i], temp);
    }
    for (i = 0; i < 3; i++)
        printf("%s\n", str[i]);
    for (i = 0; i < 3; i++)
        free(str[i]);
    return 0;
}

동적 할당 영역에 저장한 문자열을 함수로 처리하는 예

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void print_str(char **ps);

int main(void)
{
    char temp[80];
    char *str[21] = {0};
    int i = 0;
    
    while (i < 20)
    {
        printf("문자열을 입력하세요 : ");
        gets(temp);
        if (strcmp(temp, "end") == 0) break;
        str[i]  = (char *)malloc(strlen(temp) +1);
        strcpy(str[i], temp);
        i++;
    }
    print_str(str);
    
    for (i = 0; str[i] != NULL; i++)
    {
        free(str[i]);
    }
    return 0;
}

void print_str(char **ps)
{
    while (*ps != NULL)
    {
        printf("%s\n", *ps);
        ps++;
    }
}

  • 배열명을 포인터에 저장하면 포인터 자신의 값을 바꿀 수 있으므로 매개변수를 하나씩 증가시키면서 문자열을 출력할 수 있다.
profile
수학을 사랑하는 애독자📚 Stop dreaming. Start living. - 'The Secret Life of Walter Mitty'

0개의 댓글