C++정리

아무개·2024년 9월 12일

C/C++

목록 보기
4/6

C++정리

객체란?

프로그램 동작의 주체(광범위), 메모리에 존재하고 이름으로 접근 가능한 것
객체지향?
객체를 통해 코드를 구성
장점: 코드재사용, 유지보수, 큰 규모 프로그래밍
단점: 절차지향에 비해 느린 속도, 높은 설계역량, 코드 복잡성

OOP의 개념 5가지

추상화: 객체의 속성과 기능을 추출하여 정의(유연성 & 재사용성)
캡술화: 객체의 속성과 기능을 묶음(독립성 보장)
정보은닉: 감추기(private)
상속: 객체들의 공통속성을 부각시켜 하나의 개념을 만드는 것(재사용 & 확장)
다형성(핵심): 서로 다른 객체가 같은 동작 수행명령을 받았을 때 각자의 특성에 맞는 방식으로 동작하는 것(코드간결 & 유연함)

OOP 설계원칙 5가지(SOLID)

  1. 단일 책임의 원칙(SRP, Single Responsibility Principle)
    모듈이 변경되는 이유가 1가지여야 함

  2. 개방폐쇄 원칙(Open-Closed Principle, OCP)
    확장에 열려있고 수정에 닫혀있음(추상클래스)

  3. 인터페이스 분리 원칙 (Interface segregation principle, ISP)
    목적과 용도에 적합한 인터페이스 제공

  4. 리스코프 치환 원칙 (Liskov Substitution Principle, LSP)
    하위 타입은 상위 타입을 대체할 수 있어야 한다는 것

  5. 의존 역전 원칙 (Dependency Inversion Principle, DIP)
    고수준 모듈은 저수준 모듈의 구현에 의존해서는 안 되며, 저수준 모듈이 고수준 모듈에 의존해야 한다는 것

함수 이름이 겹치는 경우

C: 이름변경
C++: namespace 사용(namespace는 변수, 구조체, 함수 등을 묶는 것)
모든 C++ 표준함수는 "std" namespace 안에 있다.
::연산자는 C++에서 가장 우선순위가 높다.

namespace 접근 방법

1. qualified name(추천) 

Video::init(); 

2. using declration 

using Video::init; 

init(); 

3. using directive 

using namespace Video; 

init(); 

global namespace: 어떠한 namespace에서도 속하지 않은 것

name mangling

C++에서는 오버로딩으로 함수 여러개만들면 컴파일러가 이름을 바꿔준다.

squre(3); 

square(3.1); 

//컴파일 후 

int square_int(int a) {} 

double square_double(double a) {} 

이런식으로 이름 바꿔줌, 그래서 C랑 C++을 섞어쓰면 문제발생 여지가 있다.

템플릿(함수를 만드는 틀)

    #include <iostream> 

    template<typename T> 

    T square(T a) { 

      return (a * a); 

    } 

    int main() { 

        int c = square<int>(2); 

        double d = square<double>(3.2); 

        std::cout << c << '\n'; 

        std::cout << d << '\n'; 

        return 0; 

    } 

template은 함수가 아니다.(square는 함수가 아니다.) 함수를 만들 겠다는 규칙이다. 함수는 square 얘가 함수다.

#include <iostream> 

typedef enum _data_t { 

	int_type, double_type 

}data_t; 

void swap(void* a, void* b, data_t t) { 

	int int_tmp = 0; 

	double double_tmp = 0; 


    if (t == double_type) { 

      double_tmp = *(double*)a; 

      *(double*)a = *(double*)b; 

      *(double*)b = double_tmp; 

    } 

    else if (t == int_type) { 

      int_tmp = *(int*)a; 

      *(int*)a = *(int*)b; 

      *(int*)b = int_tmp; 

    } 

} 

  

int main() { 

  int a = 1, b = 2; 

  double c = 3.1, d = 4.5; 

  swap(&a, &b, int_type); 

  swap(&c, &d, double_type); 


  printf("a = %d b = %d\n", a, b); 

  printf("c = %.2lf d = %.2lf\n", c, d); 

  return 0; 

} 

//C언어로 오버로딩 구현(void*이용) 

#include <iostream> 

template<typename T> 

void swap(T& a, T& b){ 

  T temp; 

  temp = a; 

  a = b; 

  b = temp; 

} 

  

int main() { 

  int a = 11; 

  int b = 22; 

  double c = 3.1, d = 4.5; 

  swap(a, b); 

  swap(c, d); 

  std::cout << "a=" << a << std::endl; // 22 

  std::cout << "b=" << b << std::endl; // 11 

  std::cout << "c=" << c << std::endl; // 4.5 

  std::cout << "d=" << d << std::endl; // 3.1 

  return (0); 

} 
//C++로 swap 템플릿 구현

레퍼런스

메모리를 사용 안함, 포인터 대비 NULL검사할 필요 없음(장점)

#include <iostream> 



void bts(int  x) { x += 1; } 

void exo(int* p) { *p += 1; } 

void ses(int& r) { r += 1; } 



int main() { 

int a = 11; 

int b = 22; 

int c = 33; 


bts(a); 

exo(&b); 

ses(c); 


std::cout << "a=" << a << std::endl; // 11 

std::cout << "b=" << b << std::endl; // 23 

std::cout << "c=" << c << std::endl; // 34 

return (0); 

} 

구조체, 클래스를 인자로 넘길 때
void C(const point_t& pt1) {} // good
기본형은 컴파일러에 따라 다르지만 포인터로 만드는 경우가 있음(메모리 사용) 따라서 call by value로 넘기자
void hot(int a) {} // good

NULL

#ifndef NULL 

  #ifdef __cplusplus 

      #define NULL 0 

  #else 

      #define NULL ((void *)0) 

  #endif 

#endif 

C++에서는 0, C에서는 (void)0이다.
C는 0을 묵시적 캐스팅으로 (void
)0으로 바꿔줌
C++에서 int pc = (int)((void*)0); 해주면 잘 돌아감

C++에서 struct, class

C++에서 struct는 디폴트가 public, class는 디폴트가 private 이 차이밖에 없음

접근지정자

private: 외부 접근 불가, 멤버변수 지정 때 사용
protected: 상속된 객체만 접근 가능
public: 내부, 외부 모두 접근
C++에서 멤버함수의 정의는 바깥에서 하는게 정석, 클래스의 이름 첫글자는 대문자

클래스 변수

객체를 만들지 않고도 Foo::sfunc1(); Foo::x = 33;형태로 사용 가능(private이 아닌 경우에만)

#include <iostream> 

class Fishbun { 

  public: 

  int cnt1; 

  static int cnt2; 

  Fishbun() : 

  cnt1{ 0 } //초기화

  { 

    ++cnt1; 

    ++cnt2; 

  } 

}; 

int Fishbun::cnt2 = 0; 

int main() { 

  Fishbun f1; 

  std::cout << f1.cnt1 << "," << Fishbun::cnt2 << std::endl; 

  
  Fishbun f2; 

  std::cout << f1.cnt1 << "," << Fishbun::cnt2 << std::endl; 


  Fishbun f3; 

  std::cout << f1.cnt1 << "," << Fishbun::cnt2 << std::endl; 

  return (0); 

} 

1 1 

1 2 

1 3 

출력됨 

클래스 변수로 인스턴스 개수 파악 가능
클래스 변수 용도: 인스턴스의 생성과 상관없이 어떤 기능을 지속적으로 하고 싶을 때 사용

멤버변수는 개별로 메모리를 차지하지만 멤버함수는 코드영역에 별도로 존재 -> 멤버변수는 인스턴스마다 독립적, 멤버함수는 모든 인스턴스가 공유

new로 메모리 할당 시 nullptr반환을 안하고 나머지 코드 수행 안함 -> 예외처리 필수 반면 C에서는 NULL반환

this

용도: 멤버변수 가리킬 때, 자기참조 넘길 때

class Point { 

  private: 

  int x{ 0 }; 

  int y{ 0 }; 

  public: 

  void set(Point* this, int a, int b) { // 자기참조 넘김 + 멤버변수 가리킴 

  this->x = a; // 멤버변수 가리킴

  this->y = b; // 멤버변수 가리킴

  } 

}; 

int main() { 

  Point pt1; 

  Point pt2; 

  pt1.set(&pt1, 11, 22); //  자기참조

  pt2.set(&pt2, 33, 44); //  자기참조

  return 0; 

} 

static함수에서 this사용 불가능(아직 객체가 없을 수 있기에)

cout 형태

std::cout<<”a”<<”b”<<”c”; 

std::cout.operator<<(”A”) 

return *this 

즉, std::cout다시 반환

#include <iostream> 
  
class Counter { 

  public: 

  int count; 

  public: 

  void reset(int count = 0) { 

  this->count = count; 

} 


Counter* increment() { 

  ++count; 

  return this; 

} 
  

Counter& decrement() { 

  --count; 

  return *this; 

} 

}; 

  
int main() { 

  Counter c; 

  c.reset(); 

  c.increment()->increment()->increment()->increment(); 


  std::cout << c.count << std::endl; 

  c.decrement().decrement().decrement(); 

  std::cout << c.count << std::endl; 


  return 0; 
} 

결과값 4 1 

Counter& --> Counter로 레퍼런스 없애주면 결과값 4 3 

setter와 getter(정보은닉)

#include <iostream> 

using namespace std; 
  

class Person { 

  private: // (1) 접근지정자가 public 

  char name[32]; 

  int age; 

  public: 

  void setName(const char* n) { 

  strcpy(name, n); 

} 
  

void setAge(int a) { 

  age = a; 

} 


char* getName() { 

  return name; 

} 
  

int getAge() { 

  return age; 

} 

}; 
  

int main(void) { 

  Person p1; 

  p1.setName("abcd"); 

  p1.setAge(33); // (1) 


  cout << p1.getName() << " " << p1.getAge() << std::endl; 

  return 0; 

} 

setter, getter을 써도 잘못된 데이터를 막을 방법은 없다.

void setAge(int a) { 

	if(a <= 100 && a >= 0) age = a; 

} 

하지만 인자를 거를 수 있다(정보은닉의 핵심), 무결성을 지킴
가급적 setter을 만들지 말고 생성자에서 초기화하자

Uniform initialization

int          a2 = { 0 }; 

struct point b2 = { 0,0 }; 

int          c2[3] = { 11,22,33 }; 

rect         r2{ 1, 2 }; 
  
  
// 괄호를 빼도 된다. 

int          a2{ 0 }; 

struct point b2 { 0, 0 }; 

int          c2[3]{ 11,22,33 }; 

rect         r2{ 1, 2 }; 
#include <iostream> 
  
class Point { 

private: 

  int x; 

  int y; 

public: 

  Point() { 

  x = 0; 

  y = 0; 

  std::cout << "Point() 생성자가 호출되었음\n"; 

  } 

}; 

  

class Rect { 

  private: 

  Point x; 

  Point y; 



  public: 

  Rect() { 

  std::cout << "Rect() 생성자가 호출되었음\r\n"; 

} 

}; 

int main() { 

  Rect rect;  

  return 0; 

} 
 
Point먼저 호출되고 그 다음 Rect호출됨 

추천하는 멤버변수 초기화 방식

class Point { 

  int x; 

  int y; 

  public: 

  Point(int a, int b): x{a}, y{b} { 

  //x= a; 필요 없어진다. 

  //y= b; 

  } 

} 

초기화가 대입보다 어셈블리어가 짧아 효율적(객체 생성하면서 초기값 전달)

디폴트생성자가 없는 타입이 멤버로 있는 경우 --> 오버로딩으로 해결

class Rect { 

  Point leftTop; 

  Point rightBottom; 

  public: 

  Rect(int x1, int y1, int x2, int y2)  

  :leftTop(x1, y1), rightBottom(x2, y2) 

  { 

  } 

}; 

클래스안에 상수와 참조가 있는 경우도 멤버 이니셜라이저가 필요
Object(int n, int&x) { :c{n}, r{x} }

복사생성자

#include <iostream> 

class Point { 

public: 

	int x; 

	int y; 


Point() :x(0), y(0) {} 

	Point(int a, int b) :x(a), y(b) {} 

}; 


int main() { 

	Point p2(1, 2); //  

	Point p4(p2);  // 기본 복사생성자 Point pt4= pt2; 도 가능 

	std::cout << p4.x << std::endl; 

	std::cout << p4.y << std::endl; 

} 



Point(const Point& other) : x{ other.x }, y{ other.y } { 

	std::cout << "복사생성자" << '\n'; 

} 

복사생성자: 인자가 하나인데 자기자신과 같은 타입을 가지는 생성자
컴파일러가 제공해주는 생성자, 모든 멤버 복사

복사생성자 호출되는 경우 3가지

복사생성자 호출되는 경우 3가지

  1. 자신과 동일한 타입의 객체로 초기화 될때
Point p2(p1); 

Point p4= p2; 
  1. Call by value 방식의 함수 호출과정에서 객체를 인자로 전달하는 경우
Person func(Person p) { 

Person p; 

	return p; 

} 

int main() { 

Person p; 

	func(p);  // 복사 생성자가 호출됨, 그런데 이렇게 쓰는것보다 레퍼런스를 쓰는게 좋다. 

} 
  1. 객체를 반환하되 참조형으로 반환하지 않는 경우
Point temp; 

	Point ses(Point& p) { //  

	return temp; // 복사 생성자가 호출된다. 

} 

Point가 Point&로 바꾸면 복사생성자 호출이 안됨

(const Point& p)이걸 인자로 받으면 복사생성자 호출 안됨

(Point) 이거는 복사생성자 호출 됨

복사생성자 문제점

#include <iostream> 

class Person { 

public: 

char* name; // string name; 

int age; 


Person(const char* n, int a) : age{ a } { 

	name = new char[strlen(n) + 1]; 

	strcpy(name, n); 

} 


Person(const Person& p) : age{ p.age } { 

	age = p.age; 

	name = p.name; 

} 



~Person() { 

	delete[] name; 

	std::cout << "소멸자 호출" << std::endl; 

} 

}; 


int main() { 

Person p1("david", 30); 

Person p2{ p1 }; // 삭제가 이미 되었는데, 또 삭제하려고 하기 때문에 얕은 복사의 문제점 발생 

return 0; 
} 

밑에 코드로 해결 가능

Person(const Person& p) : age{ p.age } { 

    //age = p.age; 

    //name = p.name; 

    name = new char[strlen(p.name) + 1]; // (1) 

    strcpy(name, p.name); 

} 

단점: 동일한 자원이 메모리에 여러번 놓임(메모리 낭비)

Reference Counting

#include <iostream> 

class Person { 

public: 

char* name; 

int age; 

static int ref_count; // static 멤버 변수 
  

Person(const Person& p) :name{ p.name }, age{ p.age } { 

    ++ref_count; 

    std::cout << "복사 생성자 " << ref_count << std::endl; 

} 

Person(const char* n, int a) : age{ a } { 

    ++ref_count; 

    std::cout << "오버로딩 생성자 " << ref_count << std::endl; 

    name = new char[strlen(n) + 1]; 

    strcpy(name, n); 

} 
  

~Person() { 

    --ref_count; 

    std::cout << "소멸자 " << ref_count << std::endl; 

    if (ref_count == 0) { 

    delete[] name; // RAII 

    std::cout << "name 삭제" << std::endl; 

} 

} 

}; 

int Person::ref_count = 0; 

int main() { 

  Person p1("david", 30); 

  Person p2{ p1 }; 

  Person p3 = p1; 


  return 0; 
} 

staic멤버변수의 특징을 이용해 0이되는순간 해제한다.

하지만 중간에 name변경해버리면 나머지 객체들도 영향을 받음

상속

컴파일러는 무조건 상위객체의 기본생성자를 호출
상위객체의 기본생성자가 없으면 에러남

class Person { 

public: 

std::string name; 

int age; 

Person(std::string s, int a) : name{ s }, age{ a } {} 

}; 

  
class Professor : public Person { 

public: 

std::string major; 

int grade; 

  

Professor(std::string major, int grade, std::string name, int age) : 

Person{ name, age }, // 상위객체 먼저 초기화

major{ major }, grade{ grade } // (2) 이후에 내 객체를 초기화 

{ 

} 

}; 

상위객체 함수 오버라이딩(virtual)

#include <iostream> 

#include <string> 

  

class Animal { 

public: 

std::string name; 

int age; 

Animal(std::string name, int age) : name{ name }, age{ age } {} 

  

virtual void cry() { // 가상함수

} 

}; 

  

class Dog : public Animal { 

public: 

Dog(std::string name, int age) : Animal{ name, age } { 

  

} 

void cry() { 

	std::cout << "멍멍" << std::endl; 

} 

}; 

  
class Cat : public Animal { 

public: 

Cat(std::string name, int age) : Animal{ name, age } { 

  
} 

void cry() { 

std::cout << "야옹" << std::endl; 

} 

}; 

int main() { 

    Dog d = { "바둑이", 1 }; 

    d.cry(); 

    Cat c = { "나비", 2 }; 

    c.cry();
 	Dog* d = new Dog("111", 1); // 스택이 아니라 힙에 만듬
	Cat* c = new Cat("222", 2); // 
	
  	Animal* a1 = d; // (1) ok --> virtual이 관건이 된다.
	Animal* a2 = c; 

	a1->cry(); // (2) Dog형이 아니다. Animal에 있는 함수만 호출할수 있다.
	// 상위 객체에 virtual이 붙어있으면, 하위 객체의 cry를 호출한다. 
	a2->cry(); 
    return (0); 

} 

업캐스팅

OOP에서는 상위객체참조에 하위객체 대입 가능(업캐스팅), 상속관계에 있어야 함
그 이유로는 하위객체는 상위객체보다 멤버변수가 같거나 많다
다운캐스팅은 보통 안된다(강제로는 가능)

업캐스팅 용도

int main() { 

    Dog d1 = { "바둑이", 1 }; 

    Cat c1 = { "나비", 2 };  

    std::vector<Animal*> animal_vect; 

    animal_vect.push_back(&d1); 

    animal_vect.push_back(&c1); 
  

for (Animal* n : animal_vect) { 

	std::cout << n->name << ","; 

} 

return (0); 

} 

Animal*벡터에 Dog, Cat 넣어서 사용 가능
문제점: 상위객체의 참조는 상위객체의 메소드만 호출 가능

추상클래스(OCP원칙)

순수 가상함수가 1개이상 있는 클래스
virtual void draw() {} // 가상 함수(virtual function)
virtual void draw() = 0; // 순수 가상함수 (pure virtual function)

추상클래스는 인스턴스화가 불가능
상속받은 애도 인스턴스화 불가능
--> 추상클래스는 상속받은 파생 클래스에게 순수 가상 함수 구현을 강제한다

#include <iostream> 

class ICamera { //추상클래스

public: 

virtual ~ICamera() {} 

virtual void take() = 0; // 순수가상함수

}; 
  

class Photograpper { 

public: 

void useCamera(ICamera* camera) {  // 특정 객체를 적지 말자 


	camera->take(); 

} 

}; 

  

  

class Camera : public ICamera { 

public: 

void take() { 

std::cout << "사진을 찍다." << std::endl; 

} 

}; 

  

  

class HDCamera : public ICamera { 

public: 

void take() { 

std::cout << "고화질 사진을 찍다." << std::endl; 

} 

}; 

  

class UHDCamera : public ICamera { 

public: 

void take() { 

std::cout << "초고화질 사진을 찍다." << std::endl; 

} 

}; 

int main() { 

    Photograpper p; 

    Camera c; 

    p.useCamera(&c); 


    HDCamera hc; 

    p.useCamera(&hc); 


    UHDCamera uhc; 

    p.useCamera(&uhc); 

    return (0); 

} 

추상클래스인 ICamera를 상속 받아서 각각의 카메라클래스에서 순수가상함수 정의 한 뒤 업캐스팅으로 사용가능해짐 -> 코드 추가 필요 X
여기서 ICamera를 인터페이스라 한다.

이 방싱이 아니면 Photograpper의 useCamera인자로 여러종류의 포인터로 받아야 함(코드 추가해야 함)

연산자 오버로딩

operator+ (p1, p2); // (1) 클래스 외부에 만드는 방법
p1.operaotr+(p2); // (2) 클래스 내부에 만드는 방법

class Point { 

private: 

	int x; 

	int y; 

public: 

	Point() : x{ 0 }, y{ 0 } {} 

	Point(int x, int y) : 

		x{ x }, y{ y } 

	{ 

	} 

}; 

Point operator+(const Point& p1, const Point& p2) { 

	Point pt{ p1.x + p1.x, p1.y + p2.y }; 

	return (pt); 

} 



int main(void) { 

	Point pt1 = { 11,22 }; 

	Point pt2 = { 22,33 }; 


	int r1 = 11 + 22; // (1) 

	Point r2 = pt1 + pt2; // (2) 주석을 풀어보자. 
  

	return (0); 

} 

private인 x, y 때문에 사용 불가능 public으로 바꿔줘야 함

그러나 멤버변수를 public으로 해놓는 것은 좋지 않다 -> getter setter 방식

class Point { 

private: // (1) private은 그대로 놔두고 

	int x; 

	int y; 

  

public: 

	Point() : x{ 0 }, y{ 0 } {} 

  

	Point(int x, int y) : 

		x{ x }, y{ y } 

	{ 

	} 

  

	int getX(void) { return this->x; } // (2) setter, getter 함수 추가 

	int getY(void) { return this->y; } 

}; 

  

Point operator+(Point& p1, Point& p2) { 

	Point tmp_pt{ p1.getX() + p2.getX(), p1.getY() + p2.getY() }; // (4) getter, setter 로 호출 

	return (tmp_pt); 

} 

  

int main(void) { 

	Point pt1 = { 11,22 }; 

	Point pt2 = { 22,33 }; 

  

	Point r2 = pt1 + pt2; 

  

	std::cout << r2.getX() << "," << r2.getY() << std::endl; 

  

	return (0); 

} 

const Point& p1의 경우 접근이 안된다 사용하면 안됨 -> 값을 바꾸지는 않더라도 가능성이 있기 떄문에 쓸 수 없음

friend

가장 좋은 방법인 friend이용 방법
operator+ 함수를 friend로 선언해주면 operator+ 함수는 Point의 private에도 접근할수 있게 된다.
friend Point operator+(const Point& op1, const Point& op2); 를 클래스 내 선언해주고

Point operator+(const Point& op1, const Point& op2) { 

	// (1) 처음에 작성했던 코드 그대로 냅두고 = const를 떼지 말고 

	// private도 그대로 두고 

	Point pt{ op1.x + op2.x, op1.y + op2.y }; 

	return (pt); 

} 

const + private 접근 가능해짐

내부? 외부?

내부에 만들면 private접근에 용이
외부에 만들면 friend선언 해 줘야함

  • 주의사항
    내부에 만들면 n(정수)+op2같은거 구현 못함 n.operator(op2)는 불가능하다

    STL

terator 쓰는 이유: 느리지만 코드를 비슷하게 만들기 가능
벡터는 앞에 넣을수 있는 함수가 없는 이유: 성능저하(뒤 요소 모두이동시켜야 함)

벡터의 사이즈

std::vector v= {1,2,3,4,5,}

v.resize(3); // 사이즈를 줄일경우 메모리 용량이 3이 되는것이 아님

// 메모리의 용량(=capacity)은 5인데 size 숫자만 3으로 줄어든다.

3짜리 만든 다음 복사해야 하기 때문에 성능저하 발생

v.shrink_to_fit()을 써야 실제로 사이즈 조절을 함 여기서 push_back하면 성능저하 발생 --> push_back은 상황에 따라 빠르거나 느리다

스레드

스레드를 쓴다고 효율이 무조건 올라가는 것은 아니다 하지만 장점은 작업을 나눠서 할 수있다.

필요성: 작업을 나눠서 빨리 하기 위해

싱글스레드는 필요 없나? 필요하다, 컨텍스트스위칭을 하지 않아 효율적

스레드 실행순서는 예측할 수 없다.

#include <iostream> 

#include <thread> 

#include <vector> 



void worker(int& counter) {

    for (int i = 0; i < 10000; i++) {

        counter += 1;

    }

}



int main() {

    int counter = 0;



    std::thread w1(worker, std::ref(counter));

    std::thread w2(worker, std::ref(counter));

    std::thread w3(worker, std::ref(counter));



    w1.join();

    w2.join();

    w3.join();



    std::cout << "counter : " << counter << std::endl;

}

항상 30000이 나오지 않음
이유: Worker가 Atomic operation(1개 cycle단위)가 아니다. 여러 클럭에 나눠 실행

m.lock(); 

counter += 1; // (1) 요기가 문제! 

m.unlock(); 

mutex로 해결 가능

sleep_for()

std::this_thread::sleep_for(std::chrono::seconds(1)); 
 

using namespace std::literals; 

std::this_thread::sleep_for(100ms); 

리터럴로 쓰는게 더 편하다.

lock

m.lock(); 

counter += 1; 

m.unlock(); 

공유 자원에 접근하기 위해서는 OS에서 제공하는 mutex에 대한 잠금을 해제하고, 사용후 잠금을 해제 해야 한다.

C++의 lock는 mutex의 잠금을 얻고, 해제하는 기능을 구현한 객체

lock_guard 

std::lock_guard<std::mutex> lock(m); 

counter += 1; 

std::unique_lock와의 차이점

unique_lock은 scope안에서 잠시 풀 수 있다.

lock은 최소한 짧게만 하는것이 중요(성능저하의 요인)

std::atomic counter = 0;

이거는 C에서 제공하는 atomic이다.

#include <iostream> 

#include <mutex>  // mutex 를 사용하기 위해 필요 

#include <thread> 

  

void worker1(int& counter, std::mutex& m) { 

    for (int i = 0; i < 100; i++) { 

        std::lock_guard<std::mutex> lock(m); 

        counter += 1; 

    } 

} 

  

void worker2(int& counter, std::mutex& m) { 

    for (int i = 0; i < 80; i++) { 

        std::lock_guard<std::mutex> lock(m); 

        counter += 2; 

    } 

} 

  

void worker3(int& counter, std::mutex& m) { 

    for (int i = 0; i < 50; i++) { 

        std::lock_guard<std::mutex> lock(m); 

        counter += 3; 

    } 

} 

  

  

int main() { 

    std::mutex m; 

    int counter = 0; 

    std::thread t1(worker1, std::ref(counter), std::ref(m)); 

    std::thread t2(worker2, std::ref(counter), std::ref(m)); 

    std::thread t3(worker3, std::ref(counter), std::ref(m)); 

    t1.join(); 

    t2.join(); 

    t3.join(); 

  

    std::cout << counter; 

} 

lock_guard를 이용해서 counter하기

예외처리

if문으로 예외처리를 하는 경우 구현하고자 하는 로직과 예외처리가 섞여서 코드가 뒤죽박죽된다.

스택풀기 (Stack Unwinding)

#include <iostream> 

  

void bts(void); 

void exo(void); 

void ses(void); 

  

void bts(void) { 

	std::cout << "bts()" << std::endl; 

	exo(); 

} 

  

void exo(void) { 

	std::cout << "exo()" << std::endl; 

	ses(); 

} 

  

void ses(void) { 

	std::cout << "ses()" << std::endl; 

	throw (123); 

} 

  

  

int main(void) { 

	try { 

		std::cout << "main()" << std::endl; 

		bts(); 

	} 

	catch (int exception) { 

		std::cout << "exception: " << exception << std::endl; 

	} 

	return 0; 

} 

STL예외 클래스

std::runtime_error : 프로그램 실행 중 발생하는 오류

std::logic_error : 논리 오류. 예를 들어, 잘못된 인자를 전달한 경우 등

std::invalid_argument : 잘못된 인수를 전달한 경우 발생하는 예외 클래스

std::out_of_range : 범위를 벗어난 인덱스나 값에 접근한 경우 발생하는 예외 클래스

std::bad_alloc : 동적 메모리 할당에 실패한 경우 발생하는 예외 클래스

변수 타입

#include <iostream> 

  

int main() { 

	double x[5] = { 1,2,3,4,5 }; 

	double d1 = x[0]; 

  

	auto d2 = x[0]; 

  

	decltype(d2) a; 

  

	return (0); 

} 

decltype은 d2의 타입으로 a 선언

람다표현식

#include <iostream> 

#include <algorithm> 

  

bool compare(int a, int b) { return (a > b); } 

  

int main(void) { 

	int x[5] = { 2,3,5,1,4 }; 

	for (int i = 0; i < 5; i++) { std::cout << x[i] << ", "; } 

	std::cout << std::endl; 

  

	std::sort(x, x + 5, [](int a, int b) { return (a > b); }); // 람다 표현식 

	//std::sort(x, x + 5, compare); // 람다 표현식 

	for (int i = 0; i < 5; i++) { std::cout << x[i] << ", "; } 

  

	return (0); 

} 

람다함수의 장점: 일반함수보다 빠름, 지역변수 캡쳐 가능

지역변수 캡쳐

#include <iostream> 

#include <algorithm>  

int main(void) { 

	int score[5] = {3,1,2,5,4}; 

	int pass = 5; 

	//std::for_each(score, score + 5, limit); 

	std::for_each(score, score + 5,  

		[pass](int a) { if (a < pass) { std::cout << a << std::endl; } } 

		); 

	return (0); 

} 

[pass]로 지역변수 접근함 == 지역변수 캡쳐

lvalue : rvalue

int main() { 

	int a = 11; // (1) a는 lvalue, 11은 rvalue 

	int& la = a; // la는 lvalue reference, a는 lvalue지만 rvalue도 될수 있다. 

	//int& ra = 22; // (2) 22는 rvalue, 레퍼런스를 만들어야 하는데, 리터럴이 올수가 없다. 

	const int& ra = 33; // 요건 가능해진다. 리터럴이라도 const로 받으면 가능해 진다.
  
  
	int a = 0; 

	a++; // lvalue, 이거는 a가 나옴 

	++a; // rvalue, 이거는 값이 나옴 

	return (0); 

} 

추가로 const는 imutable lvalue expression, ID가 있고 주소구할 수 도 있는대 대입이 안되는 특별한 lvalue

lvalue expression : rvalue expression

lvalue expression = 메모리 주소를 가지는 객체, 표현식의 결과가 ID를 가짐(주소연산자로 주소 구하기 가능)

temporary object 임시 객체: 일시적으로만 존재하고, 특정 작업이 끝나면 소멸되는 객체, 보통 rvalue라고 한다.

Fishbun a, b;

Fishbun c= a+b; // 덧셈의 결과로 임시객체가 생성되지만, 이 라인이 지나면 사라진다.

Fishbun a,b,c;

c= a+b; // 여기에서는 임시객체가 없다

rvalue expression = 메모리 주소를 가지지 않는 객체, ID가 없다, a+2와 같은 연산결과로 만들어진 임시값

#include <iostream> 

  

int main() { 

	int a = 11; 

	int& ra = a; 

	int&& rc = 456; 

  

	int d = rc; // 이제 이게 가능해 진다.(rvalue reference)

	std::cout << d << std::endl; 

	return (0); 

} 
void cook(const Mom&& mom) 

void cook(Mom&& mom) 

또한 둘의 차이는 없다. rvalue는 수정이 불가능하기 때문이다.

Move Semantics(이동 의미론)

복사를 통한 자원낭비를 줄이기 위해 객체의 소유권 이전
기존에는 rvalue참조가 불가능 했는데 && 연산자를 통해 가능해짐

-> 깊은복사는 얕은복사와 달리 가리키는 객체를 복사하고 가리키는 것(메모리 낭비)

#include <iostream> 

#include <string> 

#include <utility> 

  

int main() { 

	std::string src1 = "xxasdfasdfasdfasdfasdzz"; 

	std::string dst1 = ""; 

  

	dst1 = src1;  // (1) ok, but bad 느리다 

	std::cout << dst1 << std::endl; 

  

	std::string dst2; 

	dst2 = std::move(src1); // (2) ok, good! 빠르다, 소유권 이전 

	std::cout << dst2 << std::endl; 

	std::cout << src1 << std::endl; // 이거는 안나옴, 소유권 뺏김 

	return (0); 

} 

std::move()를 사용해 소유권 이전

이동생성자

#include <iostream> 

#include <string> 

  

class Point { 

public: 

	int x; 

	int y; 

  

	Point() : x{ 0 }, y{ 0 } { 

		std::cout << "기본 생성자" << std::endl; 

	} 

  

	Point(int x, int y) : x{ x }, y{ y } { 

		std::cout << "일반 생성자" << std::endl; 

	} 

  

	Point(const Point& other) : x{ other.x }, y{ other.y } { 

		std::cout << "복사 생성자" << std::endl; 

	} 

  

	~Point() { 

		//std::cout << "소멸자" << std::endl; 

	} 

}; 

  

int main(void) { 

	Point pt1; // (1) 기본 생성자 호출됨  

	Point pt2{ 11,22 }; // (2) 일반 생성자 호출됨 

  

	//Point pt3= pt1; // (3) 복사 생성자 호출됨 

	Point pt4(pt1); // (4) 복사 생성자 호출됨 

	Point pt5(Point(33, 44)); // (5) 일반 생성자 

	return (0); 

} 

(5)에서 일반생성자 호출되는 이유는 컴파일러가 Point(33, 44)를 pt5로 만들기 때문이다.

Point(33, 44)는 임시객체지만 참조해주는 pt5가 생겨서 사라지지 않는다.

#include <iostream> 

#include <string> 

  

class Point { 

public: 

    int x; 

    int y; 

  

    Point() : x{ 0 }, y{ 0 } { 

        std::cout << "기본 생성자" << std::endl; 

    } 

  

    Point(int x, int y) : x{ x }, y{ y } { 

        std::cout << "일반 생성자" << std::endl; 

    } 

  

    Point(const Point&) = delete; 

    Point& operator=(const Point&) = delete; 

  

    Point(const Point&& rhs) noexcept : x{ rhs.x }, y{ rhs.y } { 

        std::cout << "이동 생성자" << std::endl; 

    } 

  

    ~Point() { 

        //std::cout << "소멸자" << std::endl; 

    } 

}; 

  

  

int main(void) { 

    Point pt1; // (1) 기본 생성자 호출됨  

    Point pt2{ 11,22 }; // (2) 일반 생성자 호출됨 

  

    Point pt5 = std::move(pt1); // (3) 이동 생성자 

    return (0); 

} 

const Point&& rhs가 rvalue를 참조, noexcept는 함수에서 예외발생하지 않는 다는 의미, 만약 예외를 발생해도 예외처리되지 않는다.

스마트포인터

사용자 실수에 의한 메모리 누수를 방지, 안전한 사용을 위해 소멸자가 불리면서 메모리를 해제해주는 포인터
Scope벗어나거나 Exception이 발생하거나 delete안해줘도 메모리 해제 해줌

종류
std::unique_ptr : 가장 많이 사용
std::shared_ptr
std::weak_ptr

std::unique_ptr은 복사를 허용하지 않는다. 소유권 이전은 허용한다.
이때 사용하는 함수가 move 함수
소유권이 이전되면 원본 포인터는 nullptr, 이전된 포인터는 원본 객체를 가리키게 된다.

#include <iostream> 

class Book { 

public: 

	std::string str; 

	Book(std::string str) : str{ str } { 

		//std::cout << "생성자 호출" << std::endl; 

	} 

	~Book() { 

		//std::cout << "소멸자 호출" << std::endl; 

	} 

	std::string getTitle() { 

		return this->str; 

	} 

}; 

  

int main() { 

	Book* book = new Book("aaa"); 

  

	std::unique_ptr<Book> pBook1(book); 

	std::unique_ptr<Book> pBook2(book); 

	std::unique_ptr<Book> pBook3(book); 

  

	std::cout << pBook1->getTitle() << std::endl; 

	std::cout << pBook2->getTitle() << std::endl; 

	std::cout << pBook3->getTitle() << std::endl; 

  

	return (0); 

} 

위 코드는 뭐가 문제일까?

-> 해제한 자원을 다시 해제해 프로그램에 문제 생김

이것을 피하기 위해 make_unique을 사용

#include <iostream> 

#include <memory> 

#include <typeinfo> 

  

class Book { 

public: 

	std::string str; 

	Book(std::string str) : str{ str } { 

		std::cout << "생성자 호출" << std::endl; 

	} 

	~Book() { 

		std::cout << "소멸자 호출" << std::endl; 

	} 

	std::string getTitle() { 

		return this->str; 

	} 

}; 

  

int main() { 

	auto p1 = std::make_unique<Book>("aaa"); 

	std::cout << p1->getTitle() << std::endl; 

  

	//auto p2 = p1; // (1) 복사 불가능, 소유권이 여러개 생기기 때문이다.  

  

	auto p2 = move(p1); // (2) move로는 가능, p1의 소유권은 사라진다. 

	//std::cout << p1->getTitle() << std::endl; // (3) p1 접근 불가! 

	std::cout << p2->getTitle() << std::endl; 

  

	return (0); 

} 

인자로 넘길 때 복사가 안되니까 반드시 unique_ptr로 넘기자

#include <iostream> 

#include <memory> 

#include <typeinfo> 

  

class Book { 

public: 

	std::string str; 

	Book(std::string str) : str{ str } { 

		//std::cout << "생성자 호출" << std::endl; 

	} 

	~Book() { 

		//std::cout << "소멸자 호출" << std::endl; 

	} 

	std::string getTitle() { 

		return this->str; 

	} 

}; 

  

void printBook(Book book) { // (1) error! 안돼! 

	std::cout << book.getTitle() << std::endl; 

} 

  

void printBook(Book& book) { // (2) error! 타입이 안맞아! 

	std::cout << book.getTitle() << std::endl; 

} 

  

void printBook(std::unique_ptr<Book>& book) { // ok!  

	std::cout << book->getTitle() << std::endl; 

} 

  

int main() { 

	auto p1 = std::make_unique<Book>("aaa"); 

	//std::cout << p1->getTitle() << std::endl; 

	printBook(p1); 

  

	return (0); 

} 

unique_ptr

이 포인터는, 가리킨 객체의 유일한 소유권을 갖는다

shared_ptr

이 객체는 나 포함, 여러 포인터가 소유권을 가질 수 있다.

#include <iostream> 

#include <memory> 

#include <typeinfo> 

  

class Book { 

public: 

	std::string str; 

	Book(std::string str) : str{ str } { 

		std::cout << "생성자 호출" << std::endl; 

	} 

	~Book() { 

		std::cout << "소멸자 호출" << std::endl; 

	} 

	std::string getTitle() { 

		return this->str; 

	} 

}; 

  

int main() { 

	auto p1 = std::make_unique<Book>("aaa"); 

	std::cout << p1->getTitle() << std::endl;; 

  

	auto p2 = std::make_shared<Book>("bbb"); 

	auto p3 = p2; 

	auto p4 = p2; 

  

  

	std::cout << p2->getTitle() << std::endl;; 

	std::cout << p3->getTitle() << std::endl;; 

	std::cout << p4->getTitle() << std::endl;; 

	std::cout << p4.use_count() << std::endl;; 

  

	return (0); 

} 

참조겹침규격

typedef int& T; 

T& r1; // int& &; r1 은 int& 

T&& r2; // int & &&; r2 는 int& 

 

typedef int&& U; 

U& r3; // int && &; r3 는 int& 

U&& r4; // int && &&; r4 는 int&& 

universal reference (forwarding reference)

참조겹침규격을 이용

#include <iostream> 

  

class PiggyBox { 

private: 

    int coin = 0; 

  

public: 

    PiggyBox() {} 

    PiggyBox(int coin) : coin{ coin } {} 

  

    void setCoin(int coin) { 

        this->coin = coin; 

    } 

    int getCoin() const { 

        return coin; 

    } 

}; 

  

template <typename T> // universal reference

void run(T&& p) { 

    p.setCoin(66); 

    std::cout << p.getCoin() << std::endl; 

} 

  

int main() { 

    PiggyBox box1{ 0 }; 

    run(box1); 

    run(PiggyBox{ 0 }); 

    return (0); 

} 

run(box1)의 경우 box1은 lvalue이므로 T는 PiggyBox&로 되며 T&&는 PiggyBox&로 축약됨

std::forward(p)

std::forward는 함수 인자의 값의 성질(lvalue/rvalue)을 정확히 보존하면서 다른 함수로 전달할 때 사용하는 함수

#include <iostream> 

  

class PiggyBox { 

private: 

    int coin = 0; 

  

public: 

    PiggyBox() {} 

    PiggyBox(int coin) : coin{ coin } {} 

  

    void setCoin(int coin) { 

        this->coin = coin; 

    } 

    int getCoin() const { 

        return coin; 

    } 

}; 

  

template <typename T> 

void inner_run(T&& p) { 

    p.setCoin(77); 

    std::cout << p.getCoin() << std::endl; 

} 

  

template <typename T> 

void run(T&& p) { 

    inner_run(std::forward<T>(p)); // 값 성질을 유지

} 

  

int main() { 

    PiggyBox box1{ 0 }; 

    run(box1); 

    run(PiggyBox{ 0 }); 

    return (0); 

} 

만약

template <typename T> 

void run(T&& p) { 

  inner_run(p); // p를 그대로 전달  

} 

이거만 있다면 p에 rvalue가 오던 lvalue가 오던 inner_run에는 lvalue로 전달

이거를 왜 쓸까?

func(w); // Case 1
func(std::move(w)); // Case 2

파라미터가 rvalue, lvalue에 따라 각각 기능구현 가능해진다.

std::move() vs std::forward

move()는 무조건 rvalue변환
forward()는 rvalue일 때 rvalue 반환

profile
생각 정리

0개의 댓글