클래스와 객체(Class and Object)

  • C언어에서는 절차 지향 프로그래밍만 지원함. 절차 지향 프로그래밍이란 프로그램을 단순히 데이터와 이를 처리하는 방법으로 작성하는 것을 의미함.

  • C++에서는 객체 지향 프로그래밍도 지원하는데, 객체 지향 프로그래밍이란 실세계의 특정 대상을 객체로 나타내어 수많은 객체들의 상호작용으로 프로그램을 서술하는 방식을 의미함.

  • 객체(Object)의 개념

    • 객체 지향 프로그래밍에서는 데이터와 기능을 하나로 묶어서 다룰 수 있는 자료구조.

    • 프로그램을 처리하는 절차보다도 동작되는 자료에 중점을 둔 것.

    • 객체는 프로그램의 기본 단위이며, 객체 간의 상호 관계로 프로그램의 처리를 표현함.

    • 객체는 멤버 변수(특성, Attribute)와 이를 대상으로 처리하는 동작인 멤버 함수(메서드, Method)를 하나로 묶어 만든 요소

  • 구조적 프로그래밍에서는 변수와 함수가 합쳐져 프로그램을 만들지만, 객체 지향 프로그래밍에서는 멤버 변수와 멤버 함수가 합쳐져 하나의 객체를 만들고, 객체와 객체가 합쳐져 하나의 프로그램을 만듬.

  • C언어에서 구조체는, 서로 다른 타입의 변수들을 저장할 수 있는 사용자 정의 타입으로만 사용되었으며, 함수를 저장할 수 없어서 구조체를 매개변수로 받는 함수를 따로 만들어 처리해야 했음.

  • C++에서는 변수와 함수를 함께 저장할 수 있는 클래스(Class)라는 개념을 도입하였는데, 클래스를 통해 만들어지는 변수를 객체라고 부름. 참고로 구조체(struct)도 클래스로 분류되며, 함수를 저장할 수 있음.

  • 클래스는 설계도, 객체는 이 설계도를 기반으로 만들어진, 런타임(Run-time) 시 동작하는 실체로 정의. 클래스에서 객체로 바꾸는 작업을 인스턴스화라고 함.

  • 클래스 선언 방법

    • class 클래스 이름 { 멤버 변수; 멤버 함수; ... } -> 외부에서 멤버 변수나 함수에 접근하는 범위를 지정 가능

    • struct 클래스 이름 { 멤버 변수; 멤버 함수; ... } -> 외부에서 항상 멤버 변수나 함수에 접근 가능

  • 클래스의 멤버 함수

    • 함수의 내용은 클래스 내부나 클래스 외부에서 구현 가능.

    • 클래스 내부에서 구현된 멤버 함수들은 모두 인라인 함수(inline function)로 정의. 인라인 함수란 C언어에서 매크로 함수와 비슷한 개념으로 프로그램의 실행 속도를 높이기 위해 도입되었음.

    • 함수의 본체가 긴 경우, 멤버 함수를 외부에서 정의하는 것이 편리함. -> 클래스 이름::함수 이름(매개변수) {...}

  • 데이터 추상화: 현실 세계의 사물을 데이터적인 측면과 기능적인 측면을 통해서 정의하여 추상화하는 방법

    • 예를 들어, 강아지는 다리가 4개이고, 종류(말티즈 등)와 색상(흰색 등) 등의 데이터로 정의할 수 있으며, 짖을 때는 "멍멍"이라는 소리를 내며 달릴 수도 있음.

    • 다리 개수, 종류, 색상이라는 멤버 변수와 짖기, 뛰기라는 멤버 함수로 강아지라는 클래스를 작성할 수 있음.

#include <iostream>
using namespace std;

// Define class
class Dog {
public:
    int numLegs;
    string kind;
    string color;
        
    void bark();
    void run();
};

// Instantiation: create object!
int main() {
    Dog dog = { 4, "시고르자브종", "검은색" };
    dog.bark();
    dog.run();
    return 0;
}

void Dog::bark() {
    cout << kind << ": 멍멍!" << endl;
}

void Dog::run() {
    cout << numLegs << "개 다리로 후다닥!" << endl;
}
  • C++에서는 구조체를 이용해서 클래스를 정의할 수 있음.

struct Dog {
    int numLegs;
    string kind;
    string color;

    void bark(); // { cout << "멍멍!" << endl; }
    void run(); // { cout << "후다닥!" << endl; }
};

void Dog::bark() {
	cout << kind << ": 멍멍!" << endl;
}

void Dog::run() {
	cout << numLegs << "개 다리로 후다닥!" << endl;
}
  • C언어의 경우 위와 같은 데이터 추상화는 다음과 같이 표현됨.

#include <stdio.h>

typedef struct Dog {
    int numLegs;
    char kind[100];
    char color[10];
} Dog;

void bark(Dog* dog) {
    printf("%s: 멍멍!\n", dog->kind);
}

void run(Dog* dog) {
    printf("%d개 다리로 후다닥!\n", dog->numLegs);
}

int main() {
    Dog dog = { 4, "시고르자브종", "검은색" };
    bark(&dog);
    run(&dog);
    return 0;
}

 

접근 제한자(Access Modifier)

  • C++에서는 접근 제한자를 통해 클래스 외부에서 멤버 변수나 멤버 함수에 접근할 수 있는 범위를 정함.

  • private: 자신의 멤버 함수 외에는 접근할 수 없음.

  • protected: 자식 클래스의 멤버 함수로부터의 접근만 허용

  • public: 모든 곳으로부터의 접근을 허용

  • 접근 제한 정도: private > protected > public

Class Parent
{
private:
    int priv;
protected:
    int prot;
public:
    int pub;
    void accessAllMembers();
};

void Parent::accessAllMembers() {
    priv = 100; // Success
    prot = 100; // Success
    pub = 100; // Success
}

// 자식 클래스
class Child : public Parent {
public:
    void accessParents() {
        int n;
        n = priv; // Fail
        n = prot; // Success
        n = pub; // Success
    }
};

int main() {
    Parent parent;
    int n;
    n = parent.priv; // Fail
    n = parent.prot; // Fail
    n = parent.pub; // Success
}

 

정보 은닉과 캡슐화

  • 정보 은닉(Information Hiding)

    • 프로그램을 사용하는 사용자가 알아야 하는 것은 프로그램을 사용하는 방법이기 때문에, 내부 동작이나 상세 구조를 드러낼 필요는 없음.

    • 정보 은닉이란 사용자가 굳이 알 필요가 없는 정보를 숨김으로써, 최소한 정보만으로 프로그램을 쉽게 사용하도록 하는 방법.

    • C++에서는 클래스의 정보 은닉 기능을 지원하기 위해 접근 제한자 키워드를 제공함.

    • 숨길 멤버와 공개할 멤버의 블록을 구성하여 공개된 멤버는 외부에서 자유롭게 접근 가능하나 숨겨진 멤버를 직접 참조하려고 하면 오류를 발생하도록 처리.

    • 따라서 간접적으로 접근할 필요가 있을 때는 메소드를 활용.

    • Getter: 값을 읽기 위한 멤버 함수

    • Setter: 값을 쓰기 위한 멤버 함수

    • 일반적으로, 멤버 변수는 private이고 멤버 함수는 public이며, 프로그램 설계에 따라 달라질 수 있음.

class Point {
private:
    double x;
    double y;

public:
    double getX();
    double getY();
    void setX(double _x);
    void setY(double _y);
};
  • 캡슐화(Encapsulation)

    • 관련 있는 데이터와 함수를 하나의 단위로 묶는 것.

    • 정보 은닉과는 다른 개념.

// 정보 은닉 없이 캡슐화만 된 경우
class Point {
public:
    double x;
    double y;
};

 

 

생성자(Constructor)

  • 객체가 선언될 때 실행되는 코드로, 멤버 변수들을 초기화하는 역할을 함.

  • 클래스와 같은 이름을 함수의 이름으로 가지며, 인수나 리턴 값이 없는 상태로 선언.

  • 생성자는 public 이지만 싱글턴 패턴과 같이 private 으로 선언하는 경우도 존재함.

  • 생성자가 호출되는 시기

    • 객체를 만들 때: Person p;

    • 메모리를 할당할 때: Person* p = new Person;

  • 함수 오버로딩과 같은 조건으로 생성자들도 오버로딩이 가능함.

  • 디폴트 생성자(default constructor)

    • 생성자를 만들지 않을 경우 C++에서 자동으로 만들어주는 생성자.

    • 디폴트 생성자는 인수를 가지지 않지만, 멤버 변수들이 초기화됨.

    • 단, int나 char 등의 기본 타입은 초기화되지 않음.

    • 단, 생성자를 하나라도 선언할 경우 디폴트 생성자는 만들어지지 않음.

#include <iostream>

class Person {
private:
    double height;
    double weight;
public:
    Person(double _height, double _weight) {
        height = _height;
        weight = _weight;
    }
};

int main() {
    Person p(178.5, 70.8);
    Person p; // Error
    return 0;
}
  • const나 레퍼런스 멤버 변수는 선언과 동시에 초기화를 해야하므로, 디폴트 생성자가 아닌 멤버 이니셜라이저(Member Initializer)로 초기화를 해주어야 함.

class Person {
private:
    const string SSN;
    double height;
    double weight;
public:
    Person(const string _SSN, double _height, double _weight)
        : SSN(_SSN), height(_height), weight(_weight)
    { }
};

 

소멸자(Destructor)

  • 생성자가 객체를 초기화하듯, 반대로 객체가 더 이상 필요하지 않을 때 정리하는 코드.

    • 객체가 삭제될 때 파괴: Person* p = new Person(); delete p;

    • 블록 밖으로 넘어갈 때 파괴: if (true) { Person p; } // 여기서 p의 소멸자 호출

    • 다른 소멸자에 의한 파괴: A 클래스가 멤버 변수로 B 클래스를 가질 때, A의 소멸자가 호출된 후 B의 소멸자가 호출됨.

  • 생성자가 메모리를 할당하면 사용 후에는 운영체제로 리턴되어야 함.

  • 소멸자는 생성자의 이름 앞에 ~를 붙여서 정의함.

class IntPointer {
private:
    int* intPtr;
    
public:
    IntPointer() {
        pi = new int[5];
    }
    
    ~IntPointer() {
        delete[] pi;
        pi = nullptr;
    }
};
  • 생성 순서와 소멸 순서는 반대

    • Person a; Person b; 가 있으면 생성 순서는 a, b가 되고 소멸 순서는 b, a가 된다.

    • 먼저 선언한 객체가 먼저 생성

    • 먼저 선언한 객체가 나중에 소멸

#include <iostream>
using namespace std;

class A {
public:
    A() { cout << "A() called!" << endl; }
    ~A() { cout << "~A() called!" << endl; }
};

class B {
public:
    B() { cout << "B() called!" << endl; }
    ~B() { cout << "~B() called!" << endl; }
};

int main() {
    A a;
    B b;
    return 0;
}

/* output:
 A() called!
 B() called!
 ~B() called!
 ~A() called!
 */

 

this 포인터

  • 클래스에서 사용할 수 있는 특별한 포인터로, 현재 멤버 함수가 호출된 인스턴스의 메모리 주소를 가리킴.

  • 멤버 변수들과 멤버 함수들을 연결해주는 이유.

    • 멤버 변수들은 각각의 인스턴스에서 저장할 내용이 다르기 때문에 반드시 별도로 존재해야 하지만, 멤버 함수들은 인스턴스가 늘어나더라도 바뀔 필요가 없음.

    • 따라서, 프로세스 구조상 멤버 변수들이 보관되는 영역(스택, 힙, 데이터)과 멤버 함수들이 존재하는 영역(코드)은 나누어져 있음.

    • 코드 영역은 실행 중에 변경을 막기 위해 보호되어 런타임 중에는 수정될 수 없음.

    • 즉, 멤버 함수는 멤버 변수에 접근하기 위해 인스턴스를 식별할 필요가 있음.

  • 인스턴스는 독자적인 멤버 변수들을 가지지만, 클래스 공통의 멤버 함수와 매칭됨.

  • 멤버 함수를 호출하게 되면, 멤버 함수에 호출한 인스턴스의 포인터를 같이 보내고, 멤버 함수는 인스턴스의 포인터(this)를 가지고 멤버 변수들에 접근함.

  • 멤버 함수 내에서 명칭의 우선순위: 지역 변수 > 멤버 변수 > 전역 변수

#include <iostream>
using namespace std;

class Person {
private:
    double height;
    double weight;
public:
    Person() { }
    Person(double _height, double _weight)
        : height(_height), weight(_weight) { }
        
    void setHeight_wrong(double height) {
        height = height; // Not changing
    }
    
    void setHeight_1(double height) {
        this->height = height;
    }
    
    double getHeight() { return height; }
};

int main() {
    Person p(183.4, 78.5);
    p.setHeight_wrong(182.8);
    cout << p.getHeight() << endl; // 183.4
    p.setHeight(182.8);
    cout << p.getHeight() << endl; // 182.8
    return 0;
}

 

입출력 함수

  • C 언어

#include <stdio.h>

int main() {
    int nbr = 1;
    char sentence[1024];
    print("Enter some characters: ");
    scanf("%s", sentence);
    printf("%d: %s\n", nbr, sentence);
    printf("Hello World!\n");
    return 0;
}
  • C++

#include <iostream>

int main() {
    int nbr = 1;
    char sentence[1024];
    std::cin >> sentence;
    std::cout << x << ": " << sentence << std::endl;
    std::cout << "Hello World!" << std::endl;
    return 0;
}
  • 헤더 파일의 변화: <stdio.h> 에서 <iostream>

  • C언어에서 사용된 헤더파일은 대부분 앞에 c를 붙이고 ".h"를 제외하면 똑같이 사용 가능.

    • <stdio.h> == <cstdio> 또는 <stdlib.h> == <cstdlib> 또는 <string.h> == <cstring> 등

  • C++에서 표준 입출력 함수는 입력이나 출력 형식을 지정하지 않고 사용함. 소수점 자릿수 등을 따져야할 경우 C언어의 표준 입출력을 가져다 써야함.

  • 속도는 C언어의 표준 입출력(printf/scanf)가 C++ 것(cin/cout)보다 빠름.

  • C++ 에서 개행문자는 std::endl 로도 표현 가능.

 

bool type

  • C언어에서는 조건문의 참, 거짓을 표현하기 위해 int 타입을 사용.

    • 참: 0을 제외한 모든 값

    • 거짓: 0

#include <stdio.h>

int main() {
    int x = 1;
    
    if (x) {
        printf("x is true.\n");
    } else {
        printf("x is false.\n");
    }
    return 0;
}
  • C++에서는 참과 거짓을 명확하게 사용하기 위해 bool 타입이 추가됨.

    • 참: true

    • 거짓: false

#include <iostream>

int main() {
    bool b = true;
    
    if (b) {
        std::cout << "b is true." << std::endl;
    } else {
        std::cout << "b is false." << std::endl;
    }
    
    // 표준 출력 시 int 타입
    std::cout << b << std::endl; // 1
    std::cout << !b << std::endl; // 0
    
    // bool 포맷을 유지하기
    std::cout.setf(std::ios::boolalpha);
    std::cout << b << std::endl; // true
    std::cout << !b << std::endl; // false;
    
    // 다시 int로 출력하기
    std::cout.unsetf(std::ios::boolalpha);
    std::cout << b << std::endl; // 1
    std::cout << !b << std::endl; // 0
    
    return 0;
}

 

auto keyword

  • C++11 부터 추가된 auto 타입 연산자를 사용하면, 컴파일 타임에 타입을 추론해 어떤 타입인지 결정함.

  • 기본 내장 타입을 포함해 컴파일 타임에 추론 가능한 모든 타입에 사용 가능.

  • 만약 컴파일 타임에 추론이 불가능하다면, 오류가 발생함.

#include <iostream>
#include <vector>

int main() {
    int i1 = 10;
    double d1 = 3.14;
    
    auto i2 = 10; // integer
    auto d2 = 3.14; // double
    
    // 복잡한 타입을 가지는 경우
    std::vector<std::pair<int, double> > vPairs;
    for (std::vector<std::pair<int, double> >::iterator iter = vPairs.begin();
        iter != vPairs.end(); ++iter)
    {
        std::cout << "So complex!" << std::endl;
    }
    
    // auto keyword
    for (auto iter = vPairs.begin(); iter != vPairs.end(); iter++)
    {
    	std::cout << "auto keyword is good." << std::endl;
    }
    
    return 0;
}

 

범위 기반 for문(Range-based for)

  • for 문은 지정된 횟수만큼 반복하는 작업에도 사용하나, 배열이나 각종 컨테이너에 있는 각 요소들에 무언가를 수행하기 위해 순회하는 목적으로도 많이 사용함.

  • 그러나, 조건문에 배열이나 컨테이너에 몇 개의 요소가 들어있는지 명시해야 한다는 단점이 있음.

  • 범위 기반 for문은 요소들의 개수에 상관없이 반복문을 수행함.

#include <iostream>

auto mean(const Sequence& seq)
{
    auto n = 0.0;
    for (auto& x : seq) {
        n += x;
    }
    
    return n / seq.size();
}

int main() {
    int arr[] = { 1, 2, 3, 4, 5 };
    for (auto& i : arr) {
        std::cout << i << std::endl;
    }
    
    // same as
    for (int i = 0; i < 5; ++i) {
        std::cout << i << std::endl;
    }
    return 0;
}

 

메모리 할당과 해제

  • C언어

#include <stdio.h>
#include <stdlib.h>

int main() {
    int i;
    int** arr = (int**) malloc(sizeof(int*) * 5);
    for (i = 0; i < 5; ++i) {
        arr[i] = (int*) malloc(sizeof(int) * 5);
    }
    
    for (i = 0; i < 5; ++i) {
        free(arr[i]);
    }
    free(arr);
    arr = NULL;
    
    return 0;
}
  • C++

#include <iostream>

int main() {
    int** arr = new int*[5];
    for (int i = 0; i < 5; ++i) {
        arr[i] = new int[5];
    }
    
    for (int i = 0; i < 5; ++i) {
        delete[] arr[i];
    }
    delete[] arr;
    arr = NULL;
    
    int* p1 = new int;
    delete p1;
    p1 = NULL;
    
    int* p2 = new int[10];
    delete[] p2;
    p2 = NULL;
    
    return 0;
}

 

널 포인터

  • C언어에서 널 포인터를 나타내기 위해 NULL이나 상수 0을 사용.

    • #define NULL 0

    • 따라서 NULL == 0

  • NULL 은 포인터가 아니므로, 함수에 인자로 넘기는 경우 int 타입으로 추론되는 문제가 발생함.

  • C++에서는 nullptr 키워드로 널 포인터를 표현함.

  • nullptr은 널 포인터 리터럴(Literal) 이며, 실제 타입은 std::nullptr_t 이다.

#include <iostream>

// function overloading
void fn(int a) {
    std::cout << "fn(int)" << std::endl;
}

void fn(int* a) {
    std::cout << "fn(int*)" << std::endl;
}

int main() {
    fn(0); // fn(int)
    fn(NULL); // fn(int)
    fn(nullptr); // fn(int*)
    
    std::cout << sizeof(nullptr) << std::endl; // 4
    std::cout << typeid(nullptr) << std::endl; // std::nullptr_t
    
    int* p1 = new int;
    delete p1;
    p1 = nullptr; // DO NOT USE NULL
    
    int* p2 = new int[10];
    delete[] p2;
    p2 = nullptr; // DO NOT USE NULL
    
    return 0;
}

 

명시적 캐스팅

  • C언어에서는 int(..) 또는 char(...)를 통해 명시적(explicit) 캐스팅을 할 수 있으나, 개발자의 실수를 그대로 용납하기 때문에 차후에 오류가 생길 수 있음.

  • C++에서는 보다 명시적인 캐스팅 방법으로 캐스팅의 목적을 명확하게 명시함으로써 개발자의 의도를 컴파일러에게 전달하여 오류를 방지함.

  • static_cast<타입>(표현식);

#include <iostream>

int main() {
    char* str = "Hello, World!";
    int* intPtr = static_cast<int*>(str);
    char* charPtr = static_cast<char*>(*intPtr);
    
    std::cout << charPtr << std::endl;
    
    /* In the case of C
       int* intPtr = (int*) str;
       char* charPtr = (char*)*pi;
     */
    
    return 0;
}

 

범위 지정 열거체(Scoped enum)

  • C/C++ 에서 기존에 사용하던 열거형은 전역(global) 범위로 선언되기 때문에 A 열거체에서 사용한 T라는 이름은 B 열거체에서는 사용할 수 없음.

  • 또한, 기본적으로 열거체의 타입은 int이기 때문에 묵시적으로 변환되어 서로 다른 열거체 변수끼리 더하거나 비교할 수 있음.

#include <stdio.h>

/* C/C++98 */
int main() {
    enum TrafficLight { Red, Yellow, Green };
    enum Job { Warrior, Ranger, Wizard };
    enum Coffee : unsigned char { Latte = 10, Mocha = 25 };
    
    int jobNum = Warrior;
    int i = Green + Latte; // 12
    
    if (Yellow == Ranger) {
        printf("Same!\n");
    } else {
        printf("Different!\n");
    }
    
    return 0;
}
  • C++11부터 위의 문제점들을 보완하기 위해 범위 지정 열거체를 제공.
    • enum 대신 enum class를 사용.
    • 묵시적 변환이 되지 않으므로, 명시적으로 타입을 지정하지 않으면 오류 발생.
    • 서로 다른 열거체 변수끼리 연산할 수 없음.
#include <iostream>

// C++11
int main() {
    enum class TrafficLight { Red, Yellow, Green };
    enum class Job { Warrior, Ranger, Wizard, Green };
    enum class Coffee : unsigned char { Latte = 10, Mocha = 25 };
    
    Job jobNum = Job::Warrior;
    int i = static_cast<int>(TrafficLight::Green)
        + static_cast<int>(Coffee::Latte);
    
    // TrafficLight::Yellow != Job::Ranger
    
    return 0;
}

 

Binary literal, separator

  • C/C++에서는 10진수 외에 8진수, 16진수를 표현할 수 있음.

    • 8진수: 061 (=49) --> 0으로 시작

    • 16진수: 0x3A (=58) --> 0x로 시작

  • C++14부터 2진수를 표현하는 방법이 추가됨. 0b로 시작 --> 0b110101

  • C++14부터 구분자(separator)가 추가되어 큰 숫자를 읽기 쉬움.

    • int INT_MAX = 21'4748'3647

#include <iostream>

int main() {
    int decimal = 52;
    int octal = 064;
    int hexadecimal = 0x34;
    int binary = 0b110100; // C++14
    
    int maxInt_Cpp98 = 2147483647;
    int maxInt_Cpp14 = 21'4748'3647; // C++14
    
    return 0;
}

 

문자열(string) 자료구조

  • C언어에서는 문자열을 나타내기 위해 char[] 또는 char* 를 사용하였으나, 이는 비교(strcmp), 연결(strcat), 길이(strlen) 등 문자열 관련 함수를 사용할 때 불편함.

#include <stdio.h>

int main() {
    char str1[30] = "Hello, World!";
    char* str2 = "Hello, ke2ek!";
    
    if (strcmp(str1, str2) == 0) printf("Same!\n");
    else printf("Different!\n");
    
    strcat(str1, str2);
    printf("%s\n", str1); // Hello, World!Hello, ke2ek!
    printf("%d, %d\n", strlen(str1), strlen(str2));
    
    return 0;
}
#include <iostream>
#include <string>

int main() {
    std::string str1 = "Hello, World!";
    std::string str2 = "Hello, ke2ek!";
    
    if (str1 == str2) std::cout << "Same!" << std::endl;
    else std::cout << "Different!" << std::endl;
    
    str1 = str1 + str2;
    std::cout << str1 << std::endl; // Hello, World!Hello, ke2ek!
    std::cout << str1.size() << ", " << str2.length() << std::endl;
    
    return 0;
}

 

레퍼런스(Reference) 변수

  • 다른 변수를 가리키는 변수 (포인터 기능의 일부)로, 다른 변수와 동일한 메모리 위치를 공유함.

  • 레퍼런스 변수로 연산할 때는 포인터와 달리 *나 &를 추가해야 하는 문법이 없음.

  • 레퍼런스는 항상 유효한 메모리 주소를 참조해야 하므로, const처럼 선언과 동시에 초기화를 해주어야 함.

#include <iostream>

// Pass-by-pointer
void swap(int* a, int* b) {
    int temp = *a;
    *a = *b;
    *b = temp;
}

// Pass-by-reference
void swap(int& a, int& b) {
    int temp = a;
    a = b;
    b = temp;
}

void print(int a, int b) {
    std::cout << "a = " << a;
    std::cout << ", b = " << b << std::endl;
}

int main() {
    // int& ref; NEED TO ASSIGN.
    int i = 10;
    int &ret = i;
    
    std::cout << "ref = " << ref << std::endl; // 10
    
    ret += 10;
    std::cout << "i = " << i << std::endl; // 20
    std::cout << "ref = " << ref << std::endl; // 20
    
    int a = 10, b = 20;
    print(a, b); // a = 10, b = 20
    swap(&a, &b); // Pass-by-pointer
    print(a, b); // a = 20, b = 10
    
    int c = 30, d = 40;
    print(c, d); // c = 30, d = 40
    swap(c, d); // Pass-by-reference
    print(c, d); // c = 40, d = 30
    
    return 0;
}

 

함수 오버로딩

  • C언어와 달리, C++에서는 여러 함수를 동일한 이름으로 선언할 수 있는 함수 오버로딩이라는 개념을 지원함.

  • 함수 오버로딩 규칙

    • 오버로딩된 함수는 인수의 개수가 서로 달라야 함.

    • 인수의 개수가 같은 경우, 인수의 타입이 서로 달라야 함.

    • 리턴 타입이 다른 경우, 같은 이름의 함수를 사용할 수 없음.

#include <iostream>

int sum(int num1, int num2);
int sum(int num1, int num2, int num3);
int sum(short num1, short num2);

// Compile Error!
// short sum(short num1, short num2);

int main() {
    int a = 10, b = 20, c = 30;
    int d = sum(a, b);
    int e = sum(a, b, c);
    short f = 40, g = 50;
    int h = sum(f, g);
    return 0;
}

 

네임스페이스(Namespace)

  • 어떤 프로젝트에서 A와 B가 구현한 코드를 합치는데 동일한 이름의 변수나 함수가 있을 경우, 기본 타입을 확장해서 구분해줄 필요가 있음. C언어에서는 이런 문제를 해결할 수 없으나, C++에서는 네임스페이스를 이용해서 해결 가능.

  • 네임스페이스는 namespace 이름 { ... } 으로 정의하며, 중첩해서 사용 가능.

  • 네임스페이스 A에 있는 변수 a에 접근하려면, 범위 지정 연산자(::)를 사용해야 함. A::a

  • 표준 라이브러리의 네임스페이스는 std로, cin/cout 같은 표준 입출력 사용 시 std::cin 또는 std::cout으로 접근해야 함.

  • 네임스페이스를 매번 적기 귀찮을 경우, using namespace 이름; 을 헤더파일 밑에 선언해주면 됨.

    • 표준 라이브러리 사용 시, using namespace std; 를 많이 사용함.

#include <iostream>
using namespace std;

namespace A {
    void print() {
        cout << "A's print()" << endl;
    }
}

namespace B {
    void print() {
        cout << "B's print()" << endl;
    }
}

int main() {
    A::print(); // A's print()
    B::print(); // B's print()
    
    return 0;
}

 

 

파일 시스템

  • 파일(file) 종류

    • 보통 파일(regular file or normal file): 가장 일반적인 것으로, 데이터가 들어 있는 파일

    • 디렉터리(directory): 보통 파일들을 담아 둘 수 있는 파일

    • 심볼릭 링크(symbolic link or soft link): 다른 파일을 참조만 하는 파일로, 윈도우의 바로가기 개념과 유사하다. 심볼릭 링크 파일을 열면 커널이 자동으로 연결된 파일을 열어 준다. 다른 파일에 대한 별명과 같은 개념이다. 반대로 하드 링크(hard link)도 존재하는데, 이는 완전한 복사를 의미하므로 원본을 지워도 링크로 파일을 열 수 있지만, 심볼릭 링크는 원본을 지우면 파일을 열 수 없다. >> 명령어: ln -s [원본 파일 경로] [링크 파일 경로]

    • 디바이스 파일(device file): 디바이스(하드웨어)를 파일로 표현한 것으로, 커널은 하드웨어와 소통하기 위해 디바이스 드라이버를 이용해서 디바이스 파일에 접근한다. 예를 들어, df -h를 이용해서 디스크 정보를 출력해보면 파일 시스템 필드에 디바이스 파일 이름이 보일 것이다. /dev/sda 는 첫 번째 SSD 또는 HDD를 의미한다. 이 파일에 접근하면 SDD나 HDD에 기록된 데이터를 조작할 수 있다.

      디바이스 파일은 다루는 하드웨어에 따라 문자 디바이스 파일(character device file)블록 디바이스 파일(block device file)이 있다. 차이점은 원하는 시점에 원하는 곳에 접근할 수 있는지다. 예를 들어, SSD와 HDD는 대표적인 블록 디바이스다. 그리고 프린터나 모뎀은 문자 디바이스다.

      그러나 디바이스 파일 중에 /dev/null 의 경우 내용이 비어 있고 무엇을 써도 공중으로 사라져버린다. 이는 윈도우에서 휴지통의 개념과 유사한데, 리눅스 쉘에서 표준 입출력 및 오류를 보고 싶지 않을 때 /dev/null 로 출력해서 없앨 수 있다. 예를 들어보면, "ls -l > /dev/null" 을 쉘에 입력하면 현재 파일의 목록들은 뜨지 않는다. 파일 설명자를 이용해서 특정 방향을 지정해서 무시할 수도 있다.
      "[커맨드] 2 > /dev/null" 과 같이 입력하면 표준 오류 출력만 무시한다. 참고로 0은 표준 입력, 1은 표준 출력, 2는 표준 오류 출력이다.
      "[커맨드] > /dev/null 2&1" 을 입력하면 표준 출력, 오류 출력 둘 다 무시한다는 의미이다.
      "[커맨드1] > [커맨드2]" 의미는 커맨드1의 쉘 명령어의 결과를 커맨드2로 전달하는 것을 의미한다. "<" 도 있다. 참고로 ">>" 는 커맨드2가 파일일 경우 새로 쓰는 것이 아니라 내용을 추가하는 것을 의미한다.

      이처럼 실제 물리적 디바이스가 없는 파일로는 /dev/random 이 있다. 이 파일은 랜덤 값을 추출할 때 참조하는 파일이다.

    • 명명된 파이프(named pipe): 프로세스 간 통신(IPC)에 사용하는 파일이다. FIFO 라고도 불린다.

  • 파일의 메타 정보: 파일 유형, 권한, 크기, 마지막 수정 시간 등이 있다.

    • "ls -l" 을 쳐보면 다음과 같은 결과를 얻는다. 7개의 항목을 볼 수 있다.

      • drwx------+ 3 ke2ek staff 96 Oct 23 21:15 Desktop

      • "drwx------+" 에서 첫 글자 d는 디렉터리임을 나타내는 파일 종류이고, 그 다음부터는 파일의 권한을 의미한다. 소유자만 읽기/쓰기/실행 권한만 가지고 있음을 알 수 있다. (r: 읽기, w: 쓰기: x: 실행)

      • "3" 하드 링크 수

      • "ke2ek" 소유자 이름

      • "staff" 그룹

      • "96" 파일 크기

      • "Oct 23 21:15" 갱신 시각

      • "Desktop" 파일명

  • 파일 시스템과 마운트

    • 리눅스에서는 파일 시스템 모두 트리 구조로 관리가 되며, 보통 SSD나 HDD, USB 메모리처럼 물리적인 기억 장치에 파일 시스템이 존재하는데, USB 같은 외부 장치는 마운트(mount)하여 파일 시스템에 연결할 수 있다.
      "mount -t ext4" 명령어로 현재 사용중인 파일 시스템을 확인할 수 있다. 아마 파티션 개수만큼 나올 것이다. ext4는 현재 리눅스에서 가장 일반적으로 사용되는 파일 시스템이다.
    • 마운트된 모든 파티션 보기: mount
    • 마운트 하기: mount [디바이스 파일] [마운트 할 경로]
    • 마운트 해제: umount [디바이스 파일] 또는 umount [마운트 지점]
    • 특정 옵션을 주어 파티션 다시 마운트: mount -o remount,rw [디바이스 파일]

메모리 관리

  • 가상 메모리(Virtual Memory)는 제한적인 물리 메모리를 프로세스가 모두 사용할 수 있도록 프로세스마다 독립적으로 물리 메모리를 사용하고 있음을 추상화한 것이다. 즉, 메모리를 필요로 하는 프로세스 사이에서 물리 메모리를 공유하도록 하여, 시스템이 실제 가진 것보다 더 많은 메모리를 가진 것처럼 보이게 한다.
  • 메모리 관리 서브 시스템
    • 넓은 주소 공간: 가상 메모리는 실제 물리 메모리보다 몇 배나 더 클 수 있다.
    • 보호: 시스템의 각 프로세스는 각자의 독립된 주소 공간을 갖는다. 가상 주소공간은 서로 완벽하게 분리되어 있어 어떤 응용 프로그램을 실행하는 프로세스는 다른 프로세스에 영향을 줄 수 없다.
    • 메모리 매핑: 실행 이미지와 데이터 파일을 프로세스의 주소공간에 매핑하기 위해 사용된다. 메모리 매핑에서 파일의 내용은 프로세스의 가상 주소공간에 직접 연결된다.
    • 공정한 물리적 메모리 할당: 메모리 관리 서브시스템은 시스템에서 실행중인 프로세스들이 서로 공정하게 물리적 메모리를 공유할 수 있게 한다.
    • 공유 가상 메모리: 프로세스들이 메모리를 공유해야 하는 순간이 있다. 각 프로세스의 가상 주소공간에 복사본을 각각 가지는 것이 아니라 물리적 공간에 하나의 복사본을 가지고 프로세스의 가상 주소가 해당 물리 주소를 참조하도록 한다. 리눅스의 동적 라이브러리는 여러 프로세스가 실행 코드를 공유하는 대표적인 예이다.
  • 요구 페이징(Demand Paging): 실제 가상 메모리보다 훨씬 적은 물리 메모리만 있기 때문에, 물리 메모리가 비효율적으로 사용되지 않도록 해야 한다. 이를 위해 실행 중인 프로그램이 현재 사용하는 부분만 물리 메모리에 올리는 방법이 있다. 즉, 실행 시 접근되는 부분만 메모리에 읽어들이는 기법이다.
  • 스와핑(Swapping): 비어 있는 물리 메모리 공간이 없다면, 기존의 것들과 교체해야 한다. 여기서 기존에 올라간 메모리에 값이 쓰이지 않았다면 그대로 제거하면 되지만, 값이 쓰였다면 나중에 다시 사용될 수 있도록 해당 공간(=더티 페이지=dirty page)을 저장해놔야 한다. 이를 메모리에서 제거할 때 스왑 파일(swap file)이라는 특별한 파일에 저장한다. 참고로 스왑 파일에 접근하는 것은 프로세서나 물리 메모리에 접근하는 것보다 시간이 오래 걸린다. 리눅스에서는 제거될 페이지를 공정하게 선택하기 위해 가장 최근에 사용된 페이지 수명 기법(LRU Algorithm)을 사용한다.
  • 페이지 테이블(Page Table): 가상 주소공간에서는 페이지(page) 단위로 메모리를 관리한다. 보통 4KB이다. 예를 들어, 어떤 프로세스가 10KB 메모리가 필요하다면 페이지 2개하고 2KB가 아니라 페이지 3개를 할당해야 한다. 여기서 내부 단편화가 발생한다. 물리 주소공간에서는 프레임(frame) 단위로 메모리를 관리한다. 가상 주소로 실제 물리 주소를 알아내기 위해 페이지 번호페이지 내 오프셋을 이용해서 페이지 테이블을 참조한다. 페이지 번호로 프레임 번호를 알아낸 뒤, 프레임 내에서 오프셋을 적용하면 실제 물리 메모리 주소를 알아낼 수 있다. 페이지 테이블은 크기가 매우 커서 이를 효율적으로 저장하고 관리하는 방법들이 많이 연구되었으며, 리눅스는 3단계 페이지 테이블을 가정한다.
  • 메모리 매핑(Memory Mapping): 주소 변환을 하기 전에 실행 이미지를 프로세스의 가상 주소공간으로 가져와야 한다. 실행 이미지가 링크해서 사용하는 공유 라이브러리도 마찬가지다. 리눅스는 실행파일을 실제 물리적 메모리에 가져오는 대신, 단지 프로세스의 가상 메모리와 연결만 시킨다. 그리고 응용 프로그램이 실행되면서 프로그램의 일부가 참조됨에 따라, 실행 이미지로부터 해당하는 이미지 부분을 메모리로 가져온다.(by Demand Paging) 이렇게 이미지를 가상 주소공간으로 연결하는 것을 메모리 매핑이라 한다.
    • 프로세스의 메모리 맵 보기: cat /proc/<PID>/maps
    • 현재 실행중인 프로세스의 메모리 맵 보기: cat /proc/self

프로세스

  • 프로세스실행 중인 프로그램을 의미하고, 프로그램이란 파일 형태로 존재하는 실행 가능한 파일을 의미한다. 즉, 하나의 프로그램으로 새로운 프로세스를 계속 만들 수 있다.

  • 프로세스 목록 보기: ps -ef

    • UID PID PPID C STIME TTY TIME CMD
      501 478 477 0 6:20AM ttys004 0:00.31 -zsh

    • 각 프로세스는 식별자로 프로세스 ID를 가진다. 예를 들어, 프로세스를 강제로 종료하려면 "kill -9 [PID]"를 입력한다.

    • 또한, 프로세스는 부모-자식 관계를 가진다. 부모는 자식 프로세스를 생성하고 종료할 때까지 기다리거나 등 의존적이게 해동할 수 있다.

 

'Programming Language > C in Linux System' 카테고리의 다른 글

커맨드라인 인자, GCC 컴파일  (0) 2020.11.02

커맨드라인 인자

  • argc: 인자의 개수 (최소 1)
  • argv: 각 인자의 값
  • 바이너리 파일 이름은 첫번째 인자에 해당됨.
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
	int i;
	printf("argc=%d\n", argc);
	for (i = 0; i < argc; i++) {
		printf("argv[%d]=%s\n", i, argv[i]);
	}
	exit(0);
}
  • 실행 결과
    • 쉘 특성 상 문자열을 큰 따옴표(",")로 묶어서 인자로 넘기면 공백도 포함됨.
    • 쉘 특성 상 와일드 카드 사용 시 현재 경로 기준으로 매칭되는 파일 이름을 인자로 넘김.
$ ./args
argc=1
argv[0] = ./args
$ ./args x
argc=2
argv[0]=./args
argv[1]=x
$ ./args x y
argc=3
argv[0]=./args
argv[1]=x
argv[2]=y
$ ./args x y z
argc=4
argv[0]=./args
argv[1]=x
argv[2]=y
argv[3]=z
$ ./args "x y z"
argc=2
argv[0]=./args
argv[1]=x y z


$ ./args *.c
argc=3
argv[0]=./args
argv[1]=hello.c
argv[2]=args.c
$ ./args "*.c"
argc=2
argv[0]=./args
argv[1]=*.c

 

GCC 컴파일러

gcc -g -W -Wall -o [실행파일] [C파일]
  • -o 는 목적 파일 이름 주는 옵션
  • -Wall 은 소스 수준의 정보를 디버깅 시 이용하기 위한 옵션.
  • 위 두 개는 항상 옵션으로 줄 것
  • 참고
 

gcc와 make 강좌: gcc 강좌

디렉토리명은 -I 라는 문자 바로 다음에 붙여서 씁니다. 즉 -I <디렉토리>라는 식이 아니라 -I<디렉토리> 입니다. 또한 유닉스에 있어 표준 헤더 화일 디렉토리는 /usr/include 라는 사실을 기억하시기

doc.kldp.org

man 명령어

리눅스 라이브러리 함수의 정보가 필요한 경우 매우 유용한 명령어

man strlen

 

+ Recent posts