코젤브

포인터 & 배열 & 벡터(Vector) 본문

컴공의 일상/C++

포인터 & 배열 & 벡터(Vector)

코딩하는 젤리 2024. 8. 9. 18:13

 

C++에서 포인터는 매우 중요한 개념이며, 메모리 관리, 데이터 구조, 그리고 성능 최적화 등에 널리 사용됩니다.

 

1. 포인터란?

포인터는 다른 변수나 메모리 주소를 가리키는 변수입니다. 즉, 포인터는 메모리 주소를 저장하는 변수로, 해당 주소에 저장된 값을 간접적으로 접근할 수 있게 해줍니다.

포인터의 기본 개념:

  • 메모리 주소: 컴퓨터의 메모리는 바이트 단위로 주소를 가지고 있습니다. 포인터는 이 주소를 저장하는 변수입니다.
  • 참조(Reference): 포인터가 가리키는 메모리 주소에 저장된 실제 값을 의미합니다.
  • 간접 참조 연산자(``): 포인터가 가리키는 주소에 있는 값을 접근할 때 사용됩니다.
  • 주소 연산자(&): 변수의 메모리 주소를 얻을 때 사용됩니다.

 

2. 포인터 선언과 초기화

포인터를 선언할 때는 가리킬 변수의 타입을 지정해줘야 합니다. 포인터는 특정 타입의 메모리 주소를 가리키기 때문에, 이를 명시적으로 나타내야 합니다.

int a = 10;       // 정수형 변수 a 선언 및 초기화
int* p = &a;      // 정수형 포인터 p 선언, a의 주소를 가리키도록 초기화

위 코드에서:

  • int* p는 정수형 포인터 p를 선언하는 코드입니다.
  • p = &a는 p가 변수 a의 메모리 주소를 가리키도록 초기화합니다.

 

3. 포인터를 사용한 값 접근

포인터를 사용하여 가리키고 있는 주소의 값을 읽거나 변경할 수 있습니다. 이를 위해 간접 참조 연산자(*)를 사용합니다.

int a = 10;
int* p = &a;

std::cout << "Value of a: " << *p << std::endl; // 포인터를 사용해 a의 값 출력

*p = 20;  // 포인터를 사용해 a의 값을 20으로 변경
std::cout << "Updated value of a: " << a << std::endl;

위 코드에서 *p는 포인터 p가 가리키고 있는 메모리 주소에 저장된 값을 나타냅니다. 따라서 *p = 20;은 a의 값을 20으로 변경하게 됩니다.

 

4. NULL 포인터

포인터는 항상 유효한 메모리 주소를 가리켜야 합니다. 만약 그렇지 않다면, 포인터가 잘못된 메모리 접근을 시도할 수 있습니다. 이러한 상황을 피하기 위해 포인터가 아무 것도 가리키지 않도록 할 수 있습니다. 이를 NULL 포인터라고 합니다.

int* p = nullptr; // C++11에서 도입된 nullptr 키워드를 사용해 포인터를 초기화

이 코드는 p가 어떤 유효한 메모리 주소도 가리키지 않음을 의미합니다. 이전에는 NULL 매크로를 사용했지만, C++11부터는 nullptr 키워드를 사용하는 것이 권장됩니다.

 

5. 포인터와 배열

포인터는 배열과 매우 밀접한 관계가 있습니다. 배열의 이름은 사실상 배열의 첫 번째 요소를 가리키는 포인터로 해석됩니다.

int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;  // arr은 배열의 첫 번째 요소를 가리키는 포인터로 사용됨

std::cout << "First element: " << *p << std::endl; // arr[0]과 동일
p++;
std::cout << "Second element: " << *p << std::endl; // arr[1]과 동일

위 코드에서 p는 arr 배열의 첫 번째 요소를 가리킵니다. p++를 통해 포인터를 증가시키면, 배열의 다음 요소를 가리키게 됩니다.

 

6. 이중 포인터와 다중 레벨 포인터

포인터는 또 다른 포인터의 주소를 가리킬 수 있습니다. 이를 이중 포인터라고 합니다. 이중 포인터는 주로 포인터를 수정할 필요가 있을 때 사용됩니다.

int a = 10;
int* p = &a;       // p는 a의 주소를 가리킴
int** pp = &p;     // pp는 p의 주소를 가리킴

std::cout << "Value of a: " << **pp << std::endl; // 이중 간접 참조를 통해 a의 값 접근

 

7. 동적 메모리 할당과 포인터

C++에서는 new와 delete 키워드를 사용해 동적으로 메모리를 할당하고 해제할 수 있습니다. 이때 할당된 메모리의 주소를 포인터로 관리하게 됩니다.

int* p = new int(10); // 동적으로 정수형 변수를 할당하고 초기화
std::cout << "Value: " << *p << std::endl;
delete p;            // 메모리 해제

위 코드에서는 new를 사용해 정수형 메모리를 동적으로 할당하고, 해당 메모리의 주소를 포인터 p가 가리키도록 합니다. 사용이 끝나면 delete를 통해 할당된 메모리를 해제합니다.

 

8. 함수 포인터

포인터는 함수의 주소도 가리킬 수 있습니다. 이를 통해 함수를 간접적으로 호출할 수 있으며, 콜백 메커니즘이나 동적 함수 호출에 유용합니다.

void myFunction() {
    std::cout << "Hello from myFunction!" << std::endl;
}

int main() {
    void (*funcPtr)() = &myFunction; // 함수 포인터 선언 및 초기화
    funcPtr();  // 함수 포인터를 통해 함수 호출
    return 0;
}

 

결론

포인터는 C++에서 매우 강력한 도구로, 메모리와 밀접하게 연관되어 있습니다. 하지만 포인터를 잘못 사용하면 프로그램의 불안정성을 초래할 수 있으므로 주의가 필요합니다. 포인터의 개념을 명확히 이해하고, 안전하게 사용하는 것이 중요합니다.

그렇다면 배열이 포인터로 구현된 것인가? 라는 생각이 들었습니다.

C++에서 배열의 이름은 배열의 첫 번째 요소를 가리키는 포인터로 사용될 수 있으며, 배열과 포인터는 많은 부분에서 유사하게 동작합니다. 그러나 배열과 포인터는 몇 가지 중요한 차이점이 있으며, 이 차이점을 이해하는 것이 중요합니다.


배열과 포인터의 관계

 

1. 배열 이름은 첫 번째 요소를 가리키는 포인터

C++에서 배열의 이름은 배열의 첫 번째 요소를 가리키는 포인터로 취급됩니다. 따라서 배열 이름은 사실상 포인터처럼 사용될 수 있습니다.

int arr[5] = {1, 2, 3, 4, 5};
int* p = arr; // arr은 배열의 첫 번째 요소를 가리키는 포인터로 해석됨

위 코드에서 arr은 int* 타입으로 해석되며, arr은 배열의 첫 번째 요소의 주소를 가리키는 포인터로 사용할 수 있습니다.

 

2. 배열 요소에 대한 포인터 연산

포인터를 사용하여 배열 요소에 접근할 수 있으며, 포인터 연산을 통해 배열의 다른 요소로 이동할 수 있습니다.

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

std::cout << *p << std::endl;   // 첫 번째 요소 출력, arr[0]과 동일
std::cout << *(p + 1) << std::endl; // 두 번째 요소 출력, arr[1]과 동일

이 예제에서 *(p + 1)은 p가 가리키는 메모리 주소에서 한 칸 뒤에 있는 요소를 의미하며, 이는 arr[1]과 동일한 값을 가리킵니다.

 

배열과 포인터의 차이점

배열과 포인터는 비슷하게 보이지만, 몇 가지 중요한 차이점이 있습니다.

1. 배열의 크기 정보

  • 배열: 배열은 특정 크기로 고정되어 있으며, 배열의 크기는 컴파일 타임에 결정됩니다. sizeof(arr)를 사용하면 배열 전체의 크기를 얻을 수 있습니다.
  • 포인터: 포인터는 단순히 메모리 주소를 가리키며, 가리키는 메모리 블록의 크기를 알 수 없습니다. sizeof(p)는 포인터 자체의 크기(보통 4 또는 8바이트)를 반환합니다.
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;

std::cout << "Size of arr: " << sizeof(arr) << std::endl; // 배열 전체 크기: 20 (5 * sizeof(int))
std::cout << "Size of p: " << sizeof(p) << std::endl;     // 포인터 크기: 8 (64비트 시스템에서)

 

2. 메모리 할당 방식

  • 배열: 배열은 스택 메모리에 할당되며, 그 크기는 고정되어 있습니다.
  • 포인터: 포인터를 통해 동적 메모리 할당을 할 수 있으며, 힙(heap) 메모리에서 크기가 유동적인 메모리 블록을 가리킬 수 있습니다.
int* p = new int[5]; // 동적으로 크기가 5인 정수형 배열 할당
delete[] p;          // 할당된 메모리 해제

 

3. 상수성과 수정 가능성

  • 배열 이름: 배열 이름은 상수 포인터처럼 동작하여, 다른 주소로 변경할 수 없습니다.
  • 포인터: 포인터는 가리키는 주소를 자유롭게 변경할 수 있습니다.
int arr[5] = {1, 2, 3, 4, 5};
int* p = arr;
p = nullptr; // 포인터 p는 다른 주소를 가리킬 수 있음

// arr = nullptr;  // 오류: 배열 이름(arr)은 다른 주소를 가리킬 수 없음

 

결론

C++에서 배열의 이름은 배열의 첫 번째 요소를 가리키는 포인터로 사용될 수 있으며, 배열과 포인터는 많은 부분에서 유사하게 동작합니다. 그러나 배열과 포인터는 몇 가지 중요한 차이점이 있으며, 이 차이점을 이해하는 것이 중요합니다.

특히, 배열은 고정된 크기를 가지며 스택에 할당되는 반면, 포인터는 동적 메모리 할당을 통해 유동적인 크기의 메모리 블록을 가리킬 수 있습니다. 포인터는 다양한 용도로 사용되며, 특히 동적 메모리 관리나 함수 인수로 배열을 전달할 때 유용합니다.

 

그럼 이제 배열까지 완벽하게 이해했으니.. 벡터까지 공부하고 싶은게 사람의 마음이다.

 

C++에서 자주 쓰이는 벡터 Vector란 무엇일까?


벡터(Vector)

C++의 std::vector는 매우 중요한 표준 라이브러리 컨테이너로, 동적 배열(dynamic array)로서 다양한 상황에서 사용됩니다. 벡터는 배열과 유사하지만 더 많은 기능과 유연성을 제공합니다. 배열 및 포인터와의 비교와 함께 벡터의 특성에 대해 설명드리겠습니다.

1. 벡터(Vector)란?

  • std::vector는 C++ 표준 라이브러리의 템플릿 클래스 중 하나로, 크기가 동적으로 변경 가능한 배열을 제공합니다. 벡터는 배열과 마찬가지로 연속적인 메모리 블록을 사용하지만, 동적 크기 조정, 요소 추가 및 제거, 다양한 유틸리티 메서드를 제공합니다.

 

2. 벡터와 배열의 비교

공통점

  • 연속적인 메모리 블록: 벡터와 배열 모두 메모리 내에서 연속적인 메모리 블록을 차지합니다. 따라서 배열과 벡터는 모두 포인터를 통해 효율적으로 요소에 접근할 수 있습니다.
  • 인덱스를 통한 접근: 벡터와 배열 모두 인덱스를 사용하여 개별 요소에 빠르게 접근할 수 있습니다. (O(1) 시간 복잡도)
std::vector<int> vec = {1, 2, 3, 4, 5};
int arr[5] = {1, 2, 3, 4, 5};

std::cout << vec[2] << std::endl; // 벡터에서 세 번째 요소 접근
std::cout << arr[2] << std::endl; // 배열에서 세 번째 요소 접근

차이점

  • 크기 조정:
    • 배열: 배열의 크기는 고정되어 있으며, 선언 시점에 크기를 결정해야 합니다. 크기를 변경하려면 새 배열을 할당하고 요소를 복사해야 합니다.
    • 벡터: 벡터는 요소를 추가하거나 제거함에 따라 자동으로 크기를 조정합니다. 크기가 필요한 만큼 증가하거나 감소합니다.
std::vector<int> vec = {1, 2, 3};
vec.push_back(4); // 크기를 동적으로 늘려가며 요소 추가

int arr[3] = {1, 2, 3};
// arr[3] = 4; // 오류: 배열은 크기가 고정되어 있음
  • 메모리 관리:
    • 배열: 배열은 스택에 할당되거나, 동적으로 할당된 경우 개발자가 직접 메모리를 관리해야 합니다.
    • 벡터: 벡터는 메모리를 자동으로 관리합니다. 벡터의 크기가 증가할 때마다 내부적으로 새로운 메모리 블록을 할당하고 기존 요소를 복사합니다. 벡터가 더 이상 필요하지 않으면 자동으로 메모리가 해제됩니다.
std::vector<int> vec = {1, 2, 3};
// vec는 자동으로 메모리를 관리함

int* arr = new int[3] {1, 2, 3};
delete[] arr; // 배열의 동적 메모리는 직접 해제해야 함
  • 유틸리티 메서드:
    • 배열: 배열에는 고정된 크기와 인덱스를 통한 접근 이외의 기능이 없습니다.
    • 벡터: 벡터는 크기 조정(push_back, pop_back), 삽입(insert), 삭제(erase), 크기 확인(size, empty), 메모리 관리(reserve, shrink_to_fit) 등 다양한 유틸리티 메서드를 제공합니다.
std::vector<int> vec = {1, 2, 3};
vec.push_back(4);  // 요소 추가
vec.pop_back();    // 마지막 요소 제거
vec.insert(vec.begin() + 1, 10); // 두 번째 위치에 10 삽입
vec.erase(vec.begin());          // 첫 번째 요소 삭제
  • 안전성:
    • 배열: 크기를 초과하여 접근하려고 하면 정의되지 않은 동작(예: 버퍼 오버플로우)이 발생할 수 있습니다.
    • 벡터: 벡터는 경계 검사(boundary check)를 수행하여 안전하게 사용할 수 있습니다. .at() 메서드를 사용하면 경계를 초과하는 경우 예외가 발생합니다.
std::vector<int> vec = {1, 2, 3};
try {
    std::cout << vec.at(5) << std::endl; // out_of_range 예외 발생
} catch (const std::out_of_range& e) {
    std::cout << "Exception: " << e.what() << std::endl;
}

 

3. 벡터와 포인터의 연관성

  • 벡터 내부 구현: 벡터는 내부적으로 동적 배열을 관리하기 위해 포인터를 사용합니다. 벡터는 요소를 동적으로 추가할 때마다, 내부적으로 포인터를 사용하여 새로운 메모리 블록을 할당하고, 기존 요소를 새로운 위치로 복사합니다.
  • 포인터와의 호환성: 벡터의 데이터는 연속적인 메모리 블록에 저장되므로, 배열과 유사하게 벡터의 요소에 대한 포인터를 사용할 수 있습니다. &vec[0] 또는 vec.data()를 사용하여 벡터의 내부 데이터를 가리키는 포인터를 얻을 수 있습니다.
std::vector<int> vec = {1, 2, 3, 4, 5};
int* p = &vec[0]; // 벡터의 첫 번째 요소를 가리키는 포인터
std::cout << *p << std::endl; // 벡터의 첫 번째 요소 출력
  • 벡터와 동적 배열: 벡터는 동적으로 크기가 조정되는 배열로, 크기가 초과될 때마다 새로운 메모리 블록을 할당하고, 기존 데이터를 복사하는 과정을 자동으로 처리합니다. 따라서 포인터를 사용하여 배열을 동적으로 관리하는 것보다 벡터를 사용하는 것이 훨씬 편리합니다.

결론

  • 배열: 크기가 고정된 연속적인 메모리 블록을 제공하며, 간단하고 효율적이지만 크기 변경이 불가능하고, 메모리 관리를 직접 해야 합니다.
  • 벡터: 동적 크기 조정이 가능한 배열로, 추가적인 유틸리티 메서드와 자동 메모리 관리 기능을 제공합니다. 벡터는 배열의 모든 장점을 가지면서 더 많은 유연성을 제공합니다.
  • 포인터: 벡터는 내부적으로 동적 메모리를 관리하기 위해 포인터를 사용하며, 배열과 유사하게 포인터와의 연관성을 가지고 있습니다.

벡터는 C++에서 동적 배열이 필요한 경우 가장 일반적으로 사용되는 컨테이너 중 하나로, 안전하고 유연한 메모리 관리를 가능하게 합니다. 벡터를 사용함으로써 개발자는 메모리 관리의 복잡성에서 벗어나 더 높은 수준의 추상화된 코드를 작성할 수 있습니다.

 


 

C++ 포인터, 배열, 벡터의 개념 요약

  • 포인터:
    • 메모리 주소를 저장하는 변수로, 해당 주소에 저장된 값을 간접적으로 접근할 수 있게 해줍니다.
    • * 연산자를 사용해 포인터가 가리키는 값을 참조하고, & 연산자를 사용해 변수의 주소를 얻을 수 있습니다.
    • NULL 포인터(nullptr)를 사용해 포인터가 유효한 메모리 주소를 가리키지 않도록 초기화할 수 있습니다.
    • 배열의 이름은 배열의 첫 번째 요소를 가리키는 포인터로 취급됩니다.
  • 배열:
    • 고정된 크기의 연속적인 메모리 블록을 가지며, 선언 시점에 크기가 결정됩니다.
    • 배열 이름은 상수 포인터처럼 동작하여 다른 주소로 변경할 수 없습니다.
    • 배열은 스택에 할당되며, 동적 크기 조정이 불가능합니다.
  • 벡터 (std::vector):
    • 동적으로 크기가 조정 가능한 배열로, 배열과 달리 요소를 추가하거나 제거할 수 있습니다.
    • 자동으로 메모리를 관리하며, 크기 조정 시 새로운 메모리 블록을 할당하고 기존 요소를 복사합니다.
    • 다양한 유틸리티 메서드(push_back, insert, erase 등)를 제공하여 배열보다 유연하게 사용할 수 있습니다.
    • 벡터의 데이터는 연속적인 메모리 블록에 저장되므로, 포인터로 벡터 요소에 접근할 수 있습니다.

 

배열은 고정 크기의 메모리 블록을 제공하며, 포인터와 밀접한 관계가 있습니다. 반면, 벡터는 동적 크기 조정이 가능한 배열로, 메모리 관리와 유틸리티 메서드의 이점을 제공합니다. 벡터는 배열의 단점을 보완하면서 더 많은 기능과 유연성을 제공하는 강력한 도구입니다.