1 00:00:00,487 --> 00:00:11,210 [音乐播放] 2 00:00:11,210 --> 00:00:12,100 好 3 00:00:12,100 --> 00:00:15,620 首先 一段视频 来自一个大家都很熟悉的人 4 00:00:22,080 --> 00:00:22,560 [视频播放] 5 00:00:22,560 --> 00:00:23,370 好 6 00:00:23,370 --> 00:00:27,150 这里是CS50 现在是第三周的开始 7 00:00:27,150 --> 00:00:29,980 很抱歉今天我不能和大家在一起 8 00:00:29,980 --> 00:00:32,880 但请让我给大家介绍 CS50的Rob Boden 9 00:00:32,880 --> 00:00:33,872 [视频播放结束] 10 00:00:33,872 --> 00:00:39,340 [鼓掌与欢呼] 11 00:00:39,340 --> 00:00:41,277 这视频拍得太棒了 12 00:00:47,280 --> 00:00:47,770 好 13 00:00:47,770 --> 00:00:50,960 首先 我们又有一次午餐活动了 14 00:00:50,960 --> 00:00:52,330 定在明天1:15分 15 00:00:52,330 --> 00:00:54,480 本周五将不会有午餐活动 16 00:00:54,480 --> 00:00:55,810 这一次是和Quora的午餐活动 17 00:00:55,810 --> 00:01:00,190 Tommy现在还没来 但是届时参加活动的有这门课的 18 00:01:00,190 --> 00:01:01,530 前核心助教Tommy McWilliam 19 00:01:01,530 --> 00:01:02,730 他是个很有趣的人 20 00:01:02,730 --> 00:01:04,819 你们应该来参加 21 00:01:04,819 --> 00:01:05,900 好 22 00:01:05,900 --> 00:01:11,360 上一周 我们开始分解析字串究竟是什么 23 00:01:11,360 --> 00:01:14,830 我们从一开始就知道 它是字符组成的序列 24 00:01:14,830 --> 00:01:18,130 但上一周 我们发现看起来是字符组成的序列的东西 25 00:01:18,130 --> 00:01:22,110 实际上 是字符数组 26 00:01:22,110 --> 00:01:26,450 所以我们知道 一个字串 其实是字符数组 27 00:01:26,450 --> 00:01:30,920 在它的最后 我们有null字节 也就是\0 28 00:01:30,920 --> 00:01:32,230 来表示字串的结束 29 00:01:32,230 --> 00:01:36,970 所以字串是字符数组 但我们可以不仅有字符数组 30 00:01:36,970 --> 00:01:39,530 我们还可以有 31 00:01:39,530 --> 00:01:40,890 任何东西组成的数组 32 00:01:40,890 --> 00:01:51,570 如果你还记得上周 33 00:01:51,570 --> 00:01:53,560 David演示ages程序 34 00:01:53,560 --> 00:01:57,010 所以首先 我们要做的 是问用户要一个整数 35 00:01:57,010 --> 00:01:58,800 有多少个人在这个房间里 36 00:01:58,800 --> 00:02:01,260 我们有了这个整数之后 我们就声明数组 37 00:02:01,260 --> 00:02:02,890 注意用中括号的这个句法 38 00:02:02,890 --> 00:02:04,540 你们会慢慢习惯它的 39 00:02:04,540 --> 00:02:09,430 所以我们声明了整数数组 叫ages 40 00:02:09,430 --> 00:02:12,080 在这个数组里有n个整数 41 00:02:12,080 --> 00:02:16,480 所以这里的这个模式就 for (int i = 0; i < n; i++) 42 00:02:16,480 --> 00:02:20,580 你们也会习惯这个模式的 43 00:02:20,580 --> 00:02:24,000 因为这就是我们要循环访问数组时要采取的模式 44 00:02:24,000 --> 00:02:26,330 记住 n就是数组的长度 45 00:02:26,330 --> 00:02:32,120 所以这里 我们在重复问i这个人的年龄 46 00:02:32,120 --> 00:02:36,640 然后 我们往下 然后我们决定 47 00:02:36,640 --> 00:02:40,220 要印出他们一年以后的年龄 48 00:02:40,220 --> 00:02:49,980 我们来运行这个程序 make ages, ./ages 49 00:02:49,980 --> 00:02:53,010 屋子里有多少个人 就说三个好了 50 00:02:53,010 --> 00:02:59,880 假设第一个人13岁 第二个26岁 最后一个30岁 51 00:02:59,880 --> 00:03:05,080 然后程序会循环访问这三个人 输出 14, 27 和31 52 00:03:05,080 --> 00:03:16,060 记住 当我们声明大小为n的数组时 53 00:03:16,060 --> 00:03:19,950 数组中的索引值 是从 0,1,2 54 00:03:19,950 --> 00:03:21,680 一直到 n-1 55 00:03:21,680 --> 00:03:26,255 所以当我们说 房间里有3个人 然后第一次循环访问 56 00:03:26,255 --> 00:03:29,850 这个循环时 i等于0 57 00:03:29,850 --> 00:03:31,650 所以在索引0中 58 00:03:31,650 --> 00:03:34,540 我们赋了用户输入的第一个年龄 59 00:03:34,540 --> 00:03:38,870 在下一个索引值中 我们放了用户输入的第二个年龄 60 00:03:38,870 --> 00:03:40,580 在索引值2中 放入最后一个值 61 00:03:40,580 --> 00:03:44,200 所以我们发现 大小为3的数组 62 00:03:44,200 --> 00:03:46,040 并没有索引值3 63 00:03:46,040 --> 00:03:49,036 这是不成立的 64 00:03:49,036 --> 00:03:50,250 好 65 00:03:50,250 --> 00:03:55,136 回到这里 66 00:03:57,650 --> 00:04:01,590 现在我们看了数组 对它有了一定了解 67 00:04:01,590 --> 00:04:03,780 我们下面要看命令行参数 68 00:04:03,780 --> 00:04:05,890 这和这一次的练习比较有关联 69 00:04:05,890 --> 00:04:09,670 所以到目前为止 不管你什么时候声明你的main函数 70 00:04:09,670 --> 00:04:11,230 我们都用的是int main void 71 00:04:11,230 --> 00:04:14,070 void表示我们不会 72 00:04:14,070 --> 00:04:16,440 给这个函数任何参数 73 00:04:16,440 --> 00:04:19,190 现在我们会看到 main也可以接受参数 74 00:04:19,190 --> 00:04:22,470 这里我们叫它 int argc和string argv[] 75 00:04:22,470 --> 00:04:26,930 这里的中括号 表示我们在和数组打交道 76 00:04:26,930 --> 00:04:31,850 所以在这里 string argv[] 我们是在和数组打交道 77 00:04:31,850 --> 00:04:35,360 argc 这会表示 78 00:04:35,360 --> 00:04:37,580 我们给了程序多少个参数 79 00:04:37,580 --> 00:04:46,050 为了解释这是什么意思 让我们把这个关掉 80 00:04:46,050 --> 00:04:46,490 好 81 00:04:46,490 --> 00:04:50,790 到目前为止 我们都用相同的方法运行程序 就像刚才的./ages 82 00:04:50,790 --> 00:04:55,250 我们也可以 在命令行 递参数 83 00:04:55,250 --> 00:04:56,550 也就是这里的这个概念 命令行参数 84 00:04:56,550 --> 00:04:59,760 第一个参数 hello world 85 00:04:59,760 --> 00:05:03,350 这里 argc就会是3 86 00:05:03,350 --> 00:05:07,720 它统计了命令行参数的个数 87 00:05:07,720 --> 00:05:12,840 argc至少是1 因为./ages它本身 88 00:05:12,840 --> 00:05:14,490 就是一个命令行参数 89 00:05:14,490 --> 00:05:17,010 hello就是第一个命令行参数 90 00:05:17,010 --> 00:05:20,460 如果./ages是第0个 那么hello就是第一个 world是 91 00:05:20,460 --> 00:05:22,830 第二个命令行参数 92 00:05:22,830 --> 00:05:29,490 所以string argv 我们会看到 包含了字串 93 00:05:29,490 --> 00:05:33,830 ./ages, hello 还有world 94 00:05:33,830 --> 00:05:38,945 根据David的要求 我们要放一段视频介绍一下这个概念 95 00:05:42,486 --> 00:05:43,890 [视频播放] 96 00:05:43,890 --> 00:05:46,240 目前为止 我们写过的程序中 97 00:05:46,240 --> 00:05:48,500 我们都用int main void声明了main 98 00:05:48,500 --> 00:05:51,170 一直以来 void表示的 99 00:05:51,170 --> 00:05:54,430 是程序不接受任何命令行参数 100 00:05:54,430 --> 00:05:57,750 换句话说 当用户运行一个程序时 他或她可以 101 00:05:57,750 --> 00:06:01,710 在提示处的程序名称后面 另外写一些词或词组 102 00:06:01,710 --> 00:06:03,000 来提供命令行参数 103 00:06:03,000 --> 00:06:06,550 如果你想要自己的程序接受命令行参数的话 104 00:06:06,550 --> 00:06:10,540 不管是一个还是多个这样的参数 我们需要用一些参数来取代void 105 00:06:10,540 --> 00:06:12,200 所以让我们行动起来 106 00:06:12,200 --> 00:06:15,750 Include CS50.h 107 00:06:15,750 --> 00:06:19,360 Include standard io.h 108 00:06:19,360 --> 00:06:20,760 Int main 109 00:06:20,760 --> 00:06:26,330 现在 我要做的不是输入void 还是明确一个整数叫argc 110 00:06:26,330 --> 00:06:28,780 以及一个字串的数组 叫argv 111 00:06:28,780 --> 00:06:31,820 argc与argv只是我们习惯这样写 112 00:06:31,820 --> 00:06:34,000 我们也可以给这些参数取随便什么名字 113 00:06:34,000 --> 00:06:37,630 不过重点是 argc是一个整数 因为根据定义 114 00:06:37,630 --> 00:06:41,360 它是参数的个数 也就是用户看到提示后 115 00:06:41,360 --> 00:06:43,380 一共输入了多少个词 116 00:06:43,380 --> 00:06:47,910 argv 也就是argument vector(变元向量)是一个数组 117 00:06:47,910 --> 00:06:52,020 里面储存着所有用户在提示后输入的词 118 00:06:52,020 --> 00:06:54,500 让我们接下来 119 00:06:54,500 --> 00:06:55,660 用这些命令行参数做点什么 120 00:06:55,660 --> 00:07:00,070 准确来说 让我们印出用户在提示处这个程序的名字后面 121 00:07:00,070 --> 00:07:03,960 输入的那个词 122 00:07:03,960 --> 00:07:04,730 { 123 00:07:04,730 --> 00:07:06,240 } 124 00:07:06,240 --> 00:07:10,510 printf %s\n, 125 00:07:10,510 --> 00:07:14,550 现在我要告诉printf要给占位符赋什么值 126 00:07:14,550 --> 00:07:18,600 我想要用户在程序名字后面输入的第一个词 127 00:07:18,600 --> 00:07:23,130 所以我会说 128 00:07:23,130 --> 00:07:24,830 argv[1]); 129 00:07:24,830 --> 00:07:27,290 为什么是[1]而不是[0]呢? 130 00:07:27,290 --> 00:07:30,990 因为我们发现 argv0里会自动保存 131 00:07:30,990 --> 00:07:32,620 程序的名字 132 00:07:32,620 --> 00:07:36,180 所以用户在程序名称后输入的词 133 00:07:36,180 --> 00:07:38,990 按惯例 会被储存 在argv1里 134 00:07:38,990 --> 00:07:42,380 现在让我们编译并运行这个程序 135 00:07:42,380 --> 00:07:47,780 Make argv 0, ./argv 0 136 00:07:47,780 --> 00:07:50,520 现在输入一个词比如hello 137 00:07:50,520 --> 00:07:51,670 回车 138 00:07:51,670 --> 00:07:53,520 我们就得到了 hello 139 00:07:53,520 --> 00:07:55,750 [视频播放结束] 140 00:07:55,750 --> 00:07:57,000 好 141 00:07:59,380 --> 00:08:01,230 关掉这个 142 00:08:01,230 --> 00:08:16,730 所以让我们看一下刚才介绍的程序 143 00:08:16,730 --> 00:08:24,710 只是要演示下 如果我们印出argv 0, make 然后要什么来着 argv 0, ./argv 0 144 00:08:24,710 --> 00:08:30,440 如我们所料 它印出了程序的名字 因为 145 00:08:30,440 --> 00:08:32,970 argv 0永远都是程序的名字 146 00:08:32,970 --> 00:08:35,640 但是让我们来做些更有意思的事 147 00:08:35,640 --> 00:08:42,080 在练习中 你们会认识这个函数 atoi 148 00:08:42,080 --> 00:08:44,440 我们用atoi来做什么呢? 149 00:08:44,440 --> 00:08:48,550 它会把一个字串变成一个整数 150 00:08:48,550 --> 00:08:53,280 如果我把这个字串 比如1 2 3 递给atoi 它会 151 00:08:53,280 --> 00:08:56,910 把字串转换成整数 1 2 3 152 00:08:56,910 --> 00:09:01,480 所以我们要把第一个命令行参数转换成一个整数 153 00:09:01,480 --> 00:09:05,690 然后把这个整数印出去 154 00:09:05,690 --> 00:09:09,680 所以也就是说 我们有点像在重新实现getint 155 00:09:09,680 --> 00:09:12,350 只不过我们在命令行输入了整数 156 00:09:12,350 --> 00:09:14,560 而不是在程序中输入 157 00:09:14,560 --> 00:09:23,170 所以 编译argv 0 让我们在这里做 然后关掉这个 158 00:09:23,170 --> 00:09:27,670 所以运行argv 0 然后让我们输入整数 1 2 3 4 1 2 159 00:09:27,670 --> 00:09:30,840 它会输出整数 1 2 3 4 1 2 160 00:09:30,840 --> 00:09:35,500 atoi有些微妙之处 比如它除了有效数字字符以外 161 00:09:35,500 --> 00:09:39,040 什么都不会管 但这没关系 162 00:09:39,040 --> 00:09:42,870 所以你们觉得我做这个之后会发生什么? 163 00:09:45,520 --> 00:09:47,050 分段错误 164 00:09:47,050 --> 00:09:50,410 为什么呢? 165 00:09:50,410 --> 00:09:56,060 如果你回过头来看我们的程序 我们在把argv 1 166 00:09:56,060 --> 00:09:59,610 在程序名字后的第一个参数 转换成整数 167 00:09:59,610 --> 00:10:03,350 但是程序名字后 并没有任何参数被传递给过 168 00:10:03,350 --> 00:10:08,060 所以这里 我们可以看到这是个有bug的程序 因为 169 00:10:08,060 --> 00:10:10,530 如果你不带参数地想要运行它 它会崩溃 170 00:10:10,530 --> 00:10:16,950 所以你们还会看到的一个为常见模式就是 171 00:10:16,950 --> 00:10:21,100 如果argc小于2 表示 这个程序至少有它的名字 172 00:10:21,100 --> 00:10:29,100 和第一个参数这话是错的 那我们可以用printf 173 00:10:29,100 --> 00:10:31,190 命令行参数不足 174 00:10:31,190 --> 00:10:33,170 印出这个不是个好主意 一般我们都是印出 175 00:10:33,170 --> 00:10:35,440 你应该在命令行输入一个整数 什么的 176 00:10:35,440 --> 00:10:37,450 放一个n在这里 177 00:10:37,450 --> 00:10:39,600 然后回传1 178 00:10:39,600 --> 00:10:44,740 记住 在我们程序最后 如果我们回传了0 179 00:10:44,740 --> 00:10:47,060 就代表我们成功了 180 00:10:47,060 --> 00:10:50,940 而且main会自动回传0 如果你没有这么做的话 181 00:10:50,940 --> 00:10:55,800 所以这里 我们回传了1 表示没有成功 182 00:10:55,800 --> 00:11:01,000 你可以随便回传任何东西 不过 0代表成功 183 00:11:01,000 --> 00:11:03,390 其他任何东西都表示失败 184 00:11:03,390 --> 00:11:04,855 所以让我们运行一下这个版本 185 00:11:12,880 --> 00:11:16,600 现在 如果我们没有输入命令行参数 186 00:11:16,600 --> 00:11:18,290 它就会告诉我们 命令行参数不足 187 00:11:18,290 --> 00:11:20,610 你这句不完整 188 00:11:20,610 --> 00:11:24,950 否则 如果我们递给它一个参数 它就能让程序完整 189 00:11:24,950 --> 00:11:27,920 你就可以这样用argc来确定有多少 190 00:11:27,920 --> 00:11:30,630 命令行参数被递了过去 191 00:11:30,630 --> 00:11:39,360 让我们把程序再变复杂些 192 00:11:39,360 --> 00:11:42,180 来看一下第二次循环 193 00:11:42,180 --> 00:11:46,310 现在 我们不只是要输出第一个命令行参数了 194 00:11:46,310 --> 00:11:51,210 这里 我们经过了 int i = 0 i < argc, i++ 195 00:11:51,210 --> 00:11:55,280 然后印出argv 索引值为i的 196 00:11:55,280 --> 00:11:59,300 这个模式 和先前相同 197 00:11:59,300 --> 00:12:02,600 只不过我们没有用变量n 而是argc 198 00:12:02,600 --> 00:12:09,520 这个会循环经过数组中的每一个索引 然后 199 00:12:09,520 --> 00:12:11,910 把数组中的每一个元素都印出 200 00:12:11,910 --> 00:12:20,300 所以 当你运行这个程序时 我还没有输入任何命令行参数 201 00:12:20,300 --> 00:12:22,540 所以它只会印出程序名字 202 00:12:22,540 --> 00:12:26,053 如果我输入了一堆东西 它会印出它们 每个单独一行 203 00:12:31,213 --> 00:12:32,210 好 204 00:12:32,210 --> 00:12:34,770 让我们再前进一步 205 00:12:34,770 --> 00:12:38,890 我们不想要在每行印出一个参数 而是在每行印出 206 00:12:38,890 --> 00:12:42,590 每一个参数的每一个字符 207 00:12:42,590 --> 00:12:46,700 记住 argv是字串的数组 208 00:12:46,700 --> 00:12:50,960 凹面镜字串 其实是字符数组 209 00:12:50,960 --> 00:12:57,140 这就是说 argv实际上是一个字符数组的数组 210 00:12:57,140 --> 00:13:04,920 我们可以利用这一点 但现在我们先不要管它 211 00:13:04,920 --> 00:13:08,190 让我们只考虑字串 argv 0 212 00:13:08,190 --> 00:13:14,170 如果我们想要把argv 0的每一个字符 都单独在一行中输出 213 00:13:14,170 --> 00:13:19,500 那我们会想要用我们熟悉的模式 i比数组的长度要短 214 00:13:19,500 --> 00:13:23,990 在这里 也就是strlen 这不是我想做的 215 00:13:23,990 --> 00:13:26,450 string s = argv 0 216 00:13:26,450 --> 00:13:30,390 i比我们的数组的长度要小 在这里 也就是字符数组 217 00:13:30,390 --> 00:13:34,410 i++ 218 00:13:34,410 --> 00:13:41,040 所以 就像我们上周看到的 最好把strlen移到条件外面 219 00:13:41,040 --> 00:13:45,210 因为这样每次经过循环时 n就会增加s的strlen 220 00:13:45,210 --> 00:13:47,720 而s的长度不会变 221 00:13:47,720 --> 00:13:50,230 所以我们就让它等于这边的n 222 00:13:54,260 --> 00:13:55,170 好的 223 00:13:55,170 --> 00:14:01,320 现在 我们在循环访问数组中的每一个索引 224 00:14:01,320 --> 00:14:05,630 这样 如果我们想要印出数组里的每一个字符 225 00:14:05,630 --> 00:14:06,880 我们应该用%c 作为字符的标记 226 00:14:10,750 --> 00:14:19,770 现在[i]是字串中的第i个字符 227 00:14:19,770 --> 00:14:20,970 所以如果字串是hello 228 00:14:20,970 --> 00:14:27,530 那么s[0]就会是h s[1]是e 以此类推 229 00:14:27,530 --> 00:14:30,800 现在我们想把这两者结合起来 230 00:14:30,800 --> 00:14:35,440 我们想输出每个命令行参数中的每个字符 231 00:14:35,440 --> 00:14:38,950 那我们就得有个嵌套的for循环 232 00:14:38,950 --> 00:14:47,480 照惯例 第一个计数器是i 第二个会是j 233 00:14:47,480 --> 00:14:54,450 n = strlen (argv [i]), i < n, i++ 234 00:14:59,150 --> 00:15:06,870 现在 我们不要印出argv i, argv[i]是索引 235 00:15:06,870 --> 00:15:14,280 也就是第i个命令行参数 argv [i][j] 236 00:15:14,280 --> 00:15:16,925 j是第i个参数的第j个字符 237 00:15:20,580 --> 00:15:24,810 在这儿我可以把它去掉了 因为我们已经把它放到了循环里 238 00:15:24,810 --> 00:15:33,900 这等同于 string s = argv i 然后 s[j] 239 00:15:33,900 --> 00:15:36,980 我们不需要声明这个变量s 240 00:15:36,980 --> 00:15:44,530 我们要做的 是把这两个合并成我们有的东西 argv [i][j] 241 00:15:44,530 --> 00:15:45,780 [学生提问] 242 00:15:48,850 --> 00:15:49,680 谢谢指出 243 00:15:49,680 --> 00:15:52,936 这个其实坏了 244 00:15:52,936 --> 00:15:55,510 如果我运行它的话 我们就会发现它坏了 245 00:15:55,510 --> 00:16:01,210 在这个for循环里我在意的计数器 246 00:16:01,210 --> 00:16:05,410 就是j 迭代器 247 00:16:05,410 --> 00:16:08,560 如果我们没有修正这个的话 你可能会碰到问题 248 00:16:08,560 --> 00:16:09,540 比如说一个无限循环 249 00:16:09,540 --> 00:16:12,220 所以我们今天也会讲到怎样debug 250 00:16:12,220 --> 00:16:13,120 好 251 00:16:13,120 --> 00:16:15,240 让我们来运行一下这个程序 252 00:16:15,240 --> 00:16:21,200 让我们在这加上一个printf 它会再印出一行 253 00:16:21,200 --> 00:16:27,480 这意味着当我们运行程序时 在每一个命令行参数的每一个字符间 254 00:16:27,480 --> 00:16:31,830 都会有一个空白行 255 00:16:31,830 --> 00:16:33,448 我们会知道这代表什么的 256 00:16:37,310 --> 00:16:37,790 不好 257 00:16:37,790 --> 00:16:39,870 有些bug 258 00:16:39,870 --> 00:16:42,860 隐式声明函数strlen错误 259 00:16:42,860 --> 00:16:51,630 回到我们的程序 我忘记要包括string.h了 260 00:16:54,240 --> 00:16:57,730 string.h是用来声明 261 00:16:57,730 --> 00:16:58,980 strlen这个函数的头文件 262 00:17:04,650 --> 00:17:06,060 好的 编译成功了 263 00:17:06,060 --> 00:17:09,109 现在我们来运行它 264 00:17:09,109 --> 00:17:10,930 就这样 265 00:17:10,930 --> 00:17:17,790 它会印出我们程序的名字 hello world 266 00:17:17,790 --> 00:17:23,510 它会印出每个东西 也就是一行印出一个字符 267 00:17:23,510 --> 00:17:24,540 好 268 00:17:24,540 --> 00:17:30,625 让我们再进一步 269 00:17:34,050 --> 00:17:39,700 我们不想再用string.h了 我们想怎样才能 270 00:17:39,700 --> 00:17:41,420 实行我们自己的strlen函数 271 00:17:41,420 --> 00:17:45,600 所以我要马上来给一个函数签名 272 00:17:45,600 --> 00:17:52,900 让我叫它my_strlen 然后它会接受一个字串当作参数 273 00:17:52,900 --> 00:17:57,220 我们希望它会回传这个字串的长度 274 00:17:57,220 --> 00:18:03,430 去哪了? 275 00:18:03,430 --> 00:18:04,990 好 276 00:18:04,990 --> 00:18:06,740 好 277 00:18:06,740 --> 00:18:12,900 还记得前一张幻灯里 也是我们上周讲过的 278 00:18:12,900 --> 00:18:18,890 字符数组 也就是 字串 假如说这是我们的字串s 279 00:18:18,890 --> 00:18:29,870 如果s是字串hello 那么在内存中就是 H-E-L-L-O 280 00:18:29,870 --> 00:18:35,610 加上\0这个字符 281 00:18:35,610 --> 00:18:39,170 那么我们要怎样取得s的长度? 282 00:18:39,170 --> 00:18:43,190 诀窍在于找这个\o 283 00:18:43,190 --> 00:18:44,380 这个用来结尾的空字符 284 00:18:44,380 --> 00:18:50,270 所以这个算法 285 00:18:50,270 --> 00:18:51,510 只会有几个字符 286 00:18:51,510 --> 00:18:56,180 让我们用这只手来表示计数器 让我们叫它int length 287 00:18:56,180 --> 00:19:00,060 从这边开始 我们会循环访问我们的字串 288 00:19:00,060 --> 00:19:04,100 第一个字符 也就是H还没到\0 289 00:19:04,100 --> 00:19:05,170 所以长度是1 290 00:19:05,170 --> 00:19:08,050 循环访问下一个字符 E 也没有\0 291 00:19:08,050 --> 00:19:09,630 长度为2 292 00:19:09,630 --> 00:19:10,960 L 3 293 00:19:10,960 --> 00:19:11,850 L4 294 00:19:11,850 --> 00:19:13,050 O 5 295 00:19:13,050 --> 00:19:16,690 终于 我们找到了\0 这表示 296 00:19:16,690 --> 00:19:17,780 字串结束了 297 00:19:17,780 --> 00:19:20,130 所以让我们回传5 298 00:19:20,130 --> 00:19:33,630 所以在实际过程中 一开始 int length = 0 我的右手 299 00:19:33,630 --> 00:19:36,088 然后我们开始循环访问 300 00:19:36,088 --> 00:19:38,000 [学生提问] 301 00:19:38,000 --> 00:19:38,640 不好 302 00:19:38,640 --> 00:19:39,870 谢谢指出 303 00:19:39,870 --> 00:19:42,680 好了 304 00:19:42,680 --> 00:19:44,140 int length = 0 305 00:19:46,910 --> 00:19:58,310 现在 当s长度不等于\0 306 00:19:58,310 --> 00:20:04,660 记住 \0是一个实际存在的字符 307 00:20:04,660 --> 00:20:05,820 它表示字串的结束 308 00:20:05,820 --> 00:20:09,850 就像 \n也是一个实际存在的字符一样 309 00:20:09,850 --> 00:20:14,040 \0表示字串的结束 310 00:20:14,040 --> 00:20:15,414 我不想把它放在这 311 00:20:19,190 --> 00:20:25,620 当s用长度来索引时 还没有到表示结束的空字符时 312 00:20:25,620 --> 00:20:27,130 我们就加长长度 313 00:20:29,860 --> 00:20:34,880 那么 在这个情况下 在程序最后 长度最终 314 00:20:34,880 --> 00:20:37,610 会变成5 315 00:20:37,610 --> 00:20:39,210 然后我们就会回传长度 316 00:20:42,570 --> 00:20:43,530 好 317 00:20:43,530 --> 00:20:48,290 在底下这里 我也可以这样 my_strlen 318 00:20:48,290 --> 00:20:50,700 让我们先编译一下 保证一下都会顺利运行 319 00:20:55,820 --> 00:20:58,210 我是用2做的么 320 00:20:58,210 --> 00:21:00,565 还是1? 321 00:21:00,565 --> 00:21:01,940 这应该行了 322 00:21:01,940 --> 00:21:02,690 好的 323 00:21:02,690 --> 00:21:08,490 这是argv 2 324 00:21:08,490 --> 00:21:11,585 像我们预计的一样 成功了 这是我刚才改的那个么? 325 00:21:15,060 --> 00:21:15,550 是的 326 00:21:15,550 --> 00:21:16,760 好 327 00:21:16,760 --> 00:21:21,820 这个版本后面没有printf new line 328 00:21:21,820 --> 00:21:22,910 但是它其实是一样的 329 00:21:22,910 --> 00:21:23,300 好 330 00:21:23,300 --> 00:21:25,780 像我们预期的一样成功了 331 00:21:25,780 --> 00:21:34,750 现在我们还可以进一步将这一步合并 332 00:21:34,750 --> 00:21:38,920 注意在这里 首先我们取得了argv的字串长度 然后我们循环访问了 333 00:21:38,920 --> 00:21:41,450 这个字串里的每一个字符 334 00:21:41,450 --> 00:21:47,480 现在我们不要这样做 如果我们沿用这样的逻辑 335 00:21:47,480 --> 00:21:50,740 也就是等到我们达到\0的时候 然后把这个逻辑放到for循环里 336 00:21:53,740 --> 00:22:07,490 所以 循环访问 当argv [i][j]不等于\0 337 00:22:07,490 --> 00:22:10,680 让我们先运行一下 338 00:22:19,838 --> 00:22:21,180 好 339 00:22:21,180 --> 00:22:27,655 这里 这个条件在说 340 00:22:38,090 --> 00:22:40,060 让我们把它擦掉 341 00:22:40,060 --> 00:22:49,140 现在 让这个变成我们的argv 342 00:22:49,140 --> 00:22:55,290 我刚才运行程序的时候 argv是字串的数组 343 00:22:55,290 --> 00:23:03,100 所以 如果我用./argv 2, hello world运行它 344 00:23:03,100 --> 00:23:07,650 那么argv本身长度是3 因为有 argv 0, hello 还有world 345 00:23:11,700 --> 00:23:19,660 在这些索引里面 还有数组 346 00:23:19,660 --> 00:23:23,780 也就是 . / 我不知道这样分是不是正 347 00:23:23,780 --> 00:23:25,680 看来不是 348 00:23:25,680 --> 00:23:30,110 A-R-V - 还需要更多空间 349 00:23:30,110 --> 00:23:32,570 让我们来看一下这个数组 350 00:23:32,570 --> 00:23:38,230 A-R-V-0, 然后是\0 351 00:23:38,230 --> 00:23:43,160 E?然后这个数组会是hello 352 00:23:43,160 --> 00:23:45,910 让我们假设说 H-E\0 353 00:23:45,910 --> 00:23:51,130 最终 W-O\0 354 00:23:51,130 --> 00:23:59,730 我们刚写的这个算法 这个内嵌的for循环 355 00:23:59,730 --> 00:24:07,321 它在做的 就是 我们首先有计数器i然后是j 356 00:24:07,321 --> 00:24:15,206 屏幕上有代码的话这个解释起来会容易点 所以让我们回到这来 357 00:24:15,206 --> 00:24:17,476 好 358 00:24:17,476 --> 00:24:24,600 注意 i是迭代器 它在循环访问 359 00:24:24,600 --> 00:24:25,610 每个命令行参数 360 00:24:25,610 --> 00:24:28,870 j是迭代器循环访问每个命令行参数 361 00:24:28,870 --> 00:24:30,410 里的每一个字符 362 00:24:30,410 --> 00:24:46,755 所以这个最里面的printf在做的 就是 我们有printf argv 0 0 363 00:24:46,755 --> 00:24:58,680 printf argv 0 1, printf argv 0 2, 0 3, 0 4, 0 5, 0 6, 但现在 argv 0 7 364 00:24:58,680 --> 00:25:00,670 会等于\0 365 00:25:00,670 --> 00:25:05,730 然后我们就退出循环 现在i变成1 366 00:25:05,730 --> 00:25:10,910 我们就会printf argv 1 0, argv 1 1 367 00:25:10,910 --> 00:25:17,040 现在 因为我把hello变短了 argv 1 2 又一次 368 00:25:17,040 --> 00:25:18,170 会变成\0 369 00:25:18,170 --> 00:25:25,050 所以 增加i然后继续 就这样 直到我们把world都印出来 370 00:25:25,050 --> 00:25:28,580 然后这些是三个命令行参数 371 00:25:28,580 --> 00:25:31,670 我们要从最外面这个循环里退出 然后结束我们的程序 372 00:25:38,390 --> 00:25:39,640 好 373 00:25:43,903 --> 00:25:46,795 让我们回到这里 374 00:25:49,670 --> 00:25:52,370 在这个练习中 375 00:25:52,370 --> 00:25:54,460 你们会对命令行参数更加熟悉 376 00:25:54,460 --> 00:25:56,630 现在 关于debug 377 00:25:56,630 --> 00:26:01,680 在前一个练习中 你们大概已经需要 378 00:26:01,680 --> 00:26:03,120 去debug了 379 00:26:03,120 --> 00:26:08,420 debug一个非常简单的办法 是 首先 让我们来看个有bug的程序 380 00:26:20,710 --> 00:26:23,830 我们看到这个程序 我们会问用户要一个整数 381 00:26:23,830 --> 00:26:29,350 拿这个整数 然后我们决定放一个while循环 382 00:26:29,350 --> 00:26:32,280 它会减少i直到i等于10 383 00:26:32,280 --> 00:26:35,820 让我们假设 我输入了一个比10大的整数 384 00:26:35,820 --> 00:26:38,700 减少i直到它等于10为止 385 00:26:38,700 --> 00:26:42,630 然后我们还有一个while循环 当i不等于0时 386 00:26:42,630 --> 00:26:44,540 我们就把i减3 387 00:26:44,540 --> 00:26:49,790 所以如果你们发现这个bug在做什么的话 它其实是在 388 00:26:49,790 --> 00:26:57,010 把i减少到10 然后这个while循环会把i从10减少到7,4,1 389 00:26:57,010 --> 00:27:02,880 然后到-2,-5以此类推 到负无穷 390 00:27:02,880 --> 00:27:05,920 因为i永远不会等于0 391 00:27:05,920 --> 00:27:08,610 在这个程序在最后 我们有一个foo函数 392 00:27:08,610 --> 00:27:12,130 它会把i印出来 393 00:27:12,130 --> 00:27:16,520 这只是个很短的程序 但是bug很明显 394 00:27:16,520 --> 00:27:18,790 特别是在我指出bug在哪以后 395 00:27:18,790 --> 00:27:24,840 不过我的目的 是告诉大家 这个可能会和 396 00:27:24,840 --> 00:27:30,040 你们上一个练习采取的解决方案差不多 397 00:27:30,040 --> 00:27:32,800 也许你在程序中确实遇到了无限循环 398 00:27:32,800 --> 00:27:34,100 而你不知道原因是什么 399 00:27:34,100 --> 00:27:38,690 所以一个很有用的debug技巧 就是 400 00:27:38,690 --> 00:27:40,180 在你的代码中加上很多printf 401 00:27:40,180 --> 00:27:49,200 这里 我要放一个printf 印出 在第一个while循环外 402 00:27:49,200 --> 00:27:53,155 这里我要一个printf 印出 i 403 00:27:55,670 --> 00:27:58,330 我可以说 印出 第一个while循环 i 404 00:28:05,130 --> 00:28:09,040 印出 在第二个while循环外 405 00:28:09,040 --> 00:28:12,170 又一次 在这里面印出 第二个循环里 i的值 406 00:28:16,270 --> 00:28:17,520 让我们来运行这个 407 00:28:22,620 --> 00:28:24,800 ./debug 408 00:28:24,800 --> 00:28:25,610 输入一个整数 409 00:28:25,610 --> 00:28:28,150 让我们输入13 410 00:28:28,150 --> 00:28:28,760 然后 411 00:28:28,760 --> 00:28:33,300 我们发现我们在第二个while循环里无限循环了 412 00:28:33,300 --> 00:28:36,305 现在我们就知道bug在哪里了 413 00:28:39,610 --> 00:28:45,610 用printf来debug完全没有问题 但是一但你的程序 414 00:28:45,610 --> 00:28:50,560 开始变长变复杂 我们就有更 415 00:28:50,560 --> 00:28:51,705 复杂的方法来解决问题 416 00:28:51,705 --> 00:28:52,955 让我们把这些printf都移掉 417 00:29:06,242 --> 00:29:08,896 让我确认一下我没弄坏什么东西 418 00:29:08,896 --> 00:29:09,850 好 419 00:29:09,850 --> 00:29:14,180 我们要介绍的程序 420 00:29:14,180 --> 00:29:16,715 叫做GDB 也就是GNU侦錯器 421 00:29:21,892 --> 00:29:27,510 事实上 让我们把debug移走一下 然后重新编译debug 422 00:29:31,420 --> 00:29:34,440 不过首先 关于命令行参数 423 00:29:34,440 --> 00:29:37,780 注意这个Clang命令 它在编译所有东西 424 00:29:37,780 --> 00:29:41,300 而它在命令行递出了 这些命令行参数 425 00:29:41,300 --> 00:29:46,250 所以我们前面怎么用命令行参数的 426 00:29:46,250 --> 00:29:51,500 你们在练习2中也会这样用 而Clang也是这样用命令行参数的 427 00:29:51,500 --> 00:30:00,070 注意到第一个标记 -ggdb3 这个的意思是 428 00:30:00,070 --> 00:30:03,790 Clang 你应该编译这个文件 而最终目的 429 00:30:03,790 --> 00:30:05,380 是我们最终会需要debug它 430 00:30:05,380 --> 00:30:13,840 所以只要你有这个标记 我们就可以GDB debug 431 00:30:13,840 --> 00:30:17,380 它会打开GNU侦错器 432 00:30:17,380 --> 00:30:22,920 这边有很多的命令 你们都要熟悉 433 00:30:22,920 --> 00:30:27,100 第一个 你马上就会用到的 是Run 434 00:30:27,100 --> 00:30:28,200 那么Run会做什么呢? 435 00:30:28,200 --> 00:30:30,910 它会运行我们的程序 436 00:30:30,910 --> 00:30:36,180 所以 run 运行程序 这个程序问我们要一个整数 13 437 00:30:36,180 --> 00:30:39,170 然后就是我们预期到的 无限循环 只不过 438 00:30:39,170 --> 00:30:40,500 我把printf移掉了 所以我们不会看见它 439 00:30:43,320 --> 00:30:44,600 正常退出了 440 00:30:44,600 --> 00:30:45,850 哦 441 00:30:48,570 --> 00:30:53,640 有可能它是被包住 -- 我们不管它 442 00:30:53,640 --> 00:30:55,170 假设它没有正常退出 443 00:30:59,500 --> 00:31:03,370 那么就有一个非常复杂的答案了 444 00:31:03,370 --> 00:31:07,890 所以现在 这并没有什么用 445 00:31:07,890 --> 00:31:11,480 所以在这个侦错器里运行我们的程序对我们并没有帮助 446 00:31:11,480 --> 00:31:15,610 因为我们也可以在GDB外用./debug 447 00:31:15,610 --> 00:31:21,250 所以你们可能要用的命令-- 448 00:31:21,250 --> 00:31:22,970 让我退出这个 449 00:31:22,970 --> 00:31:25,850 Control-d或退出 都行 450 00:31:25,850 --> 00:31:29,550 让我们把它重新打开 451 00:31:29,550 --> 00:31:31,130 另一个你们想要马上熟悉起来的命令 452 00:31:31,130 --> 00:31:33,600 大概是Break 453 00:31:33,600 --> 00:31:37,120 让我们对main函数用break 然后我来解释一下它 454 00:31:41,010 --> 00:31:46,370 在这里我们看到 我们在debug.c里的这一行里放了一个断点 455 00:31:46,370 --> 00:31:50,160 所以break(断)的意思是 在我输入run时 456 00:31:50,160 --> 00:31:53,560 这个程序会一直运行 直到它遇到断点为止 457 00:31:53,560 --> 00:31:59,390 所以当我运行 程序启动后 它在进入 458 00:31:59,390 --> 00:32:01,940 main函数时就会断 459 00:32:01,940 --> 00:32:06,930 break main将会是你们会经常做的事 460 00:32:06,930 --> 00:32:11,340 现在 给你们介绍更多的一些命令 461 00:32:11,340 --> 00:32:14,330 注意在这里 它说我们在第11行断了 462 00:32:14,330 --> 00:32:16,230 那里是printf 输入一个整数 463 00:32:16,230 --> 00:32:21,260 Next 这个命令会告诉我们到下一行代码去 464 00:32:21,260 --> 00:32:24,810 它会让我们一行一行地过我们的程序 465 00:32:24,810 --> 00:32:26,260 所以 next 466 00:32:26,260 --> 00:32:29,820 现在 在第12行 我们会得到整数 467 00:32:29,820 --> 00:32:30,450 next 468 00:32:30,450 --> 00:32:34,290 如果你再敲一下回车 它会重复一次你最后做的事 469 00:32:34,290 --> 00:32:36,480 所以我不用每次都输入 next 470 00:32:36,480 --> 00:32:40,100 输入一个整数 13 471 00:32:40,100 --> 00:32:46,940 现在 在第14行 当i比10大时 我来用next 472 00:32:46,940 --> 00:32:48,685 我们发现我会减少i 473 00:32:48,685 --> 00:32:50,210 所以我们会再一次减少i 474 00:32:50,210 --> 00:32:53,620 所以现在 另一个有用的命令是Print 475 00:32:53,620 --> 00:32:55,750 Print会印出变量的值 476 00:32:55,750 --> 00:32:57,825 那让我们把变量i的值拿出来 477 00:32:57,825 --> 00:32:58,705 让我们来print i 478 00:32:58,705 --> 00:33:00,910 它会说 i是11 479 00:33:00,910 --> 00:33:03,330 现在我们再来用next 然后 当i比10大时 480 00:33:03,330 --> 00:33:05,590 所以 i仍然比10大 因为它是11 481 00:33:05,590 --> 00:33:06,920 i-- 482 00:33:06,920 --> 00:33:08,250 让我们再print i 483 00:33:08,250 --> 00:33:10,950 像我们预计得那样 i是10 484 00:33:10,950 --> 00:33:12,510 再 next 485 00:33:12,510 --> 00:33:16,250 回到条件 i比10大 但现在 i是10 486 00:33:16,250 --> 00:33:20,040 所以它没有比10大 所以我们希望它会从while循环中退出 487 00:33:20,040 --> 00:33:22,220 现在我们到了那行代码下面 488 00:33:22,220 --> 00:33:28,750 另外一个命令 List 会列出前后的几行代码 489 00:33:28,750 --> 00:33:31,240 以免你不知道自己在程序的什么地方了 490 00:33:31,240 --> 00:33:35,420 所以我们刚退出了这个while循环 现在我们进入了 491 00:33:35,420 --> 00:33:37,080 这个while循环 第18行的 492 00:33:37,080 --> 00:33:39,860 当i不等于0 493 00:33:39,860 --> 00:33:46,570 下一行 i等于i-3 我们会发现 这会一直运行 494 00:33:46,570 --> 00:33:48,270 我们可以印出i 495 00:33:48,270 --> 00:33:49,990 每一个命令都有所谓的快捷方式 496 00:33:49,990 --> 00:33:51,720 p是Print的缩写 497 00:33:51,720 --> 00:33:53,400 所以我们可以 p i 498 00:33:53,400 --> 00:33:57,550 一直按住n或者一直输入next都行 499 00:33:57,550 --> 00:33:58,340 再一次Print i 500 00:33:58,340 --> 00:34:00,380 可以看到 它现在是-167了 501 00:34:00,380 --> 00:34:06,030 所以这个会一直进行下去 但不是真的 一直进行 因为你们刚也看见了 502 00:34:06,030 --> 00:34:09,330 它在某一点是会停止的 503 00:34:09,330 --> 00:34:15,699 这就是GDB的简单教程 504 00:34:15,699 --> 00:34:19,504 但让我们在GDB中再做一件事 505 00:34:19,504 --> 00:34:20,754 debug 506 00:34:23,540 --> 00:34:28,534 所以 在这个情况下 这个无限循环 507 00:34:28,534 --> 00:34:30,050 其实是在main函数里的 508 00:34:30,050 --> 00:34:35,779 现在 只要知道我要把这个无限循环移到 509 00:34:35,779 --> 00:34:37,029 foo函数里就可以了 510 00:34:40,679 --> 00:34:43,730 只要记住 在这个程序最后 原本我们在调用foo 511 00:34:43,730 --> 00:34:46,210 它的作用就是印出i 512 00:34:46,210 --> 00:34:51,880 但现在我们调用foo 它会减少i直到它是0 513 00:34:51,880 --> 00:34:54,548 然后再印出这个变量 514 00:34:54,548 --> 00:34:55,469 好 515 00:34:55,469 --> 00:34:57,970 保存 516 00:34:57,970 --> 00:35:00,175 make debug 517 00:35:00,175 --> 00:35:03,310 现在 gdb debug 518 00:35:03,310 --> 00:35:04,090 好 519 00:35:04,090 --> 00:35:10,580 所以如果我只输入Run 那我就不能一行行地 520 00:35:10,580 --> 00:35:11,730 看我的程序了 521 00:35:11,730 --> 00:35:19,820 所以让我们在main这里用break 然后输入run 522 00:35:19,820 --> 00:35:28,160 让我们过一遍 printf 输入一个整数 取得整数 13 523 00:35:34,180 --> 00:35:37,490 我们会一直减少i 直到i不大于10 524 00:35:37,490 --> 00:35:42,840 然后我们会进入while循环 然后到那一行-- 525 00:35:42,840 --> 00:35:44,364 让我们在新窗口里打开它 526 00:35:48,720 --> 00:35:53,300 所以我们减少i直到它不再大于10 527 00:35:53,300 --> 00:35:55,700 然后我们调用函数foo 528 00:35:55,700 --> 00:36:01,340 当我到foo函数这边的时候 我调用了foo 529 00:36:01,340 --> 00:36:04,030 这时我就不再对GDB有控制权了 530 00:36:04,030 --> 00:36:10,230 所以当我在这行输入Next时 它会继续运行 531 00:36:10,230 --> 00:36:12,400 直到这里 程序退出了-- 532 00:36:12,400 --> 00:36:14,450 假设它最终没有退出 533 00:36:14,450 --> 00:36:16,390 不过 你们看到它暂停了一下 534 00:36:16,390 --> 00:36:22,040 所以为什么 在这里 我失去了对程序的控制呢 535 00:36:22,040 --> 00:36:27,540 因为当我输入next时 会真的 536 00:36:27,540 --> 00:36:28,850 跳到它将要执行的下一行代码去 537 00:36:28,850 --> 00:36:35,950 所以在第21行后 它将要执行的是第22行代码 538 00:36:35,950 --> 00:36:38,520 也就是 从main中退出了 539 00:36:38,520 --> 00:36:43,810 所以我不想只是去下一行代码 540 00:36:43,810 --> 00:36:48,170 我想要去foo这个函数 然后一行一行地 541 00:36:48,170 --> 00:36:49,830 过它的代码 542 00:36:49,830 --> 00:36:53,726 这样 我们有另一个方法可以处理 543 00:36:53,726 --> 00:36:56,770 让我们再次退出这个 544 00:36:56,770 --> 00:36:58,020 break main 545 00:37:00,520 --> 00:37:06,370 1 next, next, 13, next, next, next, 要小心 546 00:37:06,370 --> 00:37:09,820 在我们到foo这一行之前 547 00:37:09,820 --> 00:37:10,520 好 548 00:37:10,520 --> 00:37:13,700 现在 我们在第21行了 在这里我们会开始用foo 549 00:37:13,700 --> 00:37:17,100 我们不想输入next因为这样就会调用foo函数 550 00:37:17,100 --> 00:37:18,710 就到下一行代码去了 551 00:37:18,710 --> 00:37:20,840 我们想要用的是Step 552 00:37:20,840 --> 00:37:25,690 Step和Next有个区别 Step会让你进入函数 553 00:37:25,690 --> 00:37:28,190 而Next会过一个函数 554 00:37:28,190 --> 00:37:32,830 它会执行整个函数 然后继续 555 00:37:32,830 --> 00:37:37,210 所以Step会把我们带入函数foo中 556 00:37:37,210 --> 00:37:41,160 我们现在可以在这里看到 我们重新回到了这个while循环 理论上来说 557 00:37:41,160 --> 00:37:44,190 它会永远循环下去 558 00:37:44,190 --> 00:37:50,420 如果你按了Step 当没有函数可以调用时 559 00:37:50,420 --> 00:37:51,720 它就和Next一样了 560 00:37:51,720 --> 00:37:55,320 所以只有当你在要调用函数的那一行时 561 00:37:55,320 --> 00:37:56,970 Step才会和Next有区别 562 00:37:56,970 --> 00:37:57,930 所以Step会把我们带到这 563 00:37:57,930 --> 00:38:02,100 Step, step, step, step, step, step 然后我们就会无限循环下去 564 00:38:02,100 --> 00:38:06,810 你们最好熟悉这个 这个判断无限循环的方法 565 00:38:06,810 --> 00:38:08,960 就是按住回车键来看你在哪卡住了 566 00:38:11,610 --> 00:38:14,780 还有更好的方法 但是现在 这样就够用了 567 00:38:14,780 --> 00:38:17,967 从风格上来说 为了去Style 50相统一 我应该这样做的 568 00:38:21,550 --> 00:38:24,030 好 569 00:38:24,030 --> 00:38:28,400 最后介绍一个命令 570 00:38:28,400 --> 00:38:30,810 让我们回到gdb debug 571 00:38:30,810 --> 00:38:35,580 这一次 不是在main函数这断 如果我知道foo函数也有问题 572 00:38:35,580 --> 00:38:39,230 那我就可以说 在foo这里断 573 00:38:39,230 --> 00:38:42,310 假设我在main和foo那都断了 574 00:38:42,310 --> 00:38:45,390 你想建多少个断点就可以建多少个 575 00:38:45,390 --> 00:38:49,230 当我输入run时 它会在-- 576 00:38:49,230 --> 00:38:52,180 让我们重新编译下 因为我改了点东西 577 00:38:52,180 --> 00:38:55,950 你们会看到这一行 警告 源文件比执行文件更新 578 00:38:55,950 --> 00:38:59,680 这表示 我刚才在这里做了些改动 579 00:38:59,680 --> 00:39:03,100 使之与Style 50统一 但我没有重新编译程序 580 00:39:03,100 --> 00:39:04,870 所以GDB提醒了我 581 00:39:04,870 --> 00:39:10,130 我要退出 重新make debug, 敲击gdb debug 582 00:39:10,130 --> 00:39:10,700 好 583 00:39:10,700 --> 00:39:12,800 现在 回到我刚才的地方 584 00:39:12,800 --> 00:39:15,720 Break main, break foo 585 00:39:15,720 --> 00:39:20,680 现在如果我运行这个程序 它会一直运行 586 00:39:20,680 --> 00:39:21,320 直到遇到断点为止 587 00:39:21,320 --> 00:39:24,680 断点在main这里 588 00:39:24,680 --> 00:39:28,630 我不想要一直next, next, next, next, next 直到我抵达foo 589 00:39:28,630 --> 00:39:35,230 我可以输入continue 它会让你继续 直到遇到下一个断点 590 00:39:35,230 --> 00:39:37,200 我得先输入整数 591 00:39:37,200 --> 00:39:40,570 Continue会让我们继续 直到我抵达下一个断点 592 00:39:40,570 --> 00:39:43,320 也就是foo函数 593 00:39:43,320 --> 00:39:50,130 所以 Run会让你运行 直到遇到断点 但是你只会在 594 00:39:50,130 --> 00:39:54,060 程序开始时输入run 从那以后 就是continue了 595 00:39:54,060 --> 00:40:01,950 如果我只在main处断 那运行它时 596 00:40:01,950 --> 00:40:03,670 它就会在main处断 然后继续 597 00:40:03,670 --> 00:40:10,050 因为我在foo这里没有断点 输入一个整数 598 00:40:10,050 --> 00:40:11,380 现在我们会在foo这里断 599 00:40:11,380 --> 00:40:16,318 它会进行无限循环 600 00:40:16,318 --> 00:40:17,568 好 601 00:40:19,500 --> 00:40:24,420 这就是对GDB的介绍 602 00:40:24,420 --> 00:40:27,790 在你们的练习中 你们应该开始用它了 603 00:40:27,790 --> 00:40:30,550 它对于确定bug在哪十分有用 604 00:40:30,550 --> 00:40:35,280 如果你能一行一行地过你的代码 605 00:40:35,280 --> 00:40:39,740 然后比对你想要它做的事和实际发生的事 606 00:40:39,740 --> 00:40:41,060 那么基本上就不会错过bug 607 00:40:45,280 --> 00:40:46,530 好 608 00:40:48,310 --> 00:40:54,040 上周David首次提到了这个密码 609 00:40:54,040 --> 00:40:59,350 我们不想把密码以纯文字文件 610 00:40:59,350 --> 00:41:03,210 储存在我们的电脑里 那样别人 611 00:41:03,210 --> 00:41:04,660 很容易过来打开并阅读它们 612 00:41:04,660 --> 00:41:07,530 理想情况下 它们应该被加密 613 00:41:07,530 --> 00:41:13,340 在练习2里 你们会遇到一种加密方法 614 00:41:13,340 --> 00:41:16,520 好吧 是两种 但它们不是太好的方法 615 00:41:16,520 --> 00:41:20,050 如果你们做黑客版的话 你们还需要去 616 00:41:20,050 --> 00:41:22,150 解密一些东西 617 00:41:22,150 --> 00:41:29,770 所以现在的问题是 如果我们有 618 00:41:29,770 --> 00:41:34,830 这世上最强的加密算法 如果你选了个很弱的密码 619 00:41:34,830 --> 00:41:37,720 它并不会帮到你什么 因为人们还是能解出它来 620 00:41:37,720 --> 00:41:41,530 即使加密过的字串看上去像一团乱麻 621 00:41:41,530 --> 00:41:44,760 对他们毫无意义 他们也许试过几次就试出来密码了 622 00:41:44,760 --> 00:41:50,560 所以这就不太安全 623 00:41:50,560 --> 00:41:55,890 让我们来看一段视频 讲的就是这个 624 00:41:59,587 --> 00:42:00,970 [视频播放] 625 00:42:00,970 --> 00:42:02,100 Helmet 你这个恶魔 626 00:42:02,100 --> 00:42:03,370 发生什么了? 627 00:42:03,370 --> 00:42:05,170 你要对我女儿做什么? 628 00:42:05,170 --> 00:42:09,910 请让我为您介绍这位年轻的天才整形师 Phillip Schlotkin 医生 629 00:42:09,910 --> 00:42:13,730 全宇宙鼻子做的最好的 630 00:42:13,730 --> 00:42:16,080 在比弗利山庄也是一样 631 00:42:16,080 --> 00:42:17,210 陛下 632 00:42:17,210 --> 00:42:18,070 鼻子? 633 00:42:18,070 --> 00:42:18,670 我不懂 634 00:42:18,670 --> 00:42:20,090 她已经整过鼻子了 635 00:42:20,090 --> 00:42:21,910 那是我送她的16岁生日礼物 636 00:42:21,910 --> 00:42:22,140 不 637 00:42:22,140 --> 00:42:23,690 不是你想的那样 638 00:42:23,690 --> 00:42:25,420 比那糟糕得多 639 00:42:25,420 --> 00:42:30,300 如果你不给我空气盾的密码 640 00:42:30,300 --> 00:42:34,226 Dr. Schlotkin就会把你女儿的旧鼻子安回去 641 00:42:34,226 --> 00:42:35,476 不 642 00:42:38,712 --> 00:42:40,516 你从哪得到这个的 643 00:42:40,516 --> 00:42:41,440 好吧 644 00:42:41,440 --> 00:42:42,180 我告诉你 645 00:42:42,180 --> 00:42:43,381 我会说的 646 00:42:43,381 --> 00:42:44,263 不爸爸 647 00:42:44,263 --> 00:42:45,590 不 你不能 648 00:42:45,590 --> 00:42:46,860 你说得对 亲爱的 649 00:42:46,860 --> 00:42:48,450 我会想念你的新鼻子的 650 00:42:48,450 --> 00:42:52,090 但我不会告诉他密码的 无论如何都不会 651 00:42:52,090 --> 00:42:53,680 非常好 652 00:42:53,680 --> 00:42:55,685 Dr. Schlotkin 怎么糟怎么来吧 653 00:42:55,685 --> 00:42:56,914 十分乐意 654 00:42:56,914 --> 00:43:00,690 [磨工具声] 655 00:43:00,690 --> 00:43:01,910 不 656 00:43:01,910 --> 00:43:02,520 等下 657 00:43:02,520 --> 00:43:03,836 等下 658 00:43:03,836 --> 00:43:05,300 我告诉你 659 00:43:05,300 --> 00:43:06,880 我会说 660 00:43:06,880 --> 00:43:09,130 我就知道这行得通 661 00:43:09,130 --> 00:43:09,900 好吧 662 00:43:09,900 --> 00:43:12,850 告诉我 663 00:43:12,850 --> 00:43:16,918 密码是1 664 00:43:16,918 --> 00:43:17,406 1 665 00:43:17,406 --> 00:43:18,382 1 666 00:43:18,382 --> 00:43:19,358 2 667 00:43:19,358 --> 00:43:19,846 2 668 00:43:19,846 --> 00:43:20,822 2 669 00:43:20,822 --> 00:43:21,310 3 670 00:43:21,310 --> 00:43:21,798 3 671 00:43:21,798 --> 00:43:22,774 3 672 00:43:22,774 --> 00:43:23,262 4 673 00:43:23,262 --> 00:43:23,750 4 674 00:43:23,750 --> 00:43:26,150 4 675 00:43:26,150 --> 00:43:27,010 5 676 00:43:27,010 --> 00:43:27,670 5 677 00:43:27,670 --> 00:43:29,010 5 678 00:43:29,010 --> 00:43:34,770 所以密码是 1 2 3 4 5 679 00:43:34,770 --> 00:43:37,460 这是我听过的最蠢的密码了 680 00:43:37,460 --> 00:43:39,710 这是哪个蠢蛋的旅行箱密码吧 681 00:43:39,710 --> 00:43:42,000 谢谢您 陛下 682 00:43:42,000 --> 00:43:43,530 你做了什么? 683 00:43:43,530 --> 00:43:44,490 我把墙关了?? 684 00:43:44,490 --> 00:43:45,420 不 你没有 685 00:43:45,420 --> 00:43:45,840 你把整部电影关了 686 00:43:45,840 --> 00:43:46,930 我一定是按错了键 687 00:43:46,930 --> 00:43:48,265 把它重开开来啊 688 00:43:48,265 --> 00:43:49,110 把电影重新开开来 689 00:43:49,110 --> 00:43:49,510 是 先生 690 00:43:49,510 --> 00:43:49,917 是先生 691 00:43:49,917 --> 00:43:50,324 我们走吧 Arnold 692 00:43:50,324 --> 00:43:51,140 Gretchen 来吧 693 00:43:51,140 --> 00:43:53,060 当然 你知道我们是要收你钱的 694 00:43:53,060 --> 00:43:53,440 [视频播放结束] 695 00:43:53,440 --> 00:43:54,690 好 696 00:43:59,690 --> 00:44:08,430 现在他们已经在讲到安全性了 697 00:44:08,430 --> 00:44:16,050 这个电影的海报不错 近来 有关国安局 698 00:44:16,050 --> 00:44:17,300 窃听事件 699 00:44:21,840 --> 00:44:26,930 在网上 感受到有隐私并不是件容易的事 700 00:44:26,930 --> 00:44:34,540 虽然我也不知道"棱镜"计划的具体细节 701 00:44:34,540 --> 00:44:42,130 不说"棱镜"了 702 00:44:42,130 --> 00:44:44,030 现在想一下你的笔记本电脑 703 00:44:44,030 --> 00:44:48,360 在上面这里 我想要换去我真正的帐户 704 00:44:48,360 --> 00:44:50,370 有我的小企鹅的那个 705 00:44:50,370 --> 00:44:57,310 所以我有一组密码 这个密码是我决定的 706 00:44:57,310 --> 00:45:02,430 但记住我是靠什么登录的 707 00:45:02,430 --> 00:45:04,850 这个登录提示 是某种程序 708 00:45:04,850 --> 00:45:07,910 是某个人写的某种程序 709 00:45:07,910 --> 00:45:13,250 所以 这个人 如果他们不怀好意 710 00:45:13,250 --> 00:45:17,780 他们可以说 好 如果我输入的密码 711 00:45:17,780 --> 00:45:22,800 等于我真正的密码 或者等于某种特殊的密码 712 00:45:22,800 --> 00:45:25,550 比如David很棒 什么的 713 00:45:25,550 --> 00:45:27,190 那就让我登入 714 00:45:27,190 --> 00:45:33,760 那么一个不怀好意的程序员 就可以 715 00:45:33,760 --> 00:45:36,150 登录你们所有人的Mac 或者Windows 或者任何东西 716 00:45:36,150 --> 00:45:41,980 但这并不是个问题 因为 这个登录程序 717 00:45:41,980 --> 00:45:48,720 是随OS X过来的 千百个人 718 00:45:48,720 --> 00:45:50,020 都查看过这个代码了 719 00:45:50,020 --> 00:45:55,330 所以 如果在你的代码里 你说 如果这个字串 720 00:45:55,330 --> 00:45:58,860 == David好棒 登录 那么有人会说 等下 721 00:45:58,860 --> 00:45:59,800 这不对 722 00:45:59,800 --> 00:46:01,790 这不应该在这的 723 00:46:01,790 --> 00:46:06,650 这就是我们用来保证安全性的一种方法 724 00:46:06,650 --> 00:46:10,300 但是想想你写的程序 725 00:46:10,300 --> 00:46:13,000 假设你写了这个登录程序 726 00:46:13,000 --> 00:46:20,440 你写了这个登录程序 所以显然 727 00:46:20,440 --> 00:46:21,210 你是个很优秀的程序员 728 00:46:21,210 --> 00:46:25,610 你是不会不怀好意 把什么x==David好棒 729 00:46:25,610 --> 00:46:27,860 放进你的代码里的 730 00:46:27,860 --> 00:46:31,930 但这个程序 你用什么来编译这个程序呢? 731 00:46:31,930 --> 00:46:34,180 某种像Clang的东西 732 00:46:34,180 --> 00:46:38,460 如果那个写Clang的人在Clang中说有这样一种特殊情况 733 00:46:38,460 --> 00:46:44,310 如果我在编译登录程序 那么把这个 734 00:46:44,310 --> 00:46:49,720 如果x==David好棒的代码放入登录程序中 735 00:46:49,720 --> 00:46:59,890 但是我们有同样的问题 Clang也一样 736 00:46:59,890 --> 00:47:03,790 有几千个 甚至几万个人都检查过Clang 737 00:47:03,790 --> 00:47:07,160 看过它的代码然后说 好 这边没有问题 738 00:47:07,160 --> 00:47:10,680 显然 没有人在干坏事 739 00:47:10,680 --> 00:47:15,780 但是假如Clang它本身 比如 如果我编译Clang会怎样? 740 00:47:15,780 --> 00:47:20,900 如果我有某种编译器 能编译Clang 然后往里植入 741 00:47:20,900 --> 00:47:25,610 这个特别的黑客 当我编译clang时 742 00:47:25,610 --> 00:47:31,290 我拿到的执行文件就会在登录程序中 743 00:47:31,290 --> 00:47:34,230 寻找并植入这个密码 ==David好棒 744 00:47:34,230 --> 00:47:37,990 记住 你的编译器 在某个时候 也是要被编译的 745 00:47:37,990 --> 00:47:42,810 如果你用某个恶意程序来编译Clang 746 00:47:42,810 --> 00:47:45,580 那你就会把后面的一切都毁了 747 00:47:45,580 --> 00:47:49,630 所以这里 我们有Ken Thompson和 Dennis Ritchie 748 00:47:49,630 --> 00:47:53,780 这是个标志性的照片 749 00:47:53,780 --> 00:47:55,470 右边的是Dennis Ritchie 750 00:47:55,470 --> 00:47:58,740 他是一名-- 751 00:47:58,740 --> 00:48:03,640 C差不多就是他写出来的 所以你们得为了这节课谢谢他 752 00:48:03,640 --> 00:48:04,840 左边的是Ken Thompson 753 00:48:04,840 --> 00:48:07,780 他俩一起差不多写出了UNIX 754 00:48:07,780 --> 00:48:10,140 他们是UNIX的主要开发者 755 00:48:10,140 --> 00:48:11,310 还有些其他人 756 00:48:11,310 --> 00:48:16,240 Ken Thompson在某年得了图灵奖 757 00:48:16,240 --> 00:48:20,860 图灵奖 我一直听人这么比喻 758 00:48:20,860 --> 00:48:23,100 是计算机界的诺贝尔奖 759 00:48:23,100 --> 00:48:27,500 所以在图灵奖上 他要致领奖辞 760 00:48:27,500 --> 00:48:31,790 他的演讲 现在已经很有名了 761 00:48:31,790 --> 00:48:35,620 叫 有关信任信任的思考 我们已经把链接放在课程网站上了 762 00:48:35,620 --> 00:48:41,670 在这个演讲中 他说 好 我写了UNIX 763 00:48:41,670 --> 00:48:43,320 你们所以人都在用UNIX 764 00:48:43,320 --> 00:48:46,960 记住 今天的Linux就是从UNIX中发展出来的 765 00:48:46,960 --> 00:48:50,140 OS X直接会用到UNIX 766 00:48:50,140 --> 00:48:53,810 Windows没用到什么UNIX 但很多概念都来自UNIX 767 00:48:53,810 --> 00:48:59,220 所以他到了台上 说 好 我写了UNIX 768 00:48:59,220 --> 00:49:03,940 就告诉你们一声 我有办法 769 00:49:03,940 --> 00:49:05,590 登入你们所有人的电脑 770 00:49:05,590 --> 00:49:14,280 因为我放了这个特别的 如果x==Ken Thomson好棒 771 00:49:14,280 --> 00:49:16,350 这样我就能登录了 772 00:49:16,350 --> 00:49:18,370 那么人们就问 你是怎么做到的? 773 00:49:18,370 --> 00:49:21,090 我们查了登录程序了 那儿什么都没有 774 00:49:21,090 --> 00:49:24,700 他就说 我改了编译器用它登入登录程序 775 00:49:24,700 --> 00:49:30,490 这样登录程序里就会有x==Ken Thompson好棒E? 776 00:49:30,490 --> 00:49:31,700 这个东西了 777 00:49:31,700 --> 00:49:33,120 然后人们说 这一定不是真的 778 00:49:33,120 --> 00:49:35,740 我们看了编译器 而编译器中 779 00:49:35,740 --> 00:49:36,400 没有这样的代码 780 00:49:36,400 --> 00:49:40,540 他说 好 但是你用什么来编译编译器呢? 781 00:49:40,540 --> 00:49:44,810 然后人们在想思考 他就说 我给了你们用来编译编译器的编译器 782 00:49:44,810 --> 00:49:50,580 所以你们在编译编译器的时候 783 00:49:50,580 --> 00:49:56,390 它本身就是恶意的 所以会破坏登录程序 784 00:49:56,390 --> 00:49:59,360 所以从本质上来说 你没办法去看登录程序的源代码 785 00:49:59,360 --> 00:50:02,450 来看它是不是有问题 786 00:50:02,450 --> 00:50:04,220 你甚至不能去看编译器的源代码 787 00:50:04,220 --> 00:50:06,790 来看有没有问题 788 00:50:06,790 --> 00:50:11,940 你得看机器码 也就是编译后的编译器的二进制码 789 00:50:11,940 --> 00:50:16,760 来看 等下 这行代码不应该在这的 790 00:50:16,760 --> 00:50:22,130 但是Ken Thompson多做了一步 说 791 00:50:22,130 --> 00:50:25,980 有三种特殊的程序 可以帮你读程序的二进制码 792 00:50:25,980 --> 00:50:29,340 所以如果有人用这个程序来读二进制 793 00:50:29,340 --> 00:50:30,490 他们会看见这些代码行 794 00:50:30,490 --> 00:50:34,020 他改变了程序 说 好 如果你在看编译器 795 00:50:34,020 --> 00:50:38,460 不要显示这些二进制码 796 00:50:38,460 --> 00:50:42,830 所以这样你也要更进一步 但是 797 00:50:42,830 --> 00:50:46,210 它可能间接有很多层 所以到了某个时候 798 00:50:46,210 --> 00:50:47,990 就不会有人去查了 799 00:50:47,990 --> 00:50:52,590 这个故事告诉我们 800 00:50:52,590 --> 00:50:54,340 你们不会在这门课上去写Clang的 801 00:50:54,340 --> 00:50:57,020 你们会在这门课中用到很多的Clang 802 00:50:57,020 --> 00:51:00,490 你们所了解到的 就是Clang是一个会损害 803 00:51:00,490 --> 00:51:03,520 你们编译的所有程序的程序 804 00:51:03,520 --> 00:51:08,206 把这句非常不祥的话留给大家 我们周三见 805 00:51:08,206 --> 00:51:10,030 [掌声] 806 00:51:10,030 --> 00:51:12,935 在下一切CS50课上 807 00:51:12,935 --> 00:51:14,580 你敢说这句试试 808 00:51:14,580 --> 00:51:15,930 你可以的 809 00:51:15,930 --> 00:51:19,440 你以前成功过的 你今天也可以的 你明天也会成功的 810 00:51:19,440 --> 00:51:20,930 你已经这么做很多年了 811 00:51:20,930 --> 00:51:22,790 就上来完成它 812 00:51:22,790 --> 00:51:24,310 你可以的 813 00:51:24,310 --> 00:51:26,102 [音乐播放]