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)로 리턴 값을 받게 되면 해당 리턴값의 생명이 연장되고, 그 연장된 기간은 레퍼런스가 사라질 때까지이다.
+) 문제: 레퍼런스가 메모리 상에 반드시 존재해야 하는 경우는 어떤 경우가 있을까요? 그리고 메모리 상에 존재할 필요가 없는 경우는 또 어떤 경우가 있을까요?
- 메모리 상에 존재할 필요가 없는 경우: 단순히 지역 변수 별명으로 사용되거나, 함수 인자로 넘어가는 상황처럼 “추가 정보 저장”이 필요하지 않을 때
- 단순히 지역적으로 사용할 때
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;
}
함수에 레퍼런스를 인자로 전달할 때, 별도의 복사 없이 원래 변수의 메모리 주소를 사용한다.
- 메모리 상에 반드시 존재해야 하는 경우: 객체의 일부로서 레퍼런스를 반드시 저장해야 할 때, 람다 캡쳐 등의 이유로 레퍼런스 자체 정보를 보관해야 할 때
- 클래스 멤버로 레퍼런스를 선언한 경우
#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;
→ 하지만, 같은 범위 안에 동일한 변수를 선언하는 것은 허용되지 않는다.
→ 쪼잔하게 변수 이름을 짓지 말자.
'공부 > C++' 카테고리의 다른 글
[C++] 초기화 리스트, static 변수/함수, this, 상수(const) 함수, 복사 생략 (0) | 2025.03.24 |
---|---|
[C++] 복사생성자(Copy Constructor), 소멸자(Destructor) (1) | 2025.03.21 |
[C++] 객체지향, 오버로딩(Overloading), 생성자(Constructor) (0) | 2025.03.20 |