Chapter 8. 포인터¶
'쉽게 풀어쓴 C언어 Express - 천인국' 11장 학습 내용
1절. 포인터 기초
2절. 주소
3절. 포인터
4절. 연산자
5절. swap()
6절. scanf()
7절. 매개 변수
1절. 포인터 기초¶
포인터(Pointer)¶
- 주소를 가지고 있는 변수
변수의 저장¶
- 변수는 메모리에 저장된다.
- 메모리는 바이트 단위로 엑세스된다.
- 첫 번째 바이트의 주소는 0, 두 번째 바이트는 1, ...
변수의 메모리¶
- 변수의 크기에 따라서 차지하는 메모리 공간이 달라진다.
- char형 변수 : 1 바이트, int형 변수 : 4 바이트, ...
2절. 주소¶
주소의 개념¶
주소 연산자 &¶
- 함수 scanf()를 사용하면서 인자를 '&변수이름'으로 사용
- &(ampersand)가 피연산자인 변수의 메모리 주소를 반환하는 주소 연산자
- 함수 scanf()에서 입력값을 저장하는 변수의 주소값이 인자의 자료형
- 함수 scanf()에서 일반 변수 앞에는 주소 연산자 &를 사용
변수의 주소¶
- 변수의 주소를 계산하는 연산자 : &
- 변수 i의 주소 : &i
변수의 주소¶
int main(void){
int i = 10;
char c = 69;
float f = 12.3;
printf("i의 주소 : %u\n", &i); // 변수 i의 주소 출력
printf("c의 주소 : %u\n", &c); // 변수 i의 주소 출력
printf("f의 주소 : %u\n", &f); // 변수 i의 주소 출력
return 0;
}
/* 출력 예시
i의 주소 : 1245024
c의 주소 : 1245015
f의 주소 : 1245000 */
변수의 주소 출력 주의사항¶
- 여러 개의 포인터 변수를 한 줄에 선언할 때는 주의해야 한다.
- 아래는 잘못된 선언 예제이다.
```C int *p1, p2, p3; // p2와 p3는 정수형 변수로 선언
3절. 포인터¶
포인터의 선언¶
- 포인터 변수 선언에서 자료형과 포인터 변수 이름 사이에 연산자 *(asterisk)를 삽입한다.
- ptrint, ptrshort, ptrchar, ptrdouble은 모두 포인터 변수이다.
- 간단히 포인터라고도 부른다.
포인터와 변수의 연결¶
다양한 포인터 선언¶
char c = 'A'; // 문자형 변수 c
float f = 36.5; // 실수형 변수 f
double d = 3.141592; // 실수형 변수 d
char *pc = &c; // 문자를 가리키는 포인터 pc
float *pf = &f; // 실수를 가리키는 포인터 pf
double *pd = &d; // 실수를 가리키는 포인터 pd
포인터 선언 예제¶
#include <stdio.h>
int main(void)
{
int i = 10;
double f = 12.3;
int *pi = NULL; // NULL로 초기화를 하면 어느 메모리도 지정하지 않음
double *pf = NULL;
pi = &i; // 포인터 변수 pi는 정수형 변수 i의 주소값을 저장
pf = &f; // 포인터 변수 pf는 실수형 변수 f의 주소값을 저장
printf("%p %p \n", pi, &i);
printf("%p %p \n", pf, &f);
return 0;
}
/* 출력 예제
0000002AFF8FFB24 0000002AFF8FFB24
0000002AFF8FFB48 0000002AFF8FFB48 */
포인터 참고사항¶
- NULL은 stdio.h 헤더 파일에 정의된 포인터 상수로 0번을 의미한다.
- 0번은 일반적으로는 사용할 수 없다.
- CPU가 인터럽트를 위해 사용한다.
- 따라서 포인터 변수의 값이 0이라면 아무것도 가리키고 있지 않다.
포인터 사용 시 주의점 1¶
- 초기화가 안된 포인터를 사용하면 안된다.
포인터 사용 시 주의점 2¶
- 포인터가 아무것도 가리키고 있지 않은 경우에는 NULL로 초기화한다.
- NULL 포인터를 가지고 간접 참조하면 하드웨어로 감지한다.
포인터 사용 시 주의점 3¶
- 포인터의 타입과 변수의 타입은 일치해야 한다.
#include <stdio.h> int main(void){ int i; double *pd; pd = &i; // ERROR : double형 pointer에 int형 변수의 주소를 대입 *pd = 36.5; return 0; }
포인터의 사용 이유¶
- 문자(char)는 'a' 처럼 글자가 하나만 있는 상태이다.
- 문자열(char *)은 "Hello"처럼 글자 여러 개가 계속 이어진 상태이다.
- 즉, 문자는 1byte 크기의 char에 저장할 수 있지만 문자열은 크기가 1byte를 넘어가기 때문에 char에 저장할 수 없다.
- 따라서 문자열은 변수에 직접 저장하지 않고 포인터를 이용해서 저장한다.
포인터 주소 연산¶
- 가능한 연산 : 증가, 감소, 덧셈, 뺄셈
- 증가 연산의 경우 증가되는 값은 포인터가 가리키는 객체의 크기
| 포인터 타입 | ++연산 후 증가되는 값 |
|---|---|
| char | 1 |
| short | 2 |
| int | 4 |
| float | 4 |
| double | 8 |
포인터 연산¶
포인터 주소 연산¶
포인터 증감 연산¶
포인터의 증가 연산 예제¶
#include <stdio.h>
int main(void){
char *pc;
int *pi;
double *pd;
pc = (char *)10000; // pc의 절대 주소에 10000을 대입
pi = (int *)10000; // pi의 절대 주소에 10000을 대입
pd = (double *)10000; // pd의 절대 주소에 10000을 대입
printf("pc = %u, pc+1 = %u, pc+2 = %u\n", pc, pc+1, pc+2);
printf("pi = %u, pi+1 = %u, pi+2 = %u\n", pi, pi+1, pi+2);
printf("pd = %u, pd+1 = %u, pd+2 = %u\n", pd, pd+1, pd+2);
return 0;
}
// 실행 결과
// pc = 10000, pc+1 = 10001, pc+2 = 10002
// pi = 10000, pi+1 = 10004, pi+2 = 10008
// pd = 10000, pd+1 = 10008, pd+2 = 10016
포인터의 형변환¶
- C언어에서는 꼭 필요한 경우에 명시적으로 포인터의 타입을 변경할 수 있다.
포인터와 함수¶
- 함수가 포인터를 통해 값을 변경할 수 없게 하려면?
- 함수의 매개 변수를 선언할 때 앞에 const를 붙인다.
- const를 앞에 붙이면 포인터가 가리키는 내용이 변경 불가능한 상수라는 뜻이다.
포인터와 배열¶
- 배열과 포인터는 밀접한 관계이다.
- 배열의 이름이 포인터이다.
- 포인터는 배열처럼 사용 가능하다.
- 인덱스 표기법을 포인터에 사용할 수 있다.
- 인덱스 표기 = 배열에서 사용
- a + i == &a[i]
- *(a + i) == a[i]
포인터를 배열처럼 사용하는 예제¶
#include <stdio.h>
int main(void)
{
int a[] = {10, 20, 30, 40, 50};
int *p;
p = a;
printf("&a[0] = %d\ta[1] = %d\ta[2] = %d\n", a[0], a[1], a[2]);
printf("&p[0] = %d\tp[1] = %d\tp[2] = %d\n\n", p[0], p[1], p[2]);
p[0] = 60;
p[1] = 70;
p[2] = 80;
printf("&a[0] = %d\ta[1] = %d\ta[2] = %d\n", a[0], a[1], a[2]);
printf("&p[0] = %d\tp[1] = %d\tp[2] = %d\n\n", p[0], p[1], p[2]);
return 0;
}
// 출력 예시
// a[0] = 10 a[1] = 20 a[2] = 30
// p[0] = 10 p[1] = 20 p[2] = 30
//
// a[0] = 60 a[1] = 70 a[2] = 80
// p[0] = 60 p[1] = 70 p[2] = 80
- 사진으로 보기
포인터를 배열의 이름으로 사용¶
포인터와 배열의 관계 예제¶
// 포인터와 배열의 관계
#include <stdio.h>
int main(void)
{
int a[] = {10, 20, 30, 40, 50};
printf("&a[0] = %u\n", &a[0]);
printf("&a[1] = %u\n", &a[1]);
printf("&a[2] = %u\n", &a[2]);
printf("a = %u\n", a);
// 배열 이름을 부호 없는 정수값으로 출력
// 첫 번째 요소(index = 0)의 주소값과 동일
return 0;
}
// 출력 예시
// &a[0] = 1245008
// &a[1] = 1245012
// &a[2] = 1245016
// a = 1245008
포인터와 배열의 관계 예제 2¶
// 포인터와 배열의 관계 2
#include <stdio.h>
int main(void)
{
int a[] = {10, 20, 30, 40, 50};
printf("a = %u\n", a);
printf("a + 1 = %u\n", a + 1);
printf("*a = %d\n", *a);
printf("*(a + 1) = %d\n", *(a + 1));
return 0;
}
// 출력 예시
// a = 1245008
// a + 1 = 1245012
// *a = 10
// *(a + 1) = 20
4절. 연산자¶
간접 참조 연산자¶
- 간접 참조 연산자 * : 포인터가 가리키는 값을 가져오는 연산자
int i = 10;
int *p; // 포인터 변수 선언(자료형과 *p)
p = &i; // p에는 i의 주소값인 4 저장
printf("%d", *p) // 간접 참조(p변수의 주소에 가서 값을 알아오기)
// 실행 결과
// 10
간접 참조 연산자의 해석¶
- 간접 참조 연산자 : 지정된 위치에서 포인터의 타입에 따라 값을 읽음
& 연산자와 * 연산자¶
- & 연산자 : 주소 연산자로, 변수의 주소를 반환한다.
-
- 연산자 : 내용 연산자로, 포인터가 가리키는 곳의 내용을 반환한다.
간접 참조 연산자 확인 예제¶
#include <stdio.h>
int main(void){
int i = 3000;
int *p = NULL;
p = &i;
printf("i = %d\n", i); // 변수의 값 출력
printf("&i = %u\n\n", &i); // 변수의 주소 출력
printf("p = %u\n", p); // 포인터의 값 출력
printf("*p = %d\n", *p); // 포인터를 통한 간접 참조 값 출력
// 변수 p에는 i의 주소값이 담겨져 있기 때문에 p를 출력하면 i의 주소값인 1245024가 출력됨
// 포인터 p는 i의 주소값의 주소값, 즉 i값이 담겨져 있기 때문에 *p를 출력하면 i의 값인 3000이 출력됨
return 0;
}
// 실행 결과
// i = 3000
// &i = 1245024
//
// p = 1245024
// *p = 3000
* 연산자 확인 예제¶
#include <stdio.h>
int main(void){
int x = 10, y = 20;
int *p;
p = &x;
printf("p = %d\n", p); // x의 주소값 출력
printf("*p = %d\n\n", *p); // x의 값 출력
p = &y;
printf("p = %d\n", p); // y의 주소값 출력
printf("*p = %d\n", *p); // y의 값 출력
// 변수 x와 y는 똑같은 경우의 수인데, 변수 p에 각각 x와 y의 주소값이 담겨있으므로 출력하면 주소값들이 출력됨
// 주소값의 주소값은 그 변수의 값임. 따라서 변수 p의 주소값은 각각 x와 y의 값이 담겨있으므로 출력하면 변수의 값들이 출력됨
return 0;
}
// 실행 결과
// p = 1245052
// *p = 10
// p = 1245048
// *p = 20
주소 연산자와 내용 연산자¶
#include <stdio.h>
int main(void){
int i = 10;
int *p;
p = &i; // 주소 연산자
printf("i = %d\n", i);
*p = 20; // 내용 연산자
// 포인터를 통해서 변수의 값을 변경(p가 가리키는 위치에 20을 대입)
printf("i = %d\n", i);
// 변수 p는 i의 주소값을 담고 있음. 따라서 처음 printf 코드가 진행되어도 i의 원래 값에는 변화가 없는 것.
// *p는 내용 연산자. 따라서 p의 내용값, 즉 *p를 20으로 변화시키면 i의 값이 바뀜. 그래서 20으로 바뀐 i의 값이 출력됨
return 0;
}
// 실행 결과
// i = 10
// i = 20
간접 참조 연산자와 증감 연산자¶
- *p++;
- p가 가리키는 위치에서 값을 가져온 후에 p를 증가한다.
- (*p)++;
- p가 가리키는 위치의 값을 증가한다.
| 수식 | 의미 |
|---|---|
| v = *p++ | p가 가리키는 값을 v에 대입한 후에 p를 증가한다. |
| v = (*p)++ | p가 가리키는 값을 v에 대입한 후에 가리키는 값을 중기힌다. |
| v = *++p | p를 증가시킨 후에 p가 가리키는 값을 v에 대입한다. |
| v = ++*p | p가 가리키는 값을 가져온 후에 그 값을 증가하여 v에 대입한다. |
간접 참조 연산자와 증감 연산자의 예제¶
// 포인터의 증감 연산
#include <stdio.h>
int main(void){
int i = 10;
int *pi = &i;
printf("i = %d, pi = %p\n", i, pi);
(*pi)++; // (10)++ -> pi가 가리키는 값을 증가한다.
printf("i = %d, pi = %p\n", i, pi);
*pi++; // pi가 가리키는 위치에서 값을 가져온 후 (11)에 pi를 증가한다.
printf("i = %d, pi = %p\n", i, pi);
return 0;
}
// 출력 예시
// i = 10, pi = 000000FFEBCFF974
// i = 11, pi = 000000FFEBCFF974
// i = 11, pi = 000000FFEBCFF978
5절. swap()¶
swap() 함수 1 - 값에 의한 호출¶
#include <stdio.h>
void swap(int x, int y);
int main(void){
int a = 100, b = 200;
printf("a = %d\tb=%d\n", a, b);
swap(a, b);
printf("a = %d\tb=%d\n", a, b);
// x, y 값은 변화했지만 a, b 값은 변화하지 않았음
return 0;
}
void swap(int x, int y) {
int tmp;
printf(“x=%d\ty=%d\n”,x, y);
tmp = x;
x = y;
y = tmp;
printf(“x=%d\ty=%d\n”,x, y);
}
// 출력 예제
// a=100 b=200
// x=100 y=200
// x=200 y=100
// a=100 b=200
값에 의한 호출 : Why error?¶
swap() 함수 2 - 참조에 의한 호출¶
#include <stdio.h>
void swap(int *px, int *py);
int main(void){
int a = 100, b = 200;
printf("a = %d\tb=%d\n", a, b);
swap(&a, &b);
printf("a = %d\tb=%d\n", a, b);
return 0;
}
void swap(int *px, int *py) {
int tmp;
tmp = *px;
*px = *py;
*py = tmp;
}
// 출력 예제
// a=100 b=200
// a=200 b=100
6절. scanf()¶
scanf() 함수¶
- 변수에 값을 저장하기 위해 변수의 주소를 입력받는다.
7절. 매개 변수¶
배열 매개 변수¶
-
일반 매개 변수와 배열 매개 변수
-
why?
- 배열을 함수로 복사하려면 많은 시간이 소모되므로 배열은 주소만을 전달
-
배열 매개 변수는 포인터로 생각할 수 있다.
-
배열 a의 주소를 가리키고 있기 때문에 배열의 이름은 포인터이다.
- 함수를 호출할 때 배열을 전달하면 자동으로 배열의 주소가 전달된다.
- 배열의 이름은 배열을 가리키는 포인터와 동일하다.
- sub() 함수를 호출할 때 배열의 이름 a를 전달한다.
- sub() 함수의 매개 변수 b는 배열을 가리키는 포인터처럼 동작한다.
- b를 통하여 배열 원소에 접근하면 배열 a의 원소들이 접근된다.
포인터를 통한 배열과 매개 변수의 관계 예제¶
#include <stdio.h>
void sub(int b[], int n);
int main(void){
int a[3] = {1, 2, 3};
printf("%d %d %d\n", a[0], a[1], a[2]);
sub(a, 3);
// 배열 a를 sub() 함수로 전달
// 배열의 이름 == 배열의 주소
// 따라서 배열의 주소가 전달
printf("%d %d %d\n", a[0], a[1], a[2]);
return 0;
}
void sub(int b[], int n){
// 매개 변수 b는 전달된 배열을 가리키는 포인터
b[0] = 4;
b[1] = 5;
b[2] = 6;
// b[]를 통해 원본 배열 a[]가 변경
}
동일한 포인터 매개 변수 방법¶
포인터를 사용한 방법의 장점¶
- 포인터가 인덱스 표기법보다 빠르다.
- Why?
- 인덱스를 주소로 변환할 필요가 없기 때문이다.
- 컴파일러 최적화를 하면 성능은 비슷해진다.