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 |