※ 스트림(stream)

순서가 있는 데이터의 연속적인 흐름으로 다음과 같은 입출력 관련 클래스가 있다.

  • ofstream  => 출력파일 stream클래스, 출력파일생성 및 파일에 데이터 쓸 때 사용
  • ifstream   => 입력파일 stream클래스, 파일에서 데이터를 읽을 때 사용한다.
  • fstream    => 일반적인 파일 스트림

 

 

※ 파일 읽기

#include <iostream>
#include <fstream>
#include <string>
using namespace std;


int main(){
    ifstream is{"example.txt"};
    string str;

    while(is){
        is >> str;
        cout << str << " ";
    }
    cout << endl;
    // 객체 is가 범위를 벗어나면 ifstream 소멸자가 파일을 닫는다.
}

 

※ 파일 쓰기

 

※ 파일 모드

쉽게 풀어쓴 C 프로그래밍 참조

 

 

 

 

 

※ 파일에서 읽은 data를 객체로 vector에 저장했다 출력하기

class Temperature {
public:
    int hour;
    double temperature;
};

int main(){
    ifstream is{"temp.txt"};
    if (!is){
        cerr << "file open failed" << endl;
        exit(1);
    }

    vector<Temperature> v;
    int hour;   double temperature;

    while (is >> hour >> temperature){
        v.push_back(Temperature{hour, temperature});
    }
    for (Temperature t:v) {
        cout << t.hour << "시의 온도는 " << t.temperature << endl;
    }
}

 

 

 

 

 

※ 저장된 txt파일을 읽어 앞에 숫자를 붙인 후 출력파일에 기록해보자.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

cf-1. 절대경로로 File I/O, File Open 하는법

1. Edit Configurations를 클릭한다.
Working directory를 원하는 절대경로로 지정해준다. (원상복구 시키고 싶으면 비워주면 됨)

 

 

 

cf-2) 상대경로로 txt파일을 내부에 만들고 사용하기

※ C++11/14에서 새롭게 추가된  자동 타입 변환 키워드이다.

 

※ auto 사용 시 주의사항

  • auto 키워드는 함수 매개변수로 사용불가!
  • auto 키워드는 구조체나 클래스의 멤버 변수로 사용 불가( 객체 자료형 크기를 모르기 때문)
  • 가독성이 떨어짐으로 적절한 사용이 필요.

 

 

※ auto 키워드

선언된 변수의 초기화 식을 사용하는 것으로 컴파일러가 알아서 해당 타입을 추론 (type inference)하게 한다.

이 기능은 생성 시 변수를 초기화 할 때만! 작동한다. ( 초기화된 값을 기준으로 자료형을 선택해서)

※ auto를 이용한 자료형

int r = 20;
double PI = 3.14159265358979;

auto *pi = &PI;  // 포인터도 가능
auto &Pi = PI;  // 참조자도 가능

auto S = r*r*PI;  // double S
cout << S;  // 출력값: 1256.64

 

※ auto를 이용한 for문 출력

auto arr = { 1, 2, 3, 4 };
for (auto num : arr)
    cout << num << ' ';  // 1 2 3 4 출력
for (const auto&  p : photos) {
    wcout << "Photo location: " << (static_pointer_cast<Photo>(p))->location_ << endl;
}

 

※ auto를 이용한 함수

auto S(int r, double pi){
    return r*r*pi;
}

 

 

 

 

※ decltype 키워드

declared type 즉, 선언된 형식의 줄임말로 주어진 이름, 표현식의 구체적 타입을 알려주는 키워드이다.

이는 template에 기반한 제네릭 프로그래밍의 어려움을 해소하기 위해 도입되었다.

ex.

template<typename FirstType, typename SecondType>
??? AddArbitrary(FirstType first, SecondType second) {
    cout << first + second << endl;
}

int main() {
    AddArbitrary(1234, 'C');    // FirstType이 returnType
    AddArbitrary('C', 1234);    // SecondType이 returnType
}
template<typename FirstType, typename SecondType>
decltype(first+second) AddArbitrary(FirstType first, SecondType second) {  //Err!
    cout << first + second << endl;
}

 

§ auto vs decltype

- auto: 값에 상응하는 타입을 "컴파일러가" 추론하는 키워드

- decltype: 값으로부터 타입을 추출하는 키워드

template<typename FirstType, typename SecondType>
auto AddArbitrary(FirstType first, SecondType second) -> decltype(first+second){
    cout << first + second << endl;
}

auto +  ->decltype()을 이용해서 문제를 해결할 수 있다!

 

 

 

 

 

 

 

 

 

※ 범위기반 for문

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

vector<int> v;

int main() {
    int num;
    for (int i = 0; i < 5; i++){
        //cin >> v[i];  // 이건 왜 오류나는가
        cin >> num;
        v.push_back(num);
    }
    sort(v.begin(), v.end(), greater<int>());

    for (const auto& num : v)
        cout << num << " ";
}

C++11에서 새롭게 도입된 유형의 루프로 더 간단하고 안전하게 배열등의 모든 요소를 반복하는 방법이다.

§ 범위기반 for문의 가장 큰 특징

표현식 안의 모든 값에 대해 한번씩 루프를 실행!

범위기반 for문은 index 정보가 없고 배열요소를 변경할 수 없다.

즉, for문을 완전히 대체하지는 못한다.

따라서 컨테이너의 간단한 순회를 위해 사용된다!

보통 auto와 같이 사용되는 경우를 심심치 않게 볼 수 있기에 사용법을 알고 있는게 좋을 것이다.

 

※ 선언방법

for (자료형 변수 : 배열종류의 이름)

예를 들어 다음과 같이 선언할 수 있다.

순서는 다음과 같다.

int main() {
    int arr[] = { 1, 3 , 4, 5, 5, 8};
    for (int num :arr)
        cout << num << " ";
}

1. for문 실행, num에 arr의 첫 요소인 0이 할당

2. 그 후 0을 출력하는 cout 실행

3. 다시 for문 실행, 이 과정을 요소 8까지 반복

 

 

 

 

 

★ 배열에 대한 인덱스가 아닌, 배열의 요소값이 num에 할당된다!

여기서 자료형 변수 위치에 배열요소와 같은 자료형을 가져야 하기에 auto키워드로 자료형을 추론하게 하는 것이 좋다.

또한, 읽기 전용으로 사용하려할 때, (수정되지 않게끔) const 키워드를 붙여 다음과 같이 선언할 수 있다.

(성능상의 이유로 범위기반 for문에서 const 참조를 사용하는 것이 좋다.)

int main() {
    int arr[] = { 1, 3 , 4, 5, 5, 8};
    for (const auto& element :arr)
        cout << element << " ";
}

 

포인터로 변환된 배열 (array decay into pointer)의 경우, 배열의 길이를 몰라 범위기반 for문을 사용할 수 없다.

int sumArray(int array[]) {
    int sum = 0;
    for (const auto& number : array) // 컴파일 에러, 배열크기를 모름
        sum += number;

    return sum;
}

int main() {
    int array[5] = { 9, 7, 5, 3, 1 };
    cout << sumArray(array); // 배열이 포인터로 decay
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

※ 예외처리(Exception Handling): 예외를 처리하는 과정

예외(Exception): 프로그램 실행도중 일어나는 비정상적인 상황

예를 들자면 아래와 같은 함수에서 분모에 0을 넣은 것과 동일한 상황이다.

좌) 정상적인 종료                                   우) 비정상적인 종료

우측 컴파일부분을 보면 Process finished with exit code -1073741676 (0xC0000094)라 적혀있다.

즉, 정상적이지 않은 값이 나와 오류가 발생했다는 뜻이다.

 

이런 경우, 프로그램이 오류가 발생했을 때, 컴파일러가 강제로 종료되거나 사전에 방지하기 위해 다음과 같은 방식을 사용한다.

 

- if문과 같은 조건문을 통한 예외처리

int fun(int a, int b){
    if (b == 0)
        exit(0);
    else
        return a / b;
}

int main() {
    int x, y;
    cin >> x >> y;
    cout << fun(x, y) << endl;
}

위와 같이 if문을 통한 예외를 제어할 수도 있다. 이는 C에서도 가능한 방식이다.

하지만 if-else문을 통한 에러처리방식에러가 발생한 객체에 대해 수명이 유지되어 에러를 처리하는동안에도 발생한 객체를 참조하는 코드가 정상적으로 컴파일된다.

따라서 C++을 포함한 여러 언어들에서는 다음과 같은 방식을 많이 사용한다.

 

 

- try catch를 통한 예외처리

try-catch는 if문의 예외처리와 달리 지역 객체들의 소멸자가 자동호출되어 메모리 누수현상을 조금 해결할 수 있다.

다만 try-catch 블록을 유지해야할 정보도 많고 실제 예외가 발생했을 때, 해줘야 할 일이 많아서 코드 크기나

예외 발생시 처리 속도는 전통적인 if 조건문 반환 값을 통한 오류처리와는 비교하기 힘들다.

try/catch는 자동으로 해주는 일이 많기에 당연히 더 느리다.

 

※ try-catch문을 통한 예외처리 매커니즘

 

class Animal {
public:
    Animal() { cout << "생성자" << endl; };
    ~Animal() { cout << "소멸자" << endl; };
};

int main(){
    try {
        Animal a;
        throw (a);
    }
    catch(Animal& a){
        cout << "catch문" << endl;
    }
}

/*출력문*/
//생성자
//소멸자
//catch문
//소멸자
다음과 같이 객체를 throw할 수 있는데, nested block 즉, try{ }가 종료가 되었으므로
Animal a를 통해 만들어진 객체 a는 소멸자에 의해 소멸하게 된다.
Q. 그렇다면 왜 출력문에서 catch문 뒤에 소멸자가 한 번 더 출력된 것일까?
A. 그건 바로 catch(Animal& a)에서 생성자가 한 번 더 생성되었다는 것의 반증이 될 것이다.
생성자는 처음 객체생성되었을 때, 초기화를 위해 딱 한번만 호출된다는 것이 기본 개념이고, 핵심이기 때문이다.
따라서 catch 블록이 종료된 이후 Animal& a를 통해 생성된 객체를 다시 소멸자가 소멸해줘서 소멸자가 두번 출력된 것!

 

 

 

 

 

Exception 클래스 (예외 클래스)

Exception 클래스:  예외처리를 위해 exception 헤더에서 제공하는 클래스 (표준 라이브러리인 std namespace에 존재)

이런 예외클래스도 클래스이기에 상속은 물론 다형성, 생성자와 연산자에도 적용 가능하다.

 

§ what() 

what은 exception 클래스에서 하나의 문자열 포인터를 반환하는 가상멤버함수이다.

what()은 exception 클래스에서는 아무 의미가 없지만 파생클래스에서 원하는 문자열을 출력할 수 있게 재정의 해준다!

 

#include <iostream>
#include <exception>
#include <string>
using namespace std;

class NewException : public exception{ //새로운 Exception NewException은 exception클래스를 상속받음
public:
    const char* what() const noexcept override{ // what 함수의 오버라이딩 진행
        return "NewException";
    }
};

int main(){
    try{
        string str;
        str.resize(-100);
    }
    catch (exception& e){
        try {
            throw NewException(); // 예외 발생시 새로운 Exception throw
        }
        catch (const NewException& newException){
            cout << "My exception is " << newException.what() << endl; // NewException의 what()에서 전달받은 문자열 출력
        }
    }
}

 

 

 

 

 

§ 표준 Exception 클래스

#include <exception>는 exception 클래스로부터 파생된 다양한 표준 exception 클래스를 정의하고 있다.

[가장 기초클래스가 되는 2개의 클래스, logic과 runtime]

logic_error 클래스는 일반적인 논리에 관한 오류들을 처리

runtime_error 클래스프로그램 실행하는 동안 발생할 수 있는 다양한 오류들을 처리

출처) http://www.tcpschool.com/cpp/cpp_exception_class

 

 

 

 

 

 

※ Custom Exception (예외클래스의 설계)

예외발생을 알리기 위한 예외 객체의 생성을 위해 정의된 클래스로 기본 자료형 데이터 뿐만 아니라

클래스의 객체도.예외데이터가 될 수 있다. 또한 예외클래스도 상속의 관계를 구성할 수 있다

 

 

 

 

 

※ 예외클래스 전달방식의 주의사항

try 뒤를 이어 등장하는 catch가 2개 이상일 때, 다음과 같은 과정을 거친다.

 

윤성우의 열혈C++

 

int throwExceptions (int c) {
    if (c == 1){
        throw string("It's string!");
    }
    if (c == 2){
        throw 2;
    }
    if (c == 3){
        throw "hello world!";
    }
    if (c == 4){
        throw 'X';
    }
    return 0;
}

int main(){
    int c;
    while (true) {
        cin >> c;
        try {
            throwExceptions(c);
        }
        catch (string& s){
            cout << "string exception: " << s << endl;
        }
        catch (const char* s) {
            cout << "string literal exception: " << s << endl;
        }
        catch (char x) {
            cout << "char exception: " << x << endl;
        }
        catch (int x) {
            cout << "int exception: " << x << endl;
        }
    }
}

/*---------------출력문---------------*/
//1
//string exception: It's string!
//2
//int exception: 2
//3
//string literal exception: hello world!
//4
//char exception: X

 

 

 

 

 

 

※ noexcept (true)   (feat. move constructor)

예외가 생성되지 않음을 뜻하는 키워드

호출자가 noexcept 여부에 의존할 수 있으며 예외를 방출하지 않을 함수는 noexcept로 선언한다.

 

[장점]

1. user와 compiler에게 힌트가 됨.

2. code의 단순화

3. compiler의 최적화를 가능하게 함.

 

int funcA() noexcept {
    throw std::runtime_error("Exception!");
    return 0;
}

int main(){
    try { 
        funcA();
    } 
    catch (char c){
        cout << "catch char: " << c;
    }
}

 

 

※ 이동생성자와 noexcept

[이동생성자 사용 시 주의할 점]

1. 이동생성자에는 noexcept 키워드가 지정되어야 한다. (이동생성자 수행중, 예외가 없다는 것을 컴파일러가 인지해야함)

2. shallow copy가 일어나는 변수(포인터, 주소관련 변수)에 nullptr를 넣어줘야 함

3, 소멸자에서 메모리할당 관련 변수가 nullptr인지 확인해 줘야함.

Animal(Animal && a) noexcept { //이동생성자에 의한 얕은복사
    age = a.age;
    name = a.name;
    cout << "이동생성자" << endl;
    a.name = nullptr;
}

 

 

 

 

 

 

 

 

 

 

 

※ rethrowing

- throw를 다시 하여 예외를 부분적으로 다루기(handling) 위해서 사용.

int funcA() {
    throw std::runtime_error("Exception!");
    return 0;
}
int funcB() {
    try {
        funcA();
    }
    catch (std::runtime_error& e) {
        cout << "catch Exception: " << e.what() << endl;
        throw;
    }
}

int main(){
    try {
        funcB();
    }
    catch (std::runtime_error& e) {
        cout << "catch Rethrowed exception: " << e.what() << endl;
    }
}

/*---------------출력문---------------*/
//catch Exception: Exception!
//catch Rethrowed exception: Exception!

※ 템플릿 함수와 함수 템플릿

- C++에서 template은 다른 객체지향언어에서 부르는 "일반화를 뜻하는" 제네릭(generic)이다.

- template 사용은 약간의 비용(실행비용)이 발생하지만 전체적으로 프로그램의 크기와 난이도를 줄일 수 있다

 예를들어 int, double, char 타입에 대한 최솟값을 구하는 min 함수를 만들어 보자면 아래와 같이 3개의 함수가 필요하다.

int min (int a, int b) { return a < b ? a : b; }
double min (double a, double b) { return a < b ? a : b; }
char min (char a, char b) { return a < b ? a : b; }

 

하지만 template을 사용한다면 모든 data type에 대한 정의가 하나의 함수로 가능하다. 아래처럼 말이다.

template<typename T>

T min (T a, T b) { return a < b ? a : b; }

cf. 위의 T는 템플릿 매개변수라 부른다.

 

※ 템플릿 함수와 함수 템플릿

※  템플릿 함수

- 템플릿을 기반으로 컴파일러가 만들어 내는 함수(템플릿 기반의 함수임을 표기한 것)

- 즉, 템플릿 기반의 호출이 가능한 “함수”라는 점에서 차이가 있다.

 

 

※ 함수 템플릿

- 함수 템플릿은 함수를 만들어 낸다.(기능은 결정되어 있으나 자료형은 결정 X)

- 따라서 함수 템플릿으로 다양한 자료형의 함수를 만들 수 있다.

- 함수를 만드는데 사용되는 템플릿으로 호출이 가능한 함수가 아닌 “템플릿”이다

 

 

 

※ 템플릿의 종류

- 함수 템플릿함수의 오버로딩을 확장한 개념

template<typename T>

T min (T a, T b) { return a < b ? a : b; }

 

 

- 클래스 템플릿:클래스를 만드는 템플릿으로 내부의 멤버변수.함수의 data type 지정시 사용

- 템플릿 클래스: 클래스 템플릿을 기반으로 컴파일러가 만든 클래스이다.
다만, 클래스 템플릿 기반 객체생성에는 반드시 <int>와 같은 자료형을 명시해 줘야 한다!

template <typename T>
class Animal{
private:
    T num;
public:
    Animal(T num) : num(num) {}
    T eat(const T& ref);
};

template <typename T>
T Animal<T>::eat(const T& ref) {}

int main() {
    Animal<int> a(4);
    a.eat(4);
}

 

 

- 타입 템플릿using으로 data type을 템플릿으로 지정시 사용

template <typename T>
using ptr = T*;
ptr<int> x;  // int *x와 동일한 선언

 

- 변수 템플릿: 변수에 적용할 수 있는 템플릿 (C++14 이후부터 적용)

template <typename T>
constexpr T PI = T(3.141592653589793238462643L);

 

 

 

 

※ 템플릿의 특수화

- 특정 자료형으로 생성된 객체에 대해 구분이 되는 다른 행동양식 적용하기 위함

- 즉, 템플릿을 구성하는 멤버함수 일부 혹은 전체를 모두 다르게 행동시킬 수 있음

 

Q. 문자열 비교할 때, 사전순이 맞을까? 아니면 길이순이 맞을까?

A: 이런 특수 상황에 따라 예외가 필요하기에 사용하는 것이 바로 템플릿의 특수화이다.

 

 

 

※ 함수 템플릿과 static 지역변수

함수템플릿을 기반, 컴파일러는 ‘템플릿 함수’들을 만들어 낸다.

따라서 static 지역변수도 템플릿 함수 별로 각각 존재하게 된다.

 

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

static멤버변수는 변수가 선언된 클래스의 객체간 공유가 가능.

따라서 클래스별 static 멤버변수를 유지하게 된다.

 

좌) 함수 템플릿과 static 지역변수&nbsp; &nbsp; &nbsp;우) 클래스 템플릿과 static 멤버변수

 

 

 

※ 템플릿기반 템플릿 클래스 객체를 저장하는 객체 생성

 

 

 

 

 

 

 

 

 

※ Advanced Template 

python에서 다음과 같이 print함수를 사용할 때, 인자로 전달되는 것들을 모두 출력할 수 있다.

print(1, 3.1, "abc")

그렇다면 C++에서도 이와 같은 방법으로 출력에서 인자에 대한 사전정보가 없을 때, 출력을 조금 더 유동적으로 하는 방법은 없을까?

 

 variadic template (가변길이 템플릿)

#include <iostream>
using namespace std;

template <typename T>
void print(T args) {
    cout << args << endl;
}

template <typename T, typename... Types>
void print(T arg, Types... args) {
    cout << arg << ", ";
    print(args...);
}

int main() {
    print(1, 3.1, "abc");
    print(1, 2, 3, 4, 5, 6, 7);
}
/*************** 출력 ***************/
// 1, 3.1, abc
// 1, 2, 3, 4, 5, 6, 7

 

- parameter pack

template <typename T, typename... Types>
void print(T arg, Types... args) {

typename 뒤에 ...으로 오는 것을 템플릿 파라미터 팩이라 부른다.

템플릿 파라미터 팩은 0개 이상의 템플릿 인자들을 나타낸다.

 

 

§ Fold Expression 

위에서 사용한 recursion진행 대신 사용하는 방법으로 variadic template에서 가변인자 처리를 위해 parameter pack을 사용하는데, 이때! C++17부터 fold expression이 제공되어 parameter pack을 더 쉽게 사용할 수 있게 된 것이다.

- 인자가 1개:

1) Unary right 
(E op ...)   ex_ (E1 op (... op (EN-1 op EN)))

2) Unary left
(... op E)   ex_ (((E1 op E2) op ...) op EN)

unary right fold

E1 op ( ... op ( En-1 op En))

- 전달받은 parameter pack을 위와 같은 표현식으로 변경
뒤에 있는 인자 부터 먼저 순차적으로 연산해서 결과 값을 생성

template<typename... Args>
auto sum(Args... args) { 
    return (args + ...); 
}

 

unary left fold

((E1 op E2) op ...) op En

- 전달받은 parameter pack을 위와 같은 표현식으로 변경
앞에 있는 인자 부터 순차적으로 연산해서 결과 값을 생성

template<typename... Args>
auto sum(Args... args) { 
    return (... + args); 
}

 

 

 

 

- 인자가 2개

3) Binary right
(E op ... op I)   ex_ (E1 op (... op (EN−1 op (EN op I))))

4) Binary left
(I op ... op E)   ex_ ((((I op E1) op E2) op ...) op EN)

 

binary right

E op ... op I
- 전달받은 parameter pack을 위와 같은 표현식으로 변경
- 단항을 사용하는 문법과 다르게 parameter pack과 별개로 초기 연산할 값을 추가할 수 있다.

template<typename ...Args>
int sum(Args... args) {
    return (args + ... + (1 * 2));
}

 

binary left

I op ... op E
- 전달받은 parameter pack을 위와 같은 표현식으로 변경

template<typename ...Args>
int sum(Args... args) {
    return ((1 * 2) + ... + args);
}

 

 

 

§ Functor (Function Object, 함수객체) 

다음과 같이 함수처럼 동작하는 클래스를 Functor, 함수객체라 부른다.

struct AddFunction {
    int operator()(const int& num1, const int& num2) {
        return num1 + num2;
    }
    double operator()(const double& num1, const double& num2) {
        return num1 + num2;
    }
};

int main() {
    auto num1 = 1.32, num2 = 12.34;
    AddFunction add;
    auto result = add(num1, num2);
    cout << result << endl;
    return 0;
}

이런 functor는 기존의 함수보다 더 flexible 즉, 유연한데, 객체를 이용하기에 임의의 상태에 대한 정보를 전달가능하다.

functor를 사용하면 기능조작이 쉽고 컴파일러가 기능 자체를 inline화 시켜 매우 빠르게 작업할 수 있다.

1) Unary right 
(E op ...)   ex_ (E1 op (... op (EN-1 op EN)))

2) Unary left
(... op E)   ex_ (((E1 op E2) op ...) op EN)

 

 

 

 

 

 

 

 

 

 

 

※ 스마트 포인터 

// int *ptr=new int;    delete ptr;
std::unique_ptr<int> ptr = make_unique<int>();

//int *ptr=new int[5];
auto ptr=make_unique<int[]>(5);

//void (*ptr)()
std::function<void(void)>

C++ 프로그램에서 new 를 사용해 동적할당한 메모리는, 반드시 delete로  해제해야 한다.

 

C++에서는 메모리 누수(memory leak)로부터 프로그램의 안전성을 보장하기 위해 스마트 포인터를 제공 (memory 헤더).

 

스마트 포인터란 포인터처럼 동작하는 클래스 템플릿으로, 사용이 끝난 메모리를 자동으로 해제한다. (delete 자동 수행)

 

이는 new로 기본포인터(raw pointer)가 실제 메모리를 가리키도록 한 후

스마트 포인터 (smart pointer)에 기본포인터를 대입하여 사용한다.

이렇게 정의된 스마트 포인터의 수명이 다하면, 소멸자는 delete 키워드를 사용하여 할당된 메모리를 자동으로 해제한다.

 

 

※ 스마트 포인터의 종류

<C++11 이전>

auto_ptr (삭제됨)

 

<C++ 이후>

 

§ 참조 카운트 (reference count): 해당 메모리를 참조하는 포인터가 몇 개인지 나타내는 값

 

1. unique_ptr: 하나의 스마트포인터만이 객체를 가리킬 수 있도록 함

                        (reference cnt1을 넘길 수 없음)

2. shared_ptr: 하나의 특정 객체를 참조하는 스마트포인터의 개수를 참조

                        (reference cnt1씩 증가or감소, 참조횟수가 0이되면 delete되어 자동 해제)

3. weak_ptr: 하나 이상의 shared_ptr가 가리키는 객체를 참조 

                         (reference cnt늘리지 않음으로 shared_ptr 객체사이 순환참조를 제거하기 위함)

 

unique_ptr  (활용도: ★★★★★) 

사람이 실수할 수 있기에, unique와 move를 이용해 최대한 shared사용을 자제하는 것이 좋다.

unique_ptr 객체는 move() 멤버함수로 소유권을 이전할 수 있다. (단, 복사는 불가능!)

소유권이 이전되면 이전 unique_ptr 객체는 더이상 해당 객체를 소유하지 않게 재설정 된다.

 

#include <iostream>
#include <memory>
#include <string>
using namespace std;

int main() {
    unique_ptr<int> ptr1(new int(10));
    //unique Pointer 초기화와 함께 int형 값 10으로 동적할당

    auto ptr2 = move(ptr1);
    //auto 키워드로 ptr2는 ptr1의 타입을 추론(unique_ptr<int> 타입)
    //move 키워드는 ptr1에서 ptr2로 메모리의 소유권을 이전하기 위해 사용된다.

    unique_ptr<int> ptr3 = ptr1; // Err
    //애초에 ptr1이 소멸되어 접근이 불가하다.
    //대입 연산자를 이용한 복사는 오류를 발생시킨다.

    if (ptr1 == nullptr) {
        cout << "I'm Dead. Call ptr2 instead." << endl;
        cout << ptr1 << endl;   // *ptr1이라 써주지 않아서 Err
    }
    cout << *ptr2 << endl;

    ptr2.reset();
    ptr1.reset();
    //reset 함수로 메모리 영역을 삭제할 수 있다.
}

 

 

<make_unique()함수를 이용>

- 전달받은 인수를 사용해 지정된 타입의 객체를 생성

- 생성된 객체를 가리키는 unique_ptr을 반환하여 예외발생에 대해 안전히 대처가능

#include <iostream>
#include <memory>
#include <string>

using namespace std;

class Animal {
public:
    string name = "";
    Animal() { cout << "construct" << endl; };
    Animal(string name) {
        this->name = name;
        cout << "construct" << endl;
    }
    ~Animal() { cout << "destruct" << endl; };

    void setName() {
        cout << name << " : Grrr!" << endl;
    }
};

int main() {
    // Animal 객체 생성
    unique_ptr<Animal> a = make_unique<Animal>("Tiger");
    a->setName();
}

//------------출력------------//
//construct
//Tiger : Grrr!
//destruct

 

 

shared_ptr

shared_ptr은 하나의 특정 객체를 참조하는 스마트 포인터가 총 몇 개인지 를 참조하는 스마트 포인터

reference count는 특정 객체에 새로운 shared_ptr이 추가될 때마다 1씩 증가하고, 수명이 다하면 1씩 감소하며

추가되었던 shared_ptr이 해제되어 참조 카운트가 0이 되면 delete 가 자동으로 진행, 메모리를 자동해제한다.

#include <iostream>
#include <memory>
#include <string>
using namespace std;

int main() {
    shared_ptr<double> ptr1(new double(123.456));
    cout << ptr1.use_count() << endl;

    auto ptr2(ptr1);
    cout << ptr2.use_count() << endl;

    auto ptr3(ptr2);
    cout << ptr3.use_count() << endl;
}

//------------출력------------//
// 1
// 2
// 3

 

 

<make_shared()함수를 이용>

- shared_ptr의 객체를 안전하게 만들 수 있다.

- 전달받은 인수를 사용해 지정된 타입의 객체를 생성, 생성된 객체를 가리키는 shared_ptr을 반환.

- 이 함수도 예외 발생에 대해 안전하다.

#include <iostream>
#include <memory>
#include <string>
using namespace std;

class Monster {
public:
    Monster() { cout << "construct" << endl; }
    ~Monster() { cout << "destruct" << endl; }
};

int main()
{
    shared_ptr<Monster> mst_ptr1 = make_shared<Monster>();
    cout << mst_ptr1.use_count() << endl;

    auto mst_ptr2 = mst_ptr1;
    cout << mst_ptr1.use_count() << endl;

    mst_ptr2.reset();
    cout << mst_ptr1.use_count() << endl;
}

//------------출력------------//
// construct
// 1
// 2
// 1
// destruct

 

 

 

 

weak_ptr

shared_ptr은 참조 카운트를 기반으로 동작하는 스마트 포인터

 

하나 이상의 shared_ptr 객체가 소유하는 객체에 대한 접근을 제공하지만, 참조 카운트에 포함되지 않는다.

 

만약에 서로가 상대를 가르키는 shared_ptr을 가지고 있다면, 참조 횟수는 절대 1 이하로 내려가지 않기 때문에,

0이 되어야 자동으로 해제되는 스마트 포인터에 가장 크리티컬한 문제가 된다.

이렇게 서로가 상대를 참조하는 상황을 순환 참조(Circular Reference)라고 한다.

 

weak_ptr은 바로 이러한 shared_ptr 인스턴스 사이의 순환 참조를 제거하기 위해서 사용한다.

#include <iostream>
#include <memory>
#include <string>
using namespace std;

class Monster {
public:
    weak_ptr<Monster> otherMonster; 
    // shared_ptr로 선언할 경우 순환 참조 발생하기에 weak_ptr로 선언하여 순환 참조를 예방
    Monster() { cout << "생성" << endl; }
    ~Monster() { cout << "소멸" << endl; }
};

int main() {
    //철수와 민수에 대한 shared_ptr을 선언
    shared_ptr<Monster> chul_su = make_shared<Monster>();
    shared_ptr<Monster> min_su = make_shared<Monster>();

    // reference count : 참조 카운트
    cout << "철수 reference count : " << chul_su.use_count() << endl;
    cout << "철수 reference count : " << min_su.use_count() << endl;

    chul_su->otherMonster = min_su;
    min_su->otherMonster = chul_su;

    cout << "철수 reference count : " << chul_su.use_count() << endl;
    cout << "민수 reference count : " << min_su.use_count() << endl;
}

//------------출력------------//
// 생성
// 생성
// 철수 reference count : 1
// 철수 reference count : 1
// 철수 reference count : 1
// 민수 reference count : 1
// 소멸
// 소멸

 

 

 

 

 

 

 

※ 스마트 포인터 형변환(typecasting)

- static_pointer_cast

- dynamic_pointer_cast

- const_pointer_cast

 

포인터를 사용할 때, 다른 포인터 타입으로의 캐스팅은 용이했다.

하.지.만! shared_ptr 등의 스마트 포인터는 그렇지가 않다.

이를 위해서 ((AnotherClass*)(ptr.get())) 와 같이 강제로 포인터를 얻어 캐스팅을 해줄 수 있지만 전혀 C++ 답지 못하다.

 

따라서 static_pointer_cast  /  dynamic_pointer_cast  / const_pointer_cast 가 추가되었다.

이를 통해 안전하고도 편한 스마트 포인터 캐스팅이 가능해진 것이다.

vector<shared_ptr<MediaAsset>> assets;

assets.push_back(shared_ptr<Song>(new Song(L"Himesh Reshammiya", L"Tera Surroor")));
assets.push_back(shared_ptr<Song>(new Song(L"Penaz Masani", L"Tu Dil De De")));
assets.push_back(shared_ptr<Photo>(new Photo(L"2011-04-06", L"Redmond, WA", L"Soccer field at Microsoft.")));

vector<shared_ptr<MediaAsset>> photos;

copy_if(assets.begin(), assets.end(), back_inserter(photos), [] (shared_ptr<MediaAsset> p) -> bool {
    // Use dynamic_pointer_cast to test whether
    // element is a shared_ptr<Photo>.
    shared_ptr<Photo> temp = dynamic_pointer_cast<Photo>(p);
    return temp.get() != nullptr;
});

for (const auto&  p : photos) {
    // We know that the photos vector contains only
    // shared_ptr<Photo> objects, so use static_cast.
    wcout << "Photo location: " << (static_pointer_cast<Photo>(p))->location_ << endl;
}

 

 

 

 

코드출처) 

https://bbagwang.com/programming/cpp/%EC%8A%A4%EB%A7%88%ED%8A%B8-%ED%8F%AC%EC%9D%B8%ED%84%B0-smart-pointer/

 

https://ence2.github.io/2020/11/c-%EC%BA%90%EC%8A%A4%ED%8C%85-%EC%B4%9D%EC%A0%95%EB%A6%AC%EC%8A%A4%EB%A7%88%ED%8A%B8%ED%8F%AC%EC%9D%B8%ED%84%B0-%EC%BA%90%EC%8A%A4%ED%8C%85-%ED%8F%AC%ED%95%A8/

※ Type Casting이란?

변수의 type을 강제로 다른 type으로 변경하는 것으로 자료형간 || 포인터간 형변환시 사용된다.

C/C++은 변수의 type을 변경해 처리해야 하는 경우가 빈번하게 발생한다.

Q. 외부 library 사용시, 인자로 넘겨야할 변수가 char인데 외부의 library가 unsigned char를 사용한다면?
A: 개발자는 unsigned char로 변경해서 넘겨주어야 컴파일 에러가 발생하지 않는다.
int print(unsigned char *str){
    cout << str << endl;
}

int main() {
    char str[20] = "Hello, world!";

    print(str); // type casting이 필요 (Err)
    print(reinterpret_cast<unsigned char*>(str));
}

 

 

 

※ Type Casting의 종류

캐스트는 크게 2가지로 나눌 수 있다.

 

- 묵시적 캐스트 (implicit cast): 캐스트 연산자를 사용하지 않고 형변환이 이뤄지는 경우
- 명시적 캐스트 (explicit cast): 캐스트 연산자를 사용하여 형변환이 이뤄지는 경우

 

 

 

 

 

 

※ static_cast

[사용시기]: 논리적으로 변경가능한 경우

- static_cast의 특성은 묵시적 캐스트와 비슷하다 보면 된다.

- 묵시적 캐스트는 컴파일 시점에서 '무결성'을 검사하는데 이때, '허용'과 '컴파일러에 의한 값 변환' 두 관점이 있다.

- static_cast는 형변환에 의한 타입 확인compile 시간에 정적으로 수행한다.

 

§ 명시적 형변환 §

float f;
int a = static_cast<int>(f);

char *str = static_cast<char*>(f);  // Err!

 

 

 

 

※ const_cast

[사용시기]: 포인터, 참조형에서만 사용가능 const 및 volatile 제거할 때 사용된다!

- const_cast는 상수성이나 volatile(최적화 제외 변수)의 속성을 제거할 때 사용한다.

 

§ 명시적 형변환 §

int x = 10;

const int *pt_const_x = new int(10);
int *ptx;

ptx = const_cast<int*>(pt_const_x);
*ptx = 20; // 20에서 10으로 값 변경


const int &rt_const_x = x;
int& rtx = const_cast<int&>(rt_const_x);

 

 

 

 

 

※ reinterpret_cast

[사용시기]: 명시적 변환과 동작이 동일해 대신 사용된다. 단, const 사용 변환대상은 불가!

- 어떤 포인터 타입도 어떤 포인터 타입으로든 변환이 가능!

- [정수 -> 포인터] 타입도, [포인터 -> 정수]타입으로도 가능하다.

- 강력한 casting 같지만 특수 케이스가 아닌이상 사용을 잘하지 않는 것을 추천 (포인터가 강제 형변환되서)

 

§ 명시적 형변환 §

int *ptr = new int(10);
char *str;

str = reinterpret_cast<char*>(ptr);
*str = 20; // 10 -> 20으로 값변경

 

§ const 지정자 사용시, 명시적 형변환 §

const int *ptr = new int(10);
char *str;

str = reinterpret_cast<char*>(const_cast <int*> (ptr));
*str = 20; // 10에서 20으로 값변경

 

 

 

 

※ dynamic_cast

[사용시기]: class의 포인터, 참조변수간 형변환 시 안전하게 down casting을 위해 사용. 

Runtime conversions로 RTTI(Requires Runtime Type Information)

단, parent에 virtual 함수가 존재해야 정상작동!

- run time에 동적으로 상속계층관계를 가로지르거나 down casting시 사용됨

- 기본클래스에 virtual 멤버함수가 하나도 없다면, 다형성을 갖는게 아님(단형성)

따라서 dynamic_cast는 다형성을 띄지 않은 객체간 변환은 불가능!

 

§ 명시적 형변환 §

#include <iostream>
using namespace std;

class Blog {
public:
    Blog() { cout << "Blog()\n"; };
    virtual ~Blog() { cout << "~Blog()\n"; };

    void Show() { cout << "This is Blog Class\n"; }
};

class Tistory : public Blog {
public:
    Tistory() { cout << "Tistory()\n"; };
    virtual ~Tistory() { cout << "~Tistory()\n"; };

    void Show() { cout << "This is Tistory Class\n"; }
};

int main(void) {
    Blog* pBlog = new Blog();
    pBlog->Show();

    Tistory* pTistory = dynamic_cast<Tistory*>(pBlog);

    if (pTistory == nullptr) { //티스토리 클래스의 포인터가 nullptr이 나올떄.
        cout << "Runtime Error\n"; 
    }
    else {
        pTistory->Show();
    }

    delete pBlog;
    system("pause");
}

 

 

※ static_cast   VS   dynamic_cast

[static_cast]: 정적으로 형변환을 해도 아무 문제가 없다 (= 이미 어떤 녀석인지 알고 있다는 뜻), Fast

[dynamic_cast]: 동적으로 형변환을 시도 해본다는 뜻 (= 이녀석의 타입을 반드시 알아봐야 한다는 뜻), Slow (RTTI)

 

따라서 dynamic_cast를 이용해 Runtype의 해당 타입을 명확히 알아봐야 하고 (RTTI, Requires Runtime Type Info)

그렇지 않은 경우, static_cast를 이용해 변환 비용을 줄이는 것이 좋다. (동적타입체크를 안해도 되서)

 

// 비행기에 여러 직군의 사람들이 탑승했다.
// 한 승객이 갑자기 급성 맹장염에 걸려 의사가 급하게 수술을 해야 한다.
class Passenger {...};
class Student : public Passenger{
    ...
    void Study();
};
class Teacher : public Passenger{
    ...
    void Teach();
};
class Doctor : public Passenger{
    ...
    void Treat();
    void Operate();
};

int main() {
    typedef vector<Passenger *> PassengerVector;
    PassengerVector passengerVect;

    Passenger* pPS = new Student();
    if (pPS){
        passengerVect.push_back( pPS );
        // 비행기 타자마자 공부한다고 치고~
        // pPS가 명확하게 어느 클래스의 인스턴스인지 알고 있다.
        // 이 경우엔 굳이 비용이 들어가는 dynamic_cast가 아닌, static_cast를 쓰는게 낫다.
        Student* pS = static_cast<Student *>( pPS );
        pS->Study();
    }

    Passenger* pPT = new Teacher();
    if ( pPT ){
        passengerVect.push_back( pPT );
    }

    // Doctor 역시 비슷하게 추가.
    ...

    // 응급 환자 발생. passengerVect 중 의사가 있다면 수술을 시켜야 한다.
    PassengerVect::iterator bIter(passengerVect.begin());
    PassengerVect::iterator eIter(passengerVect.end());
    for( ; bIter != eIter; ++bIter ) {
        // Passenger 포인터로 저장된 녀석들 중 누가 의사인지 구분해야 한다.
        // 런타임 다형성 체크에 의해 Doctor가 아닌 녀석들에 대한 형변환 결과는 NULL
        Doctor* pD = dynamic_cast<Doctor *>(*bIter);
        if (pD){
            pD->Operate();
        }
    }
}

위 예제는 static_cast와 dynamic_cast를 구분해서 언제 쓰는게 좋은지 알 수 있는 예제이므로 잘 분석해보자.

Q. 만약, 위 코드의 전체 승객 중 의사를 찾아내는 과정에서 dynamic_cast가 아니라, static_cast를 사용하였다면 어떻게 될까?

static_cast는 동적 타입체크를 하지 않고, Student와 Teacher는 Person의 파생 클래스이므로 
변환 연산 규정에도 위배되지 않으므로, 그냥 타입 변환이 일어난다.
하지만, 변환 결과는 애초 기대했던 바와 전혀 다릅니다. 실제 Student 클래스 타입이지만,
Doctor 클래스 타입으로 타입 변환이 되면서Doctor 클래스 고유 멤버 함수에 대한 접근이 불가능해진다.

포인터가 가리키는 메모리 내용을 Doctor 클래스에 맞춰서 해석하기에
Student의 내용 중 일부가 Doctor 멤버 필드에 엉뚱하게 들어가거나, 슬라이스 문제가 발생할 수 있다.

다시 말해, 껍데기만 Doctor 클래스이지 내용은 전혀 Doctor의 것이 아니게 되는데,
멤버 필드에 접근시 엉뚱한 값이 들어가 있거나, 런타임 Err가 발생할 수 있다.

코드 및 설명 출처) https://ence2.github.io/2020/11/c-%EC%BA%90%EC%8A%A4%ED%8C%85-%EC%B4%9D%EC%A0%95%EB%A6%AC%EC%8A%A4%EB%A7%88%ED%8A%B8%ED%8F%AC%EC%9D%B8%ED%84%B0-%EC%BA%90%EC%8A%A4%ED%8C%85-%ED%8F%AC%ED%95%A8/

※ 복사함수

객체를 복사하는 함수로 복사생성자복사대입연산자가 있다.

- 복사생성자 (Copy Constructor): 포인터를 멤버로 갖는 클래스는 깊은 복사(deep copy)를 수행.

- 복사대입연산자 (operator=): 따로 선언하지 않을 경우 default로 얕은 복사를 하는 대입연산자를 컴파일러가 자동생성.

 

 주소 즉, 포인터를 갖는 경우,복사생성자(Copy Constructor)와

대입연산자(Copy Assignment Operator)를 반드시 정의! 

#include <iostream>
#include <cstring>
using namespace std;

class Person {
    char* name;
    char* phone;
    int age;
public:
    Person();
    // VC++에서 char* _name은 문자열 상수를 직접 입력으로 받지 못함
    Person(const char _name[], const char _phone[], int _age);
    ~Person();
    Person(const Person& p);
    Person& operator=(const Person& p);
    void ShowData(); // 선언
};

Person::Person() {
    cout << " 매개변수 없는 생성자 호출" << endl;
    name = NULL;
    phone = NULL;
    age = 0;
}

Person::Person(const char _name[], const char _phone[], int _age) {
    cout << " 매개변수 3개 생성자 호출" << endl;
    name = new char[strlen(_name) + 1]; // NULL문자를 고려하여 +1만큼 할당
    strcpy_s(name, strlen(_name) + 1, _name);

    phone = new char[strlen(_phone) + 1];
    strcpy_s(phone, strlen(_phone) + 1, _phone);

    age = _age;

    cout << " name 주소 : " << (void*)name << ", phone 주소 : " << (void*)phone << endl;
}

Person::~Person() { // DeAllocate the heap
    cout << " 소멸자 호출" << endl;
    delete[]name;
    delete[]phone;

    cout << " name 주소 해제 : " << (void*)name << ", phone 주소 해제 : " << (void*)phone << endl;
}

Person::Person(const Person& p) : age(p.age) {
    cout << " 복사 생성자 호출" << endl;
    name = new char[strlen(p.name) + 1]; // NULL문자를 고려하여 +1만큼 할당
    strcpy_s(name, strlen(p.name) + 1, p.name); // 깊은 복사
    // The statement name = new char; will create the new heap location 
    // and then copies the value of obj content to new heap location.

    phone = new char[strlen(p.phone) + 1]; 
    strcpy_s(phone, strlen(p.phone) + 1, p.phone);

    cout << " name 주소 : " << (void*)name << ", phone 주소 : " << (void*)phone << endl;
}

Person& Person::operator=(const Person& p) {
    cout << " 복사 대입 연산자 호출" << endl;
    if (this != &p) {
        delete[]name;
        delete[]phone;

        name = new char[strlen(p.name) + 1]; // NULL문자를 고려하여 +1만큼 할당
        strcpy_s(name, strlen(p.name) + 1, p.name); // 깊은 복사

        phone = new char[strlen(p.phone) + 1]; 
        strcpy_s(phone, strlen(p.phone) + 1, p.phone);

        age = p.age;

        cout << " name 주소 : " << (void*)name << ", phone 주소 : " << (void*)phone << endl;
    }
    return *this;
}

void Person::ShowData() { // 클래스 외부에서 클래스 멤버 함수 정의
    cout << " name : " << name << ", phone : " << phone << ", age : " << age << endl;
}

int main() {
    cout << endl; // 출력 구분 목적
    Person p1("홍길동", "010-1234-5555", 34);
    p1.ShowData();

    cout << endl;
    Person p2(p1); // 복사 연산자 호출
    p2.ShowData();

    cout << endl;
    Person p3;
    p3 = p1; // 대입연산자 호출된다. p3.operator=(p1);
    p3.ShowData();

    // 객체가 소멸되는 순서는 객체가 생성된 순서의 반대다.
    cout << endl;
}

출처: https://link2me.tistory.com/1755?category=1075719 [소소한 일상 및 업무TIP 다루기:티스토리]

 

 

 

 

 

 

[Effective C++ _item 5]에서 말했듯, 복사함수는 컴파일러가 생성해 주지만 

우리가 객체복사함수를 선언한다는 것은 컴파일러의 복사함수의 기본동작이 마음에 들지 않기 때문이다.

 

[Effective C++ _item 12]: 객체의 모든 부분을 빠짐없이 복사하자!

Ex-1) 문제되는 점이 없다.

 

Ex-2) 데이터 멤버 하나를 Custormer에 추가하게 되면서 문제가 발생한다.

=> 바로 복사함수가 완전복사가 아닌, 부분복사가 된다는 점이다!

따라서 이 문제를 해결하기 위해 복사함수는 물론 생성자도 모두 다시 갱신 및 작성해줘야 한다.

이 문제가 가장 문제가 되는 경우가 있다. 바로클래스의 상속이다!

아래의 예시에서 PriorityCustomer에 선언된 데이터 멤버를 모두 복사하고 있는 것은 사실이지만

Customer로 부터 상속된 즉, PriorityCustomer복사생성자에는 기본클래스 생성자에 넘길 인자들이 명시되지 않아서 기본 생성자에 의해 초기화가 되어 버린다.

또한, 복사대입 연산자의 경우 기본클래스의 데이터 멤버를 건드릴 시도도 하지 않기에 기본 클래스의 데이터 멤버는 변경되지 않고 그대로 있게 된다.

 

따라서 아래 빨간색 글씨처럼 파생클래스의 복사함수 안에서 기본클래스의 복사함수를 호출하자!

 

 

 

 

 

 

[Effective C++ _item 11]: operator= 에서는 자기대입에 대한 처리가 빠지지 않도록 하자!

자기대입이란?

어떤 객체자기자신에 대입연산자를 적용하는 것이다.

예를 들어 arr[i] = arr[j]; 라는 문장에서 ij값이 같다면, 자기대입문이 되는 것이다.

중복참조 때문에 이러한 현상이 발생하게 되는 것이다

 

중복참조란?

여러곳에서 하나의 객체를 참조하는 것

 

operator= 구현 시, 객체가 그 자신에 대입되는 경우를 제대로 처리하는기법

1. 원본 객체와 복사대상 객체의 주소를 비교 (위의 if를 이용한 코드)
2. 예외 안정성에 집중하기 (문장의 순서를 적절히 조정 or swap기법) – item 29 참조

※ operator overloading

- 하나의 이름으로 오버로딩 된 수만큼 다양한 기능을 제공해 주기에 연산자 오버로딩을 통해 기존 연산자 외의 다른 기능을 추가할 수 있다.

- 연산자 오버로딩에는 2가지 방법이 있다.

 1. 멤버함수에 의한 방법

 2. 전역함수에 의한 방법

1. pos1.operator+(pos2);  // by 멤버함수
2. operator+(pos1, pos2); // by 전역함수

 

ex) 

const Animal& a를 인자로 받지만 사실 내부적으로는 오른쪽처럼 this도 함께 인자로 받는다는 사실!
단, this는 컴파일러가 가정하기 때문에 직접 명시하지 않는다.
아래의 경우에는 operator를 멤버함수로 구현할 경우 operator의 인자는 2가지이다.
1. 직접 명시된 인자
2. 현재의 object(this)  //컴파일러가 가정해서 직접 명시하지 않음
따라서 아래의 경우에도 내부적으로는 Animal& operator+(this, const Animal& a) {...} 가 된다.


위와 아래의 예제에서 출력의 차이가 발생하는 이유는 다음과 같다.
위의 예제의 경우, string이라는 변수에 this->name과 a.name이 합성된 값을 저장하고, 
newName을 return하므로 dog.name즉, this의 값은 원래 값이 유지되는 것이다.
아래 예제의 경우, name+=a.name은 this의 name값을 인자 a의 a.name과 합성한 것이므로 dog객체의 this->name값이 변조되는 것이다.

 

 

 

cf. 멤버함수 기반으로만! overloading이 가능한 연산자

=     대입연산자
()    함수 호출 연산자
[]    배열 접근 연산자(Index 연산자)
->    멤버 접근을 위한 포인터 연산자

이들은 객체를 대상으로 진행해야 의미가 있는 연산자 => 멤버함수 기반으로만 연산자 오버로딩을 허용한다.

 

 

- 구조

Point operator= (const Point& ref){
    Point pos( . . . );
    return pos;
}

 

 

 

※ 단항연산자 오버로딩 (ex. 증가,감소 연산자)

Point의 객체 pos에 대하여 ++pos;와 같은 표현은 아래와 같다.

i) pos의 멤버함수 호출형태: pos.operator++();
ii) 피연산자가 모두 인자로 전달되는 전역함수 형태: operator++(pos);

이때, i)에서 전달할 인자가 없는 이유는 단항연산자를 오버로딩해서 하나 있는 피연산자의 멤버함수를 호출하는 형태가 되어버렸기 때문이다.

 

cf) prefix, postfix 전후위 증감 오버로딩

 

 

※ 교환법칙

 

 

 

 

 

 

 

 

 

 

※ 대입연산자 class명& operator= (const class명& ref)

반드시 해야하는 연산자 오버로딩으로 복사생성자와 매우 유사하다.

i) 정의 하지 않으면 default 대입 연산자가 삽입된다.
ii) default 대입연산자는 멤버 대 멤버의 복사(얕은 복사)를 진행한다.
iii) 연산자 내에서 동적할당, 깊은 복사가 필요하면 직접 정의 해야 한다.
class Point {
private:
    int num1, num2;
public:
    Point(int num1 = 0, int num2 = 0) : num1(num1), num2(num2) { }
    void ShowPosition() { cout << num1 << ',' << num2 << endl; }
    
    //default operator=
    Point& operator= (const Point& ref){
        num1 = ref.num1;
        num2 = ref.num2;
        return *this;
    }
};

 

 

※ default 대입연산자의 문제.

 두 객체가 동시에 참조하는 상황이 벌어지면?

- 주소값을 잃거나 객체 소멸과정에서 지워진 값을 중복소멸시키는 문제 발생

- 따라서 연산자 내에서 동적할당or깊은 복사가 필요하다면 직접 정의한다!

※ 상속 (inheritance)

- 자기자신이 갖는 멤버는 물론 기본클래스의 멤버까지 받아오는 방법이다.

cf. 정보은닉은 하나의 객체 내에서도 진행되기에 private에 직접 접근이 안될 뿐! 간접접근은 가능하다.

 

★★<파생클래스의 생성자는 기본클래스의 멤버까지 초기화 해줘야 한다!>★★

- 유도클래스의 생성자는 기본클래스의 생성자를 호출하여 멤버초기화 하는 것이 좋다.

- 유도클래스는 initializer를 이용해 상속하는 클래스의 생성자를 호출할 수 있다.

 

 

 

※ 유도클래스의 객체생성 과정

1. 유도클래스의 객체생성과정에서 기초클래스의 생성자는 무.조.건! 호출

2. 유도클래스의 생성자에서 기초클래스의 생성자 호출을 명시하지 않으면 => 기초클래스의 void 생성자가 호출!

 

 

※ IS-A 

is-a 즉, "일종의 ~이다" 로 해석되며 상속관계가 성립하기 위한 기본 조건이다.

 

 

 

 

 

 

 

※ 바인딩 (binding)

함수나 변수의 주소가 결정되는 것으로 다음 2가지 종류가 있다.

- early binding (정적 바인딩): 컴파일시간에 바인딩 되는 것 (초기화 리스트, 일반적 변수)

- late binding (동적 바인딩): 실행시간(run time)에 바인딩 되는 것 =>  virtual 선언된 모든 것!

 

간단히 말하자면 변수의 데이터 형이 결정되는 시점에 따라 나뉜다.

 

조금 더 자세히 말하자면 다음과 같다.

정적 바인딩: 컴파일시 관련 라이브러리, 객체와 링크해 실행 모듈을 만드는 것

동적 바인딩: 컴파일시 정보를 갖고 있지만 실제 실행시간에 해당 객체와 링크하여 실행 모듈을 만드는 것. 

 

정적 바인딩을 사용하면 컴파일 시 데이터 형이 정해지므로 실행에 효율적이고,

동적 바인딩을 사용하면 실행 시 데이터 형이 변경되므로 적응력이 뛰어난 프로그램을 제작할 수 있다.

 

 

 

※ 동적 바인딩의 발생

동적 바인딩의 발생은 다음과 같다

- 기본/파생클래스 내 멤버함수가 가상함수 호출

- 외부함수에서 기본클래스의 포인터로 가상함수 호출

- 다른 클래스에서 가상함수 호출

 

이렇게 가상함수가 호출되면 실행 중 객체 내에 오버라이딩(overriding)된 가상함수를 동적으로 찾아 호출한다.

 

§ 오버라이딩한 함수를 직접 호출하는 동적바인딩 예시코드 (객체지향 프로그래밍을 위한 C++의 이해)

 

 

컴퓨터 입장에서 p  c 모두 Parent 를 가리키는 포인터들이므로, 당연히 아래처럼 호출했을 때

p->paint();
c->paint();
모두 Parent 의 print() 가 호출되어야 하겠지만, 실제로는 print() 가 가상함수므로,
"실제로 p 와 c 가 가리키는 객체의 print()",
즉 p->f() 는 Parent 의 print()를, c->f() 는 Child 의 print()가 호출된다.
이와 같은 일이 가능한 이유는 print()를 가상함수로 만들었기 때문입니다.

 

 

 

 

 

※ virtual function (가상함수)

- 가상함수: 기본 클래스 내의 함수를 파생클래스에서 재정의 (같은 이름의 함수를 overriding)하고자 할 때 사용.

- 가상함수가 포인터에 의해 호출된 경우, 포인터가 가리키는 객체의 클래스에 따라 컴파일러가 결정해 호출한다.

- 따라서 기본클래스 내의 멤버함수 앞에 virtual을 통해 overriding이 가능하다.

 

이런 가상함수가 나오게 된 계기는 다음과 같은 의문 때문이다.

 

Q. 포인터변수 자료형에 따라 호출되는 함수가 달라지는 것에 문제가 있지 않을까?

A. virtual을 이용하는 가상함수는 overriding하는 함수도 가상함수가 되어버린다.

즉, 가상함수로 선언되면 해당 함수호출시 포인터의 자료형 기반 호출이 아닌 포인터변수가 실제 실제로 가리키는 객체를 참조하여 호출의 대상을 결정한다.

 

 

 

※ 순수가상함수 

- 함수의 몸체가 정의되지 않은, 구현이 없는 가상 함수로 구현 대신, 가상함수에 0(NULL)값을 대입하면 된다.

- 가상함수: 상속관계(기본/파생클래스)에서 오버라이딩을 할 수 있는 함수

- 순수가상함수: 파생클래스에서 구현되어야하는 함수, 인터페이스로 이 클래스를 사용하겠다는 의미

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

 

※ 추상클래스와 override

- 추상클래스: 순수가상함수를 포함하는 클래스

- 추상클래스에 의한 포인터객체, 참조객체등의 정의는 가능하다.

- 추상클래스의 사용불가능한 용도: 변수, 멤버데이터  /  인수형식  /  함수 반환형식  /  명시적 변환형식

class Animal {
public:
    virtual void virtualFunc() = 0;
};

- 순수가상함수는 기본클래스내에서 특별한 연산, 동작도 정의하지 않고, 단지 선언만 하는 가상함수다.

- 따라서 순수가상함수는 본체를 갖지 않는 함수다.

- 클래스 디자인에서 반드시 필요하지만 사용자에게 그 부분을 구현할 것을 알려야 하는데, 이때 추상클래스를 사용!

 

예제 1.

#include <iostream>
using namespace std;

class Graphic{  // 추상 클래스 생성
public:
    virtual void Draw() = 0;  // 순수 가상함수
};

class Line : public Graphic{ // 추상 클래스 상속
public:
    virtual void Draw() {
        cout << "선을 그린다." << endl;
    }
};

class Circle : public Graphic {  // 추상클래스 상속
public:
    virtual void Draw() {
        cout << "원을 그린다." << endl;
    }
};

class Rect : public Graphic {
public:
    virtual void Draw() {
        cout << "사각형을 그린다." << endl;
    }
};

int main() {
    Graphic *ptG[3];     // 객체 배열 선언
    // 객체 정의
    ptG[0] = new Line;   
    ptG[1] = new Circle;   
    ptG[2] = new Rect;

    for (int i = 0; i < 3; i++) {
        ptG[i]->Draw();
    }

    for (int i = 0; i < 3; i++) {
        delete ptG[i];
    }
}

 

예제 2. Override 키워드

virtual 키워드: 가상함수의 시작부분에 붙임

override 키워드: 파생클래스의 가상함수에 붙임 (이 가상함수가 상속받아서 오버라이딩한 함수다라는 것을 표현)

class Parent{
public:
    virtual void func() = 0;
};

class Child : public Parent {
public:
    void func() override {
        cout << "func()함수 선언" << endl;
    }
};

int main() {
    Parent *p = new Child();
    Child *c = new Child();

    c->func();

    delete c;
}

 

예제 3. 순수 가상 소멸자

- 순수가상함수가 하나도 없는 클래스를 추상클래스로 만들고 싶을 때, 순수 가상 소멸자로 만들 수 있다.

class Animal{
public:
    Animal() { };
    virtual ~Animal() = 0;
};
.
.
.
Animal::~Animal() { } // 가상 소멸자 호출

 

 

 

 

 

※ 가상소멸자

virtual 키워드가 필요한 대상은 2가지이다.

가상함수
가상소멸자

- 가상 생성자는 객체가 생성되기 전에 이루어지기에 정의가 불가능하다.

- 가상 소멸자는 몸체의 내용이 없어도 가능하기에 가상생성자를 없애기 위해 사용할 수 있다.

 

- 순수 가상함수가 하나도 없는 클래스를 추상클래스로 만들고 싶을 때, 소멸자를 순수 가상함수로 만든다.

- 파생 클래스의 소멸자가 호출된 후 기본클래스 소멸자도 호출되므로 순수 가상 소멸자의 정의의 필요성을 알 수 있다.

class Parent{
public:
    virtual ~Parent(){  // 가상 소멸자 선언
        cout << "기본클래스 소멸자" << endl;
    }
};

class Child : public Parent {
public:
    ~Child(){
        cout << "파생클래스 소멸자" << endl;
    }
};

int main() {
    Parent *p = new Child;  // 파생클래스로 객체 선언
    delete p;
    // 객체 제거 => 파생클래스 가상 소멸자 => 기본클래스 가상 소멸자 실행
}

 

 

 

if, 가상함수처럼 기초클래스의 소멸자만 virtual 선언을 하면?

=> 마찬가지로 유도클래스의 소멸자들도 모두 가상소멸자로 선언된다!

 

※ 가상함수의 동작원리와 가상함수테이블 참조

 

 

 

 

 

 

 

 

※ 다중상속 (multiple inheritance)

둘 이상의 클래스를 동시에 상속하는 것으로 아래와 같은 견해들이 있다.

- "보통 다중상속은 다양한 문제가 있기에 가급적 사용하지 않는 것을 추천"

- "다만 예외적으로 매우 제한적인 사용까지는 부정할 필요는 없다."

다중상속은 어느 클래스에 선언된 멤버에 접근할 것인가에 대한 모호성이 발생한다.

따라서 같은 이름의 함수의 경우, 아래와 같이 해결할 수 있다.

Base1::Func();
Base2::Func();

또 하나의 모호성은 가상상속 부분이다.

 

virtual선언이 되지 않은 상태에서 객체가 생성되면 하나의 객체 안에 2개의 클래스 멤버가 존재하여 함수 내에서의 이름만으로 다른 함수를 호출할 수 없다.

 

따라서 이 경우 어느 클래스를 통해 간접 상속한 클래스의 멤버함수를 호출할 건지 다음과 같이 정의 해야 한다.

MiddleDerived1::Func();  //MiddleDerived1클래스가 상속한 Base클래스의 Func함수 호출
MiddleDerived2::Func();  //MiddleDerived2클래스가 상속한 Base클래스의 Func함수 호출

 

혹은 virtual 키워드로 아래와 같이 해결할 수 있다.

class MiddleDerived1 : virtual public Base { . . . };
class MiddleDerived2 : virtual public Base { . . . };
class LastDerived : public MiddleDerived1, public MiddleDerived2 { . . . };

 

 

 

 

 

 

※ 다형성 (polymorphism)

- 객체지향의 매우 중요한 요소로 동질이상(同質異像)을 의미. (모습은 같은데 형태가 다름)

즉, 문장은 같은데 결과는 다르다는 뜻이다.

ex) virtual로 선언된 같은 이름, 다른 클래스의 함수를 동일한 포인터 변수로 받아도 다른 결과값이 나올 수 있다.

- copy constructor

- move constructor

- perfect forwarding

- 임시객체

- explicit, friend, static, mutable

※ C++ 클래스의 생성자 종류

- 기본생성자 (Default Constructor): 기본적으로 컴파일러가 생성해주는 생성자.

- 복사생성자 (Copy Constructor): 포인터를 멤버로 갖는 클래스는 깊은 복사(deep copy)를 수행한다.

- 이동생성자 (Move Constructor): r-value를 parameter로 갖는 생성자. (C++ 11에서 추가된 타입의 생성자)

 

 

 

 

※ 복사생성자 (Copy Constructor)

- 자신과 같은 클래스 타입의 다른 객체에 대해 참조를 인수로 전달받아참조를 이용해 자신을 초기화 하는 방법

- 복사생성자는 새롭게 생성되는 객체가 원본객체와 같지만 완전한 독립성을 갖게 해준다.

  (복사생성자를 이용한 대입은 깊은 복사(deep copy)를 통한 값의 복사이기 때문.)

 

※ 복사생성자 형식: Add(const Add& copy) : num1(copy.num1), num2(copy.num2) { ... }

 

 

※ default 복사생성자

- 복사생성자 정의가 없을 때 자동 삽입되며 이로 인해 멤버 간 복사(얕은 복사)시 복사생성자를 직접 정의할 필요가 없다.

- 멤버 대 멤버의 단순복사를 진행한다.

- 다만 필수로 복사생성자를 정의해야하는 경우가 있다. (깊은 복사)

int main() {
    Position xpos;
    Position ypos(xpos); // default 복사생성자의 멤버 대 멤버 복사(얕은 복사)
}

 

Position ypos(xpos)는 Position ypos = xpos와 동일하게 사용될 수 있다.

 

 

※ 얕은 복사의 문제점

위의 Person p2(p1)에서 default 복사생성자의 멤버 대 멤버 복사(얕은 복사)가 진행된다.

이때, ~Person 즉, 소멸자에서 delete [ ]name;를 할 때 문제점이 발생한다. 

위의 코드의 과정을 보면, 

- 얕은 복사의 결과로 하나의 문자열을 2개의 객체가 동시에 참조하게 된다.

- 위의 예시에서 delete [ ]name;으로 p2의 소멸자가 호출되면서 문자열을 소멸시킨다.

- 이미 지워진 문자열을 대상으로 p1의 delete 연산을 진행해야 한다는 문제점이 발생한다.

 

★ 따라서 깊은 복사를 위한 복사생성자를 만들어 각각의 문자열을 참조해 문제가 발생하지 않게 해줘야 한다.

※깊은복사

- 멤버 뿐만 아니라 포인터로 참조하는 대상까지 복사하는 방법

- default 생성자가 불충분할 때 사용자 정의 복사생성자를 선언한다.

위의 코드에서 아래 코드를 추가해주면 된다.

- 멤버변수 age의 멤버 대 멤버 복사

- 메모리공간 할당 후 문자열 복사, 할당된 메모리 주소값을 멤버 name에 저장

즉, 두 객체를 각각 동적으로 할당된 배열을 갖게 하는데 이를 깊은 복사(deep copy)라 한다.

 

 

 

§ 또 다른 예제

DynamicArray (const DynamicArray& other) : mSize(other.mSize), mArray(nullptr){
    // 자신만의 동적데이터공간 할당
    mArray = new int[mSize];
    
    // 다른 객체로부터의 데이터 복사
    for (int i = 0; i < mSize; i++) {
        mArray[i] = other.mArray[i];
    }
}

 

 

 

※ 복사생성자 호출 시점: 객체를 새로 생성하되 생성과 동시에 동일한 자료형의 객체로 초기화 할 때.

1. 기존에 생성된 객체를 이용해 새로운 객체를 초기화 할 때.  [Person p2 = p1;]

2. Call-by-Value 방식의 함수호출과정에서 객체를 인자로 전달할 때

3. 객체를 반환하지만 참조형으로 반환하지 않는 경우.

- 함수의 값을 반환하면 별도의 메모리공간이 할당되고 이 공간에 반환값이 저장된다. (반환값으로 초기화)

 

2. CBV 함수호출에서 객체를 인자로 전달하는 경우 예시

출처) 윤성우의 열혈C++프로그래밍

3. 객체를 반환하지만 참조형으로 반환하지 않는경우

출처) 윤성우의 열혈C++ 프로그래밍

 

 

 

 

cf. 생성자, 소멸자, 복사생성자 호출시점 명확히 알고가자!

Q. 어 그러면 생성자가 모두 출력된 후 소멸자가 출력되는 거면 되게 쉽게 생각해도 되겠네요?

A: 그렇게 생각하면 안된다. 객체 소멸은 여러 가지의 경우가 있는데 그 중 하나로는 객체는 함수가 종료되면 사라진 다는 점이다. 정확히는 함수 내부의 nested block이 끝나면 사라지게 되는 것이다.

예를 들어 아래와 같이 p1객체에 block을 씌웠다 가정하자.

{
 Person p1(5);
cout << "부모 객체 p1 생성" << endl;
 }
Person p2(p1); //오류 발생
cout << "부모 객체 p2 생성 및 객체 p1 복사" << endl;

만약 실행되었다면 다음과 같이 출력이될 것이다.

부모의 생성자
부모 객체 p1 생성

부모의 소멸자
부모의 생성자
부모 객체 p2 생성 및 객체 p1 복사
부모의 생성자

하지만 부모의 소멸자가 호출되어 객체 p1이 없어지니 객체 p2는 객체 p1을 복사하지 못하게 되는 것이다.

 

 

이해를 위해 또 다른 예시를 들어 보겠다.

 {
     Person p1(5);
 }
cout << "부모 객체 p1 생성" << endl;

이렇게 된다면 출력은 어떻게 될까?

이는 아래와 같다.

부모의 생성자

부모의 소멸자
부모 객체 p1 생성

즉, 블럭이 종료되면 소멸자가 호출된다는 것을 알 수 있다.

 

 

cf. 만약 private멤버로 char* name과 같은 배열관련 변수가 들어갔다면 delete []name로 memory leak을 막아야 한다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

※ 임시객체를 이용한 obj.func().func(); 연산

 

 

 

 

 

 

 

 

※ 반환시 만들어진 (임시)객체의 소멸

1. 임시객체는 다음행으로 넘어가면 바로 소멸된다.

2. 참조자에 참조되는 임시객체는 바로 소멸되지 않는다.

 

또 다른 예시)

 

 

 

 

 

 

 

 

※ r-value 참조

r-value 참조는 상수나 임의 저장소를 참조하는 개념으로 데이터 타입에  Double Ampersand(&&)를 붙여 표기한다.

이런 더블 엡퍼센드를 r-value 참조자라 부른다.

 

사실 이런 r-value 참조는 l-value랑 달리 OOP의 원칙을 준수하는 것보다

프로그램의 성능향상 및 프로그램 개발 생산성에 초점을 맞춰 생겨난 개념이다.

/*---------------일반 변수--------------- */
void f(){
    int val = 10; // val 변수 생성 후 10 입력
}
// 함수 반환 시 변수를 소멸


/*---------------r value 참조 변수--------------- */
void f(){
    int&& val = 10; // 저장소를 만들어 10을 입력, 저장소에 대해 r value 참조
}
// 함수 반환 시 임시저장소와 함께 r value 참조를 소멸

위의 경우, 표현은 다르지만 작업결과는 동일한데 이말은 즉, r value 참조자는 위와 같은 목적으로 만들어진 개념이 아니다

 

 

?. r-value 참조는 목적이 무엇인가 ?

1. 클래스의 객체 생성시 사용하는 이동생성자 이동대입연산자의 인수로 사용.

2. perfect forwarding이라는, 함수 오버로딩의 개수를 줄이는데 사용

https://chan4im.tistory.com/20

 

☆Prev. C++, Modern C++의 변화

※ C++의 버전변화 C++98, C++03 ==> C++11 ==> C++14 ... C++11: 기존 C++98에서 람다 표현식을 지원한 것 C++14: 일반화된 함수반환 형식 유추를 지원한 것 ※ C++11/14 문법적 변경 사항 1. 초기화 리스트 및..

chan4im.tistory.com

 

 

 

 

 

 

 

이동생성자 (Move Constructor) _ (feat. &&의 사용)

- 이동생성자가 호출되면 얕은복사(shallow copy) => 원본의 소유권을 대상으로 이전하는(move) 방식으로 객체를 생성.

- 그 이후 원본 객체를 NULL로 초기화 하여 접근할 수 없게 한다.

 

Q. 복사생성자가 있는데 굳이 이동생성자를 써야하는 이유는 뭘까?

A. 아래 코드를 통해 설명하겠다.

#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

class Animal {
private:
    char *name;
    int age;
public:
    Animal(int age_, const char* name_) {
        age = age_;
        name = new char[strlen(name_) + 1];
        strcpy(name, name_);
    }
    Animal(const Animal & a) { //복사생성자에 의한 깊은복사
        age = a.age;
        name = new char[strlen(a.name) + 1];
        strcpy(name, a.name);
        cout << "복사생성자" << endl;
    }


    Animal(Animal && a) noexcept { //이동생성자에 의한 얕은복사
        age = a.age;
        name = a.name;
        cout << "이동생성자" << endl;
        a.name = nullptr;
    }
    ~Animal(){
        cout <<"Destructor!!" << endl;
        if(name)
            delete [] name;
    }
    void changeName(const char *newName) {
        strcpy(name, newName);
    }
    void printAnimal() {
        cout << "Name: " << name << " Age: " << age << endl;
    }
};

int main() {
    Animal A(10, "Jenny");
    A.printAnimal();

    vector<Animal> vec; // Animal 벡터 타입

    // Animal 벡터 객체 삽입
    vec.push_back(A);
    vec.push_back(A);
    vec.push_back(A);
    vec.push_back(A);
    vec.push_back(A);

    A.printAnimal();

    for (int i = 0; i < 5; i++) {
        vec[i].printAnimal();
    }
    return 0;
}

복사생성자만 있는 경우, 새 vector 생성마다 기존 vector에 저장된 객체들이
새로운 vector로 복사될 때, 복사생성자가 호출되고 성능저하가 일어남 
이는 복사생성자의 new로 인한 메모리 할당의 반복으로 일어나는 현상임.

이런 문제를 해결하기 위해 shallow copy를 수행하는 이동생성자를 정의해주는데,
새로운 vector로 이동시, 이동생성자가 호출되며 불필요한 메모리할당을 줄여준다.

 

[이동생성자 사용 시 주의할 점]

1. 이동생성자에는 noexcept 키워드가 지정되어야 한다. (이동생성자 수행중, 예외가 없다는 것을 컴파일러가 인지해야함)

2. shallow copy가 일어나는 변수(포인터, 주소관련 변수)에 nullptr를 넣어줘야 함

3, 소멸자에서 메모리할당 관련 변수가 nullptr인지 확인해 줘야함.

 

 

ex-2) 예제

출처)&nbsp;https://link2me.tistory.com/1756

또 다른 설명)

https://learn.microsoft.com/ko-kr/cpp/cpp/move-constructors-and-move-assignment-operators-cpp?view=msvc-170&viewFallbackFrom=vs-2019 

 

방법: 이동 생성자 정의 및 할당 연산자 이동(C++)

자세한 정보: 생성자 이동 및 할당 연산자 이동(C++)

learn.microsoft.com

 

 

※ Perfect Forwarding

#include <iostream>
using namespace std;

class Widget {
public:
    Widget(int&, int&){ }
};
class X_Widget {
public:
    X_Widget(const int&, int&){ }
};
class Y_Widget {
public:
    Y_Widget(int&, const int&){ }
};
class Z_Widget {
public:
    Z_Widget(const int&, const int&){ }
};

// 참조를 인수로 사용하는 함수 templated
template <typename T, typename A, typename B>
T* factory(A& a, B& b) {
    return new T(a, b);
}

int main() {
    int a = 4, b = 5;

    Widget *w = factory<Widget>(a,b);
    // 아래부터는 모든 타입을 수용하는 함수템플릿을 만들지 않는 이상 Err 발생
    X_Widget *x = factory<X_Widget>(2, b);
    Y_Widget *y = factory<Y_Widget>(a, 2);
    Z_Widget *z = factory<Z_Widget>(2, 2);

    delete w; delete x; delete y; delete z;
}

 

 

따라서 결과적으로 참조타입별 함수오버로딩이 증가하는 문제를 해결하는 방법으로 다음과 같이 r value참조를 사용한다.

#include <iostream>
using namespace std;

class Widget {
public:
    Widget(int&, int&){ }
};
class X_Widget {
public:
    X_Widget(const int&, int&){ }
};
class Y_Widget {
public:
    Y_Widget(int&, const int&){ }
};
class Z_Widget {
public:
    Z_Widget(const int&, const int&){ }
};

// 참조를 인수로 사용하는 함수 templated
template <typename T, typename A, typename B>
T* factory(A&& a, B&& b) {
    return new T(a, b);
 // return new T(std::forward<A>(a), std::forward<B>(b));
}

int main() {
    int a = 4, b = 5;

    Widget *w = factory<Widget>(a,b);
    X_Widget *x = factory<X_Widget>(2, b);
    Y_Widget *y = factory<Y_Widget>(a, 2);
    Z_Widget *z = factory<Z_Widget>(2, 2);

    delete w; delete x; delete y; delete z;
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

※ explicit: 복사생성자의 묵시적변환

- Add add2 = add1;을 Add add2(add1); 과 같은 방식을 허용하지 않는 방법

explicit Add(const Add& copy) : num1(copy.num1), num2(copy.num2){
    //empty
}

그렇다면 이런 explicit의 이점은 무엇일까?

explicit을 사용함으로 인해 컴파일러의 자동 형변환을 막을 수 있다.

이는 내부 함수, 템플릿 구현에 탁월한데 사용자가 원하지 않는 형변환이 발생하는 등의

예상할 수 없는 버그가 발생할 수 있어서 explicit으로 사용자가 상황에 맞게 직접 형변환을 해주도록 한다.

 

 

 

 

※ friend 선언: private에 접근을 허용하는 선언(남용X)

1. A클래스가 B클래스를 대상으로 friend 선언을 하면 B는 A의 private멤버에 직접 접근이 가능

2. 단, A도 B의 private에 직접접근을 하려면 B가 A를 대상으로 friend선언을 해줘야 함.

 

 

※ static 멤버변수 (class 변수)

- 메모리 공간에 딱! 하나만 할당되며 모든 객체가 static 멤버변수를 공유할 수 있다.

- 단, 생성자에서 static 멤버변수를 초기화 할 수 없다! (객체 생성될 때 마다 초기화 되기 때문)

따라서 아래와 같이 static 변수 초기화 문법은 정해져 있다.

class Position{
private:
    static int num;
public:
    Position(){ num++; }
};

int Position::num = 0;  //static변수 초기화

 

※ static 멤버함수

- 선언된 클래스의 모든 객체가 공유

- public 선언 시 클래스 이름으로 호출 가능

- static 멤버함수내에서는 static 멤버변수와 static 멤버함수만 호출 가능하다.

 

※ const static 멤버

- const 멤버변수의 초기화는 initializer를 이용해야 한다.

- const static 멤버변수의 초기화는 선언과 동시에 초기화가 가능하다!

class Country {
public:
    const static int Korea = 82;
};

int main() {
    cout << "국번: " << Country::Korea << endl;
}

 

 

 

※ mutable

- const 함수 내의 값 변경을 예외적으로 허용해주는 키워드.

 

 

 

 

 

 

 

 

 

 

※ [Effective C++_item 5]: C++이 은근슬쩍 만들어 호출해버리는 함수에 집중하자!

생성자: 새로운 객체를 메모리에 만들거나 객체의 초기화를 맡는 함수

소멸자: 객체를 없앰과 동시에 메모리에서 그 객체를 사라지게 하는 함수

대입 연산자: 기존의 객체에 다른 객체의 값을 줄 때 사용하는 함수

 

직접 클래스안에 선언하지 않더라도 C++ 컴파일러가 저절로 선언해주는 멤버함수가 있다.

복사생성자, 복사대입연산자, 소멸자 이때 컴파일러가 만드는 형태는 모두 기본형이다.

이들은 모두 public 멤버이며 inline 함수이다.(Effective C++ item 30 참조)

이때, 기본클래스의 소멸자가 가상소멸자로 되어있지 않으면 비가상 소멸자로 만들어진다.(Effective C++ item 7 참조)

 

컴파일러가 만들어낸 복사생성자와 복사대입연산자의 경우, 하는 일이 매우 단순하다.

-> 원본 객체의 비정적 데이터사본 객체로 그냥 복사하는 것이 전부이다.

 

복사 생성자란 자신과 같은 클래스 타입의 다른 객체에 대한 참조(reference)를 인수로 전달받아, 그 참조를 가지고 자신을 초기화하는 방법으로 새롭게 생성되는 객체가 원본 객체와 같으면서도, 완전한 독립성을 가지게 해주며

예를 들면, Book 클래스의 복사 생성자의 원형Book(const Book&) 와 같다.

 

 

 

 

※ [Effective C++_item 6]: 컴파일러가 만든 함수가 필요없으면 이들의 사용을 금하자!

컴파일러가 생성하는 함수(모두 public): 복사생성자 / 복사 대입 연산자 / 소멸자

 

객체의 사본을 만드는 것원천 차단하기 위한 방법(2가지)

 

방법1)

1. 컴파일러 생성함수는 public이기에 이를 private 선언 해준다.

2. 멤버함수, friend선언을 통해 접근이 가능 => 정의(define)를 하지 않는다.

 

방법 2)

1. 복사생성자와 복사대입연산자 private으로 선언

2. Sale 자체가 아닌 별도의 기본 클래스에 넣고 이것으로 부터 Sale을 파생

(다만, Uncopyable클래스가 기본 클래스이기에 이 기법은 다중상속으로 갈 가능성이 존재한다.)

+ Recent posts