C++ 문법 정리 - 1 Basic, Constructor, Operator Overloading
C++ 언어 공부
최근 Objective-C++이나 C++나 Cmake같은 걸 좀 쓸 일도 있고 다시 공부해보고 싶어져서 몇가지 주요 문법들을 정리한다.
주된 내용은 씹어먹는 C++에서 쭉 훑어보며 정독했고 좋은 튜토리얼에 감사를 표한다.
사실 Cmake 공식문서부터 다읽으려고 했었다. 그러지 않아서 다행이다.
문법 정리
namespace
namespace myname {
...
}
namespace는 using namespace
로 최대한 생략되는 일이 없어야 한다.
참조자
상수에 대한 참조자는 불가능하다. 그러나 const
를 쓴 상수 참조자는 가능하다.
int &ref = 4; // error
const int &ref = 4; // ok
참조자의 참조자, 참조자의 배열, 참조자의 포인터는 존재할 수 없다.
new
, delete
c에서의 malloc
, free
는 c++에선 new
, delete
로 사용된다.
배열인 경우 delete[]
를 사용한다.
int* a = new int[42];
delete[] a;
Initializer List
생성자에 다음과 같이 정의한다.
class Point {
int x;
int y;
public:
Point(int a, int b) : x(a), y(b) {}
};
class C {
int &a;
const int b;
Point c;
public:
C(int &a, int b, int x, int y) : a(a), b(b), c{x, y} {}
};
꼭 써주어야 하는 상황은 다음과 같다.
- 멤버 변수가 참조자일 때
- 멤버 변수가
const
일 때 - 멤버 변수의 타입이 기본 생성자가 없는 타입일 때
이는 const
멤버변수와 참조자 멤버변수도 초기화가 가능하게 한다.
Uniform Initialization
Uniform Initialization은 c++11 부터 중괄호를 이용해 초기화를 사용할 수 있는 문법이며, 매우 간편하다.
이걸 사용하면 구조체나 클래스내의 멤버변수값들이 따로 지정하지 않아도 0
에 상응하는 값으로 초기화된다.
struct S {
int a;
int b;
}
S s1{};
S s2{1, 2};
S s3 = {1, 2};
std::initializer_list<T>
를 이용해 생성자에서 이니셜라이저 리스트를 모두 리스트 형태로 받아올 수 있다.
class A {
public:
A(std::initializer_list<int> l){
...
}
}
std::initializer_list<T>
는 다른 생성자보다 우선순위가 높으며 인자가 0개이면 호출되지 않는다.
또한, ()
을 사용한다면 이 생성자는 호출되지 않는다.
Designated Initializer
struct S {
int a;
int b;
}
S s1{ .a = 1, .b = 2 };
S s2 = { .a = 1, .b = 2 };
struct
vs class
거의 차이가 없다고 보아도 무방하다.
Swift
에선 이것이 값타입, 참조타입으로 구분되어 굉장히 다르지만 c++
에선 struct
는 기본이 모두 public
, class
는 기본이 모두 private
인 점만 기억해두어도 무방하다.
명시적인 default 생성자
class Test {
public:
Test() = default;
};
생성자 리다이렉팅
class C {
int a;
public:
C(int a): a(a) {}
C(): C(1) {}
}
const
함수, mutable
키워드
멤버 함수 뒤에 const
를 붙이면 그 함수는 이 함수내의 변수를 변경하지 않겠다는 선언이며 같은 const
멤버함수만 호출할 수 있다.
단, mutable
로 선언된 멤버 변수는 변경할 수 있다.
class C {
public:
mutable int a;
int b;
void fn() const {
a = 5;
std::cout << a;
}
};
explicit
키워드
생성자에는 explicit
을 붙일 수 있고 이는 암시적 생성자의 호출을 방지한다.
class C {
public:
std::string a;
C(const std::string &a) : a(a) {}
};
이런 것을 보면 C는 다음과 같이 초기화도 될 수 있다.
C c = string("123");
그런데 이건 함수의 인자 등에서 암시적으로 호출되거나 예기치 못한 버그가 생길 수 있는데, 이걸 막으려면 생성자에 explicit
을 붙여주면 된다.
class C {
public:
std::string a;
explicit C(const std::string &a) : a(a) {}
};
int main() {
C c = C("123");
C b("123"); // ok
return 0;
}
연산자 오버로딩
연산자 오버로딩은 두 가지로 가능하다.
- 클래스/구조체 내부에 선언하는 법
- 외부에 밖에 선언하는법
class I {
public:
int a;
I(int a) : a(a) {}
// 1
I operator+(const I &rhs);
};
// 1
I I::operator+(const I &rhs) {
return a + rhs.a;
}
// 2
I operator+(const I &lhs, const I &rhs) {
return lhs.a + rhs.a;
}
통상적으로 자기 자신을 리턴하지 않는 이항 연산자들, 예를 들어 위와 같은
+
,-
,*
,/
들은 모두 외부 함수로 선언하는 것이 원칙 입니다. 반대로 자기 자신을 리턴하는 이항 연산자들, 예를 들어+=
,-=
같은 애들은 모두 멤버 함수로 선언하는 것이 원칙 입니다.
virtual
키워드
C++에선 특이하게 A
가 B
의 부모 함수이고 A a = B();
처럼 선언이 되어도 a
에서 B
에서 오버라이딩 된 함수를 호출하면 A
의 것이 호출된다.
이것을 막기위해 virtual
키워드를 A
의 함수에 붙일 수 있다.
그런데 이것이 중요한건 포인터나 참조자일때만 가능하다.
가령
class P {
public:
virtual void fn() {
cout << 1;
}
};
class C : public P {
public:
void fn() override { // override는 optional이다.
cout << 2;
}
};
int main() {
P a = C{};
a.fn(); // 1
return 0;
}
는 slicing이 발생해서 a가 P
그 자체가 되버린다.
하지만 포인터나 참조자는 그렇지 않고 오버라이딩 된 함수를 정확히 호출한다.
int main() {
P* a = new C{};
a->fn(); // 2
return 0;
}
int main() {
C c;
P& p = c;
p.fn(); // 2
return 0;
}
Comments