반응형

cout의 원리

  • cout은 ostream 타입의 객체
  • <<연산자를 각각의 primitive 타입에 대해서 연산자 오버로딩 한 것
    • cout << 5 : cout.operator<<( int )
    • cout << 3.4 : cout.operator<<( double )

ostream 구현

  • 모든 primitive 타입에 대해서 operator<<() 연산자 함수 제공
  • 연속 출력을 위해서 리턴 값을 자기 자신을 참조 리턴
  • 실제 화면 출력 원리
    • 각 OS가 제공하는 시스템 콜 사용
      • Windows : windows API - WriteFile()
      • Linux : Linux system call - write()
#include <cstdio>
using namespace std;

// std::cout 원리 파악을 위한 구현 예제
namespace std
{
    class ostream
    {
    public:
        ostream& operator<<(int n)
        {
            printf("%d", n);
            return *this;
        }
        ostream& operator<<(double d)
        {
            printf("%f", d);
            return *this;
        }
    };
    ostream cout;
}

int main()
{
    cout << 3; // ostream& operator<<(int n) 호출됨
    cout << 3.4; // ostream& operator<<(double d) 호출됨
    cout << 3 << 4; // 자기 자신을 참조로 리턴하므로 연속 출력 가능
}

유니 코드 출력 팁

  • cout의 정확한 타입은 ostream이 아닌 basic_ostream 템플릿
    • typedef basic_ostream<char> ostream;
    • typedef basic_ostream<wchar_t> wostream;
  • 유니코드 출력
    • ostream cout;
    • wostream wcout;

endl 원리

  • endl은 함수
    • cout << endl;
    • endl(cout);
  • hex, dec, alpha... 등은 모두 함수
    • 입출력 조정자 함수(i/o manipulator)
  • 사용자가 직접 만들 수 있음
    • tab 예제
#include <cstdio>
using namespace std;

namespace std
{
    class ostream
    {
    public:
        ostream& operator<<(int n)
        {
            printf("%d", n);
            return *this;
        }
        ostream& operator<<(double d)
        {
            printf("%f", d);
            return *this;
        }
        ostream& operator<<(char c) // os << '\n'; 지원 위한 연산자
        {
            printf("%c", c);
            return *this;
        }
        ostream& operator<<(ostream& (*f)(ostream&)) // endl 함수 포인터를 담기 위한 연산자
        {
            f(*this); // endl 함수에 자신의 객체 참조 전달
            return *this;
        }
    };
    ostream cout;

    ostream& endl(ostream& os) // 개행
    {
        os << '\n';
        return os;
    }
    
    ostream& tab(ostream& os) // 탭 예제
    {
        os << '\t';
        return os;
    }    
    
}

int main()
{
    cout << 3 << endl; // cout.operator<<( 함수 포인터 )
    cout << 3 << tab << endl; // cout.operator<<( 함수 포인터 )
}

cout 사용자 정의 객체 출력

  • operator<<() 연산자를 일반 함수로 제공하면 가능
class Complex
{
    int re, im;
public:
    Complex(int r = 0, int i = 0) : re(r), im(i) {}

    friend std::ostream& operator<<(std::ostream&, const Complex&);
};

// ostream의 연산자는 상수 함수가 아니므로 접근을 위해 const 객체를 사용하지 않음
std::ostream& operator<<(std::ostream& os, const Complex& c)
{
    os << c.re << "," << c.im; // 사용자 정의 객체의 멤버 데이터를 개별 출력
    return os;
}

int main()
{
    Complex c(1, 1);

    std::cout << c; // cout으로 사용자 정의 객체 출력
}

 

 

반응형

'프로그래밍 언어 > C++' 카테고리의 다른 글

C++ 증감 연산자(++, --)  (0) 2019.05.10
C++ 함수 객체(Function Object)  (0) 2019.05.10
C++ 연산자 재정의 - 기본 개념  (0) 2019.05.08
C++ static, const, this  (0) 2019.05.07
C++ 복사 생성자  (0) 2019.05.06

반응형

연산자 재정의(Operator Overloading) 개념

 

  • a + b 코드에서

    • a, b가 모두 primitive type(int, double등)일 경우 : 덧셈 수행

    • a, b중 하나라도 user defined type일 경우 : operator+() 함수를 찾게 됨

  • c1 + c2 코드를 컴파일러가 해석하는 방법

    • 우선 c1.operator+(c2) 멤버 함수를 찾게 됨

    • 없으면 operator+(c1, c2) 일반 함수를 찾게 됨

  • 멤버 함수 구현

    • +는 이항 연산자 이지만 객체 자신이 있으므로 operator+() 함수의 인자는 1개

  • 일반 함수 구현
    • operator+()의 함수 인자는 2개
    • 일반적으로 인자로 받은 객체 private 멤버 접근을 위해 friend 함수로 등록
class Complex
{
    int re, im;

public:
    Complex(int r = 0, int i = 0) : re(r), im(i) {}

    void print() const
    {
        std::cout << re << ", " << im << std::endl;
    }
   
    // + 연산자 재정의
    Complex operator+(const Complex& c)
    {
        Complex temp(re + c.re, im + c.im);
        return temp;
    }
    
    friend Complex operator+(const Complex& c1, const Complex& c2); // friend 접근자 등록
};

Complex operator+(const Complex& c1, const Complex& c2)
{
    // 일반함수에서 객체의 private 멤버에 접근이 필요하므로 friend 접근자 등록
    Complex temp(c1.re + c2.re, c1.im + c2.im);
    return temp;
}

int main()
{
    int n = 3 + 4; // OK

    Complex c1(1, 1);
    Complex c2(2, 2);
    Complex c3 = c1 + c2; // 1. c1.operator+(c2), 2. operator+(c1, c2)
    c3.print();
}

연산자 재정의 정리

  • 인자가 모두 primitive type인 경우는 재정의 불가
  • 재정의 불가 연산자
    • ".*", "::", "sizeof", "typeid", "static_cast", "dynamic_cast", "reinterpret_cast", "const_cast", "."
  • 멤버 함수와 일반 함수로 2가지 모두 제공 가능
  • 2가지 모두 제공시 멤버 함수 우선 호출
  • 1번째 인자가 user defined type이 아닌 경우는 일반 함수로만 만들 수 있음(객체가 아니므로 멤버 함수 제공 불가)
  • 멤버 함수로만 재정의 가능한 연산자
    • "=", "()", "[]", "->"
  • 새로운 연산자를 만들거나, 인자의 개수를 변경하거나, 연산자 우선순위를 변경 할 수 없음
  • 기본 파라미터 사용 불가

중요 연산자들

  • << : cout 의 원리

  • ++ : STL 반복자를 만드는 원리, 구현 과정 중요
  • -> : 스마트 포인터
  • () : 함수 객체
  • [] : 객체를 배열처럼 
  • = : 복사 생성자와 유사한 개념(Shallow copy / Deep copy)
반응형

'프로그래밍 언어 > C++' 카테고리의 다른 글

C++ 함수 객체(Function Object)  (0) 2019.05.10
C++ 연산자 재정의 - cout, endl 원리  (2) 2019.05.08
C++ static, const, this  (0) 2019.05.07
C++ 복사 생성자  (0) 2019.05.06
C++ 접근 지정자, 생성자, 소멸자  (0) 2019.05.06

반응형

static member data

  • static 멤버 변수 모양

    • 클래스 안에 선언(declaration) -> static 키워드 사용

    • 클래스 외부에 정의(definition) -> static 키워드 사용 안함

  • static 멤버 변수

    • 모든 객체가 공유(전역 변수와 유사)

    • 접근 지정자 사용 가능

    • 객체를 생성하지 않아도 메모리에 존재(전역)
    • 객체 생성시 static 멤버는 객체 메모리에 불포함(sizeof 객체로 해보면 static 멤버 제외 사이즈가 리턴)
    • "클래스 이름::변수 이름" 사용으로 클래스별 동일 이름 사용 가능
  • static 멤버 접근 방법
    • 클래스이름::멤버이름 : Car::cnt // 권장
    • 객체이름::멤버이름 : c1.cnt // 일반 멤버인지 static 멤버인지 구분 모호
class Car
{
    int speed;
public:
    static int cnt; // 선언(Declaration)
    Car() { ++cnt; }
    ~Car() { --cnt; }
};
int Car::cnt = 0; // 정의(Definition)

int main()
{
    // 1. 객체 없이도 메모리에 존재
    std::cout << Car::cnt << std::endl;

    // 2. 객체 메모리에 포함되지 않음
    Car c1, c2;
    
    std::cout << Car::cnt << std::endl; // 3. 접근 방법(클래스::멤버이름)
    std::cout << c1.cnt << std::endl; // 3. 접근 방법(객체.멤버이름)
}

static member function

  • static 멤버 함수

    • 객체 없이 호출 가능한 멤버 함수

    • 클래스이름::함수이름()으로 호출 가능

    • 객체이름.함수이름()으로도 호출 가능

    • 일반 멤버 함수 : 호출하려면 반드시 객체가 필요

class Car
{
    int speed;
public:
    static int cnt; // 선언(Declaration)
    Car() { ++cnt; }
    ~Car() { --cnt; }

    static int getCount() { return cnt; }
};
int Car::cnt = 0; // 정의(Definition)

int main()
{
    // 객체 생성과 무관하게 호출 가능(클래스::함수이름)
    std::cout << Car::getCount() << std::endl;

    Car c1, c2;
   
    // 객체를 통해서도 호출 가능(객체.함수이름)
    std::cout << c1.getCount() << std::endl;
}
  • static 멤버 함수 특징

    • 일반 멤버 data에 접근 불가

    • static 멤버 데이터만 접근 가능

    • 선언과 구현으로 분리할때 선언에만 static 표기

class Test
{
    int data1;
    static int data2; // 선언부에는 static 표기
public:
    static void f2()
    {
        data1 = 0; // 일반 멤버 data1에 접근 불가(컴파일 에러)
        data2 = 0; // static 멤버 data2에 접근 가능
    }
    static void f3(); // 선언부에는 static 표기
};

void Test::f3()  // 구현부 분리시 static 표기 안함
{

}

const member function

  • 상수 멤버 함수(const member function) 특징

    • 상수 멤버 함수 안에서는 모든 멤버를 상수 취급

    • 멤버의 값을 변경할 수 없음

    • 상수 멤버 함수 안에서는 상수 멤버 함수만 호출 가능

    • 상수 멤버 함수와 비상수 멤버 함수를 동일한 이름으로  제공 가능
class Point
{
public:
    int x, y;
    Point(int a, int b) : x(a), y(b) {}

    void set(int a, int b)
    {
        x = a;
        y = b;
    }
    
    void getX() const { return x; }
    void getY() const { return y; }

    void print() const // 상수 멤버 함수
    {
        getX() // const 멤버 함수 호출(OK)
        x = 10; // 멤버 데이터 변경(Error)
        set(1, 2); // 일반 멤버 함수 호출(Error)
        std::cout << x << ", " << y << std::endl; // 멤버 데이터 읽기(OK)
    }
};

int main()
{
    Point p(1, 2);
    p.x = 10;
    p.set(10, 10);
    p.print();
}
  • 상수 멤버 함수(const member function) 필요성

    • 상수 객체는 상수 멤버 함수만 호출할 수 있음

    • 객체의 상태를 변경하지 않는 모든 멤버 함수는 상수 함수가 되어야 함

    • getter 대부분 상수 멤버 함수가 되어야 함

class Point
{
public:
    int x, y;
    Point(int a, int b) : x(a), y(b) {}

    void set(int a, int b)
    {
        x = a;
        y = b;
    }

    void print() const; // 구현부를 분리시 선언, 구현부 모두 const 표시
};

void Point::print() const // 구현부를 분리시 선언, 구현부 모두 const 표시
{
    std::cout << x << ", " << y << std::endl;
}

// 자주쓰는 패턴
void foo(const Point& p)
{
    // p의 멤버함수가 상수 멤버함수로 제공되지 않는다면 호출 가능한 멤버가 없음
}

int main()
{
    const Point p(1, 2);
    p.print();  // 상수객체는 상수 멤버 함수만 호출 가능(const가 아닐경우 error)
}
  • mutable
    • 상수 멤버 함수 안에서 값을 변경 하고 싶을때
    • 논리적으로는 상수 멤버 함수지만 다양한 이유로 멤버의 값을 변경하고 싶을때
class Point
{
public:
    int x, y;
    int data = 0;
    mutable int fooCount = 0;

    const int* getPointer() const { return &data; }

    void foo() // 동일 이름의 상수 멤버 함수, 비상수 멤버 함수를 동시에 제공 가능
    {
        std::cout << "foo()" << std::endl;
    }
    void foo() const // 동일 이름의 상수 멤버 함수, 비상수 멤버 함수를 동시에 제공 가능
    {
        fooCount++; // 상수 멤버 함수에서 특별한 이유로 값 변경 필요시 mutable 지시자 선언
        std::cout << "foo() const" << std::endl;
    }
};


int main()
{
    Point p1;
    p1.foo(); // 1. 비상수 함수 호출, 2. 없을 경우 상수 함수 호출
}

this pointer

  • 내부 원리

    • 멤버 함수 호출 시 내부적으로 객체의 주소가 this라는 이름으로 전달됨

class Point
{
    int x = 0;
    int y = 0;
public:
    void set(int a, int b)
    {
        std::cout << this << std::endl; // main의 p1, p2 객체 주소랑 this는 동일 값
    }
};

int main()
{
    Point p1;
    Point p2;

    std::cout << &p1 << std::endl;
    p1.set(10, 20); // 내부적으로는 멤버 함수는 호출 시 객체의 주소가 추가로 전달됨

    std::cout << &p2 << std::endl;
    p2.set(10, 20); // 내부적으로는 멤버 함수는 호출 시 객체의 주소가 추가로 전달됨

}
  • 멤버의 이름과 지역변수(함수 인자)이름이 동일할때

    • 지역 변수가 우선시 됨

    • this를 사용하면 멤버 변수에 접근 가능

class Point
{
    int x = 0;
    int y = 0;
public:
    void set(int x, int y)
    {
        this->x = x; // 동일 변수명 사용시 지역변수 우선이지만 명시적인 구분을 위해 this 활용 가능
        this->y = y;
    }
};

int main()
{
    Point p1;
    p1.set(10, 20);
}
  • this를 리턴하는 함수

    • 멤버 함수 호출을 연속적으로 사용할수 있음

    • 자신(*this)을 리턴시 반드시 참조로 리턴 해야 함(return by reference 참조)

class Point
{
    int x = 0;
    int y = 0;
public:
    void set(int a, int b)
    {
        this->x = a;
        this->y = b;
    }

    Point* foo() { return this; } // this 이용 포인터 리턴
    Point& goo() { return *this; } // this 이용 값에 대한 레퍼런스 리턴
    //Point goo() { return *this; } // this 이용 값으로 바로 리턴시 임시 객체가 계속 복사됨 주의!
};

int main()
{
    Point p1;
    p1.foo()->foo()->foo(); // 포인터 재호출
    t.goo().goo().goo(); // 값 재호출
}
  • this 주의 사항
    • static 멤버 함수 안에서는 this를 사용 불가
    • static 멤버 함수 안에서는 this 사용이 불가하므로 멤버 데이터 접근이 불가능함
class Test
{
    int data;
public:
    static void foo()
    {
        std::cout << this << std::endl; // static 멤버 함수 내부에서는 this 사용 불가 Error
        data = 0; // this->data = 0; this 없어서 static 멤버 함수에서 일반 멤버 접근이 불가 Error
    }
};

int main()
{
    Test::foo(); // static 멤버 함수 호출(내부 this 객체 없음)
}
반응형

반응형

기본 개념

복사 생성자( Copy Constructor )

  • 객체가 자신과 동일한 타입으로 초기화 될때 사용

  • 사용자가 생성하지 않으면 컴파일러가 자동 생성(Default Copy Constructor)

  • 컴파일러가 제공하는 복사 생성자는 모든 멤버를 복사(Bitwise copy)

  • 필요한 경우 사용자가 직접 정의 가능

복사 생성자의 형태

  • Point( const Point& p)

class Point
{
public:
    int x;
    int y;
public:
    Point() : x(0), y(0) {} // 생성자 #1
    Point(int a, int b) : x(a), y(b) {} // 생성자 #2

    // 내부적으로 컴파일러에 의해 생성된 복사 생성자 형태
    // Point(const Point& p): x(p.x), y(p.y) {}
};

int main()
{
    Point p1; // 생성자 #1 호출
    Point p2(1, 2); // 생성자 #2 호출
    //Point p3(1); // 에러
    Point p4(p2); // 컴파일러가 생성한 복사 생성자 호출

    std::cout << p4.x << std::endl;
    std::cout << p4.y << std::endl;
}

복사 생성자가 호출되는 경우

  • 객체가 자신의 타입으로 초기화 될때

    • Point p2(p1);

    • Point p2{p1};

    • Point p2 = p1;

//객체가 자신의 타입으로 초기화 될때

class Point
{
    int x, y;
public:
    Point(int a = 0, int b = 0) : x(a), y(b) 
    {
        std::cout << "Point()" << std::endl;
    }

    Point(const Point& p): x(p.x), y(p.y)
    {
        std::cout << "Point(Point&)" << std::endl;
    }
};

int main()
{
    Point p1(1, 2); // 생성자
    Point p2(p1); // 복사 생성자
    Point p3{ p1 }; // 복사 생성자
    Point p4 = p1; // Point p4(p1) 복사 생성자
}
  • 함수 인자로 객체를 값으로 받을 때( call by value )

    • const &를 사용하면 복사 생성자/소멸자의 호출을 막을 수 있음

    • void foo( const Point& p);

// 함수 인자로 객체를 값으로 받을 때( call by value )

void foo(Point pt) // 이 순간 복사 생성자 호출
{

}

int main()
{
    Point p; // 생성자 호출
    foo(p); 
}
  • 함수가 객체를 값으로 리턴할 때( return by value )

    • RVO / NRVO

    • g++ 사용시 -fno-elide-constructors 옵션 이용하면 확인 가능

// 함수가 객체를 값으로 리턴할 때( return by value )

Point foo()
{
    Point pt(1, 2); // 생성자

    return pt; // 리턴용 임시객체 생성
}

int main()
{
    foo(); // 해당 라인동안만 임시객체는 유효
}

RVO(Return Value Optimization) / NRVO(Named RVO)

  • 임시 객체(Temporary Object)
    • "클래스이름()"으로 만드는 객체
    • 사용자가 직접 만들거나 컴파일러에 의해 임시 객체를 만드는 코드가 생성되기도 함
    • 단일 문장에서만 사용되며, 문장의 끝에서 파괴됨
    • 함수가 참조로 리턴하면 리턴용 임시객체가 생성되지 않음
    • 지역변수는 참조로 리턴할 수 없음
// RVO 적용 코드

Point foo()
{
    return Point(1, 1); // 객체 생성자체를 임시 객체로 생성하여 복사 생성을 방지
}

int main()
{
    foo();
}


// RVO 미적용 코드

// 내부 객체와 리턴용 임시 객체 총 2개가 생성된 코드
Point foo()
{
    Point pt(1, 1); // pt 생성 
    return pt; // pt를 복사한 리턴용 임시 객체생성
}

int main()
{
    foo();
}

Shallow Copy & Deep Copy

  • 얕은 복사( Shallow Copy )

    • 동적 할당된 메모리가 있을때 메모리 자체를 복사하지 않고 메모리 주소만 복사하는 현상

    • 소멸자에서 메모리를 삭제 하는 경우 문제 발생
    • 클래스 안에 포인터 멤버가 있고 동적 메모리 할당된 코드를 사용한다면 반드시 사용자가 복사 생성자를 만들어서 얕은 복사 문제를 해결 해야함

class Person
{
    char* name;
    int age;

public:
    Person(const char* n, int a) : age(a)
    {
        name = new char[strlen(n) + 1];
        strcpy_s(name, sizeof(name), n);
    }
    ~Person() { delete[] name; }
};

int main()
{
    Person p1("KIM", 2); // 단독 실행은 괜찮지만
    Person p2 = p1; // 이렇게 복사할 경우 runtime error(name을 같은 주소값을 가르키고, 소멸자에서 이중 해지 발생)
}
  • 깊은 복사( Deep Copy )

    • 주소가 아닌 메모리 내용 자체를 복사하는 것

class Person
{
    char* name;
    int age;

public:
    Person(const char* n, int a) : age(a)
    {
        name = new char[strlen(n) + 1];
        strcpy_s(name, sizeof(name), n);
    }
    ~Person() { delete[] name; }

    // 복사 생성자 직접 구현(Deep Copy)
    // 1. 포인터가 아닌 멤버는 그냥 복사(초기화 리스트)
    Person(const Person& p): age(p.age)
    {
        // 2. 메모리 할당
        name = new char[strlen(p.name) + 1];

        // 3. 메모리 내용을 복사
        strcpy_s(name, sizeof(name), p.name);
    }
};

int main()
{
    Person p1("KIM", 2);
    Person p2 = p1; // runtime error
}
  • 참조 계수(Reference Counting)

    • 얕은 복사를 기본으로 참조 카운트를 공유하여 최종 소멸시에만 메모리는 해지 하는 기법

// 참조 계수(Reference Counting)

class Person
{
    char* name;
    int age;
    int* ref; // 참조계수 메모리의 주소

public:
    Person(const char* n, int a) : age(a)
    {
        name = new char[strlen(n) + 1];
        strcpy_s(name, sizeof(name), n);

        ref = new int;
        *ref = 1;
    }

    // 소멸자에서 참조카운트 비교 후 최종 소멸시에만 동적 메모리 해지
    ~Person() 
    { 
        if (--(*ref) == 0)
        {
            delete[] name;
            delete ref;
        }
    }

    // 모든 멤버를 얕은 복사 후 참조 계수만 증가
    Person(const Person& p): age(p.age), name(p.name), ref(p.ref)
    {
        ++(*ref); // 참조 계수 증가
    }
};

int main()
{
    Person p1("KIM", 2);
    Person p2 = p1; // 복사 생성자.
}
  • 복사 금지(Delete Copy Constructor)

    • 복사 생성자를 삭제(delete)
      • 싱글턴 객체 생성시
      • C++ 표준의 unique_ptr
      • mutex 등의 동기화 객체
// 복사 금지

class Person
{
    char* name;
    int age;
    int* ref; // 참조계수 메모리의 주소

public:
    Person(const char* n, int a) : age(a)
    {
        name = new char[strlen(n) + 1];
        strcpy_s(name, sizeof(name), n);

        ref = new int;
        *ref = 1;
    }

    // 소멸자 얕은 복사
    ~Person() 
    { 
        if (--(*ref) == 0)
        {
            delete[] name;
            delete ref;
        }
    }

    // 복사 생성자 삭제
    Person(const Person& p) = delete;
};

int main()
{
    Person p1("KIM", 2);
    Person p2 = p1; // 복사 생성자.
}
  • 소유권 이전(Move)
    • 객체가 사용하던 자원을 전달 하는 개념
    • 버퍼 복사 등의 일부 알고리즘에서 사용시 성능향상을 볼수 있음
    • C++11의 move 생성자 개념
    • 복사 후 원본 객체는 사용하지 못한다는 가정(swap에서 유용)
class Person
{
    char* name;
    int age;

public:
    Person(const char* n, int a) : age(a)
    {
        name = new char[strlen(n) + 1];
        strcpy_s(name, sizeof(name), n);
    }

    ~Person() { }

    // 소유권 이전의 복사 생성자
    Person(Person& p): age(p.age), name(p.name)
    {
        // 원래 객체가 가진 name을 0으로 
        p.name = 0;
    }
};

int main()
{
    Person p1("KIM", 2);
    Person p2 = p1; // 복사 생성자.
}

 

  • C++ 표준라이브러리(STL) std::string을 활용
    • 자체적으로 내부 버퍼관리가 되므로 얕은 복사시 문제 없음
class Person
{
    std::string name;
    int age;

public:
    Person(std::string n, int a) : name(n), age(a)
    {
    }
};

int main()
{
    Person p1("KIM", 2);
    Person p2 = p1;
}
반응형

'프로그래밍 언어 > C++' 카테고리의 다른 글

C++ 연산자 재정의 - 기본 개념  (0) 2019.05.08
C++ static, const, this  (0) 2019.05.07
C++ 접근 지정자, 생성자, 소멸자  (0) 2019.05.06
C++ OOP(Object Oriented Programming)  (0) 2019.05.06
C++ Explicit Casting  (0) 2019.05.06

반응형

접근 지정자

  • 멤버 데이터를 외부에서 직접 변경하면 잘못된 값을 가질 수 있으므로 멤버 함수를 통해서만 변경하도록

  • 멤버 함수를 통해서 인자의 유효성 여부도 조사 가능

  • 접근 지정자

    • private : 멤버 함수에서만 접근 할 수 있음

    • public : 멤버 함수가 아닌 함수에서도 접근 가능

  • struct vs class

    • struct : 접근 지정자 생략시 기본 접근자가 public

    • class : 접근 지정자 생략시 기본값이 private

struct Bike // struct -> class로 변경시 기본 접근자가 private 이므로 아래 private 생략 가능
{
private:
    int gear; // private 접근 지정자로 외부에서 직접 접근 불가

public:
    void changeGear(int n)
    {
        if( n > 0 ) // 잘못된 파라미터 체크
            gear = n;
    }
};

int main()
{
    Bike b;
    b.changeGear(-10); // 안전하게 멤버 함수를 통한 접근만 허용
}
  • Friend
    • 멤버 함수는 아니지만 private 멤버에 접근 가능

    • 멤버 함수로 만들수 없는 특정 상황에서 사용

    • getter, setter 함수 제공과의 차이점

      • getter : 모든 함수가 멤버 데이터에 접근할 수 있음

      • friend : 약속된 함수만 멤버 데이터에 접근 할 수 있음

class Bike
{
private:
    int gear;

public:
    void changeGear(int n) { if( n > 0 ) gear = n; }

    // friend 함수 지정으로 멤버 함수가 아니지만 private 변수에 접근 허용
    friend void fixBike(); 

    // friend 클래스 지정으로 AAA클래스 모든 멤버 함수에서 private 변수 접근 허용
    friend class AAA;
};

class AAA
{
};

void fixBike()
{
    Bike b;
    b.changeGear = 0;
}

int main()
{
    Bike b;
    b.changeGear(-10);
}

생성자( Constructor )

  • 모양

    • 클래스 이름과 동일

    • 리턴 타입 표기 X

    • 파라미터 선택적

    • 2개 이상 생성 가능

  • 특징

    • 객체를 생성하면 자동으로 생성자가 호출됨

    • 생성자가 없으면 객체 생성 불가

    • 사용자가 생성자를 정의하지 않으면 컴파일러가 기본 생성자(Default Constructor)를 생성함

    • 생성자를 1개 이상 정의 해놓은 상태라면 기본 생성자가 생성되지 않음
class Point
{
    int x, y;
public:
    Point() { x = 0; y = 0; } // 생성자 #1
    Point(int a, int b) { x = a, y = b; } // 생성자 #2
};

int main()
{
    Point p1; // 생성자 #1 호출
    Point p2(1, 2); // 생성자 #2 호출
    Point p3{ 1, 2 }; // 생성자 #2 호출

    Point p4[3]; // 생성자 #1 3회 호출
    Point p5[3] = { Point(1, 2) }; // 생성자 #2 1회 호출, 생성자 #1 2회 호출

    Point* p6; // 생성자 호출 안됨(Point 포인터 메모리만 할당)
    p6 = static_cast<Point*>(malloc(sizeof(Point))); // 생성자 호출 안됨(Point 사이즈 메모리 할당)
    p6 = new Point(1, 2); // 생성자 #2 호출

    delete p6;
}
  • 호출 순서
    • 객체를 구성하는 멤버의 생성자가 먼저 호출 되고, 객체 자신의 생성자가 호출됨
// 생성자 호출 순서

class Point
{
    int x, y;
public:
    Point() { x = 0; y = 0; }
    Point(int a, int b) { x = a, y = b; }
};

class Rect
{
    Point p1;
    Point p2;
public:
    Rect() { std::cout << "Rect()" << std::endl; }
};

int main()
{
    Rect r; // 생성자 호출 순서 : p1 -> p2 -> Rect
}
  • default

    • 컴파일러에게 default 생성자 생성을 위임하는 방법(기본 생성자 내부에서 할일이 없는 경우 default 사용 권장)
class Point
{
    int x, y;
public:
    Point() = default; // 기본 생성자 컴파일러 생성 위임(C++11, x,y 값이 0으로 초기화)
    Point() {} // 기본 생성자 직접 생성(x, y 값 초기화 안됨)
};

int main()
{
    Point p1{};
}

소멸자( Destructor )

  • 모양

    • ~클래스 이름()
    • 리턴 타입 표기 안함
    • 파라미터 사용 불가
    • 1개만 생성 가능
  • 특징
    • 객체가 파괴될때 소멸자가 호출됨
    • 객체가 자원을 할당한 경우 소멸자에서 자원 해지 필요
    • 사용자가 소멸자를 만들지 않으면 컴파일러가 생성함
class Point
{
    int x, y;
    int* buf;
public:
    Point() 
    {
        std::cout << "Point()" << std::endl;
        buf = new int[10];
    }
    ~Point()
    {
        delete[] buf;
        std::cout << "~Point()" << std::endl;
    }
};

int main()
{
    Point p;
}

초기화 리스트

  • 생성자의 ()뒤에 : 을 표기하고 멤버를 초기화 하는것

  • 특징

    • 대입이 아닌 초기화

  • 주의사항

    • 멤버가 놓인 순서대로 초기화됨(다른 멤버의 초기화 값을 대입시에 주의)

// 초기화 리스트를 이용한 초기화
class Point
{
    int x, y;
    const int c;
public:
    Point() : x(0), y(0), c(10) // 초기화 리스트
    {
        
    }
};

int main()
{
    Point p;
}

// 대입방법으로 초기화
class Point
{
    int x, y;
    const int c = 10;
public:
    Point()
    {
        x = 0;
        y = 0;
    }
};

int main()
{
    Point p;
}
  • 초기화 리스트가 반드시 필요한 경우

    • 객체가 상수 멤버나 참조 멤버를 가진 경우

    • 기본 생성자가 없는 객체를 멤버로 가진 경우

class Point
{
    int x, y = 0;
public:
    Point(int a, int b) : x(a), y(b) {} // 생성자 #2
};

class Rect
{
    Point p1; // 기본 생성자가 없는 Point
    Point p2; // 기본 생성자가 없는 Point

    const int c;
    int& radius;
public:
    Rect(int& r) : p1(0,0), p2(0,0), c(0), radius(r) // 초기화 리스트에서 생성자 #2로 초기화 지정
    {
    }
};

int main()
{
    int radius = 15;
    Rect rect{ radius };
}

위임 생성자(Delegate Constructor)

  • 하나의 생성자에서 다른 생성자를 호출하는 문법
  • 초기화 리스트 구문에서 다른 생성자를 호출
  • C++11 이상
// Point() 생성자 호출 시 Point(int a, int b)를 호출하고 싶을때

class Point
{
    int x, y;
public:
    Point() : Point(0, 0) // 위임 생성자
    {
        Point(0, 0); // 생성자 호출이 아닌 임시 객체 생성 X
    }
    Point(int a, int b) : x(a), y(b) {} // 생성자 #2
};

 

멤버를 초기화 하는 3가지 방법

  • 생성자의 블록 안에서 초기화 -> 대입(상수, 인자 가능)

  • 초기화 리스트 -> 초기화(상수, 인자 가능)

  • 멤버 변수 선언시 초기화 -> 초기화(상수만 가능)

// 1. 대입 초기화
class Point
{
    int x;
    int y;
public:
    Point(int a, int b)
    {
        x = 0; // 파라미터, 상수 모두 사용 가능
        y = 0;
    }
};

// 2. 초기화 리스트 
class Point
{
    int x;
    int y;
public:
    Point(int a, int b) : x(a), y(b) // 상수, 파라미터 모두 사용 가능
    {
    }
};

// 3. 멤버 변수 선언 초기화
class Point
{
    int x = 0; // 생성자에서 받은 파라미터는 활용 불가, 상수만 가능
    int y = 0;
public:
    Point(int a, int b)
    {
    }
};
반응형

'프로그래밍 언어 > C++' 카테고리의 다른 글

C++ static, const, this  (0) 2019.05.07
C++ 복사 생성자  (0) 2019.05.06
C++ OOP(Object Oriented Programming)  (0) 2019.05.06
C++ Explicit Casting  (0) 2019.05.06
C++ reference 변수  (0) 2019.05.06

반응형

객체지향 프로그래밍의 개념

  • 프로그램에서 필요한 데이터 타입을 먼저 설계

  • C언어의 구조체를 사용하면 새로운 데이터 타입을 정의 가능

  • C 구조체 : 데이터만 포함 할 수 있음

  • C++ 구조체 : 데이터, 함수 포함 가능

C Style(구조체 없이 구현)

void Add(int xr, int xi, int yr, int yi, //in parameters
    int* sr, int* si) // out parameters
{
    *sr = xr + yr;
    *si = xi + yi;
}

int main()
{
    int ar = 1, ai = 1;
    int br = 2, bi = 2;
    int sr, si;

    Add(ar, ai, br, bi, &sr, &si);
}

C Style(구조체를 사용한 구현)

Complex Add(const Complex& c1, const Complex& c2)
{
    Complex temp;
    temp.re = c1.re + c2.re;
    temp.im = c1.im + c2.im;
    return temp;
}

int main()
{
    Complex c1 = { 1, 1 };
    Complex c2 = { 2, 2 };

    Complex t = Add(c1, c2);
}

 

Stack으로 배우는 OOP

Step1. 구조체 없이 전역으로 구현한 Stack

  • 쉽게 구현하였으나 Stack을 다중으로 사용하기에는 적절하지 않음

int buf[10];
int idx = 0;

void push(int n) { buf[idx++] = n; }
int pop() { return buf[--idx]; }

int main()
{
    push(10);
    push(20);
    push(30);

    std::cout << pop() << std::endl;
}

Step2. 구조체를 사용한 Stack

  • Stack 구조체를 사용하지 않았을때 보단 간결하지만 Stack 상태 조작 함수가 외부에 있음

struct Stack
{
    int buf[10];
    int idx;
};

void push(Stack* s, int n) 
{ 
    s->buf[(s->idx)++] = n; 
}
int pop(Stack* s) 
{ 
    return s->buf[--(s->idx)]; 
}

int main()
{
    Stack s1, s2;
    s1.idx = 0;
    s2.idx = 0;

    push(&s1, 10);
    push(&s1, 20);
    push(&s1, 30);

    std::cout << pop(&s1) << std::endl;
}

Step3. C++ 구조체를 사용한 Stack

  • 구조체에 데이터와 함수를 함께 구현

  • idx가 잘못된 값으로 초기화 위험성 있음
struct Stack
{
    int buf[10]; // 멤버 데이터
    int idx;

    void push(int n) // 멤버 함수
    {
        buf[idx++] = n;
    }
    int pop() // 멤버 함수
    {
        return buf[--idx];
    }
};

int main()
{
    Stack s1, s2;
    s1.idx = 0;
    s2.idx = 0;

    s1.push(10);
    s1.push(20); 
    s1.push(30); // 컴파일러에 의해서 내부적으로는 push(&s1, 20) 형태로 사용됨

    std::cout << s1.pop() << std::endl;
}

Step4. 정보 은닉(Information Hiding)

 

  • 접근 지정자
    • private : 멤버 함수에서만 접근 할 수 있음
    • public : 외부 함수에서 접근 가능
  • 정보 은닉(information hiding)
    • 멤버 데이터를 외부에서 직접 접근할 수 없게 하고, 멤버 함수를 통해서만 접근 가능하도록 함
    • 외부에 잘못된 사용으로 부터 객체가 불안해 지는것을 방지
    • Stack을 사용하는 사람은 push, pop 함수만 알면 되지 Stack  내부 데이터를 몰라도 됨
  • struct vs class
    • struct : 접근 지정자 생략시 기본 접근자 public
    • class : 접근 지정자 생략시 기본 접근자 private
class Stack
{
// class 접근 기본값은 private
    int buf[10];
    int idx;

public: // 외부 접근을 위한 public 접근자
    void init() { idx = 0; }
    void push(int n)
    {
        buf[idx++] = n;
    }
    int pop()
    {
        return buf[--idx];
    }
};

int main()
{
    Stack s1;
    s1.init();

    s1.push(10);
    s1.push(20);
    s1.push(30);

    std::cout << s1.pop() << std::endl;
}

Step5. 생성자(Constructor)

  • 클래스이름과 동일한 이름을 가지는 함수

  • 객체를 만들면 자동으로 생성자가 호출됨
  • 리턴 타입을 표지하지 않음
  • 인자 사용 여부는 선택적
class Stack
{
    int buf[10];
    int idx;

public:
    // 클래스 이름과 동일한 함수 : 생성자
    Stack() { idx = 0; }
    void push(int n) { buf[idx++] = n; }
    int pop() { return buf[--idx]; }
};

int main()
{
    Stack s1;

    s1.push(10);
    s1.push(20);
    s1.push(30);

    std::cout << s1.pop() << std::endl;
}

Step6. 소멸자(Destructor)

  • 스택의 버퍼크기를 변경 할 수 있도록 제공을 위해선 동적 메모리 할당이 필요한데 객체가 소멸되는 시점에 메모리 해지를 위해서 소멸자 이용
  • ~클래스이름() 형태의 함수

  • 객체가 파괴될 때 호출됨
class Stack
{
    int* buf;
    int idx;

public:
    // 생성자 : 객체가 생성될때 자동으로 호출
    Stack(int size = 10) 
    { 
        buf = new int[size]; // new 배열 타입으로 동적 메모리 할당
        idx = 0;
    }
    // 소멸자 : 객체가 파괴될때 자동으로 호출
    ~Stack() 
    {
        delete[] buf; // new 배열 타입으로 할당한 메모리를 delete[]로 해지
    }
    void push(int n) { buf[idx++] = n; }
    int pop() { return buf[--idx]; }
};

int main()
{
    Stack s1(20);
    s1.push(30);
    std::cout << s1.pop() << std::endl;
}

Step7. 선언과 구현의 분리

  • 선언과 구현의 분리

    • 클래스 선언 안에는 함수의 선언만 포함

    • 함수의 구현은 클래스 외부에서 구현

  • 선언 파일과 구현 파일의 분리

    • 클래스 선언부는 헤더 파일로 생성(.h)

    • 클래스 구현부는 소스 파일로 생성(.cpp)

// stack.h
class Stack
{
private:
    int* buf;
    int idx;

public:
    Stack(int size = 10);
    ~Stack();
    void push(int n);
    int pop();
};
// stack.cpp
#include "Stack.h"

Stack::Stack(int size = 10)
{
    buf = new int[size];
    idx = 0;
}
Stack::~Stack()
{
    delete[] buf;
}
void Stack::push(int n) 
{ 
    buf[idx++] = n; 
}
int Stack::pop() 
{ 
    return buf[--idx]; 
}
// main.cpp
#include <iostream>
#include "Stack.h"

int main()
{
    Stack s1(20);
    s1.push(30);
    std::cout << s1.pop() << std::endl;
}

Step8. 코딩 관례

  • 대부분의 오픈소스들은 프로젝트들은 사용자 친화적으로 public 함수를 상단, private 함수, 변수는 하단에 배치하는 경향을 보임

// Stack.h
class Stack
{
public: // 주 접근 함수를 상단
    Stack(int size = 10);
    ~Stack();
    void push(int n);
    int pop();
    
private: // 내부 접근 함수 및 변수는 하단
    int* buf;
    int idx;    
};

 

Step9. class template

  • Stack은 int 뿐 아니라 다른 타입버전도 필요하므로 템플릿을 이용하여 구현

  • 내부적으로는 컴파일 타임에 사용자 코드를 기반으로 각각의 데이터 타입별 Stack 클래스를 별도로 생성함
  • 클래스 템플릿에서 멤버 함수 구현시 주의사항
    • 클래스 외부, 내부에 구현부를 분리하여 넣어도 되지만, 별도의 소스파일로 분리하면 안됨
    • 함수 선언과 구현 모두 헤더파일에 존재 하여야함
template<typename T>
class Stack
{
    T* buf;
    int idx;

public:
    Stack(int size = 10) { 
        buf = new T[size];
    }
    ~Stack() {
        delete[] buf;
    }
    void push(T n) { buf[idx++] = n; }
    T pop() { return buf[--idx]; }
};

// 구현부 분리 시
template<typename T>
Stack<T>::Stack(){}

template<typename T> 
T Stack<T>::pop(){}


int main()
{
    Stack<int> s1(20);
    s1.push(30);

    Stack<double> s2(20);

    std::cout << s1.pop() << std::endl;
}

Step10. STL stack

  • STL

    • C++ 표준 라이브러리
    • 다양한 자료구조와 알고리즘 함수 제공
  • stack
    • 내부 버퍼 크기는 자동으로 관리됨
    • 템플릿으로 구현 됨
    • 제거용 함수와 리턴용 함수가 분리 되어 있음
      • pop : 제거만 하고 리턴 안됨
      • top : 리턴만 하고 제거되지 않음
int main()
{
    std::stack<int> s;
    s.push(10);
    s.push(20);
    s.push(30);

    int n1 = s.top(); // return 30(리턴만 하고 제거 X)
    int n2 = s.top(); // return 30(리턴만 하고 제거 X)
    s.pop(); // 제거만 하고 리턴 X
    int n3 = s.top(); // return 20
    s.pop(); // 제거만 하고 리턴 X
    int n4 = s.top(); // return 10
}

 

반응형

'프로그래밍 언어 > C++' 카테고리의 다른 글

C++ 복사 생성자  (0) 2019.05.06
C++ 접근 지정자, 생성자, 소멸자  (0) 2019.05.06
C++ Explicit Casting  (0) 2019.05.06
C++ reference 변수  (0) 2019.05.06
C++ new  (0) 2019.05.06

반응형

C 캐스팅의 문제점

  • 다른 데이터 타입의 포인터형으로 캐스팅이 가능
  • 캐스팅시 상수 주소를 비상수 포인터에 담을 수 있음
  • 이외 버그 가능성이 있는 캐스팅도 허용함
int main()
{
    int n = 3;
    double* p1 = (double*)&n; // 다른 데이터 타입 포인터로 캐스팅

    const int c = 10;
    int* p2 = (int*)& c; // 상수 주소를 비상수 포인터로 캐스팅
}

C++ 4가지 캐스팅

  • static_cast : 컴파일 타임 캐스팅
    • 가장 기본적인 캐스팅, 위험성을 내포한 경우는 캐스팅 안됨.
    • void* -> 다른타입*
  • reinterpret_cast : 컴파일 타임 캐스팅
    • 메모리의 재해석
    • 서로 다른 타입의 주소 캐스팅
    • 포인터와 정수간 캐스팅
  • const_cast : 컴파일 타임 캐스팅
    • 객체의 상수성을 제거하는 캐스팅
  • dynamic_cast : 실행시간 캐스팅
    • down cast 막기 위함
    • RTTI 기능
int main()
{
    int* p1 = static_cast<int*>(malloc(100)); // ok

    // 서로 다른 타입의 포인터로 캐스팅 필요시
    int n = 3;
    double* p2 = static_cast<double*>(&n); // error
    double* p2 = reinterpret_cast<double*>(&n); // ok

    // 상수를 비상수로 캐스팅 필요시
    const int c = 10;
    int* p3 = static_cast<int*>(&c); // error
    int* p3 = const_cast<int*>(&c); // ok
    
    // 응용
    double* p = (double*)&c; // 과거 C 스타일
    double* p = reinterpret_cast<double*>(const_cast<int*>(&c)); // 상수성 제거, 타입 호환
}
반응형

'프로그래밍 언어 > C++' 카테고리의 다른 글

C++ 접근 지정자, 생성자, 소멸자  (0) 2019.05.06
C++ OOP(Object Oriented Programming)  (0) 2019.05.06
C++ reference 변수  (0) 2019.05.06
C++ new  (0) 2019.05.06
C++ 함수 특징 #2  (0) 2019.05.05

반응형

reference 변수

  • 기존 메모리(변수)에 새로운 이름(alias)을 부여하는 것
  • 레퍼런스 변수는 메모리를 할당하지 않는가?
    • 단순한 코드의 경우 메모리를 사용하지 않음
    • 복잡한 경우(함수 인자로 사용되거나...) 상황에 따라 내부적으로 포인터를 사용
    • C++ 표준 문서는 레퍼런스의 동작만 정의 할뿐, 구현은 정의하고 있지 않음
  • 반드시 초기화 되어야 함
int main()
{
    int n = 10; // 일반 변수
    int* p = &n; // 포인터 변수

    int& r = n; // 레퍼런스 변수
}

 

Call By Reference

  • 함수 인자의 값을 변경하게 하려면 포인터 또는 레퍼런스를 사용
void Inc1(int n) { ++n; }
void Inc2(int* p) { ++(*p); }
void Inc3(int& r) { ++r; }

int main()
{
    int a = 10, b = 10, c = 10;

    Inc1(a); // call by value
    Inc2(&b); // call by pointer
    Inc3(c); // call by reference

    std::cout << a << std::endl; // 10
    std::cout << a << std::endl; // 11
    std::cout << a << std::endl; // 11
}
int main()
{
    int n = 0;
    scanf("%d", &n); // 인자를 포인터로 전달(call by pointer)
    std::cin >> n; // 인자를 레퍼런스로 전달(call by reference)
}

 

Call by const Reference

  • 함수의 인자로 사용한 변수의 값을 변경되지 않게 하려면
    • Call by value : 동일한 객체가 메모리에 중복 생성됨
    • Const Reference : 메모리 중복 생성이 되지 않고 생성자, 소멸자의 호출을 차단 할 수 있음
  • 권장 사항
    • primitive type : Call by value // C++에서 기본 제공 타입
    • user defined type : Const reference // 사용자 정의 타입
struct Data // user defined type
{
    char data[1000];
};

void Foo1(int a) {} // primitive type(call by value)

void Foo2(const Data& a) {} // user defined type(call by const reference)

int main()
{
    Data x;

    Foo1(1);
    Foo2(x);
}

 

Reference return

  •  함수 인자
    • call by value : 복사본 생성
    • call by reference : 복사본 미생성
  • 함수 리턴
    • return by value : 복사본(임시 객체)을 리턴(등호 왼쪽에 올 수 없음, lvalue가 될 수 없음)
    • return by reference : 원본 리턴(지역 변수는 참조로 리턴하면 안됨)
struct Point
{
    int x, y;
};

void F1(Point p) {} // 복사본 생성
void F2(Point& r) {} // 복사본 생성X

Point pt;
Point& Goo() { return pt; }

int main()
{
    Goo().x = 20;

    std::cout << pt.x << std::endl; // 20
}

 

rvalue reference

  • lvalue vs rvalue
    • rvalue : 등호(=)의 오른쪽에만 올 수 있는 것
    • lvalue : 등호(=)의 오른쪽과 왼쪽에 모두 올 수 있는 것
  • reference 규칙
    • lvalue reference : lvalue만 가리킬 수 있음
    • const lvalue reference : lvalue, rvalue  모두 가리킬 수 있음
    • rvalue reference : rvalue만 가리킬 수 있음
  • rvalue reference 활용 분야
    • move semantics
    • perfect forwarding
int main()
{
    int v1 = 0, v2 = 0;

    v1 = 10; // ok
    10 = v1; // error
    v2 = v1;

    // lvalue reference
    int& r1 = v1; // ok
    int& r2 = 10; // error

    // const lvalue reference
    const int& r3 = v1; // ok
    const int& r4 = 10; // ok

    // rvalue reference
    int&& r5 = v1; // error
    int&& r6 = 10; // ok

}
반응형

'프로그래밍 언어 > C++' 카테고리의 다른 글

C++ OOP(Object Oriented Programming)  (0) 2019.05.06
C++ Explicit Casting  (0) 2019.05.06
C++ new  (0) 2019.05.06
C++ 함수 특징 #2  (0) 2019.05.05
C++ 함수 특징 #1  (0) 2019.05.05

+ Recent posts