배움 저장소
홍정모의 따라하며 배우는 C언어 10.배열과 포인터 본문
10.1 배열과 메모리
- 배열의 인덱스는 (해당 값의 주소 - 첫 값의 주소) / sizeof(자료형)과 같다
- 배열의 이름은 첫 값의 주소를 가리킨다. 따라서 Ampersand(주소 연산자) 없이도 포인터에 할당할 수 있다.
- 배열의 이름이 첫 값의 주소를 가리키는 이유는 계산 과정에서 포인터로 형변환 되기 때문이다.
- 배열의 이름은 L Value로 저장공간을 차지한다.
10.2 배열의 기본적인 사용방법
int nums[3] = {0,1,2}; // O : Initialization
nums[3] = {10, 11, 12}; // X : Error, unexpected behavior
초기화 할 때만 나열한 된 값을 배열에 할당할 수 있다. 그 외에는 불가능하다.
int nums[3] = {0,1,2};
printf("Array Pointer's add:\t%p \n", &nums);
printf("Array Pointer's val:\t%p \n", nums);
printf("First Element's add:\t%p \n", &nums[0]);
for(int i=0; i<3; ++i) // 32bit : casting to (int)
printf("%lld\n", (long long)&nums[i]); // 64bit : casting to (long long)
printf("\n");
출력값
>> 15727400
>> 15727404 Diff = 4byte
>> 15727408 Diff = 4byte
/* Fully Initialized */
int nums0[3] = {10,11,12};
for(int i=0; i<3; ++i)
printf("%d ", nums0[i]);
printf("\n");
/* Not Initialized */
int nums1[3]; // => Trash Value
for(int i=0; i<3; ++i)
printf("%d ", nums1[i]);
printf("\n");
/* Partially Intialzied */
int nums2[3] = {10}; // => 10, 0, 0
for(int i=0; i<3; ++i)
printf("%d ", nums2[i]);
printf("\n");
부분적으로 값을 초기화했을 때 나머지 값은 0이 된다.
/* Omitting Size */
int nums3[] = {10, 11, 12, 13, 14};
int count = sizeof(nums3) / sizeof(nums3[0]);
// or sizeof(nums3) / sizeof(int);
for(int i=0; i<count; ++i)
printf("%d ", nums3[i]);
printf(" size=%d \n", count);
/* Designated Initializers */
int nums3[] = {10, [2]=12, 13, [5]=15};
int count = sizeof(nums3) / sizeof(nums3[0]);
// or sizeof(nums3) / sizeof(int);
for(int i=0; i<count; ++i)
printf("%d ", nums3[i]);
printf(" size=%d \n", count);
- Designated Initializer의 출력값이다. >> 10, 0, 12, 13, 0, 15 size = 6
- Index를 지정하여 값을 할당할 수 있다. 값을 지정하지 않은 Index는 0으로 초기화된다.
- Index를 지정하여 할당한 다음 값을 할당하면 Index+1의 위치에 값을 할당한다.
#define Two 2
int main()
{
/* Specifying Array Size, Right Way*/
int test0[Two]; // Symbolic Integer Constant
int test1[2]; // Literal Integer Constant
int test2[1*2];
int test3[sizeof(int)];
/* Error Occured */
int test4[0];
const int num = 1;
int test5[num*2];
/* Variable-Length Array */
int n = 8; // this works optionally
int test6[n]; // MSVC prohibiting this
}
10.3 포인터의 산술 연산( Pointer Arithmetic )
(type*)0은 NULL을 의미한다. (type*)을 생략해서 사용하기도 한다. NULL keyword를 사용함이 일반적이다.
int* ptr = 0;
printf("%d \n", *ptr); // Error!
- 이때 0은 integer literal이 아니다.
- Asterisk( * ) 역참조는 에러를 발생시킨 다.
/* char */
char* ptr = (char*)0; // or = 0;
printf("ch %p %lld \n", ptr, (long long)ptr); // >> ch 00000000 0
ptr++;
printf("ch %p %lld \n", ptr, (long long)ptr); // >> ch 00000001 1
/* int */
int* ptr_i = (int*)0; // or = 0;
printf("i %p %lld \n", ptr_i, (long long)ptr_i); // >> i 00000000 0
ptr_i++;
printf("i %p %lld \n", ptr_i, (long long)ptr_i); // >> i 00000004 4
/* double */
double* ptr_d = (double*)0; // or = 0;
printf("d %p %lld \n", ptr_d, (long long)ptr_d); // >> d 00000000 0
ptr_d++;
printf("d %p %lld \n", ptr_d, (long long)ptr_d); // >> d 00000008 8
ptr++ 혹은 ptr +1의 의미는 해당 자료형의 크기만큼 건너띈 주소를 말한다.
/* void */
void* ptr_v = (double*)0; // or = 0;
printf("v %p %lld \n", ptr_v, (long long)ptr_v);
ptr_v++;
printf("v %p %lld \n", ptr_v, (long long)ptr_v);
void는 크기가 0이기 때문에 ptr_v++는 컴파일 에러를 발생시킨다.
double arr_d[] = {10.5, 11.5, 12.5, 13.5, 14.5};
double *ptr_0 = &arr_d[0];
double *ptr_1 = &arr_d[3];
printf("%p %p \n", ptr_0, ptr_1); // O
printf("%d \n", ptr_0 + ptr_1); // X : Compile Error
Pointer 끼리 +, *, / 연산은 불가능하다. - 연산은 가능하다!
printf("%d \n", ptr_0 - ptr_1); // >> -3
printf("%lld - %lld = %lld \n", (long long)ptr_1, (long long)ptr_0, (long long)(ptr_1 - ptr_0));
>> 17824496 -17824472= 3?
연산 결과값은 24가 나와야하지만 3이 나왔다. 이 때 3이 가리키는 값은 10진수 3이 아니라 3 * double(8byte)를 뜻한다. 따라서 24byte 연산 결과와 일치한다. 이를 통하여 포인터끼리 뺄셈 값은 index의 차이, 두 주소간 거리 / 해당 자료형 크기 임을 알 수 있다.
두 포인터의 차이는 인덱스 차이와 동일하다.
참고
64bit 환경을 고려하여 lld로 출력할 때 ptr를 (long long) 데이터형으로 변환해주어야 한다. 이후 10진수로 변환된
주소를 쉽게 읽을 수 있다.
10.4 포인터와 배열
int arr_i[5];
int* ptr = arr_i;
printf("arr :%p Ptr :%p arr[0]:%p \n", arr_i, ptr, &arr_i[0]);
printf("arr+2 :%p Ptr+2:%p arr[2]:%p \n", arr_i+2, ptr+2, &arr_i[2]);
배열을 포인터에 할당한다. 포인터에 Index를 더하거나 빼면 배열에서 Index로 이동한 것과 동일한 주소를 가리킬 수 있다.
배열의 이름을 Pointer처럼 사용할 수 있다. Arr_i+2와 Ptr+2가 동일한 값을 가리킨다. 주의 할 점은 배열의 이름과 Pointer가 동일하지 않다는 것이다. 예를 들면 다음과 같다.
ptr++; // O
arr_i++ // X : Error
값을 할당하고 출력해보자. 포인터+Index, 배열 시작+Index, 배열[Index] 모두 사용 가능하다.
for(int i=0; i<count; ++i)
arr_i[i] = 10*i;
printf("arr+3 :%d Ptr+3:%d arr[3]:%d \n", *(arr_i+3), *(ptr+3), arr_i[3]);
printf(" :%d :%d arr[3]:%d \n", *arr_i+3, *ptr+3, arr_i[3]);
>> 30 30 30
>> 3 3 30
후위연산자의 활용
int* ptr = arr_i;
for(int i=0; i < count; ++i)
{
printf("%d %d \n", *ptr++, arr_i[i]);
/* == printf("%d %d \n", *ptr, arr_i[i]);
ptr++; */
/* == printf("%d %d \n", *(ptr+i), arr_i[i]); */
}
>> 0 0
>> 10 10
>> 20 20
>> 30 30
>> 40 40
후위 연산자는 Semicolon( ; )을 만난 뒤에 실행된다. postfix(후위 연산자)는 Sequence Point를 만나면 실행된다. Semicolon( ; ), Comma( , ), Logical Opearator( &&)이 있다.
후위연산자를 사용한 표현이 ( *ptr++ ) 배열에 인덱스를 넣어 접근한 표현( arr_i[i] )보다 빨랐으나 컴파일러의 발전으로 그 차이가 미미하고, 컴퓨터 성능이 좋아짐에 따라 읽기 좋은 코드를 더 선호하게 되었다.
10.5 2차원 배열과 메모리
int arr[2][3] = { {1,2,3}, {4,5,6} };
다차원 배열의 Index는 뒤에서부터 해석한다. 위 예제는 3개짜리 배열이 2개 있다.
arr[0][0] = 1 | arr[0][1] = 2 | arr[0][2] = 3 |
arr[1][0] = 4 | arr[1][1] = 5 | arr[1][2] = 6 |
위는 이해를 돕기 위한 그림이다. 실제 컴퓨터는 다음과 같이 메모리를 활용하고 있다.
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 | 1 | 2 | 3 |
arr[0][0]=1 | arr[0][1]=2 | arr[0][2]=3 | arr[1][0]=4 | arr[1][1]=5 | arr[1][2]=6 |
위의 원리로 아래와 같은 코드도 동작한다.
int arr[2][3] = { 1,2,3,4,5,6 };
다음도 가능하다
int arr[2][3] = {0,1,2,3,4,5};
int *ptr = &arr[0][0]; // Warning : int *ptr= arr;
for(int i=0; i<6; ++i)
*ptr++ = 10*i; // == *(ptr+i) = 10*i;
ptr = &arr[0][0];
for(int i=0; i<6; ++i)
printf("%d ",*ptr++); // == printf("%d ",*(ptr+i));
배열 자체 주소와 배열 첫 값의 주소가 동일하다. 이 점을 이용하여 ptr = arr을 사용하면 Warning 표시가 뜬다. arr은 3개짜리 배열을 가리키는 포인터로 int(*)[3]로 표기할 수 있다. int(*)[3] -> (int*)를 변환하는데서 경고를 주고 있으나 둘다 주소값이기에 문제없이 작동한다. &arr[0][0]을 사용하면 깔끔하게 배열의 첫 메모리 주소를 할당할 수 있다.
배열 자체 주소를 이용하여 값을 읽으려면 dereference를 2번 해주어야한다. 2차원 배열은 이중 포인터로 만들었기 때문이다.
printf("%d \n", arr[1][1]);
printf("%d \n", *(*(arr+1)+1) ); // O
printf("%d \n", *(arr+1) ); // X : Error
10.7 배열을 함수에게 전달해주는 방법
함수에서 배열을 Argument로 받을 때 포인터로 받는다.
void average(double arr[6])
{
printf("In Function, Size of Array: %zd\n", sizeof(arr));
}
int main()
{
double arr1[] = {0,1,2,4};
double arr2[] = {0,1,2,3,4,6};
printf("Out side, Size of Array1: %zd\n", sizeof(arr1));
printf("Out side, Size of Array2: %zd\n", sizeof(arr2));
}
>> Out side, Size of Array1: 32
>> Out side, Size of Array2: 48
>> In Function, Size of Array: 4
>> In Function, Size of Array: 4
arr1을 예를 들자. double 자료형의 배열이고 4개를 넣을 수 있으므로 8byte * 4 = 32가 나와야 한다. main에서 실행시켰을 때 32가 나왔다. 함수 내부에서 호출된 사이즈 출력 함수는 배열의 개수와 관계없이 4byte로 나온다. 포인터의 메모리 사이즈이다.(32bit 기준, 64bit로 변경하면 8byte가 출력된다)
함수가 배열을 Argument로 전할 때 포인터로 받는다.
average 함수를 구현 예제
average 함수에서 Parameter는 arr[ ] 배열로 구현되어 있다. 실제로 포인터로 받지만 배열로 표시하였을 때 장점이 있을까? 사용자가 입력할 때 배열을 넣어 동작하는 함수라 알릴 수 있다.
double average(double [], int n);
double average(double *, int n);
int main()
{
double arr1[] = {0,1,2,4};
double arr2[] = {0,1,2,3,4,6};
printf("Out side, Size of Array1: %zd\n", sizeof(arr1));
printf("Out side, Size of Array2: %zd\n", sizeof(arr2));
printf("arr1's avg = %f\n",average(arr1, sizeof(arr1)/ sizeof(arr1[0])) );
printf("arr2's avg = %f\n",average(arr2, sizeof(arr1)/ sizeof(arr2[0])) );
}
// arr[] is pointer, the first address of value
double average(double arr[], int n) // so it needs array's size
{
printf("In Function, Size of Array: %zd\n", sizeof(arr));
double avg = 0.0;
for(int i=0; i<n; ++i)
avg += arr[i];
return avg/(double)n; // still works well without castings, but..
}
// Same as above
double average(double *arr, int n)
{
}
10.8 두 개의 포인터로 배열을 함수에게 전달하는 방법
C++ Iterator가 아래와 같은 방식으로 동작한다
double average(double *, double *);
int main()
{
double arr1[] = {0,1,2,4};
printf("Size of Array1: %zd\n", sizeof(arr1));
// arr1 + 4(multiply 4byte=int)
printf("avg = %f\n",average(arr1, arr1+4) );
}
double average(double *start, double *end)
{
// minus operator btw pointer indicate index diff
int count = end - start;
// int count = 0;
double avg = 0.0;
while(start<end)
{
avg += *start++;
// count++;
}
return avg/(double)count; // still works well without castings, but..
}
10.9 포인터 연산 총정리
int arr[] = { 10, 20, 30, 40 };
int* ptr1, * ptr2, * ptr3;
ptr1 = arr;
printf("ptr1 val [%p]\n", ptr1); // >> ptr1 val[010FF848]
printf("ptr1 add [%p]\n", &ptr1); // >> ptr1 add[010FF83C]
printf("ptr1 dref [%d]\n\n", *ptr1); // >> ptr1 dref[10]
ptr2 = &arr[0];
printf("ptr2 val [%p]\n", ptr2); // >> ptr2 val[010FF848]
printf("ptr2 add [%p]\n", &ptr2); // >> ptr2 add[010FF830]
printf("ptr2 dref [%d]\n\n", *ptr2); // >> ptr2 dref[10]
ptr3 = arr + 3; // Note: arr+3(mul 4byte)
printf("ptr3 val [%p]\n", ptr3); // >> ptr3 val[010FF854]
printf("ptr3 add [%p]\n", &ptr3); // >> ptr3 add[010FF824]
printf("ptr3 dref [%d]\n\n", *ptr3); // >> ptr3 dref[40]
포인터끼리 -, = 연산
포인터 간 거리를 printf( ) 함수를 통해 표시할 때 %td 형식 지정자를 사용한다.
// settings
int arr[] = { 10, 20, 30, 40 };
int* ptr1, * ptr3;
ptr1 = arr;
ptr3 = arr + 3;
/* Differencing */
printf("diff [%td]\n\n", ptr3 - ptr1);// Note: t is for pointer diff
if(ptr1 == ptr3)
printf("%p == %p\n\n", ptr1, ptr3);
else
printf("%p != %p\n\n", ptr1, ptr3);
/* Warning : Incompatible Types */
double d = 3.14;
double *ptr_d = &d;
if(ptr1 == ptr_d)
printf("%p == %p\n\n", ptr1, ptr_d);
else
printf("%p != %p\n\n", ptr1, ptr_d);
/* Solution for Incompatible Types */
if((double*)ptr1 == ptr_d);
if(ptr1 == (int*)ptr_d);
if((void*)ptr1 == (void*)ptr_d);
10.10 Const, 배열, 포인터(상수 자료형을 포인터로 접근해보기)
포인터로 Const 자료형 데이터 접근하기
const int arr[] = {10, 20, 30};
printArray(arr,3);
int *ptr1 = arr;
*ptr1 = -1; // Warning
printArray(arr,3);
ptr1[1] = -2; // Warning
printArray(arr,3);
>> { 10 20 30 }
>> { -1 20 30 }
>> { -1 -2 30 }
const 자료형은 해당 이름으로 접근하여 데이터를 바꿀 수 없다. 포인터를 새로 만들어 접근했을 때는 가능하다. Warning이 표시되지만 컴파일 가능하고 작동에도 이상이 없다. 이를 방지하는 방법은 포인터를 const로 만든다.
const int *ptr2 = arr;
*ptr2 = -1; // Error
ptr2[1] = -2; // Error
포인터를 const로 지정하여도 주소의 위치를 옮기는 연산자는 작동한다.
printArray(ptr2,3);
ptr2++; // Note: Possible ptr +=1, ptr -=1, ++ptr, ptr--
printArray(ptr2,2);
>> { 10 20 30 }
>> { 20 30 }
주소 위치를 옮기는 연산자의 사용을 막아보자
const int* const ptr3 = arr;
ptr3+=1; // Error
const를 두 번 적어주면 위 코드는 에러를 발생시켜 컴파일이 불가능하다. 이때 Asterisk(*) 연산자는 int 자료형 뒤에 위치함을 유의하자. 포인터와 const를 함께 사용할 때
첫 번째 const : 포인터 주소 값에 있는 데이터 값을 바꿀 수 없다.
두 번째 const : 포인터가 가리키고 있는 주소 값을 바꿀 수 없다.
상수 배열을 함수의 Argument로 넘겨 변환하기
const 배열(상수 배열)이지만 새로 만든 포인터로 접근하면 값을 바꿀 수 있다. C++에서는 type Error가 발생한다.
void add_val(int *arr, int n)
{
for(int i=0; i<n; ++i)
*(arr+i) += 10;
}
int main()
{
const int arr[] = {1, 2, 3};
printArray(arr,3);
add_val(arr,3);
printArray(arr,3);
}
>> { 1 2 3 }
>> { 11 12 13 }
const를 Parameter 자료형 앞에 정의하기
const를 자료형 앞에 놓으면 arr 주소값에 있는 값을 변환시킬 수 없다. *arr = -100은 에러를 발생시킨다.
하지만 arr 주소값은 변환시킬 수 있다. arr +=1을 했을 때 출력값을 보면 4byte(int)만큼 이동했다. 함수 내부에서 생성된 arr 값은 main에서 정의된 arr과 다르다. 함수 내 arr의 주소값은 ~FDD8이고 main 내에 arr 주소값은 FE08이다. 따라서 main 내에 arr값에는 변화가 없다.
void add_val(const int * arr, int n)
{
// *arr = -100; // Error!
printf("in ptr's add %p \n", &arr); // >> 0118FDD8
printf("in before %p \n", arr); // >> 0118FE08
arr += 1;
printf("in after %p \n", arr); // >> 0118FE0C
}
int main()
{
const int arr[] = {1, 2, 3};
printf("out ptr's add %p \n", &arr); // >> 0118FE08
printf("out before %p \n", arr); // >> 0118FE08
add_val(arr,3);
printf("out after %p \n", arr); // >> 0118FE08
}
>> out ptr's add 0118FE08
>> out before 0118FE08
>> int ptr's add 0118FDD8
>> in before 0118FE08
>> in after 0118FE0C
>> out after 0118FE08
const를 Parameter 자료형 뒤에 정의하기
void add_val(int * const arr, int n)
{
// arr++; // Error!
*arr += 2; // Warning!
}
int main()
{
const int arr[] = {1, 2, 3};
printArray(arr,3);
add_val(arr,3);
printArray(arr,3);
}
>> { 1 2 3 }
>> { 3 2 3 }
위에서 add_val( ) 함수 내부에서 arr의 주소값을 바꾸면 에러가 생긴다. arr 주소값이 가리키는 데이터는 변경시킬 수 있다. 이때 Warning 표시가 뜨지만 컴파일 가능하다. 런타임 에러도 없다.
10.12 2중 포인터 작동원리
int i = 10;
int *ptr = &i;
int **d_ptr = &ptr;
int ***t_ptr = &d_ptr;
printf("i %8d %p\n",i, &i); // i 10 00CFFAA0
*ptr = 20;
printf("i %8d %p\n",i, &i); // i 20 00CFFAA0
printf("ptr %p %p\n",ptr, &ptr); // ptr 00CFFAA0 00CFFAB0
**d_ptr = 30;
printf("i %8d %p\n",i, &i); // i 30 00CFFAA0
printf("dptr %p %p\n",d_ptr, &d_ptr);// dptr 00CFFAB0 00CFFAC0
***t_ptr = 40;
printf("i %8d %p\n",i, &i); // i 40 00CFFAA0
printf("tptr %p %p\n",t_ptr, &t_ptr);// tptr 00CFFAC0 00CFFAD0
이때 이중 포인터와 역참조를 두 번 할 때 순서를 표시하면 다음과 같다.
int **d_ptr = &ptr;
// Same
int *(*d_ptr) = &ptr;
**d_ptr = 20;
// Same
*(*d_ptr) = 20;
10.13 포인터 배열과 2차원 배열
int arr[2][3] = { { 0,1,2 }, { 3,4,5 } };
// (int*) parr[2]
int *p_arr[2] = { arr[0], arr[1] };
위는 이해를 돕기 위한 그림이다. 실제 컴퓨터는 다음과 같이 메모리를 활용하고 있다.
int arr1[] = { 10,11,12 };
int arr2[] = { 13,14,15 };
// (int*) parr[2]
int *p_arr[2] = { arr1, arr2};
for(int j=0; j<2; ++j)
{
for(int i=0; i<3; ++i)
{
printf("%d ", p_arr[j][i] );
printf("%d ", *(p_arr[j]+i) );
printf("%d ", *(*(p_arr+j)+i) ); // Useful for Dynamic Allocation
printf("%d ", (*(p_arr+j))[i] );
}
printf("\n");
}
이때 p_arr 배열은 주소값을 여러개 가진다. p_arr[ ] = { arr1의 주소, arr2의 주소 } 이다.
따라서 p_arr + j 는 단순히 p_arr[0]에서 + j Index에 있는 값을 뜻한다. 여기서 dereference를 하면 해당 주소값으로 이동한다. arr1의 주소값으로 이동하면 arr1 배열에있는 첫 인덱스 arr1[0]을 가리고 있을 것이다.
*( (p_arr+j)[i] ) vs ( *(p_arr+j) )[i]
마지막 ( *(p_arr+j) )[i] 와 *(p_arr+j)[i]는 다르다. *보다 [Index]가 우선순위이다. 연산자를 사용하기 전에 ( )를 사용하느냐의 차이가 다른 결과값을 출력한다.
printf("%d ", *((p_arr+0)[1]) );
printf("%p \n", (p_arr+0)[1] );
printf("%d ", (*(p_arr+0))[1] );
printf("%p \n", &(*(p_arr+0))[1] );
>> 13 005CF780
>> 11 005CF754
*( (p_arr+0)[1] ) 해석
p_arr+0은 p_arr배열의 첫 값 p_arr[0]을 가리킨다. 이때 [1]은 단순히 자료형의 크기만큼 주소를 이동하므로
p_arr+0 = p_arr[0],
( p_arr[0] )[1] = p_arr[1] = &arr2 = arr2[0] = 13이 된다.
( *(p_arr+0) )[1] 해석
*(p_arr+0)은
p_arr[0] = &arr1
*(p_arr[0]) = arr1
( arr1[1] ) = 11
포인터는 주소 값을 가지고 자기 자신의 주소값도 등록되어 있다.
배열은 지정한 자료형의 값을 가지나 자기 자신의 주소는 배열의 첫 값이다.
int *ptr = arr1;
printf("%p \n", &ptr); // >> 010FF9D0 ptr's ad
printf("%p \n", ptr); // >> 010FF9A0 value of ptr
printf("%p \n", arr1); // >> 010FF9A0 Array's ad
printf("%p \n", &arr1[0]); // >> 010FF9A0 Array's 1st elm's ad
참고
포인터 배열이 3개를 담을 수 있을 때, 해당 포인터 배열의 사이즈는 12byte이다. (32bit System)
String 예제
char* nameA[] = { "Aladdin", "Jasmine", "Magic Carpet", "Genie" };
const int nA = sizeof(nameA) / sizeof(char*);
for (int i = 0; i < nA; ++i)
printf("%12s %p\n", nameA[i], &nameA[i]);
printf("\n");
char nameB[][16] = { "Aladdin", "Jasmine", "Magic Carpet", "Genie" };
const int nB = sizeof(nameB) / sizeof(char[16]);
for (int i = 0; i < nB; ++i)
printf("%12s %p\n", nameB[i], &nameB[i]);
printf("\n");
Output
//(32bit system)
name A nameB
>>Aladdin 012FF878 >> Aladdin 012FF888
>>Jasmine 012FF87C >> Jasmine 012FF898
>>Magic Carpet 012FF880 >> Magic Carpet 012FF8A8
>>Genie 012FF884 >> Genie 012FF8B8
//(64bit system)
name A
>> Aladdin 00000093F871F8A0
>> Jasmine 00000093F871F8A8
>> Magic Carpet 00000093F871F8B0
>> Genie 00000093F871F8B8
포인터 배열은 배열의 각 값간 주소 차이가 포인터의 사이즈(4byte-32bit or 8byte-64bit)이다. 이중 배열에서 바깥 배열의 원소를 생각해보자. 각 원소는 안쪽 배열 크기(위 예제에서 sizeof char * 16)만큼 떨어져있다. 아래 예제는 32bit환경에서 실행하였다.
nameA의 Memory 사용
nameA는 포인터의 배열이다. 각 값(원소)는 주소값(포인터)이며 각 값은 포인터 크기(4byte)만큼 떨어져 있다.
해당 주소값에 가면 character 배열 값을 얻을 수 있다.
포인터주소 값
Aladdin(8) 012FF878 -> 0x00372100 (8byte)
Jasmine(8) 012FF87C -> 0x00372108 (8byte)
Magic Carpet(c ) 012FF880 -> 0x00372110 (16byte-padding)
Genie(6) 012FF884 -> 0x00372120
nameB의 Memory 사용
nameB는 2차원 배열이다. nameA는 주소 값(포인터)만 배열 내부에 저장되어있고 값은 외부에 있는 주소로 갔을 때
얻을 수 있었다. nameB 2차원 배열로 모든 값이 내부에 저장되어 있다.
참고) 포인터 값을 Debug -> Memory 툴을 사용해서 찾아가보자.
배열은 값을 연속적으로 배치하므로 여러 값이 붙어있으리라 기대할 수 있다. Memory를 들여다 보면 Relase Mode를 사용해야 String 값이 붙어있음을 확인할 수 있다. Debug Mode는 배열의 값 사이에 debugging를 위하여 여분의 메모리를 할당하고 있다. 아래 사진은 Release Mode에서 확인한 nameA와 nameB이다.
10.13-1 배열의 이름과 배열의 주소
char array[16];
printf("array %p\t%zd\n", array, sizeof(array));
printf("array+1 %p\n", array + 1); // move datype size
printf("&array %p\t%zd\n", &array, sizeof(&array));
printf("&array+1 %p\n", &array + 1); // move entire size of array
Output
array 00F3F994 16
array+1 00F3F995 // 95 - 94 = 0x01 size of element(char)
&array 00F3F994 4
&array+1 00F3F9A4 // A4 - 94 = 0x10 size of array
배열의 이름은 배열이 가지고 있는 첫 값을 가리킨다. & 연산자를 사용한 배열은 배열 전체를 가리키고 있다. 주소에서 +1 연산의 의미는 해당 자료형만큼의 이동을 의미한다. +1을 한 뒤에 확인해보자.
array는 첫 값의 자료형만큼 이동하고 &array는 배열의 크기만큼 이동함을 확인할 수 있다.
10.13-2 배열의 타입
int a[5]; // type of a = int []
&a; // type of &a = int(*)[5]
10.14 2차원 배열과 포인터
arr[0] 주소와 &arr[0] 주소는 동일하다. 배열은 자기 자신의 주소를 별도로 보관하지 않기 때문이다. 만약 arr이 포인터의 배열이었다면 arr[0] &arr[0]는 달라진다.
예제
pa는 포인터로 (double 4개를 가지는 배열)을 가리킨다. 주소연산 pa+1은 배열 사이즈(32byte)만큼 이동한다.
ap는 배열로 ptr를 2개를 가지고 있다. 주소연산 ap+1은 포인터 크기(4byte)만큼 이동한다.
int* arr = (int[2]){ 1,2 };
double(*test)[4] = &((double[4]) { 1.f, 2.f, 3.f, 4.f }); // a Single Pointer to array
double arr2d[2][4] = { {1.f, 2.f, 3.f, 4.f}, {5.f, 6.f, 7.f, 8.f} };
double(*pa)[4]; // a Single Pointer to array has two array
double* ap[2]; // an array of Two pointers-to-int
printf("%zu \n", sizeof(pa)); // 4byte pointer
printf("%zu \n", sizeof(ap)); // 8byte Array has two pointer
pa = arr2d;
//pa[1] = arr2d[1]; // Error! pa is just pointer
//pa[0] = arr2d[0]; // Error! pa is just pointer
ap[0] = arr2d[0];
ap[1] = arr2d[1];
printf("%p: %f \n", pa, **pa ); // { arr2d[0], arr2d[1] }
printf("%p: %f \n\n", pa + 1, **(pa + 1));
printf("%p: %f \n", ap, **ap); // { ptr1, ptr2 }
printf("%p: %f \n", ap+1, **(ap+1));
Output
4
8
003EFC84: 1.000000
003EFCA4: 5.000000
003EFC68: 1.000000
003EFC6C: 5.000000
연습하기
2차원
char arr[2][3] = { {'a','b','c'}, {'d','e','f'}};
printf("arr %p %zd \n", arr, sizeof(arr) );
printf("arr[0] %p %zd \n", arr[0], sizeof(arr[0]) );
printf("&arr[0] %p %zd \n", &arr[0], sizeof(&arr[0]) );
printf("&arr[0][0] %p %zd \n", &arr[0][0], sizeof(&arr[0][0]) );
Output
arr 008FFBF0 6 // Size of Entire Array
arr[0] 008FFBF0 3 // Size of char[3] array
&arr[0] 008FFBF0 4 // & + elm : Pointer
&arr[0][0] 008FFBF0 4 // & + elm : Pointer
arr+1 008FFBF3 4 // Move 3byte
arr[0]+1 008FFBF1 4 // Move 1byte
&arr[0]+1 008FFBF3 4 // Move 3byte
&arr[0][0]+1 008FFBF1 4 // Move 1byte
3차원
char arr[2][3][4] = {
{ {'a', 'b', 'c','d'}, {'e','f','g','h'}, {'i','j','k','l'} },
{ {'0', '1', '2','3'}, {'4','5','6','7'}, {'9','A','B','C'} }
};
printf("arr %p %zd \n", arr, sizeof(arr));
printf("arr+1 %p %zd \n", arr + 1, sizeof(arr + 1));
printf("arr+1 %c \n\n", ***(arr + 1));
printf("arr[0] %p %zd \n", arr[0], sizeof(arr[0]));
printf("arr[0]+1 %p %zd \n", arr[0] + 1, sizeof(arr[0] + 1));
printf("arr[0]+1 %c \n\n", **(arr[0] + 1));
printf("&arr[0] %p %zd \n", &arr[0], sizeof(&arr[0]));
printf("&arr[0]+1 %p %zd \n", &arr[0] + 1, sizeof(&arr[0] + 1));
printf("&arr[0]+1 %c \n\n", ***(&arr[0] + 1));
printf("&arr[0][0] %p %zd \n", &arr[0][0], sizeof(&arr[0][0]));
printf("&arr[0][0]+1 %p %zd \n", &arr[0][0] + 1, sizeof(&arr[0][0] + 1));
printf("&arr[0][0]+1 %c \n\n", **(&arr[0][0] + 1));
printf("&arr[0][0][0] %p %zd \n", &arr[0][0][0], sizeof(&arr[0][0]));
printf("&arr[0][0][0]+1 %p %zd \n", &arr[0][0][0]+1, sizeof(&arr[0][0][0]+1));
printf("&arr[0][0][0]+1 %c \n\n", *(&arr[0][0][0] + 1));
for (int k = 0; k < 2; ++k)
{
for (int j = 0; j < 3; ++j)
{
for (int i = 0; i < 4; ++i)
printf("%p ", &arr[k][j][i]);
printf("\t");
}
printf("\n");
}
Output
arr 00EFFA80 24
arr+1 00EFFA8C 4
arr+1 0
arr[0] 00EFFA80 12
arr[0]+1 00EFFA84 4
arr[0]+1 e
&arr[0] 00EFFA80 4
&arr[0]+1 00EFFA8C 4
&arr[0]+1 0
&arr[0][0] 00EFFA80 4
&arr[0][0]+1 00EFFA84 4
&arr[0][0]+1 e
&arr[0][0][0] 00EFFA80 4
&arr[0][0][0]+1 00EFFA81 4
&arr[0][0][0]+1 b
00EFFA80 00EFFA81 00EFFA82 00EFFA83 00EFFA84 00EFFA85 00EFFA86 00EFFA87 00EFFA88 00EFFA89 00EFFA8A 00EFFA8B
00EFFA8C 00EFFA8D 00EFFA8E 00EFFA8F 00EFFA90 00EFFA91 00EFFA92 00EFFA93 00EFFA94 00EFFA95 00EFFA96 00EFFA97
4byte는 포인터 사이즈이다. 64bit system에서 작동하면 확인할 수 있다.
위 코드가 복잡해 이해하기 어렵다면 10.13-1을 보자 array + 1는 해당 배열의 첫 값 사이즈만큼 이동하고, &array + 1는 배열의 사이즈만큼 이동한다.
출력하기
printf("\n");
for (int k = 0; k < 2; ++k)
{
for (int j = 0; j < 3; ++j)
{
for (int i = 0; i < 4; ++i)
printf("%c%c ", *(*(*(arr + k) + j)+i) , arr[k][j][i]);
printf("\t");
}
printf("\n");
}
Output
aa bb cc dd ee ff gg hh ii jj kk ll
00 11 22 33 44 55 66 77 99 AA BB CC
정리
포인터는 자기 자신이 차지하고 있는 저장공간을 가지고 있다. 이 주소는 Identifier(식별자 or 변수명)으로 접근한다.
이 저장공간 안에 다른 값의 저장공간을 가리키는 주소값을 담는다.
배열은 첫 값의 주소를 자기 자신의 주소값으로 가질 뿐이다. 포인터식으로 이야기하면 자기 자신의 주소와 값이 동일하다. 신기한 점은 배열에서 & 연산자와 sizeof 연산자를 사용해보면 &Array와 Array는 다른 결과값을 출력한다.
1. sizeof(Array)는 배열 전체 공간을 출력하고 sizeof(&Array)는 포인터가 차지하는 공간을 출력한다.
2. Array + 1은 배열 첫 값의 공간만큼 이동하고 &Array+1은 배열 전체 공간만큼 이동한다.
배열을 어떻게 사용하는지를 기준으로 결과값을 예상해보면 될 것 같다. Array + i 형태는 Index로 쓰이므로 배열 첫 값의 공간만큼 이동하고, sizeof(Array)는 배열의 크기를 확인하는 것이다.
Bracket [ ]이 Asterisk *보다 우선순위가 높다.
때문에 아래 두 선언은 다른 자료형을 가진다. 먼저 실행되는 연산자를 기준으로 해당 값의 자료형이 결정된다.
char (*t1)[2]; // Single Ptr to char[2] Array
char *t2[2]; // Array of Ptr containing 2 elem
Pointer to Array
char arr1[2][3] = { {'a','b','c'}, {'d','e','f'} };
/* pointer to char[3] */
char(*t1)[3] = arr1;
printf("ptr to char :%zd \n", sizeof(t1));
for (int i = 0; i < 2; ++i)
{
for(int j=0; j<3;++j)
{
printf("%c ", *(*(t1+i)+j) );
printf("%p == ", &arr1[i][j] );
printf("%p != ", *(t1+i)+j );
printf("%p \n", (t1+i)+j );
}
}
printf("\n");
Output
ptr to char :4 // size of ptr
a 012FFD90 == 012FFD90 != 012FFD90
b 012FFD91 == 012FFD91 != 012FFD93
c 012FFD92 == 012FFD92 != 012FFD96
d 012FFD93 == 012FFD93 != 012FFD93
e 012FFD94 == 012FFD94 != 012FFD96
f 012FFD95 == 012FFD95 != 012FFD99
배열 이름+ 1을 하면 배열 첫 값의 공간만큼 움직인다. 포인터는 가리키고 있는 자료형 크기만큼 움직인다.
(t1+i)+j = (t1+i+j)
+1마다 3byte씩 움직여 위와 같은 결과가 나온다.
Array Of Pointer
char arr2[2][3] = { {'a','b','c'}, {'d','e','f'} };
/* Array of pointer containing 2 elem */
char* t2[2] = { arr2[0], arr2[1] };
t2[0] = arr2[1];
t2[1] = arr2[0];
printf("Array of ptr:%zd \n", sizeof(t2));
for (int j = 0; j < 2; ++j)
{
for (int i = 0; i < 3; ++i)
{
printf("%c ", *(*(t2+j)+i) );
printf("%c ", t2[j][i] );
}
printf("\n");
}
Output
Array of ptr:8 // { ptr1, ptr2 } = { 4byte, 4byte }
d d e e f f
a a b b c c
10.14 -1 single 포인터로 2차원 배열 출력하기
2차원 배열 값이 연속적으로 정의되어 있고 각 값의 크기가 일정하다면 1차원 배열처럼 출력할 수 있다.
void myPrint(char* arr, int n, int m)
{
printf("add=%p \n", arr);
printf("add=%p \n", arr + 1);
for (int i = 0; i < n; ++i)
{
for (int j = 0; j < m; ++j)
printf("%p ", arr + j + (m * i));
printf("\n");
}
printf("\n");
}
int main()
{
char arr1[2][3] = { {'a','b','c'}, {'d','e','f'} };
printf("add=%p \n", arr1);
for (int i = 0; i < 2; ++i)
{
for (int j = 0; j < 3; ++j)
printf("%p ", *(arr1 + i) + j);
printf("\n");
}
printf("\n");
myPrint(arr1, 2, 3);
}
Output
add=010FFBE0
010FFBE0 010FFBE1 010FFBE2
010FFBE3 010FFBE4 010FFBE5
add=010FFBE0
add=010FFBE1
010FFBE0 010FFBE1 010FFBE2
010FFBE3 010FFBE4 010FFBE5
위의 Myprint( ) 함수를 다음과 같이 바꾸어 쓸 수 있다.
void myPrint(char(*arr)[3], int n, int m)
{
printf("add=%p \n", arr);
printf("add=%p \n", arr + 1);
for (int i = 0; i < n; ++i)
{
for (int j = 0; j < m; ++j)
printf("%p ", &arr[i][j]);
printf("\n");
}
printf("\n");
}
10.15 포인터의 호환성
/* Promotion */
int i = 5;
double d;
d = i; // promotion
/* Imcompatible (int*)->(double*) */
int* ptr_i = &i;
double* ptr_d = &d;
ptr_d = ptr_i; // Warning : Imcompatible
ptr_d = (double*)ptr_i; // OK but Not Recommended
/* Ptr to Array[n] */
char(*ptr_arr)[3]; // Pointer to int[3]
char arr1[2][3];
char arr2[3][2];
ptr_arr = arr1; // OK
// ptr_arr = arr2; // Warning
/* Double Pointer */
char *ptr_c;
char ** d_ptr = &ptr_c;
*d_ptr = arr2[0]; // ptr to char[]
d_ptr = arr2; // Warning
// d_ptr (char**) : a ptr to ptr to char
// arr2 char(*)[2] : a ptr to array of 2 elem
상수 keyword const활용
오른쪽에서 왼쪽으로 읽는다.
Using this rule, even complex declarations can be decoded like,
- int ** const is a const pointer to pointer to an int.
- int * const * is a pointer to const pointer to an int.
- int const ** is a pointer to a pointer to a const int.
- int * const * const is a const pointer to a const pointer to an int.
예제
/* const keyword */
int x = 20;
const int const y = -20;
int *p1 = &x;
const int *p2 = &y;
p1 = p2; // Warning but work
*p1 = -1;
//*p2 = 50; // Error!
printf("*p1:%i *p2:%i \n", *p1, *p2);
Output
*p1:-1 *p2:-1
값을 const로 지정하기
/* const keyword */
int x = 20;
const int y = -20;
int *p1 = &x;
const int *p2 = &y;
p2 = p1; // You can change address
*p2 = -23; // Error! Can't change value
const ptr
/* const keyword */
int x = 20;
const int y = -20; // y value is constant
int *p1 = &x;
int* const p2 = &y;
*p2 = 124; // Can change value
p2 = p1; // Error!
ptr to ptr to const value
int z = 1;
int* p3 = &z;
const int** d_ptr = &p3; // same as below
d_ptr = &p3; // OK
*d_ptr = p3; // OK
**d_ptr = 2; // Error!
ptr to const ptr to const value
const int* const* d_ptr = &p3; // same as below
d_ptr = &p3; // OK
*d_ptr = p3; // Error!
const ptr to ptr to const value
const int** const d_ptr = &p3; // same as below
*d_ptr = p3; // OK
d_ptr = &p3; // Error!
10.16 다차원 배열을 함수에게 전달해주는 방법
/* int data[ROWS][COLS] = { {0,1,2},
{3,4,5} }; */
// sum2d(data,2);
int sum2d(int arr[][COLS], int row)
{
int total = 0;
for(int r=0; r< row; ++r)
for(int c=0; c<COLS; ++c)
total += arr[r][c];
return total;
}
int sum2d(int (*arr)[COLS], int row)
{
int total = 0;
for(int r=0; r< row; ++r)
for(int c=0; c<COLS; ++c)
total += arr[r][c];
return total;
}
고차원 배열을 사용할 때 첫 [ ] 는 입력해두어도 컴파일러가 기억하지 않는다. 다른 변수로 알려주자. 아래 함수는 모두 동일하다.
int funct(int arr[2][3][4][5], int row);
int funct(int arr[][3][4][5], int row);
int funct(int (*arr)[3][4][5], int row);
10.17 변수로 길이를 정할 수 있는 배열
Variable-Length Arrays(VLAs)
Visual Studio에서 지원하지 않는다.
int n;
scanf("%i", &n);
int arr[n];
for(int i=0; i<n; ++i)
arr[i] = 10+i;
for(int i=0; i<n; ++i)
printf("%d ", arr[i]);
배열의 길이가 정해진 이후에는 변경이 불가능하다.
함수에서 VLAs활용
int sum2d(int row, int col, int arr[row][col]);
함수의 Parameter에서 VLAs를 사용할 수 있다.
10.18 복합 리터럴과 배열
Literal
Constants refer to fixed values that the program may not alter during its execution. These fixed values are also called literals. |
compound literals
Constructs an unnamed object of specified type in-place, used when a variable of array, struct, or union type would be needed only once. ( type ) { initializer-list } |
같은 자료형끼리 묶은 배열 Literal은 함수의 Argument 사용에 용의하다. 포인터에도 복합 리터럴을 바로 할당할 수 있다
int b[2] = { 1, 2};
(int[2]) { 1, 2 }; // Compound Literal
// int c[2] = (int[2]) { 1, 2 }; Error!
test( (int[2][COLS]){ {1,2,3}, {4,5,6} } , 2);
int *ptr;
ptr = (int[2]){1,2};
int (*ptr2)[COLS];
ptr2 = (int[2][COLS]){ {1,2,3}, {4,5,6} };
이름 없이 자료를 활용할 수 있다.
'Programming Language > C' 카테고리의 다른 글
홍정모의 따라하며 배우는 C언어 12. Storage Classes, Linkage and Memory Management (0) | 2021.11.27 |
---|---|
홍정모의 따라하며 배우는 C언어 11 문자열 함수 (0) | 2021.11.24 |
홍정모의 따라하며 배우는 C언어 9 함수 (0) | 2021.11.20 |
홍정모의 따라하며 배우는 C언어 8 문자 입출력과 유효성 검증 (0) | 2021.11.15 |
홍정모의 따라하며 배우는 C언어 7 분기 (0) | 2021.11.15 |