Modern C++

728x90

Member Initializer List (멤버 초기화 리스트)를 알아보기 전 먼저 Class의 Constructor (생성자), Destructor (소멸자)에 대해서 얘기해보자. 

 

Class Dog
{
public:
    Dog()
    {
    	//std::cout << "Constructor" << std::endl;
    }
    ~Dog()
    {
    	//std::cout << "Destructor" << std::endl;
    }
private:    
};

Dog라는 Class를 생성했다. 위와 같이 클래스의 이름과 멤버 함수의 이름이 동일한 것을 볼 수 있다. Constructor는 클래스의 이름을 함수 이름으로 정의해주면 되고 Destructor의 경우 '~'를 붙여 정의해주면 된다. 

 

 

Member Initializer List

먼저 멤버 초기화 리스트를 이용하지 않고 클래스를 구성해보면 아래와 같다. 

Class Dog
{
public:
    Dog(int age)
    {
    	mAge = age;
    }
    ~Dog()
    {
    	//std::cout << "Destructor" << std::endl;
    }
private:
	int mAge;
};

필자도 원래 항상 이런 식으로 코드를 짜 왔다. 멤버 초기화 리스트는 기존 함수명 괄호 뒤에 세미콜론 :으로 표기한다. 그다음 초기화하고자 하는 변수들을 쉼표로 구분하고 괄호를 이용하여 초기화를 진행한다. 

Class Dog
{
public:
    Dog(int age) : mAge(age) {};
    ~Dog()
    {
    	//std::cout << "Destructor" << std::endl;
    }
private:
	int mAge;
};

멤버 초기화 리스트를 하면 코드가 간략해지는 것 의외의 차이점을 못 찾을 수 있지만 아래의 예시를 보면 왜 멤버 초기화 리스트를 사용하는지 알 수 있다. 


#include <iostream>

class Dog
{
public:
    Dog() : mAge{ 1 } {};
    Dog(int age) : mAge{ age } {};
	
private:
    int mAge;
};

class Zoo
{
public:
     //Zoo(int dogAge) : mong(Dog(dogAge)) {};
    Zoo(int dogAge)
    {
        mong = Dog(dogAge);
    }
private:
    Dog mong;
};

int main()
{
    Zoo cppZoo(5);

    return 0;
}

Zoo라는 클래스를 추가로 생성하여 Zoo클래스에 Dog객체가 생성되도록 만들었다. Zoo의 생성자를 보게 되면 주석 처리된 코드는 Member Initializer List를 이용하여 객체의 생성자를 호출하고 주석이 아닌 Zoo생성자는 일반적으로 대입 연산자를 통해 Zoo클래스의 mong객체를 초기화한다. 

 

위 코드를 어셈블리어로 확인을 하게 되면 둘의 차이를 명백하게 확인할 수 있다. 

*참고로 필자는 아직 어셈블리어를 이해하지 못하고 어셈블리 코드를 확인하기 위해 Compiler explorer를 이용했다. 

 

대입 연산자를 통해 mong객체를 초기화하는 코드를 어셈블리어 변환한 모습이다. 우측 어셈블리 코드를 확인해 보면 Zoo::Zoo(int)에 생성자 코드를 보게 되면 먼저 Dog::Dog()인 기본 Dog생성자가 1번 호출이 되고 Dog::Dog(int) 생성자가 한번 더 호출이 되게 된다. 이 뜻은 먼저 임시 객체가 생성이 되고 그 객체에 dogAge의 값으로 초기화가 되 후에 mong객체에 대입 (mov키워드) 이 되는 방식이다. 하지만 대입 초기화 연산자를 이용할 경우는 아래와 같다. 

똑같이 우측 어셈블리 코드를 보면 Zoo::Zoo(int)에서 Dog::Dog(int) 생성자가 한번 호출이 되고 그다음 mov키워드가 없는 것을 확인할 수 있다. 이러한 이유 때문에 메모리 관점에서 효율적인 코드를 짜기 위해서 Member initializer list를 사용하는 것이다. 

728x90

본 포스팅에서 다룰 내용은 static member function과 static member variable이다. 정적 멤버 함수와 정적 멤버 변수이다. 여기서 얘기하는 static은 static link와 별개의 내용이다. 

 

Static Member Function

 

static member variable은 정적 멤버 변수이다. 정적 멤버 함수란 오브젝트가 없이도 호출이 가능한 함수이다. 일반 멤버 함수의 경우는 오브젝트를 생성 후 해당 오브젝트가 가지고 있는 주소 값에 의존하여 멤버 함수를 호출하는 방식이다. 하지만 static member function은 아니다. 

#include <iostream>

class Dog
{
public:
	void spaek()
	{
		std::cout << "woof" << '\n';
	}
	static void staticSpeak()
	{
		std::cout << "WOOF!" << '\n';
		//std::cout << mAge << '\n';	불가능!
		//spaek(); 불가능!
	}
private:
	
	int mAge;
};


int main()
{
	Dog choco;

	choco.spaek();
	
	Dog::staticSpeak();		//객체
	choco.staticSpeak();	//오브젝트의 멤버 함수이기 때문에 call가능
}

위 코드에서 Dog라는 클래스에 static member function인 staticSpeak() 함수가 존재한다. 해당 함수는 main함수에서 객체를 생성하지 않고 바로 호출이 가능하다. Dog::staticSpeak()와 같은 형식으로 호출이 가능하다. 여기서 추가적으로 staticSpeak()는 클래스의 멤버 함수이기도 하기 때문에 오브젝트를 이용하여 호출 또한 가능하다. choco.staticSpeak();

 

static member function의 특징으론 해당 함수는 객체의 주소 값을 가지고 있지 않기 때문에 static member function내에서 멤버 변수 혹은 멤버 함수를 호출하는 것이 불가능하다. 왜냐하면 기본적으로 멤버 변수와 멤버 함수는 this초인터를 이용하여 호출이 진행이 되는데 위에 말했다시피 static member function은 해당 주소 값을 가지고 있지 않게 때문이다.

 

 

Static Member Variable

 

static member variable은 정적 멤버 변수이다. 해당 변수는 오브젝트 안에 존재하지 않고 static 메모리 영역에 존재하게 된다. 그 뜻은 하나의 변수를 공유하게 된다는 뜻이다. 

#include <iostream>

class Dog
{
public:
	static int count;
	void spaek()
	{
		++count;
		std::cout << "woof" << '\n';
		std::cout << "count : " << count << '\n';
	}
private:
	int mAge;
};

int Dog::count = 0;

int main()
{
	Dog choco;
	Dog mong;

	choco.spaek();
	mong.spaek();
	
}

 

클래스를 먼저 보면 static int count static member variable이 public에 선언이 되어있다. static member variable은 프로그램이 실행되기 전에 초기화가 이루어져야 한다. main함수에서 두 객체 choco와 mong의 멤버 함수 speak() 호출을 통해 count를 증가시키고 count를 출력한 결과 mong.spaek()에서 count가 2가 된 것을 확인할 수 있었다. 그 이유는 아까 설명한 대로 static 메모리에 저장이 되어 객체 간 하나의 변수를 공유하기 때문이다. 

 

위 코드에서 문제가 될 수 있는 점이 있다. 바로 static member variable이 public에 선언이 되었기 때문에 객체를 이용해서 해당 변수를 manipulate 할 수 있다는 점이 문제가 될 수 있다. 이 점을 해결하기 위해 private에 옮겨 문제를 해결하는 방법이 있지만 더 나은 해결 방안은 현재 코드에서 해당 변수를 speak() 멤버 함수에서만 사용이 되기 때문에 speak() 멤버 함수 안에 선언하는 방법 또한 안전한 방법이다. 

#include <iostream>

class Dog
{
public:
	void spaek()
	{
		static int count = 0;		//컴파일할때 한번 선언됨.
		++count;
		std::cout << "woof" << '\n';
		std::cout << "count : " << count << '\n';
	}
private:
	int mAge;
};


int main()
{
	Dog choco;
	Dog mong;

	choco.spaek();
	mong.spaek();
}

처음에 speak() 안에 선언하면 선언할떄마다 해당 정적 멤버 변수가 새로 선언이 되는 것이 아닌가라는 생각을 하게 되었는데 잘 생각해보니 해당 변수는 컴파일 타임에 선언이 되기 때문에 런타임에는 해당 선언을 무시하게 된다. 

 

 

Ref.

https://www.youtube.com/watch?v=oD6fKjyX5to&t=242s

728x90

C++ 에는 OOP 패러다임을 적용한 언어이기 때문에 클래스가 존재한다. 본 포스팅은 데이터 얼라인먼트에 대해서 얘기해본다.

 

먼저 객체를 생성하는 방법은 크게 3가지이다. stack메모리에 객체를 생성하는 방법, heap메모리에 동적으로 생성하는 방법과 static 하게 객체를 생성하는 방법이 있다. 

#include <iostream>

class Dog
{
public:
	void speak()
	{
		std::cout << "woof" << '\n';
	}
private:
	int i4a;
};

Dog staticDog;	//static

int main()
{
	Dog stackDog;	//stack
    
	Dog* heapDog = new Dog;	//heap
	
	delete heapDog;
	return 0;
}

위 코드 main함수를 보게 되면 Dog stackDog가 있다. 해당 객체는 stack영역에 저장이 된다. 다음은 Dog* heapDog는 포인터이다. stack영역에 포인터를 생성하고 heap영역에 객체를 생성한 뒤 포인터가 heap영역의 객체를 가리키는 식으로 메모리가 할당이 된다. 마지막으로 Dog staticDog는 static메모리 공간에 할당이 된다. 

내가 그린 그림

이어서 클래스 혹은 구조체의 Memory Alignment에 대해서 설명한다. 

 

Memory Alignment는 메모리가 할당이 어떤 식으로 할당이 되는지에 대한 룰이다. 구조체의 멤버 변수들의 자료형의 크기에 따라 메모리를 할당을 어떻게 해줄까에 대한 얘기이다. 예를 들어서 얘기해보자.

 

Case 1

class Dog
{
public:
	void speak()
	{
		std::cout << "woof" << '\n';
	}
private:
	double d8;
	int i4a;
	int i4b;
};

main함수에서 sizeof() 연산자로 해당 클래스의 크기를 출력을 해본 결과 16바이트의 크기를 가지고 있다. 이는 자명하다. 간단하게 메모리에 대한 그림으로 나타내면 아래와 같다. 

*char형 자료현은 1바이트, int 자료형 4바이트, double자료형은 8바이트로 가정한다. 이는 환경마다 다르기 때문이다.

 

double은 8바이트 두 개의 int는 8바이트 이기 때문에 더하면 16바이트라는 답을 쉽게 얻을 수 있다. 

 

 

Case 2

class Dog
{
public:
	void speak()
	{
		std::cout << "woof" << '\n';
	}
private:
	int i4a;
	double d8;
	int i4b;
};

이번에는 double형 d8을 int형 멤버 변수 사이에 위치하였다. 해당 케이스도 main함수에서 sizeof() 연산자를 통해 크기를 출력해본 결과 24바이트로 출력이 됐다. 왜 그런 걸까? 

 

이는 Memory Alignment의 두 가지 특징 때문에 일어난 일이다. 

 

Memory Alignment의 두 가지 특징

  • 멤버 변수는 해당 변수의 자료형 크기의 배수 위치에서 시작해야 된다. 
  • 오브젝트의 전체 사이즈는 가장 큰 멤버 변수의 크기의 배수에서 끝나야 한다. 

int형 멤버 변수 i4a는 정상적으로 4바이트 공간을 할당을 받았다. 그다음 멤버 변수인 d8을 할당하려고 했으나 할당하려는 위치가 double형 멤버 변수의 크기인 8의 배수가 아니기 때문에 자동적으로 Memory Padding이 일어나 4바이트를 placehold (빈 공간) 채워준다. 이제 시작 위지 차 8이기 때문에 정상적으로 d8을 할당하였다. 그다음 16바이트 위치는 i4b의 자료형이 int이기 때문에 4의 배수가 성립이 되어 할당을 해준다. 마지막으로 Memory Alignment의 특징인 오브젝트의 전체 사이즈는 가장 큰 멤버 변수의 크기의 배수로 끝나야 한다는 특징이 있기 때문에 가장 큰 멤버 변수는 double형 즉, 8바이트의 배수로 끝나야 하기 때문에 4바이트를 Padding을 더해주어 마지막으로 24바이트로 끝이 난다는 것을 확인할 수 있다. 

 

 

Cache line

나중에 다른 포스트에서 자세히 다룰 예정이지만 간단히 공부한 내용을 정리해보자면 프로세서는 정보를 처리할 때 cache line을 이용하여 데이터를 끊어서 처리를 하게 되는데 이 끊어지는 크기가 대체적으로 64바이트라고 한다. 그렇다면 아래 코드를 보자.

class Dog
{
public:
	void speak()
	{
		std::cout << "woof" << '\n';
	}
private:
	int i4a;
	double d8;
	int i4b;
};


int main()
{
	Dog DogArr[10];
	
	return 0;
}

 크기가 10인 Dog 객체 배열을 만들면 메모리는 아래와 같이 할당될 것이다. 

배열에 담긴 10개의 객체들이 연이어 메모리를 할당받게 되지만 cache line이 데이터를 처리하기 위해선 cache line으로 잘라서 처리를 하게 된다. 이 과정에서 3번째 객체는 사진과 같이 2조각으로 나누어 처리를 하게 된다. 이런 형상은 False Sharing이라고 한다. 이러한 불상사를 예방하기 위해 이와 같이 코드를 짤 수 있다. 

class alignas(32) Dog
{
public:
	void speak()
	{
		std::cout << "woof" << '\n';
	}
private:
	int i4a;
	double d8;
	int i4b;
};


int main()
{
	Dog DogArr[10];
	
	return 0;
}

alignas(32)를 축가 해줌으로 각 객체의 메모리가 32바이트로 할당이 되고 원래 24바이트에서 추가된 8바이트는 Padding으로 채워지게 된다.

 

 

Ref.

https://nocodeprogram.com/lecture/1/94892/ 

 

728x90

OOP Intro이기 때문에 각 특징들에 대해서 간단하게 설명한다. 

 

C++는 객체지향 OOP (Object Oriented Programming) 언어이고 다중 패러다임 (Multi Paradigm) 프로그래밍 언어이다. 

 

먼저 C++는 procedural, functional, OOP, generic 네 가지의 패러다임을 이용해서 프로그래밍을 하게 된다. 그중에서 OOP 특성을 중심으로 구현이 된다.

 

OOP란 클래스를 중심으로 프로그래밍을 하는 것을 의미한다. 클래스란 definition이라고 생각하고 그 definition으로 객체가 생성이 된다. 객체는 실제 메모리 공간을 차지하는 클래스에 대한 변수라고 생각하면 된다. 

 

class Dog
{
public:
	Dog(int age) : mAge{age} {};
private:
	int mAge;
};

int main()
{
	Dog choco {9};	//객체 생성
    return 0;
}

위 코드를 보면 "Dog"라는 클래스가 정의되었다. 클래스는 mAge라는 변수를 통해 객체의 "나이"를 저장한다. main함수에서 Dog의 Constructor를 이용하여 choco객체의 "나이"를 9로 초기화를 시켜준다. 

 

OOP는 4개의 특성이 있다. 

  • Abstraction
  • Encapsulation
  • Inheritance
  • Polymorphism

Abstraction

추상화라는 특징이다. 현실세계의 어떠한 물체 혹은 문제를 추상화를 하는 것이라고 생각하면 쉽다. 위 코드에서 현실세계에 존재하는 강아지라는 생물은 여러 특성(사족보행, 평균수명, 귀여움 등)이 있지만 현재 프로그램에서 나이만 필요할 경우 나이에 대한 멤버 변수만 생성을 해주어 강아지라는 동물을 추상화를 하여 구현한다는 특징이다. 

 

Encapsulation

캡슐화라는 특징이다. 클래스 내부의 중요한 멤버 변수들을 외부에서 수정이 불가하도록 하는 특징이다. 예를 들면 강아지 클래스에 강아지의 평균수명 값을 저장하고 있는 멤버 변수가 있다고 가정을 해보자. 그러면 외부에서 강아지의 평균수명을 임의대로 수정을 하게 되면 문제가 생기게 된다. 캡슐화는 private, protected 등 키워드를 통해 구현이 된다. 위 코드에서 mAge 멤버 변수는 private키워드에 포함이 되기 때문에 외부에서 해당 멤버 변수를 직접 접근, 수정하는 것이 불가능하다. 

 

Inheritance

상속성이라는 특징이다. 상속성의 가장 중요한 점은 코드의 재사용이라는 점이다. 예를 들면 현실 세계에서 강아지라는 생물은 동물에 속한다. 결국 강아지도 동물의 특성을 가지고 있다. 동물의 특성으로는 사람의 언어를 못한다는 특성이 있다고 가정하면 동물 클래스에 "사람과 대화 불가"라는 코드를 만들고 강아지 클래스에 동물 클래스를 상속을 받으면 동물 클래스의 특징을 이어받는다. 상속성을 이용해 중복되는 코드를 또 생성하지 않고 재사용할 수 있다는 장점이 있다.

 

Polymorphism 

다형성이라는 특징이다. 다형성에는 대표적으로 function overloading과 function overriding이 있다. 다형성이라는 특징은 형태가 여러 가지 존재한다는 특징이다. 겉은 같아 보이지만 특정 파라미터 혹은 특정 상황에 맞추어 호출하는 함수가 달라지는 특징이다. 해당 특징에 대해서는 function overloading과 function overriding 포스트에서 따로 자세하게 다룰 예정이다. 

 

 

우리가 c++ 프로그래밍을 하면서 유의할 점은 OOP가 목적이 되어서는 안된다는 점이다. 우리의 목적은 읽기 편하고, 이해하기 쉽고, 유지보수가 용이한 프로그램을 짜는 것이 목적이다. OOP를 정확하게 따르는 프로그램을 만드는 것이 목적이 아니다. 읽기 편하고, 이해하기 쉽고, 유지보수가 용이한 프로그램을 짜되 파포먼스까지 좋은 프로그램을 짜기 위해서 C++을 이용하는 것이다. 우리가 가장 포커스를 맞춰야 하는 점은 프로그래밍을 통해서 가치를 창출해내는 것이 우리의 목표임을 잊지 말자.

 

 

Ref.

https://www.youtube.com/channel/UCHcG02L6TSS-StkSbqVy6Fg

 

 

본 포스팅은 유튜버 코드없는프로그래밍 채널을 공부하면서 정리한 내용입니다. 

728x90

C 스타일 문자열 (char*, char [])에 익숙한 나로서 C++의 string 클래스보다 C 스타일 문자열이 아직까지 편하다. 개강도 다가오기 때문에 string 클래스에 대해서 정리를 해보자.

 

먼저, C 스타일 문자열의 단점에 대해서 얘기해보자. 

 

C 스타일 문자열은 내가 저장하고자 하는 문자열의 길이보다 1 크기의 길이가 더 필요하다. 왜냐면 C 스타일 문자열에서 문자열의 끝을 나타내 주는 NULL Character ('\0')가 필요하기 때문이다. 이러한 특성 때문에 문자열의 크기에 대해서 신경을 써줘야 된다. 또한 동적으로 메모리를 할당했을 경우 할당 해제를 해주지 않으면 Memory Leak이 발생하기 쉽다

 

String 클래스는 말 그대로 클래스이다. String 클래스의 객체가 우리가 통상적으로 다루게 되는 "문자열"이다. String 클래스를 사용하기 위해선 string헤더를 include 해줘야 한다. 하지만 string 헤더는 iostream 헤더에 포함되어 있기 때문에 iostream 헤더를 include 할 경우는 추가적으로 include 할 필요는 없다. 

 

String 클래스는 기존 C 스타일 문자열에서 느낀 불편을 다소 해결해준다. NULL Character를 삽입할 필요도 없고 다양한 메서드들이 존재하고 메모리에 관해서 신경 쓸 일도 많이 줄어들었다. 하지만 C 스타일 문자열이 속도가 더 빠르다는 장점이 있다. 

 

 

String 객체 생성

string str;
string str1("문자열");
string str2 = "문자열";

 

위와 같이 초기화 없이 str객체를 생성할 수 있다. 클래스를 공부했다면 선언과 동시에 Constructer (생성자)를 이용해 초기화하는 방법과 대입을 이용해 초기화하는 방법이 있다. 

 

 

String 입력

string str;
cin >> str;     //white character전까지 입력
getline(cin, str);     //한 줄 통째로 입력
getline(cin, str, '문자');	//문자전까지 입력

String 객체를 입력받을 때 가장 많이 이용되는 두 방법이다. 먼저 cin객체를 이용해서 입력받는 방법은 White Character (' ', '\t', '\n') 전까지 입력을 받는다는 사실을 잊지 말자. 반면에 getline함수를 이용할 경우 '\n'문자 전 즉, 한 줄을 통째로 입력을 받는다. 

 

 

String 출력

String str("I love jiyong");
cout << str;     //I love jiyong

string 객체의 출력은 cout 객체를 이용해 출력이 가능하다.

 

 

String의 주요 메소드

string str("Conquer");

객체 str을 생성자를 이용하여 "Conquer"로 초기화.

 

//String 객체 인덱싱 및 특정 문자 반환
cout << str.at(5) << '\n';	//e
cout << str[5] << '\n';	//e
cout << str.front() << '\n'; //C
cout << str.back() << '\n'; //r

str객체를 at메서드를 이용하여 인덱싱 하는 방법과 배열적 용법( [] )으로 인덱싱 하는 방법이 있다. front와 back을 각각 제일 앞에 잇는 문자와 마지막 문자를 반환한다. 

 

//String 객체 문자열 조작 메소드
str.append("문자열");	//str 뒤에 "문자열" 삽입(연결)
str.append("문자열", n, m);	//"문자열"의 index : n 문자부터 m개의 문자열을 str 끝에 삽입(연결)
str.append(n, '문자');	//str 끝에 n개의 '문자' 삽입(연결)
str.insert(n, "문자열");	//index : n 문자 뒤에 "문자열" 삽입
str.replace(n, m, "문자열");	//index : n 문자부터 m개의 문자를 "문자열"로 대체
str.clear();	//문자열 모주 지움
str.erase();	//clear()과 동일
str.push_back('문자');	//문자열 맨 뒤에 '문자'삽입
str.pop_back();		//문자열 맨 뒤 문자 제거

문자열을 조작하는 메소드이다. C 스타일 문자열에서 대부분 직접 구현해야 하는 기능들인데 메소드를 이용하면 편리하게 원하는 기능을 사용할 수 있다. 

 

 

이외의 더 많은 메소드들이 존재한다.

본 포스트에서 다 다루지 못하고 위에 다룬 내용들도 함수의 원형 또한 모르니 레퍼런스를 꼭 참고하기 바란다. 

 

 

ref.

https://www.cplusplus.com/reference/string/string/

 

string - C++ Reference

 

www.cplusplus.com

 

+ Recent posts