企业手机网站建设教程,免费网站制作 优帮云,做厂房的网站,番禺区网络销售费用低在过去的几周里#xff0c;我们讨论过图像是由更小的构建单元——像素——组成的。
本周#xff0c;我们将#xff1a;
深入探讨构成图像的二进制数据研究文件在内存中的存储方式学习如何直接访问和操作计算机内存中的数据掌握C语言中的指针概念
重要提示#xff1a;本周…在过去的几周里我们讨论过图像是由更小的构建单元——像素——组成的。本周我们将深入探讨构成图像的二进制数据研究文件在内存中的存储方式学习如何直接访问和操作计算机内存中的数据掌握C语言中的指针概念重要提示本周的内容可能是整个课程中最具挑战性的部分之一。涉及的概念特别是指针需要时间消化和理解这是完全正常的。不要气馁慢慢来像素Pixels什么是像素像素Pixel是图像的最小单位是排列在上下、左右网格上的方形色点。简单理解像素 Picture Element图片元素每个像素都是一个独立的彩色点成千上万的像素组成了我们看到的图像黑白图像最简单的图像是黑白图像可以用位图bitmap表示0代表黑色1代表白色通过排列0和1就能创建简单的图案和图像十六进制HexadecimalRGB颜色模型彩色图像使用RGBRed, Green, Blue颜色模型每种颜色由三个数值组成R红色、G绿色、B蓝色每个值的范围0 到 255在Adobe Photoshop中RGB设置如下所示示例纯红色RGB(255, 0, 0)纯绿色RGB(0, 255, 0)纯蓝色RGB(0, 0, 255)白色RGB(255, 255, 255)黑色RGB(0, 0, 0)问题为什么用十六进制注意图片底部有个特殊值#FFFFFF疑问为什么255被表示成FF这就引出了十六进制的概念十六进制基础十六进制Hexadecimal是一种以16为基数的计数系统也叫base-16。十六进制的数字十六进制使用16个符号0 1 2 3 4 5 6 7 8 9 A B C D E F十六进制十进制0011......99A10B11C12D13E14F15数字表示对比十进制二进制十六进制000000100011910019101010A151111F16100001025511111111FF为什么255是FF计算过程FF (十六进制) F × 16¹ F × 16⁰ 15 × 16 15 × 1 240 15 255 (十进制)位值理解十六进制的每一位代表16的某次方右边第一位16⁰ 1右边第二位16¹ 16右边第三位16² 256...为什么使用十六进制优势1简洁对比二进制11111111(8位)十六进制FF(2位)用更少的字符表示相同的值优势2与二进制的完美对应关键1个十六进制位 4个二进制位二进制 1111 1111 十六进制 F F这让程序员在二进制和十六进制之间转换非常方便优势3表示内存地址计算机内存地址通常用十六进制表示0x前缀表示十六进制数示例0xFF、0x1A2B、0x7FFFRGB颜色的十六进制表示纯红色#FF0000 RGB(255, 0, 0)纯绿色#00FF00 RGB(0, 255, 0)白色#FFFFFF RGB(255, 255, 255)黑色#000000 RGB(0, 0, 0)每两位十六进制数代表一个0-255的颜色分量内存Memory内存的可视化回顾之前的课程我们把内存想象成一排连续的格子。现在让我们用十六进制来标记这些内存位置问题看到10这个格子它是内存地址10还是存储的值10容易混淆0x 前缀为了避免混淆约定所有十六进制数都加上0x前缀现在0x10明确表示十六进制的10等于十进制的1610默认是十进制的10示例0x0 00xF 150x10 160xFF 2550x100 256地址与指针Addresses Pointers这是C语言中最强大但也最容易让人困惑的概念。让我们放慢速度一步步拆解。两个关键运算符在C语言中有两个与内存直接相关的魔法运算符1. 取地址运算符(Ampersand)作用获取某个变量在内存中的地址口诀在这个变量在什么地方2. 解引用运算符*(Asterisk)作用访问某个地址指向的内容口诀去这个地址看看里面有什么实践获取变量的地址让我们看一段简单的代码// addresses.c #include stdio.h int main(void) { int n 50; printf(%i\n, n); }这段代码会在内存中开辟一个空间4个字节来存储整数50。问题这个变量n具体在内存的哪个位置门牌号我们可以修改代码来打印它的地址#include stdio.h int main(void) { int n 50; // %p 是专门用来打印指针/地址的格式符 (pointer) // n 表示 获取变量 n 的地址 printf(%p\n, n); }运行结果示例0x7ffda0a476fc这是一个十六进制数代表了n在计算机内存中的具体位置。什么是指针指针Pointer其实非常简单它就是一个专门用来存储内存地址的变量。普通变量如int n存储的是数据如 50。指针变量如int *p存储的是地址如 0x7ffda0a476fc。定义指针int n 50; int *p n;这行代码int *p n;发生了什么int *p定义了一个指针变量名字叫p。int *表示这个指针是专门用来存int类型变量的地址的。n获取了变量n的地址。把n的地址赋值给p。结论现在p 指向了 n。可视化理解想象内存是一个巨大的储物柜n是一个柜子里面放着数字50。p是另一个柜子里面放着一张纸条纸条上写着n那个柜子的编号。使用指针访问数据既然p存了n的地址我们就可以通过p找到n。#include stdio.h int main(void) { int n 50; int *p n; // p 指向 n // 打印 p 存储的地址 printf(%p\n, p); // 打印 p 指向的地址里的值 (即 n 的值) // *p 的意思是去 p 记录的地址看看那里存了什么 printf(%i\n, *p); }输出0x7ffda0a476fc 50总结p是地址。*p是该地址处的值。字符串的真相Strings are PointersCS50库的谎言在之前的课程中我们使用string类型来定义字符串string s HI!;但其实C语言原生并没有string这个类型这只是CS50库为了方便初学者而定义的一个别名typedef。字符串到底是什么字符串本质上是字符数组而所谓的string变量实际上是一个指向该字符数组第一个字符的指针。让我们揭开面纱#include stdio.h int main(void) { // 不用 cs50.h 库使用 C 语言原生的写法 char *s HI!; printf(%s\n, s); }详细解析char *s HI!;内存中分配了一块连续空间存储字符H,I,!,\0。s是一个指针类型是char *指向字符的指针。s存储了第一个字符H的地址。验证字符串就是指针我们可以打印出字符串中每个字符的地址来验证#include stdio.h int main(void) { char *s HI!; // 打印 s 本身它存储的是 H 的地址 printf(s: %p\n, s); // 打印每个字符的地址 printf(s[0]: %p\n, s[0]); printf(s[1]: %p\n, s[1]); printf(s[2]: %p\n, s[2]); printf(s[3]: %p\n, s[3]); }输出示例s: 0x402004 s[0]: 0x402004 -- 注意s 和 s[0] 是一样的 s[1]: 0x402005 s[2]: 0x402006 s[3]: 0x402007关键发现s的值就是第一个字符s[0]的地址。字符在内存中是连续存储的地址每次1。指针算术Pointer Arithmetic既然指针存储的是数字地址我们自然可以对它进行数学运算访问字符串的另一种方式通常我们用s[0],s[1]来访问字符。但在底层编译器是这样理解的s[0]等同于*s去 s 指向的地址取值s[1]等同于*(s 1)去 s 的下一个地址取值s[2]等同于*(s 2)去 s 的下下个地址取值#include stdio.h int main(void) { char *s HI!; // 数组下标方式 printf(%c\n, s[0]); printf(%c\n, s[1]); printf(%c\n, s[2]); // 指针算术方式 printf(%c\n, *s); printf(%c\n, *(s 1)); printf(%c\n, *(s 2)); }这两种写法效果完全一样*(s1)的意思是取出 s 里的地址加上 1 个单位这里是 1 个字节然后去那个新地址看看里面有什么。字符串比较的陷阱回顾之前的疑问为什么不能用比较两个字符串char *s get_string(s: ); // 假设输入 HI! char *t get_string(t: ); // 假设输入 HI! if (s t) ... // 结果是 Different原因分析因为s和t是指针s存储的是第一个 HI! 在内存中的地址比如 0x123。t存储的是第二个 HI! 在内存中的地址比如 0x456。s t比较的是地址是否相同。因为它们存储在内存的不同位置地址肯定不同所以结果是false。正确做法使用strcmp(s, t)它会去这两个地址逐个字符比较里面的内容。字符串复制与内存分配malloc如果我们想复制一个字符串简单的赋值是不行的string s get_string(s: ); string t s; // 错误这只是复制了指针地址这样做之后t和s指向同一个内存地址。如果你修改t[0]s[0]也会跟着变这称为浅拷贝Shallow Copy。真正的复制深拷贝要制作一个真正的副本我们需要向计算机申请一块新的内存空间。把s里的字符一个个复制到新空间里。我们使用两个新函数malloc(size)Memory Allocation向系统申请指定大小的内存。free(pointer)释放之前申请的内存用完必须还。#include cs50.h #include ctype.h #include stdio.h #include stdlib.h // malloc 和 free 在这里 #include string.h int main(void) { char *s get_string(s: ); if (s NULL) return 1; // 安全检查 // 1. 申请内存 // strlen(s) 1 是为了给结尾的 \0 留位置 char *t malloc(strlen(s) 1); if (t NULL) return 1; // 申请失败检查 // 2. 复制字符 // strcpy(t, s) 是标准库函数相当于写了一个循环 strcpy(t, s); // 3. 修改副本 if (strlen(t) 0) { t[0] toupper(t[0]); } // 4. 打印结果 printf(s: %s\n, s); // 原始字符串不变 printf(t: %s\n, t); // 副本首字母变大写 // 5. 释放内存 free(t); return 0; }重要原则有借有还再借不难。每一个malloc都必须对应一个free否则会导致内存泄漏Memory Leak。垃圾值Garbage Values什么是垃圾值当你向计算机申请一块内存例如定义一个数组时你得到的内存不一定是空的。int scores[1024]; // 申请了1024个int的空间这块内存之前可能被其他程序使用过里面残留着旧数据比如 848, 1927100, -42 等。这些毫无意义的旧数据被称为垃圾值。演示垃圾值// garbage.c #include stdio.h #include stdlib.h int main(void) { int scores[1024]; // 注意我们没有初始化数组没有赋值 for (int i 0; i 1024; i) { printf(%i\n, scores[i]); } }运行结果示例4 848 0 1927100 ...教训永远不要假设未初始化的变量是0定义变量后最好立即给它赋初值。内存交换Swapping这是一个经典的面试题如何交换两个变量的值错误尝试值传递Pass by Value#include stdio.h void swap(int a, int b); int main(void) { int x 1; int y 2; printf(Before swap: x%i, y%i\n, x, y); swap(x, y); printf(After swap: x%i, y%i\n, x, y); } void swap(int a, int b) { int tmp a; a b; b tmp; }结果x和y的值没有变原因C语言函数调用默认是传值Pass by Value。main函数把x的副本传给了swap。swap里的a和b只是局部变量它们交换了但完全没影响到main里的x和y。正确做法引用传递Pass by Reference如果我们想改变main里的变量必须告诉swap函数这些变量的地址。#include stdio.h // 接收两个整数的地址指针 void swap(int *a, int *b); int main(void) { int x 1; int y 2; printf(Before swap: x%i, y%i\n, x, y); // 传递 x 和 y 的地址 swap(x, y); printf(After swap: x%i, y%i\n, x, y); } void swap(int *a, int *b) { // *a 意味着去地址 a 看看把那里的值取出来 int tmp *a; // 把地址 b 里的值赋给地址 a 指向的位置 *a *b; // 把 tmp 的值赋给地址 b 指向的位置 *b tmp; }结果交换成功原理swap拿到了x和y的钥匙地址。它直接打开了main函数的柜子进行了交换。内存布局堆与栈Heap vs Stack计算机内存被划分为不同的区域其中两个最重要的是1. 栈Stack用途存储函数的局部变量、参数。特点自动管理函数结束自动释放。空间较小。不仅向下增长高地址 - 低地址。问题如果递归太深如无限递归会耗尽栈空间导致栈溢出Stack Overflow。2. 堆Heap用途存储动态分配的内存malloc。特点手动管理需要malloc和free。空间很大。向上增长低地址 - 高地址。问题如果只借不还会耗尽堆空间导致堆溢出Heap Overflow或内存泄漏。scanf 的使用与风险我们用过get_int那是CS50库封装好的。C语言原生的输入函数是scanf。获取整数int x; printf(x: ); // 必须给地址 x因为 scanf 需要修改 x 的值 scanf(%i, x);获取字符串危险char s[4]; // 只分配了4个字节 printf(s: ); // s 本身就是地址不需要 scanf(%s, s);风险如果用户输入 hello5个字符 \0 6字节。数组s只有4个字节。scanf会继续往后写覆盖掉不属于s的内存这可能导致程序崩溃段错误或安全漏洞缓冲区溢出攻击。更安全的做法使用malloc分配足够的空间或者限制读取长度。文件操作File I/OC语言可以读写文件这是持久化存储数据的关键。写入文件Phonebook示例#include cs50.h #include stdio.h #include string.h int main(void) { // 1. 打开文件 // a 表示 append追加模式 // 如果文件不存在会自动创建 FILE *file fopen(phonebook.csv, a); if (file NULL) return 1; // 打开失败检查 char *name get_string(Name: ); char *number get_string(Number: ); // 2. 写入文件 // fprintf 是 file printf向文件打印 fprintf(file, %s,%s\n, name, number); // 3. 关闭文件 fclose(file); }读取文件复制图片示例我们可以写一个程序cp.c来复制文件即便是二进制图片。// cp.c - 复制文件 #include stdio.h #include stdint.h // 定义一个字节类型 typedef uint8_t BYTE; int main(int argc, char *argv[]) { // 检查参数 if (argc ! 3) { printf(Usage: ./cp SOURCE DESTINATION\n); return 1; } // 打开源文件二进制读模式 rb FILE *src fopen(argv[1], rb); if (src NULL) return 1; // 打开目标文件二进制写模式 wb FILE *dst fopen(argv[2], wb); if (dst NULL) return 1; BYTE buffer; // 缓冲区每次读1个字节 // 循环读取直到文件结束 // fread 返回成功读取的块数为0表示读完了 while (fread(buffer, sizeof(BYTE), 1, src) ! 0) { // 写入目标文件 fwrite(buffer, sizeof(BYTE), 1, dst); } // 关闭所有文件 fclose(dst); fclose(src); }说明fread(buffer, size, qty, file)从文件读数据到内存。fwrite(buffer, size, qty, file)从内存写数据到文件。这种方式可以复制任何类型的文件文本、图片、视频因为它操作的是最底层的字节。总结本周我们揭开了内存的神秘面纱。我们学习了十六进制更简洁地表示二进制数据。地址与指针取地址*解引用。指针就是存储地址的变量。字符串的本质char *即指向首字符的指针。指针算术*(s1)等同于s[1]。动态内存malloc申请free释放。避免内存泄漏和段错误。内存交换必须使用指针引用传递。文件操作fopen,fclose,fread,fwrite。下周我们将利用这些知识学习数据结构Data Structures看看如何在内存中构建更复杂、更高效的数据组织形式参考资料CS50 Week 4 官方笔记Pointer Fun with Binky (视频) - 强烈推荐形象解释指针。