728x90
반응형
SMALL
이번 포스팅에서 우리가 배워볼 것은 다음과 같다.
- 포인터
- 포인터 변수의 선언과 사용 예
- 포인터 증감 연산
- 포인터는 어디에서 사용되는가?
- 포인터 사용시 주의점 : nullptr
- 포인터와 배열
- 심화 : 포인터 구조 확인
1. 포인터
- 포인터 변수는 메모리의 주소를 저장하는 변수를 의미한다.
Pointer 변수 선언
- data_type *variable_name; 또는 data_type* variable_name;
- 최근에 푸른 색을 더 선호하는 편이다.
- 저렇게 선언을 하면 컴퓨터는 변수의 주소를 받을 준비가 되어 있다고 생각한다.
Pointer 연산
- 포인터 연산
- 변수의 주소를 계산하는 연산자[주소 연산자] : &
- 변수의 메모리 주소 값 : &variable
- 시작하는 위치의 주소를 계산한다!
- 아래 사진에서 number의 주소를 1008이라고 하면 포인터 변수 p에 number 변수의 주소 1008이 들어가게 되고 이에 따라 *p가 가리키는 값은 10이 된다.
-
int number = 10; // 정수형 변수 number 선언 int *p; // 포인터 변수 p 선언 p = &number; // 변수 number의 주소가 포인터 p로 대입
- 변수의 주소를 계산하는 연산자[주소 연산자] : &
-
- 간접 참조 연산자 * : 포인터가 가리키는 값을 가져오는 연산자
- Pointer 변수가 가리키는 메모리 공간의 내용을 가져오려면 *pointer_variable 를 사용하면 된다.
-
#include <iostream> using namespace std; int main() { int number = 10; // 변수 number의 주소를 계산하여 p에 저장한다. int *p = &number; cout << *p << endl; // p가 가리키는 공간에 저장된 값을 출력한다. return 0; }
CF) *의 세 가지 다른 의미
- 곱셈 연산자 (이항 연산) : a = b * c;
- Pointer variable 선언 : void separate(double num, char* signp, int* wholep, double* fracp)
- Expression에서 "포인터가 가리키는 곳"(derefernecing)
int i = 10; // 정수형 변수 i 선언
int *p; // 포인터 변수 p 선언
p = &i; // 변수 i의 주소가 포인터 p로 대입
printf("%d", *p); // 출력값 : 10
2. 포인터 변수의 선언과 사용 예
(1)
#include <iostream>
using namespace std;
int main() {
char c1;
char c2 = 'B';
char *signp;
signp = &c2; // signp: c2의 주소를 가리킵니다.
c1 = *signp; // c1에 *signp의 값, 즉 c2의 값이 "복사"됩니다. 이 시점에서 c1은
// c2와 같은 값이 됩니다.
*signp = 'A'; // *signp가 c2를 가리키는데, 이를 통해 c2의 값이 'A'로 변경됩니다.
cout << "signp 값(=c2 주소) : " << signp << endl; // signp가 가리키는 메모리에 저장된 값, 즉 'A'를 출력합니다.
cout << "c1 주소 : " << &c1 << endl;
cout << "c2 주소(=signp 값) : " << &c2 << endl;
/* 정확한 메모리 주소를 출력하려면 주소값을 문자열로 변환하지 않도록 조치해야 합니다.
여러 가지 방법이 있지만 가장 간단한 방법은 reinterpret_cast를 사용하여 주소값을 정수로
캐스팅한 후 출력하는 것입니다. 다음은 코드를 수정하여 주소값을 정수로 변환하여 출력하는 방법이다.*/
cout << "c1 주소 : " << reinterpret_cast<void*>(&c1) << " /" << " c2 주소 : " << reinterpret_cast<void*>(&c2) << " /" << " signp의 주소 : " << &signp << endl;
cout << "c1 값 : " << c1 << endl; // c1의 값, 즉 c2의 초기 값이 출력됩니다.
cout << "c2 값 : " << c2 << endl; // c2의 현재 값, 즉 'A'가 출력됩니다.
}
(2)
#include <iostream>
using namespace std;
int main() {
int i = 33;
int j = 44;
int k = 0;
int *p, *q; // 선언에서 *p, *q는 정수를 가리키는 포인터입니다.
p = &j; // p는 1004로 할당됨 – 이는 j의 주소입니다
i = j; // i는 44로 지정되어 있으며, 이는 j의 값입니다
k = *p; /*식에서 "dereference p"로 읽음 – */
/* p의 값은 1004 입니다. "deliverse 1004"는 1004번지에서 44라는 값을 가져온다는 것을 의미합니다." */
*p = 88; /*"dereference p" - p(주소값 1012)에 저장하는 것이 아니라, 1004번지에 있는 곳에 저장한다.
(p의 값을 '주소' & '그 주소에 저장하는용'으로 사용된다.*/
cout << i << endl;
cout << j << endl;
cout << k << endl;
cout << p << endl;
cout << q << endl;
}
3. 포인터 증감 연산
포인터의 증가 연산의 경우, 이란 변수와는 약간 다르다. 증가되는 값은 포인터가 가리키는 객체의 크기이다. 따라서 가리키는 객체의 크기만큼 증가하게 된다.
포인터 타입 | ++ 연산 후 증가되는 값 |
char | 1 |
short | 2 |
int | 4 |
float | 4 |
double | 8 |
예시 : int형 포인터 변수 i가 있을 때 i++ 하게 되면 아래와 같이 바뀌게 된다.
(처음 i의 값) | ===> | ===> | ===> | (p++ 후 i의 값) |
4. 포인터는 어디에서 사용되는가?
- 포인터 변수를 이용해서 주소 값을 통해 결국 변수에 접근하게 된다. 그러면 여기서 '굳이 포인터 변수를 사용하지 않고, 원래 있던 변수를 바로 참조하면 되는 것이 아닌가?'하고 의문을 가질 수 있다.
- 여기서 우리가 알아야 할 점은 일반 변수는 메모리를 컴퓨터가 관리하고, 포인터로 관리하는 변수는 개발자가 직접 메모리를 관리할 수 있다는 점이다. 즉, 장치 드라이버나 동적 메모리 할당에서 메모리를 주소로 참조해야 하는 경우가 발생하는데 이 때 포인터가 사용되는 것이다.
- 또한, 함수의 포인터 형식 인자를 통한 실제 인자 접근(access)할 때 사용한다.
- 함수의 인수 전달 방법으로 '값에 의한 호출(call by value)'와 '참조에 의한 호출(call by reference)'가 있다. '값에 의한 호출'은 C에서 기본적인 방법이며, '참조에 의한 호출'은 C에서 포인터를 이용하여 흉내낼 수 있다. 따라서 포인터 형식 인자를 통한 실제 인자 접근은 '참조에 의한 호출'이다.
- call by value(값에 의한 전달) : 실매개변수의 값을 형식매개변수로 복사하여 전달하는 기법
- 실매개변수의 값이 호출되는(called) 함수의 형식매개변수에 복사가 된다.
- 호출되는(called) 함수에서 형식매개변수의 값을 변경해도 호출하는 함수(caller) 쪽에서 실매개변수 값에 영향을 미치지가 않는다.(애초에 복사를 한 것이라 실체가 다르기 때문!)
- call by reference(참조에 의한 전달) : 실매개변수의 주소를 형식매개변수로 전달하는 기법
- 즉, 실체가 똑같게 된다.
- call by value(값에 의한 전달) : 실매개변수의 값을 형식매개변수로 복사하여 전달하는 기법
- Call by value의 예시 - 변수 2개의 값을 바꾸는 작업을 함수로 이용하기
-
#include <iostream> void swap(int x, int y); using namespace std; int main() { int a = 100, b = 200; swap(a, b); cout << "a : " << a << endl; cout << "b : " << b << endl; return 0; } void swap(int x, int y) { int tmp; tmp = x; x = y; y = tmp; }
- 이 코드는 a와 b의 값을 바꿀려고 만든 코드인데, 실제로는 바뀌지가 않는다. 왜냐하면 함수 swap(100, 200)를 호출하면 실매개변수 a, b 값이 함수 swap의 형식매개변수 x, y로 각각 전달이 되는데 원본을 복사해서 전달하기 때문이다. 함수 swap에서 x와 y의 값을 바꾸지만, 수행이 끝난 후, 호출한 곳으로 돌아오면 a와 b의 값은 변하지 않는 것이다.
-
- Call by reference의 예시
-
#include <iostream> void swap(int *px, int *py); using namespace std; int main() { int a = 100, b = 200; swap(&a, &b); cout << "a : " << a << endl; cout << "b : " << b << endl; return 0; } void swap(int *px, int *py) { int tmp; tmp = *px; *px = *py; *py = tmp; }
- 함수 호출시에 원본 주소가 복사되며, 함수 내에서 값이 변경됨에 따라 동시에 원본값도 바뀌는 것을 알 수 있다.
-
- 함수의 인수 전달 방법으로 '값에 의한 호출(call by value)'와 '참조에 의한 호출(call by reference)'가 있다. '값에 의한 호출'은 C에서 기본적인 방법이며, '참조에 의한 호출'은 C에서 포인터를 이용하여 흉내낼 수 있다. 따라서 포인터 형식 인자를 통한 실제 인자 접근은 '참조에 의한 호출'이다.
- 동적할당 ( vs 정적할당)
- 정적할당은 배열 int a[10];같은 경우이며 항상 10개로 고정이다.
- 동적할당은 배열의 크기를 조절할 수 있으며, 포인터를 이용하지 않고는 동적할당 이용할 수가 없다.
- 동적할당에 관한 포스팅은 다음을 참고하자.
- 지금까지 : 포인터 사용의 총 장점 정리
- 메모리에 직접 접근이 가능
- 구조화된 자료를 만들어 효율적 운영이 가능
- Call by Reference
- 배열, 구조체 등의 복잡한 자료 구조와 함수에 쉽게 접근
- 메모리 동적 할당
5. 포인터 사용시 주의점 : nullptr
- NULL 포인터란 말 그대로 아무 것도 가리키고 있지 않은 포인터를 말한다.
- 만약 포인터가 선언만 되고 초기화되지 않았다면 포인터는 임의의 주소를 가리키게 된다.
- => 여기서 포인터를 이용하여 메모리의 내용을 변경한다면 문제가 발생한다.
- 따라서 포인터가 아무 것도 가리키고 있지 않을 때는 nullptr로 설정해야 한다.
- NULL 값도 정수 0이여서 약간의 문제가 있다.
- Ex) int *p = nullptr; (O) / int *p = NULL; (X)
6. 포인터와 배열
- 배열과 포인터는 아주 밀접한 관계를 가지고 있다.
- 배열 이름이 바로 포인터이다.
- 포인터는 배열처럼 바로 사용이 가능하다.
- 인덱스 표기법을 포인터에 사용할 수 있다.
- 사진
- 예시) int S[5];
- 배열 이름 S는 &S[0]과 같다.
S[0] S[1] S[2] S[3] S[4] -
*S = 10; // S[0] = 10; *(S + 1) = 20; // S[1] = 20; *(S + i) = 30; // S[i] = 30;
- 배열 이름 S는 &S[0]과 같다.
- 배열 매개변수 VS Pointer 매개변수
- Array 변수는 pointer 변수로 취급되기 때문에 함수 정의에서 array 변수를 인자로 사용하는 것과 pointer 변수를 인자로 사용하는 것은 같다.
-
/* 아래의 두 prototype은 동등함. */ double sum(double a[], int n); double sum(double *a, int n);
- 코드 예시 - 배열의 원소들의 합을 구하는 함수
-
#include <iostream> using namespace std; double sum(double a[], int n){ double sum = 0.0; int i; for (i = 0; i < n; i++) { sum += a[i]; } return sum; } double sum_by_pointer(double* a, int n) { double sum = 0.0; int i; for (i = 0; i < n; i++) { sum += a[i]; // sum += *(a+i); } return sum; } int main() { double v[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; cout << sum(v, 10) << endl; // sum(&v[0], 10); 1 ~ 10 까지의 합 cout << sum_by_pointer(v, 10) << endl; // 1 ~ 10 까지의 합 cout << sum_by_pointer(&v[7],3) << endl; // 8 + 9 + 10 }
7. 심화 : 포인터 구조 확인
#include <stdio.h>
#include <iostream>
using namespace std;
int main() {
int var = 10;
int* p_var = NULL;
cout << sizeof(var) << endl;
cout << sizeof(p_var) << endl;
int a[10];
int length = sizeof(a) / sizeof(int);
for (int i = 0; i < length; i++) {
a[i] = i * i;
}
int* point = &a[0];
for (int i = 0; i < length; i++) {
int* now = point + i;
wcout << "i [" << i << "] > 메모리 주소 " << now << "배열의 값 : " << *now << endl;
}
return 0;
}
위 코드를 통해 알 수 있는 점
- 포인터 변수는 64비트 운영체제에서 8바이트 크기를 가진다.
- 8byte * 8bit = 64bit
- 0x... 으로 시작하는 것은 16진수를 의미한다.
- 16진수는 4비트로 표현 가능하다 -> 64/4 = 16자리
- 그래서 실행결과 화면에 16자리로 나오는 것이다.
반응형
LIST
'Develop > C, C++' 카테고리의 다른 글
[C/C++] 동적할당(Dynamic Memory Allocation) (0) | 2023.12.18 |
---|---|
[C/C++] 문자열(String) (0) | 2023.12.18 |
[C/C++] 함수(Function) (0) | 2023.12.18 |
[C/C++] 함수와 포인터를 사용해서 문자열을 뒤집어보자! (0) | 2023.10.12 |
[C++] 포인터를 사용해서 배열의 합계와 평균을 계산해보자! (0) | 2023.10.10 |