(공부 중)
a[i]를 i[a]로 써도 똑같이 돌아감a[i]는 *((a) + (i)) 와 정확히 동치인 표현이다. 덧셈은 교환 법칙이 성립하므로 *((i) + (a))와 동일하므로 i[a]가 작동한다.
&a[i]는 그러므로a + i이다.printf("Enter 5 numbers: "); for (int i = 0; i < 5; ++i) scanf("%d", arr + i);나는 위처럼 사용하는 편.
a[i][j]는 같은 방식으로(*(a + i))[j]=*(*(a + i) + j)이다.
typedef 키워드는 문법이 typedef <original> <new>라면서, 애매한 것들은 뭐임?StackOverflow 참고.
// typedef <original> <new>
typedef int int32;
typedef double float64;
typedef int* intp;
typedef unsigned int uint;
// 근데 이런 건 좀 애매하네; 이름 대신 쓰는 느낌?
typedef char str20[20];
typedef int (*binary_operator)(int, int);
typedef int (*array_pointer)[5];
typedef int* pointer_array[4];
// ???
long typedef long a;
애초에 typedef <original> <new> 형식이 아니고, 위의 모든 예제를 아우르는 규칙 비스무리 한 것이 하나 있다. 변수 선언과 문법이 동일하다는 것. 새로 정의하는 타입 이름을 그냥 변수 이름이라고 생각해보면 static int x나 typedef int i나 굉장히 비슷하다.
ANSI C 표준의 부록 A에서 Backus-Naur 형태 문법을 보면 typedef 키워드는 저장 클래스 지정자(storage class specifier)로 분류된다. 즉, auto, register, static, extern과 동일한 포지션이다. (4개가 아니라 5개였다는 사실) 참고
그렇기에, typedef static int sint 같은 문법이 허용되지 않는 이유 또한 저장 클래스 지정자가 2개 쓰였기 때문이다.
그러나 다른 저장 클래스 지정자들과 달리 메모리 저장 방식과는 전혀 무관하며 오직 문법적 편의성을 위한 것이다. 또한, 새 타입 명은 실제로는 변수도 아니기 때문에 typedef int i = 3 같은 것은 컴파일 에러를 발생시킨다.
따라서, long typedef long a는 typedef long long a와 동일하다. int static x나 static int x가 똑같듯이.
int[]과 포인터 int *는 동일한가?기본적으로 배열과 포인터는 다른게 맞다.
문법적으로 비슷한 부분이 있고 개념도 비슷하도록 C언어에서 유도된 거지 같다고 할 수 없다.
일단 sizeof만 넣어봐도 둘이 다른 값을 뱉는 것을 보아 런타임 단에서도 다른 개체로 취급됨을 방증한다. 그리고 배열은 주소를 변경할 수 없어서, int* const와 더 유사하다고 할 수 있다.
&arr은 왜 됨?int arr[3] = { 1, 2, 3 };
printf("%p %p\n", arr, arr + 1); // 1000 1004
printf("%p %p\n", &arr, &arr + 1); // 1000 1012
printf("%p %p\n", &arr[0], &arr[0] + 1); // 1000 1004
// 이렇게 하면 다시 arr과 같아지는게 형변환 당한거고
int* p = &arr;
printf("%lu %lu\n", p, p + 1); // 1000 1004
// 이렇게 하니까 원래대로 되는 걸 보아 &arr은 배열 포인터임
int (*q)[3] = &arr;
printf("%lu %lu\n", q, q + 1); // 1000 1012
위 코드에 따르면 arr과 &arr[0]이 동일한 값은 맞는데, &arr은 배열 포인터라 다름
char*, char[]에 대해 초기화할 때 read only 여부 등이 갈리는 것을 볼 수 있다.
// 이렇게 하면 문자열이 스택에 저장됨.
char c1[] = "Hello";
char* c2 = c1;
printf("%s\n", c2);
c2[0] = 'A';
printf("%s\n", c2);
// 이건 ROM (Read-Only Memory)에 저장됨. 상수니까 Text(Code) 영역에 저장될듯.
char* c3 = "Hello";
printf("%s\n", c3);
c3[0] = 'A'; // UB - Runtime Error
printf("%s\n", c3);
배열에 string literal을 사용하면 문자열이 스택에 저장되어 c2[0] = 'A'와 같이 mutable하나, 그렇지 않고 포인터에 넣으면 read only가 되어 immutable해짐. c3[0] = 'A' 사용 시 UB, Runtime Error가 발생함
그저 주솟값의 Pass By Value, 복사된 값임.
C++에는 Pass By Reference를 언어적 차원에서
&참조 연산자를 추가하여 지원한다.
간혹 C++의 Pass By Reference나 C나 동일한 원리인데 C에는 Pass By Reference가 없다고 말할 수는 없느냐는 토의가 있는데, 개발자가 구현하는 것은 가능하나, 언어적 차원에서 '지원'한다고 보기는 어렵다.
같은 논리를 확장하면 어셈블리도 Pass By Reference를 지원한다고 해야한다.
사실 C언어는 스택, 힙의 개념을 전혀 사용하지 않고 (표준에 관련 단어가 전혀 등장하지 않는다), 저장 수명 (storage duration)을 사용한다.
저장 수명 (storage duration)
static, thread, automatic, allocated의 4종류가 있으며,
- static은 프로그램의 시작부터 끝에까지 존재하고
- thread는 쓰레드별로 분리하여 존재하며
- automatic은 해당 블록이 시작할 때 생성되어 끝날 때 소멸되고,
- allocated는 메모리 할당 함수들에 의해 생성되고 free() 함수로 해제될 때까지 존재한다.
주로, static은 bss/data 영역에, automatic은 stack, allocated는 heap에 저장하나 보장되지는 않는다.
나무위키
그러나 그냥 스택과 힙에 저장된다고 공부하고 있는게 편하다.
__int128?16바이트 크기의 정수 타입이 존재한다. 표준은 아닌 것 같다. alignment 또한 16byte이다.
#include <stdio.h>
int add(int x, int y) {
return x + y;
}
int main() {
int (*f)(int, int);
f = add;
printf("%d\n", f(1, 3));
printf("%d\n", (*f)(1, 3));
printf("%d\n", (**f)(1, 3));
printf("%d\n", (***f)(1, 3));
printf("%d\n", (***********f)(1, 3));
f = &add;
printf("%d\n", f(1, 3));
return 0;
}
문제 없이 작동하는 코드다. StackOverflow
언어적으로 함수 포인터 자체가 특별한 문법이라, 역참조나 참조 등에 대한 cover가 이루어지지 않은 것처럼 보인다. 맞는지는 모름.
struct test {
char a;
char b;
char c;
};
반례) 이 구조체는 사이즈가 3byte이다.
그리고, 저 설명 한 줄로는 어디에 패딩이 추가되는 것이 적절한지 이해하기 힘들다.
간혹, 멤버 변수 중 가장 큰 사이즈 (sizeof로 얻어지는 값)를 기준으로 패딩이 맞춰진다고 하는데 이도 반례가 존재한다.
struct A {
int a;
int b;
int c;
};
struct B {
struct A a;
int b;
};
// 예상: A의 사이즈는 12byte이므로, B는 12 + 4 + (패딩 8) = 24byte
// 실제: 16byte
struct test {
char a[5];
int b;
};
// 예상: a의 사이즈가 5byte이므로, test 구조체는 5 + 4 + (패딩 1) = 10byte
// 실제: 5 + (패딩 3) + 4 = 12byte
http://www.catb.org/esr/structure-packing/ 에서 제대로 알 수 있었다. 정확히는 'Size'가 아니라 'Alignment' 중 최댓값을 기준으로 패딩이 형성된다.
현대 프로세서에서는 컴파일러가 기본 데이터 타입을 메모리에 배치할 때 특정 제약을 따른다. C언어는 모든 개체가 특정 주소에서 시작해야 한다는 제약이 있으며 이는 메모리 접근 속도를 높이기 위해서이다. 이런 제약은 Intel, ARM, RISC-V 등 대다수의 현대 ISA(명령어 집합 구조, Instruction Set Architecture)들에서 동일하게 적용된다.
char: 1배수, 즉 어디든 가능short: 2배수, 짝수 주소에서 시작해야 함int, float: 4배수 주소long, double: 8배수 주소__int128: 16배수 주소메모리에 위의 규칙을 준수하지 않고 데이터를 배치하면 CPU가 데이터를 한 번에 읽지 못하고 나눠 읽어야 하여 성능 저하가 발생한다.
그렇다면, Alignment를 사용하여 코드를 이해해보자.
char c;
// char pad1[M];
char *p;
// char pad2[N];
int x;
여기서 M, N를 추론해보자.
우선, 64bit 운영체제를 사용하고 있다 가정하면 p는 포인터이므로 8배수 주소에 잘 할당 되었을 것이므로 4배수 주소에서 시작해야하는 x가 바로 이어올 수 있다. 즉, N은 0이다.
M은 상황이 다른데, c는 어느 주소에서 시작해도 상관 없지만 p는 8배수 주소에 할당되므로 c가 할당된 주소에 따라 M은 0부터 7까지의 모든 정수가 가능하다.
이번엔 동일한 레이아웃을 구조체에 대해 생각해보자.
struct test {
char c;
// char pad1[M];
char *p;
// char pad2[N];
int x;
};
우선, 구조체 자체는 본인 자체의 Alignment에 맞게 첫 번째 바이트에 잘 배치될 것이므로 c가 첫 번째 바이트에 할당되면 M은 7임을 알 수 있다. N은 동일하게 0.
struct test {
long a;
char b;
short c;
int d;
};
a로 8byte, b가 1byte, c의 alignment가 2byte이므로 앞에 패딩 1byte, 자체 사이즈 2byte, d가 4byte로 총 16byte. 그리고 struct test 자체 alignment는 최댓값인 8byte이다.
구조체 끝의 패딩인 stride address는 구조체 다음에 오는, 구조체와 동일한 정렬 조건을 만족해주기 위한 패딩이다.
struct test {
char *p;
char c;
};
이 구조체는 얼핏 보면 9byte지만, 실제로는 16byte이다. struct test 자체의 alignment가 8이기 때문에 맨 뒤에 7byte의 패딩이 추가된다.