c 공부 2024.04.22
함수의데이터공유방법
main함수의 a와 add_ten의 a는 서로 다른 각자 안의 지역변수이다.
void는 반환이 없다.
main에서 printf에 출력되는 a는 main 의 지역변수인 a 가 출력되는것.
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;
}
왜 굳이 포인터로 값이 바뀌게 만들어놨을까 복사해서 하면되는데?
답 : 우리가 컨트롤 하겠다는것… (내가 운영자로서)
메모리 효율성: 포인터를 사용하면 데이터의 복사본을 생성하지 않고 직접 원본 데이터를 조작할 수 있다. 큰 크기의 데이터 구조를 다룰 때, 데이터를 복사하면 추가적인 메모리가 필요하고, 이는 메모리 사용량을 증가시키며 성능 저하를 일으킬 수 있다.
- 실행 시간 효율성: 데이터를 직접 변경하는 것은 데이터 복사에 드는 시간을 절약할 수 있다. 함수에 데이터의 주소만 전달하면 되기 때문에, 처리 시간이 단축된다.
- 함수 간의 데이터 공유: 포인터를 사용하면 여러 함수가 동일한 데이터 인스턴스에 액세스하고 수정할 수 있다. 함수 간에 데이터를 쉽게 공유하고, 상태를 유지할 수 있게 한다.
- 유연성: 포인터는 배열, 구조체, 함수 포인터 등 다양한 데이터 타입과 함께 사용될 수 있으며, 다양한 프로그래밍 패턴과 기술에서 중요한 역할을 한다.
실무에서는 변수가 아니라 포인터로 거의 선언 되어있다고 한다.
보안에 포인터 사용시 보안에 취약할 수 있다. (메모리 역추적하면 비번 어디에 있는지 알 수도 있음..) 그래서 c가 죽어가고 있음
- 첫 번째 문제는 세 가지 함수의 목적을 설명하는 것.
void swap(int *pa, int *pb);
: 두 정수를 가리키는 포인터를 인자로 받아 해당 포인터가 가리키는 값을 서로 바꾸는 함수.double avg(int a, int b);
: 두 정수 값을 인자로 받아 이들의 평균값을double
형으로 반환하는 함수.char *get_str(void);
: 인자를 받지 않고, 문자열을 가리키는 포인터(즉, C 스타일의 문자열)를 반환하는 함수.
두 번째 문제는 함수
int *get_num(void)
내에서 발생할 수 있는 문제점을 찾아 설명하는 것. 이 함수는 사용자로부터 정수를 입력받아 해당 정수를 가리키는 포인터를 반환. 그러나 함수가 반환하는 포인터는 지역 변수n
을 가리키고 있으므로, 함수가 종료되면n
의 생명주기가 끝나고 메모리에서 해제되므로, 반환된 포인터는 더 이상 유효하지 않은 메모리 영역을 가리키게 된다. 이는 위험하며, 접근할 경우 정의되지 않은 행동(undefined behavior)을 초래할 수 있다.- 세 번째 문제는
void add_by_pointer(int *pa, int *pb, int *pr)
함수를 작성하는 것입니다. 이 함수는 두 정수a
와b
를 가리키는 포인터를 인자로 받고,a
와b
의 합을 세 번째 인자로 받은 포인터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]);
배열의 크기 계산 방법
sizeof(animal)
:- 이 부분은
animal
이라는 전체 배열의 크기를 알려준다. 여기서 크기란 배열이 메모리에서 차지하는 공간의 양을 말한다. 예를 들어,animal
배열에 동물 이름을 5개 저장할 수 있는데, 각 이름은 최대 20글자까지 가능. - 즉, 각 글자는 조금의 공간을 차지하고, 모든 글자들의 공간을 합치면
animal
배열 전체가 차지하는 공간이 된다.
- 이 부분은
sizeof(animal[0])
:animal[0]
는animal
배열의 첫 번째 요소를 의미. 여기서 요소는 동물 이름 하나를 저장할 수 있는 공간.animal[0]
는 최대 20글자를 저장할 수 있으므로, 이 부분의 크기는 한 동물 이름을 저장할 수 있는 메모리 공간의 크기이다.
- 나눗셈 (
/
):- 전체 배열의 크기를 첫 번째 요소의 크기로 나누면, 배열에 몇 개의 요소(여기서는 동물 이름)가 저장될 수 있는지 알 수 있다.
- 이것은 큰 상자의 크기를 작은 상자 하나의 크기로 나누어, 큰 상자에 작은 상자가 몇 개 들어갈 수 있는지 계산하는 것과 비슷.
예시
예를 들어, 내가 사탕 상자를 가지고 있고, 그 상자 전체가 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 이 나옴 배열의 행의 첫공간 수
static
static
키워드를 함수 내의 지역변수에 사용하면 그 변수의 동작이 몇 가지 중요한 방식으로 변경된다. static
지역변수는 일반 지역변수와는 다르게 다음과 같은 특징을 가짐:
- 수명(lifetime):
static
변수는 프로그램 실행이 시작될 때 메모리에 할당되고, 프로그램 실행이 종료될 때까지 메모리에 남아 있다. 이는 함수 호출이 끝나더라도 변수가 사라지지 않음을 의미. - 초기화:
static
지역변수는 프로그램 실행 시 한 번만 초기화되며, 이후에는 초기화 코드가 실행되지 않는다. 예를 들어static int i = 0;
이라고 선언했다면,i
는 프로그램 시작 시 0으로 초기화되고, 함수가 여러 번 호출되더라도 다시 0으로 초기화되지 않는다. - 접근 범위(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 변수를 사용할 때의 장점과 단점은 무엇?
장점:
- 상태 유지:
static
변수는 함수 호출 간에 상태를 유지할 수 있다. 이는 특정 함수 내에서 계산된 값을 다음 호출까지 보존할 수 있도록 해준다. - 접근 제한: 함수 내에서 선언된
static
변수는 해당 함수에만 접근 가능하여, 전역 변수처럼 프로그램의 다른 부분에서 접근하는 것을 제한함으로써 코드의 안전성을 높일 수 있다. - 메모리 관리:
static
변수는 프로그램의 수명 동안 한 번만 할당되므로, 반복적인 메모리 할당 및 해제를 피할 수 있다. 이는 효율성을 증가시킬 수 있다.
단점:
- 메모리 사용:
static
변수는 프로그램의 실행이 종료될 때까지 메모리를 차지하므로, 필요하지 않은 시간에도 메모리 공간을 사용하게 된다. - 부작용:
static
변수의 값을 변경하는 함수는 사이드 이펙트(side effect)를 발생시킬 수 있어, 함수의 사용이 예측하기 어려워질 수 있다. 이는 함수의 재사용성과 테스트 용이성을 감소시킬 수 있다. - 병렬 처리 문제: 멀티스레딩 환경에서
static
변수는 동시성 문제를 일으킬 수 있다. 여러 스레드가 동일한static
변수에 접근하면 동기화 문제가 발생할 수 있다.
Q2: static 지역변수와 static 전역변수 사이에는 어떤 차이점이 있나?
static 지역변수:
- 정의 위치: 함수 내부에서 선언됩니다.
- 접근 범위: 선언된 함수 내에서만 접근 가능하다.
- 생명주기: 프로그램 실행 시에 초기화되며, 프로그램이 종료될 때까지 유지된다.
static 전역변수:
- 정의 위치: 함수 외부, 보통 파일의 상단에 선언됨.
- 접근 범위: 동일한 소스 파일 내에서만 접근 가능. 다른 파일에서는 접근할 수 없다(내부 링크(internal linkage)를 가짐).
- 생명주기: 프로그램 실행 시에 초기화되며, 프로그램이 종료될 때까지 유지.
기본적으로, 두 유형의 static
변수는 생명주기와 메모리 할당 시점이 같지만, 접근 범위에서 차이가 있다. static
전역변수는 한 파일 내에서만 사용할 수 있어 파일 간의 데이터 공유를 제한하여, 코드의 모듈성을 향상시킬 수 있다.