Post

c 공부 2024.04.22

함수의데이터공유방법

2024-04-22-093710

  • main함수의 a와 add_ten의 a는 서로 다른 각자 안의 지역변수이다.

  • void는 반환이 없다.

  • main에서 printf에 출력되는 a는 main 의 지역변수인 a 가 출력되는것.

2024-04-22-094026

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>  // 표준 입출력 함수를 사용하기 위한 헤더 파일 포함

// 함수 선언: 정수 포인터를 매개변수로 받는 add_ten 함수
void add_ten(int *pa);

int main()  // 메인 함수 시작
{
    int a = 10;  // 정수형 변수 a를 선언하고 10으로 초기화
    add_ten(&a);  // add_ten 함수를 호출하면서 a의 주소를 인자로 전달
    printf("a : %d\n", a);  // a의 값을 출력. add_ten 함수에서 10이 더해졌으므로 20이 출력됨

    return 0;  // 메인 함수의 종료를 알리고 0을 반환
}

// add_ten 함수 정의
void add_ten(int *pa) 
{
    *pa = *pa + 10;  // 포인터 pa를 통해 참조된 값에 10을 더하고 결과를 다시 그 위치에 저장
}
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
#include <stdio.h>

// 전역 변수 선언
int a, b;

// 두 정수의 주소를 인자로 받아 값을 입력하는 함수
void input_data(int *pa, int *pb) {
    printf("두 정수를 입력하세요: ");
    scanf("%d %d", pa, pb);
}

// 전역 변수 a와 b의 값을 바꾸는 함수
void swap_data(void) {
    int temp = a;
    a = b;
    b = temp;
}

// 두 정수의 값을 인자로 받아 출력하는 함수
void print_data(int a, int b) {
    printf("두 정수 출력: %d, %d\n", a, b);
}

int main(void) {
    input_data(&a, &b);  // 사용자로부터 두 정수 입력 받음
    swap_data();         // 두 변수의 값을 바꿈
    print_data(a, b);    // 바뀐 값을 출력함

    return 0;
}

2024-04-22-094846

왜 굳이 포인터로 값이 바뀌게 만들어놨을까 복사해서 하면되는데?

답 : 우리가 컨트롤 하겠다는것… (내가 운영자로서)

  1. 메모리 효율성: 포인터를 사용하면 데이터의 복사본을 생성하지 않고 직접 원본 데이터를 조작할 수 있다. 큰 크기의 데이터 구조를 다룰 때, 데이터를 복사하면 추가적인 메모리가 필요하고, 이는 메모리 사용량을 증가시키며 성능 저하를 일으킬 수 있다.

  2. 실행 시간 효율성: 데이터를 직접 변경하는 것은 데이터 복사에 드는 시간을 절약할 수 있다. 함수에 데이터의 주소만 전달하면 되기 때문에, 처리 시간이 단축된다.
  3. 함수 간의 데이터 공유: 포인터를 사용하면 여러 함수가 동일한 데이터 인스턴스에 액세스하고 수정할 수 있다. 함수 간에 데이터를 쉽게 공유하고, 상태를 유지할 수 있게 한다.
  4. 유연성: 포인터는 배열, 구조체, 함수 포인터 등 다양한 데이터 타입과 함께 사용될 수 있으며, 다양한 프로그래밍 패턴과 기술에서 중요한 역할을 한다.
  • 실무에서는 변수가 아니라 포인터로 거의 선언 되어있다고 한다.

  • 보안에 포인터 사용시 보안에 취약할 수 있다. (메모리 역추적하면 비번 어디에 있는지 알 수도 있음..) 그래서 c가 죽어가고 있음

2024-04-22-101719
2024-04-22-102447

  1. 첫 번째 문제는 세 가지 함수의 목적을 설명하는 것.
    • void swap(int *pa, int *pb);: 두 정수를 가리키는 포인터를 인자로 받아 해당 포인터가 가리키는 값을 서로 바꾸는 함수.
    • double avg(int a, int b);: 두 정수 값을 인자로 받아 이들의 평균값을 double형으로 반환하는 함수.
    • char *get_str(void);: 인자를 받지 않고, 문자열을 가리키는 포인터(즉, C 스타일의 문자열)를 반환하는 함수.
  2. 두 번째 문제는 함수 int *get_num(void) 내에서 발생할 수 있는 문제점을 찾아 설명하는 것. 이 함수는 사용자로부터 정수를 입력받아 해당 정수를 가리키는 포인터를 반환. 그러나 함수가 반환하는 포인터는 지역 변수 n을 가리키고 있으므로, 함수가 종료되면 n의 생명주기가 끝나고 메모리에서 해제되므로, 반환된 포인터는 더 이상 유효하지 않은 메모리 영역을 가리키게 된다. 이는 위험하며, 접근할 경우 정의되지 않은 행동(undefined behavior)을 초래할 수 있다.

  3. 세 번째 문제는 void add_by_pointer(int *pa, int *pb, int *pr) 함수를 작성하는 것입니다. 이 함수는 두 정수 ab를 가리키는 포인터를 인자로 받고, ab의 합을 세 번째 인자로 받은 포인터 pr을 통해 반환해야 한다.

예시 :

1
2
3
4
5
6
7
8
9
10
void add_by_pointer(int *pa, int *pb, int *pr) {
    *pr = *pa + *pb;
}

int main (void) {
	int a = 10, b = 20, res = 0;
	add_by_pointer(&a, &b, &res);
	printf("%d",res);
	return 0;
}

두 번째 문제에서 int *get_num(void) 함수는 사용자로부터 입력받은 정수 값을 가리키는 포인터를 반환한다. 그러나 이 포인터는 지역 변수 n을 가리키고 있으며, 함수가 종료되면 n은 스택에서 사라지기 때문에, 반환된 포인터는 더는 유효한 메모리 주소를 가리키지 않게 된다. 이렇게 반환된 포인터를 통해 접근하려고 하면, 이는 정의되지 않은 행동(undefined behavior)을 초래하고, 프로그램이 예기치 않게 동작할 수 있다.

함수에서 지역 변수의 주소를 반환하는 것은 안전하지 않으며, 이로 인해 발생할 수 있는 문제들은 다음과 같다:

  • 반환된 포인터를 통해 접근 시, 그 메모리 영역에는 다른 데이터가 저장되어 있을 수 있어서 예상치 못한 값이 나올 수 있다.
  • 그 메모리가 프로그램의 다른 부분에 의해 재사용되고 있을 경우, 메모리 무결성이 깨질 수 있고, 이는 데이터 손상이나 보안 취약점으로 이어질 수 있다.
  • 일부 시스템에서는 해당 메모리 영역에 접근하는 것 자체가 프로그램을 충돌시킬 수도 있다.

이러한 이유로, 동적 메모리 할당을 사용하거나, 전역 변수 또는 정적 변수의 주소를 반환하는 방식을 사용해야 한다. 동적 메모리 할당은 malloc 등의 함수를 사용해 힙 영역에 메모리를 할당하고, 해당 메모리의 사용이 끝난 후에는 반드시 free 함수를 사용해 할당된 메모리를 해제해야 한다.

평균 점수 출력

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
#include <stdio.h> // 표준 입력 및 출력 함수를 사용하기 위한 헤더 파일 포함

int main() // 메인 함수 시작
{
    int score[3][4]; // 3명의 학생에 대한 4과목 점수를 저장할 2차원 배열 선언
    int total; // 학생별 총점을 저장할 변수 선언
    double avg; // 학생별 평균을 저장할 변수 선언
    int i, j; // 반복문 제어를 위한 변수 선언

    // 각 학생의 점수 입력받기
    for (i = 0; i < 3; i++) { // 총 3명의 학생에 대해 반복
        printf("4과목의 점수 입력: "); // 사용자에게 입력 안내 메시지 출력
        for (j = 0; j < 4; j++) { // 각 학생에 대해 4과목의 점수 입력 받기
            scanf("%d", &score[i][j]); // 표준 입력을 통해 점수를 받아 score 배열에 저장
        }
    }
    
    // 입력받은 점수로 총점과 평균 계산 후 출력
    for (i = 0; i < 3; i++) { // 총 3명의 학생에 대해 반복
        total = 0; // 학생의 총점을 계산하기 전에 0으로 초기화
        for (j = 0; j < 4; j++) { // 해당 학생의 4과목 점수를 합산
            total += score[i][j]; // 각 과목 점수를 total에 더함
        }
        avg = total / 4.0; // 총점을 과목 수로 나누어 평균을 계산
        printf("총점 : %d, 평균 : %.2lf\n", total, avg); // 계산된 총점과 평균을 출력
    }
    return 0; // 프로그램 정상 종료
}

2차원 배열

배열을 선언할 때 [3][4]는 행이 3개이고 열이 4개인 2차원 배열을 의미. 각 행은 4개의 요소를 가지고 있다.

1
2
3
4
5
int a[3][4] = {
    {1, 2, 3, 4},
    {5, 6, 7, 8},
    {9, 10, 11, 12}
};

위 코드는 a라는 이름의 2차원 배열을 선언하고 초기화. 이 배열은 3개의 행을 가지며, 각 행은 4개의 열로 구성.

반면, int a[3]를 선언하면 이는 1차원 배열을 선언하는 것이며, 이 배열은 3개의 요소를 가진다. 예를 들어:

1
int b[3] = {1, 2, 3};

위 코드는 b라는 이름의 1차원 배열을 선언하고 초기화. 이 배열은 3개의 요소를 가지고 있다.

따라서, [3][4]는 2차원 배열(행렬)을 선언하는 것이며, [3]는 단순히 3개의 요소를 가진 1차원 배열을 선언하는 것이다.

  • 값이 안채워진곳은 선언된 크기만큼 0이 들어감
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

int main()
{
    char animal[5][20]; // 5개의 동물 이름을 저장할 수 있는 2차원 문자 배열
    int i; // 반복문 제어 변수
    int count; // 배열의 길이를 저장할 변수
    
    count = sizeof(animal) / sizeof(animal[0]); // 배열의 길이 계산
    for (i = 0; i < count; i++) {
        scanf("%19s", animal[i]); // 사용자로부터 문자열 입력 받기, 최대 19자
    }
    
    for (i = 0; i < count; i++) {
        printf("%s ", animal[i]); // 각 동물 이름 출력, 이름 사이에 공백 추가
    }
    printf("\n"); // 모든 출력이 끝난 후 줄바꿈 추가

    return 0; // 프로그램 종료
}

행의 수 계산 이해

count = sizeof(animal) / sizeof(animal[0]);

배열의 크기 계산 방법

  1. sizeof(animal):
    • 이 부분은 animal이라는 전체 배열의 크기를 알려준다. 여기서 크기란 배열이 메모리에서 차지하는 공간의 양을 말한다. 예를 들어, animal 배열에 동물 이름을 5개 저장할 수 있는데, 각 이름은 최대 20글자까지 가능.
    • 즉, 각 글자는 조금의 공간을 차지하고, 모든 글자들의 공간을 합치면 animal 배열 전체가 차지하는 공간이 된다.
  2. sizeof(animal[0]):
    • animal[0]animal 배열의 첫 번째 요소를 의미. 여기서 요소는 동물 이름 하나를 저장할 수 있는 공간.
    • animal[0]는 최대 20글자를 저장할 수 있으므로, 이 부분의 크기는 한 동물 이름을 저장할 수 있는 메모리 공간의 크기이다.
  3. 나눗셈 (/):
    • 전체 배열의 크기를 첫 번째 요소의 크기로 나누면, 배열에 몇 개의 요소(여기서는 동물 이름)가 저장될 수 있는지 알 수 있다.
    • 이것은 큰 상자의 크기를 작은 상자 하나의 크기로 나누어, 큰 상자에 작은 상자가 몇 개 들어갈 수 있는지 계산하는 것과 비슷.

예시

예를 들어, 내가 사탕 상자를 가지고 있고, 그 상자 전체가 100개의 사탕을 담을 수 있는 크기라고 해보면, 하나의 작은 칸막이(사탕 한 개를 담는 공간)가 10개의 사탕을 담을 수 있다면, 상자 전체에는 100 / 10 = 10개의 칸막이가 들어갈 수 있음.

이렇게 count = sizeof(animal) / sizeof(animal[0]); 코드는 animal 배열 전체에 몇 개의 동물 이름을 저장할 수 있는지 계산하는 데 사용된다.

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
 #include <stdio.h>

int main()
{
    char animal[5][20]; // 5개의 동물 이름을 저장할 수 있는 2차원 문자 배열
    int i; // 반복문 제어 변수
    int count; // 배열의 길이를 저장할 변수
    int a=0, b=0;
    
   /* a = sizeof(animal);
    printf("a = %d", a);
    
    b = sizeof(animal[0]);
    printf("b = %d", b);*/
    
    
    count = sizeof(animal) / sizeof(animal[0]); // 배열의 길이 계산
    for (i = 0; i < count; i++) {
        scanf("%19s", animal[i]); // 사용자로부터 문자열 입력 받기, 최대 19자
    }
    
    for (i = 0; i < count; i++) {
        printf("%s ", animal[i]); // 각 동물 이름 출력, 이름 사이에 공백 추가
    }
   // printf("\n"); // 모든 출력이 끝난 후 줄바꿈 추가

    return 0; // 프로그램 종료
}
  • a 는 100 이 나오고 b는 20 이 나옴 배열의 행의 첫공간 수

alt text

alt text

static

static 키워드를 함수 내의 지역변수에 사용하면 그 변수의 동작이 몇 가지 중요한 방식으로 변경된다. static 지역변수는 일반 지역변수와는 다르게 다음과 같은 특징을 가짐:

  1. 수명(lifetime): static 변수는 프로그램 실행이 시작될 때 메모리에 할당되고, 프로그램 실행이 종료될 때까지 메모리에 남아 있다. 이는 함수 호출이 끝나더라도 변수가 사라지지 않음을 의미.
  2. 초기화: static 지역변수는 프로그램 실행 시 한 번만 초기화되며, 이후에는 초기화 코드가 실행되지 않는다. 예를 들어 static int i = 0; 이라고 선언했다면, i는 프로그램 시작 시 0으로 초기화되고, 함수가 여러 번 호출되더라도 다시 0으로 초기화되지 않는다.
  3. 접근 범위(scope): static 변수의 접근 범위는 여전히 그 변수가 선언된 함수 내로 제한됨. 다른 함수에서는 직접적으로 접근할 수 없다.

예를 들어, 다음과 같은 코드를 생각해 볼 수 있음:

1
2
3
4
5
6
7
8
9
10
11
12
void function() {
    static int count = 0;  // static 변수 초기화
    count++;
    printf("이 함수는 %d번 호출되었습니다.\n", count);
}

int main() {
    function();
    function();
    function();
    return 0;
}

Q1: static 변수를 사용할 때의 장점과 단점은 무엇?

장점:

  1. 상태 유지: static 변수는 함수 호출 간에 상태를 유지할 수 있다. 이는 특정 함수 내에서 계산된 값을 다음 호출까지 보존할 수 있도록 해준다.
  2. 접근 제한: 함수 내에서 선언된 static 변수는 해당 함수에만 접근 가능하여, 전역 변수처럼 프로그램의 다른 부분에서 접근하는 것을 제한함으로써 코드의 안전성을 높일 수 있다.
  3. 메모리 관리: static 변수는 프로그램의 수명 동안 한 번만 할당되므로, 반복적인 메모리 할당 및 해제를 피할 수 있다. 이는 효율성을 증가시킬 수 있다.

단점:

  1. 메모리 사용: static 변수는 프로그램의 실행이 종료될 때까지 메모리를 차지하므로, 필요하지 않은 시간에도 메모리 공간을 사용하게 된다.
  2. 부작용: static 변수의 값을 변경하는 함수는 사이드 이펙트(side effect)를 발생시킬 수 있어, 함수의 사용이 예측하기 어려워질 수 있다. 이는 함수의 재사용성과 테스트 용이성을 감소시킬 수 있다.
  3. 병렬 처리 문제: 멀티스레딩 환경에서 static 변수는 동시성 문제를 일으킬 수 있다. 여러 스레드가 동일한 static 변수에 접근하면 동기화 문제가 발생할 수 있다.

Q2: static 지역변수와 static 전역변수 사이에는 어떤 차이점이 있나?

static 지역변수:

  • 정의 위치: 함수 내부에서 선언됩니다.
  • 접근 범위: 선언된 함수 내에서만 접근 가능하다.
  • 생명주기: 프로그램 실행 시에 초기화되며, 프로그램이 종료될 때까지 유지된다.

static 전역변수:

  • 정의 위치: 함수 외부, 보통 파일의 상단에 선언됨.
  • 접근 범위: 동일한 소스 파일 내에서만 접근 가능. 다른 파일에서는 접근할 수 없다(내부 링크(internal linkage)를 가짐).
  • 생명주기: 프로그램 실행 시에 초기화되며, 프로그램이 종료될 때까지 유지.

기본적으로, 두 유형의 static 변수는 생명주기와 메모리 할당 시점이 같지만, 접근 범위에서 차이가 있다. static 전역변수는 한 파일 내에서만 사용할 수 있어 파일 간의 데이터 공유를 제한하여, 코드의 모듈성을 향상시킬 수 있다.

노코드 빌더

alt text

This post is licensed under CC BY 4.0 by the author.