오늘 학습 진행 내용
게임 개발자를 위한 C++ 문법
챕터1-4 : 포인터와 레퍼런스
포인터 변수의 연산
포인터의 모든 연산은 주소값과 관련
포인터 변수는 역참조 연산자(*)를 사용해서 해당 주소값에 위치한 메모리에 실제 값을 가져오는 것 가능
배열 선언 시 배열의 이름은 배열의 시작주소를 가짐.
마찬가지로 역참조 연산자(*)를 사용해서 값 가져오는 것 가능.
배열에 대한 포인터 선언 시그래서 배열의 시작 주소를 저장.
포인터 연산 시 해당 메모리 주소 이동을 통해서 다음 메모리의 값 가져오는 것 가능 배열에서는 0번 인덱스의 배열 첫 값 주소가 ptr 이면 1번 인덱스의 값은 ptr+1
포인터 사용 예제
#include <iostream>
using namespace std;
int main() {
//일반 변수 사용
int a =10; // 변수 a 선언 및 초기화
int b = a; // 변수 b에 a의 값 복사
cout<< "초기값 - a : " << a<< ", b: " << b << endl;
b=20;
cout<<"변경 후 - a : " << a<< ",b : " << b << endl;
//배열에서 포인터 사용
int arr[3] = {10,20,30}; // 배열 선언 및 초기화
int *p = arr;
cout << "p가 가리키는 값: " << *p << endl; // 배열의 첫번째 요소 출력
cout <<"p+1이 가리키는 값: " << *(p+1) << endl;
cout <<"p+2이 가리키는 값: " << *(p+2) << endl;
//포인터 변수의 실제 크기와 값 비교
int x = 3;
char y= 'A';
int* ptr1 = &x; //x의 주소를 가리키는 포인터
char* ptr2 = &y; //y의 주소를 가리키는 포인터
int* np = nullptr; // 널 포인터로 초기화, 안전한 포인터 사용을 위해 빈 포인터 지정
cout << "int형 변수의 크기:" << sizeof(x) <<" 바이트" << endl;
cout << "char형 변수의 크기:" <<sizeof(y) <<" 바이트" << endl;
cout << "int형 포인터의 크기:" <<sizeof(ptr1) <<" 바이트" << endl;
cout << "char형 포인터의 크기:" <<sizeof(ptr2)<<" 바이트" << endl;
cout <<"x의 주소 : "<< &x <<", ptr1 : " << ptr1 << endl;
cout <<"y의 주소 : "<<&y <<", ptr2 : " << ptr2 << endl;
cout<<"포인터 ptr1이 가리키는 값" << *ptr1 << endl;
cout<<"포인터 ptr2가 가리니큰 값" << *ptr2 << endl;
cout<<"변경 전 x :" << x << endl;
*ptr1 = 100; //포인터를 통해 x의 값 변경
cout <<"변경 후 x :" << x << endl;
if (np == nullptr) {
cout << sizeof(np)<< " 바이트 크기의 포인터입니다." << endl;
cout << "포인터가 아직 어떤 변수도 가리키지 않습니다." << endl;
}
return 0;
}
배열 포인터 사용 예제
#include <iostream>
using namespace std;
int main() {
int arr[5] = {10,20,30,40,50}; // 배열 선언 및 초기화
//배열 요소 인덱스로 접근
cout<< "배열 요소 출력 :";
for(int i=0; i<5; i++){
cout<< "arr[" << i << "] : " << arr[i] << endl;
}
//배열 요소 포인터로 접근
cout << "포인터로 배열 요소 출력 :";
int* p = arr; //배열 이름은 배열의 첫번째 요소의 주소를 가리킴
for (int i=0; i<5; i++){
cout << "*(p + " << i << ") : " << *(p + i) << endl;
}
//포인터 배열 선언
int a= 10, b=20, c=30;
int* ptrrr[3] = {&a,&b,&c}; //포인터 배열 선언 (각 원소가 int* 타입으로 지정)
cout << "포인터 배열을 통한 값 출력 :" << endl;
for (int i=0; i<3; i++){
cout << "*ptrrr[" << i << "] : " << *ptrrr[i] << endl;
}
//배열 전체 가리키는 포인터
int p_arr[3] = {100,200,300}; // 배열 전체를 가리키는 포인터
int (*ptrarr)[3] = &p_arr; //배열 전체를 가리키는 포인터 선언
cout << "배열 전체를 가리키는 포인터를 통한 값 출력 :" << endl;
for (int i=0; i<3; i++){
cout << "(*ptrarr)[" << i << "] : " << (*ptrarr)[i] << endl;
}
}
레퍼런스
레퍼런스는 선언과 동시에 초기화해야되서 다른 대상과 재연결 불가, 포인터는 선언 후 대상 변경 가능
레퍼런스는 Null 존재 X, 포인터는 nullptr이 존재
포인터는 주소값 담으므로 접근할 때는 * 사용, 주소 가져올 때는 &
하지만 레퍼런스 변수 자체 별명으로 일반 변수와 연산 방법 동일
상수 레퍼런스
상수제약 걸어서 읽기 전용으로 사용
const int& cref = x; //복사과정 없이 x의 값을 읽을 수 있지만 x 값 수정 불가
참조 사용 예제
#include <iostream>
using namespace std;
int main(){
//기본적인 레퍼런스 사용
int var = 10;
int& ref = var; // var의 레퍼런스 선언(중요 사항: 레퍼런스는 선언과 동시에 반드시 초기화)
cout <<"초기 값:" <<endl;
cout <<"var:" <<var << endl;
cout <<"ref:" << ref << endl;
ref = 20;
cout <<"ref 값을 변경한 후:" <<endl;
cout<<"var:" << var <<endl;
cout<<"ref:" << ref << endl;
//포인터와 레퍼런스의 차이점
//포인터는 선언 후 나중에 다른 변수를 가리킬 수 있음.
int a = 10;
int b = 20;
int* ptr = &a;
ptr = &b;
int& ref_a = a;
cout << ref_a<<endl;
//ref_a = &b; // 오류 발생!(레퍼런스는 다른 변수에 재할당 불가)
return 0;
}
포인터 사용 실습
#include <iostream>
using namespace std;
int main(){
//1. 정수형 변수 value를 선언하고 25라는 값을 할당하세요.
int value = 25;
//2. value 변수의 주소를 저장하는 포인터 변수 ptr을 선언하세요.
int* ptr = &value;
//3. 포인터 ptr을 사용하여 value 변수의 값을 출력하세요.
cout << "포인터를 통해 접근한 value의 값: " << *ptr<<endl;
return 0;
}
참조 사용 실습
#include <iostream>
using namespace std;
int main(){
//1. 정수형 변수 score를 선언하고 80이라는 값을 할당하세요.
int score = 80;
//2. score 변수를 참조하는 레퍼런스 변수 score_ref를 선언하세요.
int& score_ref = score;
//3. 레퍼런스 score_ref를 사용하여 score 변수의 값을 출력하세요.
cout << "레퍼런스를 통해 접근한 score의 값: "<< score_ref <<endl;
//4. 레퍼런스 score_ref를 사용하여 score 변수의 값을 95로 변경하세요.
score_ref = 95;
//5. score 변수의 값이 실제로 변경되었는지 확인하기 위해 score 변수 자체를 출력하세요.
cout << "레퍼런스 변경 후 score의 값 : " << score << endl;
return 0;
}
챕터1-5 : Class 개념
클래스에서 필요한 동작들은 멤버 범수로 정의, 보통 멤버 함수는 외부에서도 접근하는 경우에 많이 사용
세부 데이터는 멤버 변수로 관리, 멤버 함수로 필요한 정보라든지, class 자체에서 내부 연산 시 필요한 정보를 멤버 변수로 관리,외부에서 접근하지 못하게 하는경우에 사용
클래스의 멤버 함수나, 멤버 변수에 접근할 때는 객체 뒤 멤버 접근 연산자 . 를 사용
C++ 접근 지정자는 public, private, protected 존재
접근 지정자 명시안된 경우 보통 private로 설정
private 멤버는 클래스 외부에서 직접 접근할 경우 컴파일 에러
public 멤버는 클래스 외부에서 멤버 접근 연산자로 접근 가능
보통 생성자는 필요한 멤버 변수 초기화, 객체 동작 준비 시 사용, 반환형 명시X, cl,ass 이름과 동일한 이름을 가진 함수로 정의
정의된 class를 변수로 선언하면 해당 객체 메모리에 올라감
(인스턴스화)
클래스 헤더 파일과 소스 파일 구분 짓는 이유
여러 파일로 나누어 작성하는 편이 유지 보수성, 가독성 면에서 유리
헤더파일을 선언 부, 소스 파일을 구현부로 작성
헤더파일 정의 시 중요한 것은 class가 중복 선언되지 않도록 하는 것 #ifndef 구문 활용해서 처리
클래스 사용 예제
#include <iostream>
#include <algorithm> //max 함수 사용
#include <string>
#include "Student.h" //Student 헤더 파일 정의 사용
using namespace std;
// //클래스 없이 구현한 성적관리 프로그램(문제점 성적관리 하는 데이터가 모두 노출 되어 있고 함수에 데이터와 다른코드가 혼용)
// //3과목의 평균을 구하는 함수
// double getAvg(int kor, int eng, int math){
// return (kor+eng+math)/ 3.0f;
// }
// //두 개의 수 중 최대 값을 반환하는 함수
// int maxNum(int num1, int num2){
// if(num1>=num2){
// return num1;
// }
// else{
// return num2;
// }
// }
// //3과목 중 가장 높은 점수를 반환 하는 함수
// int getMax(int kor, int eng, int math){
// return(maxNum(maxNum(kor,eng),math)); //두 점수 최대값 비교 후 나머지 점수 비교해서 최대값 반환
// }
// int main(){
// int kor[3];
// int eng[3];
// int math[3];
// for(int i=0; i<3; i++){
// cin>>kor[i]>>eng[i]>>math[i];
// }
// for(int i=0; i<3; i++){
// cout<<getAvg(kor[i],eng[i],math[i])<<endl;
// cout<<getMax(kor[i],eng[i],math[i])<<endl;
// }
// return 0;
// }
class Student
{
//동작 정의(이를 멤버함수라고 합니다),public 선언 외부에서 접근 가능
//getter와 setter를 사용함으로써 멤버 변수를 바꾸거나 값 가져올 때 변수를 직접 제어하지 않고 데이터 안전하게 다룰 수 있음
public:
double getAvg();
int getMaxScore();
void setMathScore(int math){
this->math = math;
}
void setKorScore(int kor){
this->kor = kor;
}
void setEngScore(int eng){
this->eng = eng;
}
int getMathScore() {return math;}
int getKorScore() {return kor;}
int getEngScore() {return eng;}
//생성자 정의 클래스 호출 시 한번씩 자동 호출되는 멤버 변수
Student(int kor=40, int math=40, int eng=40){
kor = kor;
math = math;
eng = eng;
}
//데이터 정의(이를 멤버변수라고 합니다.), private 선언 외부에선 접근 불가능
private:
int kor;
int eng;
int math;
};
double Student::getAvg()
{
return (kor + eng + math) / 3.0;
}
int Student::getMaxScore()
{
return max(max(kor, eng), math);
}
double Header_Student::getAvg(){
return (kor + eng + math) / 3.0;
}
int Header_Student::getMaxScore()
{
return max(max(kor, eng), math);
}
int main(){
//생성자 기본값 지정 시 해당 형태처럼 특정 값 지정안하거나 모두 지정 안하더라도 선언 가능
Student s;
Student s1(1);
Student s2(1,2);
Student s3(1,2,3);
Header_Student s4(100,70,80); //헤더 정의된 스튜던트 클래스 사용
s.setEngScore(100);
s.setKorScore(70);
s.setMathScore(80);
cout<<"헤더 없이 클래스 적용 결과 : " <<endl;
cout<<s.getAvg()<<endl;
cout<<s.getMaxScore()<<endl;
cout <<"헤더 사용 클래스 적용 결과 : " <<endl;
cout<<s4.getAvg()<<endl;
cout<<s4.getMaxScore()<<endl;
return 0;
}
Studnet.h(헤더 사용 예제)
#ifndef STUDENT_H_ // #ifndef STUDENT_H_의 의미는 STUDENT_H_가 정의되어 있지 않은 경우 아래 코드 수행
#define STUDENT_H_ //#ifndef일 때만 #define이 수행되므로 단 한 번만 수행
class Header_Student
{
//동작 정의(이를 멤버함수라고 합니다),public 선언 외부에서 접근 가능
//getter와 setter를 사용함으로써 멤버 변수를 바꾸거나 값 가져올 때 변수를 직접 제어하지 않고 데이터 안전하게 다룰 수 있음
public:
double getAvg();
int getMaxScore();
//생성자 정의 클래스 호출 시 한번씩 자동 호출되는 멤버 변수
Header_Student(int kor=40, int math=40, int eng=40){
this->kor = kor;
this->math = math;
this->eng = eng;
}
//데이터 정의(이를 멤버변수라고 합니다.), private 선언 외부에선 접근 불가능
private:
int kor;
int eng;
int math;
};
#endif //#ifndef가 끝났다는 것을 알려주는 코드
클래스 사용 실습
Battery.h
#ifndef BATTERY_H_ //클래스 중복정의 방지를 위한 코드 BATTERY_H_가 선언되어 있지 않을 때만 아래 코드 수행
#define BATTERY_H_ //BATTERY_H_ 정의 되어 있지 않을 경우 아래 코드 한번만 실행
#include <iostream>
class Battery {
public:
//배터리 생성자 처음 초기 잔량 기본 값 100으로 설정
Battery(int initialCharge = 100) {
if (initialCharge < 0) { //잔량이 마이너스가 될 수 없으므로 초기 잔량 마이너스로 설정될 경우 잔량 0으로 설정
charge = 0;
} else if (initialCharge > 100) { //잔량이 100을 넘어 설 경우 max인 잔량 100으로 설정
charge = 100;
} else {
charge = initialCharge; //나머지 케이스의 경우 잔량을 초기 잔량으로 설정
}
}
int getBattery() const { //잔량 값 불러오는 함수로 잔량에 대한 변경 없이 값만 불러오면 되므로 const 처리(상수)
return charge;
}
void useBattery() { //배터리 사용에 대한 함수로 잔량이 5이상일 경우 잔량에서 -5 감소
if (charge >= 5) {
charge -= 5;
} else { // 아닌 경우 감소하더라도 잔량0으로 표기
charge = 0;
}
std::cout << "Battery used. Current charge: " << charge << "%\n"; //배터리 사용됨 표시와 현재 잔량 몇인지 출력
}
void plusBattery() { // 배터리 충전에 대한 함수로
if (charge + 7 >= 100) { //배터리 충전 시 100 이상 일 경우 잔량 100으로 표기
charge = 100;
} else { //그 외의 케이스일 경우 배터리 충전 시 7씩 플러스
charge += 7;
}
std::cout << "Battery charged. Current charge: " << charge << "%\n"; //배터리 충전 표시와 현재 잔량 몇인지 출력
}
private:
int charge;
};
#endif//#ifndef 종료 코드
Battery.cpp
#include <iostream>
#include "Battery.h"
using namespace std;
int main() {
// 배터리 객체 생성 (초기 잔량 100)
Battery battery;
cout << "Initial charge: " << battery.getBattery() << "%\n";
// 배터리 사용 및 충전 테스트
battery.useBattery(); // 잔량 95%
battery.useBattery(); // 잔량 90%
battery.plusBattery(); // 잔량 97%
battery.useBattery(); // 잔량 92%
return 0;
}
클레스 사용 실습2
Fraction.h
#ifndef FRACTION_H_ //클래스 중복선언 방지, FRACTION_H_ 정의되어 있지 않을 때 아래 코드 실행
#define FRACTION_H_ //FRACTION_H_ 정의되어 있지 않을 경우 한번만 실행
#include <vector>
#include <algorithm>
//Fraction 클래스 헤더 정의
class Fraction{
public:
Fraction() : numerator(0), denominator(1) {} //기본 생성자 (분자=0, 분모=1)
Fraction(int numerator, int denominator) : numerator(numerator), denominator(denominator){
this->numerator=numerator;
this->denominator=(denominator!=0) ? denominator : 1; //분모는 0이되면 안되므로 0을 받았을 때는 1로 초기화
if(count>2){ //두 개의 분수를 받아서 곱하게 하기 위해서 2번째 까지만 받고 다시 초기화
count=0;
}
else{
count+=1;
}
}
void display() const { //분수 화면에 출력 count 전역변수에 따라 몇번째 분수인지 출력
std:: cout<<count<<" 번째 분수는 " <<numerator << "/" <<denominator <<"\n";
}
int gcd(int a, int b) const { //최대 공약수를 계산하는 유클리드 호제법 구현
while (b != 0) { //gcd 함수는 Fraction 클랙스 내부에서만 사용되므로 private로 설정(아무것도 안적으면 기본적으로 private 적용)
int r = a % b; //a를 b로 나눈 나머지 값을 r로 저장
a = b; //a를 b로 다시 설정
b = r; //b를 나머지 값 r로 설정
}
return a; //b가 0이 되었을 때의 a가 최대공약수로 반환
}
void simplify(){ //기약분수로 변환
int gcdValue = gcd(numerator,denominator); //분자와 분모의 최대공약수 gcdValue로 저장
numerator/=gcdValue; //분자를 최대공약수로 나눈 값 분자에 젖장
denominator/=gcdValue;//분모를 최대공약수로 나눈 값 분모에 저장
}
Fraction multiply(const Fraction& other) const //분수의 곱
{
int mul_numerator = numerator * other.numerator; // 분자끼리 곱
int mul_denominator = denominator * other.denominator; //분모끼리 곱
Fraction result(mul_numerator,mul_denominator); //곱 결과 분수로 저장 새로운 Fraction 객체
result.simplify(); //곱 결과 기약분수 형태로 전환
return result;
}
private:
int numerator; //분자
int denominator; //분모
static int count; //모든 객체가 공유하는 변수
};
#endif //ifndef 종료 코드
Fraction.cpp
#include<iostream>
#include "Fraction.h" //Fraction 클래스 헤더 가져오기
using namespace std;
int Fraction:: count=0; //Fraction 전역 count 변수 0으로 초기화(전역 변수는 cpp 내에서 초기화)
int main(){
Fraction f1(1,2); //첫번째 분수 선언 2분의1
f1.display(); //첫번째 분수 출력
Fraction f2(2,4); //두번째 분수 선언 4분의2
f2.display(); //두번째 분수 출력
Fraction f3 = f1.multiply(f2); //첫번째 분수와 두번째 분수의 곱 출력
f3.display();
}
CH2 학습 가이드
미니 실습
실습 제출코드
#include <iostream>
using namespace std;
struct Stats
{
int HP;
int MP;
};
void IncreaseHP_Value(Stats Stat)
{
//현재 코드 상 함수 인자로 Stats Stat을 전달 받고 있어서 Stat 구조체 통째로 카피본이 생성되서 변수로 전달
//원본과는 별개의 복사본에 대해서 변수로 다뤄지기 때문에
//해당 함수 내에서 더하더라도 원본에는 영향을 줄 수 없음.
//더하더라도 함수 내에서 더해지고 끝나는 상태
//여기서는 +10을 더합니다.
Stat.HP+=10;
// cout<<Stat.HP<<endl; //실제로 함수 내부에서 출력해보면 더해진 20 값 확인 가능
}
void IncreaseHP_Ptr(Stats* Stat)
{
//함수 인자로 Stat의 포언터를 받고 있어서 해당 원본의 주소값을 받아서 사용하므로
//해당 주소의 객체의 HP에 20을 더하는 형태로 원본에 값 더해주는 형태로 사용 가능
//여기서는 +20을 더합니다.
(*Stat).HP+=20; //또는 Stat->HP+=20; 도 가능(같은 기능 다른 표현)
}
void IncreaseHP_Ref(Stats& Stat)
{
//함수 인자로 Stat을 참조로 받아오기 때문에 원본 그자체를 가리키는 동일 구조체 가져오는 형태
//그래서 참조로 받아온 구조체 Stat의 HP에 30을 더하면 실제 원본에도 값 더해지는 형태로 사용 가능
//여기서는 +30을 더합니다.
Stat.HP+=30;
}
int main(int argc, char* argv[])
{
Stats stat;
stat.HP = 10;
stat.MP = 0;
IncreaseHP_Value(stat);
cout << "Value - HP: %d\n" << stat.HP << endl;
IncreaseHP_Ptr(&stat);
cout << "Ptr - HP: %d\n" << stat.HP << endl;
IncreaseHP_Ref(stat);
cout << "Stat - HP: %d\n" << stat.HP << endl;
return 0;
}
도전 실습
복사할 코드
#include <iostream>
using namespace std;
struct Stats
{
int HP;
int MP;
};
void IncreaseHP_Value(Stats Stat)
{
//여기서는 +10을 더합니다.
}
void IncreaseHP_Ptr(Stats* Stat)
{
//여기서는 +20을 더합니다.
}
void IncreaseHP_Ref(Stats& Stat)
{
//여기서는 +30을 더합니다.
}
int main(int argc, char* argv[])
{
Stats stat;
stat.HP = 10;
stat.MP = 0;
IncreaseHP_Value(stat);
cout << "Value - HP: %d\n" << stat.HP << endl;
IncreaseHP_Ptr(&stat);
cout << "Ptr - HP: %d\n" << stat.HP << endl;
IncreaseHP_Ref(stat);
cout << "Stat - HP: %d\n" << stat.HP << endl;
return 0;
}
실습 제출 코드
#include <iostream>
using namespace std;
struct Stats
{
int HP;
int MP;
};
void IncreaseHP_Value(Stats Stat)
{
//현재 코드 상 함수 인자로 Stats Stat을 전달 받고 있어서 Stat 구조체 통째로 카피본이 생성되서 변수로 전달
//원본과는 별개의 복사본에 대해서 변수로 다뤄지기 때문에
//해당 함수 내에서 더하더라도 원본에는 영향을 줄 수 없음.
//더하더라도 함수 내에서 더해지고 끝나는 상태
//여기서는 +10을 더합니다.
Stat.HP+=10;
// cout<<Stat.HP<<endl; //실제로 함수 내부에서 출력해보면 더해진 20 값 확인 가능
}
void IncreaseHP_Ptr(Stats* Stat)
{
//함수 인자로 Stat의 포언터를 받고 있어서 해당 원본의 주소값을 받아서 사용하므로
//해당 주소의 객체의 HP에 20을 더하는 형태로 원본에 값 더해주는 형태로 사용 가능
//여기서는 +20을 더합니다.
(*Stat).HP+=20; //또는 Stat->HP+=20; 도 가능(같은 기능 다른 표현)
}
void IncreaseHP_Ref(Stats& Stat)
{
//함수 인자로 Stat을 참조로 받아오기 때문에 원본 그자체를 가리키는 동일 구조체 가져오는 형태
//그래서 참조로 받아온 구조체 Stat의 HP에 30을 더하면 실제 원본에도 값 더해지는 형태로 사용 가능
//여기서는 +30을 더합니다.
Stat.HP+=30;
}
int main(int argc, char* argv[])
{
Stats stat;
stat.HP = 10;
stat.MP = 0;
IncreaseHP_Value(stat);
cout << "Value - HP: %d\n" << stat.HP << endl;
IncreaseHP_Ptr(&stat);
cout << "Ptr - HP: %d\n" << stat.HP << endl;
IncreaseHP_Ref(stat);
cout << "Stat - HP: %d\n" << stat.HP << endl;
return 0;
}
도전X도전 실습
기본적으로 함수의 파라미터의 갯수는 고정이에요.
가변 인자(Variable argument)라는 문법을 사용하면,
하나의 함수로도 다양한 갯수의 인자를 받을 수 있어요.
int calculateAverage(int score1, int score2,...)실습 제출 코드
#include <type_traits> // type traits(타입 정보) 관련 템플릿들을 사용하기 위한 헤더 (is_arithmetic,is_same,decay_t)
#include <iostream> // 입출력 스트림(cout, cin 등)을 사용하기 위한 헤더
using namespace std; // std:: 를 생략하고 is_arithmetic, cout 등을 바로 쓰기 위해 사용
// 가변 인자를 받아서, "숫자(정수/실수)만 골라 평균을 내는 함수 템플릿"
// - Args... : 호출 시 넣는 인자들의 타입들이 들어옴 (예: int, double, const char*, string, ...)
template<typename... Args>
double calculateAverage(Args... args) // 가변 인자로 평균을 계산하는 함수
{
double sum = 0.0; // 실제로 포함할 숫자 인자들의 합을 저장할 변수
size_t count = 0; // 실제로 포함한 숫자 인자들의 개수를 세는 변수
// 한 개의 인자 v를 처리하기 위한 람다 함수
// - auto&& v : 어떤 타입이 오든 받을 수 있는 "만능 참조(universal reference)"
// - [&] 캡처 : 바깥의 sum, count를 참조로 캡처해서 람다 안에서 수정 가능하게 함
auto process = [&](auto&& v) {
// decltype(v) : v 의 타입(참조 형태 포함)
// decay_t<decltype(v)> :
// - const, 참조(reference), 배열, 함수 타입 등을 "순수 값 타입"으로 바꿔줌
// - 예: const int& -> int, int& -> int, const char[6] -> const char*
using V = decay_t<decltype(v)>;
// if constexpr :
// - C++17부터 도입된 문법
// - 조건이 "컴파일 시간에" 평가됨
// - 조건이 false이면, 그 블록 안의 코드는 아예 컴파일 대상에서 제거됨(false일 경우 컴파일 에러 발생하더라도 발생이 안됨 걸러져서)
// else 문이 없어도 됨: 조건이 false 이면 그냥 아무 것도 안 하고 넘어감
// if constexpr 이라서, 숫자가 아닌 타입의 경우 sum += ... 같은 코드가 실행 안됨
if constexpr (
is_arithmetic<V>::value && // 1) V가 산술 타입인지? (int, float, double 등)
!is_same<decay_t<V>, char>::value && // 2) V가 char 인지 아닌지 체크 → char 이면 제외
!is_same<decay_t<V>, signed char>::value && // 3) V가 signed char 인지 아닌지 체크 → 제외
!is_same<decay_t<V>, unsigned char>::value // 4) V가 unsigned char 인지 아닌지 체크 → 제외
) {
// "숫자 타입이고 char 계열이 아닌 타입"인 경우에만 해당 부분 실행
// v 값을 double 로 변환해서 sum 에 더해줌
// - int, float, long 등 어떤 숫자 타입이든 double 로 승격해서 더함
sum += static_cast<double>(v);
// 숫자로 인정된 인자의 개수를 1 증가
++count;
}
};
// (process(args), ...) : C++17의 fold expression (왼쪽 fold)
// - Args... 가 예를 들어 (1, "hello", 3.5) 라면
// - (process(args), ...) 은
// process(1), process("hello"), process(3.5);
// 처럼 순서대로 호출하는 효과
// - 즉, 모든 인자들 각각에 대해 process 를 실행
(process(args), ...);
// count 가 0 이면 나눌 숫자가 없다는 뜻이므로
// - 여기서는 그냥 0.0 을 반환
return (count > 0) ? (sum / count) : 0.0;
}
int main() {
// 모두 정수 → 1,2,3,4,5 의 평균 (= 3.0) 반환
auto avg1 = calculateAverage(1, 2, 3, 4, 5);
// 실수/정수/float/음수 모두 is_arithmetic에 true로 걸려서 모든 수에 대한 평균 반환
auto avg2 = calculateAverage(1.5, 2, 3.7f, -4);
//1,9.5만 is_arithmetic에 true로 걸려서 해당 두수에 대한 평균 반환
auto avg3 = calculateAverage(1, "hello", 'A', 9.5, string("test"));
// 계산된 평균 값들을 출력
cout << avg1 << "\n"; // 3
cout << avg2 << "\n"; // (1.5 + 2 + 3.7 + (-4)) / 4
cout << avg3 << "\n"; // (1 + 9.5) / 2 = 5.25
return 0;
}