개인 기록용
-------------------
학습에 참고한 사이트 : learncpp.com
학습한 챕터 : 챕터 1,2
학습에 참고한 번역 프롬프트 : 해당 원문은 learncpp.com에서 발췌해온 c++ 강의 내용인데, 프로그래밍 용어는 한국어 표준 용어를 사용하되, 이해를 돕기 위해 필요한 경우 원문을 병기해서 한국어로 번역해줘.
-------------------
명령문 - Statements
함수 - Functions
• 함수는 순차적으로 위에서 아래로 실행되는 명령문(Statements)들의 집합입니다.
문자 - Character
• 문자는 글자,숫자,문장 부호,수학 기호와 같은 서면 기호나 표시 입니다.
문자열 - String
• 단어나 문장을 쓸 때처럼 많은 경우 우리는 하나 이상의 문자(Character)를 사용하고 싶어 합니다. 문자들의 나열을 문자열(String)이라고 부릅니다.
구문 - Syntax
• 특정 언어에서 유효한 문장을 만들기 위해 단어(와 문장 부호)를 어떻게 배열해야 하는지 설명하는 규칙들의 집합을 구문(Syntax, 또는 문법)이라고 합니다.
• C++에도 구문이 있으며, 이는 프로그램이 유효한 것으로 간주되기 위해 요소들이 어떻게 작성되고 배열되어야 하는지를 설명합니다. 만약 프로그램이 언어의 구문에서 벗어난 무언가를 한다면, 컴파일러는 컴파일을 중단하고 구문 오류(Syntax error)를 발생시킵니다.
값 - Value
• 데이터의 단일 조각
리터럴 - Literal
• 소스 코드에 직접 입력된 값(Value)을 리터럴(Literal)이라고 합니다.
• 리터럴은 프로그램에 값을 제공하는 가장 쉬운 방법이지만, 읽기 전용 값이므로 그 값을 수정할 수 없습니다.
객체 - Objects
• C++에서는 직접적인 메모리 접근을 권장하지 않습니다. 대신, 우리는 객체(Objet)를 통해 간접적으로 메모리에 접근합니다.
• 객체는 값을 담을 수 있는 저장 공간의 영역을 나타냅니다.
• 핵심은 "메모리 상자 번호 7532에 저장된 값을 가져와"라고 하는 대신, "이 객체가 저장하고 있는 값을 가져와"라고 말하면 컴파일러가 그 값을 어디서 어떻게 가져올지 알아서 처리한다는 점입니다. 즉, 우리는 객체가 실제 메모리 어디에 위치하는지 걱정할 필요 없이, 객체를 사용하여 값을 저장하고 가져오는 데만 집중하면 됩니다.
• C++의 객체는 이름이 없을(익명일) 수도 있지만, 대부분의 경우 우리는 식별자(identifier)를 사용하여 객체에 이름을 붙입니다. 이름이 있는 객체를 변수(variable)라고 합니다.
변수 생성 - Varible creation
• 런타임(runtime)(프로그램이 메모리에 로드되어 실행될 때)에 각 객체는 값을 저장하는 데 사용할 수 있는 실제 저장 위치(RAM이나 CPU 레지스터 등)를 부여받습니다. 객체 사용을 위해 저장 공간을 예약하는 과정을 할당(allocation)이라고 합니다. 할당이 이루어지면 객체는 생성된 것이며, 사용할 수 있게 됩니다.
• 예를 들어, 변수 x가 메모리 위치 140에 인스턴스화(instantiated)되었다고 가정해 봅시다. 프로그램이 변수 x를 사용할 때마다 메모리 위치 140에 있는 값에 접근하게 됩니다.
데이터 타입 - Data types
• 데이터 타입은 객체가 어떤 종류의 값(예: 숫자, 글자, 텍스트 등)을 저장할지 결정합니다.
변수 초기화 - Variable initialization
• 객체에 초기 값을 지정하는 과정을 초기화(initialization)라고 하며, 객체를 초기화하는 데 사용되는 구문을 초기화자(initializer)라고 합니다.
다양한 초기화 형태
• C++에는 5가지 일반적인 초기화 형태가 있습니다.
- int a; 기본 초기화 (default-initialization, 초기화자 없음)
• 전통적인 초기화 형태:
- int b = 5; 복사 초기화 (copy-initialization, 등호 뒤에 초기 값)
- int c ( 6 ); 직접 초기화 (direct-initialization, 괄호 안에 초기 값)
• 현대적인 초기화 형태 (선호됨):
- int d { 7 }; 직접 리스트 초기화 (direct-list-initialization, 중괄호 안에 초기 값)
- int e {}; 값 초기화 (value-initialization, 빈 중괄호)
• C++17부터는 대부분의 경우 복사 초기화, 직접 초기화, 직접 리스트 초기화가 동일하게 동작합니다.
기본 초기화 - Default initialization
• int x;
• 많은 경우 기본 초기화는 아무런 초기화를 수행하지 않으며, 변수에 결정되지 않은 값을 남겨둡니다.
복사 초기화 - Copy initialization
• int x = 5;
• 등호(=) 뒤에 초기 값이 제공되는 경우, 이를 복사 초기화(copy-initialization)라고 합니다. 이 초기화 형태는 C 언어에서 물려받았습니다.
직접 초기화 - Direct initialization
• int x (5);
• 직접 초기화는 원래 복잡한 객체(나중에 다룰 클래스 타입)의 더 효율적인 초기화를 허용하기 위해 도입되었습니다.
• 복사 초기화와 마찬가지로, 직접 초기화는 직접 리스트 초기화에 의해 대체되면서 현대 C++에서 선호도가 떨어졌습니다.
• 그러나 직접 리스트 초기화에도 몇 가지 특이한 점이 있어 특정 경우에는 직접 초기화가 다시 사용되고 있습니다.
리스트 초기화 - List initialization
• int x {5};
• C++에서 객체를 초기화하는 현대적인 방법은 중괄호 {}를 사용하는 초기화 형태입니다.
• 이를 리스트 초기화(list-initialization)(또는 일관된 초기화(uniform initialization), 중괄호 초기화(brace initialization))라고 합니다.
• 리스트 초기화는 거의 모든 경우에 작동하고, 일관되게 동작하며, 객체를 초기화하는 곳임을 쉽게 알 수 있는 명확한 구문을 제공하기 위해 도입되었습니다.
• C++ 초보 프로그래머에게 리스트 초기화의 주요 이점 중 하나는 "축소 변환"이 허용되지 않는다는 것입니다.
• 즉, 변수가 안전하게 담을 수 없는 값을 사용하여 변수를 리스트 초기화하려고 하면, 컴파일러는 진단(컴파일 오류 또는 경고)을 생성하여 알려야 합니다.
값 초기화와 0 초기화 - Value-initialization/Zero-initialization
• 변수가 빈 중괄호 세트를 사용하여 초기화될 때, 값 초기화(value-initialization)라는 특별한 형태의 리스트 초기화가 발생합니다.
• 대부분의 경우 값 초기화는 변수를 암시적으로 0(또는 해당 타입에서 0에 가장 가까운 값)으로 초기화합니다.
• 0으로 만드는 작업이 일어나는 경우 이를 0 초기화(zero-initialization)라고 합니다.
인스턴스화 - instantiation
• 인스턴스화(instantiation)라는 용어는 변수가 생성(할당)되고 초기화(기본 초기화 포함)되었다는 것을 의미하는 고급진 단어입니다.
• 인스턴스화된 객체는 때때로 인스턴스(instance)라고 불립니다. 이 용어는 주로 클래스 타입 객체에 적용되지만, 때로는 다른 타입의 객체에도 사용됩니다.
[[maybe_unused]] 속성 (C++17)
• 초기화된 변수가 존재하지만 그 변수를 사용하지 않을때 컴파일러는 경고를 발생시키는데, 이런 경우를 해결하기 위해 C++17은 [[maybe_unused]] 속성(attribute)을 도입했습니다. 이를 통해 컴파일러에게 변수가 사용되지 않아도 괜찮다는 것을 알릴 수 있습니다. 컴파일러는 이러한 변수에 대해 사용되지 않은 변수 경고를 생성하지 않을 것입니다.

입출력 라이브러리 <iostream>
• iostream의 io 부분은 input/output(입력/출력)을 의미합니다.
std::cout
• iostream 라이브러리는 우리가 사용할 수 있는 몇 가지 미리 정의된 변수들을 포함하고 있습니다.
• 가장 유용한 것 중 하나는 데이터를 텍스트 형태로 콘솔에 출력할 수 있게 해주는 std::cout입니다.
• cout은 "character output(문자 출력)"을 의미합니다.
std::cout은 버퍼링됩니다.
여러분이 가장 좋아하는 놀이공원의 롤러코스터를 생각해 보세요. 승객들이 (다양한 속도로) 나타나 줄을 섭니다. 주기적으로 열차가 도착하여 승객들을 태웁니다(열차의 최대 수용 인원까지). 열차가 가득 차거나 충분한 시간이 지나면, 열차는 한 무리의 승객을 태우고 출발하며 놀이기구가 시작됩니다. 현재 열차에 타지 못한 승객들은 다음 열차를 기다립니다.
이 비유는 C++에서 std::cout으로 전송된 출력이 일반적으로 처리되는 방식과 유사합니다. 우리 프로그램의 문(statement)들은 출력이 콘솔로 전송되기를 요청합니다. 하지만 그 출력은 보통 즉시 콘솔로 전송되지 않습니다. 대신, 요청된 출력은 "줄을 서게" 되고, 이러한 요청을 모으기 위해 따로 마련된 메모리 영역(버퍼(buffer)라고 함)에 저장됩니다. 주기적으로 버퍼가 플러시(flush)되는데, 이는 버퍼에 모인 모든 데이터가 목적지(이 경우 콘솔)로 전송된다는 것을 의미합니다.
핵심 통찰 (Key insight)
• 버퍼링된 출력의 반대는 버퍼링되지 않은(unbuffered) 출력입니다. 버퍼링되지 않은 출력에서는 각 개별 출력 요청이 출력 장치로 직접 전송됩니다.
• 데이터를 버퍼에 쓰는 것은 일반적으로 빠르지만, 데이터 묶음을 출력 장치로 전송하는 것은 비교적 느립니다. 버퍼링은 여러 출력 요청을 묶어서 처리함으로써 출력 장치로 데이터를 보내는 횟수를 최소화하여 성능을 크게 향상시킬 수 있습니다.
std::endl VS \n
• std::endl을 사용하는 것은 종종 비효율적입니다. 왜냐하면 실제로 두 가지 작업을 수행하기 때문입니다. 줄 바꿈을 출력하고(콘솔의 커서를 다음 줄로 이동), 버퍼를 플러시합니다(느림). std::endl로 끝나는 여러 줄의 텍스트를 출력하면 여러 번의 플러시가 발생하게 되는데, 이는 느리고 아마도 불필요할 것입니다.
• 콘솔에 텍스트를 출력할 때, 일반적으로 우리가 직접 버퍼를 플러시할 필요는 없습니다. C++의 출력 시스템은 주기적으로 스스로 플러시하도록 설계되어 있으며, 스스로 플러시하게 두는 것이 더 간단하고 효율적입니다.
• 출력 버퍼를 플러시하지 않고 줄 바꿈을 출력하려면 \n(작은따옴표 또는 큰따옴표 안)을 사용합니다. 이는 컴파일러가 줄 바꿈 문자로 해석하는 특수 기호입니다. \n은 플러시를 유발하지 않고 콘솔의 커서를 다음 줄로 이동시키므로 일반적으로 성능이 더 좋습니다. 또한 \n은 입력하기 더 간결하며 기존의 큰따옴표로 묶인 텍스트 안에 포함될 수 있습니다.
int main()
{
int x{ 5 };
std::cout << "x is equal to: " << x << '\n'; // 작은따옴표 사용 (단독일 때) (관습적)
std::cout << "Yep." << "\n"; // 큰따옴표 사용 (단독일 때) (관습적이진 않지만 괜찮음)
std::cout << "And that's all, folks!\n"; // 기존 텍스트의 큰따옴표 사이에 포함 (관습적)
return 0;
}
operator<< 대 operator>>
• 초보 프로그래머들은 종종 std::cin, std::cout, 삽입 연산자(<<), 추출 연산자(>>)를 혼동합니다. 기억하기 쉬운 방법은 다음과 같습니다.
- std::cin과 std::cout은 항상 연산자의 왼쪽에 옵니다.
- std::cout은 값을 출력하는 데 사용됩니다 (cout = character output).
- std::cin은 입력 값을 받는 데 사용됩니다 (cin = character input).
- <<는 std::cout과 함께 사용되며, 데이터가 이동하는 방향을 보여줍니다. std::cout << 4는 값 4를 콘솔로 이동시킵니다.
- >>는 std::cin과 함께 사용되며, 데이터가 이동하는 방향을 보여줍니다. std::cin >> x는 사용자가 키보드로 입력한 값을 변수 x로 이동시킵니다.
초기화되지 않은 변수 (Uninitialized variables)
• 일부 프로그래밍 언어와 달리, C/C++은 대부분의 변수를 주어진 값(예: 0)으로 자동 초기화하지 않습니다. 초기화되지 않은 변수가 데이터를 저장하기 위해 메모리 주소를 할당받으면, 그 변수의 기본값은 해당 메모리 주소에 이미 들어 있던 아무 값(쓰레기 값, garbage value)이 됩니다! (초기화나 대입을 통해) 알려진 값이 주어지지 않은 변수를 초기화되지 않은 변수(uninitialized variable)라고 합니다.
• 많은 독자가 "초기화됨(initialized)"과 "초기화되지 않음(uninitialized)"이 엄격한 반의어일 것이라고 예상하지만, 완전히 그렇지는 않습니다! 통상적인 언어에서 "초기화됨"은 객체가 정의되는 시점에 초기 값이 제공되었음을 의미합니다. "초기화되지 않음"은 객체가 (대입을 포함한 어떤 수단을 통해서도) 아직 알려진 값을 받지 못했음을 의미합니다. 따라서 초기화되지 않았던 객체라도 나중에 값이 대입되면, 더 이상 초기화되지 않은 상태가 아닙니다(이제 알려진 값을 가지게 되었기 때문입니다).
• 초기화 (Initialization): 객체가 정의되는 시점에 알려진 값을 받음.
• 대입 (Assignment): 객체가 정의된 시점 이후에 알려진 값을 받음.
• 초기화되지 않음 (Uninitialized): 객체가 아직 알려진 값을 받지 못함.
정의되지 않은 동작 (Undefined behavior)
• 초기화되지 않은 변수의 값을 사용하는 것은 정의되지 않은 동작(Undefined Behavior, 줄여서 UB)의 첫 번째 예입니다.
• 정의되지 않은 동작은 C++ 언어에 의해 동작이 명확히 정의되지 않은 코드를 실행한 결과입니다.
키워드 또는 예약어 (Keywords/reserved words)
• C++은 자체적인 사용을 위해 92개의 단어(C++23 기준)를 예약해 두고 있습니다.
• 이 단어들을 키워드(keywords)(또는 예약어(reserved words))라고 하며, 각 키워드는 C++ 언어 내에서 특별한 의미를 가집니다.
다음은 모든 C++ 키워드의 목록입니다 (C++23 기준):
• alignas, alignof, and, and_eq, asm, auto, bitand, bitor, bool, break, case, catch, char, char8_t (C++20부터), char16_t, char32_t, class, compl, concept (C++20부터), const, consteval (C++20부터), constexpr, constinit (C++20부터), const_cast, continue, co_await (C++20부터), co_return (C++20부터), co_yield (C++20부터), decltype, default, delete, do, double, dynamic_cast, else, enum, explicit, export, extern, false, float, for, friend, goto, if, inline, int, long, mutable, namespace, new, noexcept, not, not_eq, nullptr, operator, or, or_eq, private, protected, public, registe.r, reinterpret_cast, requires (C++20부터), return, short, signed, sizeof, static, static_assert, static_cast, struct, switch, template, this, thread_local, throw, true, try, typedef, typeid, typename, union, unsigned, using, virtual, void, volatile, wchar_t, while, xor, xor_eq
식별자 (Identifier)
• 변수(또는 함수, 타입, 기타 항목)의 이름을 식별자(identifier)라고 합니다.
식별자 명명 모범 사례 (Identifier naming best practices)
• C++에서는 변수 이름을 소문자로 시작하는 것이 관례입니다. 변수 이름이 한 단어이거나 약어인 경우, 전체를 소문자로 작성해야 합니다.
int value; // 관례적 (conventional)
int Value; // 비관례적 (소문자로 시작해야 함)
int VALUE; // 비관례적 (소문자로 시작하고 전체가 소문자여야 함)
int VaLuE; // 비관례적 (정신과 상담을 받아보세요) ;)
• 대문자로 시작하는 식별자 이름은 일반적으로 사용자 정의 타입(구조체, 클래스, 열거형 등, 나중에 다룰 예정)에 사용됩니다.
• 변수나 함수 이름이 여러 단어로 구성된 경우, 두 가지 일반적인 관례가 있습니다.
• 단어를 밑줄로 구분하는 방식(때때로 snake_case라고 함)과, 단어의 첫 글자를 대문자로 표기하는 방식(낙타의 혹처럼 대문자가 튀어나와 있다고 해서 camelCase라고 함)입니다.
int my_variable_name; // 관례적 (밑줄로 구분/snake_case)
int my_function_name(); // 관례적 (밑줄로 구분/snake_case)
int myVariableName; // 관례적 (대문자 표기/camelCase)
int myFunctionName(); // 관례적 (대문자 표기/camelCase)
int my variable name; // 유효하지 않음 (공백 허용 안 됨)
int my function name(); // 유효하지 않음 (공백 허용 안 됨)
int MyVariableName; // 비관례적 (소문자로 시작해야 함)
int MyFunctionName(); // 비관례적 (소문자로 시작해야 함)
리터럴 (Literals)
• 리터럴(또는 리터럴 상수)은 소스 코드에 직접 삽입된 고정된 값을 의미합니다.
• 리터럴과 변수는 모두 값(과 타입)을 가집니다. (초기화와 대입을 통해 값을 설정하고 변경할 수 있는) 변수와 달리, 리터럴의 값은 고정되어 있으며 변경할 수 없습니다.
• 리터럴 5는 항상 값 5를 가집니다. 이것이 리터럴을 상수(constants)라고 부르는 이유입니다.
연산자 (Operators)
• 수학에서 연산(operation)은 0개 이상의 입력값을 포함하여 새로운 값을 생성하는 과정입니다. 수행할 특정 연산은 연산자(operator)라고 불리는 기호로 표시됩니다.
• 예를 들어, 우리는 어릴 때 2 + 3이 5와 같다는 것을 배웁니다. 이 경우 리터럴 2와 3은 피연산자이며, 기호 +는 피연산자에 수학적 덧셈을 적용하여 새로운 값 5를 생성하라고 지시하는 연산자입니다.
• 덧셈(+), 뺄셈(-), 곱셈(*), 나눗셈(/)을 포함한 일반적인 수학 사용법을 통해 표준 산술 연산자들에 이미 익숙할 것입니다. C++에서는 대입(=)도 연산자이며, 삽입(<<), 추출(>>), 동등 비교(==) 또한 연산자입니다. 대부분의 연산자는 기호로 된 이름(예: +, ==)을 가지지만, 키워드로 된 연산자(예: new, delete, throw)도 있습니다.
• 연산자에 대해 더 자세히 논의할 때 명확해질 이유들로 인해, 기호로 된 연산자는 operator라는 단어 뒤에 연산자 기호를 붙여서 부르는 것이 일반적입니다. 예를 들어, 더하기 연산자는 operator+로, 추출 연산자는 operator>>로 표기합니다.
• 연산자가 입력으로 받는 피연산자의 개수를 연산자의 항(arity)이라고 합니다. 이 단어의 뜻을 아는 사람은 거의 없으므로, 대화 중에 이 단어를 사용하면서 상대방이 알아들을 것이라고 기대하지 마세요. C++의 연산자는 네 가지 다른 항(arity)을 가집니다.
• 단항(Unary) 연산자는 하나의 피연산자에 작용합니다. 단항 연산자의 예로는 - 연산자가 있습니다. 예를 들어 -5의 경우, operator-는 리터럴 피연산자 5를 받아 부호를 뒤집어 새로운 출력값 -5를 생성합니다.
• 이항(Binary) 연산자는 두 개의 피연산자에 작용합니다(왼쪽 피연산자가 연산자의 왼쪽에, 오른쪽 피연산자가 연산자의 오른쪽에 위치하므로 종종 left와 right라고 부릅니다). 이항 연산자의 예로는 + 연산자가 있습니다. 예를 들어 3 + 4의 경우, operator+는 왼쪽 피연산자 3과 오른쪽 피연산자 4를 받아 수학적 덧셈을 적용하여 새로운 출력값 7을 생성합니다. 삽입(<<) 및 추출(>>) 연산자는 이항 연산자로, 왼쪽에 std::cout이나 std::cin을 받고 오른쪽에 출력할 값이나 입력받을 변수를 취합니다.
• 삼항(Ternary) 연산자는 세 개의 피연산자에 작용합니다. C++에는 오직 하나의 삼항 연산자(조건 연산자)만 있으며, 나중에 다룰 것입니다.
• 무항(Nullary) 연산자는 0개의 피연산자에 작용합니다. C++에는 오직 하나의 무항 연산자(throw 연산자)만 있으며, 이 역시 나중에 다룰 것입니다.
부수 효과(Side effects)
• 일부 연산자는 추가적인 동작을 합니다. 반환값을 생성하는 것 이외에 관찰 가능한 효과를 가지는 연산자(또는 함수)는 부수 효과(side effect, 또는 사이드 이펙트)를 가진다고 말합니다.
• 예를 들어 x = 5는 변수 x에 값 5를 대입하는 부수 효과를 가집니다. x의 변경된 값은 연산자 실행이 완료된 후에도 관찰 가능합니다(예: x의 값을 출력함으로써).
• std::cout << 5는 콘솔에 5를 출력하는 부수 효과를 가집니다. 우리는 std::cout << 5의 실행이 완료된 후에도 5가 콘솔에 출력되었다는 사실을 관찰할 수 있습니다.
표현식 (Expressions)
• 표현식(expression)은 계산되어서 하나의 값(Value)을 만들어내는 코드 조각입니다.
• 표현식을 실행하는 과정을 평가(evaluation)라고 하며, 생성된 결과 값을 표현식의 결과(result)(때로는 반환값(return value))라고 합니다.
• 표현식이 평가될 때, 표현식 내부의 각 항은 하나의 값이 남을 때까지 평가됩니다. 다음은 다양한 종류의 표현식과 그 평가 방식을 설명한 예시입니다
2 // 2는 값 2로 평가되는 리터럴입니다.
"Hello world!" // "Hello world!"는 텍스트 "Hello world!"로 평가되는 리터럴입니다.
x // x는 변수 x가 가지고 있는 값으로 평가되는 변수입니다.
2 + 3 // operator+가 피연산자 2와 3을 사용하여 값 5로 평가합니다.
five() // 함수 five()의 반환값으로 평가됩니다.
• 보시다시피 리터럴은 자기 자신의 값으로 평가됩니다.
• 변수는 변수가 가진 값으로 평가됩니다.
• 연산자(예: operator+)는 피연산자를 사용하여 다른 값으로 평가합니다.
• 함수 호출은 아직 다루지 않았지만, 표현식의 문맥에서 함수 호출은 함수가 반환하는 값으로 평가됩니다.
• 표현식은 세미콜론으로 끝나지 않으며, 그 자체만으로는 컴파일될 수 없습니다. 예를 들어, x = 5라는 표현식만 컴파일하려고 하면 컴파일러는 오류를 발생시킬 것입니다(아마도 세미콜론이 빠졌다는 내용일 것입니다). 표현식은 항상 명령문(statement)의 일부로서 평가됩니다. 예를 들어 다음 문을 봅시다.
int x{ 2 + 3 }; // 2 + 3은 세미콜론이 없는 표현식입니다. 세미콜론은 표현식을 포함하는 '문'의 끝에 있습니다.
• 이 문을 구문(syntax)적으로 분해하면 다음과 같습니다: type identifier { expression };
• type은 유효한 타입(int), identifier는 유효한 이름(x), expression은 유효한 표현식(2 + 3, 두 리터럴과 연산자 사용)이 될 수 있습니다.
• 표현식 문(expression statement)은 표현식 끝에 세미콜론을 붙여 문(statement)으로 만든 표현식입니다.
함수에 대한 소개 (Introduction to functions)
• 함수는 특정한 작업을 수행하도록 설계된, 재사용 가능한 일련의 명령문(statements)들입니다.
• 직접 만든 함수를 사용자 정의 함수(User-defined functions)라고 부릅니다.
• 프로그램이 한 함수 내에서 문장들을 순차적으로 실행하다가 함수 호출(Function call)을 만납니다.
용어 설명 (Nomenclature)
• 호출자 (Caller): 함수 호출을 시작하는 함수입니다.
• 피호출자 (Callee): 호출되어 실행되는 함수입니다.
• 호출/호출하다 (Invocation/Invoke): 함수 호출을 가리키는 또 다른 표현입니다.
용어: Foo
• "foo"는 개념을 설명할 때 이름이 중요하지 않은 함수나 변수에 사용하는 의미 없는 단어입니다.
• 이런 단어들을 메타 문법적 변수(Metasyntactic variables) 혹은 흔히 자리 표시자 이름(Placeholder names)이라고 부릅니다.
• C++에서는 "bar", "baz", 그리고 "goo", "moo" 같이 'oo'로 끝나는 단어들이 자주 쓰입니다.
상태 코드 (Status codes)
• main()의 반환 값은 상태 코드(Status code)(드물게 종료 코드(exit code) 또는 반환 코드(return code))라고 불립니다.
• 상태 코드는 프로그램이 성공적으로 실행되었는지 여부를 알리는 데 사용됩니다.
• 관례적으로 상태 코드 0은 프로그램이 정상적으로 실행되었음을 의미합니다.
• 프로그램이 정상적으로 실행되었다면 main 함수는 값 0을 반환해야 합니다.
• 0이 아닌 상태 코드는 종종 일종의 실패를 나타내는 데 사용됩니다
여담으로...
• 상태 코드는 운영 체제로 전달됩니다.
• OS는 일반적으로 이 상태 코드를 해당 프로그램을 실행시킨 프로그램이 사용할 수 있게 합니다.
• 이는 프로그램을 실행시킨 쪽에서 실행된 프로그램이 성공했는지 실패했는지 판단하는 기초적인 메커니즘을 제공합니다.
함수는 하나의 값만 반환할 수 있습니다
• 값을 반환하는 함수는 호출될 때마다 호출자에게 단 하나의 값만 반환할 수 있습니다.
• 함수가 하나의 값만 반환할 수 있다는 제약을 우회하는 다양한 방법이 있으며, 이는 향후 레슨에서 다룰 것입니다.
반환 값의 의미는 함수 작성자가 결정합니다
• 함수가 반환하는 값의 의미는 전적으로 함수의 작성자가 결정합니다.
• 어떤 함수는 성공이나 실패를 나타내기 위해 반환 값을 상태 코드로 사용합니다.
• 어떤 함수는 계산되거나 선택된 값을 반환합니다.
• 어떤 함수는 아무것도 반환하지 않습니다.
함수 매개변수(Function parameter)
• 함수 매개변수(Function parameter)는 함수 헤더에서 사용되는 변수입니다.
• 함수 매개변수는 함수 내부에서 정의된 변수와 거의 동일하게 작동하지만 한 가지 차이점이 있습니다. 바로 호출자가 제공한 값으로 초기화된다는 점입니다.
• 함수 매개변수는 함수 헤더의 함수 이름 뒤 괄호 안에 정의되며, 여러 매개변수는 쉼표(,)로 구분됩니다.
함수 인자(Argument)
• 인자(Argument)는 함수 호출이 이루어질 때 호출자(caller)에서 함수(function)로 전달되는 값입니다.
• 여러 개의 인자 또한 쉼표로 구분됩니다.
매개변수와 인자가 함께 작동하는 방식
• 함수가 호출되면 함수의 모든 매개변수가 변수로 생성되고, 각 인자의 값이 대응하는 매개변수로 복사됩니다 (복사 초기화 사용).
• 이 과정을 값에 의한 전달(pass by value)이라고 합니다. 값에 의한 전달 방식을 사용하는 함수 매개변수를 값 매개변수(value parameter)라고 부릅니다.
• 일반적으로 인자의 개수는 함수 매개변수의 개수와 일치해야 하며, 그렇지 않으면 컴파일러가 에러를 발생시킵니다.
• 함수에 전달되는 인자는 유효한 표현식이면 무엇이든 가능합니다. (인자는 본질적으로 매개변수의 초기값이며, 초기값은 유효한 표현식이면 될 수 있기 때문입니다).
지역 변수 (Local variables)
• 함수 본문(body) 내부에 정의된 변수를 지역 변수(local variables)라고 합니다
• 함수 매개변수(Function parameters) 또한 일반적으로 지역 변수로 간주합니다.
지역 변수의 수명 (Local variable lifetime)
• 함수 매개변수는 함수에 진입할 때 생성 및 초기화됩니다.
• 함수 본문 내의 변수는 정의된 지점에서 생성 및 초기화됩니다.
• 지역 변수는 정의된 중괄호 세트가 끝날 때(함수 매개변수의 경우 함수가 끝날 때), 생성된 순서의 역순으로 파괴됩니다.
int add(int x, int y)
{
int z{ x + y };
return z;
} // z, y, x는 여기서 파괴됩니다.
지역 범위 (Local scope, 블록 범위)
• 지역 범위(Local Scope)란 "블록({}) 내부에서 선언된 식별자(변수 등)가 유효한 코드 영역"을 말합니다.
임시 객체 (temporary object)
• 임시 객체(temporary object)(종종 익명 객체(anonymous object)라고도 함)는 잠시 동안만 필요한 값을 저장하기 위해 사용되는 이름 없는 객체입니다.
• 임시 객체는 필요할 때 컴파일러에 의해 생성됩니다.
• 임시 객체는 범위(scope)가 전혀 없습니다. (범위는 식별자의 속성인데, 임시 객체는 식별자가 없으므로 말이 됩니다).
• 임시 객체는 생성된 전체 표현식(full expression)이 끝날 때 파괴됩니다. 즉, 임시 객체는 항상 다음 문장이 실행되기 전에 파괴됩니다.
전방 선언 (Forward declarations)
• 전방 선언은 식별자를 실제로 정의하기 전에 식별자의 존재를 컴파일러에게 알릴 수 있게 해줍니다.
• 함수의 경우, 이를 통해 함수 본문(body)을 정의하기 전에 함수의 존재를 컴파일러에 알릴 수 있습니다.
• 이렇게 하면 컴파일러가 함수 호출을 만났을 때, 비록 함수가 어디에 어떻게 정의되어 있는지 아직 모르더라도 우리가 함수 호출을 하고 있다는 것을 이해하고, 함수를 올바르게 호출하고 있는지 확인할 수 있습니다.
• 함수에 대한 전방 선언을 작성하기 위해 우리는 함수 선언문(function declaration statement)(또는 함수 프로토타입(function prototype)이라고도 함)을 사용합니다. 함수 선언은 함수의 반환 형식, 이름, 매개변수 형식으로 구성되며 세미콜론으로 끝납니다. 매개변수의 이름은 선택적으로 포함할 수 있습니다. 함수 본문은 선언에 포함되지 않습니다.
int add(int x, int y); // 함수 선언은 반환 형식, 이름, 매개변수, 세미콜론을 포함합니다. 함수 본문은 없습니다!
선언 vs 정의 (Declarations vs. definitions)
• 선언(Declaration)은 컴파일러에게 식별자의 존재와 그에 연관된 타입 정보를 알려주는 것입니다.
• 정의(Definition)는 식별자를 실제로 구현(함수나 타입의 경우)하거나 인스턴스화(변수의 경우)하는 선언입니다.
• C++에서 모든 정의는 선언입니다. 따라서 int x;는 정의이자 동시에 선언입니다.
• 반대로, 모든 선언이 정의인 것은 아닙니다.
• 정의가 아닌 선언을 순수 선언(pure declarations)이라고 부릅니다. 순수 선언의 종류에는 함수, 변수, 타입에 대한 전방 선언이 포함됩니다.
용어 설명 (Nomenclature)
• 일상적인 언어에서 "선언"이라는 용어는 보통 "순수 선언"을 의미하고, "정의"라는 용어는 "선언 역할도 하는 정의"를 의미합니다.
• 따라서 우리는 보통 int x;가 정의이자 선언임에도 불구하고 그냥 '정의'라고 부릅니다.
단일 정의 규칙 (The one definition rule, ODR)
• 단일 정의 규칙(줄여서 ODR)은 C++에서 매우 잘 알려진 규칙입니다. ODR은 세 부분으로 나뉩니다.
• 1. 파일(File) 내에서: 주어진 범위(scope) 내의 각 함수, 변수, 타입, 템플릿은 단 하나의 정의만 가질 수 있습니다. (서로 다른 범위에서 정의된 것은 위반이 아닙니다. 예: 서로 다른 함수 내의 지역 변수들)
• 2. 프로그램(Program) 내에서: 주어진 범위 내의 각 함수나 변수는 단 하나의 정의만 가질 수 있습니다. 프로그램은 여러 파일로 구성될 수 있기 때문에 이 규칙이 존재합니다. (링커에게 보이지 않는 함수나 변수는 이 규칙에서 제외됩니다. 7.6단원 참조)
• 3. 타입, 템플릿, 인라인 함수/변수: 서로 다른 파일에서 중복된 정의를 가질 수 있지만, 각 정의가 완전히 동일해야 합니다.
• ODR 1번을 위반하면 컴파일러가 재정의 에러(redefinition error)를 발생시킵니다.
• ODR 2번을 위반하면 링커가 재정의 에러를 발생시킵니다.
• ODR 3번을 위반하면 정의되지 않은 동작(undefined behavior)이 발생합니다.
다음은 ODR 1번 위반 예시입니다.
int add(int x, int y)
{
return x + y;
}
int add(int x, int y) // ODR 위반: 함수 add(int, int)를 이미 정의했음
{
return x + y;
}
int main()
{
int x{};
int x{ 5 }; // ODR 위반: x를 이미 정의했음
}
스코프 영역 (Scope Regions)
• 스코프 영역(Scope Region)은 선언된 식별자들이 다른 스코프의 이름들과 구별되는 코드 영역을 말합니다.
• 다른 스코프 영역에 있다면 이름이 같아도 충돌하지 않습니다.
• 하지만 같은 스코프 영역 안에서는 모든 이름이 고유해야 합니다.
네임스페이스 (Namespaces)
• 네임스페이스(Namespace)는 이름 충돌을 방지하기 위해 식별자들을 구분해주는 일종의 '그룹' 역할을 합니다.
• 특정 스코프 영역(네임스페이스 등) 안에 선언된 이름은 다른 스코프에 있는 동일한 이름과 구별됩니다.
• 네임스페이스 안에는 선언(Declaration)과 정의(Definition)만 들어갈 수 있습니다. (예: 변수, 함수)
• 일반적인 실행문(Executable statements)은 네임스페이스 직속으로 넣을 수 없으며, 반드시 함수 정의 내부에 있어야 합니다. 쉽게 말해, C++에서 "일을 시키는 코드(명령문)"는 반드시 함수라는 박스({ }) 안에 담겨 있어야 한다는 뜻입니다.
전역 네임스페이스 (The Global Namespace)
• C++에서 클래스, 함수, 혹은 특정 네임스페이스 안에 정의되지 않은 모든 이름은 암묵적으로 정의된 전역 네임스페이스(Global Namespace) 또는 전역 스코프(Global Scope)에 속하게 됩니다.
std 네임스페이스
• 원래 C++ 표준 라이브러리의 모든 기능은 전역 네임스페이스에 있었습니다.
• 하지만 이는 사용자가 만든 이름과 충돌할 가능성이 너무 높았죠.
• 그래서 C++는 표준 라이브러리의 모든 기능을 std(standard의 약자)라는 네임스페이스로 옮겼습니다.
명시적 네임스페이스 한정자 std:: (Explicit namespace qualifier)
• 식별자 앞에 std::를 붙여 해당 이름이 어느 네임스페이스에 속하는지 명확히 알려주는 방식입니다.
• 여기서 ::은 범위 지정 연산자(Scope Resolution Operator)라고 부릅니다.
중괄호와 들여쓰기
• C++에서 중괄호({ })는 하나의 스코프 영역을 감싸는 역할을 합니다.
• 보통 중괄호 안의 코드는 가독성을 위해 들여쓰기를 합니다. 이는 해당 코드가 특정 스코프 영역 내부에 존재함을 시각적으로 보여줍니다.
• 들여쓰기(Indentation)란 문장이나 코드의 시작 부분을 오른쪽으로 몇 칸 띄워서 쓰는 것을 말합니다.
• 키보드의 Tab 키나 Space 키를 사용하여 빈 공간을 만드는 행위입니다.
전처리기 소개(Introduction to the preprocessor)
• 컴파일에 앞서, 각 코드 파일(.cpp)은 전처리 단계(preprocessing phase) 를 거칩니다.
• 이 단계에서 전처리기(preprocessor) 라는 프로그램이 코드 파일의 텍스트를 여러 방식으로 변환합니다.
• 예전에는 전처리기가 컴파일러와 별개의 프로그램이었지만, 현대 컴파일러에서는 전처리기가 컴파일러에 통합되어 있는 경우도 많습니다.
• 전처리기가 하는 일 대부분은 큰 의미가 없습니다. 예를 들어 주석 제거, 파일 끝에 개행 문자 보장 같은 작업이 있습니다.
• 하지만 전처리기의 매우 중요한 역할이 하나 있는데, 바로 #include 지시문(directive) 을 처리하는 것입니다.
• 전처리기가 한 코드 파일에 대한 처리를 끝내면, 그 결과를 번역 단위(translation unit) 라고 부릅니다. 그리고 컴파일러는 이 번역 단위를 실제로 컴파일합니다.
• 전처리(preprocessing), 컴파일(compiling), 링크(linki.ng) 전체 과정을 통틀어 번역(translation) 이라고 합니다.
전처리기 지시문(Preprocessor directives)
• 전처리기가 실행되면 코드 파일을 위에서 아래로 훑으면서 전처리기 지시문(preprocessor directives) 을 찾습니다.
• 전처리기 지시문(Preprocessor directives)은 # 기호로 시작하고 세미콜론(;)이 아닌 줄바꿈으로 끝나는 명령어입니다.
• 전처리기 지시문들은 전처리기에게 특정한 텍스트 처리(text manipulation) 작업을 하라고 지시합니다.
• 중요한 점은 전처리기는 C++ 문법을 이해하지 못한다는 것입니다. 지시문은 C++과는 별도의 문법을 사용합니다.
#include
• #include를 하면 전처리기는 #include 지시문을 포함되는 파일의 내용으로 통째로 치환(replace) 합니다.
• 각 번역 단위(translation unit)는 보통 하나의 .cpp 파일 + 그 파일이 #include한 모든 헤더 파일들로 구성된다(헤더가 다른 헤더를 #include할 수 있으므로 재귀적으로 적용).
매크로 정의(Macro defines)와 #define
• #define 지시문은 매크로(macro) 를 만드는 데 사용됩니다.
• C++에서 매크로는 “입력 텍스트(input text)를 어떤 치환 텍스트(replacement output text) 로 바꿀지 정하는 규칙”입니다.
• 매크로에는 객체형 매크로(object-like macro),함수형 매크로(function-like macro) 크게 두 종류가 있습니다.
매크로 식별자(identifier)
• 매크로 식별자(identifier)는 일반 식별자와 같은 규칙을 따릅니다.
• 문자, 숫자, 밑줄(_) 사용 가능
• 숫자로 시작 불가
• 밑줄로 시작하는 것은 피하는 것이 좋음(관례/규칙과 충돌 가능성)
• 관례적으로 매크로 이름은 대문자 + 밑줄 형태를 많이 씁니다.
• 매크로 이름은 보통 모두 대문자로 쓰고, 단어는 밑줄로 구분하라.
함수형 매크로(function-like macro)
• 함수처럼 동작하며 비슷한 목적을 갖지만, 일반적으로 안전하지 않다고 여겨집니다.
• 그리고 할 수 있는 일 대부분은 일반 함수로 더 안전하게 처리할 수 있습니다.
객체형 매크로의 두 가지 형태
• #define IDENTIFIER
• #define IDENTIFIER substitution_text
• 위는 치환 텍스트가 없고, 아래는 치환 텍스트가 있습니다.
• 둘 다 전처리기 지시문이므로 세미콜론이 붙지 않습니다.
치환 텍스트가 있는 객체형 매크로(Object-like macros with substitution text)
• 전처리기가 다음을 만나면:
#define MY_NAME "Alex"
• MY_NAME ↔ "Alex"라는 연결이 만들어지고, 이후 코드에서(다른 전처리기 지시문 안에서 쓰는 경우를 제외하고) MY_NAME이 나올 때마다 "Alex"로 치환됩니다.
예:
#include <iostream>
#define MY_NAME "Alex"
int main()
{
std::cout << "My name is: " << MY_NA
진짜 별의 별 개념 다 있음 c++에