※ C++의 버전변화

C++98, C++03  ==>  C++11  ==>  C++14 ...

 

C++11: 기존 C++98에서 람다 표현식을 지원한 것

C++14: 일반화된 함수반환 형식 유추를 지원한 것

 

 

※ C++11/14 문법적 변경 사항

1. 초기화 리스트 및 초기화 방법의 통합

2. 새로운 타입의 추가 : long long형 정수

3. 새로운 스마트 포인터 추가 

4. 널 포인터 상수 추가 : nullptr

5. 열거체의 범위 지정

6. 자동 타입 변환 : auto

7. 타입 변환 연산자 추가 : explicit

8. 범위 기반 for 문 추가

9. 람다 함수와 람다 표현식 추가

 

 

※ C++11/14  표준 라이브러리 변경 사항

1. 확장할 수 있는 난수 생성기의 추가 : random 헤더 파일

2. 튜플 템플릿의 추가: tuple 헤더 파일

3. 정규 표현식의 추가 : regex 헤더 파일

4. 다중 프로그래밍을 위한 스레드의 지원 : thread_local 키워드, automic 헤더 파일

5. 임베디드 프로그래밍을 위한 저수준 프로그래밍 지원

 

 

※ C++11의 move semantics

C++11에서 가장 널리 쓰이는 기능은 바로 이동 의미론이다. (move semantics)

이동의미론l-value와 r-value가 구분된다는 점에서 유용하다.

l-value: 이름, 포인터, 왼값 참조를 통해 지칭할 수 있는 객체 (주소를 취할 수 있음)

r-value:이동연산이 가능한 객체를 가리킴 (함수가 돌려준 임시객체)

class Widget {
public:
    Widget (Widget&& rhs);  // rhs의 형식은 r-value참조, rhs 자체는 l-value
};

여기서 유의할 점은 rvalue 참조 형식의 매개변수지만 매개변수자체는 lvalue이다.

Widget의 이동생성자 안에서 rhs의 주소를 취하고 있기 때문이다.

 

 

※ 복사생성자를 통해 생성된 복사본 vs 이동생성자를 통해 생성된 복사본

이 둘을 구분하는 용어는 없다. 아래 예시를 보면

대체로 rvalue 복사는 이동생성자로 생성되고 lvalue 복사는 복사생성자로 생성된다.

호출지점에서 함수에 전달하는 표현식을 argument, 인자라 하고 

인수는 함수의 매개변수, parameter를 초기화하는데 사용된다.

매개변수는 l-value이지만 매개변수의 초기화에 쓰이는 인수는 l,rvalue 둘 다 가능하다.

 

 

 

 

 

 

 

 

※  C++과 람다 표현식

람다표현식으로 만들어진 함수객체를 closure라 부른다.

람다표현식과 그로부터 생성된 클로저를 구분해야하는 경우는 드물다.

또한 함수템플릿과 템플릿함수를 구분하는 경우도 드물다. (클래스 템플릿과 템플릿 클래스도 마찬가지)

 

C++에서 함수는 크게 2가지로 나뉜다.

- 일반함수(General Function): 일반적인 반환타입을 갖는 함수

- 무명함수(Anonymous Function): 람다(Lambda)이지만 함수객체처럼 함수의 이름없이 사용가능한 함수

int main() {
    // 람다식 선언
    auto Func1 = [] {
        cout << "Hello, Lambda" << endl;
    };
    
    // 람다식 인수로 int n 선언
    auto Func2 = [](int n){
        cout << "val = " << n << endl;   
    };
    
    Func2(2);
}

람다는 일반 함수처럼 사전에 선언문과 정의문 없이 하나의 문장 || 다수의 문장으로 직접 정의하고 실행시킬 수 있다.

이런 람다는 1~2번 작업을 위한 임시적인 함수를 만드는데 적합하다.

 

 

※ 람다 만드는 법

[캡쳐절] { 본문 }
[캡쳐절] (인수) { 본문 }
[캡쳐절] (인수)->반환타입{ 본문 }
[캡쳐절] (인수) mutable(옵션) constexpr(옵션) 예외(옵션) 속성(옵션) -> 반환타입{ 본문 }

마지막은 C++17이후 추가된 람다형식이다. 이를 아래와 같이 표현할 수 있다.

[캡쳐 리스트](인수) mutable(optional) constexpr(optional) 예외속성 -> 반환데이터타입{ 본문 }

 

 

 

1. 캡쳐절 (Capture Clause)

람다는 일반함수에 없는 캡쳐절이 존재한다.

 

2. 인수 리스트 (Parameter List)

람다는 일반함수에 없는 캡쳐절이 존재한다.

 

3. mutable (옵션)

람다는 일반함수에 없는 캡쳐절이 존재한다.

 

4. constexpr (옵션)

람다는 일반함수에 없는 캡쳐절이 존재한다.

 

5. throw (옵션)

람다는 일반함수에 없는 캡쳐절이 존재한다.

 

6. Attribute(옵션)

람다는 일반함수에 없는 캡쳐절이 존재한다.

 

7. 반환 데이터 타입 (옵션)

람다는 일반함수에 없는 캡쳐절이 존재한다.

 

8. 본문

람다는 일반함수에 없는 캡쳐절이 존재한다.

 

 

※ Array (배열)

※ 1차원 배열

배열은 주로 여러 개의 동일한 자료형의 데이터를 한꺼번에 만들 때 사용된다.

예를 들어 a0, a1, a2, a3, a4, a5라는 6개의 정수형 변수를 A[6]으로 간단하게 선언할 수 있다.

 

배열의 가장 기본적인 특징은 <인덱스, 요소> 쌍의 집합으로 인덱스를 이용한 직접접근이 가능하다는 것이다.

이때, 배열과 대응되는 개념으로 연결리스트를 공부한다.

- 배열: index를 이용해서 원하는 요소에 직접접근이 가능

- 연결리스트: 맨 처음요소부터 하나씩 순차적으로 찾아가야 원하는 요소에 접근할 수 있다. (순차접근, sequential access)

 

배열의 특징은 index를 건너뛸 때 마다 배열의 자료형의 크기만큼 건너뛴다.

Ex) int arr[6]; 은 int형 배열인데, arr[0] => arr[1]로 건너뛴다면 sizeof(int) = 4byte 만큼 건너뛰게 되는 것이다.

 

단! 이때, 문자열에 대한 배열은 조심해야 한다.

문자열 선언방식은 char str[12]와 같이 하는데 이때, str은 문자열 배열의 변수이름이며

문자열의 마지막 index에는 '\0'이 들어간다! ('\0'은 ASCII값의 0을 뜻하며 즉, NULL을 의미한다.)

따라서 char str[12] = "game is over"는 ['g'] ['a'] ['m'] ['e'] [' '] ['i'] ['s'] [' '] ['o'] ['v'] ['\0'] 처럼

str[11]에 '\0' 값이 들어가 최종적으로 game is ov까지만 배열에 들어가게 된다.

 

 

 

※ 2차원 배열

1차원 배열이 여러개 모여서 이루어진 것이다.

int arr[3][4]의 경우를 해석하자면 [4]개의 1차원 배열이 [3]개 있다고 생각하면 된다.

[ ] [ ] [ ] [ ]     <=  4개짜리 배열

[ ] [ ] [ ] [ ] 

[ ] [ ] [ ] [ ]

 

 

 

 

 

※ 포인터와 참조자로 배열 나타내기

※ 1차원 배열

arr[4] = *(arr + 4)

int arr[4];       int (&ref)[4] = arr;

 

※ 2차원 배열

arr[3][4] = *(arr+3)[4]  =  *(arr[3] + 4)  =  *(*(arr+3)+4)

 

 

 

※ 매개변수로 배열의 길이 전달하기

int findMacVal(int arr[], int len){
    int max = arr[0];
    for (int i = 1; i < len; i++) {
        if (max < arr[i])
            max = arr[i];
    }
    return max;
}


int main() {
    int arr_len = 0;
    cin >> arr_len;
    int arr[arr_len];
    for (int i = 0; i < arr_len; i++) {
        cin >> arr[i];
    }

    int MaxVal = findMacVal(arr, arr_len);
    cout << MaxVal << endl;
}

- 배열의 이름이 포인터 역할을 하기에 함수 호출 시 배열의 이름을 매개변수로 전달하면 호출되는 함수에서는 이 주소를 이용해 배열의 모든 요소들을 접근할 수 있다.

- 이때, 배열의 길이도 매개변수로 함께 전달해야 한다.

 

이는 2차원 배열도 마찬가지로 2차원 배열은 가로크기가 5일 때만 적용가능한 함수가 되어버린다.

int findMacVal(int arr[][5], int h, int w){
    int max = 0;
    for (int i = 0; i < h; i++) {
        for (int j = 0; j < w; j++) {
            if (max < arr[i][j])
                max = arr[i][j];
        }
    }
    return max;
}

 

Q. 만약 배열의 길이를 모르거나 매개변수로 전달하기 싫다면 어떻게 해야하나요?

 

A. Vector 클래스를 배열대신 사용한다면 가능하다.

 

 

 

※ array container (std::array) _ 고정크기배열

- memory를 자동으로 할당하고 해제하며 원소의 타입과 배열크기를 매개변수로 사용하는 템플릿클래스이며

다음과 같이 사용할 수 있다.

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

int main(){
    array<int, 3> arr1;
    arr1[0] = 1;
    cout << arr1[0] << endl;

    array<int, 5> arr2;
    for(int i=0; i < arr2.size(); i++){
        cin >> arr2[i];
    }
    for (int i = 0; i < arr2.size(); i++) {
        cout << arr2[i] << ' ';
    }
}

 

위의 std::array에서는 배열원소에 접근할 때 [ ]를 사용했는데, arr.at() 함수를 사용해서 접근할 수도 있다.

출처) https://computer-science-student.tistory.com/80?category=1163589

arr.at() 함수를 이용한 접근은 아래와 같이 할 수 있다.

 

 

 

 

std::array를 매개변수로 하는 배열출력함수

[최대값 찾기]

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

template<size_t N>

void MaxVal(array<int, N> arr){
    int max = 0;
    for (auto element : arr) {
        if (element > max)
            max = element;
    }
    cout << max;
}


int main() {
    array<int, 5> arr = { 1, 5, 3, 8, 2 };
    MaxVal(arr);
}

 

 

※ std::array 멤버함수 정리

https://computer-science-student.tistory.com/80?category=1163589

 

 

 

 

 

 

 

 

 

 

※ STL Container

STL에서 컨테이너(container)는 같은 타입의 여러 객체를 저장하는 일종의 집합이라 할 수 있습니다.

- 컨테이너 변수 선언 시, 컨테이너에 포함할 요소의 타입 명시가 가능.

- 컨테이너에는 복사 생성과 대입을 할 수 있는 타입의 객체만을 저장가능.

- 컨테이너는 요소의 추가 및 제거 등 다양한 작업을 도와주는 여러 멤버 함수를 포함한다.

 

먼저 STL에서 컨테이너는 자료를 저장하는 방식과 관리하는 방식에 따라  3가지 형태가 있다.

1. sequence container (시퀀스 컨테이너)

2. associative container (연관 컨테이너)

3. adapter container (컨테이너 어댑터)

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

이 중에서 우리는 시퀀스 컨테이너의 vetor에 대해서 알아볼 것이다.

 

 

 

 

 

 

 

 

 

 

※ vector container (std::vector) _가변배열

C++의 STL에 내장되어 있는 템플릿 클래스로 배열과 유사하며 Java의 ArrayList와 비슷한 기능을 한다.

배열에 여러가지 기능을 추가한 것으로 기존의 배열에서 불편했던 점을 개선하여 일반화 시킨 것이다.

 

- 벡터 컨테이너는 동적배열의 클래스 템플릿이다.

- 벡터 객체는 요소가 추가,삭제될 때 마다 자동으로 메모리를 재할당해 크기를 동적으로 변경한다.

- 임의의 위치에 있는 원소 접근과, 뒤에서 원소를 추가하는 연산은 O(1)  (분할상환분석)을 보장한다.

 

- vector<x> 타입 vec이 소요하는 메모리는 대략 sizeof(vector<X>) + vec.size()*sizeof(X)이다. (약 12byte)

 

 

 

 

 

※ vector 선언방법

#include <vector>
using namespace std;

vector<자료형> 변수명(숫자)    //숫자만큼 벡터 생성 후 0으로 초기화
vector<자료형> 변수명(숫자, 변수1);   //숫자만큼 벡터 생성 후 변수1으로 모든 원소 초기화
vector<자료형> 변수명{숫자, 변수1, 변수2, 변수3, ...};    // 벡터 생성 후 오른쪽 변수 값으로 초기화
vector<자료형> 변수명[]={ {변수1, 변수2}, {변수3, 변수4}, ...}   //2차원 벡터생성 (열은 고정, 행 가변)
vector<vector <자료형>> 변수명   //2차원 벡터생성 (열, 행 가변)

ex);
vector<int> vec1; // 크기가 0인 벡터 선언
vector<int> vec2(10); // 크기가 10인 벡터 선언
vector<int> vec3(10, 3); // 크기가 10이고 모든 원소가 3으로 초기화된 벡터
vector<int> vec4 = { 1,2,3,4,5 }; // 크기가 지정한 초기값으로 이루어진 벡터
vector<int> vec5[] = { {1,2},{3,4} }; // 벡터 배열 생성(행은 가변인지만, 열은 고정)
vector<vector<int>> vec6; // 2차원 벡터 생성(행과 열 모두 가변)

 

 

 

 

 

※ vector 요소 삽입, 제거, 변경

-  insert(), push_back() 같은 삽입연산자는 많은경우 list보다 vector에 좀 더 효율적이다.

#include <vector>
using namespace std;

int main() {
    vector<int> vec; // 크기가 0인 벡터 선언

    vec.push_back(10);
    vec.push_back(20); // vec = {10, 20}

    vec.insert(vec.begin() + 1, 100);  // vec = {10, 100, 20}

    vec.pop_back();       // vec = {10,100}

    vec.emplace_back(1);   // vec = {10, 100, 1}
    vec.emplace_back(2);   // vec = {10, 100, 1, 2}
    vec.emplace(vec.begin() + 2, -50); // vec = {10, 100, -50, 1, 2}

    vec.erase(vec.begin() + 1);    // vec = {1, -50, 1, 2}
    vec.resize(6); // vec = {1, -50, 1, 2, 0, 0}
    vec.clear();   // vec = empty()
}

 

※ vector의 복사생성자와 move

- Default: push_back()은 값을 넣을 때, 복사생성자를 호출한다. 이는 insert()도 새 메모리에 복사 후 넣는다.

- 이로인해 복사생성자로 인한 오버헤드가 커지고, 성능저하가능성이 있다.

=> 그래서 push_back() insert() 대신 emplace_back() emplac() 함수를 사용한다. (생성자만 호출)

 

 

 

 

 

 

 

※ vector의 iterators

vec.begin()  벡터 시작점의 주소값 반환
vec.end()    벡터 (끝부분+1) 주소값 반환
vec.rbegin() 벡터의 끝지점을 시작점으로 반환
vec.rend() 벡터의 (시작+1)을 끝부분으로 반환

 

 

※ vector의 index 접근

vec.at(i) 벡터의 i번째 요소 접근 (범위 검사O)
vec[i] (operator [])  벡터의 i번째 요소 접근 (범위 검사X)
vec.front 벡터의 첫 번째 요소 접근
vec.rend 벡터의 마지막 요소 접근

 

 

 

 

 

 

※ vec.at(i)와 vec.[i]의 차이점

- at은 범위를 검사하여 범위 밖의 요소에 접근 시 예외처리를 발생시킨다. (std::out_of_range)

- [ ]은 범위검사를 하지 않으며 예외처리를 발생시키지 않는다. (범위 밖 요소에 접근하면 디버깅 발생)

 

vector는 효율에 중점을 두기에 보통 [ ]를 권장한다.

 

 

 

 

 

 

※ vector의 크기 size, capacity

- size: vector의 크기, 즉 벡터에 실제로 저장된 원소의 개수를 뜻한다.

- capacity: vector의 용량, 즉 벡터의 최대 할당 크기를 뜻한다.

 

만약 벡터의 크기가 용량을 초과한다면 재할당이 발생한다. (기존 값 새 메모리에 복사 후 파괴)

if (size > capacity) {
    reallocate <- 모든 값 새 메모리에 복사 후 기존 벡터 파괴
}

 

Problem 1. 새 메모리의 복사과정에서 복사생성자가 발생해 성능이 저하될 수 있다.

Solution 1. reserve()라는 함수를 사용해서 벡터의 용량을 충분히 크게 생성할 수 있다.

 

Problem 2. reserve()를 너무 크게 잡으면 벡터가 불필요하게 늘어나 메모리를 많이 차지할 수 있다.

Solution 2. clear()로 벡터의 값을 지울 때, 벡터의 요소를 삭제할 수 있다.

 

Problem 3. clear()로 벡터의 요소는 없어지지만 capacity는 그대로 남아있다.

Solution 3. 이를 해결하기 위해 swap()을 사용하는데 이는 아래와 같다.

 

#include <vector>
using namespace std;

int main() {
    vector<int> vec = { 1,2,3,4,5 };
    vec.clear();
    cout << vec.capacity() << endl;  // 5 출력
    vector<int>().swap(vec);
    cout << vec.capacity() << endl;  // 0 출력
}

 

 

 

※ vector와 string  [  vector<char>    vs   string  ]

- vector<char>: char타입의 원소로 이루어진 시퀀스

- string: 문자시퀀스를 저장하는 것이 목적, 따라서 string은 문자열을 정렬하는 경우가 거의 없다. (string의 의미성이 소멸)

 

 

 

 

※ vector는 언제 사용하는게 좋을까?

- 크기 변경이 가능할 때

- 순차 접근이 가능할 때

- 랜덥 접근이 가능할 때

 

단점) 중간 삽입과 삭제가 용이하지 않기에 삽입과 삭제가 빈번할 경우, vector보다는 list나 deque를 사용하는 것이 낫다.

크기 변경 가능 O
중간 삽입, 삭제 X
순차 접근 가능 O
랜덤 접근 가능 O

 

 

 

 

 

 

※ Array vs Vector vs Linked List

 

 

 

 

 

출처 및 참고사이트) https://computer-science-student.tistory.com/83

※자료구조 (Data Structure)와 알고리즘(Algorithm)

출처) C++로 쉽게 풀어쓴 자료구조 / 천인국.최영규 저

 

 

 

 

 

 

 

※ 자료구조 preview

 

 

 

 

 

 

※ 복사함수

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

- 복사생성자 (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클래스가 기본 클래스이기에 이 기법은 다중상속으로 갈 가능성이 존재한다.)

class가 만들어지기 전, C언어에서는 어떤 방식으로 구현을 했을까?

바로 struct를 이용한 방식이다.

이런 struct 구조체와 class에 대한 차이점은 다음과 같다.

- 구조체: struct 내부에 선언된 변수들은 public과 비슷하게 어디서든 접근이 허용된다.

- 클래스: class 내부에 선언된 변수 및 함수에 대한 접근과 관련한 접근제어 지시자가 필요하며 default값은 private이다.

 

 

※ 접근제어 지시자

- private: 클래스 내 정의된 변수 및 함수에서의 접근만 허용

- public:  어디서든 접근이 허용됨

- protected: 상속관계에서 유도클래스에서의 접근만 허용

 

※ 멤버 변수와 멤버함수

- 멤버변수: class를 구성하는 변수로 주로 private에 선언된다.

- 멤버함수: class를 구성하는 함수로 주로 public에 선언된다.

 

 

※ Class

- 클래스 선언: Car.h과 같은 헤더파일처럼 클래스가 구성하는 외형적인 틀을 보여준다.

- 클래스 정의: Car.cpp 처럼 멤버함수의 정의와 소스파일을 저장, 컴파일 되도록 한다.

 

 

※ 객체지향프로그래밍(OOP):

현실에 존재하는 사물, 대상과 연관된 행동을 실체화 하기 위한 프로그래밍

- 객체: class 타입의 변수라 생각, 클래스 기반 2가지 객체생성 방법이 있는데 아래와 같다.

ClassName objName;  // 일반적인 변수 선언 방식
ClassName *ptrObj = new ClassName;  // 동적할당 방식

이때, 동적할당 방식은 반드시 마지막에 delete ptrObj; 로 할당한 객체를 없애 memory leak을 방지해 줘야 한다.

 

 

- 생성자: 일종의 객체 멤버변수를 초기화하는 함수로 아래와 같은 성질이 있다.

1. 클래스 이름과 함수의 이름이 동일

2. 반환형 선언이 없고 실제로도 반환하지 않음

3. 생성자는 객체 생성 시 딱 한번만 호출

4. 멤버변수를 초기화 할 필요없이 생성자를 이용해 객체를 생성과 동시에 초기화 가능.

 

 

 

※ 객체생성과 함수원형선언

이때, 매개변수가 선언되어 있지 않은 생성자의 경우, Simple s0();과 같은 선언은 불가능하다.

(객체생성문인지 함수 원형 생성인지 구분 불가.)

Simple s0();  --> 함수 원형 선언

Simple s0;    --> 객체 생성 선언

 

 

 

 

 

※ OOP_정보 은닉

1. 멤버변수를 private으로 선언

2. 해당 변수에 접근하는 함수를 별도로 정의

3. 안전한 형태의 멤버변수의 접근을 유도 하는 것.

 

§ access함수: GetX, SetX처럼 멤버변수를 private으로 선언, 클래스 외부에서 멤버변수 접근을 목적으로 정의하는 함수

 

※ OOP_캡슐화(encapsulation)

- 큰 목적을 위해 둘 이상의 기능이 모여 목적을 달성하는 것으로 캡슐화가 무너지면 클래스 상호관계가 복잡해진다.

 

 

※ Member Initializer List

멤버변수로 선언된 객체의 생성자 호출 및 객체와 멤버의 초기화에 사용

이니셜라이저의 장점

 - 초기화 대상을 명확히 인식 가능

 - 성능상의 이점

 - 선언과 동시에 초기화 되는 Binary code 생성

 - const 멤버변수도 Initializer로 초기화 가능

 

※ default 생성자: 생성자를 정의하지 않은 클래스에는 default constructor가 자동삽입된다.

 

※ 객체 배열 생성: Add arr[10] 또는 Add *add = new Add[10]; 형태로 생성

- 객체 배열선언에도 생성자가 호출되며 호출할 생성자를 별도로 명시하는 것은 불가능

- 따라서 Add() {...}와 같은 생성자가 반드시 있어야 한다.

 

 

※ [Effective C++_item 4]: 객체 사용전, 반드시 그 객체를 초기화 하자!

객체가 이상한 값을 갖게 되는 상황을 방지하기 위해 모든 객체를 사용전, 반드시 항상 초기화 하자.

직접 초기화를 제외하면 C++초기화의 나머지는 생성자로 귀결된다. 

객체의 data member는 생성자의 본문이 실행되기 전초기화 되어야 한다. 따라서 member initializer list를 사용한다.

 

 

 

 

※ 소멸자: 생성자 앞에 ~를 붙인 형태로 직접 정의하지 않아도 default 소멸자가 자동 생성.

- 생성자에서 new로 할당한 메모리 공간을 소멸자에서 delete로 소멸시킨다.

 

 

 

 

※ *this 포인터 (객체 자기자신을 반환한다는 의미)

- this포인터는 객체의 주소를 저장한 포인터 변수이다.

- 매개변수와 멤버변수가 명확하게 구분될 수 있다.

 

※ [Effective C++_item 10]: 대입연산자는 *this의 참조자를 반환하도록 만들자!

※ const

const가 붙은 객체는 외부 변경을 불가능하게 해주며 다방면으로 사용된다.

클래스 내부: 정적멤버, 비정적 데이터 멤버 모두 상수로 선언 가능

클래스 외부: 전역, namespace 유효범위의 상수를 선언, static 객체 등에도 const를 붙일 수 있다.

포인터: * 뒤에 const를 붙여 상수포인터로 하거나 * 앞에 const 포인터가 가리키는 데이터를 상수로 할 수 있고 const char* const p 처럼 상수포인터, 상수데이터로도 만들 수 있다.

 

const의 가장 강력한 용도함수 선언을 할 때이다.

포인터, 반복자와 [포인터반복자참조자]가 가리키는 객체, 함수의 반환값 매개변수, 지역변수는 물론 멤버함수, 함수전체 등 여러곳에서 사용될 수 있다.

 

이 함수 내에서는 멤버변수에 저장된 값을 변경하지 않겠다는 의미로

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

const 참조자를 이용한다면 const 함수만 호출이 가능하다.

class Numclass{
private:
    int num;
public:
    int GetNum() const { return num; }
};

class NumberClass{
private:
    int number;
public:
    void initNum(const Numclass& num){
        number = num.GetNum();
    }
};

 

const 반환값은 안전성과 효율성의 증가, 에러돌발상황감소 등 여러 상황을 효과적으로 막을 수 있다.

class Rational { . . . };
const Rational operator* (const Rational& lhs, const Rational& rhs);

만약 상수객체로 되어있지 않으면 아래와 같은 상황에서 Error가 발생한다.

Rational a, b, c;
(a * b) = c;  // a*b의 결과에 operator= 을 호출한 것이 되어버린다.

 

const 멤버 함수해당멤버함수가 상수객체에 대해 호출될 함수라는 뜻으로 2가지의 중요성을 갖는다.

1. 클래스로 만들어진 객체를 변경가능한 함수와 가능하지 않은 함수 구분을 시켜준다.

2. 객체전달을 상수 객체에 대한 참조자로 진행하여 상수객체를 사용하기 위해 상수멤버함수를 준비한다.

const char& operator[] (std::size_t pos) const { return text[pos]; }

void print (const TextBlock& ctb){    // 상수객체 ctb
    cout << ctb[0];  //TextBlock::operator[]의 상수
}

 

그렇다면 어떤 멤버함수가 const라는 것은 무슨 의미일까?

1.비트수준 상수성(물리적 상수성): 어떤 멤버함수가 정적멤버를 제외한 어떤 데이터도 건드리지 않음
2.논리적 상수성: 일부 비트정도는 바꿀수 있되 그것을 사용자가 모르게만 하면 상수멤버 자격이 있음

Q. 상수멤버 및 비상수멤버 함수가 기능적으로 똑같게 구현되었다면?

A. 코드중복을 피하는것이 좋은데, 이때 비상수 버전이 상수버전을 호출하도록 해야한다.

 

 

※ const 멤버 변수와  const static 멤버변수의 초기화 차이.

- const 멤버 변수: initializer를 이용해 멤버변수(객체)를 초기화 해야 한다. 이로 인해 선언과 동시에 초기화 되는 Binary code가 생성 가능하다.

class Country {
private:
    const int KoreaNum;
public:
    Country(const int KoreaNum) : KoreaNum(KoreaNum){ }
};

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

const static int KoreaNum = 82; //Country class의 public멤버
cout << "국번" << Country::Korea;

 

 

 

 

 

 

※ constexpr (C++11)

기존의 const 보다 더 상수성에 충실하며 일반화된 상수표현식 (Generalized constant expression)을 사용할 수 있게 해줌.

변수, 함수, 생성자 함수에 대하여 일반화된 상수표현식을 통해 컴파일 타임에 평가도리 수 있도록 처리해주는 키워드이다.

C++17부터는 람다함수에서도 constexpr키워드의 사용이 가능해졌다.

 

§ 변수에서의 사용

const와 constexpr의 차이점

[const]: const변수의 초기화를 런타임까지 지연시킬 수 있다.

[constexpr]: 반드시 컴파일 타임에 초기화 되어있어야 한다.

따라서 초기화가 안되어있거나 상수가 아닌 값으로 초기화 시도하면 컴파일이 되지 않는다.

constexpr float x = 42.f;    // OK
constexpr float y { 108.f }; // OK
constexpr int i;             // error C2737: 'i': 'constexpr' 개체를 초기화해야 합니다. (uninitialized 'const i' [-fpermissive])
int j = 0;                   // error: 'int j' is not const
constexpr int k = j + 1;     // error C2131 : 식이 상수로 계산되지 않았습니다.(the value of 'j' is not usable in a constant expression)

 

 

 

§ 함수에서의 사용

constexpr을 함수반환값에 사용시 다음의 제약이 있다.

1. 함수에 constexpr을 붙일 때는 inline을 암시한다.
2. 즉, 컴파일타임에 평가하기에 inline함수들과 같이 컴파일 된다.

만약 constexr의 함수인자들이 constexpr규칙에 어긋나면 컴파일타임에 실행되지 못하고 런타임에 실행된다.

constexpr int factorial(int n) {
    // 지역 변수 없음, 하나의 반환문
    return n <= 1 ? 1 : (n * factorial(n - 1));
}

// C++11에서는 컴파일 에러 발생
// C++14부터 가능
constexpr int factorial(int n) {
    // 지역 변수
    int result = 0;

    // 여러 개의 반환문
    if (n <= 1)
        result = 1;
    else
        result = n * factorial(n - 1);

    return result;
}

 

 

 

 

 

 

 

 

 

※ C++의 입출력 연산자, [>>, <<]

C++의 입출력은 C언어처럼 printf();를 사용해도 무방하다. 다만 입출력에 대해 istream& operator >>(istream&, Coord&);와 ostream& operator<<(ostream&, const Coord&); 처럼 이미 C++의 #include<iostream>에 연산자 오버로딩이 되어 있기 때문에 std::cout << 출력하고 싶은 내용or변수 << std::endl;으로 printf(%d)와 같이 변수의 data성격에 따른 출력 타입을 고려해줄 필요가 없어졌다.

 

C와 C++의 달라진 점은 여러가지가 있는데 bool이라는 자료형 추가, 함수 및 연산자 오버로딩, 클래스, inline, Call-by-Reference 등이 있다.

 

※ C가 아닌 C++ 사용시의 장점

- classes

- inheritance (derived classes)

- strong typing

- inlining

- default arguments

 

※ C++은 언어들의 연합체로 바라봐야 한다.

C++은 여러 개의 하위 언어를 제공하며 C++의 어느 부분을 사용하느냐에 따라 programming이 달라진다.

1. C언어

2. 객체지향 개념의 C++ : class, encapsulation, inheritance, polymorphism, virtual function(Dynamic binding)

3. template C++ : 주류 C++과는 다르며 Effective C++의 item 48에서 다루고 있다.

4. STL : container, iterator, algorithm, 함수객체 등 template library를 중심으로 다룬다.

 

 

※ l-value와 r-value

l-value: 특정 메모리 위치를 가리키며 변수로 존재한다. (즉, 주소값을 취할 수 있는 연산자)

r-value: 일시적이고 주로 literal 상수이다. (주소값을 취할 수 없는 연산자)

int setValue(){ return 6; }

setValue() = 4;  // Err

setValue함수는 rvalue인 숫자 6을 반환한다.

따라서 대입연산자의 왼쪽 피연산자로 사용할 수 없기에 오류가 발생한다.

 

int global = 100;
int& setValue(){ return global; }

setValue() = 4;  // ok

여기서 setValue함수는 참조를 반환한다. 참조는 global 변수의 메모리 장소를 가리키기 때문에 lvalue로 할당할 수 있다.

 

lvalue에서 rvalue로 변환은 가능하다.

int x = 1; // l-value
int y = 2; // l-value
int z = x+y; // l-value -> r-vlaue

하지만 rvalue에서 lvalue로의 변환은 불가능하다.

 

 

 

 

 

※ 함수 오버로딩

이때, 함수 오버로딩은 C언어와 다르게 함수의 매개변수 선언형태가 다르면 동일한 이름의 함수정의 허용이 가능해 졌다.

예를 들어 매개변수가 다른것 뿐만 아니라 const선언만으로도 

int Func(int num) { return this->num; }
int Func(int num)const { return this->num; }

두 함수의 이름은 같지만 단지 const를 붙였을 뿐인데 오류가 발생하지 않는다.

 

 

 

※ 함수와 Default 값

int Func (int num = 7) {…}

int main(){
    Func()    // 7이 전달 될 것으로 간주
}

함수전달인자가 왼쪽부터 채워지기에 함수 작성시에는 오른쪽부터 Default값을 채워야 한다.

 

 

 

※ inline 함수: C의 #define 즉, 매크로 함수와 동일한 기능을 한다.

 [차이점]

  - #define: preprocessor로 compile전에 실행되며 값의 치환을 주목적으로 전처리기에 의해 실행

  - inline: 전처리가 아닌 compiler에 의한 처리과정으로 Effective C++에 의하면 주로 inline함수의 애용을 추천한다.

#define square(x) ((x)*(x))

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

위의 예시에서 두 내용은 서로 같은 의미를 내포하며 출력 시 5의 제곱은 sqaure(5)로 출력할 수 있다는 매력이 있다.

매크로 함수도 자료형 정의가 필요없다는 장점이 존재하지만 아래와 같은 치명적인 단점이 존재한다.

#define CALL_MAX(a,b) f((a) > (b) ? (a) : (b))
...
int x = 5, y = 0;

CALL_MAX(++x, y);     // a가 2번 증가
CALL_MAX(++x, y+10);  // a가 1번 증가

위의 경우, (++a>b) ? ++a : b 로 해석되기에 a가 2번 증가하게 되어 버리는 것이다.

따라서 다음과 같이 매크로의 효율은 그대로 유지하면서 타입안전성까지 취하는 inline함수에 대한 template을 사용한다.

template<typename T>
inline void callMax (const T& x, const T& y){
    f (x > y ? x : y);
}

따라서 Effective C++에서는 #define을 쓰려거든 const, enum, inline을 떠올리라는 내용을 item2에서 볼 수 있다.

 

※ [Effective C++_item 2]: #define을 쓰려면 const, enum, inline을 떠올리자 (전처리기보다 컴파일러를 가까이 하자.)

매크로는 일단 정의되면 끝날 때 까지 유효하며 컴파일러로 넘어가기 전, 전처리기가 밀어버린 후 상수로 바뀌어 버린다.

따라서 매크로는 클래스 상수 정의에도 쓸 수 없을 뿐만 아니라 캡슐화 혜택도 받지 못한다.

 

이에 대해 매크로 대신 상수를 쓰는 총 3가지의 해결책을 제시한다.

1. const 사용

#define ASPECT_RATIO 1.653 를 아래와 같이 상수로 사용하는 것이다.
const double AspectRatio = 1.653; 이는 위와 달리 사본이 사용때 마다 생기지 않고 딱 1개만 생긴다.

 

주의할 점1) 포인터와 포인터가 가리키는 대상까지 const로 선언 

const char* const name = "V2LLAIN";

주의할 점2) 클래스 멤버로 상수 정의 시, 상수의 사본개수를 1개 이하로 하고 싶을 때.

class Game {
private:
    static const int Num = 5;  //static 멤버로 만들어 상수로 선언한다.
};

 

2. enum hack을 이용한다. (const 보다는 #define에 가까운 방식, 쓸데없는 메모리 할당X)

class CostEstimate{
private:
    enum {  Num = 5  };
};

3. inline template (Effective C++_item 30)을 이용한다.

template<typename T>
inline void callMax (const T& a, const T& b) { f (a > b ? a : b); }

 

 

 

 

※ namespace:

범위지정연산자(::)를 이용해 namespace지정 시 사용하며 namespace는 변수명이 같아도 자신만의 namespace에 선언하면 충돌하지 않는다. 따라서 namespace를 중첩해서도 쓸 수 있다. (cf. ::를 변수앞에 붙이면 global variable을 의미.) 또한, std라는 namespace에 대해 using namespace std;라 선언해 std::cout을 cout이라 사용가능하다.

#include <iostream>
using namespace std;

namespace Parent{
    int num = 0;
    namespace Child{
        int num1 = 1;
        namespace GrandChild{
            int num2 = 2;
        }
    }
}

int main(){
    cout << Parent::num << endl;
    cout << Parent::Child::num1 << endl;
    
    namespace G = Parent::Child::GrandChild;
    cout << G::num2 << endl;
}

 

※ reference variables (참조변수)

참조형 변수는 Ampersand 기호, &를 사용하여 나타내는데 우리는 이런 기호를 C언어에서 사용해본 적이 있다.

바로 포인터와 관련된 내용을 배울 때 사용한 적이 있는데, 아래와 같다.

int num = 3;
int *ptr = &num;

cout << ptr;  // ptr변수로 num의 주소값인 0xc3dcbffb64 출력 
cout << *ptr; // ptr변수로 num의 값인 3 출력

이때 사용된 &는 num의 주소값을 받아온다는 의미로 사용된 &연산자이다.

 

그렇다면 참조변수의 기호 &는 무엇일까?

자신이 참조하는 변수를 대신할 수 있는 일종의 별칭으로 참조자 수에는 제한이 없다.

★배열"요소"도 변수로 간주되어 참조자 사용이 가능하다.

int arr[10];
int &ref1 = arr[1];

※배열 전체를 참조값으로 함수에 전달하기

void func (const int (&arr)[3]){
    for (int i = 0; i < 3; i++) {
        cout << arr[i];
    }
}

유의 사항: 괄호() 안에 &와 참조배열이름을 적어야 하며 [ ]안에는 반드시 배열 길이를 명시해야 한다.

 

※ 참조변수와 const

const변수를 참조하기위해서는 참조변수를 반드시 const로 선언해야 한다.

함수의 참조매개변수를 const로 선언하면 변수의 값을 변경할 수 없고 상수 또한 인자로 전달가능하다.

const int &ref = 30; //30이라는 const

 

※ Call - by - reference

참조변수로 같은 메모리 공간을 접근하면서 다른 변수의 이름으로 참조가능한 방식이다.

이로 인해 함수 밖의 메모리공간에서도 접근 및 변경이 가능하다.

변수를 매개변수로 하면  함수 호출 때 마다 메모리 할당이 발생한다.

참조변수를 매개변수로 하면  함수 호출을 해도 메모리 할당이 발생하지 않는다.

이때, 반환값은 참조형태여서 반드시 참조변수로 반환받아야 한다.

 

 

※ new와 delete

C언어의 malloc과 free를 대신해서 C++에서는 new와 delete를 사용한다.

객체 생성에서는 new를, 소멸에서는 delete를 사용하여 Heap segment에 할당, 반환을 하며 

new를 이용해 heap에 할당된 변수에 대해 참조자 선언이 가능하다.

 

이와 관련된 설명은 뒤에 객체 생성 및 소멸과 관련하여 설명하겠다.

 

 

※ Prologue _ 이름짓기(Effective C++)

포인터의 이름을 지을 때, T에 대한 포인터 (pointer to T)를 줄여서 pt라고 하는 경우가 많다.

참조자에서도 비슷하게 T에 대한 참조자 (referenece to T)를 줄여서 rf라 사용할 수 있다.

gamecharcter *pgc;
gamecharcter& rgc;

 

+ Recent posts