C

C++ 을 위한 간략한 C 강좌 2 - 배열 및 포인터

kim선달 2020. 7. 25. 17:24
Select Language

기본 배열 (raw array)는 C++ 에서는 거의 지향하는 추세입니다.

아직도 성능 때문에 기본 배열을 쓰냐 std::array 를 쓰냐 싸운다던데, 그런 글들은 대부분 몇년 전의 글들이 대부분입니다.

예전의 C++ 에서면 몰라도, 현대의 C++ 컴파일러는 최적화를 매우 잘 해주기 때문에, 그래픽처럼 정말 최적화에 목숨을 걸 만한 상황이 아니면 std::vector 이나 std::array 를 무조건 쓰는 것이 권장됩니다.

그래도 가끔씩 쓸 일이 있기 때문에 간략하게 짚고 넘어가겠습니다.

 

목차

  1. 배열
  2. 포인터
  3. 요약

 

1. 배열

 

배열 선언은 간단합니다. 변수명 뒤의 대괄호에 배열 길이를 적어주면 됩니다.

int arr[5] = {1, 2, 3, 4, 5};

 

길이를 명시하지 않을 수 도 있습니다. 이때는 중괄호로 초기화된 만큼만 길이가 생성됩니다.

int arr[] = {1, 2, 3, 4, 5}; // 길이가 5

 

길이가 10인데 5개 밖에 선언하지 않았다면?

int arr[10] = {1, 2, 3, 4, 5};  // arr[] = {1, 2, 3, 4, 5, 0, 0, 0, 0, 0}

나머지 값들은 무조건 0으로 초기화 됩니다. 이는 C 언어 및 C++ 의 기본 규칙입니다.

 

다만, 선언만 하면 값들은 초기화되지 않습니다.

int arr[10]; // 0으로 초기화 되지않음

 

배열의 모든 값들을 0으로 초기화 하는 방법들 입니다.

int arr1[10] = {}; // C언어에서는 이렇게 하면 0으로 초기화 되지 않음! C++에서만 0으로 초기화 됨
int arr2[10] = {0};
int arr3[10] = {0, };
int arr4[10] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0};

 

0 이외의 다른 값들로 선언과 동시에 초기화 하는 방법은 일일이 다 써주는 방법 외에는 없습니다

 

 

다차원 배열도 만들 수 있습니다

int arr1[3][3] = {{1,2,3}, {4,5,6}, {7,8,9}};
int arr2[3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9}; 
int arr3[][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};  // 제일 첫 괄호안의 숫자만 생략가능

셋 모두 같은 값들로 초기화 됩니다.

 

 

접근은 인덱스가 0부터 시작한다는 점만 주의해서 접근하면 됩니다.

int arr[] = {1, 2, 3, 4, 5};

int x = arr[0]; // x = 1
int y = arr[4]; // y = 5

int z = arr[5]; // 정의되지 않은 값이 저장됨. 여기까지는 OK
z = 16;  // 정의되지 않은 행동(undefined behavior; ub). 절대 하면 안됨

 

또한, 동적 배열을 다음처럼 만들 수 있습니다.

#include <stdlib.h>

int size = 10;
int *arr = (int *)malloc(size * sizeof *arr); // 길이가 10인 int 배열

int new_size = 20;
int *arr2 = (int *)realloc(arr, new_size * sizeof *arr); // 길이가 20인 int 배열
if (arr2) arr = arr2;

free(arr);

 

C++ 에서는 realloc 에 해당하는 표준이 없습니다.

클래스가 아닌 기본 자료형을 담은 배열이라면 realloc 을 써도 되지만, 그렇지 않다면 무조건 std::vector을 이용해야 합니다.

 

C++ 에서는 동적 배열을 다음처럼 사용하면 됩니다.

std::vector<int> arr(10);
arr.resize(20);

 

2. 포인터

여기서부터 사람들이 C언어를 멀리하고, 자연스레 C++ 도 멀리하게 시작됩니다.

저도 처음에는 되게 헷갈렸는데, 개념이 헷갈리는게 아니라 문법이 헷갈려서 삽질을 많이 했습니다.

여기서는 배열 포인터 같은건 머리만 아파지니 다루지 않겠습니다

(C++ 을 위한 강좌다보니 C++ 에서 쓸일이 거의 없는 것은 넣지 않았습니다)

 

 

포인터는 어떠한 A를 가리키는 새로운 B를 만드는 것입니다.

정확하게는, A가 어떠한 것이 있을때, B는 A의 메모리 주소를 값으로 가지고 있습니다.

A 메모리 주소를 값으로 가지고 있어서, A를 가리킨다고 하는 것이죠.

B도 결국 값을 가지고 있는 변수입니다. B에다가 +1을 하는것도 올바른 연산입니다. 대신 이제 뭘 가리키는지는 알 수가 없게 되겠죠.

 

어떤 타입의 주소를 반환하는 법과, 저장된 주소에 접근하는 법을 알아보겠습니다.

 

int num = 3;
int* ptr = &num;
*ptr = 4;
int newnum = *ptr;

printf("%d", newnum);

포인터의 타입은 가리키고자 하는 자료형의 타입 뒤에 *를 붙이면 됩니다.

num을 가리키는 포인터의 타입은 int*가 됩니다.

만약 ptr을 가리키는 포인터를 만든다면 그 타입은 int**가 됩니다

* 기호를 int 에 붙여서 쓰나 띄워서 쓰나 상관 없습니다.

num의 주소값을 받기 위해 num 앞에 포인터를 반환하는 & 연산자(address of operator)를 붙여  ptr에 넣어주고, 가리키는 값을 반환하는 * 연산자를 통해 ptr이 가리키는 값(num 의 값)을 4로 바꾸어 주었습니다.

 

int* 에서 * 별개로는 아무 의미가 없습니다. int* 자체로 하나의 타입 입니다.

*ptr 에서 쓰이는 * 는 ptr이 가리키는 값을 반환하는 연산자입니다(indirection operator)

 

이걸 왜 쓰나 싶으실 텐데, 나중에 함수에서 함수 외부의 값을 함수 안에서 바꿀 때, 혹은 원본이 크기가 커서 전달을 빠르게 하기 위해 사용하게 됩니다. 클래스에서는 또 다른 의미로 사용하게 됩니다.

C++ 에서는 래퍼런스로 훨씬 쉽게 가능합니다. 래퍼런스는 원본 그 자체입니다. 그냥 원본에게 새로운 이름을 하나 더 붙여준 것 뿐입니다.

int x = 3;
int& ref = x;
ref = 4;
int newnum = ref;

std::cout << newnum << std::endl;

래퍼런스는 C++ 강좌에서 더 다루기로 하고, 다만 C++ 에서는 더 쉽고 직관적으로 가능하다는 사실만 알아두고 계시면 되겠습니다.

 

또한, 배열도 기본적으로는 포인터입니다.

그래서 이를 다른 포인터에 넘겨주게 되면, 배열이 포인터로 붕괴(array to pointer decay)가 일어나게 됩니다.

여기서부터 온갖 끔찍한 일이 발생하게 됩니다.

 

int num = 3;
int arr[] = {1,2,3};
int* ptr;


ptr = &num;
ptr = arr;  // 둘 다 가능

int x = ptr[1]; // x = 2; 하지만 ptr이 &num 이었다면? 

결국 ptr 만 보고서는 무엇을 가리키는지 알 수 없게 됩니다.

 

C++ 에서는 기본 컨테이너를 사용하여 이런 현상을 막을 수 있습니다

int num = 3;
std::array<int, 3> arr = {1, 2, 3};

int& ref = num;
int& ref2 = arr; // 컴파일 에러!
auto& arr_ref = arr;

 

또한 포인터에 const 를 붙여 다른 값을 가리키지 못하게 할 수 있습니다.

int x = 1;
int y = 2;
const int z = 3;
const int w = 4;

int* p1 = &x;
p1 = &y;
*p1 = 99;
p1 = &z; // 가능은 하나 적절하지 않은 사용임

const int* p2 = &x;
p2 = &z;
*p2 = 99; // 컴파일 에러

int* const p3 = &x;
p3 = &y; // 컴파일 에러
*p3 = 99;

const int* const p4 = &x;
p4 = &w; // 컴파일 에러
*p4 = 99; // 컴파일 에러

 

const int* 는 말 그대로 const int 를 가리키는 포인터이기에, 포인터 자체는 다른 값을 가리킬 수 있습니다.

int* const 는 int 를 가리키는 const 포인터이기에 다른 값을 가리킬 수는 없지만, 가리키는 값이 그냥 int 기 때문에 자신이 가리키는 값의 값은 바꿀 수 있습니다.

const int* const 는 const int 를 가리키는 const 포인터이기에 자신도 다른 값을 가리킬 수 없고, 자신이 가리키는 값의 값도 변경이 불가능합니다.

  int* ptr; const int* ptr; int* const ptr; const int* const ptr;
다른 값 가리키기 가능 가능 불가능 불가능
가리키는 값의 값을 변경 가능 불가능 가능 불가능

요약

배열은 변수명 뒤에 대괄호를 붙여 만들 수 있다.

배열의 길이보다 적은 값을 중괄호로 초기화 하면, 나머지 값들은 0으로 초기화 된다(구조체 배열도 0으로 초기화 됨)

배열의 첫번째 길이를 명시하지 않고 중괄호로 초기화 하면 길이가 알아서 정의된다.

배열의 인덱스는 0부터이다

포인터를 이용하여 동적 배열을 만들 수 있다.

 

배열을 다른 변수로 전달하는 순간 포인터로 붕괴한다 (array to pointer decay)

상수 포인터는 자신이 다른 값을 가리키지는 못하지만, 지금 가리키고 있는 값이 상수가 아니라면 그 값은 변경할 수 있다.

 

 

배열은 같은 타입밖에 담지 못하는데요, 다음 강좌에서는 여러가지 타입을 담을 수 있는 구조체와, 또다른 유용한 기능인 열거자와 공용체에 대해서 알아보겠습니다.

'C' 카테고리의 다른 글

C++ 을 위한 간략한 C 강좌 1 - 리터럴 및 기본 자료형  (0) 2020.07.25