without haste but without rest
[C] 포인터 본문
Reference - C언어 코딩도장
1. 포인터 기본
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
int* numPtr;
int num1 = 10;
int* numPtr2 = 20; // 바로 초기화해서 사용할 수도 있다.
numPtr = &num1;
*numPtr = 20;
printf("%d\n", *numPtr);
printf("%d\n", num1);
return 0;
}
포인터는 자료형에 *를 붙여서 선언한다. 포인터의 특징은 값부에 메모리 주소를 갖는다는 것이다. 위 코드에서 numPtr을 포인터로 선언했고 num1이라는 변수의 주소값을 저장했다. 즉 numPtr이라는 int형 포인터는 num1의 메모리 주소를 갖는다. (애초에 변수 할당하고 접근시 메모리 자체에 직접 액세스를 하는 것이 아니라 메모리 주소가 갖는 값부의 메모리 주소를 참조하므로 간접적이다.)
다음으로 포인터로 선언한 변수 앞에 *를 붙이는 것을 "역참조"라고한다. 포인터를 역참조하면 메모리 주소가 가진 변수를 반환한다. 위 코드에서 *numPtr은 값부로 num1의 메모리 주소를 갖고 따라서 num1의 값인 10을 리턴한다. 다음으로 *numPtr = 20 은 num1의 값부에 접근(역참조)해서 값을 변경하는 코드다. 따라서 num1의 값은 10에서 20으로 변경된다.
numPtr은 num1이 가르키는 메모리 주소를 똑같이 가르키고 있다. 위 코드에서 numPtr이 역참조를 통해서 메모리 주소가 갖는 값을 변경했다. 즉 num1 변수가 저장된 메모리에 접근해서 값을 변경했기 때문에 num1이 가진 값도 변경된 것이다.
2. 이중 포인터
포인터를 이중으로 만들 수도 있다. 아래 코드의 구조는 numPtr2가 numPtr1의 메모리 주소를 참조하고 다시 numPtr1이 num1의 메모리 주소를 참조하는 구조다. (3, 4중도 가능하다.)
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
int** numPtr1;
int** numPtr2;
int num1 = 10;
numPtr1 = &num1;
numPtr2 = &numPtr1;
printf("%d\n", **numPtr2);
return 0;
}
3. 포인터와 배열
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main() {
int size;
scanf("%d", &size);
int numArr[size];
return 0;
}
위 코드는 문제가 있는 코드다. (GCG에서는 지원하지만 Visual Studio에서는 지원하지 않는다.) 파이썬이 첫 언어인 사람들에게는 참 황당한 것들이 많다. 위처럼 유저 입력을 받아서 배열의 크기를 동적으로 지정할 수 없다.
배열의 크기를 동적으로 지정하려면 포인터를 선언해서 메모리를 할당한 뒤에 이를 배열처럼 사용해야 한다.
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
int main() {
int size;
scanf("%d", &size);
int* numPtr = malloc(sizeof(int) * size);
numPtr[0] = 10;
numPtr[1] = 15;
numPtr[9] = 20;
printf("%d\n", numPtr[0]);
printf("%d\n", numPtr[9]);
printf("%d\n", *(numPtr + 1));
free(numPtr);
return 0;
}
*포인터 연산: 위 코드에서 *(numPtr + 1) 은 numPtr[1]과 같은 값을 가져온다.
위 코드에서 메모리 얼로케이션 함수 안에 int 크기를 파라미터 값으로 주고, 여기에 입력받은 size 만큼을 곱해서 동적으로 크기를 할당했다. 그리고 배열처럼 인덱스에 접근하여 값을 할당한다.
size가 10이라고 가정해보면 numPtr 이 int 자료형 10개를 붙여서 만든 포인터고, 각각의 int 자리에 변수를 저장한다고 생각하면 이해하기 쉽다.
- 배열과 포인터를 활용한 배열의 차이점
int numArr[10]; // int형 요소 10개를 가진 배열 생성
int *numPtr = malloc(sizeof(int) * 10); // int 10개 크기만큼 메모리 할당
free(numPtr);
+ 2020-03-03 추가 내용
포인터와 배열에 대해서 파악하기 좋은 예제 코드
#include <stdio.h>
#define SIZE 6
void get_integer(int* list1) { // int* list1 == int list1[]
printf("주소= %p\n", list1);
printf("정수 6개 입력: ");
for (int i = 0; i < SIZE; ++i) {
scanf_s("%d", &list1[i]);
}
}
void print_integer(int* list) {
for (int i = 0; i < SIZE; ++i) {
printf("%d\n", list[i]);
}
}
int main() {
int A[SIZE];
get_integer(A);
print_integer(A);
return 0;
}
get_integer 함수에서 매개변수를 포인터로 받는 형태인데, 빈 배열을 매개변수로 받는 것과 동일하다. 둘다 주소값을 받는 것과 같다. 따라서 get_integer 함수에서 list1을 그대로 출력하면 배열이 아닌 주소값을 출력한다. 그리고 이는 &list1[0] 과 같다.
int* list1 == &list1[0]
매개변수인 list1은 주소값을 가르키지만, 인덱스로 접근하는 list1[i] 는 메모리 주소가 아니라 메모리에 담긴 값을 리턴한다. 처음에는 list1이 메모리 주소값을 가르키는데 list1[i] 는 왜 메모리 주소가 아닌 값을 그대로 리턴하는지 헷갈렸다.
* list1은 포인터 형태로 선언한 배열이고, 당연히 list1을 선언하면 주소를 반환한다.
바로 위에서 언급한 int* list1 == &list[0] 을 생각하면 쉽다. list1이 주소값이고 & 연산자를 붙인 &list1[0]도 주소값이다. 따라서 list1[i]에 & 연산자 (주소 연산자)를 붙인 &list[i]가 메모리 주소를 가르키므로 list[i]는 메모리 주소가 아니라 변수를 그대로 리턴한다라고 생각하면 이해하기 쉽다.
사용자 입력을 받을 때에는 메모리에 접근해서 값을 할당해줘야 하므로 sacnf_s 함수에서 주소값을 가르키는 배열의 인덱스는 &list1[i] 형태가 되어야 한다. 출력을 담당하는 print_integer 함수도 배열을 포인터 형태로 받는다. 단 이때는 메모리 주소가 아닌 메모리에 저장된 값이 필요하므로 list[i] 형태로 선언한다.
main 함수 코드를 보면 int A[SIZE] 라는 배열을 get_integer 함수의 파라미터로 삽입한다. 이때 배열은 배열 자체가 아니라 메모리 주소값을 제공한다. (이때 이 주소는 배열의 첫번째 인덱스 주소와 같다.)
* 그렇다면 이때 의문점이 생긴다.
왜 함수의 매개변수를 포인터 형태로 받아야 할까?
void swap(int x, int y) {
int temp = x;
x = y;
y = temp;
}
void ptrSwap(int* px, int* py) {
int temp = *px;
*px = *py;
*py = temp;
}
int main() {
int a = 10;
int b = 20;
swap(a, b);
printf("%d %d\n", a, b);
ptrSwap(&a, &b);
printf("%d %d\n", a, b);
return 0;
}
위 코드에서 swap 함수를 사용해도 a ,b 값은 변경되지 않는다. 왜냐하면 swap 함수에 파라미터로 주어지는 a, b 메모리 영역은 x, y의 메모리 영역과 별개다. 따라서 swap 함수 안에서 x 와 y의 값을 변경해주는 건 a, b의 메모리에 접근하지 않고 단순히 x, y의 값을 변경하고 함수를 종료하므로 의미가 없다.
그러므로 a, b 두 값을 스왑해주기 위해서는 함수에서 포인터를 활용해야한다. ptrSwap은 인트 포인터를 매개변수로 받는다. 즉 주소값을 매개변수로 받는다는 것이다. 따라서 int* px는 a, int* py는 b의 주소값을 가르킨다. 다음으로 px를 간접 참조해서 int temp에 값을 임시로 저장하고(이때는 메모리 주소가 아니라 변수다), *px의 값을 *py로 바꾼다. 즉 기존에 px (a) 의 주소값에 저장된 *px값을 *py 값으로 덮어씌워 저장한다는 것이다.
따라서 포인터를 활용하면 함수 안에서도 메모리에 접근해서 값을 변경할 수 있다.
4. 문자와 문자열 포인터
C언어는 문자 자료형 char 만 있고, 문자열을 저장하는 자료형이 없다. 따라서 문자열은 char 를 포인터 로 선언해서 사용한다.
* 왜 문자열은 포인터로 선언해줘야 할까?
character 자료형은 크기가 1 byte 다. 따라서 이 안에 문자를 배열 형태로 저장할 수가 없다. 따라서 char 를 포인터로 선언하고 메모리에 접근해서, 배열에 데이터를 저장하듯이 메모리 주소에 문자를 하나하나씩 저장한다. 이때 컴파일러가 알아서 메모리의 사이즈를 결정하고, 해당 변수는 읽기 전용으로 저장된다. 즉 수정이 불가능하다. 이를 문자열 리터럴이라고 한다. 만약 전체 문자열을 수정 가능한 형태로 저장하고 싶다면 동적 할당을 활용하면 된다. (배열을 이용한 경우 한글자 한글자씩 접근해서 수정해야 한다.)
C에서 문자열을 저장하는 방식은 아래와 같다. 메모리를 배열삼아 문자를 저장한다. 출력시 주소 값 22부터 27까지 순차적으로 출력하는 방식이다.
문자열 리터럴 선언
#include <stdio.h>
int main() {
char c1 = 'a';
char* s1 = "Hello";
printf("%c\n", c1);
printf("%s\n", s1);
return 0;
}
* 문자는 c, 문자열은 s로 출력한다.
* C 언어의 문자열은 마지막에 항상 NULL값이 붙는다. printf는 문자열을 출력할 때, 배열에서 문자를 계속 출력하다가 NULL에서 끝낸다.
문자열 포인터도 인덱스로 접근할 수 있다.
#include <stdio.h>
int main() {
char* s1 = "Hello";
printf("%c\n", s1[0]); // 문자를 출력하므로 포맷을 %c로 지정한다.
printf("%c\n", s1[3]);
return 0;
}
배열 형태로 문자열을 선언할 수도 있다. 단 아래와 같은 경우 배열의 크기는 10인데 문자열은 턱없이 작으므로 뒤에 남는 범위는 모두 NULL값으로 처리가 됨을 유의한다. 또한 인덱스로 배열에 접근해서 해당 인덱스에 해당하는 문자를 수정할 수도 있다.
#include <stdio.h>
int main() {
char s1[10] = "Hello";
printf("%s\n", s1);
s1[1] = 'I';
printf("%s\n", s1);
return 0;
}
* 단 strlen() 함수로 문자열의 길이를 출력하면 NULL값을 제외하고 출력한다.
'ProgrammingLanguage > C' 카테고리의 다른 글
[C] 공용체 (0) | 2020.02.29 |
---|---|
[C] 구조체 (0) | 2020.02.27 |
[C] sprintf / 버퍼 (0) | 2020.02.26 |
[C] 문자열 관련 함수 (0) | 2020.02.26 |
[C] 데이터 입출력 (0) | 2020.02.26 |