My Profile Photo

DongChanS's blog


수학과 학생의 개발일지


파이썬 유저를 위한 C++ (3) - 배열과 포인터

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 = &num;
double *d_ptr = &real;

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 = &num;

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. 메모리의 동적 할당

위에서 포인터의 장점은 이름 없는 메모리를 할당받을 수 있다는 겁니다.

이번에는 이에 대해서 설명하겠습니다.

  • 프로세스의 구조

    22

    • data 영역, stack 영역 : 컴파일 시기에 미리 결정됨
    • heap 영역 : 프로그램 실행 도중 (run time)에 사용자가 직접 결정
  • 메모리의 동적 할당 : heap 영역에 메모리를 할당하는것.

  • C++은 C언어보다 효율적으로 메모리 동적 할당 & 해제를 할 수 있습니다.

  1. new 연산자 : 메모리 동적 할당

    타입* 포인터이름 = new 타입;
    
    • 비어있는 메모리를 찾아서 자동으로 변수(or 객체) 할당
    • 메모리 자체의 이름이 없으므로 포인터로만 접근 가능

    여기서 타입이라는건, int/double같은것도 되고, 클래스나 구조체도 가능합니다!

  2. delete 연산자 : 메모리 반환

    • 사용하지 않는 메모리를 다시 운영체제로 반환
    delete 포인터이름
    
    • 반드시 new로 할당받은 메모리를 해제할 때만 사용해야함
comments powered by Disqus