[42seoul] 4 circle - CPP Module 01

하이초·2022년 10월 14일
0

42seoul

목록 보기
2/11
post-thumbnail

CPP01 github 😋

1. ex00

👀 ex00은 처음으로 new 연산자를 활용하여 객체를 생성해보는 과제였다. 이를 통해 stack/heap의 차이, 그리고 그에 따른 delete 연산자의 사용 유무 등에 대해 공부할 수 있는 과제.

🥨 new 연산자

  • 자바를 아주 조금 깔짝이다 온터라, 오 c++는 객체를 heap에 할당하지 않고 stack에 저장할 수 있구나! 하고 어색해했다. 처음에 자바 깔짝이면서 GC가 뭔지도 몰랐을 때는 객체 만들면서 않이.. 이거 할당 해제 안해도 되나..? 하고 이유 모를 죄책감을 느꼈었는데, 이젠 않이.. new 쓸라면 또 포인터형으루 받아야 한다구..? c,, 포인터 정말 지겹다 지겨와.. 하게 됨
  • 아무튼, newZombie는 new 연산자를 통해 heap에, randomChump는 함수 내 지역 변수로 stack에 객체가 생성 된다
  • 여기서 신기했던 건, 뭐 당연한 얘기지만! 함수 내부에서 지역변수로 선언한 객체는 자신의 쓸모를 다하면 바로 소멸된다. main 함수에 선언된 친구가 프로그램 종료시에 소멸되는 것과 다르게 말이다.
int main() {
	Zombie zombieA = Zombie("stackZombieA");
	zombieA.announce();

	Zombie* zombieB = Zombie::newZombie("heapZombieB");
	zombieB->announce();

	Zombie::randomChump("stackRandomChumpZombieC");

	delete zombieB;
}
  • 위와 같이 코드를 작성했을 경우
    함수에서 지역변수로 선언된 stackRandomChumpZombieC가 자기 쓸모를 다하고 가장 먼저 소멸되고, 그 다음 delete 연산자에 따라 heapZombieB, 그리고 마지막으로 프로그램이 종료되며 stackZombieA가 소멸되는 것을 확인할 수 있다

❓ 아 그리고 또 하나 신기했던거, 클래스가 아니라 그 클래스 함수 구현체만 담은 cpp 파일을 만들수가 있던데,, 자바도 되나? 그런 걸 해본적이 없어서 갱장히 어색했다.

2. ex01

👀 ex01은 zombieHorde(int N, std::string name) 이라는 함수를 통해 한 번에 N개의 좀비를 만드는 과제였다

🥨 delete[]

  • zombieHorde에서 배열로 객체를 생성하였기 때문에 소멸자도 이에 맞게 써야한다
  • 일반 delete 사용 시 첫 객체만 해제가 될 것이다. 그래서 배열로 객체를 선언할 경우 delete[] 연산자를 사용해야 모두 해제 된다.

🥨 sstream

  • sstream은 string stream의 약자로 string 관련 stream을 처리해준다
    	int main(void) {
    	std::stringstream stream;
       std::string str = "123 123.456";
       int i;
       double d;
       stream.str(str);
    	stream >> i >> d;
       std::cout << "int : " << i << ", double : " << d << std::endl;
    }
  • 요로코롬 문자열을 공백 기준으로 구분하여 stream으로 저장한다. 위 예제처럼 i와 d에 차례대로 넣어 줄 수도 있고 반복문으로 돌리면서 하나씩 넣어줄 수도 있다.
  • stream.str(string); 함수를 통해 문자열을 stream으로 만들어 주거나, stream에 << 연산자를 통해 값을 넣어놓고 stream.str() 함수를 통해 해당 값을 stringd으로 변환할 수 있고, stream.str(""); 함수를 통해 stream을 초기화해 줄 수 있다
  • 아무튼 그래서 좀비떼의 이름을 지어주는 데 아주 유용한 함수라고 할 수 있다

3. ex02

👀 ex02는 참조자에 대해서 처음 알아보는 과제!!!

  • 나 진짜.. 포인터도 벅찬데 참조자도 알아야 한대.. 어이없어...
  • 그치만 좀 살펴보니 포인터보다는 그래도 훨씬 더 괜찮은 놈인거 같다 햅삐

🥨 참조자

  • 일단 아직 정확하게 어떻게 돌아가는 지는 느낌으로만 이해한, 감성코딩 수준이긴 하지만 대충 이해한 걸 풀어보면 참조자는 메모리에 할당되지 않을 수 있다! 그냥 원본을 가리키는 새로운 별명이 원본에 바인딩 될 뿐 따로 포인터처럼 해당 변수가 따로 주소값을 갖거나 하지 않을 수 있다는 것이다.
  • 그냥.. 거울 같은거지 거울.. 너 여기 있니? 나 여기 있지. 이런 느낌?
  • 앞선 cpp00에서 임시 객체에 대해 잠시 얘기했었는데, 모든 객체는 매개 변수로 전달 될 때 복사 생성자에 의해 복사되어 전달 된다. 이때 새로운 메모리가 할당되게 되는데 클래스나 구조체처럼 크기가 큰 변수가 전달된다고 하면 쓸데없는 메모리의 사용이 커지게 된다. 따라서 원본값을 수정해야 할 때는 포인터나 참조자를 사용하는 것이 좋은데, 참조자를 사용하면 추가적인 메모리 사용이 없으니 잘 활용하면 좋겠지!!! 하지만 값변경이 되니 사용에 조심하라구 ^.~
  • 🚨 참조자 주의사항!!
    • 참조자는 무조건!! 선언과 동시에 초기화 되어야 한다. 일단 냅다 선언만 해도 되는 포인터와는 다르다
    • 또한, 참조자는 어떤 변수의 참조자가 되면 절대 다른 변수의 참조자가 될 수 없다!
    • 👍 참조자에 대한 정리는 여기에 너무너무 정리가 잘 되어 있으니 참고하면 좋을듯!!!

4. ex03

👀 ex03은 참조자와 포인터를 다뤄보는 과제였다. 이제 좀 OOP 과제 같아 지는 걸~!

  • cpp00 평가 당시에 한 평가자분께서 const에 대해 조언을 좀 해주셨었다. 과제 통과에 있어서 필수적인 사항은 아니었지만, 듣고 보니 참 도움이 되는 말씀이었던 터라 이번 과제를 하면서 최대한 const를 생각하면서 사용해보려 했다.

🥨 C++ 생성자 초기화

  • c언어, 특히 42seoul의 Norm에 맞춘 코딩 스타일에서는 변수를 선언과 동시에 초기화 해주지 않았다. 그런데 c++의 경우에는 변수가 정의된 후에 대입 연산자를 통해 값을 제공할 경우 복사 할당되며, 선언과 동시에 초기화 되는 것보다 성능이 떨어진다고 한다.
    • 변수 정의 -> 대입연산자 이용 2단계를 거치기 때문..?
    • 🤔 c언어도 복사 할당인가? 찾아도 잘 모르겠다... 아니겠지..?
  • C++에서 변수를 초기화하는 방법은 크게 3가지로 나눌 수 있다
      1. 등호를 사용하여 복사 초기화 수행 (int a = 1)
      1. 괄호를 사용하여 직접 초기화 수행 (int a(1))
      1. 중 괄호를 사용하여 유니폼 초기화 수행(int a{1}) <- 요건 c++11! 🚨 {}로 초기화 시 0으로 초기화 및 형변환을 허용하지 않음!
  • int와 같은 일반 자료형은 복사 및 직접 초기화가 기본적으로 동일하지만, 사용자 정의 클래스와 같은 자료형에서는 직접 초기화보다 복사 초기화가 성능이 우수하다고 한다.
  • 🚨 문제는! 참조자의 경우 '선언과 동시에 초기화' 되어야 한다고 했다. 그런데 클래스 내부 멤부 변수에 참조자 변수가 있다고 생각해보자. 위의 방법으로 초기화를 진행할 시 객체가 생성 된 후 생성자를 통해 매개변수를 전달 받을 때까지 해당 변수는 초기화 될 수 없는데, 클래스에 이미 변수가 선언되어 있는 것이다. 따라서 이 문제를 해결할 방법이 필요하다!!
    • 함수 매개변수와는 다름을 이해해야 한다. 매개변수는 호출 시 매개변수에 선언되고 동시에 호출자에 의해 초기화 되는 것과 다름없다.

🥨 초기화 리스트

  • 그 문제를 해결하기 위해 나온 것이 바로 이 초기화 리스트
     #include "HumanA.hpp"
    HumanA::HumanA(const std::string name, Weapon &weapon) 
    : _name(name), _weapon(weapon)
    {
    	std::cout << _name << " created. (weapon: " << _weapon.getType() << ")\n";
    }
    void HumanA::attack() const{
    	std::cout << _name << " attacks with their " << _weapon.getType() << std::endl;
    }
  • 초기화 리스트는 위와 같은 형태로 사용한다. 클래스 생성자 매개변수 바로 뒤, {} 스코프에 들어가기 전에! 초기화 해주는 것이다. :콜론으로 시작하며, 끝날때 ;세미콜론은 붙이지 않는다 뭐 이런 형태가 다 있어 안 예뻐
  • 초기화 리스트를 사용하면 더이상 생성자 본문에서 할당을 수행하지 않아도 된다. 당연함 그게 초기화임. 생성자 본문에서 값을 할당하는 것보다 성능이 더 우수하다고 한다. 개이득?
  • 이러한 초기화 리스트는 호출자가 초기화 값을 전달할 때 더 유용하다고 한다.
  • const, reference(참조자) 변수의 경우 초기화 리스트만이 그들을 초기화 해줄 수 있다!

🥨 참조자와 포인터

  • 과제를 해결하기 위해 나의 경우 HumanA는 참조자로, HumanB는 포인터로 weapon 변수를 만들었다
  • HumanB의 경우 생성 단계에서 Weapon을 주입받지 못하기 때문이다

5. ex04

👀 ex04는 파일 입출력을 다뤄보는 과제였다

🥨 fstream

  • 파일 입출력을 지원하는 헤더
  • std::ifstream infile(filename);, std::ofstream outfile(filename); 으로 in-out 파일을 열 수 있다
    • 선언 후 open 함수를 사용해도 되지만, 굳이??? 이게 직접 초기화겠지?
  • ifstream의 경우 파일이 없으면 오류를(기본 플래그 std::ios::in), ofstream(기본 플래그 std::ios::out)은 자동으로 새 파일을 생성해 준다고 한다
    • 이미 있는 파일의 경우에 ofstream으로 열고 쓰기를 작성할 경우 기본은 덮어쓰기(std::ios::trunc)가 되며, 이어쓰기를 원할 경우 std::ios::app 모드를 지정해줘야 이어쓰기가 가능하다
  • infile.eof()를 통해 파일 읽기를 끝까지 마쳤는 지 확인할 수 있다
  • .is_open() || .fail() 을 통해 파일 오픈 에러 처리를 해줄 수 있다. 전자의 경우 true, 후자의 경우 false를 반환받아야 파일이 제대로 열린 것이다
  • .close()를 통해 파일을 닫을 수 있다

🥨 str.find(to_find);

  • str에서 to_find가 있는지 찾아주는 함수
  • str에서 to_find를 찾으면 그 문자열의 첫번째 위치를 반환한다
  • str.find(to_find, n);을 하면 n부터 검색을 시작한다
  • 문자열을 찾지 못하면 엄청나게 큰 수를 반환하는데, 걍 (n != std::string::npos)로 검사하면 된다

🥨 str.substr(n);

  • str에서 n번째 위치부터 끝까지 부분 문자열을 반환해준다
  • str.substr(n, to)는 n부터 to까지 잘라서 반환해준다
  • 🚨 마지막 n번째는 포함되지 않으므로 포함하고 싶은 인덱스 + 1로 해야함을 주의해야 한다

🥨 요번 과제에서 빠트릴 뻔했던 것들

  • 파일에서 읽을 때 getline으로 한 줄씩 불러와서 to_find가 있는지 검색했는데, 처음에 딱 1번만 검사를 수행했다. 말인 즉슨, 한 문장 내에 to_find가 여러번인 경우를 고려하지 않은 것이다!!
  • 위의 사항을 고치기 위해 한 줄을 끝까지 돌면서 to_find를 찾으면 해당 idx를 계산하고 어쩌고 해서 진행했는데 처음에 잘못 생각해서 replace 문자열의 길이를 재서 idx를 갱신하는 멍청한 짓을 했었다.

6. ex05

👀 ex05는 C++에서 함수 포인터 다뤄보기! C에서도 별로 안해봤는데 큰일이었다!!

  • 이 과제의 원래 이름이 karen이었다는 것을 알고 기함할 뻔. 에꼴42 반성해 🤬

🥨 함수 포인터

  • 함수 포인터란 쉽게 말해 함수의 주소를 포인터 변수에 저장해서 사용하는 것을 말한다. 참 쉽죠?
  • type (*f)(type, type); 의 형태로 선언한다
    • type: 반환형, *f: 함수 포인터 변수 이름, (type, type): 매개 변수
  • type (ClassName::*f[n])() = {&ClassName::f1, &ClassName::f2};
    • 함수 포인터를 배열로 사용할 수도 있다, 매개변수는 당연히 일치해야겠지?
    • 다만 c언어의 경우 함수 이름 자체가 주소라서 배열에서도 &를 붙이지 않아도 됐었는데, c++은 꼭 붙여야 한다고 봤다. 근데 또 다른 글을 참고하면 안 붙여도 된다고하고.. 뭐징.. 뭐가 맞는거징.. 일단 &를 떼면 컴파일 조차 안되는데.. 대체 몰까? 🤔 <- 좀 더 찾아보니 멤버 함수 포인터와 일반 함수 포인터의 차이인 것 같다!
  • 당연히 이 함수 포인터 자체도 매개변수로 보낼 수 있다

🥨 this 포인터

  • 클래스 멤버 함수를 사용할 경우에 this포인터를 사용해야 한다. 멤버 한수는 객체가 꼭 필요하니까! (this->*f)(); 형태로 사용
  • 그렇다면 어떻게 호출할 객체를 찾을 수 있을까? 이것을 가능하게 하는 것이 바로 숨겨진 this 포인터
  • 위 예제를 볼 때 매개변수가 없다고 생각하지만, 사실 &classInstanse라는 생략된 매개 변수가 있다
    • 요 this 포인터는 호출한 객체의 주소를 가리키고 있는 것이다
  • 매개 변수로 전달되기 때문에 함수가 실행되는 동안에만 스택에 매개 변수가 쌓인다! 메모리 낭비가 없음!

7. ex06

👀 ex06은 ex05에서 만들어 둔 함수포인터를 활용하여 switch-case문을 사용해보는 과제였다

  • c때는 while문만 사용하게 해줬음서 흑흑 ㅠㅠ 드디어 ㅠㅠ!
  • 요거는 딱히 쓸 말이 없당.

하 진짜 피신때로 돌아간 기분!!!
효율이 극도로 저하되었지만,

🚴 CPP 가보자고!

profile
개발국대가 되는 그 날까지. 지금은 개발 응애.

0개의 댓글