gugbab2's GitBook
  • Language
    • C++
      • 강의
        • C++ 언매니지드 프로그래밍
          • C++ 프로그래밍
          • 출력(Output)
          • 입력(Input)
          • bool 타입, Reference
          • 상수(const)
          • 문자열(string)
          • 파일 입출력
          • 개체지향 프로그래밍1
          • 개체지향 프로그래밍2
          • 개체지향 프로그래밍3
          • 캐스팅(형변환, casting)
          • 인라인 함수
          • static 키워드
          • 예외(Exception)
          • STL(Standard Template Library) 컨테이너(Container) - Vector
          • STL 컨테이너 - Map
          • STL 컨테이너 - Queue, Stack, Set, List
          • 템플릿(Template) 프로그래밍
          • 새로운 키워드(C++11 ~) 1
          • 새로운 키워드(C++11 ~) 2
          • 새로운 자료형
          • 새로운 STL 컨테이너
          • 스마트(smart) 포인터
          • 이동생성자 및 이동대입연산자
          • constexpr
          • Lamda Expression
      • 책
        • The C++ Programming Lanuaage
          • 2부 : 기본 기능
            • 6. 타입과 선언
            • 7. 포인터, 배열, 참조
            • 8. 구조체(struct), 공용체(union), 열거형(enum)
            • 10. 표현식
            • 11. 선택 연산
            • 12. 함수
            • 13. 예외 처리
            • 15. 소스 파일과 프로그램
          • 3부 : 추상화 메커니즘
            • 16. 클래스
            • 17. 생성, 소멸, 복사와 이동
            • 18. 연산자 오버로딩
            • 19. 특수 연산자
            • 20. 파생클래스
        • 씹어먹는 C++
          • 2. C++ 참조자(reference) 의 도입
          • 5.1 연산자 오버로딩(비교, 대입 연산자)
          • 5-2. 연산자 오버로딩(이항, 입출력, 타입변환, 증감 연산자)
          • 6-2. 가상(virtual) 함수와 다형성
          • 6-3. 가상 함수에 대한 지식들
          • 9-1. 코드를 찍어내는 틀 - C++ 템플릿(template)
          • 9-2. 가변 길이 템플릿(Variadic template)
          • 9-3. 템플릿 메타 프로그래밍 (Template Meta Programming)
          • 9-4. 템플릿 메타 프로그래밍2
          • 16.1 유니폼 초기화(Uniform Initialization)
          • 토막글 2. 람다(lambda)
    • Java
      • 강의
        • 김영한의 실전 자바 - 기본편
          • 절차 지향 vs 객체 지향
            • 절차 지향 프로그래밍
            • 객체 지향 프로그래밍
          • 변수
            • 클래스 변수 / 인스턴스 변수, 멤버 변수 / 지역 변수
            • 기본형 vs 참조형
          • 패키지
            • 패키지
            • CLI 환경에서 .java 파일 컴파일 && 실행
          • 접근 제어자
            • 접근 제어자 - 기본
            • 캡슐화
          • static
            • 자바 메모리 구조
            • static 기본
            • 스택 영역, 힙 영역
              • 스택 영역, 힙 영역 - 기본
              • 메소드가 실행될 때 어떤일이 일어나는가?
          • 상속
            • 상속 기본
          • 다형성(Pilymorphism)
            • 다형성 기본
            • 다형성의 활용
              • 다형성의 활용 - 기본
              • 다형성의 활용 - 추상클래스
              • 다형성의 활용 - 인터페이스
            • 다형성과 설계
              • 좋은 객체 지향 프로그래밍
        • 김영한의 실전 자바 - 중급1편
          • 1. Object 클래스
          • 2. 불변 객체
          • 3. String 클래스
          • 4. 래퍼, Class 클래스
          • 5. 열거형 - ENUM
          • 6. 날짜와 시간
          • 7. 중첩 클래스, 내부 클래스1
          • 8. 중첩 클래스, 내부 클래스2
          • 9. 예외 처리1 - 이론
          • 10. 예외 처리 - 실습
        • 김영한의 실전 자바 - 중급2편
          • 1. 제네릭 - Generic1
          • 2. 제네릭 - Generic2
          • 3. 컬렉션 프레임워크 - ArrayList
          • 4. 컬렉션 프레임워크 - LinkedList
          • 5. 컬렉션 프레임워크 - List
          • 6. 컬렉션 프레임워크 - 해시(Hash)
          • 7. 컬렉션 프레임워크 - HashSet
          • 8. 컬렉션 프레임워크 - Set
            • 레드 블랙 트리
          • 9. 컬렉션 프레임워크 - Map, Stack, Queue
            • 왜(?) Set 은 내부에서 Map 을 사용할까?
          • 10. 컬렉션 프레임워크 - 순회, 정렬, 전체 정리
        • 김영한의 실전 자바 - 고급 1편, 멀티스레드와 동시성
          • 프로세스와 스레드 소개
          • 스레드 생성과 실행
          • 스레드 제어와 생명 주기1
          • 스레드 제어와 생명 주기2
          • 메모리 가시성
          • 동기화 - synchronized
            • synchronized 키워드 이해도 체크
          • 고급 동기화 - concurrent.Lock
          • 생산자 소비자 문제1
          • 생산자 소비자 문제2
          • CAS - 동기화와 원자적 연산
          • 동시성 컬렉션
          • 스레드 풀과 Executor 프레임워크1
          • 스레드 풀과 Executor 프레임워크2
        • 김영한의 실전 자바 - 고급 2편, I/O, 네트워크, 리플렉션
          • 문자 인코딩
          • I/O 기본1
          • I/O 기본2
          • I/O 활용
          • File, Files
          • 네트워크 - 프로그램1
          • 네트워크 - 프로그램2
          • 채팅 프로그램
          • HTTP 서버 만들기
          • 리플렉션
          • 애노테이션
          • HTTP 서버 활용
        • 김영한의 실전 자바 - 고급3편, 람다, 스트림, 함형 프로그래밍
          • 람다가 필요한 이유
          • 람다
          • 함수형 인터페이스
          • 람다 활용
          • 람다 vs 익명 클래스
          • 메서드 참조
          • 스트림API1 - 기본
          • 스트림 API2 - 기능
          • 스트림 API3 - 컬렉터
          • Optional
          • 디폴트 메서드
          • 병렬 스트림
          • 함수형 프로그래밍
        • 기초 탄탄! 독하게 시작하는 Java - Part2: OOP 와 JVM
          • 2. 클래스 - 첫 번째
          • 3. 클래스 - 두번째
          • 4. 상속과 관계
          • 6. JVM(Java Virtual machine) 기본 이론
          • 7. JVM 과 GC 그리고 객체
          • 8. 불변 객체와 String 클래스
      • 책
        • 자바의 신
          • 변수
            • 클래스 변수(static) 사용 주의 케이스
            • Java volatile 과 Atomic 변수(+CAS)
          • 연산자
            • 비트 연산자 활용 예제
          • 배열
          • 참조 자료형
          • 상속
          • Object 클래스
          • interface, abstract class, enum
          • 예외
          • String 클래스
            • String 구조
            • String 문자열을 byte 로 변환하기
            • String 클래스에서 자주 사용되는 메서드
            • String 클래스로 살펴보는 불변(Immutable)객체
            • StringBuilder, StringBuffer
          • Nested 클래스
          • 어노테이션
            • 어노테이션 기본
            • 어노테이션의 사용
          • JVM 이해하기
            • 왜 JVM 을 사용해?
            • JVM, JRE, JDK
            • JVM 구조 이해하기
            • 클래스 로더 시스템
            • JIT(Just-In-Time) 컴파일러
            • GC(Garbage Collector)
              • GC Part.1
              • GC Part.2
              • GC 튜닝
          • java.lang
            • Wrapper 클래스
            • System 클래스
          • Generic
            • 제네릭 기본
            • 와일드카드
            • 와일드카드 GET / SET 경계
            • 와일드카드 extends / super 사용시기
            • 혼동할 수 있는 와일드카드 표현
          • Collection
            • 자료구조
              • 이진 탐색 트리 vs 레드 블랙 트리
            • Collection
            • List
              • ArrayList
              • Vector
              • Stack
              • LinkedList
            • Set, Queue
              • HashSet
              • LinkedHashSet
              • TreeSet
              • Priority Queue
              • ArrayDeque
            • Map
              • HashMap
              • Hashtable
              • LinkedHashMap
              • TreeMap
          • Thread
            • Thread 기본
            • Thread 와 관련이 많은, Synchronized
            • Thread 를 통제하는 메서드
            • ThreadGroup
          • I/O
            • InputStream, OutputStream
            • Reader, Writer
          • Serializable, NIO
            • Serializable
            • NIO (New IO)
          • 네트워크 프로그래밍
            • 네트워크 기본 & TCP 통신
            • UDP 통신
          • 람다
            • 함수형 인터페이스
            • 람다란?
        • 벨둥(Bealdung)
          • Java Concurrency
            • Java Concurrency Basics
              • Overview of the java.util.concurrent
              • Guide to the Synchronized Keyword in Java
              • Guide to the Volatile Keyword in Java
              • Guide to the java.util.concurrent.Future
              • ThreadLocal in Java
      • 그 외
        • 시스템 콜과 자바에서의 시스템 콜 사용례
        • 자바 NIO 의 동작원리 및 IO 모델
        • 함수형 인터페이스(FunctionInterface) - 자바8
  • Spring
    • 강의
      • 스프링 핵심 원리 - 기본편
        • 큰 흐름 잡기
        • 스프링 핵심 원리 이해1 - 예제 만들기
        • 스프링 핵심 원리 이해2 - 객체 지향 원리 적용
        • 스프링 컨테이너와 스프링 빈
        • 싱글톤 컨테이너
        • 컴포넌트 스캔
        • 의존관계 자동 주입
        • 빈 생명주기 콜백
        • 빈 스코프
      • 토비의 스프링6 - 이해와 원리
        • 3. 오브젝트와 의존관계1
        • 3. 오브젝트와 의존관계2
        • 4. 테스트
        • 5. 템플릿
        • 6.예외
        • 7. 서비스 추상화
    • 책
      • JSP 2.3 웹 프로그래밍
        • Servlet
        • JSP
        • 쿠키 / 세션
        • MVC 패턴
        • 실무 때 고민할 만한 부분
      • 스프링 입문을 위한 자바 객체지향의 원리와 이해
        • 자바와 절차적/구조적 프로그래밍
        • 객체지향의 4대 특성
        • 객체지향 설계의 5원칙
        • 스프링이 사랑한 디자인 패턴
        • IoC / DI
        • AOP(Aspect Oriented Programming), 관점 지향 프로그래밍
      • 토비의 스프링 3.1
        • Spring vs Spring Boot
        • 1. 오브젝트와 의존관계
          • 1.4 제어의 역전(IoC)
          • 1.5 스프링의 IoC
          • 1.6 싱글톤 레지스트리와 오브젝트 스코프
    • 그 외
      • 스프링 부트(SpringBoot) 탄생 배경
  • CS
    • DATA STRUCTURES
      • 선택 정렬(Selection Sort)
      • 버블 정렬(Bubble Sort)
      • 삽입 정렬(Insertion Sort)
    • OS
      • 강의
      • 책
        • 혼자 공부하는 컴퓨터구조 + 운영체제
          • 1. 컴퓨터 구조 시작하기
          • 2. 데이터
          • 3. 명령어
          • 4. CPU 의 작동원리
          • 5. CPU 성능 향상 기법
          • 6. 메모리와 캐시메모리
          • 7. 보조기억장치
          • 8. 입출력장치
          • 9. 운영체제 시작하기
          • 10. 프로세스와 스레드
    • NETWORK
      • 그 외
        • REST API
          • REST API
          • URI & MIME type
          • Collection Pattern
          • Collection Pattern 적용
          • Spring Web MVC 구현
        • SSL 인증 동작
        • DTO & JSON & CROS
          • DTO
          • 직렬화(Serialization)
          • Jackson ObjectMapper
          • CROS
        • Connection Timeout / Read Timeout
      • 강의
        • 외워서 끝내는 네트워크 핵심이론 - 기초
          • Internet 기반 네트워크 입문
            • Host 는 이렇게 외우자
            • 스위치가 하는 일과 비용
          • L2 수준에서 외울 것들
            • NIC, L2 Frame, LAN 카드 그리고 MAC 주소
            • L2 스위치에 대해서
            • LAN 과 WAN 의 경계 그리고 Broadcast
          • L3 수준에서 외울 것들
            • IPv4 주소의 기본 구조
            • L3 IP Packet 으로 외워라
            • 패킷의 생성과 전달 및 계층별 데이터 단위
            • 이해하면 인생이 바뀌는 TCP/IP 송, 수신 구조
            • IP 헤더 형식
            • 서브넷 마스크와 CIDR
            • Broadcast IP 주소와 Localhost
            • TTL 과 단편화
            • 인터넷 설정 자동화를 위한 DHCP
            • ARP 과 Ping(RTT : Round Trip Time)
          • L4 수준 대표주자 TCP 와 UDP
            • TCP 와 UDP 개요
            • TCP 연결 및 상태 변화
            • TCP 연결 종료 및 상태 변화
            • TCP, UDP 헤더 형식과 게임서버 특징
            • TCP 가 연결이라는 착각
            • TCP 연결과 게임버그
          • 웹을 이루는 핵심기술
            • DNS
            • URL, URI
        • 외워서 끝내는 네트워크 핵심 이론 - 응용
          • 네트워크 장치의 구조
            • 세 가지 네트워크 장치 구조
            • Inline 구조
            • Out of path 구조와 DPI 그리고 망중립
            • Proxy(클라이언트 입장) - 우회
            • Proxy(클라이언트 입장) - 보호와 감시
            • Reverse Proxy(서버 입장)
          • 인터넷 공유기의 작동 원리
            • 공유기 개요
            • Symmetric NAT
            • Full Cone 방식
            • Restricted Cone, Port Restricted Cone
            • 포트 포워딩
            • UPnP 와 NAT
          • 부하분산 시스템 작동 원리
            • L4 부하분산 무정지 시스템
            • 대규모 부하분산을 위한 GSLB
          • VPN과 네트워크 보안 솔루션
            • PN 과 VPN
            • IPSec VPN 과 터널링 개념
            • VPN 과 재택근무
        • 외워서 끝내는 SSL 과 최소한의 암호기술
          • 기초이론
            • Checksum (검사합)
            • Hash
          • 암호기술에 대한 이해
            • 대칭키
            • 비대칭키
          • PKI 시스템과 인터넷
            • 인터넷을 위한 비대칭키 체계
            • 공개키 신뢰를 위한 검증체계
            • 웹서비스와 공인인증서
      • 책
        • 그림으로 배우는 네트워크 원리
          • 1. 네트워크 기본
          • 2. 네트워크를 만드는 것
          • 3. 네트워크의 공통 언어 TCP/IP
    • SECURITY
      • 그 외
        • Basic Auth
        • HMAC 기반 인증
    • 그 외
      • 동기/비동기 & 블로킹/논블록킹
  • DB
    • 그 외
      • 인덱스(Index)
      • 트랜잭션(TRANSACTION)
      • 실무에서 외래키를 사용하지 않는 이유
      • ORM vs SQL Mapper
      • 문자열 vs DATE
      • EXPLAIN 명령어
    • 강의
      • Real MySQL 시즌 1
        • Part.1
          • 1강. CHAR vs VARCHAR
          • 2강. VARCHAR vs TEXT
          • 3강. COUNT(*) & COUNT(DISTINCT) 튜닝
          • 4강. 페이징 쿼리 작성
          • 5강. Stored Function
      • 토크온 41차. JPA 프로그래밍 기본 다지기
        • 1. JPA 소개
        • 2. JPA 기초와 매핑
        • 3. 필드와 컬럼 매핑
        • 4. 연관관계 매핑
        • 5. 양방향 매핑
        • 6. JPA 내부구조
        • 7. JPA 객체지향쿼리
        • 8. Spring Data JPA 와 QueryDSL 이해
    • 책
  • Software Development Methodology
    • TDD
      • 강의
        • Spring Boot TDD - 입문부터 실전까지 정확하게
          • 세션2. TDD 소개
          • 세션5. API 설계
          • 세션6. TDD 주기 첫 번째 경험
          • 세션7. TDD 주기 반복
      • 그 외
        • 단위 테스트(Unit Test) 작성의 필요성
        • JUnit5
          • A Guide to JUnit 5
          • Guide to JUnit 5 Parameterized Tests
          • AssertJ Exception Assertions
          • Testing in Spring Boot
          • Junit 과 Mockito 기반의 Spring 단위 테스트 코드 작성법
        • Code Coverage
          • Code Coverage?
    • DDD
      • 책
        • 도메인 주도 설계(Domain-Driven Design)
          • 04 - 도메인의 격리
          • 05 - 소프트웨어에서 표현되는 모델
          • 06 - 도메인 객체의 생명주기
          • 07 - 언어의 사용(확장 예제) (1)
          • 07 - 언어의 사용(확장 예제) (2)
        • 도메인 주도 개발 시작하기
          • 1. 도메인 모델 시작하기
          • 2. 아키텍처 개요
          • 3. 애그리거트
          • 4. 리포지터리와 모델 구현
            • DAO vs Repository
      • 강의
        • DDD 세레나데(NEXTSTEP)
          • 1주차
            • 도메인 주도 설계 등장 배경
            • 레거시 코드
            • 유연한 설계 - ASSERTION
          • 2주차
            • 전략적 설계 - UBIQUITOUS LANGUAGE
            • 전략적 설계 - BOUNDED CONTEXT
          • 3주차
            • 전술적 설계 - VALUE OBJECT 와 ENTITY
            • 전술적 설계 - AGGREGATE 와 REPOSITORY
            • 전술적 설계 - SERVICE
    • REFACTORING
      • 일급 컬렉션(First Class Collection) 소개와 사용해야하는 이유
  • ARCHITECTURE
    • Event Driven Architecture
  • 멘토링
    • F-Lab
      • 10회차(2024.12.29)
Powered by GitBook
On this page
  • 17.2 생성자와 소멸자
  • 17.2.1 생성자와 불변속성
  • 17.2.2 소멸자와 자원
  • 17.2.3 기반 클래스와 멤버 소멸자
  • 17.2.4 생성자와 소멸자의 호출
  • 17.2.5 virtual 소멸자
  • 17.3 클래스 개체 초기화
  • 17.3.1 생성자를 정의하지 않은 경우의 초기화
  • 17.3.2 생성자를 사용한 초기화
  • 17.3.3 기본 생성자
  • 17.3.4 초기화 식 리스트 생성자
  • 17.4 멤버 및 기반 클래스 초기화
  • 17.4.1 멤버 초기화
  • 17.4.2 기반 클래스 초기화 식
  • 17.4.3 위임 생성자
  • 17.4.4 클래스 내 초기화 식
  • 17.4.5 static 멤버 초기화
  • 17.5 복사와 이동
  • 17.5.1 복사
  • 17.5.2 이동
  • 17.6 기본 연산의 생성
  • 17.6.1 명시적 기본 설정
  • 17.6.3 기본 연산의 사용
  • 17.6.4 delete 생성자
  1. Language
  2. C++
  3. 책
  4. The C++ Programming Lanuaage
  5. 3부 : 추상화 메커니즘

17. 생성, 소멸, 복사와 이동

17.2 생성자와 소멸자

17.2.1 생성자와 불변속성

  • 자신의 클래스와 동일한 이름을 가진 멤버를 생성자라고 부른다.

  • 생성자 선언은 매개변수는 지정하지만, 리턴타입은 지정하지 않는다.

class Vector {
public : 
	Vector(int s) ;
	// ...
}
  • 생성자의 역할은 소속된 클래스의 개체를 초기화하는 것이다.

  • 초기화는 클래스 불변속성(class invariant) 를 구축해야 한다.

    • 불변속성 : 멤버 함수가(클라이언트에 의해) 호출될 때마다 클래스 인스턴스가 항상 만족해야 하는 조건의 의미한다.

  • 아래 예제에서는 불변속성은 주석으로 설명된다.

class Vector {
public : 
	Vector(int s);
	// ...
private : 
	double* elem;   // elem 은 sz 개의 double 을 가진 배열을 가리킨다. 
	int sz;         // sz 는 음수가 아니다. 
};
  • 생성자는 불변 조건을 참으로 만들어야 한다.

    • 아래 생성자는 불변속성을 구축하려고 시도하며, 그렇게 할 수 없을 경우 예외를 던진다.

    • 생성자가 불변속성을 구축할 수 없다면 아무런 개체가 생성되지 않고, 생성자는 아무런 자원의 누출이 없도록 보장해야 한다.

    • 자원이란 획득해야 하고 볼 일이 끝나면 결국에 가서는 돌려줘야 하는 것을 말한다.

Vector::Vector(int s)
{
	if(s<0) throw Bad_size{s};
	sz = s;
	elem = new double[s];
}
  • 불변속성을 지켜야 하는 이유는 다음과 같다. → 평균적으로 불변속성을 정의하는 노력은 일을 덜어준다.

    • 클래스의 동작을 명확히 하기 위해서

    • 멤버 함수의 정의를 단순화하기 위해서

    • 클래스 자원 관리를 명확하게 하기 위해서

    • 클래스 문서화를 정확히 하기 위해서(?)

17.2.2 소멸자와 자원

  • 생성자는 개체 생성시 개체를 초기화 한다. → 바꿔 말하면 멤버 함수가 동작할 수 있는 환경을 생성하는 것이다.

  • 개체 생성 시 때때로 자원 획득이 수반된다. → 파일, 잠금, 약간의 메모리 등 .. → 해당 자원은 사용 후 반드시 자원이 해제되어야 한다.

  • 개체가 소멸될 때 호출되는 것은 소멸자로 ~클래스 이름 의 형태이다.

  • 클래스는 단 하나의 소멸자만을 가질 수 있다. (매개변수를 받지 않는다)

  • 소멸자는 개체가 스택 범위를 벗어나거나, 힙 영역의 자원이 해제될 때 암시적으로 호출된다.

  • 아래 예제를 살펴보자

    • Vector v1 은 f() 에서 빠져 나오자마자 소멸된다.

    • 또한 힙 영역의 new 를 이용해 f() 에 의해 생성된 Vector 는 delete 호출에 의해 소멸된다.

class Vector {
public : 
	Vector(int s) : elem{new double[s]}, sz{s} {};
	~Vector() {delete[] elem;}
	// ...
	
private : 
	double* elem;
	int sz;
};

// ...

Vector* f(int s)
{
	Vector v1(s);
	// ...
	return new Vector(s+s);
}

void g(int ss) 
{
	Vector* p = f(ss);
	// ...
	delete p;
}
  • 만약 생성자가 충분한 메모리를 획득하지 못한다면 어떻게 될까?

    • 해당 경우에는 std::bad_alloc 예외가 new 에 의해서 던져지고, 예외 처리 메커니즘이 적절한 소멸자를 호출해서 획득된 모든 메모리를 비우게 만든다.

    • 생성자, 소멸자를 기반으로 한 이런 스타일의 자원 관리는 자원 획득의 초기화(Resource Acqusition Is Initialization) 이라고 부른다. (간단히 RAII 이라고 부른다)

  • 클래스의 소멸자를 정의하지 않는다면, 기본 소멸자를 컴파일러가 정의해준다.

17.2.3 기반 클래스와 멤버 소멸자

  • 생성자와 소멸자는 클래스 계층 구조와 연관되어 작동된다.

    • 생성자는 부모 클래스 부터 호출되고

    • 소멸자는 자식 클래스부터 호출된다.

17.2.4 생성자와 소멸자의 호출

  • 소멸자는 유효 범위를 빠져나갈 때 delete 에 의해 암시적으로 실행된다.

    • 소멸자의 명시적 호출은 불필요할 뿐 아니라, 그렇게 하면 개체 소멸 흐름을 어렵게 만들 것이다.

  • 하지만 드물게 소멸자가 명시적으로 호출되어야 하는 때가 있다. (자신이 그 안에서 커지거나 줄어들 수 있는 메모리 풀을 유지해야 하는 Vector 컨테이너의 경우)

  • 아래와 같이 생성자를 활용하는 것을 ‘위치 지정 new’ 라고 부른다.

void C::push_back(const X& a)
{
	// ...
	new(p) X{a};   // 주소 p에 있는 a 값으로 x 를 복사 생성한다. 
	// ...
}
  • 반대로 하나의 원소를 삭제하고 싶다면, 컨테이너는 소멸자를 호출해야 한다.

    • 이런 표기법은 정상적으로 소멸되는 개체에 대해서는 절대로 사용되어서는 안된다.

void C::pop_back()
{
	// ...
	p->~X();   // 주소 p 에 있는 X 를 소멸시킨다. 
}
  • 개체의 소멸을 방지하고 싶다면, 소멸자를 private 로 선언하면 된다.

class Nonlocal {
public : 
	//...
	void destroy() {delete this;}   // 명시적 소멸
private : 
	// ...
	~Nonlocal();                    // 암시적으로 소멸시키지 않는다.
};

void user()
{
	Nonlocal x;                     // error : Nonlocal 을 소멸시킬 수 없다. 
	Nonlocal* p = new Nonlocal;     // OK
	//...
	delete p;                       // error : Nonlocal 을 소멸시킬 수 없다. 
	p.destroy();                    // OK
}

17.2.5 virtual 소멸자

  • virtual 소멸자가 필요한 이유는 대개 기반 클래스가 제공하는 인터페이스를 통해 조작되는 개체는 해당 인터페이스를 통해 delete 처리 되기 때문이다.

  • 일반적으로 내가 만드는 클래스를 누군가 상속받아 사용할 수도 있기 때문에, virtual 소멸자는 기본적으로 정의해주자.

class Shape {
public : 
	// ...
	virtual void draw() = 0;
	virtual ~Shape();
};

class Circle : public Shape {
public :
	// ...
	void draw();
	~Circle();
	// ...
}

// ...

void user(Shape* p){
	p->draw();   // 적절한 draw() 를 호출한다. 
	// ...
	delete p;    // 적절한 소멸자를 호출한다. 
}

17.3 클래스 개체 초기화

17.3.1 생성자를 정의하지 않은 경우의 초기화

  • 기본 제공 타입에 대해서는 생성자를 정의할 수 없지만, 적절한 타입의 값으로 초기화 할 수 있다.

int a {1};
char* p {nullptr};
  • 사용자 정의 타입(클래스, 구조체)에 대해서는 컴파일러가 기본적으로 생성해주는 생성자를 호출해 개체를 초기화 할 수 있다.

    • 멤버 단위 초기화

      • 해당 생성자는 {} 로 호출한 경우에만, 사용 가능하며 멤버 변수가 public 로 선언되어 있어야 한다. (캡슐화 문제때문에, 아마 잘 사용되지는 않을 것 같다)

    • 복사 초기화

    • 기본 초기화

      • 기본 생성자를 사용한 초기화의 경우 {} 로 초기화 되며, 멤버 변수가 타입의 기본값으로 초기화 된다.

  • 생성자를 {} 로 호출하지 않을 시, 초기화를 하지 않는 등 예기치 않는 방식으로 동작할 수 있기 때문에, {} 사용을 권장한다. → () 로 호출하는 경우, 함수 호출과 모양이 동일하기 때문에, 함수를 호출할 수도 있다.

struct Work {
	string author;
	string name; 
	int year;
};

Work s9 {"hoo",
				"notion",
				1997 
		};                        // 멤버 단위 초기화
Work currently_playint{s9};   // 복사 초기화
Work none{};                  // 기본 초기화 {{}, {}, {}}

17.3.2 생성자를 사용한 초기화

  • 사용자가 정의한 생성자를 통해서 개체의 초기화가 이루어 질 수도 있다.

  • 생성자는 종종 해당 클래스에 대한 불변 속성을 구축하거나 그것을 위해 필요한 자원들을 획득하기 위해서 사용된다. (당연한 말씀 ..)

  • 사용자가 생성자를 정의하게 되면 컴파일러는 기본 생성자를 만들지 않는다.

  • 그렇지만 복사 생성자는 사라지지 않기 때문에 복사 될 수 있다. (얕은 복사)

struct Y : X {
	X m{0};                    // Y 에 있는 멤버 X.m 에 대해 기본 초기화 식을 제공한다. 
	Y(int a) : X{a}, m{a} {}   // 기반 클래스와 멤버를 초기화한다.
	Y() : X{0} {}              // 기반 클래스와 멤버를 초기화한다. 
};
X g{1};

void f(int a) 
{ 
	X def {};                // error : X 에 대한 기본 값이 없다.
	X def2 {};               // OK : 기본 생성자 사용
	X* p {nullptr};          // 지역 변수를 초기화한다. 
	X var {2};               // 지역 변수를 초기화한다. 
	p = new X{4};            // 힙 공간의 개체를 초기화 한다. 
	X a[] {1,2,3};           // 배열 원소를 초기화한다. 
	vector<X> v{1,2,3,4};    // vector 원소를 초기화한다. 
}
  • 추가적으로 {} 초기화를 사용하면 변수 초기화부터 클래스 멤버 초기화까지 모든 초기화 상황에서 동일한 초기화 규칙이 적용된다.

  • = 와 () 를 사용한 초기화는 균일하지 않다.

    • 다행히 = 와 () 를 사용한 초기화는 균일하지도 않고,

    • 그것에 대한 예제가 모호하다! (때문에, 오류를 알아차리기 쉽다)

struct Y : X { 
	X m;
	Y(int a) : X(a), m=a {};   // 문법 오류 : 멤버 초기화에 = 를 사용할 수 없다. 
};
X g(1);

void f(int a)
{
	Xdef();                 // X를 반환하는 함수?!
	X* p(nullptr);          // 지역 변수를 초기화한다. 
	X var = 2;              // 지역 변수를 초기화한다. 
	p = new X = 4;          // 문법 오류
	X a[] (1,2,3);          // 문법 오류
	vector<X> v(1,2,3,4);   // 문법 오류
}
  • 또한 통상적인 오버로딩 해결 규칙이 적용된다.

struct s {
	S(const char*);
	S(double *);
};

S s1 {"test"};           // S::S(const char*)
S s2 {new double{1.0}};  // S::S(double*)
S s3 {nullptr};          // 어떤 것을 호출해야 할 지 모호하다 .. 

17.3.2.1 생성자에 의한 초기화

  • {} 를 사용하게 되면, 멤버 단위 초기화 or 초기화 식 리스트 초기화를 사용할 수 있게 된다.

  • {} 초기화의 사용은 C++11 에 와서야 가능하게 되었으므로 과거 코드는 () 와 = 를 많이 사용한다.

struct S1 {
	int a,b;
};

struct S2 {
	int a,b;
	S2(int aa = 0, int bb = 0) : a(aa), b(bb) {}
};

S1 x11(1,2);   // error : 생성자가 없다. 
S1 x12{1,2};   // ok : 멤버 단위 초기화

S1 x13(1);     // erorr : 생성자가 없다.  
S1 x14{1};     // ok : x14.b 는 0이 된다. 

S2 x21(1,2);   // ok : 생성자를 사용 
S2 x22{1,2};   // ok : 생성자를 사용 

S2 x23(1);     // ok : 생성자와 하나의 기본 인자를 사용한다. 
S2 x24{1};     // ok : 생성자와 하나의 기본 인자를 사용한다. 
  • vector 과 같이 생성자 호출이 어려운 경우를 제외하고는, () 표기를 사용해야 할 논리적인 이유를 찾기가 어렵다!

vector<int> v1 {77};  // 77 값을 가진 하나의 원소 
vector<int> v2 (77);  // 기본 값 0 을 가진 77 개의 원소

17.3.3 기본 생성자

  • 매개 변수 없이 호출될 수 있는 생성자를 기본 생성자(default constructor) 라고 부른다.

  • 기본 생성자는 아무 인자도 지정되지 않거나, 빈 초기화 식이 제공될 경우 사용된다.

Vector v1;     // ok
Vector v2 {};  // ok
  • 기본 인자는 인자를 받아들이는 생성자를 기본 생성자로 만들 수 있다.

class String {
public :
	String(const char* p = "");   // 기본 생성자 
};

String s1;    // ok
String s2{};  // ok

17.3.4 초기화 식 리스트 생성자

  • std::initializer_list 타입의 단일 인자를 받아들이는 생성자를 초기화 식 리스트 생성자(initializer-list constructor) 라고 부른다.

  • 표준 라이브러리 컨테이너(vector, map) 는 초기화 식 리스트 생성자를 가진다.

vector<double> v = {1,2,3,.456, 99.00};
list<pair<string, string>> language = {
	{"Nygarrd", "Simula"}, {"Richards", "BCPL"}, {"Ritchie", "C"}
};

map<vector<string>, vector<int>> years = {
	{{"Maurice", "Vincent", "Wilkes"}, {1913, 1945, 1967, 2000} },
	{{"Martin", "Richards"}, {1982, 2003, 2007} },
	{{"David", "John", "Wheeler"}, {1927, 1947, 1951, 2004} }
};
  • {} 리스트를 받아들이기 위해 메커니즘은 std::initializer_list<T> 타입의 인자를 받아들이는 함수다.

void f(initializer_list<int>); 

f({1,2});
f({23,345,4567,56789});
f({});   // 빈 리스트
f{1,2};  // error : 함수 호출 () 가 누락되어 있다. 

17.3.4.1 initializer_list 생성자 디테일

  • 하나의 클래스에서 여러개의 생성자가 있을 경우 통상적인 오버로딩 규칙에 의해 인자 집합에 맞는 생성자를 사용한다.

  • 생성자 선택에 있어서는 기본 생성자, 초기화 식 리스트 생성자가 우선권을 갖는다.

    • 만약 기본 생성자, 초기화 식 리스트 생성자 모두 호출 가능하다면 기본 생성자를 우선적으로 사용한다. → 빈 리스트를 가지고 기본 생성자 외 다른 생성자를 호출하는 것 자체가 말이 안된다.

    • 초기화 식 리스트 생성자와 일반적인 생성자 모두 호출 가능하다면 초기화 식 리스트 생성자를 우선적으로 사용한다.

      vector<int> v1 {1}; 
      vector<int> v2 {1,2};
      vector<int> v3 {1,2,3};
    • 만약 한개 또는 두개의 정수 인자를 받아들이는 생성자를 호출하고 싶다면 () 표기법을 사용해야 한다. → initailizer_list 인자를 받는 생성자가 있다면, 컴파일러가 최선을 다해서 해당 생성자와 매칭시키려고 노력한다는 의미이다.

      vector<int> v1(1);
      vector<int> v2(1,2);

17.3.4.2 initializer_list 의 사용

  • initializer_list<T> 인자를 가진 함수는 멤버 함수 begin(), end(), size() 를 이용해서 해당 인자에 접근할 수 있다. → initializer_list<T> 는 값으로 전달된다.

void f(initializer_list<int> args)
{
	for(int i=0; i!=args.size(); ++i)
		cout << args.begin()[i] << "\\n";
}

// 위 결과와 동일하다. 
void f(initializer_list<int> args)
{
	for(auto p=args.begin(); p!=args.end(); ++p)
		cout << *p << "\\n";
}

void f(initailizer_list<int> args)
{
	for(auto x : args)
		cout << x << "\\n";		
}
  • initializer_list 의 원소는 불변적이다. 그들의 값을 변경하려는 시도는 꿈도 꾸지 말아라!

int f(std::initializer_list<int> x, int val)
{
	*x.begin() = val;   // error : 초기화 식 리스트 원소의 값을 변경하려는 시도!
	return *x.begin*.   // ok
}

17.3.4.3 직접적 초기화와 복사 초기화

  • 컨테이너 생성자 중 일부는 명시적일 수 있고, 일부는 아닐 수 있다. vector 가 그런 예 중 하나이다. → std::vector<int>(int) 는 explicit 이지만, std::vector<int>(initializer_list<int>) 는 그렇지 않다.

  • 아래는 () 를 사용해 초기화를 하는 예제이다.

	vector<double> v1(7);   // ok : v1 은 7개의 원소를 가진다. {} 가 아닌 () 을 사용한다.
vector<double> v2 = 9;    // error : int 에서 vector 변환은 불가하다. 

void f(const vector<double>&);
void g()
{
	v1 = 9;   // error : int 에서 vector 로의 변환은 불가하다. 
	f(9);     // error : int 에서 vector 로의 변환은 불가하다. 
}
  • () 를 {} 로 대체함으로써 결과는 달라진다.

vector<double> v1 {7};   // ok : v1 은 1개의 원소를 갖는다.(값 7을 가진) 
vector<double> v2 = {9}; // ok : v2 는 1개의 원소를 갖는다.(값 9를 가진) 

void f(const vector<double>&);
void g() 
{
	v1 = {9};   // ok : 이제 v1 은 1개의 원소를 가진다.(값 9를 가진) 
	f({9});     // ok : f는 리스트 {9} 로 호출된다. 
}

17.4 멤버 및 기반 클래스 초기화

17.4.1 멤버 초기화

  • 멤버의 생성자를 위한 인자는 멤버 초기화 식 리스트에서 지정되는데, 멤버 초기화 식 리스트는 소속 클래스의 생성자에 대한 정의가 들어 있다.

  • 멤버 초기화는 명시적으로 사용하는 것이 가독성 측면에서 이점이 있다.

Club::Club(const string& n, Date fd)
	: name{n}, member{}, officer{}, founded{fd}
{
	// ...
}

// 위 클래스 정의와 동일하게 동작한다. 
Club::Club(const string& n, Date fd)
	: name{n}, founded{fd}
{
	// ...
}

17.4.1.1 멤버 초기화와 대입

  • 멤버 변수를 초기화 함에 있어서 2가지 방법이 있다.

    • 초기화 리스트를 사용한다. → 생성과 동시에 초기화가 이루어 진다.

    • 생성자 범위에서 대입을 한다. → 생성 후 초기화가 이루어진다. → 타입이 참조자 타입이거나 const 타입이라면 생성과 동시에 초기화가 이루어져야 하므로 해당 방법은 컴파일 오류가 발생한다.

  • 생성자에서 멤버 초기화 시 초기화 리스트를 사용하는 것이 성능적으로도 이점이 있다. → 초기화 리스트를 사용하면 초기화와 대입이 한번에 이루어진다.

class Person {
	// ...
};

Person::Person(const string& n, const string& a)
	: name{n}      // name 은 n의 사본으로 초기화된다. 
{
	address = a;   // address 는 우선 빈 문자열로 초기화 되고, a 의 사본이 대입된다. 
}

17.4.2 기반 클래스 초기화 식

  • 클래스 상속 시 기반 클래스가 초기화 식을 요구하면 해당 초기화 식은 생성자 내의 기반 클래스 초기화 식으로써 제공되어야 한다. → 그렇게 하고 싶다면 기본 생성을 명시적으로 지정할 수 있다.

  • 멤버에서와 마찬가지로 초기화 순서는 선언 순서이고, 그 순서로 초기화 식을 지정하는 것을 추천한다.

class B1{B1();};   // 기본생성자
class B2{B2(int)}; // 기본생성자가 없다.

struct D1 : B1, B2{
	D1(int i) : B1{}, B2{i} {}
};

struct D2 : B1, B2{
	D2(int i) : B2{i} {}  // B1 {} 은 암시적으로 사용된다. 
};

struct D3 : B1, B2{
	D3(int i) {}          // error : B1은 암시적으로 초기화 되지만, B2 는 int 초기화를 요구한다. 
}; 

17.4.3 위임 생성자

  • 두 개의 생성자가 똑같은 동작을 수행하게 하고 싶다면 공통된 동작을 수행할 함수를 정의할 수 있다.

class X {
	int a;
	validate(int x) {...}
public : 
	X(int x) {validate(x);}
	X() {validate(42);}
	X(string s} {validate(to<int>(s);}
}
  • 또는 생성자를 또 따른 생성자의 관점에서 정의하는 것이다. → 이러한 생성자는 위임 생성자(delegate constructor) 라고 불린다.

class X {
	int a;
public :
	X(int x) {...}
	X() : X{42} {}
	X(string s) : X{to<int>(s)} {}
}
  • 개체는 생성자가 완료될 때까지는 생성되지 않은 것으로 간주된다. → 위임 생성자를 사용할 때는 위임된 생성자가 완료되면 개체가 생성될 것으로 간주된다.

17.4.4 클래스 내 초기화 식

  • 클래스 선언 안에서 비 static 데이터 멤버에 대한 초기화 식을 지정할 수 있다. → 구문 분석 및 이름 탐색과 관련된 상당히 모호한 기술적인 이유 때문에, {} 과 = 초기화 식은 클래스 내 멤버 초기화 식에서 사용될 수 있지만, () 표기법은 사용될 수 없다. (함수 호출과 혼동된다)

class A {
public :
	int a {7}; 
	int b = 77;
}
  • 초기화 리스트를 통해서도 멤버 변수를 초기화 할 수 있는데 이 때 중복되는 초기화 값이 있다면 멤버 변수 선언과 동시에 초기화 해 공통값이 명확하게 들어날 수 있도록 사용할 수 있다.

class A {
public : 
	A() : a{7}, b{5} {}
	A(int a_val) : a{a_val}, b{5} {} 
	A(D d) : a{7}, b{g(d)} {}
	// .. 
private :
	int a, b;
	HashFunction algorithm {"MD5"};
	string state {"Constructor run"};
}

17.4.5 static 멤버 초기화

  • static 클래스 멤버는 각 클래스 개체의 일부가 아니기에 클래스가 메모리에 로드 될 때 초기화 된다. → static 멤버는 클래스 정의 외부에서 초기화해야 합니다.

class Node {
   // ...
   static int node_count;   // 선언
};

int Node::node_count = 0;   // 정의 
  • 하지만 몇가지 특수한 상황에서는 클래스 선언에서 static 멤버를 초기화 할 수 있다.

    • static const 정수형 멤버 변수는 클래스 정의 내부에서 초기화 할 수 있습니다.

    #include <iostream>
    
    class MyClass {
    public:
        static const int staticConstVar = 10; // 클래스 내부에서 초기화
    };
    
    int main() {
        std::cout << "Static Const Variable: " << MyClass::staticConstVar << std::endl;
        return 0;
    }
    • C++11부터는 constexpr 키워드를 사용하여 클래스 내부에서 정적 멤버 변수를 초기화할 수 있습니다.

    #include <iostream>
    
    class MyClass {
    public:
        static constexpr int staticConstexprVar = 20; // 클래스 내부에서 초기화
    };
    
    int main() {
        std::cout << "Static Constexpr Variable: " << MyClass::staticConstexprVar << std::endl;
        return 0;
    }
    

17.5 복사와 이동

17.5.1 복사

  • 클래스 X 에 대한 복사는 두 개의 연산으로 정의된다.

    • 복사 생성자 X(const X&)

    • 복사 대입연산자 X& operator=(const X&)

  • 복사 생성자는 개체를 변경하지 않고 개체의 사본을 만들어주는 것이다. 마찬가지로 복사 대입연산자도 동일하게 동작한다. → 복사 생성자와 복사 대입연산자는 보통 모든 비 static 멤버를 복사해야 한다.

template<typename T>
class Matrix{ 
	array<int, 2> dim;   // 2차원 
	T* elem;             // T 타입의 dim[0]*dim[1] 개의 원소를 가리키는 포인터 
	
public : 
	Matrix(int d1, int d2) : dim{d1, d2}, elem{new T[d1*d2]} {} 
	int size() const {return dim[0]*dim[1];}
	
	Matrix(const Matrix&};              // 복사 생성자 
	Matrix& operator=(const Matrix&);   // 복사 대입연산자 
	
	Matrix(Matrix(Matrix&&);            // 이동 생성자 
	Matrix& operator=(Matrix&&);        // 이동 대입연산자 
	~Matrix() {delete[] elem;}
	// ...
};
  • 복사 생성자와 복사 대입연산자의 차이는 다음과 같다.

    • 복사 생성자 : 초기화 되지 않은 메모리를 초기화 한다.

    • 복사 대입 연산자 : 이미 생성되어 자원을 소유하고 있을 수 있는 개체를 다룬다.

17.5.1.1 기본 생성자에 대해 조심할 사항

  • 복사 연산을 작성할 때는 반드시 모든 기반 클래스와 멤버를 복사해야 한다.

  • 아래의 경우 s2 를 복사하는 것을 까먹었기 때문에, 기본 초기화가 될 것이다. → 대규모의 클래스의 경우 생각보다 이런 경우가 많아질 수 있다.

class X {
	string s;
	string s2; 
	vector<string> v;
	
	X(const X& a)
		: s{a.s}, v{a.v}
	{};
};

17.5.1.2 기반 클래스의 복사

  • ‘복사’ 라는 관점에서는 기반 클래스는 하나의 멤버일 뿐이다. 즉 파생 클래스의 개체를 복사하려면 그것의 기반 클래스를 복사해야 한다.

struct B1 {
	B1(); 
	B1(const B1&);
	// ...
};

struct B2 {
	B2(int);
	B2(const B2&);
	// ...
};

struct D : B1, B2 {
	D(int i) : B1{}, B2{i}, m1{m}, m2{2*i} {}
	D(const D& a) : B1{a}, B2{a}, m1{a.m1}, m2{a.m2} {} 
	B1 m1;
	B2 m2;
};

D d{1};       // int 인자로 생성한다. 
D dd {d};     // 복사 생성자 호출 

17.5.1.3 복사의 의미

  • 올바른 복사 연산의 조건을 충족하기 위해서는 아래 두가지 기준을 통과해야 한다. → 매우 당연한 이야기지만, 기본적인 조건들을 만족해야만 단단한 프로그램을 만들 수 있다.

    • 등가성(equivalence) : x=y 후에 x, y 를 사용할 때 동일한 결과를 산출해야 한다. → x=y 는 x==y 를 뜻할 수 있어야 한다.

    • 독립성(independence) : x=y 후에 , x 에 대한 연산은 암시적으로 y의 상태를 변경하지 않아야 한다.

  • 얕은 복사의 경우 두 개체를 공유 상태에 놓이게 하며, 독립성을 해친다. → 엄청난 혼돈과 오류의 가능성을 내포하고 있다.

  • 깊은 복사를 통해서 개체의 전체 상태를 복사할 수 있지만, 이는 필요하지 않은 복사가 이루어 질 수 있기 때문에, 이동 연산을 사용하기도 한다.

  • 이동 연산은 복잡성을 더하지 않고, 복사를 최소화 해준다!

17.5.2 이동

  • a 의 값을 b 로 옮기는 전통적인 방법은 복사하는 것이다.

  • 하지만 논리적인 관점에서 보게 되면 그렇지 않다.

  • 아래 swap 예제를 살펴보자

    • tmp 를 초기화 한 후, a 값의 두 개의 사본이 나온다.

    • a 를 초기화 한 후 b 값의 두 개의 사본이 나온다.

    • b 를 초기화 한 후 tmp 값의 두 개의 사본이 나온다.

template<typename T> 
void swap(T& a, T& b) 
{
	const T tmp = a;   // a 의 사본을 tmp 에 넣는다. 
	a = b;             // b 의 사본을 a 에 넣는다. 
	b = tmp;           // tmp 의 사본을 b 에 넣는다. 
};
  • 아래 예제를 살펴보자

    • s1 이 1000 개의 문자를 갖고 있다면 어떻게 될까?

    • vs1 이 1000 개의 원소를 갖고 있다면 어떻게 될까?

    • m1 이 double 로 이뤄진 1000*1000 행렬이라면 어떻게 될까? → 복사하는 비용이 무쟈게 크다!!

void f(string& s1, string& s2, vector<string>& vs1, vector<string>& vs2, 
Matrix& m1, Matrix& m2)
{
	swap(s1, s2);
	swap(vs1, vs2);
	swap(m1, m2);
}
  • swap 은 string, vector 에 대해서 이런 오버헤드를 피할 수 있도록 신중하게 설계되었다.

  • 위 예제에서 문제는 우리는 아무것도 복사하기를 원치 않는다는 점에 있다. → 단지 값의 쌍을 교환하고 싶었을 뿐이다.

  • 이러한 문제를 해결하기 위한 방법이 이동 연산이고, 이동 연산은 소유권을 이전한다.

    • 사용자가 복사와 관련된 성능적 문제를 피할 수 있게 C++ 은 이동의 개념을 직접적으로 지원한다.

    • 이를 위해 이동 생성자와, 이동 대입을 정의할 수 있다.

  • 이동 연산의 바탕이 되는 것은 좌변 값과 우변 값을 분리해서 처리하자는 것이다.

    • 복사 생성자와 복사 대입 연산자는 좌변 값을 사용하는 반면,

    • 이동 생성자와 이동 대입 연산자는 우변 값을 사용한다.

  • 다음 예제를 살펴보자

    • 이동 생성자는 기존 값을 기반으로 멤버 변수를 초기화 하고 기존 값을 기본값으로 초기화 해준다.

    • 이동 대입 연산자는 그냥 swap 을 통해 처리할 수 있다. → 기존 값이 곧 소멸 예정이라는 전재하에 swap 으로 처리할 수 있다.

template<typename T>
class Matrix{ 
	array<int, 2> dim;   // 2차원 
	T* elem;             // T 타입의 dim[0]*dim[1] 개의 원소를 가리키는 포인터 
	
public : 
	Matrix(int d1, int d2) : dim{d1, d2}, elem{new T[d1*d2]} {} 
	int size() const {return dim[0]*dim[1];}
	
	Matrix(const Matrix&};              // 복사 생성자 
	Matrix& operator=(const Matrix&);   // 복사 대입연산자 
	
	Matrix(Matrix(Matrix&&);            // 이동 생성자 
	Matrix& operator=(Matrix&&);        // 이동 대입연산자 
	~Matrix() {delete[] elem;}
	// ...
};

// ... 

// 이동 생성자 
template<typename T> 
Matrix<T>::Matrix(Matrix&& a) 
	: dim{a.dim}, elem{a.elem}
{
	a.dim = {0,0};
	a.elem = nullptr;
}

// 이동 대입 연산자 
template<typename T>
Matric<T>& Matrix<T>::operator=(Matrix&& a) 
{
	swap(dim, a.dim);
	swap(elem, a.elem);
	return *this;
}
  • 이동 생성자와 이동 대입 연산자는 const 가 아닌 값을 매개 변수로 받아들인다. → 이동이라는 개념을 완성하기 위해서는 기존 값을 기본값으로 초기화해야 한다.

  • 복사 연산자는 예외를 던질 가능성이 있지만, 복사 생성자는 예외를 던질 가능성이 거의 없다.

    • 복사 연산자는 힙 메모리 할당 시 메모리 할당 실패(std::bad_alloc)와 같은 예외가 발생할 수 있다.

    • 이동 연산자는 리소스를 단순히 포인터나 참조를 통해 이전하는 작업이므로 예외를 던질 가능성이 적다. → 때문에 noexcept 키워드를 붙여준다. → 복사 연산자에 비해서 자원 컨트롤이 매우 간단하고 효율적이다.

  • 컴파일러는 언제 복사 연산이 아닌, 이동 연산을 사용할 수 있는지 알까?

    • 이를 위해서 몇가지 규칙이 있지만, 대부분 std::move() 함수를 사용해 명시적으로 알려주어야 한다.

    • std::move(a) 는 “a 에 대한 우변값 참조자를 주세요.” 라는 의미이다.

    template<typename T> 
    void swap(T& a, T& b) 
    {
    	T tmp = std::move(a);
    	a = std::move(b);
    	b = std::move(temp);
    }

17.6 기본 연산의 생성

  • 클래스를 생성하게 되면 컴파일러는 암시적으로 다음의 기본 연산들을 제공한다.

    • 기본 생성자

    • 복사 생성자

    • 복사 대인

    • 이동 생성자

    • 이동 대입

    • 소멸자

17.6.1 명시적 기본 설정

  • 개발자가 생성하지 않더라도 암시적으로 기본 연산들은 생성하지만, 명시적으로 보고싶은 경우도 있다.

  • default 를 통해서 명시적으로 선언해 줄 수 있다.

class gslice {
	valarray<size_t> size;
	valarray<size_t> stride;
	valarray<size_t> dl;
public : 
	gslice() = default; 
	~gslice() = default;
	gslice(const gslice&) = default;
	gslice(gslice**&&) = default;**
	gslice& operator=(const gslice**&) = default;**
	gslice& **operator=(**gslice**&&) = default;**
};

17.6.3 기본 연산의 사용

  • 이번 절에서는 복사, 이동, 소멸자가 어떻게 논리적으로 연결되어 있는가를 살펴보자

17.6.3.1 기본 생성자

  • 일반적으로 인자를 받는 생성자를 정의하면, 컴파일러가 기본 생성자를 생성하지 않는다.

  • 그럼에도, 기본 생성자를 컴파일러가 생성해주기를 원한다면 default 키워드를 통해 선언할 수 있다.

struct Y {
	string s;
	int n;
	Y(const string& s);
	Y() = default;
};

17.6.3.2 불변속성 유지

  • 대부분의 클래스는 불변속성을 가진다. → 불변속성 : 클래스 개체가 항상 참으로 유지되는 조건

  • 그렇다면 복사와 이동 연산에서 그것이 유지돼야 하고, 소멸자는 관련된 자원을 모두 비울 수 있어야 할 것이다.

  • 하지만, 컴파일러가 모든 경우마다, 프로그래머가 무엇을 불변속성이라고 생각하는지는 알 수 없는 노릇이다.

  • 억지로 꾸민듯한 아래 예제를 보자

    • 프로그래머가 불변속성을 주석에서 서술했지만, 컴파일러는 주석을 읽지 못한다.

    • 게다가 프로그래머는 해당 불변속성이 어떻게 구축되고 유지될 수 있는지에 대한 힌트를 남기지 않았다.

struct Z {
   // 불변속성 -
   // my_favorite 는 elem 에서 내가 좋아하는 const 의 색인이다. 
   // 가장 큰 값은 elem 에서 가장 높은 값을 가진 원소를 가리킨다. 
   vector<int> elem;
   int my_favorite;
   int* largest;
};
  • 때문에 불변속성을 가지기 위한 규칙은 다음과 같다.

    • 생성자에서 불변속성을 구축한다.(필요한 경우 자원 획득까지 포함해서)

    • 복사와 이동 연산에 대해 불변속성을 유지한다.

    • 소멸자에서 필요한 모든 마무리를 처리한다.(필요한 경우 자원 해제를 포함해서)

17.6.4 delete 생성자

  • 컴파일러가 생성자를 생성하지 않도록 delete 선언 할수도 있다.

  • 다음의 코드를 보자

class Base {
   // ...
   Base operator=(const Base&) = delete;
   Base(const Base&) = delete;
   
   Base& operator=(Base&&) = delete;
   Base(Base&&) = delete;
};
Previous16. 클래스Next18. 연산자 오버로딩

Last updated 11 months ago