본문 바로가기
C++/뇌를 자극하는 C++

[C++] 챕터 14 - 함수 1 : 관련된 코드 모아담는 가방

by Minkyu Lee 2023. 6. 14.

 

// 14-1, 2, 3 : 주석 참고

// 14-1 스킵. 함수 필요성 설명을 위한 예제이다. 이해할 필요 없는 예제.

// 14-2 : 화면에 문자열 출력하는 함수
#include <iostream>
using namespace std;

void PrintMessage()
{
    cout << "function";
}

int main()
{
    PrintMessage(); // 함수 호출. 호출(call, invoke)은 실행한다는 의미이다.

    return 0;
}

// 14-3 스킵. 오류 발생하도록 함수 위치를 아래로 옮긴 것이다.

/*
--- 결과 ---
function

---
함수는 좋은 프로그램 짜는 지름길이다.
프로그램은 항상 main 함수에서 시작한다.

관련 정보를 한 데 모으는 것이 중요하다.
함수가 바로 가방 역할한다.
관리가 수월해진다.

소스 코드의 재사용에 좋다.
필요할때마다 꺼내 쓸 수 있다.

어떠한 기준으로 함수를 만들것인가?
이것은 예술적 소양에 가깝다. 경험 필요하다.

// 실행흐름
함수를 만나면 실행 흐름이 해당 함수로 이동했다가 끝나면 다시 돌아간다.

// 함수 위치 이동
함수를 main 아래로 옮기면? 오류가 발생한다.
그렇담 함수를 항상 위쪽에 위치해야하나?
그렇지 않다.

main 함수 위에 PrintMessage함수가 아래에 있다 라고 알려주면 된다.
함수의 정의에서 맨 앞 줄만 복사 붙여넣기 하면 된다.

*/

 

// 14-4 : 함수의 원형

#include <iostream>
using namespace std;

void PrintMessage(); // 세미콜론 필히 붙여야한다.

int main()
{
    PrintMessage();

    return 0;
}

void PrintMessage()
{
    cout << "function";
}

/*
--- 결과 ---
function

---
main 앞에 적은 것을 함수의 원형이라고 부른다. (prototype)

*/

 

// 14-5 : 반환 값의 사용

// 예제는 스킵하였다. 그냥 return 사용한 것이다.
#include <iostream>
using namespace std;

int main()
{
    return 0;
}

/*
--- 결과 ---


---
함수의 반환 값에 대해 설명한다.
return 사용법이다.

함수가 실행된 후 다시 원위치로 돌아가며 값을 가지고 되돌아가는 것을 의미한다.
return 명령은 두가지 의미가 있다.
1. 함수의 실행을 끝내라.
2. 뒤에 오는 값을 반환하라.

반환 값 사용하지 않는 것도 가능하다.
이때는 반환 값 타입으로 void를 사용한다.
반환 값 없는 함수는 단순히 return 명령만 적어줄 수 있다.

// main 함수가 반환하는 값은 누가 받나?
main함수는 정수값 반환 함수이다.

main 함수를 호출한 함수에게로 값을 반환한다.
C++에 숨겨진 함수에게로 전달되는 것이다.

이 숨겨진 함수는 값을 운영체제에게 전달한다.
운영체제는 실행한 프로그램에게 그 값을 반환한다.
0을 반환하는 것은 정상을 의미한다. 이것은 정해진게 아니라 관례이다.

*/

 

// 14-6 : 다른 함수에 있는 변수에 접근

#include <iostream>
using namespace std;

void sub();

int main()
{
    int a = 3;
   
    sub();
   
    return 0;
}
void sub()
{
    ++a; // 오류 발생.
}
/*
--- 결과 ---

---
다른 함수에 있는 변수는 사용할 수 없다.
그래서 함수 안에서는 변수 이름에 자유가 생긴다.

서로 다른 함수에서 정의한 변수는 각자의 메모리 공간 가진다.
전혀 다른 변수인 것이다.
*/

 

// 14-7 : 펙토리얼 구하는 함수

#include <iostream>
using namespace std;

int Factorial(int n);

int main()
{
    // 팩토리얼을 구하고 결과를 저장한다.
    int result;
    result = Factorial(5);

    cout << result << "\n";

    return 0;
}

int Factorial(int n)
{
    int result = 1; // 팩토리얼은 숫자를 증가시키며 곱셈 수행해야한다. 그래서 초기값 1이다.
   
    for (int i = 1; i <= n; ++i)
        result *= i;

    return result;
}

/*
--- 결과 ---
120

---
인자 전달의 기본.
인자는 함수 호출하며 넘겨주는 값을 의미한다.
인자의 개수 여러개 될 수도 있다.

// 인자가 있는 함수의 원형
원형 선언시 인자의 타입까지만 적어주어도 된다.
하지만 인자의 이름까지 읽는 것이 코드 읽기에 좋다. 따라서 이름도 적는게 일반적이다.

// Factorial 함수
위 예제 함수 매우 비효율적이다.
매번 반복하며 값을 계산하기 때문이다.
미리 계산된 값 배열에 넣었다가 꺼내서 반환하도록 개선할 수 있다.

함수를 사용하면 이렇게 개선할 때 용이하다.
*/

 

// 14-8, 9 : 주석 참고

#include <iostream>
using namespace std;

// 14-8 : 인자 전달 규칙
int max(int a, int b)
{
    return a > b ? a : b;
}

int main()
{
    int ret = max(3, 5);

    return 0;
}

// 14-9 스킵

/*
--- 결과 ---

---
max 함수 맨 앞에 다음 코드가 있는 것과 같다.
int a = 3;
int b = 5;
실제로 C++에서는 함수 안에서 정의한 변수처럼 취급한다.
그래서 매개변수라고 부른다.

매개변수는 함수 종료시 소멸한다.

인자와 매개변수는 서로 다른 메모리 공간에 자리 잡고 있는 서로 다른 변수다.
*/

 

// 14-10 : 결과 값을 인자로 받아오기

#include <iostream>
using namespace std;

void GCD_LCM(int a, int b, int gcd, int lcm)
{
    // 유클리드 호제법 사용하여 GCD 구하기
    int z;
    int x = a;
    int y = b;
    while (true)
    {
        z = x % y;
        if (0 == z)
            break;
        x = y;
        y = z;
    }
   
    gcd = y;
    lcm = a * b / gcd;
}

int main()
{
    // 28과 35의 최대공약수와 최소공배수 구하기
    int gcd = 0;
    int lcm = 0;
    GCD_LCM(28, 35, gcd, lcm); // 결과를 받아오기 위해 gcd, lcm변수를 인자로 넘겨주었다.

    cout << gcd << "\n";
    cout << lcm << "\n";

    return 0;
}

/*
--- 결과 ---
0
0

---
제대로 동작하지 않는다.

gcd, lcm은 다른 메모리 공간에 자리잡은 전혀 다른 변수이기 때문이다.
포인터를 사용하여 해결해야한다.
*/

 

// 14-11 : 포인터를 이용한 해결

#include <iostream>
using namespace std;

void GCD_LCM(int a, int b, int* pgcd, int* plcm) // 매개변수가 포인터로 바뀌었다.
{
    // 유클리드 호제법 사용하여 GCD 구하기
    int z;
    int x = a;
    int y = b;
    while (true)
    {
        z = x % y;
        if (0 == z)
            break;
        x = y;
        y = z;
    }

    *pgcd = y; // *를 사용하여야 가리키는 변수에 값을 보관할 수 있다.
    *plcm = a * b / *pgcd;
}

int main()
{
    // 28과 35의 최대공약수와 최소공배수 구하기
    int gcd = 0;
    int lcm = 0;
    GCD_LCM(28, 35, &gcd, &lcm); // 주소값을 인자로 넘겨준다.

    cout << gcd << "\n";
    cout << lcm << "\n";

    return 0;
}

/*
--- 결과 ---
7
140

---
포인터 타입의 인자 사용하여, 함수의 결과값 얻어오는 방법 정리.
1. 매개변수는 포인터 타입으로 정의한다.
2. 인자를 넘겨줄 때, 변수의 주소를 넘겨준다.
3. 결과를 넘겨줄 때, 매개변수가 가리키는 곳에 값을 넣는다.

*/

 

// 14-12 : 구조체를 사용한 반환

#include <iostream>
using namespace std;

struct RetValues
{
    int retValue1;
    int retValue2;
};

RetValues PlusMinus(int a, int b)
{
    RetValues ret;
    ret.retValue1 = a + b;
    ret.retValue2 = a - b;

    return ret;
}

int main()
{
    RetValues result;
    result = PlusMinus(3, 5);

    cout << result.retValue1 << "\n";
    cout << result.retValue2 << "\n";

    return 0;
}

/*
--- 결과 ---
8
-2

---
함수에서 2개 이상의 결과값 반환 받는 다른 방법이다.
구조체 변수를 반환하는 것이다.

구조체 변수의 각 멤버에 값 보관한 후 반환한다.

*/

 

// 14-13 : 레퍼런스를 이용한 해결

#include <iostream>
using namespace std;

void GCD_LCM(int a, int b, int& gcd, int& lcm) // int가 int&로 바뀌었다.
{
    // 유클리드 호제법 사용하여 GCD 구하기
    int z;
    int x = a;
    int y = b;
    while (true)
    {
        z = x % y;
        if (0 == z)
            break;
        x = y;
        y = z;
    }

    gcd = y;
    lcm = a * b / gcd;
}

int main()
{
    // 28과 35의 최대공약수와 최소공배수 구하기
    int gcd = 0;
    int lcm = 0;
    GCD_LCM(28, 35, gcd, lcm); // 결과를 받아오기 위해 gcd, lcm변수를 인자로 넘겨주었다.

    cout << gcd << "\n";
    cout << lcm << "\n";

    return 0;
}

/*
--- 결과 ---
7
140

---
레퍼런스로 효율적인 해결이 가능하다.
포인터 타입의 인자 사용하지 않았다.

가상의 코드를 만들어 해석해보자.
GCD_LCM()함수 앞에는 다음의 코드가 있다.
int& gcd = gcd;
int& lcm = lcm;

gcd, lcm은 레퍼런스 변수이다.
때문에 참조할 변수를 사용해 초기화한다.
그래서 문제 없는 코드가 된다.

매개변수 gcd, lcm은 레퍼런스 타입이기에 자신의 메모리 공간이 없다.
단지 main함수 안의 gcd, lcm의 또 다른 별명으로 존재하는 것이다.

// 포인터와 레퍼런스 비교
레퍼런스를 사용을 추천한다.
사용이 쉽다.
안전하다.

포인터 사용시 가장 큰 위험은 NULL값이 넘겨져오는 경우다.
인자로 올바른 주소값이 넘어온다는 보장이 없다.
따라서 반드시 비교하는 코드를 넣어주어야한다.

비교 코드는 아래와 같다.
bool GCD_LCM(int a, int b, int* pgcd, int* plcm)
{
    if(NULL == pgcd || NULL == plcm)
        return false;

    // 생략

    return true;
}
함수의 반환값이 없었지만 bool 타입 반환하게 수정하였다.
성공여부를 확인한 것이다.

*/

 

// 14-14 : 기본적인 배열의 전달

#include <iostream>
using namespace std;

void UsingArray(char arr[]);

int main()
{
    char array[20] = "Hello, World!";
    UsingArray(array); // 함수 사용
   
    cout << array << "\n";

    return 0;
}

void UsingArray(char arr[])
{
    cout << arr << "\n";
    arr[12] = '?'; // 원소 하나 수정
}

/*
--- 결과 ---
Hello, World!
Hello, World?

---
배열은 약간 예외적인 규칙이 있다.
겉보기엔 배열 전달이지만, 실제로는 포인터를 전달한다는 점이다.

왜 배열 타입 인자 표현할 때, 배열의 원소 개수 필요없는지 알 수 있다.
어차피 포인터이기 때문이다.

배열 타입의 인자 사용법 정리.
1. 매개변수의 타입을 적을 때, 원소 개수 적지 않는다.
2. 인자로 넘겨줄 때, 배열의 이름을 넘긴다.
3. 인자로 넘어온 배열 사용할 때, 평범한 배열 사용하듯이 쓴다.

C++은 성능상의 이점이 있기에, 배열을 포인터로 넘기는 규칙을 적용한 것이다.

// 배열 원소 보호하기
고칠 수 없게 하고 싶을 때는 const를 사용한다.
이때 함수의 원형도 같이 수정하는 것에 주의하라.
void UsingArray(const char arr[])

// 언제 보호를 적용하나?
문자열 길이 측정해 반환하는 함수.
이때는 읽기만 하게 만드는 것이 좋은 습관이다.
*/

 

// 14-15 : 이차원 배열의 전달

#include <iostream>
using namespace std;

void Using2DArray(int arr[][3]); // 2차원 배열의 경우, 가장 앞쪽 대괄호만 비워두면 된다.

int main()
{
    int array[5][3] = { {1,2,3}, {4,5,6}, {7,8,9}, {10,11,12}, {13,14,15} };
    Using2DArray(array);

    return 0;
}

void Using2DArray(int arr[][3])
{
    for (int i = 0; i < 5; ++i)
    {
        for (int j = 0; j < 3; ++j)
        {
            cout << i << j << "\t" << arr[i][j] << "\n";
        }
    }
}
/*
--- 결과 ---
// 결과는 arr[0][0] = 1 과 같이 해석하면 된다.
00      1
01      2
02      3
10      4
11      5
12      6
20      7
21      8
22      9
30      10
31      11
32      12
40      13
41      14
42      15

---
한가지 제외하고는 1차원 배열과 동일하다.
매개 변수의 타입 적어줄 때, int arr[][3]처럼 맨 앞 대괄호를 비워준다.

3차원 이상도 마찬가지다.
맨 앞만 비운다.
int arr[][5][10]
*/

 

// 14-16 : 기본적인 구조체의 전달

#include <iostream>
using namespace std;

struct Point
{
    int x, y;
};

double Distance(Point p1, Point p2);

int main()
{
    Point a = { 0, 0 };
    Point b = { 3, 4 };

    double dist_a_b = Distance(a, b);

    cout << a.x << "," << a.y << "\n";
    cout << b.x << "," << b.y << "\n";
    cout << dist_a_b << "\n";

    return 0;
}

double Distance(Point p1, Point p2)
{
    cout << p1.x << "," << p1.y << "\n";
    cout << p2.x << "," << p2.y << "\n";

    return 0.0f; // 임시로 0을 반환.
}

/*
--- 결과 ---
0,0
3,4
0,0
3,4
0
---
거리값은 다음 예제에서 바꾼다.

구조체의 크기는 중요한 주제다.
구조체 변수는 멤버 개수에 따라 엄청 커질 수 있다.
심각한 성능 문제 생길 수 있다.

위 코드는 문제가 된다.
함수에서 변수가 복사되면서 메모리를 차지하기 때문이다.

이를 방지하기 위해 구조체의 포인터나 레퍼런스를 전달하는 방법을 사용할 수 있다.
하지만 문제가 있다.
함수 안쪽에서 레퍼런스를 사용해 함수 외부의 값을 변경할 수 있다는 점이다.
그래서 다음과 같은 규칙이 된다.
1. 구조체를 인자로 넘겨줄 때는 레퍼런스를 사용한다.
2. 함수 안쪽에서 구조체 읽기만 한다면 const와 레퍼런스를 사용한다.

double Distance(const Point& p1, const Point& p2)

// 일반 변수의 const와 레퍼런스 사용
크기가 크지 않아서 이득이 별로 없다.
오히려 레퍼런스 거쳐서 인자 접근하면 손해가 될 수 있다.
*/

 

// 14-17 : sqrt()와 pow()함수의 사용

#include <iostream>
#include <cmath> // c는 CRT함수라는 의미이다. math는 수학 함수라는 의미이다.
using namespace std;

int main()
{
    double sqrt_2 = sqrt(2.0);
    double pow_12_2 = pow(12, 2);

    cout << sqrt_2 << "\n";
    cout << pow_12_2 << "\n";

    return 0;
}

/*
--- 결과 ---

---
CRT 함수의 사용.
CRT는 C Runtime Library의 약자다.
CRT 함수란 기본적으로 제공되는 함수다.
종류는 다양하다.

함수를 호출하기 위해서는 원형을 알아야한다.
MSDN 등을 통해 함수의 원형을 알 수 있다.
MSDN은 마이크로소프트 개발자 네트워크를 말한다.

*/

 

// 14-18 : 두 점의 거리 구하는 함수 완성

#include <iostream>
#include <cmath> // 추가
using namespace std;

struct Point
{
    int x, y;
};

double Distance(Point p1, Point p2);

int main()
{
    Point a = { 0, 0 };
    Point b = { 3, 4 };

    double dist_a_b = Distance(a, b);

    cout << a.x << "," << a.y << "\n";
    cout << b.x << "," << b.y << "\n";
    cout << dist_a_b << "\n";

    return 0;
}

double Distance(Point p1, Point p2) // 수정
{
    double distance;
    distance = sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2)); // 피타고라스의 정리

    return distance;
}

/*
--- 결과 ---
0,0
3,4
5

---
가장 안쪽의 함수부터 호출이 된다.
안쪽의 반환값이 나와야 바깥쪽 함수 호출이 가능하기 때문이다.
*/

댓글