ch. 7, 8, 9 요약정리
C++ 프로그래밍은 도로가 없는 곳을 운전하는 것과 같다
- 원하는 곳으로 마음껏 갈 수 있지만 책임은 본인이져야 함
성능>>>안정성
7장에서는 C++의 자유로움 안에서 특히 문제가 발생하기 쉬운 포인터에 대해서 집중적으로 다룹니다
new와 malloc()의 차이
malloc()은 메모리를 동적으로 할당하는것만이 목표이므로 초기값 지정X
new는 생성자를 자동으로 호출
malloc() 왜씀?
메모리 크기를 키워야 할때, 재할당해야 할때 malloc()은 realloc() 함수로 재할당 가능
new는 새로 할당->복사-> 해제 해야함
# realloc()은 매우 위험하니 쓰지 맙시다
포인터에 대한 타입 캐스팅
포인터는 단지 메모리 주소에 불과해서 타입을 엄격히 따지지 않는다
포인터의 타입은 C 스타일 캐스팅으로 얼마든지 바꿀 수 있다
#위험하니 정적 캐스팅( static_cast<T>)을 사용합시다. 관련없는 타입으로 캐스팅할시 에러발생
하지만 상속관계의 포인터는 에러발생이 안되므로 동적 캐스팅(dynamic_cast<T>)을 사용하는게 더 안전
포인터 연산
포인터값을 1만큼 증가시키면? 자료형만큼 이동함
예) int로 선언된 포인터에 +1 하면 1올라가는게 아니라 int(4)만큼 주소가 이동함
#주소가 1단위로 이동해봤자 의미가 없으므로
가비지 컬렉션
C++엔 없다
이게 제공되면 프로그래머가 할당된 메모리를 직접 해제할 일없이 자동으로 관리해줌
다만 소멸자의 호출시점이 불분명해져 혼란스러울 수 있다
흔히 발생하는 메모리 관련 문제
1. 데이터 버퍼 과소 할당, 경계를 벗어난 메모리 접근
2. 메모리 누수
3. 중복삭제, 잘못된 포인터
스마트 포인터
메모리 누수를 방지하기 위해 적극 사용합시다
unique_ptr <- 스코프가 벗어나면 자동으로 메모리 해제
shared_ptr <- 스코프를 벗어나거나 리셋될때 마지막으로 가리키는 스마트 포인터가 그 리소스를 해제해 줌
weak_ptr <- 직접 참조를 할 수도 없고 shared_ptr에 영향도 끼치지 않음(shared의 순환참조 문제 해결)
8장에서는 기본적인 클래스의 기능을 복습합니다
객체의 라이프 사이클 - 생성, 소멸, 대입
#생성자를 직접 호출하려하면 안된다. 선언문과 분리해서 초기화 하려해도 안됨
생성자를 직접 호출하면 내부적으로 임시 객체가 만들어져 의도한 객체의 초기화가 제대로 안됨
생성자를 하나라도 직접 만들어두면 디폴트 생성자를 자동으로 만들어주지 않는다
디폴트 생성자를 만들기 귀찮을때 default 키워드를 붙여놓으면 만들어준다.
# delete 키워드를 넣어주면 그 생성자를 생성하지 않게 만든다
생성자 초기자
편하다
생성자 초기자로 데이터 멤버를 초기화 하는방식은 생성자 안에서 데이터멤버를 초기화 하는것과는 다름
C++에서 생성자를 호출하는시점은 객체를 구성하는 데이터 멤버를 모두 생성하고 나서 생성자를 호출함.
하지만 생성자 초기자를 이용하면 데이터 멤버를 생성하는 과정에서 초깃값을 설정할수 있음
-> 훨씬 효율적
이 차이 때문에 반드시 생성자 초기자로 초기화 해야 하는 경우들이 있음
const 데이터 멤버 <- 한번 생성되면 값 변경이 불가하므로 생성자에서 초기화 하려하면 안됨
레퍼런스 데이터 멤버<- 가리키는 대상이 없는 레퍼런스는 생성 불가
디폴트 생성자가 정의되지 않은 객체 데이터 멤버<- 데이터 멤버가 생성될때 객체의 경우 생성자가 호출되므로
9장은 클래스와 객체에 대한 깊은 이해를 목적으로 합니다
프렌드
class A {
friend class B;
...
}
B가 A의 private protected 멤버를 참조할 수 있음
복제와 대입
복제 생성자나 대입 연산자를 직접 정의하지 않으면 컴파일러가 자동으로 만들어준다
일반적으로 기본타입에 대해서는 비트 단위 복제(얕은 복제)나 대입이 적용된다.
즉, 원본 객체의 데이터 멤버를 대상 객체로 단순히 복제하거나 대입하기만 한다.
하지만 동적으로 할당한 메모리가 있으면 문제가 발생한다.
-> 포인터가 가리키는 곳의 데이터를 복사해와야 하지만 얕은 복사로는 포인터만 복사해온다
이런 상태에서 원본 객체가 소멸되면? 댕글링 포인터 발생
# 복제 생성자와 대입 연산자는 반드시 깊은 복제를 적용해야 한다.
이동 의미론
리소스의 소유권을 다른 객체로 이동시킨다
좌측값과 우측값이란?
좌측값
이름을 가지고 있으며 메모리에 존재함
다시 사용가능
예) 변수, 배열 요소, 참조된 값....
우측값
임시로 존재하는 값
다시 사용할 수 없는 값(메모리 주소 없음)
대부분의 리터럴, 임시객체
일반적으로 좌측값에 저장됨
move() 함수는 좌측값을 우측값으로 변환시킨다. 변환된 값의 레퍼런스를 반환한다
#include <iostream>
#include <string>
int main() {
std::string str1 = "Hello";
std::string str2 = std::move(str1); // 이동 발생
std::cout << "str1: " << str1 << ", str2: " << str2 << std::endl;
}
str1의 내부 자원이 복제 없이 str2로 이동되었다
str1 을 출력하면 값이 안나옴. 이동되었기 때문
영의 규칙
객체의 멤버로 자원관리를 직접하는것이 들어가 있으면
소멸자 복사 생성자, 복사 대입 연산자, 이동 생성자, 이동 대입 연산자를 직접 구현해야됨
그럴 필요가 없게 객체를 구현하자는 원칙
스마트포인터, 백터등을 사용하자
static 메서드
특정 객체 인스턴스에 소속되어있지 않기 때문에 this 포인터 가질 수 없다
static 메서드 함수는 비 static 멤버에 접근하는 용도로 사용할 수 없다