C 3주차 - 포인터 (2)

Gunter·2024년 3월 14일
0

C

목록 보기
9/13

배열과 포인터

 

배열과 포인터 표기법

배열은 [ ] 연산자를 사용하여 표현하고 포인터는 * 연산자를 사용하여 나타낸다.

배열 -> char data[5];
포인터 -> char *p;


두 문법은 표기만 다를 뿐 문법 구조는 비슷해서, 위와 같이 두 문법의 표기법을 바꿔서 사용할 수도 있다.











배열 표기법의 한계

data라는 1차원 배열을 선언하고 각 요소를 값 0x12345678로 초기화했다.

int data[2] = {0x12345678, 0x12345678};


위와 같이 선언한 data 배열에서 data[0] 요소는 총 4바이트로 구성되어 있다.

첫 번째 바이트에 있는 값 0x78을 0x22로 변경하고 싶어서 배열 표기법에 0x22를 대입하면 어떻게 될까?

 


배열 표기법으로 'data[0]'이라고 적으면 4바이트 크기의 메모리를 의미하기 때문에 0x22를 대입한다고 해서 data[0]의 일부 값만 변경되는 것이 아니다.

위 그림처럼 4바이트 값이 모두 변경되어 버려어 data[0]에는 0x00000022 값을 대입한 것과 같다.
배열 표기법은 요소를 구성하는 모든 바이트 값을 한 번에 수정한다.

 



배열 표기법 대신 포인터 표기법을 사용하면

*(data + 1) = 0x22;

이렇게 변경하면 다음과 같이 * 연산자와 (data + 1) 사이에 형 변환 문법을 사용가능

*(char *)(data + 1) = 0x22;   /* 일시적으로 int* 형을 char* 형으로 변환함 */


int *형이 char 형으로 변경된다는 것은 포인터가 가리키는 대상의 크기가 4바이트에서 1바이트로 변경된다는 의미다.
따라서 위 그림처럼 data[1] 영역의 첫 1바이트만 0x22 값으로 변경되는 것이다.
포인터 표기법은 배열 항목의 크기와 상관없이 자유롭게 값을 수정할 수 있다.

 








배열 시작 주소

포인터는 일반 변수의 주소만 가질 수 있는것이 아니라 배열과 같이 그룹으로 묶인 메모리의 주소도 가질 수 있다.
포인터 변수에 배열의 시작 주소를 대입할 땐 일반 변수와 마찬가지로 & 연산자를 사용하면 된다.

그런데 배열의 경우엔 첫 요소인 data[0]의 시작 주소가 배열 전체의 시작 주소와 같기 때문에 아래와 같이 &연산자를 사용한다.

char data[4];
char *p = &data[0];  /* 배열의 첫 번째 항목의 주소가 배열 전체의 시작 주소와 같음 */

 


위 설명에서 사용한 &data[0]은 표인터 표기법을 사용하면 &*(data + 0)과 같이 표기할 수 있다.


+) & * data 의 의미는 무엇일까요?

 









배열과 포인터 표기법

배열은 해당 배열이 사용할 메모리 그룹의 시작 위치를 기준으로 색인 작업된 요소의 위치를 계산해 사용한다.

배열의 색인 작업도 연산이기 때문에 같은 요소를 반복적으로 사용하는 경우에 효율이 떨어진다.

 

char data[5] = {1, 2, 3, 4, 5};
int i, sum = 0, select = 2;

for(i = 0 ; i < 10 ; i++) sum = sum + data[select]; 
/* sum 변수에 data[select] 값을 10번 더함. 즉, data[2] 요소 값을 10번 더하는 것과 같음. */

위는 char형으로 선언한 data 배열의 3번째 요소(data[2])를 sum 변수에 10번 더하는 코드이다.

단순하게 생각하면 data[select] 요소를 sum에 10번 더하는 연산이라고 생각할 수 있지만, data[select] 요소를 사용하기 위해서는 내부적으로 data + select 연산을 해야하기 때문에 + select 연산도 10번 수행하는 것이다.

for(i = 0 ; i < 10 ; i++) sum = sum + *(data + select);

위와 같이 표현할 수도 있다.
(for 반복문을 실행한 수 sum에 저장된 값은 30)

 

char data[5] = {1, 2, 3, 4, 5};
int i, sum = 0, select = 2;
char *p = data + select;
for(i = 0 ; i < 10 ; i++) sum = sum + *p; 

위는 좀 더 효율적인 코드 구성이다.
data[select] 요소의 주소를 포인터 p가 저장하고 있으므로, data[select] 값을 나타내는 *p를 사용하여 sum에 10번 더하면 된다.

 

/* 예제 : 포인터를 사용하여 배열의 각 요소에 저장된 값 합산하기 */
#include <stdio.h>
void main(){
	char data[5] = {1, 2, 3, 4, 5};
    int result = 0, i;  /* 합산에 사용할 result 변수는 0으로 초기화 */
    char *p = data;   /* data 배열의 시작 위치를 포인터 변수 p에 저장 */
    
    /* 5번 반복하면서 포인터 변수 p를 사용해 배열의 각 요소를 result 변수에 합산 */
    for(i = 0 ; i < 5 ; i++){
    	result = result + *p; /* 포인터 변수 p가 가리키는 대상의 값을 result에 합산 */
        p++;  /* data 배열의 다음 항목으로 주소를 이동 : data[0] -> data[1] -> ... */
        }
        
    printf("data 배열의 각 요소의 합은 %d 이다.\n", result);
}

 










배열과 포인터의 합체

 

배열을 기준으로 포인터와 합체하기

포인터도 변수라서 배열을 이용하여 그룹으로 묶을 수 있다.

char *형 포인터 변수가 3개 필요하다면 아래와 같이 선언해서 포인터 변수를 만든다.

char *p1, *p2, *p3;

포인터가 100개가 필요하다면 하나씩 나열하는 것은 불편하기 때문에 아래와 같이 선언할 수 있다.

char *p[5]; /* char *p1, *p2, *p3, *p4, *p5; 라고 선언한 것과 같음 */

위와 같이 선언하면 포인터가 5개 선언된 것이기 때문에 p 배열의 크기는 20바이트 이다.(포인터 변수는 주소를 저장하기 때문에 크기가 4바이트 * 5)

개별 포인터를 사용하고 싶다면 p[0], p[1], p[2], p[3], p[4] 라고 사용하면 되녹, 각 포인터가 가리키는 대상에 값을 읽거나 쓰고 싶다면 앞에 * 연산자를 추가하면 된다.

 




포인터를 기준으로 배열과 합체하기

char *p[5]의 *p에 괄호를 사용하여 아래와 같이 변경하면 의미가 달라진다.

char (*p)[5];

포인터 문법에 괄호를 사용했기 때문에 포인터가 기준이 되는데, 이렇게 선언하면 괄호 속에 있는 *p가 먼저 처리되기 때문에 p 변수는 배열이 아니라 포인터라는 뜻이다.

따라서 p변수의 크기는 4바이트이다. 그리고 그 다음 조건인 char[5]에 의해서 포인터 변수 p가 가리키는 대상의 크기가 5바이트라는 뜻이 된다.

일반 포인터는*p라고 적으면 자신이 가리키는 대상에 가서 값을 읽거나 쓸 수 있지만, 위 포인터 변수는 가리키는 대상이 배열 형식(char[5])으로 선언되어 있기 때문에 [ ]를 사용해 대상을 한번 더 선택해야 한다.

 

그리고 포인터 변수 p는 다음과 같이 주소 연산을 하면 p에 저장된 주소가 5씩 증가하게 된다. p가 가리키는 대상의 크기가 char[5], 즉 5바이트 이기 때문이다.

p++; /* p = p + 1; 과 같으므로 주소가 5가 증가함 */

위와 같은 포인터는 2차원 배열을 가리키는 용도로 쓰기에 적합하다.

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;과 같음

 













이중 포인터

이중 포인터(double pointer)는 포인터의 포인터를 의미한다. 즉, 포인터가 가리키는 대상이 또 다른 포인터인 경우를 말한다. 이중 포인터는 주로 동적으로 할당된 2차원 배열을 다루거나, 포인터를 함수의 인자로 전달할 때 사용된다.

int **pp; // 이중 포인터 선언

int x = 10; // 정수 변수 선언
int *p = &x; // 포인터 선언 및 초기화

pp = &p; // 이중 포인터에 포인터의 주소 할당

위의 코드에서 pp는 이중 포인터로, 포인터 p의 주소를 저장한다. 따라서 *pp를 통해 p를 참조하고, **pp를 통해 p가 가리키는 x를 참조할 수 있다.

 














연습해보기

// Call by Value, Call by Reference 개념을 생각하며 다음과 같은 코드를 이해해봅시다.
void swap(int *a, int *b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

int main() {
    int x = 5, y = 10;
    printf("x: %d, y: %d\\n", x, y);
    swap(&x, &y);
    printf("x: %d, y: %d\\n", x, y);
    return 0;
}


x: 5, y: 10
x: 10, y: 5

 

// 아래의 코드의 실행 결과를 예상하여 봅시다
int main() {
	char m[] = "ABC";
	char* ap = m;
	*ap++ += 1;		
	*++ap += 3;		
	ap -= 2;		
	ap[1] += 2;		
	puts(m);		
}

BDF

  1. char m[] = "ABC"; : 문자열 "ABC"를 선언합니다.
  2. char* ap = m; : 포인터 ap를 문자열 m의 첫 번째 문자 'A’를 가리키도록 합니다.
  3. *ap++ += 1; : 현재 ap가 가리키는 문자 'A’에 1을 더하고, ap를 다음 문자 'B’로 이동시킵니다. 따라서 문자열은 "BBC"가 됩니다.
  4. *++ap += 3; : ap를 다음 문자 'C’로 이동시키고, 그 문자에 3을 더합니다. 따라서 문자열은 "BBF"가 됩니다.
  5. ap -= 2; : ap를 두 문자 앞으로 이동시켜 'B’를 가리키게 합니다.
  6. ap[1] += 2; : ap에서 한 문자 뒤인 'B’에 2를 더합니다. 따라서 문자열은 "BDF"가 됩니다.
  7. puts(m); : 변경된 문자열 "BDF"를 출력합니다.

 

// 아래의 코드의 실행 결과를 예상하여 봅시다
int main() {
	short m[2][2] = { {10,20},{30,40} };
	short a, sum = 0;
	*m[0] = 15;
	*(m[0] + 1) = 25;
	*m[1] = 35;
	m[0][3] = 45;
	
	**m = 17;
	*(*(m + 1) + 1) = 47;
	*(*m + 1) = 27;
	**(m + 1) = 37;
	m[1][-1] += 2;
	for ( a = 0; a < 4; a++)
	{
		sum += m[0][a];
	}

	printf("%d\n", sum);
}

130

  1. m[2][2] 초기화: {{10,20},{30,40}}
  2. *m[0] = 15; - m[0][0]의 값을 15로 변경합니다. 행렬은 이제 {{15,20},{30,40}}입니다.
  3. *(m[0] + 1) = 25; - m[0][1]의 값을 25로 변경합니다. 행렬은 {{15,25},{30,40}}입니다.
  4. *m[1] = 35; - m[1][0]의 값을 35로 변경합니다. 행렬은 {{15,25},{35,40}}입니다.
  5. m[0][3] = 45; - 이 부분은 조금 주의가 필요합니다. m[0][3]은 실제로 m[1][1]의 위치와 같습니다. 따라서 m[1][1]의 값을 45로 변경합니다. 행렬은 {{15,25},{35,45}}입니다.
  6. **m = 17; - 이는 m[0][0]의 값을 17로 변경합니다. 행렬은 {{17,25},{35,45}}입니다.
  7. *(*(m + 1) + 1) = 47; - 이는 m[1][1]의 값을 47로 변경합니다. 행렬은 {{17,25},{35,47}}입니다.
  8. *(*m + 1) = 27; - 이는 m[0][1]의 값을 27로 변경합니다. 행렬은 {{17,27},{35,47}}입니다.
  9. **(m + 1) = 37; - 이는 m[1][0]의 값을 37로 변경합니다. 행렬은 {{17,27},{37,47}}입니다.
  10. m[1][-1] += 2; - 이는 m[0][1]의 값을 2 증가시킵니다. 즉, m[0][1]이 27에서 29로 변경됩니다. 행렬은 {{17,29},{37,47}}입니다.
  11. 이제 for 루프를 통해 m[0]의 모든 요소의 합을 구합니다. 하지만 주의할 점은 m[0][3] (즉, m[1][1])까지 포함한다는 것입니다. 따라서, sum17 + 29 + 37 + 47 = 130이 됩니다.

0개의 댓글