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} {}  
};

꼭 써주어야 하는 상황은 다음과 같다.

  1. 멤버 변수가 참조자일 때
  2. 멤버 변수가 const일 때
  3. 멤버 변수의 타입이 기본 생성자가 없는 타입일 때

이는 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;  
}

연산자 오버로딩

연산자 오버로딩은 두 가지로 가능하다.

  1. 클래스/구조체 내부에 선언하는 법
  2. 외부에 밖에 선언하는법
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++에선 특이하게 AB의 부모 함수이고 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;  
}

Categories:

Updated:

Comments