친구 함수와 클래스
여러개의 클래스 복잡하게 상호작용하면 클래스간의 상호작용을 깔끔히 정리하기 어렵다.
이때 친구함수와 친구클래스를 사용한다.

클래스안에 함수를 friend로 선언한다.

A,B 두 class가 있다고 생각해보자.
A class에 B class를 friend로 선언하게되면
B class에서 A class private:(캡슐화)에 관여가 가능하게된다.

friend선언은 class를 통째로도 가능하고 일부함수만 따로도 가능하다.

friend함수 선언할때 함수의 생성되는 위치를 잘보고 구현해야한다.

 

#include <iostream>

using namespace std;
//전방선언
class A;

class B
{
private:
	int m_val = 2;

public:
	//A가 선언되어 있기에 A를 사용가능하다.
	void doSomething(A& a);
};

class A
{
private:
	int m_val = 1;
	//class A가 생성될떄 B를 알 수없다.
	//class통쨰로 선언
	//friend class B;
	//B Class가 먼저 선언되어 있어 함수를 구현할 수 있다.
	//class일부함수만 선언
	friend void B::doSomething(A& a);
};

//A에 m_val변수가 있는지 확인 불가능하기때문에 A Class가 구현된후 함수 구현
void B::doSomething(A& a) {
	a.m_val = 3;
	cout << a.m_val << endl;
}

int main()
{
	A a;
	B b;
	b.doSomething(a);
	

	return 0;
}

정적멤버변수 (static)

정적변수는 한번선언되면 초기화하지않고 계속 증가한다.

정적멤버변수도 마찬가지로 한 메모리 주소를 보고 있기에
인스턴스를 몇번이고 생성해도 같은 변수주소를 보고 있다.

static만 선언하면 변수를 클래스 밖에서 초기화해주어야하고
static const로 선언하면 클래스안에서 초기화가 가능하다. 

 

#include <iostream>

using namespace std;

int generateID() {
	static int s_id = 0;
	cout << &s_id << endl;
	return ++s_id;
}

class Something
{
public:
	static int s_value;
};

int Something::s_value = 1;

int main()
{
	//계속 늘어나는걸 볼 수 있다.
	cout << generateID() << endl;
	cout << generateID() << endl; 
	cout << generateID() << endl;

	Something st1;
	Something st2;

	st1.s_value = 2;

	cout << &st1.s_value << " " << st1.s_value << endl;
	cout << &st2.s_value << " " << st2.s_value << endl;
	return 0;
}

정적멤버함수

정적멤버함수와 정적멤버변수와 비슷한듯 다르다.

정적멤버변수,함수는 인스턴스로 정의되기전 클래스 접근 또는
모든 인스턴스에서 접근이가능하다. (메모리공유)

정적멤버함수는 this를 사용하지 못한다.
정적멤버함수는 정적멤버변수만 리턴할 수 있다.

#include <iostream>

using namespace std;

class Something
{
private:
	static int m_value;
	int p_value = 1024;
public:
	Something() {
	}
	
	static int getValue() {
		return m_value+1;
	}

	int getPValue() {
		return this -> p_value + this ->m_value;
	}

};

int Something::m_value = 1024;

int main()
{
	cout << Something::getValue << endl;
	cout << Something::getValue() << endl;

	Something st;
	cout << st.getValue() << endl;

	//멤버함수
	int(Something::*ptr)() = &Something::getPValue;
	//정적멤버함수
	int (*ptr2)() = Something::getValue;

	cout << "=============" << endl;
	cout << (st.*ptr)() << endl;
	cout << (*ptr2)() << endl;

	return 0;
}

클래스와 const

const는 변수를 상수로 만들고 싶을때 사용한다.

클래스의 인스턴스(변수,오브젝트)를 생성할때 const를 선언하면 

인스턴스(변수,오브젝트)는 상수이기때문에 내부의 값을 변경할 수 없다.


보통 멤버함수로는 내부의 값을 변경하지 않더라도 값을 가져오는 함수도 사용할 수 없다.

	const Something something;
	//상수이기 떄문에 에러가 난다.
	//something.setValue(1);


사용할려면 컴파일러가 판단할때는 멤버함수 뒤에 const를 선언해줘야한다.
멤버함수에 const를 선언하면 컴파일러에게 완벽히 const라고 확인해주는 것이다.

	//const를 선언해주어야한다.
	int getValue() const {
		return m_value;
	}



인스턴스를 파라미터로 복사하게되면 클래스 내부에 숨어져있는 복사 컨스트럭터가 인스턴스를 복사해준다.

	//복사 컨스트럭터
	Something(const Something& st_in) {
		m_value = st_in.m_value;
	}


인스턴스(오브젝트,객체) 파라미터도 const 클래스명&를 해주면 복사하지 않는다.

또 const로 파라미터구분없이 오버로딩을 할 수 있다.

	string& getStrValue() {
		return m_StrValue;
	}

	const string& getStrValue() const{
		return m_StrValue;
	}

 

코드정리

#include <iostream>
#include <string>

using namespace std;

class Something
{
public:
	int m_value = 0;
	string m_StrValue = "default";

	//복사 컨스트럭터
	Something(const Something& st_in) {
		cout << "copy Constructor" << endl;
		m_value = st_in.m_value;
	}
	Something() {
		cout << "Constructor" << endl;
	}

	void setValue(int value) {
		m_value = value;
	}
	//const를 선언해주어야한다.
	int getValue() const {
		return m_value;
	}
	
	string& getStrValue() {
		return m_StrValue;
	}

	const string& getStrValue() const{
		return m_StrValue;
	}

};

void print(Something st) {
	cout << &st << endl;
	cout << st.getValue() << endl;
}

void print2(const Something& st) {
	cout << &st << endl;
	cout << st.getValue() << endl;
}

int main()
{
	const Something something;
	//상수이기 떄문에 에러가 난다.
	//something.setValue(1);
	//클래스안 멤버함수에 const를 선언해주어야한다.
	something.getValue();

	cout << "=====================================" << endl;
	//복사 컨스트럭터 작동
	Something something2;
	cout << &something2 << endl;
	print(something2);

	cout << "=====================================" << endl;
	//const 파라미터
	cout << &something2 << endl;
	print2(something2);

	cout << "=====================================" << endl;

	//string 테스트
	something2.getStrValue() = "Test";
	cout << something2.getStrValue() << endl;

	const Something something3;
	//변경불가
	//something3.getStrValue() = "TEST";
	cout << something3.getStrValue() << endl;
	return 0;
}

소멸자 Destructor

변수가 영역을 벗어나 사라질때 호출이되는 함수

소멸자를 선언할땐 ~클래스명()으로 선언한다.

//IntArr클래스의 소멸자 생성
public:
	//소멸자 Destructor
	//지역을 벗어날때 자동으로 실행된다.
	~IntArr() {
		if (m_arr != nullptr) {
			delete[] m_arr;
		}


동적할당을 받을때 자주 사용한다.
반복문에서 동적할당을 할때 delete를 소멸자로 선언해 놓으면 자동으로 메모리를 반납한다.

#include <iostream>

using namespace std;

class Simple
{
private:
	int m_id;
public:
	Simple(const int& id)
		:m_id(id) {
		cout << "Constructor" << m_id << endl;
	}

	~Simple() {
		cout << "Destructor" << m_id << endl;
	}
};

class IntArr
{
private:
	int *m_arr = nullptr;
	int m_length = 0;

public:
	IntArr(const int length_in) {
		m_length = length_in;
		m_arr = new int(length_in);
	}
	//소멸자 Destructor
	//지역을 벗어날때 자동으로 실행된다.
	~IntArr() {
		if (m_arr != nullptr) {
			delete[] m_arr;
		}
	}

	int size() {
		return m_length;
	}
};
int main()
{
	//{}지역을 벗어날때 자동으로 소멸자가 자동으로 실행
	Simple *s1 = new Simple(0);
	Simple s2(1);
	Simple s3(2);
	delete s1;

	return 0;
}

this포인터

클래스를 붕어빵찍는 틀을 비유한다.
생성된 붕어빵은 인스턴스이다.

각인스턴스들을 구분하는 방법

this포인터는 클래스안에 숨어있다.
숨어있는 this로 자기 자신의 주소를 알 수 있다.

 

클래스안에 함수들에 this가 선언되어있지만 안보이는것이다.

	void setId(int id) {
    	//this -> m_id = id;
		m_id = id;
	}


클래스안에 함수들은 클래스가 선언될때마다 만들지 않는다.
함수가 하나만 선언되고 메모리주소와 설정한 인자값을 넣어주는 구조이다.


연쇄호출 Chaining Member Functions
체이닝함수

this포인터를 이용해 자기자신을 다시 호출한다.
체이닝함수는 한번에 여러번 선언할 수 있다.

#include <iostream>

using namespace std;

class Simple
{
private:
	int m_id;
public:
	Simple(const int &id)
		:m_id(id) {
		cout <<"this : "<< this << endl;
		//->포인터에 값을 넣는것
		this->m_id;
	}
	void setId(int id) {
    	//아래 this->가 생략되어 보이는것이다.
        //this->m_id =3;
		m_id = id;
	}
	int getId() {
		return m_id;
	}
	//체이닝 함수
	Simple& nextId() {
		m_id += 1;
		return *this;
	}
	void print() {
		cout << "nextId : " << m_id << endl;
	}
};
int main()
{
	Simple s1(1), s2(2);
	s1.setId(3);
	s2.setId(4);
	s2.print();
	/*
	체이닝함수 동작의 이해
	Simple& sim = s2.nextId();
	sim = s2.nextId();
	sim = s2.nextId();*/
	s2.nextId().nextId().nextId().print();

	cout << "&s1 : " << &s1 << " &s2 : " << &s2 << endl;
	return 0;
}

 

생성자의 멤버들을 초기화
Member initializer list

생성자 선언할때 :를 이용해 선언한다.

public:
	Something()	//우선순위가 가장 높다.
		: m_i(1), m_d(3.14), m_c('a') ,m_b(m_i-1)
	{
	}


생성자 선언이 멤버선언시 기본값선언, 생성자에 변수에 값대입 보다 가장 우선순위가 높다.

#include <iostream>

using namespace std;
class B
{
private:
	int m_b;
public:
	B(const int& m_b_in)
		: m_b(m_b_in) {

	}
};
class Something
{
private://생성자 초기화가 있을시 기본값설정은 무시된다.
	int m_i = 100;
	double m_d = 200.1;
	char m_c = 'b';
	B m_b{ 2 };

public:
	Something()	//우선순위가 가장 높다.
		: m_i(1), m_d(3.14), m_c('a') ,m_b(m_i-1)
	{//생성자 초기화 선언후 동작한다.
		m_i *= 3;
		m_d *= 3;
	}
	void print() {
		cout << m_i << " " << m_d << endl;
	}
};

int main() {
	Something s;
	s.print();

	return 0;
}

위임생성자
Delegathing Constructors
생성자가 다른생성자를 사용하는것을 말한다.
파라미터가 여러개일때 자주 사용한다.

객체지향은 초기화,인풋은 하나인게 좋다.

과거엔 초기화함수(ex:init)를사용했으나 최근 c++에선 위임생성자를 지원해준다.

 

#include <iostream>
#include <string>

using namespace std;

class Student
{
private:
	int		m_id;
	string	m_name;

public:

	Student(const string& name)
		: Student(0,name) //위임생성자
	{

	}
	Student(const int& id, const string& name)
		:	m_id(id),
			m_name(name) 
	{
		//과거엔 초기화함수를 사용했다.
		init(id, name);
	}

	//초기화함수
	void init(const int& id, const string& name) {
		m_id = id;
		m_name = name;
	}

	void print() {
		cout << "id : " << m_id << " name : " << m_name << endl;
	}
	
};
int main()
{
	Student st1(0, "Choi");
	st1.print();

	//위임생성자
	Student st2("Choi");
	st2.print();
	return 0;
}

생성자 Constructor
객체를 설계하면 객체가 생성될때 객체가 생성되자마자 기능(값설정)을 수행해야하는 때가있다.
이때 생성자를 사용한다.

private으로 캡슐화된 클래스 변수의 기본값을 정의한다.
변수에 기본값을 선언해줄 수 있지만 기본값이 매번 달라질 수 있다.

반환 리턴타입이 없고 클래스와 이름이 같으면 생성자이다.

public:
	//기본생성자
	Fraction()
	{
		m_numerator = 0;
		m_denominator = 1;
	}
    


자동실행
생성자를 선언해놓으면 클래스가 메모리가 올라가 인스턴스가 될때 생성자를 실행한다.

생성자도 함수인데 파라미터가 없다면 ()를 하면안된다.
파라미터에 default value도 선언가능하다.
모든 파라미터에 default value를 선언하면 기본생성자를 생성하지 못한다.



개발자가 생성자를 선언안하면 클래스안에 default생성자(기본)가 숨어있다.

 

클래스안에 다른 멤버클래스가 선언되어있으면 멤버클래스가 먼저 생성된다.

class Second
{
public:
	Second() {
		cout << "Second" << endl;
	}
};

class First
{
	//멤버클래스가 먼저생성된다.
	Second sec;

public:
	First() {
		cout << "First" << endl;
	}
};


생성자를 선언하면 {}초기화가 가능해진다. {}은 형변환이 지원되지 않는다.

#include <iostream>

using namespace std;

class Second
{
public:
	Second() {
		cout << "Second" << endl;
	}
};

class First
{
	//멤버클래스가 먼저생성된다.
	Second sec;

public:
	First() {
		cout << "First" << endl;
	}
};
class Fraction
{
private:
	//분자
	int m_numerator;
	//분모
	int m_denominator;

public:
	//기본생성자
	Fraction()
	{
		m_numerator = 0;
		m_denominator = 1;
	}
	Fraction(const int& n, const int& d)
	{
		m_numerator = n;
		m_denominator = d;
	}
	void print()
	{
		cout << m_numerator << " / " << m_denominator << endl;
	}

};

int main()
{
	Fraction frac(1.0,3);
	frac.print();
	//기본생성자는 ()가 없다.
	Fraction frac2;
	frac2.print();

	//frac와 다르게 1.0으로 초기화되지않는다.
	Fraction frac3 = Fraction{ 1,2 };
	frac3.print();

	//class안에 class생성순서확인
	First f;
	return 0;
}

캡슐화 Encapsulation
접근 지정자 Access Specifiers 
접근함수Access Functions

큰프로그램을 개발하게 되면 내용이 복잡해진다.
많은변수,많은함수,많은클래스가 사용된다.

뛰어난 프로그래머는 복잡해보이는걸 단순하게 정리잘한다.
재사용을 강조한다.

접근 지정자 Access Specifiers 
public 밖에서 접근이 가능하게한다.
private 밖에서 접근하는걸 막아준다.(기본으로 지정)

private으로 접근지정자가 지정된 클래스는 access Functions(접근함수)를 만들어 주어야한다.
클래스 내부안에 access Functions는 private 변수나 함수에 접근 가능하다.

class cDate
{
//private 밖에서 접근이 불가능하다.
private:	//access specifier(접근지정자)
	int m_month;
	int m_day;
	int m_year;
//access functions 생성
public:
	void setDate(const int &m, const int &d, const int &y)
	{
		m_month = m;
		m_day = d;
		m_year = y;
	}
}

같은 클래스로 생성된 인스턴스는 같은 클래스의 인스턴스에 접근이 가능하다.

class cDate
{
private:	//access specifier(접근지정자)
	int m_month;
	int m_day;
	int m_year;
//access functions 생성
public:
	void copyFrom(const cDate& original) {
		//original안에 변수들은 private이다.
		//같은 클래스 변수접근
		m_month = original.m_month;
		m_day = original.m_day;
		m_year = original.m_year;
	}
};


캡슐화장점
public로 생성 밖에서 변수에 접근해 작업하면 클래스안에 변수가 변경되면 변경해야할 코드가 엄청많아진다.

 

#include <iostream>

using namespace std;


struct sDate
{
	int m_month;
	int m_day;
	int m_year;
};

class cDate
{
private:	//access specifier(접근지정자)
	int m_month;
	int m_day;
	int m_year;
//access functions 생성
public:
	void setDate(const int &m, const int &d, const int &y)
	{
		m_month = m;
		m_day = d;
		m_year = y;
	}

	const int& getMonth() {
		return m_month;
	}

	const int& getDay() {
		return m_day;
	}

	const int& getYear() {
		return m_year;
	}

	void copyFrom(const cDate& original) {
		//original안에 변수들은 private이다.
		//같은 클래스 변수접근
		m_month = original.m_month;
		m_day = original.m_day;
		m_year = original.m_year;
	}
};

int main()
{
	//struct는 바로접근이 가능하다.
	sDate toDay{ 8,4,2025 };
	toDay.m_month = 8;
	toDay.m_day = 4;
	toDay.m_year = 2025;

	//클래스는 바로 접근할 수 없다.
	//접근하려면 public 접근지정자를 선언해주어야한다.
	cDate cToDay;
	cToDay.setDate(8, 4, 2025);
	cToDay.getMonth();

	cDate copy;
	copy.copyFrom(cToDay);

	return 0;
}

'개발 소발 > 개발 C++(기초)' 카테고리의 다른 글

c++ 생성자멤버초기화,위임생성자  (0) 2019.08.05
c++ 생성자 Constructor  (0) 2019.08.05
c++ OOP 객체지향 프로그래밍  (0) 2019.08.02
c++ assert,Ellpsis  (0) 2019.08.02
c++ 방어적프로그래밍 기초  (0) 2019.08.02

Object Oriented Progamming

OOP 객체지향 프로그래밍

객체는 데이터와 기능이 묶여있는 개념이다.

예를 들어

친구들의 정보를 출력 프로그램을 작성한다고 생각해보자.
한명이아닌 여러명의 이름,주소,나이등을 출력해야한다.

데이터를 묶어서 구조체 struct를 생성하는 방법이 있다.

구조체안에 기능을 구현하는건 추천하지 않는다.

class는 Object를 문법을 구현할때 사용한다고 생각하면된다.
구조체보다 class는 많은 기능을 사용할 수 있다.

Class도 변수처럼 선언해주어야한다.
Object 메모리를 차지하도록 정의해주는것을 instanciation이라 한다.
메모리에 할당된 class를 instance라고 한다.

class는 접근지정자access specifier를 지정해주어야한다.
접근지정자는 public,private,protected 세가지가 있다.

구조체struct와 class의 큰 차이점
구조체는 접근지정자가 없다.

#include <iostream>
#include <string>
#include <vector>

using namespace std;

struct Friend{
	//멤버
	string m_name;
	string address;
	int age;

	void printFriend() {
		cout << "name : " << m_name << " addr : " << address << " age : " << age << endl;
	}
};

class ClassFriend{
//접근제한자
public:
	//멤버
	string name;
	string address;
	int age;

	void printFriend() {
		cout << "name : " << name << " addr : " << address << " age : " << age << endl;
	}
};

//함수를 따로 만들어 각변수 파라미터로 입력하기
void printFriend(const string &name, const string &address, const int &age) {
	cout << "name : " << name << " addr : " << address << " age : " << age << endl;
}

//함수를 따로 만들어 구조체를 파라미터로 입력하기
void printFriend(Friend f) {
	cout << "name : " << f.m_name << " addr : " << f.address << " age : " << f.age << endl;
}

int main()
{
	Friend f1;
	f1.m_name = "CHOI";
	f1.address = "test";
	f1.age = 100;
	
	//파라미터로 다 직접 넣기는 귀찮다.
	printFriend(f1.m_name, f1.address, f1.age);

	//구조체로 더 간단하게 코드를 작성한다.
	Friend f2{ "KIM","TEST",133 };
	//구조체를 파라미터로 사용한다.
	printFriend(f2);

	//구조체 struct안에 print를 선언한다.
	f2.printFriend();

	//클래스로 생성하기
	ClassFriend f3{ "KIM","TEST",133 };
	f3.printFriend();

	//vector를 클래스로 선언해 쉽게 활용할 수 있다.
	vector<ClassFriend> friendVec;
	friendVec.push_back(f3);
	friendVec.push_back(ClassFriend{ "KIM1","TEST1",1 });
	for (int i = 0; i < friendVec.size(); i++) {
		friendVec[i].printFriend();
	}

	return 0;
}

'개발 소발 > 개발 C++(기초)' 카테고리의 다른 글

c++ 생성자 Constructor  (0) 2019.08.05
c++ 캡슐화,접근지정자,접근함수  (0) 2019.08.05
c++ assert,Ellpsis  (0) 2019.08.02
c++ 방어적프로그래밍 기초  (0) 2019.08.02
c++ 재귀적 함수호출  (0) 2019.08.01

프로그래밍은 디버깅이 시간이 오래걸린다.
assert은 디버깅을 컴파일러에게 도움받는 방법이다.

디버그 모드에서만 작동한다.
assert(false)는 에러가 출력된다.
assert(조건) 방식으로 작성한다. 조건에 맞지않으면 오류내용을 출력해준다.

static_assert는 컴파일타임에 결정되는 변수만 사용가능하다.
또, 오류내용 입력이 가능하다.

#include <iostream>
#include <cassert>
#include <array>

using namespace std;

void getArrData(array<int, 5> &arr, const int &idx) {
	//콘솔창에 오류내용이 출력된다.
	assert(idx >= 0);
	assert(idx <= arr.size() - 1);

	cout << arr[idx] << endl;
}

int main() {

	const int num = 5;
	//반드시 맞아야하는 데이터를 입력한다.
	assert(num == 5);
	//static_assert는 컴파일타임에 결정되는 변수만 저장가능하다.
	static_assert(num == 5,"GOOD");

	array<int, 5> arr{ 1,2,3,4,5 };
	getArrData(arr, 100);

	return 0;
}

 

함수를 구현하다보면 파라미터가 정해져있지 않으면 좋겠다 싶을때가있다.
이때 Ellipsis를 사용한다.

 

주의할점
파라미터 갯수를 맞춰주어야한다.
타입도 미리 정해주어야한다.

사용하기가 위험하고 디버깅도 어렵다.

고수가 되면 사용하도록 하자!

#include <iostream>
#include <cstdarg>

using namespace std;

double findAverage(int count, ...) {
	double sum = 0;
	//파라미터로 꺼내올 리스트를 선언한다.
	va_list list;

	//리스트와 크기를 정해준다.
	va_start(list, count);

	for (int arg = 0; arg < count; arg++) {
		//리스트안에서 값을 꺼내온다.
		//intiger로 변환
		sum += va_arg(list, int);
	}
	//리스트를 마무리해준다.
	va_end(list);

	return sum / count;
}

int main()
{
	cout << findAverage(3, 1, 2, 3) << endl;
	//사용자가 지정한 카운트와 크기가 안맞으면 문제가 생긴다.
	cout << findAverage(100, 1, 2, 3) << endl;
	return 0;
}

다수가 사용하는 프로그램을 작성할땐 사용자가 어찌 사용하든 정상적으로 작동하는게 좋다.

문법오류 syntax error
- 코드를 잘못 작성했을때를 말한다.

문맥오류 semantic error
- 논리오류:프로그래밍 로직 자체가 틀렸을때를 말한다.


가정위반 violated assumption
- 사용자가 개발자가 의도한 행위가 아니게행동하는 경우를 말한다.

방어적으로 프로그래밍하는 이유
작성한 프로그램이 에러가 방지차원.
사용자를 믿는게 아닌 개발자가 미리 에러를 방지한다.
방어적프로그래밍은 시간이 오래걸리지만 꼭해주어야한다.

 

#include <iostream>
#include <string>

using namespace std;
int a = 0;
int main()
{
	string s = "test test test";
	while (true)
	{
		//사용자에게 범위를 말한다.
		cout << "input from 0 to " << s.size() - 1 << endl;
		int x;
		cin >> x;
		//사용자에게 범위를 말하는것만 아니라 방어까지 해놓는다.
		if (x <= s.size() - 1) {
			cout << "GOOD! " << s[x] << endl;
			break;
		}
		else {
			cout << "again" << endl;
		}

	}

	return 0;
}

+ Recent posts