VioletaBabel

1. C++ 기초 본문

BCA/10. 모던 C++ 입문
1. C++ 기초
Beabletoet 2018. 12. 27. 12:31
1. 변수

기본 타입은 내장 타입(Intrinsic Type)이라고 한다.


1
2
3
4
5
6
7
8
9
10
11
12
13
#include<iostream>
using namespace std;
int main()
{
    int i1 = 2// i1 = 2
    int i2, i3 = 5// i2는 초기화 되지 않음, i3 = 5
    float pi = 3.14159// pi = 3.14159
    double x = -1.5e6// x = -1.5e+06 = -1500000
    double y = -1.5e-6// y = -1.5e-06 = -0.0000015
    char c1 = 'a', c2 = 35// c1 = a, c2 = #
    bool cmp = i1 < pi, happy = true// cmp = 1, happy = 1 (1 = true)
    auto i4 = i3 + 7//auto (C++11) : 변수의 타입을 추론한다. i4 = 12, type은 int.
}
cs


auto는 C++11에 추가된 기능으로, 변수의 타입을 추론한다.

타입은 자동으로 정해지지만, 가능한 동일하게 유지한다.




2. 상수

문법적으로 불변이라는 추가 속성을 갖는 특별한 변수.


1
2
3
4
5
6
7
8
#include<iostream>
using namespace std;
int main()
{
    const int ci1 = 2;
    const float pi = 3.14159;
    const int ci3; // 값을 할당하지 않아 에러가 남
}
cs

선언과 동시에 값을 설정해야 한다.




3. 리터럴

리터럴의 타입을 명시적(강제 변환)으로 선언할 필요는 없으나, 필요에 따라 다음과 같이 사용할 수 있다.

2 = int

2u = unsigned

2l = long

2ul = unsigned long

2.0 = double

2.0f = float

2.0l = long double

042 = 8진수 (0으로 시작하는 수는 8진수로 해석)

0x42 = 16진수 (0x, 0X라는 접두사를 붙이면 16진수)

0b42 = 2진수 (0b, 0B라는 접두사를 붙이면 2진수. C++14에 추가됨)


그리고 C++14부터 길이가 긴 리터럴의 가독성 향상을 위해 ' 로 숫자를 분리할 수 있다.

const long double pi = 3.141'592'653'589'793'238'462l;

문자열 리터럴은 char 배열로 할당한다.

그러나 string 타입을 더 추천하며, 매우 긴 텍스트는 다음 예제의 s3와 같이 부분 문자열로 분할 가능하다.


1
2
3
4
5
6
7
8
9
10
11
#include<iostream>
#include<string>
using namespace std;
int main()
{
    char s1[] = "Old C Style";
    string s2 = "In C++ better like this";
    string s3 = "This is a very long and clumsy text."
        "that is too long for one line.";
    cout << s1 << endl << s2 << endl << s3 << endl;
}
cs




4. 축소하지 않는 초기화

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<iostream>
using namespace std;
int main()
{
    long l2 = 1234567890123;            // 오버플로우로 값이 변함. l2 = 1912276171
    long l = { 1234567890123 };        // 값 보장이 안되므로 에러.
    long long l3 = { 1234567890123 };    // 값이 보장됨. l3 = 1234567890123
    int i1 = 3.14;                        // i1 = 3
    int i2 = { 3.14 };                // 소수 값 보장이 안되므로 에러.
    unsigned u2 = -3;                    // u2 = 4294967293
    unsigned u3 = { -3 };                // unsigned에 음수 값 보장 불가, 에러.
    float f1 = 3.14;                    // f1 = 3.14
    float f2 = { 3.14 };                // f2 = 3.14
    double d = 3.14;                    // d = 3.14
    float f3 = { d };                    // float에 double 값 보장 불가, 에러.
    unsigned u3 = { 3 };                // u3 = 3;
    int i2 = { 2 };                        // i2 = 2;
    unsigned u4 = { i2 };                // unsigned는 음수 값 보장 불가, 에러.
    int i3 = { u3 };                    // unsigned의 큰 값 보장 불가, 에러.
}
cs


C++11부터 데이터가 손실되지 않음을 보장하기 위한 중괄호 초기화를 지원.

중괄호 초기화 시, 데이터를 보장할 수 없는 경우 에러가 난다.




5. 범위


전역 변수는 const같은 경우를 제외하고는 사용을 추천하지 않는다. 

소프트웨어의 규모가 커짐에 따라 수정 사항을 추적하기 더 어려워지고 오류 사태를 촉발하게 되는 잠재력을 지녔기 때문.


지역 변수는 { } 괄호 안에서만 수명을 가진다. 그 밖에서는 존재하지 않는 것으로 친다. 


만약 같은 이름의 변수가 중첩된 범위에 있으면 안쪽 범위의 변수는 바깥쪽 범위에 있는 동명의 변수를 숨긴다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include<iostream>
using namespace std;
int main()
{
    int a = 5// a(1) = 5
    {
        // a(1) = 5
        a = 3// a(1) = 3
        int a = 8// a(1) = 3 , a(2) = 8
        {
            a = 7// a(1) = 3 , a(2) = 7
        }
        // a(1) = 3 , a(2) = 7
    }
    // a(1) = 3
    a = 1// a(1) = 1
}
cs


a(1)의 수명은 5번 줄부터 17번 줄 까지이고

a(2)의 수명은 9번 줄부터 14번 줄 까지이다.

범위는 충돌이 발생하지 않게 하지만, 바깥쪽 범위의 변수에게 접근하는 것을 막아버린다.

이를 해결할 방법은 이름을 바꾸거나, 네임스페이스를 이용하는 것이다.




6. 콤마 연산자


콤마 연산자는 왼쪽의 연산을 우선으로 시행한다.

주로 for문에서 사용.


1
2
3
4
5
6
7
#include<iostream>
using namespace std;
int main()
{
    int i1 = 3, i2 = 4, i3 = 9;
    cout << (i2 += i1, i2*i3); // 63이 출력
}
cs




7. typeid 연산자


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include<iostream>
using namespace std;
 
class Base{};
class Derived : public Base {};
 
int main()
{
    Derived* d = new Derived;
    Base* b = d;
    cout << typeid(b).name() << endl;
    cout << typeid(*b).name() << endl;
    cout << typeid(d).name() << endl;
    cout << typeid(*d).name() << endl;
}
cs


런타임 타입을 식별하는 타입 식별자.




8. sizeof 연산자

1
2
3
4
5
6
7
8
9
10
#include<iostream>
using namespace std;
 
int main()
{
    int a = 4;
    cout << sizeof a << endl;
    cout << sizeof(a) << endl;
    cout << sizeof(long long<< endl;
}
cs


타입이나 오브젝트의 크기




9. sizeof... 연산자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
using namespace std;
template<class... Types>
struct Count
{
    static const int value = sizeof...(Types);
};
int main()
{
    Count<intchar> a;
    cout << a.value << endl// 2
    Count<intintint> b;
    cout << b.value << endl// 3
}
cs

C++11부터 도입된 연산자로 인수의 개수를 리턴하며, ...을 이용하는 variadic template에서만 사용할 수 있다.




10. alignof 연산자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<iostream>
using namespace std;
 
struct one {};
 
struct two
{
    int a;
    int b;
    char c;
};
 
int main()
{
    cout << alignof(one) << endl// 1
    cout << alignof(two) << endl// 4
    cout << alignof(int*<< endl// 4
    cout << alignof(int<< endl// 4
    cout << alignof(char*<< endl// 4
    cout << alignof(char<< endl// 1
}
cs

C++11부터 도입된 연산자로, 타입이나 오브젝트의 메모리 맞춤을 리턴한다.

C++에서 구조체, 클래스는 정렬된 크기만큼 크기가 늘어나며, 크기가 그보다 작아도 padding으로 채워 그만큼의 크기를 잡는다.

alignof 연산자는 현재 정렬의 크기가 얼마인지를 리턴하는 셈.




11. 범위 기반 for문

C++11에 도입된 문법

1
2
3
4
5
6
7
8
9
#include<iostream>
using namespace std;
int main()
{
    int num[] = { 1,2,3,4,5,6,7,8,9,10 }, res = 0;
    for (int i : num)
        res += i;
    cout << res; // 55
}
cs

타 언어의 foreach와 같은 거라고 보면 될 듯.




12. assert

c에서부터 이어져 내려오는 매크로.

표현식을 계산한 후 결과가 false면 프로그램을 즉시 종료한다.

1
2
3
4
5
6
7
8
9
10
#include<iostream>
#include<cassert>
using namespace std;
int main()
{
    int plusNum;
    cin >> plusNum;
    assert(plusNum > -1); // 음수를 입력 시 에러
    cout << plusNum;
}
cs
양수 입력 시

음수 입력 시




13. 예외 처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
using namespace std;
int main()
{//a/b, a%b 출력
    int a, b;
    cin >> a >> b;
    try 
    {
        if (b == 0)
            throw b;
        cout << a / b << endl << a % b << endl;
    }
    catch (int exception)
    {
        cerr << "b = 0" << endl;
        exit(EXIT_FAILURE); // 리턴 1과 같다고 보면 됨
    }
}
cs

b에 0을 넣을 경우엔 바로 catch문의 항목을 실행.


그리고 C++11부터는 함수 뒤에 noexcept를 붙임으로서 예외를 던지지 않는 함수라고 지정하는 새로운 지정자 'noexcept'를 도입.

또, C++11부터 컴파일  과정에서 감지할 수 있는 프로그램 오류는 static_assert를 이용해 오류를 출력하고 컴파일을 중단하도록 함.




14. 서식 지정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<iostream>
#include<iomanip>
using namespace std;
int main()
{
    int old_precision = cout.precision();
    double pi = 3.141592653589793;
    cout << "1:" << pi << endl;
    cout << "2:" << setprecision(16<< pi << endl;
    cout << "3:" << setw(30<< pi << endl;
    cout << "4:" << right << setw(30<< pi << endl;
    cout << "5:" << setfill('-'<< left << setw(30<< pi << endl;
    cout << "6:" << setfill('-'<< right << setw(30<< pi << endl;
    cout.setf(ios_base::showpos);
    cout << "7:" << scientific << pi << endl;
    cout.unsetf(ios_base::adjustfield | ios_base::basefield | ios_base::floatfield | ios_base::showpos | ios_base::boolalpha);
    cout.precision(old_precision);
    cout << "8:" << oct << 63 << endl;
    cout << "9:" << hex << 63 << endl;
    cout << "10:" << dec << 63 << endl;
    cout << "11:" << (pi < 3<< endl;
    cout << "12:" << boolalpha << (pi < 3<< endl;
    cout << "13:" << noboolalpha << (pi < 3<< endl;
}
cs

setprecision = 정밀도를 지정한다.

setw = 출력의 너비를 설정한다

right = 우측 정렬

left = 좌측 정렬

setfill = 특정 문자로 여백을 채운다

setf = 특정 서식 플래그를 직접 설정한다. showpos는 + - 부호를 보인다.

scientific = 과학적 표기법을 강제한다

oct = 8진수

hex = 16진수

dec = 10진수

boolalpha = bool값을 1,0이 아닌 true,false로 출력한다

unsetf = 특정 서식 플래그를 직접 해제한다.




15. 포인터

초기화하지 않은 포인터에는 무작위의 값이 할당되어있다.

포인터가 아무 것도 가리키고 있지 않다는 것을 명시적으로 알려주려면 다음과 같이 하면 된다.

이는 C++11 이상에서 사용하는 방법이다.

1
2
3
4
5
6
7
#include<iostream>
using namespace std;
int main()
{
    int* ip = nullptr;
    int* ip2{};
}
cs

그 외에도 이전 컴파일러에서 사용하던 다음과 같은 방법이 있지만, C++11 이상에서는 비추천한다.
1
2
3
4
5
6
7
#include<iostream>
using namespace std;
int main()
{
    int* ip = 0;
    int* ip2 = NULL;
}
cs

리터럴 0은 함수 오버로딩에서 모호함을 유발할 수 있고, NULL 매크로는 단지 0으로 치환될 뿐이므로 c++11이상에서는 nullptr이라는 포인터 리터럴의 키워드를 사용할 것을 추천한다.


포인터는 메모리 누수의 위험이 있다.

이를 예방하려는 전략을 소개한다.

1) 표준 컨테이너를 사용하라 : 표준 라이브러리나 유효성이 검증된 라이브러리는 메모리를 자동으로 해제해준다.

2) 캡슐화 : 개체를 소멸할 때 개체가 할당한 모든 메모리를 해제한다면 메모리 할당 빈도는 더 이상 중요하지 않다. 이러한 원칙을 RAII(Resource Acquisition Is Initialization)이라고 한다.

3) 스마트 포인터를 사용하라 : 포인터는 개체를 참조하고 동적 메모리를 관리하는 데에 사용한다. 소위 원시 포인터의 문제는 포인터가 데이터를 참조하고 있는지, 아니면 더 이상 필요하지 않은지에 대한 개념이 없다는 점이다. 그러므로 스마트 포인터를 사용할 수 있다.




16. 스마트 포인터

C++03에서 지원된 스마트 포인터 auto_ptr은 더는 사용하지 않을 것을 추천한다.

그 후 C++11에서 unique_ptr, shared_ptr, weak_ptr이라는 3가지 포인터를 도입했다.


1) unique_ptr : 이 포인터의 이름은 참조한 데이터의 고유 소유권을 나타낸다.

1
2
3
4
5
6
7
#include<iostream>
using namespace std;
int main()
{
    unique_ptr<double> d{ new double };
    *= 10;
}
cs

유니크포인터 d를 만들고 그 포인터가 가리키는 주소의 값에 10을 넣는다.


1
2
3
4
5
6
7
8
9
#include<iostream>
using namespace std;
int main()
{
    double a = 2;
    unique_ptr<double> d{ &a };
    *= 10;
    cout << &<< endl << a << endl << d << endl << *<< endl;
}
cs

유니크포인터 d를 만들고 거기에 a의 주소를 넣는다.

그 다음 d가 가리키는 주소의 값에 10을 넣으면 a도 10이 되어있다.

a의 주소와 d는 일치하며, 그러므로 a의 값과 d가 가리키는 주소의 값 역시 일치한다.


1
2
3
4
5
6
7
8
9
#include<iostream>
using namespace std;
int main()
{
    unique_ptr<double> d{ new double };
    *= 10;
    double* raw = d.get();
    cout << *raw << endl// 10
}
cs

다른 원시 포인터에서 유니크포인터의 데이터를 얻으려면 멤버 함수 get을 이용한다.


1
2
3
4
5
6
7
8
9
#include<iostream>
using namespace std;
int main()
{
    unique_ptr<double> d{ new double };
    *= 10;
    unique_ptr<double> d2{ d }; // 유니크포인터는 복사 금지라서 에러
    d2 = d; // 여기도 에러
}


cs
유니크포인터는 다른 유니크포인터에 할당할 수 없다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
using namespace std;
int main()
{
    unique_ptr<double> d{ new double };
    *= 10;
    cout << *<< endl// 10
    unique_ptr<double>d2{ move(d) }, d3;
    //cout << *d << endl; // d는 사용이 끝나 메모리가 해제되므로 에러.
    cout << *d2 << endl// 10
    d3 = move(d2);
    //cout << *d2 << endl; // d2도 메모리가 해제. 에러.
    cout << *d3 << endl// 10
}
cs

유니크포인터는 복사가 아닌 이동만 가능.

기존의 포인터는 메모리가 해제됨.


1
2
3
4
5
6
7
8
9
10
11
#include<iostream>
using namespace std;
unique_ptr<double> uniqDouble()
{
    return unique_ptr<double>{new double};
}
int main()
{
    unique_ptr<double> d;
    d = uniqDouble();
}
cs

이 경우는 함수의 결과는 이동될 임시 값이므로 move()가 없어도 됨.


배열은 이런식으로 사용

1
2
3
4
5
6
7
8
9
10
#include<iostream>
using namespace std;
int main()
{
    unique_ptr<double[]> da{ new double[3] };
    for (int i = 0; i < 3++i)
        da[i] = i + 1;
    for (int i = 0; i < 3++i)
        cout << da[i] << ' '// 1 2 3 출력
}
cs

대신 연산자 *는 배열에 사용 불가.


멤버 함수

get = 유니크포인터의 stored_ptr 반환

reset = 유니크포인터의 리소스를 해제하고 새 리소스를 넣음 // d.reset(new double);

release = 포인터의 해제 없이 소유권만 제거. 이런 경우 리턴된 포인터는 직접 해제하여야 함.


유니크포인터의 중요한 이점은 원시 포인터에 비해 시간과 메모리에 대한 오버헤드가 전혀 없다는 점.



2) shared_ptr

여러 파티가 각각 포인터를 가지고 있으며, 그 파티들이 공통으로 사용하는 메모리를 관리한다.

모든 스레드가 스레드의 접근이 끝나면 메모리를 자동으로 해제하는 속성이 있다.

참조 횟수라는 값을 두고, 참조할 때마다 1씩 증가, 수명이 다할 때마다 1씩 감소시키며 참조 횟수가 0이 되면 delete한다.

그리고 unique_ptr과 달리 원하는 만큼 자주 복사할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
using namespace std;
shared_ptr<int> SharedInt()
{
    shared_ptr<int> p1{ new int };
    shared_ptr<int> p2{ new int }, p3 = p2;
    cout << "p3 : " << p3.use_count() << endl// 2
    return p3;
}
int main()
{
    shared_ptr<int> p = SharedInt();
    cout << "p : " << p.use_count() << endl// 1
}
cs

함수가 반환하면 포인터를 파괴하고 p1이 참조하는 메모리를 해제한다.

p2, p3는 main의 p가 여전히 참조 중이기에 계속 존재한다.


그리고 make_shared를 이용해 보다 안전하게 만들 수도 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include<iostream>
using namespace std;
shared_ptr<int> SharedInt()
{
    auto p1 = make_shared<int>();
    shared_ptr<int> p2 = make_shared<int>(), p3 = p2;
    cout << "p3 : " << p3.use_count() << endl// 2
    return p3;
}
int main()
{
    shared_ptr<int> p = SharedInt();
    cout << "p : " << p.use_count() << endl// 1
}
cs

다음과 같이 auto를 사용할 수도 있다.


shared_ptr은 메모리와 실행 시간에 약간의 오버헤드를 가진다. 하지만 프로그램을 단순화할 수 있다는 이점이 있다.

하지만 메모리 해제를 방해하는 순환 참조 문제가 발생할 수 있다.



3) weak_ptr

weak_ptr은 위 shared_ptr의 순환 참조 문제를 중단할 수 있다.

공유하지만 소유권을 주장하지 않는다.

이 포인터는 나중에 다시 언급하기로 한다.




17. 배열용 컨테이너


1) vector

1
2
3
4
5
6
7
8
9
10
11
#include<iostream>
#include<vector>
using namespace std;
int main()
{
    vector<int> v(3), w(3);
    v = { 1,2,3 };
    w = { 7,8,9 };
    for (int i = 0; i < 3++i)
        cout << v[i] + w[i] << ' '// 8 10 12
}
cs


1
2
3
4
5
6
7
8
9
#include<iostream>
#include<vector>
using namespace std;
int main()
{
    vector<int> v = { 1,2,3 }, w = { 7,8,9 };
    for (int i = 0; i < 3++i)
        cout << v[i] + w[i] << ' '// 8 10 12
}
cs


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
#include<vector>
#include<cassert>
using namespace std;
vector<int> add(const vector<int>& v1, const vector<int>& v2)
{
    assert(v1.size() == v2.size()); // 사이즈가 다르면 종료시킴
    vector<int> s(v1.size());
    for (int i = 0; i < v1.size(); ++i)
        s[i] = v1[i] + v2[i];
    return s;
}
int main()
{
    vector<int> v = { 1,2,3 }, w = { 7,8,9 }, s = add(v, w);
    for (int i = 0; i < 3++i)
        cout << s[i] << ' '// 8 10 12
}
cs

벡터는 자세한 설명은 생략한다.


2) valarray

요소별 연산을 사용하는 1차원 배열.

1
2
3
4
5
6
7
8
9
10
11
12
#include<iostream>
#include<valarray>
using namespace std;
int main()
{
    valarray<float> v = { 1,2,3 }, w = { 7,8,9 }, s = v + 2.0f * w, s2 = sin(s);
    for (float i : s)
        cout << i << ' '// 15 18 21
    cout << endl;
    for (float i : s2)
        cout << i << ' '// 0.650288 -0.750987 0.836656
}
cs

각 요소 별로 연산을 수행한다.

슬라이스에 접근하는 기능이 강점이며, 각각의 연산을 포함해 행렬과 고차 텐서를 대리 실행할 수 있다.

허나 선형 대수 연산을 직접 지원하지 않는 편이라 valarray를 널리 사용하지는 않는다.




18. 포함방지 매크로

1
2
3
4
5
6
7
8
9
10
#include<iostream>
#ifndef MATH_FUNCTIONS_INCLUDE
#define MATH_FUNCTIONS_INCLUDE
#include<cmath>
#endif
using namespace std;
int main()
{
    
}
cs

간접적 포함으로 자주 사용하는 헤더 파일이 하나의 번역 단위에 여러번 들어가는 것을 막는 매크로.

일반적으로 _INCLUDE나 _HEADER를 끝에다가 붙인다.

편리하게 사용할 대안은 pragma once가 있다.

1
2
3
4
5
6
7
8
#include<iostream>
#pragma once
#include<cmath>
using namespace std;
int main()
{
    
}
cs


pragma once가 속도 등은 더 이점이 있지만, 구형 컴파일러에도 동작해야 한다면 ifndef~endif를 쓰는 것이 낫다.

'BCA > 10. 모던 C++ 입문' 카테고리의 다른 글

2. 클래스  (0) 2018.12.28
Comments