음악, 삶, 개발

4. Computation (계산) 본문

개발 공부/Principles And Practice Using C++

4. Computation (계산)

Lee_____ 2020. 7. 14. 07:27

이 챕터에서는 expression, selection, iteration, function, vector 를 배운다.

본격적으로, 프로그램안에서 연산을 하고, 함수를 만드는것에 관한 이야기.

프로그래밍에 대한 정신자세등도 담겨있으니 넘기지말고 읽을것.


< 목차 >

 

4.1 Computation

4.2 Objectives and tools

4.3 Expressions

4.3.1 Constant expressions

4.3.2 Operators

4.3.3 Conversions

4.4 Statements

4.4.1 Selection

4.4.1.1 if-statements

4.4.1.2 switch-statements

4.4.1.3 switch technicalities

4.4.2 Iteration

4.4.2.1 whilte-statements

4.4.2.2 Blocks

4.4.2.3 for-statements

4.5 Functions

4.5.1 Why bother with functions?

4.5.2 Function declarations

4.6 vector

4.6.1 Traversing a vector

4.6.2 Growing a vector

4.6.3 A numeric example

4.6.4 A text example

4.7 Language features


4.1 Computation

사실 결국 프로그램을 만든다는것은 일종에 계산기를 만드는것이다.

프로그램은 어찌됐든 input 을 받고 output 을 만든다. 

내가 만들었던 코미디스트도 결국 계산기였다.

 

input 과 output 의 그림

Input 이 될수있는것들을 한번 나열해보자.

 

  • 키보드
  • 마우스
  • 터치 스크린
  • 미디 건반
  • 비디오 레코더
  • 네트워크 연결
  • 온도 센서
  • 디지털 카메라 이미지 센서

셀수없다.

 

이러한 Input 을 에 반응하기위해서, 프로그램은 보통 data, data 구조, 혹은 data 의 상태등을 가진다.

예를 들어, 달력 프로그램을 생각해보라. 

달력 프로그램은 아마 data 로 내부적으로 기념일, 공휴일등의 정보를 가지고있을것이다.

어떤 데이터는 이미 프로그램의 시작부터 생성되어있을것이고,

어떤 데이터는 사용자 input 을 읽고, 그로부터 데이터를 생성하고 수집할것이다. 

달력 프로그램은 사용자가 주는 input 에 따라 기념일, 약속등을 설정할것이다.

달력 프로그램에서 input 은, 마우스를 클릭하여 달과 날짜를 선택하고, 키보드로 정보를 입력하면

output 은 달력에 약속, 기념일등을 다시 보여주는것이다. 

Input 은 매우 다양한 소스가 될수있고, 유사하게 output 도 굉장히 넓은 방향으로 다양하게 나아갈수있다.

output 은 컴퓨터 screen, 파일 , 네트워크 연결, 다른 기기, 다른 프로그램 등이 될수있다.

Daw 에서 음악을 만들때 생기는 복잡한 input, output 을 역시 떠올려보라..
아니면 Max patching 을 떠올려보라.

프로그래밍 관점에서, 결국 이 책은 끝까지

"어떻게 프로그램을 협력하는 파트의 집합으로 표현하고, 그것들이 데이터를 교환하는지?"

에 관한 이야기일것이다.

Input 과 output 을 시각적으로 표현해보면 아래와 같다.

 

프로그램의 input 과 output
나의 3년전 Max 패치...

output 은 결국 누군가의 input 이 되고 다시 output 이 되고...etc..

보통 우리가 input, output 을 이야기하면 computer 한대의 in, out 을 떠올리지만

프로그램 자체가 생산한 수많은 정보들이 input 이 될수있다.

프로그램안에서의 input 을 우리는 종종 argument 라고 부른다.

프로그램안에서의 output 을 result 라고 한다.


4.2 Objectives and tools

프로그래머로써 항상 마음속에 기억해야하는 3가지 원칙.

 

  • 정확성 (correctness)
  • 단순함 (simplicity) : 지져분한 코드를 피해야한다.
  • 효율성 (efficiency)

따라서 코딩을 할때 나의 프로그램은 언제나 수정될것이고, 업데이트 될것이라는것을 염두해야한다.

우리가 프로 프로그래머가 되기위해 마음먹은이상 이것들에 대한 피할수없는 책임감을 받아들어야한다.

위의 3가지를 어길시, 추후 새로운 버전을 내기위해 처음부터 코드를 다시 써야하게될것이다.

그리고 대부분 코드의 질, 코드의 구조를 아주 잘 고려한후 작성하는것이 가장 지름길이기도 하다.

잘 만든 프로그램 구조는 실수를 최소화하고, error 를 찾기위한 시간낭비를 줄여줄것이다. 

아주 잘 짜여진 프로그래밍을 하는 주요한 방법은 

큰 것을 매우 작은 것들로 쪼개는 것이다. (Break up a big computation into many little ones)

이 테크닉은 2가지 방식을 가진다.

 

  • Abstraction : 추상화 (디테일을 숨기고, 편하고 일반적인 interface 화함 
  • Divde and conquer : 거대한 문제를 세분화된 작은 문제들로 놔눠서 해결한다.

위와 같이 하는 이유는, 인간은 매우 큰 문제를 해결하는데 아주 약하기 떄문이다.

우리는 큰 문제를 계속해서 할수있는데까지 작은 문제로 쪼개야 한다.

우리가 충분히 이해하고 해결할수있는 사소한 문제가 될때까지.

만약 너의 코드가 10,000,000 줄이라면 AbstractionDivde and conquer

더 이상 option 이 아닌 필수가 된다.

우리가 "어떻게 하면 잘 코딩을 하지?" 를 상상하는것만으로는 전혀 충분하지않고,

좋은 도구를 찾아야하는데, 그것은 Library 다.

C++ 는 iostream 과 같은 라이브러리를 가지고있으니, 이를 잘 공부하여 실전에

적극적으로 활용해야한다. (직접 다 만들려고하지말고)

이 sub 챕터에서, 저자는 계속해서 좋은 코드 구조에 대해 강조하고있다.

코드를 작성할려고 달려들지말고, 좋은 구조를 충분히 떠올린후 시작하는게 옳다는 의미이다.


4.3 Expressions 

프로그램의 가장 기본 단위는 expression (식) 이다. 

expression 이 무엇인가? 그냥 아래것들과 같은 것이다.

int length = 20; // 리터럴 
length = 99 // assign 99 to length

4.3.1 Constant expressions (상수식)

constant expression (constexpr) 이란, compile-time 에 값을 알수있을때 사용하는 표현식이다.

절대 변하지않을 값을 표현할때 constexpr 또는 const 를 사용하도록 한다.

이 constant를 바꿀려고 할시 에러가 발생하므로,

우리가 실수로 값을 수정할 원인을 애초에  차단할수있다. 코드의 가독성에도 도움이 된다.

constexpr double pi = 3.1459;

pi = 7; // error : assignment to constant

double c = 2 * pi ; // OK : 우리는 pi 값을 읽을 뿐이지, 바꿀려고 시도하고있지않음.

 

또한 코드내에서 literal 의 사용을 무조건 피하자.

대신 constant에 이름을 부여하여 대입하도록 한다.

아래 예를 통해 그 이유를 알수있다.

/* 나쁜 예 */

int main() {

	 // 여기서 3.14 를 magic constant 라고 부른다.
     // 우리가 10년뒤, 이 코드를 봤을때 3.14의 의미를 알수있을까?
	std::cout << 3.14 << std::endl ;

}

/* 올바른 예 */

constexpr double pi = 3.14;

int main() {

	// 3.14 대신 pi 로 표기됨으로써 추후 pi 라는 의미를 알수있다.
	std::cout << pi << std::endl;

}

참고로 코드상에서 constant expression 을 무조건 해야하는 구간이 있다.

예를 들어, switch 문안에 case 안에는 constant expression 이 들어가야한다.

무엇이 constant expression 인지 아닌지 예를 통해 확인해보자.

constexpr int max = 17;  // literal은 constant expression 이다.
int val = 19; 

max + 2 // 이것또한 constant expression 이다. 왜냐면 숫자 2 는 리터럴, 즉 constant expression. 
val +2 // constant expression 이 아니다. 변수를 사용하고있기때문.

정리해보면 

  • constexpr + constexpr 은 constant expression
  • literal 은 constant expression (magic constant 라고도하며 사용을 피해야함. 그냥 하지말자..)
  • constexpr + literal  은 constant expression
  • variable + literal 은 non-constant expression
  • const 는 const expression 이 아니다!

constexpr vs const 

constexpr 의 값은 compile-time 에 알수있는값 을 표현할떄 사용한다.
const 는 값을 compile-time 에는 알수없지만, 코드상에서 초기화후에 절대 변하지않을값을 표현할때 사용한다. 

아래의 예를 보도록 한다.

constexpr int max = 100;

void use(int n) {
	
    	constexpr int c1 = max + 7 // OK  : c1 은 107 이라는것을 compile-time 에 알수있다.
        constexpr int c2 = n + 7 // error : n 의 값이 무엇이 될지 compile-time 에서 알수없다. 따라서 c2 를 알수없다.
        const int c3 = n + 7 / /OK 
        c3 = 7; // error : c3 is const
        
}

4.3.2 Operators (연산자)

아래의 연산자 표를 아닥하고 일단 외우도록 하자.

Operators

혹시나 하는 마음에 적지만,

a < b < c  식의 조건식은 절대 적지말자.

 

1을 추가해주는 increment operator 는 3가지 방법이 있다.

++a;         // 가장 좋은 방법!
a += 1;
a = a + 1;

모두 동일하지만, 가독성(readability) 과 정확성 (correctness) 를  위해 ++a 를 사용하도록 한다. 

 

만약 1이 아닌 다른 숫자를 사용한다면, 2가지 선택지가 있다.

a += 2;     // 이걸 사용한다!
a = a + 2;  

a+=2 를 사용하도록 한다. (a *=scale)


4.3.3 Conversions (변환)

double 과 int 가 혼용되어 연산될때에 관한 이야기이다.

아래의 코드를 통해 발생할수있는 다양한 결과를 배우도록 한다.

/*  int 와 double 연산 */

double d = 2.5;
int i = 2;

double dResult = d / i; // 1.25 가 된다.
int iResult = d/ i; // 소숫점이 사라지고 1 이 된다. (narrowing 발생)
int iResult2 { d / i; } // error : double 이 narrowing 된다고 에러를 발생시켜준다. uniform 사용이 가장 안전.

/* double 연산시 주의 사항 */

double d = 3 / 2; // 1.5 가 될것같지만 1이 출력된다. int 끼리의 연산이 일어나고 d 로 대입되기때문.
double d2 = 3.0 / 2; // 1.5 가 출력된다. double 과 int 의 연산은 double 이 되므로.

정리하면, lvalue 가 double 이니까 rvalue 도 알아서 double 로 연산되겠지..라고 생각하면 안된다.

int 와 int 의 연산이 일어나면 int가 된다.

double 과 int 의 연산은 double 이 된다.  

rvalue 에서 이런 모든 연산을 마치고 결과를 lvalue 로 넣어주기때문이다.

{ } 을 사용하여 uniform 초기화를 하면 error 를 뿜어주기때문에 가장 안전한 방식이다.


4.4 Statements (문)

생략. 


4.4.1 Selection

if 문과 swtich문을 사용한다.

아래 챕터부터 자세히 설명.


4.4.1.1 if-statements (if문)

if문은 여러가지의 대안 사이에서 무언가를 선택하는것이다.

if (unit == 'i') ...              // 1st 대안
else if (unit == 'c') ...   //  2nd 대안
else ...                                //  3rd 대안

if문을 통해서 우리는 항상 사용자로부터의 bad input 을 대비해야한다.


4.4.1.2 switch-statements (switch문)

많은 constants 를 비교할때는 switch 문을 사용해야한다.

언제나 default 는 설정하는것이 좋은 습관이다.

case 다음에 오는 값을 label 이라고 한다.

switch (unit) {
	
 case 'i' : /* do something */ break;
 case 'c' : /* do something */ break;
 case 'a' : /* do something */ break;
 default : /* do default */ break;
    
}

4.4.1.3 switch technicalities

switch문의 기술적인 detail 에 대해 알아본다.

 

  1.  swtich 다음 () 속에는 int, char, enum 타입만 들어갈수있다. (특히, string 은 불가)
  2.  case label 은 반드시 constant expressions (compile-time 에 알수있는값) 이여야한다. (변수 사용 불가)
  3.  case lable 은 서로 다른 값이어야한다.
  4.  single case 를 위해, 여러개의 case label 을 사용할수있다. 
  5.  각 case 의 끝은 항상 break 으로 끝나야한다. break 이 빠져도, compiler 경고해주지않는다.

예제 코드를 보자.

/*  1. swtich 다음 () 속에는 int, char, enum 타입만 들어갈수있다. (특히, string 은 불가) 
		string 을 조건문에 쓸려면, if문 혹은 map (추후 배움) 을 써야한다.
*/

string str;

// error : string 을 switch 안에 넣을수없다!
switch (str) {
	
    case "no" : ... break;
    case "yes" : ... break;
    
}

/*  2. case label 은 반드시 constant expressions (compile-time 에 알수있는값) 이여야한다. (변수 사용 불가)
	  3. case lable 은 서로 다른 값이어야한다.

*/

char a;
std::cin >> a;
constexpr char n = 'n';
int y = 'y';

switch (a) {

	case n : ... break; // ok : n 은 constexpr
	case y : ... break; // error : variable in case lable!
	case 'n' : ... break; // error : 'n' 은 char n 의 값과 같으므로 중복.

}

/* 4. single case 를 위해, 여러개의 case label 을 사용할수있다.  */

char a;
std::cin >> a;

swtich (a) {

	case '0' : case '2' : case '4' : case '6' : case '8' : ... break;
	case '1' : case '3' : case '5' : case '7' : case '9' : ... break;
    default: ... break;
    
}


4.4.2 Iteration (반복)

우리는 거의 무언가를 한번만 하지않는다. 따라서, 프로그래밍 언어는 무언가를 여러번 반복할수있는 도구를 제공한다.

repetition, 특히 데이터구조의 연속적인 자료로 반복을 하면 iteration 이라고 한다.


4.4.2.1 whilte-statements (while문)

while문의 () 안에 들어가는 loop variable 은, 

반드시 while문의 바깥에서, 앞에서 정의되고 초기화되어야한다.

int i = 0; // loop variable 은 while loop 의 바깥에서, 앞에서 반드시 정의되고 초기화되어야한다.

while ( i < 100) {

	std::cout << i << std::endl;
	
    ++i; 
	
}

4.4.2.2 Blocks

{ } 안에 문장의 나열을 block 또는 compound statement 라고 한다.

가끔은 { } 안에 아무것도 적지않아, empty block 을 만드는데 때론 유용하다.

 

예제 

if (a <= b) {

	// do nothing 

}

else {
	
    int t = a;

}

4.4.2.3 for-statements (for문)

for문이 while문보다 가독성이 높다.

따라서 대부분의 경우 for문을 사용하고, for문으로 가능하지않은 logic 인 경우에만 while 을 쓰도록 한다.

for 문안에 loop variable 은 절대 수정하지않는다. (당연한 소리)

뒤에 나올 vector 와 함께 자주 쓰이는 range-for-loop 에 대해서는 추후 다루겠다.


4.5 Functions

type indentifier ( parameter-list)  { function-body

 

아무것도 return 하지않고자할때는 void 를 사용한다.

Chapter 8 에서 기술적인 면을 훨씬 자세하게 다룰것이다. (내용이 아주 많음)


4.5.1 Why bother with functions?

함수를 작성하는 이유는 아래와 같다.

  • 계산을 이론적으로 분리 
  • 코드 가독성 향상 
  • 재사용성 
  • 테스트를 수월하게해줌.

함수를 작성할때 중요한 원칙은, 2가지 기능을 하는 함수를 작성하지않는다. 

예를 들어 caculateAndPrint 같은 함수라면, caculate 과 print 는 분리되어야한다.

한 함수는 한가지 기능만을 해야한다.


4.5.2 Function declarations

함수의 선언 (declaration) 과 정의 (definition) 은 분리하도록 한다.

예를 보자.

// 선언 (declaration)
int square(int x);

// 정의 (definition)
int square(int x) {
	
    return x*x;

}

보통 선언문은 따로 파일을 만들어서 #include 하는것이 일반적이다.

정의문은 어디든 와도 상관없는데, Chapter 8 에서 이또한 다룰것이다.

선언과 정의의 분리는 거대한 프로그램을 만들때 매우 필수적이다.

함수의 정의를 보는것은 우리를 집중할수없게 만들기때문이다.


4.6 vector 

vector 는 c++ 의 container 이다.

vector 는 스스로의 element 를 저장할뿐만 아니라, 자신의 size 도 저장한다.

 

vector 를 만드는 방법은, 간단하다. vector<타입명> 이름 이다.

코드로 보자.

vector<int> v = {5, 7, 9, 4, 6, 8};
vector<string> philosopher = { "Kant", "Plato", "Hume", "Kierkegaard"};

// vector 는 오직 선언된 type 만 받아들인다.

philosopher[2] = 99; // error : trying to assign an int to a string
v[2] = "Hume"; // error : trying to assign a string to an int

 

위와 같이, { } 안에 element 를 넣어 초기화 하지않고도, size 를 지정하여 만들수도있다.

vector<타입명> 이름 (사이즈

vector<int> v1(6); // 각 element 는 0 으로 초기화된다.
vector<string> v2(4); // 각 element 는 " "  빈 string 으로 초기화된다.

// 단 이렇게, vector 를 생성할시 주의사항이 있다.
v1[20000] = 44; // run-time error!!

위와 같이, vector 안에서 존재하지않는 element 를 우리는 참조할수없다.

Chapter 5 에서 vector 사용시 발생할수있는 run-time error 에 대해 다시 다룰것이다.


4.6.1 Traversing a vector

vector 는 스스로의 size 를 알고있다. 이 size 는 백터이름.size() 함수로 접근한다.

이 size() 함수를 이용해 아래와 같은 코드를 작성해볼수있다.

vector<int> v = { 5, 7, 9, 4, 6, 8};

for (int i = 0; i < v.size(); ++i) {

	std::cout << v[i] << std::endl;

}

size() 가 0 이면 아무것도 없는 empty vector 를 의미한다.

첫번째 element 는 v[0] 이고, 마지막 element 는 v[v.size() - 1] 이다.

 

Range-For-Loop

 

range-for-loop 을 이용해 아래와 같이 코드를 작성할수도있다.

vector<int> v = { 5, 7, 9, 4, 6, 8};

for (int each : v) {

	std::cout << each << std::endl;

}

 

loop 중 우리가 index 를 알아야할때는 (3번쨰 element 를 오직 고른다던가)

그냥 for loop 를, 그외에는 range-for-loop 를 사용한다.


4.6.2 Growing a vector

우리는 빈 vector 로 시작해서, 우리가 원할때마다 data 를 추가해나가고싶을때가 많다.

이때 사용하는 함수가 push_back() 이다.

push_back() : 새 element 를 vector 의 마지막 element 로 추가함.

아래 그림을 보자.

 

push_back() 을 통해 vector 에 새 element 를 추가한다.

C 를 공부했던 사람이라면, C 의 array 를 생각할것이다.

하지만 vector 의 큰 차이점은, 아래와같다.

  • 미리 vector 의 size 를 정할 필요가 없다.
  • 원하는만큼 element 를 추가할수있다.

4.6.3 A numeric example

 

생략.


4.6.4 A text example

 

생략.


4.7 Language features

 

생략.


Review

  1.  What is a computation?
  2.  What do we mean by inputs and outputs to a computation? Give examples. 
  3.  What are the three requirements a programmer should keep in mind when expressing computations?
  4.  What does an expression do?
  5.  What is the difference between a statement and an expression, as described in this chapter?
  6.  What is an lvalue? List the operators that require an lvalue. 
  7.  What is a constant expression?
  8.  What is a literal?
  9.  What is a symbolic constant and why do we use them?
  10.  What is a magic constant? Give examples.
  11.  What are some operators that we can use for integers and floating-point values?
  12.  What operators can be used on integers but not on floating-point numbers?
  13.  What are some operatros that can be used for strings?
  14.  When would a programmer prefer a switch-statement to an in-statement?
  15.  What are some common problems with switch-statements?
  16.  What is the function of each part of the header line in a for-loop, and in what sequence are they executed?
  17.  When should the for-loop be used and when should the while-loop be used?
  18.  How do you print the numeric value of a char?
  19.  Describe what the line char foo(int x) means in a function definition.
  20.  When should you define a seperate function for part of a program? List reasons.
  21.  What can you do to an int that you cannot do to a string?
  22.  What can you do to a string that you cannot do to an int?
  23.  What is the index of the third element of a vector?
  24.  How do you write a for-loop that prints every element of a vector?
  25.  What does vector<char> alphabet(26); do?
  26.  Describe what push_back() does to a vector.
  27.  What do vector's member functions begin(), end(), size() do?
  28.  What makes vector so popular/useful?
  29.  How do you sort the elements of a vector?

Terms

  • abstraction
  • begin()
  • computation
  • conditional statement
  • declaration
  • definition
  • divide and conquer
  • else
  • end()
  • expression
  • for-statement
  • range-for-statement
  • function
  • if-statement
  • increment
  • input
  • iteration
  • loop
  • lvalue
  • member function
  • ouput
  • push_back()
  • repetition
  • rvalue
  • selection
  • size()
  • sort()
  • statement
  • switch-statement
  • vector
  • while-statement