6) 배열과 포인터
배열 : 여러 개의 데이터를 저장하는 가장 기본적인 자료구조
6-1. C++ 배열 다루기
-
배열 초기화
int grade[3] = {1,2,3}; // 가능 int grade2[3]; grade2[3] = {1,2,3}; // 불가능!!
- 빈 중괄호 (
{}
)를 사용하면 전부 0으로 초기화됨.
- 빈 중괄호 (
-
C++ 컴파일러는 배열의 길이를 전혀 신경쓰지 않음.
=> 컴파일러는 실제 배열에 저장되어있는지를 확인하는게 아니라,
grade[3] = 4
를 하면 4번째 데이터가 저장될 주소만 계산해서 메모리에 값을 넣어주기 때문입니다.int grade[3] = {1,2,3}; int sum = 0; int main(){ grade[3] = 4; for (int i=0; i<4; i++){ sum += grade[i]; } std::cout << sum; // 10 -> 결과값은 4번째 원소까지 포함되었습니다. std::cout << sizeof(grade); // 12 (= 4*3) -> 하지만 4번째 원소는 배열에 없습니다. return 0; }
6-2. 2차원 배열
2차원 배열 = 1차원 배열들의 배열
하지만, 파이썬 리스트와 다른 점이라 하면 각 1차원 배열들의 크기가 같아야 한다는 점입니다. (사실 파이썬 리스트도 내부적으로는 비슷할것같아요)
//int two_dim_arr[3][]; -> 에러
int two_dim_arr[][4];
그렇기 때문에 각 1차원 배열들의 크기를 명시하지 않으면 에러가 납니다.
6-3. 포인터 변수
-
주소 : 해당 데이터가 저장된 메모리의 시작 주소
ex) int형 변수가 1~4까지를 점거하고 있다면 변수의 주소는 1이 됩니다.
-
포인터 변수 : 메모리의 주소값을 저장하는 변수
int n = 100; int *ptr = &n; // 포인터변수
*
와&
는 포인터 변수를 위해서 사용되는 포인터 연산자입니다. -
왜 포인터 변수가 필요한가?
=> 런타임에 이름 없는 메모리를 할당받아서 포인터를 이용해서 접근하기 위해.
-
포인터 변수의 크기는 일반적으로 자료형이 아니라 CPU에 따라서 결정됩니다.
- 64비트 CPU에서는 8바이트!
6-4. 포인터 연산자
-
&
: 해당 변수의 주소값을 반환 -
*
: (포인터 변수에 저장된) 주소값에 저장되어 있는 값을 반환ex) 사용예제
*포인터 변수
-> 포인터변수가 가지고 있는 주소값의 데이터*변수
-> 변수가 가지고 있는 주소값의 데이터- 이것도 역시 overloading의 일종입니다! (곱셈기호와 동일)
6-5. 포인터 변수의 연산
사실 6-3만 놓고 보면, 포인터 변수와 일반 변수의 차이가 딱히 느껴지지 않을 것입니다. 하지만, 포인터 변수의 연산은 조금 특별합니다.
- 포인터 변수의 덧셈
int num = 12;
double real = 12.34;
int *i_ptr = #
double *d_ptr = ℜ
int main() {
std::cout << (i_ptr) << std::endl;
std::cout << (i_ptr+1) << std::endl;
std::cout << (d_ptr) << std::endl;
std::cout << (d_ptr+1) << std::endl;
return 0;
}
출력값
0x600d68
0x600d6c // 4차이
0x600d70
0x600d78 // 8차이
포인터 변수에 정수를 더하면 정수 * 자료형 크기만큼 값이 증가한다는 것을 알 수 있습니다.
- 포인터 변수의 뺄셈
int num = 12;
int *i_ptr = #
int main() {
std::cout << (i_ptr+1) - (i_ptr) << std::endl;
std::cout << typeid((i_ptr+1) - (i_ptr)).name() << std::endl;
return 0;
}
출력값
1 // 4가 아님!!
l // 정수 포인터 두개를 빼면 long 타입의 자료형이?!
아무래도 주소값은 단위가 크다보니 long 타입을 사용하는 것 같습니다.
6-6. 포인터와 배열
배열 이름을 포인터 변수로 지정하면 배열처럼 사용할 수 있습니다.
왜냐하면 배열 이름은 배열의 시작 주소를 가리키기 때문입니다.
int arr[3] = {1, 22, 333};
int *ptr_arr = arr;
int main() {
for (int i=0; i<4; i++){
std::cout << ptr_arr[i] << std::endl; // 방법1
std::cout << *(ptr_arr+i) << std::endl; // 방법2
}
return 0;
}
포인터 변수를 인덱싱하기 위해서는 두 가지 방법을 사용할 수 있습니다.
배열과 차이점이라고 하면, 변수크기가 고정되어있고 최대크기를 넘어가면(다음 메모리에 값이 저장되어있지 않으면) 0이 출력된다는 점입니다.
// 출력값
1
1
22
22
333
333
0
0
status: 0
6-7. 메모리의 동적 할당
위에서 포인터의 장점은 이름 없는 메모리를 할당받을 수 있다는 겁니다.
이번에는 이에 대해서 설명하겠습니다.
-
프로세스의 구조
- data 영역, stack 영역 : 컴파일 시기에 미리 결정됨
- heap 영역 : 프로그램 실행 도중 (run time)에 사용자가 직접 결정
-
메모리의 동적 할당 : heap 영역에 메모리를 할당하는것.
-
C++은 C언어보다 효율적으로 메모리 동적 할당 & 해제를 할 수 있습니다.
-
new
연산자 : 메모리 동적 할당타입* 포인터이름 = new 타입;
- 비어있는 메모리를 찾아서 자동으로 변수(or 객체) 할당
- 메모리 자체의 이름이 없으므로 포인터로만 접근 가능
여기서 타입이라는건, int/double같은것도 되고, 클래스나 구조체도 가능합니다!
-
delete
연산자 : 메모리 반환- 사용하지 않는 메모리를 다시 운영체제로 반환
delete 포인터이름
- 반드시 new로 할당받은 메모리를 해제할 때만 사용해야함