c++

C++ Primer CH2. Variables and Basic Types

big whale 2024. 1. 21. 16:00

c++는 정적 타입 언어이다.

정적 타입 언어란, 컴파일러가 컴파일할 때, 모든 식별자의 타입을 알아야 한다는 것이다.

c++은 class에 들어갈 data와 class가 수행할 연산도 커스텀할 수 있다.

 

Type(타입)은 데이터와 연산자의 의미를 결정한다.

char = same size as machine basic character set = single Byte

 

char 8

wchar_t 16

char16_t 16

char32_t 32

short: 16bits

int: 16bits(2Bytes)

long 32

long long 64

float32

double 64

long double 96

 

메모리는 워드 단위로 읽힌다.

워드 = 32bit(4B) or 64bit(8)

int main() {
    char c2 = 60;
    int a = c2 + 20;
    cout << "result: "<< c2 << endl;
    return 0;
}
// 결과: result: <
// 60이 안나온 이유: c2의 타입이 char이고, 아스키코드상 60은 <와 매칭되기 때문이다.
// c2대신 a 넣으면 80 나옴. 이유: c2가 더 큰 타입인 int로 바뀌기 때문.

 

undefined behavior: 컴파일은 성공하는데 런타임시에 발생할 수도 있는 에러

implementation-defined behavior: 머신A에서는 잘 돌아갔던 프로그램이 머신B로 갔을 때 에러 발생하는 것

예: int 타입 크기를 고정시켜 놓은 경우.(변수할당을 통해)

 

signed와 unsigned value를 섞어 사용하지 말라(표현식에서)

signed → unsigned로 implicit type casting되므로 위험하다.(예기치 못한 동작)

literal(값 그자체, self-evident자명함)

 

literal 종류들

- 숫자 literal

- integer literal: 1,2,3…(decimal) 024(octal), 0x14(hexadecimal)

- char

   - ‘a’

- char string

   - “Hello World”,

   - H, e, l, l,… → 각각 const char이다.

   - 컴파일러가 string 맨끝에 ‘\0’ 붙임.

 

escape sequence

   - ‘\n’ 이런식으로 표현하는데 문자 1개로 취급함

 

literal 쓸 때 명시하는 법

prefix(접두사, 앞에붙음)

u → unicode 16 character, char16_t

U → unicode 32 character, char32_t

L → wide character, wchar_t

u8 → utf-8(string literals only), char

 

suffix(접미사, 뒤에붙음)

u,U → unsigned

l,L → long

ll,LL → long long

 

floating point literal에 붙이면,

f,F → float

l,L → long double

 

unsigned를 나타내는 u,U는 number literals에 붙을 수 있다.

ex)UL: unsigned long

 

boolean and pointer literals

boolean: true, false

pointer: nullptr

 

initialization과 assignment는 c++에서 동작과정이 다르다

초기화(initialization): 변수가 생성될 때 변수 선언 및 값 할당 등의 과정

할당(assignment): 변수의 현재 값을 지우고 다가올 값으로 교체하는 것

 

List initialization

{} 써서 초기화하는 것.

ex)

long double ld = 3.1415926536;
int a{ld}, b = {ld}; // error: narrowing conversion required
int c(ld), d = ld; // ok: but value will be truncated

여기서 a{ld} → 이게 direct-list-initialization인데, {}쓰려면 narrowing conversion 일어나면 안됨.

narrowing conversion은 변환 후 precision이 감소하는 변환을 말함.

 

여기서 long double → int 되면서 소수점은 날라가므로 precision이 줄어듦 → narrowing conversion → {}는 narrowing conversion 허용 안함 → error

 

함수 밖에 선언된 변수들은 초기값이 0으로 initialize 된다.

함수 안에 선언된 변수들은 uninitialize 된다 → 값이 undefined임.

 

Objects 종류

built-in type(int나 char 등)

class type(우리가 만든것, stl 포함)

 

built-in type에 대해 initialize 하는게 좋다. → 컴파일러는 warning을 띄우지만 안띄우는 경우도 있고 런타임시에도 됐다 안됐다 하는 경우 있어서 어디서 버그 발생하는지 찾기 어렵기 때문

 

Declaration and Definition

변수 선언, 정의, 초기화

선언: 프로그램에게 해당 이름을 가진 변수가 있다 라고 알려주는 것

정의: 실제 개체(entity)를 만들어 이름과 매칭시키는 것

선언만: extern int i;

선언+정의: int j;

 

정의는 한번만 되어야 하지만, 선언은 여러곳에 할 수 있다.

 

c++에서 scope → curly braces가 기준 → block scope

 

변수가 사용되는 곳에다가 정의해라. 미리 다해놓지 말고

→ 초기화시 값 넣을 때 편함.

 

전역 스코프에서 정의된 변수를 사용하고 싶을 때 → ::변수명

 

Compound types

References

  • lvalue reference(지금 배울것)
  • rvalue reference(이후에 배움)

일반적인 변수 초기화(int a = 10)와 참조(int &b = a)의 다른 점

전자: 초기자(initializer)를 복사해서 생성될 object에 넣음

후자: 초기자와 참조를 bind(묶음) → 복사 과정 X

int main()
{
    int ival = 1024;
    int &refVal = ival;
    // int &refVal2; // reference는 무조건 initialized 되어야 함.
    ival = 10;
    cout << refVal << endl;
		int &test = 10; // 안됨. object만 bind 가능
}

refVal과 ival binding 함. binding이란? → 초기자 object와 묶이는 것

ival object의 값을 1024→10으로 바꿈

출력결과 10

 

같은 reference(참조) 변수에 대해 rebind 하는 법 없음.

 

왜 rebind되지 않도록 만든 것인가?

Reference는 기존 변수의 또다른 이름이라고 보면 된다.

Reference는 object가 아니다.

Pointers

reference와의 차이점

  • pointer는 정의시 initialize 안해줘도 됨.
  • 재할당 가능
  • 복사 가능
  • 실제로 object임

포인터에 포인터를 할당할 땐 타입이 같아야 함.

포인터의 역참조*

int *p = &val;

cout << *p;

or

int *p;

*p = 10;

cout << *p // 10

 

*p를 써서 할당할 때, p가 가리키고 있는 object에 할당하고 있다라고 해석하면 편하다.

따라서,

int main() {
    int val;
    int *p = &val;
		int *q;
    *p = 10;
		*q = 100;
    cout << *p;
		cout << *q; // error -> q가 가리키는 object가 없으므로.
}

 

Null Pointers

nullptr

int *p1 = nullptr == int *p1 = 0 == int *p1 = NULL;

NULL은 preprocessor에서 처리되어 컴파일러가 컴파일하기 전에 0으로 바꿔줌 → 0과 똑같음 → 안씀

아무 object도 가리키고 있지 않다는 것을 명시해줌으로써 undefined behavior를 방지함(runtime crash)

int zero = 0;

pi = zero

→ 이거는 int타입을 int*타입 변수 p1에 할당해주는거라서 error

void*

object를 가리키고 있지만 그것이 어떤 타입인지는 정해지지 않았을 때

아무 타입의 포인터든 할당될 수 있다.

사용처: 함수 인자로, 반환타입으로. 메모리 주소 저장할 때.

reference는 pointer를 가리킬 수도 있다.

int *p;

int *&r = p;

*r = 10;

cout << *p; // 10

2.4 const Qualifier

변하지 않을 변수를 정의하고 싶을 때 사용

재할당시 error 발생하므로, 정의시점에 초기화까지 해줘야 한다.

const 변수를 다른 변수에 할당하는건 아무런 특이사항이 없다.

 

const 변수가 정의(정의는 곧 초기화)된 file에 대해 local성을 띈다.

만약, 해당 const 변수를 사용하고 싶다면, 해당 파일 내에 initializer가 있어야 함.

이러면 같은 const 변수를 여러개의 file에 동시에 정의해놓아야 함.

→ extern const

 

extern이 붙으면 다른 파일에서 해당 변수를 접근할 수 있게 됨.

extern이 const 변수에 붙고 해당 변수가 초기화되어있으면,

이후, 해당 변수를 사용하고 싶은 파일에서 똑같이 extern const로 선언만 해주면, 컴파일러가 해당 변수의 초기자를 찾으려고 할 때, 초기자 탐색 범위를 파일 내로 한정하지 않고 해당 변수는 local 변수가 아니고 외부 어딘가에 있음을 명시함. extern 까지 확인함으로써 초기자를 찾을 수 있게됨.

→ 정의를 한번만 내려도 다른 파일에서 사용할 수 있게됨.

extern은 선언(declaration)만 해주는 것임.

선언 + 정의 + 초기화 까지 해줘야 변수를 사용 가능

References to const

const int ci = 1024;

const int &r1 = ci; OK, not object

r1 = 42; Error bcs r1 is const’s reference

int &r2 = ci; Error bcs r2 is int’sreference

근데,

const int a = 10;

int b = a; → 이건 됨. 값을 할당하는거라서.

double dval = 3.14;
1. const int &ri = dval; // 컴파일됨. ri = 3
2. int &ri = dval; // 컴파일에러.

1. int &ri = 42; // 컴파일에러.
2. const int &ri = 42; // 컴파일됨.

const 붙은 reference 변수는 초기자가 literal이든 다른 타입이든 다 컴파일됨

 

int i = 42;
int &r1 = i;
const int &r2 = i;
r1 = 0; // legal. 
r2 = 10; // illegal because r2 is const.

Pointer const

const double pi = 3.14;
// double *ptr = &pi; // ptr은 const object를 가리키지 않고 그냥 생짜 double을 가리켜서 컴파일 안됨.
const double *cptr; // const 붙이면 됨.
// *cptr = 33; // 당연히 const니까 재할당 불가.

// double dval = 3.14;
// cptr = &dval; // 근데 const포인터가 일반 포인터 할당받을 수 있음.
// *cptr = 1111; // 당연히 const라서 컴파일 에러.

const 변수를 가리키는 포인터는 변하지 않아야 하는게 가리키는 오브젝트다.

const 변수처럼 const 포인터 변수도 정의될 때 초기화도 되어야 함. → const포인터 변수는 초기화 안해줘도 됨. 재할당 가능

const 포인터에 일반변수주소를 할당할 수는 있는데 이유 → 어차피 변경하려고 시도할 때 const 포인터 변수 선에서 막히기 때문

일반 포인터에 const 변수주소 컴파일 안됨 → 일반포인터라서 변경이 되기 때문에 const인데 변경되니까 막음.

 

const Pointers(변하면 안되는 포인터)

포인터가 가리키는 오브젝트(주소값)이 바뀌면 안될 때 사용

int main()
{
    int errNumb = 0;
    int *const p1 = &errNumb;

    const double pi = 3.14;
    const double *const cp1 = &pi;

    cout << *p1 << *cp1 << endl; 
}

Top-Level Const vs Low-Level Const

top-level const: 나 자체가 const

low-level const: 내가 가리키는 object가 const

할당자가 top-level이면 컴파일 되는데 low-level이면 할당받는 변수도 low-level이어야 함.

할당받는 변수가 const이면 허용 범위가 커짐. 일반변수면 const 할당 못받음(이건 참조인 경우에 해당. 바꾸려고 하므로)

#include <iostream>
using namespace std;

int main()
{
    int i = 0;
    int *const p1 = &i; // top-level
    const int ci = 42; // top-level
    const int *p2 = &ci; // low-level
    const int *const p3 = p2; // top and low level
    const int &r = ci; // top-level

    i = ci; // ci is top-level so const in ci is ignored
    p2 = p3; // same

    // int *p = p3; // p3 has low-level so error
    p2 = p3; // p3 has low-level but p2 also has low-level so OK
    // p2 = &i; // converted int* to const int*
    // int &r = ci; // error. ci is const and r is ordinary
    // const int &r2 = i; // OK
    // p2 = &i;
    cout << *p2;
    
}

constexpr and Constant Expressions

constexpr(const expression)

  • const 변수가 const한 expresison으로 초기화되는걸 보장하고 싶을 때 사용
constexpr int mf = 20;// OK
constexpr int limit = mf + 1;// OK
constexpr int sz = size() -> // size()함수가 constexpr 함수일 때만 가능. define시 추가해줘야함.

Pointers and constexpr

constexpr int *p = &val; // expression을 const화 시키기 때문에 top-level const
constexpr int *constp = &val; // object와 pointer object 둘다 const, top,low level const

Type Aliases

기존 타입을 좀 더 해석하기 쉬운 이름으로 바꾸는 것

typedef double wages;
typedef wages base, *p;

p val = &val; // val은 double pointer 변수다.
const p val = &val2; // const pointer 생성.

const 붙으면 좀 어렵다.

auto Type Specifier

컴파일러가 initializer 결과를 보고 변수 타입을 추론하도록 하는 것.

auto item = item1 + item2;
auto i = 1, *p = &i; // OK 둘다 int
auto sz = 1, df = 3.14 // error. 두 변수의 추론타입이 다름(int, float)
auto &n = i, *p2 = &ci; // error. n:int, p2: const int

const int ci = i, &cr = ci;
auto b = ci; // ci: top-level -> auto=int
auto c = cr; // cr = ci = top-level -> auto=int
auto d = &i; // int*
auto e = &ci; // const int * = low-level -> const int*

명시적으로 const 넣어줄 수 있음.
const auto f = ci; // const int

헤더파일을 한번만 include 되도록 하는 법 → preprocessor 사용

directives

  • #define: preprocessor variable
  • #ifdef: true if the variable has been defined
  • #ifndef: true if the variable has not been defined

그래서, Sales.item.h 맨 위에

#ifndef SALESITEM_H
#define SALESITEM_H
이렇게 있는데, 아직 해당 SALES_item.h 파일이 전처리기가 안 읽었으면 SALESITEM_H not define이므로
#ifndef는 true라서 다음으로 넘어가고, 이후에 #define을 통해 SALESITEM_H가 define 된다.
이후, 다른 파일에서 SALES_item.h가 include 되었을 때, #ifndef가 false가 되면서 전처리기가 해당 파일
을 건너뛰게된다.
true면 #endif 만날 때까지 코드 읽으면서 메모리에 로딩함.
false면 #endif 사이 코드들 다 무시함.
SALESITEM_H같은 변수를 header guard 라고 부름

헤더 가드는 프로그램 내에서 유니크한 이름이어야 함. 따라서 클래스 이름을 대문자화_H 한 이름으로 주로함.

헤더가드 설치하는걸 습관화해야함.