logo

C언어의 특징과 함수 알아보기

C · 2022년 11월 26일 · 25 min read
poster

C언어란?

C언어는 ALGOL의 전통을 이어받은 명령형 절차형 언어이다. C언어에서 실행 가능한 모든 코드는 서브 루틴(함수형 프로그래밍의 의미는 아니지만 '함수'라고도 함)에 포함된다. 함수의 매개변수는 값으로 전달되지만, 배열은 포인터, 즉 배열의 첫 번째 항목의 주소로 전달되는데, C언어에서는 참조할 대상에 대한 포인터를 명시적으로 전달함으로써 참조 전달을 시뮬레이션하고 있다.

C 프로그램의 소스 텍스트는 자유 형식의 코드이다. 세미콜론은 문장을 끝내고, 중괄호는 문장을 블록으로 묶는 데 사용된다.

C언어의 언어적 특징

이 언어에는 if/else, for, do/while, while, while, switch와 같은 제어 흐름 프리미티브의 전체 세트를 포함한 몇 가지 고정된 키워드가 있다. 사용자 정의 이름은 키워드와 구별되지 않는다. 많은 산술 연산자, 비트 연산자, 논리 연산자(+, +=, ++, &, || 등)가 있다. 한 문장에서 여러 대입을 할 수 있다.

함수

함수의 반환값은 필요하지 않은 경우 무시할 수 있다. 함수와 데이터 포인터는 임시 실행시간 다형성을 가능하게 한다. 함수는 다른 함수의 어휘 범위 내에서 정의할 수 없다. 함수의 범위 내에서 변수를 정의할 수 있다. 함수는 스스로를 호출할 수 있으므로 재귀가 지원된다. 데이터 타입은 정적이지만 약하게 강제된다. 모든 데이터는 타입을 가지지만 암묵적인 변환이 가능하다. 사용자 정의형(typedef)과 복합형이 있다. 이종 집합 데이터 유형(struct)을 통해 관련 데이터 요소를 하나의 단위로 접근하고 할당할 수 있다. 전체 구조체의 내용은 단일 내장 연산자를 사용하여 비교할 수 없다(요소는 개별적으로 비교해야 한다).

유니온은 중복된 멤버를 가진 구조체로서 여러 데이터 유형이 동일한 메모리 위치를 공유할 수 있도록 한다. 배열의 인덱싱은 이차 표기법이며, 포인터 연산으로 정의된다. 전체 배열을 하나의 내장 연산자로 할당하거나 비교할 수 없으며, "array" 키워드는 사용되지도 정의되지도 않는다. 대신 대괄호는 예를 들어 month와 같이 구문적으로 배열을 나타낸다. 열거형은 enum 키워드로 지정할 수 있다. 이들은 정수와 자유롭게 상호 변환할 수 있다. 문자열은 별도의 데이터 유형은 아니지만 관례적으로 널 종결 문자 배열로 구현된다. 컴퓨터 메모리에 대한 저수준 액세스는 기계 주소를 포인터로 변환하여 가능하다. 프로시저(값을 반환하지 않는 서브 루틴)는 함수의 특수한 경우이며, 빈 반환값 유형은 void이다. 메모리는 라이브러리 루틴을 호출하여 프로그램에 할당할 수 있다. 프리프로세서는 매크로 정의, 소스 코드 파일 포함, 조건부 컴파일을 수행한다. 파일들은 개별적으로 컴파일하고 링크할 수 있으며, static 속성과 extern 속성을 통해 어떤 함수나 데이터 객체가 다른 파일에서 볼 수 있는지를 제어할 수 있다. I/O, 문자열 조작, 수학 함수와 같은 복잡한 기능은 일관되게 라이브러리 루틴에 위임된다. 컴파일 후 생성된 코드는 기본 플랫폼에서 비교적 간단한 요구 사항을 가지고 있기 때문에 운영체제를 만들거나 임베디드 시스템에서 사용하기에 적합하다. C 언어에는 다른 언어에서 볼 수 있는 특정 기능(객체 지향, 가비지 수집 등)이 포함되어 있지 않지만, 외부 라이브러리(GLib 객체 시스템, Boehm 가비지 컬렉터 등)를 사용하여 이를 구현하거나 에뮬레이트할 수 있다.

C언어의 언어적 기원

C 언어의 기원은 데니스 리치와 켄 톰슨이 PDP-7에서 어셈블리 언어로 구현한 유닉스 운영체제의 개발과 밀접한 관련이 있다. 결국 그들은 운영체제를 PDP-11로 포팅하기로 결정했고, 유닉스의 원래 PDP-11 버전도 어셈블리 언어로 개발되었다.

C언어의 메모리 관리 방법

프로그래밍 언어의 가장 중요한 기능 중 하나는 메모리와 메모리에 저장된 객체를 관리할 수 있는 기능을 제공하는 것. C 언어는 객체에 메모리를 할당하는 세 가지 주요 방법을 제공한다.

정적 메모리 할당: 컴파일 시 바이너리에 객체를 위한 공간이 제공된다. 이러한 객체는 이를 포함하는 바이너리가 메모리에 로드되는 한, 그 객체들은 확장(또는 수명)을 가진다. 자동 메모리 할당: 임시 객체를 스택에 저장할 수 있으며, 선언된 블록이 종료되면 이 공간은 자동으로 해제되어 재사용할 수 있게 된다. 동적 메모리 할당: 임의의 크기의 메모리 블록을 힙이라는 메모리 영역에서 malloc과 같은 라이브러리 함수를 사용하여 런타임에 요청할 수 있다. 이 블록은 이후 라이브러리 함수의 realloc 또는 free를 호출하여 재사용될 때까지 지속된다. 이 세 가지 접근법은 서로 다른 상황에서 적절하며, 서로 다른 트레이드오프가 있다. 예를 들어, 정적 메모리 할당에는 할당 오버헤드가 거의 없고, 자동 할당에는 약간의 오버헤드가 있으며, 동적 메모리 할당에는 할당과 해제 모두에서 큰 오버헤드가 발생할 수 있다. 정적 객체의 영구적인 특성은 함수 호출에 걸쳐 상태 정보를 유지하는 데 편리하고 자동 할당은 사용하기 쉽지만, 스택 공간은 일반적으로 정적 메모리나 힙 공간보다 훨씬 제한적이고 일시적이며, 동적 메모리 할당은 실행 시점에만 그 크기를 알 수 있다. 객체를 편리하게 할당할 수 있다. 대부분의 C 프로그램은 이 세 가지를 모두 많이 사용한다.

가능하면 자동 또는 정적 할당이 가장 간단하다. 왜냐하면, 컴파일러가 스토리지를 관리하고 프로그래머가 수동으로 스토리지를 할당하거나 해제하는 오류 발생 가능성이 높은 작업에서 벗어날 수 있기 때문이다. 그러나 많은 데이터 구조는 실행 시 크기가 변할 수 있고, 정적 할당(및 C99 이전의 자동 할당)은 컴파일 시 고정된 크기여야 하기 때문에 동적 할당이 필요한 상황이 많이 존재한다. (동적으로 할당되는 배열의 예는 malloc 문서를 참고한다). 실행 시 실패하여 제어할 수 없는 결과를 초래할 수 있는 자동 할당과 달리, 동적 할당 함수는 필요한 스토리지를 할당할 수 없는 경우 (널 포인터 값의 형태로) 지시를 반환한다. (너무 큰 정적 할당은 일반적으로 링커 또는 로더에 의해 감지되며, 프로그램이 실행되기 전에 감지된다).

특별한 지정이 없는 한, 정적 객체는 프로그램 시작 시 0 또는 널 포인터의 값을 포함한다. 자동 및 동적으로 할당된 객체는 초기값이 명시적으로 지정된 경우에만 초기화된다. 그렇지 않은 경우, 초기값은 불확실한 값(일반적으로 저장소에 우연히 존재하는 비트 패턴이 무엇이든 상관없으며, 해당 유형에 유효한 값은 아니다)이 된다. 프로그램이 초기화되지 않은 값에 접근하려고 하면 결과는 정의되지 않은 값이 된다. 최근 많은 컴파일러가 이 문제를 감지하고 경고하려고 하지만, 오탐과 미탐이 발생할 수 있다.

힙 메모리 할당은 모든 프로그램에서 가능한 한 많이 재사용될 수 있도록 실제 사용 상황과 동기화되어야 한다. 예를 들어, 힙 메모리 할당에 대한 유일한 포인터가 명시적으로 할당 해제되기 전에 범위를 벗어나거나 그 값을 덮어쓰는 경우, 해당 메모리는 나중에 재사용하기 위해 복구할 수 없어 사실상 프로그램에서 사라지게 된다(메모리 누수라고도 함). 현상). 반대로 메모리가 해제되었음에도 불구하고 나중에 참조되어 예측할 수 없는 결과를 초래할 수도 있다. 일반적으로 오류의 증상은 오류의 원인이 된 코드와 무관한 프로그램의 일부에 나타나기 때문에 오류를 진단하기 어렵다. 이러한 문제는 자동 가비지 컬렉션이 있는 언어에서는 개선된다.