[01] C++ 기초


목차

  1. 1. Namespace
  2. 2. 기초문법
  3. 3. Reference
  4. 4. New, Delete
  5. 5. Class

1. Namespace

C++에서 이름 공간(namespace)은 변수, 함수, 클래스와 같은 이름들이 어느 소속에 속해 있는지 구분하기 위한 개념으로 같은 이름이라도 서로 다른 namespace에 속하면 전혀 다른 것으로 취급된다.

  • std namespace
    std::cout << "Hello World";
  • cout, cin, string과 같은 객체들은 모두 std라는 Namespace공간안에 포함되어있다.
  • ::범위 지정 연산자(scope resolution operator) 로, cout이 std라는 이름 공간에 속해있음을 의미한다.

Sample Code

같은 함수명 print를 호출해도 Namespace에 따라 다르게 호출된다는 것을 알 수 있다.

#include <iostream>

namespace A{
    void print() {
        std::cout << "Hello from A!" << std::endl;
    }
}

namespace B{
    void print() {
        std::cout << "Hello from B!" << std::endl;
    }
}

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

정리하며

참고로 using namespace std; 와 같이 어떠한 이름 공간을 사용하겠다라고 선언하는 것은 권장되지 않는다. 왜냐하면 std 에 이름이 겹치는 함수를 만들게 된다면, 오류가 발생하기 때문이다.

2. 기초문법

1. For

기본적인 For문 문법

#include <iostream>

int main(){
    int i;
    for (i =0; i < 10; i++){
        std::cout << i << std::endl;
    }
}

2. while

#include <iostream>

int main(){
    int i=0;
    while (i < 10){
        std::cout << i << std::endl;
        i++;
    } 
}

2-1. do while

#include <iostream>

int main() {
    int i = 1;

    do {
        std::cout << i << std::endl;
        i++;
    } while (i <= 5);

    return 0;
}

3. if else

#include <iostream>

int main(){
    int i;

    std::cout << "Enter a number: ";
    std::cin >> i; // 입력받기

    if (i < 10){
        std::cout << "i는 10보다 작습니다." << i << std::endl;
    }else if(i > 10){
        std::cout << "i는 10보다 큽니다." << std::endl;
    }
}

4. switch

#include <iostream>

int main(){
    int i;

    std::cout << "Enter a number: ";
    std::cin >> i;

    switch(i) {
        case 1:
            std::cout << "You entered one." << std::endl;
            break;
        case 2:
            std::cout << "You entered two." << std::endl;
            break;
        case 3:
            std::cout << "You entered three." << std::endl;
            break;
        default:
            std::cout << "You entered a number other than 1, 2, or 3." << std::endl;
            break;
    }
    return 0;
}

3. Reference

레퍼런스(reference)는 기존 변수에 대한 또 다른 이름(별명)을 의미한다.

새로운 변수를 만드는 것이 아니라 기존 변수와 동일한 메모리를 공유하는 이름을 하나 더 만드는 것이다.

레퍼런스는 반드시 선언과 동시에 초기화되어야 한다.

Sample code 1

기본적인 reference code이다.

#include <iostream>

void swap(int& a, int& b){
    int temp = a;
    a = b;
    b = temp;
}


int main() {
    int x = 5, y = 10;

    swap(x, y);

    std::cout << "x: " << x << ", y: " << y << std::endl;

    return 0;
}

Sample code 2

배열에 대한 reference code이다.

배열 레퍼런스를 사용할 때는 크기까지 포함해서 타입이 완전히 일치해야 한다.

#include <iostream>

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

    int (&refArr)[3] = arr;  // 배열 전체에 대한 레퍼런스

    refArr[0] = 10;  // 원본 배열 수정

    std::cout << arr[0] << std::endl;  // 10

    return 0;
}

reference와 pointer차이

  • reference는 선언과 동시에 초기화 해야한다.
  • 포인터는 변수의 주소를 재할당 받을 수 있지만 레퍼런스는 한 번 초기화되면 다른 변수로 재할당 받을 수 없다.
  • 포인터 변수는 별도의 메모리 공간을 가지지만 레퍼런스는 변수의 별명으로 별도의 메모리 공간을 갖지 않고 원본 변수의 주소공간을 갖는다.
  • 레퍼런스는 ref.val과 같이 . 연산자로 접근한다.

4. New, Delete

  • C 언어의 malloc과 free 함수 대신, C++에서는 언어 차원에서 제공하는 new와 delete 연산자를 사용하여 힙(Heap) 공간에 메모리를 할당하고 해제한다.
  • new로 할당한 메모리만 delete로 해제할 수 있으며, 일반 지역 변수를 delete 할 수 없다.

가장 기본예제 코드는 아래와 같다.

sample code 1

#include <iostream>

int main() {
  int* p = new int; // int 크기의 공간 할당
  *p = 10;

  std::cout << *p << std::endl; // 10 출력

  delete p; // 할당된 공간 해제
  return 0;
}

sample code 2

  • 배열의 크기를 입력으로 받고 그 크기만큼 배열을 구성하는 코드이다.
  • 배열을 정적할당하는 경우에는 컴파일 전에 정해줘야 하며 런타임 환경에서는 배열의 크기를 변경할 수 없는 반면, 동적할당인 경우 런타임내에서 배열의 크기가 결정된다는 차이점이 있다.
  • 또한 정적할당의 경우 stack에 메모리 공간이 할당되며 동적할당은 heap에 할당되어 명시적으로 delete를 통해 배열을 소멸시켜야 한다.
    • 동적 배열할당
      void func() {
          int* arr = new int[10];
      } // ❌ 삭제 안 됨 → 메모리 누수
    • 정적 배열할당
      void func() {
          int arr[10];
      } // 여기서 자동 삭제
#include <iostream>

int main() {
  int arr_size;
  std::cout << "array size : ";
  std::cin >> arr_size;
  
  // 크기가 arr_size인 int 배열 동적 할당
  int *list = new int[arr_size]; 
  
  for (int i = 0; i < arr_size; i++) {
    std::cin >> list[i];
  }
  
  for (int i = 0; i < arr_size; i++) {
    std::cout << i << "th element of list : " << list[i] << std::endl;
  }
  
  // 배열 형태의 메모리 해제
  delete[] list; 
  return 0;
}

5. Class

  • 캡슐화
    • 데이터를 숨기고 (private) 접근을 제한하며 정해진 인터페이스(함수)를 통해서만 다루게 하는 것

sample code 1

  • private으로 외부에서 값 변경을 막음
  • 함수를 통해서 값을 변경가능 하도록하는 캡슐화
#include <iostream>

class Animal {
private:
    int food;
    int weight;

public:
    void set_animal(int _food, int _weight) {
        food = _food;
        weight = _weight;
    }

    void increase_food(int inc) {
        food += inc;
        weight += inc / 3;
    }

    void view_stat() {
        std::cout << "이 동물의 food   : " << food << std::endl;
        std::cout << "이 동물의 weight : " << weight << std::endl;
    }
};

int main() {
    Animal animal;
    animal.set_animal(100, 50);
    animal.increase_food(30);
    animal.view_stat();
}

함수의 오버로딩

같은 이름의 함수 여러 개 정의 가능하되 그 구분을 매개변수(타입/개수)로 한다.

sample code 1

#include <iostream>

void print(int x) {
    std::cout << "int: " << x << std::endl;
}

void print(double x) {
    std::cout << "double: " << x << std::endl;
}

void print(char x) {
    std::cout << "char: " << x << std::endl;
}

int main() {
    print(10);      // int 버전
    print(3.14);    // double 버전
    print('A');     // char 버전
}

생성자

아래 처럼 동물이라는 인스턴스를 만들면 초깃값을 항상 함수를 통해서 지정해줘야 한다는게 매우 번거로움으로 이를 해결하기 위해 사용한다.

Animal a;
a.set_animal(100, 50);  // 따로 초기화
  • 생성자(함수) 이름 = 클래스 이름 으로 설정한다.
  • return 타입 없으며 객체 만들 때 자동 실행한다.
#include <iostream>

class Animal {
private:
    int food;
    int weight;

public:
    Animal(int f, int w) {   // 생성자
        food = f;
        weight = w;
    }

    void show() {
        std::cout << food << ", " << weight << std::endl;
    }
};

int main() {
    Animal a(100, 50);  // 생성자 자동 호출
    a.show();
}
  • Default 생성자
    • 사용하는 이유
      • 데이터가 준비되지 않은 상태에서도 객체를 먼저 만들어두어야 할 때가 있다.
      • 예를 들어, 나중에 사용자로부터 입력을 받아 값을 채울 객체라면, 일단 빈 객체를 생성해 두어야 함으로 사용된다.
    • 특징
      • 사용자가 생성자를 하나도 만들지 않으면, 컴파일러가 아무 일도 하지 않는 "디폴트 생성자"를 자동으로 만들어준다.
      • 하지만, 사용자가 인자가 있는 생성자를 하나라도 만드는 순간, 컴파일러는 "아, 이 프로그래머는 특별한 생성 방식을 원하는구나"라고 판단하여 디폴트 생성자를 더 이상 자동으로 만들지 않는다.
class Animal {
public:
    Animal() {   // 디폴트 생성자
        std::cout << "기본 생성자 호출\n";
    }

    Animal(int f) {
        std::cout << "일반 생성자 호출\n";
    }
};

int main() {
    Animal a;        // 디폴트 생성자
    Animal b(10);    // 일반 생성자
}

소멸자

객체가 사라질 때 자동으로 호출되는 함수

클래스 내부에서 new를 통해 할당한 메모리는 힙(Heap) 영역에 저장된다.

객체(스택에 있는 포인터나 변수)는 함수가 끝나면 자동으로 사라지지만, 힙에 할당된 메모리는 누군가 delete를 해주지 않으면 프로그램이 끝날 때까지 메모리를 차지하고 있다. (이것을 메모리 누수, Memory Leak라고 함)

  1. 함수 실행 중: 스택(Stack)에 a라는 객체가 생기고, 그 안에 포인터 변수 arr이 힙(Heap)의 배열 주소를 가리키고 있다.
  2. 함수 종료 직후(소멸자가 없다면): 스택에 있던 객체 a와 포인터 변수 arr은 자동으로 삭제됩니다.
  3. 결과: 힙(Heap)에 생성된 new int[10] 배열은 여전히 메모리에 남아있지만, 그 주소를 알고 있던 arr이 사라졌기 때문에 우리는 이제 그 배열을 지울 방법이 영원히 사라진다.
class MyArray {
private:
    int* arr;

public:
    // 생성자: 메모리를 할당받음
    MyArray(int size) {
        arr = new int[size]; 
        std::cout << "메모리 할당됨!" << std::endl;
    }

    // 소멸자: 할당받은 메모리를 해제함
    ~MyArray() {
        delete[] arr; 
        std::cout << "메모리 해제됨 (소멸자 호출)!" << std::endl;
    }
};

void function() {
    MyArray a(10); // 객체 a 생성 (생성자 호출)
} // 함수가 끝나면서 a가 사라짐 -> 소멸자 자동 호출!

복사 생성자

복사 생성자는 객체가 복사될 때 어떻게 동작할지 정의하는 함수다.

  • 디폴트 복사 생성자는 단순히 값만 복사하는 얕은 복사를 한다.
  • 포인터나 동적 할당(new)을 포함한 클래스라면 반드시 직접 깊은 복사를 수행하는 복사 생성자를 만들어야 한다.
  • 그렇지 않으면 이중 해제(Double Free) 에러가 난다.

아래 예시는 디폴트 복사 생성자의 예시이다.

#include <iostream>

class Test {
public:
    int x;
};

int main() {
    Test a;
    a.x = 10;

    Test b = a;   // 디폴트 복사 생성자 호출

    std::cout << "a.x: " << a.x << std::endl;
    std::cout << "b.x: " << b.x << std::endl;
}

아래는 문제가되는 디폴트 복사 생성자에 대한 예시인데, 따로 복사 생성자를 만들어주지 않아 객체를 복사할 때 얕은 복사를 하게되는 경우 a.p, b.p가 같은 주소값을 가지게되어 나중에 소멸자에 의해 메모리를 free해줄 때 이중 free가 발생하여 문제가되는 예시이다.

따라서 복사 생성자를 만들 때에는 반드시 따로 지정을 해주는 것이 좋다.

#include <iostream>

class Test {
public:
    int* p;

    Test(int value) {
        p = new int(value);
    }

    ~Test() {
        std::cout << "delete p\n";
        delete p;
    }
};

int main() {
    Test a(10);
    Test b = a;   // ❗ 디폴트 복사 생성자 (얕은 복사)

    std::cout << *a.p << std::endl;
    std::cout << *b.p << std::endl;
}

아래는 올바른 예시코드이다.

  • 복사 생성자는 일반 생성자와 똑같이 생겼지만 입력이 객체인 경우이며 자기 자신의 타입 객체를 const 참조로 받는 생성자로 받는다. (규칙임)
Test(int value);        // 일반 생성자
Test(const Test& t);    // 복사 생성자
#include <iostream>

class Test {
public:
    int* p;

    Test(int value) {
        p = new int(value);
    }

    Test(const Test& other) {
        p = new int(*other.p); # 힙에 int 1개를 할당하고 그 값을 10으로 초기화한다
    }

    ~Test() {
        delete p;
    }
};

int main() {
    Test a(10);
    Test b = a;

    std::cout << "a value: " << *a.p << ", address: " << a.p << std::endl;
    std::cout << "b value: " << *b.p << ", address: " << b.p << std::endl;
}

Initalist/ Const/ Static

  • initalize list: 성자 뒤에 :를 붙여 멤버 변수를 초기화하는 문법
  • const 멤버 변수: 객체가 만들어질 때 값이 정해지면, 그 이후로는 절대로 수정할 수 없는 변수
    • 보통 고유 ID 등으로 사용
  • static: 객체(인스턴스)마다 각자 가지는 것이 아니라, 해당 클래스로 만든 모든 객체가 공유하는 변수/함수
#include <iostream>
#include <string>

class Warrior {
private:
    // 2. const 멤버: 전사의 고유 ID는 태어날 때 정해지면 바꿀 수 없음
    const int id;
    std::string name;
    int hp;

public:
    // 3. static 멤버: 생성된 모든 전사의 총 숫자를 공유함
    static int total_warriors;

    // 1. 초기화 리스트 사용: : id(_id), name(_name), hp(_hp)
    Warrior(int _id, std::string _name, int _hp) 
        : id(_id), name(_name), hp(_hp) // 생성과 동시에 초기화!
    {
        // id = _id; // 에러 발생! const 멤버는 여기서 대입할 수 없음
        
        total_warriors++; // 새로운 전사가 생길 때마다 공유 변수 증가
        std::cout << name << " 전사 생성 (ID: " << id << ")" << std::endl;
    }

    // static 함수: 객체 없이도 호출 가능하며 static 멤버만 다룰 수 있음
    static void showTotal() {
        std::cout << "현재 총 전사 수: " << total_warriors << std::endl;
    }

    void status() {
        std::cout << "[" << name << " HP: " << hp << "]" << std::endl;
    }
};

// static 멤버 변수의 실제 메모리 할당 및 초기화 (클래스 외부에서 필수)
int Warrior::total_warriors = 0;

int main() {
    Warrior::showTotal(); // 객체가 없어도 호출 가능 (출력: 0)

    Warrior w1(101, "아라곤", 100);
    Warrior w2(102, "레골라스", 80);

    w1.status();
    w2.status();

    Warrior::showTotal(); // 모든 객체가 공유하므로 출력: 2

    // w1.id = 999; // 에러! const 멤버는 수정 불가능
    
    return 0;
}

1. static 멤버 변수 (정적 멤버 변수)

  • C++에서 static 키워드가 클래스 멤버에 붙으면, 그것은 "개별 객체의 것"이 아니라 "클래스 자체의 것"이 된다는 뜻
  • 일반적인 변수는 객체가 생성될 때마다 메모리에 새로 만들어지지만, static 변수는 프로그램이 시작될 때 단 한 번만 메모리에 올라가며 모든 객체가 이를 공유한다.
    • 공유 메모리: 해당 클래스로 만든 모든 객체가 하나의 변수를 같이 씁니다.
    • 생명 주기: 객체가 생성되기 전부터 이미 존재하며, 프로그램이 종료될 때 사라집니다.
    • 외부 초기화 필수: 클래스 내부에서는 "이런 변수가 있다"라고 선언만 하고, 클래스 밖에서 반드시 실제 메모리를 할당(초기화) 해줘야 한다. (단, const static 은 내부 초기화 가능)
    • 따라서  static 멤버 변수는 생성자 및 클래스내에서 “초기화”할 수 없고 값 변경은 가능하다.
  • 사용 예시: 게임의 총 플레이어 수, 생성된 객체의 고유 ID 카운터, 모든 유닛이 공유하는 업그레이드 수치 등.
#include <iostream>

class Counter {
public:
    int count = 0;          // 일반 멤버 변수 (객체마다 별도)
    static int staticCount; // static 멤버 변수 (모든 객체가 공유)

    Counter() {
        count++;
        staticCount++;
    }
};

// 클래스 외부에서 반드시 초기화 필요! (중요)
int Counter::staticCount = 0;

int main() {
    Counter c1;
    Counter c2;
    Counter c3;

    // c1, c2, c3는 각자 자신만의 count를 가짐 (모두 1)
    std::cout << "c1.count: " << c1.count << std::endl; 

    // c1, c2, c3는 하나의 staticCount를 공유함 (총 3)
    // 객체 이름(c1)으로도 접근 가능하지만, 클래스 이름(Counter::) 권장
    std::cout << "Counter::staticCount: " << Counter::staticCount << std::endl; 

    return 0;
}

2. static 멤버 함수 (정적 멤버 함수)

객체를 생성하지 않고도 클래스 이름만으로 직접 호출할 수 있는 함수

  • 객체 독립적: Counter::show()와 같이 호출하며, 특정 객체에 소속되어 있지 않습니다.
  • this 포인터 없음: 객체가 생성되기도 전에 존재할 수 있으므로, "현재 객체"를 가리키는 this 포인터를 사용할 수 없다.
  • 접근 제한: static 멤버 변수나 다른 static 함수만 호출할 수 있습니다. (일반 멤버 변수는 "어느 객체의 것인지" 알 수 없기 때문에 접근이 불가능하다.)
  • 사용예시: 특정 데이터를 저장할 필요 없이, 입력값에 대해 계산 결과만 내어주는 도구(Utility) 성격의 함수를 만들 때 사용
class Math {
public:
    static int count;
    int value = 10;

    // static 함수
    static int add(int a, int b) {
        // value = 20; // 에러! static 함수는 일반 멤버 변수에 접근 불가
        count++;       // 가능! static 변수에는 접근 가능
        return a + b;
    }
};

int Math::count = 0;

int main() {
    // 객체 생성 없이 클래스 이름으로 바로 호출
    int result = Math::add(5, 10); 
    std::cout << "결과: " << result << std::endl;
}

6. Explicit/mutable

기본 상태 (implicit): 정수 10을 넣었네? 대충 MyString 객체로 바꿔서 처리한다.

explicit 상태: 정수 10? 이건 MyString이 아니야. 정확하게 MyString(10)이라고 써야만 인정한다

#include <iostream>
#include <string>

class MyString {
public:
    // explicit이 없다면? 컴파일러는 숫자 10을 MyString(10)으로 자동 변환함
    explicit MyString(int capacity) {
        std::cout << capacity << " 크기의 메모리 예약됨\n";
    }

    MyString(const char* str) {
        std::cout << str << " 문자열로 객체 생성\n";
    }
};

void printString(MyString s) {
    // 처리 로직
}

int main() {
    MyString s1("Hello"); // OK
    
    // explicit 덕분에 아래 코드는 컴파일 에러가 발생함
    // MyString s2 = 10;    // 에러! (정수가 객체로 자동 변환되는 것 방지)
    
    MyString s3(10);        // OK (명시적 호출)
    
    // printString(20);     // 에러! (함수 인자 전달 시 암시적 변환 방지)
    printString(MyString(20)); // OK
}

Mutable은 const예약어를 가진 함수내에서도 값을 변경 가능하도록 한다.

const 예약어가 붙은 함수 안에서는 그 객체의 어떤 멤버 변수도 변경할 수 없다.여

class GirlGroup {
private:
    string groupName = "NewJeans";
    // mutable이 없으면 누적 조회수를 올릴 수 없음!
    mutable int viewCount = 0; 

public:
    // const: 그룹 이름을 바꾸지 않겠다는 약속
    string getName() const {
        viewCount++; // 약속 위반 같지만, mutable이라 허용됨!
        return groupName;
    }

    int getViewCount() const {
        return viewCount;
    }
};

7. 연산자 오버로딩

기존의 연산자(+, -, *, / 등)가 내가 만든 클래스 객체들 사이에서도 동작하게 만드는 것이다

예를 들어, 사과 + 사과를 하면 에러가 나지만, 연산자 오버로딩을 쓰면 사과 객체끼리 더했을 때 '무게'나 '가격'이 합쳐지도록 정의할 수 있다.

#include <iostream>

class Point {
private:
    int x, y;

public:
    Point(int x = 0, int y = 0) : x(x), y(y) {}

    // '+' 연산자 오버로딩 (멤버 함수 방식)
    // p1 + p2를 하면, p1의 멤버 함수인 operator+ 가 호출되고 p2가 인자로 들어옴
    Point operator+(const Point& other) const {
        // 새로운 객체를 만들어서 반환 (기본 객체는 const라 안 변함)
        return Point(this->x + other.x, this->y + other.y);
    }

    // 출력을 편하게 하기 위한 함수
    void print() const {
        std::cout << "(" << x << ", " << y << ")" << std::endl;
    }
};

int main() {
    Point p1(10, 20);
    Point p2(5, 15);

    // 연산자 오버로딩 덕분에 객체끼리 '+' 가능!
    Point p3 = p1 + p2; 

    p3.print(); // 출력: (15, 35)

    return 0;
}

Friend

원래 클래스의 private 멤버는 외부에서 절대 건드릴 수 없지만, friend로 등록된 대상(함수나 다른 클래스)은 private까지 자유롭게 드나들 수 있는 권한을 가진다.

#include <iostream>
#include <string>

class Person {
private:
    std::string secret = "사실 난 어피치를 좋아해.";

public:
    // BestFriend 함수를 내 친구로 등록!
    // 이제 이 함수는 나의 private 영역에 접근 가능함.
    friend void BestFriend(const Person& p);
};

// 클래스 외부 함수
void BestFriend(const Person& p) {
    // 원래는 p.secret에 접근하면 에러가 나지만, friend라서 가능!
    std::cout << "친구만 아는 비밀: " << p.secret << std::endl;
}

int main() {
    Person me;
    BestFriend(me); // 출력: 친구만 아는 비밀: 사실 난 어피치를 좋아해.
    return 0;
}

8. 상속

상속을 사용하면 똑같은 코드를 여러 번 쓸 필요 없이, 공통된 특징을 가진 '부모'를 만들어두고 '자식'들이 이를 재사용하게 된다

#include <iostream>
#include <string>

// 1. 부모 클래스 (Base Class)
class Robot {
protected: // 자식에게는 허용, 외부에서는 차단 (private보다 조금 너그러움)
    std::string name;

public:
    Robot(std::string n) : name(n) {}

    void move() {
        std::cout << name << "이(가) 이동합니다.\n";
    }
};

// 2. 자식 클래스 (Derived Class)
// : public Robot -> "Robot의 기능을 공공연하게 물려받겠다"는 뜻
class BattleRobot : public Robot {
public:
    // 자식 생성자에서 부모 생성자를 먼저 호출해줘야 함
    BattleRobot(std::string n) : Robot(n) {}

    void attack() {
        std::cout << name << "이(가) 레이저 공격을 합니다! 빔~~\n";
    }
};

int main() {
    // 자식 객체 생성
    BattleRobot terminator("T-800");

    // 부모로부터 물려받은 기능 사용
    terminator.move(); 

    // 자기만의 새로운 기능 사용
    terminator.attack();

    return 0;
}