01. OOP Intro
# Multi paradigm
C++은 Multi paradigm 언어이다
그 종류는 아래와 같다
1) Procedural
2) Functional
3) OOP
4) generic
# Class와 Object
OOP에서
Class는 Definition이고
이러한 Class를 통해 Object가 생성된다
아래의 예시에서는
Cat이 Class이고
kitty가 Object이다
class Cat
{
public:
void speak(int name);
void speak(float weight);
private:
int mAge;
};
int main()
{
Cat kitty;
kitty.speak();
Cat nabi;
nabi.speak();
}
# C++를 OOP중심의 언어로 만들어주는 4가지
1) Abstraction
객체의 공통적인 속성과 기능을 추출하여 정의하는 것을 추상화라고 한다
따라서
Animal Class를 abstract class라고 부르고
Cat Class를 interface라고 부른다
2) Encapsulation
외부에서 접근 가능하지 못하도록 private으로 막는 것을 의미한다
3) Inheritance
상속을 통해 상위(부모) 클래스와 하위(자식) 클래스 간의 관계가 발생한다
4) Polymorphism
1] function overloading
동일한 함수 이름을 가지고 인자가 다른 경우
2] function overriding
override와 virtual 키워드를 통해
자식 클래스에서 함수를 오버로딩하는 경우
02. Object Alignment
# 멤버변수의 Alignment 규칙
1) 각 멤버변수는 본인 사이즈의 배수의 위치에 시작해야 함
2) 가장 큰 멤버변수의 사이즈의 배수로 끝이 나야 함
아래의 Cat Class는
위의 규칙에 따라 총 24바이트를 차지한다
class Cat
{
public:
void speak();
private:
char c1; //1 byte
int i4a; //4 bytes
int i4b; //4 bytes
double d8;//8 bytes
};
# alignas
캐시라인은 일반적으로 64바이트이다
만약에,
사이즈가 큰 클래스 배열인 경우에는
캐시라인에 맞춰서 배열이 잘리다 보니
클래스의 데이터가 불균일하게 조각화가 되어
False Sharing 문제가 발생할 수 있다
따라서,
alignas(32)를 통해 클래스의 사이즈를 32로 만들어주면
캐시라인에 맞춰져서 False Sharing 문제가 발생하지 않는다
#include <iostream>
class alignas(32) Cat
{
public:
void speak();
private:
char c1; //1 byte
int i4a; //4 bytes
int i4b; //4 bytes
double d8;//8 bytes
};
int main()
{
Cat cats[100];
//prefer std::array
return 0;
}
03. Static Members
# Static의 3가지
1) static variable in a function
2) static member function
3) static member variable
# static member function
class static member function은
오브젝트의 주소를 가리키는 this와 바인딩되어있지 않다
따라서,
오브젝트를 만들지 않아도 호출이 가능하고
오브젝트와 전혀 연관이 없다
this가 없기 때문에 static member function 내에서
멤버 변수 및 멤버 함수를 사용할 수 없다
static member function도 결국 멤버함수이기에
오브젝트에서 접근이 가능하고
오브젝트 없이도 접근이 가능하다
#include <iostream>
class Cat
{
public:
static void speak()
{
//mAge = 1;// 불가능
std::cout << "meow" << std::endl;
};
private:
int mAge;
};
int main()
{
Cat::speak();// 객체 생성 없이
Cat kitty;
kitty.speak();// 객체 생성 후 멤버함수 호출
return 0;
}
# static member variable
static member variable은 프로그램이 실행되기 전에 반드시 초기화를 해줘야 한다
static member variable은 모든 오브젝트들이 static 메모리에서 하나의 공간을 공유한다
static member variable이 함수 내에서만 사용이 된다면,
멤버 변수로 가지지 않고 함수 내에 선언하면 초기화 및 안전성에서 좋다
const 키워드나 constexpr 키워드와 사용할 때는
선언과 초기화를 동시에 할 수 있다
#include <iostream>
class Cat
{
public:
void speak()
{
static int count = 0;// 함수 내에서만 사용된다면 이렇게 바로 선언 및 초기화
count++;
std::cout << count << "meow" << std::endl;
};
private:
int mAge;
//static int count;
const static int a = 0;// const 키워드를 사용하면, 선언과 초기화를 동시에 가능
constexpr static int b = 0;// constexpr 키워드를 사용하면, 선언과 초기화를 동시에 가능
};
//int Cat::count = 0;// 반드시 외부에서 초기화가 필요
int main()
{
Cat kitty;
Cat nabi;
kitty.speak();
nabi.speak();
return 0;
}
04. Member Init List
초기화를 생성자와 소멸자 구현 부분에서 하게 되면,
멤버 변수를 생성하고 재할당을 하게 된다
반면, Member Initializer List를 사용하면
멤버 변수를 생성없이 바로 초기화 하기에 메모리 낭비가 없다
#include <iostream>
class Cat
{
public:
Cat() :mAge(1) {};
Cat(int age) :mAge(age) {};
private:
int mAge;
};
class Zoo
{
public:
Zoo(int kittyAge) :mKitty(Cat(kittyAge))
{
//mKitty = Cat(kittyAge);
// 임시 오브젝트가 생성
};
private:
Cat mKitty;
};
int main()
{
Zoo cppZoo(5);
return 0;
}
05. Copy/Move Constructor
# 자동으로 생성되는 함수
클래스에서 자동으로 만들어주는 함수는 아래와 같다
1) Constructor
2) Destructor
3) Copy/Move Constructor
4) Copy/Move Assignment
# 기본 Constructor가 자동으로 생성되지 않는 경우
인자를 받는 Constructor를 우리가 직접 만들어준다면,
기본 Constructor가 자동으로 만들어지지 않는다
따라서 기본 Constructor를 반드시 직접 작성해야 하고 default 문법을 이용하는 것이 좋다
#include <iostream>
#include <string>
class Cat
{
public:
Cat() = default;
Cat(std::string name, int age) :mName{ std::move(name) }, mAge{ age }
{
std::cout << mName << " constructor" << std::endl;
};
void print()
{
std::cout << mName << " " << mAge << std::endl;
};
private:
std::string mName;
int mAge;
};
int main()
{
Cat a;// default를 해주었기 때문에 가능
Cat kitty{ "kitty",1 };
Cat nabi{ "nabi",2 };
kitty = nabi; //copy assignment
kitty = std::move(nabi); //move
return 0;
}
# 멤버 변수에 포인터
만약 멤버 변수에 포인터형이 있다면
(클래스 내부에서 포인터를 통해 리소스 관리를 Heap에서 하는 경우)
Destructor
, Copy/Move Constructor
, Copy/Move Assignment를 직접 선언해줘야 한다
- Destructor
Constructor가 오브젝트가 생성될 때, 가장 먼저 호출이 되는 것처럼
Destructor는 오브젝트가 소멸될 때, 가장 마지막에 호출이 된다
- Copy Constructor
기존 오브젝트를 Copy 해서 새로운 오브젝트를 만들 때 사용된다
- Move Constructor
기존 오브젝트를 Move 해서 새로운 오브젝트를 만들 때 사용된다
당연히 기존 오브젝트의 소유권은 모두 사라진다
#include <iostream>
#include <string>
class Cat
{
public:
Cat() = default;
Cat(std::string name, int age) :mName{ std::move(name) }, mAge{ age }
{
std::cout << mName << " constructor" << std::endl;
};
~Cat()
{
std::cout << mName << " destructor" << std::endl;
}
Cat(const Cat& other)
: mName{ other.mName }
, mAge{ other.mAge }
{
std::cout << mName << " copy constructor" << std::endl;
// 멤버 변수에 포인터가 있다면,
// std::memcpy()를 이용해서 리소스 전체를 복사
}
Cat(Cat&& other)
: mName{ std::move(other.mName) }
, mAge{ other.mAge }
{
std::cout << mName << " move constructor" << std::endl;
// 멤버 변수에 포인터가 있다면,
// mPtr = other.mPtr;
// other.mPtr = nullptr;
}
void print()
{
std::cout << mName << " " << mAge << std::endl;
};
private:
std::string mName;
int mAge;
// char * mPtr;
};
int main()
{
Cat kitty{ "kitty",1 };
Cat nabi{ "nabi",2 };
Cat kitty2{ kitty };// copy constructor 1번 방식
Cat kitty3 = kitty;// copy constructor 2번 방식
Cat kitty4{ std::move(kitty) };
return 0;
}
06. Copy/Move Assignment
# Copy Assignment와 Move Assignment
- Copy Assignment
하나의 오브젝트에 다른 오브젝트를 대입할 때, 복사가 되며 불러지는 연산자이다
- Move Assignment
하나의 오브젝트에 다른 오브젝트(R-value)를 대입할 때, 이동이 되며 불러지는 연산자이다
#include <iostream>
#include <string>
class Cat
{
public:
Cat() = default;
Cat(std::string name, int age) :mName{ std::move(name) }, mAge{ age }
{
std::cout << mName << " constructor" << std::endl;
};
~Cat() noexcept
{
std::cout << mName << " destructor" << std::endl;
}
Cat(const Cat& other)
: mName{ other.mName }
, mAge{ other.mAge }
{
std::cout << mName << " copy constructor" << std::endl;
// 멤버 변수에 포인터가 있다면,
// std::memcpy()를 이용해서 리소스 전체를 복사
}
Cat(Cat&& other) noexcept
: mName{ std::move(other.mName) }
, mAge{ other.mAge }
{
std::cout << mName << " move constructor" << std::endl;
// 멤버 변수에 포인터가 있다면,
// mPtr = other.mPtr;
// other.mPtr = nullptr;
}
Cat& operator=(const Cat& other)
{
// self assignment를 하게되면,
// 멤버 변수에 포인터가 있을 시 예기치 못한 오류가 발생할 수 있기에 작성
if (&other == this)
return *this;
mName = other.mName;
mAge = other.mAge;
std::cout << mName << " copy assignment" << std::endl;
return *this;
}
Cat& operator=(Cat&& other) noexcept
{
if (&other == this)
return *this;
mName = std::move(other.mName);
mAge = other.mAge;
std::cout << mName << " move assignment" << std::endl;
return *this;
}
void print()
{
std::cout << mName << " " << mAge << std::endl;
};
private:
std::string mName;
int mAge;
// char * mPtr;
};
int main()
{
Cat kitty{ "kitty",1 };
Cat nabi{ "nabi",2 };
kitty = kitty;// self assignment
kitty = std::move(kitty);// self assignment
kitty = nabi; // copy assignment
kitty = std::move(nabi); // move assignment
return 0;
}
# noexcept
Destructor, Move Constructor, Move Assignment는
새로운 리소스를 요청하지 않고
exception이 throw 되지 않기에 noexcept를 작성해줘야 한다
그러면 추후 컴파일러가 move가 필요할 때 noexcept라는 것을 인지하고
확실하게 move를 호출해 주게 된다
# delete
특정 함수를 막으려면 delete를 사용하면 된다
C++ 11 이전에는 delete 기능이 없어서
private을 활용했다
class Cat
{
public:
Cat(const Car& other) = delete;// delete 이용
private:
Cat();// C++ 11 이전
std::string mName;
int mAge;
};
07. Operator Overloading
# Function Overloading
함수의 이름은 같은데
인자의 개수 및 종류가 다른 함수들을 생성하는 것을 의미한다
static polymorphism이라고 불리는데
어떤 함수가 바인딩이 될지 컴파일 시 결정이 되기 때문이다
반면, dynamic polymorphism은
어떤 함수가 바인딩이 될지 런타임시 결정이 된다
# Operator Overloading
연산자를 직접 오버로딩하는 것을 의미하고
*, %, /, +=, new, delete, [], ()와 같은 다양한 연산자가 지원된다
특히, Operator Overloading은 STL와 함께
지원이 되며 유용하다
예를 들어 비교 연산자를 오버로딩하면
그 연산자를 기반으로 std::sort()가 작동한다
아래는
outstream 연산자인 "<<"를 직접 작성해 준 것이다
#include <iostream>
#include <string>
class Cat
{
public:
Cat(std::string name,int age): mName{std::move(name)},mAge{age} {};
const std::string& name() const
{
return mName;
};
int age() const
{
return mAge;
};
// void print(std::ostream& os) const
// {
// os << mName << " " << mAge << std::endl;
// };
private:
std::string mName;
int mAge;
};
std::ostream& operator<<(std::ostream& os, const Cat& c)
{
return os<< c.name() <<" " << c.age();
};
int main()
{
Cat kitty{"kitty",1};
Cat nabi{"nabi", 2};
std::cout << kitty << std::endl;
std::cout << nabi << std::endl;
// ==, < , <<
// kitty.print(std::cout);
// nabi.print(std::cout);
return 0;
}
08. Class Keywords
# const
멤버 함수 내부에서
멤버 변수의 값을 바꾸지 않는다면
해당 멤버 함수 뒤에 const를 무조건 붙이는 것이 좋다
그 이유는
컴파일러가 const를 확인하고
const 멤버 함수 구현부에서 멤버 변수를 바꾼다면
컴파일 에러로 리포팅되기 때문에 더욱 안전한 개발이 가능하기 때문이다
또한,
클래스를 const로 생성했다면
해당 클래스는 const 멤버 함수만 호출이 가능하고
일반적인 멤버 함수는 호출이 불가능하며 에러가 발생한다
mutable이 붙은 멤버 변수는
const가 붙은 멤버 함수에서 마음대로 변경이 가능하다
그렇지만 const를 사용하는 이유가 없어지기에
mutable은 가급적 사용을 하지 않는 것이 좋다
#include <iostream>
#include <string>
class Cat
{
public:
void age(int age)
{
mAge = age;
}
int age() const
{
// mAge = 100;// 에러 발생
mHp = 100;// mutable 변수는 const가 붙은 함수에서도 변경 가능
return mAge;
}
void initAge()
{
mAge = 100;
}
private:
int mAge;
std::string mName;
mutable int mHp;
};
int main()
{
const Cat kitty{ "kitty" };
kitty.age();
//kitty.initAge();// 호출 불가
return 0;
}
# explicit
constructor에서 implicit conversion이 일어나는 것이 기본적이다
implicit conversion을 막기 위해서는
explicit 키워드를 이용하면 된다
특히,
constructor의 argument를 하나일 때는
implicit conversion을 막기 위해서 explicit 키워드를 반드시 작성하는 것이 좋다
#include <iostream>
#include <string>
class Cat
{
public:
explicit Cat(int age)
: mAge(age)
{
};
private:
int mAge;
};
int main()
{
//Cat kitty = 3;// implicit conversion을 막는 explicit 키워드로 인해 에러
Cat kitty{3};
return 0;
}
# friend
private 함수를 호출할 수 있는 클래스를 지정하는 키워드이다
OOP의 콘셉트를 파괴할 수 있기 때문에 가급적 사용을 피하는 것이 좋다
# volatile
variable optimization을 꺼주는 키워드이다
# inline
함수의 내용을 그대로 가져다가 함수 caller 부분에 넣는 키워드이다
# constexpr
컴파일 시간에 해당 키워드 변수들을 모두 값으로 지정한다
(반면, const는 컴파일 or 런타임 시간에 지정한다)
# Encapsulation Interface
값이 작을 때는 return by value를 해도 되지만
값이 클 때는 return by const ref가 더 효율적이다
멤버 함수 앞에 const를 붙이는 것은
해당 반환값을 수정하지 못하도록 하는 Encapsulation Inteface에서
사용한다
#include <iostream>
#include <string>
class Cat
{
public:
int age() const// 값이 작은 경우 return by value
{
return mAge;
}
const std::string& name() const// 값이 큰 경우 return by const ref
{
return mName;
}
private:
int mAge;
std::string mName;
};
int main()
{
const Cat kitty{ "kitty" };
std::string name = kitty.name();// deep copy
const std::string& nameRef = kitty.name();//no deep copy
return 0;
}
'C++ > [노코프] C++' 카테고리의 다른 글
[C++ NOTE] Smart Pointer (0) | 2024.03.29 |
---|---|
[C++ NOTE] Inheritance (0) | 2024.03.28 |
[C++ NOTE] Resource Move (0) | 2024.03.26 |
[C++ NOTE] Compile Process (0) | 2024.03.25 |
[C++ NOTE] Memory Structure (0) | 2024.03.24 |