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
3. ex02
👀 ex02는 참조자에 대해서 처음 알아보는 과제!!!
- 나 진짜.. 포인터도 벅찬데 참조자도 알아야 한대.. 어이없어...
- 그치만 좀 살펴보니 포인터보다는 그래도 훨씬 더 괜찮은 놈인거 같다 햅삐
🥨 참조자
- 일단 아직 정확하게 어떻게 돌아가는 지는 느낌으로만 이해한, 감성코딩 수준이긴 하지만 대충 이해한 걸 풀어보면 참조자는 메모리에 할당되지 않을 수 있다! 그냥 원본을 가리키는 새로운 별명이 원본에 바인딩 될 뿐 따로 포인터처럼 해당 변수가 따로 주소값을 갖거나 하지 않을 수 있다는 것이다.
- 그냥.. 거울 같은거지 거울.. 너 여기 있니? 나 여기 있지. 이런 느낌?
- 앞선 cpp00에서 임시 객체에 대해 잠시 얘기했었는데, 모든 객체는 매개 변수로 전달 될 때 복사 생성자에 의해 복사되어 전달 된다. 이때 새로운 메모리가 할당되게 되는데 클래스나 구조체처럼 크기가 큰 변수가 전달된다고 하면 쓸데없는 메모리의 사용이 커지게 된다. 따라서 원본값을 수정해야 할 때는 포인터나 참조자를 사용하는 것이 좋은데, 참조자를 사용하면 추가적인 메모리 사용이 없으니 잘 활용하면 좋겠지!!! 하지만 값변경이 되니 사용에 조심하라구 ^.~
- 🚨 참조자 주의사항!!
- 참조자는 무조건!!
선언과 동시에 초기화
되어야 한다. 일단 냅다 선언만 해도 되는 포인터와는 다르다
- 또한, 참조자는 어떤 변수의 참조자가 되면 절대
다른 변수의 참조자가 될 수 없다!
- 👍 참조자에 대한 정리는 여기에 너무너무 정리가 잘 되어 있으니 참고하면 좋을듯!!!
4. ex03
👀 ex03은 참조자와 포인터를 다뤄보는 과제였다. 이제 좀 OOP 과제 같아 지는 걸~!
- cpp00 평가 당시에 한 평가자분께서
const
에 대해 조언을 좀 해주셨었다. 과제 통과에 있어서 필수적인 사항은 아니었지만, 듣고 보니 참 도움이 되는 말씀이었던 터라 이번 과제를 하면서 최대한 const를 생각하면서 사용해보려 했다.
🥨 C++ 생성자 초기화
- c언어, 특히 42seoul의 Norm에 맞춘 코딩 스타일에서는 변수를 선언과 동시에 초기화 해주지 않았다. 그런데 c++의 경우에는 변수가 정의된 후에 대입 연산자를 통해 값을 제공할 경우 복사 할당되며, 선언과 동시에 초기화 되는 것보다 성능이 떨어진다고 한다.
- 변수 정의 -> 대입연산자 이용 2단계를 거치기 때문..?
- 🤔 c언어도 복사 할당인가? 찾아도 잘 모르겠다... 아니겠지..?
- C++에서 변수를 초기화하는 방법은 크게 3가지로 나눌 수 있다
- 등호를 사용하여
복사 초기화
수행 (int a = 1)
- 괄호를 사용하여
직접 초기화
수행 (int a(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 가보자고!