본문 바로가기
프로그래밍 공부/C++ 프로그래밍

[C++ 기초] 반복문(Loop Statement)과 스택 프레임(Stack Frame), 배열 형식과 STL 컨테이너

by 섬댕이 2023. 5. 10.

 

오늘자 코딩 문제를 풀던 중 반복문을 이용하여 char 배열에 입력 데이터(문자열)를 저장하고 이를 STL vector 클래스의 요소로 저장하려는 과정에서, 배열을 요소로 직접 컨테이너에 넣을 수가 없어 char* 형의 요소를 가지는 vector를 선언하여 해결해보려 했는데 이것 때문에 오류가 발생하였다.

* STL의 컨테이너는 배열로 선언된 데이터를 요소로 가질 수 없다(아래의 2) 참고).

 

 

// 문제의 코드
#include <iostream>
#include <vector>

int main()
{
	int N = 10;
	std::vector<char*> Pokemons;
    
	while (N--)
	{
		char pokemon[21] = { 0, };
		scanf("%s", pokemon);
   
		Pokemons.push_back(pokemon);
	}
    
	for (const char* pokemon : Pokemons)
		printf("%s\n", pokemon);
    
	return 0;
}

 

위와 같이 코드를 짜고 난 뒤에 문제에서 예시로 제공하는 코드를 복사하여 커맨드 라인에 붙여넣어 실행해보면, 가장 마지막에 입력된 문자열인 "Raichu" 라는 문자열만 반복하여 26줄이 출력되는 현상이 발생한다. 반복문에서는 지속적으로 문자열이 scanf 함수에 의해 초기화 되어 표면상으로는 마치 입력받은 문자열 데이터를 가리키는 포인터를 vector 컨테이너에 잘 보관하는 것처럼 보이나, 컨테이너에 보관되는 포인터가 가리키는 주소는 변하지 않기 때문이다. 이러한 결과가 나오는 이유에 대해서 좀 더 분석해보았다.

 

버그 폼 미쳣다

 

1) 반복문 내 로컬 변수의 주소 관련

이와 관련하여 구글링을 해본 결과로 알아낸 사실은, 컴파일러에 따라 다를 수는 있으나 일반적으로는 반복문이 각 루프를 반복할 때 반복문 내에서 선언되어 사용되는 로컬 변수의 주소는 반복문이 처음 시작하고 나면 종료될 때까지 바뀌지 않는다는 것이다(직접 주소를 출력해보면 이 사실을 직접 확인해볼 수 있다).

 

2) 배열을 요소로 가질 수 없는 STL의 컨테이너

STL의 컨테이너는 요소로서 배열의 형태로 된 데이터를 가질 수 없다. STL의 컨테이너에 저장될 수 있는 데이터는 반드시 단순 복사 연산(copyable)과 대입 연산이 가능(assignable)해야하기 때문이라고 한다(배열은 그 자체로는 복사 연산과 대입 연산이 둘 다 안 된다, 관련 내용 링크). 그래서 vector<char*> 클래스의 형태로 문자열을 vector 컨테이너를 이용해 저장해보려 했는데 이러한 점이 문제 발생 요인이 되었다(배열의 이름과 포인터는 정확하게 같은 개념은 아니기 때문, 배열의 이름이 포인터 역할을 할 수 있을 뿐이다).

 

3) STL vector 클래스의 push_back 메서드 작동 원리 관련

STL vector 클래스 내의 메서드 중 하나인 push_back 메서드는 컨테이너에 인수로 전달되는 데이터를 추가하는 메서드이다. 이 메서드는 전달된 인수의 복사본을 만들어 vector 클래스 내에 저장하는 방식으로 작동된다(이는 구조체나 클래스를 복사 생성자를 직접 정의하여 구현한 다음, 해당 구조체나 클래스 형의 데이터를 push_back 할 때마다 복사 생성자가 호출된다는 것을 확인해볼 수 있다).

 

4) 스택 (프레임)의 원리

스택에서 사용하던 메모리 공간에는 스택이 해제될 때 별도로 비워지지 않고 마지막으로 사용되었던 값이 그대로 저장된 채로 남아있으며, 해당 메모리 주소를 사용하기 위해 접근하기 위해서 사용되었던 로컬 변수의 이름(식별자)과의 연결만 해제된다(C++ 기초 플러스 내용을 참고함).

* 참고) 스택 프레임(stack frame): 함수 호출 시 생성되는 메모리 공간으로, 기본적으로 C++ IDE의 대표적인 visual studio는 스택 사이즈의 디폴트 값으로 1 MB를 제공한다.

 

 

즉 상기의 코드를 실행할 때 일어나는 일을 정리해보면(세 번째 문장은 현상을 제대로 이해한 것인지 확신이 안 섬)

 

  • 반복문 내의 char형 배열의 주소는 변하지 않고, 문자열만 새로 덮어씌워지는 형태.
  • 문자열 자체가 아닌, char 배열의 주소를 가리키는 포인터를 복사하여 vector 클래스에 저장(= 얕은 복사).
  • 마지막으로 입력받아 저장한 문자열은 반복문이 끝나도 사라지지 않아 주소로 간접 접근이 가능.

 

결국에는 문자열 자체를 복사하는 게 아니라 문자열을 가리키는 포인터만 복사하는 것이 문제였기 때문에 이 것을 해결해주면 되는 문제이다. 동적 할당과 관련된 복사 문제가 아니었기 때문에 간단하게 문자열을 어떠한 구조체로 감싸는(래핑, wrapping) 방법을 사용해봤는데 다행히 바로 해결이 되었다(클래스나 구조체의 디폴트 복사 생성자는 배열 형태의 멤버에 대한 멤버별 복사 과정에서 같은 크기의 배열을 만든 뒤, 요소별로 복사를 수행한다고 한다. 관련 내용 링크).

* 동적 할당을 통해 문자열을 저장할 공간을 계속 새로 만들어줘도 해결 가능. 단, 할당 해제에도 신경써야한다.

 

 

항상 느끼지만, 문자열에 대해서 좀 알 것 같다고 생각할 때마다 이런 일을 겪는데 참 어려운 것 같다...

 


 

* 해당 카테고리의 글은 Microsoft 공식 홈페이지의 기술 문서 페이지와 Stephan Prata의 저서 C++ 기초 플러스 (6판, 번역본) 서적 및 기타 구글링을 통한 정보 수집 등을 토대로 개인적으로 요약/정리해 본 글입니다. 만약 잘못 정리된 내용이 있다면, 댓글이나 이메일로 공유해주시면 수정하도록 하겠습니다.

댓글