※ switch 문

보통 if문 대신 사용하며 case를 나눌 필요가 있을 때, if-else를 여러번 쓰기 힘들 때 사용한다.

switch (n) // n은 정수형 변수로 전달되는 인자정보, n에 지정된 값에 따라 case 영역 실행
{
    case 'M':
        // 조건 입력
        break;
    case 2:
        // 조건입력
        break;
    default:
        // else문과 비슷한 역할
}

이때, break를 하지 않으면 다음 break가 있는 case문까지 같이 실행되기에 적는 것이 좋다.

§ break와 continue

break: 가장 가까운 반복문을 탈출한다.
continue: 실행위치와 상관없이 반복문의 조건검사위치로 이동한다.
          이때, 이후부터는 continue는 생략하고 재실행한다.

 

 

 

※ while 문 vs for 문

보통 while문은 특정 종료조건이 나타나기 전까지의 지속적인 실행을 위해,

보통 for문은 반복횟수가 정해져 있는 경우에 많이 사용된다.

 

※ 비트 연산자

메모리에 할당된 정수값을 bit단위로 논리연산을 실행하기 위해 사용한다.

 

 

 

 

 

 

 

 

※ 함수 선언

[함수 return 타입] [함수 이름] (인자) {
    /*
     * 
     */
}

인자 (argument): 함수 호출 시 전달되는 "값"

매개변수(parameter): 그런 인자를 받는 "변수"

 

 

 

 

 

 

※ 지역변수와 전역변수, static과 block scope

지역변수(local variable): 중괄호에 의해 형성되는 영역안에 존재, "스택"이라는 메모리 영역에 할당
전역변수(global variable): 초기화를 하지 않으면 0으로 초기화되며 많이 사용하면 효율이 떨어진다.

 

§ 지역변수와 Block Scope

특정함수, 명령문의 블록안에 선언된 변수는 블록범위(Block Scope)밖에서는 사용이 불가능하다!

int a = 1;

{
    a = 2; // a는 2로 초기화 됨
}

{
    int a = 5; // 앞선 a와는 전혀 다른 a
    // Block scope가 끝나면 사라짐
}

{
    a++; // 여기까지 a값은 2, 아래줄 실행 시 a=3으로 증가
}

printf("%d", a); // 3 출력

 

 

§ static: "한 함수 내"에서 "지역변수의 전역변수화"를 해주는 tool

선언된 함수내에서만 접근 가능 (지역변수의 특징)
1회만 초기화, 종료전까지 메모리 공간에 저장 (전역변수 특징)
★ 전역변수와 달리 Block Scope 내부에서만 접근 가능

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

※ 입출력 방식

scanf("%d", &num); // 입력
printf("%d", num); // 출력

이때, 입력의 &num은 변수 num에 입력한 내용을 저장한다는 뜻이며

printf에서는 num에 저장된 입력값을 출력한다는 뜻이다.

 

물론 %d이외에 다른 방법으로도 입력받을 data type을 정할 수 있다.

%d // 부호가 있는 "10진수 정수" (int, short, char)
%c // 값에 대응하는 "문자" (char, int, short)
%s // "문자열" (char*)
%p // "포인터 주소값" (void*)

이때, char는 정수형임을 주의!

char는 문자형처럼 보이지만 "실제로는 char형 변수에 저장되는 것은 정수이다."

 

※ 복합대입연산자

a += b  // a = a + b
        // b가 1이면 a++로도 사용

l-value에 저장하는 것! (l-value: https://chan4im.tistory.com/11)

 

this.code(1)_ C++, based by C

※ C++의 입출력 연산자, [>>, (istream&, Coord&);와 ostream& operator (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

chan4im.tistory.com

 

 

※ 전위, 후위 증감연산자

전위연산: ++a // a값 1 증가 후 실행         [선증가, 후연산]
후위연산: a++ // 속한문장 진행 후 값 1증가   [선연산, 후증가]

즉, 후위연산의 경우 "다음문장으로 넘어가야만!" 비로소 증감연산이 실행된다.

int a = 5;
a++; // 여기서는 아직 a값은 5
printf("%d", a); // 여기서의 a값은 6이됨

 

 

※ NOT 연산자 (with if문)

a가 3의 배수라면? 과 관련된 코드이다.

if (a%3 == 0)
if (!(a%3))

위의 둘은 같은 뜻인데, if(!(a%3))의 경우 생소할 수 있다. 이에 대한 풀이는 아래와 같다,

a3의 배수일 떼 => a%3 == 0 즉, 0이 나온다.
if (!0) 즉, 0(거짓)이 아님이란 뜻으로
if (true)와 같은 뜻이다.

 

 

※ main문의 구조와 return 0; 의 의미

int main(){
    /*
     * 
     */
    return 0;
}

여기서, return 0의 의미는 다음 2가지이다.

1. 실행중인 "함수의 종료"
2. 함수 호출영역으로 "값의 반환"

cf. C언어는 boolean 타입이 없어서 0만! false, 0 이외의 값이 true로 간주된다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

- 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