객체 배열과 포인터 톺아보기_using_C

서론

C++은 객체 지향 프로그래밍 언어로서, 메모리와 객체를 직접적으로 다룰 수 있는 기능을 제공한다. 특히 객체 포인터, 객체 배열, 동적 메모리 할당 등의 개념은 프로그래밍의 효율성과 유연성을 크게 향상시킨다. 본 보고서에서는 객체 포인터와 배열의 선언 및 사용 방법, 메모리 할당 방식(정적/동적), 그리고 C++ STL에서 제공하는 vector 컨테이너에 대해 체계적으로 설명하고자 한다.



본론

2.1 객체 포인터

객체 포인터는 클래스의 인스턴스를 가리키는 포인터로 해당 객체의 주소를 저장한다.
포인터를 통해 객체의 멤버 함수나 멤버 변수에 접근할 수 있으며 일반적으로 -> 연산자를 사용한다.
여기서 인스턴스는 클래스 기반으로 만들어진 실제 객체를 의미한다.

2.2 객체 배열

객체 배열은 다음과 같은 형식으로 선언된다. 배열을 선언하면 각 원소 객체가 자동으로 생성되며, 생성자는 원소 수만큼 호출된다. 단, 매개변수가 있는 생성자는 사용할 수 없으며, 반드시 기본 생성자가 정의되어 있어야 한다. 이는 C++이 각 객체를 자동으로 생성할 때 매개변수를 전달하지 않고 기본 생성자를 호출하기 때문이다. 이를 공장에서 로봇 강아지 세 마리를 제작 요청한 상황에 비유할 수 있다. 공장(C++)은 설계서에 따라 로봇 강아지 객체(Dog)를 3개 생성해야 하는데, 설계가 이름을 필수로 받는 형태(Dog(string name))라면 문제가 발생한다. 이름을 제공하지 않으면 객체를 만들 수 없기 때문에, 기본 생성자가 반드시 필요하다.
예를 들어 Dog dogs[3];은 이름 없이 강아지 3마리를 생성하려고 하지만, 기본 생성자가 없고 Dog(string) 생성자만 존재할 경우 컴파일 오류가 발생한다. 반면 기본 생성자를 정의하면 정상적으로 작동한다.

그림 1.

그림 1.

그림 2.

그림 2.

Dog dogs[3]; 는 이름 없이 강아지 3마리를 만들고자 하지만 Dog(string) 생성자밖에 없어서 에러가 나는 것을 볼 수 있다.

그림 3.

그림 3.

그림 4.

그림 4.
위의 예시 코드는 기본 생성자를 정의했을 때 정상적으로 작동하는 것을 볼 수 있는 코드이다.

2.3 메모리 할당 방식 : 정적 할당

정적 할당은 프로그램 실행 시 미리 정해진 크기로 메모리를 할당한다.
컴파일할 때 크기가 결정되며 메모리 누수 걱정이 없다는 장점이 있다.
단점으로 크기 변경이 불가능하다는 점이 있다. 이것도 비유를 통해 쉽게 설명하자면 정적 할당은 마치 도시락을 싸가는 것과 같다. 도시락을 쌀 때 반찬통 5칸짜리로 정해 놓으면 밥이나 반찬이 더 필요해도 칸을 늘릴 수 없어 더 담을 수가 없다. 그래서 단점으로 크기 변경이 불가능하다는 점이 있던 것이다. 메모리 누수 걱정이 없다는 건 도시락을 다 먹으면 내가 설치를 하지 않아도 도시락통이 자동으로 비워지는 느낌이라고 생각하면 된다.참고로 정적 할당은 스택 메모리에서 사용되는 것이다. 조금 이따가 힙 메모리가 나올텐데 그럼 정적 할당은 어떤 메모리에서 사용되는 것인지 궁금해 할 사람도 있을 것 같아 참고 사항으로 적어둔다.

2.4 메모리 할당 방식 : 동적 할당

동적 할당은 실행 중 메모리를 필요에 따라 할당하며 newdelete 키워드를 사용한다. 힙 영역에서 메모리에 할당하며, 해제하지 않으면 메모리 누수가 발생한다.
동적 할당은 마치 뷔페와 같다. 내가 먹고 싶은 만큼만 음식을 접시에 담아 먹고 다 먹었으면 내가 직접 식기를 반납해야 하는 점에서 뷔페와 같다. 필요한 만큼만 담아가니 경제적이고 효율적이지만 다 먹고 나서 내가 치우지 않으면 식탁에 음식이 계속 남아있는 상태라서 결국에는 자원 낭비로 이어지게 된다.

2.5 new와 delete 연산자

2.5.1 new 연산자

new 연산자는 C++에서 프로그램 실행 중에 필요한 만큼 메모리를 동적으로 할당하는 연산자이다.
메모리는 **힙(heap)**이라는 공간에 할당되며 이 공간은 사용자가 직접 관리해줘야 한다. 즉, 언제 얼마나 메모리가 필요한지 미리 알 수 없을 때 또는 데이터를 공간에 유지해야 할 때 사용한다.
그림 5.

그림 5.

위와 같은 예시 코드와 함께 설명해보자면 int* pint = new int;는 정수형 1개 크기만큼 힙 메모리 공간을 동적으로 할당한다. new int는 힙에 정수 공간을 만들고 pint는 그 메모리 공간의 주소를 가지게 된다. char* pchar = new char; 은 문자형 1개 크기만큼 힙 메모리 공간을 동적으로 할당하고 pchar 는 그 메모리 공간의 주소를 가진다. delte pint;는 앞서 new int로 만든 정수형 힙 메모리를 해제한다. 동적으로 할당한 메모리는 반드시 delete로 해제해야 메모리 누수를 막을 수 있다. delete pchar;도 마찬가지로 앞서 new char로 만든 문자형 힙 메모리를 해제한다. 이 줄로 프로그램이 힙에 할당했던 메모리를 모두 반환하는 걸 알 수 있다. new 연산자 사용 시 주의사항을 예시 코드와 함께 알아보겠다.
그림 6.

그림 6.

위의 코드는 배열 전체를 10으로 초기화하려고 시도한 것이지만 (10)은 단일 변수일 때만 사용 가능하고 배열에서는 사용할 수 없어서 에러가 난다.
이 코드로 배열은 new로 할당할 때 특정 값으로 초기화할 수 없다는 사실을 알 수 있다.

2.5.2 delete 연산자

C++에서 new 연산자를 통해 동적으로 할당된 힙 메모리를 반환하는 데 사용하는 연산자이다. 힙 메모리는 자동으로 반환되지 않기 때문에 사용자가 직접 delete 또는 delete[]로 반환해야 한다.
delete 연산자 사용 시 주의사항이 2가지 있는데 이것도 예시 코드와 함께 알아보겠다.
그림 7.

그림 7.

n은 스택 메모리에 있는 변수로, 프로그램이 자동으로 관리하는데 delete는 힙 메모리에서 new로 생성한 데이터만 해제할 수 있다. 이 코드는 마치 내 자가용을 렌터카 반납소에 반납하려는 상황과 비슷하다.
new = 렌터카 빌리기, delete = 렌터카 반납하기라고 하면 자가용(n)을 렌터카 반납소에 들고가면 시스템이 망가지게 되는 것이다.
이로 인해 new로 생성하지 않은 메모리는 delete 하면 안 된다는 주의사항을 알 수 있다.
그림 8.

그림 8.

new int는 단일 변수를 생성하고 delete[]는 배열을 해제하려고 시도한다.
즉, 배열로 만들지 않은 메모리를 배열처럼 해제하려고 하는 것이다. 이로 인해 [] 생략에 주의해야 함을 알 수 있다.


2.6 객체 배열과 생성자/소멸자 호출

객체 배열을 생성하면 배열의 각 칸마다 객체가 하나씩 생성된다.
예를 들어 Circle arr[3]; 이라면 arr[0], arr[1], arr[2] 세 개의 Circle 객체가 만들어진다. 각 객체는 생성자를 통해 초기화된다. 객체 배열이 더 이상 사용되지 않거나 프로그램이 종료될 때 각 객체의 소멸자가 호출된다. 중요한 점은 생성자의 반대 순서로 소멸자들이 호출된다는 점이다. 이 말은 객체가 만들어질 때는 순서대로 생기고 사라질 때는 역순으로, 거꾸로 사라진다는 뜻이다. 이유가 궁금해서 찾아보니 스택 메모리는 나중에 넣은 게 먼저 나가는 구조인 LIFO구조를 사용하기 때문에 나중에 생성된 객체가 먼저 소멸되고 먼저 생성된 객체가 나중에 소멸된다. 배열을 만들 때 명시적으로 초기화한 원소들은 생성자를 호출하고 나머지 원소들은 기본 생성자로 초기화한다. 다음과 같은 예시 코드들 통해 설명해보겠다.
그림 9.

그림 9.

Circle circle[3] = {Circle(10), Circle(20)}; 이 줄로 보면 Circle circle[3]; 이라고 배열을 선언하고 {Circle(10), Circle(20)}으로 두 개의 원소만 초기화 된 것을 확인할 수 있다. 배열의 크기는 3인 것도 알 수 있다. 그러므로 circle[0], circle[1], circle[2] 이렇게 세 개의 원소가 생긴다. 배열의 첫 번째 원소인 circle[0]과 두 번째 원소인 circle[1]Circle(10)Circle(20)으로 초기화되고 세 번째 원소 circle[2]는 기본 생성자 Circle()로 초기화 된다.


2.7 객체 new 연산자와 객체 delete 연산자

객체 new 연산자는 필요한 만큼 메모리를 할당받는 동적 할당과 같은데 데이터형 자리엔 클래스 이름으로 대체될 뿐이다.
클래스이름 *포인터변수 = new 클래스; , 클래스이름 *포인터변수 = new 클래스이름(생성자 매개변수 리스트); 형식이 기본 형식이다. 클래스 크기의 메모리를 할당받아 객체를 생성하여 생성자를 호출하고 포인터변수에 해당 주소를 대입한다. 매개 변수가 없는 경우 기본 생성자가 호출된다.
그림 10.

그림 10.

그림 11.

그림 11.

위의 예시 코드와 함께 설명해보자면 Point* p = new Point(0,0);Point* p2 = new Point(10,20); 는 기본 생성자 Point()를 호출한다. x,y는 기본값 0,0으로 초기화 되고 메모리는 힙에 할당된다. p는 그 객체의 주소를 가지고 x,y가 각각 10과 20으로 초기화된다. 객체 delete 연산자도 같이 살펴보면 delete 포인터변수; 형식으로 쓰는데 여기서는 delete p;delete p2;를 써서 반환시킨 것을 확인할 수 있다. 객체 배열 new 연산자를 쓰는 또 다른 방법으로는 클래스이름* 포인터변수 = new 클래스이름[배열크기]; 형식이 있다.
이건 객체 배열을 힙에 동적으로 만드는 방법인데 이렇게 만든 객체 배열은 delete
아니라 delete[]로 지워야 한다.


2.8 vector 컨테이너

벡터는 C++의 STL에 속해 있는 라이브러리 클래스동적배열을 모델링한 클래스이다. 일반 배열은 크기가
정해져 있는 정적 배열인데 동적 배열은 크기가 가변적이고 벡터는 템플릿으로 설계되어 제공된다. 여기서 템플릿이란 미정적 자료형으로 설계된 클래스를 의미한다. 따라서 벡터는 배열의 크기를 미리 정할 필요가 없고 객체 생성 시 자료형을 확정해야한다. 클래스 이름 뒤에 <자료형>을 이용해서 자료형을 확정한다. vector<int> scores(10) 이런식으로 배열의 자료형, 배열의 이름(scores), 배열의 크기까지 정해준다.

다음으로는 벡터에서 사용하는 멤버 함수에 대해 알아볼건데, 많은 멤버 함수가 있지만 가장 많이 쓰이는 함수에 대해서만 알아보겠다 v.push_backv.pop_back이 있다. push_back은 말 그대로 element를 추가하는 것이고, pop_back은 마지막 element를 삭제하는 것이다. 이외에도 지정한 index 위치의 요소를 반환하는 at(int index), 첫번째 배열 요소를 가리키는 반복자를 반환하는 begin(), 벡터의 마지막 요소 다음 위치를 가리키는 반복자를 반환하는 end(), 벡터가 비어 있으면 true를 반환하고, 요소가 하나라도 있으면 false를 반환하는 empty(), 지정한 위치의 요소를 벡터에서 삭제하는 erase, 지정한 위치에 새로운 요소를 삽입하는 insert, 벡터에 저장된 요소의 개수를 반환하는 size(), [] 연산자를 사용해 요소에 접근하며 범위 검사를 하지 않는 operator[], 하나의 벡터를 다른 벡터에 복사 대입할 때 사용되는 operator=()이 있다.


결론

본 블로그에서는 C++에서 객체와 메모리 관리를 다루는 다양한 개념들을 체계적으로 살펴보았다.
객체 포인터와 객체 배열을 통해 클래스 인스턴스를 동적으로 생성하고 활용하는 방법을 익혔으며 정적 할당과 동적 할당의 차이를 일상적 비유와 함께 쉽게 이해할 수 있었다. 특히 new와 delete 연산자를 활용한 동적 메모리 관리 방법은 C++에서 매우 중요한 부분으로 적절한 메모리 해제가 이루어지지 않으면 메모리 누수와 같은 심각한 문제로 이어질 수 있다는 점을 강조하였다.
또한 객체 배열의 생성자 및 소멸자 호출 순서를 통해 객체 생명주기의 흐름을 이해하였고 STL의 벡터 컨테이너를 통해 배열보다 더 유연하고 안전한 자료구조 사용법을 익혔다. 벡터는 크기 조절이 자유롭고 다양한 멤버 함수를 제공함으로써 프로그램의 효율성과 가독성을 향상시켜준다는 점을 알 수 있었다. 반복자(iterator)를 통한 요소 접근, 삽입, 삭제 등의 멤버 함수 덕분에 실무에서 유용하게 사용할 수 있다는 점도 깨달았다.
결론적으로 C++에서 객체와 메모리 관리는 다소 복잡하다고 느낄 수 있지만 개념과 원리를 충분히 이해하고 실습을 반복한다면 누구나 효과적으로 잘 활용할 수 있다고 생각한다. 객체 포인터, 배열, 메모리 할당, 그리고 벡터와 같은 STL 컨테이너의 사용법을 익힌다면 프로그래밍 실력을 한층 더 업그레이드 시키는 데 도움이 될 것이다.

참고문헌

  • [C++]객체 포인터, 객체 배열, 동적 메모리 할당 . (2017). https://andamiro25.tistory.com/71.
  • [명품C++프로그래밍] 4장 객체 포인터와 객체 배열, 객체의 동적 생성 요약 정리 . (2023). https://airforce836.tistory.com/entry/%EB%AA%85%ED%92%88C%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-4%EC%9E%A5-%EA%B0%9D%EC%B2%B4-%ED%8F%AC%EC%9D%B8%ED%84%B0%EC%99%80-%EA%B0%9D%EC%B2%B4-%EB%B0%B0%EC%97%B4-%EA%B0%9D%EC%B2%B4%EC%9D%98-%EB%8F%99%EC%A0%81-%EC%83%9D%EC%84%B1-%EC%9A%94%EC%95%BD-%EC%A0%95%EB%A6%AC.
  • [명품 C++] 04 객체 포인터와 객체 배열, 객체의 동적 생성 . (2022). https://danhandev.tistory.com/entry/%EB%AA%85%ED%92%88-C-04-%EA%B0%9D%EC%B2%B4-%ED%8F%AC%EC%9D%B8%ED%84%B0%EC%99%80-%EA%B0%9D%EC%B2%B4-%EB%B0%B0%EC%97%B4-%EA%B0%9D%EC%B2%B4%EC%9D%98-%EB%8F%99%EC%A0%81-%EC%83%9D%EC%84%B1.
  • C++에서 new delete와 std::vector를 이용한 동적배열의 차이 . (2023). https://desafinado.tistory.com/46.