배열 변수의 이름은 배열의 시작 주소
포인터는 일반 변수의 주소만 가질 수 있는 것이 아니라 배열과 같이 그룹으로 묶인 메모리의 주소도 가질 수 있다.
포인터 변수에 배열의 시작 주소를 대입할 때는 일반 변수와 마찬가지로 &연산자를 사용하면 된다.
배열의 경우에는 첫 요소인 data[0]의 시작 주소가 배열 전체의 시작 주소와 같기 때문에 다음과 같이 &연산자를 사용한다.
char data[4];
char *p = &data[0]; //배열의 첫 번째 항목의 주소가 배열 전체의 시작 주소와 같음
위 설명에서 사용한 &data[0]은 포인터 표기법을 사용하면 &*(data+0)과 같이 표기할 수 있다. 그리고 +0은 생략할 수 있기 때문에 & *data라고 적어도 된다.
char *p = &data[0]
//배열 표기법을 포인터 표기법으로 변경
char *p = & * (data + 0)
//0생략
char *p = & * data;
//번지(주소)를 얻는 &연산자와 번지(주소)를 지정하는 * 연산자는 서로 반대 개념의 연산자이기 때문에 서로 상쇄된다.
char *p = data;
*주의 &연산자와 *연산자는 연산자 우선순위가 같기 때문에 함께 사용하면 뒤쪽에서 앞쪽방향으로 연산이 수행된다.
그래서 &*data는 &(*data)와 같다. &(*data)의 의미는 data가 가리키는 대장(*data)의 주소를 얻겠다(&)는 뜻이다.
따라서 &(*data)는 data라고 적을 수 있다.
배열은 포인터가 아니다
배열이 포인터처럼 작동한다고 해서 완벽하게 포인터가 될 수는 없다. 문법적으로 포인터는 다른 변수의 주소를 저장할 수 있지만 배열은 컴파일러가 제공하는 메모리 그룹화 기술이다. 즉 배열 이름은 변수처럼 보이지만 내부를 들여다 보면 실제로는 상수화된 주소이기 때문에 변경할 수 없다는 뜻이다.
즉 배열은 일반 변수들을 묶어 놓은 개념이기 때문에 변수가 자신이 위치한 주소를 변경할 수 없듯이 배열도 자신이 위치한 메모리 주소를 변경할 수 없다. 따라서 배열의 시작도 주소도 변경이 불가능하다.
포인터로 배열의 주소를 저장하여 사용하기
배열은 해당 배열이 사용할 메모리 그룹의 시작 위치를 기준으로 색인 작업된 요소의 위치를 계산해 사용한다.
배열의 색인 작업도 연산이기 때문에 같은 요소를 반복적으로 사용하는 경우에는 효율이 떨어진다. 예를 들어 char형으로 선언한 data배열의 3번째 요소를 sum변수에 10번 더하는 코드이다.
char data[5] = {1, 2, 3, 4, 5};
int i, sum = 0, select = 2;
//sum 변수에 data[select] 값을 10번 더함. 즉 data[2] 요소 값을 10번 더하는 것과 같음
for(i = 0; i < 10; i++;) sum = sum + data[select]
단순하게 생각혀면 data[select] 요소를 sum에 10번 더하는 연산이라고 생각할 수 있다. 하지만 data[select] 요소를 사용하기 위해서는 내부적으로 data + select 연산을 해야 하기 때문에 + select 연산도 10번 수행하는 것이다. 위 코드에서 for 반복문을 포인터 표기법으로 변경해 보면 이렇게 반복 연산을 하는 상황을 좀 더 이해하기 쉬울 것이다.
for(i = 0; i < 10; i++) sum = sum + *(data + select);
*(data+2) 값은 3이기 때문에 for 반복문을 실행한 후 sum에 저장된 값은 30이다.
data[select]를 10번 반복해서 더하는 것처럼, 배열의 특정 요소들이 지속적으로 많이 사용되는 경우에는 좀 더 효율적인 표현을 위해 배열의 특정 요소 주소를 포인터 변수에 저장해 놓고 사용하기도 한다.
char data[5] = {1, 2, 3, 4, 5};
int i, sum = 0, select = 2;
char *p = data + select; //p는 data[select] 요소의 주소를 가지므로 char *p = &data[select]; 라고 사용한 것과 같다.
(배열의 첫번쨰 항목의 주소가 배열 전체의 시작 주소와 같다.)
for(i = 0; i < 10; i++;) sum = sum + *p;
위와 같이 코드를 구성하면 data[select] 요소의 주소를 포인터 p가 저장하고 있다. 따라서 data[select] 값을 나타내는
*p를 사용하여 sum에 10번 더하면 된다
포인터를 사용하여 배열의 각 요소에 저장된 값 합산하기(변형)
배열과 포인터의 합체
지금까지는 배열 문법과 포인터 문법은 서로 독립적인 문법처럼 설명했다. 소스 코드를 구성할 때 함께 사용한 적은 있지만 변수를 선언할 때에는 각자의 문법을 사용했다.
지금부터는 이 두 문법을 결합해서 사용하는 문법에 대해 소개하겠다.
char *p[5] //char *p1, *p2, *p3, *p4, *p5라고 선언한 것과 같음
위와 같이 선언하면 포인터가 5개 선언된 것이기 때문에 p 배열의 크기는 20바이트이다.
개별 포인터를 사용하고 싶다면 'p[0], p[1], p[2], p[3], p[4]'라고사용하면되고,각포인터가가리키는대상에값을읽거나쓰고싶다면앞에*연산자를추가하여 '*p[0], *p[1], *p[2], *p[3], *p[4]'라고 사용하면 된다.
char data1, data2, data3, data4, data5, i;
char *p[5] = {&data1, &data2, &data3, &data4, &data5};
for(i = 0; i <5; i++;) *p[i] =0; //data1~5에 모두 0이 저장됨
*포인터도 변수이기 때문에 배열을 이용하여 그룹으로 묶을 수 있다.
포인터를 기준으로 배열과 합체하기
char *p[5]; 의 *p에 괄호를 사용하여 오른쪽과 같이 변경하면 의미가 완전히 달라진다. 이렇게 선언해도 배열과 포인터가 결합되는 것인데, 이번에는 포인터 문법에 괄호를 사용했기 때문에 포인터가 기준이 된다.
char (*p)[5];
이렇게 선언하면 괄호 속에 있는 *p가 먼저 처리되기 때문에 p 변수는 배열이 아니라 포인터라는 뜻이다. 따라서 p 변수의 크기는 4 바이트이다. 그리고 그 다음 조건인 char[5]에 의해서 포인터 변수 p가 가리키는 대상의 크기가 5 바이트라는 뜻이 된다.
일반 포인터는 *p라고 적으면 자신이 가리키는 대상에 가서 값을 읽거나 쓸 수 있지만, 위 포인터 변수는 가리키는 대상이 배열 형식(char [5])으로 선언되어 있기 때문에 []를 사용하여 대상을 한 번 더 선택해야 한다. 예를 들어 p가 가리키는 대상의 5바이트 중 3번째 항목에 7을 넣고 싶다면 다음과 같이 사용한다.
(*p)[2] = 7;
그리고 포인터 변수 p는 다음과 같이 주소 연산을 하면 p에 저장된 주소가 5씩 증가하게 된다. p가 가리키는 대상의 크기가 char[5], 즉 5바이트이기 때문이다. 예를 들어 p에 100번지가 저장되어 있었다면 주소 연산후에 105번지가 된다.
char data[3][5];
char (*p)[5]; //char[5] 크기의 대상을 가리킬 수 있는 포인터를 선언함
p = data; //포인터 변수 p는 2차원 배열 data변수의 시작 주소를 저장함
(*p)[1] = 3; //p가 가리키는 대상의 2번째 항목에 3을 대입함 p[0][1];과 같음
(*(p+1))[2] = 4; //p+1이 가리키는 대상의 3번째 항목에 4를 대입함 p[1][2] = 4;와 같음
(*(p+2))[4] = 5; //p+2가 가리키는 대상의 5번째 항목에 5를 대입함 p[2][4] = 5;와 같음
코드를 더 간단하게 적으려면 포인터 표기법을 배열 표기법으로 변경하여 사용하면 된다.
'C언어' 카테고리의 다른 글
12-1) 동적 메모리 할당 및 해제 (0) | 2021.05.01 |
---|---|
12) 프로세스와 메모리 할당 (0) | 2021.04.10 |
11) 배열과 포인터 표기법 (0) | 2021.03.05 |
10-2) 표준입력함수 - scanf (0) | 2021.03.03 |
10) 표준 입력 함수 - getchar, gets (0) | 2021.02.25 |