프로그램 및 데이터 구조화 (학생 점수 구하기) 1
라이브러리의 기능들은 다음과 같은 몇가지 공통적인 특징이 있습니다.
- 특정 문제를 해결
- 다른 기능들과 연계하지 않고 독립적
- 기능의 이름이 있음
연산 구조화
// 중간시험 점수, 기말시험 점수, 종합 과제 점수에서 학생의 종합 점수를 구함
double grade(double midterm, double final, double homework)
{
return 0.2 * midterm + 0.4 * final + 0.4 * homework;
}
함수 대부분은 이와 비슷하게 정의합니다.
반환 타입, 함수 이름, 소괄호로 묶인 매개변수 목록 (parameter list)
다른 함수를 반환하는 함수를 정의하는 방법은 좀더 복잡합니다.
median 함수
// vector<double>의 중앙값을 구함.
// 함수를 호출하면 인수로 제공된 벡터를 통째로 복사
double median(vector<double> vec)
{
typedef vector<double>::size_type vec_sz;
vec_sz size = vec.size();
if (size == 0)
throw domain_error("median of an empty vector");
sort(vec.begin(), vec.end());
vec_sz mid = size / 2;
return size % 2 == 0 ? (vec[mid] + vec[mid - 1]) / 2 : vec[mid];
}
median 함수는 과제 점수의 중앙값만이 아니라 모든 벡터의 중앙값을 구할 수 있습니다.
또한 계산이 끝나자마자 중앙값을 return 할수 있습니다.
size 와 mid 변수는 median 함수를 호출하면 만들어지고 반환할 때 소멸됩니다.
vec_sz 는 다른 목적으로 해당이름을 사용할 때 충돌을 피하려고 지역 타입으로 정의합니다.
이 함수에서 눈에 띄는 부분은 벡터가 비었을 때의 동작입니다.
이럴 때는 프로그램을 실행하는 사람에게 문제가 있음을 알려야합니다.
벡터가 비었을 때 예외(exception)를 던집(throw)니다.
프로그램이 예외를 던지면 throw 가 있는 부분에서 실행을 멈춘 후, 호출자에게 전달한 정보가 있는 예외 객체(exception object)
와 함께 프로그램의 또 다른 부분으로 이동합니다.
domain_error 는 표준 라이브러리 <stdexcept> 헤더에 정의한 형식입니다.
함수의 인수가 함수가 받을 수 있는 값의 범위를 벗어났음을 보고할 때 사용합니다.
domain_error는 객체를 생성할 때 무엇이 잘못되었는지 설명하는 문자열을 넣을 수 있습니다.
median 함수를 호출할 때 인수로 사용한 벡터를 vec 로 복사합니다.
median 함수라면 상당한 시간이 걸리더라도 인수를 매개변수에 복사하는 것이 유용합니다.
sort 함수를 호출하여 매개변수의 값을 바꾸기 때문입니다.
벡터의 중앙값을 구하는 것이 벡터 자체를 바꾸면 안되기 때문입니다.
성적 산출 방식 다시 구현하기
종합 과제 점수를 구하는 것은 성적을 산출하는 과정의 일부입니다.
함수 오버로딩을 사용해서 벡터로 학생의 종합 점수를 구합니다.
// 중간시험 점수, 기말시험 점수, 과제 점수의 벡터로 학생의 종합 점수를 구함.
// 이 함수는 인수를 복사하지 않고 median 함수가 해당 작업을 실행.
double grade(double midterm, double final, const vector<double>& hw)
{
if (hw.size() == 0)
throw domain_error("student has done no homework");
return grade(midterm, final, median(hw));
}
grade 함수에는 세 가지 흥미로운 점이 있습니다. 첫 번째는 세 번째 인수의 타입으로 지정한
const vector<double>&입니다. 이 type 은 const double 벡터를 참조한다 라고도 합니다.
vector<double> homework;
vector<double>& hw = homework; // hw는 homework의 동의어
// chw는 조회만 가능한 homework의 동의어
const vector<double>& chw = homework;
chw 는 homework의 또 다른 이름이지만 const를 사용했으므로 chw로 값을 바꾸는 어떠한 작업도 실행
하지 않겠다는 약속입니다.
참조는 원본의 또 다른 이름입니다. 참조의 참조라는 것은 무의미합니다. 참조의 참조를 정의 하는것은
원본 객체의 참조를 정의하는 것과 같습니다.
예를 들어
//hw1 과 chw1은 homework의 동의어
//chw1 은 조회만 가능
vector<double>& hw1 = hw;
const vector<double>& chw1 = chw;
hw1은 hw와 마찬가지로 homework의 또 다른 이름이고 chw1은 chw처럼 조회만 할 수 있는 homework의 또 다른 이름입니다.
const가 아닌 참조(쓰기를 허용하는 참조)를 정의할 때는 const 객체나 const 참조를 참조할 수 없습니다.
const가 허용하지 않는 권한을 요청했기 때문..!
즉 chw를 수정하지 않기로 약속 했기 떄문에 다음과 같은 코드는 작성할 수 없습니다.
vector<double>& hw2 = chw; // 오류: chw에 쓰기 권한을 요청
read_hw 함수
벡터에 과제 점수를 넣는 부분을 함수로 만든다고 생각해봅시다.
이 함수의 동작을 설계하려면 한 번에 값 2개를 반환해야 합니다. 2개의 값은 입력된 과제 점수에 해당하는 값과
입력이 제대로 일어났는지 나타내는 값입니다.
함수가 하나이상의 값을 직접 반환하는 방법은 없습니다.
간접적인 방법은 반환하려는 값 2개중 하나를 담은 객체의 참조를 함수의 매개변수로 지정하는것
// 입력 스트림에서 과제 점수를 읽어서 vector<double>에 넣음
istream& read_hw(istream& in, vector<double>& hw)
{
//앞으로 완성시켜야 할 부분
return in;
}
여기에서는 const를 떼겠습니다. const가 없는 참조 매개변수는 보통 함수의 인수로 사용하는 객체를
수정하겠다는 의미입니다.
vector<double> homework;
read_hw(cin, homework);
함수에서 인수를 바꿀 것이 예상되므로 표현식 형태의 인수로 함수를 호출할 수 는 없습니다.
대신 lvalue 인수를 참조 매개변수로 전달합니다. lvalue라는 것은 비일시적(nontemporary)객체를 나타내는 값입니다.
sum / count처럼 산술 값을 만드는 표현식은 lvalue가 아닙니다.
read_hw 함수는 참조 타입인 in을 return합니다. 결론적으로 객체를 참조로 받은 후 복사 하지않은 채로 return합니다.
입력 스트림을 return 하므로 다음처럼 코드를 작성할 수 있습니다.
if (read_hw(cin, homework)) { /* 생략 */ }
// 위와 동일한 의미
read_hw(cin, homework);
if (cin) { /* 생략 */ }
일단 다음 형태로 코드를 작성합니다.
// 아직 완성된 코드가 아님.
double x;
while (in >> x)
hw.push_back(x);
이 코드는 제대로 동작하지 않습니다.
첫 번째로는 hw를 정의하지 않았다는 것과 hw 안에 이미 어떤 데이터가 있을지도 모릅니다.
많은 학생의 과제 점수를 처리하려고 함수를 사용하므로 이전 학생의 점수가 hw에 남아 있을수 있습니다.
입력받은 작업을 시작하기 전 hw.clear()를 호출하여 이러한 문제를 해결합니다.
두번째 이유는 반복문을 언제 멈출지 알수 없다는 점입니다.
점수를 더 이상 읽을 수 없을 때 까지 계속 반복 실생할 수 있지만 두가지 문제점이 있습니다.
EOF에 도달했을 수도 있따는 것과 입력 스트름에 점수가 아닌 다른 무언가가 있을수도 있다는 점
EOF에 도달했다고 여길 때는 EOF 표시가 모든 데이터를 성공적으로 읽은 뒤에야 나타나므로 오류가 있을 수 있습니다.
보통 EOF 표시는 입력 시도가 실패했음을 의미합니다.
점수가 아닌 무언가가 입력 됬을땐 라이브러리에서 입력 스트림을 실패상태로 표시하게끔 합니다.
실패 상태라는 것은 EOF에 도달한 것과 같은 의미로, 앞으로의 입력 요청이 실패할 것입니다.
그러므로 마지막으로 읽은 과제 점수에 뒤따르는 것이 과제 점수가 아닐 때 호출자는 입력 데이터에 문제가 있다고 여깁니다.
// 입력 스트림에서 과제 점수를 읽어서 vector<double>에 넣음
istream& read_hw(istream& in, vector<double>& hw)
{
if (in)
{
// 이전 내용을 제거.
hw.clear();
// 과제 점수를 읽음.
double x;
while (in >> x)
hw.push_back(x);
// 다음 학생의 점수 입력 작업을 고려해 스트림을 지움.
in.clear();
}
return in;
}
멤버 함수 clear가 istream 객체와 벡터 객체 사이에서 완전히 다른 동작을 실행하는 것에 주의하세요.
istream 객체일 때는 계쏙 입력을 시도할 수 있도록 오류 표시를 재설정하고, 벡터 객체일 때는 벡터에 있던 내용을
버리고 다시 빈 벡터로 만듭니다.
함수의 매개변수
지금까지 벡터 homework를 다루는 3개 함수 (median, grade, read_hw)를 정의 했습니다.
함수 각각은 목적에 맞게 서로 다른 방식으로 매개변수를 처리합니다.
매개변수가 벡터나 문자열처럼 복사하는 데 시간이 많이 필요하거나 매개변수의 값을 바꾸지 않으려 할 때
const vector<double>& 타입을 사용하는 것이 적합합니다 프로그램을 더 효율적으로 만드는 중요한 기술입니다.
일반적으로 int 나 double 같은 간단한 기본 타입 매개변수에 const 참조를 사용하는 것은 효용 가치가 낮습니다.
기본 타입 매개변수 같은 작은 객체는 값을 복사해서 전달할 때 오버헤드가 거의 없고 전달 시간도 오래 걸리지 않기 때문입니다.
const가 아닌 참조 매개변수에 해당하는 인수는 반드시 lvalue, 즉, 비일시적 객체여야 합니다.
반면 값으로 전달되거나 const 참조 타입인 인수는 어떠한 값도 될수 있습니다.
예를 들어 빈 벡터를 반환하는 함수가 있다고 가정해봅시다.
vector<double> emptyvec()
{
vector<double> v; // 요소가 있지 않음
return v;
}
이 함수를 호출하고 그 결과를 함수 grade의 인수로 사용해봅시다.
grade(midterm, final, emptyvec());
실행하면 함수 grade는 인수가 비었으므로 즉시 예외를 던집니다.
어쨋든 이러한 방식으로 함수 grade 호출하기를 문법적으로 허용합니다.
함수 read_hw를 호출할 때는 두 매개변수가 const가 아닌 참조 타입이므로 두 인수 모두 lvalue 여야합니다.
read_hw에 lvalue가 아닌 벡터를 인수로 전달할때
read_hw(cin, emptyvec()); // 오류: emptyvec()는 lvaue가 아님.
함수 emptyvec을 호출하여 만드는 벡터는 함수read_hw의 실행이 끝나면 사라질 것이므로 컴파일러는 오류를 알립니다.
즉, 접근할 수 없는 객체에 입력을 저장하는 것과 같습니다.
함수를 사용하여 학생 성적 구하기
현재 까지의 코드를 종합 해보겠습니다.
#include <iostream>
#include <vector>
#include <iomanip>
#include <algorithm>
#include <string>
using namespace std;
// 중간시험 점수, 기말시험 점수, 종합 과제 점수에서 학생의 종합 점수를 구함
double grade(double midterm, double final, double homework)
{
return 0.2 * midterm + 0.4 * final + 0.4 * homework;
}
// vector<double>의 중앙값을 구함.
// 함수를 호출하면 인수로 제공된 벡터를 통째로 복사
double median(vector<double> vec)
{
typedef vector<double>::size_type vec_sz;
vec_sz size = vec.size();
if (size == 0)
throw domain_error("median of an empty vector");
sort(vec.begin(), vec.end());
vec_sz mid = size / 2;
return size % 2 == 0 ? (vec[mid] + vec[mid - 1]) / 2 : vec[mid];
}
// 중간시험 점수, 기말시험 점수, 과제 점수의 벡터로 학생의 종합 점수를 구함.
// 이 함수는 인수를 복사하지 않고 median 함수가 해당 작업을 실행.
double grade(double midterm, double final, const vector<double>& hw)
{
if (hw.size() == 0)
throw domain_error("student has done no homework");
return grade(midterm, final, median(hw));
}
// 입력 스트림에서 과제 점수를 읽어서 vector<double>에 넣음
istream& read_hw(istream& in, vector<double>& hw)
{
if (in)
{
// 이전 내용을 제거.
hw.clear();
// 과제 점수를 읽음.
double x;
while (in >> x)
hw.push_back(x);
// 다음 학생의 점수 입력 작업을 고려해 스트림을 지움.
in.clear();
}
return in;
}
int main()
{
// 학생의 이름을 묻고 읽음.
cout << "Please enter your first name: ";
string name;
cin >> name;
cout << "Hello, " << name << "!" << endl;
// 중간시험과 기말시험의 점수를 묻고 읽음.
cout << "Please enter your midterm and final exam grades: ";
double midterm, final;
cin >> midterm >> final;
// 과제 점수를 물음.
cout << "Enter all your homework grades, "
"followed by end-of-file: ";
vector <double> homework;
// 과제 점수를 읽음.
read_hw(cin, homework);
// 종합 점수를 계산해 생성.
try
{
double final_grade = grade(midterm, final, homework);
streamsize prec = cout.precision();
cout << "Your final grade is " << setprecision(3)
<< final_grade << setprecision(prec) << endl;
}
catch (domain_error)
{
cout << endl << "You must enter your grades. "
"Please try again." << endl;
return 1;
}
return 0;
}
학생의 과제 점수를 묻고 난 후 read_hw를 호출하여 데이터를 읽습니다.
함수 read_hw 내부의 while문은 EOF를 만나거나 double이 아닌 유효하지 않은 데이터 값이 발생할 때까지 과제 점수를 반복해서 읽습니다.
이 프로그램에서 가장 중요하고도 새로운 개념은 try문입니다. 중괄호 속 실행문을 실행합니다.
그러다가 domain_error 예외가 발생하면 실행문의 실행을 멈추고 catch 다음에 오는 중괄호 안 실행문을 실행합니다.
catch문은 발견된 예외 타입을 알려줍니다.
프로그램을 실행하려면 main 함수 이전에 적절한 순서대로 함수들을 정의했는지 확인해야 합니다.
실제로 함수 정의 확인이 끝나면 사용하기 불편할 정도로 크기가 큰 프로그램이 된 것을 알 수도 있습니다.
따라서 프로그램을 더 간결하게 파일로 분할하는 방법을 살펴보고 그 전에 좋은 해결책인 데이터 구조화를 알아봅시다.
데이터 구조화
지금까지 작성한 프로그램은 학생 1명의 성적을 구하는 데 유용합니다. 하지만 해당 연산은 휴대용 계싼기로 대신할 수 있을만큼
간단합니다. 하지만 어떤 과목을 가르치는 강사라면 수강하는 모든 학생의 성적을 구해야합니다.
강사에게 유용하도록 프로그램을 수정해보겠습니다.
학생 점수를 대화 형태로 입력받는 대신 학생의 이름과 점수가 포함된 파일을 제공한다고 가정하겠습니다.
각 이름 뒤에는 중간시험 점수, 기말시험 점수, 하나이상의 과제 점수가 차례대로 나열됩니다.
프로그램은 각 학생의 종합 점수를 구성할 때 중간시험 20% 기말시험 40% 과제 점수의 중앙값 40% 라는 가중치를 반영합니다. 입력에 따른 출력 형태는 다음과 같습니다.
학생 이름은 알파벳 순으로 나열하고 최종 점수를 읽기 쉽게 점수를 수직으로 정렬 하고자 합니다.
이러한 조건을 만족하려면 알파벳 순으로 모든 학생의 기록을 저장할 공간이 필요합니다.
또한 각 이름과 해당 점수 사이에 들어갈 공백의 개수를 결정해야 하므로 가장 긴 학생 이름을 알아야 합니다.
학생 1명의 데이터를 저장할 공간이 있다면 벡터를 사용하여 모든 학생의 데이터를 저장할 수 있습니다.
모든 학생의 데이터가 벡터에 저장되면 이를 정렬한 후 각 학생의 최종 점수를 계산하여 출력할 수 있습니다.
지금부터 학생의 데이터를 저장하는 데이터 구조를 만들고 저장한 데이터를 읽고 처리하는 몇가지 보조 함수를 만들 것입니다. 이 함수들을 사용하여 전체 문제를 해결해봅시다.
학생 데이터 통합하기
각 학생의 데이터를 읽은 다음 학생 이름을 알파벳 순으로 나열하려면 이름과 점수를 한군데 모아야합니다. 따라서 학생 1명과
관련된 모든 정보를 하나의 공간에 저장할 수 있는 방법이 필요합니다.
즉, 학생 이름, 중간시험 점수, 기말시험 점수, 모든 과제 점수를 담는 하나의 공간이 데이터 구조가 되어야합니다.
C++ 에서는 데이터 구조를 이렇게 정의합니다.
struct Student_info
{
string name;
double midterm, final;
vector<double> homework;
}; // 세미콜론을 잊지 않도록 주의
구조체(struct) 정의에서 Student_info는 4개의 데이터 멤버가 있는 type입니다.
Student_info가 type 이므로 4개의 데이터 멤버가 있는 Studen_info 타입의 객체를 각각 정의할 수 있습니다.
첫 번째 멤버는 문자열 타입의 name, 두번째와 세 번째 멤버는 double 타입의 midterm과 fianl
네번째 멤버는 double 타입의 요소가 있는 벡터 homework입니다.
Student_info는 type이므로 여러개의 과제 점수를 담는데 vector<double>의 객체를 사용한 것처럼 여러학생의 정보를
담는 데 vector<Student_info>의 객체를 사용할 수 있습니다.
학생 점수 관리
학생들의 점수를 다루는 개념을 처리하기 쉽게 나누면 세단계의 개별 함수로 표현 할수 있습니다.
Student_info 객체에 데이터를 입력하고, Stundent_info 객체의 종합 점수를 생성하고 , 객체들을 요소로 갖는 벡터를 정렬해야 합니다.
위에서 사용한 read_hw 함수와 비슷한 점수를 읽는 read_hw 함수를 사용합니다.
istream& read(istream& is, Student_info& s)
{
// 학생의 이름, 중간시험 점수, 기말시험 점수를 읽어 저장
is >> s.name >> s.midterm >> s.final;
read_hw(is, s.homework);
return is;
}
이 함수를 read라고 이름 짓는 것이 모호하다고 생각 할수 있지만 두 번째 매개변수의 타입으로 학생 정보를 읽는 함수라는걸
확실하게 알수 있습니다.
참고로 다른 종류의 구조체를 읽는 함수를 read라고 명명해도 오버로딩할 때 서로 구별할 수 있습니다.
이 함수는 Student_info 객체의 최종 점수를 구합니다.
grade 함수를 오버로딩하여 Student_info 객체의 종합 점수를 구하는 버전의 grade 함수를 구현합니다.
double grade(const Student_info& s)
{
return grade(s.midterm, s.final, s.homework);
}
이 함수는 Student_info 타입의 객체를 사용하여 double 타입의 종합 점수를 구해 반환합니다.
매개변수의 타입이 const Student_info& 이므로 함수를 호출할 때 Student_info 객체를 통째로 복사하면서 발생하는 오버헤드를 피합니다.
또한 내부에서 호출하는 grade 함수가 일으키는 예외에 대응하지 않는 것도 유의해야 합니다.
왜냐하면 내부의 grade 함수는 이미 실행이 끝난 이후라면 예외를 다루기 위해 함수가 할수 있는 것이 아무 것도 없기 때문 입니다.
마지막으로 살펴볼 내용은 Student_info 객체의 벡터를 어떻게 정렬할지 정하는 것입니다.
median 함수에서는 다음처럼 라이브러리의 sort 함수를 사용하여 vector<double>타입의 매개변수 vec를 정렬했습니다.
sort(vec.begin(), vec.end());
그렇다면 정렬하려는 데이터가 Stundent_info 객체들을 요소로 갖는 students라는 벡터에 있 을때
sort(students.begin(), students.end()); // 틀림!!
sort 함수의 원리를 잠깐 생각해보도록 합시다. sort는 함수가 벡터의 요소들을
순서대로 비교할때 < 연산자를 사용합니다. vector<double> 타입인 벡터에 sort 함수를 호출하면
< 연산자는 2개의 double 타입 값을 비교하여 알맞은 결과를 return 합니다.
그렇다면 sort 함수가 Student_info 타입의 값들을 비교할 때는 어떤 일이 일어날까요?
< 연산자는 Student_info 객체에 적용할 수 없습니다.
실제로 sort 함수가 2개의 객체를 비교하려 한다면 컴파일러는 문제가 있음을 알립니다.
다행히 sort 함수는 세 번째의 선택 인수로 서술함수가 있습니다.
서술함수는 bool 타입의 진릿값을 반환하는 함수입니다.
sort 함수가 세 번째 인수가 있다면 < 연산자를 사용하는 대신 세 번째 인수인 함수를 사용하여 요소들을 비교합니다.
따라서 Student_info 타입인 인수 2개가 있는 함수를 정의하여 첫 번째 인수가 두 번째 인수보다 작은지 판별해야 합니다.
학생 이름을 알파벳순으로 정렬하려면 다음처럼 학생 이름만 비교하는 함수를 작성해야 합니다.
bool compare(const Student_info& x, const Student_info& y)
{
return x.name < y.name;
}
앞 함수는 문자열을 비교하려고 < 연산자를 제공하는 문자열 클래스에 Student_info를 비교하는 작업을 맡깁니다.
여기서 < 연산자는 일반적인 사전 순서대로 문자열을 비교합니다.
즉, 왼쪽 피연산자가 오른쪽 피연산자보다 알파벳순으로 작으면 오른쪽 피연산자보다 작다고 간주합니다.
이제 sort 함수의 세 번째 인수에 compare 함수를 넣으면 벡터를 정렬할 수 있습니다.
sort(students.begin(), students.end(), compare);
sort 함수는 comapre 함수를 호출하여 요소들을 비교합니다.
내용이 길어 2편에서 마저 쓰도록 하겠습니다.