본문 바로가기

개발공부(C, C++)

TBCPP 정리 ( 마크다운 문서로 모두 변환 예정)

SECTION 1. 시작해봅시다

폴더구조 & 실행

  1. 하나의 솔루션 안에 여러 개의 프로젝트를 생성할 수 있고, 각 프로젝트의 Source Files 폴더 오른쪽 클릭 후 Add > New Item 을 해서 새로운 소스 파일을 만들어 작성한다.
  2. 프로젝트 생성시 precompiled 를 체크하면 멀티 플랫폼을 지원 못하게 되므로 주의하자.
  3. 여러 개의 프로젝트가 있을 때, 실행될 프로젝트를 고르려면 오른쪽 클릭을 해서 set as startup project 를 클릭해준다.

VS 단축키, 사용팁

  1. Ctrl + 마우스 휠 을 통해 작업창 글씨 크기를 크게나 작게 할 수 있다.
  2. Ctrl + F5 로 Start without debugging 한다. 빌드할 거냐고 물어보니까 따로 빌드부터 할 필요X
  3. VS 에서 주석 처리는 위의 툴바에서 클릭하거나, Ctrl+K, Ctrl+C 단축키를 이용한다. 되돌릴 때는 Ctrl+K, Ctrl+U 한다.
  4. 함수 이름을 바꾸고 싶을 때, 오른쪽 클릭에서 Rename 한 후 apply 하면 그 함수가 이용된 모든 부분에서 이름이 바뀐다.
  5. 에러가 떴을 때, 에러 라인을 클릭하면 해당 에러와 관련된 코드로 바로가기가 된다.

주의사항

  1. x64 버전으로 바꾸자.
  2. Release 모드로 바꿔서 빌드하고 릴리즈하자.
  3. 프로젝트 생성시 precompiled 를 체크하면 멀티 플랫폼 지원이 안 되므로 주의하자.

문제발생과 해결법

  1. 프로젝트를 실행했는데 깜빡하고 바로 사라져버리는 경우가 있다. 그럴 경우, 프로젝트 파일 오른쪽 클릭하고, Properties 탭에 들어가서, Linker > System 탭에 들어가서, Subsystem 항목을 :CONSOLE 로 바꿔줘야 한다. (왼쪽 위 Configuration 의 Debug 와 Release 각각 설정 가능)

파이썬과 다른 점

파이썬과 달리 함수 내에 또다른 함수를 선언할 수 없다. 최신 C++ 에서는 Nested class 로는 가능하다고는 하지만, 일단 일반 함수 내에서는 불가능하다는 것.

SECTION 1. C++의 기초적인 사용법

용어정리

  • 보통 '명령문' 이라고 번역되는 것이 ‘Statement’ 로, 수학에서는 참이나 거짓을 가릴 수 있는 것을 의미하지만, 컴퓨터에서는 보통 어떤 기능을 수행하는 코드를 의미한다. 대표적으로 return statement 가 있다. statement 의 끝을 알리기 위해 쓰는 게 ; (세미콜론) 이다. 이 statement 를 구성하는 요소들(수식 등등)을 expression 이라고 부른다.

  • 할당된 메모리 안에서 변할 수 있는 변수와 달리, 3, 5 같은 한 번 사용되고 마는 일반 상수(문자열도 포함)는 literal 이라고 한다. 보통 변수같이 왼쪽에 쓰이는 걸 L-value, 오른쪽에 임시로 쓰이는 것들을 R-value 라고도 한다. 물론 R-value 에도 x 가 들어갈 수 있으나, 이 때는 x 의 값을 불러와 쓰는 것일 뿐이다. 예를 들어 1 + 2 라면 1과 2 는 literal, + 는 연산자(operator) 이고, 1과 2는 피연산자(operand) 가 된다. –1- 도 음수로 바꾸는 연산자라고 보면 된다. 보통은 연산자 하나당 2개의 피연산자가 있는 이항 연산자지만, int y = (x > 0) ? 1 : 2 같은 삼항 연산자(ternary operator) 도 알아두자. (단항 연산자는 -x 같이 음수로 바꿔주는 연산자 같은 피연산자가 1개)
    &x 처럼 x 가 차지하는 메모리 주소를 나타내는 것의 이름은 ampersand 이다.

  • int x = 123;initialization 이고, int x; 이후 x = 5;assignment 이다. 편의를 위해 모양은 같지만, initialization 은 주소지를 할당 받는 순간 바로 값을 복사해서 넣어버려서 다르다고 한다.

  • 변수든 함수든 객체든 구분하기 위한 이름을 짓는데 이 이름을 Identifier 라고 한다.

  • 함수를 정의할 때 어떤 변수가 쓰일 것인지 임의로 변수들을 선언해 놓는 걸 매개변수(parameter) 라고 하고, 그 함수를 사용할 때 실제로 쓰기 위해 그 매개변수 위치에 집어넣는 걸 인자(argument) 라고 한다.

컴퓨터 기본 상식

1 byte == 8 bit

입출력 스트림

  • ‘\t’ 는 단순히 4칸 정도를 띄워주는 게 아니라, 줄끼리 라인(컬럼) 을 맞춰준다.

  • ‘\n’std::endl 과 유사하지만 완전히 같은 역할을 하지는 않는다. endl 은 버퍼에 있는 걸 모두 출력하고 줄을 바꾼다. 줄 바꿈을 하진 않지만 버퍼에 있는 걸 모두 출력하는 건 std::flush 라는 게 있다.

  • ‘\a’ 은 오디오 소리를 출력해준다. 띠링~

  • #include <cstdio> 를 해주면, C 언어에서 주로 쓰는 printfscanf 를 쓸 수 있다.

  • C 의 printf, scanf 와 달리 cout 과 cin 이 더 좋은 점이 있다면, operating overloading 등과 결합되어 단순 입출력 뿐 아니라 이후 파일 입출력과 네트워크 통신에도 그대로 사용될 수 있다는 것이다.

선언과 정의의 분리

사용하는 함수의 개수가 많아지면, 보기도 힘들 뿐 아니라, 함수가 선언되는 순서에 따른 에러가 발생할 수 있다. 이 때 정의 순서와 관계 없이 함수의 프로토타입, 즉 첫 줄인 출력 자료형과 매개함수(parameter) 부분만을 따로 떼어서 소스 파일에 선언해주면, 그 함수의 정의가 main 함수보다 뒤에 있더라도 에러없이 사용할 수 있다. 함수 오른쪽 클릭에서 go to definition, go to declaration 을 클릭하면 각각 정의와 선언부분으로 갈 수 있으니, 함수 찾을 걱정도 할 필요 없다. peek definition 으로 바로 보는 것도 가능하다. 하지만 결국 같은 파일 내에서 이 짓을 한다면 함수의 길이가 길어졌을 때 한 파일의 길이를 감당할 수 없기 때문에 ‘헤더파일’을 만들게 된다. 이 관계들을 잘 알아놔야 이렇게 분리했을 때 주로 발생하는 Linking error 에 잘 대처할 수 있다.

헤더파일

Source Files 폴더에 새로운 파일을 분리해 만들고 add 라는 함수를 그 곳에 정의한 뒤에, main 파일 위에 add 의 프로토타입을 선언해주면 사용하는 게 가능하다. 하지만 만약 분리한 파일에 다양한 함수가 있고, 또 그 분리한 파일의 함수를 다양한 곳에서 사용하고 싶다면 일일이 프로토타입을 선언해주는 것도 귀찮아 진다. 따라서 .h 확장자의 헤더파일을 만들게 된다. 헤더 파일 안에 프로토타입 선언을 해준 뒤, 해당 헤더파일에 있는 함수를 사용하고 싶을 때 사용하고 싶은 파일 내에서 #include “Myheaders/add.h” 같이 #include 로 불러올 수 있다. (왼쪽 예시는 Myheaders 라는 폴더를 따로 만들어 그곳에 add.h 라는 헤더파일을 만든 경우다.) 개발이 익숙해지면

  1. 헤더파일부터 만들고
  2. 헤더 파일 내에 프로토타입으로 선언한 함수를 따로 .cpp 파일에 분리해 정의하고
  3. 해당 헤더파일에 모아져 선언되어 있는 함수들을 사용할 파일에서 #include 해서 쓰는 방식으로 작업이 진행된다.

(헤더파일의 현실과 이상, 그에 따른 헤더가드의 필요성): 물론 헤더 파일에는 프로토 타입 선언만 하고, 함수의 정의는 따로 .cpp 파일을 만드는 것이 깔끔한 방식이지만, 실제로 개발하면서 일일이 파일을 다 나누면서 테스트하기는 어렵다. 그래서 헤더파일에 함수 정의까지 다 하는 경우가 종종 발생한다. 뿐만 아니라, 헤더파일 내에서 다른 헤더파일을 #include 하는 경우도 종종 발생하며, 또 여러 헤더 파일을 #include 하는 파일이 있다면, 같은 함수를 여러 번 #include 하는 상황이 발생하면서(여러 번 정의하는 셈이 된다.) 에러가 발생한다. 그래서 이미 해당 함수가 정의가 되었다면 다시 하지 말라는 전처리가 필요하다. 원래 전처리 문법인 #ifdef MY_ADD #define MY_ADD #endif 같은 게 있지만, 일일이 하기 어렵기 때문에 #pragma once 라는 것으로 대체한다. 물론 VS 는 헤더 파일을 만들 때부터 이 코드를 써 준다.

namespace

굳이 같은 이름의 함수를 다른 기능을 하고 싶게 하고 싶다면, namespace Myspace1 { } 로 그 중 하나의 함수를 감싸줄 수 있다. 이렇게 되면 Myspace1:: 처럼 해당 네임스페이스 이름 뒤에 :: 를 붙여 그 함수에 접근할 수 있다. 네임스페이스 안에 또다른 네임스페이스를 만들어서 이중으로 접근하는 것도 가능하다.

using namespace std; 같은 식으로 함수를 알아서 해당 namespace 에서 찾아 쓰도록 할 수도 있다. using namespace 했다고 해서 기존 네임스페이스 사용문법인 std::cout 을 못 쓰게 되는 건 아니다. 그냥 cout 이라고 해도 쓸 수 있게 편해졌을 뿐이지.

전처리기

전처리기는 컴파일 전에 결정되는 것들이며 매크로라는 걸 쓰게 된다. #define, #ifdef 같은 식으로 쓰이는데, 해당 코드 파일에 존재하는 모든 코드를 그대로 대체 해준다고 보면 된다. 예를 들어 #define MAX_PRICE 100 이라고 써놓으면 해당 코드 파일에 있는 모든 MAX_PRICE 라는 단어는 무조건 100으로 변환된다.. 즉 어떤 고정된 값을 할당한다. 즘은 매크로보다 그냥 함수 정의를 통해 쓰는 편이며, 멀티 플랫폼을 위한 코드를 짜야되는 경우에만 주로 쓰인다.

자료형 개괄

상식: 1byte == 1bit
char == 1byte
어떤 변수가 가진 메모리 사이즈가 궁금하다면 sizeof() 로 감싸주고 출력해보면 된다. 특이하게sizeof 는 함수가 아니라 연산자다.

자료형 변환은 앞에 (float) 같이 괄호 안에 원하는 자료형을 써주거나(C 스타일), float(x) 같이 괄호 안에 넣어서 변환할 수 있다. (C++ 스타일) 이런 자료형 변환을 casting 이라고 한다.

초기화의 3가지 방법:

  1. copy initialization. 일반적으로 많이 쓰는 int x = 123;
  2. direct initialization. int x(123);
  3. uniform initialization. int x{123}; 3가지 방법 중에 가장 엄격하다. 자료형 잘못 넣으면 warning 대신 에러가 떠버린다.
    2와 3의 방식은 객체지향 문법에서 나만의 자료형을 새로 만들어 쓸 때 사용하게 된다.

어떤 자료형을 초기화 할 때, 어느 정도의 차이는(int 에 float이나 double 을 넣는다던가) 데이터 loss 를 경고만 하고 적당히 변환해 컴파일 해주는 걸 casting 이라고 한다. 근데 그냥 자료형 변환하는 걸 casting 이라고도 한다.
char 과 그 외 캐릭터 타입 자료형은 사실 별로 쓸 일이 없고, 주로 쓰는 문자열 자료형은 string 이라는 standard library 를 많이 사용한다.

int == 4byte
double 은 float(4byte)의 2배의 메모리를 사용하는데(8byte), 따라서 float 형을 초기화해줄 때는, 반드시 할당 값의 마지막에 f 를 붙임으로써 float 형임을 명시해주어야 한다. 안 그러면 double 값을 float 에 쑤셔 넣었다고 경고 메시지가 뜬다. ex) float fValue = 3.1415f
모던 C++ 에는 auto 라는 자동 자료형 할당해주는 자료형이 생겼다.

int 자료형

signed, 즉 음수까지 표현 가능한 자료형들은 맨 앞 bit 를 부호를 구분하는 데 쓴다. 그래서 signed int 의 최대 표현 가능한 수가 2^31 - 1 다. 뒤에 – 1 은 0 을 제외해 준 것이다.
자료형이 표현할 수 있는 범위를 벗어나면, overflow 가 발생하는데, 문제는 얘가 에러를 일으키지 않고 그냥 반대편으로 넘겨버린다는 거다. 예를 들어 signed int 는 2^31 -1 까지 표현 가능한데 2^31 을 넣으면 signed int 가 표현 가능한 가장 작은 수가 되어 버린다. 마찬가지로 가장 작은 수에서 1을 더 빼면 가장 큰 수가 되어버린다… 아주아주 주의해야함.
OS 에 따라 자료형에 할당하는 메모리 크기가 달라 문제가 생길 수 있기 때문에 모던 C++ 에는 고정너비 정수라는 자료형도 있다. 최소 몇 바이트를 쓰라고 지정할 수 있다는 것.
C++ 에서는 숫자의 3자리마다 ‘ 를 집어넣어 구분할 수 있다.

부동소수점수 (floating point numbers)

float, double, long double 이 있다. 각각 최소 크기는 4, 8, 8 바이트다. 최근 언어는 double 을 기본으로 사용하는 경우가 많다. (python 을 포함)
부호부(sign), 지수부(exponent), 가수부(mantissa) 로 나누어 표현한다.
부호부: 0이면 양수, 1이면 음수
지수부: 2진법. 00000111 이면 7
가수부: 2진법의 역수로 표현. 예를 들어 1100000….0 이면 2의 -1 승 + 2의 -2 승 인 0.75 다.. 이해하기 쉽지 않음.
파이썬과 마찬가지로, 1e9 은 1 * 10의 9 승, 10e-1 은 10*10의 -1 승임.
소수점 자리수를 조절하고 싶다면, #include 한 후, cout << std::setprecision(16) <<endl; 같은 식으로 출력 설정을 바꿔줄 수 있다. 이진수 기반이기 때문에, 생각했던 값이 아닐 수 있음에 주의..
0으로 나눈다거나 0끼리 나누거나 해서 inf, -inf, nan 값 등이 나올 수 있는데, #include 해서 isnan()나 isinf() 같은 함수로 판별하고 처리할 수 있다.

Boolean 자료형

보통 cout 하면 true 는 1, false 는 0 으로 출력되는데, true 와 false 로 출력되게 하고 싶다면 cout << std::boolalpha; 라고 설정해주면 된다. 반대로는 std::noboolalpha.
!true == false
논리연산자:
1. && (and)
2. || (or)

문자형 char type

하나의 문자는 ‘A’ 같이 single quotation mark 를 쓰고, 문자열의 경우엔 “Hello world” 같이 double quotation mark 를 쓴다. 파이썬이랑 달라..

리터럴 상수 literal constants

2진수로 표현하고 싶을 때 앞에 0b 를 붙인다.
8진수(Octal)로 표현하고 싶을 때 앞에 0을 붙인다. 즉 int x = 012; 라고 하면 x 는 십진수(Decimal)로 10이다… 함부로 0붙이면 안 돼…
16진수(Hexa)는 앞에 0x 를 붙인다. 즉 0xF 는 15다.
C++14 부터 binary literal 도 가능해졌다. 앞에 0b를 붙인다. 즉 0b1010 은 10이다. 이진수가 길어져 사람이 읽기 힘들 수 있기 때문에 중간중간에 ‘ 를 넣어서 구분해도 무시하고 컴파일 된다.

심볼릭 상수

앞에 const 를 붙여서 변수 값을 바꿀 수 없게 한다. 매개변수(parameter)에 많이 쓴다. 바꾸더라도 복사해서 쓰는 게 맞으니까.
상수에는 크게 컴파일 상수와 런타임 상수가 있는데, 컴파일 상수는 컴파일 될 때 이미 그 값이 정해져 고정된 값이고, 런타임 상수는 예를 들어 cin 으로 받은 수를 const 로 다시 저장해 쓰기 때문에 값을 받기 전에는 어떤 값으로 고정될 지 모르는 상수를 의미한다. c++11 에서는 아예 컴파일상수를 위한 자료형인 constexpr이 존재하며, 이걸 런타임 상수에 쓸 수 없다.
컴파일 상수 같은 경우, 아예 헤더 파일 하나에 모아서 불러다 쓰는 경우가 많다. 예를 들어 MY_CONSTANTS.h 라는 헤더파일을 만들어서,
namespace constants {
constexpr double pi = 3.141592;
constexpr double Avogadro = 6.0221413e23;
}
같이 만들어 놓고, const 값을 써야할 때 #include 해서 contants::pi 같은 식으로 쓰게 된다.

연산자 우선순위와 결합 법칙

결합법칙(Associativity) : 우선 순위가 같은 연산자끼리 만났을 때 어디서 어느 쪽으로 계산할 지 말해준다. 보통 left to right 다. right to left 도 생각보다 많은데, 대표적인 건 역시 = (assignment operator) 이다. 오른쪽 값을 왼쪽 변수에 할당하는 거니까.. -3 같이 오른쪽 3을 음수로 바꿔주는 이것도 right to left operator 라고 할 수 있다.
나누기 연산자(/) 의 경우, C++11 부터 int 형끼리 나눴을 경우 소수점을 절삭(버림) 한다. 그게 음수든 양수든 상관없이. 나머지 연산자(%) 의 경우에도, 왼쪽 수가 음수면 음수 나머지, 양수면 양수 나머지가 나오도록 정해졌다.
증감연산자: 앞에 붙이냐 뒤에 붙이냐에 따라 결과가 다를 수 있음.
콤마연산자(,): 콤마 왼쪽 것을 계산하고(시행하고), 오른쪽 것을 할당한다. 예를 들어 int z = (++x, ++y) 라고 하면, z 에는 ++ 된 y 값이 할당된다. ++x 는 수행되지만 z 에 할당되지는 않는다. 이 이상한 연산자는 주로 for loop 을 돌 때 쓰인다. 단순 구분을 위한 기호로 쓰인 콤마와 연산자로서의 콤마를 잘 구분해야 한다. 할당 연산자(=) 보다도 콤마 연산자(,) 의 우선 순위가 낮기 때문에, 콤마 연산자로서 기능하게 하려면 보통 괄호를 씌워준다. int z = (++a, a + b); 처럼 해줘야 z에 a+b 가 할당된다.
조건부연산자(conditional operator(arithmetric if): 현재까진 삼항연산자만이 해당된다. if 문보다 삼항연산자가 꼭 필요할 때가 있다면, 조건에 따라 값이 달라지는 const 자료형 변수를 쓰고 싶을 때다. 예를 들어 const int price = (onSale == true)? 10: 100; 은 가능하지만 일반적인 if 문으로는 price를 const 로 초기화할 수가 없다. 물론 따로 값을 구하는 함수를 이용한다면 삼항연산자를 꼭 안 써도 되긴 한다.

관계연산자

==, !=, >= 등등.
주의사항! : C++ 에서 관계연산자는 부동소수점수를 비교할 때 문제가 생길 수 있다. 예를 들어 100-99.99 와 10-9.99 는 0.001 로 같다고 나와야 하지만, 정작 관계연산자로 비교해보면 100-99.99가 더 크다고 나온다. 그런데 그 차이를 보면 5.32907e-15 같이 어엄청나게 작은 차이다. 그래서 오차의 한계를 본인이 필요로 하는 정교함에 따라 따로 설정할 필요가 있다.
#include
const double epsilon = 1e-10;
if (std::abs(d1-d2) < epsilon)
cout << “Approximately equal” << endl;
else
cout << “Not equal” << endl;

논리연산자

&& || 등을 의미한다. 주로 if 문과 함께 쓰인다.
주의사항1: short circuit evaluation
if (x == 1 && y++ == 2) {
// do something
}
위 식에서, x 가 1이 아니라면, 그 순간 바로 if 조건이 맞지 않아 넘어가버리기 때문에, 뒷부분인 y++ 가 실행되지 않는다. 즉, 앞 조건에 따라 같이 쓰인 어떤 수식이 작동하지 않을 수 있음을 알아야 하고, 고수가 되면 오히려 이런 특성을 이용할 수도 있다.
주의사항2: or 과 and 의 우선순위
or 과 and 가 혼용되었을 때, and 가 우선순위가 높아서 먼저 계산된다. 혼동을 막기 위해, 그냥 && 주변에 괄호를 씌워두는 게 좋다.
De Morgan’s Law
!(x && y); 는
!x || !y; 와 같다. 즉 분배법칙이 적용되지 않고, and 는 or, or 은 and 가 된다.
XOR
OR과 유사하게 동작하지만, 특이하게
true true 면 false 가 된다. 그런데 C++ 에는 이런 논리 연산자가 없다. 대신에 x != y 라고 하면 같은 결과가 나온다.

전역 변수(Global variable), 정적 변수(static variable), 내부 연결(Internal Linkage), 외부 연결(External Linkage)

전역 변수는 사실 최대한 사용 안 하는 게 좋다. 하지만 전역변수를 부득이하게 사용하게 되는 경우가 있다.

“연결(Linkage)” 의 개념
지역변수 (Local variable), 즉 블록 안에 갇혀 있는 변수의 경우, Linkage 가 없다 라고 하고, 이 파일 내에서는 어디서든 접근할 수 있다 를 내부 연결이 있다고 한다. 또 cpp 파일이 여러 개가 있는데, 한 cpp 파일에서 선언한 걸 어느 파일에서든 접근 가능한 걸 외부 연결이 있다고 한다.

전역 변수  main 함수 밖에 따로 블록({}) 없이 선언한 것. 전역 변수로 int value = 4; 라고 한 뒤 main 함수에서 int value = 7; 이라고 다시 초기화했을 때, 덮어씌워지는 게 아니라 서로 다른 주소로 만들어진다. 먼저 만들어진 전역변수에는 어떻게 접근하냐면 ::value 같이 마치 어떤 namespace 에 접근하듯이 :: 를 앞에 붙이면 된다. 전역 변수를 쓰면 생각지 못한 문제가 많이 발생할 수 있으니 g_value 같이 이름으로 구분을 해주거나, 객체 지향을 제대로 배워서 아예 안 쓰는 것이 좋다.
정적 전역 변수(static)  어떤 함수 내에서 static int a = 1; 처럼 앞에 static 을 붙여 선언해주면, 그 변수는 맨처음 초기화되었던 그 메모리가 static 해서 함수가 호출될 때마다 새로 메모리 주소를 할당받지 않고(초기화하지 않고) 원래 주소를 그대로 받아오도록 컴파일러가 도와주기 때문에, 값을 유지한다. 즉 1을 더하는 함수를 만들었다면, 함수가 호출될 때마다 2 만 출력하는 게 아니라, 2 3 4 5 6… 처럼 누적해서 더해진다. 나중에 디자인패턴을 배우면 싱글턴? 의 개념에 연결된다. 정적 전역 변수 자체는 디버깅할 때 많이 쓰인다. 함수가 몇번 호출되었는지.. 등을 체크할 때. 작동자체는 함수 밖에 사용하는 전역 변수와 거의 비슷하게 작동한다. 그럼 전역변수랑 뭐가 다르냐? 하면 static 은 외부 파일에서 접근할 수 없다. (포인터를 엄청 복잡하게 쓰면 가능하긴 하지만 굳이? 또 굳이 접근하자면 객체지향을 쓰면 좋다.) 함수 블록 안에 없는 전역변수라도 앞에 static 을 붙여주면 다른 파일에서 접근이 불가능하게 만들어주는 역할을 한다.
Internal Linkage 
외부연결 (External Linkage)  가급적 외부 파일을 직접 #include 하는 짓은 피하는 게 좋다. 보통은 forward declaration 을 해준다. 즉 다른 파일에 정의한 void doSometing() { } 같은 함수나 변수가 있다면, 그 함수를 불러다 쓸 곳에서 void doSomething(); 이라고 머리부분만 선언한다. 그럼 컴파일러가 어딘가에 저게 있겠거니 하고 찾아서 연결해준다. 그런데 사실 이 forward declaration 은 앞에 extern 이 생략된 것이다. 즉 원래는 extern void doSomething(); 이라고 해주어야 명확하다. extern 은 어디서 가져올 때 뿐만 아니라 어디로 내보낼 예정인 변수 앞에도 쓴다. 즉 external linkage 관련 변수임을 수출,수입하는 모든 곳에서 쓰는 습관을 들이면 좋다. 이 때 주의할 것은, 초기화 되지 않은 변수나 함수를 가져오려고 하면 당연히 에러가 뜨고, extern 변수를 어디선가 한 번 초기화했는데, 또 다른 파일에서 초기화하려고 하면 복수 초기화라며 에러가 뜬다.
외부연결과 헤더가드  헤더파일에 전역상수 변수를 만들어놨을 때, 헤더파일 내에서 초기화하면, 그 헤더파일 안의 변수를 불러다 쓰는 모든 cpp 파일이 그 때마다 새로운 주소를 할당받아 쓴다. 즉 1000개의 파일에서 각각 헤더파일을 불러다가 쓰면, 1000개의 메모리를 할당받는다… 그래서 좀 귀찮더라도 따로 cpp 파일을 하나 만들어서 거기서 초기화 하고, (extern 을 앞에 붙여서 하면 명확해진다. ex extern const double pi = 3.141592;) 헤더파일에서 다시 그 cpp 파일의 변수들을 초기화없이 선언만해야 된다.. 일종의 forward declaration 이라고 생각하면 될 듯. (이때도 밖으로 수출할 것이니 extern 을 앞에 쓰길 바란다.)

[auto 키워드와 자료형 추론]
auto 키워드는 계산을 위한 요소가 다 주어졌을 때, 그 결과값을 컴파일러가 알아서 추론하라고 써주는 키워드다. 반환 자료형을 굳이 기억하지 않아도 된다는 장점이 있지만, 어쨋거나 어떤 재료를 가지고 계산을 할 건지는 명확해야 하기 때문에 함수의 매개변수 같은 데는 auto 를 쓸 수 없다. 매개변수까지 추론할 수 있게 하는 것은 추후 배우는 template 에서 배운다.
[열거형, enum, enumerated types]
enum Color // 일종의 사용자 지정 자료형.
{
COLOR_BLACK,
COLOR_RED,
COLOR_BLUE,
}; // 이런 식으로 열거해서 열거형. 주로 대문자로 씀.
내부적으로는 각 원소가 0부터 1씩 증가하는 integer 처럼 저장이 된다. 맨 첫 원소에 COLOR_BLACK = -3 처럼 초기화 비슷하게 써주면, -3부터 1씩 증가하게 된다. 즉 시작값을 바꿀 수 있으며 중간부터도 다른 값으로 초기화할 수도 있다. 물론 대부분은 걍 0부터 시작되도록 내버려 둔다.

[영역 제한 열거형 (열거형 클래스)]
ENUM 은 기본적으로 integer 형태로 저장하는데, ENUM 끼리 겹쳤을 때 겹쳤다는 걸 인식못하는 문제가 있다. 즉, 순서(인덱스)만 같으면 서로 다른 enum 의 원소인데도 같은 것으로 인식하거나, enum1 의 RED 라는 원소와 enum2의 RED 라는 원소를 구분하지 못한다. 그래서 enum 클래스라는 걸 써서 구분지을 수 있다. enum class Color 같이 class 라는 단어를 끼워 넣어주면, Color::Red 같이 명확하게 써줘야만 제대로 작동하게 되어 안정성이 높아진다. 마치 네임스페이스랑 비슷하다.
[자료형에게 가명(별명) 붙여주기]
같은 double 자료형이라도, 구분 편의를 위해 이름을 붙여줄 수 있다.
typedef double distance_t; // 이렇게 타입이라는 의미로 _t 를 많이 붙인다.
typedef 대신 using 을 쓸 수도 있다. 여기서 using 은 using namespace 처럼 쓰겠다란 의미보다는 앞으로 이런 이름의 자료형에는 이런 걸 쓰겠다.. 라는 것으로 좀 다르다.
ex) typedef vector<pair<string, int>>pairlist_t; 이 것과 아래 코드는 거의 같게 쓰인다.
using pairlist_t = vector<pair<string, int>>;

[구조체 struct]
C언어에서는 아주 중요한 주제지만, C++ 에서는 클래스로 넘어가기 전 중간단계라고 보면 된다. 이해를 돕는 중간다리 역할.
struct Person
{
double height;
float weight; …
}
이런식으로 해당 structure 의 member 들을 정의한 후에,
Person me{176.0, 74,…} 같이 uniform initialization 을 통해 초기화 해 줌. 옛날방식은 me.height = 176.0 이런식.

[Switch 문 주의점]
0. switch 문의 else 는 default 문이라는 걸로 할 수 있다.

  1. if 문과는 다르게, switch 문은 조건이 만족되는 이후에 있는 모든 코드를 실행시켜버린다. 즉, 어떤 케이스에 대해 구획을 확실히 나누고 싶다면 무조건 각 케이스마다 끝에 break 을 걸어주어야 한다.
  2. case 문 안에 변수를 선언하는 것은 되지만, 마치 case 문 밖, 즉 switch 문 최상단에서 선언되는 것처럼 동작한다. 즉 변수 자체는 어느 케이스에서나 접근이 가능하지만, 초기화 값은 각 케이스에서 달리 할 수 있다. 근데 이렇게 코드 짜지마.. 차라리 switch 문 밖에서 변수 선언하고 그 전체를 또 중괄호로 감싸라.

[goto]
요즘 거의 안 씀.
원하는 위치에 ‘라벨’ 을 붙이고, (그냥 tryAgain: 같이 : 과 함께 쓰기만 하면 된다.)
그 위치에 가고 싶은 때가 생겼을 때 goto tryAgain; 같이 써주면 tryAgain: 이 있는 위치로 이동한다.
[do-while]
do {} while (); 순. do 문을 반드시 한 번은 실행해야 하는 상황에서 쓴다. while 조건문 바로 뒤에 ; 을 붙인다는 것에 주의.
do 의 바디 안에서 continue 를 잘못 적용하면, 반복 조건과 관련된 변수(count 같은) 가 변하지 않아 무한루프에 빠질 수 있다. 이런 경우에는 뒤의 while 문의 조건에 ++ 를 붙여주던가 해서 방지해야 한다.

[for 반복문]
for(int count = 0; count <10; ++count) {}
첫번째 ; 전에 사용할 변수를 정의하고, 두번째에 바디 실행여부를 판단할 조건을 넣고, 세번째에 바디 실행이 끝난 후 할 액션을 넣는다. 각 부분을 빈칸으로 두고 ; 만 넣어서 사용할 수도 있음을 알 필요가 있다. 예를 들어,
int count = 0;
for(; count <10; ++count){} // 이렇게 써서 for 문이 끝난 이후에도 count 값이 사라지지 않고 이용할 수 있도록 남길수도 있고,
for(; true; ++count){} // 이렇게 for 문으로 무한루프를 만들 수도 있다. (true 대신 빈칸도 true 처럼 동작한다.)
, 를 이용해서 for 문에서 사용할 변수 여러 개를 초기화하고 변화시킬 수도 있다.
for (int i = 0, j = 0; i< 10; ++i, --j) { }

[ARRAY 배열]
int my_array[] {1,2,3,4,5} 처럼 [] 안에 숫자를 특정하지 않아도 뒤에 uniform initialization 개수가 확실하면 알아서 만들어준다. 이점을 이용해서 오히려 배열의 원소 개수를 구할 수도 있다. (매개변수로 받았을 때는 사용하기 힘들다. 포인터 문제로, 자세히는 아래 쪽 어딘가에 적어놨음)
ex) int my_array[] = {12, 23, 53, 63, 32};
const int len_my_array = sizeof(my_array) / sizeof(int);
int my_array[5] = {1,2,3,4,5} 라고 써도 되고 뭐 알아서 다양하게 배열 초기화 가능.
struct 를 배열로 만드는 등 다양한 걸 array 로 만들 수 있다.
enum 과 조합해서 배열을 정의하면 이름을 가지고 배열을 만들어 보기 좋게 만들 수도 있다. (갠적으로 이거 쓸 일 있을 듯..)
cin 으로 배열 크기를 정하는 등의 행위는 할 수 없다. array 사이즈를 런타임에 결정할 수는 없기 때문이다. 동적으로 정하고 싶다면 동적할당이란 걸 따로 쓴다. 변수로 array 사이즈를 정해주고자 한다면 const 로 변수를 선언해야 한다.

배열의 식별자는 그 자체가 주소이기 때문에, 굳이 & 가 없어도 cout 하면 주소를 출력한다.
그런데 어떤 함수에서 매개변수를 받을 때, 배열을 받으면, 그 매개변수로 쓰인 배열의 주소를 직접 가지고 쓰지는 않고, 따로 포인터 변수에 해당 배열의 첫번째 주소값을 복사해 쓰기 때문에, 함수 안에서 매개변수의 주소를 출력하면 그 포인터 변수의 주소가 출력된다. (즉 원본 배열의 주소가 출력되지 않는다. 같은 이유로, 원본 배열의 사이즈가 80바이트라도, 함수 안에서 그 배열의 사이즈를 출력하면 포인터의 크기인 4(x32)나 8(x64)바이트가 출력된다.) 그런데 어쨌거나 그 포인터 변수가 원본 배열의 주소를 가리키고 있는 것은 맞기 때문에, 매개변수의 원소에 인덱싱으로 접근하면, 원본 배열의 원소 주소와 같은 게 출력된다. 정리하자면, 걍 함수에서 배열을 매개변수로 받아올 때 포인터가 이용된다. 그래서 함수에서 배열을 받아올 때 굳이 배열의 사이즈를 적어줄 필요가 없다. 관심도 없고 어차피 포인터 쓰니까. 만약 배열 자체를 함수에 복사해 넣고 싶다면, struct 나 class 안에 array 를 초기화해서 그 struct 나 class 를 매개함수로 넣어줄 수 있다. 물론 그 struct 나 class 를 다시 & 로 포인터화해서 매개변수로 넣을 수도 있고…
배열 뿐 아니라 애초에 어떤 함수에서 매개변수를 받을 때는 그 변수의 주소를 포인터로 복사해서 쓴다는 것을 알고 있자. 심지어 매개변수로 포인터를 받아도 그 포인터의 주소를 다시 새로운 포인터로 복사해서 쓴다…

[선택정렬]
순회 시작점, 시작점부터 배열 끝까지 사이의 가장 작은 수의 값과, 그 값의 인덱스. 이 3가지를 반복하는 정렬. 즉 시작점을 한 칸씩 증가하면서 배열의 가장 작은 값부터 순서대로 정렬시킨다.
3 5 2 1 4
1 5 2 3 4
1 2 5 3 4
1 2 3 5 4
1 2 3 4 5

[정적 다차원 배열]
int array[3][5]; // row 는 빈칸으로 둬도 되는데, column 개수는 꼭 선언해주어야 함.
int array[num_rows][num_cols] =
{
{ 1,2,3,4,5 },
{ 6,7,8,9,10 },
{ 11,12,13,14,15 }
};
int array[][num_cols] = {0}; // 아예 전부 0 으로 초기화해주세요

[c언어 스타일의 배열 문자열]
배열 맨 마지막에 \0 (null값) 이 존재한다. 실전에서는 std::string 을 쓰지만, 이건 공부용.
char myString[255];
cout << myString << endl;
cout 으로 배열 문자열 출력시 null 값, 즉 \0 까지 출력하기 때문에, 남은 배열이 있어도 중간 값을 \0 으로 바꿔주면 그곳에서 출력이 멈춘다.

file #include 하면 아래 것들 등등 사용가능.

  1. strcpy_s(복사받을 변수, 할당 메모리 크기, 복사할 값 가진 변수) : string copy_ secured 의 줄임말.
  2. strcat(추가받을 변수, 추가할 변수) : 뒤에걸 앞에 거 뒤에 append.
  3. strcomp(비교대상1, 비교대상2) : 서로 같으면 0, 서로 다르면 -1 을 리턴.

[포인터]
기본적인 사용법.
우리가 변수를 선언한다는 것은 운영체제로부터 메모리 공간과 그 주소를 빌려온다는 것을 뜻한다. 그리고 이 메모리주소를 담는 변수를 포인터라고 한다. 즉 포인터를 너무 어렵게 생각하지 말고 그냥 주소를 담고 있는 변수라고 생각하면 좋다.
de- reference operator (*):
C++ 에서 포인터와 reference 는 다른거다. 사실 포인터가 reference 의 일종이라고 볼 수 있다. de-reference 는, 포인터가 ‘저쪽 주소에 가면 이 데이터가 있어요’ 라고 간접적으로 가리키기만 하는 것에 대해서, ‘그럼 거기에 진짜 뭐가 있는지 내가 들여다볼게’ 라며 직접적으로 접근하겠다는 의미이다. 간단하게 말하면 & 으로 알아낸 주소에 어떤 값이 있는지 출력해준다.
ex) int x = 5;
&x 는 메모리주소
*&x 는 5 를 출력한다. 즉 서로 상쇄되는 듯 작동함.

ex) 포인터 자료형을 직접 define 해서 사용할 수도 있다.
int x = 5
typedef int* pint;
pint ptr_x = &x, ptr_y = &x;
ex) 대부분은 * 을 직접 하나하나 붙여서 포인터 변수를 선언한다. 여기서 * 이 앞에 붙어있다고 해서 포인터변수의 identifier 가 * 이 붙은 것은 아니다. 포인터 변수를 사용할 때는 * 뺀 나머지만 써도 된다.
int *ptr_x = &x, *ptr_y = &x;
포인터 변수를 선언할 때 * 을 붙이고, 출력할 때 다시 * 를 붙여주면 그 주소 안의 값을 가져올 수 있다.
포인터에는 특정 자료형이 강제되지 않기 때문에, 그냥 본인이 보기 편한 자료형으로 선언하면 된다. 주로 int 를 쓰겠지.
포인터가 초기화되지 않았는데 de-reference 를 하면, (즉 제대로 된 주소값 대신 쓰레기 값이 있는데 불러오려고 하면) 에러가 난리가 난다. 그래서 이 문제를 해결하기 위해서 널포인터(Null Pointer) 을 쓴다. 0 이나 nullptr 로 초기화를 해놓고 시작하는 것이다.
ex) 모던c++ 에선 아예 초기화를 nullptr 로 하고, 사용시에 체크하는 방식을 쓴다.
double *ptr = nullptr;
if (ptr != nullptr)
{
// do something
}
else
{
// do nothing
}

[포인터와 정적 배열]
결론부터 말하면, 포인터 == 정적 배열 이다. array[0] 같은 원소값이 아니라 array 자체를 cout 하면 어떤 주소값을 출력하는데, 이 주소값은 &array[0] 과 값이 같다. 즉 array 자체가 어떤 sequence 의 시작 주소를 가리키는 포인터인 것이다. 다만 정적 배열은 사용자가 쓰기 편하게 편리한 기능을 몇 개 덧붙인 것에 불과하다. sizeof() 출력하면 포인터 크기인 4나 8이 아니 연결된 array 사이즈 모두를 출력해 준다는가 하는..
따라서 *array 와 같이 de-referencing이 가능하며 array[0] 값이 출력된다.
질문 : struct 나 class 로 함수에 매개변수를 넣으면 포인터로 넣었을 때보다 메모리 사용량이나 연산량이 늘어나거나 하나요?  초보들이 많이들 궁금해하시는 부분이지요. (function) call by value라면 아무래도 늘어납니다. 작은 구조체일 경우에는 크게 영향을 주지 않습니다만 데이터가 많은 경우에는 보통 reference로 넘기는 것이 일반적입니다. c++에서는 주로 std::vector같은 container를 많이 사용하지요. 이 문제에 대해서는 뒤에 더 자세하게 여러 번 만나시게 될겁니다.

[포인터 연산과 배열 인덱싱]
포인터에 +1, -1 등 했을 때 1 당 증감하는 주소의 크기는 포인터의 자료형의 크기다. ptr+1 을 하면, int 자료형 포인터라면 4 바이트, char 자료형의 포인터라면 1 바이트만큼 증가하는 것이다. 예를 들어 int *ptr = &value; 라고 했었다면, cout << uintptr_t(ptr + 1) << endl; 의 출력값은 uintptr_t(ptr) 출력값에 4를 더한 주소를 출력한다.
using namespace std;
int main()
{
int array[] = { 9,7,5,3,1 };
int *ptr = array;
for (int i = 0; i < 5; i++)
{
cout << array[i] << " " << (uintptr_t)&array[i] << endl;
//cout << *(ptr + i) << " " << (uintptr_t)(ptr + i) << endl;
}
}
// 즉 위의 두 cout 은 같은 결과를 출력한다.

위에서 말했듯이 정적 배열은 0번째 원소의 주소를 가리키고 있는 포인터이기 때문에, * (de-reference) 를 조금만 이해해도 배열을 순회하는 데 아무런 무리가 없는 것이다. 문자열도 마지막에 \0 (널값) 을 원소로 가진 배열이기 때문에, 문자열 순회도 포인터로 가능하다.
char name[] = "jack jack";
const int n_name = sizeof(name) / sizeof(char);
int *ptr = name;

for (int i = 0; i < n_name; ++i)
{
    cout << *(ptr + i);
} // 보다시피, 아예 포인터끼리의 연산만으로 문자열 순회가 가능하다. 

[C언어 스타일의 문자열 심볼릭 상수(기호 상수)]
포인터가 주소를 담는 ‘변수’ 라기는 하나, 문자열을 직접 담을 수는 없다. 본래 포인터의 역할은 주소만을 입력받게 되어있기 때문이다. 그러나 앞에 const 를 붙여 포인터를 초기화하면, 그 포인터 자체가 하나의 기호 상수로서 값을 지닐 수 있다.
ex) const char *name = “Jack Jack”; cout << name << endl;
위 코드는 “Jack Jack” 을 출력한다… 다만 ‘구분을 위한 특정 주소’ 를 담는 본래 역할에 맞게, 두 개의 포인터가 같은 값을 가질 수 없다. 즉 또다른 포인터에 “Jack Jack” 을 넣어 초기화한다고 해도, 컴파일러가 알아서 이 두 포인터가 같은 값을 지닌다는 것을 인식하고 같은 주소값을 부여한다.
[cout 과 문자열]
char char_arr[] = “Hello”; 와 같이 문자열도 배열이기는 하지만, cout 은 일반 배열이 아니라 문자열인 경우 알아서 포인터값이 아닌 문자열을 출력해준다. 그냥 cout 의 특성이라고 외우면 된다. 특이한 점은, 문자 ‘열’ 이 아닌 그냥 char c = “Q”; 와 같이 char 값도 같은 방식으로 인식하기 때문에 &c 는 오히려 제대로 된 메모리 주소를 출력하지 못한다. 이 모든 세세한 오류는 standard library 를 쓰게 되면 신경 안 써도 되게 되니 너무 걱정하지 말자.

[메모리 동적 할당 new 와 delete]
가장 까다로운 부분 중 하나. 하지만 매우 중요하다.
정적 할당은 stack 에 저장되고, 동적 할당은 heap 에 저장되는데, stack 보다 heap 의 사이즈가 훨씬 크다. 즉, 자료형 크기는(array[1000000] 같은.. 사실 4mb 도 안 됨에도 불구하고 불가능) 내가 미리 그 정적 크기를 알고 있다고 해도 선언하는 것이 불가능하다. 즉 동적할당을 잘 활용할 줄 알아야 한다. 사실 배열에 많이 쓰이지만 아래에서 일단 변수로 개념을 읽혀보자. 그리고 사실 요즘은 ‘스마트 포인터’ 가 생겨서 엄청 하이엔드 개발 하는 게 아니라면 이 개념도 완벽히 숙지할 필요는 없다.
new 를 이용해 포인터로 저장하는 방식을 이용하며, 고급 프로그래밍으로 가면 이용한 메모리를 다시 os 에 반환할줄도 알아야 한다. 이 때 delete 를 이용하고, delete 를 해도 해당 포인터에 주소는 남아있기 때문에, 아무 값도 없다는 nullptr 을 대입해놓는 습관을 들이는 게 좋다. 어차피 스마트 포인터 쓰겠지만, 암튼 동적 할당을 받기만 하고 제대로 지워주지 않으면 ‘메모리 누수’ 라는 엄청난 일이 생긴다…
#include
using namespace std;
int main()
{
// int val = 7;
int *ptr = new int{ 7 };
cout << *ptr << endl;

delete ptr;
ptr = nullptr;
cout << "After delete" << endl;
if (ptr != nullptr)
{
    cout << *ptr << endl;
}

}

[동적 할당 배열]
정적 할당 배열은 const int length=5; 처럼 배열의 길이를 const 로 초기화한 값만을 받아 배열 초기화가 가능했다. 동적 할당은 다르다.
#include
using namespace std;
int main() {
int length;
cin >> length;

int * array = new int[length] {}; // 빈칸이면 default value 0, 혹은 내가 적어 넣은 만큼은 배열에 대입

array[0] = 1;
array[1] = 2;

for (int i = 0; i < length; i++)
{
    cout << array[i] << endl;
    cout << (int)&array[i] << endl;
}

delete[] array;  // 꼭 삭제해주자.

return 0;

}

[포인터와 const]

  1. 포인터와 포인터가 가리키는 value 모두 const 인 경우
    value 의 값이(변수가) const 라면 그것을 가리키는 포인터도 const 로 정의해주어야 한다. 변하지 않는 값이라도 출력은 되어야 하니까.
  2. 포인터만 const
    value 가 바뀌면 그 바뀐 값을 가져온다. 포인터의 역참조(de-reference) 로 재할당할 수는 없지만 그냥 value 자체를 바꾸면 바뀐다. 포인터에 const를 붙인다는 건(const int *ptr = &value; 같이) 포인터가 가리키는 값을 안 바꾸겠다는 거지 포인터의 주소 자체를 안 바꾸겠다는 것은 아니다. 그래서 사실 value 값을 바꾸는 건 원래 안 되야 할 걸 해버리는 좀 헷갈리는 느낌이 든다…
    위에서 말한 이유대로, ptr = &value2; 같이 포인터에 새로운 주소값을 부여하는 것은 아무 문제가 없다.
  3. 진정한 포인터 const
  4. 에서 말했듯, 단순히 const int *ptr = &value; 같이 맨 앞에 const 를 붙이는 건 우회해서 바꿀 수 있는 방법이 있고, 포인터에서의 const 역할을 괜히 헷갈리게만 만든다. 그런데 const 를 이렇게 쓸 수도 있다. int *const ptr = &value; 이렇게 되면, ptr = &value2; 같이 포인터가 가진 주소값을 바꾸는 것이 불가능해진다. 즉, *const 를 중간에 넣는 것이 진정한 포인터를 위한 const 라고 볼 수 있다. 물론 const int *const ptr = &value; 같이 const 를 두 번 써서 주소 변경도, 역참조 값 재할당도 불가능하게 만드는 것도 가능하다.

그래서 이 모든 const 이용 포인터를 어디 쓰냐? 하면 주로 어떤 함수에 매개변수로 포인터를 넣을 때 (주로 배열), 값 변경을 막기 위해서 적절히 사용한다. 근데 이것도 사실 요즘은 참조 변수를 더 많이 활용한다.

[참조변수 reference variable]
포인터는 일일이 * 을 붙여주고 해야되서 귀찮다. 그래서 특정 경우에는 reference variable 이 훨씬 편할 수 있다.
int value = 5;
int &ref = value; // 메모리 주소 가져오는 & 과 기호는 똑같지만 다른 역할임을 주의.
위처럼 이용하며, 주소를 찍어보면 value 의 주소와 ref 의 주소가 아예 같게 나온다. 포인터의 경우 value 의 주소를 담고있을 뿐 포인터 자신의 주소는 따로 있는 것과 달리 ref 는 아예 value 와 주소까지 같은 것이다. 즉, 마치 value 의 또다른 이름(별명)인 것처럼 작동한다.
포인터와 달리 반드시 선언과 초기화가 분리되지 못하고 만들 때부터 반드시 초기화 되어야 한다. 그리고, 어떤 literal 을 값으로 가지지 못하고 반드시 다른 변수가 할당되어야 한다. 또한, 할당받은 변수가 const 라면(const int y = 8;), ref 도 const int &ref = y; 처럼 반시 const 여야 한다. 원본이 const 인데 바꿀 수는 없으니까.

Reference variable 의 가장 큰 활용도는 역시 어떤 함수의 매개변수(parameter) 로 reference variable 을 넣었을 때다. 그 방법은 간단하다. 함수의 매개변수를 정의할 때, 변수이름 앞에 & 만 붙여주면 된다. 이렇게 하면, 본래 함수에서는 매개변수로 받은 변수의 원본을 바꾸지 않지만, reference 로 받은 변수는 원본도 변화시킨다. 심지어 pointer 를 썼을 때와 달리, 따로 값을 복사하는 과정이 없기 때문에(reference variable 은 변수 자체가 넘어간다.) 성능상으로도 뛰어나다.

뿐만 아니라 struct 가 서로 다중으로 얽혀 있고, 구조의 깊숙한 곳에 있는 값을 꺼내오고 싶을 때, reference variable 에 그 깊은 곳의 접근주소를 저장해서 쓸 수 있다. 예를 들어서, ot.st.v1 에 접근해야 할 일이 많다면, int &v1 = pt.st.v1; 처럼 정의해서 짧게 가져다 쓸 수 있다.

[참조와 const]
int x = 5;
int &ref_x = x;

원래 reference variable 은 반드시 다른 변수로 초기화 해야 한다고 위에서 말했다. 하지만
const int &x = 3+6; 같이, 앞에 const 를 붙이면 뒤에 literal 의 연산값을 대입하는 것이 가능하다. 왜냐하면, 이런 방식으로 어떤 함수의 매개변수를 정의할 수 있어야만 함수를 불러올 때 인자로 literal 혹은 literal 의 연산값을 넣을 수 있기 때문이다.

[포인터와 참조의 멤버 선택]
포인터와 참조를 어떤 구조체(struct) 나 클래스에 대해서 사용할 때는, 그 포인터나 참조로 해당 구조체나 클래스의 멤버에 접근할 수도 있다.
아래는 다양한 방법으로 참조와 포인터로 구조체를 갖고 논 것을 보여준다.
#include
using namespace std;

struct Person
{
int age;
double weight;
};

int main() {
Person person;

person.age = 5;
person.weight = 30;

Person &ref = person;
ref.age = 15;

Person *ptr = &person;
ptr->age = 30;
(*ptr).age = 20;  // . (member selection operator) 가 * (de-reference) 보다 우선순위가 높기 때문에 반드시 괄호쳐주어야 한다. 물론 이런 짓 거의 안 해.

Person &ref2 = *ptr;
ref2.age = 45;

cout << &person << endl;
cout << &ref2 << endl;
return 0;

}

[For-each 문] C++11

[다중 포인터와 동적 다차원 배열]
이중 포인터 : 포인터의 포인터
ex)
int ptr = nullptr;
int *
ptrptr = nullptr;

int value = 5;

ptr = &value;
ptrptr = &ptr;

// 이렇게 했을 때, ptrptr 이 &ptr 이고, *ptrptr 이 ptr 이다. 복잡해 보이지만 걍 역참조랑 주소 값이야.. 어려울 것 없음

이중 포인터는 이차원 배열을 만들 때 많이 쓴다.

ex)
const int row = 3;
const int col = 5;

const int s2da[][5]

[정적 배열 대체는 std::array, 동적 배열 대체는 std::vector]

#include

std::vector array;
std::vector

[std::vector 사용시 주의할 점] (유튜브 javidx9 참고함)
vector 사용시 vector 의 특정 원소의 주소를 포인터에 담아 사용하는 것을 극도로 경계해야 한다. 동적 할당을 하는 vector 의 특징상, 벡터의 크기가 너무 커져서 원래의 시작점 주소로부터 연속적인 메모리 공간을 차지할 수 없어지면 더 여유있는 공간을 찾아 새로운 메모리 주소를 시작점으로 삼아 벡터를 통째로 옮기게 된다. 그런데 기존 벡터의 특정 원소의 메모리 주소를 담아서 이것저것 하려다 보면 이미 옮겨간 벡터와 그 전 벡터끼리 메모리 주소가 연속적이지 못해 디버깅하기도 힘든 복잡한 문제가 발생할 수 있다.

<함수>
[매개변수(parameter)와 실인자(argument || actual parameter) 의 구분]
argument == actual parameter (용어 혼용해서 쓴대.. 몰랐다.)

[값에 의한 인수 전달] 7.2
Call by Value (passing arguments by value)
함수가 호출될 때, 함수의 매개변수에 값을 전달하기 위해, 호출할 때 매개변수가 ‘선언’ 되고, 그 선언된 매개변수에 값이 복사가 되어 초기화 된다. 왜 그런 내부적인 과정이 발생하냐고? doSomething(5) 처럼 인자로 넣을 변수를 따로 선언하지 않고 바로 값을 넣거나, doSomething(x+1) 같이 연산을 넣는 때도 있기 때문에, 당연히 복사해서 매개변수로 쓰일 값을 새로 초기화하는 단계가 있어야 하는 것이다.
변수를 따로 초기화해서 그 변수를 넣어 함수를 호출한다고 해도, 그 변수가 전달되는 것이 아니라, 그 변수가 가진 값만이 복사되어 전달된다. 따라서 속도가 느릴 수 있다. (주소를 찍어보면 원 변수의 주소와 매개변수로 쓰인 변수의 주소가 다른 것을 알 수 있다.)

복사되는 것의 장점 : 복사해서 쓰기 때문에, 함수 안에서 무슨 짓을 하든 원본 값(바깥 값)이 바뀌지 않는다. 또 매개변수로 선언되었던 것이 함수가 끝날 때 알아서 반환되기 때문에 깔끔하다.

[참조에 의한 인수 전달] 7.3
Call by Reference ( passing arguments by reference)
// 최근에 가장 많이 쓰이는 방식. 오픈소스에도 많이 쓰인다. 복사가 일어나지 않는다.
함수 호출 때 인자로 어떤 변수를 넣었을 때, 복사가 이루어지지 않고 그 변수 주소 자체를 넘긴다. 즉 함수 밖에서든 안에서든 그 변수의 주소가 같다.
매개변수 정의할 때 식별자(identifier) 앞에 & 를 붙여 reference 로 전달하도록 해주면 된다. 이렇게 하면, 매개변수가 함수내에서 변경되면, 그 레퍼런스 주소를 가진 변수가 함수 바깥에서도 변화가 적용된다.
함수를 정의할 때, 함수 안에서 쓸 뿐 변하지 않기 바라는 것은 값으로 받는 매개변수로, 함수의 실행 결과로 인해 바뀌길 바라는 매개변수는 참조를 받도록 정의한다. (7.3 코드 참고)

reference 매개변수 의 단점  주소를 받아야 되기 때문에 함수를 호출할 때 직접 값(쌩 숫자, 문자, 연산값 등 literal 값)을 입력받지 못한다. 하지만 굳이 매개변수로 받고 싶다면 매개변수 정의 때 앞에 const 를 붙여서 가능하다! 요즘 많이 쓰는 방식 (물론 이 경우엔 함수 안에서 그 변수 값을 변경하지 못한다.)
ex)
void foo(const int &value) {
cout << value << endl;
}

int main() {
foo(6);
return 0;
}

포인터에 대한 참조를 보낼 수도 있다.

void foo(int *&ptr) {
cout << ptr << “ “ << &ptr << endl;
}

int main() {
int x =5;
int *ptr = &x;

foo(ptr);

return 0;
}

위 매개함수 정의가 이해 안 간다면, (int) &ptr 라고 분리해서 이해하면 된다. 좀 더 명확하게 이해하기 위해 아예 타입을 선언해보면, typedef int pint;  이렇게, 하고 pint ptr = &x; 초기화 후 매개 변수 정의도 pint &ptr 으로 받아주면 된다. 즉, 일반적인 reference 를 받아준 것처럼 보이게 되어 이해가 쉽다. (코드 Chapter7_3_2 참고)

array 를 파라미터로 넘기는 방법(코드 Chapter7_3_3)
정적 array 를 reference 로 보낸다면, array 길이를 꼭 매개변수 정의에 써주어야 한다.
ex) void printElements(int (&arr)[4])
그러나 요즘은 동적 array 나 vector 을 쓰는 경우가 많아 저런 귀찮은 짓 안 해도 된다.
#include
#include
using namespace std;

void printElements(vector&arr){
//void printElements(int (&arr)[4]) {
for (auto &itr : arr)
cout << itr << " ";
cout << endl;
}

int main()
{
// int arr[] = { 1,2,3,4 };
vector arr{ 1,2,3,4 };

printElements(arr);

return 0;

}

[주소에 의한 인수 전달]

c style 코딩에서 많이 쓰인다. 특히 array 쓸 때..

포인터와 밀접하게 관련되어 있다. 말 그대로 주소를 넘긴다.그런데 배웠듯이 포인터도 결국 별개의 메모리를 차지하는 변수이고, typedef int* pint; 처럼 자료형을 정의하고 쓰면 매개변수 정의 때 void foo(pint ptr) 처럼 쓸 수 있듯이 마치 ‘값에 의한 전달’ 과 똑같이 쓸 수 있다. 즉, 주소에 의한 인수 전달도 결국엔 매개변수로 쓸 때 복사가 일어난다.(함수 밖에서의 포인터 주소와 매개변수로 포인터를 받아서 쓰는 함수 내에서의 포인터 주소가 다르다는 것이다.) 따라서 성능상 이득이랄 게 없다. 다만 값에 의한 전달과 다른 점은, 마치 참조에 의한 인수 전달에서 함수 내에서의 변수 변화가 함수 밖에도 영향을 미치듯이, 주소에 의한 인수 전달에서도 함수 내에서 매개변수로 받은 포인터를 de-referencing 해서 값을 변경하면 함수 밖에도 영향을 미친다는 것이다. ex) 함수 내에서  *ptr = 10; 하면 함수 밖에서도 그 포인터가 가리키고 있던 변수의 값이 변경된다.

[다양한 반환(return) 값들(값, 참조, 주소, 구조체, 튜플]
값으로 반환 받는 거야 뭐 기본이고,
주소로 반환 받는 건 오류가 발생할 여지가 많아서 실무에서 잘 쓰이지 않는다…
참조로 반환 받는 건 그냥 함수 정의 때 반환 자료형 뒤에 & 붙여주면 된다.
ex)
int& getValue(int x) {
int value = x* 2;
return value;
}
int main() {
int value = getValue(5);
cout << value << endl;
return 0;
}
근데 주소로 반환 받는 것도 그렇고 참조로 반환 받는 것도 그렇고, 함수 실행이 끝난 후 버려질 (지역)변수의 주소값을 반환받는 것 자체가 오류가 발생할 여지가 크다. 실제로 저렇게 반환받은 값을 2번 cout 하면 두번째엔 garbage 값(쓰레기 값)이 출력된다. 즉, 해당 주소가 사라져버렸기 때문에, 그 값을 제대로 지니고 있지 못한다. de-referencing 을 제대로 할 수가 없는 것이다. 다만 std::array 를 쓸 때 해당 array 의 원소의 값을 효과적으로 바꾸기 위해서 사용하는 경우가 있다. 함수에서 바꾸고 싶은 인덱스의 reference 를 반환해서 그 reference 에 값을 대입하는 것이다. 그럼 함수 실행 이후에는 알아서 그 인덱스 주소도 버려주고 좋겠지? (7_5_1 코드 참고)

여러 개의 변수를 반환 받고 싶을 때  원래 c style 에서는 구조체(struct)를 만들어서 return 값을 받으면 한꺼번에 여러 값을 받는 효과를 낼 수 있었다. 하지만 이것의 단점은 함수 하나를 만들 때마다 구조체를 만들어야 했다..
다른 방법은 tuple 을 만들어 쓰는 방법이 있다. #include

ex)
std::tuple<int, double> getTuple()
{
int a = 10;
double d = 3.14;
return std::make_tuple(a,d);
}

int main()
{
std::tuple<int, double> my_tp = get_tuple();
std::get<0>(my_tp); // a
std::get<1>(my_tp); // d
}
하지만 이것도 여전히 불편하다.. 그래서 c++17부터는 좀 더 편하게 초기화가 가능하다.
ex) main 함수에서 
auto[a, d] = getTuple();
cout << a<< endl;
cout << d << endl;

[인라인 함수]
함수를 호출  매개변수에 값 복사  함수 실행  결과값 반환  반환된 값 사용
의 단계를 줄여보고자 함수를 정의할 때 반환 자료형 앞에 inline 이라고 붙여서 정의하면 inline 함수가 된다. 마치 함수 호출부 부분에 함수 바디 부분을 직접적으로 써넣은 것처럼 컴파일 된다. 즉 함수가 호출되는 게 아니다.
보통은 헤더 파일에 함수를 정의할 때 많이 쓰인다. (애초에 목적이 많이 실행되는 함수를 조금이라도 더 빨리 실행시키려고 쓰는거라…)

근데 사실 인라인 함수라는 게 그냥 컴파일러에게 ‘권장’ 정도의 수준이고, 컴파일러 수준이 높아진 오늘날은 어차피 알아서 최적화 해준다.. 즉 쓸모가 없다. 코드 최적화를 하고 싶으면 다른 방법을 써라..

[함수 오버로딩]
동일한 이름을 가진 함수를 여러 개 만드는 것.
들어오는 매개변수가 다른데(특히 자료형만이 다른 경우), 비슷한 기능을 수행하고 싶은 경우에 쓴다. 기능이 아예 달라도 오버로딩 할 수는 있지만 그럼 헷갈리니까 그런식으로 쓰지 말자.
왜 이게 가능하냐면, 함수가 정의될 때 함수의 이름(식별자) 를 기준으로 구분하지 않고 매개변수를 기준으로 구분하기 때문이다. 즉 자료형이 다르거나 매개변수가 다르면, 컴파일러가 어떤 함수를 호출할 때, 매개변수가 더 잘 맞는 함수를 골라서 호출해준다. 다르게 말하면, 매개변수가 달라야 하고, 컴파일할 때 어떤 함수가 더 잘 맞는지 결정이 되어야 한다.

그런데 보통 매개변수가 다르다면 반환하는 형태도 달라야 하기 때문에, 함수 오버로딩을 할 때는 void 형태의 반환 자료형을 놓고, 매개변수를 참조로 받는 방법이 많이 쓰인다. (파이썬에서는 상상하지 못할 방법이지.)
어떤 함수를 쓸 지 애매하면 에러를 뱉으므로 주의.. 캐스팅을 해서 넣던가 해서 명확하게 인자를 넣어줘야 한다. 확신 없으면 걍 오버로딩 쓰지마.

[매개변수의 기본값]
파이썬에서와 유사하게 매개변수 정의 내에서 초기화해주면 된다.
주의할 점  forward declaration 같은 걸 했을 때는 둘 중 하나에만 default 값을 써놔야 한다. 안 그러면 어떤 걸 default 로 해야할지 헷갈려 할 거 아냐. 같은 맥락에서, 보통은 헤더 파일에 default 값들을 설정해놓게 된다. 보통 거기에 다 프로토타입 선언을 해놓으니까.

[함수 포인터] 7.9

지금까지는 변수에 대한 포인터만 배웠다면, 함수에도 포인터가 있다.
사실 함수 자체를 출력해보면(ex> cout << func << endl;) 애초에 주소값을 뱉는다. 함수도 포인터인 것이다. 그래서 포인터를 만들 때 & 을 앞에 붙일 필요도 없다.
함수포인터를 초기화 할 때는, 반환하는 자료형과, 매개변수의 자료형까지 맞춰서 정의해주어야 한다. 즉 내가 가리키려는 함수 func가 int 를 반환하고 int 를 매개변수로 가진다면, int(funcptr)(int) = func; 같이 해주어야 에러가 안 뜬다.  괄호가 * 를 포함하고 있음에 주의.. 변수의 포인터 할 때는 int 라고 하는 게 더 자연스럽다고 생각했었는데 함수의 포인터에서는 그렇게 묶으면 아예 에러가 떠버린다..
함수포인터가 가리키는 함수를 바꾸는 것도 가능하다. 나중에 객체지향을 깊이 배우면 유용하게 쓰이는 기능.(다형성 이란 걸 이용할 때 쓰인다고 한다.)
ex 
int func() {
return 5;
}

int goo() {
return 10;
}

int main()
{
// 함수 포인터 초기화
int(*funcptr)() = func;
cout << funcptr() << endl;
funcptr = goo; // 포인터가 가리키는 함수 변경
cout << funcptr() << endl;
return 0;
}

함수 포인터를 잘 쓰면, 매개변수에 함수를 집어 넣을 수 있다. 즉 어떤 함수를 호출할 때, 매개변수로 원하는 함수를 넣을 수 있다는 것. 신기신기..
ex)
bool isEven(const int &number) {
if (!(number % 2)) return true;
else return false;
}

bool isOdd(const int &number) {
if (number % 2) return true;
else return false;
}

void printNumbers(const array<int, 10> &my_array, bool (*chk_funcptr)(const int &) = isEven) {
for (auto element : my_array) {
if (chk_funcptr(element)) cout << element;
}
cout << endl;
}

int main()
{
std::array<int, 10> my_array{ 0,1,2,3,4,5,6,7,8,9 };

printNumbers(my_array, isEven);
printNumbers(my_array, isOdd);

return 0;

}

위 예시에서 이 함수 포인터가 너무 길고 복잡하다 싶으면 typedef 해서 갖다 써도 된다.
typedef bool(*chk_funcptr_t)(const int &);
void printNumbers(const array<int, 10> &my_array, chk_funcptr_t chk_func = isEven)

잘 기억은 안 나지만 type alias 란 것도 쓸 수 있다.
using chk_funcptr_t = bool(*)(const int&);

c++11 에서는 functional 이라는 걸 #include 해서 더 간편하게 사용할 수 있다. 요즘은 이걸 함수 포인터보다 더 많이 쓴다고 한다. (사실 함수 포인터 사용법이랑 뭔 차이가 있는 지 잘 모르겠다.)

std::function<bool(const int&)> funcptr = isEven; 같은 식으로 초기화하고, 매개변수에서는 똑같이 std::function<bool(const int&)> chk_func 같이 정의한다.

[스택과 힙]
포인터 같은 건 왜이리 복잡한 지 이해하려면 컴퓨터가 메모리를 사용하는 방식을 알 필요가 있다.

전역 변수  main() 함수  main 함수 내 다른 함수  그 다른 함수 안의 다른 함수  의 순서대로 쌓이고 그 후 위에서부터 순서대로 다시 뺀다.

스택은 빠르긴 한데 너무 사이즈가 작다. 그래서 main 함수에 너무 큰 변수를 선언하거나 재귀를 너무 쌓으면 stack overflow 가 발생한다.
그래서 큰 데이터를 선언하려면 동적할당을 고, 이건 heap 에 저장된다. 그렇지만 heap 은 stack 과 달리 어디에 저장되는 지 알 수 없다는 문제가 있다.
힙에 메모리 할당했었는데 delete 를 안 해주면 힙에도 쓸데없는 매모리 누수가 쌓이고, 그럼 다른 프로그램이 쓸 공간이 없어져버린다..

[std vector 을 스택처럼 사용하기]
new, delete 를 통해 힙을 관리하는 건 사실 느린 과정이다. 즉 최대한 덜 쓰는 것이 좋다. 그래서 는 size 개념과 capacity 개념 두 개를 분리해서 이해해야 한다. size 는 우리가 일반적으로 알고 있는 동적 할당 개념과 같다. 즉 초기화 값이나 resize 된 값에 맞춰서 값을 지닌다. 그런데 3 개 원소에서 2개로 resize 해도, v.capacity() 를 찍어보면 여전히 3이라고 나온다. 즉 vector 는 성능을 위해 resize 를 해도 delete 를 하지는 않고 그냥 가릴 뿐이다. 즉 vector 의 진짜 메모리 크기와 관련된 것은 capacity 개념이다. v.reserve(1024) 같이 메모리 크기를 미리 크게 잡아줬을 때, resize(1024) 를 했으면 남는 부분을 모두 0으로 채웠겠지만, capacity 는 숨어서 관리하는 것이기에 0으로 채우지 않고 그냥 메모리를 차지하고만 있는다. reserve 를 하는 이유는 필요할 때마다 new, delete 를 일일이 하면 속도가 느리기 때문이다.

벡터를 stack 처럼 종종 사용한다.(특히 재귀함수를 쓸 때)
ex) std::vector stack;
stack.push_back(3);
stack.pop_back();

[단언하기 assert]
디버그에서 사용. 단순히 주석으로 이건 이래야만 한다.. 라고 써놓는 것을 넘어 문제가 발생하면 바로 컷 시키고 어디가 문제인지 런타임 때 캐치해서 알려줄 수 있게 된다. 주로 assert() 안에 어떤 조건문을 넣어 false 인 경우 중지시킨다. 두번째 인자로 오류 메시지도 써놓을 수 있다.

#include
assert(number == 5);
assert(idx <= my_array.size() – 1);

‘static_assert()’ 를 쓰면 컴파일 시점에서 결정되는 변수 등에 대한 오류를 잡아준다. 즉 const 등 런타임에 바뀔 수 없는 변수들에 대한 체크를 한다. 사실 그래서 ide 에서는 코드 작성 시점에서 빨간줄을 그어준다.
ex)
static_assert(x==5, “x should be 5”);

[명령줄 인수 command line argument]
main 함수의 괄호엔 뭐가 들어가나?

int main(int argc, char *argv[]) // argc 는 개수, argv 가 cmd 에서 인자로 받은 것들
{
for (int count = 0; count < argc; ++count)
{
cout << argv[count] << endl;
}
return 0;
}

즉 cmd 에서 실행파일을 실행시킬 때, ~~.exe random message 132 3 42  같이 쓰면, random message 132 3 42 가 출력된다. 주의할 점은 0번째, 즉 첫번째 출력되는 것은 그 실행파일 자체의 파일경로이다. 근데 뭐 이런 걸 쓸 일이 있으면 그냥 Boost 라는 라이브러리를 사용하는 게 좋다. 거의 준표준 라이브러리며 .Program_options 에 웬만한 옵션은 다 있다.

[생략부호 Ellipsis]
함수가 받는 인자의 개수를 제한하고 싶지 않을 때 쓴다. ex) int findAverage(int count, …) {}
어떤 타입일지는 미리 알고 있어야 하고, 개수를 잘못 쓰면 쓰레기값이 나온다.. 즉 쓰기 어려우니 이왕이면 쓰지말고 array 등을 잘 활용하도록 하자.

<객체기향의 기초>

드디어 객체지향.
기본적으로 struct 를 기반으로 struct 안에 멤버 변수, 멤버 함수를 추가시켜 나가다 보니 클래스의 개념이 탄생했다. 즉 사실 struct 와 class 가 구조적으론 큰 차이가 안 나지만, 주로 멤버’함수’ 가 추가되는 순간 클래스를 쓰게 된다. 나중에 class 안에 쓰는 public: , 즉 access specifier 의 용도를 더 배우면 차이점을 더 잘 알게 될 것이다. (근데 사실 모양만 비슷하지 public 을 안 붙여주면 애초에 멤버 변수에 접근도 안 된다.. 즉 class 를 struct 처럼 쓸 수는 없다.)

class Friend {}
클래스 정의 후 실제로 메모리를 차지하는 변수를 정의해주는 것을 instantiation 이라고 하고 이 변수를 클래스의 instance 라고 한다. 즉 클래스 자체는 메모리 주소가 존재하지 않으나 instantiation 된 instance 는 주소가 존재한다.
클래스 안의 멤버 변수는 멤버 변수임을 알리기 위해 앞에 m_ 을 붙이곤 한다. 근데 최근에는 앞이나 뒤에 _ 만 붙이고 끝내곤 한다.

[캡슐화(encapsulation), 접근 지정자(access specifier), 접근 함수(access function)]
뛰어난 프로그래머는 복잡한 것을 단순해 보이도록 정리를 잘 하는 사람이다. (연관관계 명확화)
그래서 캡슐화로 딱 정리하는 습관이 좋다.

클래스 멤버들을 외부에서 접근하게 할 수 있으려면 public: 을 통해 외부 접근을 허용해주어야 한다. 즉 클래스는 public, private, protected 같은 access specifier 에 따라 외부 접근 가능여부가 달라진다. (protected 는 나중에 배우는 상속에서, 자식 클래스는 접근이 가능한 access specifier)
사실 빠르게 프로토타입을 만든다거나 프로젝트가 대단히 커지거나 하지 않으면 걍 모든 멤버변수를 public 에 두고 맘대로 바꿔가며 쓰는 게 훨씬 편하다. 하지만 프로젝트 규모가 아주 커지거나 오픈소스라거나 뭐 암튼 어른들의 이유가 생길 예정이라면, private 로 외부접근을 막고 외부에서 접근하려면 따로 access function 을 만들어서 접근하도록 강제하는 것이 좋다.
access function 은, 같은 class 내라면 private 멤버에도 접근할 수 있다는 점을 이용해서, public 에 private 멤버에 접근하는 함수를 만드는 것이다. 즉 외부에서 class 와 소통하는 통로가 된다. 보통 변수 값을 변화시키는 setter, 값을 그냥 읽어오기만 하는 getter 로 나눈다. 굳이 이런 짓을 왜 하나 싶겠지만, 멤버 변수에 직접 접근하게 하는 것은 나중에 멤버변수 이름을 바꿀 일이 생겼을 때 엄청난 노가다가 발생할 우려가 있다. 즉 getter 나 setter 을 통하게 하면 class 내에서 멤버 변수와 getter, setter 을 수정하면 끝이지만, 외부에서 변수에 직접 접근하도록 했었다면 모든 프로젝트에서 그 변수를 썼던 곳을 찾아 변경해주어야 한다..
위에 같은 class 라는 걸 강조한 이유는, 서로 다른 인스턴스여도 같은 클래스이기만 하면 private 에 접근할 수 있다는 것이다. 즉 멤버 함수 중에 같은 클래스의 다른 인스턴스의 private 멤버 변수에 접근하는 게 가능하다.
ex)  Date 라는 클래스 내에서, m_month, day, year 은 Date클래스의 private 멤버
void copyFrom(const Date &original)
{
m_month = original.m_month;
m_day = original.m_day;
m_year = original.m_year;
}

[생성자 Constructors]
인스턴스가 생성되는 시점에 실행되는 함수. 인스턴스를 생성하자 마자 이런 걸 해야 해. 를 설정해 놓는 것이라고 보면 된다.
instance 를 만들자 마자 멤버 변수의 값을 초기화 하는 방법은 다양하다.
“Fraction” 이란 클래스가 있을 때

  1. 인스턴스 생성 후 하나하나 따로 해줄 수도 있고(멤버가 public 일 때),
    ex)
    Fraction frac;
    frac.m_denominator = 1;

  2. 인스턴스 만들 때 바로 uniform initialization 을 해줄 수도 있다.(멤버가 public 일 때) uniform initialization 이 좀 더 자료형에 엄격하다.(자료형을 자동 변환해주지 않는다.)
    ex)
    Fraction frac{ 0,1 } // 이러면 순서대로 초기화 됨.

  3. 애초에 클래스 만들 때 default 값을 초기화 해놓을 수도 있다.

  4. 생성자 사용. 생성자는 public 에 클래스 이름과 똑같은 이름을 마치 함수를 호출하듯 쓴다. 생성자는 인스턴스가 생성될 때 자동으로 실행된다. 즉, 인스턴스 생성이 이루어질 때(construct) 자동으로 실행되는 함수가 생성자(constructor)다. 주의할점1 이 생성자가 인스턴스 자체를 생성하는 큰 역할을 한다고 과대 해석하진 말자. 걍 자동으로 실행되는 놈인거다. 주의할점2 생성자는 자동으로 실행될 뿐 가장 먼저 실행되는 것은 아니다. 멤버변수부터 순서대로 초기화한다. 같은 맥락에서 멤버변수에서 초기화를 했다고 해도 생성자에서 다시 초기화를 하면 생성자가 더 뒤에 실행되므로 생성자의 초기화 값이 남는다. 주의할점3 클래스를 만들 때 따로 이 생성자를 만들어놓지 않아도 아무것도 하지 않는 기본 생성자가 숨어있다. 주의할점4문법적으로 c++ 의 단점이라고 할 수 있는 점이, 이 생성자가 parameter 가 하나도 없으면 인스턴스 생성시 ( ) 를 빼도록 되어있다는 것이다. 즉 생성자에 parameter 가 없다면 Fraction frac(); 이 아닌 Fraction frac; 이 맞는 문법이 된다. 즉 원래는 parameter 가 있으면 반드시 괄호를 붙여서 인스턴스를 만들어야 한다.
    팁1한 클래스에 여러 개의 생성자를 만들어 놓을 수도 있다. 이 경우 함수 오버로딩 개념을 생각하면 된다.

생성자를 private 으로 만드는 건 상식적으로 말이 안되지만, 굳이 그렇게 하는 프로그래밍 기법도 있긴 하다. 지금 당장은 알아보지 말자.

[생성자의 멤버 이니셜라이져 리스트 Member Initializer List]
생성자에서 일반적으로 하듯 중괄호 안에 멤버변수 초기화를 하는 게 아닌, : (콜론) 을 치고 바로 초기화 하는 것. 걍 표기의 차이? 같은 느낌임. 굳이 안 써도 됨. 이걸 썼다고 해서 기존 생성자의 중괄호를 못 쓰는 것은 아니다. 병행이 가능하다..
ex)
public:
Something()
: m_i{ 1 }, m_d{3.14},…
{
m_i += 3; // 이렇게 다시 초기화 하는 등 병행 사용 가능.
}

[위임 생성자] C++11
다른 생성자를 사용하는 것.
함수 오버로딩의 심화 느낌이다. (내 생각) 생성자 내에서 다른 생성자를 호출하는 방식으로 쓴다.
즉 이미 클래스 내의 어떤 생성자에서 멤버 변수를 초기화하는 기능을 가지고 있다면, 그것을 그대로 활용하되, 새로운 생성자를 만들고 그곳에서 본인이 덮어쓰고 싶은 매개변수만을 매개변수로 받아 순서와 관계 없이 덮어 쓸 수 있다. 기본적으로 매개변수의 기본값(default value) 기능이 없던 옛날에 주로 쓰였던 기능인데, 지금도 기본값 순서를 마음대로 바꿀 수 없는 c++ 제약상 쓰일 때가 있다.
class Student
{
private:
int m_id;
string m_name;

public:
Student(const string &name_in)
//: m_id(0), m_name(name_in)
: Student(0, name_in)
{}

Student(const int &id_in, const string &name_in)
    : m_id(id_in), m_name(name_in)
{}

void print()
{
    cout << m_id << " " << m_name << endl;
}

};

int main()
{
Student st1(0, "Jack Jack");
st1.print();
}
// 꼭 이니셜라이져 리스트를 쓸 필요는 없다. 걍 중괄호 안에 넣어도 돼.

그런데 위 위임 생성자 방식은 생각보다 복잡하고 오히려 권장하지 않는 경우도 있다. (C++11 이후로만 사용 가능하단 점도 문제가 될 수 있다.) 그래서 많은 개발자들이 만능 초기화 함수를 따로 만들어서 쓴다. 즉 멤버변수 초기화 함수를 따로 만들어 놓고, 그 함수를 생성자 내에서 호출함으로써 입맛에 맞게 수정할 수 있게 하는 것이다.(코드 8_5 참고) 교수님도 이렇게 쓰고 나도 이게 더 이해하기 쉬운 듯 하다.

[소멸자 destructor]
생성자가 생성을 하기보다는 생성할 때 실행되는 함수였듯이, 소멸자도 소멸시키기보다는 소멸 시점에 실행되는 함수라고 생각하면 된다.
생성자와 똑같이 클래스 명을 가진 함수를 정의하되, 앞에 ~ 를 붙인다.

소멸자는 주로 클래스 내에 동적할당을 받는 멤버 변수가 있을 때, 인스턴스가 제 역할을 끝냈을 때 메모리를 자동으로 반환하게 만들기 위해 사용한다. 이걸 안 하면 메모리 누수(memory leak)가 생겨… 근데 걍 vector 써.

[this 포인터와 연쇄호출(chaining member function)]
당연하게도, 어떤 클래스의 인스턴스가 만들어질 때마다 클래스 내의 멤버 함수를 복사해서 새로 만들거나 하지 않는다. 즉 클래스의 멤버 함수를 공유하는 것이다(멤버변수는 인스턴스마다 별개의 메모리 주소를 가지지만 멤버함수의 메모리주소는 같다.)그럼 인스턴스마다 멤버함수를 구분하고 싶다면 어떻게 해야할까? this 포인터를 쓰면 된다. 클래스 내에서 this 를 출력해보면 해당 인스턴스의 주소가 뜬다. 즉 this 는 현재 인스턴스를 가리키는 포인터고, 따라서 member selection operator(->) 를 통해 해당 클래스의 멤버 함수에 접근할 수 있다. ex) this -> setID(id); this 도 포인터이기 때문에, de-referencing 을 통해서 -> 이 아닌 .으로 멤버 함수나 변수에 접근 가능하다. ex) (*this).setID(id); 물론 굳이 이럴 이유가 없지. 다시 말해서 this 는 포인터 개념으로 본 클래스의 작동방식이다. 만약 Simple 이라는 클래스가 있고, 그 클래스에 정수 인자를 한 개 받는 setID 라는 멤버 함수가 있다면, 이론적으로는 아래 두 방식이 같다.
Simple s1; // 인스턴스를 instantiation 하고나서,

  1. Simple::setID(&s1, 4);
  2. s1.setID(4);
    하지만 2번 방식만 쓰인다. 클래스 내에서도 이론적으로는 모두 this->setID(id); 같이 써야 명확하나, 암묵적으로 this-> 는 생략된다. 즉 this는 이론적으로만 존재할 뿐, 고급 프로그래밍이 아니면 크게 쓸 일이 없긴 하다. 이 이론을 잘 활용하면 멤버함수 chaining 같이 보기에 쌈박해보이는 코드를 작성할 수 있으나 (실제로 이런 방식이 유행하는 언어도 있다. 특히 자바스크립트?), c++ 에서는 실용성에 의문이 있다.

[클래스 코드와 헤더파일]
클래스는 작성하다 보면 꽤 길어지기 때문에, 보통 선언과 정의를 분리하는 경우가 많다. 클래스 명과 같은 이름의 .h 헤더파일을 만들어주고, 그 클래스를 쓸 곳에서 #include 해준다. 거기다 헤더 파일에도 forward declaration 만 해주고, 멤버함수를 또다시 해당 클래스명과 같은 .cpp 파일에 따로 옮겨 정의해주면 더 깔끔하다. 이때 .cpp 파일로 멤버함수를 옮기면 자신이 어디에 속한 함수인지 몰라 에러를 일으킬 수 있다. 따라서 각 멤버함수의 반환 자료형 과 이름 사이에 클래스명:: 를 붙여 소속을 명확히 해준다. ex) Calc 라는 클래스의 add 라는 멤버 함수를 옮긴다면 Calc& Calc::add(int value) { } 같이 옮겨준다. (맨 앞의 Calc& 는 참조 자료형을 리턴한다는 뜻.

[클래스와 const]
기본적으로 const 를 쓴다는 건 바꾸지 않겠다는 의미를 가지고 있기 때문에, 어떤 인스턴스를 만들 때 앞에 const 를 붙였다면, 멤버 변수에 변화를 주는 모든 멤버함수의 사용이 금지된다. 그런데 변화시키는 게 없는 멤버함수도 일단은 기본적으로 막힌다(getter 같이 그냥 값을 읽는 것도 막힌다.) 컴파일러가 그것까지 해석해주지 않기 때문에, const 인스턴스로 만들어도 쓸 수 있게 하려면 멤버 함수가 const 임을 알려주기 위해 int getFunction() const {} 같이 중괄호 시작 전에 const 를 직접 써넣어줘야 한다. 변화시키는 게 없으면 무조건 const 를 쓰는 습관을 들이자.

복사 Constructor : 어떤 클래스의 인스턴스를 어떤 함수의 인자로 사용한다면, 참조 매개변수로 넘긴 것이 아니라면 복사가 발생할 것이다(값에 의한 인수 전달). 그런데 클래스의 생성자 호출을 찍어보면 복사할 때는 생성자가 실행되지 않는 것처럼 보인다. 하지만 사실 ‘복사’ 되는 때를 위한 복사 생성자가 따로 있는 것이다. 따로 만들어 놓지 않으면 숨겨져 작동하지만, 명시적으로 작성할 수도 있다. 예를 들어 Something 이라는 클래스가 있다면,
ex) 아래와 같이 매개변수로 자기와 같은 클래스의 주소를 받는 생성자를 만들면 이게 명시적으로 작성된 복사 생성자이다.
Something(const Something& st_in) {}
근데 이것도 어쨋거나 복사가 발생하는 것이기에, 요즘은 인자로 인스턴스를 넣을 때는 const 참조형으로 많이 넣는다. (ex: void printInstance(const Something &st) {} )

복사를 막고 싶다면 copy constructor 을 private 영역에 정의해 놓으면 된다.

[정적 멤버 변수]
static 을 써서 멤버 변수를 선언하면, static const 를 써서 아예 변화시키지 않겠다고 하지 않는 이상 클래스 내에서 초기화 할 수 없다. static 멤버 변수를 초기화 하려면 클래스 정의 밖에서 초기화 해주어야 하며, static 의 특징에 맞게 해당 클래스의 모든 인스턴스는 같은 메모리 주소에 있는 static 변수를 공유한다. 심지어 인스턴스가 생성되기도 전에 강제로 해당 변수에 접근해도(&Something::s_value; 물론 public 일 때만 접근 가능) 메모리 주소가 같다. 상수를 정의하거나, 나중에 싱글턴? 디자인패턴에서 쓰게 될 것이다.

언급했듯이 본래는 정적 멤버 변수를 클래스 내에서 초기화할 수 없지만, inner class 를 만들어 간접적으로 초기화하는 게 가능하긴 하다. inner 클래스를 정의하고, 해당 inner 클래스 내에서 static 변수를 초기화할 수 있다. (사실 이것보다 좀 더 복잡한 적용법이 있으나 일단 넘어가자..)

[정적 멤버 함수 static member function]
정적 멤버 변수에 접근하려면 정적 멤버 함수가 있어야 한다. 근데 위에서도 말했지만 애초에 클래스의 멤버 함수는 인스턴스마다 복사되어 만들어지는 것이 아니라 같은 메모리 주소를 공유한다. 멤버 함수를 실행할 때마다, 해당 인스턴스의 포인터를 가져와서, 그 인스턴스의 멤버변수를 이용해서 정해진 메모리 주소에 자리한 멤버 함수를 실행하는 형태인 것이다. 따라서 각 인스턴스마다 멤버함수가 따로 존재하는 것이 아니라서, 인스턴스의 멤버함수의 주소를 가져오려 하면 오류가 난다. 즉, 함수포인터를 만들 때, 반드시 해당 클래스에 직접 접근해서 주소를 가져와야 한다. (ex: int (Something::fptr1)() = &Something::m_function;)
이 함수포인터를 이용할 때는 또 반드시 인스턴스의 member selection operator 로 사용해야 한다. (ex: s1.
fptr1)() 처럼). 이 때, 해당 멤버 함수는 s1 이란 인스턴스의 포인터 주소를 가져와 쓰게 된다. 왜냐면 정적 멤버 함수가 아닌 이상, 모든 멤버 함수는 어떤 인스턴스에 종속되기 때문이다. 그 인스턴스의 내용물을 알아야만 작동할 수 있다.

그러나 정적 멤버 함수는 다르다. 인스턴스마다 달라지지 않는 메모리 주소를 가진 정적 멤버 변수를 다루는 함수이기 때문에, 자신도 인스턴스에 종속되지 않는다. 따라서 함수 포인터도 앞에 클래스명:: 없이 만들 수 있으며, 인스턴스 없이 독립적으로 실행하는 것도 가능하다. 같은 맥락에서, 정적 멤버 함수 내에서는 this 포인터를 쓸 수 없다. 당연하겠지?

[친구 함수와 클래스 friend]
어떤 클래스 A의 private 정의 내에 friend 접두사를 붙여서 클래스 외부의 함수의 프로토타입이나, class의 프로토타입을 선언해놓으면, 클래스 A의 private 멤버변수에 그 함수나 클래스가 접근할 수 있게 된다. 근데 순서에 따라 서로 간의 존재를 모를 수 있기 때문에 forward declaration 을 많이 활용하게 된다.

[익명 개체]
클래스 내의 멤버 함수를 사용하고 싶을 때, 인스턴스를 따로 만들지 않고 사용가능한 방법이 있다. 예를 들어 클래스 A 와 A 의 멤버함수 print() 가 존재한다면, A().print() 같이 인스턴스를 만들지 않고 바로 사용 가능하다.

[클래스 안에 포함된 자료형(Nested types)]

특정 클래스 내에서만 사용되는 데이터 타입이 있는 경우, 클래스 내의 public 에 사용할 수 있게 바로 사용 가능하다. 같은 맥락에서 클래스 내에 inner class, inner struct 등을 만들 수 있다.

[실행 시간 측정하기]

[산술 연산자 오버로딩]
사용자 정의 자료형(클래스) 끼리 덧셈, 뺄셈 과 같은 연산을 좀 더 편하게 할 수 있다.
함수를 정의하면서 함수명 대신에 operator + 같은 걸 쓰면 해당 자료형(사용자 정의) 에 대한 연산자를 오버로딩 한다. 이 함수를 아예 해당 클래스의 friend 로 등록하면 추가적인 getter 없이도 연산 결과를 반환받을 수 있다.

[입출력 연산자 오버로딩]

[단항 연산자 오버로딩]
단항 연산자 기억 안나겠지만 – (음수로 바꿔주는 연산자.), ! (not operator) 같은 게 있다.

[비교 연산자 오버로딩]
==, <, > 같은 비교하는 연산자도 인스턴스끼리 비교하는 데 쓰기 위해 오버로딩 할 수 있다.
< 과 > 덮어쓸 수 있는 건 < 다. 즉 왼쪽이 더 작냐를 확인하는 걸 덮어쓰고, 오름차순, 내림차순 여부는 반환값을 그것에 맞춰주면 된다.

[증감연산자 오버로딩]
전위냐 후위냐에 따라서 조금 다르다. 후위 증감 연산자는 매개변수 정의 안에 int 를 더미로 넣어줘야 하고, 아무것도 없으면 전위 증감 연산자 오버로딩이다.
ex_)
Digit operator ++ (int)
{
Digit temp(m_digit);
++m_digit; // 전위 증감 연산자를 위에 만들었었다면 ++(*this); 라고도 가능.
return temp;
}

[…첨자 연산자, 괄호연산자, 형변환, 복사 생성자, 복사초기화반환값 최적화, 변환 생성자, explicit, delete, 대입 연산자 오버로딩, 깊은 복사, 얕은 복사, 이니셜라이져 리스트 일단 생략.]

[객체들 사이의 관계에 대해]
관계를 표현하는 동사 관계의 형태 다른 클래스에도 속할 수 있는가? 멤버의 존재를 클래스가 관리? 방향성
구성(요소)
Composition Part-of 전체/부품 No Yes 단방향
집합
Aggregation Has-a 전체/부품 Yes No 단방향
연계, 제휴
Association Uses-a 용도 외엔 무관 Yes No 단방향 or 양방향
의존
Dependency Depends-on 용도 외엔 무관 Yes Yes 단방향
아래로 갈수록 의존도가 약함. (맨 마지막 의존은 이름만 엄청 의존적일 것 같을 뿐.)

[구성 관계]
좀 귀찮더라도, 클래스를 쪼개서 쓰는 습관을 들이자. 즉 기능을 분리해서, 상위 클래스는 뭘 해야될지만 알면 되고 어떻게 할 지는 몰라도 되는 상황을 만들어야 한다.

[집합 관계]

[상속의 기본(1) Inheritance (is-a relationship)]
부모 클래스를 generalized class, 자식 클래스를 derived class 라고도 한다.
class Child : public Mother
{
};  라고 하면 Mother 클래스의 것들을 다 가져다 쓸 수 있다.

자식클래스에서 부모 클래스에 있는 함수명을 오버로딩 하면 자식 클래스로 덮어씌워진다.
자식 클래스의 인스턴스가 만들어질 때, 부모 클래스의 생성자가 자동으로 호출된다. 이 특성 때문에, 자식 클래스에 default 생성자 외에 별도로 생성자를 명시적으로 정의했을 때, 만약 부모 클래스에 생성자가 없다면 문제가 생긴다. 자식 클래스의 생성자의 initialize list 안에, 부모 클래스를 호출하는 단계를 써주어 강제적으로 부모 클래스의 default 생성자를 실행시켜주거나, 명시적으로 부모 클래스에 생성자를 만들어 놓아야 한다. 다시 말해, 원래 자식 클래스의 default 생성자의 initialize list 안에는 Mother() 이 숨어 있다고 볼 수 있다.
ex) Mother 클래스, Child 클래스가 있고, i_in 은 Mother 의 멤버 변수에 들어갈 인자, d_in 은 m_d 라는 Child 클래스의 멤버 변수일 때 
public:
Child(const int &i_in, const double &d_in)
:Mother(i_in), m_d(d_in)
{
}

[상속의 기본(2)]

[유도된derived 클래스들의 생성 순서]

[상속과 접근 지정자]
상속 받을 때 부모 클래스 명 앞에 public, protected, private 를 붙이는 건 부모에게서 받아온 것을 자식 클래스에서 어떤 멤버처럼 다룰 것이냐에 관련됨. 즉, 사실상 Grand Child, 즉 자식의 자식 클래스부터 확실한 효력이 생긴다고 보면 된다. 예를 들어 private 로 상속 받았다면, 그렇게 받아온 부모 클래스의 멤버는 부모 클래스에서 public 이었다고 해도, 자식 클래스에서 private 로 인식되어, grand child 에서는 접근이 불가능해 진다.

[유도된(자식) 클래스에 새로운 기능 추가하기]
그냥 부모 클래스에서 protected 로 해 놓으면 자식 클래스에서 가져다가 쓸 수 있다.

[상속받은 함수를 오버라이딩 하기]
사실 굳이 부모클래스의 멤버함수와 같은 이름의 함수를 자식 클래스에 만들 이유는 없어보인다. 하지만 이후에 배울 ‘다형성’ 과 관련해 오버라이딩하면 훨씬 편할 경우가 있다. 그럼 어떻게 구분할 수 있을까? 그냥 같은 이름으로 호출해버리면 무한루프가 발생할 수 있기 때문에, 앞에 부모 클래스의 이름을 붙이고 :: 을 해서 구분해준다. ex) Base::print();
그리고 그 이후에 추가적으로 하고 싶은 내용을 작성해준다.

[상속 받은 함수를 감추기]
부모 클래스에 protected 라고 했었더라도, 자식 클래스에서 상속 받은 protected 영역의 변수를 public 으로 만들 수 있다.  자식 클래스의 public 영역에서 using Base::m_i; 같이 using 문을 쓰면 해당 변수가 public이 되어 외부에서 쓰일 수 있게 된다.
반대로 부모에게서 물려받은 public 변수든 함수를 외부에서 사용하지 못하게 하려면 자식 클래스의 private 영역에 using 문을 써주면 된다. ex) using Base::print; 자식 클래스에서, 내부적으로도 사용하지 못하게 하려면 아예 private 영역에서 delete 해주면 된다. ex) void print() = delete;

[다중 상속]
: 뒤에 , 으로 구분해서 여러 클래스로부터 상속 받을 수도 있다. 다만 자식 클래스의 생성자에서 그 부모 클래스들의 생성자를 적절히 잘 실행시켜줘야 한다.(시작 인자를 넣어주는 등)
그리고 두개의 부모 클래스에 겹치는 함수가 존재하는 경우에도 접근에 혼란이 생긴다. 즉 보다 명확하게 부모클래스명::함수명 을 써줄 필요가 있다.

[다형성의 기본 개념]
포인터의 개념을 적용.. 자식 클래스의 객체에 부모 클래스의 포인터를 사용한다면?
다형성과 관련된 개념이다.

자식 클래스의 주소를, 부모 클래스의 자료형의 포인터로 저장하면(부모 클래스의 포인터로 캐스팅해서 저장하면), 그 포인터는 자신이 부모클래스라고 생각한다. 예를 들어 Animal 이란 부모 클래스, Cat, Dog 라는 자식 클래스가 있을 때, cat, dog 라는 자식 인스턴스를 만들어 놓고 Animal *ptr_animal1 = &cat; 같이 포인터를 만들고, ptr_animal1 -> speak(); 같이 부모와 자식 공통된 함수를 실행시키면, 부모 클래스에 정의되어있는 speak 이 시행된다.
이 특성을 어디다 써먹나?
virtual 이라는 키워드를 함께 사용하면 유용하게 쓸 수 있다. 부모 클래스의 멤버 함수 정의의 맨 앞에 virtual 이라는 키워드를 붙여놓으면, 같은 부모 클래스를 상속 받은 모든 자식 함수에 맞게 그 함수가 그때그때 조정된다. 즉 부모클래스 포인터 array 에 Cat 과 Dog 의 다양한 인스턴스를 넣어놓고, (혹은 그 외 더 다양한 자식 클래스) for 문을 돌리면서 부모 클래스의 virtual 함수를 실행시키면, 각 자식 클래스의 특성에 맞게 오버로딩 된 함수를 실행시킬 수 있다.

포인터(*) 뿐 아니라 레퍼런스(&)로 캐스팅해도 똑같다. 결국에는 주소 넘겨주는거니까..

[가상함수virtual functions와 다형성 polymorphism]
A > B > C 의 순서로 상속받았다고 할 때, (C 가 A 의 손자) A 에만 virtual 이라고 해 놓으면 C 도 자기 자신의 함수를 쓴다. 즉, 최상위 클래스의 함수에만 virtual 해 놓으면 그 아래 클래스들은 전부 가상함수를 오버라이딩 한다.
오버라이딩 되는 함수들은 리턴 자료형이 같아야만 컴파일 된다. virtual 이 재밌긴 하나 성능상으로는 좋지 않다.

[override, final, 공변 반환값(covariant return type]
위에서 배운 개념을 적용하고자 하는 상황을 명확히 해주는 게 override 이다. 즉 자식클래스가 상속받은 함수를 덮어 씌워 쓰려고 하는 상황에서, 중괄호 이전에 override 라고 써놓으면, 함수명이 다르거나, 매개변수 자료형이 다르거나 하는 실수를 잡아준다. 의도적으로 쓰는 습관을 들이면 디버깅하기 좋다.

final 키워드는 자신을 상속받은 다른 클래스들이 함수를 오버라이딩 하는 것을 막아버린다. 즉 위에서 배운 개념을 사용하지 못하게 한다. 이것 역시 중괄호 전에 쓴다.

[가상 소멸자]
자식 클래스를 다형성 적용해서 만들면(포인터나 참조형), 나중에 delete 를 부모 포인터만 해주면 자식 클래스를 소멸시키지 않는 문제가 생긴다. 이 경우 메모리 누수가 생기기 때문에, 부모 클래스의 소멸자 앞에 virtual 붙여주면 부모를 상속받은 모든 자식 클래스의 소멸자가 자동으로 작동하게 된다. 굳굳.

[동적 바인딩과 정적 바인딩]
동적 포인터는 함수 포인터를 nullptr 로 정해놓고, 경우에 따라 함수 메모리 주소를 대입해서 함수를 호출하는 것. 속도는 정적 바인딩보다 느리지만 훨씬 유연하다. 게임엔진에서는 동적 엔진을 잘 쓴다. 위에서 배운 다형성은 이 동적 바인딩이 내부적으로 가상 함수표를 이용해 설계된 상태다. 부모의 포인터나 참조형을 캐스팅하여 사용되고 있는 상황일 때, virtual 이 붙어 있는 멤버함수는 그냥 자기껄 쓰고, 붙어있지 않으면 부모의 가상 함수표에 있는 함수를 가져다 쓰는거다.

[순수 가상 함수, 추상 기본 클래스, 인터페이스 클래스]
순수(pure) 가상 함수는 바디가 없는 함수다. 즉 virtual void speak() const = 0; 같이 바디를 작성하지 않는다. 이 순수 가상 함수의 목적은 “자식 함수에서 꼭 이 함수를 오버라이딩 해라” 라는 뜻이다. 예를 들어 Animal 이라는 부모 클래스가 있을 때, Animal 이라는 건 너무 추상적인 개념이라 울음소리를 정하는 speak() 이란 함수를 두는 게 이상하지만, Animal 을 상속 받는 다양한 동물 클래스들 각각엔 꼭 울음소리를 넣어야 된다는 걸 강조하고 싶다면, 부모 클래스에 순수 가상 함수를 둔다. 그럼 자식 클래스에서 이 함수를 오버라이딩 하지 않으면 클래스 정의가 불가능하다고 컴파일러가 알려준다.
인터페이스 클래스는 모든 멤버 함수가 순수 가상함수 일 때 인터페이스 클래스라고 부른다. 즉, 인터페이스 클래스는 무조건 부모 클래스일 것이며, 이 인터페이스 클래스 인스턴스를 함수의 매개변수로 받도록 해놓은 뒤, 함수를 호출할 때 적절한 자식 클래스의 인스턴스를 인자로 집어넣으면, 자식 클래스에 맞는 순수 가상 함수를 실행시켜 준다. 즉 인터페이스 클래스는 자기 혼자서 할 수 있는 건 아무것도 없는(인스턴스를 만들 수 없는) 클래스이지만, 자식 클래스가 유연하게 들어갈 수 있는 통로로써 활용된다.

[가상 기본 클래스와 다이아몬드 상속 문제]
가상 기본 클래스를 이용해서 다이아몬드 상속 문제를 해결할 수 있대. 제대로 이해 못함..

[객체 잘림과 reference wrapper]
주로 실수겠지만, 부모 클래스의 인스턴스에 자식 클래스의 인스턴스를 복사 대입하면, 데이터가 잘리는 현상이 발생할 것이다. 주로 자식이 부모보다 정보를 많이 가지고 있기 때문이다. 특히 다형성도 깨져버린다. 당연한거다. 부모 클래스는 자식 클래스의 멤버 변수나 함수를 알 수가 없으니까..
주로 vector 을 사용할 때 이 실수가 발생하게 되는데, 다형성과 벡터를 함께 써보겠답시고
Base b;
Derived d;

std::vector my_vec;
my_vec.push_back(b);
my_vec.push_back(d);

라고 작성한 뒤 출력해보면,
for(auto & ele : my_vec) { ele.print(); }

개객체 잘림 현상이 발생한다. 즉, 제대로 하려면 vector<Base*> 처럼 포인터로 넣어줘야 한다. (물론 인스턴스도 & 로 주소로 변환해서 push_back 해주고 출력도 ele->print() 해야겠지?) vector 는 참조형을 받을 수 없기 때문에 포인터밖에 방법이 없다.
그런데 굳이 참조형을 vector 에 써주고 싶다면 을 #include 해서, vector<std::reference_wrapper> my_vec; 처럼 써주면 참조형 벡터를 만들 수 있다. 이 경우 출력 할 때 ele.get().print() 처럼 get() 으로 참조 를 가져와 줘야 한다.

[동적 형변환]
다형성을 이용하기 위해 자식 클래스의 인스턴스 주소를 부모 클래스의 포인터나 참조형으로 캐스팅하는 경우가 많은데, 부득이하게 그 부모 클래스의 포인터나 참조형을 다시 자식 클래스의 포인터로 바꾸어 자식 클래스의 멤버변수나 함수에 접근할 때 dynamic_cast 를 사용한다.
Base base = &d1;
auto *base_to_d1 = dynamic_cast<Derived1
>(base);
cout << base_to_d1->m_j << endl;

근데 가급적 쓰지마. 특히 자식 클래스가 여러 갠데 dynamic_cast 로 본래 자식 클래스가 아닌 다른 자식 클래스로 변환시켜 버리면 null 로 변환시켜버리는 등 디버깅이 엄청 힘들어진다..

[유도 클래스에서 출력 연산자 사용하기]
본래 출력 연산자는 멤버 함수가 아니기에 오버라이딩 할 수 없지만, 출력 연산자에 오버라이딩 할 수 있는 멤버함수를 virtual 로 만들어서 넣어 쓰면 간접적으로 출력 연산자를 오버라이딩 한 것 처럼 쓸 수 있다.

[함수 템플릿]
주로 자료형에 관련된 반복 작업을 줄여줌
같은 기능을 하는 함수가 자료형만 다른 경우, 또 값으로 받았던 걸 또 참조로 변경하는 등 별 거 아니지만 노가다가 필요한 경우에 템플릿을 쓴다.
주로 자료형이 들어갈 자리에 T 를 써놓으면 컴파일러가 알아서 매칭시켜 준다.
// typename 대신 class 를 쓸 수도 있다. 용어적으로도 T 에 들어가는 자료형에 따라 int 면 int 에 대한 인스턴스 라고 부르고 그 인스턴스를 만드는 과정을 instantiation 이라고 부르는 등, 개념이 좀 혼용되어 있다.

template T getMax(T x, T y)
{
return (x > y) ? x: y;
}

[클래스 템플릿]
클래스 위에도 template 를 붙여 템플릿화 할 수 있다. 이 경우 T 를 적용받아야 할 멤버변수와 그 멤버변수를 반환하는 멤버함수 모두 T 로 잘 바꿔줘야 한다.

그리고 이 템플릿 클래스의 인스턴스를 만들 때는, 클래스명 에서 T 의 위치에 어떤 타입으로 인스턴스를 만들 것인지 명시해주어야 한다. 뭘로 만들지는 알려줘야 하니까.

그리고 이 템플릿 클래스 멤버함수의 바디 부분을 다른 cpp 파일로 옮겨주고 싶을 때는, 이전에 일반 멤버함수를 옮길 때보다 뭐가 많이 붙는다. 맨 위에 template 도 써줘야 하고, 프로토타입 부분도 void MyArray::fun() 같은 기본형이 아닌 void MyArray::func() 같이 를 유연하게 받을 것임을 명시해줘야 한다. 뿐만 아니라 header 파일이 아닌 별도의 cpp 파일로 옮기고 일반적으로 하는 것처럼 #include “원래있던헤더파일.h” 만 해주고 끝내면 linking error 가 발생한다. 왜냐면 main 함수는 헤더파일만을 include 하고 있기 때문에 따로 빼놓은 cpp 파일에 있는 멤버함수의 T 를 결정짓지 못하기 때문이다. 그렇다고 main 함수가 있는 파일에 cpp 파일도 #include 해줘버리면 문제가 해결은 되지만 프로젝트 규모가 커질수록 겉잡을 수 없는 문제가 발생할 수 있다. 따라서 따로 멤버함수를 빼놓은 cpp 파일에서 explicit 하게 어떤 타입으로 instantiation 하고 싶은지 명시해주어야 한다.
// explicit instantiation
template void MyArray::print();
template void MyArray::print();
// print() 외에도 더 많은 멤버함수를 옮겨올 것을 대비해서 클래스 전체를 explicit instantiation 할수도 있다.
template class MyArray;
template class MyArray;

사실 이 클래스 템플릿뿐 아니라 앞에서 배운 함수 템플릿도 그 함수를 호출할 때 이 라는 어떤 자료형으로 instantiation 할 지 말해주는 게 숨겨져 있다. 다만 함수를 호출할 때 인자를 넣기 때문에 그 인자를 보고 자동으로 결정해줄 뿐이다. 함수 템플릿을 호출할 때도 explicit 하게 를 써주면 원하는 자료형으로 호출할 수 있다. ex) getMax(1,2); 이라고 하면 자동으로 했을 때는 int 로 했을 것을 double 로 호출할 수 있다.

[함수, 클래스 템플릿 특수화]
템플릿을 써서 T 로 뭉뚱거려 놓았는데, 특정 자료형일 때는 좀 달리 처리하고 싶을 때 함수 템플릿 특수화를 쓸 수 있다. 즉, int 든 double 이든 float 이든 다 똑같이 처리하지만 char 인 경우에는 경고하고 싶다. 같은 경우에 쓴다.
아래처럼 본래 함수 템플릿 아래에 빈 <> 를 가진 같은 이름의 함수 템플릿을 추가로 작성한다.
template<>
char getMax(char x, char y) { //… }

클래스 템플릿 특수화도 크게 다르지 않다. 본래 클래스 형태를 생각하며 만 적절한 자리들에 추가하면된다.
template<>
class A
{
public:
void doSomething() {}
}
다만 클래스 템플릿 특수화는 사실상 새로운 클래스를 작성하는 것과 유사하다. 즉, 특정 타입에 대한 멤버함수를 모두 구현해놓지 않으면, 그 멤버 함수를 해당 자료형 인스턴스가 쓸 수 없다. 차라리 멤버함수만을 따로 특수화 하는 게 나을 수 있는 것이다.

(제대로 이해 못했지만) bool 타입의 자료형에 대한 특수화를 잘 쓰면, 8 개의 bool 을 처리하는 데 1 바이트만 쓰도록 작성가능하다.

[템플릿을 부분적으로 특수화하기]
(제대로 안 들음)
위에서 언급한, 클래스 템플릿 특수화에서 사실상 새로운 클래스를 모두 작성하지 않으면 모든 멤버함수를 쓸 수 없다는 점을 해결하기 위해 ‘상속’ 을 활용하기도 한다. 즉, 특수화하고자 하는 클래스를 상속받는 자식 클래스를 만들고, 그 자식 클래스를 특수화하면, 멤버함수도 모두 물려받고 특수화도 할 수 있다는 것. 다만 자식 클래스 인스턴스를 만들어서 사용해야 겠지?

[포인터에 대한 템플릿 특수화]
포인터에 대한 특수화를 하고 싶을 때 쓴다. class A<T*> 같이 써야 겠지?

[멤버함수를 한 번 더 추가적으로 템플릿화 하기]
멤버함수의 위에 또다시 템플릿화 하는 코드를 작성해서 재 템플릿화 할 수 있다. 본래 클래스 템플릿에서 쓰고 있던 와 다른 template 같이 써주면 되겠지? 이 멤버함수를 호출할 때는 당연히 어떤 타입으로 호출할 것인지 명시해주어야 한다.
ex) class A 의 멤버함수 doSomething 을 또다시 템플릿화했을 경우 
a.doSomething();
다만 해당 멤버함수가 매개변수를 가지고 있고, 그 매개변수를 위한 인자를 집어넣으면서 해당 인자의 형태를 컴파일러가 추정할 수 있는 경우에는 <> 를 굳이 붙여넣지 않고 호출할 수 있다. 전에 배웠던 함수의 템플릿화에서 굳이 를 명시하지 않아도 자동으로 배정해줬던 이유와 같음.

[예외처리의 기본]
어떤 문제가 생길 경우에 if 문이나 flag 등을 활용해서 문제점을 해결하는 것이 옛날부터 사용되어져 왔지만, 예외처리라는 특수 문법을 사용할 때도 있다. 이 예외처리를 쓰면 성능상으로는 좀 느리기 때문에, 상황에 맞게 사용여부를 결정해야 한다.
try, catch, throw
try 문에서 시도하고, 문제가 생기면 if 문 등으로 인식한 뒤 throw 로 에러메시지를 던지고, 그 에러 메시지를 catch 문에서 잡아서 출력을 해준다던가 한다. try catch 문은 일반 함수보다 엄격하기 때문에, 자동으로 형변환 해주지 않는다. 즉 catch 문의 자료형도 잘 받아줘야 한다. int, double, float 등 throw 형태가 다양하다면 각 자료형에 대한 catch 문을 모두 만들어 줘야 한다는 것이다.

[예외처리와 스택되감기(unwinding)]
함수 내에서 함수를 호출하고, 또 거기서 다른 함수를 호출하고.. 하는 상황에서 안 쪽 함수가 에러 메시지를 throw 하면 그것을 어디서 받아줄 지 함수 호출 스택을 거슬러 오면서 찾는다. 위에서 말한 것처럼, throw 된 에러 메시지(메시지라고 무조건 char* 이나 string 인 것은 아니다) 의 자료형과 일치하는 catch 문을 찾아 스택을 거슬러 올라가는 것이다. 만약에 어디서도 잡아줄 수 없으면, 아예 runtime error 를 던져준다. 구현되지 않은 경우의 수니까.
그런데 모든 자료형의 경우의 수를 고려해주기 어려울 수 있다. 그런 경우를 대비해서 ellipses 를 사용한다. catch(…) { } 를 작성해 놓으면, 이전 함수 호출 스택에서 잡지 못한 throw 메시지를 모두 걸러준다.
exception specifier 라는, throw(int) 같은 구문을 함수 프로토타입 옆에 작성해 놓으면 “int 형의 throw 를 던질 가능성이 있습니다.” 를 알리는 문법이 있지만, 거의 안 쓰인다고 한다.

[예외 클래스와 상속]
예외를 관리하는 클래스를 만들 수 있고, 예외를 관리하는 클래스를 상속받는 보다 세밀한 에러를 잡는 자식 클래스도 만들 수 있다. 이 경우 이 예외 클래스의 인스턴스를 에러 상황에서 throw 해주면 된다. 자식 클래스 내에서 부모 클래스가 먼저 시행 되기 때문에, 우선 순위를 자식 클래스로 두기 위해서는 자식 클래스의 catch 를 앞쪽에 배치해야 한다. 즉 상속 관계를 이용해서, 여러 디테일한 자식 클래스 exception 과, 그 모두를 마지막에 잡아줄 수 있는 부모 클래스의 구조를 만들 수 있다. 그리고 re-throw 라는 것도 가능한데, catch 한 부분에서 다시 throw 할 수 있는 것이다. 이 때 throw e; 같이 하지 않고 그냥 throw; 라고만 하는 게 부모 자식 간의 관계에 의한 객체 잘림 현상 없이 re-throw 가 될 수 있다.

[std::exception 소개]
이미 구현되어 있는 예외 처리 표준 라이브러리.
#include
catch(std::exception& exception) { cout << exception.what() << endl; } 따위로 사용.

[함수 try]
자식 클래스 생성자에서 부모 클래스의 생성자를 호출하는 시점에 바로 try 를 쓰는 경우가 있다. 특이한 점은, 이 경우 re-throw 를 한 것처럼 이 자식 클래스 생성자에서도 catch 를 한 번하고, 밖에서도(주로 main함수) 에서도 또 catch 를 한다.
ex)
B(int x) try : A(x) { } catch (…) { cout << “Catch in B constructor” << endl; //throw }

[예외처리의 위험성과 단점]
예를 들어 동적 할당을 한 상황에서 throw 로 인해 현재 블록을 이탈해버리면 delete 가 제대로 실행되지 못해 메모리 누수가 발생할 수 있다. (요즘엔 스마트 포인터를 이용해서 이런 문제도 예방할 수 있긴 하다. 채.신. 기술을 쓰자.)
destructor 에서도 예외처리를 쓰면 안 된다.

[의미론적 이동과 스마트 포인터]
동적할당을 썼다면, 모든 경우의 수(예외처리, early반환, 실수로까먹)에 대해 어떻게든 delete 를 쓰라고 가르쳤던 과거와 달리 요즘은 스마트 포인터를 사용하라고 한다.
스마트 포인터 이전에 auto_ptr 이라는 포인터 역할을 하는 클래스를 자체적으로 만들거나 std:: 에 있는 걸 주로 썼었는데 c++11 부터 쓰지 않기로 했으므로 개념만 알고 스마트 포인터를 배우자.
왜 auto_ptr 을 안 쓰게 됐냐면, 2개의 포인터가 같은 주소를 가리키고 있을 때, 이미 한 쪽이 destructor 을 통해 지운 걸 다른 애도 지우려고 하는 과정에서 에러가 생기는 문제를 제대로 해결하지 못했기 때문이다.
물론 스마트 포인터가 등장하기 전에는 이 두개의 auto 포인터가 같은 주소를 가리키는 문제를 해결하기 위해서 좀 더 복잡하게 직접 ‘move’ 의 개념을 구현해서 썼지만, 세상이 발전했으니 그냥 스마트 포인터 써라

semantics  syntax 는 그냥 문법적으로 맞냐 아니냐만 따진다면 semantics 는 문법이야 들어맞는다 치고, 그래서 그 “의미” 가 무엇인지 묻는 것. 예를 들어 숫자의 더하기와 문자열끼리의 더하기는 둘 다 문법적으로는 문제가 없지만 의미는 좀 다르다. 후자는 사칙연산이 아니라 append 개념이니까.

c++ 에서 ‘=’ 는 3가지 역할이 있다고 보면 된다.

  1. value semantics (==copy semantics)  파이썬에서도 쓰는 일반 값 저장 역할
  2. reference semantics (pointer)  주소를 전달하는 포인터개념
  3. move semantics (move)  지금 처음으로 배운 ‘이동’ 의 개념. 즉 주소의 소유권을 넘겨주는 것.

[오른쪽-값 참조 R-value reference]
L-value, R-value 를 단순히 왼쪽 오른쪽이라고 생각하는 것 벗어나자. 한 번 쓰이고 사라지지 않고 자기만의 메모리 주소를 가지고 여러 번 쓰일 수 있는 걸 L-value, 한 번 어디 대입되거나 출력되고 사라지는 걸 R-value 라고 하자.

그래서 참조라고 하면, 당연히 L-value reference 였다. 애초에 참조라는 게 주소값을 가지고 작업을 하는 것인데, 자기만의 주소 없이 곧 없어질 값을 참조로 받는다는 게 이치에 맞지 않기 때문이다. 다만 const& 를 쓰면 R-value 도 참조로 받는 게 가능하기는 했었다.

r-value reference 는 지금까지 배운 것과 좀 다르다. 일단 && 을 두개를 붙인다. (and 와 상관 없다.) 얘는 l-value reference 와 반대로, 오직 r-value 만 참조로 받을 수 있다.
ex)
int&& rr3 = 5;
int&& rrr = getResult();

즉 임시로만 존재하는 애들을 보관해 놓는다는 건데, 신기한 건 이렇게 저장한 r-value 의 값을 변화시켜 저장하는 것도 가능하다. rr3 = 10; 즉, r-value reference 는 마치 “나만 접근할 수 있어” 같은 개념이다. move semantics 와 깊이 연관되어 쓰인다.

컴파일러가 l-value reference 와 r-value reference 를 구분할 수 있기 때문에, 클래스 내에서 별개의 함수로 구분해준다.

&& 를 그러면 어디다 쓰냐? deep copy, shallow copy 의 개념과 연관된 것으로, “어차피 없어질 놈” 이라는 걸 활용해서 deep copy 할 걸 shallow copy 하게 만들면 엄청난 성능 향상이 있다. 예를 들어 어떤 클래스의 인스턴스를 큰 array 형태로 인자로 받아 copy 하는 작업이 있다고 했을 때, & 이 아닌 && 을 쓰면 훠얼씬 빠르다. 마치 이삿짐을 일일이 복사해서 옮겨야 하는 deep copy 와 달리 shallow copy 는 그냥 집 키를 넘겨주는 듯한 과정만 거치기에 훨씬 빠르다. r-value 를 이용함으로써 컴파일러가 인식하기에도, Copy sementics 가 아닌 move sementics 라고 별도로 인식하는 것이다..즉 원래 주소를 갖고 있던 애는 nullptr 이 되고 넘겨 받은 애가 그 주소를 갖게 된다. (원래부터 주소가 없는 r-value 였으면 상관없지만 바로 뒤에 나오는 std::move() 로 바꿔준 걸 넣으면 기존에 그 주소를 갖고 있던 애가 nullptr 이 된다.)

std::move( ) 로 l-value 를 감싸주면 그 l-value 로 인스턴트를 만들 때 copy constructor 가 아닌 move constructor 를 호출해준다. (쉽게 생각하면 마치 r-value 로 바꿔준다고 생각하면 됨)

[std::unique_ptr]  이 이후 강의 3개 제대로 이해 못함..

[표준템플릿 라이브러리, 컨테이너]
<입력과 출력>
[istream 으로 입력받기]

std thread 와 멀티스레딩 기초

프로세스는 운영체제가 우리가 사용하는 프로그램을 관리하는 단위

하나의 프로세스가 여러 개의 스레드를 관리할 수 있다.

멀티 스레딩이란 우리가 여러 개의 스레드를 만들어 쓰는 프로그램을 짜서, 여러개의 코어를 활용하는 것.

옛날에는 cpu 하나에 코어가 하나였기 때문에, 속도를 올리기 위해 여러 개의 cpu 를 꽂아서 멀티 프로세싱을 하기도 했지만 큰 성능 향상이 없었다. 그래도 어떻게든 활용하겠다고 네트워크를 통해 코어들을 연결해 사용하던 개념이 분산처리. 메모리를 공유할 수 없기 때문에 비효율적인 면이 많다.

하나의 cpu 에 여러 코어가 있게 되면서, 멀티스레딩을 하게 되었고, 성능 향상이 뛰어나다. 서로 메모리를 공유하기 때문에 할 수 있는 게 많지만 그만큼 위험하기도 하다.

Main Thread 가 Thread 여러 개에 업무를 나눠준다. Main Thread 는 각 Thread 가 언제 일이 끝나는 지 알 수 없지만, 다 끝날 때까지 기다렸다가, 모든 Thread 가 일을 끝내면 일을 마무리한다.

<입력과 출력>
[정규 표현식 소개]
#include // C++11 부터 도입됨.

#include
#include
#include
#include

using namespace std;

int main()
{
regex re("(^[0-9]{2})[0-9]+ - ([0-9])");
smatch matches;
string str;

while (true)
{
getline(cin, str);
if (regex_search(str, matches, re))
{
cout << "Match" << endl;
for (size_t i = 0; i < matches.size(); i++)
{
cout << i << " " << typeid(matches[i]).name() << "\n";
cout << i << " " << typeid(matches[i].str()).name() << "\n";
}
}
else
{
cout << "No match" << endl;
}
}

return 0;
}

CHAPTER 10

객체들 사이의 관계에 대해

10.1 객체들의 관계

어떤 기능을 가진 객체들이 어떻게 서로를 도울 것인지 설계

아래로 갈수록 더 느슨한 관계

관계를 표현하는 동사 예시
구성(요소) Composition Part-of 두뇌는 육체의 일부이다.
집합 Aggregation Has-a 어떤 사람이 자동차를 가지고 있다.
연계, 제휴 Association Uses-a 환자는 의사의 치료를 받는다. 의사는 환자로부터 치료비를 받는다.
의존 Dependency Depends-on 나는 (다리가 부러져서) 목발을 짚었다.

CHAPTER 20 중급 프로그래머들의 상식

20.1 비쥬얼 스튜디오로 프로파일링 하기

Microsoft 의 Profiling in Visual Studio 강의를 기초로 함.

프로그램을 작성하고 나서, 어떤 부분이 연산 속도 및 메모리 관리에 어려움을 주고 있는지 가려내는 문제 발견 및 해결 과정(최적화)을 프로파일링이라고 한다.

CPU 프로파일링 기본 방식

디버그 모드로 실행시켰을 때 뜨는 Diagnostic Tools(진단도구)Record CPU Profile 탭을 이용한다.

  1. Breakpoint 를 진단을 원하는 앞과 뒤 각각에 찍고 디버그 모드 실행
  2. 진단도구의 Record CPU Profile 버튼 누르고 Continue 로 프로그램 진행
  3. CPU Usage 탭을 보면 각 함수의 전체 중 차지 비율을 알 수 있다.
  4. main() 함수 이전에 실행되는 숨겨진 함수가 몇 개 있음도 알 수 있다.
  5. 복잡한 프로그램일수록, 맨위의 프로젝트명.exe 리포트 파일을 클릭해보면 더 쉽게 프로파일링 가능하다.
  6. 리포트 페이지에서 오른쪽 위 빨간 영역을 클릭해나가며 문제가 될 부분을 찾는다.

메모리 프로파일링 기본 방식

진단도구 내 Memory Usage 탭의 Take Snapshot 버튼을 눌러 미리 찍어둔 Breakpoint 마다의 메모리 사용량 증감 스냅샷을 찍어볼 수 있다.

Release 모드에서의 프로파일링

실제 성능 프로파일링을 위해 Release 모드로 바꾸어야만 이용가능한 프로파일링 도구가 있다. 디버그 메뉴의 Performance Profiler 를 이용한다.

20.2 깃, 깃헙 사용하기 Git, Github

기본 상식

  1. Git 은 기본적으로 여러 사람이 이용하기 위해 만든 것이고, 까먹을 스스로를 위해서라도 변경사항이 있을 때마다 Commit 메시지를 남겨야 한다.
  2. Settings 탭의 Collaborators 항목에 이 repository 를 같이 관리할 사람을 초대할 수 있다.

Git 폴더

  1. git config --global user.name "내깃헙아이디"
  2. git con

29.4 Vcpkg 설치 방법

파이썬의 pip install 처럼, c++ 을 위한 외부 라이브러리 설치 관리 도구라고 생각하면 된다. 개방적으로 변화해가고 있는 Microsoft 의 모습을 보여주는 사례 중 하나. 빠르게 안정화되고 있기 때문에 충분히 실무에 활용할 수 있다.

설치방법

  1. Vcpkg github Repository 를 찾아 적당한 폴더에 git clone 한다.
  2. cd vcpkg 로 clone 된 폴더에 들어간다.
  3. bootstrap-vcpkg.bat 명령어로 초기세팅을 해준다.
  4. 이제 vcpkg 로 외부 패키지를 설치할 수 있다.

외부 패키지 다운로드, 사용

Boost 라이브러리를 설치해보자.

  1. vcpkg.exe install boost:x64-windows 로 설치한다. 여기서 :x64-windows 를 붙이는 이유는 설치 기본 세팅이 32비트이기 때문이다. 물론 32비트를 써도 되지만 실무에서는 64비트를 쓰겠지? 물론 64비트로 설치했다면 프로젝트 설정도 64비트로만 해야 한다.
  2. TIP: 설치가 잘 안 되는 경우 커맨드 창을 관리자 권한으로 실행시켜 다시 해보면 잘 된다.
  3. vcpkg integrate install 명령어를 치면 vcpkg 로 설치한 패키지를 Visual Studio 에서 #include 해서 쓸 수 있게 된다. 기존 방식대로라면 Visual Studio 프로젝트 세팅의 Additional include Directories 에 헤더 파일의 위치를 적고, Linker 설정에서도 Additional Library Directories 를 잡아주는 등 해야할 일이 겁나 많았다...
  4. 어떤 패키지가 있는지 찾기 위해 vcpkg search eigen3 같이 vcpkg search 를 이용해서 패키지 존재 유무를 알아볼 수 있다.
  5. vcpkg list 를 이용해 설치된 라이브러리의 리스트를 받아볼 수 있다.

20.5 TCP IP 네트워킹 맛보기 - Boost.Asio Socket IOStream

준표준 라이브러리인 Boost 를 이용해서 네트워크 서버를 만들고 이용할 수 있다. 이전 강의에서 설치한 Boost 를 이용한다.

1) 특징

  1. Boost 는 거대한 라이브러리이기 때문에 설치에 오랜 시간이 걸릴 수 있다.
  2. 공식 문서가 있기는 한데, 숙련도가 높은 분들 기준에 맞춰져 있어 이해하기 어려울 수 있다. 그냥 필요한 기능이라도 잘 쓸 수 있도록 노력하며 실력을 높여가자.

20.6 외부 라이브러리 설치, 프로젝트 템플릿

nanogui 라는 유명한 gui 라이브러리를 설치하는 과정을 통해 vcpkg 같은 자동화 도구 없이 외부 라이브러리를 설치해보자. nanogui 를 이용하는 이유는 설치하기에 가장 복잡한 편에 속하기 때문이다.

1) Nanogui 설치 방법

  1. nanogui 깃헙 저장소를 검색해 들어간다.
  2. git clone --recursive https://github.com/wjakob/nanogui.git 코드를 이용해서 클론한다. 여기서 --recursive 란 해당 라이브러리가 이용하고 있는 다른 외부 라이브러리까지 모두 클론하겠다는 걸 뜻한다.
  3. CMake 를 설치한다. .zip 파일이 아닌 .msi 같은 실행 파일을 다운 받아야 한다. (다른 방법은 모르니까..) CMake 는 좀 더 좋은 성능의 빌드 툴 같이 생각하면 되나보다.
  4. 설치된 CMake 를 실행시키고, Where is the source code: 칸에 Nanogui 가 설치된 폴더 경로를 입력해준다. 이 때 Nanogui 폴더에 CMakeLists.txt 파일이 있는지 확인하자. CMake 는 이 파일을 기준으로 빌드한다고 한다.
  5. Where to build the binaries 칸에는 위에 쓴 경로에 /build 만 추가해서 작성하는 게 보통이라고 한다.
  6. Configure 을 누른 후 Generate 를 누른다.
  7. 그 후 Open Project 버튼을 누르거나 설정해둔 /build 경로의 .sln 솔루션 파일을 실행하면 외부 라이브러리 내용을 볼 수 있다.
  8. Debug 와 Release 둘 다 빌드한다.
  9. 이제 nanogui 의 /build 경로에 Visual Studio 로 빌드한 Debug 폴더와 Release 폴더가 생긴 것을 볼 수 있다.
  10. 각 폴더에 들어가 있는 .dll, .lib 파일이 제일 중요하다고 볼 수 있다.

2) 다른 프로젝트에서 nanogui 사용하기

TRDR;

걍 vcpkg 를 최대한 쓰자...

새로운 프로젝트에서 nanogui 를 사용하려면 일단 프로젝트의 Properties 설정부터 맞춰줘야 한다. 새로운 프로젝트를 만들고 nanogui 예시들에서 설정을 가져오자.

  1. Visual Studio 에서 nanogui 프로젝트에 오른쪽 클릭해 Properties 에 들어간다.
  2. 총 4가지를 체크해야 한다.
    1. C/C++ 탭의 General 의 Additional Directories 내용을 베껴온다.
    2. Linker 탭의 Input 의 Additional Dependencies 내용을 베껴온다. 사이사이의 세미콜론을 빼먹지 않도록 주의하자. 맨 앞에 써져 있는 건 폴더명이다. 이 외부 폴더 경로를 Linker > General > Additional Library Directories 에 작성할 수도 있다.
    3. 어떤 경우에는 C/C++ 탭의 Preprocessor 의 Preprocessor Definitions 내용이 다를 수 있다. 이 부분도 체크하자.
    4. C/C++ 탭의 Code Generation 의 Runtime Library 가 일치하는지 확인해야 한다.
  3. 이제 .dll 파일을 옮겨줘야 한다. nanogui/build/Release 폴더 내의 .dll 파일을 복사해 사용하고자 하는 프로젝트 폴더에 옮긴다.
  4. 주의점! nanogui 처럼Debug 모드의 .dll 과 이름이 같은 경우 단순히 새로운 프로젝트 폴더 내에 .dll 을 옮겨놓으면 디버그할 때 문제가 생길 수 있다. 따라서 새로운 프로젝트의 x64/Release 폴더(실행파일이 있는 폴더)로 .dll 파일을 옮겨 어디 쓰일지 명확히 하는 게 좋다. 사실 vcpkg 를 쓰면 알아서 복사해 준다...
  5. 이제 이 모든 세팅을 재반복하고 싶지 않다면, Visual Studio 의 맨 위 Project 메뉴의 export template 기능을 통해 템플릿을 만들 수 있다. 새로운 프로젝트를 만들 때 이 템플릿을 이용해 지금껏 한 모든 세팅과 소스파일을 복사해 쓸 수 있다. 물론 이 경우에도 .dll 파일은 수고롭지만 옮겨줘야 한다.