C# 기본 구조
- 가장 단순한 예제
int x = 12 * 30; // Statement 1
Console.WriteLine(x); // Statement 2
- 문(또는 문장이라고 부름)(Statement)은 프로그램의 기본 실행 단위
- 각 문은 세미콜론(;)으로 끝난다.
- 주석은 // 뒤에 작성하며, 컴파일러는 이를 무시한다.
- /**/ 도 있다.
- 문자열이나 숫자와 같은 값을 리터럴이라고 한다.
- x : 변수(또는 식별자 라고 부른다.)
- : 산술연산자
- WriteLine은 콘솔에 값을 출력하는 메서드다.
- 자동으로 개행(n\)을 해준다.
- Write 함수도 있다. : 개행(n\)이 없는 함수다.
C++과의 차이점
- C#은 메모리 관리를 프로그래머가 하지 않도록 만들어졌다
- C++ 메모리 제어 직접 가능
- C# : 백그라운드에서 메모리를 관리해준다.
- 프로그램 진입점이 다르다.
- C++ : main의 내부가 시작점이다.
- C# : 최상위문 사용 하냐 안하냐에 따라 달라진다.
- C#은 헤더파일과 cpp 파일이 분리되어있지 않는다.
- 클래스 내에 어딘가에 선언만 되어있으면 어디서든 쓸 수 있다.
최상위문
- 프로그램의 시작점을 의미한다.
- C++의 main 함수 역할이다.
- C# 9.0부터는 Program 클래스와 Main 메서드 없이도 프로그램을 작성할 수 있다.
using System; // System 네임스페이스 사용 선언
// 네임스페이스 사용 전
System.Console.WriteLine("Hello!"); // 정규화된 이름 사용
// 네임스페이스 사용 후
Console.WriteLine("Hello!"); // 간단한 이름 사용 가능
//흔히 사용하는 방식
static void Main()
{
}
- Unity에서는 최상위문을 사용할 수 없다.
- 임의로 main 함수를 만들어서 사용한다.
- C#은 전역함수가 없다.
- 전부 클래스의 멤버 함수여야 한다.
- 모든 것이 ‘객체’로 이루어져야하기 때문이다.
- 대소문자 구분을 잘 해야한다.
- static으로 선언해야한다.
- 클래스마다 ‘하나만’ 가지고 있어야하기 때문이다.
메서드 사용
using System;
// 메서드 호출
Console.WriteLine(FeetToInches(30)); // 360
Console.WriteLine(FeetToInches(100)); // 1200
// 메서드 정의
int FeetToInches(int feet)
{
int inches = feet * 12;
return inches;
}
- 같은 계산을 반복해야 할 때는 메서드를 정의하여 코드를 재사용할 수 있다.
- 반환 타입 (int) - 메서드가 돌려주는 값의 형식
- 메서드 이름 (FeetToInches) - 메서드를 호출할 때 사용
- 매개변수 목록 (int feet) - 메서드에 전달되는 입력 값
- 메서드 본문 - 중괄호로 둘러싸인 실행할 코드
- return 문 - 계산 결과를 호출한 곳으로 반환
매개변수 없는 메서드
- void : 메서드가 아무 값도 반환하지 않음을 나타낸다.
- return 문을 생략하거나 return; 형태로 사용할 수 있다.
- DateTime : 시간 관련 데이터형
- Now : DateTime형을 반환한다.
- C#은 멤버접근이든 전역 접근이든 .(연산자)으로 통일이다.
- $ : 서식화 해주는 역할이다.
- 문자열 보간 이라고 한다.
- %d의 역할이다.
- : → 문자열을 의미한다.
컴파일 과정과 IL 코드
- HW 위에 붙은것 : OS
- 프로그래머가 명령을 OS에게 내리고, OS가 HW에서 실행하게 한다.
컴파일과 IL 코드 생성
- C# 코드가 실행 파일이 되는 과정:
- C# 소스 코드(.cs)를 컴파일러가 IL(Intermediate Language) 코드로 변환
- IL 코드는 CPU에 독립적인 중간 언어로, .NET의 공통 언어다.
- 실행환경에서 돌릴 수 있는 결과물
- C++은 OS에서 바로 돌린다.
- IL은 OS에서 실행할 수 없다.
- 다만, IL은 어떤 개발 환경에서도 동일하게 동작할 수 있다.
- IL 코드는 실행 시점에 JIT(Just-In-Time) 컴파일러가 기계어로 변환
어셈블리
- .NET의 배포 단위를 의미한다.
- solution : 프로젝트의 집합
- 프로젝트 집합을 지금 어셈블리로 사용하는 것이다.
- IL 코드를 포함한 실행 파일(.exe) 또는 라이브러리(.dll)로 빌드할 수 있다.
- 우리는 실행파일로 프로젝트를 세팅해서 사용하고있는 것이다.
- 메타데이터(형식 정보, 버전 정보 등)
- 여러 개의 소스 파일을 하나의 어셈블리로 컴파일 가능함
컴파일 시간과 실행 시간(런타임)
- const로 선언된 상수의 값이 계산됨
- const : 정해진 값이 있다면 실행 중 변경이 되지 않게한다.
- 문법 오류를 검사함 - 가장 중요
- 타입 검사를 수행함
- C++이랑 C#이랑 통일하게 강타입(타입을 빡빡하게 검사)이다.
- 최적화를 수행함
- 중복 코드, 같은 값 할당 같은 일이 일어난 경우 수행하지 않고 넘어가고 다음 코드를 실행한다.
실행 시간(런타임)에 발생하는 일:
- 변수의 값이 계산됨
- 메서드가 호출됨
- 객체가 생성됨
- 예외가 발생할 수 있음
// 컴파일 시간에 발생하는 동작들
const int a = 10 * 20; // 컴파일러가 200으로 직접 치환함
const int max = int.MaxValue + 1; // 컴파일 시 오류: 상수 값이 int 범위를 벗어남
// 실행 시간에 발생하는 동작들
int b = 10 * 20; // 프로그램 실행 중에 계산됨
int userInput = int.Parse(Console.ReadLine()); // 실행 중에만 알 수 있는 값
int result = checked(2000000000 * 2); // 실행 중에 OverflowException 발생 가능
- int.MaxValue : int 클래스의 MaxValue 변수에 접근하는 것이다.
- int.Parse : int 형의 static 함수를 호출하는 것이다.
- checked :
C# 구문
식별자
- 프로그래머가 지정하는 클래스, 메서드, 변수 등의 이름이다.
식별자 규칙
- 문자나 밑줄로 시작해야 한다.
- Unicode 문자, 숫자, 밑줄로 구성된다.
- 대소문자를 구분한다.
- C# 키워드와 동일한 이름은 사용할 수 없다.
- return, void 등이 키워드다.
using System;
int x = 12 * 30;
Console.WriteLine (x);
- System : 네임스페이스 식별자
- Console : 클래스 식별자
- x : 변수 식별자
- WriteLine : 메서드 식별자
명명 관례
- 매개변수, 지역 변수 : 카멜 케이스(camelCase) 사용
- 시작을 소문자로, 띄어쓰기 나오면 대문자로
- 그 외 모든 식별자 : 파스칼 케이스(PascalCase) 사용
- 시작을 대문자로, 나머지는 소문자로
- 띄어쓰기가 나올때마다 대문자로 바꾼다.
// 카멜 케이스 - 매개변수, 지역 변수
void ProcessData(string firstName, int maxCount)
{
int currentValue = 0;
string tempName = firstName;
}
// 파스칼 케이스 - 클래스, 메서드
public class CustomerOrder
{
public void ProcessPayment() { }
}
키워드
예약 키워드
- 컴파일러에서 특별한 의미를 가지는 단어다.
- 식별자로 사용할 수 없다.
- abstract, as, base, bool 등이 있다.
// 예약 키워드 사용 예시
public class MyClass // class 키워드
{
private int number; // private 키워드
public void Test() // public, void 키워드
{
if (number > 0) // if 키워드
{
return; // return 키워드
}
}
}
문맥 키워드
- 특정 문맥에서만 특별한 의미를 가짐
- 식별자로도 사용 가능함
- 문맥에서만 유효한 키워드이기 때문이다.
- get, set, value 등이 있음
// 문맥 키워드 사용 예시
public class Temperature
{
private float temp;
// get, set은 속성에서 문맥 키워드로 사용됨
public float Celsius
{
get { return temp; }
set { temp = value; } // value는 setter에서 문맥 키워드
}
// get을 일반 식별자로 사용 가능
public void get() { } // 메서드 이름으로 사용
}
- 프로퍼티 : get set
- 프로퍼티 안에서만 유효만 문맥 키워드다.
- 밖에서는 일반 식별자로 사용 가능하다.
- 하지만, 웬만해선 겹치게 쓰지 않는게 좋다.
@ 식별자
- 키워드를 식별자로 사용해야 할 경우 @ 기호를 앞에 붙임
- 이것도 쓰지 않는게 좋다.
리터럴, 구두점과 연산자
- 프로그램에 직접 삽입된 데이터 값이다.
- 숫자(12, 30), 문자열("Hello") 등이 있다.
// 정수 리터럴
int dec = 42; // 10진수
int hex = 0x2A; // 16진수
int bin = 0b101010; // 2진수
// 실수 리터럴
double d1 = 123.456; // 소수점 표기
double d2 = 1.23e2; // 지수 표기
decimal m = 123.456m; // decimal 리터럴
// 문자와 문자열 리터럴
char ch = 'A';
string s1 = "Hello";
string s2 = @"C:\\\\Program Files"; // 축자 문자열 리터럴
// 불리언 리터럴
bool t = true;
bool f = false;
- 리터럴 앞에 접두사 또는 접미사가 있을 수 있다.
- 0b : 2진수
- 0x : 16진수
- 소수점 표기는 default가 double형이다.
- e2 : 지수표기
- decimal :
- f붙이면 float 형 리터럴이 된다.
- string : 문자열 데이터형
- C#은 string이 문자열 기본 데이터형이다.
구두점
- 세미콜론(;): 문장의 끝을 나타낸다.
int x = 12 * 30;
Console.WriteLine(x);
- 중괄호({}): 문장 블록을 구분한다.
if (x > 0)
{
Console.WriteLine("Positive");
}
- 마침표(.): 멤버 접근에 사용된다.
Console.WriteLine("Hello");
- 괄호(): 메서드 호출과 매개변수 목록에 사용된다.
Math.Max(5, 10);
문장 래핑(Wrapping)
- 문장은 여러 줄에 걸쳐 작성할 수 있다.
Console.WriteLine(
1 + 2 + 3 + 4 + 5 +
6 + 7 + 8 + 9 + 10
);
연산자
- 단항 연산자(Unary operator): 하나의 피연산자에 작용 (예: -x)
- 이항 연산자(Binary operator): 두 개의 피연산자에 작용 (예: x + y)
- 삼항 연산자(Ternary operator): 세 개의 피연산자에 작용
- 대부분의 이항 연산자는 중위 표기법(Infix notation) 사용
- 이항 연산자를 기준으로 한다.
// 연산자 예시
-x // 단항 연산자
x + y // 이항 연산자(중위 표기법)
condition ? x : y // 삼항 연산자
주석(Comments)
- // 기호 뒤의 텍스트를 주석 처리함
// Statement 1
int x = 12 * 30;
여러 줄 주석
- /* \*/ 사이의 모든 텍스트를 주석 처리함
/* This is a comment that
spans two lines */
int x = 3;
C# 타입 (자료형, 데이터타입)
- 타입(Type)은 값의 청사진을 정의한다.
- 해당 값이 어떤 종류의 데이터인지 지정한다.
- 값이 차지하는 메모리 크기를 결정한다.
- 해당 값으로 수행할 수 있는 연산을 정의한다.
- 변수는 타입의 인스턴스를 저장하는 메모리 공간이다.
- 프로그램에서 데이터를 임시로 저장하는 용도로 사용한다.
- 선언된 타입의 값만 저장할 수 있다.
- 프로그램 실행 중에 값을 변경할 수 있다.
int x = 12 * 30; // x는 변수, 12와 30은 int 타입의 값
Console.WriteLine(x); // 360 출력
변수와 상수의 차이
- 변수(Variable): 시간에 따라 다른 값을 가질 수 있는 저장소다.
- 상수(Constant): 한 번 값이 정해지면 변경할 수 없다.
const int y = 360; // 상수 선언
y = 100; // 컴파일 오류: 상수는 변경 불가
미리 정의된 타입
- 기본 데이터 형을 의미한다.
int 타입 사용 예제
- 정수형 타입(Integer Type)인 int는 가장 기본적인 숫자 타입임
- 32비트(4바이트) 메모리를 사용함
- 2,147,483,648부터 2,147,483,647 범위의 정수를 저장할 수 있음
- 덧셈(+), 뺄셈(-), 곱셈(*), 나눗셈(/) 등의 산술 연산 가능
- 프로그램에서 숫자를 다룰 때 가장 많이 사용되는 타입임
string 타입 사용 예제
- 문자열(String) 타입은 텍스트 데이터를 저장하는 참조 타입임
- 문자들의 시퀀스(연속된 나열)를 저장함
- 텍스트는 유니코드(Unicode) 문자로 저장되어 전 세계의 문자를 표현할 수 있음
- 불변(Immutable)타입으로, 한번 생성된 문자열은 변경할 수 없음
- 메모리에 저장된 값이 변경되는게 아니라 할당될때 다른 메모리가 생성되어서 그 메모리를 가리키게 되는것이다.
- 본래라면 메모리 누수가 일어나지만 C#에는 가비지 컬렉터가 있다.
- 장점 : 구현이 간단해진다.
- 메모리에 저장된 값이 변경되는게 아니라 할당될때 다른 메모리가 생성되어서 그 메모리를 가리키게 되는것이다.
- 문자열 조작을 위한 다양한 메서드(ToUpper(), ToLower(), Substring() 등)를 제공함
- \+ 연산자를 사용하여 문자열을 연결할 수 있음
- ToString() 메서드
- 모든 타입에서 사용 가능한 메서드임
- 객체를 문자열로 표현하는 방법을 정의함
- 숫자, 날짜 등을 문자열로 변환할 때 사용함
string message = "Hello world";
string upperMessage = message.ToUpper();
Console.WriteLine(upperMessage); // HELLO WORLD 출력
int x = 2022;
message = message + x.ToString();
Console.WriteLine(message); // Hello world2022 출력
bool 타입과 조건문 예제
- 논리형(Boolean) 타입은 참/거짓을 표현하는 가장 단순한 타입임
- true 또는 false 두 가지 값만 가질 수 있음
- 1바이트의 메모리를 사용함 (실제로는 1비트만 필요하지만 CPU 처리 효율성을 위해 1바이트 사용)
- 조건문(if)이나 반복문(while)의 조건식에서 주로 사용됨
- 비교 연산자(\==, !=, <, >, <=, >=)의 결과값으로 사용됨
bool simpleVar = false;
if (simpleVar) // false이므로 실행되지 않음
Console.WriteLine("This will not print");
int x = 5000;
bool lessThanAMile = x < 5280; // 비교 연산의 결과를 저장
if (lessThanAMile) // true이므로 실행됨
Console.WriteLine("This will print");
사용자 정의 타입
- C#에서는 필요한 타입을 직접 정의할 수 있음
- 클래스(class)나 구조체(struct)를 사용하여 새로운 타입을 만듦
- C++은 접근지정자를 제외하고 다른게 없다.
- C#은 클래스와 구조체에 따라서 많이 다르다.
- 사용자 정의 타입은 데이터와 해당 데이터를 처리하는 메서드를 하나의 단위로 캡슐화함
타입의 구성 요소
- 데이터 멤버(필드)
- 타입의 상태를 저장하는 변수 - 멤버 변수
- public, private 등의 접근 제한자로 보호 수준 지정
- 변수, 함수 뿐만 아니라 클래스도 접근지정자를 작성해야한다.
- 생략하면 기본 접근 권한이 설정된다.
- 클래스 : interval
- 멤버 : private
- 구조체 : public
- 인스턴스 필드와 정적(static) 필드로 구분됨
- 함수 멤버(메서드)
- 타입의 행동을 정의하는 코드 블록
- 데이터 멤버를 조작하는 연산 수행
- 매개변수를 받아 결과를 반환할 수 있음
public class UnitConverter
{
private int ratio; // 데이터 멤버(필드) - 변환 비율 저장
// 함수 멤버(생성자) - 객체 초기화
public UnitConverter(int unitRatio)
{
ratio = unitRatio; // 필드 초기화
}
// 함수 멤버(메서드) - 단위 변환 수행
public int Convert(int unit)
{
return unit * ratio;
}
}
- ratio 필드는 변환 비율을 저장하는 데이터 멤버임
- 생성자는 객체가 생성될 때 ratio 필드를 초기화함
- Convert 메서드는 입력값에 비율을 곱해 변환된 값을 반환함
// 인치 변환기 (1피트 = 12인치)
UnitConverter feetToInchesConverter = new UnitConverter(12);
// 피트 변환기 (1마일 = 5280피트)
UnitConverter milesToFeetConverter = new UnitConverter(5280);
Console.WriteLine(feetToInchesConverter.Convert(30)); // 360 (30피트 = 360인치)
Console.WriteLine(milesToFeetConverter.Convert(1)); // 5280 (1마일 = 5280피트)
// 체이닝: 1마일을 인치로 변환
Console.WriteLine(feetToInchesConverter.Convert(
milesToFeetConverter.Convert(1))); // 63360 (1마일 = 63360인치)
- C#은 구조체는 무조건 정적할당으로 쓰이고, 클래스로 만든 인스턴스는 무조건 동적할당으로 쓰인다.
- new : 동적할당할때 쓴다.
- C++과의 차이점 : C++에서는 delete를 해줘야 했지만, C#은 가비지 컬렉터가 있어 자동으로 삭제된다.
인스턴스 멤버와 정적 멤버
- 멤버의 소속에 따라 두 가지로 구분됨:
- 인스턴스 멤버: 객체 각각에 속하는 멤버
- 정적(static) 멤버: 타입 자체에 속하는 멤버
Panda p1 = new Panda("Pan Dee"); // 첫 번째 판다 객체
Panda p2 = new Panda("Pan Dah"); // 두 번째 판다 객체
Console.WriteLine(p1.Name); // Pan Dee - 인스턴스 멤버 접근
Console.WriteLine(p2.Name); // Pan Dah - 인스턴스 멤버 접근
Console.WriteLine(Panda.Population);// 2 - 정적 멤버 접근 (총 판다 수)
public class Panda
{
public string Name; // 인스턴스 필드 - 각 판다의 이름
public static int Population; // 정적 필드 - 전체 판다의 수
public Panda(string n)
{
Name = n; // 각 판다의 이름 설정
Population = Population + 1;// 전체 판다 수 증가
}
}
- C#은 초기화 안하면 데이터형의 기본값이 들어간다.
- string : null, int : 0 등
- 클래스로 만드는 변수는 모두 ‘참조형’이다. - string 등
- 포인터나 레퍼런스처럼
- 구조체로 만드는 변수는 참조형이 아니다. - int 등
- 인스턴스 멤버의 특징:
- 객체마다 별도의 복사본을 가짐
- 객체 참조를 통해 접근 (예: p1.Name)
- 객체의 고유한 상태를 표현할 때 사용
- 정적 멤버의 특징:
- 모든 객체가 공유하는 하나의 복사본만 존재
- 타입 이름을 통해 직접 접근 (예: Panda.Population)
- 객체 생성 없이도 사용 가능
- 모든 객체가 공유해야 하는 데이터에 사용
- C++과는 다르게 반드시 ‘클래스 명으로’ 접근해서 호출해야한다.
네임스페이스 정의
- 네임스페이스(Namespace)는 타입을 조직화하는 방법임
- 타입 이름의 충돌을 방지함
- 관련된 타입을 논리적으로 그룹화함
- 코드의 모듈화와 재사용성을 향상시킴
using System; // System 네임스페이스 사용 선언
using Animals; // Animals 네임스페이스 사용 선언
Panda p = new Panda("Pan Dee"); // Panda 타입 사용
//Animals.Panda p = new Panda("Pan Dee"); //원래 형태
Console.WriteLine(p.Name); // System.Console 타입 사용
//아래의 Panda와는 다른 데이터형이 된다.
public class Panda // Animals.Panda가 전체 이름이 됨
{
public string Name { get; set; }
public Panda(string name)
{
Name = name;
}
}
namespace Animals // Animals 네임스페이스 정의
{
public class Panda // Animals.Panda가 전체 이름이 됨
{
public string Name { get; set; }
public Panda(string name)
{
Name = name;
}
}
}
타입과 형변환
- 형변환(Type Conversion)은 한 타입의 값을 다른 타입의 값으로 변환하는 것임
암시적 형변환(Implicit Conversion)
- 데이터 손실의 위험이 없는 안전한 변환
- ex) 업캐스팅
- 컴파일러가 자동으로 수행
- 작은 크기의 타입에서 큰 크기의 타입으로 변환할 때 사용
int x = 12345; // int는 32비트 정수
long y = x; // 암시적으로 64비트 정수로 변환 (안전)
float f = x; // int에서 float로 암시적 변환 (안전)
int x = 1234567890;
float f = x; // 정밀도 손실 가능성 있음
Console.WriteLine(f); // 1.234568E+09 출력 (정밀도 손실 발생)
명시적 형변환(Explicit Conversion)
- 데이터 손실 가능성이 있는 변환
- ex) 다운캐스팅
- 프로그래머가 명시적으로 지정해야 함
- 캐스트 연산자 () 사용
int x = 12345;
short z = (short)x; // 명시적으로 16비트 정수로 변환 (데이터 손실 가능)
int i = (int)3.99; // double에서 int로 변환 (소수점 이하 손실)
값 타입과 참조 타입
값 타입(Value Types)
- 값 자체를 직접 저장함
- 변수가 선언된 메모리에 직접 저장됨
- 할당 시 값이 복사됨
- 값만 같고 다른 메모리 공간이 된다.
- struct로 선언된 사용자 정의 타입임
public struct Point { public int X, Y; }
Point p1 = new Point();
p1.X = 7;
Point p2 = p1; // p1의 값이 p2로 복사됨
Console.WriteLine(p1.X); // 7
Console.WriteLine(p2.X); // 7
p1.X = 9; // p1만 변경
Console.WriteLine(p1.X); // 9
Console.WriteLine(p2.X); // 7 (p2는 영향 받지 않음)

- 값의 참조(메모리 주소)를 저장함
- 실제 데이터는 힙(heap) 메모리에 저장됨
- 할당 시 참조가 복사됨
- class로 선언된 사용자 정의 타입임
public class Point { public int X, Y; }
Point p1 = new Point();
p1.X = 7;
Point p2 = p1; // p1의 참조가 p2로 복사됨
Console.WriteLine(p1.X); // 7
Console.WriteLine(p2.X); // 7
p1.X = 9; // 같은 객체를 가리키므로
Console.WriteLine(p1.X); // 9
Console.WriteLine(p2.X); // 9 (p2도 영향 받음)

- null은 "참조 없음"을 나타내는 특별한 값임
- 참조 타입만 null 값을 가질 수 있음
- null인 객체의 멤버에 접근하면 NullReferenceException 발생
Point p = null; // 참조가 없는 상태
Console.WriteLine(p == null); // True
Console.WriteLine(p.X); // 실행 시점 오류: NullReferenceException
- Nullable\<T> || T?
int? nullableInt = null;
if (nullableInt == null)
Console.WriteLine("값이 없습니다.");
- nullable 형이라고 부른다.
기본값
- 모든 타입은 자동 초기화되는 기본값을 가짐
- 기본값은 해당 메모리를 0으로 초기화한 결과임
- 참조 타입의 경우 기본값은 null
// 숫자 타입의 기본값은 0
Console.WriteLine(default(int)); // 0
Console.WriteLine(default(double)); // 0.0
// bool 타입의 기본값은 false
Console.WriteLine(default(bool)); // False
// char 타입의 기본값은 '\\\\0'
Console.WriteLine(default(char)); // '\\0'
// 참조 타입의 기본값은 null
Console.WriteLine(default(string)); // null
- default 키워드로 모든 타입의 기본값을 얻을 수 있음
- C# 7.1부터는 타입 추론이 가능한 경우 타입을 생략할 수 있음
int x = default; // default(int)와 동일
string s = default; // default(string)과 동일
미리 정의된 타입의 분류
- 숫자 타입
- 부호 있는 정수
- sbyte: 8비트, -128 ~ 127
- short: 16비트, -32,768 ~ 32,767
- int: 32비트, -2,147,483,648 ~ 2,147,483,647
- long: 64비트, -9,223,372,036,854,775,808 ~ 9,223,372,036,854,775,807
- 부호 없는 정수
- byte: 8비트, 0 ~ 255
- ushort: 16비트, 0 ~ 65,535
- uint: 32비트, 0 ~ 4,294,967,295
- ulong: 64비트, 0 ~ 18,446,744,073,709,551,615
- 실수
- float: 32비트 부동소수점
- double: 64비트 부동소수점
- decimal: 128비트 정밀 소수점
- 부호 있는 정수
- 논리 타입
- bool: true/false 값
- 문자 타입
- char: 16비트 유니코드 문자
참조 타입 (Reference Types) - 클래스
- string
- 문자열을 나타내는 특별한 참조 타입
- 불변(immutable) 타입임
- object
- 모든 타입의 기본 클래스
- 모든 타입은 object로부터 직접 또는 간접적으로 상속됨
C# 타입과 CLR 타입의 관계
- C# 키워드는 .NET의 타입에 대한 별칭임
// 다음 두 선언은 동일함
int i = 5; // C# 키워드 사용
System.Int32 i = 5; // .NET 타입 이름 사용
// 다음 두 선언도 동일함
string s = "Hello"; // C# 키워드 사용
System.String s = "Hello"; // .NET 타입 이름 사용
- int == Int32 이다.
- 프리미티브 타입(Primitive Types)
- CLR에서 특별히 지원하는 기본 타입들임
- 컴파일된 코드에서 특별한 지원을 받음
- decimal을 제외한 모든 숫자 타입은 IL에서 직접 지원되는 프리미티브 타입임
C# 숫자 타입과 연산자
C#의 수치 데이터형아래 표는 C#의 기본 제공 수치 데이터형을 보여줌:C# 타입 .NET 타입 접미사 크기 범위
| 부호있는 정수 | ||||
| sbyte | SByte | - | 8비트 | -2⁷ ~ 2⁷-1 |
| short | Int16 | - | 16비트 | -2¹⁵ ~ 2¹⁵-1 |
| int | Int32 | - | 32비트 | -2³¹ ~ 2³¹-1 |
| long | Int64 | L | 64비트 | -2⁶³ ~ 2⁶³-1 |
| 부호없는 정수 | ||||
| byte | Byte | - | 8비트 | 0 ~ 2⁸-1 |
| ushort | UInt16 | - | 16비트 | 0 ~ 2¹⁶-1 |
| uint | UInt32 | U | 32비트 | 0 ~ 2³²-1 |
| ulong | UInt64 | UL | 64비트 | 0 ~ 2⁶⁴-1 |
| 실수 | ||||
| float | Single | F | 32비트 | ±(~10⁻⁴⁵ ~ 10³⁸) |
| double | Double | D(생략가능) | 64비트 | ±(~10⁻³²⁴ ~ 10³⁰⁸) |
| decimal | Decimal | M | 128비트 | ±(~10⁻²⁸ ~ 10²⁸) |
- .NET 타입 앞에 System.을 생략하고 작성한것이다.
- decimal : 정밀 소수점 데이터형
수치 데이터형의 분류와 특징
- 정수형(Integral Types)
- int와 long이 가장 많이 사용됨
- 다른 정수형은 메모리 효율이나 호환성을 위해 사용됨
- 기본 정수 리터럴은 int로 해석됨
- 부동소수점형(Floating-Point Types)
- float와 double은 과학/공학 계산용
- double이 기본 실수 리터럴 타입임
- 2진수 기반으로 저장되어 10진 소수 표현에 오차 있음
- 고정소수점형(Fixed-Point Type)
- decimal은 금융 계산용
- 10진수 기반으로 저장되어 정확한 소수 계산 가능
- 다른 수치 타입보다 처리 속도가 느림
CLR 타입과의 관계
- CLR : 실행환경의 얘기다.
- C# 키워드는 .NET의 타입에 대한 별칭임
// 다음 두 선언은 동일함
int i = 5;
System.Int32 i = 5;
숫자 리터럴(Numeric Literals)
리터럴의 기본 형식
- 정수 리터럴은 10진수 또는 16진수로 표현 가능함:
int x = 127; // 10진수
long y = 0x7F; // 16진수 (0x 접두사)
- 가독성을 위해 밑줄(_) 사용 가능:
int million = 1_000_000;
- 2진수 표현은 0b 접두사 사용:
var b = 0b1010_1011_1100_1101_1110_1111;
리터럴 타입 추론
- 컴파일러는 다음 규칙으로 타입을 추론함:
- 소수점이나 지수(E)가 있으면 double로 추론
- 정수는 int, uint, long, ulong 순서로 맞는 타입 선택
Console.WriteLine(1.0.GetType()); // Double
Console.WriteLine(1E06.GetType()); // Double
Console.WriteLine(1.GetType()); // Int32
Console.WriteLine(0xF0000000.GetType()); // UInt32
Console.WriteLine(0x100000000.GetType()); // Int64
- 모든 자료형은 object 클래스를 항속한것이다.
숫자 접미사(Suffix)
- 리터럴의 타입을 명시적으로 지정할 수 있음:
- F: float
- D: double
- M: decimal
- U: uint
- L: long
- UL: ulong
float f = 1.0F;
double d = 1D;
decimal m = 1.0M;
uint i = 1U;
long l = 1L;
ulong ul = 1UL;
- 주의사항:
- 접미사는 대소문자를 구분하지 않음 (1F와 1f는 동일)
- 실수 리터럴에서 f나 F 접미사는 필수임 (float 타입)
- d나 D 접미사는 생략 가능 (실수의 기본 타입이 double)
숫자 형변환(Numeric Conversions)
- 더 큰 범위로의 변환은 암시적으로 가능
- 더 작은 범위로의 변환은 명시적 캐스팅 필요
int x = 12345; // int는 32비트 정수
long y = x; // 암시적 변환 가능 (32비트 → 64비트)
short z = (short)x; // 명시적 변환 필요 (32비트 → 16비트)
부동소수점 간의 변환
- float → double: 암시적 변환 가능
- double → float: 명시적 변환 필요
정수와 부동소수점 간의 변환
- 모든 정수 타입은 부동소수점으로 암시적 변환 가능
- 부동소수점에서 정수로는 명시적 변환 필요
int i = 1;
float f = i; // 암시적 변환
int i2 = (int)f; // 명시적 변환 필요
// 주의: 큰 정수를 float로 변환 시 정밀도 손실 가능
int i1 = 100000001;
float f = i1; // 정밀도는 유지되나 정확도 손실
int i2 = (int)f; // 100000000
산술 연산자(Arithmetic Operators)
- 기본 산술 연산자:
- + (덧셈)
- - (뺄셈)
- * (곱셈)
- / (나눗셈)
- % (나머지)
증가/감소 연산자
- ++: 값을 1 증가
- --: 값을 1 감소
- C++ C# 차이
- 전위와 후위 형식의 차이:
int x = 0, y = 0;
Console.WriteLine(x++); // 출력: 0, x는 1
//x++ + x++ => 출력 : 1 x++(바뀌기 전) + x++(바뀐 후)
Console.WriteLine(++y); // 출력: 1, y는 1
정수 타입의 특수 연산
- 정수 나눗셈은 소수부분을 버림
- 0으로 나누면 DivideByZeroException 발생
int a = 2 / 3; // 결과: 0
int b = 0;
int c = 5 / b; // DivideByZeroException 발생
오버플로우(Overflow)
- 표현할 수 있는 값의 범위를 넘어갈때 발생한다.
- 기본적으로 오버플로우는 무시됨
- checked 연산자로 검사 가능
int a = int.MinValue;
a--; // 오버플로우: int.MaxValue가 됨
checked
{
int c = int.MaxValue;
c++; // OverflowException 발생
}
// 단일 식에 대한 검사
int y = checked(x + 1);
unchecked 연산자
- 컴파일러 수준의 확인을 비활성화함
int x = int.MaxValue;
int y = unchecked(x + 1); // 오버플로우 허용
비트 연산자(Bitwise Operators)
- C#은 다음과 같은 비트 연산자를 제공함:
연산자 의미 예제 결과
| ~ | 보수(Complement) | ~0xfU | 0xfffffff0U |
| & | AND | 0xf0 & 0x33 | 0x30 |
| OR | 0xf0 | ||
| ^ | XOR(베타적 OR) | 0xff00 ^ 0x0ff0 | 0xf0f0 |
| << | 왼쪽 시프트 | 0x20 << 2(비트의 갯수) | 0x80 |
| >> | 오른쪽 시프트 | 0x20 >> 1 | 0x10 |
- 시프트 연산을 2로 곱하거나 나누는것과 같은 연산의 결과가 나온다.
비트 연산자 활용 예제
- byte : 부호가 없는 정수형
비트 연산자 활용 - ARGB 쉬프트 연산주의사항
- 시프트 연산 시 비트 수가 타입의 크기를 넘어가면 무시됨
- 부호 있는 타입의 오른쪽 시프트(>>)는 부호를 유지함
8비트와 16비트 정수 타입
- byte, sbyte, short, ushort는 자체 산술 연산자가 없음
- 연산 시 자동으로 int로 변환됨
- 정수의 승격 이라고 부른다.
short x = 1, y = 1;
short z = x + y; // 컴파일 오류
short z = (short)(x + y);// 정상 동작
특수한 부동소수점 값특수값 설명 Double 상수 Float 상수
| NaN | Not a Number | double.NaN | float.NaN |
| +∞ | 양의 무한대 | double.PositiveInfinity | float.PositiveInfinity |
| -∞ | 음의 무한대 | double.NegativeInfinity | float.NegativeInfinity |
| -0 | 음의 0 | -0.0 | -0.0f |
Console.WriteLine(double.NegativeInfinity); // -Infinity
Console.WriteLine(1.0 / 0.0); // Infinity
Console.WriteLine(-1.0 / 0.0); // -Infinity
Console.WriteLine(1.0 / -0.0); // -Infinity
Console.WriteLine(-1.0 / -0.0); // Infinity
- 실수는 0으로 나눠도 컴파일 오류가 생기지 않는다.
NaN(불능) 관련 연산
- 0을 0으로 나누거나 무한대에서 무한대를 뺄 때 발생
- NaN 값과의 비교는 항상 false
- IsNaN : 불능 확인 함수
- Equals : 같은지 확인하는 함수
Console.WriteLine(0.0 / 0.0); // NaN
Console.WriteLine((1.0/0.0) - (1.0/0.0)); // NaN
// NaN 비교
Console.WriteLine(0.0 / 0.0 == double.NaN); // False
Console.WriteLine(double.IsNaN(0.0 / 0.0)); // True
// object.Equals 사용 시
Console.WriteLine(object.Equals(0.0/0.0, double.NaN)); // True
double과 decimal 비교항목 double decimal
| 내부 표현 | 2진수 기반 | 10진수 기반 (96비트 정수 + 스케일) |
| 값 표현 방식 | 2의 거듭제곱의 합 (1/2, 1/4, 1/8, ...) | 10의 거듭제곱의 합 (1/10, 1/100, ...) |
| 0.1 표현 | 근사값 (0.000110011001100... 무한반복) | 정확한 값 |
| 주요 용도 | 과학/공학 계산 | 금융 계산 |
| 정밀도 | 15-16자리 (10진수) | 28-29자리 (10진수) |
| 처리 방식 | CPU 하드웨어 가속 (IEEE 754) | 소프트웨어 처리 |
| 연산 속도 | 빠름 | 느림 (약 10배 차이) |
반올림 오차 문제
- float/double은 이진 소수점으로 인한 오차 발생:
float x = 0.1f;
Console.WriteLine(x + x + x + x + x + x + x + x + x + x);
// 1.0000001 출력
decimal m = 1M / 6M; // 0.1666666666666666666666666667M
double d = 1.0 / 6.0; // 0.16666666666666666
// 반복 연산에서의 오차
decimal notQuiteWholeM = m + m + m + m + m + m;
// 1.0000000000000000000000000002M
double notQuiteWholeD = d + d + d + d + d + d;
// 0.99999999999999989
// 비교 연산에서의 문제
Console.WriteLine(notQuiteWholeM == 1M); // False
Console.WriteLine(notQuiteWholeD < 1.0); // True
C# 불 타입과 연산자
불 타입의 기초
- 불리언(Boolean) 타입은 true/false 두 가지 값만 가질 수 있는 논리값을 표현하는 타입임
- C#의 bool 키워드는 System.Boolean 타입의 별칭임
- 메모리 상에서는 1비트만 필요하지만, 실제로는 1바이트를 사용함
- CPU가 효율적으로 처리할 수 있는 최소 단위가 1바이트이기 때문임
배열에서의 메모리 효율
- 많은 수의 불리언 값을 저장할 때는 BitArray 클래스 사용을 고려해야 함
- System.Collections 네임스페이스에 정의되어 있음
- 각 불리언 값당 1비트만 사용하므로 메모리 효율적임
불리언 형변환
- bool 타입과 숫자 타입 사이의 직접적인 형변환은 불가능함
- 명시적(explicit) 캐스팅도 허용되지 않음
int i = 1;
bool b = (bool)i; // 컴파일 오류
동등성과 비교 연산자
- == : 두 값이 같은지 비교
- != : 두 값이 다른지 비교
- 동등성 연산은 항상 bool 값을 반환함
int x = 1;
int y = 2;
int z = 1;
Console.WriteLine (x == y); // False
Console.WriteLine (x == z); // True
참조 타입의 동등성
- 값형 → 값을 비교 / 참조형 → 같은 ‘객체’를 참조하고 있는지 비교
- 참조 타입의 경우 기본적으로 참조(reference)의 동등성을 비교함
- 같은 내용이라도 다른 객체이면 false를 반환함
- class가 아니라 struct를 비교하는것은 참조 비교가 아니라 값을 비교하는 것이다.
Dude d1 = new Dude ("John");
Dude d2 = new Dude ("John");
Console.WriteLine (d1 == d2); // False - 다른 객체임
Dude d3 = d1;
Console.WriteLine (d1 == d3); // True - 같은 객체를 참조함
public class Dude
{
public string Name;
public Dude (string n) { Name = n; }
}
조건 연산자
- && : 조건부 AND 연산자 (둘 다 true일 때만 true)
- || : 조건부 OR 연산자 (하나라도 true면 true)
- ! : NOT 연산자 (true를 false로, false를 true로)
static bool UseUmbrella (bool rainy, bool sunny, bool windy)
{
return !windy && (rainy || sunny);
}
단락 평가(Short-circuiting)
- && 연산자는 첫 번째 조건이 false면 두 번째 조건을 평가하지 않음
- || 연산자는 첫 번째 조건이 true면 두 번째 조건을 평가하지 않음
- 이를 통해 null 체크 등을 안전하게 수행할 수 있음
// sb가 null이면 Length 속성 접근 전에 평가 중단
if (sb != null && sb.Length > 0) { ... }
비 단락 연산자(&와 |)
- & : 논리 AND (항상 양쪽 피연산자 평가)
- | : 논리 OR (항상 양쪽 피연산자 평가)
- 상태 변경이 필요한 경우가 아니면 &&와 || 사용을 권장함
return !windy & (rainy | sunny); // 양쪽 모두 평가됨
조건(삼항) 연산자
- ? : 연산자는 세 개의 피연산자를 가지는 유일한 연산자임
- 조건식 ? 참일때값 : 거짓일때값 형식으로 사용함
static int Max (int a, int b)
{
return (a > b) ? a : b;
}
문자 타입과 리터럴
- 문자(char) 타입은 유니코드 문자를 나타내는 2바이트(UTF-16) 크기의 타입임
- 문자 리터럴은 작은따옴표로 표현함:
char c = 'A'; // 단순 문자
이스케이프 시퀀스(Escape Sequence)
- 직접 표현하기 어려운 문자는 이스케이프 시퀀스로 표현함
- 역슬래시(\\)와 특별한 의미를 가진 문자의 조합으로 구성됨
char newLine = '\\n'; // 줄바꿈
char backSlash = '\\\\'; // 역슬래시
- 주요 이스케이프 시퀀스 목록:
시퀀스 의미
| \' | 작은따옴표 |
| \" | 큰따옴표 |
| \\ | 역슬래시 |
| \0 | 널 문자 |
| \b | 백스페이스 |
| \f | 폼피드 |
| \n | 줄바꿈 |
- 유니코드 문자는 \u 또는 \x를 사용하여 16진수로 표현할 수 있음:
char copyrightSymbol = '\\u00A9'; // © 문자
char omegaSymbol = '\\u03A9'; // Ω 문자
char newLine = '\\u000A'; // 줄바꿈
문자 형변환
- char는 unsigned short와 같은 크기를 가짐
- char보다 큰 숫자 타입으로의 암시적 변환이 가능함
- 작은 크기의 타입으로 변환할 때는 명시적 캐스팅이 필요함
문자열 타입
- 문자열(string)은 불변(immutable)한 유니코드 문자의 시퀀스를 나타내는 참조 타입임
- 문자열 리터럴은 큰따옴표로 표현함:
string a = "test";
string b = "test";
Console.WriteLine(a == b); // True - 값 비교
- string은 == 연산자 재정의가 되어있다.
- 객체 비교가 아닌 값 비교로 이루어진다.
- 문자 이스케이프 시퀀스를 문자열에서도 사용할 수 있음:
string a = "Here's a tab:\\t";
- 역슬래시가 많이 포함된 경우 가독성이 떨어짐:
string path = "\\\\\\\\server\\\\fileshare\\\\helloworld.cs";
축자 문자열(Verbatim String)
- @ 기호로 시작하는 문자열은 이스케이프 시퀀스를 처리하지 않음:
- \가 일반 문자열이 된다.
string path = @"\\server\\fileshare\\helloworld.cs";
- 여러 줄의 문자열을 직접 표현할 수 있음:
string escaped = "First Line\\r\\nSecond Line";
string verbatim = @"First Line
Second Line";
- 줄바꿈도 같이 출력된다.
- 축자 문자열에서 큰따옴표는 두 번 연속 사용:
string xml = @"<customer id=""123""></customer>";
- 2개의 큰 따옴표가 하나의 큰따옴표 문자로 출력된다.
문자열 연산
- 연산자로 문자열을 연결할 수 있음:
string s = "a" + "b"; // "ab"
- 다른 타입의 값도 자동으로 ToString() 호출하여 연결:
string s = "a" + 5; // "a5"
문자열 보간법(String Interpolation)
- $ 기호로 시작하는 문자열에서 중괄호 안의 식을 값으로 대체함:
int x = 4;
Console.WriteLine($"A square has {x} sides"); // "A square has 4 sides"
- 중괄호 안의 식은 모든 C# 식이 가능하며 ToString()이 자동 호출됨
- 형식 지정자를 콜론 뒤에 추가할 수 있음:
string s = $"255 in hex is {byte.MaxValue:X2}"; // "255 in hex is FF"
- X : 16진수
- X2 : 16진수 2자리를 찍어라
- 조건 연산자 등에서 콜론이 필요한 경우 전체 식을 괄호로 묶음:
bool b = true;
Console.WriteLine($"The answer is {(b ? 1 : 0)}");
- 보간 문자열은 한 줄에 작성해야 하지만, @를 함께 사용하면 여러 줄 가능:
int x = 2;
string s = $@"this interpolation
spans {x} lines";
- 문자열에서 중괄호를 표현하려면 중괄호를 두 번 사용함
배열
배열의 기초
- 배열은 고정된 개수의 같은 타입 변수들을 순차적으로 저장하는 자료구조임
- 배열의 요소들은 메모리에 연속적으로 저장되어 빠른 접근이 가능함
- 모든 배열은 System.Array 클래스를 상속받음
배열의 선언과 사용
- C#의 배열은 []를 ‘인덱서’라고 부르며 재정의가 되어있다.
기본 선언과 접근
- 배열 타입은 요소 타입 뒤에 대괄호([])를 붙여 표현함:
// 5개의 문자를 저장할 수 있는 배열 선언
char[] vowels = new char[5];
// 인덱스로 배열 요소에 접근 (0부터 시작)
vowels[0] = 'a';
vowels[1] = 'e';
vowels[2] = 'i';
vowels[3] = 'o';
vowels[4] = 'u';
Console.WriteLine(vowels[1]); // 출력: e
- 배열도 참조형이기에 null을 할당할 수 있다.
- 반복문으로 배열의 모든 요소에 접근할 수 있음:
// Length 속성으로 배열 크기 확인
for (int i = 0; i < vowels.Length; i++)
Console.Write(vowels[i]); // 출력: aeiou
배열 초기화식
- 선언과 동시에 배열을 초기화할 수 있음:
// 초기화식 사용
char[] vowels = new char[] {'a','e','i','o','u'};
// 더 간단한 형태 - 이렇게만 적어도 가능하다.
char[] vowels = {'a','e','i','o','u'};
배열의 기본값 초기화
- 배열 생성시 모든 요소는 해당 타입의 기본값으로 초기화됨
int[] a = new int[1000];
Console.WriteLine(a[123]); // 출력: 0
- 값 타입 배열의 경우 각 요소가 배열의 일부로 할당됨:
Point[] a = new Point[1000]; //포인트 객체가 1000개 만들어진다.
int x = a[500].X; // 0
public struct Point { public int X, Y; }
- 참조 타입 배열의 경우 각 요소는 null로 초기화됨:
Point[] a = new Point[1000];
int x = a[500].X; // 런타임 에러: NullReferenceException
public class Point { public int X, Y; }
// 참조 타입 배열을 사용하려면 각 요소를 초기화해야 함
Point[] a = new Point[1000];
for (int i = 0; i < a.Length; i++)
a[i] = new Point();
인덱스와 범위 (C# 8.0 이상)
- ^ 연산자를 사용하여 배열의 끝에서부터 요소에 접근할 수 있음:
char[] vowels = new char[] {'a','e','i','o','u'};
char lastElement = vowels[^1]; // 'u'
char secondToLast = vowels[^2]; // 'o'
- Index 타입을 사용할 수도 있음:
Index first = 0;
Index last = ^1;
char firstElement = vowels[first]; // 'a'
char lastElement = vowels[last]; // 'u'
범위 연산자(..)
- .. 연산자로 배열의 일부분을 선택할 수 있음:
char[] firstTwo = vowels[..2]; // 'a', 'e'
char[] lastThree = vowels[2..]; // 'i', 'o', 'u'
char[] middleOne = vowels[2..3]; // 'i'
- ^ 연산자와 함께 사용 가능:
char[] lastTwo = vowels[^2..]; // 'o', 'u'
- Range 타입 사용 가능:
Range firstTwoRange = 0..2;
char[] firstTwo = vowels[firstTwoRange]; // 'a', 'e'
다차원 배열
- 쉼표로 구분된 차원 수를 지정:
int[,] matrix = new int[3,3];
// GetLength() 메서드로 각 차원의 길이 확인
for (int i = 0; i < matrix.GetLength(0); i++)
for (int j = 0; j < matrix.GetLength(1); j++)
matrix[i,j] = i * 3 + j;
// 초기화식 사용
int[,] matrix = new int[,]
{
{0,1,2},
{3,4,5},
{6,7,8}
};
- int[,] : 2차원 배열
가변 배열(Jagged Array)
- 배열의 배열을 나타내며, 각 하위 배열의 길이가 다를 수 있음:
int[][] matrix = new int[3][];
// 각 하위 배열을 개별적으로 초기화
//matrix.Length ; 3
for (int i = 0; i < matrix.Length; i++)
{
matrix[i] = new int[3];
for (int j = 0; j < matrix[i].Length; j++)
matrix[i][j] = i * 3 + j;
}
// 초기화식 사용
int[][] matrix = new int[][]
new int[] {0,1,2},
new int[] {3,4,5},
new int[] {6,7,8,9}
};
경계 검사
- 모든 배열 접근은 실행 시점에 경계 검사를 수행함
- 유효하지 않은 인덱스 사용시 IndexOutOfRangeException 발생:
int[] arr = new int[3];
arr[3] = 1; // IndexOutOfRangeException 발생
변수와 메모리 구조
변수의 개념
- 변수(Variable)는 수정 가능한 값을 저장하는 메모리 공간임
- 프로그램 실행 중에 다른 값으로 변경될 수 있음
- 다음과 같은 종류가 있음:
- 지역 변수(Local variable): 메서드 내에서 선언된 변수
- 매개변수(Parameter): value, ref, out, in 형태로 메서드에 전달되는 변수
- 필드(Field): 클래스나 구조체에 속한 변수(인스턴스 또는 정적)
- 배열 요소(Array element): 배열의 각 항목을 저장하는 변수
스택과 힙의 메모리 구조
- 변수가 저장되는 두 가지 주요 메모리 영역이 있음:
- 스택(Stack): 메서드 호출과 관련된 데이터를 저장하는 영역
- 힙(Heap): 객체가 저장되는 영역
스택 메모리의 특징
- 메서드가 호출될 때마다 스택 프레임이 생성됨
- 지역 변수와 매개변수가 스택에 저장됨
- 메서드 종료시 자동으로 정리됨
- 후입선출(LIFO: Last In First Out) 구조임
static int Factorial(int x)
{
// 매개변수 x와 반환값이 스택에 저장됨
if (x == 0) return 1;
return x * Factorial(x-1); // 재귀 호출마다 새로운 스택 프레임 생성
}
// 사용 예
Console.WriteLine(Factorial(4)); // 출력: 24
힙 메모리의 특징
- 객체(참조 타입의 인스턴스)가 저장되는 영역임
- C++의 동적할당을 생각하면 된다.
- 한번 만들어지면 계속 유지된다.
- 가비지 컬렉터(GC)가 자동으로 메모리를 관리함
- 메모리 릭(누수)이 발생하지 않는다.
- 삭제되는 타이밍 : 참조하는 변수가 없는 메모리가 생길때 가비지 컬렉터가 자동으로 메모리를 삭제해준다.
- 삭제는 정해진 타이밍이 없고 가비지 컬렉터가 작동하는 시기에 삭제되기에 삭제되는 타이밍을 사용할 수 없다.
- 단점 : 성능에 손해가 발생한다.
- 프로그래머가 원할때 메모리 해제가 불가능하다.
- A씬에서 명시적으로 해제하고 B씬으로 넘어가고 싶지만 그런것은 불가능해진다.
- 프로그래머가 원할때 메모리 해제가 불가능하다.
- 참조되지 않는 객체는 GC에 의해 제거됨
using System.Text;
StringBuilder ref1 = new StringBuilder("object1");
Console.WriteLine(ref1); // 출력: object1
// 이 시점에서 ref1이 참조하는 객체는 곧 GC 대상이 됨
StringBuilder ref2 = new StringBuilder("object2");
StringBuilder ref3 = ref2; // ref2와 ref3가 같은 객체를 참조
Console.WriteLine(ref2); // 출력: object2
Console.WriteLine(ref3); // 출력: object2
// 두 참조가 같은 객체를 가리키므로 아직 GC 대상이 아님
- ref1 : 스택 영역에 올라가는 변수
- new StringBuilder("object1") : 힙 영역에 저장되는 메모리공간
명확한 할당(Definite Assignment)
- C#은 엄격한 변수 할당 규칙을 가짐
- 이는 초기화되지 않은 변수 사용으로 인한 버그를 방지함
- 주요 규칙:
- 지역 변수는 사용하기 전에 반드시 할당되어야 함
- 메서드 매개변수는 호출 시점에 값이 제공되어야 함
- 필드와 배열 요소는 자동으로 기본값으로 초기화됨
int x;
Console.WriteLine(x); // 컴파일 오류: x가 할당되지 않음
int[] ints = new int[2];
Console.WriteLine(ints[0]); // 출력: 0 (배열 요소는 자동 초기화)
class Test {
public static int X; // 필드는 자동으로 0으로 초기화
}
Console.WriteLine(Test.X); // 출력: 0
기본값(Default Values)
- 자동 초기화되는 경우의 기본값:
- 숫자 타입: 0
- bool 타입: false
- char 타입: '\0'
- 참조 타입: null
- 구조체: 모든 필드가 기본값으로 초기화된 상태
매개변수 수정자(Parameter Modifiers)
- C#의 기본 매개변수 전달 방식임
- 값의 복사본이 메서드에 전달됨
- 메서드 내부에서 매개변수를 수정해도 원본은 변경되지 않음
int x = 8;
Foo(x); // x의 복사본이 전달됨
Console.WriteLine(x); // 출력: 8 (원본 값은 변경되지 않음)
static void Foo(int p)
{
p = p + 1; // 복사본만 수정됨
Console.WriteLine(p); // 출력: 9
}
참조 타입의 값 전달
- 참조의 복사본이 전달됨
- 객체의 멤버는 수정 가능하지만 참조 자체는 변경 불가
StringBuilder sb = new StringBuilder();
Foo(sb);
Console.WriteLine(sb.ToString()); // 출력: test
static void Foo(StringBuilder fooSB)
{
fooSB.Append("test"); // 원본 객체 수정 가능
fooSB = null; // 참조 복사본만 변경됨
}
ref 수정자(Reference Modifier) - 가장 제약이 없다
- 참조로 매개변수를 전달함
- 메서드가 원본 변수의 저장 공간을 직접 사용함
- 호출자와 메서드가 같은 메모리 위치를 공유함
string x = "Penn";
string y = "Teller";
Swap(ref x, ref y); // ref 키워드로 호출
Console.WriteLine(x); // 출력: Teller
Console.WriteLine(y); // 출력: Penn
static void Swap(ref string a, ref string b)
{
string temp = a;
a = b;
b = temp;
}
out 수정자
- ref와 유사하지만 초기값이 필요 없음
- 메서드는 반드시 out 매개변수에 값을 할당해야 함
- null 할당도 가능하다
- 여러 값을 반환할 때 유용함
string a, b;
Split("Stevie Ray Vaughan", out a, out b);
Console.WriteLine(a); // 출력: Stevie Ray
Console.WriteLine(b); // 출력: Vaughan
void Split(string name, out string firstNames, out string lastName)
//void Split(string name, out string firstNames, out string_) //이것도 가능하다
{
int i = name.LastIndexOf(' ');
firstNames = name.Substring(0, i);
lastName = name.Substring(i + 1);
}
out 변수 선언과 무시
- C# 7부터 out 매개변수를 호출 시점에 선언 가능
- 불필요한 out 매개변수는 _로 무시 가능
// 호출 시점에 변수 선언
Split("Stevie Ray Vaughan", out string a, out string b);
// 두 번째 매개변수 무시
Split("Stevie Ray Vaughan", out string a, out _);
// 여러 개의 out 매개변수 중 일부만 사용
SomeBigMethod(out _, out _, out _, out int x, out _, out _, out _);
in 수정자
- 참조로 전달하되 수정을 방지함
- 큰 구조체를 효율적으로 전달할 때 유용함
- 메서드 내에서 매개변수 수정 시도 시 컴파일 오류 발생
// in 매개변수 정의
void Foo(in SomeBigStruct a) { ... }
// 호출 방법
SomeBigStruct x = ...;
Foo(x); // in 키워드 생략 가능
Foo(in x); // in 키워드 명시적 사용
params 수정자 - 가변 매개변수
- 메서드의 마지막 매개변수에 사용됨
- 가변 개수의 인수를 배열로 전달받을 수 있음
- 배열 타입으로 선언해야 함
int total = Sum(1, 2, 3, 4);
Console.WriteLine(total); // 출력: 10
// 배열로도 전달 가능
int total2 = Sum(new int[] { 1, 2, 3, 4 });
Console.WriteLine(total2); // 출력: 10
int Sum(params int[] ints)
{
int sum = 0;
for (int i = 0; i < ints.Length; i++)
sum += ints[i];
return sum;
}
- params 위치에 인수가 없으면 빈 배열이 생성됨
- 일반 배열을 params 매개변수로 전달할 수도 있다