Giter Club home page Giter Club logo

2018-d.com-cpp-study's People

Contributors

justkode avatar

Watchers

 avatar  avatar  avatar  avatar

2018-d.com-cpp-study's Issues

한 프로젝트에 여러개의 소스파일을 집어넣었더니 main.obj 오류가 뜬다구요..?

여러분들이 프로젝트의 소스 파일 폴더에 두개의 cpp 파일을 넣어서 돌리려 할 때 높은 확률로 오류가 뜰것입니다!

1
2


3
main.cpp


4
main2.cpp

컴파일 : 우리가 쓴 코드를 기계가 이해하기 쉬운 기계어로 바꾸는 과정

여러분 Python 공부할때는 한 프로젝트에 여러개의 파일을 넣고도 파일 별로 돌리면 잘 돌아갔는데 말이죠... C++은 왜 그런게 되지 않는지 많이 궁금해 하실 것입니다! 그 이유는 Python은 인터프리터가 py파일 하나를 실행하는 형식으로 돌아가지만, C++은 컴파일러가 프로젝트 단위로 컴파일 을 하기 때문입니다. main함수는 그렇기 때문에 프로젝트에 단 하나만 있어야 하기 때문에 저런 오류를 띄우는 겁니다. py파일들은 project내에서 import 키워드를 통해 상호간 연결이 됩니다! 미리 컴파일 하지도 않아요! 그때 그때 상황에 따라서 컴파일 하면 되니까요! 하지만 C++에서의 cpp파일들은 프로그램의 시작 전에 프로젝트 내에서 미리 컴파일이 되기 때문에 제일 먼저 실행 되는 메인 함수는 오로지 단 하나만 존재해야 한다는 것이죠!

뭐, 각설이 너무 길었습니다. 그러므로 어떻게 해야 한다고 물으신다면...

5

새 프로젝트를 대충 만들어 주고...

6

그 프로젝트에 파일을 넣어 준 다음 시작 프로젝트로 설정해 주면 됩니다! 그리고 컴파일 하세요! (Ctrl + F5)

6차시 과제 피드백 (김범구, 김서영, 김수민, 김승찬, 김진호, 심규진, 윤창신)

6.1

아래는 모범 답안입니다.

#include <iostream>
#include <string>
#include <fstream>
using namespace std;

int main() {
	string file_name;
	string line;
	ofstream output;
	cout << "문서 이름을 입력 하시오 : ";
	getline(cin, file_name);
	cout << "문서 작성을 시작하겠소. 종료시 'close()'를 입력 해 주시오 : " << endl;
	output.open(file_name.c_str());
	while (true) {
		getline(cin, line);
		if (line == "close()") {
			break;
		}
		else {
			output << line << endl;
		}
	}
	output.close();
}

코드 정말 잘 짰어요. 딱히 흠 잡을 것이 없는 것 같아요.
한 가지, 이건 취향 문제라고도 볼 수 있지만.. 이렇게 간단한 조건을 갖고 있는 반복문인 경우, 무한 루프에서 탈출 조건(break)을 명시해주는 것보다는 반복문의 조건식에 반복 조건을 적어주는 것이 더 보기 좋아요. 가령 다음과 같이요.

for (getline(cin, line); line != "close()"; getline(cin, line)) {
	output << line << endl;
}

저한테 과제를 낸 친구들에게는 답장으로 c언어 스타일의 string에 대해서 잠시 언급했는데요.
지금 사용하는 string같은 경우에는 c++로 올라오며 잘 정의된 객체이기 때문에 상관 없는 문제이지만, c언어에는 객체의 개념이 없기 때문에 string을 표현하기 위해 char의 배열로 사용했어요. 그래서 위 코드처럼 ==, != 등으로 문자열을 비교하려고 한다면 값이 비교되는 것이 아니라 각 문자열 배열(char[])의 첫번째 글자(=원소)의 주솟값이 비교되어서 제대로 된 비교가 되지 않아요. 그렇기 때문에 c언어에서는 문자열 관련 함수가 따로 정의되어 있습니다. 문자열을 복사하는 strcpy, 비교하는 strcmp, 길이를 구하는 strlen과 같이요.
객체지향 프로그래밍을 공부하시다가 가끔 c언어 스타일의 문자열을 다룰 때가 있을텐데, 이럴 때 당황하지 않고 사용하길 바랍니다. (이것도 c++로 올라오며 개선된 것 중 하나겠죠?)

위와 같은 문제점 때문에 아래 코드를 실행하면 재미있는 결과가 있을거에요.

char* str1 = "abcd";
char str2[] = "abcd";

cout << (str1 == "abcd") << endl; // true
cout << (str2 == "abcd") << endl; // false
cout << (str1 == str2) << endl; // false

이유까지 쓰기에는 너무 길어질 것 같으니 궁금한 사람은 따로 물어보는 걸로 할게요.

6.2

아래는 모범 답안입니다.

#include <iostream>
#include <string>
#include <fstream>
using namespace std;

float cal(float x, string y, float z) {
	if (y == "+") {
		return x + z;
	}
	else if (y == "-") {
		return x - z;
	}
	else if (y == "*") {
		return x * z;
	}
	else if (y == "/") {
		return x / z;
	}
	else if (y == "%") {
		return int(x) % int(z);
	}
}

int main() {
	float a, c;
	string b, d;
	ifstream input;
	ofstream output;
	input.open("quiz.txt");
	output.open("answer.txt");
	if (input.is_open()) {
		while (input >> a >> b >> c >> d) {
			output << a << " " << b << " " << c << " " << d << " ";
			output << cal(a, b, c) << endl;
		}
	}
	input.close();
	output.close();
}

이것도 역시 흠 잡을 곳 없는 코드입니다.
문제가 있는 코드였습니다 ㅠㅠ 위 코드는 수정된 코드입니다. 아래 댓글 확인해주세요.

stream을 사용하시면서 close를 안 해주는 친구들이 간혹 있는 것 같아요. 동적 할당을 사용하면서 메모리 해제를 반드시 해주는 것과 같이, stream을 사용하면 close를 반드시 해주셔야 합니다.
지금 코드와 같이 파일 종료시까지 stream을 사용해야 한다면 사실 프로그램이 끝나고 객체가 소멸되면서 자동으로 close되긴 합니다. 하지만 stream의 사용이 프로그램 중간에 끝났다면 바로 close해주는 것이 메모리 낭비를 줄일 수 있겠죠?
java와 같은 언어에서는 close를 해주지 않으면(정확히는 flush를 해주지 않으면, close에서 flush를 호출함) 기껏 output stream으로 작성한 내용이 실제 파일에 쓰이질 않아요.
다른 언어를 사용할때도 , c++을 사용할때도 close를 해주는 습관을 가지길 바랍니다.

6주차도 수고 많았습니다.

1차시 과제 피드백 (김범구, 김서영, 김수민, 김승찬, 김진호, 심규진, 윤창신)

딱 한 명 제출했는데 너무 훌륭한 답변이 나와서 공유해볼까 합니다.
제출한 친구 너무 대단합니다. 수준 높은 답변이었어요.

입출력 스트림에 관해
C++의 모든 것은 객체로 표현된다. 그러므로 입출력의 수단은 함수가 아닌 객체이다. C++에서는 cout 객체로 출력 작업을, cin 객체로 입력을 대신한다. 또한 C++에서도 기존의 C언어처럼 함수를 이용해 입출력 작업을 수행할 수 있다.

cout 객체를 사용하는 문법은 std::cout << 출력할데이터; 이다.
"<<"는 삽입연산자이다. 이러한 명령어를 통해 출력 스트림에 “출력할데이터”를 전달한다.

cin 객체는 std::cin >> 저장할변수; 이다.
">>"은 추출 연산자이다. 사용자가 입력한 데이터를 입력 스트림에서 추출해 오른쪽에 위치한 변수에 저장한다.

C++ 표준 입출력 객체만의 차이점은 삽입연산자와 추출연산자가 데이터의 흐름을 나타내므로 좀 더 직관적이다. 또한 입출력 데이터의 타입을 자동으로 변환시켜주므로 더 편리하고 안전하다. 대신 기존의 함수에 비해 속도가 조금 느리다는 단점이 있다.

버퍼에 관해
버퍼는 임시 메모리 공간으로 입력과 출력을 효율적으로 처리하기 위한 공간이다. 버퍼를 사용하지 않는다면 키보드의 입력이 키를 누르는 즉시 바로 전달되지만 버퍼를 이용하면 입력한 문자들을 버퍼에 전송해놨다가 버퍼가 가득 차거나 개행 문자가 나타나면 버퍼의 내용을 한 번에 전송한다. 보통 C++에서는 사용자가 엔터를 누르면 입력 버퍼를 비우고, 출력시에는 개행 문자를 전달받으면 출력 버퍼를 비우게 된다.
버퍼를 사용함으로써 얻는 장점은 묶어서 한 번에 전달하므로 전송시간이 적게 걸려 성능이 향상된다. 사용자가 문자를 잘못 입력했을 경우 수정을 할 수 있다는 장점이 있다.

반드시 버퍼를 사용하는 것이 좋은 것은 아니다. 빠른 반응이 요구되는 게임과 같은 프로그램에서는 키를 누르는 즉시 전달되어야 하므로 버퍼를 사용해서는 안된다. 그러므로 버퍼는 자신의 사용 목적에 맞게 사용하여야 한다.

"입출력 데이터의 타입을 자동으로 변환시켜주므로 더 편리하고 안전하다." 이 부분에 대해서 이해가 안 되는 분들이 있을까봐 부연 설명만 조금 덧붙이겠습니다.

위 답에서 나왔다시피 C언어에서 입력을 받기 위해서는 scanf 라는 함수를 사용합니다.
이 함수는 받아야 하는 데이터 타입을 지정해주어야만 사용이 가능합니다.
가령, 정수를 입력 받는다고 할 때,
scanf("%d", a); // a라는 변수에 정수 값을 입력 받는다. %d는 정수라는 의미입니다.
문자열을 입력 받는다고 할 때,
scanf("%s", a); // a라는 변수에 문자열을 입력 받는다. %s는 문자열이라는 의미입니다.
이렇게 데이터 타입을 지정해주게 됩니다.
실제 프로그래밍을 하게 된다면 앞서 초기화 과정에서 a라는 변수의 자료형이 지정이 될 것이기 때문에, scanf 함수에서 데이터 타입을 또 적어주는 것은 불필요하다고 느낄 수가 있습니다.

이것을 C++에서는 개선했습니다.
그 결과물이 바로 cin과 같은 입력 스트림입니다.
덕분에 여러분들이 cin을 통해 입력을 받을때마다, cin(int) >> a 와 같이 사용하지는 않죠. (실제로도 잘못된 문법입니다. 오류나요..)
이건 단순히 불편함을 해결한 것에서 벗어나서, 나중에 배울 overloading이라는 객체지향에서의 중요한 개념과도 연결지을 수 있습니다. 이건 그때 가서 다시 언급해드리도록 하겠습니다.

1, 2차시 과제 피드백 (은승우, 이재혁, 장예원, 최병준, 최성원, 최용욱, 최지민)

void swap(int& x, int& y) {
	int k = 0;//값 설정 안 해줘도 괜찮! int k=x;이렇게 해도 됩니다!
	k = x;
	x = y;
	y = k;
}
if (math < 5) {
		//math<5 라고 하면 1,2,3,4제외한 다른 범위 내 숫자들도 포함되기 때문에
		//ex) 0을 입력해도 에러가 나지 않듯이
		//switch로 케이스 나누면 괜찮을 듯 합니다!
		cout << "숫자를 입력해 주세요 : "; cin >> num1; cin >> num2;
		if (math == 1) {
			a = num1 + num2;

			cout << "답은 " << a << "입니다!" << endl;
		}

		else if (math == 2) {
			a = num1 - num2;

			cout << "답은 " << a << "입니다!" << endl;
		}

		else if (math == 3) {
			a = num1 * num2;

			cout << "답은 " << a << "입니다!" << endl;
		}

		else if (math == 4) {
			a = num1 / num2;

			cout << "답은 " << a << "입니다!" << endl;
		}

	}
float div(float x, float y)
{
	float result = x / y;
	return result;
} // 이 친구처럼 float으로 나누는 것이 정석입니다. 정수형 연산을 의도 하지 않은 경우에는 5 / 2 == 2.5로 해야 제대로 답이 나오기 때문이에요! 하지만 정수형 연산을 의도한 상황에서는 꼭 return도 int, 파라미터도 int로 해 주어야 합니다. 어떻게 연산을 하느냐에 따라 return type, paremeter type도 다르게 해 주어야 해요!

int yee(int x){
	return x / 10 + x % 10;
} // 이렇게 두자리 수를 합친 연산을 할 때 실수 연산을 하면 큰일 나겠죠..?

이미 1강은 앞에 너무 설명이 잘 되어 있는 관계로 피드백은 하지 않겠습니다 ㅎㄷㄷ 그리고 벌써 try, catch문을 쓴 친구가 있는 것 같더라구요 ㅋㅋㅋㅋㅋㅋ 고인물 친구....

3차시 과제 피드백 (김범구, 김서영, 김수민, 김승찬, 김진호, 심규진, 윤창신)

3.1

아래는 정답 예시입니다. 가장 잘 작성한 친구의 코드를 살짝 수정해서 가져왔습니다.

#include <iostream>
using namespace std;

int main() {
	int num = 0;
	float x, y;
	while (num != 5) {
		system("cls");
		cout << "===== 건방진 계산기 =====" << endl;
		cout << "1. 덧셈" << endl;
		cout << "2. 뺄셈" << endl;
		cout << "3. 곱셈" << endl;
		cout << "4. 나눗셈" << endl;
		cout << "5. 나가기" << endl; 
		cout << "=========================" << endl;
		cout << "번호를 입력해라. : ";
		cin >> num;

		switch (num) {
		case 1:
			cout << "수를 두 개 입력해라." << endl;
			cin >> x >> y;
			cout << "답은 " << x+y << endl;
			system("pause");
			break;
		case 2:
			cout << "수를 두 개 입력해라." << endl;
			cin >> x >> y;
			cout << "답은 " << x-y << endl;
			system("pause");
			break;
		case 3:
			cout << "수를 두 개 입력해라." << endl;
			cin >> x >> y;
			cout << "답은 " << x*y << endl;
			system("pause");
			break;
		case 4:
			cout << "수를 두 개 입력해라." << endl;
			cin >> x >> y;
			cout << "답은 " << x/y << endl;
			system("pause");
			break;
		case 5:
			cout << "이제 가라." << endl;
			break;
		default:
			cout << "이상한걸 입력했구나." << endl;
			system("pause");
			break;
		}
	}
	
}

이 친구를 포함해서 답은 모두 맞았습니다만..
여러분들이 컴퓨터공학과라면 불편해야 할 부분이 있습니다.
제출하신 모든 분들 코드에 공통되는 부분입니다.

cout << "수를 두 개 입력해라." << endl;
cin >> x >> y;

같은 코드가 4군데에 걸쳐서 여러 번 반복됩니다.
앞으로 여러분들이 코드를 보실 때, 같은 코드가 여러 번 반복되고 있다면 불편하셔야 합니다.
실제로 좋지 못한 방법입니다.
가령, 교수님이 "숫자 두 개 입력해라"는 너무 건방진 거 아니야? "숫자 두개 입력해주세요"로 고쳐! 라고 했을 때, 여러분들은 4줄의 코드를 수정해야 합니다.
이 경우에는 아주 작은 경우지만, 여러분들이 나중에 더 큰 프로젝트를 할 때도 이런 습관을 고치지 않았다면 수정할 때 엄청 고생하겠죠.
코딩은 수정의 반복입니다. 이런 습관 하나하나가 생산성을 떨어트리는 요소가 됩니다.

잔소리는 그만하고, 어떻게 수정하면 되느냐.
여러분들이 2차시에서 열심히 배우고 사용한 것이 있죠, 함수.
입력을 받는 함수를 사용해서 입력을 처리하는 겁니다.

void inputTwoNumber(int& x, int& y) {
	cout << "숫자 두 개 : "
	cin >> x >> y
}

이렇게요.
(여기서 pass by reference를 사용한 이유는, 실제 x와 y에 입력받은 값을 넣기 위해서입니다.)
이걸 넣어서 코드를 바꾸면

switch (num) {
case 1:
	inputTwoNumber(x, y);
	cout << "답은 " << x+y << endl;
	system("pause");
	break;
case 2:
	inputTwoNumber(x, y);
	cout << "답은 " << x-y << endl;
	system("pause");
	break;
case 3:
	inputTwoNumber(x, y);
	cout << "답은 " << x*y << endl;
	system("pause");
	break;
case 4:
	inputTwoNumber(x, y);
	cout << "답은 " << x/y << endl;
	system("pause");
	break;
case 5:
	cout << "이제 가라." << endl;
	system("pause");
	break;
default:
	cout << "이상한걸 입력했구나." << endl;
	system("cls");
	break;
}

마찬가지로, "답은 ~ "하고 보여주는 부분과 system("pause") 역시 함수화를 시킬 수 있겠죠.

함수화를 할 때는 두 가지만 고려하세요.

  1. 이 함수를 여러 번 반복해서 사용할 것인가?
  2. 굳이 함수화를 할 정도로 의미가 있는가?

만약 지금 작성한 inputTwoNumber 라는 함수가 2번에 의해서 별로 의미 없는 함수라고 생각하고 함수화를 굳이 시키지 않은 분이 있다면, 그것 또한 훌륭한 정답입니다.

3.2

아래는 정답 예시입니다. 가장 잘 작성한 친구의 코드를 약간만 수정했습니다.

#include <iostream>
using namespace std;

int main(void) {
	int num;
	int i;
	cout << "자연수를 입력해 주세요 : ";
	cin >> num;

	for (i = 2; i < num; i++) {
		if (num % i == 0) {			// 소수아님 
			cout << "소수가 아닙니다." << endl;
			break;
		}
	}	
	
	if (num == i) {
		cout << "소수입니다!" << endl;
	}

	return 0;
}

세 친구가 이런 맥락으로 작성했고, 한 친구는 나누어질때마다 count를 증가시켜서 count 값을 이용해서 소수 판별을 했습니다.
우선은 이 방법이 count를 이용한 방법보다 좋은 방법입니다.
count를 이용해서 계속 카운팅을 해준다면 이미 소수가 아님이 확정이 났음에도 반복문이 돌아가기 때문입니다. "소수인지 아닌지"만 판별하는 이 코드에서는 소수가 아님이 확정이 났다면 break를 해주는 것이 좋습니다.

다만, 굳이 이 방법을 언급한 이유는 코딩 습관이 다른 세 친구들보다 좋았다는 점 때문입니다.
위 코드를 포함한 세 친구의 방법에는 반복문이 끝나고 소수임을 판별할 때 num과 i의 값이 같으면 소수라고 출력하도록 했습니다.
제가 제 3자의 입장에서 코드를 봤을 때 왜 num과 i가 같은 것이 소수인지 바로 이해하기가 어려웠습니다.

코드는 이해하기 쉽게 작성되어야 합니다.
설령 이 방법이 가장 효율적인 방법이라고 하더라도 num과 i가 같을 때 소수라는 것이 바로 이해되지 않는다면 좋은 코드라고 하기 힘듭니다.
이건 제 3자가 봤을때도, 미래의 자기 자신이 봤을 때도 마찬가지입니다.
앞으로 여러분들이 프로그래밍을 하다보면 지난 코드를 돌아볼 때가 종종 있습니다. 생각보다 본인 코드를 이해하지 못하는 경우가 많아요. 이것도 아마 그런 경우 중 하나일겁니다.

이렇게 이해가 되지 않는 부분에는 주석을 사용하세요.
if(num == i) 부분 옆에 주석으로 "소수를 판별하는 for 문을 중간에 탈출하지 못했으니 소수임." 정도의 주석만 달아주면 나중에 볼 때도, 다른 사람이 볼 때도 훨씬 이해하기 쉽겠죠.

제가 굳이 위에 코드를 가져온 이유도 이것입니다.
"소수 아님"이라는 주석이 if문 옆에 있습니다.
우리가 일반적으로 나누어떨어지지 않는다면 소수가 아니라는 사실을 압니다.
이 부분은 굳이 주석을 달 필요가 없는 부분이에요. 그 줄 코드로 모든 것이 이해가 되니까요.
효율적인 면에서는 정답이지만 주석을 다는 부분에서는 모범 오답이었습니다.

count를 사용하는 방법은 이와 반대로 굳이 주석을 달지 않아도 이해하기가 쉽습니다.
주석이 없이도 이해가 된다면 굳이 주석을 달지 않아도 된다고 위에서 말씀드렸죠.
반대로 말하면, 굳이 주석을 작성하지 않아도 되는 코드를 작성하는 것이 정말 좋은 코딩 습관이에요.
그러기 위해서는 count와 같이 이해하기 쉬운 변수명을 사용하고, 이해하기 쉬운 함수명을 사용하시는게 좋습니다.

이 두 가지 방법의 장점만 뽑아서 모범 답안을 다시 만들어볼게요.

#include <iostream>
using namespace std;

int main(void) {
	bool is_primary = true;
	int num;
	cout << "자연수를 입력해 주세요 : ";
	cin >> num;

	for (int i = 2; i < num; i++) {
		if (num % i == 0) {
			is_primary = false;
			break;
		}
	}	
	
	if (is_primary) {
		cout << "소수입니다!" << endl;
	} else {
		cout << "소수가 아닙니다!" << endl;
	}

	return 0;
}

bool 타입 변수 is_primary를 만들어, 소수인지 아닌지를 담도록 했습니다.
이렇게 변수 이름 자체에서 의미를 전달해주면 굳이 주석을 달 필요도 없이 이해가 잘 되는 코드를 작성할 수 있겠죠.

작성하다보니 너무 길게 작성한 것 같네요. 읽느라 고생하셨습니다.

5차시 과제 피드백 (김범구, 김서영, 김수민, 김승찬, 김진호, 심규진, 윤창신)

5.1

아래는 모범 답안입니다.

#include <iostream>
using namespace std;

void swap(int* ptr1, int* ptr2) {
	int temp = *ptr1;
	*ptr1 = *ptr2;
	*ptr2 = temp;

}
int main() {
	int a = 5;
	int b = 7;
	swap(&a, &b);
	cout << a << " " << b << endl;

	return 0;
}

이 swap 함수와 같이 포인터를 파라미터로 넘기는 방식을 pass by address(=call by address)라고 합니다.
여러분들은 지금까지 총 3가지 방식을 배웠어요. pass by value, pass by reference, pass by address
pass by value는 값만 넘기는 것이기 때문에 원래 값의 변조가 일어날 수 없습니다.
pass by reference는 주솟값이 넘어가기 때문에 원래 값의 변조가 일어날 수 있습니다.
pass by address는 주솟값이 넘어가기 때문에 원래 값의 변조가 일어날 수 있습니다.
아래 2개가 똑같네요?

지난 4차시 피드백에서 C언어의 불편한 점을 C++에서는 어떻게 개선하는지를 보면 좋은 공부가 될 것이라고 했었죠.
사실 C언어에는 pass by reference라는 방법이 없습니다. 따라서 함수 안에서 원래 값에 직접 접근해야 하는 경우에는 pass by address 방법 밖에는 사용할 수 없었어요.

이 swap 함수처럼 pass by address 방식으로 넘기는 경우는 (물론 실제 주솟값을 이용한 처리를 하는 경우도 있지만) 원래 값을 건들기 위해서 사용한 것이 대부분이었고, 그러기 위해서 이렇게 포인터를 불편하게 사용해야 한다는 C언어의 불편한 점을 해결하기 위해서 C++에서는 pass by reference라는 방법이 등장한 것입니다. 그렇기 때문에, C++에서는 이렇게 원래 값을 건드는 함수를 작성하기 위해서 포인터를 쓸 필요가 없습니다. 여러분들이 실제로 pass by address와 pass by reference를 둘 다 사용해봐서 아시겠지만, pass by reference 방법이 압도적으로 편한 것을 느낄 수 있었을 겁니다.

이 문제를 풀면서, pass by address는 대부분의 경우 pass by reference로 대체가 가능하다는 점을 얻어가셨으면 좋겠습니다. (물론 주솟값을 직접 다루는 프로그래밍을 할 때는 당연히 pass by address가 사용됩니다. pass by address가 구시대적인 방법이라는 의미는 결코 아닙니다.)

5.2

동적 할당 후 메모리 해제는 선택이 아닌 필수입니다.
따라서, 정답자는 없습니다.
아래는 모범 답안입니다. 가장 깔끔한 코드에서 메모리 해제 부분만 추가했습니다.

#include <iostream>
using namespace std;

int main() {
	int length;
	int sum = 0;
	cout << "배열의 길이를 입력해 주세요 : ";
	cin >> length;
	int* arr = new int[length];
	for (int i = 0; i < length; i++) {
		cout << i + 1 << "번째 수를 입력해 주세요 : ";
		cin >> arr[i];
		sum += arr[i];
	}
	cout << "총 평균은 : " << (float)sum / length << "입니다!" << endl;
	delete[] arr;
}

이 문제를 못 푼 친구는 없기 때문에 문제에 대한 설명보다도 메모리 해제에 대해서 더 자세히 말씀드리겠습니다.

python과 같은 쉬운 언어가 아닌, C++이라는 불친절하고 복잡한 언어를 왜 사용할까요?
c와 c++에는 가장 큰 장점이자 가장 큰 고난이 있습니다. 포인터.
메모리를 직접 제어할 수 있다는 장점이 있지만, 반대로 그 메모리에 대한 책임은 전적으로 개발자에게 있다는 단점도 있습니다. 메모리를 개발자가 직접 다루기 때문에, 메모리를 효율적으로 다룰 줄 모른다면 C++이라는 언어를 배우는 의미가 없습니다.

여러분들이 python을 쓰실 때는, 이렇게 코딩하면 메모리가 어떻게 된다.. 하는 걸 전혀 신경쓰지 않았을 겁니다. 그게 당연합니다. python과 같은 개발자에 친숙한 고급 언어들은 GC(garbage collector)라는 놈이 안 쓰는 메모리를 알아서 수거해갑니다. 메모리를 따로 관리해주는 놈이 있기 때문에 내가 굳이 신경 쓸 필요가 없는 거죠.

C++은 다릅니다. C++에는 GC라는 친절한 놈이 없거든요. 메모리를 오로지 개발자 스스로가 모두 책임지고 있어요. 개발자만 더 귀찮아진다고 생각할 수도 있겠지만, GC가 신경쓰지 못하는 세세한 부분까지 개발자가 건드릴 수 있다는 정말 큰 장점이 있어요. 그래서 다른 모든 고급 언어로 개발한 결과물보다 C나 C++로 개발한 결과물이 대부분의 경우에 더 빠릅니다. 메모리 관리를 세세하게 해주니까요. (사실 GC의 성능이 그렇게 좋지가 않거든요.)

말이 좀 길어졌는데, 결론은 이거에요.
동적 할당을 했으면 무조건 delete를 해주자.
동적 할당한 메모리를 다 쓴 경우에는 delete를 해줘서 메모리 낭비를 막아야 합니다. 이건 C++ 개발자의 의무에요.
아마 그렇기 때문에 실제 학교 시험이나 과제를 하실 때 delete를 안 하면 감점을 받을거에요.

그리고.. 이건 delete 사용법입니다. 사용법을 모르는 분들이 많은 것 같아서요.

int형 포인터

int *ptr = new int;
*ptr = 10;
cout << *ptr;

이런 식으로 ptr을 int형 포인터로 사용한 경우에는
delete ptr;
이렇게 메모리 해제를 해줍니다.

int형 배열

int *ptr = new int[10];
for(int i=0; i<10; i++) {
    cin >> ptr[i];
}

이런 식으로 ptr을 int형 배열로 사용한 경우에는
delete[] ptr;
이렇게 메모리 해제를 해줍니다.

int형 2차원 배열

int **ptr = new int*[5];
for(int i=0; i<5; i++) {
    ptr[i] = new int[3];
}

이런 식으로 ptr을 2차원 int형 배열로 사용한 경우에는

for(int i=0; i<5; i++) {
    delete[] ptr[i];
}
delete[] ptr;

이렇게 메모리 해제를 해줍니다. (1차원에 대해서 반복문으로 해제해주고, 2차원에 대해서 해제해준 것입니다.)
이건 좀 어려운 예시에요.

5.3

5.2와 마찬가지로 메모리 해제를 하지 않아 정답자는 없습니다.
모범 답안이 둘입니다. 마찬가지로 메모리 해제 부분만 추가했습니다.

모범답안1

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

int main() {
	int max, count, num;
	cout << "추첨 번호의 갯수를 알려 주세요 : ";
	cin >> max;
	cout << "당첨 번호의 갯수를 알려 주세요 : ";
	cin >> count;

	int* ball = new int[max];
	for (int i = 0; i < max; i++) { //추첨번호가 저장된 배열 생성
		ball[i] = i + 1;
	}

	int* lotto = new int[count]; //당첨 번호를 저장할 배열 생성

	srand((unsigned int)time(NULL));
	for (int j = 0; j < count; j++) { // 난수를 당첨번호 lotto 배열에 저장하는 반복문
		num = rand() % max;
		if (ball[num] != NULL) { // 추첨번호 ball 배열에 값이 있을 경우 lotto에 저장
			lotto[j] = ball[num];
			ball[num] = NULL; // 배열의 해당 번호를 NULL로 만들기
		}
		else { //없을 경우 다시 반복문 실행
			j = j - 1;
		}

	}

	cout << "당첨 번호는 : ";

	for (int i = 0; i < count; i++) { // 당첨번호 lotto 배열 출력
		cout << lotto[i] << ' ';
	}

	cout << "입니다" << endl;

	delete[] ball
	delete[] lotto
}

모범답안2

#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;

int main()
{
	int choo , dang  , a = 0;
	cout << "추첨 번호의 갯수를 알려주세요 : ";
	cin >> choo;
	cout << "당첨 번호의 갯수를 알려주세요 : ";
	cin >> dang;
	int* ptr = new int[dang];
	srand((unsigned int)time(NULL));
	for (int i = 0; i < dang; i++)
	{
		ptr[i]  = rand() % choo + 1;
		for (int a = 0; a < i; a++)
		{
			if (ptr[a] == ptr[i])
			{
				ptr[i] = rand() % choo + 1;
				a = 0;	
			}	 
		}
	}
	cout << "당첨 번호는 : ";
	for (int i = 0; i < dang; i++)
	{
		cout << ptr[i] << " ";
	}
	cout << endl;

	delete[] ptr
}

모범 답안을 둘 뽑은 이유는 중복 체크를 하는 두 알고리즘을 소개해주기 위해서입니다.

중복 체크를 하는 알고리즘 중 두 가지를 소개해드리면,

  1. 전체 경우에 대해서 이미 사용되었는지를 담고 있는 리스트를 사용하기
  2. 값을 지정하기 전에, 이전 값들을 하나씩 중복체크를 해보기

모범답안1이 1번 알고리즘을 사용했고, 모범답안2가 2번 알고리즘을 사용했습니다.

모범답안1부터 보겠습니다.
ball이라는 배열에 NULL이 있을 경우에는 그 번호가 사용이 된 것이고, 숫자가 있는 경우에는 사용이 되지 않은 것입니다. 이후 랜덤 값을 생성한 후에, 그 값을 그대로 ball 인덱스로 넣어봐서 NULL인지 아닌지만 체크해주면 중복인지 아닌지를 바로 알 수 있죠. NULL이 아닌 경우 생성된 랜덤 값을 당첨 번호 리스트에 담고, ball의 해당 인덱스의 값을 NULL로 만들기만 하면 됩니다.

다음은 모범답안2입니다.
랜덤 값을 만들면, 이전에 넣은 당첨 번호 리스트의 모든 값과 비교해서 중복되는 경우 랜덤 값을 다시 생성하도록 했습니다. 간단하네요.

이 두 알고리즘은 서로 장단점이 있습니다. 시간적인 측면에서 먼저 봅시다.
모범답안1 : 랜덤 값을 만든 후 ball의 인덱스에 넣어서 NULL인지 아닌지만 체크하면 됨. 모든 경우에서 단 한 번의 비교만 있으므로 매우 빠름!
모범답안2 : 랜덤 값을 만든 후 이전에 넣은 당첨 번호 리스트이 모든 값과 비교함. 이전에 넣은 당첨 번호 갯수가 많으면 많을 수록 느려짐.

공간적인 측면에서 봅시다.
모범답안1 : 전체 경우에 대한 리스트인 ball이 차지하는 공간이 따로 필요함. 전체 경우(=추첨 번호 갯수)가 많으면 많을수록 공간을 많이 차지함.
모범답안2 : 따로 공간을 차지하지 않음.

이와 같이 같은 결과물을 내더라도 여러가지의 알고리즘이 사용될 수 있으며, 그 알고리즘마다 각각 시간적 공간적인 측면에서 장단점이 다릅니다. 이런 점을 모두 고려해서 알고리즘을 선택한다면 더 빠르고 효율적인 프로그램을 만들 수 있겠죠.

이 경우에는 시간적인 측면에서는 "당첨 번호 갯수"가, 공간적인 측면에서는 "추첨 번호 갯수"가 각각 성능을 결정합니다. 따라서 당첨 번호 갯수가 적은 경우에는 모범답안2의 알고리즘을, 추첨 번호 갯수가 적은 경우에는 모범답안1의 알고리즘을 사용하는 것이 좋겠죠?

4차시 보충

4차시 피드백에서 아래 코드가 동작하지 않는 이유를 5차시 피드백에서 설명한다고 했었습니다.

float average(int arr[]) {
    int sum = 0;
    int length = sizeof(arr) / sizeof(arr[0]);
    for(int i=0; i<length; i++) {
        sum += arr[i];
    }
    return (float) sum / length;
}

int main() {
    int subject[4];
    cout << average(subjects);
    return 0;
}

5차시 공부를 제대로 했다면, 배열과 포인터는 비슷하게 쓰일 수 있다는 것을 아실겁니다.
배열 변수는 첫번째 원소의 주솟값을 가지는 포인터 변수라고도 할 수 있죠.

C++은 효율을 중요시합니다.
함수의 파라미터로 배열 하나가 통째로 pass by value로 넘어가면 매우 큰 메모리가 복사되어 함수로 전달되는 것이기 때문에 엄청난 메모리 손실이 발생합니다. (당장 크기가 1000인 float형 배열을 함수로 넘기면 4KB가 함수로 넘어가요.)
그렇기 때문에, 따로 pass by address라고 언급하지 않았어도 배열을 파라미터로 넘길 때는 자동으로 pass by address로 전환되어서 넘어갑니다. 여기서의 address는 배열 첫번째 원소의 주솟값이죠. 이렇게 되면 엄청난 양의 메모리가 넘어가야 할 것을, 하나의 포인터 주솟값만 넘어가게 되므로 정말 많은 메모리가 절약됩니다.
결과적으로, 함수가 받은 파라미터는 배열 첫번재 원소의 주솟값밖에는 없게 됩니다. 이게 원래 배열이었는지 포인터였는지는 알 수 없는, 그저 포인터 하나가 넘어온 것 뿐입니다.

위 코드로 넘어가보겠습니다.
average 함수 입장에서는 그저 subject[0]의 주솟값이 arr에 넘어온 것이기 때문에, sizeof(arr)을 하면 포인터 변수 하나의 크기가 나옵니다. sizeof(arr[0])에는 subject[0]의 크기가 나오구요. 제 pc에서는 int가 4바이트를 차지하고 (환경에 따라 8바이트일 수도 있습니다.) 포인터 변수도 4바이트를 사용하기 때문에, average 함수 안의 length 변수의 값은 1이 됩니다. 그래서 배열 사이즈를 못 구해요 ㅠㅠ

이번에도 너무 길게 작성한 것 같네요..
5차시도 수고 많으셨습니다.

8차시 과제 피드백 (김범구, 김서영, 김수민, 김승찬, 김진호, 심규진, 윤창신)

과제를 제출한 친구가 없어서 민재가 올린 예제코드로 피드백하겠습니다.

8.1

예제코드 보기

gcd 함수만 이해하면 그렇게 어려운 문제는 아닙니다.
이 함수의 원리는 이곳에서 확인해보세요.

객체를 만들어보는 것 자체에 의미가 있는 문제이기 때문에.. 코드에 대한 설명은 따로 하지는 않겠습니다.
다만 pdf에 설명이 조금 부족한 부분이 있어서 이 부분만 보충하겠습니다.

'.' 연산자와 '->' 연산자의 차이입니다.
'.' 연산자는 객체(또는 구조체)의 멤버 변수, 멤버 함수에 접근할 때 사용합니다.
지금 문제와 같은 코드가 있을 때 f1이라는 객체의 멤버 변수 numerator에 접근하기 위해서는 f1.numerator (물론 private 이기 때문에 접근은 안 됩니다.) 를 사용할 수 있고, 멤버 함수 getDeno에 접근하기 위해서 f1.getDeno() 를 사용할 수 있습니다.
자 그럼 이 f1이라는 객체를 가리키는 포인터가 있다고 해봅시다. 이걸 pf1이라고 합시다. 초기화는 아래와 같이 하겠죠?
Fraction * pf1 = &f1
이 pf1을 이용해서 f1의 numerator에 접근하려면 (1) pf1의 값인 f1를 꺼내온 후, (2) 이 f1의 numerator를 가져오도록 해야겠죠. (1)은 '*'라는 연산자를 사용하고, (2)는 '.'라는 연산자를 사용하면 되겠네요. (*pf1).numerator 이렇게요. 멤버 함수도 마찬가지입니다.
C++을 사용하다보면 포인터를 사용할 일이 굉장히 많은데, 이 모든 순간에 "(*포인터)." 형태로 작성한다면 불편하지 않을까요?
이걸 간편하게 줄여준 것이 '->' 연산자입니다. 간단해요. "(*포인터)." == "포인터->" 입니다. pf1->numerator, pf1->getDeno() 이렇게 사용할 수 있겠죠.

예제코드를 보면 this에 '->' 연산자를 사용합니다. this가 "객체"가 아니라 "객체 포인터"라는 거죠. 정확히는 "객체 포인터 상수" 에요. 가리키는 값이 변할 수 없는 포인터인거죠. 사실 조금만 생각해보면 객체 포인터 상수가 할 수 있는 일들은 객체 그 자체로도 할 수 있어요. (그럴 일은 없겠지만 객체 포인터 상수의 주솟값을 사용하지 않는다면요.) 그렇다면 궁금해야죠. 왜 "객체"가 아니라 "객체 포인터 상수"인가?
사실 답변은 이전 피드백에 남겨놨었어요. 5차시 피드백 에 있는 "4차시 보충" 부분에 있는 배열에 대한 설명을 객체로 바꾸면 바로 이해할 수 있을 겁니다. 사실 this는 멤버 함수의 하나의 파라미터로 넘어가기 때문에 (명시해주지는 않음.) 객체 그 자체로 넘어가면 메모리가 많이 소모됩니다. 그래서 메모리 손해가 적은 pass by address를 선택한 것이죠. 왜 똑같이 메모리 손해가 적고 더 편한 pass by reference가 아니냐? 라고 굳이 묻는다면.. this라는 개념이 생길 당시에 reference라는 개념이 없었기 때문입니다.

8.2

예제답변 보기

이렇게 말로 하는 것보다는 코드를 작성하면서 스스로 느끼는 것이 더 좋을 것 같습니다.
설명은 따로 하지 않겠습니다. 직접 느끼기 전에는 어떻게 설명해도 완벽히 이해되지 않을 겁니다.

수고하셨습니다.

9차시 과제 피드백 (김범구, 김서영, 김수민, 김승찬, 김진호, 심규진, 윤창신) - 마지막

과제를 제출한 친구가 없어서 민재가 올린 예제코드로 피드백하겠습니다.

8.1

예제코드 보기

와! 드디어 1차시 피드백에서 말한 것을 설명할 수 있게 됐네요.
friend ofstream& operator<<(ostream&, const Fraction&);
이 부분을 보시면, <<라는 연산자를 오버로딩했습니다.
cout과 같은 output stream의 출력 연산을 Fraction 객체에 한해서 정의를 해준 것이죠.

이와 반대로
friend ifstream& operator>>(ifstream&, const Fraction&);
이렇게 >>라는 연산자를 오버로딩해서 입력을 받도록 한다면 Fraction 객체에 한해 입력받는 함수를 정의할 수 있게 됩니다.

1차시 피드백에서 scanf와 cin을 비교했었습니다.
Fraction 같은 객체를 사용자에게 입력받는다고 했을 때, scanf는 별도의 방법이 없습니다. 직접 하나하나의 멤버 변수를 입력받아서 연산해주어야 해요. 하지만 cin은 이렇게 오버로딩을 해주면 되는거죠. "cin >> Fraction객체" 형태로 사용할 수 있습니다.

이걸 이해하고 다시 1차시 피드백으로 넘어간다면 새로운 깨달음을 얻을 수 있을거에요.


하나 더 중요한 개념이 있습니다. 예제코드에서 객체를 받을 때 reference 상수(const &)로 받습니다.

"아니, reference는 변수를 함수 안에서 수정할 수 있도록 하기 위해서 쓰이는데 왜 여기에 상수를 붙여서 그런걸 못하게 만들어!" 라는 생각을 할 수 있습니다.

지난 피드백들에서 계속 이야기했지만 pass by value는 메모리 손실이 큽니다. 값을 그대로 복사하다보니 원래 값의 메모리 크기를 그대로 복사해서 사용하기 때문이죠. 객체는 큰 데이터를 담고 있을 확률이 매우 높기 때문에 객체를 pass by value로 넘긴다면 많은 메모리가 차지됩니다.

하지만 이를 pass by reference 또는 pass by address를 사용한다면 변수(여기서는 객체) 그 자체의 주솟값만 복사되어 넘어가기 때문에 메모리 낭비를 줄일 수 있습니다. 다만 address가 아닌 reference를 사용하는 이유는 reference로 넘기는 경우에는 value로 넘긴 것과 동일한 문법을 사용할 수 있고, const로 제약을 주기 때문에 원래 값을 건드리지도 못하니 안전하기도 하죠. 그래서 reference를 사용합니다.


음 그리고 operator overloading에 대해서 간단한 설명만 덧붙이자면
멤버 함수로 operator overloading을 하는 경우 1개의 파라미터(상대방 객체)만 사용하고,
일반 함수로 operator overloading을 하는 경우 2개의 파라미터(각각)을 사용합니다.
아래와 같이요. 아래 두 함수는 동일한 역할을 합니다.
Fraction Fraction::operator+(const Fraction& other);
Fraction operator+(const Fraction& one, const Fraction& other);

이걸 헷갈리는 사람들이 많은 것 같더라구요.

8.2

예제코드 보기

그냥 개념을 따라한 문제라 크게 말할 내용은 없는 것 같습니다.

마치며

지금 이걸 보고 있는 분들이라면 아마 끝까지 열심히 공부한 친구들이겠죠.
모든 친구들이 열심히 공부해서 과제를 풀고 피드백을 받는다면 정말 좋겠지만 그러기 힘들다는 것 잘 압니다. 각자의 중요한 일들이 있었겠죠. 그저 한 명에게라도 도움이 되었으면 그것으로 만족합니다.

앞으로 객체지향프로그래밍이라는 과목을 수강하시면서 많이 어려울겁니다. 웹/파이썬 프로그래밍을 배우고 "나도 이제 코딩할 수 있어!"라고 생각했는데, 갑자기 이상한 언어와 어려운 개념들이 나타나서 괴롭힐겁니다. 그 힘든 싸움에서 조금이나마 도움이 되고 싶어서 복잡한 이야기도 섞어 설명했습니다.

앞으로 여러분이 객체지향프로그래밍이라는 과목을 공부하면서 주목해야 할 것은 3가지입니다.

  1. 객체지향프로그래밍이 무엇이고 왜, 어떻게 쓰이는가?
  2. 왜 굳이 어려운 C++을 배워야 하는가?
  3. C++은 C와 어떻게 다른가?

개정된 커리큘럼에서 어떨지는 모르겠지만, 작년 제가 배웠을 당시에는 1번에 대해서는 정말 잘 가르쳐줍니다. 수업만 듣고도 문제 없을 정도로 객체지향에 대해서 자세히 배울거에요. 하지만 2번과 3번에 대해서는 좀 아쉬웠어요. 그래서 제가 피드백을 할 때 2번과 3번에 대한 이야기를 많이 했습니다.

2. 왜 굳이 어려운 C++을 배워야 하는가?
요즘에는 컴퓨터가 정말 많이 좋아져서 큰 차이가 느껴지지 않지만, 프로그래밍을 하면서 "속도"는 반드시 신경써야 할 요소입니다. 여러분이 1학기에 배운 python 언어는 스크립트 언어, C++은 컴파일 언어입니다. python은 한 줄씩 기계어로 변환되면서 처리되는 반면, C++은 컴파일이라는 과정을 거쳐 기계어로 변환된 상태에서 사용자에게 주어집니다. 컴퓨터가 기계어만 처리할 수 있다는 점을 고려할 때, 후자의 경우가 무조건 더 빠릅니다. "공간"도 마찬가지입니다. 모든 것이 동적으로 처리되는 python과 달리 C++은 컴파일 이전에 지정되는 정적인 영역을 최대한 사용하여 더 적은 메모리 공간을 차지할 수 있도록 합니다.
그래서 저는 학교에서 굳이 python이 아닌 C++로 객체지향프로그래밍을 가르친다는 것은 "속도"와 "공간"을 신경쓰라는 의도가 아니었을까 생각합니다.
여러분들이 C++로 프로그래밍을 할 때만큼은 반드시 "속도"와 "공간"을 신경쓰시기 바랍니다.

3. C++은 C와 어떻게 다른가?
C++은 C에서 발전된 형태입니다. 단순히 C와 C++을 비교하는 것만으로도 프로그래밍이 어떻게 발전되어왔는지를 확인할 수 있습니다. 그 발전상을 눈여겨본다면 과거의 위대한 프로그래머들의 생각을 조금이나마 엿볼 수 있을 것이라 생각합니다.

피드백은 여기까지입니다. 그동안 수고 많았습니다.
궁금한 내용 있으면 연락주세요. (훈련소에 있는 기간만 아니라면) 언제든지 답해드리겠습니다
학기 중에 좋은 성과 거두시길 바랍니다.

2차시 과제 피드백 (김범구, 김서영, 김수민, 김승찬, 김진호, 심규진, 윤창신)

2-1.

제출한 모든 분들이 정답입니다.
아래는 정답 예시입니다.

#include <iostream>

using namespace std;

void swap(int& x, int& y) {
	int temp = x;
	x = y;
	y = temp;
}

int main(void) {
	int x = 5;
	int y = 7;

	swap(x, y);

	cout << x << ' ' << y << endl;
}

이 코드에서 void swap(int x, int y) 이렇게 바뀐다면 어떻게 될지 한 번 상상해보세요.

2-2.

제출한 모든 분들이 정답입니다.
아래는 정답 예시입니다.

#include <iostream>
using namespace std;

int addDigit(int x) {
	int tens = x / 10;
	int ones = x % 10;

	return tens + ones;
}

int main() {
	int x;
	cout << "두 자리수를 입력해 주세요 : ";
	cin >> x;
	cout << "두 자릿수를 더하면... : " << addDigit(x) << endl;
}

많은 분들이 ones를 계산할 때 x - tens * 10 을 해서 계산을 했는데,
%라는 좋은 연산자를 배운 만큼 %를 사용해서 표현한다면 더 간단하게 표현할 수 있겠죠.

2-3.

정답자는 한 명입니다.
아래는 정답 예시입니다.

#include <iostream>
using namespace std;

float add(float x, float y) {
	return x + y;
}
float subtract(float x, float y) {
	return x - y;
}
float multiply(float x, float y) {
	return x * y;
}
float divide(float x, float y) {
	return x / y;
}
int main() {
	cout << divide(subtract(multiply(divide(add(20, 35), 5), subtract(45, 30)), 15), 5) << endl;
}

C++연산에서는 중요한 특징이 있습니다.

같은 자료형끼리의 연산의 결과는 그 자료형이다.

예를 들어, int와 int의 연산 결과는 int이고, float와 float의 연산 결과는 float입니다.
실제 우리가 사용하는 수학에서 덧셈, 뺄셈, 곱셈에서는 문제가 되지 않지만 나눗셈은 정수와 정수의 연산에서 유리수가 나올 수 있죠.
이 문제에서 divide 함수가 실제 나눗셈의 역할을 하려면 리턴 값은 int가 아니라 float가 되어주어야 합니다.
하지만 C++에서는 int x와 int y의 나눗셈 결과인 x / y는 float가 아니라 int입니다. 따라서 x와 y를 강제로 float로 바꾸거나, 애초에 x와 y를 float로 선언해주는 방법으로 리턴 값이 실제 float가 되도록 해주어야 합니다. 전자의 방법은 아직 배우지 않았으니 후자의 방법으로 하겠습니다. 그렇다면 입력받는 매개변수 x와 y의 자료형은 float가 되어야 합니다.
문제는 이 결과물이 다른 함수인 multiply의 매개변수로 들어간다는 점입니다. multiply의 매개변수의 타입이 int라면 multiply 함수 안에서 소숫점 아래는 무시되겠죠. 따라서 그 값을 살려주기 위해서는 multiply의 매개변수도 float가 되어야 하고, 그 연산의 결과인 리턴도 float가 되어야 합니다.
이렇게 반복하다보면 모든 매개변수와 모든 리턴을 float로 바꿔주어야만 문제가 해결됩니다.

모든 매개변수와 리턴을 int로 해준 친구, divide의 리턴만 float로 하고 나머지를 int로 해준 친구 모두 주어진 숫자가 조금만 달라지면 오답이 나올겁니다.

아래는 모범 오답입니다.

float divide(int x, int y) {
	float d;
	d= x / y;
	return d;
}

x와 y의 연산 결과는 int입니다. 이미 연산 자체에서(int와 int의 연산 결과는 int !) 소숫점 이하가 사라져있는데, 그것을 다시 float 변수인 d에 넣더라도 사라진 소숫점 이하가 살아나지는 않죠.
실제로 divide(5, 2)를 해도 2라고 나올겁니다.

float divide(int x, int y) {
	return x / y
}

이렇게 중간 변수 없이 써도 마찬가지입니다.

해결 방법으로는, x와 y의 자료형을 강제로 float로 바꿔서 연산 결과가 float가 나오도록 해야합니다.
아마 안 배운 문법일텐데 자료형 변경은 다음과 같이 사용하면 됩니다.

float divide(int x, int y) {
	return (float)x / (float)y
}

(참고로 x나 y중 하나의 값만 float로 바꿔도 제대로 동작합니다. float와 int의 연산 결과는 float입니다.)

쓰고 보니 너무 어렵게 쓴 것 같은데 이해 안 되는 부분 있으면 댓글 달아주시거나 따로 질문주세요~

4차시 과제 피드백 (김범구, 김서영, 김수민, 김승찬, 김진호, 심규진, 윤창신)

4.1

전원 정답입니다. 아래는 모범 답안입니다. (아래 답안이 두 친구가 똑같네요..?)

#include <iostream>

using namespace std;

int main(void) {
	int num;
	
	cout << "자연수를 입력해 주세요 : ";
	cin >> num;

	for (int i = 0; i < num; i++) {
		for (int j = 0; j <= i; j++) {
			cout << "*";
		}
		cout << endl;
	}

	return 0;
}

쉬운 문제인 만큼, 특별히 언급할 것은 없구요.
개인 피드백을 드린 것과 같이 특별한 경우가 아니라면 for문의 counter(위 코드에서의 i, j)는 0부터 시작해서 num보다 작을 동안 반복하는게 좋습니다. 1부터 시작해서 num까지 반복하는 친구들이 종종 있네요. for문을 주로 배열과 함께 사용하고 배열의 인덱스가 0부터 num-1까지 있기 때문에 이 방법이 더 자주 쓰입니다.
(이 모범 답안을 제출한 두 친구는 개인 피드백에 이 내용을 적지는 않았습니다.)

4.2

abs 함수를 이용해서 수학적인 방법으로 식을 만들어 for문을 사용하는 문제입니다.
abs 함수를 잘 이용한다면 하나의 이중 for문으로 해결할 수 있습니다.
아래는 모범 답안입니다.

#include <iostream>
using namespace std;

int main() {
	int num;
	cout << "자연수를 입력 해 주세요 : ";
	cin >> num;
	for (int i = num - 1; num - abs(i) > 0; i--) { // num - abs(i)는 한 줄에 있는 별의 개수 (1부터 올라가다 num을 찍고 1까지 내려옴)
		for (int j = 1; j <= num - abs(i); j++) {
			cout << '*';
		}
		cout << endl;
	}
	system("pause");
}

하나의 이중 for문으로 해결한 친구가 2명 있었는데요, 이 답안을 선택한 것은 주석이 잘 달아져 있기 때문입니다. 지난 3차시 피드백에서 주석을 어떻게 달아야 하는지 잠깐 언급했었습니다. 이 방법이 잘 지켜져있는 코드입니다.

이 문제를 abs를 사용하지 않고 2개의 이중 for문으로 푼 경우 주석이 필요하지는 않습니다. 코드 자체가 이해하기 쉽기 때문이죠. 하지만 모범 답안과 같이 복잡한 수학적인 식을 사용해서 하나의 이중 for문으로 푼 경우 주석을 달아주면 코드가 간결해진다는 장점을 얻으면서, 더 이해하기 어려워진다는 단점을 어느정도 극복할 수 있습니다.

4.3

아래는 모범 답안입니다.

#include <iostream>
using namespace std;

int main() {
	int num;
	cout << "자연수를 입력 해 주세요 : ";
	cin >> num;
	for (int i = num - 1; abs(i) + 1 <= num; i--) { // abs(i) + 1는 한 줄에 있는 별의 개수 (num에서 내려가다 1을 찍고 num까지 올라옴)
		for (int j = 1; j <= abs(i) + 1; j++) {
			cout << '*';
		}
		cout << endl;
	}
	system("pause");
}

방향성과 문제 의미 모두 4.2와 동일합니다. 따로 코멘트를 달지는 않겠습니다.

4.4

아래는 모범 답안입니다.

#include <iostream>
using namespace std;

float arr_avg(int arr[], int length) {
	int sum = 0;
	for (int i = 0; i < length; i++) {
		sum += arr[i];
	}
	return (float)sum / length;
}

int main() {
	int subject_arr[4];
	cout << "국어 점수를 입력 해 주세요 : ";
	cin >> subject_arr[0];
	cout << "수학 점수를 입력 해 주세요 : ";
	cin >> subject_arr[1];
	cout << "영어 점수를 입력 해 주세요 : ";
	cin >> subject_arr[2];
	cout << "과학 점수를 입력 해 주세요 : ";
	cin >> subject_arr[3];
	cout << "평균 점수는 : " << arr_avg(subject_arr, 4) << " 입니다." << endl;
	system("pause");
}

배열 요소들의 평균을 구하는 함수를 만들어보는 문제입니다.
평균을 만드는 과정에서 정수의 연산이 소수의 결과를 내야 하므로 (float)로 강제로 형변환을 해준 것을 확인할 수 있습니다.

이것만으로도 정말 좋은 답변이지만, 한 가지 더 좋은 방향을 덧붙여보겠습니다.
arr_avg라는 함수의 2번째 파라미터는 배열의 길이를 받고 있습니다. c++에서 함수에 배열을 넘기면 길이를 알 수 없다는 점 때문인데요. (이렇게 되는 이유는 5차시 피드백에서 말씀드리겠습니다.) 그래서 함수에 배열과 그 배열의 길이를 함께 넘기는 경우가 c++ 프로그래밍을 할 때 종종 있습니다.
그렇기 때문에 많은 수의 배열을 사용하는 경우, 각각 배열의 사이즈를 직접 선언부를 확인하지 않고 쉽게 알아낼 수 있다면 그만큼 생산성이 증가하겠죠.

이걸 도와주는 두 가지 방법이 있습니다.

첫번째 방법은, 상수화를 시키는 것입니다. 배열을 선언하기 이전에 배열 사이즈를 상수로 선언해서 배열 선언을 할 때와 배열을 함수로 넘길 때 사용합니다. 아래와 같은 방법이 있습니다.

#define NUM_SUBJECTS
int main() {
    int subjects[NUM_SUBJECTS];
    cout << average(subjects, NUM_SUBJECTS);
    return 0;
}

두 번째 방법은, sizeof 함수를 사용하는 것입니다. sizeof 함수는 해당 요소의 크기(byte)를 반환해주는 함수입니다. int의 크기가 4byte라고 할 때, 크기가 50인 int형 배열의 크기는 200이 되겠죠. 이걸 역산하면, 이 배열의 크기를 알기 위해서는 sizeof(배열)/sizeof(배열에 있는 하나의 요소) 을 하면 됩니다. 따라서 이름이 arr인 배열의 크기를 알기 위해서 sizeof(arr)/sizeof(arr[0])을 하면 됩니다. 아래와 같이 사용할 수 있습니다.

int main() {
    int subjects[4];
    cout << average(subjects, sizeof(subjects)/sizeof(subjects[0]));
    return 0;
}

이렇게만 보면, 후자의 방법을 이용해서 굳이 길이를 함께 넘기지 않고 그냥 함수 안에서 배열 사이즈를 구하면 되는거 아니야? 라고 생각할 수도 있겠습니다. 가령, 다음과 같이요.

float average(int arr[]) {
    int sum = 0;
    int length = sizeof(arr) / sizeof(arr[0]);
    for(int i=0; i<length; i++) {
        sum += arr[i];
    }
    return (float) sum / length;
}

int main() {
    int subject[4];
    cout << average(subjects);
    return 0;
}

가장 간단한 방법인 것 같죠? 하지만 정상적으로 작동하지 않는 코드입니다. 못 믿으시겠다면 직접 한 번 돌려보시기 바랍니다. 안 되는 이유를 정말 설명해주고 싶은데, 아직 안 배운 부분을 언급해야 하기 때문에.. 이것도 마찬가지로 5차시 피드백에서 설명드리겠습니다.
사실 이 부분은 c++의 정말 불편한 점 중 하나입니다. 여러분이 써보신 파이썬을 비롯해서 다른 고급 언어들은 함수 안에서 배열 사이즈 구하는게 정말 간단한데, c++에서는 파라미터로 따로 넘겨주는 등 다른 방법을 사용해야 하니까요.

이런 불편한 점은 사실 c언어에 있는 불편한 점이 그대로 계승된 것입니다. 여러분들이 지금 배우고 있는 문법은 대부분 c언어 문법입니다. 아직 제대로된 c++ 사용법은 시작도 안 했어요. c++은 c언어에서 더 많은 기능들이 추가된 언어이기 때문에, 다른 여러가지 방법들을 통해 이런 불편한 점들이 많이 보완되었습니다. 여러분들이 앞으로 c++을 계속 배우시면 이런 불편한 점들이 하나씩 개선될 것입니다. c언어 문법의 불편한 점들이 c++에서는 어떻게 개선되는지를 눈여겨 보신다면 좋은 공부가 될 겁니다.

4차시도 수고 많았습니다.

7차시 과제 피드백 (김범구, 김서영, 김수민, 김승찬, 김진호, 심규진, 윤창신)

7.1

아래는 모범 답안입니다.

#include <iostream>
#include <vector>

using namespace std;

int Sfunc(int(*fptr)(int, int)) {
	return fptr(2, 3);
}
int func1(int a, int b) {
	return a + b;
}
int func2(int a, int b) {
	return a * b;
}
int func3(int a, int b) {
	return a - b;
}

int main(void) {
	cout << "2, 3의 곱셈은? : " << Sfunc(func1) << endl;
	cout << "2, 3의 덧셈은? : " << Sfunc(func2) << endl;
	cout << "2, 3의 뺄셈은? : " << Sfunc(func3) << endl;
}

문제 자체는 정말 쉬웠는데, 한 가지 실수가 보입니다.
#include <vector>
이 코드에서 벡터를 사용하지 않았죠. 불필요한 헤더입니다.
불필요한 헤더를 불러오면 컴파일이 될 때 불필요한 코드가 작성이 됩니다.
당연히 성능이 더 안 좋아지겠죠.
실수라고 생각하기에는 답 제출한 모든 친구들이 이 실수를 범해서 잠깐 적어봤습니다.

함수 포인터는 제가 배울 때는 안 배웠는데 객체지향프로그래밍 개정되면서 추가됐다고 하네요.
함수 포인터를 이용하면 c++에서도 콜백 함수라는 것을 구현할 수 있습니다.
콜백 함수를 사용할 수 있다는 것 자체가 중요한 것은 아니고, 이 콜백 함수를 이용하면 비동기식 처리를 가능하게 할 수 있다는 것이 중요합니다.

기존에 여러분들이 코드를 작성하시면 A->B->C->D 이렇게 순차적으로 진행이 되면서 각 단계가 끝날때까지 기다렸다가 다음 단계를 실행하는 방식으로 프로그램이 돌아갔죠. 이 방법을 동기식 명령 처리라고 합니다. 반대로, A->B->C->D 순서는 유지하되 각 단계가 끝날때까지 기다리지 않고 바로 다음 단계를 실행시키는 것이 비동기식 명령 처리라고 합니다.
비동기식 명령 처리가 대부분의 경우에서 속도가 더 빠릅니다. 다만 큰 문제점이 있죠. 만약 C를 실행하기 위해서 A의 실행 결과가 필요하다면 어떨까요? 말씀드렸다시피 비동기 처리는 각 단계가 끝날때까지 기다리지 않기 때문에 C가 실행된 상황에서 A가 아직 끝나지 않았을 수 있습니다. 이러면 C에 제대로된 값이 들어가지 않아 문제가 발생할 수 있겠죠. (그래서 상대적으로 속도가 빠름에도 불구하고 항상 비동기처리가 좋은 방법은 아닙니다.)
이걸 해결해주는 것이 바로 콜백 함수입니다. A 단계를 실행할 때 "야 너 다 끝나면 C 실행해라~" 하고 말해주고, A->B->C->D의 단계를 A->B->D의 단계로 바꾸면 비동기 처리는 그대로 유지되면서 C는 무조건 A 다음에 실행되므로 위 문제점이 사라지겠죠. 여기서 "야 너 다 끝나면 C 실행해라~"라고 하는게 바로 콜백 함수입니다.

이걸 코드로 표현해보면 다음과 같아요.


동기 명령 처리

#include <iostream>
using namespace std;

int A() {
	cout << "A";
	return 2 + 3;    // a_result
}
void B() {
	cout << "B";
}
void C(int a_result) {
	cout << "C";
	cout << a_result;
}
void D() {
	cout << "D";
}

int main() {
	int a_result = A();
	B();
	C(a_result);
	D();

	return 0;
}

이렇게 그냥 실행을 해도 아무 문제가 없습니다. 무조건 A->B->C->D 순서로 진행될테니까요.
실행 결과는 ABC5D로 나옵니다.




비동기 명령 처리

#include <iostream>
#include <future>
using namespace std;

void A(void(*callback)(int)) {
	cout << "A";
	callback(2 + 3);    // call C
}
void B() {
	cout << "B";
}
void C(int a_result) {
	cout << "C";
	cout << a_result;
}
void D() {
	cout << "D";
}

int main() {
	// 아래는 그냥 비동기 함수를 실행하는 문법 정도로 생각하면 됩니다.
	// async(launch::async, function, parameter) 형태입니다.

	future<void> a = async(launch::async, A, C);
	future<void> b = async(launch::async, B);
	future<void> d = async(launch::async, D);

	return 0;
}

A가 끝난 후 C를 실행시키기 위해 함수 포인터를 이용해 콜백 함수를 구현했습니다.
실행 결과는 실행시마다 제각각입니다. ABC5D로 실행되기도, ABDC5로 실행되기도, 심지어는 ACBD5처럼 C랑 5가 따로 놀 수도 있어요.

비동기 처리를 객체지향 프로그래밍에서 직접 사용할지는 모르겠지만, 함수 포인터라는 것의 의미는 조금이나마 이해하셨으리라 생각합니다.
아마 자바스크립트 프로그래밍을 하게 된다면 자주 접하는 방식의 코딩 스타일이 될겁니다.

7.2

아래는 모범 답안입니다.
사실 다들 풀이가 비슷해서 별로 의미는 없을 것 같아요.

#include <iostream>
#include <vector>

using namespace std;

int main(void) {
	int a, b, c;		//입력받는 세 정수
	int number;			//입력받은 세 정수의 곱
	int digit;			//반복문의 각 실행 회차마다 얻는 자릿수의 숫자

	vector<int> vec_num(10);

	cout << "숫자 3개를 입력해주세요 : ";
	cin >> a >> b >> c;
	
	number = a * b *c;

	while (number > 0) {
		digit = number % 10;		//10으로 나눈 나머지
		vec_num[digit] += 1;		//얻은 나머지를 index로 하여 1씩 추가
		number /= 10;				//나머지를 얻은 뒤 한 자리씩 줄여 나감
	}

	
	cout << endl << "답은 (0, 1, 2, 3, 4, 5, 6, 7, 8, 9) 순으로 : ";
	for (int i = 0; i < 10; i++) {
		cout << vec_num[i] << ' ';
	}
	cout << endl << endl;
}

문제에서 벡터를 사용하라고 하지 않았으면 벡터가 굳이 쓰일 필요는 없어 보입니다.
그냥 각 자리를 더하는 방법, 10으로 나누고 10으로 나눈 나머지를 이용하는 방법을 알고 있다면 쉽게 풀었을 것 같아요.

그래서 문제에 대한 설명보다는 이 답변을 제출한 친구가 처음에 겪었던 시행 착오에 대해서, 어떤 문제가 있었고 어떻게 해결하면 좋을지 설명드릴게요.

아래 코드는 이 친구가 처음으로 작성했던 코드입니다.

#include <iostream>
#include <vector>

using namespace std;

int main(void) {
	int a, b, c;				//입력받는 세 정수
	int temp_int;				//int형 정수의 곱
	double digit;				//for문의 각 실행 회차에서 얻는 temp_int의 숫자
	double temp_double;			//double형 정수의 곱
	vector<int> vec_num(10);	//index 0~9 벡터 공간에 1씩 더하는 방식으로

	cout << "숫자 3개를 입력해주세요 : ";
	cin >> a >> b >> c;
	temp_int = a * b * c;
	temp_double = (double)temp_int;
	cout << temp_int << endl;
		
	for (int i = 0; temp_int > 0; i++) {			//10씩 계속 나누면 정수형 숫자는 0이 되므로, 그 전까지 실행
		temp_double /= 10;							
		temp_int /= 10;
		digit = temp_double - (double)temp_int;		//실수형은 10으로 나누면 소숫점이 남고, 정수형은 남지 않으므로 소수부만 남길 수 있음

		cout << digit << '\t';				
		digit *= (double)10;							
		
		cout << digit<<'\t'<< (int)digit << endl;
		vec_num[(int)digit] += 1;					//각 숫자의 벡터공간에 1씩 더함
	}
	
	cout << endl << "답은 (0, 1, 2, 3, 4, 5, 6, 7, 8, 9) 순으로 : ";
	for (int i = 0; i < 10; i++) {
		cout << vec_num[i] << ' ';
	}
	cout << endl;
}

double을 int로 형변환을 하면 소수 부분이 사라진다는 특징을 이용해 문제를 풀었습니다. 출력 부분은 아마 디버깅을 하기 위해서 넣어둔 것 같구요. 실행 결과는 아래와 같습니다.

숫자 3개를 입력해주세요 : 150 266 427
17037300
0 0 0
0 0 0
0.3 3 2
0.73 7.3 7
0.373 3.73 3
0.0373 0.373 0
0.70373 7.0373 7
0.170373 1.70373 1

답은 (0, 1, 2, 3, 4, 5, 6, 7, 8, 9) 순으로 : 3 1 1 1 0 0 0 2 0 0

음.. 이상한 부분이 보이죠? 5번째 줄에 보면 double 타입의 3을 int로 형변환을 했더니 2가 되어버렸습니다.

컴퓨터에서 소수를 표시할 때, 지수 부분과 정수 부분을 나누어서 1.6*10^(-7) 이런 식으로 표시합니다. (=부동소수점 표현 방식.)
0.5, 0.25같이 2의 음의 제곱같은 소수를 표현하는 것은 컴퓨터가 정말 잘 하지만 (2진법을 사용하므로)
0.3과 같이 2진법으로는 표현할 수 없는 소수는 컴퓨터가 그 수에 최대한 근사해서 표현합니다.
궁금하면 printf("%.24f", 0.3); 을 해보세요. 저는 0.299999999999999988897770 로 나오네요.

이렇게 컴퓨터가 최대한 근사해서 표현했다고 해도, 어느정도 복잡한 수가 있거나 특정 연산을 할 때 꽤나 큰 오차가 날 수도 있어요. 이런 걸 부동 소수점 오류라고 합니다.

위 경우에서 17037.3을 double로 표현하면 17037.299999999999272404238582 으로 나오는데, 이 값에 10을 곱하고 int로 형변환을 해버리면 170372.99999999999272404238582 를 int로 형변환을 하는 것이 되어 1의 자리가 3이 아닌 2가 나오게 됩니다. 출력 값에는 사실 어느정도 보정이 들어가서 (double)3이라고 나오겠지만 이는 사실 (double)2.9999999999999 였던거죠. 그래서 형변환을 했을 때 2가 나오게 된 것이구요.

이걸 보정해주기 위해서는 적당히 작은 값인 epsilon을 사용합니다.
epsilon은 소수를 표현하는 자료형마다 하나씩 있어요. float 타입의 FLT_EPSILON, double 타입의 DBL_EPSILON, 잘 사용하지는 않지만 long double 타입의 LDBL_EPSILON
이 epsilon 들은 0과 정말 가까운 값이기 때문에 더하거나 빼도 실제 소수 값에 거의 변화를 주지 않아요.
하지만 2.99999999999999 같은 수를 3.000000000000000으로 보정해주는데는 충분히 유효한 값입니다.

그래서 뭐 결론은 형변환을 해주기 전에 이 epsilon을 더한 후 형변환을 해주면 된다.. 이거에요.
하지만 double이 float보다 더 표현할 수 있는 범위가 넓다보니 DBL_EPSILON은 너무나도 0에 가까워서 DBL_EPSILON을 더해도 보정이 되지 않는 경우가 종종 있어요. 이 경우도 그렇더라구요.
그래서 FLT_EPSILON을 사용해서 보정해주는 것을 추천합니다.

이 답을 제출한 친구에게는 더 자세한 이야기를 해줬는데 여기에 다 적기에는 너무 길어질 것 같아서 링크 하나 남기고 마치겠습니다.
https://www.acmicpc.net/blog/view/37
자세하게 설명이 되어 있지는 않지만 이 정도 수준으로 이해하면 코딩할 때 문제는 없을 것 같습니다.

7주차도 수고 많았습니다.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.