[C++] 템플릿(Template)(2)

C++ 2017. 12. 25. 22:27

1] 템플릿 개념의 확장
1. 클래스 템플릿의 확장

template <class T>
class Point
{
private:
    T xpos, ypos;
public:
    Point(T x=0, T y=0);
    void ShowPosition() const;
};

template <class T>
class BoundCheckArray
{
private:
    T *arr;
public:
    BoundCheckArray(T); 
};

위 클래스 기반으로 Point<int> 템플릿 클래스의 객체를 저장할 수 있는 객체는 어떻게 생성해야할까? 딱히 어렵지 않아 보인다.

BoundCheckArray<Point<int>> pointArr(50);

Point<Int>형 포인터라면, 다음과 같이 객체를 생성하면 된다.

BoundCheckArray<Point<int>*> pointArrPtr(50);

타입이 너무 길다면, typedef 선언으로 간략화 할 수 있다.

typedef Point<int>* POINT_PTR;

* 특정 템플릿 클래스의 객체를 인자로 받는 일반함수의 정의와 friend 선언
: Point<int>와 같은 템플릿 클래스의 자료형을 대상으로도 일반함수의 정의가 가능하고, 클래스 템플릿 내에서 이러한 함수를 대상으로 friend 선언도 가능하다. 


2] 클래스 템플릿의 특수화
: 클래스 템플릿의 특수화도 함수 템플릿의 특수화랑 매우 비슷하다.

1. 클래스 템플릿 특수화
: 클래스 템플릿을 특수화하는 이유는, 특정 자료형을 기반으로 생성된 객체에 대해, 구분이 되는 다른 행동양식을 적용하기 위해서이다.

template <class T>
class SoSimple
{
public:
    T SimpleFunc(T num)
    {
        ...
    }
};

위 클래스의 int형 특수화는 다음과 같다.

template<>
class SoSimple<int>
{
public:
    int SimpleFunc(int num)
    {
        ...
    }
};

2. 클래스 템플릿의 부분 특수화

template <class T1, class T2>
class MySimple { ... };

다음은 위 클래스 템플릿을 부분 특수화한 코드이다.

template <class T1>
class MySimple<T1, int> { ... }

그렇다. 두개의 템플릿 타입에 대하여 하나의 타입만 특수화를 한것이다. 

template <>
class MySimple<double, int> { ... }

위는 전체 타입에 대해 특수화를 시킨것이다. 만약 두개의 정의가 겹치는 즉 MySimple<double, int> 타입의 객체를 생성하려고 하면 전체 특수화에 대한 클래스 템플릿이 우선시 된다. 


3] 템플릿 인자
: 위에서 사용된 T 또는 T1, T2 같은 문자를 가리켜 "템플릿 매개변수" 라고 한다. 그리고 템플릿 매개변수에 전달되는 자료형 정보를 가리켜 "템플릿 인자" 라고 한다. 

1. 템플릿 매개변수에는 변수의 선언이 올 수 있다. 
: 다음의 클래스 템플릿 정의를 보자. 이 정의에서 독특한 사실은 템플릿 매개변수의 선언에 마치 함수처럼 변수의 선언이 등장했다는 점이다.

template <typename T, int len> 
class SimpleArray
{
private:
    T arr[len];
public:
    T& operator[] (int idx)
    {
        return arr[idx];
    }
};

이렇듯 템플릿 매개변수에도 변수가 올 수 있다. 그리고 이를 기반으로 다음의 형태로 객체생성이 가능하다.

SimpleArray<int, 5> i5arr;
SimpleArray<double, 7> d7arr;

위의 두 문장에서 템플릿 매개변수 len에 전달된 인자 5와 7은 해당 템플릿 클래스에서 상수처럼 사용된다. 즉, len은 각각 5와 7로 치환되어 템플릿 클래스가 각각 생성된다.

class SimpleArray<int, 5>
{
private:
    int arr[5];
public:
    int& operator[] (int idx)
    {
        return arr[idx];
    }
};

class SimpleArray<double, 7>
{
private:
    int arr[7];
public:
    double& operator[] (int idx)
    {
        return arr[idx];
    }
};

물론, 위의 두 템플릿 클래스 SimpleArray<int, 5>와 SimpleArray<double, 7>은 완전히 다른 타입이다. 

위에서 보인 것처럼 배열의 길이를 결정하기 위해서 굳이 위에처럼 하지말고 생성자를 이용해서 하면 더욱 편리할 것이다. 
그런데 굳이 이러한 템플릿 클래스를 만드는 이유는, 길이가 다른 두 배열 객체간의 대입 및 복사에 대한 부분을 신경 쓰지 않아도 된다는 것이다. 서로 다른 타입이기 때문에 길이가 다른 배열에 객체에 대해 대입 및 복사 연산을 할 경우 컴파일 에러가 발생할 것이기 때문이다. 
만약 생성자를 이용해서 배열의 길이를 결정하게 했다면, 길이가 같은 배열에 대해서만 대입을 허용하기 위해서 추가적인 코드의 삽입이 불가피하며, 이러한 추가적인 코드는 대입 및 복사의 과정에서 CPU가 수행해야 할 일을 늘리는 결과로 이어진다. 

2. 템플릿 매개변수는 디폴트 값 지정도 가능하다.
: 템플릿 매개변수에도 디폴트 값의 지정이 가능하다. 

template <class T=int, int len=7> 
class SimpleArray
{
    ....
}

SimpleArray <> arr;


4] 템플릿과 static
1. 함수 템플릿과 static 지역변수

template <class T>
void ShowStaticVlaue(void)
{
    static T num = 0;
    num += 1;
    cout << num << endl;
}

위의 함수 템플릿은 다음과 같은 템플릿 함수를 만들어 내며 각각의 함수가 static 변수를 따로 가지고 있다. 

void ShowStaticVlaue<int>(void)
{
    static int num = 0;
    num += 1;
    cout << num << endl;
}

void ShowStaticVlaue<long>(void)
{
    static long num = 0;
    num += 1;
    cout << num << endl;
}

2. 클래스 템플릿과 static 멤버변수

template <class T>
class SimpleStaticMem
{
private:
    static T mem;
public:
    void  AddMem(int num)
    {
        mem += num;
    }

    void ShowMem()
    {
        cout<<mem<<endl;
    }
};

template <class T>
T SimpleStaticMem<T>::mem = 0; // 템플릿 기반의 static 멤버 초기화 문장이다.

위 클래스 템플릿도 타입 별 static 멤버 변수가 각각 유지된다.

3. 템플릿 static 멤버변수 초기화의 특수화
위에서 다음과 같이 static 멤버변수를 초기화 하였다.

template <class T>
T SimpleStaticMem<T>::mem = 0;

그러면 long 타입만 5로 초기화하고 싶을때는 어떻게 할까? 간단하다.

template <>
long SimpleStaticMem<long>::mem = 5;


'C++' 카테고리의 다른 글

[C++] 템플릿(Template)(1)  (0) 2017.12.25
[C++] 상속과 다형성  (0) 2017.12.25
[C++] 상속  (0) 2017.12.25
[C++] Friend와 Static 그리고 Const  (0) 2017.12.25
[C++] 복사 생성자(Copy Constructor)  (0) 2017.12.23
Posted by 홍성곤
,

[C++] 템플릿(Template)(1)

C++ 2017. 12. 25. 21:31

1] 템플릿에 대한 이해와 함수 템플릿
1. 함수를 대상으로 템플릿 이해하기 

template <typename T>
T Add(T num1, T num2)
{
    return num1 + num2;
}

위 템플릿 함수의 정의는 다음과 같다.
- 기능 : 덧셈
- 대상 자료형 : 결정되어 있지 않음

template <typename T>은 T라는 이름을 이용해서 아래이 함수를 템플릿으로 정의한다는 의미이다.(template <class T> 도 똑같은 의미이다.)

다음은 위 템플릿 함수를 이용해서 여러가지 함수를 만들어 보겠다.

int main(void)
{
    cout<< Add<int>(15, 20) << endl;
    cout<< Add<double>(2.9, 3.7) << endl;
    cout<< Add<int>(3.2, 3.2) << endl;  // 값 손실이 발생되어 6이 출력 됨.
    cout<< Add<double>(3.15, 2.75) << endl;

    return 0;
}

Add<int>(15, 20) 의 의미는 "T를 int로 해서 만들어진 Add 함수를 호출한다."
그래서 위 문장을 실행하면 컴파일러는 함수 하나를 만든다. 그러나 두번째의 Add<int>(3.2, 3.2) 문장은 다시 함수를 만들지 않는다. 기존에 만들어져있던 함수를 사용한다. 
그러나, 컴파일 타임에 함수가 만들어지기 때문에 런타임상 속도 저하를 가져오지 않는다. 

위 main 함수를 다음과 같이 변경해도 상관없다. 

int main(void)
{
    cout<< Add(15, 20) << endl;

    cout<< Add(2.9, 3.7) << endl;
    cout<< Add(3.2, 3.2) << endl; 
    cout<< Add(3.15, 2.75) << endl; 

    return 0;
}

위 코드를 컴파일하면 컴파일러가 자동으로 자료형을 결정해서 함수를 만들어 준다. 

2. 함수 템플릿과 템플릿 함수 
- 함수 템플릿
template <typename T>
T Add(T num1, T num2)
{
    return num1 + num2;
}

- 템플릿 함수
: 함수 템플릿 기반으로 컴파일러가 만들어내는 함수다.
: 컴파일러가 생성해 내는 함수라는 뜻으로 Generated Function으로도 불린다.

int Add<int>(int num1, int num2)
{
    return num1 + num2;
}

템플릿 함수와 일반 함수와 구분이 된다.

위 예제에 다음과 같은 일반함수가 추가되었다고 생각해보자.

int Add(int num1, int num2)
{
    return num1 + num2
}

int main(void)
{
    cout<< Add(5, 7) << endl;  // 일반함수를 호출한다.
    cout<< Add<int>(5, 7) << endl; // 템플릿 함수를 호출한다.

    return 0;
}

3. 둘 이상의 Type에 대해 템플릿 선언하기 

template <class T1, class T2>
void ShowData(double num)
{
    cout << (T1)num << " , " << (T2)num << endl; // 형 변환을 "T1(num)" 이런 형태로도 가능.
}

int main(void)
{
    ShowData<char, int>(65); // A, 65 출력
    ShowDate<short, double>(69.2) // 69, 69.2 출력

    return 0;
}

4. 함수 템플릿의 특수화(Specialization)

template <class T>
T max(T a, T b)
{
    return a > b ? a : b;
}

int main(void)
{
    cout<< Max(11, 15) << endl;  // 15 출력
    cout << Max('T', 'Q') << endl; // T 출력
    cout << Max("Simple", "Best") << endl; // 뭐가 출력될지 모름, 단순 주소값을 비교하기 때문
    
    return 0;
}

이런 경우 문자열의 경우 의도하지 않은 결과가 나타난다. 문자열 길이 비교가 목적이라면 다음과 같이 정의 해야 한다. 

const char* Max(const char* a, const char* b)
{
    return strlen(a) > strlen(b) ? a : b;
}

이렇듯 상황에 따라서 템플릿 함수의 구성방법에 예외를 둘 필요가 있는데, 이 때 사용되는 것이 함수 템플릿의 특수화 이다.

char* 타입과 const char* 타입에 대해서 특수화 해보겠다.

template <class T>
T max(T a, T b)
{
    return a > b ? a : b;
}

template <>
char* Max<char *>(char* a, char* b) // <char *> 생략 가능
{
    return strlen(a) > strlen(b) ? a : b;
}

template <>
const char* Max<const char*>(const char* a, const char* b) // <const char*> 생략 가능
{
    return strcmp(a, b) > 0 ? a : b;
}

"char*, const char*형 함수는 내가 이렇게 제시를 하니 필요한 경우에는 별도로 만들지 말고 이것을 써라" 라는 의미다. 


2] 클래스 템플릿
1. 클래스 템플릿 정의
template <class T>
class Point
{
private:
    T xpos, ypos;
public:
    Point(T x =0, T y=0) : xpos(x), ypos(y) {  }
    
    void ShowPosition() const
    {
        cout<<xpos<<", ">>ypos<<endl;
    }
};

int main(void)
{
    Point<int> pos1(3, 4);
    Point<double> pos2(2.4, 3.6);
    
    return 0;
}

위 예제를 보면 템플릿 함수와 매우 비슷해서 쉽게 이해가 갈것이다. 
다만, 템플릿 함수와 달리 템플릿 클래스의 객체를 생성할 때 <int>, <double>과 같은 자료형 정보를 생략하는것은 불가능하다. 

2. 클래스 템플릿을 파일 분할할 때의 주의점
: 컴파일은 파일단위로 이뤄진다는 사실을 이미 알고 있을 것이다. 그렇다면 밑에 main 함수를 보자.

<main.cpp 파일>
int main(void)

{
    Point<int> pos1<3, 4>;

    return 0;
}

위 파일을 컴파일 하기 위해서는 Point 클래스의 정의 및 구현부분까지 알아야 한다. int 자료형에 맞게 템플릿 클래스를 생성해 내야되기 때문이다. 
만약 Point 클래스의 선언과 구현이 PointTemplate.h PointTemplate.cpp에 나눠져 있다면 두 파일 모두 include 해야 컴파일이 성공한다. 
만약 PointTemplate.cpp 파일을 include하기 싫으면 PointTemplate.h에 Point의 생성자와 멤버함수의 정의를 모두 넣어야 한다. 



'C++' 카테고리의 다른 글

[C++] 템플릿(Template)(2)  (0) 2017.12.25
[C++] 상속과 다형성  (0) 2017.12.25
[C++] 상속  (0) 2017.12.25
[C++] Friend와 Static 그리고 Const  (0) 2017.12.25
[C++] 복사 생성자(Copy Constructor)  (0) 2017.12.23
Posted by 홍성곤
,

[C++] 상속과 다형성

C++ 2017. 12. 25. 20:10

1] 객체 포인터의 참조관계
1. 객체 포인터 변수 
: 객체의 주소 값을 저장하는 포인터 변수다.

Person *ptr; 
ptr = new Person();

위의 두 문장이 실행되면, 포인터 ptr은 Person 객체를 가리키게 된다. 
그러나 Person형 포인터는 Person 객체 뿐만 아니라 Person을 상속받은 Person의 자식클래스의 객체도 가리킬 수 있다. 
 
Base 클래스가 부모 클래스이고, Derived 클래스가 자식 클래스이다. 그리고 두 클래스 모두 aaa() 라는 메서드가 정의되어 있다면, 다음과 같은 행동도 가능하다.

Derived derived;

derived.Base::aaa(); 

즉, 자신의 aaa() 메서드를 호출하는 것이 아닌, 부모 클래스의 aaa() 메서드를 호출하는 것이다. 
단, 저렇게 사용할 일이 거의 없으니 문법적인 요소로만 알아두길 바란다.

2. 상속 클래스들의 메서드 호출 범위

class First
{
public:
    void FirstFunc()
    {
        cout<<"First Func"<<endl;
    }
};

class Second : public First
{
public:
    void SecondFunc()
    {
        cout<<"Second Func"<<endl;
    }
};

class Third : public Second
{
public:
    void ThirdFunc()
    {
        cout<<"Third Func"<<endl;
    }
};

int main(void)
{
    Third* third = new Third();
    Second* second = third;
    First* first = second;
    
    third->FirstFunc();     // 0
    third->SecondFunc(); // 0
    third->ThirdFunc();    // 0
    
    second->FirstFunc();  // 0
    second-> SecondFunc(); // 0
    second-> ThirdFunc() // X

    first->FristFunc(); //0
    first-> SecondFunc(); // X
    first->ThirdFunc(); // X

    return 0;
}

위의 코드에서 보는것과 같이, 실제 instance의 자료형이 무엇이던지 상관 없이 포인터 형에 해당하는 클래스에 정의된 멤버에만 접근이 가능하다. 


2] 가상 함수(Virtual Function)
1. 가상 함수의 필요성
: 만약 위에 예제에서 First 클래스의 멤버 함수를 Second, Third 클래스에서 오버라이딩 한다면 당연히 포인터 형에 해당하는 메서드가 호출될 것이다. 
하지만, 함수를 오버라이딩 했다는 것은 해당 객체에서 호출되어야 하는 함수를 바꾼다는 의미인데, 포인터 변수의 자료형에 따라서 호출되는 함수의 종류가 달라지는 것은 문제가 있어 보일수도 있다.

그래서 가상함수가 필요한 것이다. 

class First
{
public:
    virtual void MyFunc()
    {
        cout<<"First Func"<<endl;
    }
};

class Second : public First
{
public:
    virtual void MyFunc()
    {
        cout<<"Second Func"<<endl;
    }
};

class Third : public Second
{
public:
    virtual void MyFunc()
    {
        cout<<"Third Func"<<endl;
    }
};

위와 같이 선언해 놓으면 포인터 변수의 자료형에 상관없이, 실제 instance의 자료형에 따라 메서드가 호출된다. 

*그리고 virtual 함수를 오버라이딩 하면 그 함수도 virtual 함수가 되어야만 한다. 

int main(void)
{
    First* first = new Third();
   
    first->MyFunc();
}

-> 결과값
: "Third Func"

2. 순수 가상함수(Pure Virtual Function)와 추상 클래스(Abstract Class)
: 추상 클래스는 다들 알다시피, 객체 생성을 목적으로 하는 클래스가 아니다. 순수 가상함수를 구현함으로써, 자식 클래스 들에게 구현의 강요성을 부여하는 것이다. 

class Employee 
{
private:
    char name[100];
public:
    Employee(char* name)
    {
        .....
    }

    void ShowYourName() const
    {
        ....
    }

    virtual int GetPay() const = 0;
    virtual void ShowSalaryInfo() const = 0;
};

위의 코드에서 가상 함수에 0을 대입한 것은 순수 가상 함수라는것을 표현하기 위함이다. 즉, 순수 가상함수는 함수의 몸체가 정의되지 않은 함수인 것이다.


3] 가상 소멸자와 참조자의 참조 가능성
: 가상함수 말고도 virtual 키워드를 붙여줘야 할 대상이 하나 더 있다. 그것은 바로 소멸자이다. 

1. 가상 소멸자(Virtual Destructor)

class First
{
private:
    char* strOne;
public:
    First(char* str)
    {
        strOne = new char[strlen(str) + 1];
    }
   
    ~First()
    {
        delete []strOne;
    }
};

class Second : public First
{
private:
    char* strTwo;
public:
    Second(char* str1, char* str2) : First(str1)
    {
        strTwo = new char[strlen(str2)+1];
    }

    ~Second()
    {
        delete []strTwo;
    }
};

int main(void)
{
    First* ptr = new Second("simple", "complex");

    delete ptr;

    return 0;
}

위 코드에서 ptr 포인터 변수는 First 포인터 형이기 때문에 Second 형 인스턴스를 참조하고 있더라도 delete ptr; 이 호출되면 First 객체의 소멸자만 호출된다. 따라서 메모리 leak이 발생된다. 

이러한 상황을 방지해주기 위해서 virtual 키워드를 소멸자에도 선언해줘야 한다. 

2. 참조자의 참조 가능성
: 참조자(reference)도 포인터 변수의 특성을 그대로 가져간다.
First형 참조자는 Second형, Third형 타입을 참조할 수 있고, 함수 호출 및 가상 함수에 적용되는 규칙도 모두 그대로 적용된다. 


'C++' 카테고리의 다른 글

[C++] 템플릿(Template)(2)  (0) 2017.12.25
[C++] 템플릿(Template)(1)  (0) 2017.12.25
[C++] 상속  (0) 2017.12.25
[C++] Friend와 Static 그리고 Const  (0) 2017.12.25
[C++] 복사 생성자(Copy Constructor)  (0) 2017.12.23
Posted by 홍성곤
,

[C++] 상속

C++ 2017. 12. 25. 19:30

1] 상속의 문법적인 이해
1. 상속의 방법과 그 결과

class Person
{
private:
    int age;
    char name[50];
public:
    Person(int myage, char* myname) : age(myage)
    {
        strcpy(name, myname);
    }
    
    void WhatYourName() const
    {
        cout<<"My name is "<<name<<endl;
    }
   
    void HowOldAreYou() const
    {
        cout<<"i'm "<<age<<" years old"<<endl;
    }
};

class UnivStudent : public Person
{
private:
    char major[50];
public:
    UnivStudent(char* myname, int myage, char* mymajor) : Person(myage, myname)
    {
        strcopy(major, mymajor);
    }
    
    void WhoAreYou() const
    {
        WhatYourName();
        HowOldAreYou();
        
        cout<<"My major is "<<major<<endl;
    }
};

위 코드에서 class UnivStudent : public Person 부분이 상속 문법이다. pulbic의 의미는 나중에 살펴보기로 하자. 

2. 자식(유도) 클래스의 객체 생성과정

class SoBase
{
private:
    int baseNum;
public:
    SoBase() : baseNum(20)
    {
        cout<<"SoBase()"<<endl;
    }
    
    SoBase(int n) : baseNum(n)
    {
        cout<<"SoBase(int n)"<<endl;
    }
    
    void ShowBaseData()
    {
        cout<<baseNum<<endl;
    }
};

class SoDerived : public SoBase
{
private:
    int derivNum;
public:
    SoDerived() : derivNum(30)
    {
        cout<<"SoDerived()"<<endl;
    }
    
    SoDerived(int n) : derivNum(n)
    {
        cout<<"SoDerived(int n)"<<endl;
    }
   
    SoDerived(int n1, int n2) : SoBase(n1), derivNum(n2)
    {
        cout <<"SoDerived(int n1, int n2)"<<endl;
    }

    void showDerivData()
    {
        ShowBaseData();
        cout<<derivNum<<endl;
    }
};

int main(void)
{
    cout<<"case1....."<<endl;
    SoDerived dr1;
    dr1.ShowDerivData();
    cout<<"-------------"<<endl;

    cout<<"case2...."<<endl;
    SoDerived dr2(12);
    dr2.ShowDerivData();
    cout<<"-------------"<<endl;

    cout<<"case3...."<<endl;
    SoDerived dr3(23, 34);
    dr3.ShowDerivData();
    cout<<"--------------"<<endl;

    return 0;
}

-> 결과
case1.....
SoBase()
SoDerived()
20
30
-------------

case2.....
SoBase()
SoDerived(int n)
20
12
-------------

case3.....
SoBase(int n)
SoDerived(int n1, int n2)
23
34
-------------

결론
: 자식 클래스의 객체생성 과정에서 부모 클래스의 생성자는 100% 호출된다.
: 자식 클래스의 생성자에서 부모 클래스의 생성자 호출을 명시하지 않으면, 부모 클래스의 void 생성자가 호출된다.

3. 자식(유도) 클래스의 객체 소멸과정
: 생성 과정과는 반대로 자식 클래스의 소멸자가 먼저 불리고, 부모 클래스의 소멸자가 나중에 불린다. 


2] 세 가지 형태의 상속
: protected는 알고있다 시피 상속 클래스에서의 접근을 허용한다. 그러나 상속관계 에서도 정보은닉의 원칙을 지키는 것이 좋기 때문에 자주 사용되지는 않는다. 

class Base
{
private:
    int num1;
protected:
    int num2;
private:
    int num3;
}

1. protected 상속
: protected 상속의 의미는 "protected 보다 접근 범위가 넓은 멤버는 protected로 변경시켜서 상속하겠다." 는 뜻이다. 

class Derived : protected Base
{
접근불가:     
    int num1;
protected:
    int num2;
protected:
    int num3;
};

Base 클래스에서 num1은 private 으로 선언되었기 때문에 자식 클래스인 Derived 에서도 접근이 불가능 하다. num3은 public으로 protected 보다 접근범위가 넓기 때문에 protected로 바껴서 상속된다. 

2. private 상속
: private 상속의 의미는 "private 보다 접근 범위가 넓은 멤버는 private로 변경시켜서 상속하겠다." 라는 뜻이다. 

class Derived : private Base
{
접근불가:
    int num1;
private:
    int num2;
private:
    int num3;
};

이 경우 Derived 클래스를 다른 클래스가 상속한다면 접근할 수 있는 멤버변수가 하나도 없게 된다.

3. public 상속
: public 상속의 의미는 public 보다 넓은 접근 범위는 없기 때문에, 바꿔 말하면 이렇게 된다. 
"private을 제외한 나머지는 그냥 그대로 상속한다." 

사실 protected, private 상속은 다중상속과 같이 특별한 경우가 아니면 잘 사용하지 않는다. 


'C++' 카테고리의 다른 글

[C++] 템플릿(Template)(1)  (0) 2017.12.25
[C++] 상속과 다형성  (0) 2017.12.25
[C++] Friend와 Static 그리고 Const  (0) 2017.12.25
[C++] 복사 생성자(Copy Constructor)  (0) 2017.12.23
[C++] 클래스의 완성  (1) 2017.12.23
Posted by 홍성곤
,

1] Const
1. 객체의 상수화
: C++ 에서는 변수에 const를 붙이는것 이외에 객체에도 const를 붙일 수 있다. 

const SoSimple sim(20);

이렇게 객체에 const가 붙게 되면, 이 객체를 대상으로는 const 멤버함수만 호출이 가능하다. 
즉, 이 객체의 데이터 변경을 허용하지 않는다. 

2. const와 함수 오버로딩
: 함수 오버로딩이 성립하려면 매개변수의 수나 자료형이 달라야 한다. 하지만 const 선언유무도 함수 오버로딩의 조건에 해당이 된다. 

void SimpleFunc() { ... }
void SimpleFunc() const { ... }

즉, const로 선언된 객체가 함수를 호출하면 const 함수가 불리고 일반 객체가 함수를 호출하면 일반 함수가 호출된다. 

2] 클래스와 함수에 대한 friend 선언
1. 클래스의 friend 선언
: friend 선언은 private 멤버의 접근을 허용하는 선언이다. 

class Girl; // Girl 이라는 이름이 클래스의 이름임을 알림

class Boy
{
private:
    int height;
    friend class Girl;  // 사실 위에 class  Girl; 문장이 없어도 된다. 이 문장은 Girl이 클래스라는 사실과 Girl 클래스가 friend 라는 사실을 동시에 알려준다. 
};

class Girl
{
private:
    char phNum[20];
public:
    Girl(char* num)
    {
        strcpy(phNum, num);
    }
    
    void ShowYourFriendInfo(Boy &frn)
    {
        cout << "His height: "<<frn.height << endl;
    }
};

Boy 클래스가 Gril 클래스를 Friend 선언했다. 그래서 Girl 클래스는 Boy 클래스의 private 멤버의 접근이 가능하다. 그 반대는 안된다. 

* friend 선언은 얽히고 설킨 클래스의 관계를 friend로 풀려는 사람들을 종종 볼수 있다. 이 방법은 friend 사용의 잘못된 예이다. 정보의 은닉성을 훼손하는 행위이기 때문이다. friend 사용은 최대한 소극적으로 하는게 맞다. 다만, 이후에 배울 연산자 오버로딩에서 효율적으로 friend를 사용하는 방법을 배울것이다. 

2. 함수의 friend 선언
: 전역함수를 대상으로도, 클래스 멤버함수를 대상으로도 friend 선언이 가능하다. 

class Point;

class PointOP
{
private:
    int opcnt;
public:
    PointOP() : opcnt(0) {  }
    
    Point PointAdd(const Point&, const Point&);
};

class Point
{
private:
    int x;
    int y;
public:
    Point(const int &xpos, const int &ypos) : x(xpos), y(ypos) {  }
   
    friend Point PointOP::PointAdd(const Point&, const Point&);
    friend void ShowPointPos(const Point&);
};

Point PointOP::PointAdd(const Point& pnt1, const Point& pnt2)
{    
    opcnt++;
    return Point(pnt1.x+pnt2.x, pnt1.y+pnt2.y);
}

void ShowPointPos(const Point& pos)
{
    cout<<"x: "<<pos.x<<endl;
    cout<<"y: "<<pos.y<<endl;
}

위 코드에서 Point클래스에서 PointOP 클래스의 PointAdd 멤버함수를 friend 선언했기 때문에, PointAdd 멤버함수 에서는 Point의 private 멤버의 접근이 가능하다. 
또한 Point클래스에서 ShowPointPos() 전역 함수도 friend 선언했기 때문에 ShowPointPos() 전역함수에서도 Point 클래스의 private 멤버의 접근이 가능하다.

그리고 Point 클래스에서 friend void ShowPointPos(const Point&); 선언은 friend 선언 이외에 함수 원형 선언의 의미도 포함되어 있어서 따로 함수 원형 선언을 할 필요가 없다. 


3] C++ 에서의 static 
1. C언어에서 이야기한 static
- static 전역변수 
: 선언된 파일 내에서만 참조를 허용하겠다는 의미
- 함수 내에 선언된 static 의미
: 한번만 초기화되고, 지역변수와 달리 함수를 빠져나가도 소멸되지 않는다. 

2. static 멤버 변수
: 멤버 변수를 static 으로 선언하면, 해당 변수는 instance 마다 하나씩 존재하는 변수가 아니라 클래스에 하나만 존재하는 변수이고, 모든 instance가 해당 변수를 공유한다. 

class SoSimple
{
private:    
    static int simObjCnt;
public:
    SoSimple()
    {
        simObjCnt++;
    }    
};

int SoSimple::simObjCnt = 0;

위 코드에서 simObjCnt는 static 멤버변수이다. instance가 생성될 때 마다 값이 1 증가할 것이다. 
int SoSimple::simObjCnt = 0; 는 static 멤버변수를 초기화 할때의 문법이다. 풀어서 해석하면 "simObjCnt가 메모리 공간에 할당될때 0으로 초기화 해줘라" 라는 뜻이다.

3. static 멤버변수의 또 다른 접근방법
: static 멤버변수는 public으로 선언되기만 하면 어디서든 접근이 가능한 변수다.
위 SoSimple 클래스에서 simObjCnt가 public으로 선언되어 있으면 아래와 같은 문법으로 접근이 가능하다. 

SoSimple sim;

SoSimple.simObjCnt;
sim.simObjCnt; // 이 문법은 인스턴스 멤버변수에 접근하는것과 같은 착각을 일으킨다. 가급적 클래스 이름으로 접근하는 방법을 택하자. 

4. static 멤버 함수
: 개념은 static 멤버 변수와 같다. 단지 변수가 함수로 바뀌었을 뿐이다. 그리고 static 멤버 함수 안에서는 인스턴스 멤버변수에 접근할 수 없다. 
즉, static 멤버 함수 안에서는 static 멤버 변수와 static 멤버 함수만 호출이 가능하다. 

5. const static 멤버
: 이전에 배웠듯이 const 멤버변수(상수)는 이니셜라이저를 통해서만 초기화할 수 있었다. 그러나 const static으로 선언되는 멤버변수는 다음과 같이 선언과 동시에 초기화가 가능하다. 

class CountryArea
{
public:
    const static int RUSSIA = 1707540;
    const static int CANADA = 998467;
};

6. 키워드 mutable
: "mutable" 키워드의 의미는 "const 함수 내에서의 값의 변경을 에외적으로 허용한다." 이다. 설명만 들어도 가급적 사용하면 안될것 같은 느낌이 든다.

class SoSimple
{
private:
    mutable int num2;
public:
    void setNum2(int num) const
    {
        num2 = num;
    }
};

위 코드와 같이 setNum2() 는 const 멤버함수로 선언되어 있는데도 불구하고 num2 멤버 변수가 mutable 선언되어 있기 때문에 const 멤버함수에서 예외적으로 값 변경이 가능하다. 



'C++' 카테고리의 다른 글

[C++] 상속과 다형성  (0) 2017.12.25
[C++] 상속  (0) 2017.12.25
[C++] 복사 생성자(Copy Constructor)  (0) 2017.12.23
[C++] 클래스의 완성  (1) 2017.12.23
[C++] 클래스의 기본  (0) 2017.12.23
Posted by 홍성곤
,

1] 복사 생성자?
1. C++ 스타일의 초기화
우리는 지금까지 밑의 방법으로 변수 및 참조자(reference)를 초기화해 왔다.

int num = 20;
int &ref = num;

하지만 C++ 에서는 다음의 방식으로 초기화가 가능하다. 

int num(20);
int &ref(num);

즉, int num이 int num(20)과 의미가 동일하다. 이것을 객체와 연관시켜보자. 

class SoSimple
{
private:
    int num1;
    int num2;
public:
    SoSimple(int n1, int n2) : num1(n1), num2(n2) { }
}
       
int main(void)
{
    SoSimple sim1(15, 20);
    SoSimple sim2 = sim1;

    return 0;
}

위 코드에서 sim2는 sim1에 멤버변수값을 깊은 복사하게 된다. 

이전에 살펴봤듯이, 

SoSimple sim2 = sim1
SoSimple Sim2(sim1)

위 두 문장은 동일한 의미로 해석된다. 
SoSimple Sim2(sim1) 코드가 정상적으로 작동하려면 SoSimple 객체를 인자로 받는 생성자가 있어야 되는것 아닌가? 그렇다! 이 생성자가 바로 복사 생성자이고, 이것은 컴파일러에 의해서 자동으로 생성된다. 
즉, 아래와 같은 복사 생성자가 자동으로 삽입된다.

SoSimple(const SoSimple& copy) : num1(copy.num1), num2(copy.num2) {  } 

2. explicit로 복사 생성자의 묵시적 호출을 막자!
SoSimple sim2 = sim1은 SoSimple sim2(sim1)로 컴파일러에 의해 변환되어 복사생성자가 호출된다. 즉, 프로그래머가 의도하지 않더라도 컴파일러에 의해 호출되는 것을 묵시적 호출이라 부른다. 
다만, 위와 같은 묵시적 호출이 마음에 들지 않으면 "explicit" 키워드를 사용해서 막을 수 있다.

explicit SoSimple(const SoSimple& copy) : num1(copy.num1), num2(copy.num2) {  } 

위와같이 선언하면, 복사생성자를 호출하기 위해서는 명시적으로 호출해야 한다. 
그리고 이와같은 묵시적 호출은 복사 생성자에게만 일어나는 것이 아니다. 전달인자가 하나인 생성자가 있다면, 이 역시 묵시적 변환이 발생한다. 다음 코드를 보자. 

class AAA
{    

private:       
    int num;
public:
    AAA(int n) : num(n) { }
}

위와 같이 정의된 클래스가 있으면,

AAA obj = 3; 은 AAA obj(3); 으로 변환된다. 이와같은 경우에 explicit 키워드를 써서 명시적 호출만 허용하면, AAA obj(3); 와 같은 방법으로 객체를 생성해야 한다. 

2] 복사 생성자의 호출 시점 
1.  메모리 공간의 할당과 초기화가 동시에 일어나는 상황
: 복사 생성자의 호출시기를 논하기에 앞서, 먼저 메모리 공간이 할당과 동시에 초기화되는 상황을 나열해 보자. 

int num1 = num2;

위 코드에서 num1 이라는 메모리 공간을 할당해서 num2의 값으로 초기화 시키고 있다. 

int SimpleFunc(int n)

    ...
    return n;
}

int num = 10;
cout<<SimpleFunc(num)<<endl; 

위 코드에서 SimpleFunc의 매개변수 int n은 SimpleFunc 호출 시점에 메모리 공간이 할당됨과 동시에 num 변수의 값인 10으로 초기화 된다. 

그리고! return n; 문장에서도 변수 n의 값을 return 하기위한 임시 메모리 공간이 할당됨과 동시에 n 변수의 값으로 초기화 된다.
?? 반환하는데 메모리 공간이 할당된다? 조금 이해가 안갈 수도 있다. 
하지만, cout<<SimpleFunc(num)<<endl; 문장이 실행되는 과정을 생각해보면 이해할 수 있을것이다.
SimpleFunc 함수에 의해서 반환되는 값을 메모리 공간의 어딘가에 저장해 놓지 않았다면, cout에 의해 출력이 가능하겠는가? 
값이 출력되기 위해서는 그 값을 참조할 수 있어야 하고, 참조가 가능 하려면 메모리 공간의 어딘가에 저장되어야 한다. 

객체의 경우도 마찬가지다. 

SoSimple SimpleFuncObj(SoSimple ob)
{
    ...
    return ob;
}
    
int main(void)
{
    SoSimple obj;
    SimpleFuncObj(obj);
    
    return 0;
}

위의 코드에서도 SimpleFuncOb 함수가 호출되는 순간, 매개변수 ob의 객체 생성과 동시에 초기화가 일어나고 return ob; 에 의해서도 임시 객체 생성과 동시에 초기화가 일어난다. 

2. 할당 이후, 복사 생성자를 통한 초기화
: 위에서 할당과 동시에 초기화가 되는 세가지 시점을 살펴보았다. 
- 객체 생성과 동시에 초기화
- 함수 전달시 매개 변수 객체 생성과 동시에 초기화
- 함수에서 return시 반환을 위한 임시 객체 생성과 동시에 초기화

위와 같은 상황에서 SoSimple 클래스 인스턴스에 대한 메모리가 생성되고 SoSimple 인스턴스를 매개 변수로 받아 복사생성자가 호출되면서 해당 인스턴스가 초기화 된다. 
즉,  SimpleFuncObj() 함수를 한번 호출하면 매개변수 ob의 복사생성자가 호출되고, return 문에 의한 임시 객체에 대한 메모리가 할당되고 그 임시 객체의 복사생성자가 호출된다. 

* 다만, SoSimple tempRef = SimpleFuncObj(obj) 호출시 추가적으로 tempRef 객체가 생성되고 tempRef에 대한 생성자 호출이 일어나지 않는다. SimpleFuncObj 함수의 return 문에 의한 임시객체가 생성되고 그 임시객체를 반환하지 않고 tempRef가 참조하게 만듦으로써, 객체의 생성 수를 하나 줄여준다. 

'C++' 카테고리의 다른 글

[C++] 상속  (0) 2017.12.25
[C++] Friend와 Static 그리고 Const  (0) 2017.12.25
[C++] 클래스의 완성  (1) 2017.12.23
[C++] 클래스의 기본  (0) 2017.12.23
[C++] C언어 기반의 C++(2)  (1) 2017.12.19
Posted by 홍성곤
,

[C++] 클래스의 완성

C++ 2017. 12. 23. 20:17

1] 정보 은닉
: 멤버 변수에 직접 접근하는 것을 허용하지 않고 setter, getter를 통해 접근하도록 하는 것이다. 
이는 제한된 방법으로의 접근만 허용을 해서 잘못된 값이 저장되지 않도록 도와야 하고, 또 실수를 했을 때 실수가 쉽게 발견 될수 있도록 한다. 

1. const 함수 
특히 아래와 같은 종류들의 함수에 const 함수를 적용할 수 있을 것이다.

int GetX() const;
int GetY() const;
void ShowRecInfo() const;

위 함수들은 이름으로도 알 수 있듯이 멤버변수를 단순히 get, 또는 show 하는 용도로 쓰이는 함수들이다. 그러니 멤버 변수의 변경이 일어나면 안되는 종류의 함수들이다. 그래서 이러한 함수 안에서는 멤버변수의 변경이 불가하도록 하는 장치가 "const" 키워드이다. 

또한 const 함수 내에서는 const가 아닌 함수의 호출이 제한된다!

그리고 매개변수에도 "const" 키워드를 추가할 수 있다. 

void InitNum(const EasyClass &east)
{
    num=easy.GetNum();
}

class EasyClass
{
    public:
    int GetNum()  // const 선언이 추가되어야 컴파일 에러 소멸.
    {
        return num;
    }   
}

매개 변수의 const 선언은 참조자를 대상으로 값의 변경 능력을 가진 함수의 호출을 허용하는 것을 막는다. 


2] 캡슐화(Encapsulation)
: 캡슐화는 관련 있는 함수와 변수를 하나의 클래스 안에 묶는 것이다. 별로 어렵지 않게 느껴질 수 있지만, 캡슐화는 어려운 개념이다. 왜냐하면 캡슐화의 범위를 결정하는 일이 쉽지 않기 때문이다. 


3] 생성자(Constructor)와 소멸자(Destructor)
1. 생성자의 이해
- 클래스의 이름과 생성자의 이름이 동일해야 한다. 
- 반환형이 선언되어 있지 않으며, 실제로 반환하지 않는다.
- 생성자도 함수의 일종이니 오버로딩이 가능하다. 
- 생성자도 함수의 일종이니 매개변수에 디폴트 값을 설정할 수 있다.

class SimpleClass
{
private:
    int num1;
    int num2;
public:
    SimpleClass(int n1) // 생성자
    {
        num1 = n1;
        num2 = 2;
    }

    SimpleClass(int n1, int n2)
    {
        num1 = n1;
        num2 = n2;
    }

    int GetNum1() const
    {
        return num1;
    }

    int GetNum2() const
    {
        return num2;
    }
}

* 함수의 원형을 지역적으로 선언

int main(void)
{
    SimpleClass sc1(); // 함수의 원형 선언!
    SimpleClass mysc = sc1();
    
    return 0;
}

SimpleClass sc1()
{
    SimpleClass sc(20, 30);
    
    return sc;
}

2. 멤버 이니셜라이저를 이용한 멤버 초기화

아래는 Point 클래스 선언이다.

class Point 
{
private:
    int x;
    int y;
public:
    Point(const int &xpos, const int &ypos); // 생성자
    int GetX() const;
    int GetY() const;
    bool SetX(int xpos);
    bool SetY(int ypos);
}

아래는 Point 클래스의 생성자 정의다.

Point::Point(const int &xpos, const int &ypos)
{
    x = xpos;
    y = ypos;
}

아래는 Rectangle 클래스의 선언이다.

class Rectangle
{
private:
    Point upLeft;
    Point lowRight;
public:
    Rectangle(const int &x1, const int &y1, const int &x2, const int &y2);
};

아래는 Rectangle 클래스의 생성자 정의다. 

Rectangle:Rectangle(const int &x1, const int &y1, const int &x2, const int &y2) : upLeft(x1, y1), lowRight(x2, y2)
{
    //empty
}

이 중에서 :upLeft(x1, y1), lowRight(x2, y2) 가 멤버 이니셜라이저다. 
풀어서 설명하면,
- 객체 upLeft의 생성과정에서 x1과 y1을 인자로 전달받는 생성자를 호출하라
- 객체 lowRight의 생성과정에서 x2와 y2를 인자로 전달받는 생성자를 호출하라

위 생성자 호출시 실행 순서는 멤버 이니셜라이저가 먼저 호출되고 생성자의 몸체가 나중에 실행된다. 

3. 멤버 이니셜라이저를 이용한 변수 및 const 상수(변수) 초기화
: 멤버 이니셜라이저는 객체가 아닌 멤버의 초기화에도 사용할 수 있다. 

class SoSimple
{
private: 
    int num1;
    int num2;
public:
    SoSimple(int n1, int n2) : num1(n1)
    {
        num2 = n2;
    }
};

이와 같이 객체가 아닌 멤버 변수도 멤버 이니셜라이저로 초기화 할 수 있다. 그리고 멤버 변수들을 초기화 할때 생성자에서 하는것 보다 멤버 이니셜라이저를 통하는것이 더 권장된다. 그 이유는 다음과 같다.
- 초기화의 대상을 명확히 인식할 수 있다.
- 성능에 약간의 이점이 있다.

두번째 이유에 대해서 조금 더 설명하자면, 위 예재에서 num2를 생성자에서 초기화 하고 있는데 이는 바이너리 코드가 구성될 때 아래와 같이 구성된다.

int num2;
num2 = n2;

멤버 이니셜라이저를 이용해 초기한 num1의 경우는 다음과 같이 구성된다. 

int num1 = n1;

위와 같은 관점에서 const 멤버 변수에 대해서도 생각해보면 const 멤버 변수는 생성과 동시에 초기화가 되어야만 한다.
그래서 const 멤버 변수도 멤버 이니셜라이저를 통해 초기화가 가능하다.

* const 변수와 마찬가지로 참조자(reference)도 선언과 동시에 초기화가 이루어져야 한다.
따라서 이니셜라이저를 이용하면 참조자도 멤버변수로 선언될 수 있다. 

4. 디폴트 생성자
: 모든 클래스는 하나 이상의 생성자를 가지고 있다. 프로그래머가 생성자를 따로 정의하지 않을 경우 컴파일러가 디폴트 생성자를 하나 만들어 준다.
디폴트 생성자는 매개변수를 하나도 받지 않고 아무 행동도 하지 않는 생성자이다. 
프로그래머가 생성자를 하나라도 따로 정의한 경우 디폴트 생성자는 만들어지지 않는다. 

* malloc을 통한 동적 할당
: malloc을 통해 객체의 메모리를 동적으로 할당 했을 경우 생성자가 불리지 않는다.

AAA *ptr = (AAA *)malloc(sizeof(AAA));

위 코드에서 보듯이 메모리를 할당할 때 AAA 객체의 크기만 받기 때문에 생성자를 호출해 줄리 없다. 따라서 C++ 객체를 생성할 경우에는 "new" 를 통해 생성해야 한다. 

5. private 생성자 
: 클래스 내부에서만 객체의 생성을 허용하려는 목적으로 생성자를 private으로 선언하기도 한다. 

class AAA
{
private: 
    int num;
public:
    AAA& createInitObj(int n) const
    {
        AAA *ptr = new AAA(n);
        
        return *ptr;
    }   
private:
    AAA(int n) : num(n) {}
}

위 예제에서는 힙 영역에 생성된 객체를 참조(reference)의 형태로 반환하고 있다.
이는 앞서 설명한 "힙에 할당된 메모리 공간은 변수로 간주하여, 참조자를 통한 참조가 가능하다" 라는 사실을 다시 한번 확인시켜 준다. 

6. 소멸자의 이해와 활용 
: 소멸자는 대게 생성자에서 할당한 리소스의 소멸에 사용된다. 예를 들어서 생성자 내에서 new 연산자를 이용해서 할당해 놓은 메모리 공간이 있다면, 소멸자에서는 delete 연산자를 이용해서 이 메모리 공간을 소멸한다. 

- 클래스의 이름 앞에 "~"가 붙은것이 소멸자다. 

4] 클래스와 배열 그리고 this 포인터 
1. 객체 배열
: SoSimple 클래스의 배열의 선언은 다음과 같다. 

SoSimple arr[10];

동적 할당하는 경우에는 다음과 같다.

SoSimple *ptrArr = new SoSimple[10];

위와 같이 일괄적으로 10개의 객체를 배열로 선언하면 각 객체에 생성자가 불리면서 객체가 만들어지는데, 생성자에 매개변수를 전달하지 못한다. 
그래서 위와 같이 선언하려면 아래와 같은 생성자가 필히 존재해야 한다. 

SoSimple() { ... }

2. 객체 포인터 배열 

Person *parr[2];

parr[0] = new Person("sung gon", 19);
parr[1] = new Person("gil dong", 25);

delete parr[0];
delete parr[1];

3. Self-Reference의 반환
: this가 자신을 가리키는 포인터라는 것은 다 알것이다. 
Self-Reference는 자신을 참조할 수 있는 참조자(Reference) 이다. 우리는 this 포인터를 이용해서 객체가 자신의 참조에 사용할 수 있는 참조자의 반환문을 구성할 수 있다. 

Class SelfRef
{
private:
    int num;
public:
    SelfRef& Adder(int n)
    {
        num += n;
        
        return *this;
    }
}

'C++' 카테고리의 다른 글

[C++] Friend와 Static 그리고 Const  (0) 2017.12.25
[C++] 복사 생성자(Copy Constructor)  (0) 2017.12.23
[C++] 클래스의 기본  (0) 2017.12.23
[C++] C언어 기반의 C++(2)  (1) 2017.12.19
[C++] C언어 기반의 C++(1)  (0) 2017.12.18
Posted by 홍성곤
,

[C++] 클래스의 기본

C++ 2017. 12. 23. 17:19

1] C++ 에서의 구조체
1. C++ 에서의 구조체 변수의 선언
C언어 에서는 구조체를 다음과 같이 선언한다. 

struct Car basicCar;
struct Car simpleCar;

위와같이 C 언어에서 struct 키워드를 포함해야 하며 생략하기 위해서는 typedef을 별도로 추가해야 한다. 하지만 C++에서는 일반 자료형 변수의 선언방식과 크게 다를바 없다. 

Car basicCar;
Car simpleCar;

2. 구조체 안에 함수, enum 삽입하기

struct Car
{
    enum
    {
        ID_LEN = 20,
        MAX_SPD = 200,
        FUEL_STEP = 2,
        ACC_STEP = 10,
        BRK_STEP = 10
    }

    void ShowCarState()
    {
         ...
    }
    
    void Accel()
    {
        ...
    }

    void Break()
    {
        ...
    }
};

3. 함수는 외부로 뺄 수 있다. 

struct Car
{
    char gamerID[20];
    int fuelGuage;
    int curSpeed;
    
    void showCarState();
    void Accel();
};

void Car::ShowCarState()
{
    ...
}

void Car::Accel()
{
    ...
}

int main(void)
{
    Car run99 = { "run99", 100, 0 };
    
    run99.Accel();
    run99.ShowCarState();
}

사실 구조체 안에 함수가 정의되어 있으면, 다음의 의미가 더불어 내포된다. 
"함수를 인라인으로 처리해라!"
반면, 위의 예제와 같이 함수를 구조체 밖으로 빼내면, 이러한 의미가 사라진다. 따라서 인라인의 의미를 그대로 유지하려면 다음과 같이 키워드 "inline"을 이용해서 명시적으로 지시해야 한다. 

inline void Car::showCarState()
{
    ...
}

inline void Car::Accel()
{
    ...
}


2] 클래스(Class)와 객체(Object)
1. 클래스와 구조체의 차이점
앞서 선언했던 구조체 Car를 클래스로 정의해 보겠다.

Class Car
{
    char gamerID[20];
    int furlGauge;
    int curSpeed;
    
    void ShowCarState()
    {
        ...
    }
    
    void Accel()
    {
        ...
    }
}

구조체와 다른점은 앞에 "struct" 키워드를 "class"로 바꾼것 밖에 없다. 이제 사용할때의 차이점을 알아 보겠다.

Car run99 = {"run99", 100, 0}

위 코드는 컴파일 되지 않는다. 클래스 내에 선언된 변수는 앞에 접근지정자를 지정하지 않으면 자동으로 클래스 내의 함수에서 밖에 접근하지 못한다.

이렇듯 구조체와 클래스의 유일한 차이점은 외부에서 각각의 변수 및 함수에 접근 가능하도록 하려면 접근범위를 별도로 지정해야 되는지 아닌지에 차이가 있다. (사실 더 많은 차이가 있지만, 지금은 이 정도로만 알고 넘어가자.)

2. 접근제어 지시자 
- public: 어디서든 접근 허용
- protected: 상속관계에 놓여있을 때, 자식클래스만 접근 허용
- private: 클래스 내에서만 접근 허용

1) 만약 접근제어 지시자 A가 선언되면, 그 이후에 등장하는 변수나 함수는 A에 해당하는 범위 내에서 접근이 가능하다. 
2) 그러나 새로운 접근제어 지시자 B가 선언되면, 그 이후로 등장하는 변수나 함수는 B에 해당하는 범위 내에서 접근이 가능하다.
3)  구조체에서 접근제어 지시자를 지정하지 않으면 자동으로 public 지정되고, Class에서 지정하지 않으면 자동으로 private 지정된다. 

3. 파일 분할
: C++ 에서는 클래스의 선언을 .h 파일에 담고, 정의는 .cpp 파일에 담아 파일분할을 한다. 
즉, .h 파일에는 컴파일에 필요한 정보들을 담고 .cpp 파일에는 실제 구현내용을 담는다. 
링커는 .cpp에 있는 내용들로 서로 링크시켜서 하나의 실행파일 만든다. 

*inline 함수는 헤더파일에 함께 넣어야 한다. 
: inline 함수는 컴파일러에 의해 함수 호출부분을 구현부분으로 대체해야 하기 때문에 inline 함수의 구현은 .h 파일에 위치해야 컴파일러가 적절하게 컴파일 할 수 있다. 

'C++' 카테고리의 다른 글

[C++] Friend와 Static 그리고 Const  (0) 2017.12.25
[C++] 복사 생성자(Copy Constructor)  (0) 2017.12.23
[C++] 클래스의 완성  (1) 2017.12.23
[C++] C언어 기반의 C++(2)  (1) 2017.12.19
[C++] C언어 기반의 C++(1)  (0) 2017.12.18
Posted by 홍성곤
,

[C++] C언어 기반의 C++(2)

C++ 2017. 12. 19. 10:46

1] Remind C
1. const의 의미 
const int num = 10; // 변수 num을 상수화
const int* ptr1 = &val1; // ptr1을 이용해서 val1의 값을 변경할 수 없음
int* const ptr2 = &val2; // ptr2를 상수화

2. 메모리 영역
- 데이터 영역: 전역변수가 저장되는 영역
- 스택: 지역변수 및 매개변수가 저장되는 영역
- 힙: malloc 함수 호출에 의해 프로그램이 실행되는 과정에서 동적으로 할당이 이뤄지는 영역.
- malloc & free : malloc 함수호출에 의해 할당된 메모리 공간은 free 함수호출을 통해서 소멸되지 않으면 해제되지 않는다. 


2] bool
: true와 false는 1과 0이 아니다. 

cout<<"true : "<<true<<endl;
cout<<"false : "<<false<<endl;

->
true : 1
false : 0

위 결과는 보고 true는 1, false는 0 이라고 생각할 수도 있다.

cout<<"sizeof 1: "<<sizeof(1)<<endl;
cout<<"sizeof 0: "<<sizeof(0)<<endl;
cout<<"sizeof true: "<<sizeof(true)<<endl;
cout<<"sizeof false: "<<sizeof(false)<<endl;

->
sizeof 1 : 4
sizeof 0 : 4
sizeof true : 1 
sizeof false : 1

위 결과를 보면 true, false의 크기와 0, 1의 크기가 다르다. 
true와 false가 정의되기 전에는 참과 거짓을 표현하기 위해서 1, 0으르 사용했기 때문에 이 둘을 출력하거나 정수의 형태로 형 변환하는 경우에 각각 1과 0으로 변환되도록 정의되어 있을 뿐이다. 

따라서 true와 false는 참과 거짓을 나타내는 목적으로 정의도니 데이터로 인식하는 것이 바람직하다. 


3] reference의 이해
: 할당된 하나의 메모리 공간에 다른 이름을 부여하는 것이다.

int &num2=num1;

위에서 "&"는 주소값을 반환하는 연산자가 아니라, 참조자의 선언을 뜻한다. 

int num1 = 1020;
int &num2 = num1;

num2 = 3047;

위 코드를 실행하면 num1, num2 모두 3047을 가진다. 이 결과는 하나의 메모리 공간을 똑같이 가리키니까 어찌보면 당연한 결과다.

1) 참조자(reference)를 이용한 Call-by-reference

void SwapByRef(int &ref1, int &ref2)
{
    int temp=ref1;

    ref1=ref2;
    ref2=temp;
}

2) 참조자(reference)를 이용한 Call-by-reference, 그리고 const 참조자
: 지금까지만 보면, 포인터를 이용한 Call-by-reference 보다 reference를 이용한 것이 더 간단하고 편해보인다. 하지만 단점이 있다.

int num=24;

HappyFunc(num);

위 코드에서 num의 값은 HappyFunc 함수 호출이후 어떤값이 될까? C라면 100프로 24가 되야 하지만, C++ 에서는 알수가 없다. reference를 이용한 Call-by-reference일 수도 있기 때문이다. 

이런 단점을 극복하려면 const 키워드를 사용해서 상수화 시켜야 한다. 

void HappyFunc(const int &ref) { ... }

위에서 ref는 함수내에서 값을 변경할 수 없다. 따라서, 함수 내에서 참조자를 통한 값의 변경을 진행하지 않은경우는 const 키워드를 사용해서 함수 원형만 봐도 값의 변형이 이뤄지지 않음을 알수있게 해라.

3) 반환형이 참조형(reference type)인 경우

int& RefRetFuncOne(int &ref)
{
    ref++;
    return ref;
}

int num1=1;
int &num2=RefRetFuncOne(num1);

num1++;
num2++;

cout<<"num1: "<<num1<<endl;
cout<<"num2: "<<num2<<endl;

-> 출력
num1: 4
num2: 4 

그런데 위 함수의 반환값을 int형으로 받으면 어떻게 되는지 보겠다.

num1 = 1;
int num2 = RefRetFuncOne(num1);

num1+=1;
num2+=100;

cout<<"num1: "<<num1<<endl;
cout<<"num2: "<<num2<<endl;

-> 출력
num1: 3
num2: 102

int&을 int형으로 받을 수 있고 이는 완전 다른 변수가 된다. 
그렇지만 int 반환값을 int&로 받을 수는 없다. 

다음은 잘못된 예를 들어보겠다.

int& RetuRefFunc(int n)
{
    int num = 20;
    
    num+=n;
    
    return num;
}

int &ref = RetuRefFunc(10);

int를 return 하지만 함수의 반환형으로 인해 int& 타입이 return 된다. 
그래서 ref 변수는 num 변수의 메모리를 같이 참조하게 되지만, num은 지역변수 이기 때문에 반환되어 ref 변수는 쓰레기값을 가지게 된다. 

4) 참조자(reference)의 상수 참조
: 리터럴 상수를 reference가 참조하려면 const 키워드를 사용해야 한다. 

const int *ref = 30;

위와같은 코드가 과연 필요한지 의문이 들기는 하지만 굳이 사용법을 찾아보겠다.

int Adder(const int& num1, const int& num2)
{
    return num1+num2;
}

위 함수에서 매개변수 둘을 더한값을 return하기 위해 변수 하나를 더 만드는것이 귀찮기 때문에 저런식으로 사용한 것이다. 


4] malloc & free를 대신하는 new & delete
: malloc, free를 사용해서 heap에 메모리를 할당, 해제하는것보다 조금 더 간단해졌다. 

int* ptr1 = new int; // int형 변수의 할당
double* ptr2 = new double; // double형 변수의 할당
int* arr1 = new int[3]; // 길이가 3인 int 형 배열의 할당
double * arr2 = new double[7]; //길이가 7인 double형 배열의 할당

delete ptr1;
delete ptr2;
delete []arr1;
delete []arr2;

* C++ 에서는 malloc, free 대신에 꼭 new, delete를 사용하자. 둘은 사용방식에 차이가 있어서 C++에서 malloc, free의 사용이 오류를 일으킬 수 있다. 

* heap에 할당된 변수도 참조자(reference)로 접근할 수 있다. 

int* ptr = new int;
int& ref = (*ptr);

ref = 20;

cout<<(*ptr)<<endl;

-> 출력
20




'C++' 카테고리의 다른 글

[C++] Friend와 Static 그리고 Const  (0) 2017.12.25
[C++] 복사 생성자(Copy Constructor)  (0) 2017.12.23
[C++] 클래스의 완성  (1) 2017.12.23
[C++] 클래스의 기본  (0) 2017.12.23
[C++] C언어 기반의 C++(1)  (0) 2017.12.18
Posted by 홍성곤
,

[C++] C언어 기반의 C++(1)

C++ 2017. 12. 18. 23:03

1] 헤더파일 선언문 #include<iostream>
: C에서는 입출력을 위해 <stdio.h>를 포함했는데, C++은  <iostream>를 포함해야 한다.
그러면, C++은 헤더파일 확장자(.h)가 없는가? 아니다. 요즘 버전의 C++ 에서는 프로그래머가 정의하는 헤더파일의 선언이 아닌, 표준 헤더파일의 선언에서는 확장자를 생략하기로 약속되어 있다. 

때문에, <iostream.h>는 과거 C++ 버전의 표준 입출력 라이브러리 및 헤더를 의미하는 것이다. 
참고로 C++ 컴파일러는 점차 <iostream.h>을 지원하지 않는 추세로 접어들었다. 


2] 매개변수의 디폴트 값(Default Value) 
: 함수의 매개변수는 default 값을 가질 수 있다. 

int Adder (int num1=1, int num2=2)
{
    return num1+num2;
}

int main(void)
{
    std::cout<<Adder()<<std::endl;
    std::cout<<Adder(5)<<std::endl;
    std::count<<Adder(3, 5)<<std::endl;
}

// 3, 7, 8 이 차례대로 출력된다. 

디폴트 값은 함수의 선언부분에만 표현하면 된다. 

int Adder(int num=1, int num2=2);

int main(void)
{
    ...
}

int Adder(int num1, int num2)
{
    ...
}

위와 같이 디폴트 매개변수 값은 선언 부분에만 명시하면 된다. 구현 부분에는 명시할 필요 없다. 

int function(int num1=3, int num2)

위 코드는 컴파일 오류가 발생한다. 매개변수 디폴트 값은 오른쪽부터 지정할 수 있다. 


3] inline 함수 
: in은 "내부"를 의미하고, line은 "프로그램 코드라인"을 의미한다. 그래서 의역해보면, "프로그램 코드라인 안으로 들어가 버린 함수" 라는 뜻이다. 

#define SQUARE(x) ((X)*(X))

int main(void)
{
    std::cout<<SQUARE(5)<<std::endl;
    
    return 0;
}

위 코드에서 출력문 부분이 전처리 과정을 거치면 아래처럼 된다.

    std::cout<<((5)*(5))<<std::endl;

1. 매크로 함수의 장점 
- 장점 : 일반적인 함수에 비해서 실행속도의 이점이 있다. 
- 단점 : 정의하기가 어렵다. 복잡한 함수를 매크로의 형태로 정의하는데 한계가 있다.

매크로 함수의 장점은 유지하되 단점은 제거했으면 좋겠다. 그래서 C++의 inline 함수가 나온것이다. 

2. C++ 기반의 인라인 함수 정의

inline int SQUARE (int x)
{
    return x*x;
}

int main(void)
{
    std::cout<<SQUARE(5)<<std::endl;
}

SQUARE 함수는 inline 함수이니 몸체부분이 함수 호출문을 대체하게 된다. 
* 참고로 매크로를 이용한 함수의 인라인화는 전처리기에 의해서 처리되지만, 키워드 "inline"을 이용한 함수의 인라인화는 컴파일러에 의해서 처리가 된다. 
따라서 컴파일러는 함수의 인라인화가 오히려 성능에 해가 된다고 판단할 경우, 무시해버리기도 한다. 또한 컴파일러는 필요한 경우 일부 함수를 임의로 인라인 처리하기도 한다. 


4] 이름공간(namespace)에 대한 소개 
: 프로젝트가 방대하거나, 다른 모듈을 import하는 경우 똑같은 함수의 이름이 충돌할 수 있다. 이럴때를 대비해서 namespace 라는 것을 사용한다. 

namespace BestComImpl
{
    void SimpleFunc(void)
    {
    }
}

namespace ProgComImpl
{
    viod SimpleFunc(void)
    {
    }
}

int main(void)
{
    BestComImpl::SimpleFunc();
    ProgComImpl::SimpleFunc();

    return 0;
}

* "::" 은 범위지정 연산자(scope resolution operator)라 한다. namespace를 지정할 때 사용하는 연산자다. 

1. 이름공간 기반의 함수 선언과 정의의 구분 
: 우리는 일반적으로 함수의 선언은 헤더파일에 저장하고 함수의 정의는 소스파일에 저장하는 것이 보통이다. 이름공간 기반에서 함수의 선언과 정의를 구분하는 방법을 설명하겠다.

namespace BestComImpl
{
    void SimpleFunc(void);
}

namespace ProgComImpl
{
    viod SimpleFunc(void);
}

int main(void)
{
    BestComImpl::SimpleFunc();
    ProgComImpl::SimpleFunc();

    return 0;
}

void BestComImpl::SimpleFunc(void)
{
    ....
}

viod ProgComImpl::SimpleFunc(void)
{
    ....
}

* 참고로, 동일한 이름공간에 정의된 함수를 호출할 때에는 이름공간을 명시할 필요가 없다. 

2. 이름공간의 중첩

namespace Parent
{
    int num=2;
    
    namespace SubOne
    {
        int num=3;
    }
    
    namespace SubTwo
    {
        int num=4;
    }
}

std::cout<<Parent::num<<std::endl;
std::cout<<Parent::SubOne::num<<std::endl;
std::cout<<Parent::SubTwo::num<<std::endl;

3. using을 이용한 이름공간의 명시
: cout, cin, endl을 사용할 때 마다 "std" 라는 이름공간을 붙여주기가 귀찮다. 이럴때는 "using"을 사용하자.

namespace Hybrid
{
    void HybridFunc(void)
    {
        ...
    }
}

int main(void)
{
    using Hybrid::HybridFunc;

    HybridFunc();

    return 0;
}

위 using Hybrid::HybridFunc은 "HybridFunc은 namespace Hybrid 안에서 찾아라" 라는 것이다. 
위에서는 지역적으로 선언했기 때문에 main 함수안에서 밖에 사용할 수 없다. 전역적으로 사용하려면 전역 공간에 명시해야 한다. 

4. 이름공간에 별칭 지정
: 이름공간이 과도하게 지정된 경우 별칭을 이용해 편리하게 사용할 수 있다. 

namespace AAA
{
    namespace BBB
    {
        namespace CCC
        {
            int num1;
        }
    }
}

namespace ABC=AAA::BBB::CCC;

ABC::num1 = 10;

5. 범위지정 연산자 "::"의 또 다른 기능
: 전역변수와 지역번수의 이름이 같은경우 지역변수는 전역변수를 가려버린다. 이러한 경우에 "::"을 사용해서 전역변수를 편리하게 지정할 수 있다.

int val = 100; //전역변수

int SimplFunc(void)
{
    int val = 20; // 지역변수
   
    val+= 3; // 지역변수 val의 값 3 증가
    ::val+=7; // 전역변수 val의 값 7 증가
}



'C++' 카테고리의 다른 글

[C++] Friend와 Static 그리고 Const  (0) 2017.12.25
[C++] 복사 생성자(Copy Constructor)  (0) 2017.12.23
[C++] 클래스의 완성  (1) 2017.12.23
[C++] 클래스의 기본  (0) 2017.12.23
[C++] C언어 기반의 C++(2)  (1) 2017.12.19
Posted by 홍성곤
,