포인터 Pointer
메모리 주소값을 저장하는 변수
데이터가 저장된 메모리 위치를 가리키는 값을 저장한다
포인터 선언
참조 연산자 * 를 사용하여 선언
자료형은 포인터가 가리지고자 하는 변수의 자료형 명시
선언과 동시에 초기화를 함께 하는 것을 권유
(의도하지 않은 메모리 장소에 값을 저장
자료형 * 포인터이름 = &변수이름 or 주소값;
포인터와 주소 연산자
& 주소 연산자 - 변수의 메모리를 가져올 수 있다
* 역참조 연산자 - 포인터가 가리키는 주소의 실제 값을 읽거나 쓸 수 있게 한다
& 연산자 - 주소 연산자
변수의 주소를 반환하는 연산자, 변수가 메모리에서 저장된 위치 (주소) 를 얻을 수 있다
int x = 10;
int* ptr = &x; // x의 주소를 포인터 ptr에 저장
위 코드에서 &x 는 변수 x 의 주소를 나타내고 이 주소는 포인터 변수 ptr 에 저장된다
* 연산자 - 역참조 연산자
포인터가 가리키는 주소에 있는 데이터를 가져오는 데 사용하는 연산자,
포인터가 저장한 메모리 주소에 접근하여 실제 저장된 데이터를 읽거나 수정할 수 있다
int x = 10;
int* ptr = &x;
int y = *ptr; // ptr이 가리키는 값(x의 값)을 y에 저장
*ptr = 20; // ptr이 가리키는 x의 값을 20으로 변경
위 코드에서 *ptr 은 포인터 ptr 이 카리키는 주소에 있는 데이터를 의미하며,
이를 통해 변수 x 의 값을 읽고 수정할 수 있다
포인터는 변수의 주소를 저장한다
& 연산자는 변수의 주소를 가져온다
* 연산자는 포인터가 가리키는 주소에 있는 값을 가져온다
전체 예시 코드
int x = 10; // x라는 정수형 변수 선언
int* ptr = &x; // x의 주소를 포인터 ptr에 저장
*ptr = 20; // ptr이 가리키는 주소의 값을 20으로 변경 (즉, x = 20이 됨)
&x 는 x 의 메모리 주소를 가져온다
ptr 은 ptr 이 가리키는 주소에 있는 값을 읽거나 변경한다
배움 출처 https://living-fruit-731.notion.site/
포인터 예제 코드
#include <stdio.h>
int main(void)
{
int a = 10; // 정수형 데이터 변수 a 선언
int *p = &a; // 정수형 데이터 a 를 가리키는 포인터 변수 p 선언 및 a 주소값 할당
printf("a = %d\n", a);
printf("*p = %d\n", *p);
printf("p = %p\n", p);
printf("&a = %p\n", &a);
}
// 결과 출력
a = 10
*p = 10
p = 0x16ee531fc
&a = 0x16ee531fc
포인터에 타입을 선언하는 이유
1) 주소를 저장할 변수의 타입을 명시하기 위해서
2) 포인터를 연산하기 위해서
3) 포인터 변수의 값을 읽고 쓸 때, 타입에 맞게 메모리를 사용하기 위해서
포인터와 메모리
포인터는 메모리의 직접 접근을 가능하게 한다
변수를 직접 가리키는 대신, 포인터를 통해 간접적으로 변수에 접근할 수 있다
이로 인해, 포인터는 동적 메모리 할당, 배열, 함수 전달 등에서 유용하게 사용된다
NULL 포인터
유효하지 않은 메모리 주소를 가리키는 포인터
포인터가 아직 아무 것도 가리키지 않음을 나타내고 포인터를 사용하기 전에 초기화 값으로 사용한다
int *p = NULL; # p 는 현재 아무 메모리도 가리키지 않음
포인터의 활용
배열과 포인터 - 배열의 이름은 배열의 첫번째 요소를 가리키는 포인터처럼 작동한다
함수 포인터 - 포인터는 함수의 주소를 저장할 수 있으며 이를 통해 함수도 인자로 전달하거나 호출할 수 있다
동적 메모리 할당 - malloc 과 같은 함수를 사용하여 런타임에 메모리를 동적으로 할당받고, 이를 포인터로 관리한다
int arr[3] = {1, 2, 3};
int *p = arr; // arr은 첫 번째 요소의 주소를 가리킴
printf("%d", *(p+1)); // p가 가리키는 다음 요소의 값 출력 (2)
포인터와 상수
1) 상수 포인터
상수 포인터는 포인터 자체가 상수인 경우로, 한 번 특정 주소를 가리키면 그 이후에는 다른 주소를 가리킬 수 없다
int a;
int b;
int* const pa = &a; // 상수 포인터 pa를 선언하여 a의 주소를 저장
*pa = 3; // OK, pa 가 가리키는 a 의 값을 3 으로 변경
pa = &b; // Error, pa 는 상수 포인터이므로 다른 주소로 변경할 수 없음
2) 상수를 가리키는 포인터
상수를 가리키는 포인터는 포인터가 가리키는 값을 변경할 수 없다
int a;
int b;
const int* pa = &a; // 상수를 가리키는 포인터 pa 를 선언하여 a 의 주소를 저장
*pa = 3; // Error, pa 가 가리키는 a 의 값을 변경할 수 없음
pa = &b; // OK, pa 는 b 의 주소로 변경할 수 있음
포인터의 장점
효율성 - 메모리 주소를 통해 데이터에 직접 접근할 수 있어 처리 속도가 빠름
메모리 관리 - 동적 메모리 할당을 통해 프로그램이 실행 중에 메모리를 효율적으로 관리할 수 있음
유연성 - 배열, 구조체, 함수 등 다양한 자료 구조에서 활용 가능하며, 특히 참조 전달로 효율적인 메모리 사용이 가능함
포인터 사용 시 주의점
포인터 연산에서 잘못된 메모리 접근은 Segmentation Fault 를 유발할 수 있다
Dangling Pointer - 해제된 메모리 공간을 가리키는 포인터로, 예기치 않은 동작을 초래할 수 있음
포인터 배열에서 주의점
arr[i] 와 *(arr+i) 는 같다
배열 이름에 sizeof 연산자와 주소값 연산자를 사용할 때 빼고는 암묵적으로 포인터로 변환된다
2차원 배열 arr[][] 에서
1) arr - 배열의 첫번째 행을 가리키는 포인터, 타입은 int (*)[N]
2) arr[0] - 첫번째 행을 가리키는 포인터, 타입은 int*
3) &arr[0] - 첫번째 행의 주소, 타입은 int (*)[N]
arr 과 arr[0] 은 메모리에서 같은 주소를 가리키고 동일한 데이터를 참조하지만
타입이 다르기 때문에 사용하는 방식이 다를 수 있으니 주의하기
// 2차원 배열과 포인터 배열을 사용하여 두 배열 간의 데이터를 출력하는 예제
#include <stdio.h>
int main() {
int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
int(*parr)[3]; // 괄호를 꼭 붙이세요
parr = arr; // parr 이 arr 을 가리키게 한다.
printf("parr[1][2] : %d , arr[1][2] : %d \n", parr[1][2], arr[1][2]);
return 0;
}
// 출력 결과
parr[1][2] : 6 , arr[1][2] : 6
dot 과 allow 연산자
C언어에서 구조체를 다룰 때 . dot 과 -> arrow 는 구조체의 멤버에 접근하는 방법을 나타낸다
이 두 연산자는 포인터와 구조체 변수를 다룰 때 각각 사용되며, 그 차이는 다음과 같다
1) . dot 연산자
. 연산자는 구조체 변수에서 직접적으로 멤버에 접근할 때 사용된다
구조체 변수가 직접적으로 정의되어 있을 때, 그 변수의 멤버에 접근하기 위해 사용된다
#include <stdio.h>
# Point 구조체 정의
# 두 개의 정수 좌표 (x, y) 를 저장
typedef struct {
int x; // x 좌표
int y; // y 좌표
} Point;
int main() {
Point p1; // Point 구조체 변수 p1 선언
p1.x = 10; // p1 의 x 멤버에 값 10 할당
p1.y = 20; // p2 의 y 멤버에 값 20 할당
// p1 의 x, y 값 출력
printf("p1.x = %d, p1.y = %d\n", p1.x, p1.y);
return 0; // 프로그램 종료
}
2) -> arrow 연산자
-> 연산자는 구조체 포인터에서 간접적으로 멤버에 접근할 때 사용된다
#include <stdio.h>
#include <stdlib.h>
// Point 구조체 정의: 두 개의 정수 좌표 (x, y)를 저장
typedef struct {
int x; // x 좌표
int y; // y 좌표
} Point;
int main() {
// Point 구조체에 대한 포인터 변수 p1 선언 및 메모리 동적 할당
Point *p1 = malloc(sizeof(Point));
// -> 연산자를 사용하여 p1 이 가리키는 구조체의 멤버에 접근하여 값 할당
p1->x = 10; // p1 의 x 멤버에 10 할당
p1->y = 20; // p2 의 y 멤버에 20 할당
// 할당된 값 출력
printf("p1->x = %d, p1->y = %d\n", p1->x, p1->y);
// 동적으로 할당된 메모리 해제
free(p1);
return 0; // 프로그램 종료
}
Call by Value VS Call by Reference
Call by Value
함수에 인자로 전달되는 값은 원본 값의 복사본이다
함수 내에서 인자의 값을 변경해도 원래 변수에는 영향을 미치지 않는다
C언어에서 기본적으로 사용하는 방식이다
#include <stdio.h>
void modifyValue(int x) {
x = 10; // 함수 내에서 값 변경
}
int main() {
int a = 5;
modifyValue(a);
printf("a: %d\n", a); // a의 값은 여전히 5
return 0;
}
Call by Reference
변수의 주소를 전달하여 함수가 원본 데이터를 직접 수정할 수 있다
포인터를 이용하여 변수의 주소를 함수로 전달하고, 이를 통해 함수 내에서 원본 변수의 값을 변경할 수 있다
#include <stdio.h>
void modifyValue(int *x) {
*x = 10; // 포인터를 통해 원본 값 변경
}
int main() {
int a = 5;
modifyValue(&a); // a의 주소 전달
printf("a: %d\n", a); // a의 값이 10으로 변경됨
return 0;
}
'TIL > C언어' 카테고리의 다른 글
[TIL] Red-Black Tree 구현하기 #1 (1) | 2024.10.13 |
---|---|
[TIL] qsort 정렬 C언어 (1) | 2024.10.05 |
[TIL] GCC, GNU Complier Collection (1) | 2024.10.04 |
[TIL] 분할 정복 (1) | 2024.09.09 |
[TIL] 복잡도 (3) | 2024.09.09 |