CPP module 01은 동적 할당, 생성자, 소멸자, getter와 setter, 참조자에 대해 학습하는 내용이다.
뭐 하나 빠질 것 없이 앞으로 C++을 함에 있어서 평생 함께 갈 문법들이므로 자세히 보고 가는 것이 좋다.
// Zombie.hpp
#ifndef ZOMBIE_HPP
# define ZOMBIE_HPP
# include <iostream>
# include <string>
# include <sstream>
class Zombie {
private:
std::string name;
public:
Zombie();
~Zombie();
void setName(std::string name);
void announce();
};
Zombie *newZombie(std::string name);
Zombie *zombieHorde(int N, std::string name);
#endif
// Zombie.cpp
Zombie::Zombie() {
std::cout << "Zombie is born" << std::endl;
}
Zombie::~Zombie() {
std::cout << this->name << " is dead" << std::endl;
}
void Zombie::announce(void)
{
std::cout << this->name << ": BraiiiiiiinnnzzzZ..." << std::endl;
}
void Zombie::setName(std::string name)
{
this->name = name;
}
// ZombieHorde.cpp
Zombie *zombieHorde(int N, std::string name)
{
Zombie *covey = new Zombie[N];
std::stringstream ss;
for (int i = 0; i < N; i++)
{
ss.str(std::string());
ss << i + 1;
covey[i].setName(name + ss.str());
}
return (covey);
}
//main.cpp
#include "Zombie.hpp"
int main(void)
{
Zombie *zombie = zombieHorde(5, "zombie");
for (int i = 0; i < 5; i++)
zombie[i].announce();
delete [] zombie;
}
좀비 떼를 할당하고, 좀비 떼들이 생성될 때마다 생성자로 Zombie is born을 출력하게 한다.
delete로 할당이 해제되거나, 지역 함수에서 해제될 때(소멸자가 호출될 때) is dead를 호출하고, 좀비들은 태어날 때마다 울부짖게 된다.
핵심은 각각 좀비마다 이름을 붙여주는 setName 함수이다.
zombieHorde 함수를 잘 보면, 각각의 좀비마다 Zombie(인덱스)를 붙여주는 로직이 있다.
std::stringstream ss
는 c++에서 제공하는 stringstream이라는 클래스로 ss라는 객체를 생성한다.
이 객체는, 스트림 연산자를 사용하여 문자열에 데이터를 쉽게 추가하거나 문자열에서 데이터를 읽어 오는 것에 용이하다.
ss.str(std::string());
해당 코드는 "ss"의 내용을 모두 비워버린다. ss 안에 있는 모든 문자열 데이터를 지워버리며, 루프의 반복에서 새로운 문자열을 생성하기 위해 ss를 초기화 하기 위해 사용하는 것이다.
ss << i + 1;
해당 코드는 ss에 i + 1 의 값을 할당하는 것이며, i의 값이 0이라면 첫 번째로 생성된 좀비가 Zombie 1이 될 수 있게 하는 로직이다.
i + 1 이라는 정수형 값을 자동으로 문자열로 변환하여 저장하게 된다.
covey[i].setName(name + ss.str());
해당 코드는 좀비의 이름인 Zombie와 ss에 저장된 문자열을 연결하여 각각의 이름에 인덱스를 붙이는데에 사용된다.
#include <iostream>
int main(void)
{
std::string str = "HI THIS IS BRAIN";
std::string* stringPTR = &str;
std::string& stringREF = str;
std::cout << "Address of str : " << &str << std::endl;
std::cout << "Address of stringPTR : " << stringPTR << std::endl;
std::cout << "Address of stringREF : " << &stringREF << std::endl;
std::cout << "------------------------------" << std::endl;
std::cout << "Value of str : " << str << std::endl;
std::cout << "Value of stringPTR : " << *stringPTR << std::endl;
std::cout << "Value of stringREF : " << stringREF << std::endl;
return 0;
}
코드는 짧지만 module 02에서 사실상 가장 중요한 내용이 아닌가 싶다.
포인터와 레퍼런스
포인터의 레퍼런스의 차이점
생성 및 초기화
포인터: 선언 시 초기화하지 않으면 "포인터를 초기화하지 않음" 경고가 발생할 수 있음
레퍼런스: 선언과 동시에 초기화해야 함
Null 값
포인터: NULL 또는 nullptr 값을 가질 수 있음
레퍼런스: NULL 값이나 nullptr 값으로 초기화될 수 없음
재할당
포인터: 다른 주소로 재할당 가능
레퍼런스: 한번 초기화되면 변경할 수 없음
코드 설명
std::string str = "Hi This Is Brain"
문자열을 선언한다.
std::string *stringPTR = &str;
'str'의 주소를 저장하는 포인터 'stringPTR' 선언
std::string &stringREF = str;
'str'을 참조하는 레퍼런스 'stringREF' 선언
출력
각 변수의 메모리 주소와 값을 출력한다. 이를 통해 포인터와 레퍼런스가 실제로 어떻게 작동하는지 확인 할 수 있다.
결론 및 학습 포인트
#include <fstream>
#include <iostream>
#include <string>
int main(int ac, char **av)
{
if (ac != 4)
{
std::cerr << "Usage : " << av[0] << "<filename> <s1> <s2>" << std::endl;
return 1;
}
std::string filename = av[1];
std::string s1 = av[2];
std::string s2 = av[3];
std::ifstream inputFile(filename.c_str());
if (!inputFile.is_open())
{
std::cerr << "Error opening the file: " << filename << std::endl;
return 1;
}
std::ofstream outputFile((filename + ".replace").c_str());
if(!outputFile.is_open())
{
std::cerr << "Error creating the output file." << std::endl;
return 1;
}
std::string line;
while (std::getline(inputFile, line))
{
std::string::size_type pos = 0;
while ((pos = line.find(s1, pos)) != std::string::npos)
{
line.erase(pos, s1.length());
line.insert(pos,s2);
pos += s2.length();
}
outputFile << line << std::endl;
}
inputFile.close();
outputFile.close();
return 0;
}
Ex04는 4개의 파라미터를 받는다.
첫 번째는 파일의 이름, 두 번째는 파일 안에 있는 내용, 세 번째는 파일 안에 있는 내용을 무엇으로 치환할 것인가.
치환한 내용은 파일이름 + .replace 파일 이름 안에 저장이 된다.
파라미터의 수가 4개가 아닐 경우 에러!
문자열 3개를 선언하여, 각 파라미터의 내용을 담는다.
파일 읽기용 스트림 ifstream 클래스로 inputFile을 선언
파일 쓰기용 스트림 ofstream 클래스로 outputFile을 선언
! ifstream, ofstream 안에 바로 std::string을 박아 넣으면 C++98 문법에 어긋난다. (cpp reference 참고)
중요한 포인트는 c_str 이다. 사실 CPP 05 평가를 갔었는데, 이 부분에 대해 정확히 알지 못해서 KO 코드를 OK를 주고 말았다... 실수다 ㅠ
이 글을 보는 카뎃이 제대로 된 C++98 문법과 함수를 사용하여 코딩을 하길 바라는 마음으로 적는다.
ifstream, ofstream을 42 과제에서 하기 위해서는 const char *로 만든 문자열을 넣어야 한다.
이를 위해서는 c_str() 함수가 필요한데, 문자열을 C 스타일로 바꾸어주는 다리 역할을 수행한다.
C 스타일의 문자열이라 함은, 문자열의 마지막을 알릴 때 NULL을 넣음으로서 배열의 마지막을 알려주는 스타일의 문자열이다.
c_str 함수는 C 스타일 문자열을 제공하여 ifstream, ofstream을 98버전 플래그를 걸어도 걸리지 않게 만들어준다.
타입과 저장 방식:
std::string에 관하여:
std::string은 C++ 표준 라이브러리에 포함된 클래스다.
동적 메모리 할당을 활용하여 문자열 데이터를 관리한다.
문자열의 크기가 변경될 때마다 자동으로 메모리를 재할당한다.
const char*에 관하여:
const char*는 C 스타일 문자열을 참조하는 포인터다.
이 포인터는 문자 배열의 주소를 참조하며, 해당 문자열은 널 종료 문자('\0')로 종료된다.
이 문자열의 크기는 고정되어 있어, 수정하거나 크기를 바꾸는 것이 불가능하다.
수정 가능성:
std::string에 관하여:
std::string을 사용하면 문자열의 내용과 크기를 간편하게 변경할 수 있다.
const char*에 관하여:
"const" 키워드가 의미하는 바와 같이, 이 문자열은 수정이 불가능하다. 수정을 시도하면 컴파일 오류나 런타임 오류가 발생할 수 있다.
메모리 관리:
std::string에 관하여:
std::string 객체는 소멸될 때 메모리를 자동으로 해제한다.
const char*에 관하여:
메모리가 동적으로 할당된 경우, delete[]를 사용하여 메모리를 직접 해제해야 한다.
기능과 메서드:
std::string에 관하여:
std::string은 문자열 조작, 검색, 비교 등의 다양한 메서드와 연산자를 제공한다. 예로는 substr(), find(), + 연산자 등이 있다.
const char*에 관하여:
C 스타일 문자열은 기본 연산만 가능하므로, 보다 복잡한 작업을 위해서는 추가적인 함수를 사용해야 한다. 이에 해당하는 함수로는 strcpy(), strcat(), strlen() 등이 있다.
호환성:
std::string에 관하여:
대부분의 C++ 표준 라이브러리 함수들은 std::string을 지원한다.
const char에 관하여:
C 스타일 문자열은 C와 C++ 양쪽에서 모두 널리 사용되므로, C 라이브러리와의 호환성이 필요할 때 유용하다.
결론:
std::string과 const char은 각기 다른 특성을 가진 문자열 표현 방식이다. C++에서는 std::string을 활용하면 문자열 관련 작업을 보다 쉽고 안전하게 수행할 수 있다. 하지만 C 라이브러리와의 호환성이나 레거시 코드와의 상호 운용성을 고려해야 할 상황에서는 const char*의 사용을 고려해야 할 수도 있다.
Switch, case 문을 활용해보는 과제이다.
// main.cpp
int main(int ac, char **av)
{
Harl harl;
if (ac != 2)
{
std::cout << "Usage ./harlFilter <log level>" << std::endl;
return 1;
}
std::string levelStr(av[1]);
harl.complain(levelStr);
return 0;
}
// Harl.cpp
#include "Harl.hpp"
Harl::Harl() {}
Harl::~Harl() {}
void Harl::debug(void)
{
std::cout << "[ DEBUG ]" << std::endl;
std::cout << "I love having extra bacon for my 7XL-double-cheese-triple-pickle-specialketchup burger. I really do!" << std::endl;
}
void Harl::info(void)
{
std::cout << "[ INFO ]" << std::endl;
std::cout << "I cannot believe adding extra bacon costs more money. You didn’t put enough bacon in my burger! If you did, I wouldn’t be asking for more!" << std::endl;
}
void Harl::warning(void)
{
std::cout << "[ WARNING ]" << std::endl;
std::cout << "I think I deserve to have some extra bacon for free. I’ve been coming for years whereas you started working here since last month." << std::endl;
}
void Harl::error(void)
{
std::cout << "[ ERROR ]" << std::endl;
std::cout << "This is unacceptable! I want to speak to the manager now." << std::endl;
}
void Harl::complain(const std::string& levelStr)
{
logLevel level = strToLogLevel(levelStr);
switch(level)
{
case DEBUG:
debug();
// fallthrough
case INFO:
info();
// fallthrough
case WARNING:
warning();
// fallthrough
case ERROR:
error();
break ;
default:
std::cout << "[ Probably complaining about insignificant problems ]" << std::endl;
}
}
Harl::logLevel Harl::strToLogLevel(const std::string& levelStr)
{
if (levelStr == "DEBUG")
return DEBUG;
if (levelStr == "INFO")
return INFO;
if (levelStr == "WARNING")
return WARNING;
if (levelStr == "ERROR")
return ERROR;
return NONE;
}
생성자와 소멸자에는 별 내용이 담기지 않는다.
complain 함수와 strToLogLevel 함수만 잘 살펴보면 된다.
파일 이름은 harlFilter이며, 이후 다음 파라미터는 파라미터에서 지정해준 출력에 맞는 문자열을 출력한다.
main 함수에서 또 다른 c++의 초기화 문법이 나온다.
std::string levelStr(av[1]);
levelStr이라는 string 객체를 선언함과 동시에, 인자에 C스타일 문자열이자 main함수의 파라미터인 av[1]을 넣음으로서,
levelStr을 av[1]로 초기화 시킬 수 있다.
즉, main 함수를 그대로 따라가보면 Harl::complain 함수는 첫 번째 파라미터로 받아온 DEBUG, ERROR... 등등을 인자로 받아올 수 있음을 알 수 있다.
strToLogLevel이라는 함수를 통해 각각의 케이스들이 Enum 값(int) 로 반환이 되어 complain 함수의 case에 들어가게 되는데
해당 과제에서는 의도적으로 debug 함수를 사용했을 때 그 아래에 있는 info, warning, error가 모두 출력되도록 해야 한다.
vscode에서 makefile로 컴파일 했을 때는 아무 문제가 없는데, codespace와 같은 컴파일 플래그가 제대로 적용되는 환경에서는 에러를 출력했다.
c++17 환경에서는 자동으로 fallthrough 를 제공해준다고 하는데, C++98 로 플래그를 세워서 컴파일을 하는 이러한 경우에는
주석으로 //fallthrough 를 달아주면 case 를 의도적으로 종료하지 않아도 바로 다음 케이스로 넘어갈 수 있게 해준다고 한다.