1. C++ 기본 - [변수, namespace, C#과 C++에서의 static]
2. C++ 참조자 - [참조자(reference), 레퍼런스의 배열과 배열의 레퍼런스,  지역변수의 레퍼런스를 리턴?]
3. new, delete

1. C++ 기본

변수

→ 변수는 이름을 보았을 때, 무엇을 하는지 확실히 알 수 있어야 한다.

구글의 경우 변수 이름 내 띄어쓰기를 모두 _로 구분하는 방식을 취하며, 함수의 이름의 경우 첫 글자로 대문자를 사용하는 방식을 사용한다.

언리얼에서는, 파스칼케이싱(합성어의 첫 자를 대문자)을 사용한다.

 

+) 클래스 네이밍에는 접두사를 앞에 붙인다.

 

namespace

→ 흔히 사용하는 cout의 앞에 std는 C++ 표준 라이브러리의 모든 함수, 객체 등이 정의된 이름 공간(namespace)이다. 다른 사람들이 쓴 코드를 가져다 쓰는 경우가 많아지며, 중복된 이름을 가진 함수가 많아졌고, 따라서, C++에서는 이를 구분하기 위해 소속된 namespace가 다르면 다른 것으로 취급하게 되었다.

 

  • Header1.h, Header2.h
// header1.h 의 내용
namespace Header1 {
	int Foo();
	void Bar();
}
// header2.h 의 내용
namespace Header2 {
	int Foo();
	void Bar();
}

위 아래 코드에서의 foo는 다르다.

 

  • 사용 예시
  • 1번째
#include "Header1.h"
#include "Header2.h"

namespace Header1 {
	int Func() {
	  Foo();           // 알아서 Header1::Foo()가 실행
	  Header2::Foo();  // Header2::Foo()가 실행
	}
}  // namespace Header1

 

  • 2번째
#include "Header1.h"
#include "Header2.h"

using namespace Header1;
int main() {
  Foo();  // Header1 에 있는 함수를 호출
  Bar();  // Header1 에 있는 함수를 호출
}

 

  • 3번째
#include "Header1.h"
#include "Header2.h"
using namespace Header1;

int main() {
  Header2::Foo();  // Header2 에 있는 함수를 호출
  Foo();           // Header1 에 있는 함수를 호출
}

 

  • Tip

C++ 표준 라이브러리는 매우 거대하고, 수많은 함수들이 존재하고 있다. 또, std에는 매번 많은 함수들이 새로이 추가되므로, C++ 버전이 바뀔 때마다 기존에 잘 작동하던 코드의 충돌이 발생할 수 있다. 따라서, 권장하는 방식은 using namespace std; 사용하기보다, std::를 앞에 붙여 std의 namespace 함수라는 것을 명시하는게 좋다. 또, 작성하는 코드는 본인만의 이름 공간에 넣어 혹시 모를 이름 충돌로부터 보호하는 것도 권장되는 방법이다.

 

  • namespace에 굳이 이름을 설정하지 않아도 된다.

 

→ 여기서 정의된 것들은 해당 파일 안에서만 접근 할 수 있다. static 키워드를 사용한 것과 같은 효과이다.

 

C#과 C++에서의 static

  • static 변수

→ C/C++: 함수가 여러 번 호출되어도, 한 번만 할당되어 값이 지속된다.

선언된 파일 안에서만 접근 가능하고, 다른 파일에서 같은 이름의 변수를 사용해도 서로 간섭하지 않는다.

#include <iostream>
void func() {
    static int x = 0;  // x는 함수가 처음 호출될 때 한 번 초기화됨
    x++;
    std::cout << "x: " << x << std::endl;
}

int main() {
    func();  // 출력: x: 1
    func();  // 출력: x: 2, 이전의 x값이 유지됨
    return 0;
}

 

→ C#: 클래스 내부(모든 변수는 클래스나 구조체 내부에 선언된다.)에서 static 변수를 선언하면 해당 클래스의 인스턴스를 생성하지 않아도 접근 가능하고, 프로그램 전체에서 하나의 인스턴스가 유지된다.

 

한 클래스에 static 변수나 메서드가 public 등 접근 가능한 상태라면, 다른 클래스에서도 클래스 이름을 통해 직접 호출하거나 사용할 수 있다.

// MyClass에 my_static_var이 정의되어있다고 가정
// AnotherClass.cs
public class AnotherClass {
    public void SomeMethod() {
        // 클래스 이름을 사용하여 static 변수에 접근
        int value = MyClass.my_static_var;
        System.Console.WriteLine("my_static_var의 값: " + value);

        // 클래스 이름을 사용하여 static 메서드를 호출
        MyClass.MyStaticMethod();
    }
}

→ 두 언어에서 static 변수는 값이 유지된다는 점에서 공통적이다.

 

static 변수에 대한 정리:

  • C++: 지역적인 상태 유지, 정보 은닉
  • C#: 전역적 접근 및 공용 데이터 관리

 

  • static 함수

→ C/C++: 파일 내부에서 static 함수는 해당 파일 내에서만 호출 가능하다.

// example.cpp
#include <iostream>

// 이 함수는 오직 example.cpp 파일 내에서만 호출할 수 있다.
static void helperFunction() {
    std::cout << "C/C++의 static 함수 호출됨!" << std::endl;
}

int main() {
    helperFunction();  // 정상 호출
    return 0;
}

 

→ C#: static method는 인스턴스 없이 호출할 수 있다. 즉, 클래스에 소속되어는 있지만, 인스턴스와 관계없이 사용 가능하다.

// StaticMethodExample.cs
using System;

public class MyClass {
    // 인스턴스 없이 호출 가능한 static 메서드
    public static void StaticMethod() {
        Console.WriteLine("C#의 static 메서드 호출됨!");
    }
}

public class Program {
    public static void Main() {
        // 클래스 인스턴스를 생성하지 않고 호출
        MyClass.StaticMethod();
    }
}
// 보통은, `MyClass obj = new MyClass(); obj.Method();`
// 이런 식으로 인스턴스를 만들어서 사용한다.

 

  • static 클래스

→ C/C++: static 클래스 개념이 따로 없는 듯하고, 클래스의 멤버 변수나 함수에 static을 적용하면 인스턴스와 관계 없이 클래스 이름을 통해 바로 (접근)동작하도록 만들 수 있다.

// StaticMemberExample.cpp
#include <iostream>

class MyClass {
public:
    // 모든 인스턴스가 공유하는 하나의 변수
    static int sharedValue;

    // 인스턴스 생성 없이 호출 가능한 static 멤버 함수
    static void PrintValue() {
        std::cout << "Value: " << sharedValue << std::endl;
    }
};

// 반드시 한 번만 정의되어야 함
int MyClass::sharedValue = 42;

int main() {
    MyClass::PrintValue();  // 인스턴스 없이 호출
    return 0;
}

 

→ C#: 해당 클래스를 인스턴스화 할 수 없고, 내부 모든 멤버도 반드시 static이어야 한다. 공통 기능이나 유틸리티를 구현할 때 활용된다.

// StaticClassExample.cs
using System;

public static class Utility {
    // 클래스 내부의 모든 멤버는 static이어야 한다.
    public static void PrintMessage() {
        Console.WriteLine("C#의 static 클래스의 메서드 호출됨!");
    }
}

public class Program {
    public static void Main() {
        // 인스턴스 없이 호출
        Utility.PrintMessage();
    }
}

 

2. C++ 참조자

참조자(reference)

→ C언어에서, 변수를 가리키고 싶을 때 포인터를 사용했다. 그런데 C++에서는 다른 변수나 상수를 가리키는 방법으로 참조자(referece)도 사용된다.

#include <iostream>

int main() {
  int a = 3;
  int& anotherA = a;

  anotherA = 5;

  std::cout << "a : " << a << std::endl;
  std::cout << "anotherA : " << anotherA << std::endl;
  // 5
  // 5
  
  return 0;
}

anotherA에 어떤 작업을 수행하든, 사실상 a에 그 작업을 수행하는 것과 마찬가지가 된다.

포인터 타입 참조자는 int*& 로 만들 수 있다.

 

  • 참조자(reference)와 포인터

참조자는 정의 시, 누구의 별명인지 명시해야 한다. 또, 한 번 어떤 변수의 참조자가 되면, 더 이상 다른 변수를 참조할 수 없다.

//이렇게 말고
int& anotherA;

//아래와 같이
//어떤 변수의 참조자로 사용될지를 명시한다.
int& anotherA = a; 

int b = 3;
anotherA = b;
//a에 b를 넣은 것이다.

 

  • 참조자는 메모리 상에 존재하지 않을 수도 있다.
int a = 10;
int* p = &a;
// 여기서 p는 메모리 상 8바이트를 차지한다.(32 bit 상에서는 4바이트)

int &anotherA = a;
// another_a 를 위한 공간을 할당할 필요가 있을까? 아니다.
// 하지만, 항상 존재하지 않는 것은 아니다.

 

  • 함수 인자로 레퍼런스 받기
#include <iostream>

int ChangeVal(int &p) {
  p = 3;

  return 0;
}
int main() {
  int number = 5;

  std::cout << number << std::endl;
  ChangeVal(number);
  std::cout << number << std::endl;
}

 

  • 참조자의 참조자?

→ 실제 문법 상, 참조자의 참조자를 만드는 것은 금지되어 있다.

+) 그냥 포인터 사용하면 되지 왜 참조자로 할까? 참조자를 사용하면 &와 *가 필요 없어 코드를 더 간결하게 나타낼 수 있기 때문이다.

#include <iostream>

int main() {
  int x;
  int& y = x;
  int& z = y;

  x = 1;
  std::cout << "x : " << x << " y : " << y << " z : " << z << std::endl;
// 1 1 1
  y = 2;
  std::cout << "x : " << x << " y : " << y << " z : " << z << std::endl;
// 2 2 2 
  z = 3;
  std::cout << "x : " << x << " y : " << y << " z : " << z << std::endl;
// 3 3 3
}

 

  • scanf 에서는 &를 붙여서 받았었다.
scanf("%d", &userInput);
// 어떤 변수의 값을 다른 함수에서 바꾸기 위해서는
// 항상 포인터로 주소값을 전달해야 한다.

std::cin >> userInput);
// cin은 레퍼런스로 userIntput을 받아서, &를 붙이지 않아도 된다.

 

  • 상수에 대한 참조자?
int &ref = 4;
// 오류가 발생한다.

상수 값 자체는 리터럴(소스코드 상 고정된 값)이므로, 참조가 가능하다면 리터럴 값을 바꾸게 되는 말도 안되는 행위가 가능해진다.

 

하지만,

const int &ref = 4
// 상수 참조자로 선언하면 리터럴도 참조할 수 있다.

int a = ref; // == (a = 4;)

 

레퍼런스의 배열과 배열의 레퍼런스

  • 배열의 레퍼런스

int& arr[2] = {a, b}; 는 안된다. 배열의 이름인 arr는 문법상 첫 번째 원소의 주소값으로 변환될 수 있다. 여기서 “주소값이 존재한다” 라는 것은 해당 원소가 메모리 상 존재한다는 의미이고, 레퍼런스는 특별한 경우가 아닌 이상 메모리 상 공간을 차지하지 않는다. 따라서, 레퍼런스들의 배열을 정의하는 건 금지되어 있다.

 

하지만 불가능한 건 아니고, 크기와 타입이 같은 형태의 레퍼런스가 별명이 될 수 있다.

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

int arr2[3][2] = {1, 2, 3, 4, 5, 6};
int (&ref2)[3][2] = arr2;

 

지역변수의 레퍼런스를 리턴?

int& Function() {
  int a = 2;
  return a;
}

int main() {
  int b = Function();
  b = 3;
  return 0;
}

→ Function 함수를 실행시키면, return 되는 타입은 int의 reference형이다. 따라서, 참조자를 리턴하게 된다. int a = 2;는 Function 안에서만 정의되어 있고, 함수의 리턴과 함께 사라진다. 참조하던 것이 사라진 레퍼런스를 댕글링 레퍼런스(Function 함수가 반환하는 참조)라고 한다.

→ 위처럼 레퍼런스를 리턴하는 함수에서 지역 변수의 레퍼런스를 리턴하지 않도록 조심해야 한다.

 

  • 참조자가 아닌 값을 리턴하는 함수를 참조자로 받기
int Function() {
  int a = 5;
  return a;
}

int main() {
  int& c = Function();
  return 0;
}

→ ERROR

test.cc: In Function ‘int main()’: test.cc:7:20: error: cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type ‘int’ 7 | int& c = Function(); | ~~~~~~~~^~

 

상수가 아닌 레퍼런스(non-const)가 리턴 값을 참조할 수 없다. 즉, int& c는 const로 정의된 값이 아니고, 이 값은 return 값을 참조할 수 없다는 것이다.

  • 왜 c는 function의 리턴 값을 참조할 수 없을까? → 이전에 했듯, 함수의 리턴 값이 해당 문장에 끝난 후 바로 사라지는 값으로, 댕글링 레퍼런스가 되어버리게 된다.
  • 그런데, const 참조자로 받았더니 문제 없이 컴파일 된다.
#include <iostream>

int Function() {
  int a = 5;
  return a;
}

int main() {
  const int& c = Function();
  std::cout << "c : " << c << std::endl;
  return 0;
}

→ 즉, 상수 레퍼런스(const reference)로 리턴 값을 받게 되면 해당 리턴값의 생명이 연장되고, 그 연장된 기간은 레퍼런스가 사라질 때까지이다.

 

 

+) 문제: 레퍼런스가 메모리 상에 반드시 존재해야 하는 경우는 어떤 경우가 있을까요? 그리고 메모리 상에 존재할 필요가 없는 경우는 또 어떤 경우가 있을까요?

  • 메모리 상에 존재할 필요가 없는 경우: 단순히 지역 변수 별명으로 사용되거나, 함수 인자로 넘어가는 상황처럼 “추가 정보 저장”이 필요하지 않을 때
  1. 단순히 지역적으로 사용할 때
int main() {
    int x = 10;
    int& r = x;  // r은 x의 또 다른 이름입니다.
    r = 20;      // x의 값이 20이 됩니다.
    return 0;
}

 

  2. 함수의 인자로 전달할 때

void Increment(int& num) {
    num++;  // num은 호출 시 전달된 변수의 또 다른 이름
}

int main() {
    int value = 10;
    Increment(value);  // value를 그대로 참조하여 직접 수정
    return 0;
}

함수에 레퍼런스를 인자로 전달할 때, 별도의 복사 없이 원래 변수의 메모리 주소를 사용한다.

 

  • 메모리 상에 반드시 존재해야 하는 경우: 객체의 일부로서 레퍼런스를 반드시 저장해야 할 때, 람다 캡쳐 등의 이유로 레퍼런스 자체 정보를 보관해야 할 때
  1. 클래스 멤버로 레퍼런스를 선언한 경우
#include <iostream>

class FWrapper {
public:
    int& ref;  // 클래스 내부에 저장되는 reference 멤버
    
	  // reference 멤버나 const 멤버는 생성자 본문 전에 초기화 리스트를 통해
	  // 초기화 해야 한다.
    FWrapper(int& r) : ref(r) {
			// 생성자 본문
    }
    
};

int main() {
    int a = 10;
    
    // FWrapper 생성자에 a가 참조 인자로 전달된다.
    // FWrapper 내부 멤버인 ref가 a를 참조하게 된다.
    FWrapper w(a); 
    
    // w.ref는 a를 가리키는 표현이 된다.
    std::cout << "w.ref: " << w.ref << std::endl;  // 10 출력
    return 0;
}

 

 

   2. 람다 캡쳐에서 레퍼런스로 변수 캡쳐

#include <iostream>

int main() {
    int x = 10;
    auto lambda = [&]() {  // x를 레퍼런스로 캡쳐
        std::cout << x << std::endl;
    };
    lambda();  // x의 값을 출력 (10)
    return 0;
}

→ 람다 객체는 x라는 변수를 레퍼런스로 캡쳐했다는 정보를 어딘가에 저장해야 한다. 람다 객체 내부에 x의 주소(or reference)가 보관되어 이 정보가 메모리에 존재해야 한다.

 

+) 포인터와 참조자를 언제 사용하는 것이 적합할까?

 

매개변수에 NULL 포인터를 넘겨주는 것 or 리턴값으로 NULL 포인터를 반환하는 것이 허용될 경우, 포인터를 사용해야 한다.참조자는 선언과 동시에 초기화 되어야해서 NULL이 허용되지 않기 때문이다.

 

3. new, delete

  • new, delete 기본 사용

[일반적인 사용 방식: T* pointer = new T; ]

프로그램이 정확하게 실행되기 위해서 컴파일 시 모든 변수의 주소값이 확정되어야 했다. 하지만, 이를 위해서는 프로그램에 많은 제약이 따랐고, 프로그램 실행 시 자유롭게 할당/해제 될 수 있는 힙(Heap) 공간이 따로 생기게 되었다.

 

스택과 달리 힙은 사용자가 스스로 제어해야 하는 부분으로, 책임이 따른다. C언어에서는 malloc과 free 함수를 지원해 힙 상 메모리 할당을 지원했다. C++에서도 마찬가지로 malloc, free 함수를 사용할 수 있다.

 

하지만 언어 차원에서 지원하는 것은 new, delete이다. new는 말 그대로, malloc과 대응된다. 즉, 메모리를 할당한다. delete는 free에 대응되는 것으로 메모리를 해제한다.

 

ex) new, delete 사용

#include <iostream>

int main() {
  int* p = new int;
  *p = 10;

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

  delete p;
  return 0;
}

+) delete로 해제할 수 있는 메모리 공간은 사용자가 new를 통해서 할당한 공간만 가능하다. 일반 지역 변수를 무리하게 delete로 해제하려고 하면, Heap이 아닌 공간을 해제하려고 한다는 경고 메시지가 나타나게 된다.

 

  • new로 배열 할당
#include <iostream>

int main() {
  int arr_size;
  std::cout << "array size : ";
  std::cin >> arr_size;
  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;
}

 

+) C에서는 변수 선언을 모두 최상단에 몰아서 해야 했지만, C++은 그렇지 않다. 소스의 아무 곳에서나 변수를 선언할 수 있고, 그 변수는 변수를 포함하고 있는 중괄호를 빠져나갈 때 소멸된다.

  • 변수를 사용할 때 컴파일러는 변수를 가장 가까운 범위부터 찾는다.
int a = 4;
{
  std::cout << "외부의 변수 1" << a << std::endl;
  int a = 3;
  std::cout << "내부의 변수 " << a << std::endl;
}

std::cout << "외부의 변수 2" << a << std::endl;

→ 하지만, 같은 범위 안에 동일한 변수를 선언하는 것은 허용되지 않는다.

→ 쪼잔하게 변수 이름을 짓지 말자.