배움 저장소

홍정모의 따라하며 배우는 C언어 10.배열과 포인터 본문

Programming Language/C

홍정모의 따라하며 배우는 C언어 10.배열과 포인터

시옷지읏 2021. 11. 22. 20:17

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+0p_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} };

이름 없이 자료를 활용할 수 있다.

Comments