Part 10 포인터 ©우균, 창병모 이 슬라이드는 부산대학교 우균이 작성하였습니다. 오류나 수정할 사항 있으면 woogyun@pusan.ac.kr로 연락 주세요.
이 장의 내용 포인터란 포인터 인수 전달 배열과 포인터 더 복잡한 선언문 typedef void 포인터 함수 포인터
10.1 포인터란
메모리 구조 컴퓨터 메모리 메모리 셀 자체 정보 메모리 셀이 연속적으로 나열된 형태 각 메모리 셀에는 주소가 부여되어 있음 데이터는 각 데이터 크기에 필요한 만큼 메모리 셀을 차지함 메모리 셀 자체 정보 어떤 데이터 a가 차지하는 메모리 셀 개수는 sizeof(a)로 알 수 있음 메모리 셀 주소는?
포인터 포인터(pointer)란? 간접참조(indirection, dereferencing) 데이터를 저장하기 위해 할당된 메모리 공간의 주소 포인터 상수(pointer constant): 메모리 주소 값 포인터 변수(pointer variable): 주소 값을 저장할 수 있는 변수 데이터에는 자료형이 연관되어 있으므로 포인터에도 자료형이 연관됨(예: int 포인터, double 포인터 등) 간접참조(indirection, dereferencing) 포인터가 가리키는 곳을 따라가 연관된 데이터 혹은 그 데이터가 저장된 공간을 참조하는 것 간접참조한 데이터의 자료형은 포인터 자료형을 이용하여 판단함 포인터 간접참조 int 포인터 상수 1024 int형 데이터 double 포인터 변수 1052 double형 데이터
포인터 선언과 사용 포인터 선언 형식 포인터 사용 예 포인터와 일반 변수를 함께 선언 자료형 *포인터변수; int one = 1; // int 변수 int *to_one; // int 포인터 변수 to_one = &one; // to_one은 one을 가리킴 one = one + 1; // one ≡ 2 one = *to_one + 1; // one ≡ 3 *to_one = one + 1; // one ≡ 4 포인터와 일반 변수를 함께 선언 int one, *to_one; int one, *to_one = &one; // 선언과 함께 초기화! 1 one 3 2 4 to_one
toOne.c 간접참조 수식 *to_one의 의미는 대입 연산자의 어느 쪽에 있느냐에 따라 정해짐 왼편: to_one이 가리키는 곳에 할당된 변수(int 변수) 실행결과: one = 1 one = 2 one = 3 one = 4
포인터 제1법칙(law1.c) 포인터 제1법칙 *(& a) ≡ a 포인터 제1법칙의 의미 주소연산을 취한 결과에 간접참조연산을 취하면 원래 변수와 같다 실행결과: 숫자 n을 입력해 주세요. 275 n = 275 *&n = 275
10.2 포인터 인수전달
포인터 인수전달 포인터를 인수로 전달하면 포인터 인수 전달을 사용하는 이유 포인터를 간접참조함으로써 해당 포인터가 가리키는 데이터 값을 변경할 수 있다 호출된 함수에서 호출한 함수의 변수 값을 변경할 수도 있다 포인터 인수 전달을 사용하는 이유 호출된 함수의 ‘부수효과(side-effect)’로서 호출한 함수의 변수 값을 변경하기 위해 인수로 전달할 데이터의 크기가 매우 큰 경우에 인수전달 효율을 높이기 위해 인수 데이터 크기가 큰 경우의 예: 배열, 구조체 사실 C에서 배열은 항상 포인터를 통해 전달된다.
변수 값 교환 프로그램(swap0.c) 이 부분을 함수로 만들어 봅시다! 실행결과: 변수 값을 순서대로 출력하면 2, 3 입니다. 변수 값을 순서대로 출력하면 3, 2 입니다.
변수 값 교환 함수?(swap1.c) 교환되지 않은 이유? swap의 a, b는 main의 a, b가 아니다. main의 a, b의 사본일 뿐이다. 실행결과: 변수 값을 순서대로 출력하면 2, 3 입니다.
변수 값 교환 함수(swap2.c) swap에서 main의 a, b의 주소를 알고 있으므로 이들 변수 값을 변경할 수 있다. 실행결과: 변수 값을 순서대로 출력하면 2, 3 입니다. 변수 값을 순서대로 출력하면 3, 2 입니다.
10.3 배열과 포인터
배열과 포인터 배열 이름을 포인터에 저장할 수 있음 포인터 가감연산 배열 이름은 배열이 할당된 공간의 주소이므로 포인터 상수임 따라서 배열 이름을 포인터 변수에 저장할 수 있음 이 때, 포인터의 타입은 배열 원소를 가리킬 수 있는 타입이어야 함 포인터 가감연산 포인터에 가감연산을 취하면 포인터가 가리키는 자료의 크기 단위로 포인터 값이 증감함 포인터 가감 연산 예: int a[10], *p = a; p += 2; 포인터 p 값은 2만큼 증가하는 것이 아니라 2 * sizeof(int)만큼 증가함
arrayName.c char형 배열 이름은 char형 포인터에 저장할 수 있다.
ptrIncr.c 실행결과: sizeof(int) = 4 a + 0 = 0012FF58 a + 1 = 0012FF5C b - 2 = 0012FF60 b - 1 = 0012FF64
포인터를 통한 배열원소 참조 배열 첨자연산대신 포인터 간접참조연산을 사용할 수 있다. 실행결과: I am just a poor boy ~
포인터 제2법칙(law2.c) 포인터 제2법칙 p[n] ≡ *(p + n) 포인터 제2법칙의 의미 배열 첨자연산은 본질적으로 포인터 연산이다. 실행결과: I am just a poor boy ~
포인터 상수에도 적용됨 포인터 제2법칙은 포인터 상수에도 똑같이 적용된다. 배열이름은 포인터 상수 실행결과: I am just a poor boy ~ 배열이름은 포인터 상수
배열 인수 전달 복습 배열 형태로 선언한 매개변수에는 배열 이름(포인터 상수)이 전달되므로 사실 포인터임 따라서 배열 매개변수 선언 시 크기 선언은 중요하지 않음 T p[250] ≡ T p[3] ≡ T p[] 배열 형태지만 사실은 char 포인터임 실행결과: I am just a poor boy ~
포인터다운 배열 참조 방식 배열 시작 위치를 기억할 필요가 없다면 포인터를 증가시켜가며 배열 원소를 참조할 수 있다. p가 가리키는 원소가 '참'이면 반복. 즉 p가 가리키는 원소가 '\0'이 아니면 반복. p가 가리키는 원소를 참조한 후 p를 1 증가시킴 실행결과: I am just a poor boy ~
10.4 더 복잡한 선언문
포인터의 포인터 int 포인터의 포인터를 한 번 따라가면 int 포인터를 얻을 수 있다. ppi는 포인터 변수를 가리킬 수 있는 포인터 실행결과: i = 5 *pi = 5 **ppi = 5 **ppi = 12345 *pi = 12345 i = 12345
C 선언문에 내재된 철학 사용 형태대로 선언한다. 선언문을 다시 살펴봅시다. int i, a[10], *p; i는 int형이므로 정수를 저장할 수 있다. i = 5; a에 첨자연산을 취한 a[…] 형태는 int형이다. 그래서 a는 int 배열형이다. a[0] = 5; p에 간접참조연산을 취한 *p 형태는 int형이다. 그래서 p는 int 포인터형이다. *p = 5;
더 복잡한 선언문 이해 포인터의 배열 배열 포인터 int *a[3], *pi; // *(a[3])과 같음 a에 첨자연산을 취한 a[…] 형태가 int 포인터이므로 a는 포인터의 배열이다. 배열 포인터 int (*q)[3], ai[3]; q에 간접참조연산을 취한 *q 형태가 int 배열이므로 q는 int형 배열에 대한 포인터다. 정수 포인터의 배열 q
다시 보자! 매개변수 자료형 매개변수에서 배열 형태는 사실 포인터다. 좀더 복잡한 경우는? void print(int a[]); int ai[10];으로 선언된 ai를 전달할 수 있음 int 배열이름을 전달할 수 있으므로 int 포인터 좀더 복잡한 경우는? void print(int *x[]); // int **x;와 같음 int *p[10];으로 선언된 p를 전달할 수 있음 int 포인터 배열을 전달할 수 있으므로 int 포인터의 포인터 void print(int y[][10]); // int (*y)[10];과 같음 int a[5][10];으로 선언된 a를 전달할 수 있음 int 배열의 배열을 전달할 수 있으므로 int 배열 포인터 여기서 두 번째 크기 10은 생략할 수 없다!
10.5 typedef
typedef typedef는… typedef 사용 예 자료형 별칭(type alias)을 정의하는데 사용되는 키워드 복잡한 자료형을 간단한 이름으로 지칭할 수 있음 선언문 앞에 typedef를 붙이면 선언하는 이름은 타입 이름이 됨 typedef 사용 예 int에 대한 타입 별칭 int i; // i는 int 변수 typedef int iType; // iType은 int 타입의 다른 이름 int 포인터에 대한 타입 별칭 int *p; // p는 int 포인터 변수 typedef int *pType; // pType은 int 포인터 타입 pType q = &i; // q는 pType, 즉 int 포인터
typedef.c 이렇게 선언한 뒤에는 IntType ≡ int RealType ≡ double 이다. 따라서 변수 선언은 물론, 자료형 변환에도 사용할 수 있다. 실행결과: 5 + 7 = 12 5 / 7 = 0.714286
복잡한 타입을 typedef로… typedef를 이용하면 복잡한 타입을 쉽게 다룰 수 있음 배열 포인터 선언 예 int (*q1)[20]; typedef를 이용하여 단계적으로 선언하는 예 typedef int AI[20]; AI *q2; // q1과 q2모두 크기 20인 int 배열에 대한 포인터임
10.6 void 포인터
void 포인터 이 부분을 함수로 바꾸면… void는 ‘없음(nothing)’ void*는 ‘아무것이나 가리키는 포인터(pointer to anything)’ 그래서 void*를 ‘범용 포인터(generic pointer)’라고 부르기도 한다. 이 부분을 함수로 바꾸면… 실행결과: i의 주소 = 0012FF74 f의 주소 = 0012FF78
void 포인터에 대한 간접참조 void 포인터는 간접참조할 수 없음 간접참조하려면 명시적으로 형변환을 수행해야 함 실행결과: f = 3.141592
10.7 함수 포인터
함수 포인터 함수 포인터(function pointer)란? 함수 포인터 활용 예 함수를 가리키기 위한 포인터 함수 포인터를 이용하여 함수를 호출할 수 있음 함수 포인터로 함수를 호출할 때에는 간접참조하지 않아도 됨 리턴타입 뿐만 아니라 매개변수 자료형도 함수의 타입에 해당함 함수 포인터 활용 예 int (*fp)(int); int add1(int x) { return x + 1; } void prInt(int x) { printf("%d", x); } int add(int x, int y) { return x + y; } ... fp = add1; // prInt는 저장불가(리턴타입이 다름) // add는 저장불가(매개변수 타입이 다름) two = fp(1); // add1을 호출하게 됨
funPtr.c 실행결과: 자연수 하나를 입력하세요. 64 팔진표기: 100 십진표기: 64
average.c (1/2) 산술평균 조화평균 기하평균 세 함수의 타입이 모두 같다는 사실에 주의하자. a_mean: double × double double h_mean: double × double double g_mean: double × double double
average.c (2/2) 함수 포인터 배열 fun 실행결과: 평균을 구할 두 수를 입력해 주세요. 5 10 어떤 평균을 구하고 싶으신가요? 1. 산술평균 2. 조화평균 3. 기하평균 번호를 입력해 주세요. 1 선택하신 산술평균은 7.500000
Key Point
Key Point 1 포인터는 메모리에 부여된 주소를 나타낸다. 주소 ‘값’을 나타낼 때는 포인터 상수라고 하고 주소를 저장할 수 있는 ‘변수’를 나타낼 때는 포인터 변수라고 한다. 연산자 *가 단항 연산자로 사용될 경우에는 간접참조 연산자로 사용된다. 포인터 p에 대하여 *p는 p가 가리키는 메모리 구획(변수)를 나타낸다. 임의의 변수 a에 대하여 다음과 같은 법칙이 성립한다. *(& a) ≡ a 피호출자에서 호출자의 변수를 변경하려면 변경하고자 하는 변수의 포인터를 넘겨주어야 한다. 배열 이름은 포인터 상수이므로 포인터 변수에 저장할 수 있다.
Key Point 2 T*형 포인터를 1만큼 증가시키면 실제 포인터 값은 sizeof(T)만큼 증가한다. 임의의 포인터 p와 정수 n에 대하여 다음 등식이 성립한다. p[n] ≡ *(p + n) 복잡한 자료형에 대한 선언문은 typedef를 이용하면 이해하기 쉽다. void 포인터는 아무 대상이나 다 가리킬 수 있지만 간접참조 할 수 없다. void 포인터를 간접참조하려면 먼저 다른 포인터 형으로 형 변환을 수행해야 한다.
Key Point(고급 주제) 포인터의 포인터는 포인터를 가리키는 포인터이므로 포인터의 포인터를 간접참조하면 포인터를 얻을 수 있다. C 언어 선언문의 가장 큰 특징은 사용 형태대로 선언한다는 것이다. 자료형 T를 이용한 다음 선언문에서 a와 b는 모두 포인터의 배열이다. T *a[M], *(b[N]); 자료형 T를 이용한 다음 선언문에서 a는 배열 T[M]에 대한 포인터다. T (*a)[M]; 함수 포인터란 함수를 가리키기 위해 사용되는 포인터로서 함수 선언 형태에서 ‘함수명’ 대신 ‘(*함수포인터명)’을 사용함으로써 선언할 수 있다.
요약(1/3) 메모리 구조 포인터 포인터 연산자 포인터 제1법칙: *(&v) ≡ v 일정한 크기의 메모리 셀이 연속되어 나열되어 있음 각 메모리 셀에는 주소가 부여되어 있음 데이터는 크기에 따라 필요한 개수만큼 메모리 셀을 차지함 포인터 주소를 나타내는 자료 또는 그러한 자료형 포인터가 가리킬 데이터의 자료형 정보도 가지고 있음 포인터 연산자 주소 연산자(&): 변수의 주소를 돌려줌 간접참조 연산자(*): 포인터가 가리키는 위치 또는 그 위치의 데이터를 돌려줌 포인터 제1법칙: *(&v) ≡ v
요약(2/3) 포인터 인수 전달 포인터와 배열 연동 포인터 제2법칙: p[i] ≡ *(p + n) C 선언문의 철학 포인터를 인수로 전달하면 호출된 함수에서 호출한 함수의 변수 내용을 변경할 수 있음 크기가 큰 데이터(배열, 구조체 등)는 포인터로 전달하면 효율적임 포인터와 배열 연동 배열 이름은 포인터 상수이므로 포인터 변수에 저장 가능 포인터는 배열 원소에 대한 포인터형이어야 함 포인터 증감연산은 가리키는 데이터의 크기만큼 증감됨 포인터 제2법칙: p[i] ≡ *(p + n) C 선언문의 철학 사용하는 형태대로 선언함 예: T *(a[10]);는 포인터의 배열 T (*p)[10];는 배열 포인터
요약(3/3) typedef void 포인터 함수 포인터 타입 별칭(type alias)을 생성함 void는 "아무것도 아닌 것(nothing)"을 나타내지만 void*는 "임의의 것(anything)"을 가리킬 수 있음(generic pointer) void 포인터는 간접참조할 수 없으므로 void 포인터를 간접참조하려면 먼저 자료형 변환을 수행해야 함 함수 포인터 함수를 가리키는 포인터 매개변수 개수 및 자료형, 리턴타입 등이 같은 함수만 저장 가능 함수 포인터로 함수를 호출할 때에는 간접참조 연산을 생략할 수 있음
프로그래밍 실습
▶ 프로그래밍 실습 1 명령줄 인수를 처리하는 프로그램 Do.c를 작성하여라. Do.c의 실행 파일을 Do(혹은 Do.exe)라고 하면 Do는 자신을 포함하여 문장을 입력으로 받는다. 그리고 마치 그 문장을 이해하고 답변하는 것처럼 반응한다. 예를 들면, 다음과 같다. > Do you have match? Yes, I have match. > Do you like C language? No, I don't like C language. 긍정적인 답변을 할 것인지 부정적인 답변을 할 것인지는 무작위로 결정하라. 무작위로 결정하기 위해 rand()와 srand()를 활용하라.
▶ 프로그래밍 실습 2 암호화 알고리즘 중에는 “증분 k(displacement k)” 알고리즘이 있다. 예를 들어, k = 3인 경우에는 각 알파벳을 3개 뒤로 미룬다. 즉 a는 d로 b는 e로 변경한다. z 다음의 알파벳은 다시 a가 오는 것으로 가정한다. 이런 방식으로 암호화하면 k = 3일 때 문자열 "Joy of C programming" 은 "Mrb ri F surjudpplqj" 이 된다. 사용자로부터 k를 입력 받은 후 그 다음 행부터 주어지는 문자열을 암호화하는 증분 k 암호화 프로그램을 작성하라. k는 0이상 25이하의 수라고 가정하자.