1.1 C++ 기초

1.1.1 프로그래밍 언어의 공식 예제 'Hello, World!'

1. 주석

2. 모듈 임포트

import <iostream>

3. 전처리 지시자

#include <iostream>

빌드 작업 세단계

  • 전처리 : 소스코드에 담긴 메타 정보 처리
  • 컴파일 : 소스코드를 머신이 읽을 수 있는 객체 파일로 변환
  • 링크 : 앞에서 변환한 여러 객체 파일을 애플리케이션으로 엮음

헤더 파일의 용도

  • 소스파일에서 정의할 함수를 선언
    • 함수의 호출 방식, 매개변수의 개수와 타입, 리턴 타입 등을 컴파일러에 알려줌

자주 사용하는 전처리 지시자

#include [파일]
  • 기능 : 저장할 파일의 내용을 지시자 위에 넣는다
  • 사용 예 : 다른 곳에 정의된 함수를 사용할 목적으로 해당 함수의 선언문이 담긴 헤더파일을 가져온다
#define [키] [값]
  • 용도 : 코드에서 '키'에 해당하는 부분을 모두 '값'으로 지정한 내용으로 바꾼다
  • 사용 예 : C에서는 주로 상수값이나 매크로를 정의하는데 사용
  • c++에서 더 나은 메커니즘이 있음
#ifdef [키]
#endif
#ifndef [키]
#endif
#pragma [xyz]

ex) pragma once

  • 중복 인클루드를 막는 용도

4. main() 함수

  • 프로그램은 항상 main() 함수에서 시작한다
  • main() 함수는 int 타입의 값을 리턴
  • 리턴 문장을 생략하도 되는데, 자동으로 0을 리턴
  • main() 함수는 매개변수를 받지 않거나 다음과 같이 두 개를 받도록 작성할 수 있다
int main(int argc, char* argv[])

argc는 프로그램에 전달할 인수의 개수를 지정하고 argv는 전달할 인수의 값을 담는다
argv[0] 에는 프로그램 이름이 담기는데 공백 스트림으로 지정될 수도 있어서 프로그램 이름을 참조하려면 이 값보다 플랫폼에서 제공하는 기능을 사용하는 것이 좋다

5. I/O 스트림

std::cout << "There are" << 219 << "ways I lve you." << std::endl;
std::cout << std::format("There are {} ways I love you.", 219) << std::endl;

이스케이프 시퀀스(escape sequence)

  • \n : 줄바꿈. 커서를 다음 라인의 맨 앞으로 이동시킨다
  • \r : 캐리지 리턴. 커서를 현재 라인의 맨 앞으로 이동시킨다
  • \t : 탭
  • \ : 역슬래시

endl은 스트림에 줄바꿈 문자를 추가한 뒤 현재 버퍼에 있는 내용을 출력장치로 내보낸다. endl은 성능에 영향을 미치기 때문에 루프와 같은 문장에서 남용하면 좋지 않다. 반면 \n도 스트림에 줄바꿈 문자를 추가하지만 버퍼를 자동으로 비우지 않는다.

1.1.2 네임스페이스

  • 코드에서 이름이 서로 충돌하는 문제를 해결하기 위해 나온 개념

  • 스코프 지정 연산자(scope resolution operator (::))

  • 네임스페이스 블록 안에서 접근할 때는 네임스페이스 접두어를 붙이지 않아도 된다.

    • 코드의 가독성 향상
  • using 지시자로 네임스페이스 접두어를 생략할 수도 있다.

헤더 파일 안에서는 절대로 using 문을 작성하면 안된다. 그러면 그 파일을 include 하는 모든 파일에서 using 문으로 지정한 방식으로 호출해야 한다.

1. 중첩 네임스페이스

  • 다른 네임스페이스 안에 있는 네임스페이스를 말한다.

2. 네임스페이스 앨리어스

  • 네임스페이스의 이름을 다르게 만들거나 또는 더 짧게 만들 수 있다.
    namespace MyFTP = MyLibraries::Networking::FTP;

1.1.3 리터럴

  • 리터럴은 코드에 표시한 숫자나 스트링과 같은 값을 의미한다.

1.1.4 변수

  • 변수를 선언할 때 반드시 값을 대입(초기화)할 필요는 없다. 하지만 초기화하지 않은 변수는 선언할 시점의 메모리값을 기반으로 무작위 값이 대입될 수 있는데, 그냥 사용하면 버그가 발생할 수 있다.
int unitializedInt;
// 균일초기화
int initializedInt { 7 };
  • 균일 초기화(uniform initialization)

  • 균일 초기화는 c++11 버전부터 도입되었다. 기존 대입 문법 대신 균일 초기화를 사용하는 것이 바람직

  • c++은 강타입(strongly typed) 언어이다. 그러므로 항상 타입을 구체적으로 지정해야한다.

    c++에서 자주 사용하는 타입

  • (signed) int , signed : 부호가 있는 정수(양수와 음수)를 표현하며, 값의 범위는 컴파일러마다 다르다(4바이트)

  • (signed) short (int) : 작은 범위의 정수(2바이트)

  • (signed) long (int) : 큰 범위의 정수

  • (signed) long long (int) : 아주 큰 범위의 정수. (8바이트)

  • char, unsigned char, signed char : 단일 문자

  • char8_t, char16_t, char32_t : 단일 n 비트 UTF n 인코딩을 적용한 유니코드 문자

  • char 타입은 signed char나 unsigned char와 다른 타입이다. 이 타입은 문자로 표현해야 한다. 컴파일러의 종류에 따라 signed로 처리할 수도 있고 unsigned로 처리할 수도 있음

std::byte b { 42 };

1. 숫자 경곗값

numeric_limits<int>::max();
numeric_limits<int>::min();
numeric_limits<int>::lowest();
  • min()과 lowest() 가 다르다
  • 정수에서는 최솟값(minimum)과 최젓값(lowest value)이 같지만, 부동소수점수에서는 최솟값은 표현 가능한 가장 작은 양의 값인 반면, 최젓값은 표현가능한 가장 작은 음수다.

2. 0 초기화

  • 빈 중괄호 {}로 표기한 유니폼 초기화를 영 초기자(zero initializer)라고 한다.
    float myFloat {};
    int myInt {};

3. 캐스트

  • 변수의 타입은 실행 중에 바꿀 수 있다. 이를 캐스트라 한다.
  • 변수의 타입을 명시적으로 변환할 수 있음.
  • 데이터가 손실될 수 있다는 점에 주의해야 함.

4. 부동소수점수

1.1.5 연산자

  • 연산자 : 변숫값 변경에 사용

    • 이항연산자, 단항연산자, 삼항연산자
  • % : 나머지 연산자, 모드(mod), 모듈로(modulo)

  • <<, >> : 비트값을 오른쪽에 나온 수만큼 shift tlzlsek.

1.1.6 열거 타입

강타입 열거 타입(strongly typed enumeration type)을 적용하면 변수에 지정할 수 있는 값의 범위를 엄격하게 제한

enum class PieceType { King, Queen, Rook, Pawn };
  • c++20 부터는 using enum으로 선언하면 열것값을 길게 풀어 쓰지 않아도 된다.

    이 기능은 조심해서 사용!!

1. 예전 타입의 열거 타입

enum PieceType { PieceTypeKing, PieceTypeQueen };
bool ok { false };
enum Status { error, ok };
  • 오류 발생. 상위스코프에 같은 이름이 있으면 컴파일 에러가 발생
  • 따라서 예전 방식으로 열거 타입을 정의할 때는 멤버 이름을 고유한 이름으로 지정해야 한다.
  • 이처럼 예전 방식의 열거 타입을 정의할 때는 고유한 이름으로 지정해야 한다.

열거 타입을 사용할 때는 타입에 안전하지 않은, 즉 타입 언세이프한 예전 방식의 enum보다는 강타입 버전인 enum class로 작성하는 것이 좋다.

1.1.7 구조체

export module employee;

export struct Employee {
    char firstInitial;
    int salary;
}
import employee;

int main()
{
    Employee employee;
}

1.1.8 조건문

if/else 문

  • if문 안에 초기자를 넣을 수 있다.
  • if(<초기자>; <조건문>)
if(Employee employee { getEmployee() }; employee.salary > 100) {
}

switch문

  • switch문도 초기자를 지정할 수 있다.

1.1.9 조건연산자

  • [조건] ? [동작1] : [동작 2]

1.1.10 논리연산자

c++은 논리표현식을 평가할 때 단락논ㄹ니(short-circuit logic)을 사용
평가하는 도중에 최종 결과가 나오면 나머지 부분은 평가하지 않는다.

1.1.11 3방향 비교 연산자

  • c++ 20에서 새로 생김

  • aka 우주 연산자(spaceship operator)

  • <=> 연산자는 주어진 표현식의 평가 결과가 비교 대상이 되는 값과 같은지 아니면 그보다 크거나 작은지 알려준다. 이 연산자는 true/false가 아니기 때문에 compare에 정의 되고 std 네임스페이스에 속한 열거 타입으로 리턴한다.

  • 피연산자가 정수 = 강한 순서(strong ordering)

  • strong_ordering::less : 첫번째 피연산자가 두번째 연산자보다 작다

  • strong_ordering::greater

  • strong_ordering::equal

  • 피연산자가 부동소수점 = partial ordering

  • 자신이 정의한 타입에 대해 3방향 비교 연산자

  • weak_ordering::less

  • weak_ordering::greater

  • weak_ordering::equivalent

1.1.12 함수

  • 프로그램의 가독성을 높이려면 코드를 간결하고 명확한 함수 단위로 나눠야함
  • 이 파일 안에서만 사용 > 선언+구현 모두 소스파일 안에
  • 다른 데서도 쓸거다 -> 모듈 인터페이스 파일로부터 익스포트하고, 정의는 모듈 인터페이스 파일이나 모듈 구현 파일에 작성

함수를 선언하는 문장 : 함수 프로토타입(function prototype) 또는 함수 헤더(function header)라 부른다. 함수의 내용을 보지 않고 그 함수에 접근하는 방식만 표현

함수의 리턴 타입을 제외한 함수 이름과 매개변수 목록을 함수 signature라 부른다.

함수의 리턴 타입 추론

auto addNumbers(int number1, int number2)
{
    return number1+number2;
}

현재 함수 이름

int addNumbers()
{
    cout << __func__ << std::endl;
}

함수 오버로딩

  • 이름은 같지만 매개변수 구성은 다른 함수를 여러 개 제공한다는 뜻. 리턴타입만 달라서는 안된다. 매개변수의 타입이나 개수도 달라야한다.

1.1.13 어트리뷰트

  • 소스코드에 벤더에서 제공하는 정보나 옵션을 추가하는 메커니즘

[[nodiscard]]

이 함수가 호출될 때 리턴값에 아무런 작업을 하지 않으면 경고 메시지를 출력함.

\[\[nodiscard]] int func()
{
    return 42;
}


//cpp 20 부터
\[\[nodiscard("Some explanation")]] int func()
{
    return 42;
}

[[maybe_unused]]

int func(int a, int b)
{
    return 42;
}
  • 컴파일 warning이 생길 수 있다.
int func(int a, \[\[maybe_unused]] int b)
{
    return 42;
}
  • 이 attribute는 클래스, 구조체, 비 static 데이터 멤버, 유니온, typedef, 변수 등에 지정할 수 있다.

[[noreturn]]

  • 호출지점으로 다시 돌아가지 않는다. 주로 프로세스나 스레드 종류와 같이 뭔가가 끝나게 만들거나, 익셉션을 던지는 함수가 여기에 해당
\[\[noreturn]] void forceProgramTermination()
{
    std::exit(1);
}

bool isFeaturedLiscensed(int featureId)
{
    if(!isDongleAvailable()) {
        forceProgramTermination();
    }
}

[[deprecated]]

  • 지원중단된 대상임을 지정하는데 사용.
  • 현재 사용할 수는 있지만 권장하지 않는 대상임을 표시

[[likely]] [[unlikely]]

  • c++ 20
  • 컴파일러가 최적화 작업을 수행하는데 도움을 줌

1.1.14 C 스타일 배열

int myArray[3];
myArray[0] = 0;
myArray[1] = 0;
myArray[2] = 0;

int myArray[3] = {0};
int myArray[3] = {};
int myArray[3] {};

int myArray[3] = {2};

1.1.15 std::array

  • c++에서 하는 것
  • C 스타일의 배열에 비해 장점이 많다
    • 항상 크기를 정확히 알 수 있다.
    • 자동으로 포인터를 캐스트하지 않아서 특정한 종류의 버그를 방지할 수 있고, 반복자로 배열에 대한 반복문을 쉽게 작성할 수 있다.
array<int, 3> arr{9,8,7};
arr.size();

array arr{9,8,7};
// CTAD class template argument deduction(클래스 템플릿 인수 추론) : template 타입을 지정하지 않아도 됨.

1.1.16 std::vector

vector<int> myVector{11,22};
myVector.push_back(33);
myVector.push_back(44);

vector myVector{11, 12}; //OK CTAD
vector myVector; //XXXX

1.1.17 std::pair

pair<double, int> myPair {1.23, 5};
cout << myPair.first << myPair.second << std::endl;

1.1.18 std::optional

1.1.19 구조적 바인딩

  • 여러 변수를 선언할 때 array, struct, pair 등에 담긴 원소들을 이용하여 변숫값을 한꺼번에 초기화할 수 있다.
array values {11,12,13};

auto [x,y,z] {values};
  • 이때 그냥 auto라 쓰지 않고 auto&나 const auto&를 이용하여 구조적 바인딩으로 비 const에 대한 레퍼런스나 const에 대한 레퍼런스를 생성할 수도 있다.
방식 복사? 원본 변경 가능?
auto O
auto&
const auto&

1.1.20 반복문

while

do/while문

for

범위 기반 for문

1.1.21 초기자 리스트

#include <initializer_list>

int makeSum(initializer_list<int> values)
{

}

1.1.23 클래스

클래스 이름부터 적는다.

  • 데이터멤버, 메서드를 선언한다.
  • 접근 수준을 지정한다.

1.1.24 스코프 지정

스코프 : 변수, 함수, 클래스명과 같이 프로그램에 나오는 모든 이름은 저마다 스코프가 있다. 스코프는 namespace, 함수정의, 등등

class Demo
{
    public:
        int get() {return 5;}
};

int get() {return 5;}

namespace NS
{
    int get() {return 20;}
}
int main()
{
    Demo d;
    d.get (); //5
    NS::get(); //20
    ::get(); //10
    get(); //10
}

1.1.25 균일 초기화

  • c++ 11 이전에는 타입의 초기화 방식이 일정하지 않았다.
    //c++ 11이전
    CircleStruct myCircle1 = {10,10,2.5};
    CircleClass myCircle2(10,10,2.5);
    

//c++ 11 이후
CircleStruct myCircle1 = {10,10,2.5};
CircleClass myCircle2 = {10,10,2.5};
CircleClass myCircle3 {10,10,2.5};


```cpp
int* pArray = new int[4] {0,1,2,3};

균일초기화를 가능한 모든 곳에서 이용하는게 바람직

지정초기자

  • c++20 부터 지정초기자가 도입
struct Employee {
    char firstInitial;
    char lastInitial;
    int employeeNumber;
    int salary {75000};
};

// 균일초기화
Employee employee {'J','D',42,80000};

// 지정초기화
Employee employee {
    .firstInitial = 'J',
    .lastInitial = 'D',
    .salary = 80000
};

// 초기화 생략도 가능

1.1.26 포인터와 동적 메모리

스택과 프리스토어

스택 : 독립적인 공간, 실행이 끝나면 삭제됨(할당해제 할 필요없음)
스택 프레임

프리스토어 : 스택과는 다른 독립적인 메모리 공간 / 직접 할당해제 해야함

포인터 사용법

  • 메모리 공간을 적당히 할당하기만 하면 어떠한 값이라도 free store에 저장 가능
  • 정숫값을 프리스토어에 저장하려면 정수 타입에 맞는 메모리 공간을 할당해야함
int* myPointer;

// 초기화 필수
int* myPointer { nullptr };
    • 은 이 변수가 정수 타입에 대한 메모리 공간을 가리킨다는 것을 의미
  • 동적으로 할당된 프리스토어 메모리를 가리키는 화살표와 같다.
myIntegerPointer = new int;
  • 정숫값 하나에 대한 메모리 주소를 가리킨다. 이 포인터가 가리키는 값에 접근하려면 포인터를 역참조(참조해제) 해야 한다. 포인터가 프리스토어에 있는 실제 값을 가리키는 화살표를 따라간다는 뜻
*myPointer = 8;

delete myPointer;
myPointer = nullptr;

포인터를 역참조하려면 반드시 메모리가 할당되어 있어야 한다.

int i{8};
int* myPointer {&i}; // 8이란 값을 가진 변수 i의 주소를 가리키는 포인터
Employee* employee{getEmployee()};
cout << (*employee).salary << endl;

// -> 연산자
cout << employee->salary << endl;
  • *연산자로 역참조 해서 구조체 자체(시작지점)에 접근한 뒤 필드에 접근할 때는 . 연산자로 표기
bool isValidSalary {employee && employee->salary > 0};

동적으로 배열할당하기

int arraysize{8};
int* array{new int[arraysize]};

1.1.27 const의 다양한 용도

  • 변경되면 안될 대상을 선언할 때 사용

1. const 상수

#define num 2

// 이거 대신 const 사용
const int num {2};

define은 전처리기(preprocessor)에서 처리하고 const는 컴파일러가 처리.
define문은 코드를 메타 수준으로 처리해서 단순히 텍스트 매칭 작업
const는 컴파일러가 평가. 타입이나 스코프를 적용할 수 있다는 장점

const 포인터

int* ip;
ip = new int[10];
ip[4] = 5;
const int* ip;
ip = new int[10];
ip[4] = 5; // 컴파일 에러 발생
int* const ip{nullptr};
ip = new int[10]; // 컴파일 에러
ip[4] = 5; // 에러 : 널포인터 역참조

const키워드는 바로 왼쪽에 나온 대상에 적용된다.

int const* const ip {nullptr};
// ==
const int* const ip {nullptr};
int* const ip; // ip는 int에 대한 포인터에 const
int const* ip; // ip는 const가 적용된 int에 대한 포인터

const 매개변수

  • 함수 내에서 매개변수 값을 변경할 수 없도록

2. const 메서드

  • 클래스 메서드에도 const를 지정할 수 있다.
  • 해당 클래스의 데이터 멤버를 수정할 수 없게 만든다.
  • const 메서드 중에서 어느하나라도 데이터멤버를 수정하려고 하면 컴파일 에러가 발생
export class AirlineTicket
{
    public:
        double calculatePriceInDollors() const;
}

1.1.28 constexpr 키워드

  • 상수표현식 : 컴파일시간에 평가되는 표현식
contexpr int getArraySize() {return 32;}

int main()
{
    int arr[getArraySize()];
}

1.1.29 consteval 키워드

  • constexpr : 함수가 컴파일 시간에 실행될 수 있다고 지정, 보장 X
  • 함수가 항상 컴파일 시간에 평가되도록 보장 : c++ 20부터 제공하는 consteval 키워드

1.1.30 레퍼런스

  • 레퍼런스(참조)란 변수에 대한 alias다.
  • 레퍼런스에 대해 수정한 내용은 그 레퍼런스가 가리키는 변수의 값에 그대로 반영
  • 변수의 주소를 가져오거나 변수에 대한 역참조 연산을 수행하는 작업을 자동으로 처리해주는 특수한 포인터
  • 또는 변수에 대한 다른 이름(별칭)

레퍼런스 변수

반드시 생성 즉시 초기화

int x {3};
int& xRef {x};

//값 변경
xRef = 10;

레퍼런스 대상 변경

레퍼런스를 초기화하고 나면 그 레퍼런스가 가리키는 대상을 다른 변수로 변경할 수 없고, 그 레퍼런스가 가리키는 변수의 값만 바꿀 수 있다.

int x {3}, y{4};
int& xRef {x};
xRef = y; // xRef가 가리키는 대상이 y로 변경되지 않고 x의 값이 4로 바뀐다.
xRef = &y; // 컴파일 에러 
// y의 주소는 포인터이지만 xRef 는 포인터에 대한 레퍼런스가 아닌 int에 대한 레퍼런ㅅ느 이기 때문이다

const 레퍼런스

  • 포인터에 const보다 쉬움
    • 레퍼런스는 가리키는 대상을 변경할 수 없기 때문에 기본적으로 const. 명시적으로 지정할 필요가 없다.
    • 레퍼런스에 대한 레퍼런스를 만들 수 없기 때문에 참조가 한단계 뿐이다.
int z;
const int& zRef {z};
zRef = 4;
  • int&에 const를 지정하면 zRef에 다른 값을 대입할 수 없다.
  • 하지만 zRef를 const라고 지정한 것은 z에 영향을 미치지 않는다. 바로 z에 접근하면 값 변경 가능
int& unnamedRef1 {5}; // 컴파일 에러
const int& unnamedRef2 {5}; // 정상 작동

포인터에 대한 레퍼런스와 레퍼런스에 대한 포인터

int* intP {nullptr};
int*& ptrRef {intP};
ptrRef = new int;
*ptrRef = 5;

int*& = 포인터 변수 자체를 수정할 수 있게 해주는 참조

  • ptrRef는 intP에 대한 reference
  • intP는 int에 대한 포인터

초기:
intP → null ptrRef → intP

ptrRef = new int;
intP → heap int ptrRef → same

*ptrRef = 5;
heap int = 5

int x{3};
int& xRef {x};
int* xPtr {&xRef};
*xPtr = 100;

x에 대한 레퍼런스의 주소를 가져와서 xPtr이 x를 가리키도록 설정
*xPtr에 100을 대입하면 x의 값이 100으로 바뀐다

구조적 바인딩과 레퍼런스

auto& [string, int] {pair}; // 비 const 레퍼런스로 분해
const auto& [string, int] {myPair}; // const 레퍼런스로 분해

2. 레퍼런스 데이터 멤버

  • 레퍼런스 데이터 멤버는 반드시 생성자의 본문이 아닌 생성자 초기자에서 초기화

3. 레퍼런스 매개변수

pass by reference.

const 레퍼런스 전달 방식

  • 성능이 더 좋아짐

레퍼런스 전달 방식과 값 전달 방식

레퍼런스 전달 방식의 장점

  • 큰 객체를 복제하는데 시간이 오래 걸릴 수 있다. 하지만 레퍼런스 전달 방식은 객체에 대한 레퍼런스만 함수에 전달
  • 지원 : 값 전달 방식을 허용하지 않는 클래스가 있다.

4. 레퍼런스 리턴값

  • 함수나 메서드의 리턴값도 레퍼런스 타입으로 지정할 수 있다. 물론 함수 종료 후에도 계속 남아있는 객체에 대해서만 레퍼런스로 리턴할 수 있다.

5. 레퍼런스와 포인터의 선택 기준

포인터보다 레퍼런스를 사용하는 것이 좋다. 레퍼런스로 충분하지 않을 때만 포인터를 사용한다.

1.1.31 const_cast

  • const 속성을 추가하거나 제거하는 다섯가지 캐스트 방법

1.1.32 익셉션

1.1.33 타입 Alias

  • 기존에 선언된 타입에 다른 이름을 붙이는 것
using IntPtr = int*;

1.1.34 typedef

  • type alias 와 마찬가지로 기존에 선언된 타입에 다른 이름을 붙여줌.
typedef int* IntPtr;

1.1.35 타입 추론

1. auto 키워드

  • 함수의 리턴 타입 추론
  • 구조적 바인딩 사용

auto&

  • auto를 표현식 타입을 추론하는데 사용하면 reference와 const가 제거된다.

auto를 지정하면 reference와 const 지정자가 사라져서 값이 복제된다. 복제 방식으로 전달되지 않게 하려면 auto&나 const auto&로 지정한다.

auto*

2. decltype 키워드

int x {123};
decltype(x) y {456};
  • 인수로 전달한 표현식의 타입을 알아낸다

1.4 연습문제

BELATED ARTICLES

more