- 키워드 mutable
Const 함수 내에서의 값의 변경을 예외적으로 허용한다.
가급적 사용하지 말아야 할 키워드 이다.
- 상속(Inheritance)
class UnivStudent : public Person
{
...
}
class UnivStudent : protected Person
{
...
}
- Person 옆의 public, protected 키워드의 의미는?
의미) 부모 클래스를 상속시에 키워드 보다 범위가 넓은 맴버는 선언된 키워드 범위로 상속한다는 의미 이다.
다시 말해서 protected의 키워드를 사용하면 Person의 맴버중 public맴버를 모두 protected로 바꾸어서 상속 하겠다는 의미 이다.
특별한 경우가 아니면 C++에서는 public상속을 주로 사용한다.
- UnivStudent 생성자는 Person의 멤버까지 초기화 해야하는 의무가 있다.
UnivStudent(char* name, int age, char* mymajor)
:Person(myage, myname)
{
...
}
- 위 와 같이 Base(부모)클래스의 생성자를 바로 호출해서 맴버를 초기화 해준다.
- Derived(자식) 클래스를 생성 할때 부모 클래스의 생성자를 명시적으로 호출 하지 않으면, 부모 클래스의 Void 생성자가 호출 된다.
클래스의 맴버는 클래스의 생성자를 통해서 초기화 해야 한다.
- 다형성
객체 포인터의 참조관계
부모 클래스형 포인터는 자식클래스의 객체를 가리킬 수 있다.
Person(부모 클래스) , Student(자식클래스)
Person *ptr = new Student();
overriding된 자식 클래스의 함수는 부모 클래스의 함수를 (hide)가린다.
명시적으로 부모 클래스의 함수를 호출 할 수 있다.
BBB bbb = new BBB();
bbb.AAA::ParentFunction();
명시적으로 부모클래스의 함수를 호출 할 수 있으나 거의 사용할 일이 없다.
가상함수
class First
{
public:
virtual void MyFunc(){cout<<"hello World"<<endl;}
};
가상함수는 virtual이라는 키워드로 선언된다. 이 함수를 오버라이딩 하는 함수도 가상함수가 된다.
자식 클래스에서 오버라이딩 할 때에도 함수에 Virtual 키워드를 붙여 주는 것이 좋다.(명시적으로 가상함수를 표시하기 위해서)
순수 가상함수 와 추상 클래스
객체 생성이 목적이 아닌 클래스가 존재 하게 된다. 이 클래스의 객체를 생성하지 못하도록 강제하는 방법.
순수 가상함수를 선언하자.
class Employee
{
private:
char name[100];
public:
virtual int GetPay() const = 0; //순수 가상함수
}
순수 가상함수는 몸체가 없으며, virtual 키워드와 = 0 을 사용해서 선언한다.
이 함수를 선언해서 두가지 이점이 생긴다.
- 잘못해서 객체를 생성하는 실수를 막을 수 있다.(객체를 생성할 수 없게 강제하기 때문에)
- 실제로 Employee에서 실행되는 함수가 아니라는 것을 명시적으로 나타낸다.
순수 가상함수가 하나 이상 존재하는 클래스는 추상클래스가 된다.
가상 소멸자
class First
{
public:
~First(){...}
}
class Second:public First
{
public:
~Second(){...}
}
int main()
{
First* first = new Second();
delete first
return 0;
}
위의 코드를 실행 하면 소멸자가 2번 호출되어야 memory leak 없이 정상적으로 종료 될 것이다.
하지만 실제로 실행해 보면 소멸자는 한번만 호출이 된다.
그렇다면 소멸자가 정상적으로 두번 호출 되려면 어떻게 해야할까?
class First
{
public:
virtual ~First(){...}
}
class Second:public First
{
public:
virtual ~Second(){...}
}
int main()
{
First* first = new Second();
delete first
return 0;
}
바로 소멸자에 virtual키워드를 붙여주는 것이다.
이렇게 하면 정상적으로 2번 소멸자가 호출된다.
다중 상속
C++는 다중 상속을 지원한다(사용하지 않는것을 추천)
class BaseOne
{
public:
void SimpleFunc(){...}
}
class BaseTwo
{
public:
void SimpleFunc(){...}
}
class MultiDerived : public BaseOne, protected BaseTwo
{
public:
void ComplexFunction()
{
//명시적으로 호출해줘야 한다.
BaseOne::SimpleFunc();
BaseOne::SimpleFunc();
}
}
가상 상속
class Base
{
public:
Base(){...}
void SimpleFunc(){...}
}
class MiddleDerivedOne : virtual public Base
{
public:
MiddleDerivedOne() : Base()
{
...
}
}
class MiddleDerivedTwo : virtual public Base
{
public:
MiddleDerivedTwo() : Base()
{
...
}
}
class LastDerived : public MiddleDerivedOne, public MiddleDerivedTwo
{
public:
LastDerived() : MiddleDerivedOne() , MiddleDerivedTwo()
{
...
}
void ComplexFunc()
{
MiddleFuncOne();
MiddleFuncTwo();
//명시적으로 호출해줄 필요가 없다 Virtual로 Base를 상속 받았기 때문에 한번 만 상속을 했기 때문이다.
//다중 상속을 막아 준다.
//만약 virtual키워드로 상속 하지 않았다면, 아래와 같이 호출 해야 할것 이다.
//MiddleDerivedOne::SimpleFunc();
//MiddleDerivedTwo::SimpleFunc();
SimpleFunc();
}
}
또한 Virtual로 상속을 받았기 때문에 되면 Base의 생성자도 한번만 호출 된다.
- 연산자 오버로딩
class A
{
public:
A(){...}
A operator+(const A &ref)
{
...
}
}
int main()
{
A a = new A();
A aa = new A();
A result1 = a.operator+(aa); //1번
A result2 = a+aa; //2번
}
C++에서는 규약이 존재하는데, 규약은 operator키워드를 사용하면 1번과 2번을 동일하게 취급하겠다는 것이다.
다시 설명하면 operator키워드 와 연산자를 선언해서 정의한 함수는 연산자만 써도 동일하게 함수를 호출 한것 처럼 동작을 보장해 주겠다는 말이다.
연산자를 오버로딩 하는 두가지 방법
- 맴버함수에 의한 연산자 오버로딩
- 전역함수에 의한 연산자 오버로딩
class Point
{
private:
int xpos, ypos;
public:
Point(int x=0, int y=0) : xpos(x), ypos(y)
{}
void ShowPosition() const{...}
//맴버함수로 operator선언
Point& operator++()
{
...
}
//전역함수로 operator선언
friend Point& operator--(Point &ref);
}
Point& operator--(Point &ref)
{
...
}
전위 증가와 후위 증가의 구분
//전위 감소
Point& operator--(Point& ref)
{
...
}
//후위 감소
Point& operator--(Point& ref, int)
{
...
}
int를 파라미터로 넣어준다는 의미가 아니라 , 저렇게 선언을 하면 후위로 동작 하겠다는 의미이다.
전역 연산자 오버로딩이 필요한 이유.
class Point
{
public:
Point& operator*(int value)
{
...
}
}
int main()
{
//곱셈의 교환 법칙
Point point = new Point();
Point result = point*3;
//그렇다면 3*point 도 같은 결과를 나타내게 하려면...
//operator가 오른쪽에 오도록 해야 하는데 맴버함수로는 불가능 하다.
//이래서 전역 함수로의 연산자 오버로딩이 필요한것.
return 0;
}
//이렇게 선언해 주면 되겠다.
Point& operator*(int value, Point& ref)
{
...
}
//그리고 Point Class는 이렇게 바꿔주면 되겠다.
class Point
{
public:
Point& operator*(int value)
{
...
}
friend Point& operator*(int value, Point &ref);
}
작지만 뭔가 하나씩 알아가는 기분이 참 좋다 ㅎㅎㅎ
반드시 해야 하는 대입 연산자(assignment operator)의 오버로딩
복사 생성자와 매우 유사하다.
복사 생성자에 대한 복습
- 정의하지 않으면 디폴트 복사 생성자가 삽입
- 디폴트 복사 생성자는 얕은 복사(shallow copy)를 한다.
- 생성자 내에서 동적 할당을 하거나, 깊은 복사(deep copy)를 원하면 직접 정의해야 한다.
대입 연산자의 특징
- 정의하지 않으면 디폴트 대입 연산자가 삽입.
- 디폴트 대입 연산자는 얕은 복사(shallow copy)를 한다.
- 연산자 내에서 동적 할당을 하거나, 깊은 복사가 필요하면 직접 정의해야 한다.
복사 생성자 와 대입 연산자는 매우 유사한 특징이 있다.
//복사 생성자 호출
int main()
{
Point pos1(5,7);
Point pos2 = pos1;
...
}
//대입 연산자 호출
int main()
{
Point pos1(5,7);
Point pos2(3,4);
pos2 = pos1;
...
}
위 의 코드의 차이점은 무엇일까?
대입 연산자를 호출하는 케이스는 두 객체(pos1, pos2)가 이미 초기화된 상태라는 점이다.
그렇다면 언제 대입 연산자를 직접 정의 해야 하는걸까? 아래의 코드를 보자.
class Person
{
private:
char *name;
int age;
public:
Person(char *myname, int myage)
{
name = new char[100];
strcpy(name, myname);
age = myage
}
~Person()
{
delete []name;
}
}
int main(void)
{
Person p1("kim", 29);
Person p2("Lee", 28);
p2 = p1; // 대입 연산자 발생.
}
어떤 문제가 발생 할까?
- 얕은 복사(shallow copy)를 하기 때문에 p2->"lee" 가르키던 name 변수에 p1->"kim" 주소값을 할당 해버리면서
"lee"를 더이상 참조 할 수 없게 되면서 메모리 누수가 발생한다.
때문에 다음과 같이 연산자 오버로딩을 직접 구현해 줘야 한다.
Person& operator=(const Person& ref)
{
delete []name //메모리 누수를 막기위한 메모리 해제
name = new char[100];
strcpy(name, ref.name)
age = ref.age;
return *this;
}
상속 구조에서의 대입 연산자 호출
유도 클래스(Derived class)의 대입 연산자를 직접 구현하고 아무런 명시를 하지 안으면, 기초 클래스의 대입 연산자가 호출되지 않는다.
때문에 유도클래스의 대입연산자를 직접 구현 했다면, 기초클래스의 대입연산자를 명시적으로 호출 해주어야 한다.
배열의 인덱스 연산자 오버로딩
//반환하는 타입에 따라 조금 달라 지겠지만 아래와 같이 선언한다.
int operator[](int index)
{
...
}
객채의 저장을 위한 배열 클래스의 정의
- 객체를 저장하는 배열 기반
- 객체의 주소 값을 저장하는 배열 기반.
//객체를 저장하는 배열
class Point
{
private:
int xpos, ypos;
public:
Point(int x=0, int y=0) : xpos(x), ypos(y){}
}
class BoundCheckPointArray
{
private:
Point *arr;
int arrlen;
//복사 생성자, 대입 연산자를 사용할 수 없도록 private로 정의
BoundCheckPointArray(const BoundCheckPointArray& arr){}
BoundCheckPointArray& operator=(const BoundCheckPointArray& arr){}
public:
BoundaryCheckPointArray(int len) :arrlen(len)
{
arr = new Point[len]
}
Point& operator[](int indx)
{
return arr[idx];
}
Point operator[] (int idx) const
{
return arr[idx]
}
int GetArrLen() const {return arrlen;}
~BoundaryCheckPointArray(){ delete []arr; }
}
int main(void)
{
BoundaryCheckPointArray arr(3);
arr[0]=Point(1,2);
arr[1]=Point(1,2);
arr[2]=Point(1,2);
return 0;
}
//객체의 주소값을 저장하는 배열
typedef Point* POINT_PTR;
class BoundCheckPointPtrArray
{
private:
POINT_PTR * arr;
int arrlen;
//복사 생성자, 대입 연산자를 사용할 수 없도록 private로 정의
BoundCheckPointArray(const BoundCheckPointArray& arr){}
BoundCheckPointArray& operator=(const BoundCheckPointArray& arr){}
public:
BoundaryCheckPointArray(int len) :arrlen(len)
{
arr = new POINT_PTR[len];
}
POINT_PTR& operator[](int idx)
{
return arr[idx];
}
POINT_PTR operator[](int idx) const
{
return arr[idx];
}
int GetArrLen() const {return arrlen;}
~BoundaryCheckPointArray(){ delete []arr; }
}
int main()
{
BoundCheckPointPtrArray arr(3);
arr[0] = new Point(1,2);
arr[1] = new Point(3,4);
arr[2] = new Point(5,6);
delete arr[0];
delete arr[1];
delete arr[2];
return 0;
}
객체의 생성과 소멸을 위한 new, delete를 신경써 줘야 하기 때문에 힘들겠지만, Deep copy와 Shallow copy를
신경 쓰지 않아도 되기 때문에 이 방법이 더 많이 사용된다.
스마트 포인터
- 따로 공부 필요
()연산자의 오버로딩과 Functor
함수 호출에 사용되는 "()" 도 연산자라는 사실
class Adder
{
public:
int operator()(const int& n1, const int& n2)
{
return n1+n2;
}
double operator(const double& e1, const double& e2)
{
return e1+e2;
}
}
int main()
{
Adder adder;
cout<<adder(1,3)<<endl
cout<<adder(1.5, 3.5)<<endl
return 0;
}
위 예제 처럼 Adder 클래스가 함수처럼 동작하고 있으며, 이러한 클래스를 "Functor" 라고 한다.
임시객체로의 자동 형변환 , 형변환 연산자
class Number
{
private:
int num;
public:
Number(int n=0) : num(n)
{
}
void ShowNumber(){ cout<<num<<endl; }
}
int main()
{
Number num;
num=30; //일치하지 않는 자료형 간의 대입이 가능??
num.ShowNumber();
return 0;
}
위 예제는 오류가 날것 같지만, 컴파일과 실행이 정상적으로 동작한다.
어떻게 가능할까?
num = Number(30); //임시객체를 생성한다.
num.operator=(Number(30)); //생성된 임시객체를 대상으로 대입연산자가 호출된다.
임시객체로의 자동 형변환에는 규칙이 존재한다.
"A형 객체가 와야할 위치에 B형 데이터 가 왔을경우, B형 데이터를 인자로 전달받는 A형 클래스의 생성자 호출을 통해서 A형 임시 객체를 생성한다."
요약 하면, 위의 예제에서 int형 을 인자로 받는 생성자가 Number클래스에 있을경우에 동작한다는 말이다.
이번에는 형변환 연산자 오버로딩에 대해서 알아보자.
class Number
{
private:
int num;
public:
Number(int n=0) : num(n)
{
}
Number& operator=(const Number& ref)
{
num= ref.num;
return *this;
}
operator int () //형변환 연산자 오버로딩.
{
return num;
}
}
int main()
{
Number num1;
num1 = 30;
Number num2=num1+20; //형변환 연산자가 호출됨.
}
형 변환 연산자는 반환형을 명시하지 않는다. 하지만 return문에 의한 값의 반환은 얼마든지 가능하다.
"위의 예제는 int형으로 형변환 해야하는 상황에서 호출되는 함수이다."
템플릿(Template)
Template에 대한 이해와 함수 템플릿
함수 template의 기본 형태
template <typename T>
T Add(T num1, T num2) //함수 템플릿
{
return num1 + num2;
}
int main()
{
Add<int>(1,2)
Add<double>(1.2, 1.4);
//이렇게 호출해도 알아서 변환 함.
//하지만 위처럼 명시 적으로 호출해주는 것이 좋다고 생각함.
Add(1,2)
Add(1.2, 1.4);
}
//위를 컴파일 하면, 컴파일러가 아래와 같이 템플릿 함수를 만들어 낸다.
int Add(int num1, int num2)
{
return num1 + num2;
}
double Add(double num1, double num2)
{
return num1 + num2;
}
template -> template 둘다 대체 가능하다.
또한 T 대신다른 문자를 사용해도 된다.
함수 템플릿의 특수화
template <typename T>
T Max(T a, T b)
{
return a > b ? a : b;
}
int main()
{
cout<<Max(11, 15);
cout<<Max("kim","hong");
}
헌데 Max("kim","hong")같은 경우는 string의 주소값을 비교하기 때문에 의미 없는 함수 가 된다.
그렇다면 어떻게 정의 해야 할까?
template <typename T>
T Max(T a, T b)
{
return a > b ? a : b;
}
template<>
char* Max<char*>(char* a, char* b)
{
}
template<>
const char* Max<const char*>(const char* a, const char* b)
{
}
위와 같이 정의하게 되면, char* 나 const char*로 정의가 필요하면 별도로 만들지 말고 이것을 쓰라고 명시하는 것이다.
클래스 템플릿
클래스도 템플릿으로 정의가 가능하며, 이렇게 정의 된 템플릿을 클래스 템플릿이라고 한다.
컴파일러가 클리스 템플릿을 기반으로 생성한 클랫스를 템플릿 클래스라고 한다.
template <typename T>
class Point
{
private:
T xpos, ypos;
public:
Point(T x=0, T y=0) : xpos(x), ypos(y)
{}
}
클랫스 템플릿의 선언과 정의 분리
//선언
template <typename T>
class SimpleTemplate
{
public:
T SimpleFunc(const T& ref);
}
//정의
template <typename T>
T SimpleFunc<T>::SimpleFunc(const T& ref)
{
...
}
파일을 나눌때의 고려사항.
int main()
{
Point<int> pos1(3,4);
Point<double> pos2(0.1, 0.3);
}
main인 컴파일 될때 template class를 컴파일러가 생성하기 위해서는 Point의 선언 뿐만 아니라,
구현 정보 까지도 모두 알고 있어야 하지만, 헤더 파일만 참조해서는 알 수가 없다.
그렇다면 어떻게 해야 할까?
- 헤더 파일에 Point의 생성자와 구현 까지 모두 넣는다.
- #include "PointTemplate.cpp" 처럼 cpp파일을 include한다.
이렇게 두가지 방법이 있다.
클래스 템플릿 특수화
template <typename T>
class SoSimple
{
public:
T SimpleFunc(T num){...}
}
//특수화
template<>
class SoSimple<int>
{
public:
int SimpleFunc(int num){....}
}
클래스 템플릿의 부분 특수화
template <typename T1, typename T2>
//전체 특수화.
class Mytemplate<char, int>{...}
//부분 특수화.
template<typename T1>
class Mytemplate<T1, int>{...}
템플릿 인자
템플릿 매개변수에는 변수의 선언이 올 수 있습니다.
template <typename T , int len> //템플릿 매개변수에 변수선언.
class SimpleArray
{
private:
T arr[len];
public:
T& operator[](int idx)
{
return arr[idx];
}
}
int main()
{
SimpleArray<int, 5> i5arr;
SimpleArray<int, 7> i7arr;
SimpleArray<double, 6> d6arr;
}
그렇다면 위에 선언을 했을때 어떠한 이점이 있을까?
i5arr과 i7arr은 서로 다른 형(type)이 된다.
그렇기 때문에 대입 연산시 컴파일 에러가 난다.
길이가 다른 배열에 대해서 대입 및 복사에 관련 사항을 신경쓰지 않아도 된다는 이점이 있다.
####템플릿 매개변수에는 디폴트(default)값도 설정 가능하다.
template <typename T=int, int len =7>
class SimpleArray
{
private:
T arr[len];
public:
T& operator[](int idx){return arr[idx];}
}
int main()
{
//이렇게 선언하면
//기본 int형 7개의 배열형태로 선언이 된다.
SimpleArray<> arr;
}
템플릿 과 Static
가장 중요한 키포인트는 컴파일러가 생성한 템플릿 클래스의 갯수만큼 Static변수도 생긴다는 것이다.
다시 말하면 아래의 코드를 보자
template <typename T>
class SimpleStaticMem
{
private:
static T mem;
public:
void AddMem(int num){mem+=num;}
void ShowMem(){cout<<mem<<endl;}
}
int main()
{
SimpleStaticMem<int> obj1;
SimpleStaticMem<int> obj2;
SimpleStaticMem<double> obj3
SimpleStaticMem<long>
}
//컴파일러에 생성된 템플릿 클래스들.
class SimpleStaticMem<int>
{
private:
static int mem;
public:
void AddMem(int num){mem+=num;}
void ShowMem(){cout<<mem<<endl;}
}
class SimpleStaticMem<long>
{
private:
static long mem;
public:
void AddMem(int num){mem+=num;}
void ShowMem(){cout<<mem<<endl;}
}
class SimpleStaticMem<double>
{
private:
static double mem;
public:
void AddMem(int num){mem+=num;}
void ShowMem(){cout<<mem<<endl;}
}
//생성된 템플릿 클래수 갯수 만큼 Static 변수가 생성된다.
예외처리 (Exception Handling)
예외상황과 예외처리의 이해
if문으로 예외의 처리를 하게 되면, 논리적 분기처리와 예외처리의 구분이 잘 안된다.
C++의 예외처리 메커니즘.
- try
- catch
- throw
int SimpleFunc(void)
{
try
{
if(...)
throw -1; //int형 예외 발생
}
catch(char ex){....} //char형 예외를 처리
//결국 SimpleFunc을 호출한 상위로 예외가 전달된다.
}
전달하는 예외를 명시
int throwFunc(int num) thorw (int, char)
//다른 자료형의 예외 데이터가 전달되면 terminate함수의 호출로 인해서 프로그램은 종료된다.
int SimpleFunc(void) throw()
{
...
}
//어떠한 예외도 전달하지 않음 , 만약 예외가 발생하면 프로그램은 종료된다.
모든 예외를 처리하는 catch블로
try
{
}
catch(...)
{
}
//발생한 예외에 대한 어떤 정보도 알 수 없으며, 예외 종류도 구분이 불가능하다.
윤성우 의 열혈강의 독학을 끝마치며....
음.. 대학교때 배우고 거의 사용하지 않아서 잊었던 기억을 되살리는데 많은 도움이 된것같다.
갑자기 c++ 관련 코드를 수정하고 프로젝트를 진행할 일이 있어서 기초부터 다시 보는데 몰랐던 내용도 있고
여러가지로 도움이 되었다.
이제 다음 책으로 고고~
'Programing > C++' 카테고리의 다른 글
C++/cli c# Unmanaged code -> managed Code Event Call(이벤트 호출) (0) | 2017.05.24 |
---|