1.1 C++의 기초
- C++는 흔히 C의 개선판 또는 C의 상위 집합으로 알려져 있다.
- C++는 원래 C에 객체지향 개념을 추가하려는 목적으로 설계했기 때문에 클래스가 추가된 C라고도 부른다.
1.1.1 프로그래밍 언어의 공식 예제 'Hello, World'
import <iostream> // C++20 이전 버전에서는 #include <iostream>
int main()
{
std::out << "Hello, Worlde!" << std::endl;
return 0;
}
+ 주석
- 주석은 프로그래머에게 전달하는 메시지며 컴파일러는 이 부분을 무시한다.
- 슬래시 두 개를 연달아 적으면 그 지점부터 그 줄 끝까지 나오는 모든 문자를 주석으로 처리한다.
- 여러 줄 주석은 /*로 시작해서 */로 끝나며, 그 사이의 모든 문자를 주석으로 처리한다.
+ 모듈 임포트
- 모듈은 예전에 헤더 파일이라 부르던 메커니즘을 완전히 대체한다. 어떤 모듈에 담긴 기능을 사용하고 싶다면 그 모듈을 import 문으로 불러오면 된다.
- C++20부터 새롭게 추가된 대표적인 기능 중 하나이다. 현재 사용하는 컴파일러에서 모듈을 지원하지 않는다면 다음 절에서 소개하는 것처럼 import라고 적힌 문장을 모두 #include 전처리기 지시자로 바꾼다.
+ 전처리 지시자
- 전처리 지시자는 C나 C++ 같은 언어에서 컴파일 전에 실행되는 명령어이다. #으로 시작하며 주로 매크로 정의, 파일 포함, 조건부 컴파일 등에 사용된다.
- 중복 인클루드를 막는 용도로 전처리 지시자를 사용하는 예는 다음과 같다.
- C++로 작성된 소스 코드를 프로그램으로 만드는 빌드 작업은 세 단계를 거친다.
- 전처리 단계 : 소스 코드에 담긴 메타 정보를 처리한다.
- 컴파일 단계 : 소스 코드를 머신이 읽을 수 있는 객체 파일로 변환한다.
- 링크 단계 : 앞에서 변환한 여러 객체 파일을 애플리케이션으로 엮는다.
- 지시자(디렉티브)는 전처리기에 전달할 사항을 표현하며, 앞에 나온 예제의 #include <iostream>처럼 # 문자로 시작한다.
- 헤더 파일의 주 용도는 소스 파일에서 정의할 함수를 선언하는 것이다.
- 함수 선언은 그 함수의 호출 방식, 매개변수의 개수와 타입, 리턴 타입 등을 컴파일러에 알려준다.
- C++20의 모듈이 등장하기 전에는 선언문을 주로 확장자가 .h인 헤더 파일에 작성했다.
- 함수 정의(구현)는 실제로 수행할 동작을 작성한다.
- 정의는 확장자가 .cpp인 소스 파일에 작성했다.
- 모듈이 추가되면서 더 이상 선언과 정의를 분리할 필요가 없게 되었다. 물론 예전처럼 분리하는 것도 가능하다.
- C에서는 표준 라이브러리의 헤더 파일을 표현할 때 대부분 .h 확장자로 표기하며, 네임스페이스는 사용하지 않는다.
- C++에서는 표준 라이브러리 헤더를 불러올 때는 확장자 .h를 생략하며, 모든 항목을 std 네임스페이스 또는 std의 하위 네임스페이스에 정의한다.
- C 표준 라이브러리 헤더를 C++에서도 사용할 수 있는데, 다음 두 가지 형태로 제공된다.
- 공식적으로 권장하는 새로운 방식 : .h 확장자 대신 c 접두어가 붙는다
- 기존 방식 : .h 확장자로 표기하고 네임스페이스를 사용하지 않는다
- C 표준 라이브러리 헤더는 import 선언문으로 불러오지 못할 수도 있다.
+ main() 함수
- 프로그램은 항상 main() 함수에서 시작한다.
- main() 함수는 int 타입의 값을 리턴하는데, 이 값으로 프로그램의 실행 상태를 표시한다.
- main() 함수 안에서는 리턴 문장을 생략해도 되는데, 그러면 자동으로 0을 리턴한다.
- main() 함수는 매개변수를 받지 않거나 다음과 같이 두 개를 받도록 작성할 수 있다.
int main(int argc, char* argv[])
- argc는 프로그램에 전달할 인수의 개수를 지정하고, argv는 전달할 인수의 값을 담는다.
- argv[0]에는 프로그램 이름이 담기는데, 공백 스트링으로 지정될 수도 있어서 프로그램 이름을 참조하려면 이 값보다는 플랫폼에서 제공하는 기능을 사용하는 것이 좋다. 실제 매개변수는 인덱스 1부터 시작한다.
+ I/O 스트림
- 스트림이란 데이터의 흐름을 추상화한 개념으로, 입출력을 다룰 때 사용되는 데이터 흐름을 의미한다. 보통 프로그램과 외부 장치간의 데이터를 주고받을 때 사용된다.
- 출력 스트림은 데이터를 나르는 컨베이어 벨트에 비유할 수 있다.
- std::out : 표준 출력, 사용자 콘솔에 출력
- std::cerr : 에러 콘솔에 출력
- 데이터를 컨베이어 벨트에 올리는 작업은 << 연산자로 표현한다.
- std::endl은 문장이 끝났다는 것을 의미한다. 출력 스트림에서 std::endl이 나타나면 지금까지 던달한 내용을 모두 출력하고 다음 줄로 넘어간다.
- 문장의 끝은 \n 문자로도 표현할 수 있으며, 줄바꿈을 의미하고 이러한 형태의 문자를 이스케이프 시퀀스라 부른다.
- endl은 스트림에 줄바꿈 문자를 추가한 뒤 현재 버퍼에 있는 내용을 출력 장치로 내보낸다. endl은 성능에 영향을 미치기 때문에 특히 루프와 같은 문장에서 남용하면 좋지 않다.
- 반면 \n도 스트림에 줄바꿈 문자를 추가하지만 버퍼를 자동으로 비우지 않는다.
- 입력 스트림은 >> 연산자로 표현한다.
- std::cin 입력 스트림은 사용자가 키보드로 입력한 값을 받는다.
- C++20부터는 스트링 포맷은 <format>에 정의된 std::format()으로 지정하는 방식을 권장한다.
- format() 과 스트림 라이브러리를 사용하는 것이 바람직하다. printf()나 scanf()는 타입 안전성을 보장하지 않기 때문이다.
1.1.2 네임스페이스
- 네임스페이스는 코드에서 이름이 서로 충돌하는 문제를 해결하기 위해 나온 개념이다.
- 네임스페이스는 이름이 속할 문맥을 정의한다.
- 네임스페이스를 적용한 함수를 호출하려면 스코프 지정 연산자를 이용하여 함수 이름 앞에 네임스페이스를 붙인다.
- 네임스페이스 블록 안에서 접근할 때는 네임스페이스 접두어를 붙이지 않아도 된다.
- using 지시자로 네임스페이스 접두어를 생략할 수도 있다. 컴파일러는 using 지시자를 보면 그 뒤에 나오는 문장부터는 using에 지정된 네임스페이스에 속한다고 처리한다.
- 한 소스 파일에 using 지시자를 여러 개 지정할 수 있다. 동일한 이름을 포함하는 두 개의 네임스페이스를 사용하는 경우에는 이름 충돌이 다시 발생할 것이다.
- 네임스페이스 안에 있는 특정한 항목만 가리키도록 using 문을 작성할 수도 있다.
- 헤더 파일 안에서는 절대로 using 문을 작성하면 안 된다. 그러면 그 헤더 파일을 인클루드하는 모든 파일에서 using 문으로 지정한 방식으로 호출해야 하기 때문이다.
- 네임스페이스나 클래스 스코프와 같은 작은 스코프에서는 그렇게 해도 상관없다. 모듈 인터페이스 파일에서도 익스포트하지 않는다면 using 디렉티브나 선언문을 작성해도 아무 문제 없다.
+ 중첩 네임스페이스
- 중첩 네임스페이스는 다른 네임스페이스 안에 있는 네임스페이스를 말한다. 이때 각 네임스페이스는 더블 클론으로 구분한다.
namespace MyLibraries::Networking::FTP {...}
- C++17 이전에는 이러한 간결한 문법을 지원하지 않아서 중첩해서 작성했다.
namespace MyLibraries{ namespace Networking { namespace FTP {...}}}
+ 네임스페이스 앨리어스
- 네임스페이스 앨리어스를 사용하면 네임스페이스의 이름을 다르게 만들거나 또는 더 짧게 만들 수 있다.
namespace MyFTP = MyLibraries::Networking::FTP;
1.1.3 리터럴
- 리터럴은 코드에 표시한 숫자나 스트링과 같은 값을 의미한다. C++는 다양한 표준 리터럴을 제공한다.
- 리터럴은 소스 코드에서 직접 값을 나타내는 상수를 의미한다. 즉, 변수에 저장되기 전의 그 자체로 값을 가지는 데이터.
십진수 리터럴 (123), 8진수 리터럴 (0173), 16진수 리터럴 (0x7B), 이진수 리터럴 (0b1111011)
부동소수점 (3.14f), 배정도 부동소수점 값 (3.15), 16진수 부동소수점 리터럴 (0x3.ABCp-10, 0xb. cp121)
단일 문자 ('a'), 0으로 끝나는 문자 배열 ("character array")
직접 정의할 수도 있다.
- 숫자 리터럴에서는 자릿수 구분자를 사용할 수 있다. 자릿수 구분자는 작은따옴표로 표현한다.
1.1.4 변수
- 변수는 코드 안 어디에서나 선언할 수 있으며, 현재 블록 안에서 변수를 선언한 줄 다음부터 어디에서나 그 변수에 접근할 수 있다.
- 변수를 선언할 때 반드시 값을 대입할 필요는 없다. 그러나 초기화하지 않은 변수는 선언할 시점의 메모리 값을 기반으로 무작위 값이 대입될 수 있는데 그냥 사용하면 버그가 발생할 수 있다.
- 이를 위해 C++에서는 변수의 선언과 동시에 초깃값을 대입하는 기능을 제공한다.
- 균일 초기화(유니폼 초기화) 문법이란 C++11부터 도입된 일관된({} 중괄호) 초기화 문법이다. 기존의 ()나 = 초기화 방식보다 더 안전하고 직관적인 초기화 방식을 제공한다.
- 데이터 손실을 방지해서 안전한 초기화가 가능하다.
- C++는 강타입 언어다. 그러므로 항상 타입을 구체적으로 지정해야 한다.
- char 타입은 signed char나 unsignel char와 다른 타입이다. 이 타입은 문자만 표현해야 한다. 컴파일러의 종류에 따라 signed로 처리할 수도 있고, unsigned로 처리할 수도 있으므로 어느 하나라고 단정하면 안 된다.
- char를 위해 단일 타입을 표현하는 std::byte 타입을 <cstddef> 헤더에서 제공한다.
- std::byte를 사용하면 메모리의 한 바이트를 다룬다는 사실을 분명하게 표현할 수 있다.
+ 숫자 경곗값
- C++는 현재 플랫폼에서 지원하는 숫자의 경곗값(정수의 양의 최댓값)을 알아내는 표준 방법을 제공한다.
- C에서는 INT_MAX처럼 #define 문으로 정의된 값을 구했다.
- C++에서도 여전히 이 방식을 사용할 수 있지만, <limits>에서 제공하는 std::numeric_limits 클래스 템플릿을 사용하는 것이 바람직하다,
- 클래스 템플릿을 사용하려면 꺾쇠괄호 사이에 원하는 타입을 지정해야 한다.
numeric_limits::max()
- min()와 lowest()는 다르다. 정수에서는 최솟값과 최젓값이 같지만, 부동소수점수에서는 최솟값은 표현 가능한 가장 작은 양의 값인 반면, 최젓값은 표현 가능한 가장 작은 음수(-max())다.
+ 영 초기화
- 변수는 유니폼 초기자(균일 초기자) {0}을 이용하여 0으로 초기화할 수 있다. 이때 0은 생략해도 된다. 이처럼 빈 중괄호 {}로 표기한 유니폼 초기자를 영 초기자라 한다.
- 영 초기화는 기본 정수 타입을 0으로, 기본 부동소수점 타입을 0.0으로, 포인터 타입을 nullptr로, 객체를 디폴트 생성자로 초기화한다.
+ 캐스트
- 변수의 타입은 실행 중에 바꿀 수 있다. 이를 캐스트라 한다.
- C++에서 변수의 타입을 명시적으로 변환하는 방법 세 가지
- 1. (int)myFloat; // C 언어에서 사용하던 것으로 C++에서는 피해야 할 방식이지만 여전히 많이 사용함.
- 2. int(myFloat); // 거의 사용하지 않는다.
- 3. static_cast<int>(myFloat); // 가장 명확해서 권장하는 방식.
- 문맥에 따라 변수의 타입이 강제로 캐스트(강제 형변환)될 때도 있다.
- 변수를 자동으로 캐스트할 때 데이터가 손실될 수 있다는 점에 주의한다.
- 왼쪽에 나온 타입이 오른쪽에 나온 타입과 완전히 호환된다고 확신할 때만 자동 캐스트를 사용한다.
+ 부동소수점수
- 부동소수점수는 소수점 위치가 고정되지 않고 변할 수 있는 실수 표현 방식이다.
- 소수점 자리가 다른 부동소수점끼리 연산할 때 에러가 발생할 가능성이 있다. 또한 거의 동일한 부동소수점수끼리 뺄셈을 할 때 정밀도 손실이 발생할 수 있다.
- +/-무한 : 예를 들어 0을 0으로 나눈 결과와 같이 양의 무한과 음의 무한을 나타낸다.
- 주어진 부동소수점수가 +/-무한인지 확인하려면 std::isinf()을 사용한다. // <cmath>에 정의
- NaN : Not a Number(숫자가 아님)의 줄임말이다. 예를 들어 0이 아닌 수를 0으로 나눈 결과처럼 수학에서 정의되지 않은 값이 여기에 해당한다.
- 주어진 부동소수점수가 NaN인지 확인하려면 std::isnan()을 사용한다. // 에 정의
- 이처럼 특별한 부동소수점수를 구하려면 numeric_limits를 활용한다. 예를들면 numeric_limits<double>::infinity.
1.1.5 연산자
- 변수의 값은 변경할 수 없다면 변수를 사용할 이유가 없다. 다음 표는 C++에서 변숫값 변경에 가장 흔히 사용하는 연산자와 사용 예를 보여주고 있다.
- C++ 연산자는 표현식 두개를 계산하는 이항 연산자, 표현식 하나만 계산하는 단항 연산자, 표현식 세 개를 계산하는 삼항 연산자로 분류할 수 있다.
- 여러 가지 연산자가 복잡하게 뒤엉켜 있는 표현식은 연산 순서를 가늠하기 힘들다. 이럴때는 복잡한 표현식을 작은 표현식으로 나누거나 소괄호로 묶어서 표현하는 것이 좋다.
- C++에서는 /, *, %를 가장 먼저 계산하고, 그 다음으로 덧셈과 뺄셈을 계산하고, 마지막으로 비트 연산을 계산한다.(우선순위가 같을 때는 왼쪽에서 오른쪽 순서로 계산한다.) 즉 연산자는 우선순위에 따라 평가(계산)된다.
- 표현식에 소괄호를 넣어주면 컴파일러에 평가 순서를 명확히 알려줄 수 있다.
1.1.6 열거 타입
- 열거 타입은 여러 개의 관련된 상수를 정의할 때 사용하는 데이터 타입이다.
- 열거 타입을 사용하면 숫자를 나열하는 방식과 범위를 마음대로 정의해서 변수를 선언하는 데 활용할 수 있다.
- 강타입 열거 타입을 적용하면 변수에 지정할 수 있는 값의 범위를 엄격하게 제한하기 때문에 이러한 문제를 방지할 수 있다.
- 각 멤버마다 할당되는 값의 범위를 별도로 지정할 수도 있다.
- 열거 타입의 멤버에 값을 따로 할당하지 않으면 컴파일러는 이전 멤버의 값에 1을 더한 값으로 알아서 할당한다.
- 첫 번째 멤버 값을 지정하지 않았다면 컴파일러는 0으로 설정한다.
- 열거 타입의 값이 내부적으로 정수로 표현된다고 해서 자동으로 정수로 변환되지 않는다.
- 기본적으로 열거 타입의 값은 정수 타입(int)으로 저장되지만 다음과 같이 내부 표현 타입을 다른 타입으로 바꿀 수 있다.
- enum class로 정의한 열거 타입 값의 스코프(유효 범위)는 자동으로 확장되지 않는다. 다시 말해 상위 스코프에 똑같은 이름이 있더라도 충돌되지 않는다. 따라서 서로 다른 열거 타입에 동일한 이름의 멤버가 존재할 수 있다.
- 이런 원칙의 가장 큰 장점은 열거 타입의 값 이름을 짧게 지정할 수 있다는 것이다.
- 하지만 열것값을 길게 풀어 쓰거나 using enum 또는 using 선언문을 적어줘야 한다.
- C++20부터는 using enum으로 선언하면 열것값을 길게 풀어 쓰지 않아도 된다.
- using enum 또는 using 선언문의 스코프를 최소화하기를 바란다. 스코프가 너무 크면 이름이 충돌하는 현상이 발생할 수 있다.
+ 예전 방식의 열거 타입
- 예전에 작성된 레거시 코드에서는 enum class가 enum으로 선언된 것을 알 수 있다.
- 예전 방식으로 열거 타입을 정의할 때는 멤버 이름을 고유한 이름으로 지정해야 한다.
- 예전 방식의 열거 타입은 강타입이 아니기 때문에 타입에 안전하지 않다.
- 값이 항상 정수로 해석되기 때문에 본의 아니게 열것값을 전혀 다른 열거 타입과 비교할 수도 있고, 함수를 호출할 때 엉뚱한 열거 타입 값을 전달하는 오류가 발생할 수 있다.
- enum 과 enum class 차이점
네임스페이스 | 없음 | 있음 (EnumName::Value 형태) |
암묵적 형변환 | 가능 (정수로 변환됨) | 불가능 (명시적 변환 필요) |
타입 안전성 | 낮음 | 높음 |
동일한 이름 허용 | 안 됨 // 에러 발생 | 가능 |
- 열거 타입을 사용할 때는 타입에 안전하지 않은 득 타입 언세이프한 예전 방식의 enum보다는 강타입 버전인 enum class로 작성하는 것이 좋다.
1.1.7 구조체
- 구조체는 서로 다른 데이터 타입의 변수들을 하나로 묶어서 하나의 타입으로 정의할 수 있는 데이터 구조이다.
- 구조체를 사용하면 기존에 정의된 타입을 한 개 이상 묶어서 새로운 타입으로 정의할 수 있다.
- 모듈 인터페이스 파일은 주로 프로그램에서 여러 모듈 간의 상호작용을 정의하고, 서로의 기능을 사용할 수 있도록 해주는 파일이다. 보통 함수나 변수, 클래스 등의 선언이 포함되며, 실제 구현은 다른 파일에서 이루어진다.
- 모듈 인터페이스 파일의 확장자는 일반적으로 .cppm이다.
- 모듈 파일에서 처음 나오는 문장은 모듈 선언문으로서, 지정한 이름의 모듈을 새로 정의한다는 뜻이다.
- 모듈은 명시적으로 익스포트export해야 한다. 다시 말해 다른 곳에서 이 모듈을 임포트import할 수 있도록 드러내야 한다.
- 지정한 이름으로 선언한 변수는 해당 구조체에 있는 모든 필드를 가진다. 구조체를 구성하는 각 필드는 도트(.) 연산자로 접근한다.
- 필요한 정보들을 구조체로 모듈 인터페이스 파일에 정의할 수 있다
1.1.8 조건문
- 조건문을 사용하면 어떤 값이 참 또는 거짓인지에 따라 주어진 코드를 실행할지 결정할 수 있다.
+ if/else 문
- if문은 가장 흔히 사용하는 조건문이며, else문과 함께 쓸 수 있다. if문의 조건이 참이면 if문에 속한 코드를 실행한다. 조건이 참이 아니면 if 블록을 빠져나와 그다음 문장을 실행하거나, else문이 있다면 해당 코드를 실행한다.
- 연속 if문을 사용하면 if문 뒤에 이어지는 else 문에 다시 if문이 이어지는 문장을 갈끔하게 표현할 수 있다.
- if문 뒤 소괄호 안에는 반드시 부울 타입의 값을 지정하거나 평가 결과가 부울값인 표현식을 지정해야 한다.
- if의 초기자
if(<초기자>; <조건문>) {
<if의 본문>
} else if (<else if의 조건문>) {
<else if의 본문>
} else {
<else의 본문>
}
if(Employee employee { getEmployee() }; employee.salary > 1000 ) { ... }
- if문의 <초기자>에서 정의한 변수는 <조건문>과 <if의 본문>, <else if의 조건문>과 <else if의 본문>, <else의 본문> 안에서만 사용할 수 있고 if 문 밖에서는 사용할 수 없다.
+ switch 문
- 또 다른 조건문으로 switch 문이 있다. if 문과 마찬가지로 조건으로 지정한 표현식의 결과에 따라 수행할 동작을 선택한다.
- C++에서 switch 문에 지정할 수 있는 표현식은 결괏값이 반드시 정수 타입이거나, 정수 타입으로 변환할 수 있는 타입이거나, 열거 타입이거나, 강타입 열거 타입이어야 하며, 상수와 비교할 수 있어야 한다.
- 이 표현식의 결과에 해당하는 상숫값마다 특정한 경우를 case 문으로 표현할 수 있다.
- switch 문의 표현식 결과와 일치하는 case가 있으면 그 아래에 나오는 코드를 실행하다가 break문이 나오면 멈춘다.
- 또한 default란 키워드로 표현하는 case문은 앞에 나온 case 문 중에서 일치하는 것이 하나도 없을 때 실행된다.
- switch문은 모두 if/else 문으로 변환할 수 있다.
- switch문은 표현식의 만족 여부가 아닌 표현식의 다양한 결괏값마다 수행할 동작을 결정하는데 주로 사용한다. 동작을 나눌 기준값이 하나뿐일 때는 if나 if/else 문이 낫다.
- switch 문의 조건과 일치하는 case문이 있다면 그 아래 문장을 break 문이 나타날 때까지 실행한다. 이때 break 문이 없다면 다음에 나오는 case 문도 계속해서 실행하는데, 이렇게 실행되는 폴스루(흘려보내기)라 부른다.
- 컴파일러는 switch문에서 폴스루 구문을 발견했는데 해당케이스가 비어있지 않으면 경고 메시지를 발생한다.
- 폴스루에 대한 경고 멧시지를 출력하지 않게 하려면 [[fallthrough]] 어트리뷰트를 지정해서 의도적으로 폴스루 방식으로 작성했다고 컴파일러에 알려준다.
- switch 문의 초기자
switch (<초기자>; <표현식>) { <본문> }
- <초기자>에서 선언한 변수는 <표현식>과 <본문> 안에서만 사용할 수 있고, switch문 밖에서는 사용할 수 없다.
1.1.9 조건 연산자
- 조건 연산자(?:)는 C++에서 인수 세 개를 받는 유일한 삼항 연산자로 ?와 :로 표현한다.
- [조건] > [동작1] : [동작2] 조건을 만족하면 동작1을 수행하고 그렇지 않으면 동작 2을 수행한다.
- 조건 연산자는 if나 switch문과 달리 문장이 아니라 표현식이기 때문에 코드 안에서 원하는 곳에 간편히 추가할 수 잇다는 것이 장점이다.
1.1.10 논리 연산자
- 논리 연산자는 논리적 관계를 처리하는 연산자로, 주로 참(True) 또는 거짓(False) 값을 다룬다.
- C++는 논리 표현식을 평가할 때 단락 논리(축약 논리)를 사용한다. 다시 말해 표현식을 평사하는 도중에 최종 결과가 나오면 나머지 부분은 평가하지 않는다. 이처럼 C++는 불필요한 작업을 생략한다. 이는 프로그램 성능을 높이는 데 도움이 된다.
- 단락되는 논리식을 작성할 대는 가볍게 검사할 수 있는 부분을 앞에 적고, 시간이 걸리는 부분은 뒤에 둔다.
1.1.11 3방향 비교 연산자
- 3방향 비교 연산자는 두 값의 순서를 결정하는 데 사용된다. 이 연산자는 우주선 연산자라고도 부르는데, 연산자 기호인 <=>가 우주선처럼 생겼기 때문이다.
- 주어진 표현식의 평가 결과가 비교 대상이 되는 값과 같은지 아니면 그보다 크거나 작은지 알려준다.
- 이 연산자는 true나 false가 아닌 세 가지 결과 중 하나를 알려줘야 하기 때문에 부울 타입을 리턴할 수 없다. 그러므로 <compare>에 정의되고 std 네임스페이스에 속한 열거 타입으로 리턴한다.
- 피연산자가 정수 타입이면 강한 순서라고 부르며, 다음 세 가지 중 하나가 된다.
- strong_ordering::less : 첫 번재 피연산자가 두 번째 피연산자보다 작다.
- strong_ordering::greater : 첫 번째 피연산자가 두 번째 피연산자보다 크다.
- strong_ordering::equal : 두 피연산자가 같다.
- 피연산자가 부동소수점 타입이라면 결과는 부분 순서다.
- partial_ordering::less : 첫 번째 피연산자가 두 번째 피연산자보다 작다.
- partial_ordering::greater : 첫 번재 피연산자가 두 번재 피연산자보다 크다.
- partial_ordering::equicvalent : 두 피연산자가 같다.
- partial_ordering::unordered : 두 피연산자 중 하나는 숫자가 아니다.
- 약한 순서도 있으며, 자신이 직접 정의한 타입에 대해 3방향 비교 연산을 구현할 때 이 타입을 활용할 수 있다.
- weak_ordering::less : 첫 번째 피연산자가 두 번째 피연산자보다 작다.
- weak_ordering::greater : 첫 번째 피연산자가 두 번째 피연산자보다 크다.
- weak_ordering::equivalent : 두 피연산자가 같다.
- 기본 타입에 대해서는 3방향 비교 연산자를 사용해도 기존 비교 연산자인 ==, <, >를 사용하는 것보다 좋은 점은 없다. 하지만 비교 작업이 복잡한 객체에서는 상당히 유용하다.
- 객체의 순서를 비교할 때 다소 무거운 기존 비교 연산자를 두 번 호출할 필요 없이 3방향 비교 연산자 하나만으로 결정할 수 있기 때문이다.
- <compare>에서는 순서의 결과를 해석해주는 이름 있는 비교 함수인 std::is_eq(), std::is_neq(), std::is_lt(), std::is_lteq(), std::is_gt(), std::is_gteq()를 제공한다.
- ==, !=, <, <=, >, >=로 비교한 결과를 true나 false로 리턴한다.
int i { 11 };
strong_ordering result { i <=> 0 };
if (is_lt(result)) { cout << "less" << endl; };
if (is_gt(result)) { cout << "greater" << endl; };
if (is_eq(result)) { cout << "equal" << endl; };
1.1.12 함수
- 함수는 특정한 작업을 수행하는 코드 블록이다. 한 번 정의해 놓으면 필요할 때마다 호출해서 사용할 수 있어 코드의 재사용성을 높이고, 유지보수를 쉽게 해준다.
- 규모가 큰 프로그램에서 모든 코드를 main() 안에 담으면 관리하기 힘들다. 프로그램의 가독성을 높이려면 코드를 간결하고 명확한 함수 단위로 나눠야 한다.
- C++에서 함수를 사용하려면 먼저 선언해야 한다. 특정한 파일 안에서만 사용할 함수는 선언과 구현(정의)을 모두 소스 파일 안에 작성한다. 반면 함수를 다른 모듈이나 파일에서도 사용한다면 선언은 모듈 인터페이스 파일로부터 익스포트하고, 정의는 모듈 인터페이스 파일이나 모듈 구현 파일에 작성한다.
- 함수를 선언하는 문장을 함수 프로토타입 또는 함수 헤더라 부른다.
- 함수의 구체적인 내용은 보지 않고, 그 함수에 접근하는 방식만 표현한다는 의미가 강하다.
- 또한 함수의 리턴 타입을 제외한 함수 이름과 매개변수 목록을 함수 시그니처라 부른다.
- C언어에서는 컴파일러에 따라 매개변수를 받지 않는 함수를 작성할 대 매개변수 자리에 void를 적어야 할 수도 있다. 이와 달리 C++에서는 함수의 매개변수 리스트 자리에 void 대신 그냥 비워두면 된다. 하지만 리턴값이 없다는 것은 표시할 때는 C언어와 마찬가지로 리턴 타입 자리에 반드시 void라고 적어햐 한다.
- 함수는 당연히 호출한 측으로 값을 리턴할 수 있다.
+ 함수 리턴 타입 추론
- 함수의 리턴 타입을 컴파일러가 알아서 지정할 수 있다. 다음과 같이 리턴 타입 자리에 auto 키워드만 적으면 된다.
- 컴파일러는 return 문에 나온 표현식의 타입에 따라 리턴 타입을 추론한다.
- 함수 안에 return 문이 여러 개가 있다면 모두 타입이 같아야 한다. 리턴값이 재귀 호출(자기 자신에 대한 호출)일 수도 있는데, 이때는 재귀 호출이 아닌 return 문도 반드시 함께 있어야 한다.
+ 현재 함수 이름
- 모든 함수는 내부적으로 _func_라는 로컬 변수가 정의되어 있다. 이 변수의 값은 현재 함수의 이름이며, 주로 로그를 남기는데 활용한다.
+ 함수 오버로딩
- 함수를 오버로딩한다는 말은 이름은 같지만 매개변수 구성은 다른 함수를 여러 개 제공한다는 뜻이다.
- 리턴 타입만 달라서는 안 된다. 매개변수의 타입이나 개수도 달라야 한다.
- 컴파일러는 주어진 인수를 기반으로 오버로딩된 함수 중에서 적합한 버전을 선택한다.
1.1.13 어트리뷰트
- 어트리뷰트는 소스 코드에 벤더에서 제공하는 정보나 옵션을 추가하는 메커니즘이다.
- C++ 표준에 어트리뷰트가 추가되기 전에는 벤더마다 이런 정보를 지정하는 방법이 달랐다. 예를 들어 _attribute_, _declspec 등을 사용했다.
- C++11부터 [[어트리뷰트]]와 같이 대괄호를 이용한 형식을 사용하도록 표준화되기 시작했다.
+ [[nodiscard]]
- [[nodiscard]] 어트리뷰트는 어떤 값을 리턴하는 함수에 대해 지정할 수 있다.
- 이 기능은 에러 코드를 리턴하는 함수 등에 활용할 수 있다. 그런 함수에 해당 어트리뷰트를 붙이면 에러 코드를 무시하지 않는다.
- 일반적으로 해당 어트리뷰트는 클래스, 함수, 열거형에 적용할 수 있다.
- C++20부터 [[nodiscard]] 어트리뷰트에 이유를 설명하는 스트링을 추가할 수 있다.
[[nodiscard("Some explanation")]] int func();
+ [[maybe_unused]]
- [[maybe_unused]] 어트리뷰트는 뭔가 사용하지 않았을 때 컴파일러가 경고 메시지를 출력하지 않도록 설정하는 데 사용된다.
- 해당 어트리뷰트는 클래스, 구조체, 비 static(static이 아닌) 데이터 멤버, 유니온, typedef, 타입 앨리어스, 변수, 함수, 열거형, 열것값 등에 대해 지정할 수 있다.
+ [[noreturn]]
- 함수에 [[noreturn]] 어트리뷰트를 지정하면 호출 지점으로 다시 돌아가지 않는다. 주로 프로세스나 스레드 종료와 같이 뭔가가 끝나게 만들거나, 익셉션을 던지는 함수가 여기에 해당한다.
+ [[deprecated]]
- [[deprecated]] 어트리뷰트는 지원 중단된 대상임을 지정하는 데 사용된다. 즉 현재 사용할 수는 있지만 권장하지 않는 대상임을 표시한다.
- 이 어트리뷰트는 지원 중단되는 이유를 표현하는 인수를 옵션으로 지정할 수 있다.
+ [[likely]]와 [[unlikely]]
- [[likely]]와 [[unlikely]] 어트리뷰트를 지정하면 컴파일러가 최적화 작업을 수행하는 데 도움을 줄 수 있다. 예를 들어 이 어트리뷰트를 이용하여 if와 switch문에서 수행될 가능성이 높은 브랜치를 표시할 수 있다.
1.1.14 C 스타일 배열
- 배열은 같은 타입의 값을 연달아 저장하며, 각 값은 배열에서 해당 위치를 이용해 접근한다.
- C++에서 배열을 선언할 때는 반드시 배열의 크기를 지정해야 하는데, 변수로 지정할 수는 없고 반드시 상수 또는 상수 표현식으로 지정해야 한다.
- 각 원소마다 초기화하지 않고 다음 절에서 설명할 반복문(루프)을 활용해도 된다.
- 앞에 나온 방법이나 반복문 말고도 다음과 같이 영 초기화 구문으로 한 번에 초기화하는 방법도 있다.
- 초기자 리스트를 사용해도 된다. 그러면 배열의 크기를 컴파일러가 알아서 결정한다.
- 초기자 리스트에 나온 원소의 개수가 배열의 크기로 지정한 수보다 적으면 나머지 원소는 0으로 초기화 된다.
- 스택 기반의 C 스타일 배열의 크기는 std::size() 함수로 구할 수 있다.
- 해당 함수는 <cstddef>에 정의된 부호 없는 정수 타입인 size_t 값을 리턴한다. 이는 크기를 바이트 단위로 리턴한 것이므로 스택 기반 배열에 담긴 원소의 개수를 알아내려면 이 연산자가 리턴한 값을 첫 번째 원소의 크기로 나눠야 한다.
size_t arraySize { sizeof(myArray) / sizeof(myArray[0]) };
- C++는 다차원 배열도 지원한다.
1.1.15 std::array
- C++에서는 std::array라는 고정 크기 컨테이너를 제공한다. 이 타입은 <array> 헤더 파일에 정의되어 있다.
- 해당 컨테이너는 크기를 정확히 알 수 있고, 자동으로 포인터를 캐스트(동적 형변환)하지 않아서 특정한 종류의 버그를 방지할 수 있고, 반복자(이터레이터)로 배열에 대한 반복문을 쉽게 작성할 수 있다.
- C++는 CTAD(클래스 템플릿 인수 추론)이라는 기능을 제공한다.
- 꺽쇠괄호 사이에 템플릿 타입을 지정하지 않아도 된다는 정도만 알고 넘어가자.
- CTAD는 초기자를 사용할 때만 작동한다. 컴파일러가 템플릿 타입을 자동으로 추론하는 데 이 초기자를 사용하기 때문이다.
- array arr { 9, 8, 7};
- C스타일 배열과 std::array는 둘 다 크기가 컴파일 시간에 결정되어야 하며, 실행 시간에 늘어나거나 줄어들 수는 없다.
1.1.16 std::vector
- C++ 표준 라이브러리는 크기가 고정되지 않은 컨테이너를 다양하게 제공한다. 대표적인 예로 <vector> 헤더 파일에 선언된 std::vector가 있다.
- vector는 C스타일의 배열 대신 사용할 수 있고 훨씬 유연하고 안전하다. 프로그래머는 메모리 관리를 신경 쓸 필요가 없다.
- vector는 원소를 모두 담을 수 있도록 메모리를 확보하는 작업을 알아서 처리해주며, 동적으로 실행 시간에 원소를 추가하거나 삭제할 수 있다.
- std::vector는 꺾쇠괄호 안에 템플릿 매개변수를 지정해야 한다. vector는 제너릭 컨테이너이며 거의 모든 종류의 객체를 담을 수 있다.
- vector도 CTAD를 지원한다. vector myVector { 11, 22 }; // 초기자를 지정해야 CTAD가 작동한다.
1.1.17 std::pair
- <utility> 헤더에 정의된 std::pair 클래스 템플릿은 두 값을 하나로 묶는다. 각 값은 public 데이터 멤버인 first와 second로 접근할 수 있다.
- pair도 CTAD를 지원한다.
1.1.18 std::optional
- <optional>에 정의된 std::optional은 특정한 타입의 값을 가질 수도 있고, 아무 값도 가지지 않을 수도 있다.
- 기본적으로 함수 매개변수에 전달된 값이 없을 수도 있는 상황에 사용된다. 또한 값을 리턴할 수도 있고, 그렇지 않을 수도 있는 함수의 리턴 타입으로 사용하기도 한다.
- optional 타입은 클래스 템플릿이므로 optional와 같이 실제 타입을 꺽쇠괄호 안에 반드시 지정해야 한다.
- optional에 값이 있는지 확인하려면 has_value() 메서드를 사용할 수 있다.
- optional에 값이 있을 때는 value() 나 역참조 연산자로 그 값을 가져올 수 있다.
- 값이 없는 optional에 대해 value()를 호출하면 std::bad_optional_access 익셉션이 발생한다.
- value_or()을 사용하면 optional에 값이 있을 때는 그 값을 리턴하고, 값이 없을 때는 다른 값을 리턴하다.
- 이때 레퍼런스는 optional에 담을 수 없다. 대신 포인터는 저장할 수 있다.
1.1.19 구조적 바인딩
- 구조적 바인딩을 이용하면 여러 변수를 선언할 때 array, struct, pair 등에 담긴 원소들을 이용하여 변숫값을 한꺼번에 초기화할 수 있다.
- 구조적 바인딩을 적용하려면 반드시 auto 키워드를 붙여야 한다.
- auto [x, y, z] { values };
- 구조적 바인딩에서 왼쪽에 나온 선언할 변수 개수와 오른쪽에 나온 표현식에 담긴 값의 개수는 반드시 일치해야 한다.
- 구조적 바인딩은 배열뿐만 아니라 비 static 멤버가 모두 public으로 선언된 구조체에도 적용할 수 있다.
- auto&나 const auto&를 이용하여 구조적 바인딩으로 비 const에 대한 레퍼런스나 const에 대한 레퍼런스를 생성할 수도 있다.
1.1.20 반복문
- C++는 while, do/while, for, 범위 기반 for 등 네 가지 반복 메커니즘을 제공한다.
+ while문
- while문은 주어진 표현식이 true인 동안 주어진 코드 블록을 계속해서 반복한다.
- break 키워드를 사용하면 반복문(루프)을 즉시 빠져나와 프로그램을 계속 진행한다.
- continue 키워드를 사용하면 즉시 반복문의 첫 문장으로 돌아가서 while문에 지정한 표현식을 다시 평가한다.
- continue 는 프로그램의 실행 흐름이 갑작스레 건너뛰기 때문에 자주 사용하는 것은 바람직하지 않다.
+ do/while문
- C++는 while 문을 약간 변형한 do/while문도 제공한다. 동작은 while 문과 비슷하지만, 먼저 코드 블록부터 실행한 뒤 조건을 검사하고, 그 결과에 따라 루프를 계속 진행할지 결정하는 점이 다르다.
- 이 구문을 활용하면 코드 블록을 최소 한 번 실행하고, 그 뒤에 더 실행할지 여부는 주어진 조건에 다라 결정할 수 있다.
+ for문
- for 문으로 작성한 코드는 모두 while문으로 변환할 수 있고, 그 반대도 가능하다. 하지만 for의 문법이 좀 더 편할 때가 많다.
- 초기 표현식과 종료 조건, 매번 반복이 끝날 때마다 실행할 문장으로 반복문을 구성할 수 있기 때문이다.
- 초깃값, 종료 조건, 반복할 때마다 실행할 문장을 모두 한 줄에 표현해서 좀 더 이해하기 쉽다.
+ 범위 기반 for문
- 컨테이너에 담긴 원소에 대해 반복문을 실행하는 데 편하다.
- C 스타일의 루프, 초기자 리스트, 그리고 std::array, std::vector, 표준 라이브러리에서 제공하는 모든 컨테이너처럼 반복자를 리턴하는 begin()과 end() 메서드가 정의된 모든 타입과 표준 라이브러리에서 제공하는 모든 컨테이너에 적용할 수 있다.
- 범위 기반 for문으로 원소에 접근할 시 복제본을 사용하는데, 복제하지 않고 반복문을 실행하려면 뒤에서 설명할 레퍼런스 변수를 활용하면 된다.
- 범위 기반 for문의 초기자
- C++20부터는 범위 기반 for문에서도 if문이나 switch문처럼 초기자를 사용할 수 있다.
for (<초기자>; <for-범위-선언> : <for-범위-초기자>) { <본문> }
- <초기자>에 지정한 변수는 모두 <for-범위-초기자>와 <본문>에서 사용할 수 있지만, 범위 기반 for문 밖에서는 사용할 수 없다.
1.1.21 초기자 리스트
- 초기자 리스트는 <initializer_list> 헤더 파일에 정의되어 있으며, 이를 활용하면 여러 인수를 받는 함수를 쉽게 작성할 수 있다. std::initializer_list 타입은 클래스 타입은 클래스 템플릿이다.
#include <initializer_list>
using namespace std;
int makeSum(initializer_list<int> values)
{
int total { 0 };
for (int value : values ) {
total += value;
}
rturn total;
}
- 초기자 리스트는 타입에 안전하다. 그러므로 초기자 리스트를 정의할 때 지정한 타입만 허용한다.
1.1.22 C++의 스트링
- C++에서 스트링을 다루는 방법은 두 가지다.
- C 스타일 : 스트링을 문자 배열로 표현
- C++ 스타일 : C 스타일로 표현된 스트링을 쉽고 안전하게 사용할 수 있도록 스트링 타입으로 감싼 방식
- C++의 std::string 타입은 <string>헤더 파일에 정의되어 있고, 기본 타입처럼 사용할 수 있다는 정도만 알아두자.
- string을 문자 배열처럼 다룰 수 있다.
1.1.23 C++의 객체지향 언어 특성
- C와 달리 C++는 객체지향 언어라는 점이다. C는 절차지향 언어이다.
- 객체지향 프로그래밍(OOP)에서는 코드 작성 방식이 기존과 달리 훨씬 직관적이다.
+ 클래스 정의
- 클래스는 객체의 특성을 정의한 것이다. C++에서 클래스를 정의하는 코드는 주로 모듈 인터페이스 파일(.cppm)에 작성하고, 이를 구현하는 코드는 .cppm에 함께 적거나 소스 파일(.cpp)에 작성한다.
- 클래스를 정의할 때는 먼저 클래스 이름부터 적으며, 중괄호 안에 이 클래스를 구성하는 데이터 멤버와 메서드를 선언한다.
- 각각의 데이터 멤버와 메서드마다 public, protected, private 등으로 접근 수준을 지정한다. 이러한 레이블은 나열하는 순서는 따로 없고 중복되어도 상관없다.
- public으로 지정한 멤버는 클래스 밖에서 접근할 수 있는 반면 private으로 지정한 멤버는 클래스 외부에서 접근할 수 없다.
- 대체로 데이터 멤버는 모두 private로 지정하고 이에 대한 게터나 세터를 public으로 지정한다.
- 모듈 인터페이스 파일을 작성할 때는 작성하려는 모듈을 반드시 export module 선언문으로 시작해야 한다는 것이다. 또한 그 모듈을 사용하는 이들에게 제공할 타입을 명시적으로 익스포트하는 것도 잊어선 안 된다.
- 클래스와 이름이 같고 리턴 타입이 없는 메서드를 생성자라 부른다. 이 메서드는 해당 클래스의 객체를 생성할 때 자동으로 호출된다.
- 생성자와 형태는 같지만 앞에 틸드(~)를 붙인 메서드를 소멸자라 부른다. 이 메서드는 객체가 제거될 때 자동으로 호출된다.
- 생성자로 데이터 멤버를 초기화하는 방법은 여러 가지가 있다.
- 하나는 생성자 초기자를 사용하는 것으로 생성자 이름 뒤에 콜론(:)을 붙여서 표현한다.
- 두 번째 방법은 생성자의 본문에서 초기화하는 것이다.
- 생성자에서 다른 일은 하지 않고 데이터 멤버를 초기화하는 일만 한다면 굳이 생성자를 따로 정의할 필요가 없다. 클래스를 정의하는 코드 안에서 곧바로 데이터 멤버를 초기화할 수 있기 때문이다.(클래스 내부 초기자)
- 파일을 닫거나 메모리를 해제하는 등의 정리 작업이 필요하다면 소멸자를 작성해야 한다.
+ 클래스 사용
- 특정 클래스를 사용하려면 먼저 모듈을 임포트해야 한다.
1.1.24 스코프 지정
- 스코프는 변수나 함수가 유효한 범위를 의미한다.
- 변수, 함수, 클래스명과 같이 프로그램에 나오는 모든 이름은 저마다 스코프가 있다. 스코프는 네임스페이스, 함수 정의, 중괄호로 묶은 블록, 클래스 정의 등으로 생성한다.
- for문이나 범위 기반for문의 초기화 문장에서 초기화되는 변수의 스코프는 해당 for문 안으로 한정되며 for문 밖에서는 보이지 않는다. 마찬가지로 if나 switch문의 초기자에서 초기화된 변수도 스코프가 해당 if나 switch 문 안으로 한정되며 밖에서는 보이지 않게 된다.
- 변수나 함수, 클래스 등에 접근할 때는 가장 안쪽 스코프에 있는 이름부터 검색하고, 거기에 없으면 바로 다음 바깥의 스코프를 검색하는 등 글로벌 스코프에 이르기까지 계속 진행한다.
- 함수 바깥에서 선언된 변수는 글로벌 스코프를 가지며, 모든 함수에서 접근 가능하다.
- 네임스페이스나 함수, 중괄호로 묶은 블록, 클래스 등에 없는 이름은 모두 글로벌 스코프에 있다고 간주한다. 그 이름이 글로벌 스코프에도 없다면 컴파일러는 알 수 없는 기호 에러를 발생시킨다.
- 스코프 안에 있는 이름이 바깥 스코프에 있는 동일 이름에 가려질 수도 있다. 또한 현재 다루려는 스코프가 프로그램의 해당 라인에서 디폴트 스코프 지정 범위에 없는 경우도 있다.
- 어떤 이름이 디폴트 스코프 지정 범위에 적용되지 않게 하려면 그 이름 앞에 스코프 지정 연산자인 ::를 이용하여 원하는 스코프를 지정하면 된다.
1.1.25 균일 초기화
- C++11 이전에는 Struct 타입 변수와 Class 타입 변수를 초기화하는 방법이 서로 달랐다. 구조체에 대해서는 {...}문법을 적용한 반면 클래스에 대해서는 함수 표기법인 (...)로 생성자를 호출했다.
- C+11부터 타입을 초기화할 때 다음과 같이 {...} 문법을 사용하는 균일 초기화(줄괄호 초기화, 유니폼 초기화)로 통일되었다.
- 균일 초기화 구문은 구조체나 클래스뿐만 아니라 C++에 있는 모든 대상을 초기화하는데 적용된다.
- 균일 초기화는 변수를 영 초기화(제로 초기화)할 때도 적용할 수 있다. 다음과 같이 중괄호로 빈 집합 표시만 해주면 된다.
- 군일 초기화를 사용하면 축소 변환(좁히기)을 방지할 수 있다. 축소 변환으로 인한 에러 메시지가 생성된다.
- 축소 변환 캐스트를 하려면 GSL(Guideline Support Library)에서 제공하는 gsl::narrow_cast() 함수를 사용하기 바란다.
- 균일 초기화는 동적으로 할당되는 배열을 초기화할 때도 적용할 수 있다.
- 생성자의 초기자에서 클래스 멤버로 정의한 배열을 초기화할 때도 사용할 수 있다.
- std::vector와 같은 표준 라이브러리 컨테이너에도 적용할 수 있다.
- 이 모든 장점을 감안할 때 변수 초기화에 대입 구문을 이용하기보다는 균일 초기화를 사용하는 것이 바람직하다.
+ 지정 초기자
- C++20부터 지정 초기자가 도입되었다. 이 초기자는 묶음 타입의 데이터 멤버를 초기화하는 데 사용된다.
- 묶음 타입이란 public 데이터 멤버만 갖고, 사용자 정의 생성자나 상속된 생성자가 없고, virtual 함수도 없으며, virtual, private, protected 베이스 클래스도 없는 배열 타입 객체나 구조체 객체, 클래스 객체를 말한다.
- 지정 초기자는 점 뒤에 데이터 멤버의 이름을 적는 방식으로 표기한다. 지정 초기자에 나오는 데이터 멤버는 반드시 데이터 멤버가 선언된 순서를 따라야 한다.
- 지정 초기자와 비지정 초기자를 섞어 쓸 수 없다.
- 지정 초기자로 초기화되지 않은 데이터 멤버는 모두 디폴트값으로 초기화된다.
- 클래스 내부 초기자를 가진 데이터 멤버는 거기서 지정된 값을 갖게 된다.
- 클래스 내부 초기자가 없는 데이터 멤버는 0으로 초기화된다.
struct Employee {
char firstInitial;
char lastInitial;
..
};
// 균일 초기자
Employee anEmployee { 'J', 'D', 42, 80'000 };
// 지정 초기자
Employee anEmployee {
.firstInitial = 'J';
.lastInitial = 'D';
..
};
- 지정 초기자를 사용하면 균일 초기자를 사용할 때보다 초기화할 대상을 훨씬 쉽게 파악할 수 있다.
- 지정초기자를 사용할 때 주어진 디폴트값을 사용하고 싶은 멤버에 대해 초기화를 생략할 수 있다. 내부 초기자가 없을 시 0으로 초기화한다.
- 지정 초기자의 마지막 장점은 구조체에 멤버를 추가하더라도 지정 초기자를 이용한 기존 코드는 그대로 작동한다는 것이다. 새로 추가된 데이터 멤버는 디폴트값으로 초기화된다.
1.1.26 포인터와 동적 메모리
- 동적 메모리를 이용하면 컴파일 시간에 크기를 확정할 수 없는 데이터를 다룰 수 있다. 아주 단순한 프로그램이 아니라면 대부분 어떤 형태로든 동적 메모리를 사용한다.
+ 스택과 프리스토어
- C++ 애플리케이션에서 사용하는 메모리는 크게 스택과 프리스토어로 나뉜다.
- 스택은 테이블에 쌓아둔 접시에 비유할 수 있다. 제일 위에 놓인 접시는 프로그램의 현재 스코프를 표현하며, 주로 현재 실행 중인 함수를 가리킨다. 현재 실행 중인 함수에 선언된 변수는 모두 최상단은 접시에 해당하는 최상단 스택 프레임의 메모리 공간에 담긴다. (스택 프레임)
- 스택 프레임은 각 함수마다 독립적인 메모리 공간을 제공한다는 점에서 굉장히 유용하다.
- 스택에 할당된 변수는 프로그래머가 직접 할당 해제할 필요 없이 자동으로 처리된다. 즉 함수 실행이 끝나면 해당 스택 프레임이 삭제된다.
- 프리스토어는 현재 함수 또는 스택 프레임과는 완전히 독립적인 메모리 공간이다. 함수가 끝난 후에도 그 안에서 사용하던 변수를 계속 유지하고 싶다면 프리스토어에 저장한다. 프리스토어는 스택보다 구조가 간결하다. 마치 비트 더미와 같다. 프로그램에서 원하는 시점에 언제든지 이 비트 더미에 새로운 비트를 추가하거나 기존에 있던 비트를 수정할 수 있다. 프리스토어에 할당된 메모리 공간은 직접 할당 해제해야 한다. 프리스토어는 스마트 포인터를 사용하지 않는 한 자동으로 할당 해제되지 않기 때문이다.
- 프리스토어는 힙의 일부이다.
+ 포인터 사용법
- 메모리 공간을 적당히 할당하기만 하면 어떠한 값이라도 프리스토어에 저장할 수 있다. 예를 들어 정숫값을 프리스토어에 저장하려면 정수 타입에 맞는 메모리 공간을 할당해야 하는데, 이때 포인터를 선언해야 한다.
- int 타입 뒤에 붙은 *는 해당 변수가 정수 타입에 대한 메모리 공간을 가리킨다는 것을 의미한다. 이때 포인터는 동적으로 할당된 프리스토어 메모리를 가리키는 화살표와 같다. 아직 값을 할당하지 않았기 때문에 포인터가 구체적으로 가리키는 대상은 없다. 이를 초기화되지 않은 변수라 부른다.
- 변수를 선언한 후에는 반드시 초기화해야 한다. 초기화하지 않고 사용하면 거의 대부분 프로그램이 뻗어버린다(크래시). 그러므로 포인터 변수는 항상 선언하자마자 초기화한다. 포인터 변수에 메모리를 당장 할당하고 싶지 않다면 널 포인터로 초기화한다.
- 널 포인터란 정상적인 포인터라면 절대로 가지지 않을 특수한 값이며, 부울 표현식에서는 false로 취급한다.
- 포인터 변수에 메모리를 동적으로 할당할 때는 new 연산자를 사용한다.
- 포인터가 가리키는 값에 접근하려면 포인터를 역참조(참조 해제) 해야 한다.
- 역참조란 포인터가 프리스토어에 있는 실제 값을 가리키는 화살표를 따라간다는 뜻이다. 앞에서 프리스토어에 새로 할당한 공간에 정숫값을 넣으려면 "*변수이름 = 새로운 값"을 작성한다.
- 동적으로 활당한 메모리를 다 쓰기 나면 delete 연산자로 그 공간을 해제해야 한다. 메모리를 해제한 포인터를 다시 사용하지 않도록 다음과 같이 곧바로 포인터 변수의 값을 nullptr로 초기화하는 것이 좋다.
- 포인터는 프리스토어뿐만 아니라 스택과 같은 다른 종류의 메모리를 가리킬 수도 있다. 원하는 변수의 포인터값을 알고 싶다면 레퍼런스(참조) 연산자인 &를 사용한다.
- C++는 구조체의 포인터를 다루는 부분은 좀 다르게 표현한다. 다시 말해 먼저 *연산자로 역참조해서 구조체 자체(시작 지점)에 접근한 뒤 필드에 접근할 때는 .연산자로 표기한다.
- 코드를 좀 더 간결하게 표현하고 싶다면 -> 연산자로 구조체를 역참조해서 필드에 접근하는 작업을 한 단계로 표현할 수 있다.
+ 동적으로 배열 할당하기
- 배열을 동적으로 할당할 때도 프리스토어를 활용한다. 이때 new[] 연산자를 사용한다.
- 이렇게 하면 정수 타입 원소에 대해 arraySize 변수로 지정한 개수만큼 메모리가 할당된다.
- 포인터 변수는 여전히 스택 안에 있지만, 동적으로 생성된 배열은 프리스토어에 있다.
- 배열을 이용한 작업이 끝나면 다른 변수가 프리스토어의 메모리 공간을 쓸 수 있도록 이 배열을 프리스토어에서 제거한다. C++에서 이 작업은 delete[] 연산자로 처리한다.
+ 널 포인터 상수
- C++11 이전에는 NULL이란 상수로 널 포인터를 표현했다. NULL은 실제로 상수 0과 같아서 문제가 발생할 여지가 있다.
- 널 포인터 상수인 nullptr을 사용하면 해결할 수 있다.
1.1.27 const의 다양한 용도
- const 키워드는 상수를 의미하는 'constant'의 줄임말로서 변경되면 안 될 대상을 선언할 때 사용한다.
- 컴파일러는 const로 지정한 대상을 변경하는 코드를 발견하면 에러를 발생시킨다. 또한 const로 지정한 대상을 최적화할 때 효율을 더욱 높일 수 있다.
+ const 상수
- const는 이름에서 알 수 있듯이 상수로도 사용된다.
- C 언어에서는 변경되지 않을 값에 이름을 붙일 때 전처리 구문인 #define을 주로 사용했다.
- #define은 전처리기가 처리한다. #define 문은 코드를 메타 수준으로 처리해서 언어의 구문과 의미에 대해 신경 쓰지 않고 워드프로세서의 찾아 바꾸기처럼 단순히 텍스트 매칭 작업을 수행한다.
- C++에서는 상수를 #define 대신 const로 정의하는 것이 바람직하다.
- const는 컴파일러가 처리한다.메타 수준이 아닌 C++ 코드 문맥 안에서 컴파일러가 평가한다. 그러므로 const로 정의할 대상에 타입이나 스코프를 정용할 수 있다.
- const로 상수를 정의하는 방법은 변수를 정의할 때와 거의 같고, 값이 변경되지 않도록 보장하는 작업은 컴파일러가 처리한다는 점만 다르다.
- const 포인터
- 포인터로 가리키는 값이 수정되게 않게 하려면 다음과 같이 const 키워드를 포인터 타입 변수의 선언문에 붙인다.
- const int* 는 값을 변경할 수 없고, int* const 는 주소 자체를 변경하지 못한다.
- 이렇게 하면 자체를 변경할 수 없게 되기 때문에 이 변수를 선언과 동시에 초기화해야 한다.
- const int* const 이렇게 사용할 수 있다.
- const 매개변수
+ const 메서드
1.1.28 constexpr 키워드
✅ 컴파일 타임에 계산될 수 있는 변수, 함수, 객체를 정의
✅ 함수와 변수를 정의할 때 사용 가능
✅ 반환값이 컴파일 타임 상수일 수도 있고, 런타임에 결정될 수도 있음
1.1.29 consteval 키워드
✅ 반드시 컴파일 타임에 실행되어야 하는 함수
✅ 런타임 실행 불가능!
✅ 즉, consteval 함수는 반드시 컴파일 타임 상수를 반환해야 함
📌 constexpr vs consteval 차이점 정리
constexpr | ✅ 가능 | ✅ 가능 | ✅ 가능 | constexpr int square(int x) { return x * x; } |
consteval | ✅ 반드시 컴파일 타임 실행 | ❌ 불가능 (오류 발생) | ✅ 가능 | consteval int square(int x) { return x * x; } |
1.1.30 레퍼런스
✅ 레퍼런스(Reference) 는 이미 존재하는 변수의 별명(Alias) 입니다.
✅ 변수와 같은 방식으로 사용되지만, 초기화 이후 다른 변수를 가리킬 수 없음
✅ & 기호를 사용하여 선언
1.1.31 const_cast()
1.1.32 익셉션
- C++는 굉장히 유연한 반면 안전한 편은 아니다. 메모리 공간을 무작위로 접근하거나 0으로 나누는 연산을 수행하는 코드를 작성해도 컴파일러는 내버려둔다. 그러므로 c++의 안전성을 좀 더 높이기 위해 익셉션(예외)이라는 기능을 제공한다.
- 익셉션이란 예상하지 못한 상황을 말한다. 익셉션을 활용하면 문제가 발생했을 때 좀 더 융통성 있게 대처할 수 있다.
- 코드에서 특정한 조건을 만족해서 익셉션을 발생시키는 것을 익셉션을 던진다고 표현하고 throw 구문으로 작성한다.
- 또한 이렇게 발생된 익셉션에 대해 적절한 동작을 수행하는 것을 익셉션을 잡는다고 표현하고, catch문으로 작성한다.
- 익셉션이 발생하는 함수를 호출할 때는 다음 코드처럼 try/catch 블록으로 감싼다.
- 표준 라이브러리의 익셉션 클래스는 what()이란 메서드를 제공한다. 이 메서드는 발생한 익셉션에 대한 간략한 설명을 담은 스트링을 리턴한다.
- 익셉션을 발생시킨 에러에 대한 구체적인 정보를 제공하고 싶다면 익셉션 타입을 직접 정의한다.
- C++ 컨파일러는 발생 가능한 모든 익셉션을 꼭 잡도록 강제하지 않는다.
- 익셉션을 처리하는 코드를 따로 작성하지 않으면 프로그램 자체에서 처리하는데, 그러면 프로그램이 그냥 종료된다.
1.1.33 타입 앨리어스
- 타입 앨리어스란 기존에 선언된 타입에 다른 이름을 붙이는 것이다. 타입을 새로 정의하지 않고 기존 타입 선언에 대한 동의어를 선언하는 문법이라 생각할 수 있다.
- 너무 복잡하게 선언된 타입 표현을 좀 더 간편하게 만들기 위한 용도로 많이 사용한다. 흔히 템플릿을 이용할 대 이런 경우가 많다.
1.1.34 typedef
- 타입 앨리어스 C++11부터 도입되었다. 그전에는 타입 앨리어스로 하는 일을 typedef로 구현해야 했는데 코드가 다소 복잡했다. 오래된 방식이긴 하지만 아직까지 레거시 코드에서 종종 볼 수 있다.
- typedef도 타입 앨리어스와 마찬가지로 기존에 선언된 타입에 다른 이름을 붙여준다.
- 가독성이 훨씬 떨어지며 선언된 순서가 반대라서 헷갈리기 쉽다.
- 타입 앨리어스와 typedef가 완전히 똑같은 것은 아니며 템플릿에 활용할 때는 typedef보다 타입 앨리어스를 사용하는 것이 훨씬 유리하다.
- typedef보다는 타입 앨리어스를 사용하기 바란다.
1.1.35 타입 추론
- 타입 추론은 표현식의 타입을 컴파일러가 스스로 알아내는 기능이다.
+ auto 키워드
- 함수의 리턴 타입을 추론한다.
- 구조적 바인딩에 사용한다.
- 표현식의 타입을 추론하는 데도 사용한다.
- 비타입 템플릿 매개변수의 타입을 추론하는 데 사용한다.
- 축약 함수 템플릿 구문.
- decltype (auto)에서 사용한다.
- 함수에 대한 또 다른 문법으로 사용한다.
- 제네릭 람다 표현식에서 사용한다.
- auto 키워드는 컴파일 시간에 타입을 자동으로 추론할 변수 앞에 붙인다.
- 해당 함수를 반환 값에 사용할 시 리턴 타입을 변경하더라도 코드에서 그 함수가 나온 모든 지점을 일일이 찾아서 고칠 필요 없이 간단히 수정할 수 잇다.
- auto&
- auto를 표현식 타입을 추론하는 데 사용하면 레퍼런스와 const가 제거된다.
- auto를 지정하면 레퍼런스와 const 한정자가 사라지기 때문에 값이 복제되어버린다. const 레퍼런스 타입으로 지정하려면 auto 키워드 앞뒤에 레퍼런스 타입과 const 키워드를 붙인다.
- 복제 방식으로 전달되지 않게 하려면 auto&나 const auto&로 지정한다.
- auto*
- auto 키워드는 포인터에도 적용할 수 있다.
- 복제가 되어버리는 경우가 발생하지는 않으나 포인터를 다룰 때는 auto* 구문을 사용하는 것이 바람직하다. 대상이 포인터임을 명시적으로 드러내기 때문이다.
- auto*를 사용하면 auto, const 그리고 포인터를 함께 쓸 때 발생하는 이상한 동작도 방지할 수 있다.
- const auto p1 { &i }; 일 시 int* const이다. 즉 비 const 정수에 대한 const 포인터인 것이다.
- auto const p2 { &i }; 일 시 int* const이다.
- auto*와 const를 함께 쓰면 의도한 대로 작동한다.
- const auto* p3 { &i }; 일 시 const int*가 된다. const int 대신 const 포인터를 꼭 써야 한다면 const를 끝에 붙인다.
- auto* const p4 { &i }; 일 시 int* const가 된다.
- const auto* const p5 { &i }; 일 시 const int* const이다.
- 복제 리스트 초기화와 직접 리스트 초기화
- 초기화 구문은 다음 두 가지가 있으며, 초기자 리스트를 중괄호로 묶어서 표현한다.
- 복제 리스트 초기화 : T obj = { arg1, arg2, ... };
- 직접 리스트 초기화 : T obj { arg1, arg2, ... };
- C++17부터는 auto 타입 추론 기능과 관련하여 복제 리스트 초기화와 직접 리스트 초기화의 차이가 커졌다.
- C++17부터는 <initializer_list>를 인클루드해야 한다.
- 복제 리스트 초기화에서 중괄호 안에 나온 원소는 반드시 타입이 모두 같아야 한다.
+ decltype 키워드
- decltype 키워드는 인수로 전달한 표현식의 타입을 알아낸다.
- decltype은 레퍼런스나 const 지정자를 삭제하지 않는다는 점에서 auto와 다르다. 따라서 복제 방식으로 처리하지 않으며 템플릿을 사용할 때 상당히 강력한 효과를 발휘한다.
1.1.36 표준 라이브러리
- C++는 표준 라이브러리를 제공한다. 여기에는 코드에서 쉽게 가져다 쓸 수 있도록 구성된 유용한 클래스가 다양하게 정의되어 있다.
- 표준 라이브러리에서 제공하는 클래스는 수많은 사용자에 의해 엄청난 테스트와 검증 과정을 거친 것이다. 따라서 고성능으로 튜닝된 것으로 여러분이 직접 구현하는 것보다 훨씬 성능이 뛰어날 가능성이 높다.
- C++를 사용할 때 표준 라이브러리를 활용하면 C방식보다 훨씬 쉽고 안전하게 구현할 수 있다.
- std::string, std::array, std::vector, std::pair, std::optional 등이 있다.
1.2 어느 정도 규모 있는 첫 C++ 프로그램
1.2.1 직원 정보 관리 시스템
1.2.2 Employee 클래스
1.2.3 Database 클래스
1.2.4 사용자 인터페이스
1.2.5 프로그램 평가하기
1.3 정리
1.4 연습 문제
'Programming II > C++' 카테고리의 다른 글
[전문가를 위한 C++] CHAPTER3 코딩 스타일 (0) | 2025.03.30 |
---|---|
[전문가를 위한 C++] CHAPTER2 스트링과 스트링 뷰 다루기 (0) | 2025.03.30 |
전문가를 위한 C++ (0) | 2025.03.30 |
Effective C++ (0) | 2025.03.26 |
[Effective C++] Chapter9 그 밖의 이야기들 (0) | 2025.03.26 |