在本系列的最后一部分中,我们讨论了基本的硬件基本原理和汇编语言。现在是时候看一下推动现代编程的最大影响的编程语言之一:C。
起源
c当然不是集会后出现的第一种高级语言。实际上,大约有20种语言早于C,包括COBOL,FORTRAN和BASIC。丹尼斯·里奇(Dennis Ritchie)与肯·汤普森(Ken Thompson)紧密合作,在1972年首次开发了该语言。他们俩当时都在开发UNIX的同一团队中。 C名称来自它早于的语言:B语言。
标准化
Brian Kernighan随后建议Dennis写书。 1978年,“ C编程语言”(通常称为K&R书籍)。这是第一个C规范,C有时称为“ K&R C”。 1985年,美国国家标准研究所(ANSI)发布了C规格的初稿。经过大量工作,它在1989年被批准成为第一个ANSI C,通常称为C89。 C17是当前的标准,而C2X正在进行中(据信它将在2023年完成,成为C23)。
实际的C语言自己做得并不多。为了提供功能,您希望标准C库将大量使用。大多数Linux操作系统都使用GLIBC,即C库的GNU实现。另一个标准的C库是Musl,它更轻巧,并且由Alpine Linux使用(许多容器图像)。这些库包含一定级别的POSIX specification for the C library。重要的是要注意,某些C库也可以具有针对使用C库的功能的扩展,因此,不是100%的代码是整个系统中的标准配置。
代码布局
C程序主要由两个主要文件组成:标题文件和源代码文件。标头文件用于描述功能,结构等的定义。源代码文件为定义的函数提供了实际的代码实现。这是一个非常简单的示例(请注意,这不包括一些最佳实践,以简化目的):
my_code.h
int my_code(int x);
my_code.c
int my_code(int x)
{
return x + x;
}
main.c
#include <stdio.h>
#include "my_code.h"
int main(void)
{
int y = my_code(6);
printf("%d\n", y);
return 0;
}
如果我使用gcc
构建此文件,则假设文件在同一目录中:
$ gcc -o my_binary main.c my_code.c
$ $ ./my_binary
12
One GCC呼叫看起来很简单,但实际上正在发生很多事情,这将在下一部分中涵盖,因为这将需要大量的时间来描述。要注意的一个特别有趣的项目是代码块的{}
和;
终止语句,您会以某些语言(例如JavaScript)找到。
C功能
因此,请查看上面的内容,我会剖析一些C功能。
预处理器指令
#include <stdio.h>
#include "my_code.h"
#include
是被称为预处理器指令。 #include
类似于导入模块代码,尽管技术实现大不相同。至于<> vs double引号,<>参考系统级标头文件和“”旨在引用项目内部的标头文件。包括指令还可以使用路径,因此:
#include <netinet/tcp.h>
#include "../my_code/my_code.h"
#include "/some/path/here/header.h"
可能包含在内的方法(尽管绝对路径不是很可维护的,也不建议使用)。也可以做这样的奇怪的事情:
#include <stdio.h>
#include "stdio.h"
尽管您永远不应该这样做。特别是<stdio.h>
是标准C库之一,涉及标准缓冲输入和输出。这就是声明printf
或格式打印的内容。还有其他一些预处理器指令可以:
- 定义值
- 给特定类型A标签
- 使有条件的代码存在
这使它们非常有组织地组合代码。
打字
在某些语言中,您可能会看到类似于以下的代码:
my_variable = "Hello World"
您不需要告诉基础语言这是字符串。相反,C是一种打字语言,这意味着您必须实际说出哪种类型变量是:
int my_variable = 5;
某些值也可能会被签名,这增加了最大可能的值范围:
unsigned int my_variable = 5;
特定类型还具有特定的字节大小。例如:
int my_number = 10;
printf("%lu\n", sizeof(my_number));
这将打印4,因为int
类型为4个字节。这里的主要优点是,数据类型的定义非常低,并适合确切的字节大小。与某些现代语言相比,这使得类型非常轻,甚至可以将各种属性附加到原始数据类型上。尽管这是以偶尔必须经过巨大长度来实现操作的代价,这些操作得到了所述类型附加的其他属性的支持。
字符在技术上也被用作可以映射到整数的值。操作系统具有字符编码的概念,可将二进制值映射到其各自的字符。更常用的是ASCII编码,该编码在扩展版本中从0到255(在正常版本中为127)。这意味着这两个都会打印出一个小写A:
char x = 'a';
char y = 97;
这里要注意的是,""
适用于字符串,而''
适用于字符。
数组
数组是在一个项目中包含许多值的方法。一些编程语言将类似的功能与列表相似。阵列通过基于所保存的数据类型分配内存的顺序块来对后端进行工作。例如:
int my_ints[4] = { 1, 7, 9, 10 };
int my_ints[] = { 1, 7, 9, 10 };
第一种声明类型分配了特定数量的元素。如果没有通过[]
声明尺寸约束,则将通过声明的元素自动设置。现在,尽管您可以在不给它们值的情况下初始化数组,但结果并不是您期望的:
#include <stdio.h>
int main(void)
{
int x[4];
printf("%d\n", x[1]);
return 0;
}
$ ./my_binary
32766
给定的值低于整数数据类型的最大值。这不是很有用,这种行为不是最好的实践,不应依靠。有一种方法可以采用固定数组,并通过确定的默认值分配了所有值:
int x[4] = {0};
这将带来一个更容易推理的结果,并使调试少问题。数组的另一个有趣的概念是,它们的第一个元素从0开始,最后一个元素是size of array - 1
:
int main(void)
{
int my_ints[4] = { 1, 7, 9, 10 };
printf("%d\n%d\n%d\n%d\n", my_ints[0], my_ints[1], my_ints[2], my_ints[3]);
printf("%d\n", my_ints[4]);
return 0;
}
第一个printf
将打印所有数组值,第二个printf
将具有不确定的行为,从本质上讲,试图在数组的边界之后访问内存。迭代是另一个有趣的概念。在C中,大多数情况下,这是通过for循环发生的,这会迭代段落索引。有两种基本方法可以实现这一目标:
// static declaration
int array_size = 4;
int my_ints[array_size] = { 1, 7, 9, 10 };
// dynamic declaration
int my_ints[] = { 1, 7, 9, 10 };
int array_size = sizeof(my_ints) / sizeof(my_ints[0]);
// loop
for (int i = 0; i < array_size; i++) {
printf("%d\n", my_ints[i]);
}
动态声明之所以起作用,是因为数组是许多特定类型的内存段。重要的是要注意,sizeof()
可以使用字节,因此数组的大小是内存的总尺寸,除以其所容纳的对象之一的大小。由于最后一个元素索引是最后一个元素-1,迭代子句使用< array_size
。相反,使用<=
会导致通常被称为“一个off One”错误。
字符串
从技术上讲,每个弦类型都不是。取而代之的是字符串是一组字符,由通常写为\0
的空字符终止。 C:
中字符串的一个例子
#include <stdio.h>
int main(void)
{
char hello_world1[] = "Hello World";
char hello_world2[12] = { 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '\0' };
char hello_world3[12] = { 72, 101, 108, 108, 111, 32, 87, 111, 114, 108, 100, 0 };
printf("%s\n%s\n%s\n", hello_world1, hello_world2, hello_world3);
return 0;
}
输出给出:
$ ./my_binary
Hello World
Hello World
Hello World
此处提到的"
表格是针对字符串的,并让您省略“ 0” null字符。有趣的是,尽管字符串不是C的特定数据类型,但C库确实包含string.h
,并具有一些有用的功能用于使用字符串。
结构
结构(有时称为“结构”)是C中的数据类型,用于封装用于结构化访问的各种数据类型。这是语言最接近对象的概念,尽管更原始。示例声明和实例化:
#include <stdio.h>
typedef struct {
int x;
int y;
int z;
} Numbers;
int main(void)
{
Numbers my_numbers = { 1, 2, 3 };
Numbers my_numbers2 = { .x = 1, .z = 3, .y = 2 };
printf("%d\n%d\n", my_numbers.z, my_numbers2.z);
return 0;
}
首先,typedef
是该语言的有趣部分,它可以使您可以从本质上吸用具有名称的特定类型。从技术上讲,您可以做类似struct Numbers
的事情,但是您必须将其称为“ struct Numbers
”。 typedef
允许您使用此功能,因此您只需要键入Numbers
即可使用变量类型,如果您习惯了现代语言处理它的方式,则可以更容易使用。格式:
Numbers my_numbers2 = { .x = 1, .z = 3, .y = 2 };
实际上是在后来的C标准(C99)中实现的一种初始化方法。它允许分配类似于某些语言如何处理关键字参数。基本级别上的成员访问使用.
符号,您可能会在其他多种语言中看到对象属性访问的内容。
指针
按价值通过并通过引用传递是您在编程语言中可能遇到的概念。指针本质上是通过参考。而不是传递整个值,而是只有一个变量指向您在内存中想要的值。这主要是由于在计算早期挤压性能的需求而出现的,当时记忆,CPU,网络吞吐量和磁盘空间不存在于今天的丰富之处。这是指针声明和实例化的示例:
#include <stdio.h>
int main(void)
{
int x = 3;
int * y = &x;
printf("%d\n", *y);
return 0;
}
这将输出指向y的值,即x或3。&
用于获取一个值需要指向的值的地址位置。然后,*
用于降低指针并在指向的内存地址获得值。请注意,由于指针需要指向某些东西,因此不能将它们分配给以某种形式实例化的东西,因此这不起作用:
int *y = &3;
因为3只是一个数字,并且没有分配的地址空间。与指针的另一个有趣的关系是能够使用指针数学遍历数组。例如:
#include <stdio.h>
int main(void)
{
int my_numbers[] = { 1, 2, 3, 4 };
printf("%d\n%d\n", *my_numbers, *(my_numbers + 1));
return 0;
}
这是因为数组在某种意义上是指向一系列内存块中的第一项的指针。因此,将1添加到指针中,将其移动到下一个顺序内存块或下一个元素。不幸的是,这创造了一个有趣的困境安全:
int my_numbers[] = { 1, 2, 3, 4 };
printf("%d\n%d\n", *my_numbers, *(my_numbers + 7));
尽管数组没有8个元素,但我的编译器可以让通行证可以运行,操作系统很好。这里的危险是指针可以访问记忆。恶意演员可以使用代码中的指针错误来读取他们不应该的内存,或者将事物放在记忆中以将其作为恶意代码执行。
动态内存
那么,如果您不知道运行时数组的大小怎么办? C库的标题称为stdlib.h
,该标头包含功能以动态分配此类内存。它还可以使您能够调整内存大小。例如:
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int original_size = 4;
int * my_numbers = (int*) calloc(original_size, sizeof(int));
my_numbers[0] = 10;
printf("%d\n", my_numbers[0]);
my_numbers = realloc(my_numbers, sizeof(int) * (original_size + 4));
my_numbers[7] = 23;
printf("%d\n", my_numbers[7]);
free(my_numbers);
return 0;
}
因此,首先是呼叫calloc
。这与malloc
不同,该malloc
初始化了一个内存块,但没有将其设置为任何东西。第一个论点是多少个项目,第二个参数是字节中每个项目的大小。 (int*)
称为演员,从本质上讲,该系统分配的内存是为了指向INT的指针。接下来是realloc
:
my_numbers = realloc(my_numbers, sizeof(int) * (original_size + 4));
my_numbers[7] = 23;
printf("%d\n", my_numbers[7]);
第一个参数是由calloc
保留的内存,下一个是要调整它的大小。与calloc
不同,您必须进行尺寸乘法。这为我们提供了一个具有8个元素的调整大小的数组,从而使my_numbers[7]
起作用。现在最重要的部分:
free(my_numbers);
这可以释放记忆。对于这样的小程序,从技术上讲,它不会伤害您,因为操作系统将释放程序附加的分配内存。例如,例如Web服务器(例如Web服务器)的长期运行程序。只要malloc
或calloc
尖的内存未释放,其他应用程序就无法使用。如果您不这样做,它是一些连续循环的一部分,它将开始吞噬系统内存,直到没有留下。这就是所谓的内存泄漏。这就是为什么始终将free()
用于动态内存后的最佳实践。
结论
因此,这是对C的一种一般性的看法。特别是对指针的解释被缩减了,因为仅此一项就可以填写整篇文章。因此
- 类型绑定到字节大小 +已签名/未签名允许有趣的性能优化
- c仅如果没有C标准库来支持它 ,就不会有很多实际用途
- 指示器和内存分配虽然强大,但如果不正确使用,可能会产生灾难性的后果
- 可变初始化有时会变得相当奇怪,如果使用指针或动态内存
- C中没有多种数据类型,因此必须在需要时手工制作它们(例如诸如哈希表/地图/词典之类的东西)
- C非常基于限制资源,该资源无法很好地映射到现代硬件(嵌入式系统除外)
- 由于原始c的原始c如何需要更多时间来完成现代语言的简单任务,例如与JSON REST APIS进行交流
但是,了解C的工作原理可以帮助理解使用一些更现代的语言以及它们可能如何实施事物的容易。在下一部分中(可能需要大约一周左右的时间才能写出),我将研究如何编译C代码以帮助理解机器过程中运行的代码。