Chapter 7. Functions: C++'s Programming Modules

지환·2022년 6월 20일
0
C 함수랑 비슷하긴하지만, 오개념 잡혀있을 수 있으니 잘 보고,,
C 함수에서 더 추가된 기능들은 주로 Ch 8에서 다룬다고 함.

Function

아래와 같이 함수 사용.

  1. 함수 definition
  2. 함수 declaration
  3. 함수 call
정의할때 argument type이 같더라도 일반 변수 선언하듯 할 수 없다.
다 따로 해줘야됨. func1(int a, int b){~}

Return

return type과 type이 다르다면 맞게 변환된다.
return statement가 여러개라면 제일 처음 만나는 놈 다음 함수 종료된다.

array 제외하고 아무거나 다 return 가능
(웃긴건 array도 structure나 object에 들어있으면 가능함)

함수가 반환될때 그 값은 특정 CPU register나 memory location에 copy된다.
그리고 calling function이 그 location을 확인하는 원리이다.
(어셈블리어할때 배운대로네. 레지스터에 넣거나 SS 특정위치에 넣어두거나)

calling function과 called function은 반환 type을 약속해야함.
function prototye이 calling f에 예상 type을 알려주고,
function definition이 called f에 어떤 type을 반환해야하는지 알려준다.
두 정보가 일치

Prototype

prototype을 통해 compiler는 해당 function이 어떤 return type인지, 어떤 argument를 갖는지 정보를 얻을 수 있다.
(정보를 모르는 상태에서 예측하는건 좋지 않음)

  1. argument 정보를 미리 알고, function call에서 오류를 잡는다. 개수맞는지..등
    type만 좀 다르면 그에 맞게 변환해준다.

  2. return type 정보를 미리 알고, specified location에서 return 값을 읽어온다.
    return type을 알고 있으므로, compiler가 몇 byte를 읽어올지나 어떻게 해석할지를 알 수 있다.

    정보 미리 알려주는거면 그냥 file 뒷부분 뒤져서 보면 되지않나?
    굳이 prototype을??
    : 그걸 다 뒤지기 쉽지 않을 수도 있고, 다른 파일에 definition 있을 수도 있다.
    
    prototype 안쓰는 유일한 방법은 함수 definition을 main 이전에 적는건데,
    알겠지만 이게 함수끼리 서로 호출하면 prototype 쓸 수 밖에 없음.

Prototype Syntax

function prototype은 statement이다. -> semicolon으로 끝나야함.

argument는 type만 명시해줘도 OK
name 적더라도 definition의 argument variable name과 일치할 필요 없다.

C++에서 prototype은 의무 (C99도 그렇긴 함, 호출전에 prototype이나 definition 나와야됨.)

C에서 prototype에 argument list 안적는건
그냥 아무 정보도 주지않는 것이지만
C++에선 argument가 없단 뜻이다.

C++에서도 이런 뜻으로 쓰려면 void say(...); 식으로 ellipsis를 이용하면 된다.
(C에선 ellipsis가 variable argument list에서만 씀)

prototyping은 compile time 중에 처리되는데, 이를 static type checking 이라 한다.
(runtime 중에 잡기 어려운 많은 error를 잡아줌)

Passing by Value

C++은 보통 passes by value
(Ch5에서 말했던 reference type 같은 경우를 빼고 대부분 기본은 pass by value라는 것 같다.)

called function에서 넘겨받는 변수를 formal argument(formal parameter) 라 하고,
calling function에서 실제로 넘겨주는 변수를 actual argument(actual parameter) 라 한다.
C에서처럼 간단하게 위 순서대로 parameter, argument라고 하기도 함.

Tip.
숫자 계산할때 곱하기보다 나누기를 더 많이 배치하면
결과는 같아도 중간값들이 작아져서 overflow 가능성 low
ex) (10*9) / (2*1) == (10/2) * (9/1)

Functions and Arrays

  • int sum_arr(int arr[], int n);
  • int sum_arr(int * arr, int n);

arr은 pointer이고(parameter에선(그리고 paremeter에서만) 구분 안됨), 당연히 배열이름처럼 그냥 쓰일 수도 있다.
배열 길이는 따로 받아야한다. arr에 sizeof 연산해봤자 arr은 int* type이라서 int크기만큼만 나온다.

배열자체가 복사되면 공간/시간 낭비 될 수 있어서 차라리 포인터로 넘겨받는게 good

const 이용해서 값 보호
일반 변수는 value만 복사되므로 알아서 보호가 된다.
하지만 포인터나 배열이름이 parameter일땐, 이를 보호하기위해 const를 써야한다.(수정안할거라면)

원래 변수가 const일 필요는 없고, parameter가 그렇단 것이다.
그렇게 하면 그 함수 안에선 해당 변수의 값이 read-only이므로 값 수정으로부터 보호된다.

OOP로 가는 첫걸음

data가 어떻게 저장되고, 그 data에 어떤 operation을 할지,,
이렇게 storage properties과 operation을 합쳐서 생각하는 것이 OOP mindset의 첫걸음

data+operation을 정의 -> OOP
(예시 p.325~)

이렇게 data부터 정의해서 프로그램을 만들어나가는 bottom-up programming 방식을 OOP에서 잘 지원한다.
예전 procedural programming은 top-down programming이다.
(둘 다 유용함)

Functions Using Array Ranges

array 다루는 함수를 만들려면 (1)시작주소 (2)type크기 (3)개수 를 받아야한다.

그래서 기존 C/C++ 방법은 '시작주소 pointer'+'배열 크기' 를 인자로 받는 것이었다.
다른 방법은 range를 이용하는 것인데, '시작주소 pointer'와 '끝주소 pointer'를 받는 것이다.
(STL에서 이런 식으로 구현하는데, '마지막주소+1'을 이용한다.. 고 하네)

for (int i = p; p < q; p++) {
	sum += *p;
}
pointer 연산은 그냥 C에서의 규칙 그대로 적용되는 것 같다.
포인터끼리 빼기하면 배열 중간 개수만큼 나온다고 하네

Pointers and const

(1) const object를 가리키는 pointer

int age = 21;
const int * pt = &age;

pt가 const int를 가리키는 꼴이된다. 그래서 pt는 본인이 가리키는 곳의 값을 변경할 수 없다.
위처럼 age 자체가 const일 필요는 없다. pt가 관여하면 변경될 수 없단 소리일 뿐이다.
age = 20; : VALID
*pt = 20; : INVALID

남은 두가지 경우)

// (1)const를 cosnt에 : VALID
const float earth_g = 9.8;
const float * pe = &earth_g;
//이러면 당연히 일반 변수로 접근해도 값 변경 불가

// (2)const를 일반 포인터에 : INVALID
const float moon_g = 1.63;
float * pm = &moon_g;

후자는 왜 안되나?
: 그냥 간단하게 치팅이라서 막아둔 것
정말 하고싶으면 `const_cast` operator 이용해서 assign 할 수 있다.

포인터끼리는?

int * pt = &age;
const int * pd = pt;

위처럼 const pointer에 non-const pointer를 assign하는 경우
: indirection level이 1인 경우만 VALID
(const pointer를 non-const pointer에 assign하는건 당연히 안된다.)

아래의 경우 때문에 indirection level 1인 경우만 허용하다.

const int **pp2;
int *p1;
const int n = 13;
pp2 = &p1;  //INVALID. 근데 가능하다고 해보자, 왜 안되는지 알 수 있음
*pp2 = &n;  //VALID. 둘다 const임. p1이 n을 가리키도록 한다.
*p1 = 10;   //VALID. const인 n을 바꾼다.
//pp2를 이용하지 않았다면 이렇게 non-const pointer를 이용해서 const 변수 수정 불가
//이 부분이 문제되므로 indirection level 1인 경우만 허용

정리
const data이든 non-const data이든 얘네의 address는 pointer-to-const에 assign 할 수 있다.
단, non-const data라면 해당 data type이 pointer이면 안된다.(indirection level 1만 가능하단 뜻)
non-const pointer에는 non-const data의 address만 assign 할 수 있다.

함수가 int sum(int arr[], int n); 이런 식으로 선언돼있으면, const로 선언된 배열은 넘겨줄 수 없다.(const pointer를 non-const pointer에 assign하는거니까)

그래서 함수의 "배열 parameter"를 처음부터 const로 해두기도 한다.
그럼 non-const든 const든 상관없이 arguemnt로 작성할 수 있고, 함수내에서 함부로 내용을 수정하지도 못한다.
(수정해야되는 함수면 이렇게하면 안되겠지 당연히)

(2) pointer 자체가 const

int sloth = 3;
int * const finger = &sloth;

이 경우 finger는 가리키는 대상을 바꿀 수 없다. 즉, pointer 변수 자체가 const가 된 것.
(knk declaration 해석 파트에서 본대로 적용해보면 이해됨. "finger는 일단 const"이고, int* type.)

const int * const finger = &sloth;
이러면 sloth의 값과 finger의 값 모두 보호된다.

의문
: const pointer에 data를 저장하는데, non-const data의 경우 indirection level 1인 경우만 가능하다고 했다.
그럼 indirection level이 2 이상인 경우 const면 괜찮다는건데, 이 const의 기준이 뭔지 모르겠네.
그 변수 자체를 처음부터 const로 했어야하나? 아니면 non-const로 선언된 data를 const pointer로 가리키는 경우여도 괜찮은건가?

indirection level이 2 이상이라면 포인터 타입끼리 하는거라서 이런 의문이 들었었는데, 실제로 해보려고하니 안된다.
실제로 데이터만 const인 경우를 해보려고 하니, 의문 해결ㅋㅋ

int * p1;
const int **p2 = &p1;  //invalid

여기서 더 이상 뭐 안됨. p1이 애초에 const int*이었어야 함. p1이 const를 가리키든 non const를 가리키든 상관없이 이 단계에서 막히므로 p1의 type이 중요한 것.


2차원 배열 parameter

prototype

int sum(int (*ar2)[4], int size);
: 이 경우 배열 이름과 "row 크기"를 인자로 넘겨준다.(column 4짜리만 입력받을 수 있다.)
첫번째 parameter 자체는 배열이 아니므로 size를 통해 필요한 나머지 정보를 보강해주는 것이다.
첫번째 parameter 자체는 4개짜리 int 배열을 가리키는 포인터일뿐이다.

int sum(int ar2[][4], int size);
: 위와 모양만 다르지 같다.(knk에선 이 경우만 봤었는데, 배열 이름이 무슨 type인지 생각해보면 위가 더 자연스럽긴하네)

사용법
이렇게 해두면 실제 2차원배열 쓰듯이 사용할 수 있다. ar2[1][2] = 3;
왜?
ar2는 "int 4개짜리 배열이 element인 배열"의 첫 element를 가리킨다.
즉, ar2가 가리키는 대상은 "1차원 배열"인 것이다.
ar2[r]을 하면 r번째 element를 가리키는데, 바로 그 r번째 element는 int가 4개 들어있는 1차원 배열인 것이다.
즉, ar2[r]은 "1차원 배열"(==int가 element인 배열)인데, 거기다가 ar2[r][c]를 하면 당연히 int 원소가 잘 나와주는 것이다.

ar2[r][c] == *(*(ar2 + r) + c)
*(ar2+r) : row r을 나타냄. == int[4] 1차원 배열 == 해당 int[4] 1차원 배열의 첫번째 element(int)를 가리키는 pointer

뭐 그냥 int* type으로 배열이름 받아서(넘길때 casting하고) 포인터 연산으로 접근해도 될 것 같기도하고..

Functions and C-Style Strings

결국 C-style string도 배열이기때문에 위에서 한 내용들 그대로 적용됨. 단, 길이는 필요 없다.
배열이름이든, 포인터이든, string literal이든,, 결국 모두 다 char * 이라 char* 을 인자로 받는 곳이면 (용도에 맞게) 셋 중 아무나 와도 상관 X

//string 다루는 전형적인 방법
//null character나 null pointer는 0이므로(null pointer는 0으로 해석되는 것) 이런식으로 작성할 수 있다.
//C++에서도 C에서처럼 null pointer는 0으로 해석된다.

while (*str) {
	statements
    str++;
}

string을 반환하고 싶다면, new로 만들거나 해야한다. 지역변수로 만들어놓고 string 시작주소 반환하면, 결국 deallocated된다.


Functions and Structures

structure일반 data처럼 넘기고 받고 할 수 있기때문에 크게 어려울건 없다.
근데 structure가 크면 그걸 다 넘겨주기에 시간/공간 낭비이고, 처음엔 C에서 structure를 모두 넘기는걸 허용하지 않았었기 때문에 많은 C programmer는 그냥 배열에서처럼 포인터로 주고 받는걸 선호한다.(passing by reference(Ch 8) 사용할 수도 있음.)

cin을 통한 숫자 입력

앞에서도 나왔던 내용들이긴한데, 한번 더 쭉 정리

while (cin >> rplace.x >> rplace.y)
이 while test문에서 쓰인 것을 보면, cin이 연쇄적으로 쓰임.
>> 같은 class operator는 함수로 구현된다. 즉, cin >> rplace.x는 내부적으로 함수를 호출하는 것이다.
근데 이 함수는 istream value를 반환한다. 즉, cin >> rplace.x는 istream object를 나타내고, 그렇기때문에 여기에 또다시 >> operator를 적용할 수 있는 것이다.
그리고 (ch5에서 봤듯이) cin이 입력에 실패하면 test문내에선 false를 반환한다.
ch6에서 봤듯이 숫자형 변수에 문자를 입력하면 input에 실패하므로 false가돼서 위 while문이 성립할 수 있는 것이다.

cmath 같은 header 사용하려면, 몇몇 compiler에선 math library 사용하라고 명확히 지시해줄 필요가 있다.
ex) older versions of g++은 `g++ sourcename.C -lm` command line을 사용

Functions and string Class Object

string class object는 array보단 structure와 더 관련있다.
complete entity로서 함수에 넘겨줄 수도 있고, assign도 가능하다.
여러 string이 필요하다면, char의 2차원 배열도 가능하지만, string object의 1차원 배열로 만들 수도 있다.


Functions and array Objects

class object는 structures에 기반을 둔다.
그렇기때문에 structures에 가능한 연산은 어느 정도 가능
예를들어 class의 object나 object의 pointer를 함수에 넘겨줄 수 있다.(pass by value이므로 사본이 넘겨짐)

std::array<double, 4> expenses;를 함수에 넘겨주고싶으면,
void show(std::array<double, 4> da); 이런 prototype을 가져야 한다.

얘도 마찬가지로 pointer 주고 받는 편이 효율적
근데 pointer로 주고받으면 좀 복잡해 "보인다". 역참조 해야되고.. 주소연산자(&) 써야되고.. 그럴때 뒤에 설명할 reference type을 이용하면 됨.


Recursion

C: main에서 recursion 허용/ C++: main에서 recursion 불가

void recurs(argList) {
	statements1  //여기서 재귀호출 이전의 statement(statement1)는 순서대로 실행되고
    if (test)
    	recurs(argumments)
    statements2  //재귀호출 이후의 statement(statement2)는 역순으로 실행된다.
}

재귀문이 두개 이상이면 재귀호출 한 level마다 늘어나는 stack양이 크다. quick sort 경우만 봐도 2배수로 늘어남.
적당한 크기면 재귀문이 보기도 좋고 하지만 많은 recursion level이 필요하면 비추


Pointers to Functions

함수도 data처럼 address가 있다.(당연하다. code도 데이터로 결국 저장된다. CS)
함수의 주소는 그 함수가 시작하는 machine language code가 저장된 곳이다.

Function's address 얻기

그냥 함수 이름만 쓰면 그게 함수 주소이다.

'Pointer to a Function' 선언(Declare)

일반 포인터와 마찬가지로 pointer가 무엇을 가리킬지 정확한 type을 명시해야함.
double pam(int);의 prototype을 가진 함수를 가리키고자 한다면, double (*pf)(int); 로 포인터 변수를 선언해야한다.

prototype에서처럼 parameter 변수 이름은 명시해도되고 안해도 된다.
initialization도 해도 된다. const double (*p1)(double) = f1;
(C++11) 이럴때 type을 auto로 명시할 수 있다. auto p2 = f2;

Pointer 이용해서 함수 호출

(*pf)(5); : 이렇게 함수 포인터 역참조해서 호출
pf(5); : 이렇게 그냥 함수 이름처럼 사용하는 것도 허용한다. (C에서도 허용)

어떻게 pf랑 (*pf)가 같단 말이지?
: 한쪽에선, pf가 function 포인터이므로 (*pf)가 function이 되고, 그래서 당연히 (*pf)()로 호출해야한다하고,
다른 쪽에선, 함수 이름이 함수의 pointer이므로, 반대로 봤을때 함수의 pointer도 함수 이름처럼 쓸 수 있어야한다고 한다.
그래서 이 둘이 논리적으로 일관성이 없더라도 C++에선 둘 다 허용한다.

다양한 Function Pointers

const duoble * (*pa[3])(const double *, int) = {f1, f2, f3};
이렇게 함수포인터의 배열로 선언된 경우도 auto가 사용가능한가?
No, auto는 list initializer가 아니라 single initializer에만 적용된다.
저렇게 선언해뒀으면, auto pb = pa; 이런건 가능하다.

함수 포인터의 배열이라면 함수 호출은 (*pa[1])(d, i); 혹은 pa[1](d, i); 식으로 한다.

전체 배열 가리키는 pointer

1차원 배열에선 전체 배열 가리키는 포인터를 만들어보진 않았다.
해봐야 2차원 배열에서, 그 안의 1차원 배열 전체를 가리키는 포인터가 있는 정도였을 뿐인데,
이게 왜 필요할까?
이런 형태의 pointers to arrays of pointers to functions을
virtual class method 구현에 사용한다. (Ch 13)

pa는 배열의 첫번째 인자를 가리키는 포인터이다. 전체 배열을 가리키는 포인터는 어떻게 만들까?
auto pd = &pa;로 간단하게 가능
auto가 아니라 직접 만들고 싶다면, pd가 배열의 포인터가 돼야한다.
따라서 const duoble * (*(*pd)[3])(const double *, int) = &pa; 가 된다.
순서대로 읽어보면 일단 pd는 포인터이고, 크기가 3인 배열을 가리키는 포인터가 된다. 그리고 그 배열의 type(원소의 type)은 포인터인데, 기술된 함수 유형을 가리키는 포인터이다.
함수 호출은 (*pd)[1](d,i); 혹은 (*(*pd)[1])(d,i); 식으로 한다.

pa와 &pa의 차이를 알아야 한다. pa는 해당 배열의 첫번째 원소를 가리키고, &pa는 전체 배열을 가리킨다.
즉, 둘의 값은 같지만, 둘이 가리키는 범위가 다른 것이다. int*과 char*이 가리키는 범위가 다르듯이.
pa+1을 하면 배열 한칸만큼만 포인터가 증가하지만, &pa+1을 하면 배열 전체만큼 포인터가 증가한다.
그리고 dereferencing할때도 pa는 한번만 하면 되지만, &pa는 두번을 해야 첫번째 element가 나온다.

위처럼 auto를 쓸 수 있는건 compiler의 역할이 변하면서이다.
C++98에서 compiler는 본인의 지식으로 잘못된걸 지적하는 것이었지만,
C++11에서는 compiler의 knowledge를 이용해 programmer를 도와주는 것이다.

typedef 활용

typedef const double *(*p_fun)(const double *, int);
이렇게 복잡한 형태를 미리 typedef로 만들어둘 수 있다. 이제 p_fun은 위와 같은 type이다.
p_fun p1 = f1; 이런식으로 사용 가능


Summary

function call은 (1)프로그램이 함수에 argument를 pass하도록 하고, (2)program execution이 그 function code로 이동하게 한다.
(assembly 짜보면 실제로 함수 호출시 argument 지정된 방식(register이든 stack이든)으로 넘겨주고, program excution도 해당 function code로 이동하게 함.)

포인터를 넘겨주더라도 const를 통해 해당 data의 integrity를 지킬 수 있다.

STL에선 배열 크기 넘겨받지않고 첫포인터와 끝포인터로 구현한다고 하네

Review

*"pizza"의 의미는?: character 'p', string literal은 첫번째 charcater의 주소이기때문
"taco"[2]의 의미는?: character 'c', 위와 비슷한 이유.. 첫번째 character주소+2 연산 이후 dereference 연산을 하면 'c'가 나온다.

값 그대로 넘기는 것 VS 주소로 넘기는 것
전자의 경우 기존 값이 자동으로 보호된다. 후자는 본래 의미를 알아보기 좀 더 힘들 수 있다.
대신 데이터 크기가 클 경우 후자의 경우 값을 복사하지 않으므로 시/공간 절약된다.


영단어

lull 잠잠한 시기, 안심시키다
expertise 전문용어
courier 운반원
abdication 사직
forgo 포기하다
head off 막다, 저지하다
insulate 절단하다, 보호하다
integrity 진실성, 온전함
psychic 초자연적인
numerator (분수의) 분자
denominator (분수의) 분모
plausible 타당한 것 같은
implication 영향, 함축
remedy 해결방안, 치료, 바로잡다
mockery 조롱, 엉터리
ravel 더 복잡하게 만들다 <-> unravel 풀다
ruthless 무자비한
axis 축
wilderness 황야
superficial 얄팍한, 표면적인
ad infinitum 끝없이
annotated 주석이 달린
hallmark 특징
intimidate 겁을 주다

0개의 댓글