Modern C++ Summary (vs. C#)

w99hyun·2025년 9월 9일


개인적으로 공부하면서, C#과 같이 사용하면서 헷갈릴 수 있는 부분(C++과 다른 점)과 C++에서 특정 버전 이상에서만 지원하는 문법을 정리한다.

C++ 버전별 주요 특징

- typedef

typedef double my_type_t;
== using my_type_t = double; (C++11 이상)

- enum

enum { };
enum class { }; (C++11 이상)

//c#의 enum과 같이 사용하려면 enum class를 사용한다.
//enum은 스코프를 사용하지 않지만 enum class에서는 C#과 마찬가지로 스코프를 사용한다.

- string

//c++의 string은 std 라이브러리에 포함된다.

//cin으로 문자열을 온전히 받기 위해선
std::getline(std::cin, {변수명})
//이 사용된다.

//cin으로 입력받은 버퍼를 비우기 위해
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
== std::cin.ignore(32767, '\n');
//사용이 필요하다.

- 코드에서 긴급한 탈출 (halt)

HALT → exit(0);

- 랜덤 난수 생성

#include <cstdlib>

std::srand(5323); //5323 = seed → 시드 넘버 지정
std::srand(static_cast<unsigned int>(std::time(0)); // 시간과 연동하여 시드가 계속 변경됨
std::rand();
#include <random> (C++11 이상)

std::random_device rd;
std::mt19937_64 mesenne(rd()); // or mt19937
std::uniform_int_distribution<> dice(1 ,6); // 1~6까지 같은 확률

- cin 활용

# 버퍼 지우기
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
== std::cin.ignore(32767, '\n');

# 입력 범위 문제 확인하기 (둘이 같이 사용됨.)
std::cin.fail(); // return true / false
std::cin.clear(); // 내부 상태 플래그 초기화

- 배열과 포인터

배열을 정의하면 배열의 이름 자체가 주소가 된다.
다만 배열을 함수의 인자로 넘겨, 매개변수로 배열을 받는다면 이 매개변수는 '포인터'로 취급된다.
때문에 매개변수로 받은 배열의 이름(포인터)은 배열의 주소를 저장하는 다른 주소가 된다.
→ 함수에서 이 (배열로 보이지만 포인터 변수인)배열의 sizeof를 찍으면 4바이트(32비트, 64비트에서는 8바이트)로 출력된다. 

- 포인터를 그냥 선언해서는 변수값을 담을 수 없다.
> char *name = "nolda"; (X)
변수값을 담기 위해선 const 선언을 넣으면 사용할 수 있다.
> const char *name = "nolda"; (O)

//참고
(*this).memberValue;
== this->memberValue;

- foreach

C#의 foreach와 사용법은 비슷하지만 문법의 차이가 존재한다.

for (int value : array)
{
}

- 메모리 할당

정적으로 할당된 메모리는 stack에 저장되며, stack은 용량이 작고 컴파일 타임에 크기가 결정된다.
동적으로 할당된 메모리는 Heap에 할당된다. Heap 영역은 런타임에 결정된다.

- Stack / Heap

Memory의 Segment
1. Code - Program
2. BSS - uninitialized data
3. Data - initialized data
4. Stack
 - 로컬 변수, 메인 함수, 메소드 등이 스택으로 쌓임
 - 사이즈가 작기 때문에 Stack Overflow 발생 가능
5. Heap
 - 동적 할당일 경우 Heap 영역에 저장됨

- const와 포인터

int value = 5;
const int *ptr1 = &value; //ptr1에 저장돼있는 주소를 바꾸는건 가능하지만, 주소가 가리키는 value 값을 바꾸는건 안됨
int *const ptr2 = &value; //ptr2에 저장돼있는 주소 값 바꾸는게 안됨
const int *const ptr3 = &value; //주소 값도 바꿀 수 없고 de-referencing으로 값을 바꿀 수도 없음 (다 안됨)

- std::vector

#include <vector>
std::vector<int> array_value;

동적할당 배열에 유용하게 쓰이고 널리 쓰임. C#의 List와 비슷하다.
//size : 사용하는 용량 -> .resize()
//capacity : 총 용량(size에서 가려진 총 용량) -> .reserve

//vector를 stack처럼 사용하기
.push_back();, .pop_back()

- std::tuple

여러 개의 반환 값을 return 시킬 수 있다.

#include <tuple>

std::tuple<int, double> getTuple()
{
	return std::make_tuple(5, 3.14);
}

int main()
{
	std::tuple<int, double> tp = getTuple();
    std::get<0>(tp); //int 값
    std::get<1>(tp); //double 값
    
    //C++17 이상에서는 아래가 가능하다.
    auto[a, b] = getTuple();
}

- 함수포인터

int func() { return 5; }
int func2() { return 9; }

//함수포인터 변수 선언
int(*fcnptr)() = func;
-> int(*변수이름)(매개변수)

//다른 함수 할당
fcnptr = func2;

//functional Library (C++11 이상)
#include <functional>

std::functional<리턴타입(매개변수)> fcnptr = func;

- 일립시스 (Ellipsis)

//매개변수의 갯수제한을 두지 않고 받는 방법

double findAverage(int count, ...) //count : 매개변수 갯수
{
	double sum = 0;
    va_list list;
    var_start(list, count);
    
    for (int arg = 0; arg < count; ++arg)
    {
    	sum += va_arg(list, int);
    }
    
    var_end(list);
    
    return sum / count;
}

- 연쇄호출(Chaining)

class 내부 함수의 리턴을 class 자신의 reference 값으로 설정하면,
연쇄적으로 호출이 가능하다.

class Calc
{
	int m_value;
    
    Calc& Add(int value) { m_value += value; return *this; }
    Calc& Sub(int value) { m_value -= value; return *this; }
};

int main()
{
	Calc cal(10);
    cal.Add(10).Sub(20).Add(10);
}

- friends keyword

class에서 다른 함수의 선언부를 가져와 friend 선언을 해주면 해당 함수에서는 선언된 class의 private 멤버에 접근할 수 있다.

각자 다른 class에서 공통 함수에 friend를 선언할 경우 전방선언이 필요할 수 있음.

순서에 의해 friend 함수에서 멤버 변수 등을 인식하지 못하면, class에는 선언부만 남겨두고,
인식하지 못한 class의 하단에 구현부를 넣어준다.

- 익명 변수

class A
{
	void print()
    {
    	cout << "A Print" << endl;
    }
};

int main()
{
	A().print();
    A().print(); //위의 객체와 다르기 때문에 생성자와 소멸자가 각각 호출됨.
}

- 연산자 오버로딩

//산술 연산자
class Cents
{
private:
	int m_cents;
public:
	int& getCents() { return m_cents; }
    
	Cents operator + (const Cents &c2) //멤버 함수
    {
    	return Cents(this->m_cents + c2.m_cents);
    }
    
    friend Cents operator + (const Cents &c1, const Cents &c2) //friend 함수
    {
    	return Cents(c1.getCents() + c2.getCents());
    }
};
//입출력 연산자
class Point
{
private:
    double m_x, m_y, m_z;

public:
    Point(double x = 0.0, double y = 0.0, double z = 0.0)
        : m_x(x), m_y(y), m_z(z)
    { }

	//outstream
    friend std::ostream& operator << (std::ostream &out, const Point &point)
    {
        out <<<< point.m_x << ", " << point.m_y << ", " << point.m_z;

        return out;
    }
	
    //instream
    friend std::istream& operator >> (std::istream& in, Point& point)
    {
        in >> point.m_x >> point.m_y >> point.m_z;

        return in;
    }
};

int main()
{
    Point p1(0.0, 0.1, 0.2), p2(3.4, 1.5, 2.0);
    Point p3, p4;

    cout << p1 << " " << p2 << endl;
    cin >> p3 >> p4;
}
//단항 연산자
Cents operator - () const
{
	return Cents(-m_cents);
}

bool operator ! () const
{
	return (m_cents == 0 ? true : false);
}
//비교 연산자

friend bool operator == (const Cents& c1, const Cents& c2)
{
    return c1.m_cents == c2.m_cents;
}

friend bool operator < (const Cents& c1, const Cents& c2)
{
    return c1.m_cents < c2.m_cents;
}
//증감 연산자
Cents& operator ++ () //prefix
{
    ++m_cents;
    return *this;
}

Cents& operator ++ (int) //postfix
{
    Cents temp(m_cents);
    ++(*this);
    return *temp;
}
//첨자 연산자 []
class IntList
{
private:
    int m_list[10];
public:
    int& operator [] (const int index)
    {
        return m_list[index];
    }
    const int& operator [] (const int index) const
    {
        return m_list[index];
    }
};

int main()
{
    IntList list;

    list[3] = 10;
    
    //if
    IntList *list = new IntList;
    
    list[3] = 10; //X
    (*list)[3] = 10; //O
}
//형변환

operator int()
{
	return m_cents;
}

- explicit / delete

class Fraction
{
	int m_numerator;
    int m_denominator;
    
    Fraction(char) = delete; //사용 못하게 막음
    
	(explicit) Fraction(int num = 0, int den = 1)
    	: m_numerator(num), m_denominator(den)
    { }
};
void doSomething(Fraction frac)
{
	cout << frac << endl;
}

int main()
{
	doSomething(7); //원래는 컴파일러에서 Fraction(7)처럼 변환해줌
    				//만약 Fraction 생성자에 explicit 키워드가 앞에 붙는다면 불가능해짐
}

- class 깊은 복사 주의점

//class의 멤버변수로 char *m_data = nullptr;가 정의되어 있을 때,
//이 class를 기본 복사 생성자를 통해 복사하면 새로운 class instance에도 같은 포인터 주소를 가리킨다.
//이 때, 새로운 class instance가 삭제되면, 소멸자에서 해당 포인터 변수가 가리키는 값을 지워버리게 되고,
//원본의 데이터까지 지워버리는 상황이 발생한다. (얕은 복사)
//이 때문에, 깊은 복사를 위해선 별도의 복사 생성자를 정의해줘야 한다.

MyString(const MyString &source) //복사 생성자 (깊은 복사)
{
	m_length = source.m_length;
    
    if (source.m_data != nullptr)
    {
    	m_data = new char[m_length];
        for (int i = 0; i < m_length; ++i)
        	m_data[i] = source.m_data[i];
    }
    else
    	m_data = nullptr;
	}
}

//operator = (대입 연산자) 의 경우에도 비슷하게 정의해줄 수 있음.
MyString& operator = (const MyString &source)
{
	if (this == &source)
    	return *this;
    
    delete[] m_data; //기존에 갖고있던 메모리 할당 해제
    
    m_length = source.m_length;
    
    if (source.m_data != nullptr)
    {
    	m_data = new char[m_length];
        for (int i = 0; i < m_length; ++i)
        	m_data[i] = source.m_data[i];
    }
    else
    	m_data = nullptr;
	}
}

//std::string을 사용하면 필요 없는 일이다.

- Initializer List 생성자

IntArray(unsigned length)
	: m_length(length)
{
	m_data = new int[length];
}

IntArray(const std::initializer_list<int> &list)
	: IntArray(list.size())
{
	int count = 0;
    for (auto & element : list)
    {
    	m_data[count] = element;
        ++count;
    }
}

- 상속 관련 Keyword

//virtual
상속 구조에서 부모 클래스의 메소드에 virtual 키워드를 사용시
자식 클래스의 객체를 부모 클래스의 포인터에 넣어서 호출해도
자식클래스의 메소드가 호출됨.
부모 클래스의 함수를 자식 클래스에서 오버라이딩한 것으로 인식

- 가상 소멸자
부모 클래스 객체에 자식 클래스를 넣을 경우,
자식 클래스의 동적 할당된 메모리를 지우기 위해 소멸자에도 
virtual 키워드를 붙여주면 자식 클래스의 소멸자도 실행됨.

//override
상속 구조에서 자식 클래스의 함수 매개변수 끝에 override 키워드 작성시
오버로딩이 아닌 오버라이드를 의도한것이라고 인식하게 하는 키워드

//final
final 키워드 사용시 자식 클래스에서 더 이상 오버라이딩할 수 없음

- 다이아몬드 상속 문제

A라는 부모클래스를 통해 B와 C클래스를 상속받아 생성할 때,
상속 접근제한자에 virtual을 붙여주지 않으면
B와 C클래스 각각 다른 A클래스를 상속받는 문제가 발생할 수 있음.

class B : virtual public A
와 같이 상속받아야 함.

- Object Slicing

부모 클래스로부터 상속받아 생성된 자식클래스에 새로운 변수가 있는데,
부모 클래스에 자식 클래스 인스턴스를 넣어버린 경우 데이터 슬라이싱 발생

std::vector를 사용하는 경우 
std::vector<std::reference_wrapper<부모 클래스>> vec;
을 사용할 수 있다.

- dynamic cast

Derived d1;
Base *base = &d1;

auto *base_to_d1 = dynamic_cast<Derived1*>(base);
Base로 형변환 됐던 변수를 다시 Derived로 형변환

dynamic_cast의 경우 에러 체크를 통해 에러일 경우 nullptr 반환함. (안전한 형변환)
static_cast는 에러 체크를 하지 않음.

- Template

//함수 템플릿

template<typename T>
T getMax(T x, T y)
{
	return (x > y) ? x : y;
}
//클래스 템플릿

<*.h>
template<typename T>
class MyArray
{
private:
	int m_length;
    T *m_data;
    
public:
	MyArray(int length)
    {
    	m_lenth = length;
        m_data = new T [length];
    }
    
    void print();
}
...

<*.cpp>
template<typename T>
void MyArray<T>::print()
{
	...
}

template void MyArray<char>;
//템플릿 클래스의 멤버함수의 구현부를 cpp 파일로 옮길 경우
explicit instantiation 필요
//

0개의 댓글