프롤로그
ELITE HACKER Bootcamp 3rd 3주차 수업 공부 내용
aws 우분투 서버 하나 파서 연습
포인터, 사용이유, 장점, 사용 방법, 포인터와 배열, call by value, call by reference, 함수 포인터, 이중포인터
포인터
메모리 주소를 저장하는 변수이다. 흔히들 int형, char등을 선언하게 되면 해당 값을 저장하게 되는데, 포인터의 경우 해당 값이 저장되어있는 메모리 주소 값을 저장한다.
이를 통해서 다른 변수 또는 함수를 가리킬 수 있으며, 포인터를 통해 변수를 읽거나 쓸 수 있다.
왜 굳이 포인터를 사용하냐...라는 질문이 있을 수도 있는데... 이게 확실하진 않고 뇌피셜이긴 하다.
컴퓨터 구조에 opcode라는 것이 있다.
mode | opcode | operand 이런식으로 16진수 또는 2진수 값이 저장이 되는데, mode가 0이면 직접참조, 즉 해당 메모리의 있는 값을 그대로 사용하고, mode가 1이면 간접참조로, 해당 주소로 가리키는 곳으로 가서, 그 값이 가리키는 곳으로 또 넘어가서 값을 불러오는 연산을 진행한다. 이 메모리 값을 저장해야하기 떄문에 포인터라는 개념이 생성된게 아닐까...라는 뇌피셜이 있다.
그래서 기능에 대해서 설명을 해주면, 메모리 주소를 알고있기 때문에 해당 메모리 주소로 이동해서 바로 데이터를 수정 및 접근을 할 수 있다. 그리고, long long int 이 자료형은 8바이트를 차지하고, long double은 16바이트를 차지한다. 즉 굉장히 큰 메모리를 차지하고 있는데, 포인터는 메모리 주소를 저장하기에
64바이트 운영체제에서는 8바이트 크기, 32비트 운영체제에서는 4바이트 크기를 가진다. int, float, double 이런 거와 상관없이 무조건 8바이트이다. [근데, 리눅스 64비트 체계에선 왜 4바이트냐 ㅋㅋ]
그래서 큰 데이터 또는 함수의 값을 전달할 때 메모리를 사용하여 전달하게 되면 메모리와 처리 시간을 줄일 수가 있다.
또한, 이 포인터는 배열처럼 관리가 가능해서 빠르게 접근할 수도 있다
그래서 장점을 정리를 하면
- 효율적인 메모리 사용:
- 포인터를 통해 동적 메모리를 관리하거나 배열처럼 사용할 수 있어 메모리를 효율적으로 활용할 수 있다.
- 직접적인 메모리 접근:
- 포인터를 사용하면 변수의 값을 직접 변경하거나, 배열의 특정 요소에 접근할 수 있다.
- 함수와 데이터 공유:
- 포인터를 사용하면 함수와 데이터 간의 효율적인 데이터 공유가 가능하기에, 함수에서 데이터를 복사하지 않고, 포인터를 통해 원본 데이터를 직접 조작할 수 있다.
- 동적 메모리 할당:
- 실행 중에 필요한 메모리 크기를 결정하고, 해당 메모리를 관리할 수 있다.
- 다양한 응용:
- 포인터는 문자열 처리, 2차원 배열, 구조체, 함수 포인터, 콜백 함수 등 다양한 영역에서 활용된다.
포인터 사용 방법
메모리 주소라고 했다. 그래도 이게 본래 가리키려고 하는 자료형에 맞게 포인터를 선언해 주어야한다.
자료형 *포인터변수명;
이렇게 써준다. 예시를 들면
#include <stdio.h>
int main() {
int a = 10; // 일반 변수
int *p = &a; // 포인터 선언 및 초기화
printf("a의 값: %d\n", a); // a의 값 출력
printf("a의 주소: %p\n", &a); // a의 주소 출력
printf("포인터 p에 저장된 주소: %p\n", p); // p에 저장된 주소 출력
printf("p가 가리키는 값: %d\n", *p); // 포인터를 통해 a의 값 출력
return 0;
}
포인터는 *을 이용해서 사용한다. 그리고 a의 주소를 알려기 위해서는 &을 이용한다.
그래서 a의 주소를 포인터 변수 p에다가 선언한 것이다.
주소를 출력하기 위해서는 %p를 이용하고,
그래서 &로 a의 주소를 출력한 것을 포인터 p에 저장했기에 둘은 같다.
다시 p의 메모리 주소에서 값을 가져오기 위해서는 p에 *를 붙여서 해당 저장된 주소로 이동하여 값을 얻어올 수 있다.
#include <stdio.h>
int main(){
int a = 5;
int *p = &a;
printf("%d\n", a);
a = 10;
printf("%d\n", a);
*p = 30;
printf("%d\n", a);
}
포인터에 *를 붙여서 해당 지정된 주소로 이동하여 값을 얻어왔을 떄 값을 대입해줘서 a라는 변수를 변경할 수 있다.
근데, 포인터를 선언만 해주고 주소를 연결을 안 해준다면...?
#include <stdio.h>
int main(){
int *p;
printf("%p\n", p);
printf("%d\n", *p);
}
큰일난다. 포인터가 주소에 잘 연결이 되어있는지 확인하자.
Call by Value, Call by Reference
딱 해석만 해보자.
Call by Value : 값을 불러온다
Call by Reference : 주소를 불러온다
이 의미이다. 일단 기본적으로 C언어는 Call by value 구조이고, 파이썬 같은 경우는 Call by Reference 구조이다. 그냥 이렇게 보면 이해가 안 갈테니까, 아래 코드로 확인해보자.
#include <stdio.h>
void CallbyValue(int a){
a = 30;
}
void CallbyReference(int* p){
*p = 30;
}
int main(){
int a = 10;
printf("a의 원본 값 : %d\n", a);
CallbyValue(a);
printf("Call by Value : %d\n",a);
int b = 10;
int *p = &b;
printf("b의 원본값 : %d\n", b);
CallbyReference(p);
printf("Call by Reference : %d\n", b);
}
a의 같은 경우 값을 매개변수를 통해서 복사를 해줘서 함수로 넘겨준다. 함수에서 값이 처리가 되었어도 원본 데이터는 보호가 되어서 그대로 10이 되었다. b의 경우에는 ㅎ주소를 넘겨주었고, 함수에서 값 변경을 처리했을 때 원본 데이터도 같이 변경된 것을 알 수 있다.
a가 call by value 방식이고, b가 call by reference구조인 것을 알 수 있다.
a같은 경우 원본 데이터가 보호되기에 안정성이 있고, 함수에서는 원본에서 값을 복사해와서 매개변수로 전달받고 사용하기에, 함수 내부의 변경이 원본에 영향을 미치지 않는다.
b같은 경우 큰 데이터를 복사하지 않고 주소만 전달하므로 메모리와 시간을 절약할 수 있다. 그리고 원본 데이터를 직접 변경할 수 있다.
즉 정리를 하면
Call by Value | Call by Reference | |
데이터 전달 방식 | 값(복사본) | 메모리 주소 |
원본 데이터 변경 여부 | 원본 데이터 변경 불가 | 원본 데이터 변경 가능 |
메모리 사용 | 데이터 복사로 메모리 더 많이 사용 | 주소만 전달하므로 메모리 사용량 적음 |
안전성 | 원본 데이터 보호 | 원본 데이터가 변경될 수 있음 |
사용 사례 | 원본 데이터 변경을 방지할 때 | 원본 데이터를 변경해야 할 때 |
언제 사용하냐?
- Call by Value:
- 원본 데이터를 보호해야 할 때.
- 작은 크기의 데이터(예: int, char)를 전달할 때.
- Call by Reference:
- 함수 내부에서 원본 데이터를 수정해야 할 때.
- 큰 데이터(예: 배열, 구조체)를 함수에 전달할 때, 복사를 피하고 메모리를 절약하고 싶을 때.
배열과 포인터 관계
배열이 일종의 포인터 역할을 할 수 있다. 좀 더 정확히 얘기를 하자면, 배열의 이름이 포인터 변수처럼 사용하게 되는데, 포인터에 배열을 넘겨주면, 배열의 첫 번째 주소가 포인터에 넘겨줄 수 있다.
무슨말인지 모를테니 예시를 보자.
#include <stdio.h>
int main(){
int arr[5] = {1, 2, 3, 4, 5};
int *p= arr;
printf("arr[0] : %d\n", arr[0]);
printf("arr[0] : %d\n", *p);
}
p 포인터 변수를 선언해주었을 때, 원래 변수같은 경우에는 &를 이용하여 주소를 넘겨주었다. 그런데 배열 같은 경우에는 그냥 arr이라고 넘겨버리면, 된다. & 없이. 왜냐면 주소 자체기 때문에. 주소는 첫 번째 주소를 알려준다.
그래서 *p를 했을 때 바로 arr[0]번째 값을 가져오는 것을 알 수 있다.
#include <stdio.h>
int main(){
int arr[5] = {1, 2, 3, 4, 5};
int *p= arr;
printf("arr[0] : %d\n", arr[0]);
printf("arr[0] : %d\n", *p);
*(p+1) = 20;
*(p+2) = 30;
printf("arr[1] : %d\n", arr[1]);
printf("arr[1] : %d\n", *(p+2));
}
arr배열의 값을 바꿀려면 인덱스로 접근하거나 포인터로 (p+1) 이런식으로 접근하면 된다.
#include <stdio.h>
int main(){
int arr[5] = {1, 2, 3, 4, 5};
int *p= arr;
printf("arr[0] : %d\n", arr[0]);
printf("arr[0] : %d\n", *p);
*(p+1) = 20;
*(p+2) = 30;
// printf("arr[1] : %d\n", arr[1]);
// printf("arr[1] : %d\n", *(p+2));
printf("arr[0] : %p\n", p);
printf("arr[0] : %p\n", arr);
printf("arr[5]? : %d\n", arr[5]);
printf("arr[5]? : %p\n", &arr[5]);
printf("arr[5]? : %p\n", (p+5));
}
다만, 인덱스를 넘어서는 건 주의해야한다.
#include <stdio.h>
int main(){
int arr[5] = {1, 2, 3, 4, 5};
int *p= arr;
printf("arr[0] : %d\n", arr[0]);
printf("arr[0] : %d\n", *p);
*(p+1) = 20;
*(p+2) = 30;
// printf("arr[1] : %d\n", arr[1]);
// printf("arr[1] : %d\n", *(p+2));
printf("arr[0] : %p\n", p);
printf("arr[0] : %p\n", arr+1);
printf("arr[0] : %p\n", arr+2);
printf("arr[0] : %p\n", arr+3);
printf("arr[0] : %p\n", arr+4);
}
보면 주소가 4바이트씩 올라간 것을 볼 수 있다.
2차원 배열은?
#include <stdio.h>
int main() {
int arr[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
int *p = arr[0];
printf("%d\n", *p);
printf("%d\n", *(p+1));
printf("%d\n", *(p+2));
printf("%d\n", *(p+3));
p = arr[1];
printf("%d\n", *(p+1));
printf("%d\n", *(p+2));
printf("%d\n", *(p+3));
return 0;
}
대충 감이 잡힌다.
근데 하나 좀 더 알아보자면,
#include <stdio.h>
int main() {
int arr[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
int (*p)[3] = arr;
printf("%d\n", *(*(p + 1) + 2)); // 6 출력 (포인터 연산)
printf("%d\n", p[0][2]);
return 0;
}
(*p)[3] = arr;
arr은 2차원 배열이고, 타입은 int (*)[3]이다.
근데, 행이 2개밖에 없는데 왜 3을 쓰냐?
p+1을 했을 때 그만큼의 이동을 하기 위해서...레이아웃 때문이다.
저렇게 설정을 해야 p+1을 했을 때 정확하게 이동을 할 수가 있다.
함수와 포인터 관계
함수포인터는 결국엔 그냥 함수의 주소를 저장하는 것이다. 그렇다면, 이 메모리를 주소에있는 것을 실행시킨다면? 함수도 바로 실행이 될 것이다.
함수 포인터는
반환형 (*포인터이름)(매개변수 목록);
이런식으로 선언을 한다.
int (*func_ptr)(int, int);
함수도 똑같이 함수포인터를 사용하면 함수의 시작주소를 가르킨다.
#include <stdio.h>
int add(int a, int b){
return a+b;
}
int main(){
int a, b;
printf("공백을 기준으로 두 값을 입력하세요 : ");
scanf("%d %d", &a, &b);
int (*func_ptr)(int, int)= add;
int result = func_ptr(a, b);
printf("%d\n", result);
}
이렇게 사용하면
값이 나온다. 그런데 굳이 왜 이렇게 함수포인터를 사용하냐?
아래 두 가지 방법을 한 번 봐바라. 와 편하다 느낀다.
#include <stdio.h>
int add(int a, int b){
return a+b;
}
int sub(int a, int b){
return a-b;
}
int mul(int a, int b){
return a*b;
}
int main(){
int a, b;
printf("공백을 기준으로 두 값을 입력하세요 : ");
scanf("%d %d", &a, &b);
int (*func_ptr)(int, int)= add;
printf("%d\n", func_ptr(a, b));
func_ptr = sub;
printf("%d\n", func_ptr(a, b));
func_ptr = mul;
printf("%d\n", func_ptr(a, b));
}
함수 호출을 하나의 변수에 넣어서 실행할 수 있으니까 편하다. 아직까지 그렇게 안 보인다?
그러면 이걸 봐라.
#include <stdio.h>
int add(int a, int b){
return a+b;
}
int sub(int a, int b){
return a-b;
}
int mul(int a, int b){
return a*b;
}
int main(){
int a, b;
printf("공백을 기준으로 두 값을 입력하세요 : ");
scanf("%d %d", &a, &b);
int (*func_ptr[3])(int, int)= {add, sub, mul};
printf("%d\n", func_ptr[0](a, b));
printf("%d\n", func_ptr[1](a, b));
printf("%d\n", func_ptr[2](a, b));
}
함수가 배열을 이용해서 다 넣어서 사용할 수 있다. 이 얼마나 편한가.
뭐, 함수 내부에 다른 함수를 매개변수로 넣어줄 때도 이 함수 포인터를 이용해서 넘겨주는 방식도 있다.
이중포인터
포인터 자체가 어떤 값의 주소를 가리키는 것이다. 이중포인터는 그 포인터의 주소를 가리키는 것이다. 즉 값->주소->주소를 저장하는 변수인 것이다.
int a = 10;
int *p = &a; // p는 a의 주소를 저장
int **pp = &p; // pp는 p의 주소를 저장
이게 굳이 왜 필요하냐?
다차원 배열 처리.
함수에서 포인터 변경.
문자열 배열 처리.
동적 메모리 할당.
이렇게 쓴다는게, 뭐 어떻게 쓰는지는 알겠는데..P랑의 차이점을 잘 모르겠어서 넘어가겠다.
에필로그
포인터끝!
'KnockOn' 카테고리의 다른 글
[KnockOn] Linux/Ubuntu C언어 <string.h> (0) | 2024.11.24 |
---|---|
[KnockOn] Linux/Ubuntu C언어 문자열 (0) | 2024.11.23 |
[KnockOn] Linux/Ubuntu C언어 함수 -2 (0) | 2024.11.20 |
[KnockOn] Linux/Ubuntu C언어 함수 -1 (2) | 2024.11.19 |
[KnockOn] Linux/Ubuntu C언어 배열 (0) | 2024.11.17 |