slug
type
status
category
summary
date
tags
password
icon
This is a computer; it’s just a piece of tape that holds ones and zeros along with a device that can read and write to it. It’s called a Turing Machine, and in theory, it can compute anything like the graphics in this video or the algorithm that recommended you to watch it.
At the core of modern computers, we have the Central Processing Unit (CPU). If we crack it open, we find a piece of silicon that contains billions of tiny transistors, which are like microscopic on/off switches. The value at one of these switches is called a bit and is the smallest piece of information a computer can use. However, one bit by itself is not very useful, so they come in a package of eight called a byte. One byte can represent 256 different values, like all the characters you type on your keyboard. In fact, when you type into your keyboard, the character produced is actually mapped to a binary value in a character encoding like ASCII or UTF-8.
Binary is just a system for counting like the base 10 system you normally use when counting on your fingers, but it only has two characters: one and zero. Humans have a hard time reading binary, so most often it’s represented in a hexadecimal base 16 format, where ten numbers and six letters can represent a four-bit group called a nibble. As a developer, when you write code in a programming language, it will eventually be converted into machine code, which is a binary format that can be decoded and executed by the CPU. What it doesn’t do, though, is store data for your applications. For that, computers have Random Access Memory (RAM). It’s like a neighborhood, and inside every house lives a byte. Every location has a memory address that the CPU can read and write to.
You can think of the CPU and RAM as the brain of the computer, but in order for a computer to be useful, it needs to handle input and output. An input device might be the keyboard and mouse, while an output device might be your monitor. Luckily, most developers don’t need to worry about how this hardware fits together because we have operating system kernels like Linux, Mac, and Windows that control all hardware resources via device drivers. Now, to start hacking on the operating system, your first entry point is the shell, which is a program that exposes the operating system to the end user. It’s called a shell because it wraps the kernel. It takes a line of text as input and produces an output. This is called a command line interface. Not only can it connect to your own computer, but with the Secure Shell (SSH) Protocol, it can also connect to remote computers over a network.
Now that you have access to the mainframe, it’s time to pick a programming language, which is a tool that uses the abstraction principle to make computers practical to work with for humans by simplifying different systems layer by layer. Some languages, like Python, are interpreted. That means there’s a program called an interpreter that will execute each line of code one by one. Other languages, like C++, are compiled. They use a compiler to convert the entire program into machine code in advance before the CPU attempts to execute it. This results in an executable file that can be run by the operating system without any extra dependencies.
Now, every programming language has a variety of built-in data types to represent the data we’re working with in our code. Instead of bytes, we work with more human-friendly things like characters and numbers. The most fundamental way to use data in your application is to declare a variable. This attaches a name to a data point, allowing you to reuse it somewhere else in your code. Python is a dynamically typed language, which means we don’t need to tell the program exactly which data type is assigned to a variable; it just figures it out automatically. However, other languages like C are statically typed, meaning you need to specify the data type of a variable in your code.
When you define a variable, its value is stored somewhere in memory on the hardware, and you may need to allocate and free up memory throughout the program. A pointer is a variable whose value is the memory address of another variable, which can be used for low-level memory control. Many languages don’t want to deal with low-level memory management and instead implement a garbage collector, which automatically allocates and deallocates memory when an object is no longer referenced in the program.
Now, the data types available are different in every programming language, but typically you’ll find int to represent whole numbers, which may or may not be signed or unsigned to represent negative numbers as well. When numbers require a decimal point, they typically use the floating-point type. It’s called a float because there’s only enough memory to represent a certain range of numbers at a certain precision and is basically a form of scientific notation to make computers faster. If you need more range or precision, many languages also have a double that doubles the amount of memory used for the number. When it comes to characters, you’ll typically find the char data type to represent a single character or, more commonly, a string to represent multiple characters together.
Ultimately, these characters get stored in a memory address somewhere, but they need to be stored in a certain order. When the order starts with the most significant byte at the smallest memory address, it’s called big-endian, or vice versa if the least significant byte is stored at the smallest address, it’s called little-endian.
When it comes to practical software engineering, one of the most fundamental things we do is organize data into data structures. The most useful data structure is probably the array or list. Just like a shopping list, it organizes multiple data points in order. However, it also maintains an index of integers that starts at zero and goes up for every new item in the list. That can be useful, but you don’t actually need an index to create a list of items. Another option is a linked list, where each item has a pointer to the next item in front of it. Another option is a stack that follows the last-in, first-out principle. It’s like stacking a set of plates; when you want to access the data, you pop the last one off the top. The inverse option is a queue, which is first-in, first-out. Just like when you get into the red line, the first person there is the first one to be fed.
Another extremely useful data structure is the hash, which might also be called a map or dictionary. It’s like an array, but instead of an index of integers, you define the keys that point to each individual item, giving you a collection of key-value pairs. In many cases, though, it’s not efficient to organize data in a linear way. To address that problem, we have trees, which organize nodes together in a hierarchy that can often be traversed more quickly. This can sometimes be too rigid of a data structure, so instead, a graph can be created to connect multiple nodes together in a virtually unlimited number of ways. A graph has a node for the data and an edge for the relationship between the data points.
Data structures are essential, but they don’t do anything by themselves. To do something useful, you’ll need to code up an algorithm, which is just code that solves a problem. In our code, we have several mechanisms for implementing algorithms, the most fundamental of which is a function, which is a block of code that takes an input, does something, and returns an output. Like a variable, a function has a name, and it can be called from other parts of your code with different input parameters called arguments.
One thing you might do in the function body is compare one value to another. Every language has a variety of built-in operators like equality, greater than, and less than, that you can use to compare two values. If A is greater than B, it forms a value of true, but if B is greater than A, then the value is false. True and false are what’s known as a Boolean data type, and whenever your code produces a value like this, it’s known as an expression. But not all code will produce a value; sometimes your code will simply do something, which is known as a statement. A good example is the if statement, which handles conditional logic. For example, if the condition is true, it will execute this code; otherwise, it will short circuit and run the code inside of the else block.
Another very common type of **statement* is a loop. A while loop will run this block of code over and over again until the condition in the parentheses becomes false. That can be useful, but more often than not, you’ll want to loop over an iterable data type like an array. Most languages have a for loop that can run some code for every object in the array or iterable data structure. In some cases, a function may not have an output, which is generally called a void function. An interesting thing about functions is that they can call themselves. When a function calls itself, it’s called recursion because, when done like this, by default it will recurse forever, creating an infinite loop. That happens because when you call a function, the programming language will put it into memory on what’s known as the call stack, which is a short-term chunk of memory for executing your code. When a function keeps calling itself, the language will keep pushing frames onto the call stack until you get a stack overflow error. To avoid this, your algorithm needs a base condition so it knows when to terminate the loop.
When you write an algorithm, you’ll need to determine if it’s any good, and the system for doing that is called Big-O Notation. It’s a standard format for approximating the performance of an algorithm at scale. It may reference time complexity, which is how fast your algorithm will run, and space complexity, which deals with how much memory is required to run it. Developers have many different algorithm types at their disposal. The most crude option is brute force, where you might loop over every possible combination to hack somebody’s credit card PIN. A more sophisticated approach might be divide and conquer, like binary search, where you cut the problem in half multiple times until you find what you’re looking for. Another option is dynamic programming algorithms, where a problem is broken down into multiple smaller sub-problems, and the result of each computation is stored for later use using a technique called memoization. That means if a function has already been called, it will use the existing value instead of recomputing it again from scratch. Then we have greedy algorithms that will make the choice that is most beneficial in the short term without considering the problem as a whole. One example of this is Dijkstra’s shortest path algorithm. On the flip side, we have backtracking algorithms, which take a more incremental approach by looking at all the possible options, like a rat in a maze exploring all the different potential paths.
When it comes to implementing your code, there are always multiple ways to get the job done. One programming paradigm is declarative, where your code describes what the program does and the outcome but doesn’t care about things like control flow. This style of programming is often associated with functional languages like Haskell. The other paradigm is imperative programming, where your code uses statements like if and while, providing explicit instructions about how to produce an outcome. It’s associated with procedural languages like C. Today, most general-purpose languages like Python, JavaScript, Kotlin, Swift, and so on are multi-paradigm, which means they support all these options at the same time, in addition to object-oriented programming. The idea behind OOP is that you use classes to write a blueprint for the data or objects in your code. A class can encapsulate variables, which are commonly called properties, as well as functions, which are usually called methods in this context. It’s a common way to organize and reuse code because classes can share behaviors between each other through inheritance, where a subclass can extend and override the behaviors of the parent class, and it opens the door to all kinds of other ideas called design patterns.
Now, a class by itself doesn’t actually do anything; instead, it’s used to instantiate objects, which are actual chunks of data that live in your computer’s memory. Often, you’ll want to reference the same object over and over again in your code. When data is long-lived, it can’t go in the call stack. Instead, most languages have a separate area of memory called the heap, which, unlike the call stack, can grow and shrink based on how your application is used. It also allows you to pass objects by reference, which means you can use the same object in multiple variables without increasing the memory footprint because it always points to the same chunk of memory in the heap.
What’s interesting is that if we go back to the CPU that we talked about in the beginning, you’ll notice that it contains multiple threads. A thread takes the physical CPU core and breaks it into virtual cores that allow it to run code simultaneously. There are some programming languages that support parallelism, where you can write code that literally executes on two different threads at the same time. However, many languages out there are only single-threaded, but that doesn’t mean they can’t do two things at the same time. Instead, they implement concurrency models like an event loop or coroutines that can pause or delay the normal execution of code to handle multiple jobs on a single thread at the same time.
In modern computing, we’re rarely working with the bare metal CPU and RAM. Instead, we work in the cloud with a virtual machine, which is just a piece of software that simulates hardware. This allows us to take really big computers and split them up into a bunch of smaller virtual computers. These machines are the backbone of the internet and are connected via the Internet Protocol. Each machine has a unique IP address to identify it on the network. That IP address is usually aliased to a URL that is registered in a global database called the Domain Name Service (DNS).
To establish a connection, the two computers will perform a TCP handshake, which will allow them to exchange messages called packets. On top of that, there’s usually a security layer like SSL to encrypt and decrypt the messages over the network. Now the two computers can securely share data with the Hypertext Transfer Protocol (HTTP). The client may request a web page, and the server will respond with some HTML. Modern servers provide a standardized way for a client to request data, called an Application Programming Interface (API). The most common architecture is REST, where URLs are mapped to different data entities available on the server.
And that brings us to our final topic: printers. You’re going to need to learn how these things work inside and out because every time you go to grandma’s house, she’s going to ask you to fix it, which shouldn’t be a problem for a computer scientist like you.
这是一台计算机;它只是一段保存着一和零的磁带,以及一个可以读写它的设备。它被称为图灵机,理论上,它可以计算任何东西,比如这个视频中的图形或推荐你观看它的算法。
在现代计算机的核心,我们有中央处理器(CPU)。如果我们将其打开,会发现一块硅片,里面包含数十亿个微小的晶体管,就像微型的开关。这些开关上的值被称为比特,是计算机可以使用的最小信息单位。然而,单个比特本身并不太有用,因此它们会以八个一组的形式出现,称为字节。一个字节可以表示256种不同的值,就像你在键盘上输入的所有字符。实际上,当你在键盘上输入时,产生的字符实际上被映射到ASCII或UTF-8等字符编码中的二进制值。
二进制只是一个用于计数的系统,就像你通常用手指计数时使用的十进制系统一样,但它只有两个字符:一和零。人类很难阅读二进制,所以大多数情况下它以十六进制的格式表示,十个数字和六个字母可以表示一个四位的组,称为半字节。作为开发者,当你用一种编程语言编写代码时,它最终会被转换成机器代码,这是一种可以被CPU解码和执行的二进制格式。然而,它不会为你的应用程序存储数据。为此,计算机有随机存取存储器(RAM)。它就像一个社区,每个房子里住着一个字节。每个位置都有一个内存地址,CPU可以读取和写入。
你可以将CPU和RAM看作是计算机的大脑,但为了使计算机有用,它需要处理输入和输出。输入设备可能是键盘和鼠标,而输出设备可能是你的显示器。幸运的是,大多数开发者不需要担心这些硬件如何配合,因为我们有操作系统内核,如Linux、Mac和Windows,通过设备驱动程序控制所有硬件资源。现在,要开始在操作系统上进行操作,你的第一个入口是shell,它是一个将操作系统暴露给最终用户的程序。它被称为shell是因为它包裹着内核。它将一行文本作为输入,并产生输出。这被称为命令行接口。它不仅可以连接到你自己的计算机,还可以通过**安全外壳协议(SSH)**连接到网络上的远程计算机。
现在你有了访问主机的权限,是时候选择一种编程语言了,这是一种利用抽象原理使计算机对人类实用的工具,通过一层层简化不同的系统。一些语言,如Python,是解释型的。这意味着有一个叫做解释器的程序将逐行执行代码。其他语言,如C++,是编译型的。它们使用编译器将整个程序预先转换为机器代码,然后CPU尝试执行它。这会生成一个可以由操作系统运行的可执行文件,无需任何额外的依赖。
现在,每种编程语言都有各种内置的数据类型来表示我们在代码中处理的数据。我们用更易于人类理解的东西如字符和数字,而不是字节。在你的应用程序中使用数据的最基本方法是声明一个变量。这将一个名称附加到一个数据点上,使你可以在代码的其他地方重用它。Python是一种动态类型语言,这意味着我们不需要告诉程序分配给变量的确切数据类型,它会自动识别。然而,其他语言如C是静态类型的,这意味着你需要在代码中指定变量的数据类型。
当你定义一个变量时,它的值会存储在硬件的某个内存位置上,并且你可能需要在整个程序中分配和释放内存。指针是一个变量,其值是另一个变量的内存地址,可用于低级别的内存控制。许多语言不想处理低级别的内存管理,而是实现了垃圾收集器,它在程序中不再引用某个对象时自动分配和释放内存。
现在,每种编程语言中的数据类型有所不同,但通常你会发现int用于表示整数,可能是有符号或无符号的,用于表示负数。当数字需要小数点时,通常使用浮点型。它被称为浮点型是因为内存只能表示一定范围和精度的数字,基本上是一种科学计数法,使计算机更快。如果你需要更多的范围或精度,许多语言还有double,它会加倍使用数字的内存。当涉及到字符时,你通常会找到char数据类型来表示单个字符,或者更常见的是字符串,表示多个字符。
最终,这些字符会存储在某个内存地址中,但它们需要按一定顺序存储。当顺序以最高有效字节和最小的内存地址开始时,称为大端;反之,如果最低有效字节存储在最小的地址,则称为小端。
在实际的软件工程中,我们最基本的任务之一是将数据组织到数据结构中。最有用的数据结构可能是数组或列表。就像购物清单一样,它按顺序组织多个数据点。然而,它还维护一个从零开始并为每个新项目递增的整数索引。这可能很有用,但你实际上不需要索引来创建项目列表。另一种选择是链表,每个项目都有一个指向前一个项目的指针。另一种选择是遵循后进先出原则的栈。就像堆叠一组盘子,当你想访问数据时,你会将最后一个从顶部弹出。相反的选择是队列,它是先进先出。就像你排队一样,第一个到达的人是第一个被服务的人。
另一种非常有用的数据结构是哈希,它也可能被称为映射或字典。它类似于数组,但不是整数索引,你定义指向每个单独项目的键,给你一个键值对的集合。在许多情况下,以线性方式组织数据并不高效。为了解决这个问题,我们有树,它们将节点组织在一起,形成一个层次结构,通常可以更快地遍历。这有时可能是一种太过刚性的数据结构,因此可以创建一个图,以几乎无限的方式连接多个节点。图有一个表示数据的节点和表示数据点之间关系的边。
数据结构是必要的,但它们本身并不做任何事情。要做一些有用的事情,你需要编写一个算法,它只是解决问题的代码。在我们的代码中,有几种实现算法的机制,其中最基本的是函数,它是一个接受输入、执行某些操作并返回输出的代码块。像变量一样,函数有一个名称,可以从代码的其他部分调用,并带有不同的输入参数,称为参数。
你可能会在函数体中比较一个值和另一个值。每种语言都有各种内置的运算符,如等于、大于和小于,可以用来比较两个值。如果A大于B,则其值为真,但如果B大于A,则其值为假。真和假就是所谓的布尔数据类型,当你的代码生成这样的值时,它被称为表达式。但并非所有代码都会生成一个值;有时你的代码只是做某事,这被称为语句。一个很好的例子是if语句,它处理条件逻辑。例如,如果条件为真,它将执行这段代码;否则,它将短路并运行**else块*中的代码。
另一种非常常见的语句是循环。一个while循环将一遍又一遍地运行这段代码,直到括号中的条件变为假。这可能很有用,但更多的时候,你会希望遍历一个可迭代数据类型,如数组。大多数语言都有一个for循环,可以对数组或可迭代数据结构中的每个对象运行一些代码。在某些情况下,函数可能没有输出,这通常称为空函数。关于函数的一个有趣之处在于它们可以调用自己。当一个函数调用自己时,这被称为递归,因为这样做时,默认情况下它会无限递归,创建一个无限循环。这是因为当你调用一个函数时,编程语言会将其放入内存中的调用栈,这是用于执行代码的短期内存块。当一个函数不断调用自己时,语言会不断将帧推送到调用栈,直到你得到栈溢出错误。为避免这种情况,你的算法需要一个基本条件,以便它知道何时终止循环。
当你编写一个算法时,你需要确定它是否足够好,评估系统称为大O表示法。它是一种标准格式,用于近似算法在规模上的性能。它可能引用时间复杂度,即你的算法运行的速度,和空间复杂度,即运行它所需的内存量。开发者可以使用许多不同的算法类型。最粗糙的选项是暴力破解,你可能会遍历所有可能的组合来破解某人的信用卡PIN。一个更复杂的方法可能是分治法,如二分查找,你会多次将问题对半分,直到找到你要找的东西。另一种选择是动态规划算法,将一个问题分解为多个更小的子问题,每个计算结果存储以供以后使用,使用称为记忆化的技术。这意味着如果一个函数已经被调用,它将使用现有值,而不是从头重新计算。然后我们有贪心算法,它会在短期内做出最有利的选择,而不考虑整体问题。这方面的一个例子是Dijkstra的最短路径算法。另一方面,我们有回溯算法,它采用更渐进的方法,通过查看所有可能的选项,就像迷宫中的老鼠,探索所有不同的潜在路径。
在实现代码时,总有多种方法可以完成任务。一种编程范式是声明式,你的代码描述程序做什么和结果,但不关心控制流。这种编程风格通常与函数式语言如Haskell相关。另一种范式是命令式编程,你的代码使用语句如if和while,提供关于如何产生结果的明确指示。它与过程式语言如C相关。今天,大多数通用语言如Python、JavaScript、Kotlin、Swift等都是多范式,这意味着它们同时支持所有这些选项,此外还有面向对象编程。OOP的想法是你使用类为代码中的数据或对象编写蓝图。一个类可以封装变量,通常称为属性,以及函数,在这种上下文中通常称为方法。这是组织和重用代码的一种常见方式,因为类可以通过继承共享彼此之间的行为,子类可以扩展和覆盖父类的行为,这为所有其他称为设计模式的想法打开了大门。
现在,单独的类实际上并不做任何事情,而是用于实例化对象,这些对象是实际存在于计算机内存中的数据块。通常,你会希望在代码中反复引用相同的对象。当数据是长期存在的,它不能放在调用栈中。相反,大多数语言有一个单独的内存区域,称为堆,与调用栈不同,它可以根据应用程序的使用情况增长和收缩。它还允许你通过引用传递对象,这意味着你可以在多个变量中使用相同的对象,而不增加内存占用,因为它总是指向堆中的同一块内存。
有趣的是,如果我们回到开始时谈到的CPU,你会注意到它包含多个线程。一个线程将物理CPU核分解为虚拟核心,允许它同时运行代码。有些编程语言支持并行性,你可以编写代码,实际在两个不同的线程上同时执行。然而,许多语言只支持单线程,但这并不意味着它们不能同时做两件事。相反,它们实现并发模型,如事件循环或协程,可以暂停或延迟正常代码执行,以在同一线程上同时处理多个任务。
在现代计算中,我们很少直接与裸机的CPU和RAM打交道。相反,我们在云中使用虚拟机,它只是一个模拟硬件的软件。这允许我们将非常大的计算机分割成许多更小的虚拟计算机。这些机器是互联网的支柱,通过互联网协议连接。每台机器都有一个唯一的IP地址来在网络上识别它。这个IP地址通常被别名为一个注册在全局数据库中的URL,称为域名系统(DNS)。
要建立连接,两台计算机将执行TCP握手,这将允许它们交换称为数据包的消息。在此之上,通常有一个安全层,如SSL,用于加密和解密通过网络传输的消息。现在,两台计算机可以使用超文本传输协议(HTTP)安全地共享数据。客户端可能请求一个网页,然后服务器将响应一些HTML。现代服务器提供了一种标准化的方式供客户端请求数据,称为应用程序编程接口(API)。最常见的架构是REST,其中URL映射到服务器上不同的数据实体。
这将我们带到最后一个话题:打印机。你需要学习这些东西的工作原理,因为每次你去奶奶家,她都会让你修理它,这对你这样的计算机科学家来说不应该是问题。
- 作者:现代数学启蒙
- 链接:https://www.math1234567.com/bcc
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章