학부 수업 내용 정리/자료구조

Array

supersumin 2025. 3. 12. 17:15

Array란?

Array는 memory에 연속적으로 저장되는 elements의 집합으로, index와 element의 pair로 구성된다.

Example code

int array[6];			// create: array 선언은 data type과 size
array[1] = 100;			// set: array 한 element에 값을 저장
int value = array[1];		// get: array에서 값을 가져오기

 

Array의 index 접근 방식은 pointer 연산과 동일하게 동작(+ sizeof)

Array는 memory에 연속적으로 저장된 elements의 집합이다.

 

Array는 어떻게 여러 element에 접근할 수 있을까? Array의 이름은 첫 번째 element의 address를 의미하며, pointer처럼 사용될 수 있기 때문이다.

 

따라서, array index (arr[i])는 내부적으로 *(arr + i)와 같은 방식으로 동작하게 된다.

Example code

#include <iostream>
#include <stdio.h>
#include <stdlib.h>

int main()
{
	// 02. Array
	double arr[5] = { 1, 20, 300, 4000, 50000 };	// 배열의 크기 5로 고정, 모든 element 초기화
	double arr2[5] = { 1, 2 };	// 일부만 초기화하면 나머지 요소는 0.0으로 자동 초기화
	double arr3[] = { 1, 2 };	// 초기화된 요소 개수에 따라 배열 크기가 고정


	printf("(1) Address of arr: %p\n", (void*)arr);	// %p 형식지정자로 address를 출력시 data type은 (void *)으로 변환 필요
	printf("(2) Address of arr: %p\n", &arr);	// arr, &arr, &arr[0] 모두 배열의 첫 번째 주소를 가리키지만, &arr은 double *[5] type이다.
	printf("(3) Address of arr[0]: %p\n", &arr[0]);	// arr의 첫 번째 element이다.
	printf("(4) Address of arr[1]: %p\n", &arr[1]); // arr[1]의 주소 → arr보다 sizeof(double) 만큼 증가

	printf("(5) Address of arr[0] (in decimal): %llu\n", (unsigned long long)(uintptr_t) & arr[0]);
	printf("(6) Address of arr[1] (in decimal): %llu\n", (unsigned long long)(uintptr_t) & arr[1]);

	printf("\n");
	printf("\n");


	// sizeof operator
	printf("sizeof arr: %d\n", (int)sizeof(arr));	// arr의 전체 크기이다.
	printf("sizeof arr[0]: %d\n", (int)sizeof(arr[0]));
	printf("sizeof int: %d\n", (int)sizeof(int));
	printf("sizeof double: %d\n", (int)sizeof(double));
	printf("number of elements in arr: %d\n", (int)(sizeof(arr) / sizeof(arr[0])));

	printf("\n");
	printf("\n");



	// indexing methods using variable pointer
	double* arr_ptr = arr;	// variable pointer
	int num_of_elements = (int)(sizeof(arr) / sizeof(arr[0]));

	for (int i = 0; i < num_of_elements; i++)
	{
		printf("1) arr[%d]: %f\n", i, arr[i]);
		printf("2) *(arr + %d): %f\n", i, *(arr + i));	// arr은 첫 번째 요소의 주소로 변환되기 때문에 point 연산이 가능하다.
		printf("3) arr_ptr[%d]: %f\n", i, arr_ptr[i]);	// arr_ptr은 arr의 첫 번째 요소의 주소를 받았기 때문에 포인터를 배열처럼 사용할 수 있다.
		printf("4) *(arr_ptr + %d): %f\n", i, *(arr_ptr + i));	// arr_ptr 또한 pointer이기 때문에 포인터 연산이 가능하다.
		printf("\n");
	}

	// difference between arr (constant pointer) and arr_ptr (variable pointer)
	// arr = arr + 1;	// error
	// arr_ptr = arr_ptr + 1;	// ok

	for (int i = 0; i < num_of_elements; i++)
	{
		printf("%f\n", *arr_ptr);
		arr_ptr++;	// arr은 포인터 상수와 같이 변하지 않는 주소를 가진 값이므로, ++연산을 사용하지 못하지만, arr_ptr은 그저 pointer이기 때문에 가능하다.
	}

	printf("\n");
	printf("\n");

	// sizeof operator for variable pointer 
	printf("sizeof arr_ptr: %d\n", (int)sizeof(arr_ptr));
	printf("sizeof arr: %d\n", (int)sizeof(arr));

	int temp = 0;
}

Appendix

  • 출력 시 void *로 casting: 형식지정자 %p를 통해 변수의 address를 출력할 때, 컴파일러가 (void *)로 출력하는 것이 정석이기 때문에 사용한다.
  • &: 변수의 memory address를 가져온다.
  • *: pointer가 가리키는 값을 가져오거나 수정한다.
  • array는 포인터 상수처럼 선언할 때의 주소가 변하지 않고, 값은 바뀔 수 있다.
  • 포인터 연산은 해당 타입의 크기만큼 증가한다.

배열과 포인터의 차이 및 관련 연산

arr, &arr, &arr[0]은 모두 배열의 첫 번째 요소를 가리키만 그 의미가 다르다.

  • arr(배열의 이름):  arr == &arr[0]으로 배열의 첫 번째 요소의 주소와 같은 값이다.
    arr은 배열의 이름으로 arr을 연산으로 사용할 때 pointer가 아니지만 마치 배열의 첫 번째 요소의 주소와 같은 pointer로 동작한다.
    배열 요소에 접근하는 방식으로 배열 index 연산과 pointer 연산이 있다. arr은 첫 번째 요소의 주소처럼 동작하기 때문에 포인터 연산 arr + 1을 수행하면 배열 요소 크기만큼 증가한다.
  • &arr (배열 전체의 주소): &arr == arr으로 주소 값은 동일하지만 의미적으로는 다르다. &arr은 배열 자체의 주소를 의미하기 때문에 배열 전체를 하나의 단위로 취급한다.
    포인터 연산 시 &arr + 1을 하면 배열 전체의 크기만큼 증가한다.
  • &arr[0] (배열 첫 번째 요소의 주소): &arr[0] == arr로 같은 주소이며, &arr[0]은 정확히 첫 번째 우너소의 주소를 나타내는 것이고, arr은 배열 전체의 이름이라는 차이가 있다.
printf("%p\n", arr);    // 첫 번째 요소의 주소
printf("%p\n", &arr);   // 배열 전체의 주소 (일반적으로 위와 같음)
printf("%p\n", arr + 1); // 두 번째 요소의 주소 (첫 번째 요소 주소 + sizeof(int))
printf("%p\n", &arr + 1); // 배열 끝 주소 (배열 크기만큼 증가)

Dynamic memory allocation

malloc (memory allocation, C/C++)

지정한 크기만큼 연속된 memory를 할당하고 초기화는 하지 않는다.

calloc (clear allocation, C/C++)

지정한 개수만큼 memory를 할당하고 모든 byte를 0으로 초기화한다. malloc()과 다르게 두 개의 인자를 받는다. (요소 개수, 요소 크기)

Ex Code

#include <stdio.h>
#include <stdlib.h>  // malloc, calloc, free 함수 사용을 위해 필요

int main() {
    int size = 5;
    const int size_const = 5;

    // try using static memory allocation
    // int arr_static[size];  // error: variable-sized object may not be initialized
    //int arr_static[size_const];  // error: variable-sized object may not be initialized

    // using memory allocation functions
    int* arr_malloc = (int*)malloc(size * sizeof(int));
    printf("address of arr_malloc: %p\n", (void*)arr_malloc);

    // using clear memory allocation functions
    int* arr_calloc = (int*)calloc(size, sizeof(int));
    printf("address of arr_calloc: %p\n", (void*)arr_calloc);

    printf("size of arr_malloc: %d\n", (int)sizeof(arr_malloc));
    printf("size of arr_calloc: %d\n", (int)sizeof(arr_calloc));

    printf("malloc allocated memory (uninitialized):\n");
    for (int i = 0; i < size; i++) {
        printf("%d ", arr_malloc[i]);  // garbage value (uninitialized)
    }
    printf("\n");

    printf("calloc allocated memory (zero initialized):\n");
    for (int i = 0; i < size; i++) {
        printf("%d ", arr_calloc[i]);  // initialized to zero
    }
    printf("\n");

    // release allocated memory
    free(arr_malloc);
    free(arr_calloc);

    printf("address of arr_malloc: %p\n", (void*)arr_malloc);
    printf("address of arr_calloc: %p\n", (void*)arr_calloc);


    printf("\n");
    printf("\n");

    ////////////////////////////////////////////////////////////////////////////
    // double the size
    printf("double the size of memory\n");
    arr_malloc = NULL;
    arr_calloc = nullptr;

    // using memory allocation functions
    arr_malloc = (int*)malloc(size * 2 * sizeof(int));
    printf("address of arr_malloc: %p\n", (void*)arr_malloc);

    // using clear memory allocation functions
    arr_calloc = (int*)calloc(size * 2, sizeof(int));
    printf("address of arr_calloc: %p\n", (void*)arr_calloc);

    printf("malloc allocated memory (uninitialized):\n");
    for (int i = 0; i < size * 2; i++) {
        printf("%d ", arr_malloc[i]);  // garbage value (uninitialized)
    }
    printf("\n");

    printf("calloc allocated memory (zero initialized):\n");
    for (int i = 0; i < size * 2; i++) {
        printf("%d ", arr_calloc[i]);  // initialized to zero
    }
    printf("\n");

    free(arr_malloc);
    free(arr_calloc);

    return 0;
}

 

2D Arrays

table 형식의 data를 주로 저장할 때 자주 사용되며, 차원이 올라가며 tensor라고 불린다.

#include <stdio.h>
#include <stdlib.h>

#define ROWS 3
#define COLS 4

int main() {
    // 1️⃣ 정적 2D 배열 선언 및 초기화
    int arr[ROWS][COLS] = {  
        {1, 2, 3, 4},  
        {5, 6, 7, 8},  
        {9, 10, 11, 12}  
    };

    // 2️⃣ 배열 이름을 포인터로 활용하기
    int *ptr = &arr[0][0];  // 배열의 첫 번째 요소 주소를 저장

    // 3️⃣ 2D 배열을 다양한 방법으로 출력
    printf("1️⃣ 기본적인 2D 배열 접근\n");
    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            printf("%2d ", arr[i][j]);  // 기본적인 배열 인덱싱
        }
        printf("\n");
    }
    
    printf("\n2️⃣ 포인터 연산을 활용한 접근\n");
    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            printf("%2d ", *(*(arr + i) + j));  // 배열 이름을 포인터처럼 사용
        }
        printf("\n");
    }

    printf("\n3️⃣ 포인터를 사용하여 선형 메모리 접근\n");
    for (int i = 0; i < ROWS * COLS; i++) {
        printf("%2d ", *(ptr + i));  // 1D 배열처럼 사용
        if ((i + 1) % COLS == 0) printf("\n");  // 줄바꿈
    }

    // 4️⃣ 동적 메모리 할당을 사용하여 2D 배열 생성
    printf("\n4️⃣ 동적 메모리 할당을 이용한 2D 배열\n");
    int **dynArr = (int**)malloc(ROWS * sizeof(int*));
    for (int i = 0; i < ROWS; i++) {
        dynArr[i] = (int*)malloc(COLS * sizeof(int));
    }

    // 동적 배열 초기화 및 출력
    for (int i = 0; i < ROWS; i++) {
        for (int j = 0; j < COLS; j++) {
            dynArr[i][j] = (i + 1) * (j + 1);  // 값 설정
            printf("%2d ", dynArr[i][j]);
        }
        printf("\n");
    }

    // 5️⃣ 동적 메모리 해제
    for (int i = 0; i < ROWS; i++) {
        free(dynArr[i]);
    }
    free(dynArr);

    return 0;
}

Appendix

  • 동적 할당은 선언한 즉시 즉각 free 해줘야 project 수행시 안전하다. Heap이 부족할 수 있기 때문이다.
  • arr = &arr[0] 즉, 배열의 첫 번째 요소의 주소로 변환하기 때문에 +1과 같은 pointer 연산을 수행시, pointer가 가리키는 data type의 크기만큼 memory를 이동한다.

List (리스트)

List는 array와 달리 동적으로 크기가 조절 가능하며, 삭제와 삽입에 유연하다. array는 중간에 삽입/삭제를 하면 뒤의 elements를 이동해야 하지만, list는 node 단위로 pointer로 연결되어 있기 때문에 삽입/삭제가 빠르다.

'학부 수업 내용 정리 > 자료구조' 카테고리의 다른 글

Linked lists  (0) 2025.03.18
Constant (상수)  (0) 2025.03.13
Recursion  (0) 2025.03.12
동적할당을 왜 하는 걸까요?  (0) 2024.09.28
프로그램 실행 시 알아야 할 기본적인 메모리 구조  (0) 2024.08.01