클라이언트/ 서버/ 엔지니어 " 게임 개발자"를 향한 매일의 공부일지

루프와 관계 표현식 3 : while 루프, do while 루프, 2차원 배열 본문

프로그래밍 언어/C++

루프와 관계 표현식 3 : while 루프, do while 루프, 2차원 배열

huenuri 2024. 8. 25. 13:29

이제 새벽에 공부할 시간이 2시간 반 정도밖에 남지 않는 것 같다. 남은 시간 동안 5장만이라도 다 마칠 수 있었으면 좋겠다. 5장도 이제 절반 정도 남았다. 중요한 내용이니 하나라도 놓칠 수 없다.
그럼 바로 학습을 시작해보자!


 

 

2. While 루프

while 루프는 for 루프에서 초기화 부분과 갱신 부분을 없애고, 루프 몸체와 조건 검사 부분만 남겨 놓은 것이다.
프로그램은 먼저 괄호 안의 조건 검사 표현식을 평가한다. 표현식이 true로 평가되면 루프 몸체에 있는 구문을 실행한다. 루프 몸체의 실행이 끝나면 조건 검사 표현식으로 돌아와서 표현식을 다시 실행한다. 이와 같은 루프 주기는 조건 검사 표현식이 false가 될 때까지 반복된다.

루프를 끝내려면 조건 검사 표현식에 영향을 주는 것이 루프 몸체 안에 들어있어야 한다. for 루프와 마찬가지로, while 루프도 진입 조건 루프이다. 그러므로 조건 검사 표현식이 처음부터 false이면 루프 몸체는 한번도 실행되지 않는다.

 

 

아래 예제에서 while 루프는 문자열을 구성하는 각각의 문자와 그 문자에 해당하는 아스키 코드를 출력한다. 널 문자를 만나면 루프를 종료한다. 이렇게 널 문자에 도달할 때까지 문자열을 구성하고 있는 문자들을 순서대로 처리하는 것이 C++가 문자열을 처리하는 표준 방법이다.

 

 

예제 13번 : while.cpp

// while 루프 입문

#include <iostream>
const int ArSize = 20;
int main()
{
	using namespace std;
	char name[ArSize];

	cout << "영문 이름을 입력하십시오: ";
	cin >> name;
	cout << "귀하의 영문 이름을 한 줄에 한 자씩\n";
	cout << "ASCII 코드와 함께 표시하면 이렇습니다.\n";
	int i = 0;					// 문자열의 첫머리에서 시작
	while (name[i] != '\0')		// 문자열의 끝까지 처리
	{
		cout << name[i] << ": " << int(name[i]) << endl;
		i++;					// 이 단계를 절대 빠뜨리지 말 것
	}
	return 0;
}

 

while 조건 검사를 다음과 같이 변경해보자.

while (name[i])

 

이렇게 변경해도 프로그램이 바르게 동작한다. 그 이유는 name[i]가 일반적인 문자이고, 그 값이 0이 아닌 문자 코드(true)이기 때문이다. 그러나 name[i]가 널 문자가 되면 문자 코드의 값이 0이 되어 false가 된다.

C 스타일 문자열과는 달리, string 클래스 객체는 문자열의 끝을 인식하기 위해 널 문자를 사용하지 않는다. 따라서 char 형 배열을 string 객체로 대체함으로써 string 클래스 버전으로 간단하게 변환할 수 없다.


 

 

for와 while

C++에서 for 루프와 while 루프는 사실상 같은 것이다. for 루프와 while 루프는 거의 동등하기 때문에, 어떤 것을 선택하느냐는 사용자의 프로그래밍 스타일에 달려 있다.
일반적으로 for 루프는 초기값, 종료값, 카운터 갱신 방법을 한곳에 넣을 수 있기 때문에 루프를 카운트해야 할 때 주로 사용한다. while 루프는 얼마만큼 루프를 반복해야 할지 미리 알수 없을 때 사용한다.

 

루프를 설계할 때는 다음과 같은 지침을 고려

  • 루프 실행을 종료시키는 조건을 파악한다
  • 첫번째 조건 검사를 하기 전에 그 조건을 초기화한다
  • 조건 검사를 다시 하기 전에 매 루프 주기마다 그 조건을 갱신한다

 

잘못된 구두점

 

블록을 정의하는 것은 들여쓰기가 아니라 한 쌍의 중괄호라는 사실을 잊으면 안된다. 예를 들어, 다음과 같은 루프를 살펴보자.

	i = 0;
	while (name[i] != '\0')
		cout << name[i] << endl;
		i++;
	cout << "종료\n";

 

아무리 들여쓰기를 했어도 중괄호가 없으므로 컴파일러는 첫번째 cout 구문만을 루프 몸체로 인식한다. 결과적으로 배열의 첫번째 원소만을 무한정 출력하게 된다.

i = 0;
while (name[i] != '\0');
{
	cout << name[i] << endl;
	i++;
}
cout << "종료\n";

 

이번에는 한쌍의 중괄호를 제대로 넣었다. 그러나 필요 없는 세미콜론을 추가했다. 세미콜론은 하나의 구문을 완결짓는다. 여기서는 아무것도 없는 구문이 while 루프의 몸체가 된다.


 

 

 

잠시만 - 시간 지연 루프

시간을 지연시키는 동작을 프로그램 안에 넣어야 할 때가 간혹 있다. 이럴 때 메시지를 출력한 후 대략 5초 정도 기다렸다가 다음 동작으로 옮겨간다면 좋은 프로그램으로 평가받을 것이다.
while 루프는 간단하게 이런 효과를 만들어낸다.

 

예제 14번 : waiting.cpp

// clock()을 시간 지연 루프에 사용한다

#include <iostream>
#include <ctime>
int main()
{
	using namespace std;
	cout << "지연 시간을 초 단위로 입력하십시오: ";
	float secs;
	cin >> secs;
	clock_t delay = secs * CLOCKS_PER_SEC;
						// 지연 시간을 시스템 단위 클록 수로 변환
	cout << "카운트를 시작합니다.\a\n";
	clock_t start = clock();
	while (clock() - start < delay)		// 시간이 경과할 때까지 대기
		;								// 세미콜론에 유의
	cout << "종료\a\n";
	return 0;
}

 

5초를 입력하니 처음에 알림이 울리고 5초 후에 다시 한번 울리게 설정이 된다.


 

 

 

 

3. do while 루프

 

do while 루프는 for나 while 루프와 달리 탈출 조건 루프이다. 이 말은 루프 몸체를 먼저 실행하고, 조건을 나중에 검사한다는 뜻이다.
검사하는 조건이 false로 평가되면 루프가 종료된다. 검사하는 조건이 true로 평가되면 새로운 루프 주기와 조건 검사가 시작된다. 그러므로 do while 루프는 조건을 평가하기 전에 루프 몸체를 적어도 한번은 실행한다.

 

 

일반적으로 먼저 조건을 검사하는 루프가 나중에 조건을 검사하는 루프보다 더 나는 선택이다. 예를 들어, 예제 13번에서 while 루프 대신 do while 루프를 사용했다고 가정해보자. 그와 같은 경우에 루프는 조건 검사에서 문자열의 끝에 도달했다는 것을 판단하기 전에 널 문자와 그것의 아스키 코드를 출력할 것이다.
그러나 do while 루프가 더 적당한 경우도 있다. 예를 들어, 사용자로부터 입력을 받아야 할 때는 조건을 검사하기 전에 우선 입력부터 받아야 한다.

 

 

예제 15번 : dowhile.cpp

// 탈출 조건 루프

#include <iostream>
int main()
{
	using namespace std;
	int n;

	cout << "1부터 10까지의 수 중에서 ";
	cout << "내가 좋아하는 수를 한번 맞추어 보십시오.\n";
	do
	{
		cin >> n;
	} while (n != 7);
	cout << "맞았습니다. 내가 좋아하는 수는 럭키 세븐 7입니다.\n";
	return 0;
}

 

 


 

 

4. 루프와 텍스트 입력

이번에는 파일이나 키보드로부터 문자를 하나씩 읽어들이는 방법을 알아보자. 예를 들어, 입력받은 문자 수, 줄 수, 단어 수를 카운트하는 프로그램을 작성해야 한다고 가정해보자. 전통적으로 C나 C++에서는 이런 종류의 작업을 하는데 while 루프를 사용한다.
C++의 while 루프가 C의 while 루프와 같다고는 해도 C와 C++의 I/O 기능은 다르다. 실제로 cin 객체는 단일 문자 입력에서 세 가지 서로 다른 모드를 사용한다. 이들은 사용자 인터페이스가 제각각 다르다. 이제 이들과 함께 while 루프를 사용하는 방법을 살펴보자.

 

cin을 이용한 입력

키보드로부터 텍스트를 입력받기 위해 루프를 사용할 경우에, 프로그램은 입력이 언제 끝나는지 알아야한다. 이를 아는 한 가지 방법은 표지 문자라고 부르는 특수 문자를 입력 중지 신호로 사용하는 것이다.
다음 예제는 # 문자를 만나면 입력을 중지한다. 읽어들인 문자 수를 카운트하고, 읽어들인 문자를 화면으로 불러들인다. 즉, 읽어들인 문자를 다시 출력한다.

 

예제 16번 : textin1.cpp

// while 루프로 문자 읽기

#include <iostream>
int main()
{
	using namespace std;
	char ch;
	int count = 0;			// 카운트를 0으로 설정한다
	cout << "문자들을 입력하시오; 끝내려면 #을 입력하시오:\n";
	cin >> ch;				// 문자를 넘겨받는다
	while (ch != '#')		// 문자가 #인지 검사한다
	{
		cout << ch;			// 문자를 화면으로 불러들인다
		++count;			// 문자 카운터를 증가시킨다
		cin >> ch;			// 다음 문자를 넘겨받는다
	}
	cout << endl << count << " 문자를 읽었습니다.\n";
	return 0;
}

 

 

프로그램 분석

 

첫 번째 문자가 #일 경우에 프로그램은 전체 루프를 건너뛴다. 첫 번째 문자가 #이 아닐 때, 루프 안에 들어가서 그 문자를 에코하고, cout를 증가시킨다. 그런 다음 문자를 입력 받는다. 루프의 마지막 단계에서 새로운 문자를 입력받는 것이 매우 중요하다. 이 부분이 없다면 첫 번째 문자만 반복해서 처리하게 된다.
그런데 왜 문자를 에코할 때 빈칸 문자들은 화면에 나타나지 않았을까? cin 때문이다. char형 값을 입력받을 때 cin은 빈칸 문자, 개행 문자들을 무시하고 건너뛴다. 그들은 읽은 문자 수에 포함되지 않는다.

이 예제에서 cin 입력은 버퍼를 이용한다. 즉, 사용자가 입력한 문자들은 Enter 키를 누르기 전까지는 프로그램에 전달되지 않는다. # 문자 뒤에 문자들을 더 입력할 수 있는 것은 바로 이 때문이다. Enter 키가 눌리면 입력받은 모든 문자들은 프로그램에 전달된다. 그러나 프로그램은 # 문자까지만 처리하고 끝낸다.


 

cin.get(char)를 이용한 입력

 

이 함수는 빈칸 문자까지도 입력받아 ch 변수에 저장한다.

 

예제 17번 : textin2.cpp

// cin.get(char) 사용하기

#include <iostream>
int main()
{
	using namespace std;
	char ch;
	int count = 0;

	cout << "문자들을 입력하시오; 끝내려면 #을 입력하시오:\n";
	cin.get(ch);			// cin.get(ch) 함수를 사용한다
	while (ch != '#')		
	{
		cout << ch;			
		++count;			
		cin.get(ch);		// cin.get(ch)를 다시 사용한다
	}
	cout << endl << count << " 문자를 읽었습니다.\n";
	return 0;
}

 

빈칸을 포함한 모든 문자들을 에코하고 카운트한다. cin.get(ch) 함수 호출은 ch 변수에 어떤 값을 넣는다, 이것은 그 변수의 값을 변경한다는 것을 뜻한다. C에서는 변수의 값을 변경하려면 그 변수의 주소를 함수에 전달해야 한다.
그런데 cin.get() 함수 호출의 매개변수로 &ch가 아닌 ch를 사용하고 있다. C++에서는 함수의 매개변수를 참조형으로 선언할 경우 이렇게 사용할 수 있다.

 

어느 cin.get()을 사용할까?

C에서는 char형 포인터와 int형을 두 개의 매개변수로 사용하는 어떤 함수가 있을 때, 다른 데이터형을 매개변수로 사용하는 동일한 이름의 함수를 만들 수 없다. 그러나 C++에서는 함수 오버로딩이라는 OOP 기능이 제공되기 때문에 이것이 가능하다.
함수 오버로딩은 매개변수 리스트를 다스게 하면 동일한이름의 함수를 사용할 수 있도록 허용하는 것이다.

 

파일 끝(End-of-File) 조건

C++ 입력 기능은 운영 체제의 도움을 받아 입력이 파일의 끝에 도달했을 때 이를 인식하고, 그 정보를 프로그램에 전달할 수 있다.

이 부분은 조금 난해해서 다음에 한번 살펴보며 학습해보려고 한다. 지금은 사용법만 익혀볼 것이다.

 

예제 18번 : textin3.cpp

// 파일 끝까지 문자 읽기

#include <iostream>
int main()
{
	using namespace std;
	char ch;
	int count = 0;
	cin.get(ch);					// 문자 하나를 읽는다
	while (cin.fail() == false)		// EOF인지 검사한다
	{
		cout << ch;					// 문자를 에코한다
		++count;
		cin.get(ch);				// 다음 문자를 읽는다
	}
	cout << count << " 문자를 읽었습니다.\n";
	return 0;
}

 

문장을 쓰면 그대로 읽어들일 수 있다. 여기서 끝내고 싶으면 ctrl + z와 Enter를 누르면 된다.

 

 

문자 입력에 자주 쓰는 구문

EOF가 나타날 때까지 텍스트를 한번에 한 문자씩 읽는 루프의 필수적인 설계는 다음과 같다.

cin.get(ch);						// 문자 하나를 읽는다
	while (cin.fail() == false)		// EOF인지 검사한다
	{
		···							// 일을 처리한다
		cin.get(ch);				// 다음 문자를 읽는다
	}

 

! 연산자를 사용해 다음과 같이 고칠 수 있다.

while (!cin.fail())			// 입력이 실패하지 않았으면 루프 실행

 

istream 클래스는 cin과 같은 istream 객체를 bool 값으로 변환하는 함수를 제공한다. while 루프의 조건 검사와 같이 bool 값이 필요한 곳에 cin이 나올 때 이 변환 함수가 호출된다. 따라서 이 조건 검사를 다음과 같이 고칠 수 있다.

while (cin)			// 입력이 성공했으면 루프 실행

 

cin.get(char)의 리턴값이 cin이므로 while 루프를 다음과 같이 줄일 수 있다.

while (cin.get(ch))				// 입력이 성공했으면 루프 실행
	{
		···						// 일을 처리한다
	}

 

cin.get()의 또 다른 버전

 

C의 문자 입출력 함수인 getchar()와 putchar()를 C++에서도 사용할 수 있다. C에서 사용하던 방식대로 헤더 파일을 포함시키면 된다. 매개변수가 없는 cin.get() 멤버 함수는 입력으로부터 그 다음 문자를 읽어 리턴한다. 다음과 같이 사용할 수 있다.

ch = cin.get();

 

cin.get()을 성공적으로 사용하려면, 이 함수가 EOF 조건을 어떻게 다루는지 알아야 한다. 이 함수가 EOF에 도달했을 때는 더 이상 리턴할 문자가 남아 있지 않다. 대신에 cin.get()은 기호 상수 EOF로 나타내는 특별한 값을 리턴한다.

다음 예제는 cin.get()을 사용하여 새로운 버전으로 만드는 것이다. while 루프의 조건 검사와 문자 입력을 하나로 결합함으로써 코드가 보다 간결해졌다.

 

예제 19번 : textin4.cpp

// cin.get()으로 문자 읽기

#include <iostream>
int main(void)
{
	using namespace std;
	int ch;					// char형이 아니라 int형이어야 한다
	int count = 0;

	while ((ch = cin.get()) != EOF)		// 파일 끝 검사
	{
		cout.put(char(ch));
		++count;
	}
	cout << count << " 문자를 읽었습니다.\n";
	return 0;
}

while ((ch = cin.get()) != EOF)	

 

이 경우에 != 연산자가 = 연산자보다 우선순위가 높기 때문에, 프로그램은 cin.get()의 리턴값을 EOF와 먼저 비교한다. 비교의 결과는 false 또는 true가 되는데, 이 bool값은 0 또는 1로 변환되어 ch에 대입된다.
cin.get(char) 함수는 EOF를 발견하더라도 ch에 특별한 값을 대입하지 않는다는 것을 기억하라.


 

 

5. 중첩 루프와 2차원 배열

for 루프 안에 또 다른 for 루파가 들어 있는 중첩 루프를 사용하여 2차원 배열을 처리하는 방법에 대해 알아볼 것이다.
2차원 배열이란 무엇일까? 2차원 배열은 행과 열을 모두 가지고 있는 표와 비슷하다. C++는 2차원 배열을 나타래는 데이터형을 따라 제공하지 않는다. 대신에 배열의 배열, 즉 다른 배열을 원소로 사용하는 배열을 만들 수 있다.

예를 들어, 5개 도시의 지난 4년 동안의 연중 최고 온도를 저장하는 2차원 배열을 만들고 싶다면 다음과 같이 선언한다.

int maxtemps[4][5];

 

이 선언 구문은 maxtemps가 4개의 원소를 갖는 배열이고, 그 배열의 각 원소는 5개의 정수를 저장할 수 있는 또 다른 배열이라는 뜻이다.

 

maxtemps[0]은 maxtemps 배열의 첫 번째 원소로서 그 자체가 5개의 int값을 갖는 1차원 배열이다. maxtemps[0]의 첫 번째 원소는 maxtemps[0][0]으로서 하나의 int형 변수이다. 첫 번째 인덱스는 행을 나타내고 두 번째 인덱스를 열을 나타낸다.
이 배열에 들어있는 모든 내용을 출력하고 싶다면 다음과 같이 만들 수 있다.

	for (int row = 0; row < 4; row++)
	{
		for (int col = 0; col < 5; col++)
			cout << maxtemps[row][col] << "\t";
		cout << endl;
	}

 

2차원 배열의 초기화

 

2차원 배열을 만들 때 모든 원소들을 초기화할 수 있다. 방법은 1차원 배열의 초기화와 유사하다. 1차원 배열은 중괄호 안에 각 원소의 값을 나열하고 이들을 콤마로 분리함으로써 초기화한다는 것을 기억하자.

int btus[5] = {23,26,24,31,28};

 

2차원 배열의 각 원소는 자체가 1차원 배열이다. 따라서 2차원 배열의 초기화는 1차원 배열의 초기화 값들이 나열된 중괄호를 나열하고, 이들을 다시 콤마로 분리하는 것이다.

int maxtemps[4][5] =				// 2차원 배열
	{
		{35,32,33,36,35},			// maxtemps[0]의 값들
		{33,32,34,35,31},			// maxtemps[1]의 값들
		{33,34,32,35,34},			// maxtemps[2]의 값들
		{36,35,34,37,35}			// maxtemps[3]의 값들
	};

 

 

2차원 배열 사용하기

예제 20번 : nested.cpp

// 중첩 루프와 2차원 배열

#include <iostream>
const int Cities = 5;
const int Years = 4;
int main()
{
	using namespace std;
	const char* cites[Cities] =		// 5개의 문자열을 지시하는
	{								// 포인터의 배열
		"Seoul", "Jeju", "Busan", "gangneung", "Daugu"
	};

	int maxtemps[Years][Cities] =	// 2차원 배열
	{
		{35,32,33,36,35},			// maxtemps[0]의 값들
		{33,32,34,35,31},			// maxtemps[1]의 값들
		{33,34,32,35,34},			// maxtemps[2]의 값들
		{36,35,34,37,35}			// maxtemps[3]의 값들
	};

	cout << "2019년부터 2012년까지의 연중 최고 온도\n\n";
	for (int city = 0; city < Cities; ++city)
	{
		cout << cites[city] << ":\t";
		for (int year = 0; year < Years; ++year)
			cout << maxtemps[year][city] << "\t";
		cout << endl;
	}
	return 0;
}

 

이상하게도 한 자리가 한칸씩 밀려간다. 여러번 시도했는데 되지 않아서 다음에 더 연구해보려고 한다. 탭을 한칸 더 띄어주었는데도 이런 증상이 억어지지 않는다.

포인터 배열 대신에 string 클래스 객체의 배열을 문자열 데이터를 나타내는데 사용할 수 있다.

const string cities[Cities] =		// string의 5개 배열
{
	"Seoul",
	"Jeju",
	"Busan",
	"gangneung",
	"Daugu"
};

 

 

5단원 학습을 마치고

이렇게 5단원의 개념 학습을 모두 마쳤다. 어젯밤부터 날을 새고 공부를 했는데 하나도 졸리지가 않았다. 오늘부터 인공지능 사관학교 수업을 들으려면 정신 바짝 차리며 집중해야 할 것 같다.
피곤하면 점심시간에 낮잠이라도 자야겠다. 이제 for문, while문도 어느 정도 정리가 되었다.

바로 이어서 단원 연습문제를 풀어야지! 여기까지가 오늘의 새벽 공부 분량이 될 것 같다.