C++11,14,17 기본 문법 훑어보기 (1)

학부 때 C++을 배웠고 이후 언어 자체에 많은 변화가 있었다. 요즘에는 어떻게 구현을 하는지 공부를 하면서 C++에서 사용하는 문법을 정리 해보았다! 자세한 내용은 Professional C++를 참고하길 바란다.

Hello, world!

역시 프로그래밍을 하면서 가장 첫 코드는 “Hello, World!”를 출력하는 것부터 시작해야지 않겠는가. 거의 모든 책에서 이 코드로부터 시작한다.

// helloworld.cpp
#include <iostream>

int main()
{
    std::cout << "Hello, World!" << std::endl;
    return 0;
}

이 코드 안에는 다양한 개념들이 포함되어있는데 하나씩 설명을 해보면 다음과 같다.

– Comments

C++에서는 다음과 같이 주석을 달 수 있다.

// 한 줄로 주석을 달 때
/* 여러 줄로 주석을 
 달 때에는 이렇게 */

보통 IDE에서 Ctrl+/(윈도우), Command+/을 누르면 자동으로 해당 줄에서 주석이 달리게 된다.

– Preprocessor directives

C++은 파이썬과 다르게 다음과 같이 코드를 수행하기 위한 과정을 거친다

Preprocessor 실행 -> 컴파일 -> linking

Preprocessor는 코드의 메타 정보로 코드에서 특정 값들을 특정 텍스트로 변환한다든지, 다른 코드를 포함시키는 등의 동작을 한다. 컴파일은 코드를 기계어로 변환하는 작업이고 이 과정 중에 object file이라는 게 생성된다. Linking은 여러 objective file들을 하나의 application으로 만든다.

#include <iostream>은 iostream이라는 기본으로 제공해주는 헤더파일을 현재 파일에 넣어주는 역할을 한다. C++에는 헤더파일 (.h)과 코드(.cpp)파일로 나뉘는데 각각 일반적으로 쓰는 내용이 다르다

  • function declaration
    • 일반적으로 .h 파일에 존재
    • 함수 호출방법이나 파라미터 개수 및 타입, 반환 타입들을 선언한다
    • 실제 함수가 어떻게 돌아가는지에 대한 코드는 적지 않는다.
  • definition
    • 일반적으로 .cpp 파일에 정의
    • function declaration에서 선언한 함수 형태에 따라 각 함수별로 실제 코드를 적음

C에서 사용하던 헤더파일들은 #include <stdio.h>와 같이 사용했는데 C++에서 사용하는 헤더파일들은 .h를 생략한다. <iostream>와 같이. 나중에 나오겠지만 모든 함수나 클래스들은 std 라는 네임스페이스 안에 정의되어 있다.

C++에는 C에서 사용하던 헤더파일을 사용할 수 있다. #include <stdio.h>와 같이 include할 수 있지만 추천하지 않는다. C에서 사용하던 헤더파일을 사용하려면 앞에 c를 붙이고 .h는 제거한 #include <cstdio>와 같이 사용하는 걸 추천함. 모두 std 라는 namespace안에 정의되어 있다

많이 사용하는 preprocessor directive는 다음과 같다

  • #include [file]
    지정된 파일을 코드에 추가한다
  • #define [key] [value]
    코드에서 key가 등장하면 value로 대체할 때 사용한다. 보통 상수들을 정의할 때 C에서 사용하던 방식인데, C++에는 더 나은 방식이 존재하므로 사용하는건 비추천한다.
  • #ifdef [key] 혹은 #ifndef [key]
    #endif
    키가 #define으로 정의되어있는지 확인하고 정의되어있거나(#ifdef), 정의되어있지 않으면 (#ifndef), #endif 까지의 코드를 수행하게 한다. 보통 circular include 문제를 피하려고 이걸 사용한다.
  • #pragma [xyz]
    여기에서 xyz는 컴파일러마다 들어가는게 다르다

코딩을 하다보면 중복해서 동일 헤더파일을 #include하며 에러를 발생시킬 수 있는데 다음과 같이 회피할 수 있다. 헤더파일을 정의할 때 다음과 같이 정의하면 된다.

#ifndef MYHEADER_H
#define MYHEADER_H
// 헤더 파일 내용이 여기 들어간다.
#endif

요즘의 대부분의 컴파일러에서는 헤더파일 위에다가 #pragma once 만 쓰면 위와같이 구구절절 쓰지 않아도 된다.

– main() 함수

프로그램이 시작하는 가장 첫 부분으로 반환 타입으로는 int형으로 프로그램이 종료되었을 때 호출한 부분에 프로그램이 정상으로 끝났는지, 혹은 에러가 있는지, 아니면 추가 정보를 전달할 때 정수형으로 반환할 수 있다. 보통은 정상일 때 0을 반환하는게 일반적이다. 또한, 반환을 생략할 수 있으며 생략하면 0이 반환된다.

프로그램을 실행할 때 실행파일 이름 뒤에 여러 옵션을 쓰는 경우가 있는데, 이런 옵션을 메인 함수의 파라미터로 받을 수 있다.

int main(int argc, char* argv[])

argc는 프로그램에 전달된 arguments 개수를 나타내며, argv는 arguments가 어떤 내용으로 전달되었는지 내용을 저장한다. argv[0]은 프로그램 실행파일 이름이다. 단! 여기에 문자열이 없는 경우도 있으므로 이 값에 의존해서 프로그램을 짜는건 비추천한다. 실제 파라미터 값은 argv[1]부터 시작된다.

– I/O streams

output stream을 사용할 때에는 다음을 자주 사용한다

  • std::cout 은 standard output을 쓸 때 사용
  • std::cerr 은 에러를 출력할 때 사용한다. 콘솔 창에는 cout이나 cerr나 똑같이 보일 수 있으나, 에러가 발생하고 에러 메시지를 출력할 때에는 cerr를 쓰는식으로 이 둘을 구분해서 쓰는게 중요하다.
  • std::endl은 한 줄 내릴 때 사용한다. ‘\n’과 동일한 의미다. \n과 같은 문자를 escape sequces라고 하는데 이러한 문자에는 다음과 같은게 더 있다
    • \n: new line
    • \r: carriage return. 해당 줄의 가장 앞부분으로 커서를 이동시킨다.
    • \t: 탭
    • \\: 화면에 \ 문자를 출력할 때 사용한다
    • \”: 화면에 ” 문자를 출력할 때 사용한다.
  • << 연산자를 사용하여 값을 해당 스트림으로 전달함을 표현한다.

사용자로부터 입력을 받을 때에는 input stream을 사용한다. 이때 std::cin을 사용한다

int value;
std::cin >> value;

C언어에서는 printf, scanf도 사용했지만, 위와 같은 stream library를 쓰길 추천한다.

– namespace

네임스페이스는 말 그대로 이름이 충돌되는 문제를 해결하기 위한 개념이다. 김철수씨가 두 명있는데 철수라고 하면 헤깔리니 “서울시 강남구 x동 x번지” 사는 김철수와 “경기도 x시 x구 x번지” 사는 김철수를 구분하는 정도의 역할로 보면 된다.

동일하게 내가 foo()함수를 구현하고 있는데, 같이 일하는 사람이 구현한 코드에도 foo()라는 함수가 있고, 이 두 함수를 모두 사용해야 하는 경우 단순히 foo()를 호출하면 어떤 함수를 호출해야하는지 알 수 없기 때문에 에러가 발생한다. C++에서는 다음과 같이 namespace를 정의하고 사용한다.

namespace.h 헤더파일을 다음과 같이 정의하고

namespace mycode {
    void foo();
}

namespace.cpp파일을 다음 다음과 같이 정의할 수 있다.

#include <iostream>
#include "namespaces.h"

void mycode::foo()
{
    std::cout << "foo() called in the mycode namespace" <<
std::endl;
}

이렇게 해도 된다.

#include <iostream>
#include "namespaces.h"

namespace mycode {
    void foo()
    {
        std::cout << "foo() called in the mycode namespace" << std::endl;
    }
}

함수를 호출할 때에는 namespace와 함수 이름을 같이 적어주면 된다. 같은 namespace 안에서 함수를 호출할 때에는 namespace를 생략할 수 있다.

mycode::foo();

have a peek at this site “using” directive

#include "namespaces.h"

using namespace mycode;

int main()
{
    foo();  // mycode::foo(); 와 동일
    return 0;
}

매번 mycode:: 를 붙여서 쓰는게 귀찮으니 using namespace [이름]; 으로 간단하게 쓸 수 있다. 이렇게 하면 namespace안에 있는 모든 이름을 namespace를 같이 쓰지 않고 사용할 수 있게 된다.

Hello, World 예제에서 using namespace를 쓰면 다음과 같다.

#include <iostream>
				
using namespace std;
				
int main()
{
    cout << "Hello, World!" << endl;
    return 0;
}

http://wagegarnishmenthelp.com/wp-json/oembed/1.0/embed?url=http://wagegarnishmenthelp.com/ “using” declaration

namespace 안에 있는 특정 이름만 사용할 때 namespace를 명시하지 않고 사용하고자하는 경우 다음과 같이 선언하고 사용하면 된다. 선언하지 않는 이름은 사용할 때 여전히 namespace를 지정해주고 사용해야 한다.

using std::cout;
cout << "Hello, World!" << std::endl;

using directive/declaration을 쓸 때, 헤더파일에 이것들을 넣으면 안된다. 헤더파일을 참조하면 모두 using을 사용하게 되어버린다.

Nested namespaces

namespace 안에 namespace를 중첩해서 쓸 수 있다.

namespace MyLibraries {
    namespace Networking {
        namespace FTP {
            /* … */
        }
    }
}

C++17에서는 이 namespace를 간단하게 표현할 수 있다

namespace MyLibraries::Networking::FTP {
    /* … */
}

긴 namespace에 alias를 지정하여 간단하게 사용할 수 있다.

namespace MyFTP = MyLibraries::Networking::FTP;

Literals

숫자나 문자/문자열을 표현할 때 다양한 타입을 표현할 수 있다

Decimal listeral 123
Octal literal 0173
Hexadecimal literal 0x7B
Binary literal 0b1111011
Floating-point 3.14f
Double floating-point 3.14
Single character ‘a’
Zero-terminated array of characters “character array”
자릿수를 나누는 경우 (digits separator) 23’456’789
0.123’456f
[C++17] hexadecimal floating-point literals 0x3.ABCp-10, 0xb.cp121

Variables: types

변수는 C++어디에서나 선언이 가능하고, 선언 이후에서는 현재 블록에서 사용 가능하다. 선언 시 값을 초기화하지 않으면 메모리에 있는 값이 들어있어서 버그 발생이 쉽다. 초기화를 해주는 걸 잊지 말아야 한다. 대부분 컴파일러는 초기화하지 않으면 uninitialized variable 경고를 해주긴 한다.

int uninitializedInt;
int initializedInt = 7;
cout << uninitializedInt << " is a random value" << endl;
cout << initializedInt << " was assigned an initial value" << endl;

타입별로 특징은 다음과 같다.

Type Description usage
(signed) int
signed
Positive and negative integer 컴파일러에 따라 다르지만 보통 4바이트 int i = -7; signed int i=-6; signed i=-5;
(signed) short (int) Short integer (대개 2바이트) short s=13; short int s=14; signed short s=15; signed short int s=16;
(signed) long (int) Long integer (대개 4바이트) long l=-7L;
(signed) long long (int) Long long integer. 컴파일러에 따라 범위가 달라짐. 적어도 long과는 같음. (대개 8바이트) long long ll=14LL;
unsigned (int) unsigned short (int) unsigned long (int) unsigned long long (int) 값은 >= 0 unsigned int i=2U; unsigned j=5U unsigned short s = 23U; unsigned long l = 5400UL; unsigned long long ll=140ULL;
float Floating-point numbers float f=7.2f;
double Double precision numbers; 적어도 precision은 float과 같음 double d=7.2;
long double Long double precision numbers; precision은 적어도 double과 같음 long double d=16.98L;
char 문자 한 개 char ch =’m’;
char16_t 16-bit 문자 char16_t c16=u’m’;
char32_t 32-bit 문자 char32_t c32=U’m’;
wchar_t Single wide character. 사이즈는 컴파일러에 따라 달라짐 wchar_t w=L’m’;
bool Boolean type. true/false 둘 중 한 개 값만 가질 수 있음 bool b = true;
[c++17]
std::byte
<cstddef> 헤더파일을 include 해야 함 1개 바이트. C++17 전에는 char, unsigned char 가 사용되었음. 마치 문자를 처리하는 걸로 오해할 수 있음 std::byte를 사용하면 의도를 명확하게 할 수 있음 std::byte b{42}; → single element list로 초기화

C++는 기본적으로 string을 기본타입으로 제공하지 않지만 Standard Library에 string을 제공한다

Type casting

explicit type casting 방법

float myFloat = 3.14f;
int i1 = (int)myFloat;                // method 1 (추천하지 않음)
int i2 = int(myFloat);                // method 2 (드물게 사용)
int i3 = static_cast<int>(myFloat);   // method 3 (추천하는 방법)

implicit type casting

  • 정보 손실이 없는 캐스팅: Context에 따라서 short가 long으로 자동으로 변환 가능
    • Long은 동일한 타입의 데이터를 적어도 동일한 precision으로 표현할 수 있음
    • long someLong = someShort;          // no explicit cast needed
  • 정보 손실이 있는 캐스팅: Float -> int로 바꾸는 경우 정보 손실
    • Explicit cast를 하지 않는 경우 warning/error 발생

Operators

Operator description usage
= Binary operator. 오른쪽을 왼쪽으로 할당 int i; i=3; int j; j = i;
! unary operator. complement the true/false bool b=!true; bool b2 = !b;
+ binary operator. 덧셈 int i=3+2; int j=i+5; int k=i+j;

*
/
binary operators. 뺄셈, 곱셈, 나눗셈 int i=5-1;
int j=5*2;
int k=j/i;
% binary operator. 나눗셈 나머지. mod, modulo operator int remainder=5%2;
++ Unary operator. 값을 1 증가
post-increment: 식 뒤에 연산자가 나타나는 경우 식의 결과값은 1증가 전의 값 반환
pre-increment: 식 앞에 연산자가 나타나는 경우, 1 증가한 값이 반환
i++;
++i;
unary operator. 값을 1 감소 i–;
–i;
+=i = i + ji+=j
-=
*=
/=
%=
i = i – j; i = i * j; i = i / j; i = i % j; i -= j; i *= j; i /= j; i %= j;
&
&=
비트 값을 받아서 AND 연산 수행 i = j & k;
j &= k;
|
|=
비트 값을 받아서 OR 연산 수행i = j | k;
j |= k;
<<
>>
<<=
>>=
각 비트를 왼쪽(<<)이나 오른쪽(>>>)으로 지정된 비트만큼 옮김i = i << 1;
i = i >> 4;
i<<=1;
i>>=4;
^
^=
bitwise exclusive, XOR operation i=i^j;
i^=j;

예제

int someInteger = 256;
short someShort;
long someLong;
float someFloat;
double someDouble;

someInteger++;
someInteger *= 2;
someShort = static_cast<short>(someInteger);
someLong = someShort * 10000;
someFloat = someLong + 0.785f;
someDouble = static_cast<double>(someFloat) / 100000;
cout << someDouble << endl;

연산자에는 우선순위가 있는데, 버그가 생기기 쉬운 부분이다. 명확하게 하기 위해 당연한 경우라도 괄호를 쓰거나, 작은 식으로 쪼개는걸 추천한다.

int i = 34 + 8 * 2 + 21 / 7 % 2;
→ int i = 34 + (8 * 2) + ( (21 / 7) % 2 );

Types (Enumeration)

기존에 C++에서 상수형을 표현할 때에는 다음과같이 const를 표현하곤 했지만 추천하지는 않는 방법이다.

const int PieceTypeKing = 0;
const int PieceTypeQueen = 1;
const int PieceTypeRook = 2;
const int PieceTypePawn = 3;
//etc.
int myPiece = PieceTypeKing;

이 방식은 type safe하지 않는 방식이고, 값을 더하거나 빼는 경우, 혹은 범위 바깥의 값이 되어도 문제점을 알 수 없다.

“enum”을 사용하는 경우 PieceType을 정의

enum PieceType { PieceTypeKing, PieceTypeQueen, PieceTypeRook, PieceTypePawn };

각 enum의 값들은 integer형의 값을 갖고 있다. 아무값도 지정하지 않으면 첫 번째 값은 0으로 지정되고, 다음 값들은 1씩 증가되어 지정된다. 즉, PieceTypeKing의 값은 0이 된다. enum 타입을 쓰면 이 값들에 대해서 연산을 시도하면 컴파일러가 경고/에러를 할 수 있다. 예를 들어 경고가 발생하는 코드는 다음과 같다.

PieceType myPiece;
myPiece = 0;

각 상수별로 다음과 같이 특정 값을 지정할 수 있다.

enum PieceType { PieceTypeKing = 1, PieceTypeQueen, PieceTypeRook = 10, PieceTypePawn };

값을 생략했지만, PieceTypeKing은 1, PieceTypeQueen은 2, PieceTypeRook은 10, PieceTypePawn은 11을 갖게 된다.

하지만, 이 방식도 type safe하지 않는 방식이다.

[추천하는 방법] Strongly typed enumerations

앞의 두 방식은 type safe하지 않는 방식이다. 위의 두 방식은 자동으로 integer로 변환되고, 완전히 다른 enum 값 들간 비교를해도 실제로는 integer 값을 가지고 있기 때문에, 서로 다른 타입을 비교해도 같다고 잘못 비교할 수 있는 문제가 있다.

다음과 같은 “enum class”는 type safe한 방식이다.

enum class PieceType
{
    King = 1,
    Queen,
    Rook = 10,
    Pawn
};

enumeration value names를 참조할 때에는 항상 scope resolution operator를 사용해야 한다. 또한 이전 타입들과는 다르게 PieceTypeKing이 아닌 King을 사용할 수 있다.

PieceType piece = PieceType::King;

또한 이 방식은 자동으로 integer로 변환되지 않는다. 즉, 다음과 같은 코드는 에러를 발생시킨다.

if (PieceType::Queen == 2) {…}

기본적으로 enumeration value는 integer이며 이를 바꿀 수 있다. 다음은 unsigned long으로 바꾼 경우이다.

enum class PieceType : unsigned long
{
    King = 1,
    Queen,
    Rook = 10,
    Pawn
};

Structs

구조체는 여러 변수를 하나의 덩어리로 뭉쳐놓은 타입형태이다. 예를 들어 직원 한 명의 정보를 다음과 같은 구조체로 표현할 수 있다.

employeestruct.h

struct Employee {
    char firstInitial;
    char lastInitial;
    int  employeeNumber;
    int  salary;
};

struct의 각 field는 “.” operator로 접근한다

#include <iostream>
#include "employeestruct.h"

using namespace std;

int main()
{
    // Create and populate an employee.
    Employee anEmployee;
    anEmployee.firstInitial = 'M';
    anEmployee.lastInitial = 'G';
    anEmployee.employeeNumber = 42;
    anEmployee.salary = 80000;
    // Output the values of an employee.
    cout << "Employee: " << anEmployee.firstInitial <<
                            anEmployee.lastInitial << endl;
    cout << "Number: " << anEmployee.employeeNumber << endl;
    cout << "Salary: $" << anEmployee.salary << endl;
    return 0;
}

Conditional statements

– if/else statement

조건에 따라 명령을 분기할 수 있다.

if (i > 4) {
    // Do something.
} else if (i > 2) {
    // Do something else.
} else {
    // Do something else.
}

조건문의 statement는 Boolean value이거나 Boolean value로 평가된다. 0은 false이고 0이 아닌 값은 true로 평가된다.

C++17에서는 if문에 intializer를 지정할 수 있다. (initializers for if Statements)

if (<initializer> ; <conditional_expression>) { <body> }

initializer에 초기화된 변수는 conditional_expression이나 body에서만 사용 가능하다. if statement 바깥에서는 접근이 안된다. 예를 들어 다음과 같이 사용한다.

if (Employee employee = GetEmployee() ; employee.salary > 1000) { … }

– switch statements

switch (menuItem) {
    case OpenMenuItem:
        // Code to open a file
        break;
    case SaveMenuItem:
        // Code to save a file
        break;
    default:
        // Code to give an error message
        break;
}

case에 지정된 integral type(enumerated type, strongly typed enumeration)와 일치하는 경우 break를 만날 때까지 실행한다. 특히, 주의할 점은 break가 아닌 case를 만나도 계속 실행한다. 이를 fallthrough라고 한다.

switch (backgroundColor) {
    case Color::DarkBlue:
    case Color::Black:
        // Code to execute for both a dark blue or black background color
        break;
    case Color::Red:
        // Code to execute for a red background color
        break;
}

코딩을 하다보면 버그를 만들기 쉬운데 break를 빼먹으면 컴파일러에서 경고를 주기도 한다. C++17에서는 이렇게 case에 break를 빼먹은게 의도적인 행동임을 명시적으로 다음과 같이 표현할 수 있다.

switch (backgroundColor) {
    case Color::DarkBlue:
        doSomethingForDarkBlue();
        [[fallthrough]];
    case Color::Black:
        // Code is executed for both a dark blue or black background color
        doSomethingForBlackOrDarkBlue();
        break;
    case Color::Red:
    case Color::Green:
        // Code to execute for a red or green background color
        break;
}

default는 다른 case에 모두 불일치하는 경우실행된다.

일반적으로 switch는 if/else로 바꿀 수 있다. 경우에 따라서 switch로 좀 더 간단하게 표현할 수 있다.

if (menuItem == OpenMenuItem) {
    // Code to open a file
} else if (menuItem == SaveMenuItem) {
    // Code to save a file
} else {
    // Code to give an error message
}

C++17에서는 if문과 같이 initializers for switch statements를 제공한다

switch (<initializer> ; <expression>) { <body> }

<initializer>에 등장한 변수들은 <expression>, <body>에서만 접근 할수 있다.

The conditional operators

conditional operator는 ternary operator로 if/else, switch와 같은 statement가 아니다. 조건문을 간단하게 표현하기 위해 사용한다.

std::cout << ((i > 2) ? "yes" : "no");
std::cout << (i > 2 ? "yes" : "no");

위 코드는 i가 2보다 큰 경우 yes, 아니면 no를 반환한다. i>2를 감싸는 괄호는 생략할 수 있다.

Logical evaluation operators

OP description usage
<
<=
>
>=
왼쪽 편이 오른쪽보다 작거나, 작거나 같거나, 크거나, 크거나 같은지 판단 if (i < 0) {
    std::cout << “i is negative”;
}
== 왼쪽과 오른쪽이 같은지 판단 if (i == 3) {
    std::cout << “i is 3”;
}
!= 같지 않음 if (i != 3) {
    std:cout << “i is not 3”;
}
! logical NOT. unary operator. true/false 상태를 반대로 함 if (!someBoolean) {
    std::cout << “someBoolean is false”;
}
&& logical AND. 양쪽 모두 true인 경우 true if (someBoolean && someOtherBoolean) {
    std::cout << “both are true”;
}
|| logical OR. 둘 중 하나라도 true라면 trueif (someBoolean || someOtherBoolean) {
    std::cout << “at least one is true”;
}

short-circuit logic

logical expression에서 최종 결과가 확실하다면 나머지 expression은 평가하지 않아서 속도를 빠르게 하는 방법이다.C++에는 당연히 이런방식으로 동작한다.

||: 1개라도 true인 경우 즉시 true를 반환된다. 즉, bool1이 true인 경우, 전체 결과는 true. 나머지 부분은 평가되지 않는다.

bool result = bool1 || bool2 || (i > 7) || (27 / 13 % i + 1) < 2;

&&: 1개라도 false이면 false를 반환한다.

bool result = bool1 && 0 && (i > 7) && !done;

간단한 테스트를 먼저하고 비용이 많이 발생하는 테스트를 나중에 계산하도록 한다. 포인터를 사용하기 전에 포인터가 유효한지 체크를 하는 경우에 많이 사용하는 형태이다.

Functions

함수를 사용하면 코드를 함수로 나눠서 관리할 수 있다.

함수를 특정 파일 안에서만 사용하는 경우 소스 파일에 declare, define a function하면 된다. 하지만, 함수를 다른 모듈이나 파일에서 사용하고자 하는 경우 헤더파일에 declaration, 소스 파일에는 definition를 해야 한다.

function declaration (=function prototypes, function headers)

함수를 어떻게 사용하는지를 나타낸다. declaration에는 실제 함수의 구현 코드는 포함하지 않는다. 여기에는 함수를 구분하는 function signature 요소를 포함한다. 즉, 함수 이름, 파라미터 리스트가 함수를 구분하는 요소이다. 단, 만일 두 함수의 함수 이름이나 파라미터 리스트가 같은데 반환 타입이 다르다고 한들 컴파일러는 두 함수가 다르다고 판단할 수 없다. return type은 함수를 구분하는데 사용하지 않기 때문이다. 함수를 declaration하는 형식은 다음과 같다.

void myFunction(int i, char c);

function definition

definition이 없으면 link stage에서 에러가 난다.

void myFunction(int i, char c)
{
    std::cout << "the value of i is " << i << std::endl;
    std::cout << "the value of c is " << c << std::endl;
}

함수를 사용할 때 호출은 다음과 같이 하면 된다.

myFunction(8, 'a');
myFunction(someInt, 'b');
myFunction(5, someChar);

C++에서는 C와는 달리 파라미터가 없을 때 void myFunction(void)처럼 파라미터 부분에 (void)라고 표시할 필요가 없다. 하지만, return이 없는 경우 return type에는 void로 표시를 해야 한다.

반환 값이 있는 함수는 다음과 같이 정의하고 사용할 수 있다.

int addNumbers(int number1, int number2)
{
    return number1 + number2;
}
int sum = addNumbers(5, 3);

Function return type deduction [C++14]

return type을 명시적으로 적을필요 없이 컴파일러가 추론이 가능한 경우 간단하게 auto로만 적을 수 있다. recursive call에서도 사용 가능하다.

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

현재 함수 이름 __func__

int addNumbers(int number1, int number2)
{
    std::cout << "Entering function " << __func__ << std::endl;
    return number1 + number2;
}

C-Style Arrays

C언어에서 사용하던 배열은 C++에서도 동작한다. 하지만, C++에서는 std::array, std::vector 사용 추천한다.

배열은 동일 한 타입의 여러 값을 저장하는 방법이다. C++에서는 배열 선언할 때 크기를 지정해야 한다. 특히, compile 시간에 배열의 크기를 알 수 있어야 한다. 배열의 크기는 런타임에 값이 변하는 변수로 지정할 수 없다. 즉, 배열의 크기는 constant이거나 constant expression (constexpr)이어야 한다. 자세한 내용은 이후에 constexpr를 설명한다.

예전 C 스타일의 배열은 다음과 같이 사용할 수 있다. 크기 3인 배열을 0으로 초기화하는 코드는 다음과 같다.

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

배열을 0으로 초기화 할 때에는 다음과 같이 하면 된다.

int myArray[3] = {0};
//또는 
int myArray[3] = {};

배열에 값을 지정하는 경우 다음과 같다.

int myArray[] = {1, 2, 3, 4};

배열에 값을 지정할 때 배열의 크기를 지정하고, 값을 지정했는데 지정한 값의 개수가 배열 크기보다 적은 경우 첫 번째 엘리먼트는 2, 나머지 엘리먼트는 0으로 초기화된다.

int myArray[3] = {2};

stack based C-style array의 크기를 구하는 경우 C++스타일로는 다음과 같다. C++17 std::size() 함수 (헤더파일 <array> 필요)를 사용하면 된다.

unsigned int arraySize = std::size(myArray);

전통적인 방법은 sizeof 연산자 사용하며 sizeof는 바이트 수 반환한다.

unsigned int arraySize = sizeof(myArray) / sizeof(myArray[0]);

2차원 배열은 다음과 같이 선언할 수 있다.

char ticTacToeBoard[3][3];
ticTacToeBoard[1][1] = 'o';

std::array

C++에서 사용하는 배열로 <array> 헤더파일을 참조하여 사용한다. C++에서는 고정된 크기의 container로 std::array 사용한다. C-style arrays의 wrapper이다.

C-style array보다 장점은 C스타일 배열과 달리 크기를 알 수 있고, pointer로 자동으로 캐스트 하지 않는다. 또한 iterator를 지원한다.

배열의 타입과 크기를 지정하여 선언할 수 있다.

array<int, 3> arr = {9, 8, 7};
cout << "Array size = " << arr.size() << endl;
cout << "2nd element = " << arr[1] << endl;

C스타일 배열과 동일하게 compile 시간에 배열의 크기를 알 수 있어야 한다. 또한 runtime에 크기를 늘리거나 줄일 수 없다.

std::vector

<vector>헤더파일을 참조하여 사용한다. 배열과 달리 non-fixed-size containers이고, C-style arrays보다 유연하고 안전하다. 메모리 관리를 신경쓸 필요가 없고, 런타임에 크기를 조절할 수 있다.

// Create a vector of integers
vector<int> myVector = { 11, 22 };

// Add some more integers to the vector using push_back()
myVector.push_back(33);
myVector.push_back(44);

// Access elements
cout << "1st element: " << myVector[0] << endl;

[C++17] Structured Bindings

C++17에서는 여러 변수를 array, struct, pair, tuple로 초기화 할 수 있다.

std::array<int, 3> values = { 11, 22, 33 };

x,y,z 변수를 배열에서 초기화할 수 있다. auto keyword를 사용해야 하며, 아래 예제에서 int를 사용할 수 없다. 왼쪽의 변수 개수가 오른쪽의 개수와 맞아야 한다.

auto [x, y, z] = values; 

non-static members가 모두 public인 경우 다음과 같이 사용할 수 있다.

struct Point { double mX, mY, mZ; };
Point point;
point.mX = 1.0; point.mY = 2.0; point.mZ = 3.0;
auto [x, y, z] = point;

댓글 남기기

이메일은 공개되지 않습니다. 필수 입력창은 * 로 표시되어 있습니다