본문 바로가기
개인공부/C++ 기초플러스

C++ Primer 07

by 하고싶은건많은놈 2023. 2. 19.

학습목표

  • 함수의 기초(복습)
  • 함수의 원형
  • 함수 매개변수를 값으로 전달
  • 배열을 처리하는 함수의 설계
  • const 포인터 매개변수
  • 문자열을 처리하는 함수의 설계
  • 구조체를 처리하는 함수의 설계
  • string 클래스 객체를 처리하는 함수의 설계
  • 자기 자신을 호출하는 (재귀) 함수
  • 함수를 지시하는 포인터

7.1 함수의 기초(복습)

C++에서 함수를 사용하기 위해 해야하는 작업

  • 함수 정의 제공
    • 라이브러리 함수 사용시 이미 정의되어 컴파일되어있음
    • 사용자 정의 함수는 스스로
  • 함수 원형 제공
    • 라이브러리 함수 사용시 표준 라이브러리의 헤더 파일을 이용
    • 사용자 정의 함수는 스스로
  • 함수 호출
// calling.cpp

#include <iostream>
void simple();

int main()
{
	using namespace std;
	cout << "main()에서 simple() 함수를 호출합니다 : \n";
	simple();
	cout << "main()이 simple() 함수와 종료됩니다.\n";
	return 0;
}

void simple()
{
	using namespace std;
	cout << "여기는 simple() 함수입니다.\n";
}

함수 정의

일반적으로 리턴값이 없는 void형 함수는 어떤 종류의 행동을 수행함
리턴값이 있는 함수의 경우 자신을 호출한 함수에게 되돌려줄 리턴값을 만듬

void functionName(parameterList)
{
	statement(s)
	return ;
}

parameterList : 함수에 전달되는 매개변수의 데이터형과 개수를 지정
return : 함수의 끝을 표시

  • void형에서는 생략할 수 있으며 생략시 함수는 닫는 중괄호에서 종료됨
  • 상수 / 변수 / 일반적인 표현식이 리턴값이 되고 해당 표현식의 값은 함수에서 선언된 typeName형으로 변환되어 리턴됨
  • 단, 배열은 리턴값으로 사용할 수 없음
  • 일반적으로 리턴값을 CPU에 지정된 레지스터나 메모리에 복사하는 방법으로 리턴함

함수 원형과 함수 호출

// protos.cpp

#include <iostream>
void cheers(int);
double cube(double x);

int main()
{
	using namespace std;
	cheers(5);
	cout << "하나의 수를 입력하십시오 : ";
	double side;
	cin >> side;
	double volume = cube(side);
	cout << "한 변의 길이가 " << side << "센티미터인 정육면체의 부피는 ";
	cout << volume << " 세제곱센티미터입니다.\n";
	cheers(cube(2));
	return 0;
}

void cheers(int n)
{
	using namespace std;
	for (int i = 0; i < n; i++)
		cout << "Cheers! ";
	cout << endl;
}

double cube(double x)
{
	return x * x * x;
}

함수 원형은 컴파일러에게 함수의 인터페이스인 리턴값의 데이터형, 매개변수의 개수와 각 매개변수의 데이터형을 알려줌

  • 컴파일러는 함수 원형에 근거하여 에러를 검출
  • 특정한 위치에 저장된 함수의 리턴값을 꺼내올 때, 컴파일러는 함수 원형을 보고 데이터형을 판단하여 몇 바이트를 꺼내고 어떻게 처리해야하는지 판단함
  • 컴파일러가 함수 원형 대신 함수를 직접 확인할 경우, 라이브러리 함수 등 독립적인 파일에 들어있는 함수들은 찾지 못할 수 있음
  • 함수 원형도 하나의 구문이기 때문에 세미콜론으로 끝나야 하며 함수 머리에 세미콜론을 붙이는 것만으로도 완성할 수 있음
  • 함수 원형에서의 변수 이름은 생략 가능

함수 원형은 다음과 같은 것들을 보장함

  • 컴파일러가 함수의 리턴값을 바르게 처리
  • 사용자가 정확한 개수의 매개변수를 사용했는지 컴파일러가 검사
  • 사용자가 정확한 데이터형의 매개변수를 사용했는지 컴파일러가 검사, 부정확한 데이터형 사용시 정확한 데이터형으로 변환

함수 원형 비교는 컴파일 시 이루어지며, 이를 정적 데이터형 검사라고 부름



7.2 함수 매개변수와 값으로 전달하기

함수에 매개변수를 전달하면 해당 함수는 매개변수의 복사본을 가지고 작업함

  • 즉, 원본 데이터에는 영향을 주지 않음
  • 이 때 전달되는 값을 넘겨받는데 쓰이는 변수를 형식 매개변수(formal parameter)라고 부름
  • 함수에 전달되는 값을 실제 매개변수(actual argument)라고 함
  • 형식 매개변수를 포함해 함수 안에서 선언된 모든 변수들은 해당 함수 내에서만 활동하며, 함수 종료시 메모리가 해제됨
    이러한 변수들을 지역 변수(local variable)이라고 부름

여러 개의 매개변수

// twoarg.cpp

#include <iostream>
using namespace std;
void n_chars(char, int);


int main()
{
	int times;
	char ch;

	cout << "문자를 하나 입력하십시오 : ";
	cin >> ch;
	while (ch != 'q')
	{
		cout << "정수를 하나 입력하십시오 : ";
		cin >> times;
		n_chars(ch, times);
		cout << "\n계속하려면 다음 문자를 입력하고, 끝내려면 q를 누르십시오 : ";
		cin >>ch;
	}
	cout << "현재 times의 값은 " << times << "입니다.\n";
	cout << "프로그램을 종료합니다.\n";
	return 0;
}

void n_chars(char c, int n)
{
 while (n-- > 0)
	cout << c;
}

함수는 하나 이상의 매개변수를 가질 수 있으며, 매개변수들은 콤마로 분리됨
이 때 일반적인 변수 선언에서 가능했던 변수 선언의 결합은 허용되지 않음

두 개의 매개변수를 사용하는 또 다른 함수

// lotto.cpp

#include <iostream>
long double probability(unsigned numbers, unsigned picks);

int main()
{
	using namespace std;
	double total, choices;
	cout << "전체 수의 개수와 뽑을 수의 개수를 입력하십시오 : \n";
	while ((cin >> total >> choices) && choices <= total)
	{
		cout << "당신이 이길 확률은 ";
		cout << probability(total, choices);
		cout << "번 중에서 한번입니다.\n";
		cout <<"다시 두 수를 입력하십시오. (끝내려면 q를 입력) : ";
	}
	cout << "프로그램을 종료합니다.\n";
	return 0;
}

long double probability(unsigned numbers, unsigned picks)
{
	long double result = 1.0;
	long double n;
	unsigned p;

	for (n = numbers, p = picks; p > 0; n--, p--)
		result = result * n / p;
	return result;
}

오버플로를 방지하기 위해 중간 계산값이 작도록 수식을 설정하는게 유리함



7.3 함수와 배열

// arrfun1.cpp

#include <iostream>
const int ArSize = 8;
int sum_arr(int arr[], int n);

int main()
{
	using namespace std;
	int cookies[ArSize] = {1,2,4,8,16,32,64,128};

	int sum = sum_arr(cookies, ArSize);
	cout << "먹은 과자 수 합계 : " << sum << "\n";
	return 0;
}

int sum_arr(int arr[], int n)
{
	int total = 0;
	for (int i = 0; i < n; i++)
		total = total + arr[i];
	return total;
}

C++도 C와 마찬가지로 대부분의 상황에서 배열 이름을 포인터처럼 사용함
sizeof를 배열 이름에 적용시 배열의 전체 크기가 바이트 단위로 구해짐
주소 오퍼레이터 &를 배열 이름에 적용하면 전체 배열 주소가 생성됨

C++의 함수 머리 혹은 함수 원형에서의 int * arrint arr[]는 둘 다 arr이 int형을 지시하는 포인터라는 동일한 의미를 지님
단, 다른 상황에서는 서로 다른 의미를 가지기 때문에 혼용할 수 없음

배열을 매개변수로 사용하는 것의 의미

배열의 실제 내용을 전달하는 것이 아님
대신 배열의 주소와 배열의 종류, 배열 원소의 개수를 함수에 전달함
이 정보들을 이용하여 함수는 배열의 원본에 접근할 수 있음

  • 전체 배열을 복사하는 것보다 시간과 메모리가 절약됨
  • 원본을 대상으로 작업하므로 부주의에 의해 데이터가 손상될 가능성이 있으나, 이는 const 제한자를 통해 해결할 수 있음
// arrfun2.cpp

#include <iostream>
const int ArSize = 8;
int sum_arr(int arr[], int n);

int main()
{
	int cookies[ArSize] = {1,2,4,8,16,32,64,128};

	std::cout << cookies << " = 배열 주소, ";
	std::cout << "sizeof cookies = " << sizeof cookies << std::endl;
	int sum = sum_arr(cookies, ArSize);
	std::cout << "먹은 과자 수 합계 : " << sum << std::endl;
	sum = sum_arr(cookies, 3);
	std::cout << "처음 세 사람은 과자 " << sum << "개를 먹었습니다.\n";
	sum = sum_arr(cookies + 4, 4);
	std::cout << "마지막 네 사람은 과자 " << sum << "개를 먹었습니다.\n";
	return 0;
}

int sum_arr(int arr[], int n)
{
	int total = 0;
	std::cout << arr << " = arr, ";
	std::cout << "sizeof arr = " << sizeof arr << std::endl;
	for (int i = 0; i < n; i++)
		total = total + arr[i];
	return total;
}

포인터 자체가 배열의 크기를 나타내지는 않기 때문에 함수에 배열의 크기를 명시적으로 전달해주어야 함
단, 이 때 대괄호 표기 형태가 아닌 서로 독립된 매개변수의 형태로 넘겨주어야 함


배열을 처리하는 함수에 대한 보충

특정 유형의 데이터를 처리하기 위해서는 특정 함수를 작성하는 것이 프로그램의 신뢰성 / 수정의 용이성 / 디버깅의 용이성 등을 포함해 더 유리함
함수의 목적이 데이터의 변경이 아니라면 배열의 내용 출력시 원본을 변경시키지 않도록 const 키워드를 사용하여 보호해야함
매개변수 선언시 const 키워드를 추가하면 해당 변수를 읽기 전용 데이터로 취급함

// arrfun3.cpp

#include <iostream>
const int Max = 5;
int fill_array(double ar[], int limit);
void show_array(const double ar[], int n);
void revalue(double r, double ar[], int n);

int main()
{
	using namespace std;
	double properties[Max];

	int size = fill_array(properties, Max);
	show_array(properties, size);
	if (size > 0)
	{
		cout << "재평가율을 입력하십시오 : ";
		double factor;
		while (!(cin >> factor))
		{
			cin.clear();
			while (cin.get() != '\n')
				continue;
			cout << "잘못 입력했습니다, 수치를 입력하세요 : ";
		}
		revalue(factor, properties, size);
		show_array(properties, size);
	}
	cout << "프로그램을 종료합니다.\n";
	return 0;
}

int fill_array(double ar[], int limit)
{
	using namespace std;
	double temp;
	int i;
	for (i = 0; i < limit; i++)
	{
		cout << (i + 1) << "번 부동산의 가격 : $";
		cin >> temp;
		if (!cin)
		{
			cin.clear();
			while (cin.get() != '\n')
				continue;
				cout << "입력 불량; 입력 과정을 끝내겠습니다.\n";
				break;
		}
		else if (temp < 0)
			break;
		ar[i] = temp;
	}
	return i;
}

void show_array(const double ar[], int n)
{
	using namespace std;
	for (int i = 0; i < n; i++)
	{
		cout << (i + 1) << "번 부동산 : $";
		cout << ar[i] << endl;
	}
}

void revalue(double r, double ar[], int n)
{
	for (int i = 0; i < n; i++)
		ar[i] *= r;
}

각 데이터형에 대해 적절하게 처리할 수 있는 개별 함수를 설계 후, 하나의 프로그램으로 결합하는 방식을 상향시(bottom-up) 프로그래밍이라고 부름


배열의 범위를 사용하는 함수

// arrfun4.cpp

#include <iostream>
const int ArSize = 8;
int sum_arr(const int * begin, const int * end);

int main()
{
	using namespace std;
	int cookies[ArSize] = {1,2,4,8,16,32,64,128};

	int sum = sum_arr(cookies, cookies + ArSize);
	cout << "먹은 과자 수 합계 : " << sum << std::endl;
	sum = sum_arr(cookies, cookies + 3);
	cout << "처음 세 사람은 과자 " << sum << "개를 먹었습니다.\n";
	sum = sum_arr(cookies + 4, cookies + 8);
	cout << "마지막 네 사람은 과자 " << sum << "개를 먹었습니다.\n";
	return 0;
}

int sum_arr(const int * begin, const int * end)
{
	const int *pt;
	int total = 0;
	for (pt = begin; pt != end; pt++)
		total = total + *pt;
	return total;
}

배열의 시작 위치와 배열의 크기를 전달하여 배열을 처리하는 방식 외에도, 원소들의 범위를 지정하는 방법도 존재함
즉, 배열의 시작을 지시하는 포인터와 배열의 끝을 지시하는 포인터를 전달
C++ 표준 템플릿 라이브러리에서는 이런 식의 범위접근 방법을 일반화함

  • '끝 바로 다음' 이라는 개념을 사용
  • 배열의 마지막 원소 다음을 지시하는 포인터로 배열의 끝을 인식
  • 포인터를 정확한 순서로 전달하는 것이 중요

포인터와 const

상수 객체를 지시하는 포인터를 만들시 해당 포인터를 사용하여 포인터가 지시하는 값을 변경할 수 없음

  • 포인터가 지시하는 값 자체가 상수임을 의미하지는 않으므로 포인터를 사용하지 않는다면 값을 변경할 수 있음
  • const 변수의 주소를 일반 포인터에 대입하는 것은 불가능함
  • const를 사용하는 함수는 constconst가 아닌 매개변수를 모두 처리할 수 있으나, const를 생략한 함수는 const가 아닌 데이터만 처리할 수 있음

포인터 자신을 상수로 만들시 상수 포인터를 사용하여 해당 포인터가 지시하는 장소를 변경할 수 없음



7.4 함수와 2차원 배열

2차원 배열을 매개변수로 사용시

  • int sum(int (*ar2)[4], int size) 의 형식으로 4개의 열을 가진 2차원 배열을 선언
  • int sum(int ar2[][4], int size) 위와 동일하지만 더 읽기 쉬움
  • 기본형을 지시하는 포인터에 대해서만 const를 사용할 수 있기 때문에 위의 경우와 같이 포인터를 지시하는 포인터인 ar2에 const를 붙일 수는 없음


7.5 함수와 C스타일의 문자열

C스타일 문자열을 매개변수로 사용하는 함수

문자열을 나타내는 방법 세가지 모두 char형을 지시하는 포인터인 char *형이기 때문에 문자열 처리 함수에 매개변수로 사용할 수 있음

  • char형의 배열
  • 큰따옴표로 묶은 문자열 상수(문자열 리터럴)
  • 문자열의 주소로 설정된 char형을 지시하는 포인터
// strgfun.cpp

#include <iostream>
unsigned int c_in_str(const char * str, char ch);

int main()
{
	using namespace std;
	char mmm[15] = "minimum";
	char const *wail = "ululate";

	unsigned int ms = c_in_str(mmm, 'm');
	unsigned int us = c_in_str(wail, 'u');
	cout << mmm << "에는 m이 " << ms << "개 들어있습니다.\n";
	cout << wail << "에는 u가 " << us << "개 들어있습니다.\n";
	return 0;
}

unsigned int c_in_str(const char * str, char ch)
{
	int count = 0;

	while (*str)
	{
		if (*str == ch)
			count++;
		str++;
	}
	return count;
}

문자열 원본의 변경을 막기 위해 const 제한자를 사용
매개변수로 문자열 전달시 실제로는 문자열을 구성하는 첫 문자의 주소를 전달함
문자열은 반드시 널 문자로 종결되어야하며, 널 문자가 없을 경우는 문자열이 아닌 그냥 배열이기 때문에 문자열의 크기는 매개변수로 전달할 필요가 없음
단, 문자열을 처리하는 함수에서 널 문자에 도달할 때까지 문자들을 검사해야함


C스타일 문자열을 리턴하는 함수

// strgback.cpp

#include <iostream>
char * buildstr(char c, int n);

int main()
{
	using namespace std;
	int times;
	char ch;

	cout << "문자 하나를 입력하십시오 : ";
	cin >> ch;
	cout << "정수 하나를 입력하십시오 : ";
	cin >> times;
	char *ps = buildstr(ch, times);
	cout << ps << endl;
	delete [] ps;
	ps = buildstr('+', 20);
	cout << ps << "-DONE-" << ps << endl;
	delete [] ps;
	return 0;
}

char * buildstr(char c, int n)
{
	char * pstr = new char[n + 1];
	pstr[n] = '\0';
	while (n-- > 0)
		pstr[n] = c;
	return pstr;
}

함수로는 문자열 자체를 리턴할 수 없고 대신 문자열의 주소를 리턴할 수 있음
buildstr 함수에서 선언한 변수 pstr은 지역변수이기 때문에 함수가 종결되면 자동으로 해제되지만, 함수가 끝나기 전 그 값을 리턴하기 때문에 리턴값이 대입된 ps 포인터를 통해 문자열에 접근할 수 잇음



7.6 함수와 구조체

구조체 변수도 보통의 변수처럼 함수의 값으로 전달할 수 있음
구조체를 기본적인 데이터형처럼 값으로 전달할 경우 구조체의 덩치에 따라 복사에 걸리는 시간과 메모리 요구 때문에 시스템 성능이 저하됨
따라서 일반적으로는 구조체의 주소를 통해 내용에 접근하는 포인터를 이용하는 방식을 선호하며, C++에서는 참조로 전달(passing by reference)하기도 함

구조체의 전달과 리턴

// travel.cpp

#include <iostream>
struct travel_time
{
	int hours;
	int mins;
};
const int Mins_per_hr = 60;

travel_time sum(travel_time tl, travel_time t2);
void show_time(travel_time t);

int main()
{
	using namespace std;
	travel_time day1 = {5, 45};
	travel_time day2 = {4, 55};

	travel_time trip = sum(day1, day2);
	cout << "이틀간 소요시간 : ";
	show_time(trip);

	travel_time day3 = {4, 32};
	cout << "사흘간 소요시간 : ";
	show_time(sum(trip, day3));

	return 0;
}

travel_time sum(travel_time t1, travel_time t2)
{
	travel_time total;

	total.mins = (t1.mins + t2.mins) % Mins_per_hr;
	total.hours = t1.hours + t2.hours + (t1.mins + t2.mins) / Mins_per_hr;
	
	return total;
}

void show_time(travel_time t)
{
	using namespace std;
	cout << t.hours << "시간 "
		 << t.mins << "분\n";
}

구조체로 선언한 travel_time은 기본적인 데이터형의 이름처럼 동작하므로 변수 선언, 함수의 리턴형, 매개변수의 데이터형으로 사용할 수 있음


함수와 구조체의 두번째 예제

// strctfun.cpp

#include <iostream>
#include <cmath>
struct polar
{
	double distance;
	double angle;
};
struct rect
{
	double x;
	double y;
};

polar rect_to_polar(rect xypos);
void show_polar(polar dapos);

int main()
{
	using namespace std;
	rect rplace;
	polar pplace;

	cout << "x값과 y값을 입력하십시오 : ";
	while (cin >> rplace.x >> rplace.y)
	{
		pplace = rect_to_polar(rplace);
		show_polar(pplace);
		cout << "x값과 y값을 입력하십시오(끝내려면 q를 입력) : ";
	}
	cout << "프로그램을 종료합니다. \n";
	return 0;
}

polar rect_to_polar(rect xypos)
{
	using namespace std;
	polar answer;

	answer.distance =
		sqrt(xypos.x * xypos.x + xypos.y * xypos.y);
	answer.angle = atan2(xypos.y, xypos.x);
	return answer;
}

void show_polar(polar dapos)
{
	using namespace std;
	const double Rad_to_deg = 57.29577951;

	cout << "거리 = " << dapos.distance;
	cout << ", 각도 = " << dapos.angle * Rad_to_deg;
	cout << "도\n";
}

cinistream클래스의 객체이며, cin >> rplace.x와 같이 추출 연산자 >>cin에 사용시 우측의 변수가 cin과 같은 형의 객체가 됨

  • 클래스 연산자들은 모두 함수로 구현되어있기 때문에, cin >> 사용시 실제로는 프로그램이 istream형의 값을 리턴하는 함수를 호출함
  • 따라서 cin >> rplace.x에 추출 연산자를 한번 더 적용하면 또다시 cin 객체를 리턴받게되며, 결국 while 루프의 조건 검사 표현식은 cin값으로 평가됨
  • 이 때 cin의 값은 입력 성공 여부에 따라 bool형인 true 또는 false로 변환됨

수를 읽어들이는 루프 설정시, 조건 검사에 cin >>를 사용할 경우 모든 유효한 수치 입력을 받아들이기 때문에 제한이 줄어듬

  • 수가 아닌 것이 입력되는 경우 더이상 입력을 받지 않도록 에러조건을 설정함
  • 추가적인 입력이 필요할 경우 cin.clear()를 사용해야함

구조체 주소의 전달

// strctfun.cpp

#include <iostream>
#include <cmath>
struct polar
{
	double distance;
	double angle;
};
struct rect
{
	double x;
	double y;
};

void rect_to_polar(const rect * pxy, polar * pda);
void show_polar(const polar * pda);

int main()
{
	using namespace std;
	rect rplace;
	polar pplace;

	cout << "x값과 y값을 입력하십시오 : ";
	while (cin >> rplace.x >> rplace.y)
	{
		rect_to_polar(&rplace, &pplace);
		show_polar(&pplace);
		cout << "x값과 y값을 입력하십시오(끝내려면 q를 입력) : ";
	}
	cout << "프로그램을 종료합니다. \n";
	return 0;
}

void rect_to_polar(const rect * pxy, polar * pda)
{
	using namespace std;
	pda->distance = 
		sqrt(pxy->x * pxy->x + pxy->y * pxy->y);
	pda->angle = atan2(pxy->y, pxy->x);
}

void show_polar(const polar * pda)
{
	using namespace std;
	const double Rad_to_deg = 57.29577951;

	cout << "거리 = " << pda->distance;
	cout << ", 각도 = " << pda->angle * Rad_to_deg;
	cout << "도\n";
}

함수 호출시 구조체 대신 구조체 구조체 주소를 전달
구조체 포인터형을 매개변수로 선언하고, 함수를 실행하는 과정에서 구조체를 변경하는 것이 필요하지 않기 때문에 const 키워드를 사용함
매개변수가 포인터이므로 멤버 연산자 . 대신 간접 멤버 연산자 -> 사용



7.7 함수와 string 클래스 객체

// topfive.cpp

#include <iostream>
#include <string>
using namespace std;
const int SIZE = 5;
void display(const string sa[], int n);

int main()
{
	string list[SIZE];
	cout << "좋아하는 밤하늘의 광경을 " << SIZE << "개 입력하시오 :\n";
	for (int i = 0; i < SIZE; i++)
	{
		cout << i + 1 << " : ";
		getline(cin, list[i]);
	}

	cout << "입력하신 내용은 다음과 같습니다 : \n";
	display(list, SIZE);
	return 0;
}

void display(const string sa[], int n)
{
	for (int i = 0; i < n; i++)
		cout << i + 1 << " : " << sa[i] << endl;
}

여러개의 문자열이 필요한 경우 char형의 2차원 배열 대신 string 객체의 1차원 배열을 선언할 수 있음



7.8 함수와 array 객체

C++의 클래스 객체는 구조체에 기반을 두고있음

  • 함수에 객체를 값으로 전달시 해당 함수가 원본 객체의 사본으로 동작함
  • 포인터를 객체로 전달시 해당 함수가 원본 객체로 동작함
// arrobj.cpp

#include <iostream>
#include <array>
#include <string>
const int Seasons = 4;
const std::array<std::string, Seasons> Sname = 
	{"Spring", "Summer", "Fail", "Winter"};

void fill(std::array<double, Seasons> *pa);
void show(std::array<double, Seasons> da);

int main()
{
	std::array<double, Seasons> expenses;
	fill(&expenses);
	show(expenses);
	return 0;
}

void fill(std::array<double, Seasons> * pa)
{
	using namespace std;
	for (int i = 0; i < Seasons; i++)
	{
		cout << Sname[i] << "에 소요되는 비용 : ";
		cin >> (*pa)[i];
	}
}

void show(std::array<double, Seasons> da)
{
	using namespace std;
	double total = 0.0;
	cout << "\n계졀별 비용\n";
	for (int i = 0; i < Seasons; i++)
	{
		cout << Sname[i] << " : $" << da[i] << endl;
		total += da[i];
	}
	cout << "총 비용 : $" << total << endl;
}

show()의 경우 expenses의 크기를 위해 새로운 객체를 생성하고 그 안에 값을 복사해야 하기 때문에 비효율적임
expenses를 확대하기 위해 프로그램을 수정할 경우 문제가 더욱 커질 수 있음

fill()의 경우 포인터를 사용해 원본 객체에 함수가 작동하도록 함으로써 비효율적인 문제를 피함
그러나 (*pa)[i]와 같이 연산자 우선순위 고려 등 프로그램이 복잡해지는 문제가 있음



7.9 재귀 호출

C++은 main()함수 외에는 재귀호출을 허용함

단일 재귀 호출

// recur.cpp

#include <iostream>
void countdown(int n);

int main()
{
	countdown(4);
	return 0;
}

void countdown(int n)
{
	using namespace std;
	cout << "카운트 다운 ... " << n << endl;
	if (n > 0)
		countdown(n-1);
	cout << n << " : Kaboom!\n";
}

재귀 호출의 무한반복을 방지하기 위해 일반적으로 호출 부분을 if구문의 일부로 만드는 방법을 사용함
함수 호출시 해당 함수의 실행을 끝나고 다음 구문으로 넘어가는 것과 같이, 재귀 호출시에도 마찬가지로 재귀로 호출된 자기 자신의 실행이 끝난 뒤 호출한 함수의 다음 구문으로 넘어감


다중 재귀 호출

// ruler.cpp

#include <iostream>
const int Len = 66;
const int Divs = 6;
void subdivide(char ar[], int low, int high, int level);

int main()
{
	char ruler[Len];
	int i;
	for (i = 1; i < Len - 2; i++)
		ruler[i] = ' ';
	ruler[Len - 1] = '\0';
	int max = Len - 2;
	int min = 0;
	ruler[min] = ruler[max] = '|';
	std::cout << ruler << std::endl;
	for (i = 1; i <= Divs; i++)
	{
		subdivide(ruler, min, max, i);
		std::cout << ruler << std::endl;
		for (int j = 1; j < Len - 2; j++)
			ruler[j] = ' ';
	}
	return 0;
}

void subdivide(char ar[], int low, int high, int level)
{
	if (level == 0)
		return;
	int mid = (high + low) / 2;
	ar[mid] = '|';
	subdivide(ar, low, mid, level - 1);
	subdivide(ar, mid, high, level - 1);
}

하나의 작업을 서로 비슷한 두 개의 작은 작업으로 반복적으로 분할해가면서 처리해야 하는 상황에서 유용함
이러한 재귀적 접근을 분할 정복(divide-and-conquer)이라고 함



7.10 함수를 지시하는 포인터

함수도 주소를 가지고있으며, 그 주소는 함수에 해당하는 기계어 코드가 저장되어있는 메모리 블록의 시작 주소임
함수의 주소를 매개변수로 취하는 함수를 작성시 다른 시각에 다른 함수의 주소를 전달하는 것이 가능해지며, 이는 첫번째 함수가 다른 시각에 다른 함수를 사용할 수 있음을 의미함

함수 포인터의 기초

process(think);
thought(think());
  • process(think);의 경우 process함수 호출시 해당 함수 내부에서 think함수를 불러냄
  • thought(think());의 경우 thought함수 호출시 think함수가 먼저 실행되고, 해당 함수의 리턴값이 thought함수에 전달됨

함수 포인터 선언시에도 해당 포인터가 지시하는 함수의 데이터형을 지정해야함
함수의 원형에서 함수 이름을 (*pf) 형태의 표현식으로 대체하여 사용함
선언한 함수 포인터는 함수 이름과 같은 역할을 함


함수 포인터 예제

// fun_ptr.cpp

#include <iostream>
double gildong(int);
double moonsoo(int);
void estimate(int lines, double (*pf)(int));

int main()
{
	using namespace std;
	int code;

	cout << "필요한 코드의 행 수를 입력하십시오 : ";
	cin >> code;
	cout << "홍길동의 시간 예상 : \n";
	estimate(code, gildong);
	cout << "박문수의 시간 예상 : \n";
	estimate(code, moonsoo);
	return 0;
}

double gildong(int lns)
{
	return 0.05 * lns;
}

double moonsoo(int lns)
{
	return 0.03 * lns + 0.0004 * lns * lns;
}

void estimate(int lines, double (*pf)(int))
{
	using namespace std;
	cout << lines << "행을 작성하는 데 ";
	cout << (*pf)(lines) << "시간이 걸립니다.\n";
}

이런 식으로 함수 포인터를 활용했을 경우 추가적인 함수를 사용해야 할시 estimate()함수를 다시 작성하지 않고도 새로운 함수를 제공하기만 하면 되므로 프로그램 개발에 용이함


함수 포인터의 변형

const double * f1(const double ar[], int n);
const double * f2(const double [], int);
const double * f3(const double *, int);

위의 세 함수는 모두 동일한 특징과 리턴값을 가지는 함수 원형임
함수 원형의 식별자는 생략할 수 있으나, 함수 정의에서는 생략할 수 없음

const double * (*p1)(const double *, int) = f1;
auto p1 = f1;

함수 포인터 선언시 C++의 자동 형변환을 사용하면 단순하게 작성이 가능함

const double * (*pa[3])(const double *, int) = {f1, f2, f3};

함수 포인터의 배열도 사용 가능함
단, 단일 값을 초기화하는 것이 아니기 때문에 이 때는 auto를 사용할 수 없음
auto pb = pa;와 같이 이미 선열된 함수 포인터 배열에 자동 형변환을 통해 대입하는 것은 가능함
pa&pa[0]과 같은 함수 포인터 배열의 첫번째 원소의 주소이며, &pa는 전체 배열의 주소임

  • pa&pa가 같은 주소값을 가질 수도 있으나, 데이터형이 다름
  • **&pa == *pa == pa[0]의 계층을 가지고있음
// arfupt.cpp

#include <iostream>
const double * f1(const double ar[], int n);
const double * f2(const double [], int);
const double * f3(const double *, int);

int main()
{
	using namespace std;
	double av[3] = {1112.3, 1542.6, 2227.9};

	const double *(*p1)(const double *, int) = f1;
	auto p2 = f2;
	cout << "함수 포인터 : \n";
	cout << "주소 값\n";
	cout << (*p1)(av, 3) << " : " << *(*p1)(av, 3) << endl;
	cout << p2(av, 3) << " : " << *p2(av, 3) << endl;

	const double *(*pa[3])(const double *, int) = {f1, f2, f3};
	auto pb = pa;
	cout << "\n함수 포인터를 원소로 가지는 배열 : \n";
	cout << "주소값\n";
	for (int i = 0; i < 3; i++)
		cout << pa[i](av, 3) << " : " << *pa[i](av, 3) << endl;
	cout << "\n함수 포인터를 가리키는 포인터 : \n";
	cout << "주소값\n";
	for (int i = 0; i< 3; i++)
		cout << pb[i](av, 3) << " : " << *pb[i](av, 3) << endl;

	cout << "\n포인터를 원소로 가지는 배열을 가리키는 포인터 : \n";
	cout << "주소값\n";
	auto pc = &pa;
	cout << (*pc)[0](av, 3) << " : " << *(*pc)[0](av, 3) << endl;
	const double *(*(*pd)[3])(const double *, int) = &pa;
	const double * pdb = (*pd)[1](av, 3);
	cout << pdb << " : " << *pdb << endl;
	cout << (*(*pd)[2])(av, 3) << " : " << *(*(*pd)[2])(av, 3) << endl;
	return 0;
}

const double * f1(const double * ar, int n)
{
	return ar;
}

const double * f2(const double ar[], int n)
{
	return ar+1;
}

const double * f3(const double ar[], int n)
{
	return ar+2;
}

typedef를 이용한 단순화

typedef const double *(*p_fun)(const double *, int);
p_fun p1 = f1;
p_fun pa[3] = {f1, f2, f3};
p_fun (*pd)[3] = &pa;

C++에서는 auto 외에 typedef키워드를 통해 선언을 단순화할 수 있음
식별자로 가명을 선언하고 앞에 typedef 키워드를 삽입하는 것으로 원하는 데이터형을 가명으로 처리
입력한 가명을 저장하는 것 뿐만 아니라 코드 작성시의 오류를 줄이면서 더 쉽게 이해할 수 있도록 도와줌



연습문제

  1. 함수의 정의, 함수 원형 제공, 함수 호출
  2. a) void igor(void);
    b) float tofu(int);
    c) double mpg(double a, double b);
    d) long summation(long a[], int size);
    e) double doctor(const char []);
    f) void ofcourse(boss a);
    g) char * plot(map * a);
  3. void set_array(int* arr; int size; int key)
    {
    	for (int i = 0, i < size, i++)
    		arr[i] = key;
    }
  4. void set_array(int* start, int* end, int key)
    {
    	for (int* i = start; start < end; i++)
    		*i = key;
    }
  5. double big_num(const double arr[], int size)
    {
    	double max = 0;
    	for (int i = 0; i < size; i++)
    	{
    		if (arr[i] > max)
    			max = arr[i];
    	}
    	return max;
    }
  6. const 제한자는 포인터에 의해 지시되는 원본 데이터가 변경되지 않도록 보호하는데, 함수 호출시 기본 데이터형은 원본이 아닌 복사된 값으로 전달되므로 데이터가 항상 보호되기 때문에 const 제한자가 필요하지 않음
  7. char[] - char형 배열 / char * - 첫번째 문자를 지시하는 포인터 / abc - 문자열 상수
  8. int replace(char * str, char c1, char c2)
    {
    	int count = 0;
    
    	for (int i = 0; str[i]; i++)
    	{
    		if (str[i] == c1)
    		{
    			str[i] = c2;
    			count++;
    		}
    	}
    	return count;
    }
  9. *"pizza" : 첫 번째 원소의 값인 p / "taco"[2] : c
    문자열 상수는 배열 이름과 같은 역할을 함
  10. 값으로 전달시 구조체의 이름을 전달, 주소 전달시 주소 연산자 &를 구조체 이름 앞에 붙여 전달
    값으로 전달시 복사하여 사용하기 때문에 원본을 건드리지 않지만 복사하는데 시간과 메모리가 사용된다는 단점이 있음
    주소로 전달시 원본을 직접 건드리기 때문에 시간과 메모리를 절약할 수 있으나 원본 데이터가 보호되지 않음
  11. int judge(int (*pt)(const char *));
  12. a)
void show(applicant a)
{
	cout << a.name << endl;
	for (int i = 0; i < 3; i++)
		cout << a.credit_ratings[i] << endl;
}

b)

void show(applicant * a)
{
	cout << a->name << endl;
	for (int i = 0; i < 3; i++)
		cout << a->credit_ratings[i] << endl;
}
  1. typedef void * (*func1)(applicant *);
    typedef const char * (*func2)(const applicant *, const applicant *);
    
    func1 p1 = f1;
    func2 p2 = f2;
    func1 ap[5];
    func2 (*pa)[10];

 

'개인공부 > C++ 기초플러스' 카테고리의 다른 글

C++ Primer 08  (0) 2023.02.19
C++ Primer 07 Exercise  (0) 2023.02.19
C++ Primer 06 Exercise  (0) 2023.02.18
C++ Primer 06  (0) 2023.02.18
C++ Primer 05 Exercise  (0) 2023.02.18

댓글