├── .gitignore ├── .nojekyll ├── 404.html ├── CNAME ├── Dockerfile ├── LICENSE ├── README.md ├── SUMMARY.md ├── asset ├── docsify-apachecn-footer.js ├── docsify-baidu-push.js ├── docsify-baidu-stat.js ├── docsify-clicker.js ├── docsify-cnzz.js ├── docsify-copy-code.min.js ├── docsify.min.js ├── prism-c.min.js ├── prism-darcula.css ├── search.min.js ├── style.css └── vue.css ├── cover.jpg ├── donors.md ├── ex0.md ├── ex1.md ├── ex10.md ├── ex11.md ├── ex12.md ├── ex13.md ├── ex14.md ├── ex15.md ├── ex16.md ├── ex17.md ├── ex18.md ├── ex19.md ├── ex2.md ├── ex20.md ├── ex21.md ├── ex22.md ├── ex23.md ├── ex24.md ├── ex25.md ├── ex26.md ├── ex27.md ├── ex28.md ├── ex29.md ├── ex3.md ├── ex30.md ├── ex31.md ├── ex32.md ├── ex33.md ├── ex34.md ├── ex35.md ├── ex36.md ├── ex37.md ├── ex38.md ├── ex39.md ├── ex4.md ├── ex40.md ├── ex41.md ├── ex42.md ├── ex43.md ├── ex44.md ├── ex45.md ├── ex46.md ├── ex47.md ├── ex5.md ├── ex6.md ├── ex7.md ├── ex8.md ├── ex9.md ├── img └── qr_alipay.png ├── index.html ├── introduction.md ├── postscript.md ├── preface.md ├── styles └── ebook.css └── update.sh /.gitignore: -------------------------------------------------------------------------------- 1 | _book 2 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/lcthw-zh/477a8c2af9ccee0781abe5b7295308c0293866ef/.nojekyll -------------------------------------------------------------------------------- /404.html: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /404.html 3 | --- 4 | 5 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | lcthw.apachecn.org -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM httpd:2.4 2 | COPY ./ /usr/local/apache2/htdocs/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 笨办法学C 中文版 2 | 3 | 来源:[Learn C The Hard Way](http://c.learncodethehardway.org/book/) 4 | 5 | 作者:[Zed A. Shaw](https://twitter.com/lzsthw) 6 | 7 | 译者:[飞龙](https://github.com/wizardforcel) 8 | 9 | 自豪地采用[谷歌翻译](https://translate.google.cn/) 10 | 11 | > **一句 MMP 送给在座的各位程序正义垃圾。** 12 | 13 | + [在线阅读](https://lcthw.apachecn.org) 14 | + [在线阅读(Gitee)](https://apachecn.gitee.io/lcthw-zh/) 15 | + [PDF格式](https://www.gitbook.com/download/pdf/book/wizardforcel/lcthw) 16 | + [EPUB格式](https://www.gitbook.com/download/epub/book/wizardforcel/lcthw) 17 | + [MOBI格式](https://www.gitbook.com/download/mobi/book/wizardforcel/lcthw) 18 | + [Github](https://github.com/wizardforcel/lcthw-zh) 19 | 20 | ## 下载 21 | 22 | ### Docker 23 | 24 | ``` 25 | docker pull apachecn0/lcthw-zh 26 | docker run -tid -p :80 apachecn0/lcthw-zh 27 | # 访问 http://localhost:{port} 查看文档 28 | ``` 29 | 30 | ### PYPI 31 | 32 | ``` 33 | pip install lcthw-zh 34 | lcthw-zh 35 | # 访问 http://localhost:{port} 查看文档 36 | ``` 37 | 38 | ### NPM 39 | 40 | ``` 41 | npm install -g lcthw-zh 42 | lcthw-zh 43 | # 访问 http://localhost:{port} 查看文档 44 | ``` 45 | 46 | ## 赞助我 47 | 48 | ![](img/qr_alipay.png) 49 | 50 | ## 协议 51 | 52 | 此版本遵循[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)协议,原版无此约束。 53 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | + [笨办法学C 中文版](README.md) 2 | + [前言](preface.md) 3 | + [导言:C的笛卡尔之梦](introduction.md) 4 | + [练习0:准备](ex0.md) 5 | + [练习1:启用编译器](ex1.md) 6 | + [练习2:用Make来代替Python](ex2.md) 7 | + [练习3:格式化输出](ex3.md) 8 | + [练习4:Valgrind 介绍](ex4.md) 9 | + [练习5:一个C程序的结构](ex5.md) 10 | + [练习6:变量类型](ex6.md) 11 | + [练习7:更多变量和一些算术](ex7.md) 12 | + [练习8:大小和数组](ex8.md) 13 | + [练习9:数组和字符串](ex9.md) 14 | + [练习10:字符串数组和循环](ex10.md) 15 | + [练习11:While循环和布尔表达式](ex11.md) 16 | + [练习12:If,Else If,Else](ex12.md) 17 | + [练习13:Switch语句](ex13.md) 18 | + [练习14:编写并使用函数](ex14.md) 19 | + [练习15:指针,可怕的指针](ex15.md) 20 | + [练习16:结构体和指向它们的指针](ex16.md) 21 | + [练习17:堆和栈的内存分配](ex17.md) 22 | + [练习18:函数指针](ex18.md) 23 | + [练习19:一个简单的对象系统](ex19.md) 24 | + [练习20:Zed的强大的调试宏](ex20.md) 25 | + [练习21:高级数据类型和控制结构](ex21.md) 26 | + [练习22:栈、作用域和全局](ex22.md) 27 | + [练习23:认识达夫设备](ex23.md) 28 | + [练习24:输入输出和文件](ex24.md) 29 | + [练习25:变参函数](ex25.md) 30 | + [练习26:编写第一个真正的程序](ex26.md) 31 | + [练习27:创造性和防御性编程](ex27.md) 32 | + [练习28:Makefile 进阶](ex28.md) 33 | + [练习29:库和链接](ex29.md) 34 | + [练习30:自动化测试](ex30.md) 35 | + [练习31:代码调试](ex31.md) 36 | + [练习32:双向链表](ex32.md) 37 | + [练习33:链表算法](ex33.md) 38 | + [练习34:动态数组](ex34.md) 39 | + [练习35:排序和搜索](ex35.md) 40 | + [练习36:更安全的字符串](ex36.md) 41 | + [练习37:哈希表](ex37.md) 42 | + [练习38:哈希算法](ex38.md) 43 | + [练习39:字符串算法](ex39.md) 44 | + [练习40:二叉搜索树](ex40.md) 45 | + [练习41:将 Cachegrind 和 Callgrind 用于性能调优](ex41.md) 46 | + [练习42:栈和队列](ex42.md) 47 | + [练习43:一个简单的统计引擎](ex43.md) 48 | + [练习44:环形缓冲区](ex44.md) 49 | + [练习45:一个简单的TCP/IP客户端](ex45.md) 50 | + [练习46:三叉搜索树](ex46.md) 51 | + [练习47:一个快速的URL路由](ex47.md) 52 | + [后记:“解构 K&R C” 已死](postscript.md) 53 | + [捐赠名单](donors.md) -------------------------------------------------------------------------------- /asset/docsify-apachecn-footer.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var footer = [ 3 | '
', 4 | '
', 5 | '

我们一直在努力

', 6 | '

apachecn/lcthw-zh

', 7 | '

', 8 | ' ', 9 | ' ', 10 | ' ML | ApacheCN

', 11 | '

', 12 | '
', 13 | ' ', 17 | '
', 18 | '
' 19 | ].join('\n') 20 | var plugin = function(hook) { 21 | hook.afterEach(function(html) { 22 | return html + footer 23 | }) 24 | hook.doneEach(function() { 25 | (adsbygoogle = window.adsbygoogle || []).push({}) 26 | }) 27 | } 28 | var plugins = window.$docsify.plugins || [] 29 | plugins.push(plugin) 30 | window.$docsify.plugins = plugins 31 | })() -------------------------------------------------------------------------------- /asset/docsify-baidu-push.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var plugin = function(hook) { 3 | hook.doneEach(function() { 4 | new Image().src = 5 | '//api.share.baidu.com/s.gif?r=' + 6 | encodeURIComponent(document.referrer) + 7 | "&l=" + encodeURIComponent(location.href) 8 | }) 9 | } 10 | var plugins = window.$docsify.plugins || [] 11 | plugins.push(plugin) 12 | window.$docsify.plugins = plugins 13 | })() -------------------------------------------------------------------------------- /asset/docsify-baidu-stat.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var plugin = function(hook) { 3 | hook.doneEach(function() { 4 | window._hmt = window._hmt || [] 5 | var hm = document.createElement("script") 6 | hm.src = "https://hm.baidu.com/hm.js?" + window.$docsify.bdStatId 7 | document.querySelector("article").appendChild(hm) 8 | }) 9 | } 10 | var plugins = window.$docsify.plugins || [] 11 | plugins.push(plugin) 12 | window.$docsify.plugins = plugins 13 | })() -------------------------------------------------------------------------------- /asset/docsify-cnzz.js: -------------------------------------------------------------------------------- 1 | (function(){ 2 | var plugin = function(hook) { 3 | hook.doneEach(function() { 4 | var sc = document.createElement('script') 5 | sc.src = 'https://s5.cnzz.com/z_stat.php?id=' + 6 | window.$docsify.cnzzId + '&online=1&show=line' 7 | document.querySelector('article').appendChild(sc) 8 | }) 9 | } 10 | var plugins = window.$docsify.plugins || [] 11 | plugins.push(plugin) 12 | window.$docsify.plugins = plugins 13 | })() -------------------------------------------------------------------------------- /asset/docsify-copy-code.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * docsify-copy-code 3 | * v2.1.0 4 | * https://github.com/jperasmus/docsify-copy-code 5 | * (c) 2017-2019 JP Erasmus 6 | * MIT license 7 | */ 8 | !function(){"use strict";function r(o){return(r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(o){return typeof o}:function(o){return o&&"function"==typeof Symbol&&o.constructor===Symbol&&o!==Symbol.prototype?"symbol":typeof o})(o)}!function(o,e){void 0===e&&(e={});var t=e.insertAt;if(o&&"undefined"!=typeof document){var n=document.head||document.getElementsByTagName("head")[0],c=document.createElement("style");c.type="text/css","top"===t&&n.firstChild?n.insertBefore(c,n.firstChild):n.appendChild(c),c.styleSheet?c.styleSheet.cssText=o:c.appendChild(document.createTextNode(o))}}(".docsify-copy-code-button,.docsify-copy-code-button span{cursor:pointer;transition:all .25s ease}.docsify-copy-code-button{position:absolute;z-index:1;top:0;right:0;overflow:visible;padding:.65em .8em;border:0;border-radius:0;outline:0;font-size:1em;background:grey;background:var(--theme-color,grey);color:#fff;opacity:0}.docsify-copy-code-button span{border-radius:3px;background:inherit;pointer-events:none}.docsify-copy-code-button .error,.docsify-copy-code-button .success{position:absolute;z-index:-100;top:50%;left:0;padding:.5em .65em;font-size:.825em;opacity:0;-webkit-transform:translateY(-50%);transform:translateY(-50%)}.docsify-copy-code-button.error .error,.docsify-copy-code-button.success .success{opacity:1;-webkit-transform:translate(-115%,-50%);transform:translate(-115%,-50%)}.docsify-copy-code-button:focus,pre:hover .docsify-copy-code-button{opacity:1}"),document.querySelector('link[href*="docsify-copy-code"]')&&console.warn("[Deprecation] Link to external docsify-copy-code stylesheet is no longer necessary."),window.DocsifyCopyCodePlugin={init:function(){return function(o,e){o.ready(function(){console.warn("[Deprecation] Manually initializing docsify-copy-code using window.DocsifyCopyCodePlugin.init() is no longer necessary.")})}}},window.$docsify=window.$docsify||{},window.$docsify.plugins=[function(o,s){o.doneEach(function(){var o=Array.apply(null,document.querySelectorAll("pre[data-lang]")),c={buttonText:"Copy to clipboard",errorText:"Error",successText:"Copied"};s.config.copyCode&&Object.keys(c).forEach(function(t){var n=s.config.copyCode[t];"string"==typeof n?c[t]=n:"object"===r(n)&&Object.keys(n).some(function(o){var e=-1',''.concat(c.buttonText,""),''.concat(c.errorText,""),''.concat(c.successText,""),""].join("");o.forEach(function(o){o.insertAdjacentHTML("beforeend",e)})}),o.mounted(function(){document.querySelector(".content").addEventListener("click",function(o){if(o.target.classList.contains("docsify-copy-code-button")){var e="BUTTON"===o.target.tagName?o.target:o.target.parentNode,t=document.createRange(),n=e.parentNode.querySelector("code"),c=window.getSelection();t.selectNode(n),c.removeAllRanges(),c.addRange(t);try{document.execCommand("copy")&&(e.classList.add("success"),setTimeout(function(){e.classList.remove("success")},1e3))}catch(o){console.error("docsify-copy-code: ".concat(o)),e.classList.add("error"),setTimeout(function(){e.classList.remove("error")},1e3)}"function"==typeof(c=window.getSelection()).removeRange?c.removeRange(t):"function"==typeof c.removeAllRanges&&c.removeAllRanges()}})})}].concat(window.$docsify.plugins||[])}(); 9 | //# sourceMappingURL=docsify-copy-code.min.js.map 10 | -------------------------------------------------------------------------------- /asset/prism-c.min.js: -------------------------------------------------------------------------------- 1 | Prism.languages.c=Prism.languages.extend("clike",{comment:{pattern:/\/\/(?:[^\r\n\\]|\\(?:\r\n?|\n|(?![\r\n])))*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},"class-name":{pattern:/(\b(?:enum|struct)\s+(?:__attribute__\s*\(\([\s\S]*?\)\)\s*)?)\w+/,lookbehind:!0},keyword:/\b(?:__attribute__|_Alignas|_Alignof|_Atomic|_Bool|_Complex|_Generic|_Imaginary|_Noreturn|_Static_assert|_Thread_local|asm|typeof|inline|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|int|long|register|return|short|signed|sizeof|static|struct|switch|typedef|union|unsigned|void|volatile|while)\b/,function:/[a-z_]\w*(?=\s*\()/i,operator:/>>=?|<<=?|->|([-+&|:])\1|[?:~]|[-+*/%&|^!=<>]=?/,number:/(?:\b0x(?:[\da-f]+\.?[\da-f]*|\.[\da-f]+)(?:p[+-]?\d+)?|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?)[ful]*/i}),Prism.languages.insertBefore("c","string",{macro:{pattern:/(^\s*)#\s*[a-z]+(?:[^\r\n\\/]|\/(?!\*)|\/\*(?:[^*]|\*(?!\/))*\*\/|\\(?:\r\n|[\s\S]))*/im,lookbehind:!0,greedy:!0,alias:"property",inside:{string:[{pattern:/^(#\s*include\s*)<[^>]+>/,lookbehind:!0},Prism.languages.c.string],comment:Prism.languages.c.comment,directive:{pattern:/^(#\s*)[a-z]+/,lookbehind:!0,alias:"keyword"},"directive-hash":/^#/,punctuation:/##|\\(?=[\r\n])/,expression:{pattern:/\S[\s\S]*/,inside:Prism.languages.c}}},constant:/\b(?:__FILE__|__LINE__|__DATE__|__TIME__|__TIMESTAMP__|__func__|EOF|NULL|SEEK_CUR|SEEK_END|SEEK_SET|stdin|stdout|stderr)\b/}),delete Prism.languages.c.boolean; -------------------------------------------------------------------------------- /asset/prism-darcula.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Darcula theme 3 | * 4 | * Adapted from a theme based on: 5 | * IntelliJ Darcula Theme (https://github.com/bulenkov/Darcula) 6 | * 7 | * @author Alexandre Paradis 8 | * @version 1.0 9 | */ 10 | 11 | code[class*="lang-"], 12 | pre[data-lang] { 13 | color: #a9b7c6 !important; 14 | background-color: #2b2b2b !important; 15 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 16 | direction: ltr; 17 | text-align: left; 18 | white-space: pre; 19 | word-spacing: normal; 20 | word-break: normal; 21 | line-height: 1.5; 22 | 23 | -moz-tab-size: 4; 24 | -o-tab-size: 4; 25 | tab-size: 4; 26 | 27 | -webkit-hyphens: none; 28 | -moz-hyphens: none; 29 | -ms-hyphens: none; 30 | hyphens: none; 31 | } 32 | 33 | pre[data-lang]::-moz-selection, pre[data-lang] ::-moz-selection, 34 | code[class*="lang-"]::-moz-selection, code[class*="lang-"] ::-moz-selection { 35 | color: inherit; 36 | background: rgba(33, 66, 131, .85); 37 | } 38 | 39 | pre[data-lang]::selection, pre[data-lang] ::selection, 40 | code[class*="lang-"]::selection, code[class*="lang-"] ::selection { 41 | color: inherit; 42 | background: rgba(33, 66, 131, .85); 43 | } 44 | 45 | /* Code blocks */ 46 | pre[data-lang] { 47 | padding: 1em; 48 | margin: .5em 0; 49 | overflow: auto; 50 | } 51 | 52 | :not(pre) > code[class*="lang-"], 53 | pre[data-lang] { 54 | background: #2b2b2b; 55 | } 56 | 57 | /* Inline code */ 58 | :not(pre) > code[class*="lang-"] { 59 | padding: .1em; 60 | border-radius: .3em; 61 | } 62 | 63 | .token.comment, 64 | .token.prolog, 65 | .token.cdata { 66 | color: #808080; 67 | } 68 | 69 | .token.delimiter, 70 | .token.boolean, 71 | .token.keyword, 72 | .token.selector, 73 | .token.important, 74 | .token.atrule { 75 | color: #cc7832; 76 | } 77 | 78 | .token.operator, 79 | .token.punctuation, 80 | .token.attr-name { 81 | color: #a9b7c6; 82 | } 83 | 84 | .token.tag, 85 | .token.tag .punctuation, 86 | .token.doctype, 87 | .token.builtin { 88 | color: #e8bf6a; 89 | } 90 | 91 | .token.entity, 92 | .token.number, 93 | .token.symbol { 94 | color: #6897bb; 95 | } 96 | 97 | .token.property, 98 | .token.constant, 99 | .token.variable { 100 | color: #9876aa; 101 | } 102 | 103 | .token.string, 104 | .token.char { 105 | color: #6a8759; 106 | } 107 | 108 | .token.attr-value, 109 | .token.attr-value .punctuation { 110 | color: #a5c261; 111 | } 112 | 113 | .token.attr-value .punctuation:first-child { 114 | color: #a9b7c6; 115 | } 116 | 117 | .token.url { 118 | color: #287bde; 119 | text-decoration: underline; 120 | } 121 | 122 | .token.function { 123 | color: #ffc66d; 124 | } 125 | 126 | .token.regex { 127 | background: #364135; 128 | } 129 | 130 | .token.bold { 131 | font-weight: bold; 132 | } 133 | 134 | .token.italic { 135 | font-style: italic; 136 | } 137 | 138 | .token.inserted { 139 | background: #294436; 140 | } 141 | 142 | .token.deleted { 143 | background: #484a4a; 144 | } 145 | 146 | code.lang-css .token.property, 147 | code.lang-css .token.property + .token.punctuation { 148 | color: #a9b7c6; 149 | } 150 | 151 | code.lang-css .token.id { 152 | color: #ffc66d; 153 | } 154 | 155 | code.lang-css .token.selector > .token.class, 156 | code.lang-css .token.selector > .token.attribute, 157 | code.lang-css .token.selector > .token.pseudo-class, 158 | code.lang-css .token.selector > .token.pseudo-element { 159 | color: #ffc66d; 160 | } -------------------------------------------------------------------------------- /asset/search.min.js: -------------------------------------------------------------------------------- 1 | !function(){var h={},f={EXPIRE_KEY:"docsify.search.expires",INDEX_KEY:"docsify.search.index"};function l(e){var n={"&":"&","<":"<",">":">",'"':""","'":"'"};return String(e).replace(/[&<>"']/g,function(e){return n[e]})}function p(e){return e.text||"table"!==e.type||(e.cells.unshift(e.header),e.text=e.cells.map(function(e){return e.join(" | ")}).join(" |\n ")),e.text}function u(r,e,i,o){void 0===e&&(e="");var s,n=window.marked.lexer(e),c=window.Docsify.slugify,d={};return n.forEach(function(e){if("heading"===e.type&&e.depth<=o){var n=function(e){void 0===e&&(e="");var a={};return{str:e=e&&e.replace(/^'/,"").replace(/'$/,"").replace(/(?:^|\s):([\w-]+:?)=?([\w-%]+)?/g,function(e,n,t){return-1===n.indexOf(":")?(a[n]=t&&t.replace(/"/g,"")||!0,""):e}).trim(),config:a}}(e.text),t=n.str,a=n.config;s=a.id?i.toURL(r,{id:c(a.id)}):i.toURL(r,{id:c(l(e.text))}),d[s]={slug:s,title:t,body:""}}else{if(!s)return;d[s]?d[s].body?(e.text=p(e),d[s].body+="\n"+(e.text||"")):(e.text=p(e),d[s].body=d[s].body?d[s].body+e.text:e.text):d[s]={slug:s,title:"",body:""}}}),c.clear(),d}function c(e){var r=[],i=[];Object.keys(h).forEach(function(n){i=i.concat(Object.keys(h[n]).map(function(e){return h[n][e]}))});var o=(e=e.trim()).split(/[\s\-,\\/]+/);1!==o.length&&(o=[].concat(e,o));function n(e){var n=i[e],s=0,c="",d=n.title&&n.title.trim(),p=n.body&&n.body.trim(),t=n.slug||"";if(d&&(o.forEach(function(e){var n,t=new RegExp(e.replace(/[|\\{}()[\]^$+*?.]/g,"\\$&"),"gi"),a=-1;if(n=d?d.search(t):-1,a=p?p.search(t):-1,0<=n||0<=a){s+=0<=n?3:0<=a?2:0,a<0&&(a=0);var r,i=0;i=0==(r=a<11?0:a-10)?70:a+e.length+60,p&&i>p.length&&(i=p.length);var o="..."+l(p).substring(r,i).replace(t,function(e){return''+e+""})+"...";c+=o}}),0\n\n

'+e.title+"

\n

"+e.content+"

\n
\n"}),t.classList.add("show"),a.classList.add("show"),t.innerHTML=s||'

'+m+"

",d.hideOtherSidebarContent&&(r.classList.add("hide"),i.classList.add("hide"))}function a(e){d=e}function o(e,n){var t=n.router.parse().query.s;a(e),Docsify.dom.style("\n.sidebar {\n padding-top: 0;\n}\n\n.search {\n margin-bottom: 20px;\n padding: 6px;\n border-bottom: 1px solid #eee;\n}\n\n.search .input-wrap {\n display: flex;\n align-items: center;\n}\n\n.search .results-panel {\n display: none;\n}\n\n.search .results-panel.show {\n display: block;\n}\n\n.search input {\n outline: none;\n border: none;\n width: 100%;\n padding: 0 7px;\n line-height: 36px;\n font-size: 14px;\n border: 1px solid transparent;\n}\n\n.search input:focus {\n box-shadow: 0 0 5px var(--theme-color, #42b983);\n border: 1px solid var(--theme-color, #42b983);\n}\n\n.search input::-webkit-search-decoration,\n.search input::-webkit-search-cancel-button,\n.search input {\n -webkit-appearance: none;\n -moz-appearance: none;\n appearance: none;\n}\n.search .clear-button {\n cursor: pointer;\n width: 36px;\n text-align: right;\n display: none;\n}\n\n.search .clear-button.show {\n display: block;\n}\n\n.search .clear-button svg {\n transform: scale(.5);\n}\n\n.search h2 {\n font-size: 17px;\n margin: 10px 0;\n}\n\n.search a {\n text-decoration: none;\n color: inherit;\n}\n\n.search .matching-post {\n border-bottom: 1px solid #eee;\n}\n\n.search .matching-post:last-child {\n border-bottom: 0;\n}\n\n.search p {\n font-size: 14px;\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n}\n\n.search p.empty {\n text-align: center;\n}\n\n.app-name.hide, .sidebar-nav.hide {\n display: none;\n}"),function(e){void 0===e&&(e="");var n='
\n \n
\n \n \n \n \n \n
\n
\n
\n ',t=Docsify.dom.create("div",n),a=Docsify.dom.find("aside");Docsify.dom.toggleClass(t,"search"),Docsify.dom.before(a,t)}(t),function(){var e,n=Docsify.dom.find("div.search"),t=Docsify.dom.find(n,"input"),a=Docsify.dom.find(n,".input-wrap");Docsify.dom.on(n,"click",function(e){return-1===["A","H2","P","EM"].indexOf(e.target.tagName)&&e.stopPropagation()}),Docsify.dom.on(t,"input",function(n){clearTimeout(e),e=setTimeout(function(e){return r(n.target.value.trim())},100)}),Docsify.dom.on(a,"click",function(e){"INPUT"!==e.target.tagName&&(t.value="",r())})}(),t&&setTimeout(function(e){return r(t)},500)}function s(e,n){a(e),function(e,n){var t=Docsify.dom.getNode('.search input[type="search"]');if(t)if("string"==typeof e)t.placeholder=e;else{var a=Object.keys(e).filter(function(e){return-1ul:nth-child(1) { 3 | display: none; 4 | } 5 | 6 | #main>ul:nth-child(2) { 7 | display: none; 8 | } 9 | 10 | .markdown-section h1 { 11 | margin: 3rem 0 2rem 0; 12 | } 13 | 14 | .markdown-section h2 { 15 | margin: 2rem 0 1rem; 16 | } 17 | 18 | img, 19 | pre { 20 | border-radius: 8px; 21 | } 22 | 23 | .content, 24 | .sidebar, 25 | .markdown-section, 26 | body, 27 | .search input { 28 | background-color: rgba(243, 242, 238, 1) !important; 29 | } 30 | 31 | @media (min-width:600px) { 32 | .sidebar-toggle { 33 | background-color: #f3f2ee; 34 | } 35 | } 36 | 37 | .docsify-copy-code-button { 38 | background: #f8f8f8 !important; 39 | color: #7a7a7a !important; 40 | } 41 | 42 | body { 43 | /*font-family: Microsoft YaHei, Source Sans Pro, Helvetica Neue, Arial, sans-serif !important;*/ 44 | } 45 | 46 | .markdown-section>p { 47 | font-size: 16px !important; 48 | } 49 | 50 | .markdown-section pre>code { 51 | font-family: Consolas, Roboto Mono, Monaco, courier, monospace !important; 52 | font-size: .9rem !important; 53 | 54 | } 55 | 56 | /*.anchor span { 57 | color: rgb(66, 185, 131); 58 | }*/ 59 | 60 | section.cover h1 { 61 | margin: 0; 62 | } 63 | 64 | body>section>div.cover-main>ul>li>a { 65 | color: #42b983; 66 | } 67 | 68 | .markdown-section img { 69 | box-shadow: 7px 9px 10px #aaa !important; 70 | } 71 | 72 | 73 | pre { 74 | background-color: #f3f2ee !important; 75 | } 76 | 77 | @media (min-width:600px) { 78 | pre code { 79 | /*box-shadow: 2px 1px 20px 2px #aaa;*/ 80 | /*border-radius: 10px !important;*/ 81 | padding-left: 20px !important; 82 | } 83 | } 84 | 85 | @media (max-width:600px) { 86 | pre { 87 | padding-left: 0px !important; 88 | padding-right: 0px !important; 89 | } 90 | } 91 | 92 | .markdown-section pre { 93 | padding-left: 0 !important; 94 | padding-right: 0px !important; 95 | box-shadow: 2px 1px 20px 2px #aaa; 96 | } -------------------------------------------------------------------------------- /cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/lcthw-zh/477a8c2af9ccee0781abe5b7295308c0293866ef/cover.jpg -------------------------------------------------------------------------------- /donors.md: -------------------------------------------------------------------------------- 1 | # 捐赠名单 2 | 3 | 感谢以下童鞋的捐助,你们的慷慨是我继续的动力: 4 | 5 | | donor | value | 6 | | --- | --- | 7 | | jxdwinter | 6.00 | 8 | | 贾**@悠云.com | 20.00 | 9 | | Mr.Moon | 2.00 | -------------------------------------------------------------------------------- /ex0.md: -------------------------------------------------------------------------------- 1 | # 练习0:准备 2 | 3 | > 原文:[Exercise 0: The Setup](http://c.learncodethehardway.org/book/ex0.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 在这一章中,你将为C语言编程配置好你的系统。一个好消息是对于所有使用Linux或者Mac的人,你的系统是为C语言编程而设计的。C语言的创造者也对Unix操作系统的创造做出了贡献,并且Linux和OSX都是基于Unix的。事实上,安装工作会非常简单。 8 | 9 | 对于Windows上的用户,我有一个坏消息:在Windows上学习C非常痛苦。你可以在Windows上编写C代码,这并不是问题。问题是所有的库、函数和工具都和其它的C语言环境有些差异。C来自于Unix,并且和Unix平台配合得比较好。恐怕这是一个你并不能接受的事实。 10 | 11 | 然而你并不需要为此恐慌。我并不是说要完全避免Windows。然而我说的是,如果你打算以最短的时间来学习C,你需要接触Unix并适应它。这同时也对你有帮助,因为懂得一些Unix的知识,也会让你懂得一些C编程的习惯,以及扩充你的技能。 12 | 13 | 这也意味着每个人都需要使用命令行。嗯,就是这样。你将会进入命令行并且键入一些命令。不要为此感到害怕,因为我会告诉你要键入什么,以及结果应该是什么样子,所以你实际上会学到很多东西,同时扩充自己的技能。 14 | 15 | ## Linux 16 | 17 | 在多数Linux系统上你都需要安装一些包。对于基于Debian的系统,例如Ubuntu你需要使用下列命令来安装一些东西: 18 | 19 | ```sh 20 | $ sudo apt-get install build-essential 21 | ``` 22 | 23 | 上面是命令行提示符的一个示例。你需要接触到能输入它的地方,找到你的“终端”程序并且运行它。接着,你会看到一个类似于`$`的Shell提示符,并且你可以在里面键入命令。不要键入`$`,而是它后面的东西。 24 | 25 | 下面是在基于RPM的Linux系统,例如Fedora中执行相同安装工作的方法: 26 | 27 | ```sh 28 | $ su -c "yum groupinstall development-tools" 29 | ``` 30 | 31 | 一旦你运行了它,它会正常工作,你应该能够做本书的第一个练习。如果不能请告诉我。 32 | 33 | ## Mac OSX 34 | 35 | 在 Mac OSX上,安装工作会更简单。首先,你需要从苹果官网下载最新的`XCode`,或者找到你的安装DVD并从中安装。需要下载的文件很大,要花费很长时间,所以我推荐你从DVD安装。同时,上网搜索“安装xcode”来指导你来安装它。 36 | 37 | 一旦你安装完XCode,可能需要重启你的电脑。你可以找到你的终端程序并且将它放到快捷启动栏中。在本书中你会经常用到终端,所以最好将它放到顺手的区域。 38 | 39 | ## Windows 40 | 41 | 对于Windows用户,你需要在虚拟机中安装并运行一个基本的Ubuntu Linux系统,来做本书的练习,并且避免任何Windows中安装的问题。 42 | 43 | > 译者注:如果你的Windows版本是Win10 14316及之后的版本,可以开启Ubuntu子系统来获取Linux环境。 44 | 45 | ## 文本编辑器 46 | 47 | 对于程序员来说,文本编辑器的选择有些困难。对于初学者我推荐他们使用[`Gedit`](http://projects.gnome.org/gedit/),因为它很简单,并且可以用于编写代码。然而,它在特定的国际化环境中并不能正常工作。如果你已经是老司机的话,你可以选用你最喜欢的编辑器。 48 | 49 | 出于这种考虑,我打算让你尝试一些你所在平台上的标准的用于编程的文本编辑器,并且长期使用其中你最喜欢的一个。如果你已经用了Gedit并且很喜欢他,那么就一致用下去。如果你打算尝试一些不同的编辑器,则赶快尝试并选择一个。 50 | 51 | 最重要的事情是,不要纠结于寻找最完美的编辑器。文本编辑器几乎都很奇怪,你只需要选择一个并熟悉它,如果你发现喜欢别的编辑器可以切换到它。不要在挑选它和把它变得更好上面花很多时间。 52 | 53 | 这是亦可以尝试的一些编辑器: 54 | 55 | + Linux和OSX上的[`Gedit`](http://projects.gnome.org/gedit/)。 56 | + OSX上的[`TextWrangler`](http://www.barebones.com/products/textwrangler/)。 57 | + 可以在终端中运行并几乎在任何地方工作的[`Nano`](http://www.nano-editor.org/)。 58 | + [`Emacs`](http://www.gnu.org/software/emacs/)和[`Emacs OSX`](http://emacsformacosx.com/)。需要学习一些东西。 59 | + [`Vim`](http://www.vim.org/)和[`Mac Vim`](http://code.google.com/p/macvim/)。 60 | 61 | 每个人都可能选择一款不同的编辑器,这些只是一部分人所选择的开源编辑器。在找到你最喜欢的那个之前,尝试其中的一些,甚至是一些商业编辑器。 62 | 63 | ## 警告:不要使用IDE 64 | 65 | IDE,或者“集成开发工具”,会使你变笨。如果你想要成为一个好的程序员,它会是最糟糕的工具,因为它隐藏了背后的细节,你的工作是弄清楚背后发生了什么。如果你试着完成一些事情,并且所在平台根据特定的IDE而设计,它们非常有用,但是对于学习C编程(以及许多其它语言),它们没有意义。 66 | 67 | > 注 68 | 69 | > 如果你玩过吉他,你应该知道TAB是什么。但是对于其它人,让我对其做个解释。在音乐中有一种乐谱叫做“五线谱”。它是通用、非常古老的乐谱,以一种通用的方法来记下其它人应该在乐器上弹奏的音符。如果你弹过钢琴,这种乐谱非常易于使用,因为它几乎就是为钢琴和交响乐发明的。 70 | 71 | > 然而吉他是一种奇怪的乐器,它并不能很好地适用这种乐谱。所以吉他手通常使用一种叫做TAB(tablature)的乐谱。它所做的不是告诉你该弹奏哪个音符,而是在当时应该拨哪根弦。你完全可以在不知道所弹奏的单个音符的情况下学习整首乐曲,许多人也都是这么做的,但是如果你想知道你弹的是什么,TAB是毫无意义的。 72 | 73 | > 传统的乐谱可能比TAB更难一些,但是会告诉你如何演奏音乐,而不是如果玩吉他。通过传统的乐谱我可以在钢琴上,或者在贝斯上弹奏相同的曲子。我也可以将它放到电脑中,为它设计全部的曲谱。但是通过TAB我只能在吉他上弹奏。 74 | 75 | > IDE就像是TAB,你可以用它非常快速地编程,但是你只能够用一种语言在一个平台上编程。这就是公司喜欢将它卖给你的原因。它们知道你比较懒,并且由于它只适用于它们自己的平台,他们就将你锁定在了那个平台上。 76 | 77 | > 打破这一循环的办法就是不用IDE学习编程。一个普通的文本编辑器,或者一个程序员使用的文本编辑器,例如Vim或者Emacs,能让你更熟悉代码。这有一点点困难,但是终结果是你将会熟悉任何代码,在任何计算机上,以任何语言,并且懂得背后的原理。 78 | -------------------------------------------------------------------------------- /ex1.md: -------------------------------------------------------------------------------- 1 | # 练习1:启用编译器 2 | 3 | > 原文:[Exercise 1: Dust Off That Compiler](http://c.learncodethehardway.org/book/ex1.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 这是你用C写的第一个简单的程序: 8 | 9 | ```c 10 | int main(int argc, char *argv[]) 11 | { 12 | puts("Hello world."); 13 | 14 | return 0; 15 | } 16 | ``` 17 | 18 | 把它写进 `ex1.c` 并输入: 19 | 20 | ```sh 21 | $ make ex1 22 | cc ex1.c -o ex1 23 | ``` 24 | 25 | 你的编译器可能会使用一个有些不同的命令,但是最后应该会产生一个名为`ex1`的文件,并且你可以运行它。 26 | 27 | ## 你会看到什么 28 | 29 | 现在你可以运行程序并看到输出。 30 | 31 | ```c 32 | $ ./ex1 33 | Hello world. 34 | ``` 35 | 36 | 如果没有,则需要返回去修复它。 37 | 38 | ## 如何使它崩溃 39 | 40 | 在这本书中我会添加一个小节,关于如何使程序崩溃。我会让你对程序做一些奇怪的事情,以奇怪的方式运行,或者修改代码,以便让你看到崩溃和编译器错误。 41 | 42 | 对于这个程序,打开所有编译警告重新构建它: 43 | 44 | ```sh 45 | $ rm ex1 46 | $ CFLAGS="-Wall" make ex1 47 | cc -Wall ex1.c -o ex1 48 | ex1.c: In function 'main': 49 | ex1.c:3: warning: implicit declaration of function 'puts' 50 | $ ./ex1 51 | Hello world. 52 | $ 53 | ``` 54 | 55 | 现在你会得到一个警告,说`puts`函数是隐式声明的。C语言的编译器很智能,它能够理解你想要什么。但是如果可以的话,你应该去除所有编译器警告。把下面一行添加到`ex1.c`文件的最上面,之后重新编译来去除它: 56 | 57 | ```c 58 | #include 59 | ``` 60 | 61 | 现在像刚才一样重新执行make命令,你会看到所有警告都消失了。 62 | 63 | ## 附加题 64 | 65 | + 在你的文本编辑器中打开`ex1`文件,随机修改或删除一部分,之后运行它看看发生了什么。 66 | + 再多打印5行文本或者其它比`"Hello world."`更复杂的东西。 67 | + 执行`man 3 puts`来阅读这个函数和其它函数的文档。 68 | -------------------------------------------------------------------------------- /ex10.md: -------------------------------------------------------------------------------- 1 | # 练习10:字符串数组和循环 2 | 3 | > 原文:[Exercise 10: Arrays Of Strings, Looping](http://c.learncodethehardway.org/book/ex10.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 你现在可以创建不同类型的数组,并且也知道了“字符串”和“字节数组”是相同的东西。接下来,我们要更进一步,创建一个包含字符串的数组。我也会介绍第一个循环结构,`for`循环来帮我们打印出这一新的数据结构。 8 | 9 | 这一章的有趣之处就是你的程序中已经有一个现成的字符串数组,`main`函数参数中的`char *argv[]`。下面这段代码打印出了所有你传入的命令行参数: 10 | 11 | ```c 12 | #include 13 | 14 | int main(int argc, char *argv[]) 15 | { 16 | int i = 0; 17 | 18 | // go through each string in argv 19 | // why am I skipping argv[0]? 20 | for(i = 1; i < argc; i++) { 21 | printf("arg %d: %s\n", i, argv[i]); 22 | } 23 | 24 | // let's make our own array of strings 25 | char *states[] = { 26 | "California", "Oregon", 27 | "Washington", "Texas" 28 | }; 29 | int num_states = 4; 30 | 31 | for(i = 0; i < num_states; i++) { 32 | printf("state %d: %s\n", i, states[i]); 33 | } 34 | 35 | return 0; 36 | } 37 | ``` 38 | 39 | `for`循环的格式是这样的: 40 | 41 | ```c 42 | for(INITIALIZER; TEST; INCREMENTER) { 43 | CODE; 44 | } 45 | ``` 46 | 47 | 下面是`for`循环的工作机制: 48 | 49 | + `INITIALIZER`中是用来初始化循环的代码,这个例子中它是`i = 0`。 50 | + 接下来会检查`TEST`布尔表达式,如果为`false`(0)则跳过`CODE`,不做任何事情。 51 | + 执行`CODE`,做它要做的任何事情。 52 | + 在`CODE`执行之后会执行`INCREMENTER`部分,通常情况会增加一些东西,比如这个例子是`i++`。 53 | + 然后跳到第二步继续执行,直到`TEST`为`false`(0)为止。 54 | 55 | 例子中的`for`循环使用`argc`和`argv`,遍历了命令行参数,像这样: 56 | 57 | + OS将每个命令行参数作为字符串传入`argv`数组,程序名称`./ex10`在下标为0的位置,剩余的参数紧随其后。 58 | + OS将`argc`置为`argv`数组中参数的数量,所以你可以遍历它们而不会越界。要记住如果你提供了一个参数,程序名称是第一个,参数应该在第二个。 59 | + 接下来程序使用`i < argc`测试`i`是否使用`argc`,由于最开始`1 < 2`,测试通过。 60 | + 之后它会执行代码,输出`i`,并且将`i`用做`argv`的下标。 61 | + 然后使用`i++`来运行自增语句,它是`i = i + 1`的便捷形式。 62 | + 程序一直重复上面的步骤,直到`i < argc`值为`false`(0),这时退出循环但程序仍然继续执行。 63 | 64 | ## 你会看到什么 65 | 66 | 你需要用两种方法运行它来玩转这个程序。第一种方法是向命令行参数传递一些东西来设置`argc`和`argv`。第二种是不传入任何参数,于是你可以看到第一次的`for`循环没有被执行,由于`i < argc`值为`false`。 67 | 68 | ## 理解字符串数组 69 | 70 | 你应该可以从这个练习中弄明白,你在C语言中通过混合`char *str = "blah"`和`char str[] = {'b','l','a','h'}`语法构建二维数组来构建字符串数组。第十四行的`char *states[] = {...}`语法就是这样的二维混合结构,其中每个字符串都是数组的一个元素,字符串的每个字符又是字符串的一个元素。 71 | 72 | 73 | 感到困惑吗?多维的概念是很多人从来都不会去想的,所以你应该在纸上构建这一字符串数组: 74 | 75 | + 在纸的左边为每个字符串画一个小方格,带有它们的下标。 76 | + 然后在方格上方写上每个字符的下标。 77 | + 接着将字符串中的字符填充到方格内。 78 | + 画完之后,在纸上模拟代码的执行过程。 79 | 80 | 理解它的另一种方法是在你熟悉的语言,比如Python或Ruby中构建相同的结构。 81 | 82 | ## 如何使它崩溃 83 | 84 | + 使用你喜欢的另一种语言,来写这个程序。传入尽可能多的命令行参数,看看是否能通过传入过多参数使其崩溃。 85 | + 将`i`初始化为0看看会发生什么。是否也需要改动`argc`,不改动的话它能正常工作吗?为什么下标从0开始可以正常工作? 86 | + 将`num_states`改为错误的值使它变大,来看看会发生什么。 87 | 88 | ## 附加题 89 | 90 | + 弄清楚在`for`循环的每一部分你都可以放置什么样的代码。 91 | + 查询如何使用`','`(逗号)字符来在`for`循环的每一部分中,`';'`(分号)之间分隔多条语句。 92 | + 查询`NULL`是什么东西,尝试将它用做`states`的一个元素,看看它会打印出什么。 93 | + 看看你是否能在打印之前将`states`的一个元素赋值给`argv`中的元素,再试试相反的操作。 94 | -------------------------------------------------------------------------------- /ex11.md: -------------------------------------------------------------------------------- 1 | # 练习11:While循环和布尔表达式 2 | 3 | > 原文:[Exercise 11: While-Loop And Boolean Expressions](http://c.learncodethehardway.org/book/ex11.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 你已经初步了解C语言如何处理循环,但是你可能不是很清楚布尔表达式`i < argc`是什么。在学习`while`循环之前,让我先来对布尔表达式做一些解释。 8 | 9 | 在C语言中,实际上没有真正的“布尔”类型,而是用一个整数来代替,0代表`false`,其它值代表`true`。上一个练习中表达式`i < argc`实际上值为1或者0,并不像Python是显式的`Ture`或者`False`。这是C语言更接近计算机工作方式的另一个例子,因为计算机只把值当成数字。 10 | 11 | 现在用`while`循环来实现和上一个练习相同的函数。这会让你使用两种循环,来观察两种循环是什么关系。 12 | 13 | ```c 14 | #include 15 | 16 | int main(int argc, char *argv[]) 17 | { 18 | // go through each string in argv 19 | 20 | int i = 0; 21 | while(i < argc) { 22 | printf("arg %d: %s\n", i, argv[i]); 23 | i++; 24 | } 25 | 26 | // let's make our own array of strings 27 | char *states[] = { 28 | "California", "Oregon", 29 | "Washington", "Texas" 30 | }; 31 | 32 | int num_states = 4; 33 | i = 0; // watch for this 34 | while(i < num_states) { 35 | printf("state %d: %s\n", i, states[i]); 36 | i++; 37 | } 38 | 39 | return 0; 40 | } 41 | ``` 42 | 43 | 你可以看到`while`循环的语法更加简单: 44 | 45 | ```c 46 | while(TEST) { 47 | CODE; 48 | } 49 | ``` 50 | 51 | 只要`TEST`为`true`(非0),就会一直运行`CODE`中的代码。这意味着如果要达到和`for`循环同样的效果,我们需要自己写初始化语句,以及自己来使`i`增加。 52 | 53 | ## 你会看到什么 54 | 55 | 输出基本相同,所以我做了一点修改,使你可以看到它运行的另一种方式。 56 | 57 | ```sh 58 | $ make ex11 59 | cc -Wall -g ex11.c -o ex11 60 | $ ./ex11 61 | arg 0: ./ex11 62 | state 0: California 63 | state 1: Oregon 64 | state 2: Washington 65 | state 3: Texas 66 | $ 67 | $ ./ex11 test it 68 | arg 0: ./ex11 69 | arg 1: test 70 | arg 2: it 71 | state 0: California 72 | state 1: Oregon 73 | state 2: Washington 74 | state 3: Texas 75 | $ 76 | ``` 77 | 78 | ## 如何使它崩溃 79 | 80 | 在你自己的代码中,应优先选择`for`循环而不是`while`循环,因为`for`循环不容易崩溃。下面是几点普遍的原因: 81 | 82 | + 忘记初始化`int i`,使循环发生错误。 83 | + 忘记初始化第二个循环的`i`,于是`i`还保留着第一个循环结束时的值。你的第二个循环可能执行也可能不会执行。 84 | + 忘记在最后执行`i++`自增,你会得到一个“死循环”,它是在你开始编程的第一个或前两个十年中,最可怕的问题之一。 85 | 86 | ## 附加题 87 | 88 | + 让这些循环倒序执行,通过使用`i--`从`argc`开始递减直到0。你可能需要做一些算数操作让数组的下标正常工作。 89 | + 使用`while`循环将`argv`中的值复制到`states`。 90 | + 让这个复制循环不会执行失败,即使`argv`之中有很多元素也不会全部放进`states`。 91 | + 研究你是否真正复制了这些字符串。答案可能会让你感到意外和困惑。 92 | -------------------------------------------------------------------------------- /ex12.md: -------------------------------------------------------------------------------- 1 | # 练习12:If,Else If,Else 2 | 3 | > 原文:[Exercise 12: If, Else-If, Else](http://c.learncodethehardway.org/book/ex12.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | `if`语句是每个编程语言中共有的特性,包括C语言。下面是一段代码,使用了`if`语句来确保只传入了一个或两个命令行参数: 8 | 9 | ```c 10 | #include 11 | 12 | int main(int argc, char *argv[]) 13 | { 14 | int i = 0; 15 | 16 | if(argc == 1) { 17 | printf("You only have one argument. You suck.\n"); 18 | } else if(argc > 1 && argc < 4) { 19 | printf("Here's your arguments:\n"); 20 | 21 | for(i = 0; i < argc; i++) { 22 | printf("%s ", argv[i]); 23 | } 24 | printf("\n"); 25 | } else { 26 | printf("You have too many arguments. You suck.\n"); 27 | } 28 | 29 | return 0; 30 | } 31 | ``` 32 | 33 | `if`语句的格式为: 34 | 35 | ```c 36 | if(TEST) { 37 | CODE; 38 | } else if(TEST) { 39 | CODE; 40 | } else { 41 | CODE; 42 | } 43 | ``` 44 | 45 | 下面是其它语言和C的差异: 46 | 47 | + 像之前提到的那样,`TEST`表达式值为0时为`false`,其它情况为`true`。 48 | + 你需要在`TEST`周围写上圆括号,其它语言可能不用。 49 | + (只有单条语句时)你并不需要使用花括号`{}`来闭合代码,但是这是一种非常不好的格式,不要这么写。花括号让一个分支的代码的开始和结束变得清晰。如果你不把代码写在里面会出现错误。 50 | 51 | 除了上面那些,就和其它语言一样了。`else if`或者`else`的部分并不必须出现。 52 | 53 | ## 你会看到什么 54 | 55 | 这段代码非常易于运行和尝试: 56 | 57 | ```sh 58 | $ make ex12 59 | cc -Wall -g ex12.c -o ex12 60 | $ ./ex12 61 | You only have one argument. You suck. 62 | $ ./ex12 one 63 | Here's your arguments: 64 | ./ex12 one 65 | $ ./ex12 one two 66 | Here's your arguments: 67 | ./ex12 one two 68 | $ ./ex12 one two three 69 | You have too many arguments. You suck. 70 | $ 71 | ``` 72 | 73 | ## 如何使它崩溃 74 | 75 | 使这段代码崩溃并不容易,因为它太简单了。尝试把`if`语句的测试表达式搞乱: 76 | 77 | + 移除`else`部分,使它不能处理边界情况。 78 | + 将`&&`改为`||`,于是你会把“与”操作变成“或”操作,并且看看会发生什么。 79 | 80 | ## 附加题 81 | 82 | + 我已经向你简短地介绍了`&&`,它执行“与”操作。上网搜索与之不同的“布尔运算符”。 83 | + 为这个程序编写更多的测试用例,看看你会写出什么。 84 | + 回到练习10和11,使用`if`语句使循环提前退出。你需要`break`语句来实现它,搜索它的有关资料。 85 | + 第一个判断所输出的话真的正确吗?由于你的“第一个参数”不是用户输入的第一个参数,把它改正。 86 | -------------------------------------------------------------------------------- /ex13.md: -------------------------------------------------------------------------------- 1 | # 练习13:Switch语句 2 | 3 | > 原文:[Exercise 13: Switch Statement](http://c.learncodethehardway.org/book/ex13.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 在其它类似Ruby的语言中,`switch`语句可以处理任意类型的表达式。一些语言比如Python没有`switch`语句,因为带有布尔表达式的`if`语句可以做相同的事情。对于这些语言,`switch`语句比`if`语句更加灵活,然而内部的机制是一样的。 8 | 9 | C中的`switch`语句与它们不同,实际上是一个“跳转表”。你只能够放置结果为整数的表达式,而不是一些随机的布尔表达式,这些整数用于计算从`swicth`顶部到匹配部分的跳转。下面有一段代码,我要分解它来让你理解“跳转表”的概念: 10 | 11 | ```c 12 | #include 13 | 14 | int main(int argc, char *argv[]) 15 | { 16 | if(argc != 2) { 17 | printf("ERROR: You need one argument.\n"); 18 | // this is how you abort a program 19 | return 1; 20 | } 21 | 22 | int i = 0; 23 | for(i = 0; argv[1][i] != '\0'; i++) { 24 | char letter = argv[1][i]; 25 | 26 | switch(letter) { 27 | case 'a': 28 | case 'A': 29 | printf("%d: 'A'\n", i); 30 | break; 31 | 32 | case 'e': 33 | case 'E': 34 | printf("%d: 'E'\n", i); 35 | break; 36 | 37 | case 'i': 38 | case 'I': 39 | printf("%d: 'I'\n", i); 40 | break; 41 | 42 | case 'o': 43 | case 'O': 44 | printf("%d: 'O'\n", i); 45 | break; 46 | 47 | case 'u': 48 | case 'U': 49 | printf("%d: 'U'\n", i); 50 | break; 51 | 52 | case 'y': 53 | case 'Y': 54 | if(i > 2) { 55 | // it's only sometimes Y 56 | printf("%d: 'Y'\n", i); 57 | } 58 | break; 59 | 60 | default: 61 | printf("%d: %c is not a vowel\n", i, letter); 62 | } 63 | } 64 | 65 | return 0; 66 | } 67 | ``` 68 | 69 | 在这个程序中我们接受了单一的命令行参数,并且用一种极其复杂的方式打印出所有原因,来向你演示`switch`语句。下面是`swicth`语句的工作原理: 70 | 71 | + 编译器会标记`swicth`语句的顶端,我们先把它记为地址Y。 72 | + 接着对`switch`中的表达式求值,产生一个数字。在上面的例子中,数字为`argv[1]`中字母的原始的ASCLL码。 73 | + 编译器也会把每个类似`case 'A'`的`case`代码块翻译成这个程序中距离语句顶端的地址,所以`case 'A'`就在`Y + 'A'`处。 74 | + 接着计算是否`Y+letter`位于`switch`语句中,如果距离太远则会将其调整为`Y+Default`。 75 | + 一旦计算出了地址,程序就会“跳”到代码的那个位置并继续执行。这就是一些`case`代码块中有`break`而另外一些没有的原因。 76 | + 如果输出了`'a'`,那它就会跳到`case 'a'`,它里面没有`break`语句,所以它会贯穿执行底下带有代码和`break`的`case 'A'`。 77 | + 最后它执行这段代码,执行`break`完全跳出`switch`语句块。 78 | 79 | > 译者注:更常见的情况是,gcc会在空白处单独构建一张跳转表,各个偏移处存放对应的`case`语句的地址。Y不是`switch`语句的起始地址,而是这张表的起始地址。程序会跳转到`*(Y + 'A')`而不是`Y + 'A'`处。 80 | 81 | 这是对`swicth`语句工作原理的一个深究,然而实际操作中你只需要记住下面几条简单的原则: 82 | 83 | + 总是要包含一个`default:`分支,可以让你接住被忽略的输入。 84 | + 不要允许“贯穿”执行,除非你真的想这么做,这种情况下最好添加一个`//fallthrough`的注释。 85 | + 一定要先编写`case`和`break`,再编写其中的代码。 86 | + 如果能够简化的话,用`if`语句代替。 87 | 88 | ## 你会看到什么 89 | 90 | 下面是我运行它的一个例子,也演示了传入命令行参数的不同方法: 91 | 92 | ```sh 93 | $ make ex13 94 | cc -Wall -g ex13.c -o ex13 95 | $ ./ex13 96 | ERROR: You need one argument. 97 | $ 98 | $ ./ex13 Zed 99 | 0: Z is not a vowel 100 | 1: 'E' 101 | 2: d is not a vowel 102 | $ 103 | $ ./ex13 Zed Shaw 104 | ERROR: You need one argument. 105 | $ 106 | $ ./ex13 "Zed Shaw" 107 | 0: Z is not a vowel 108 | 1: 'E' 109 | 2: d is not a vowel 110 | 3: is not a vowel 111 | 4: S is not a vowel 112 | 5: h is not a vowel 113 | 6: 'A' 114 | 7: w is not a vowel 115 | $ 116 | ``` 117 | 118 | 记住在代码的开始有个`if`语句,当没有提供足够的参数时使用`return 1`返回。返回非0是你提示操作系统程序出错的办法。任何大于0的值都可以在脚本中测试,其它程序会由此知道发生了什么。 119 | 120 | ## 如何使它崩溃 121 | 122 | 破坏一个`switch`语句块太容易了。下面是一些方法,你可以挑一个来用: 123 | 124 | + 忘记写`break`,程序就会运行两个或多个代码块,这些都是你不想运行的。 125 | + 忘记写`default`,程序会在静默中忽略你所忘记的值。 126 | + 无意中将一些带有预料之外的值的变量放入`switch`中,比如带有奇怪的值的`int`。 127 | + 在`switch`中是否未初始化的值。 128 | 129 | 你也可以使用一些别的方法使这个程序崩溃。试着看你能不能自己做到它。 130 | 131 | ## 附加题 132 | 133 | + 编写另一个程序,在字母上做算术运算将它们转换为小写,并且在`switch`中移除所有额外的大写字母。 134 | + 使用`','`(逗号)在`for`循环中初始化`letter`。 135 | + 使用另一个`for`循环来让它处理你传入的所有命令行参数。 136 | + 将这个`switch`语句转为`if`语句,你更喜欢哪个呢? 137 | + 在“Y”的例子中,我在`if`代码块外面写了个`break`。这样会产生什么效果?如果把它移进`if`代码块,会发生什么?自己试着解答它,并证明你是正确的。 138 | -------------------------------------------------------------------------------- /ex14.md: -------------------------------------------------------------------------------- 1 | # 练习14:编写并使用函数 2 | 3 | > 原文:[Exercise 14: Writing And Using Functions](http://c.learncodethehardway.org/book/ex14.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 8 | 到现在为止,你只使用了作为`stdio.h`头文件一部分的函数。在这个练习中你将要编写并使用自己的函数。 9 | 10 | ```c 11 | #include 12 | #include 13 | 14 | // forward declarations 15 | int can_print_it(char ch); 16 | void print_letters(char arg[]); 17 | 18 | void print_arguments(int argc, char *argv[]) 19 | { 20 | int i = 0; 21 | 22 | for(i = 0; i < argc; i++) { 23 | print_letters(argv[i]); 24 | } 25 | } 26 | 27 | void print_letters(char arg[]) 28 | { 29 | int i = 0; 30 | 31 | for(i = 0; arg[i] != '\0'; i++) { 32 | char ch = arg[i]; 33 | 34 | if(can_print_it(ch)) { 35 | printf("'%c' == %d ", ch, ch); 36 | } 37 | } 38 | 39 | printf("\n"); 40 | } 41 | 42 | int can_print_it(char ch) 43 | { 44 | return isalpha(ch) || isblank(ch); 45 | } 46 | 47 | 48 | int main(int argc, char *argv[]) 49 | { 50 | print_arguments(argc, argv); 51 | return 0; 52 | } 53 | ``` 54 | 55 | 在这个例子中你创建了函数来打印任何属于“字母”和“空白”的字符。下面是一个分解: 56 | 57 | ex14.c:2 58 | 59 | 包含了新的头文件,所以你可以访问`isalpha`和`isblank`。 60 | 61 | ex14.c:5-6 62 | 63 | 告诉C语言你稍后会在你的程序中使用一些函数,它们实际上并没有被定义。这叫做“前向声明”,它解决了要想使用函数先要定义的鸡和蛋的问题。 64 | 65 | ex14.c:8-15 66 | 67 | 定义`print_arguments`,它知道如何打印通常由`main`函数获得的相同字符串数组。 68 | 69 | ex14.c:17-30 70 | 71 | 定义了`can_print_it`,它只是简单地将`isalpha(ch) || isblank(ch)`的真值(0或1)返回给它的调用者`print_letters`。 72 | 73 | ex14.c:38-42 74 | 75 | 最后`main`函数简单地调用`print_arguments`,来启动整个函数链。 76 | 77 | 我不应该描述每个函数里都有什么,因为这些都是你之前遇到过的东西。你应该看到的是,我只是像你定义`main`函数一样来定义其它函数。唯一的不同就是如果你打算使用当前文件中没有碰到过的函数,你应该事先告诉C。这就是代码顶部的“前向声明”的作用。 78 | 79 | ## 你会看到什么 80 | 81 | 向这个程序传入不同的命令行参数来玩转它,这样会遍历你函数中的所有路径。这里演示了我和它的交互: 82 | 83 | ```sh 84 | $ make ex14 85 | cc -Wall -g ex14.c -o ex14 86 | 87 | $ ./ex14 88 | 'e' == 101 'x' == 120 89 | 90 | $ ./ex14 hi this is cool 91 | 'e' == 101 'x' == 120 92 | 'h' == 104 'i' == 105 93 | 't' == 116 'h' == 104 'i' == 105 's' == 115 94 | 'i' == 105 's' == 115 95 | 'c' == 99 'o' == 111 'o' == 111 'l' == 108 96 | 97 | $ ./ex14 "I go 3 spaces" 98 | 'e' == 101 'x' == 120 99 | 'I' == 73 ' ' == 32 'g' == 103 'o' == 111 ' ' == 32 ' ' == 32 's' == 115 'p' == 112 'a' == 97 'c' == 99 'e' == 101 's' == 115 100 | $ 101 | ``` 102 | 103 | `isalpha`和`isblank`做了检查提供的字符是否是字母或者空白字符的所有工作。当我最后一次运行时,它打印出除了`'3'`之外的任何东西,因为它是一个数字。 104 | 105 | ## 如何使它崩溃 106 | 107 | 下面是使它崩溃的两种不同的方法: 108 | 109 | + 通过移除前向声明来把编译器搞晕。它会报告`can_print_it` 和 `print_letters`的错误。 110 | + 当你在`main`中调用`print_arguments`时,试着使`argc`加1,于是它会越过`argv`数组的最后一个元素。 111 | 112 | ## 附加题 113 | 114 | + 重新编写这些函数,使它们的数量减少。比如,你真的需要`can_print_it`吗? 115 | + 使用`strlen`函数,让`print_arguments`知道每个字符串参数都有多长,之后将长度传入`print_letters`。然后重写`print_letters`,让它只处理固定的长度,不按照`'\0'`终止符。你需要`#include `来实现它。 116 | + 使用`man`来查询`isalpha`和`isblank`的信息。使用其它相似的函数来只打印出数字或者其它字符。 117 | + 上网浏览不同的人喜欢什么样的函数格式。永远不要使用“K&R”语法,因为它过时了,而且容易使人混乱,但是当你碰到一些人使用这种格式时,要理解代码做了什么。 118 | -------------------------------------------------------------------------------- /ex15.md: -------------------------------------------------------------------------------- 1 | # 练习15:指针,可怕的指针 2 | 3 | > 原文:[Exercise 15: Pointers Dreaded Pointers](http://c.learncodethehardway.org/book/ex15.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 指针是C中的一个著名的谜之特性,我会试着通过教授你一些用于处理它们的词汇,使之去神秘化。指针实际上并不复杂,只不过它们经常以一些奇怪的方式被滥用,这样使它们变得难以使用。如果你避免这些愚蠢的方法来使用指针,你会发现它们难以置信的简单。 8 | 9 | 要想以一种我们可以谈论的方式来讲解指针,我会编写一个无意义的程序,它以三种方式打印了一组人的年龄: 10 | 11 | ```c 12 | #include 13 | 14 | int main(int argc, char *argv[]) 15 | { 16 | // create two arrays we care about 17 | int ages[] = {23, 43, 12, 89, 2}; 18 | char *names[] = { 19 | "Alan", "Frank", 20 | "Mary", "John", "Lisa" 21 | }; 22 | 23 | // safely get the size of ages 24 | int count = sizeof(ages) / sizeof(int); 25 | int i = 0; 26 | 27 | // first way using indexing 28 | for(i = 0; i < count; i++) { 29 | printf("%s has %d years alive.\n", 30 | names[i], ages[i]); 31 | } 32 | 33 | printf("---\n"); 34 | 35 | // setup the pointers to the start of the arrays 36 | int *cur_age = ages; 37 | char **cur_name = names; 38 | 39 | // second way using pointers 40 | for(i = 0; i < count; i++) { 41 | printf("%s is %d years old.\n", 42 | *(cur_name+i), *(cur_age+i)); 43 | } 44 | 45 | printf("---\n"); 46 | 47 | // third way, pointers are just arrays 48 | for(i = 0; i < count; i++) { 49 | printf("%s is %d years old again.\n", 50 | cur_name[i], cur_age[i]); 51 | } 52 | 53 | printf("---\n"); 54 | 55 | // fourth way with pointers in a stupid complex way 56 | for(cur_name = names, cur_age = ages; 57 | (cur_age - ages) < count; 58 | cur_name++, cur_age++) 59 | { 60 | printf("%s lived %d years so far.\n", 61 | *cur_name, *cur_age); 62 | } 63 | 64 | return 0; 65 | } 66 | ``` 67 | 68 | 在解释指针如何工作之前,让我们逐行分解这个程序,这样你可以对发生了什么有所了解。当你浏览这个详细说明时,试着自己在纸上回答问题,之后看看你猜测的结果符合我对指针的描述。 69 | 70 | ex15.c:6-10 71 | 72 | 创建了两个数组,`ages`储存了一些`int`数据,`names`储存了一个字符串数组。 73 | 74 | ex15.c:12-13 75 | 76 | 为之后的`for`循环创建了一些变量。 77 | 78 | ex15.c:16-19 79 | 80 | 你知道这只是遍历了两个数组,并且打印出每个人的年龄。它使用了`i`来对数组索引。 81 | 82 | ex15.c:24 83 | 84 | 创建了一个指向`ages`的指针。注意`int *`创建“指向整数的指针”的指针类型的用法。它很像`char *`,意义是“指向字符的指针”,而且字符串是字符的数组。是不是很相似呢? 85 | 86 | ex15.c:25 87 | 88 | 创建了指向`names`的指针。`char *`已经是“指向`char`的指针”了,所以它只是个字符串。你需要两个层级,因为`names`是二维的,也就是说你需要`char **`作为“指向‘指向字符的指针’的指针”。把它学会,并且自己解释它。 89 | 90 | ex15.c:28-31 91 | 92 | 遍历`ages`和`names`,但是使用“指针加偏移`i`”。`*(cur_name+i)`和`name[i]`是一样的,你应该把它读作“‘`cur_name`指针加`i`’的值”。 93 | 94 | ex15.c:35-39 95 | 96 | 这里展示了访问数组元素的语法和指针是相同的。 97 | 98 | ex15.c:44-50 99 | 100 | 另一个十分愚蠢的循环和其它两个循环做着相同的事情,但是它用了各种指针算术运算来代替: 101 | 102 | ex15.c:44 103 | 104 | 通过将`cur_name`和`cur_age`置为`names`和`age`数组的起始位置来初始化`for`循环。 105 | 106 | ex15.c:45 107 | 108 | `for`循环的测试部分比较`cur_age`指针和`ages`起始位置的距离,为什么可以这样写呢? 109 | 110 | ex15.c:46 111 | 112 | `for`循环的增加部分增加了`cur_name`和`cur_age`的值,这样它们可以只想`names`和`ages`的下一个元素。 113 | 114 | ex15.c:48-49 115 | 116 | `cur_name`和`cur_age`的值现在指向了相应数组中的一个元素,我们我可以通过`*cur_name`和`*cur_age`来打印它们,这里的意思是“`cur_name`和`cur_age`指向的值”。 117 | 118 | 这个看似简单的程序却包含了大量的信息,其目的是在我向你讲解之前尝试让你自己弄清楚指针。直到你写下你认为指针做了什么之前,不要往下阅读。 119 | 120 | ## 你会看到什么 121 | 122 | 在你运行这个程序之后,尝试根据打印出的每一行追溯到代码中产生它们的那一行。在必要情况下,修改`printf`调用来确认你得到了正确的行号: 123 | 124 | ```sh 125 | $ make ex15 126 | cc -Wall -g ex15.c -o ex15 127 | $ ./ex15 128 | Alan has 23 years alive. 129 | Frank has 43 years alive. 130 | Mary has 12 years alive. 131 | John has 89 years alive. 132 | Lisa has 2 years alive. 133 | --- 134 | Alan is 23 years old. 135 | Frank is 43 years old. 136 | Mary is 12 years old. 137 | John is 89 years old. 138 | Lisa is 2 years old. 139 | --- 140 | Alan is 23 years old again. 141 | Frank is 43 years old again. 142 | Mary is 12 years old again. 143 | John is 89 years old again. 144 | Lisa is 2 years old again. 145 | --- 146 | Alan lived 23 years so far. 147 | Frank lived 43 years so far. 148 | Mary lived 12 years so far. 149 | John lived 89 years so far. 150 | Lisa lived 2 years so far. 151 | $ 152 | ``` 153 | 154 | ## 解释指针 155 | 156 | 当你写下一些类似`ages[i]`的东西时,你实际上在用`i`中的数字来索引`ages`。如果`i`的值为0,那么就等同于写下`ages[0]`。我们把`i`叫做下标,因为它是`ages`中的一个位置。它也能称为地址,这是“我想要`ages`位于地址`i`处的整数”中的说法。 157 | 158 | 如果`i`是个下标,那么`ages`又是什么?对C来说`ages`是在计算机中那些整数的起始位置。当然它也是个地址,C编译器会把任何你键入`ages`的地方替换为数组中第一个整数的地址。另一个理解它的办法就是把`ages`当作“数组内部第一个整数的地址”,但是它是整个计算机中的地址,而不是像`i`一样的`ages`中的地址。`ages`数组的名字在计算机中实际上是个地址。 159 | 160 | 这就产生了一种特定的实现:C把你的计算机看成一个庞大的字节数组。显然这样不会有什么用处,于是C就在它的基础上构建出类型和大小的概念。你已经在前面的练习中看到了它是如何工作的,但现在你可以开始了解C对你的数组做了下面一些事情: 161 | 162 | + 在你的计算机中开辟一块内存。 163 | + 将`ages`这个名字“指向”它的起始位置。 164 | + 通过选取`ages`作为基址,并且获取位置为`i`的元素,来对内存块进行索引。 165 | + 将`ages+i`处的元素转换成大小正确的有效的`int`,这样就返回了你想要的结果:下标`i`处的`int`。 166 | 167 | 如果你可以选取`ages`作为基址,之后加上比如`i`的另一个地址,你是否就能随时构造出指向这一地址的指针呢?是的,这种东西就叫做指针。这也是`cur_age`和`cur_name`所做的事情,它们是指向计算机中这一位置的变量,`ages`和`names`就处于这一位置。之后,示例程序移动它们,或者做了一些算数运算,来从内存中获取值。在其中一个实例中,只是简单地将`cur_age`加上`i`,这样等同于`array[i]`。在最后一个`for`循环中,这两个指针在没有`i`辅助的情况下自己移动,被当做数组基址和整数偏移合并到一起的组合。 168 | 169 | 指针仅仅是指向计算机中的某个地址,并带有类型限定符,所以你可以通过它得到正确大小的数据。它类似于将`ages`和`i`组合为一个数据类型的东西。C了解指针指向什么地方,所指向的数据类型,这些类型的大小,以及如何为你获取数据。你可以像`i`一样增加它们,减少它们,对他们做加减运算。然而它们也像是`ages`,你可以通过它获取值,放入新的值,或执行全部的数组操作。 170 | 171 | 指针的用途就是让你手动对内存块进行索引,一些情况下数组并不能做到。绝大多数情况中,你可能打算使用数组,但是一些处理原始内存块的情况,是指针的用武之地。指针向你提供了原始的、直接的内存块访问途径,让你能够处理它们。 172 | 173 | 在这一阶段需要掌握的最后一件事,就是你可以对数组和指针操作混用它们绝大多数的语法。你可以对一个指针使用数组的语法来访问指向的东西,也可以对数组的名字做指针的算数运算。 174 | 175 | ## 实用的指针用法 176 | 177 | 你可以用指针做下面四个最基本的操作: 178 | 179 | + 向OS申请一块内存,并且用指针处理它。这包括字符串,和一些你从来没见过的东西,比如结构体。 180 | + 通过指针向函数传递大块的内存(比如很大的结构体),这样不必把全部数据都传递进去。 181 | + 获取函数的地址用于动态调用。 182 | + 对一块内存做复杂的搜索,比如,转换网络套接字中的字节,或者解析文件。 183 | 184 | 对于你看到的其它所有情况,实际上应当使用数组。在早期,由于编译器不擅长优化数组,人们使用指针来加速它们的程序。然而,现在访问数组和指针的语法都会翻译成相同的机器码,并且表现一致。由此,你应该每次尽可能使用数组,并且按需将指针用作提升性能的手段。 185 | 186 | ## 指针词库 187 | 188 | 现在我打算向你提供一个词库,用于读写指针。当你遇到复杂的指针语句时,试着参考它并且逐字拆分语句(或者不要使用这个语句,因为有可能并不好): 189 | 190 | `type *ptr` 191 | 192 | `type`类型的指针,名为`ptr`。 193 | 194 | `*ptr` 195 | 196 | `ptr`所指向位置的值。 197 | 198 | `*(ptr + i)` 199 | 200 | (`ptr`所指向位置加上`i`)的值。 201 | 202 | > 译者注:以字节为单位的话,应该是`ptr`所指向的位置再加上`sizeof(type) * i`。 203 | 204 | `&thing` 205 | 206 | `thing`的地址。 207 | 208 | `type *ptr = &thing` 209 | 210 | 名为`ptr`,`type`类型的指针,值设置为`thing`的地址。 211 | 212 | `ptr++` 213 | 214 | 自增`ptr`指向的位置。 215 | 216 | 我们将会使用这份简单的词库来拆解这本书中所有的指针用例。 217 | 218 | ## 指针并不是数组 219 | 220 | 无论怎么样,你都不应该把指针和数组混为一谈。它们并不是相同的东西,即使C让你以一些相同的方法来使用它们。例如,如果你访问上面代码中的`sizeof(cur_age)`,你会得到指针的大小,而不是它指向数组的大小。如果你想得到整个数组的大小,你应该使用数组的名称`age`,就像第12行那样。 221 | 222 | > 译者注,除了`sizeof`、`&`操作和声明之外,数组名称都会被编译器推导为指向其首个元素的指针。对于这些情况,不要用“是”这个词,而是要用“推导”。 223 | 224 | ## 如何使它崩溃 225 | 226 | 你可以通过将指针指向错误的位置来使程序崩溃: 227 | 228 | + 试着将`cur_age`指向`names`。可以需要C风格转换来强制执行,试着查阅相关资料把它弄明白。 229 | + 在最后的`for`循环中,用一些古怪的方式使计算发生错误。 230 | + 试着重写循环,让它们从数组的最后一个元素开始遍历到首个元素。这比看上去要困难。 231 | 232 | ## 附加题 233 | 234 | + 使用访问指针的方式重写所有使用数组的地方。 235 | + 使用访问数组的方式重写所有使用指针的地方。 236 | + 在其它程序中使用指针来代替数组访问。 237 | + 使用指针来处理命令行参数,就像处理`names`那样。 238 | + 将获取值和获取地址组合到一起。 239 | + 在程序末尾添加一个`for`循环,打印出这些指针所指向的地址。你需要在`printf`中使用`%p`。 240 | + 对于每一种打印数组的方法,使用函数来重写程序。试着向函数传递指针来处理数据。记住你可以声明接受指针的函数,但是可以像数组那样用它。 241 | + 将`for`循环改为`while`循环,并且观察对于每种指针用法哪种循环更方便。 242 | -------------------------------------------------------------------------------- /ex16.md: -------------------------------------------------------------------------------- 1 | # 练习16:结构体和指向它们的指针 2 | 3 | > 原文:[Exercise 16: Structs And Pointers To Them](http://c.learncodethehardway.org/book/ex16.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 在这个练习中你将会学到如何创建`struct`,将一个指针指向它们,以及使用它们来理解内存的内部结构。我也会借助上一节课中的指针知识,并且让你使用`malloc`从原始内存中构造这些结构体。 8 | 9 | 像往常一样,下面是我们将要讨论的程序,你应该把它打下来并且使它正常工作: 10 | 11 | ```c 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | struct Person { 18 | char *name; 19 | int age; 20 | int height; 21 | int weight; 22 | }; 23 | 24 | struct Person *Person_create(char *name, int age, int height, int weight) 25 | { 26 | struct Person *who = malloc(sizeof(struct Person)); 27 | assert(who != NULL); 28 | 29 | who->name = strdup(name); 30 | who->age = age; 31 | who->height = height; 32 | who->weight = weight; 33 | 34 | return who; 35 | } 36 | 37 | void Person_destroy(struct Person *who) 38 | { 39 | assert(who != NULL); 40 | 41 | free(who->name); 42 | free(who); 43 | } 44 | 45 | void Person_print(struct Person *who) 46 | { 47 | printf("Name: %s\n", who->name); 48 | printf("\tAge: %d\n", who->age); 49 | printf("\tHeight: %d\n", who->height); 50 | printf("\tWeight: %d\n", who->weight); 51 | } 52 | 53 | int main(int argc, char *argv[]) 54 | { 55 | // make two people structures 56 | struct Person *joe = Person_create( 57 | "Joe Alex", 32, 64, 140); 58 | 59 | struct Person *frank = Person_create( 60 | "Frank Blank", 20, 72, 180); 61 | 62 | // print them out and where they are in memory 63 | printf("Joe is at memory location %p:\n", joe); 64 | Person_print(joe); 65 | 66 | printf("Frank is at memory location %p:\n", frank); 67 | Person_print(frank); 68 | 69 | // make everyone age 20 years and print them again 70 | joe->age += 20; 71 | joe->height -= 2; 72 | joe->weight += 40; 73 | Person_print(joe); 74 | 75 | frank->age += 20; 76 | frank->weight += 20; 77 | Person_print(frank); 78 | 79 | // destroy them both so we clean up 80 | Person_destroy(joe); 81 | Person_destroy(frank); 82 | 83 | return 0; 84 | } 85 | ``` 86 | 87 | 我打算使用一种和之前不一样的方法来描述这段程序。我并不会对程序做逐行的拆分,而是由你自己写出来。我会基于程序所包含的部分来给你提示,你的任务就是写出每行是干什么的。 88 | 89 | 包含(`include`) 90 | 91 | 我包含了一些新的头文件,来访问一些新的函数。每个头文件都提供了什么东西? 92 | 93 | `struct Person` 94 | 95 | 这就是我创建结构体的地方了,结构体含有四个成员来描述一个人。最后我们得到了一个复合类型,让我们通过一个名字来整体引用这些成员,或它们的每一个。这就像数据库表中的一行或者OOP语言中的一个类那样。 96 | 97 | `Pearson_create` 函数 98 | 99 | 我需要一个方法来创建这些结构体,于是我定义了一个函数来实现。下面是这个函数做的几件重要的事情: 100 | 101 | + 使用用于内存分配的`malloc`来向OS申请一块原始的内存。 102 | + 向`malloc`传递`sizeof(struct Person)`参数,它计算结构体的大小,包含其中的所有成员。 103 | + 使用了`assert`来确保从`malloc`得到一块有效的内存。有一个特殊的常量叫做`NULL`,表示“未设置或无效的指针”。这个`assert`大致检查了`malloc`是否会返回`NULL`。 104 | + 使用`x->y`语法来初始化`struct Person`的每个成员,它指明了所初始化的成员。 105 | + 使用`strdup`来复制字符串`name`,是为了确保结构体真正拥有它。`strdup`的行为实际上类似`malloc`但是它同时会将原来的字符串复制到新创建的内存。 106 | 107 | > 译者注:`x->y`是`(*x).y`的简写。 108 | 109 | `Person_destroy` 函数 110 | 111 | 如果定义了创建函数,那么一定需要一个销毁函数,它会销毁`Person`结构体。我再一次使用了`assert`来确保不会得到错误的输入。接着我使用了`free`函数来交还通过`malloc`和`strdup`得到的内存。如果你不这么做则会出现“内存泄露”。 112 | 113 | > 译者注:不想显式释放内存又能避免内存泄露的办法是引入`libGC`库。你需要把所有的`malloc`换成`GC_malloc`,然后把所有的`free`删掉。 114 | 115 | `Person_print` 函数 116 | 117 | 接下来我需要一个方法来打印出人们的信息,这就是这个函数所做的事情。它用了相同的`x->y`语法从结构体中获取成员来打印。 118 | 119 | `main` 函数 120 | 121 | 我在`main`函数中使用了所有前面的函数和`struct Person`来执行下面的事情: 122 | 123 | + 创建了两个人:`joe`和`frank`。 124 | + 把它们打印出来,注意我用了`%p`占位符,所以你可以看到程序实际上把结构体放到了哪里。 125 | + 把它们的年龄增加20岁,同时增加它们的体重。 126 | + 之后打印出每个人。 127 | + 最后销毁结构体,以正确的方式清理它们。 128 | 129 | 请仔细阅读上面的描述,然后做下面的事情: 130 | 131 | + 查询每个你不了解的函数或头文件。记住你通常可以使用`man 2 function`或者`man 3 function`来让它告诉你。你也可以上网搜索资料。 132 | + 在每一行上方编写注释,写下这一行代码做了什么。 133 | + 跟踪每一个函数调用和变量,你会知道它在程序中是在哪里出现的。 134 | + 同时也查询你不清楚的任何符号。 135 | 136 | ## 你会看到什么 137 | 138 | 在你使用描述性注释扩展程序之后,要确保它实际上能够运行,并且产生下面的输出: 139 | 140 | ```sh 141 | $ make ex16 142 | cc -Wall -g ex16.c -o ex16 143 | 144 | $ ./ex16 145 | Joe is at memory location 0xeba010: 146 | Name: Joe Alex 147 | Age: 32 148 | Height: 64 149 | Weight: 140 150 | Frank is at memory location 0xeba050: 151 | Name: Frank Blank 152 | Age: 20 153 | Height: 72 154 | Weight: 180 155 | Name: Joe Alex 156 | Age: 52 157 | Height: 62 158 | Weight: 180 159 | Name: Frank Blank 160 | Age: 40 161 | Height: 72 162 | Weight: 200 163 | ``` 164 | 165 | ## 解释结构体 166 | 167 | 如果你完成了我要求的任务,你应该理解了结构体。不过让我来做一个明确的解释,确保你真正理解了它。 168 | 169 | C中的结构体是其它数据类型(变量)的一个集合,它们储存在一块内存中,然而你可以通过独立的名字来访问每个变量。它们就类似于数据库表中的一行记录,或者面向对象语言中的一个非常简单的类。让我们以这种方式来理解它: 170 | 171 | 172 | + 在上面的代码中,你创建了一个结构体,它们的成员用于描述一个人:名称、年龄、体重、身高。 173 | + 每个成员都有一个类型,比如是`int`。 174 | + C会将它们打包到一起,于是它们可以用单个的结构体来存放。 175 | + `struct Person`是一个复合类型,这意味着你可以在同种表达式中将其引用为其它的数据类型。 176 | + 你可以将这一紧密的组合传递给其它函数,就像`Person_print`那样。 177 | + 如果结构体是指针的形式,接着你可以使用`x->y`通过它们的名字来访问结构体中独立的部分。 178 | + 还有一种创建结构体的方法,不需要指针,通过`x.y`来访问。你将会在附加题里面见到它。 179 | 180 | 如果你不使用结构体,则需要自己计算出大小、打包以及定位出指定内容的内存片位置。实际上,在大多数早期(甚至现在的一些)的汇编代码中,这就是唯一的方式。在C中你就可以让C来处理这些复合数据类型的内存构造,并且专注于和它们交互。 181 | 182 | ## 如何使它崩溃 183 | 184 | 使这个程序崩溃的办法涉及到使用指针和`malloc`系统的方法: 185 | 186 | + 试着传递`NULL`给`Person_destroy`来看看会发生什么。如果它没有崩溃,你必须移除Makefile的`CFLAGS`中的`-g`选项。 187 | + 在结尾处忘记调用`Person_destroy`,在`Valgrind`下运行程序,你会看到它报告出你忘记释放内存。弄清楚你应该向`valgrind`传递什么参数来让它向你报告内存如何泄露。 188 | + 忘记在`Person_destroy`中释放`who->name`,并且对比两次的输出。同时,使用正确的选项来让`Valgrind`告诉你哪里错了。 189 | + 这一次,向`Person_print`传递`NULL`,并且观察`Valgrind`会输出什么。 190 | + 你应该明白了`NULL`是个使程序崩溃的快速方法。 191 | 192 | ## 附加题 193 | 194 | 在这个练习的附加题中我想让你尝试一些有难度的东西:将这个程序改为不用指针和`malloc`的版本。这可能很困难,所以你需要研究下面这些东西: 195 | 196 | + 如何在栈上创建结构体,就像你创建任何其它变量那样。 197 | + 如何使用`x.y`而不是`x->y`来初始化结构体。 198 | + 如何不使用指针来将结构体传给其它函数。 199 | -------------------------------------------------------------------------------- /ex18.md: -------------------------------------------------------------------------------- 1 | # 练习18:函数指针 2 | 3 | > 原文:[Exercise 18: Pointers To Functions](http://c.learncodethehardway.org/book/ex18.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 函数在C中实际上只是指向程序中某一个代码存在位置的指针。就像你创建过的结构体指针、字符串和数组那样,你也可以创建指向函数的指针。函数指针的主要用途是向其他函数传递“回调”,或者模拟类和对象。在这个练习中我们会创建一些回调,并且下一节我们会制作一个简单的对象系统。 8 | 9 | 函数指针的格式类似这样: 10 | 11 | ```c 12 | int (*POINTER_NAME)(int a, int b) 13 | ``` 14 | 15 | 记住如何编写它的一个方法是: 16 | 17 | + 编写一个普通的函数声明:`int callme(int a, int b)` 18 | + 将函数用指针语法包装:`int (*callme)(int a, int b)` 19 | + 将名称改成指针名称:`int (*compare_cb)(int a, int b)` 20 | 21 | 这个方法的关键是,当你完成这些之后,指针的变量名称为`compare_cb`,而你可以将它用作函数。这类似于指向数组的指针可以表示所指向的数组。指向函数的指针也可以用作表示所指向的函数,只不过是不同的名字。 22 | 23 | ```c 24 | int (*tester)(int a, int b) = sorted_order; 25 | printf("TEST: %d is same as %d\n", tester(2, 3), sorted_order(2, 3)); 26 | ``` 27 | 28 | 即使是对于返回指针的函数指针,上述方法依然有效: 29 | 30 | + 编写:`char *make_coolness(int awesome_levels)` 31 | + 包装:`char *(*make_coolness)(int awesome_levels)` 32 | + 重命名:`char *(*coolness_cb)(int awesome_levels)` 33 | 34 | 需要解决的下一个问题是使用函数指针向其它函数提供参数比较困难,比如当你打算向其它函数传递回调函数的时候。解决方法是使用`typedef`,它是C的一个关键字,可以给其它更复杂的类型起个新的名字。你需要记住的事情是,将`typedef`添加到相同的指针语法之前,然后你就可以将那个名字用作类型了。我使用下面的代码来演示这一特性: 35 | 36 | ```c 37 | #include 38 | #include 39 | #include 40 | #include 41 | 42 | /** Our old friend die from ex17. */ 43 | void die(const char *message) 44 | { 45 | if(errno) { 46 | perror(message); 47 | } else { 48 | printf("ERROR: %s\n", message); 49 | } 50 | 51 | exit(1); 52 | } 53 | 54 | // a typedef creates a fake type, in this 55 | // case for a function pointer 56 | typedef int (*compare_cb)(int a, int b); 57 | 58 | /** 59 | * A classic bubble sort function that uses the 60 | * compare_cb to do the sorting. 61 | */ 62 | int *bubble_sort(int *numbers, int count, compare_cb cmp) 63 | { 64 | int temp = 0; 65 | int i = 0; 66 | int j = 0; 67 | int *target = malloc(count * sizeof(int)); 68 | 69 | if(!target) die("Memory error."); 70 | 71 | memcpy(target, numbers, count * sizeof(int)); 72 | 73 | for(i = 0; i < count; i++) { 74 | for(j = 0; j < count - 1; j++) { 75 | if(cmp(target[j], target[j+1]) > 0) { 76 | temp = target[j+1]; 77 | target[j+1] = target[j]; 78 | target[j] = temp; 79 | } 80 | } 81 | } 82 | 83 | return target; 84 | } 85 | 86 | int sorted_order(int a, int b) 87 | { 88 | return a - b; 89 | } 90 | 91 | int reverse_order(int a, int b) 92 | { 93 | return b - a; 94 | } 95 | 96 | int strange_order(int a, int b) 97 | { 98 | if(a == 0 || b == 0) { 99 | return 0; 100 | } else { 101 | return a % b; 102 | } 103 | } 104 | 105 | /** 106 | * Used to test that we are sorting things correctly 107 | * by doing the sort and printing it out. 108 | */ 109 | void test_sorting(int *numbers, int count, compare_cb cmp) 110 | { 111 | int i = 0; 112 | int *sorted = bubble_sort(numbers, count, cmp); 113 | 114 | if(!sorted) die("Failed to sort as requested."); 115 | 116 | for(i = 0; i < count; i++) { 117 | printf("%d ", sorted[i]); 118 | } 119 | printf("\n"); 120 | 121 | free(sorted); 122 | } 123 | 124 | 125 | int main(int argc, char *argv[]) 126 | { 127 | if(argc < 2) die("USAGE: ex18 4 3 1 5 6"); 128 | 129 | int count = argc - 1; 130 | int i = 0; 131 | char **inputs = argv + 1; 132 | 133 | int *numbers = malloc(count * sizeof(int)); 134 | if(!numbers) die("Memory error."); 135 | 136 | for(i = 0; i < count; i++) { 137 | numbers[i] = atoi(inputs[i]); 138 | } 139 | 140 | test_sorting(numbers, count, sorted_order); 141 | test_sorting(numbers, count, reverse_order); 142 | test_sorting(numbers, count, strange_order); 143 | 144 | free(numbers); 145 | 146 | return 0; 147 | } 148 | ``` 149 | 150 | 在这段程序中,你将创建动态排序的算法,它会使用比较回调对整数数组排序。下面是这个程序的分解,你应该能够清晰地理解它。 151 | 152 | ex18.c:1~6 153 | 154 | 通常的包含,用于所调用的所有函数。 155 | 156 | ex18.c:7~17 157 | 158 | 这就是之前练习的`die`函数,我将它用于错误检查。 159 | 160 | ex18.c:21 161 | 162 | 这是使用`typedef`的地方,在后面我像`int`或`char`类型那样,在`bubble_sort`和`test_sorting`中使用了`compare_cb`。 163 | 164 | ex18.c:27~49 165 | 166 | 一个冒泡排序的实现,它是整数排序的一种不高效的方法。这个函数包含了: 167 | 168 | ex18.c:27 169 | 170 | 这里是将`typedef`用于` compare_cb`作为`cmp`最后一个参数的地方。现在它是一个会返回两个整数比较结果用于排序的函数。 171 | 172 | ex18.c:29~34 173 | 174 | 栈上变量的通常创建语句,前面是使用`malloc`创建的堆上整数数组。确保你理解了`count * sizeof(int)`做了什么。 175 | 176 | ex18.c:38 177 | 178 | 冒泡排序的外循环。 179 | 180 | ex18.c:39 181 | 182 | 冒泡排序的内循环。 183 | 184 | ex18.c:40 185 | 186 | 现在我调用了`cmp`回调,就像一个普通函数那样,但是不通过预先定义好的函数名,而是一个指向它的指针。调用者可以像它传递任何参数,只要这些参数符合`compare_cb` `typedef`的签名。 187 | 188 | ex18.c:41-43 189 | 190 | 冒泡排序所需的实际交换操作。 191 | 192 | ex18.c:48 193 | 194 | 最后返回新创建和排序过的结果数据`target`。 195 | 196 | ex18.c:51-68 197 | 198 | `compare_cb`函数类型三个不同版本,它们需要和我们所创建的`typedef`具有相同的定义。否则C编辑器会报错说类型不匹配。 199 | 200 | ex18.c:74-87 201 | 202 | 这是`bubble_sort`函数的测试。你可以看到我同时将`compare_cb`传给了`bubble_sort`来演示它是如何像其它指针一样传递的。 203 | 204 | ex18.c:90-103 205 | 206 | 一个简单的主函数,基于你通过命令行传递进来的整数,创建了一个数组。然后调用了`test_sorting`函数。 207 | 208 | ex18.c:105-107 209 | 210 | 最后,你会看到`compare_cb`函数指针的`typedef`是如何使用的。我仅仅传递了`sorted_order`、`reverse_order`和`strange_order`的名字作为函数来调用`test_sorting`。C编译器会找到这些函数的地址,并且生成指针用于`test_sorting`。如果你看一眼`test_sorting`你会发现它把这些函数传给了`bubble_sort`,并不关心它们是做了什么。只要符合`compare_cb`原型的东西都有效。 211 | 212 | ex18.c:109 213 | 214 | 我们在最后释放了我们创建的整数数组。 215 | 216 | ## 你会看到什么 217 | 218 | 运行这个程序非常简单,但是你要尝试不同的数字组合,甚至要尝试输入非数字来看看它做了什么: 219 | 220 | ```sh 221 | $ make ex18 222 | cc -Wall -g ex18.c -o ex18 223 | $ ./ex18 4 1 7 3 2 0 8 224 | 0 1 2 3 4 7 8 225 | 8 7 4 3 2 1 0 226 | 3 4 2 7 1 0 8 227 | $ 228 | ``` 229 | 230 | ## 如何使它崩溃 231 | 232 | 我打算让你做一些奇怪的事情来使它崩溃,这些函数指针都是类似于其它指针的指针,他们都指向内存的一块区域。C中可以将一种指针的指针转换为另一种,以便以不同方式处理数据。这些通常是不必要的,但是为了想你展示如何侵入你的电脑,我希望你把这段代码添加在`test_sorting`下面: 233 | 234 | ```c 235 | unsigned char *data = (unsigned char *)cmp; 236 | 237 | for(i = 0; i < 25; i++) { 238 | printf("%02x:", data[i]); 239 | } 240 | 241 | printf("\n"); 242 | ``` 243 | 244 | 这个循环将你的函数转换成字符串,并且打印出来它的内容。这并不会中断你的程序,除非CPU和OS在执行过程中遇到了问题。在它打印排序过的数组之后,你所看到的是一个十六进制数字的字符串: 245 | 246 | ``` 247 | 55:48:89:e5:89:7d:fc:89:75:f8:8b:55:fc:8b:45:f8:29:d0:c9:c3:55:48:89:e5:89: 248 | ``` 249 | 250 | 这就应该是函数的原始的汇编字节码了,你应该能看到它们有相同的起始和不同的结尾。也有可能这个循环并没有获得函数的全部,或者获得了过多的代码而跑到程序的另外一片空间。这些不通过更多分析是不可能知道的。 251 | 252 | ## 附加题 253 | 254 | + 用十六进制编辑器打开`ex18`,接着找到函数起始处的十六进制代码序列,看看是否能在原始程序中找到函数。 255 | + 在你的十六进制编辑器中找到更多随机出现的东西并修改它们。重新运行你的程序看看发生了什么。字符串是你最容易修改的东西。 256 | + 将错误的函数传给`compare_cb`,并看看C编辑器会报告什么错误。 257 | + 将`NULL`传给它,看看程序中会发生什么。然后运行`Valgrind`来看看它会报告什么。 258 | + 编写另一个排序算法,修改`test_sorting`使它接收任意的排序函数和排序函数的比较回调。并使用它来测试两种排序算法。 259 | -------------------------------------------------------------------------------- /ex2.md: -------------------------------------------------------------------------------- 1 | # 练习2:用Make来代替Python 2 | 3 | > 原文:[Exercise 2: Make Is Your Python Now](http://c.learncodethehardway.org/book/ex2.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 在Python中,你仅仅需要输入`python`,就可以运行你想要运行的代码。Python的解释器会运行它们,并且在运行中导入它所需的库和其它东西。C是完全不同的东西,你需要事先编译你的源文件,并且手动将它们整合为一个可以自己运行的二进制文件。手动来做这些事情很痛苦,在上一个练习中只需要运行`make`就能完成。 8 | 9 | 这个练习是GNU make 的速成课,由于你在学C语言,所以你就必须掌握它。Make 将贯穿剩下的课程,等效于Python(命令)。它会构建源码,执行测试,设置一些选项以及为你做所有Python通常会做的事情。 10 | 11 | 有所不同的是,我会向你展示一些更智能化的Makefile魔法,你不需要指出你的C程序的每一个愚蠢的细节来构建它。我不会在练习中那样做,但是你需要先用一段时间的“低级 make”,我才能向你演示“大师级的make”。 12 | 13 | ## 使用 Make 14 | 15 | 使用make的第一阶段就是用它已知的方式来构建程序。Make预置了一些知识,来从其它文件构建多种文件。上一个练习中,你已经使用像下面的命令来这样做了: 16 | 17 | ```sh 18 | $ make ex1 19 | # or this one too 20 | $ CFLAGS="-Wall" make ex1 21 | ``` 22 | 23 | 第一个命令中你告诉make,“我想创建名为ex1的文件”。于是Make执行下面的动作: 24 | 25 | + 文件`ex1`存在吗? 26 | + 没有。好的,有没有其他文件以`ex1`开头? 27 | + 有,叫做`ex1.c`。我知道如何构建`.c`文件吗? 28 | + 是的,我会运行命令`cc ex1.c -o ex1`来构建它。 29 | + 我将使用`cc`从`ex1.c`文件来为你构建`ex1`。 30 | 31 | 上面列出的第二条命令是一种向make命令传递“修改器”的途径。如果你不熟悉Unix shell如何工作,你可以创建这些“环境变量”,它们会在程序运行时生效。有时你会用一条类似于`export CFLAGS="-Wall"`的命令来执行相同的事情,取决于你所用的shell。然而你可以仅仅把它们放到你想执行的命令前面,于是环境变量只会在程序运行时有效。 32 | 33 | 在这个例子中我执行了`CFLAGS="-Wall" make ex1`,所以它会给make通常使用的`cc`命令添加`-Wall`选项。这行命令告诉`cc`编译器要报告所有的警告(然而实际上不可能报告所有警告)。 34 | 35 | 实际上你可以深入探索使用make的上述方法,但是先让我们来看看`Makefile`,以便让你对make了解得更多一点。首先,创建文件并写入以下内容: 36 | 37 | ```make 38 | CFLAGS=-Wall -g 39 | 40 | clean: 41 | rm -f ex1 42 | ``` 43 | 44 | 45 | 将文件在你的当前文件夹上保存为`Makefile`。Make会自动假设当前文件夹中有一个叫做`Makefile`的文件,并且会执行它。此外,一定要注意:确保你只输入了 TAB 字符,而不是空格和 TAB 的混合。 46 | 47 | > 译者注:上述代码中第四行`rm`前面是一个 TAB ,而不是多个等量的空格。 48 | 49 | `Makefile`向你展示了make的一些新功能。首先我们在文件中设置`CFLAGS`,所以之后就不用再设置了。并且,我们添加了`-g`标识来获取调试信息。接着我们写了一个叫做`clean`的部分,它告诉make如何清理我们的小项目。 50 | 51 | 确保它和你的`ex1.c`文件在相同的目录中,之后运行以下命令: 52 | 53 | ```sh 54 | $ make clean 55 | $ make ex1 56 | ``` 57 | 58 | ## 你会看到什么 59 | 60 | 如果代码能正常工作,你应该看到这些: 61 | 62 | ```sh 63 | $ make clean 64 | rm -f ex1 65 | $ make ex1 66 | cc -Wall -g ex1.c -o ex1 67 | ex1.c: In function 'main': 68 | ex1.c:3: warning: implicit declaration of function 'puts' 69 | $ 70 | ``` 71 | 72 | 你可以看出来我执行了`make clean`,它告诉make执行我们的`clean`目标。再去看一眼Makefile,之后你会看到在它的下面,我缩进并且输入了一些想要make为我运行的shell命令。你可以在此处输入任意多的命令,所以它是一个非常棒的自动化工具。 73 | 74 | > 注 75 | 76 | > 如果你修改了`ex1.c`,添加了`#include`,输出中的关于`puts`的警告就会消失(这其实应该算作一个错误)。我这里有警告是因为我并没有去掉它。 77 | 78 | 同时也要注意,即使我们在`Makefile`中并没有提到`ex1`,`make`仍然会知道如何构建它,以及使用我们指定的设置。 79 | 80 | ## 如何使它崩溃 81 | 82 | 上面那些已经足够让你起步了,但是让我们以一种特定的方式来破坏make文件,以便你可以看到发生了什么。找到`rm -f ex1`的那一行并去掉缩进(让它左移),之后你可以看到发生了什么。再次运行`make clean`,你就会得到下面的信息: 83 | 84 | ```sh 85 | $ make clean 86 | Makefile:4: *** missing separator. Stop. 87 | ``` 88 | 89 | 永远记住要缩进,以及如果你得到了像这种奇奇怪怪的错误,应该复查你是否都使用了 TAB 字符,由于一些make的变种十分挑剔。 90 | 91 | ## 附加题 92 | 93 | + 创建目标`all:ex1`,可以以单个命令`make`构建`ex1`。 94 | + 阅读`man make`来了解关于如何执行它的更多信息。 95 | + 阅读`man cc`来了解关于`-Wall`和`-g`行为的更多信息。 96 | + 在互联网上搜索Makefile文件,看看你是否能改进你的文件。 97 | + 在另一个C语言项目中找到`Makefile`文件,并且尝试理解它做了什么。 98 | -------------------------------------------------------------------------------- /ex21.md: -------------------------------------------------------------------------------- 1 | # 练习21:高级数据类型和控制结构 2 | 3 | > 原文:[Exercise 21: Advanced Data Types And Flow Control](http://c.learncodethehardway.org/book/ex21.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 这个练习是C语言中所有可用的数据类型和控制结构的摘要。它也可以作为一份参考在补完你的知识,并且不含有任何代码。我会通过创建教学卡片的方式,让你记住一些信息,所以你会在脑子里记住所有重要的概念。 8 | 9 | 这个练习非常有用,你应该花至少一周的时间来巩固内容并且补全这里所没有的元素。你应学出每个元素是什么意思,以及编写程序来验证你得出的结论。 10 | 11 | ## 可用的数据类型 12 | 13 | `int` 14 | 15 | 储存普通的整数,默认为32位大小。 16 | 17 | > 译者注:`int`在32或64位环境下为32位,但它不应该被看作平台无关的。如果需要用到平台无关的定长整数,请使用`int(n)_t`。 18 | 19 | `double` 20 | 21 | 储存稍大的浮点数。 22 | 23 | `float` 24 | 25 | 储存稍小的浮点数。 26 | 27 | `char` 28 | 29 | 储存单字节字符。 30 | 31 | `void` 32 | 33 | 表示“无类型”,用于声明不返回任何东西的函数,或者所指类型不明的指针,例如`void *thing`。 34 | 35 | `enum` 36 | 37 | 枚举类型,类似于整数,也可转换为整数,但是通过符号化的名称访问或设置。当`switch`语句中没有覆盖到所有枚举的元素时,一些编译器会发出警告。 38 | 39 | ## 类型修饰符 40 | 41 | `unsigned` 42 | 43 | 修改类型,使它不包含任何负数,同时上界变高。 44 | 45 | `signed` 46 | 47 | 可以储存正数和负数,但是上界会变为(大约)一半,下界变为和上界(大约)等长。 48 | 49 | > 译者注:符号修饰符只对`char`和`*** int`有效。`*** int`默认为`signed`,而`char`根据具体实现,可以默认为`signed`,也可以为`unsigned`。 50 | 51 | `long` 52 | 53 | 对该类型使用较大的空间,使它能存下更大的数,通常使当前大小加倍。 54 | 55 | `short` 56 | 57 | 对该类型使用较小的空间,使它储存能力变小,但是占据空间也变成一半。 58 | 59 | ## 类型限定符 60 | 61 | `const` 62 | 63 | 表示变量在初始化后不能改变。 64 | 65 | `volatile` 66 | 67 | 表示会做最坏的打算,编译器不会对它做任何优化。通常仅在对变量做一些奇怪的事情时,才会用到它。 68 | 69 | `register` 70 | 71 | 强制让编译器将这个变量保存在寄存器中,并且也可以无视它。目前的编译器更善于处理在哪里存放变量,所以应该只在确定这样会提升性能时使用它。 72 | 73 | ## 类型转换 74 | 75 | C使用了一种“阶梯形类型提升”的机制,它会观察运算符两边的变量,并且在运算之前将较小边的变量转换为较大边。这个过程按照如下顺序: 76 | 77 | + long double 78 | + double 79 | + float 80 | + long long 81 | + long 82 | + int (short, char) 83 | 84 | > 译者注:`short`和`char`会在运算之前转换成`int`。同种类型的`unsigned`和`signed`运算,`signed`保持字节不变转换成`unsigned`。 85 | 86 | ## 类型大小 87 | 88 | 89 | `stdint.h`为定长的整数类型定义了一些`typedef`,同时也有一些用于这些类型的宏。这比老的`limits.h`更加易于使用,因为它是不变的。这些类型如下: 90 | 91 | `int8_t` 92 | 93 | 8位符号整数。 94 | 95 | `uint8_t` 96 | 97 | 8位无符号整数。 98 | 99 | `int16_t` 100 | 101 | 16位符号整数。 102 | 103 | `uint16_t` 104 | 105 | 16位无符号整数。 106 | 107 | `int32_t` 108 | 109 | 32位符号整数。 110 | 111 | `uint32_t` 112 | 113 | 32位无符号整数。 114 | 115 | `int64_t` 116 | 117 | 64位符号整数。 118 | 119 | `uint64_t` 120 | 121 | 64位无符号整数。 122 | 123 | > 译者注:当用于对类型大小有要求的特定平台时,可以使用这些类型。如果你怕麻烦,不想处理平台相关类型的今后潜在的扩展的话,也可以使用这些类型。 124 | 125 | 下面的模式串为`(u)int(BITS)_t`,其中前面的`u`代表`unsigned`,`BITS`是所占位数的大小。这些模式串返回了这些类型的最大(或最小)值。 126 | 127 | `INT(N)_MAX` 128 | 129 | `N`位符号整数的最大正值,例如`INT16_MAX`。 130 | 131 | `INT(N)_MIN` 132 | 133 | `N`位符号整数的最小负值。 134 | 135 | `UINT(N)_MAX` 136 | 137 | `N`位无符号整数的最大正值。为什么不定义其最小值,是因为最小值是0,不可能出现负值。 138 | 139 | > 警告 140 | 141 | > 要注意,不要从字面上在任何头文件中去找`INT(N)_MAX`的定义。这里的`N`应该为特定整数,比如8、16、32、64,甚至可能是128。我在这个练习中使用了这个记法,就不需要显式写出每一个不同的组合了。 142 | 143 | 在`stdint.h`中,对于`size_t`类型和足够存放指针的整数也有一些宏定义,以及其它便捷类型的宏定义。编译器至少要保证它们为某一大小,并允许它们为更大的大小。 144 | 145 | `int_least(N)_t` 146 | 147 | 至少`N`位的整数。 148 | 149 | `uint_least(N)_t` 150 | 151 | 至少`N`位的无符号整数。 152 | 153 | `INT_LEAST(N)_MAX` 154 | 155 | `int_least(N)_t`类型的最大值。 156 | 157 | `INT_LEAST(N)_MIN` 158 | 159 | `int_least(N)_t`类型的最小值。 160 | 161 | `UINT_LEAST(N)_MAX` 162 | 163 | `uint_least(N)_t`的最大值。 164 | 165 | `int_fast(N)_t` 166 | 167 | 与`int_least(N)_t`相似,但是是至少`N`位的“最快”整数。 168 | 169 | `uint_fast(N)_t` 170 | 171 | 至少`N`位的“最快”无符号整数。 172 | 173 | `INT_FAST(N)_MAX` 174 | 175 | `int_fast(N)_t`的最大值。 176 | 177 | `INT_FAST(N)_MIN` 178 | 179 | `int_fast(N)_t`的最小值。 180 | 181 | `UINT_FAST(N)_MAX` 182 | 183 | `uint_fast(N)_t`的最大值。 184 | 185 | `intptr_t` 186 | 187 | 足够存放指针的符号整数。 188 | 189 | `uintptr_t` 190 | 191 | 足够存放指针的无符号整数。 192 | 193 | `INTPTR_MAX` 194 | 195 | `intptr_t`的最大值。 196 | 197 | `INTPTR_MIN` 198 | 199 | `intptr_t`的最小值。 200 | 201 | `UINTPTR_MAX` 202 | 203 | `uintptr_t`的最大值。 204 | 205 | `intmax_t` 206 | 207 | 系统中可能的最大尺寸的整数类型。 208 | 209 | `uintmax_t` 210 | 211 | 系统中可能的最大尺寸的无符号整数类型。 212 | 213 | `INTMAX_MAX` 214 | 215 | `intmax_t`的最大值。 216 | 217 | `INTMAX_MIN` 218 | 219 | `intmax_t`的最小值。 220 | 221 | `UINTMAX_MAX` 222 | 223 | `uintmax_t`的最大值。 224 | 225 | `PTRDIFF_MIN` 226 | 227 | `ptrdiff_t`的最小值。 228 | 229 | `PTRDIFF_MAX` 230 | 231 | `ptrdiff_t`的最大值。 232 | 233 | `SIZE_MAX` 234 | 235 | `size_t`的最大值。 236 | 237 | ## 可用的运算符 238 | 239 | 这是一个全面的列表,关于你可以在C中使用的全部运算符。这个列表中我会标明一些东西: 240 | 241 | 二元 242 | 243 | 该运算符有左右两个操作数:`X + Y`。 244 | 245 | 一元 246 | 247 | 该运算符作用于操作数本身`-X`。 248 | 249 | 前缀 250 | 251 | 该运算符出现在操作数之前:`++X`。 252 | 253 | 后缀 254 | 255 | 通常和前缀版本相似,但是出现在操作数之后,并且意义不同:`X++`。 256 | 257 | 三元 258 | 259 | 只有一个三元运算符,意思是“三个操作数”:`X ? Y : Z`。 260 | 261 | ## 算数运算符 262 | 263 | 下面是基本的算数运算符,我将函数调用`()`放入其中因为它更接近“算数”运算。 264 | 265 | `()` 266 | 267 | 函数调用。 268 | 269 | 二元 `*` 270 | 271 | 乘法。 272 | 273 | `/` 274 | 275 | 除法。 276 | 277 | 二元 `+` 278 | 279 | 加法。 280 | 281 | 一元 `+` 282 | 283 | 无变化。 284 | 285 | 后缀 `++` 286 | 287 | 读取变量然后自增。 288 | 289 | 前缀 `++` 290 | 291 | 自增变量然后读取。 292 | 293 | 后缀 `--` 294 | 295 | 读取变量然后自减。 296 | 297 | 前缀 `--` 298 | 299 | 自减变量然后读取。 300 | 301 | 二元 `-` 302 | 303 | 减法。 304 | 305 | 一元 `-` 306 | 307 | 取反,可用于表示负数。 308 | 309 | ## 数据运算 310 | 311 | 它们用于以不同方式和形式访问数据。 312 | 313 | `->` 314 | 315 | 结构体指针的成员访问。一元`*`和`.`运算符的复合。 316 | 317 | `.` 318 | 319 | 结构体值的成员访问。 320 | 321 | `[]` 322 | 323 | 取数组下标。二元`+`和一元`*`运算符的复合。 324 | 325 | `sizeof` 326 | 327 | 取类型或变量大小。 328 | 329 | 一元 `&` 330 | 331 | 取地址。 332 | 333 | 一元 `*` 334 | 335 | 取值(提领地址)。 336 | 337 | ## 逻辑运算符 338 | 339 | 它们用于测试变量的等性和不等性。 340 | 341 | `!=` 342 | 343 | 不等于。 344 | 345 | `<` 346 | 347 | 小于。 348 | 349 | `<=` 350 | 351 | 小于等于。 352 | 353 | `==` 354 | 355 | 等于(并不是赋值)。 356 | 357 | `>` 358 | 359 | 大于。 360 | 361 | `>=` 362 | 363 | 大于等于。 364 | 365 | ## 位运算符 366 | 367 | 它们更加高级,用于修改整数的原始位。 368 | 369 | 二元 `&` 370 | 371 | 位与。 372 | 373 | `<<` 374 | 375 | 左移。 376 | 377 | `>>` 378 | 379 | 右移。 380 | 381 | `^` 382 | 383 | 位异或。 384 | 385 | `|` 386 | 387 | 位或。 388 | 389 | `~` 390 | 391 | 取补(翻转所有位)。 392 | 393 | ## 布尔运算符。 394 | 395 | 用于真值测试,仔细学习三元运算符,它非常有用。 396 | 397 | `!` 398 | 399 | 取非。 400 | 401 | `&&` 402 | 403 | 与。 404 | 405 | `||` 406 | 407 | 或。 408 | 409 | `?:` 410 | 411 | 三元真值测试,`X ? Y : Z`读作“若X则Y否则Z”。 412 | 413 | ## 赋值运算符 414 | 415 | 复合赋值运算符在赋值同时执行运算。大多数上面的运算符都可以组成复合赋值运算符。 416 | 417 | `=` 418 | 419 | 赋值。 420 | 421 | `%=` 422 | 423 | 取余赋值。 424 | 425 | `&=` 426 | 427 | 位与赋值。 428 | 429 | `*=` 430 | 431 | 乘法赋值。 432 | 433 | 434 | `+=` 435 | 436 | 加法赋值。 437 | 438 | `-=` 439 | 440 | 减法赋值。 441 | 442 | `/=` 443 | 444 | 除法赋值。 445 | 446 | `<<=` 447 | 448 | 左移赋值。 449 | 450 | `>>=` 451 | 452 | 右移赋值。 453 | 454 | `^=` 455 | 456 | 位异或赋值。 457 | 458 | `|=` 459 | 460 | 位或赋值。 461 | 462 | ## 可用的控制结构 463 | 464 | 465 | 下面是一些你没有接触过的控制结构: 466 | 467 | `do-while` 468 | 469 | `do { ... } while(X);`首先执行花括号中的代码,之后再跳出前测试`X`表达式。 470 | 471 | `break` 472 | 473 | 放在循环中用于跳出循环。 474 | 475 | `continue` 476 | 477 | 跳到循环尾。 478 | 479 | `goto` 480 | 481 | 跳到你已经放置`label`的位置,你已经在`dbg.h`中看到它了,用于跳到`error`标签。 482 | 483 | ## 附加题 484 | 485 | + 阅读`stdint.h`或它的描述,写出所有可能出现的大小定义。 486 | + 查询本练习的每一项,写出它在代码中的作用。上网浏览资料来研究它如何正确使用。 487 | + 将这些信息做成教学卡片,每天看上15分钟来记住它们。 488 | + 创建一个程序,打印出每个类型的示例,并验证你的研究结果是否正确。 489 | -------------------------------------------------------------------------------- /ex22.md: -------------------------------------------------------------------------------- 1 | # 练习22:栈、作用域和全局 2 | 3 | > 原文:[Exercise 22: The Stack, Scope, And Globals](http://c.learncodethehardway.org/book/ex22.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 许多人在开始编程时,对“作用域”这个概念都不是很清楚。起初它来源于系统栈的使用方式(在之前提到过一些),以及它用于临时变量储存的方式。这个练习中,我们会通过学习栈数据结构如何工作来了解作用域,然后再来看看现代C语言处理作用域的方式。 8 | 9 | 这个练习的真正目的是了解一些比较麻烦的东西在C中如何存储。当一个人没有掌握作用域的概念时,它几乎也不能理解变量在哪里被创建,存在以及销毁。一旦你知道了这些,作用域的概念会变得易于理解。 10 | 11 | 这个练习需要如下三个文件: 12 | 13 | `ex22.h` 14 | 15 | 用于创建一些外部变量和一些函数的头文件。 16 | 17 | `ex22.c` 18 | 19 | 它并不像通常一样,是包含`main`的源文件,而是含有一些`ex22.h`中声明的函数和变量,并且会变成`ex22.o`。 20 | 21 | `ex22_main.c` 22 | 23 | `main`函数实际所在的文件,它会包含另外两个文件,并演示了它们包含的东西以及其它作用域概念。 24 | 25 | ## ex22.h 和 ex22.c 26 | 27 | 你的第一步是创建你自己的`ex22.h`头文件,其中定义了所需的函数和“导出”变量。 28 | 29 | ```c 30 | #ifndef _ex22_h 31 | #define _ex22_h 32 | 33 | // makes THE_SIZE in ex22.c available to other .c files 34 | extern int THE_SIZE; 35 | 36 | // gets and sets an internal static variable in ex22.c 37 | int get_age(); 38 | void set_age(int age); 39 | 40 | // updates a static variable that's inside update_ratio 41 | double update_ratio(double ratio); 42 | 43 | void print_size(); 44 | 45 | #endif 46 | ``` 47 | 48 | 最重要的事情是`extern int THE_SIZE`的用法,我将会在你创建完`ex22.c`之后解释它: 49 | 50 | ```c 51 | #include 52 | #include "ex22.h" 53 | #include "dbg.h" 54 | 55 | int THE_SIZE = 1000; 56 | 57 | static int THE_AGE = 37; 58 | 59 | int get_age() 60 | { 61 | return THE_AGE; 62 | } 63 | 64 | void set_age(int age) 65 | { 66 | THE_AGE = age; 67 | } 68 | 69 | 70 | double update_ratio(double new_ratio) 71 | { 72 | static double ratio = 1.0; 73 | 74 | double old_ratio = ratio; 75 | ratio = new_ratio; 76 | 77 | return old_ratio; 78 | } 79 | 80 | void print_size() 81 | { 82 | log_info("I think size is: %d", THE_SIZE); 83 | } 84 | ``` 85 | 86 | 这两个文件引入了一些新的变量储存方式: 87 | 88 | `extern` 89 | 90 | 这个关键词告诉编译器“这个变量已存在,但是他在别的‘外部区域’里”。通常它的意思是一个`.c`文件要用到另一个`.c`文件中定义的变量。这种情况下,我们可以说`ex22.c`中的`THE_SIZE`变量能被`ex22_main.c`访问到。 91 | 92 | `static`(文件) 93 | 94 | 这个关键词某种意义上是`extern`的反义词,意思是这个变量只能在当前的`.c`文件中使用,程序的其它部分不可访问。要记住文件级别的`static`(比如这里的`THE_AGE`)和其它位置不同。 95 | 96 | `static`(函数) 97 | 98 | 如果你使用`static`在函数中声明变量,它和文件中的`static`定义类似,但是只能够在该函数中访问。它是一种创建某个函数的持续状态的方法,但事实上它很少用于现代的C语言,因为它们很难和线程一起使用。 99 | 100 | 在上面的两个文件中,你需要理解如下几个变量和函数: 101 | 102 | `THE_SIZE` 103 | 104 | 这个你使用`extern`声明的变量将会在`ex22_main.c`中用到。 105 | 106 | `get_age`和`set_age` 107 | 108 | 它们用于操作静态变量`THE_AGE`,并通过函数将其暴露给程序的其它部分。你不能够直接访问到`THE_AGE`,但是这些函数可以。 109 | 110 | `update_ratio` 111 | 112 | 它生成新的`ratio`值并返回旧的值。它使用了函数级的静态变量`ratio`来跟踪`ratio`当前的值。 113 | 114 | `print_size` 115 | 116 | 打印出`ex22.c`所认为的`THE_SIZE`的当前值。 117 | 118 | ## ex22_main.c 119 | 120 | 一旦你写完了上面那些文件,你可以接着编程`main`函数,它会使用所有上面的文件并且演示了一些更多的作用域转换: 121 | 122 | ```c 123 | #include "ex22.h" 124 | #include "dbg.h" 125 | 126 | const char *MY_NAME = "Zed A. Shaw"; 127 | 128 | void scope_demo(int count) 129 | { 130 | log_info("count is: %d", count); 131 | 132 | if(count > 10) { 133 | int count = 100; // BAD! BUGS! 134 | 135 | log_info("count in this scope is %d", count); 136 | } 137 | 138 | log_info("count is at exit: %d", count); 139 | 140 | count = 3000; 141 | 142 | log_info("count after assign: %d", count); 143 | } 144 | 145 | int main(int argc, char *argv[]) 146 | { 147 | // test out THE_AGE accessors 148 | log_info("My name: %s, age: %d", MY_NAME, get_age()); 149 | 150 | set_age(100); 151 | 152 | log_info("My age is now: %d", get_age()); 153 | 154 | // test out THE_SIZE extern 155 | log_info("THE_SIZE is: %d", THE_SIZE); 156 | print_size(); 157 | 158 | THE_SIZE = 9; 159 | 160 | log_info("THE SIZE is now: %d", THE_SIZE); 161 | print_size(); 162 | 163 | // test the ratio function static 164 | log_info("Ratio at first: %f", update_ratio(2.0)); 165 | log_info("Ratio again: %f", update_ratio(10.0)); 166 | log_info("Ratio once more: %f", update_ratio(300.0)); 167 | 168 | // test the scope demo 169 | int count = 4; 170 | scope_demo(count); 171 | scope_demo(count * 20); 172 | 173 | log_info("count after calling scope_demo: %d", count); 174 | 175 | return 0; 176 | } 177 | ``` 178 | 179 | 我会把这个文件逐行拆分,你应该能够找到我提到的每个变量在哪里定义。 180 | 181 | ex22_main.c:4 182 | 183 | 使用了`const`来创建常量,它可用于替代`define`来创建常量。 184 | 185 | ex22_main.c:6 186 | 187 | 一个简单的函数,演示了函数中更多的作用域问题。 188 | 189 | ex22_main.c:8 190 | 191 | 在函数顶端打印出`count`的值。 192 | 193 | ex22_main.c:10 194 | 195 | `if`语句会开启一个新的作用域区块,并且在其中创建了另一个`count`变量。这个版本的`count`变量是一个全新的变量。`if`语句就好像开启了一个新的“迷你函数”。 196 | 197 | ex22_main.c:11 198 | 199 | `count`对于当前区块是局部变量,实际上不同于函数参数列表中的参数。 200 | 201 | ex22_main.c:13 202 | 203 | 将它打印出来,所以你可以在这里看到100,并不是传给`scope_demo`的参数。 204 | 205 | ex22_main.c:16 206 | 207 | 这里是最难懂得部分。你在两部分都有`count`变量,一个数函数参数,另一个是`if`语句中。`if`语句创建了新的代码块,所以11行的`count`并不影响同名的参数。这一行将其打印出来,你会看到它打印了参数的值而不是100。 208 | 209 | ex22_main.c:18-20 210 | 211 | 之后我将`count`参数设为3000并且打印出来,这里演示了你也可以修改函数参数的值,但并不会影响变量的调用者版本。 212 | 213 | 确保你浏览了整个函数,但是不要认为你已经十分了解作用娱乐。如果你在一个代码块中(比如`if`或`while`语句)创建了一些变量,这些变量是全新的变量,并且只在这个代码块中存在。这是至关重要的东西,也是许多bug的来源。我要强调你应该在这里花一些时间。 214 | 215 | `ex22_main.c`的剩余部分通过操作和打印变量演示了它们的全部。 216 | 217 | ex22_main.c:26 218 | 219 | 打印出`MY_NAME`的当前值,并且使用`get_age`读写器从`ex22.c`获取`THE_AGE`。 220 | 221 | 222 | ex22_main.c:27-30 223 | 224 | 使用了`ex22.c`中的`set_age`来修改并打印`THE_AGE`。 225 | 226 | ex22_main.c:33-39 227 | 228 | 接下来我对`ex22.c`中的`THE_SIZE`做了相同的事情,但这一次我直接访问了它,并且同时演示了它实际上在那个文件中已经修改了,还使用`print_size`打印了它。 229 | 230 | ex22_main.c:42-44 231 | 232 | 展示了`update_ratio`中的`ratio`在两次函数调用中如何保持了它的值。 233 | 234 | ex22_main.c:46-51 235 | 236 | 最后运行`scope_demo`,你可以在实例中观察到作用域。要注意到的关键点是,`count`局部变量在调用后保持不变。你将它像一个变量一样传入函数,它一定不会发生改变。要想达到目的你需要我们的老朋友指针。如果你将指向`count`的指针传入函数,那么函数就会持有它的地址并且能够改变它。 237 | 238 | 上面解释了这些文件中所发生的事情,但是你应该跟踪它们,并且确保在你学习的过程中明白了每个变量都在什么位置。 239 | 240 | ## 你会看到什么 241 | 242 | 这次我想让你手动构建这两个文件,而不是使用你的`Makefile`。于是你可以看到它们实际上如何被编译器放到一起。这是你应该做的事情,并且你应该看到如下输出: 243 | 244 | ```sh 245 | $ cc -Wall -g -DNDEBUG -c -o ex22.o ex22.c 246 | $ cc -Wall -g -DNDEBUG ex22_main.c ex22.o -o ex22_main 247 | $ ./ex22_main 248 | [INFO] (ex22_main.c:26) My name: Zed A. Shaw, age: 37 249 | [INFO] (ex22_main.c:30) My age is now: 100 250 | [INFO] (ex22_main.c:33) THE_SIZE is: 1000 251 | [INFO] (ex22.c:32) I think size is: 1000 252 | [INFO] (ex22_main.c:38) THE SIZE is now: 9 253 | [INFO] (ex22.c:32) I think size is: 9 254 | [INFO] (ex22_main.c:42) Ratio at first: 1.000000 255 | [INFO] (ex22_main.c:43) Ratio again: 2.000000 256 | [INFO] (ex22_main.c:44) Ratio once more: 10.000000 257 | [INFO] (ex22_main.c:8) count is: 4 258 | [INFO] (ex22_main.c:16) count is at exit: 4 259 | [INFO] (ex22_main.c:20) count after assign: 3000 260 | [INFO] (ex22_main.c:8) count is: 80 261 | [INFO] (ex22_main.c:13) count in this scope is 100 262 | [INFO] (ex22_main.c:16) count is at exit: 80 263 | [INFO] (ex22_main.c:20) count after assign: 3000 264 | [INFO] (ex22_main.c:51) count after calling scope_demo: 4 265 | ``` 266 | 267 | 确保你跟踪了每个变量是如何改变的,并且将其匹配到所输出的那一行。我使用了`dbg.h`的`log_info`来让你获得每个变量打印的具体行号,并且在文件中找到它用于跟踪。 268 | 269 | ## 作用域、栈和Bug 270 | 271 | 如果你正确完成了这个练习,你会看到有很多不同方式在C代码中放置变量。你可以使用`extern`或者访问类似`get_age`的函数来创建全局。你也可以在任何代码块中创建新的变量,它们在退出代码块之前会拥有自己的值,并且屏蔽掉外部的变量。你也可以响函数传递一个值并且修改它,但是调用者的变量版本不会发生改变。 272 | 273 | 需要理解的最重要的事情是,这些都可以造成bug。C中在你机器中许多位置放置和访问变量的能力会让你对它们所在的位置感到困扰。如果你不知道它们的位置,你就可能不能适当地管理它们。 274 | 275 | 下面是一些编程C代码时需要遵循的规则,可以让你避免与栈相关的bug: 276 | 277 | + 不要隐藏某个变量,就像上面`scope_demo`中对`count`所做的一样。这可能会产生一些隐蔽的bug,你认为你改变了某个变量但实际上没有。 278 | + 避免过多的全局变量,尤其是跨越多个文件。如果必须的话,要使用读写器函数,就像`get_age`。这并不适用于常量,因为它们是只读的。我是说对于`THE_SIZE`这种变量,如果你希望别人能够修改它,就应该使用读写器函数。 279 | + 在你不清楚的情况下,应该把它放在堆上。不要依赖于栈的语义,或者指定区域,而是要直接使用`malloc`创建它。 280 | + 不要使用函数级的静态变量,就像`update_ratio`。它们并不有用,而且当你想要使你的代码运行在多线程环境时,会有很大的隐患。对于良好的全局变量,它们也非常难于寻找。 281 | + 避免复用函数参数,因为你搞不清楚仅仅想要复用它还是希望修改它的调用者版本。 282 | 283 | ## 如何使它崩溃 284 | 285 | 对于这个练习,崩溃这个程序涉及到尝试访问或修改你不能访问的东西。 286 | 287 | + 试着从`ex22_main.c`直接访问`ex22.c`中的你不能访问变量。例如,你能不能获取`update_ratio`中的`ratio`?如果你用一个指针指向它会发生什么? 288 | + 移除`ex22.h`的`extern`声明,来观察会得到什么错误或警告。 289 | + 对不同变量添加`static`或者`const`限定符,之后尝试修改它们。 290 | 291 | ## 附加题 292 | 293 | + 研究“值传递”和“引用传递”的差异,并且为二者编写示例。(译者注:C中没有引用传递,你可以搜索“指针传递”。) 294 | + 使用指针来访问原本不能访问的变量。 295 | + 使用`Valgrind`来观察错误的访问是什么样子。 296 | + 编写一个递归调用并导致栈溢出的函数。如果不知道递归函数是什么的话,试着在`scope_demo`底部调用`scope_demo`本身,会形成一种循环。 297 | + 重新编写`Makefile`使之能够构建这些文件。 298 | -------------------------------------------------------------------------------- /ex23.md: -------------------------------------------------------------------------------- 1 | # 练习23:认识达夫设备 2 | 3 | > 原文:[Exercise 23: Meet Duff's Device](http://c.learncodethehardway.org/book/ex23.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 这个练习是一个脑筋急转弯,我会向你介绍最著名的C语言黑魔法之一,叫做“达夫设备”,以“发明者”汤姆·达夫的名字命名。这一强大(或邪恶?)的代码中,几乎你学过的任何东西都被包装在一个小的结构中。弄清它的工作机制也是一个好玩的谜题。 8 | 9 | > 注 10 | 11 | > C的一部分乐趣来源于这种神奇的黑魔法,但这也是使C难以使用的地方。你最好能够了解这些技巧,因为他会带给你关于C语言和你计算机的深入理解。但是,你应该永远都不要使用它们,并总是追求简单易读的代码。 12 | 13 | 达夫设备由汤姆·达夫“发现”(或创造),它是一个C编译器的小技巧,本来不应该能够正常工作。我并不想告诉你做了什么,因为这是一个谜题,等着你来思考并尝试解决。你需要运行这段代码,之后尝试弄清它做了什么,以及为什么可以这样做。 14 | 15 | ```c 16 | #include 17 | #include 18 | #include "dbg.h" 19 | 20 | 21 | int normal_copy(char *from, char *to, int count) 22 | { 23 | int i = 0; 24 | 25 | for(i = 0; i < count; i++) { 26 | to[i] = from[i]; 27 | } 28 | 29 | return i; 30 | } 31 | 32 | int duffs_device(char *from, char *to, int count) 33 | { 34 | { 35 | int n = (count + 7) / 8; 36 | 37 | switch(count % 8) { 38 | case 0: do { *to++ = *from++; 39 | case 7: *to++ = *from++; 40 | case 6: *to++ = *from++; 41 | case 5: *to++ = *from++; 42 | case 4: *to++ = *from++; 43 | case 3: *to++ = *from++; 44 | case 2: *to++ = *from++; 45 | case 1: *to++ = *from++; 46 | } while(--n > 0); 47 | } 48 | } 49 | 50 | return count; 51 | } 52 | 53 | int zeds_device(char *from, char *to, int count) 54 | { 55 | { 56 | int n = (count + 7) / 8; 57 | 58 | switch(count % 8) { 59 | case 0: 60 | again: *to++ = *from++; 61 | 62 | case 7: *to++ = *from++; 63 | case 6: *to++ = *from++; 64 | case 5: *to++ = *from++; 65 | case 4: *to++ = *from++; 66 | case 3: *to++ = *from++; 67 | case 2: *to++ = *from++; 68 | case 1: *to++ = *from++; 69 | if(--n > 0) goto again; 70 | } 71 | } 72 | 73 | return count; 74 | } 75 | 76 | int valid_copy(char *data, int count, char expects) 77 | { 78 | int i = 0; 79 | for(i = 0; i < count; i++) { 80 | if(data[i] != expects) { 81 | log_err("[%d] %c != %c", i, data[i], expects); 82 | return 0; 83 | } 84 | } 85 | 86 | return 1; 87 | } 88 | 89 | 90 | int main(int argc, char *argv[]) 91 | { 92 | char from[1000] = {'a'}; 93 | char to[1000] = {'c'}; 94 | int rc = 0; 95 | 96 | // setup the from to have some stuff 97 | memset(from, 'x', 1000); 98 | // set it to a failure mode 99 | memset(to, 'y', 1000); 100 | check(valid_copy(to, 1000, 'y'), "Not initialized right."); 101 | 102 | // use normal copy to 103 | rc = normal_copy(from, to, 1000); 104 | check(rc == 1000, "Normal copy failed: %d", rc); 105 | check(valid_copy(to, 1000, 'x'), "Normal copy failed."); 106 | 107 | // reset 108 | memset(to, 'y', 1000); 109 | 110 | // duffs version 111 | rc = duffs_device(from, to, 1000); 112 | check(rc == 1000, "Duff's device failed: %d", rc); 113 | check(valid_copy(to, 1000, 'x'), "Duff's device failed copy."); 114 | 115 | // reset 116 | memset(to, 'y', 1000); 117 | 118 | // my version 119 | rc = zeds_device(from, to, 1000); 120 | check(rc == 1000, "Zed's device failed: %d", rc); 121 | check(valid_copy(to, 1000, 'x'), "Zed's device failed copy."); 122 | 123 | return 0; 124 | error: 125 | return 1; 126 | } 127 | ``` 128 | 129 | 这段代码中我编写了三个版本的复制函数: 130 | 131 | `normal_copy` 132 | 133 | 使用普通的`for`循环来将字符从一个数组复制到另一个。 134 | 135 | `duffs_device` 136 | 137 | 这个就是称为“达夫设备”的脑筋急转弯,以汤姆·达夫的名字命名。这段有趣的邪恶代码应归咎于他。 138 | 139 | `zeds_device` 140 | 141 | “达夫设备”的另一个版本,其中使用了`goto`来让你发现一些线索,关于`duffs_device`中奇怪的`do-while`做了什么。 142 | 143 | 在往下学习之前仔细了解这三个函数,并试着自己解释代码都做了什么。 144 | 145 | ## 你会看到什么 146 | 147 | 这个程序没有任何输出,它只会执行并退出。你应当在`Valgrind`下运行它并确保没有任何错误。 148 | 149 | ## 解决谜题 150 | 151 | 首先需要了解的一件事,就是C对于它的一些语法是弱检查的。这就是你可以将`do-while`的一部分放入`switch`语句的一部分的原因,并且在其它地方的另一部分还可以正常工作。如果你观察带有`goto again`的我的版本,它实际上更清晰地解释了工作原理,但要确保你理解了这一部分是如何工作的。 152 | 153 | 第二件事是`switch`语句的默认贯穿机制可以让你跳到指定的`case`,并且继续运行直到`switch`结束。 154 | 155 | 最后的线索是`count % 8`以及顶端对`n`的计算。 156 | 157 | 现在,要理解这些函数的工作原理,需要完成下列事情: 158 | 159 | + 将代码抄写在一张纸上。 160 | + 当每个变量在`switch`之前初始化时,在纸的空白区域,把每个变量列在表中。 161 | + 按照`switch`的逻辑模拟执行代码,之后再正确的`case`处跳出。 162 | + 更新变量表,包括`to`、`from`和它们所指向的数组。 163 | + 当你到达`while`或者我的`goto`时,检查你的变量,之后按照逻辑返回`do-while`顶端,或者`again`标签所在的地方。 164 | + 继续这一手动的执行过程,更新变量,直到确定明白了代码如何运作。 165 | 166 | ## 为什么写成这样? 167 | 168 | 当你弄明白它的实际工作原理时,最终的问题是:为什么要把代码写成这样?这个小技巧的目的是手动编写“循环展开”。大而长的循环会非常慢,所以提升速度的一个方法就是找到循环中某个固定的部分,之后在循环中复制代码,序列化地展开。例如,如果你知道一个循环会执行至少20次,你就可以将这20次的内容直接写在源代码中。 169 | 170 | 达夫设备通过将循环展开为8个迭代块,来完成这件事情。这是个聪明的办法,并且可以正常工作。但是目前一个好的编译器也会为你完成这些。你不应该这样做,除非少数情况下你证明了它的确可以提升速度。 171 | 172 | ## 附加题 173 | 174 | + 不要再这样写代码了。 175 | + 查询维基百科的“达夫设备”词条,并且看看你能不能找到错误。将它与这里的版本对比,并且阅读文章来试着理解,为什么维基百科上的代码在你这里不能正常工作,但是对于汤姆·达夫可以。 176 | + 创建一些宏,来自动完成任意长度的这种设备。例如,你想创建32个`case`语句,并且不想手动把它们都写出来时,你会怎么办?你可以编写一次展开8个的宏吗? 177 | + 修改`main`函数,执行一些速度检测,来看看哪个实际上更快。 178 | + 查询`memcpy`、`memmove`和`memset`,并且也比较一下它们的速度。 179 | + 不要再这样写代码了! 180 | -------------------------------------------------------------------------------- /ex24.md: -------------------------------------------------------------------------------- 1 | # 练习24:输入输出和文件 2 | 3 | > 原文:[Exercise 24: Input, Output, Files](http://c.learncodethehardway.org/book/ex24.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 你已经学会了使用`printf`来打印变量,这非常不错,但是还需要学习更多。这个练习中你会用到`fscanf`和`fgets`在结构体中构建关于一个人的信息。在这个关于读取输入的简介之后,你会得到C语言IO函数的完整列表。其中一些你已经见过并且使用过了,所以这个练习也是一个记忆练习。 8 | 9 | ```c 10 | #include 11 | #include "dbg.h" 12 | 13 | #define MAX_DATA 100 14 | 15 | typedef enum EyeColor { 16 | BLUE_EYES, GREEN_EYES, BROWN_EYES, 17 | BLACK_EYES, OTHER_EYES 18 | } EyeColor; 19 | 20 | const char *EYE_COLOR_NAMES[] = { 21 | "Blue", "Green", "Brown", "Black", "Other" 22 | }; 23 | 24 | typedef struct Person { 25 | int age; 26 | char first_name[MAX_DATA]; 27 | char last_name[MAX_DATA]; 28 | EyeColor eyes; 29 | float income; 30 | } Person; 31 | 32 | 33 | int main(int argc, char *argv[]) 34 | { 35 | Person you = {.age = 0}; 36 | int i = 0; 37 | char *in = NULL; 38 | 39 | printf("What's your First Name? "); 40 | in = fgets(you.first_name, MAX_DATA-1, stdin); 41 | check(in != NULL, "Failed to read first name."); 42 | 43 | printf("What's your Last Name? "); 44 | in = fgets(you.last_name, MAX_DATA-1, stdin); 45 | check(in != NULL, "Failed to read last name."); 46 | 47 | printf("How old are you? "); 48 | int rc = fscanf(stdin, "%d", &you.age); 49 | check(rc > 0, "You have to enter a number."); 50 | 51 | printf("What color are your eyes:\n"); 52 | for(i = 0; i <= OTHER_EYES; i++) { 53 | printf("%d) %s\n", i+1, EYE_COLOR_NAMES[i]); 54 | } 55 | printf("> "); 56 | 57 | int eyes = -1; 58 | rc = fscanf(stdin, "%d", &eyes); 59 | check(rc > 0, "You have to enter a number."); 60 | 61 | you.eyes = eyes - 1; 62 | check(you.eyes <= OTHER_EYES && you.eyes >= 0, "Do it right, that's not an option."); 63 | 64 | printf("How much do you make an hour? "); 65 | rc = fscanf(stdin, "%f", &you.income); 66 | check(rc > 0, "Enter a floating point number."); 67 | 68 | printf("----- RESULTS -----\n"); 69 | 70 | printf("First Name: %s", you.first_name); 71 | printf("Last Name: %s", you.last_name); 72 | printf("Age: %d\n", you.age); 73 | printf("Eyes: %s\n", EYE_COLOR_NAMES[you.eyes]); 74 | printf("Income: %f\n", you.income); 75 | 76 | return 0; 77 | error: 78 | 79 | return -1; 80 | } 81 | ``` 82 | 83 | 这个程序非常简单,并且引入了叫做`fscanf`的函数,意思是“文件的格式化输入”。`scanf`家族的函数是`printf`的反转版本。`printf`用于以某种格式打印数据,然而`scanf`以某种格式读取(或者扫描)输入。 84 | 85 | 文件开头没有什么新的东西,所以下面只列出`main`所做的事情: 86 | 87 | ex24.c:24-28 88 | 89 | 创建所需的变量。 90 | 91 | ex24.c:30-32 92 | 93 | 使用`fgets`函数获取名字,它从输入读取字符串(这个例子中是`stdin`),但是确保它不会造成缓冲区溢出。 94 | 95 | ex24.c:34-36 96 | 97 | 对` you.last_name`执行相同操作,同样使用了`fgets`。 98 | 99 | ex24.c:38-39 100 | 101 | 使用`fscanf`来从`stdin`读取整数,并且将其放到`you.age`中。你可以看到,其中使用了和`printf`相同格式的格式化字符串。你也应该看到传入了`you.age`的地址,便于`fscnaf`获得它的指针来修改它。这是一个很好的例子,解释了使用指向数据的指针作为“输出参数”。 102 | 103 | ex24.c:41-45 104 | 105 | 打印出用于眼睛颜色的所有可选项,并且带有`EyeColor`枚举所匹配的数值。 106 | 107 | ex24.c:47-50 108 | 109 | 再次使用了`fscanf`,从`you.eyes`中获取数值,但是保证了输入是有效的。这非常重要,因为用户可以输入一个超出`EYE_COLOR_NAMES`数组范围的值,并且会导致段错误。 110 | 111 | ex24.c:52-53 112 | 113 | 获取`you.income`的值。 114 | 115 | ex24.c:55-61 116 | 117 | 将所有数据打印出来,便于你看到它们是否正确。要注意`EYE_COLOR_NAMES`用于打印`EyeColor`枚举值实际上的名字。 118 | 119 | ## 你会看到什么 120 | 121 | 当你运行这个程序时,你应该看到你的输入被适当地转换。你应该尝试给它非预期的输入,看看程序是怎么预防它的。 122 | 123 | ```sh 124 | $ make ex24 125 | cc -Wall -g -DNDEBUG ex24.c -o ex24 126 | $ ./ex24 127 | What's your First Name? Zed 128 | What's your Last Name? Shaw 129 | How old are you? 37 130 | What color are your eyes: 131 | 1) Blue 132 | 2) Green 133 | 3) Brown 134 | 4) Black 135 | 5) Other 136 | > 1 137 | How much do you make an hour? 1.2345 138 | ----- RESULTS ----- 139 | First Name: Zed 140 | Last Name: Shaw 141 | Age: 37 142 | Eyes: Blue 143 | Income: 1.234500 144 | ``` 145 | 146 | ## 如何使它崩溃 147 | 148 | 这个程序非常不错,但是这个练习中真正重要的部分是,`scanf`如何发生错误。对于简单的数值转换没有问题,但是对于字符串会出现问题,因为`scanf`在你读取之前并不知道缓冲区有多大。类似于`gets`的函数(并不是`fgets`,不带`f`的版本)也有一个我们已经避免的问题。它并不是道输入缓冲区有多大,并且可能会使你的程序崩溃。 149 | 150 | 要演示`fscanf`和字符串的这一问题,需要修改使用`fgets`的那一行,使它变成`fscanf(stdin, "%50s", you.first_name)`,并且尝试再次运行。你会注意到,它读取了过多的内容,并且吃掉了你的回车键。这并不是你期望它所做的,你应该使用`fgets`而不是去解决古怪的`scanf`问题。 151 | 152 | 接下来,将`fgets`改为`gets`,接着使用`valgrind`来执行`valgrind ./ex24 < /dev/urandom`,往你的程序中输入一些垃圾字符串。这叫做对你的程序进行“模糊测试”,它是一种不错的方法来发现输入错误。这个例子中,你需要从`/dev/urandom`文件来输入一些垃圾,并且观察它如何崩溃。在一些平台上你需要执行数次,或者修改`MAX_DATA`来使其变小。 153 | 154 | `gets`函数非常糟糕,以至于一些平台在程序运行时会警告你使用了`gets`。你应该永远避免使用这个函数。 155 | 156 | 最后,找到`you.eyes`输入的地方,并移除对其是否在正确范围内的检查。然后,为它输入一个错误的数值,比如-1或者1000。在`Valgrind`执行这些操作,来观察会发生什么。 157 | 158 | > 译者注:根据最新的C11标准,对于输入函数,你应该总是使用`_s`后缀的安全版本。对于向字符串的输出函数,应该总是使用C99中新增的带`n`的版本,例如`snprintf`。如果你的编译器支持新版本,就不应该使用旧版本的不安全函数。 159 | 160 | ## IO函数 161 | 162 | 这是一个各种IO函数的简单列表。你应该查询每个函数并为其创建速记卡,包含函数名称,功能和它的任何变体。 163 | 164 | + `fscanf` 165 | + `fgets` 166 | + `fopen` 167 | + `freopen` 168 | + `fdopen` 169 | + `fclose` 170 | + `fcloseall` 171 | + `fgetpos` 172 | + `fseek` 173 | + `ftell` 174 | + `rewind` 175 | + `fprintf` 176 | + `fwrite` 177 | + `fread` 178 | 179 | 过一遍这些函数,并且记住它们的不同变体和它们的功能。例如,对于`fscanf`的卡片,上面应该有`scanf`、`sscanf`、`vscanf`,以及其它。并且在背面写下每个函数所做的事情。 180 | 181 | 最后,为了获得这些卡片所需的信息,使用`man`来阅读它的帮助。例如,`fscanf`帮助页由`man fscanf`得到。 182 | 183 | ## 附加题 184 | 185 | + 将这个程序重写为不需要`fscanf`的版本。你需要使用类似于`atoi`的函数来将输入的字符串转换为数值。 186 | + 修改这个程序,使用`scanf`来代替`fscanf`,并观察有什么不同。 187 | + 修改程序,是输入的名字不包含任何换行符和空白字符。 188 | + 使用`scanf`编写函数,按照文件名读取文件内容,每次读取单个字符,但是不要越过(文件和缓冲区的)末尾。使这个函数接受字符串大小来更加通用,并且确保无论什么情况下字符串都以`'\0'`结尾。 189 | -------------------------------------------------------------------------------- /ex25.md: -------------------------------------------------------------------------------- 1 | # 练习25:变参函数 2 | 3 | > 原文:[Exercise 25: Variable Argument Functions](http://c.learncodethehardway.org/book/ex25.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 在C语言中,你可以通过创建“变参函数”来创建你自己的`printf`或者`scanf`版本。这些函数使用`stdarg.h`头,它们可以让你为你的库创建更加便利的接口。它们对于创建特定类型的“构建”函数、格式化函数和任何用到可变参数的函数都非常实用。 8 | 9 | 理解“变参函数”对于C语言编程并不必要,我在编程生涯中也只有大约20次用到它。但是,理解变参函数如何工作有助于你对它的调试,并且让你更加了解计算机。 10 | 11 | ```c 12 | /** WARNING: This code is fresh and potentially isn't correct yet. */ 13 | 14 | #include 15 | #include 16 | #include 17 | #include "dbg.h" 18 | 19 | #define MAX_DATA 100 20 | 21 | int read_string(char **out_string, int max_buffer) 22 | { 23 | *out_string = calloc(1, max_buffer + 1); 24 | check_mem(*out_string); 25 | 26 | char *result = fgets(*out_string, max_buffer, stdin); 27 | check(result != NULL, "Input error."); 28 | 29 | return 0; 30 | 31 | error: 32 | if(*out_string) free(*out_string); 33 | *out_string = NULL; 34 | return -1; 35 | } 36 | 37 | int read_int(int *out_int) 38 | { 39 | char *input = NULL; 40 | int rc = read_string(&input, MAX_DATA); 41 | check(rc == 0, "Failed to read number."); 42 | 43 | *out_int = atoi(input); 44 | 45 | free(input); 46 | return 0; 47 | 48 | error: 49 | if(input) free(input); 50 | return -1; 51 | } 52 | 53 | int read_scan(const char *fmt, ...) 54 | { 55 | int i = 0; 56 | int rc = 0; 57 | int *out_int = NULL; 58 | char *out_char = NULL; 59 | char **out_string = NULL; 60 | int max_buffer = 0; 61 | 62 | va_list argp; 63 | va_start(argp, fmt); 64 | 65 | for(i = 0; fmt[i] != '\0'; i++) { 66 | if(fmt[i] == '%') { 67 | i++; 68 | switch(fmt[i]) { 69 | case '\0': 70 | sentinel("Invalid format, you ended with %%."); 71 | break; 72 | 73 | case 'd': 74 | out_int = va_arg(argp, int *); 75 | rc = read_int(out_int); 76 | check(rc == 0, "Failed to read int."); 77 | break; 78 | 79 | case 'c': 80 | out_char = va_arg(argp, char *); 81 | *out_char = fgetc(stdin); 82 | break; 83 | 84 | case 's': 85 | max_buffer = va_arg(argp, int); 86 | out_string = va_arg(argp, char **); 87 | rc = read_string(out_string, max_buffer); 88 | check(rc == 0, "Failed to read string."); 89 | break; 90 | 91 | default: 92 | sentinel("Invalid format."); 93 | } 94 | } else { 95 | fgetc(stdin); 96 | } 97 | 98 | check(!feof(stdin) && !ferror(stdin), "Input error."); 99 | } 100 | 101 | va_end(argp); 102 | return 0; 103 | 104 | error: 105 | va_end(argp); 106 | return -1; 107 | } 108 | 109 | 110 | 111 | int main(int argc, char *argv[]) 112 | { 113 | char *first_name = NULL; 114 | char initial = ' '; 115 | char *last_name = NULL; 116 | int age = 0; 117 | 118 | printf("What's your first name? "); 119 | int rc = read_scan("%s", MAX_DATA, &first_name); 120 | check(rc == 0, "Failed first name."); 121 | 122 | printf("What's your initial? "); 123 | rc = read_scan("%c\n", &initial); 124 | check(rc == 0, "Failed initial."); 125 | 126 | printf("What's your last name? "); 127 | rc = read_scan("%s", MAX_DATA, &last_name); 128 | check(rc == 0, "Failed last name."); 129 | 130 | printf("How old are you? "); 131 | rc = read_scan("%d", &age); 132 | 133 | printf("---- RESULTS ----\n"); 134 | printf("First Name: %s", first_name); 135 | printf("Initial: '%c'\n", initial); 136 | printf("Last Name: %s", last_name); 137 | printf("Age: %d\n", age); 138 | 139 | free(first_name); 140 | free(last_name); 141 | return 0; 142 | error: 143 | return -1; 144 | } 145 | ``` 146 | 147 | 这个程序和上一个练习很像,除了我编写了自己的`scanf`风格函数,它以我自己的方式处理字符串。你应该对`main`函数很清楚了,以及`read_string`和`read_int`两个函数,因为它们并没有做什么新的东西。 148 | 149 | 这里的变参函数叫做`read_scan`,它使用了`va_list`数据结构执行和`scanf`相同的工作,并支持宏和函数。下面是它的工作原理: 150 | 151 | + 我将函数的最后一个参数设置为`...`,它向C表示这个函数在`fmt`参数之后接受任何数量的参数。我可以在它前面设置许多其它的参数,但是在它后面不能放置任何参数。 152 | + 在设置完一些参数时,我创建了`va_list`类型的变量,并且使用`va_list`来为其初始化。这配置了`stdarg.h`中的这一可以处理可变参数的组件。 153 | + 接着我使用了`for`循环,遍历`fmt`格式化字符串,并且处理了类似`scanf`的格式,但比它略简单。它里面只带有整数、字符和字符串。 154 | + 当我碰到占位符时,我使用了`switch`语句来确定需要做什么。 155 | + 现在,为了从`va_list argp`中获得遍历,我需要使用`va_arg(argp, TYPE)`宏,其中`TYPE`是我将要向参数传递的准确类型。这一设计的后果是你会非常盲目,所以如果你没有足够的变量传入,程序就会崩溃。 156 | + 和`scanf`的有趣的不同点是,当它碰到`'s'`占位符时,我使用`read_string`来创建字符串。`va_list argp`栈需要接受两个函数:需要读取的最大尺寸,以及用于输出的字符串指针。`read_string`使用这些信息来执行实际工作。 157 | + 这使`read_scan`比`scan`更加一致,因为你总是使用`&`提供变量的地址,并且合理地设置它们。 158 | + 最后,如果它碰到了不在格式中的字符,它仅仅会读取并跳过,而并不关心字符是什么,因为它只需要跳过。 159 | 160 | ## 你会看到什么 161 | 162 | 当你运行程序时,会得到与下面详细的结果: 163 | 164 | ```sh 165 | $ make ex25 166 | cc -Wall -g -DNDEBUG ex25.c -o ex25 167 | $ ./ex25 168 | What's your first name? Zed 169 | What's your initial? A 170 | What's your last name? Shaw 171 | How old are you? 37 172 | ---- RESULTS ---- 173 | First Name: Zed 174 | Initial: 'A' 175 | Last Name: Shaw 176 | Age: 37 177 | ``` 178 | 179 | ## 如何使它崩溃 180 | 181 | 这个程序对缓冲区溢出更加健壮,但是和`scanf`一样,它不能够处理输入的格式错误。为了使它崩溃,试着修改代码,把首先传入用于`'%s'`格式的尺寸去掉。同时试着传入多于`MAX_DATA`的数据,之后找到在`read_string`中不使用`calloc`的方法,并且修改它的工作方式。最后还有个问题是`fgets`会吃掉换行符,所以试着使用`fgetc`修复它,要注意字符串结尾应为`'\0'`。 182 | 183 | ## 附加题 184 | 185 | + 再三检查确保你明白了每个`out_`变量的作用。最重要的是`out_string`,并且它是指针的指针。所以,理清当你设置时获取到的是指针还是内容尤为重要。 186 | + 使用变参系统编写一个和`printf`相似的函数,重新编写`main`来使用它。 187 | + 像往常一样,阅读这些函数/宏的手册页,确保知道了它在你的平台做了什么,一些平台会使用宏而其它平台会使用函数,还有一些平台会让它们不起作用。这完全取决于你所用的编译器和平台。 188 | -------------------------------------------------------------------------------- /ex29.md: -------------------------------------------------------------------------------- 1 | # 练习29:库和链接 2 | 3 | > 原文:[Exercise 29: Libraries And Linking](http://c.learncodethehardway.org/book/ex29.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | C语言编程的核心能力之一就是链接OS所提供的库。链接是一种为你的程序添加额外特性的方法,这些特性由其它人在系统中创建并打包。你已经使用了一些自动包含的标准库,但是我打算对库的不同类型和它们的作用做个解释。 8 | 9 | 首先,库在每个语言中都没有良好的设计。我不知道为什么,但是似乎语言的设计者都将链接视为不是特别重要的东西。它们通常令人混乱,难以使用,不能正确进行版本控制,并以不同的方式链接到各种地方。 10 | 11 | C没有什么不同,但是C中的库和链接是Unix操作系统的组件,并且可执行的格式在很多年前就设计好了。学习C如何链接库有助于理解OS如何工作,以及它如何运行你的程序。 12 | 13 | C中的库有两种基本类型: 14 | 15 | 静态 16 | 17 | 你可以使用`ar`和`ranlib`来构建它,就像上个练习中的`libYOUR_LIBRARY.a`那样(Windows下后缀为`.lib`)。这种库可以当做一系列`.o`对象文件和函数的容器,以及当你构建程序时,可以当做是一个大型的`.o`文件。 18 | 19 | 动态 20 | 21 | 它们通常以`.so`(Linux)或`.dll`(Windows)结尾。在OSX中,差不多有一百万种后缀,取决于版本和编写它的人。严格来讲,OSX中的`.dylib`,`.bundle`和`framework`这三个之间没什么不同。这些文件都被构建好并且放置到指定的地方。当你运行程序时,OS会动态加载这些文件并且“凭空”链接到你的程序中。 22 | 23 | 我倾向于对小型或中型项目使用静态的库,因为它们易于使用,并且工作在在更多操作系统上。我也喜欢将所有代码放入静态库中,之后链接它来执行单元测试,或者链接到所需的程序中。 24 | 25 | 动态库适用于大型系统,它的空间十分有限,或者其中大量程序都使用相同的功能。这种情况下不应该为每个程序的共同特性静态链接所有代码,而是应该将它放到动态库中,这样它仅仅会为所有程序加载一份。 26 | 27 | 在上一个练习中,我讲解了如何构建静态库(`.a`),我会在本书的剩余部分用到它。这个练习中我打算向你展示如何构建一个简单的`.so`库,并且如何使用Unix系统的`dlopen`动态加载它。我会手动执行它,以便你可以理解每件实际发生的事情。之后,附加题这部分会使用c项目框架来创建它。 28 | 29 | ## 动态加载动态库 30 | 31 | 我创建了两个源文件来完成它。一个用于构建`libex29.so`库,另一个是个叫做`ex29`的程序,它可以加载这个库并运行其中的程序: 32 | 33 | ```c 34 | #include 35 | #include 36 | #include "dbg.h" 37 | 38 | 39 | int print_a_message(const char *msg) 40 | { 41 | printf("A STRING: %s\n", msg); 42 | 43 | return 0; 44 | } 45 | 46 | 47 | int uppercase(const char *msg) 48 | { 49 | int i = 0; 50 | 51 | // BUG: \0 termination problems 52 | for(i = 0; msg[i] != '\0'; i++) { 53 | printf("%c", toupper(msg[i])); 54 | } 55 | 56 | printf("\n"); 57 | 58 | return 0; 59 | } 60 | 61 | int lowercase(const char *msg) 62 | { 63 | int i = 0; 64 | 65 | // BUG: \0 termination problems 66 | for(i = 0; msg[i] != '\0'; i++) { 67 | printf("%c", tolower(msg[i])); 68 | } 69 | 70 | printf("\n"); 71 | 72 | return 0; 73 | } 74 | 75 | int fail_on_purpose(const char *msg) 76 | { 77 | return 1; 78 | } 79 | ``` 80 | 81 | 这里面没什么神奇之处。其中故意留了一些bug,看你是否注意到了。你需要在随后修复它们。 82 | 83 | 我们将要使用`dlopen`,`dlsym`,和`dlclose`函数来处理上面的函数。 84 | 85 | ```c 86 | #include 87 | #include "dbg.h" 88 | #include 89 | 90 | typedef int (*lib_function)(const char *data); 91 | 92 | 93 | int main(int argc, char *argv[]) 94 | { 95 | int rc = 0; 96 | check(argc == 4, "USAGE: ex29 libex29.so function data"); 97 | 98 | char *lib_file = argv[1]; 99 | char *func_to_run = argv[2]; 100 | char *data = argv[3]; 101 | 102 | void *lib = dlopen(lib_file, RTLD_NOW); 103 | check(lib != NULL, "Failed to open the library %s: %s", lib_file, dlerror()); 104 | 105 | lib_function func = dlsym(lib, func_to_run); 106 | check(func != NULL, "Did not find %s function in the library %s: %s", func_to_run, lib_file, dlerror()); 107 | 108 | rc = func(data); 109 | check(rc == 0, "Function %s return %d for data: %s", func_to_run, rc, data); 110 | 111 | rc = dlclose(lib); 112 | check(rc == 0, "Failed to close %s", lib_file); 113 | 114 | return 0; 115 | 116 | error: 117 | return 1; 118 | } 119 | ``` 120 | 121 | 我现在会拆分这个程序,便于你理解这一小段代码其中的原理。 122 | 123 | ex29.c:5 124 | 125 | 我随后会使用这个函数指针定义,来调用库中的函数。这没什么新东西,确保你理解了它的作用。 126 | 127 | ex29.c:17 128 | 129 | 在为一个小型程序做必要的初始化后,我使用了`dlopen`函数来加载由`lib_file`表示的库。这个函数返回一个句柄,我们随后会用到它,就像来打开文件那样。 130 | 131 | ex29.c:18 132 | 133 | 如果出现错误,我执行了通常的检查并退出,但是要注意最后我使用了`dlerror`来查明发生了什么错误。 134 | 135 | ex29.c:20 136 | 137 | 我使用了`dlsym`来获取`lib`中的函数,通过它的字面名称`func_to_run`。这是最强大的部分,因为我动态获取了一个函数指针,基于我从命令行`argv`获得的字符串。 138 | 139 | ex29.c:23 140 | 141 | 接着我调用`func`函数,获得返回值并进行检查。 142 | 143 | ex29.c:26 144 | 145 | 最后,我像关闭文件那样关闭了库。通常你需要在程序的整个运行期间保证它们打开,所以关闭操作并不非常实用,我只是在这里演示它。 146 | 147 | > 译者注:由于能够使用系统调用加载,动态库可以被多种语言的程序调用,而静态库只能被C及兼容C的程序调用。 148 | 149 | ## 你会看到什么 150 | 151 | 既然你已经知道这些文件做什么了,下面是我的shell会话,用于构建`libex29.so`和`ex29`并随后运行它。下面的代码中你可以学到如何手动构建: 152 | 153 | ```sh 154 | # compile the lib file and make the .so 155 | # you may need -fPIC here on some platforms. add that if you get an error 156 | $ cc -c libex29.c -o libex29.o 157 | $ cc -shared -o libex29.so libex29.o 158 | 159 | # make the loader program 160 | $ cc -Wall -g -DNDEBUG ex29.c -ldl -o ex29 161 | 162 | # try it out with some things that work 163 | $ ex29 ./libex29.so print_a_message "hello there" 164 | -bash: ex29: command not found 165 | $ ./ex29 ./libex29.so print_a_message "hello there" 166 | A STRING: hello there 167 | $ ./ex29 ./libex29.so uppercase "hello there" 168 | HELLO THERE 169 | $ ./ex29 ./libex29.so lowercase "HELLO tHeRe" 170 | hello there 171 | $ ./ex29 ./libex29.so fail_on_purpose "i fail" 172 | [ERROR] (ex29.c:23: errno: None) Function fail_on_purpose return 1 for data: i fail 173 | 174 | # try to give it bad args 175 | $ ./ex29 ./libex29.so fail_on_purpose 176 | [ERROR] (ex29.c:11: errno: None) USAGE: ex29 libex29.so function data 177 | 178 | # try calling a function that is not there 179 | $ ./ex29 ./libex29.so adfasfasdf asdfadff 180 | [ERROR] (ex29.c:20: errno: None) Did not find adfasfasdf 181 | function in the library libex29.so: dlsym(0x1076009b0, adfasfasdf): symbol not found 182 | 183 | # try loading a .so that is not there 184 | $ ./ex29 ./libex.so adfasfasdf asdfadfas 185 | [ERROR] (ex29.c:17: errno: No such file or directory) Failed to open 186 | the library libex.so: dlopen(libex.so, 2): image not found 187 | $ 188 | ``` 189 | 190 | 需要注意,你可能需要在不同OS、不同OS的不同版本,以及不同OS的不同版本的不同编译器上执行构建,则需要修改构建共享库的方式。如果我构建`libex29.so`的方式在你的平台上不起作用,请告诉我,我会为其它平台添加一些注解。 191 | 192 | > 译者注:到处编写、到处调试、到处编译、到处发布。--vczh 193 | 194 | ‍ 195 | 196 | > 注 197 | 198 | > 有时候你像往常一样运行`cc -Wall -g -DNDEBUG -ldl ex29.c -o ex29`,并且认为它能够正常工作,但是没有。在一些平台上,参数的顺序会影响到它是否生效,这也没什么理由。在Debian或者Ubuntu中你需要执行`cc -Wall -g -DNDEBUG ex29.c -ldl -o ex29`,这是唯一的方式。所以虽然我在这里使用了OSX,但是以后如果你链接动态库的时候它找不到某个函数,要试着自己解决问题。 199 | 200 | > 这里面比较麻烦的事情是,实际平台的不同会影响到命令参数的顺序。将`-ldl`放到某个位置没有理由与其它位置不同。它只是一个选项,还需要了解这些简直是太气人了。 201 | 202 | ## 如何使它崩溃 203 | 204 | 打开`lbex29.so`,并且使用能够处理二进制的编辑器编辑它。修改一些字节,然后关闭。看看你是否能使用`dlopen`函数来打开它,即使你修改了它。 205 | 206 | ## 附加题 207 | 208 | + 你注意到我在`libex29.c`中写的不良代码了吗?我使用了一个`for`循环来检查`'\0'`的结尾,修改它们使这些函数总是接收字符串长度,并在函数内部使用。 209 | + 使用项目框架目录,来为这个练习创建新的项目。将`libex29.c`放入`src/`目录,修改`Makefile`使它能够构建`build/libex29.so`。 210 | + 将`ex29.c`改为`tests/ex29_tests.c`,使它做为单元测试执行。使它能够正常工作,意思是你需要修改它让它加载`build/libex29.so`文件,并且运行上面我手写的测试。 211 | + 阅读`man dlopen`文档,并且查询所有有关函数。尝试`dlopen`的其它选项,比如`RTLD_NOW`。 212 | -------------------------------------------------------------------------------- /ex3.md: -------------------------------------------------------------------------------- 1 | # 练习3:格式化输出 2 | 3 | > 原文:[Exercise 3: Formatted Printing](http://c.learncodethehardway.org/book/ex3.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 不要删除Makefile,因为它可以帮你指出错误,以及当我们需要自动化处理一些事情时,可以向它添加新的东西。 8 | 9 | 许多编程语言都使用了C风格的格式化输出,所以让我们尝试一下: 10 | 11 | ```c 12 | #include 13 | 14 | int main() 15 | { 16 | int age = 10; 17 | int height = 72; 18 | 19 | printf("I am %d years old.\n", age); 20 | printf("I am %d inches tall.\n", height); 21 | 22 | return 0; 23 | } 24 | ``` 25 | 26 | 写完之后,执行通常的`make ex3`命令来构建并运行它。一定要确保你处理了所有的警告。 27 | 28 | 这个练习的代码量很小,但是信息量很大,所以让我们逐行分析一下: 29 | 30 | + 首先你包含了另一个头文件叫做`stdio.h`。这告诉了编译器你要使用“标准的输入/输出函数”。它们之一就是`printf`。 31 | + 然后你使用了一个叫`age`的变量并且将它设置为10。 32 | + 接着你使用了一个叫`height`的变量并且设置为72。 33 | + 再然后你使用`printf`函数来打印这个星球上最高的十岁的人的年龄和高度。 34 | + 在`printf`中你会注意到你传入了一个字符串,这就是格式字符串,和其它语言中一样。 35 | + 在格式字符串之后,你传入了一些变量,它们应该被`printf`“替换”进格式字符串中。 36 | 37 | 这些语句的结果就是你用`printf`处理了一些变量,并且它会构造出一个新的字符串,之后将它打印在终端上。 38 | 39 | ## 你会看到什么 40 | 41 | 当你做完上面的整个步骤,你应该看到这些东西: 42 | 43 | ```sh 44 | $ make ex3 45 | cc -Wall -g ex3.c -o ex3 46 | $ ./ex3 47 | I am 10 years old. 48 | I am 72 inches tall. 49 | $ 50 | ``` 51 | 52 | 不久之后我会停下来让你运行`make`,并且告诉你构建过程是什么样子的。所以请确保你正确得到了这些信息并且能正常执行。 53 | 54 | ## 外部研究 55 | 56 | 在附加题一节我可能会让你自己查找一些资料,并且弄明白它们。这对于一个自我学习的程序员来说相当重要。如果你一直在自己尝试了解问题之前去问其它人,你永远都不会学到独立解决问题。这会让你永远都不会在自己的技能上建立信心,并且总是依赖别人去完成你的工作。 57 | 58 | 打破你这一习惯的方法就是强迫你自己先试着自己回答问题,并且确认你的回答是正确的。你可以通过打破一些事情,用实验验证可能的答案,以及自己进行研究来完成它。 59 | 60 | 对于这个练习,我想让你上网搜索`printf`的所有格式化占位符和转义序列。转义序列类似`\n`或者`\r`,可以让你分别打印新的一行或者 tab 。格式化占位符类似`%s`或者`%d`,可以让你打印字符串或整数。找到所有的这些东西,以及如何修改它们,和可设置的“精度”和宽度的种类。 61 | 62 | 从现在开始,这些任务会放到附加题里面,你应该去完成它们。 63 | 64 | ## 如何使它崩溃 65 | 66 | 尝试下面的一些东西来使你的程序崩溃,在你的电脑上它们可能会崩溃,也可能不会。 67 | 68 | + 从第一个`printf`中去掉`age`并重新编译,你应该会得到一大串的警告。 69 | + 运行新的程序,它会崩溃,或者打印出奇怪的年龄。 70 | + 将`printf`恢复原样,并且去掉`age`的初值,将那一行改为`int age;`,之后重新构建并运行。 71 | 72 | ```sh 73 | # edit ex3.c to break printf 74 | $ make ex3 75 | cc -Wall -g ex3.c -o ex3 76 | ex3.c: In function 'main': 77 | ex3.c:8: warning: too few arguments for format 78 | ex3.c:5: warning: unused variable 'age' 79 | $ ./ex3 80 | I am -919092456 years old. 81 | I am 72 inches tall. 82 | # edit ex3.c again to fix printf, but don't init age 83 | $ make ex3 84 | cc -Wall -g ex3.c -o ex3 85 | ex3.c: In function 'main': 86 | ex3.c:8: warning: 'age' is used uninitialized in this function 87 | $ ./ex3 88 | I am 0 years old. 89 | I am 72 inches tall. 90 | $ 91 | ``` 92 | 93 | ## 附加题 94 | 95 | + 找到尽可能多的方法使`ex3`崩溃。 96 | + 执行`man 3 printf`来阅读其它可用的'%'格式化占位符。如果你在其它语言中使用过它们,应该看着非常熟悉(它们来源于`printf`)。 97 | + 将`ex3`添加到你的`Makefile`的`all`列表中。到目前为止,可以使用`make clean all`来构建你所有的练习。 98 | + 将`ex3`添加到你的`Makefile`的`clean`列表中。当你需要的时候使用`make clean`可以删除它。 99 | -------------------------------------------------------------------------------- /ex30.md: -------------------------------------------------------------------------------- 1 | # 练习30:自动化测试 2 | 3 | > 原文:[Exercise 30: Automated Testing](http://c.learncodethehardway.org/book/ex30.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 自动化测试经常用于例如Python和Ruby的其它语言,但是很少用于C。一部分原因是自动化加载和测试C的代码片段具有较高的难度。这一章中,我们会创建一个非常小型的测试“框架”,并且使用你的框架目录构建测试用例的示例。 8 | 9 | 我接下来打算使用,并且你会包含进框架目录的框架,叫做“minunit”,它以[Jera Design](http://www.jera.com/techinfo/jtns/jtn002.html)所编写的一小段代码作为开始,之后我扩展了它,就像这样: 10 | 11 | ```c 12 | #undef NDEBUG 13 | #ifndef _minunit_h 14 | #define _minunit_h 15 | 16 | #include 17 | #include 18 | #include 19 | 20 | #define mu_suite_start() char *message = NULL 21 | 22 | #define mu_assert(test, message) if (!(test)) { log_err(message); return message; } 23 | #define mu_run_test(test) debug("\n-----%s", " " #test); \ 24 | message = test(); tests_run++; if (message) return message; 25 | 26 | #define RUN_TESTS(name) int main(int argc, char *argv[]) {\ 27 | argc = 1; \ 28 | debug("----- RUNNING: %s", argv[0]);\ 29 | printf("----\nRUNNING: %s\n", argv[0]);\ 30 | char *result = name();\ 31 | if (result != 0) {\ 32 | printf("FAILED: %s\n", result);\ 33 | }\ 34 | else {\ 35 | printf("ALL TESTS PASSED\n");\ 36 | }\ 37 | printf("Tests run: %d\n", tests_run);\ 38 | exit(result != 0);\ 39 | } 40 | 41 | 42 | int tests_run; 43 | 44 | #endif 45 | ``` 46 | 47 | 原始的内容所剩不多了,现在我使用`dbg.h`宏,并且在模板测试运行器的末尾创建了大量的宏。在这小段代码中我们创建了整套函数单元测试系统,一旦它结合上shell脚本来运行测试,你可以将其用于你的C代码。 48 | 49 | ## 完成测试框架 50 | 51 | 为了基础这个练习,你应该让你的`src/libex29.c`正常工作,并且完成练习29的附加题,是`ex29.c`加载程序并合理运行。练习29中我这事了一个附加题来使它像单元测试一样工作,但是现在我打算重新想你展示如何使用`minunit.h`来做这件事。 52 | 53 | 首先我们需要创建一个简单的空单元测试,命名为`tests/libex29_tests.c`,在里面输入: 54 | 55 | ```c 56 | #include "minunit.h" 57 | 58 | char *test_dlopen() 59 | { 60 | 61 | return NULL; 62 | } 63 | 64 | char *test_functions() 65 | { 66 | 67 | return NULL; 68 | } 69 | 70 | char *test_failures() 71 | { 72 | 73 | return NULL; 74 | } 75 | 76 | char *test_dlclose() 77 | { 78 | 79 | return NULL; 80 | } 81 | 82 | char *all_tests() { 83 | mu_suite_start(); 84 | 85 | mu_run_test(test_dlopen); 86 | mu_run_test(test_functions); 87 | mu_run_test(test_failures); 88 | mu_run_test(test_dlclose); 89 | 90 | return NULL; 91 | } 92 | 93 | RUN_TESTS(all_tests); 94 | ``` 95 | 96 | 这份代码展示了`tests/minunit.h`中的`RUN_TESTS`宏,以及如何使用其他的测试运行器宏。我没有编写实际的测试函数,所以你只能看到单元测试的结构。我首先会分解这个文件: 97 | 98 | libex29_tests.c:1 99 | 100 | 包含`minunit.h`框架。 101 | 102 | libex29_tests.c:3-7 103 | 104 | 第一个测试。测试函数具有固定的结构,它们不带任何参数并且返回`char *`,成功时为`NULL`。这非常重要,因为其他宏用于向测试运行器返回错误信息。 105 | 106 | libex29_tests.c:9-25 107 | 108 | 与第一个测试相似的更多测试。 109 | 110 | libex29_tests.c:27 111 | 112 | 控制其他测试的运行器函数。它和其它测试用例格式一致,但是使用额外的东西来配置。 113 | 114 | libex29_tests.c:28 115 | 116 | 为`mu_suite_start`测试设置一些通用的东西。 117 | 118 | libex29_tests.c:30 119 | 120 | 这就是使用`mu_run_test`返回结果的地方。 121 | 122 | libex29_tests.c:35 123 | 124 | 在你运行所有测试之后,你应该返回`NULL`,就像普通的测试函数一样。 125 | 126 | libex29_tests.c:38 127 | 128 | 最后需要使用`RUN_TESTS`宏来启动`main`函数,让它运行`all_tests`启动器。 129 | 130 | 这就是用于运行测试所有代码了,现在你需要尝试使它运行在项目框架中。下面是我的执行结果: 131 | 132 | ```sh 133 | not printable 134 | ``` 135 | 136 | 我首先执行`make clean`,之后我运行了构建,它将模板改造为`libYOUR_LIBRARY.a`和`libYOUR_LIBRARY.so`文件。要记住你需要在练习29的附加题中完成它。但如果你没有完成的话,下面是我所使用的`Makefile`的文件差异: 137 | 138 | ```diff 139 | diff --git a/code/c-skeleton/Makefile b/code/c-skeleton/Makefile 140 | index 135d538..21b92bf 100644 141 | --- a/code/c-skeleton/Makefile 142 | +++ b/code/c-skeleton/Makefile 143 | @@ -9,9 +9,10 @@ TEST_SRC=$(wildcard tests/*_tests.c) 144 | TESTS=$(patsubst %.c,%,$(TEST_SRC)) 145 | 146 | TARGET=build/libYOUR_LIBRARY.a 147 | +SO_TARGET=$(patsubst %.a,%.so,$(TARGET)) 148 | 149 | # The Target Build 150 | -all: $(TARGET) tests 151 | +all: $(TARGET) $(SO_TARGET) tests 152 | 153 | dev: CFLAGS=-g -Wall -Isrc -Wall -Wextra $(OPTFLAGS) 154 | dev: all 155 | @@ -21,6 +22,9 @@ $(TARGET): build $(OBJECTS) 156 | ar rcs $@ $(OBJECTS) 157 | ranlib $@ 158 | 159 | +$(SO_TARGET): $(TARGET) $(OBJECTS) 160 | + $(CC) -shared -o $@ $(OBJECTS) 161 | + 162 | build: 163 | @mkdir -p build 164 | @mkdir -p bin 165 | ``` 166 | 167 | 完成这些改变后,你现在应该能够构建任何东西,并且你可以最后补完剩余的单元测试函数: 168 | 169 | ```c 170 | #include "minunit.h" 171 | #include 172 | 173 | typedef int (*lib_function)(const char *data); 174 | char *lib_file = "build/libYOUR_LIBRARY.so"; 175 | void *lib = NULL; 176 | 177 | int check_function(const char *func_to_run, const char *data, int expected) 178 | { 179 | lib_function func = dlsym(lib, func_to_run); 180 | check(func != NULL, "Did not find %s function in the library %s: %s", func_to_run, lib_file, dlerror()); 181 | 182 | int rc = func(data); 183 | check(rc == expected, "Function %s return %d for data: %s", func_to_run, rc, data); 184 | 185 | return 1; 186 | error: 187 | return 0; 188 | } 189 | 190 | char *test_dlopen() 191 | { 192 | lib = dlopen(lib_file, RTLD_NOW); 193 | mu_assert(lib != NULL, "Failed to open the library to test."); 194 | 195 | return NULL; 196 | } 197 | 198 | char *test_functions() 199 | { 200 | mu_assert(check_function("print_a_message", "Hello", 0), "print_a_message failed."); 201 | mu_assert(check_function("uppercase", "Hello", 0), "uppercase failed."); 202 | mu_assert(check_function("lowercase", "Hello", 0), "lowercase failed."); 203 | 204 | return NULL; 205 | } 206 | 207 | char *test_failures() 208 | { 209 | mu_assert(check_function("fail_on_purpose", "Hello", 1), "fail_on_purpose should fail."); 210 | 211 | return NULL; 212 | } 213 | 214 | char *test_dlclose() 215 | { 216 | int rc = dlclose(lib); 217 | mu_assert(rc == 0, "Failed to close lib."); 218 | 219 | return NULL; 220 | } 221 | 222 | char *all_tests() { 223 | mu_suite_start(); 224 | 225 | mu_run_test(test_dlopen); 226 | mu_run_test(test_functions); 227 | mu_run_test(test_failures); 228 | mu_run_test(test_dlclose); 229 | 230 | return NULL; 231 | } 232 | 233 | RUN_TESTS(all_tests); 234 | ``` 235 | 236 | 我希望你可以弄清楚它都干了什么,因为这里没有什么新的东西,除了`check_function`函数。这是一个通用的模式,其中我需要重复执行一段代码,然后通过为之创建宏或函数来使它自动化。这里我打算运行`.so`中所加载的函数,所以我创建了一个小型函数来完成它。 237 | 238 | ## 附加题 239 | 240 | + 这段代码能起作用,但是可能有点乱。清理框架目录,是它包含所有这些文件,但是移除任何和练习29有关的代码。你应该能够复制这个目录并且无需很多编辑操作就能开始新的项目。 241 | + 研究`runtests.sh`,并且查询有关`bash`语法的资料,来弄懂它的作用。你能够编写这个脚本的C版本吗? 242 | -------------------------------------------------------------------------------- /ex31.md: -------------------------------------------------------------------------------- 1 | # 练习31:代码调试 2 | 3 | > 原文:[Exercise 31: Debugging Code](http://c.learncodethehardway.org/book/ex31.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 我已经教给你一些关于我的强大的调试宏的技巧,并且你已经开始用它们了。当我调试代码时,我使用`debug()`宏,分析发生了什么以及跟踪问题。在这个练习中我打算教给你一些使用gdb的技巧,用于监视一个不会退出的简单程序。你会学到如何使用gdb附加到运行中的进程,并挂起它来观察发生了什么。在此之后我会给你一些用于gdb的小提示和小技巧。 8 | 9 | ## 调试输出、GDB或Valgrind 10 | 11 | 我主要按照一种“科学方法”的方式来调试,我会提出可能的所有原因,之后排除它们或证明它们导致了缺陷。许多程序员拥有的问题是它们对解决bug的恐慌和急躁使他们觉得这种方法会“拖慢”他们。它们并没有注意到,它们已经失败了,并且在收集无用的信息。我发现日志(调试输出)会强迫我科学地解决bug,并且在更多情况下易于收集信息。 12 | 13 | 此外,使用调试输出来作为我的首要调试工具的理由如下: 14 | 15 | + 你可以使用变量的调试输出,来看到程序执行的整个轨迹,它让你跟踪变量是如何产生错误的。使用gdb的话,你必须为每个变量放置查看和调试语句,并且难以获得执行的实际轨迹。 16 | + 调试输出存在于代码中,当你需要它们是你可以重新编译使它们回来。使用gdb的话,你每次调试都需要重新配置相同的信息。 17 | + 当服务器工作不正常时,它的调试日志功能易于打开,并且在它运行中可以监视日志来查看哪里不对。系统管理员知道如何处理日志,他们不知道如何使用gdb。 18 | + 打印信息更加容易。调试器通常由于它奇特的UI和前后矛盾显得难用且古怪。`debug("Yo, dis right? %d", my_stuff);`就没有那么麻烦。 19 | + 编写调试输出来发现缺陷,强迫你实际分析代码,并且使用科学方法。你可以认为它是,“我假设这里的代码是错误的”,你可以运行它来验证你的假设,如果这里没有错误那么你可以移动到其它地方。这看起来需要更长时间,但是实际上更快,因为你经历了“鉴别诊断”的过程,并排除所有可能的原因,直到你找到它。 20 | + 调试输入更适于和单元测试一起运行。你可以实际上总是编译调试语句,单元测试时可以随时查看日志。如果你用gdb,你需要在gdb中重复运行单元测试,并跟踪他来查看发生了什么。 21 | + 使用Valgrind可以得到和调试输出等价的内存相关的错误,所以你并不需要使用类似gdb的东西来寻找缺陷。 22 | 23 | 尽管所有原因显示我更倾向于`debug`而不是`gdb`,我还是在少数情况下回用到`gdb`,并且我认为你应该选择有助于你完成工作的工具。有时,你只能够连接到一个崩溃的程序并且四处转悠。或者,你得到了一个会崩溃的服务器,你只能够获得一些核心文件来一探究竟。这些货少数其它情况中,gdb是很好的办法。你最好准备尽可能多的工具来解决问题。 24 | 25 | 接下来我会通过对比gdb、调试输出和Valgrind来详细分析,像这样: 26 | 27 | + Valgrind用于捕获所有内存错误。如果Valgrind中含有错误或Valgrind会严重拖慢程序,我会使用gdb。 28 | + 调试输出用于诊断或修复有关逻辑或使用上的缺陷。在你使用Valgrind之前,这些共计90%的缺陷。 29 | + 使用gdb解决剩下的“谜之bug”,或如要收集信息的紧急情况。如果Valgrind不起作用,并且我不能打印出所需信息,我就会使用gdb开始四处搜索。这里我仅仅使用gdb来收集信息。一旦我弄清发生了什么,我会回来编程单元测试来引发缺陷,之后编程打印语句来查找原因。 30 | 31 | ## 调试策略 32 | 33 | 这一过程适用于你打算使用任何调试技巧,无论是Valgrind、调试输出,或者使用调试器。我打算以使用`gdb`的形式来描述他,因为似乎人们在使用调试器是会跳过它。但是应当对每个bug使用它,直到你只需要在非常困难的bug上用到。 34 | 35 | + 创建一个小型文本文件叫做`notes.txt`,并且将它用作记录想法、bug和问题的“实验记录”。 36 | + 在你使用`gdb`之前,写下你打算修复的bug,以及可能的产生原因。 37 | + 对于每个原因,写下你所认为的,问题来源的函数或文件,或者仅仅写下你不知道。 38 | + 现在启动`gdb`并且使用`file:function`挑选最可能的因素,之后在那里设置断点。 39 | + 使用`gdb`运行程序,并且确认它是否是真正原因。查明它的最好方式就是看看你是否可以使用`set`命令,简单修复问题或者重现错误。 40 | + 如果它不是真正原因,则在`notes.txt`中标记它不是,以及理由。移到下一个可能的原因,并且使最易于调试的,之后记录你收集到的信息。 41 | 42 | 这里你并没有注意到,它是最基本的科学方法。你写下一些假设,之后调试来证明或证伪它们。这让你洞察到更多可能的因素,最终使你找到他。这个过程有助于你避免重复步入同一个可能的因素,即使你发现它们并不可能。 43 | 44 | 你也可以使用调试输出来执行这个过程。唯一的不同就是你实际在源码中编写假设来推测问题所在,而不是`notes.txt`中。某种程度上,调试输出强制你科学地解决bug,因为你需要将假写为打印语句。 45 | 46 | ## 使用 GDB 47 | 48 | 我将在这个练习中调试下面这个程序,它只有一个不会正常终止的`while`循环。我在里面放置了一个`usleep`调用,使它循环起来更加有趣。 49 | 50 | ```c 51 | #include 52 | 53 | int main(int argc, char *argv[]) 54 | { 55 | int i = 0; 56 | 57 | while(i < 100) { 58 | usleep(3000); 59 | } 60 | 61 | return 0; 62 | } 63 | ``` 64 | 65 | 像往常一样编译,并且在`gdb`下启动它,例如:`gdb ./ex31`。 66 | 67 | 一旦它运行之后,我打算让你使用这些`gdb`命令和它交互,并且观察它们的作用以及如何使用它们。 68 | 69 | help COMMAND 70 | 71 | 获得`COMMAND`的简单帮助。 72 | 73 | break file.c:(line|function) 74 | 75 | 在你希望暂停之星的地方设置断点。你可以提供行号或者函数名称,来在文件中的那个地方暂停。 76 | 77 | run ARGS 78 | 79 | 运行程序,使用`ARGS`作为命令行参数。 80 | 81 | cont 82 | 83 | 继续执行程序,直到断点或错误。 84 | 85 | step 86 | 87 | 单步执行代码,但是会进入函数内部。使用它来跟踪函数内部,来观察它做了什么。 88 | 89 | next 90 | 91 | 就像是`step`,但是他会运行函数并步过它们。 92 | 93 | backtrace (or bt) 94 | 95 | 执行“跟踪回溯”,它会转储函数到当前执行点的执行轨迹。对于查明如何执行到这里非常有用,因为它也打印出传给每个函数的参数。它和Valgrind报告内存错误的方式很接近。 96 | 97 | set var X = Y 98 | 99 | 将变量`X`设置为`Y`。 100 | 101 | print X 102 | 103 | 打印出`X`的值,你通常可以使用C的语法来访问指针的值或者结构体的内容。 104 | 105 | ENTER 106 | 107 | 重复上一条命令。 108 | 109 | quit 110 | 111 | 退出`gdb`。 112 | 113 | 这些都是我使用`gdb`时的主要命令。你现在的任务是玩转它们和`ex31`,你会对它的输出更加熟悉。 114 | 115 | 一旦你熟悉了`gdb`之后,你会希望多加使用它。尝试在更复杂的程序,例如`devpkg`上使用它,来观察你是否能够改函数的执行或分析出程序在做什么。 116 | 117 | ## 附加到进程 118 | 119 | `gdb`最实用的功能就是附加到运行中的程序,并且就地调试它的能力。当你拥有一个崩溃的服务器或GUI程序,你通常不需要像之前那样在`gdb`下运行它。而是可以直接启动它,希望它不要马上崩溃,之后附加到它并设置断点。练习的这一部分中我会向你展示怎么做。 120 | 121 | 当你退出`gdb`之后,如果你停止了`ex31`我希望你重启它,之后开启另一个中断窗口以便于启动`gdb`并附加。进程附加就是你让`gdb`连接到已经运行的程序,以便于你实时监测它。它会挂起程序来让你单步执行,当你执行完之后程序会像往常一样恢复运行。 122 | 123 | 下面是一段会话,我对`ex31`做了上述事情,单步执行它,之后修改`while`循环并使它退出。 124 | 125 | ```sh 126 | $ ps ax | grep ex31 127 | 10026 s000 S+ 0:00.11 ./ex31 128 | 10036 s001 R+ 0:00.00 grep ex31 129 | 130 | $ gdb ./ex31 10026 131 | GNU gdb 6.3.50-20050815 (Apple version gdb-1705) (Fri Jul 1 10:50:06 UTC 2011) 132 | Copyright 2004 Free Software Foundation, Inc. 133 | GDB is free software, covered by the GNU General Public License, and you are 134 | welcome to change it and/or distribute copies of it under certain conditions. 135 | Type "show copying" to see the conditions. 136 | There is absolutely no warranty for GDB. Type "show warranty" for details. 137 | This GDB was configured as "x86_64-apple-darwin"...Reading symbols for shared libraries .. done 138 | 139 | /Users/zedshaw/projects/books/learn-c-the-hard-way/code/10026: No such file or directory 140 | Attaching to program: `/Users/zedshaw/projects/books/learn-c-the-hard-way/code/ex31', process 10026. 141 | Reading symbols for shared libraries + done 142 | Reading symbols for shared libraries ++........................ done 143 | Reading symbols for shared libraries + done 144 | 0x00007fff862c9e42 in __semwait_signal () 145 | 146 | (gdb) break 8 147 | Breakpoint 1 at 0x107babf14: file ex31.c, line 8. 148 | 149 | (gdb) break ex31.c:11 150 | Breakpoint 2 at 0x107babf1c: file ex31.c, line 12. 151 | 152 | (gdb) cont 153 | Continuing. 154 | 155 | Breakpoint 1, main (argc=1, argv=0x7fff677aabd8) at ex31.c:8 156 | 8 while(i < 100) { 157 | 158 | (gdb) p i 159 | $1 = 0 160 | 161 | (gdb) cont 162 | Continuing. 163 | 164 | Breakpoint 1, main (argc=1, argv=0x7fff677aabd8) at ex31.c:8 165 | 8 while(i < 100) { 166 | 167 | (gdb) p i 168 | $2 = 0 169 | 170 | (gdb) list 171 | 3 172 | 4 int main(int argc, char *argv[]) 173 | 5 { 174 | 6 int i = 0; 175 | 7 176 | 8 while(i < 100) { 177 | 9 usleep(3000); 178 | 10 } 179 | 11 180 | 12 return 0; 181 | 182 | (gdb) set var i = 200 183 | 184 | (gdb) p i 185 | $3 = 200 186 | 187 | (gdb) next 188 | 189 | Breakpoint 2, main (argc=1, argv=0x7fff677aabd8) at ex31.c:12 190 | 12 return 0; 191 | 192 | (gdb) cont 193 | Continuing. 194 | 195 | Program exited normally. 196 | (gdb) quit 197 | $ 198 | ``` 199 | 200 | > 注 201 | 202 | > 在OSX上你可能会看到输入root密码的GUI输入框,并且即使你输入了密码还是会得到来自`gdb`的“Unable to access task for process-id XXX: (os/kern) failure.”的错误。这种情况下,你需要停止`gdb`和`ex31`程序,并重新启动程序使它工作,只要你成功输入了root密码。 203 | 204 | 我会遍历整个会话,并且解释我做了什么: 205 | 206 | gdb:1 207 | 208 | 使用`ps`来寻找我想要附加的`ex31`的进程ID。 209 | 210 | gdb:5 211 | 212 | 我使用`gdb ./ex31 PID`来附加到进程,其中`PID`替换为我所拥有的进程ID。 213 | 214 | gdb:6-19 215 | 216 | `gdb`打印出了一堆关于协议的信息,接着它读取了所有东西。 217 | 218 | gdb:21 219 | 220 | 程序被附加,并且在当前执行点上停止。所以现在我在文件中的第8行使用`break`设置了断点。我假设我这么做的时候,已经在这个我想中断的文件中了。 221 | 222 | gdb:24 223 | 224 | 执行`break`的更好方式,是提供`file.c line`的格式,便于你确保定位到了正确的地方。我在这个`break`中这样做。 225 | 226 | gdb:27 227 | 228 | 我使用`cont`来继续运行,直到我命中了断点。 229 | 230 | gdb:30-31 231 | 232 | 我已到达断点,于是`gdb`打印出我需要了解的变量(`argc`和`argv`),以及停下来的位置,之后打印出断点的行号。 233 | 234 | gdb:33-34 235 | 236 | 我使用`print`的缩写`p`来打印出`i`变量的值,它是0。 237 | 238 | gdb:36 239 | 240 | 继续运行来查看`i`是否改变。 241 | 242 | gdb:42 243 | 244 | 再次打印出`i`,显然它没有变化。 245 | 246 | gdb:45-55 247 | 248 | 使用`list`来查看代码是什么,之后我意识到它不可能退出,因为我没有自增`i`。 249 | 250 | gdb:57 251 | 252 | 确认我的假设是正确的,即`i`需要使用`set`命令来修改为`i = 200`。这是`gdb`最优秀的特性之一,让你“修改”程序来让你快速知道你是否正确。 253 | 254 | gdb:59 255 | 256 | 打印`i`来确保它已改变。 257 | 258 | gdb:62 259 | 260 | 使用`next`来移到下一段代码,并且我发现命中了`ex31.c:12`的断点,所以这意味着`while`循环已退出。我的假设正确,我需要修改`i`。 261 | 262 | gdb:67 263 | 264 | 使用`cont`来继续运行,程序像往常一样退出。 265 | 266 | gdb:71 267 | 268 | 最后我使用`quit`来退出`gdb`。 269 | 270 | ## GDB 技巧 271 | 272 | 下面是你可以用于GDB的一些小技巧: 273 | 274 | gdb --args 275 | 276 | 通常`gdb`获得你提供的变量并假设它们用于它自己。使用`--args`来向程序传递它们。 277 | 278 | thread apply all bt 279 | 280 | 转储所有线程的执行轨迹,非常有用。 281 | 282 | gdb --batch --ex r --ex bt --ex q --args 283 | 284 | 运行程序,当它崩溃时你会得到执行轨迹。 285 | 286 | ? 287 | 288 | 如果你有其它技巧,在评论中写下它吧。 289 | 290 | ## 附加题 291 | 292 | + 找到一个图形化的调试器,将它与原始的`gdb`相比。它们在本地调试程序时非常有用,但是对于在服务器上调试没有任何意义。 293 | + 你可以开启OS上的“核心转储”,当程序崩溃时你会得到一个核心文件。这个核心文件就像是对程序的解剖,便于你了解崩溃时发生了什么,以及由什么原因导致。修改`ex31.c`使它在几个迭代之后崩溃,之后尝试得到它的核心转储并分析。 294 | -------------------------------------------------------------------------------- /ex33.md: -------------------------------------------------------------------------------- 1 | # 练习33:链表算法 2 | 3 | > 原文:[Exercise 33: Linked List Algorithms](http://c.learncodethehardway.org/book/ex33.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 我将想你介绍涉及到排序的两个算法,你可以用它们操作链表。我首先要警告你,如果你打算对数据排序,不要使用链表,它们对于排序十分麻烦,并且有更好的数据结构作为替代。我向你介绍这两种算法只是因为它们难以在链表上完成,并且让你思考如何高效操作它们。 8 | 9 | 为了编写这本书,我打算将算法放在两个不同的文件中,`list_algos.h`和`list_algos.c`,之后在`list_algos_test.c`中编写测试。现在你要按照我的结构,因为它足以把事情做好,但是如果你使用其它的库要记住这并不是通用的结构。 10 | 11 | 这个练习中我打算给你一些额外的挑战,并且希望你不要作弊。我打算先给你单元测试,并且让你打下来。之后让你基于它们在维基百科中的描述,尝试实现这个两个算法,之后看看你的代码是否和我的类似。 12 | 13 | ## 冒泡排序和归并排序 14 | 15 | 互联网的强大之处,就是我可以仅仅给你[冒泡排序](http://en.wikipedia.org/wiki/Bubble_sort)和[归并排序](http://en.wikipedia.org/wiki/Merge_sort)的链接,来让你学习它们。是的,这省了我很多字。现在我要告诉你如何使用它们的伪代码来实现它们。你可以像这样来实现算法: 16 | 17 | + 阅读描述,并且观察任何可视化的图表。 18 | + 使用方框和线条在纸上画出算法,或者使用一些带有数字的卡片(比如扑克牌),尝试手动执行算法。这会向你形象地展示算法的执行过程。 19 | + 在`list_algos.c`文案总创建函数的主干,并且创建`list_algos.h`文件,之后创建测试代码。 20 | + 编写第一个测试并且编译所有东西。 21 | + 回到维基百科页面,复制粘贴伪代码到你创建的函数中(不是C代码)。 22 | + 将伪代码翻译成良好的C代码,就像我教你的那样,使用你的单元测试来保证它有效。 23 | + 为边界情况补充一些测试,例如空链表,排序号的链表,以及其它。 24 | + 对下一个算法重复这些过程并测试。 25 | 26 | 我只是告诉你理解大多数算法的秘密,直到你碰到一些更加麻烦的算法。这里你只是按照维基百科来实现冒泡排序和归并排序,它们是一个好的起始。 27 | 28 | ## 单元测试 29 | 30 | 下面是你应该通过的单元测试: 31 | 32 | ```c 33 | #include "minunit.h" 34 | #include 35 | #include 36 | #include 37 | 38 | char *values[] = {"XXXX", "1234", "abcd", "xjvef", "NDSS"}; 39 | #define NUM_VALUES 5 40 | 41 | List *create_words() 42 | { 43 | int i = 0; 44 | List *words = List_create(); 45 | 46 | for(i = 0; i < NUM_VALUES; i++) { 47 | List_push(words, values[i]); 48 | } 49 | 50 | return words; 51 | } 52 | 53 | int is_sorted(List *words) 54 | { 55 | LIST_FOREACH(words, first, next, cur) { 56 | if(cur->next && strcmp(cur->value, cur->next->value) > 0) { 57 | debug("%s %s", (char *)cur->value, (char *)cur->next->value); 58 | return 0; 59 | } 60 | } 61 | 62 | return 1; 63 | } 64 | 65 | char *test_bubble_sort() 66 | { 67 | List *words = create_words(); 68 | 69 | // should work on a list that needs sorting 70 | int rc = List_bubble_sort(words, (List_compare)strcmp); 71 | mu_assert(rc == 0, "Bubble sort failed."); 72 | mu_assert(is_sorted(words), "Words are not sorted after bubble sort."); 73 | 74 | // should work on an already sorted list 75 | rc = List_bubble_sort(words, (List_compare)strcmp); 76 | mu_assert(rc == 0, "Bubble sort of already sorted failed."); 77 | mu_assert(is_sorted(words), "Words should be sort if already bubble sorted."); 78 | 79 | List_destroy(words); 80 | 81 | // should work on an empty list 82 | words = List_create(words); 83 | rc = List_bubble_sort(words, (List_compare)strcmp); 84 | mu_assert(rc == 0, "Bubble sort failed on empty list."); 85 | mu_assert(is_sorted(words), "Words should be sorted if empty."); 86 | 87 | List_destroy(words); 88 | 89 | return NULL; 90 | } 91 | 92 | char *test_merge_sort() 93 | { 94 | List *words = create_words(); 95 | 96 | // should work on a list that needs sorting 97 | List *res = List_merge_sort(words, (List_compare)strcmp); 98 | mu_assert(is_sorted(res), "Words are not sorted after merge sort."); 99 | 100 | List *res2 = List_merge_sort(res, (List_compare)strcmp); 101 | mu_assert(is_sorted(res), "Should still be sorted after merge sort."); 102 | List_destroy(res2); 103 | List_destroy(res); 104 | 105 | List_destroy(words); 106 | return NULL; 107 | } 108 | 109 | 110 | char *all_tests() 111 | { 112 | mu_suite_start(); 113 | 114 | mu_run_test(test_bubble_sort); 115 | mu_run_test(test_merge_sort); 116 | 117 | return NULL; 118 | } 119 | 120 | RUN_TESTS(all_tests); 121 | ``` 122 | 123 | 建议你从冒泡排序开始,使它正确,之后再测试归并。我所做的就是编写函数原型和主干,让这三个文件能够编译,但不能通过测试。之后你将实现填充进入之后才能够工作。 124 | 125 | ## 实现 126 | 127 | 你作弊了吗?之后的练习中,我只会给你单元测试,并且让自己实现它。对于你来说,不看这段代码知道你自己实现它是一种很好的练习。下面是`list_algos.c`和`list_algos.h`的代码: 128 | 129 | ```c 130 | #ifndef lcthw_List_algos_h 131 | #define lcthw_List_algos_h 132 | 133 | #include 134 | 135 | typedef int (*List_compare)(const void *a, const void *b); 136 | 137 | int List_bubble_sort(List *list, List_compare cmp); 138 | 139 | List *List_merge_sort(List *list, List_compare cmp); 140 | 141 | #endif 142 | ``` 143 | 144 | ```c 145 | #include 146 | #include 147 | 148 | inline void ListNode_swap(ListNode *a, ListNode *b) 149 | { 150 | void *temp = a->value; 151 | a->value = b->value; 152 | b->value = temp; 153 | } 154 | 155 | int List_bubble_sort(List *list, List_compare cmp) 156 | { 157 | int sorted = 1; 158 | 159 | if(List_count(list) <= 1) { 160 | return 0; // already sorted 161 | } 162 | 163 | do { 164 | sorted = 1; 165 | LIST_FOREACH(list, first, next, cur) { 166 | if(cur->next) { 167 | if(cmp(cur->value, cur->next->value) > 0) { 168 | ListNode_swap(cur, cur->next); 169 | sorted = 0; 170 | } 171 | } 172 | } 173 | } while(!sorted); 174 | 175 | return 0; 176 | } 177 | 178 | inline List *List_merge(List *left, List *right, List_compare cmp) 179 | { 180 | List *result = List_create(); 181 | void *val = NULL; 182 | 183 | while(List_count(left) > 0 || List_count(right) > 0) { 184 | if(List_count(left) > 0 && List_count(right) > 0) { 185 | if(cmp(List_first(left), List_first(right)) <= 0) { 186 | val = List_shift(left); 187 | } else { 188 | val = List_shift(right); 189 | } 190 | 191 | List_push(result, val); 192 | } else if(List_count(left) > 0) { 193 | val = List_shift(left); 194 | List_push(result, val); 195 | } else if(List_count(right) > 0) { 196 | val = List_shift(right); 197 | List_push(result, val); 198 | } 199 | } 200 | 201 | return result; 202 | } 203 | 204 | List *List_merge_sort(List *list, List_compare cmp) 205 | { 206 | if(List_count(list) <= 1) { 207 | return list; 208 | } 209 | 210 | List *left = List_create(); 211 | List *right = List_create(); 212 | int middle = List_count(list) / 2; 213 | 214 | LIST_FOREACH(list, first, next, cur) { 215 | if(middle > 0) { 216 | List_push(left, cur->value); 217 | } else { 218 | List_push(right, cur->value); 219 | } 220 | 221 | middle--; 222 | } 223 | 224 | List *sort_left = List_merge_sort(left, cmp); 225 | List *sort_right = List_merge_sort(right, cmp); 226 | 227 | if(sort_left != left) List_destroy(left); 228 | if(sort_right != right) List_destroy(right); 229 | 230 | return List_merge(sort_left, sort_right, cmp); 231 | } 232 | ``` 233 | 234 | 冒泡排序并不难以理解,虽然它非常慢。归并排序更为复杂,实话讲如果我想要牺牲可读性的话,我会花一点时间来优化代码。 235 | 236 | 归并排序有另一种“自底向上”的实现方式,但是它太难了,我就没有选择它。就像我刚才说的那样,在链表上编写排序算法没有什么意思。你可以把时间都花在使它更快,它比起其他可排序的数据结构会相当版。链表的本质决定了如果你需要对数据进行排序,你就不要使用它们(尤其是单向的)。 237 | 238 | ## 你会看到什么 239 | 240 | 如果一切都正常工作,你会看到这些: 241 | 242 | ```sh 243 | $ make clean all 244 | rm -rf build src/lcthw/list.o src/lcthw/list_algos.o tests/list_algos_tests tests/list_tests 245 | rm -f tests/tests.log 246 | find . -name "*.gc*" -exec rm {} \; 247 | rm -rf `find . -name "*.dSYM" -print` 248 | cc -g -O2 -Wall -Wextra -Isrc -rdynamic -DNDEBUG -fPIC -c -o src/lcthw/list.o src/lcthw/list.c 249 | cc -g -O2 -Wall -Wextra -Isrc -rdynamic -DNDEBUG -fPIC -c -o src/lcthw/list_algos.o src/lcthw/list_algos.c 250 | ar rcs build/liblcthw.a src/lcthw/list.o src/lcthw/list_algos.o 251 | ranlib build/liblcthw.a 252 | cc -shared -o build/liblcthw.so src/lcthw/list.o src/lcthw/list_algos.o 253 | cc -g -O2 -Wall -Wextra -Isrc -rdynamic -DNDEBUG build/liblcthw.a tests/list_algos_tests.c -o tests/list_algos_tests 254 | cc -g -O2 -Wall -Wextra -Isrc -rdynamic -DNDEBUG build/liblcthw.a tests/list_tests.c -o tests/list_tests 255 | sh ./tests/runtests.sh 256 | Running unit tests: 257 | ---- 258 | RUNNING: ./tests/list_algos_tests 259 | ALL TESTS PASSED 260 | Tests run: 2 261 | tests/list_algos_tests PASS 262 | ---- 263 | RUNNING: ./tests/list_tests 264 | ALL TESTS PASSED 265 | Tests run: 6 266 | tests/list_tests PASS 267 | $ 268 | ``` 269 | 270 | 这个练习之后我就不会向你展示这样的输出了,除非有必要向你展示它的工作原理。你应该能知道我运行了测试,并且通过了所有测试。 271 | 272 | ## 如何改进 273 | 274 | 退回去查看算法描述,有一些方法可用于改进这些实现,其中一些是很显然的: 275 | 276 | + 归并排序做了大量的链表复制和创建操作,寻找减少它们的办法。 277 | + 归并排序的维基百科描述提到了一些优化,实现它们。 278 | + 你能使用`List_split`和`List_join`(如果你实现了的话)来改进归并排序嘛? 279 | + 浏览所有防御性编程原则,检查并提升这一实现的健壮性,避免`NULL`指针,并且创建一个可选的调试级别的不变量,在排序后实现`is_sorted`的功能。 280 | 281 | ## 附加题 282 | 283 | + 创建单元测试来比较这两个算法的性能。你需要`man 3 time`来查询基本的时间函数,并且需要运行足够的迭代次数,至少以几秒钟作为样本。 284 | + 改变需要排序的链表中的数据总量,看看耗时如何变化。 285 | + 寻找方法来创建不同长度的随机链表,并且测量需要多少时间,之后将它可视化并与算法的描述对比。 286 | + 尝试解释为什么对链表排序十分麻烦。 287 | + 实现`List_insert_sorted`(有序链表),它使用`List_compare`,接收一个值,将其插入到正确的位置,使链表有序。它与创建链表后再进行排序相比怎么样? 288 | + 尝试实现维基百科上“自底向上”的归并排序。上面的代码已经是C写的了,所以很容易重新创建,但是要试着理解它的工作原理,并与这里的低效版本对比。 289 | -------------------------------------------------------------------------------- /ex36.md: -------------------------------------------------------------------------------- 1 | # 练习36:更安全的字符串 2 | 3 | > 原文:[Exercise 36: Safer Strings](http://c.learncodethehardway.org/book/ex36.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 我已经在练习26中,构建`devpkg`的时候介绍了[Better String](http://bstring.sourceforge.net/)库。这个练习让你从现在开始熟悉`bstring`库,并且明白C风格字符串为什么十分糟糕。之后你需要修改`liblcthw`的代码来使用`bstring`。 8 | 9 | ## 为什么C风格字符串十分糟糕 10 | 11 | 当人们谈论C的问题时,“字符串”的概念永远是首要缺陷之一。你已经用过它们,并且我也谈论过它们的种种缺陷,但是对为什么C字符串拥有缺陷,以及为什么一直是这样没有明确的解释。我会试着现在做出解释,部分原因是C风格字符串经过数十年的使用,有足够的证据表明它们是个非常糟糕的东西。 12 | 13 | 对于给定的任何C风格字符串,都不可能验证它是否有效。 14 | 15 | + 以`'\0'`结尾的C字符串是有效的。 16 | + 任何处理无效C字符串的循环都是无限的(或者造成缓冲区溢出)。 17 | + C字符串没有确定的长度,所以检查它们的唯一方法就是遍历它来观察循环是否正确终止。 18 | + 所以,不通过有限的循环就不可能验证C字符串。 19 | 20 | 这个逻辑非常简单。你不能编写一个循环来验证C字符串是否有效,因为无效的字符串导致循环永远不会停止。就是这样,唯一的解决方案就是包含大小。一旦你知道了大小,你可以避免无限循环问题。如果你观察练习27中我向你展示的两个函数: 21 | 22 | > 译者注:检验C风格字符串是否有效等价于“停机问题”,这是一个非常著名的不可解问题。 23 | 24 | ```c 25 | void copy(char to[], char from[]) 26 | { 27 | int i = 0; 28 | 29 | // while loop will not end if from isn't '\0' terminated 30 | while((to[i] = from[i]) != '\0') { 31 | ++i; 32 | } 33 | } 34 | 35 | int safercopy(int from_len, char *from, int to_len, char *to) 36 | { 37 | int i = 0; 38 | int max = from_len > to_len - 1 ? to_len - 1 : from_len; 39 | 40 | // to_len must have at least 1 byte 41 | if(from_len < 0 || to_len <= 0) return -1; 42 | 43 | for(i = 0; i < max; i++) { 44 | to[i] = from[i]; 45 | } 46 | 47 | to[to_len - 1] = '\0'; 48 | 49 | return i; 50 | } 51 | ``` 52 | 53 | 想象你想要向`copy`函数添加检查来确保`from`字符串有效。你该怎么做呢?你编写了一个循环来检查字符串是否已`'\0'`结尾。哦,等一下,如果字符串不以`'\0'`结尾,那它怎么让循环停下?不可能停下,所以无解。 54 | 55 | 无论你怎么做,你都不能在不知道字符串长度的情况下检查C字符串的有效性,这里`safercopy`包含了程度。这个函数没有相同的问题,因为他的循环一定会中止,即使你传入了错误的大小,大小也是有限的。 56 | 57 | > 译者注:但是问题来了,对于一个C字符串,你怎么获取其大小?你需要在这个函数之前调用`strlen`,又是一个无限循环问题。 58 | 59 | 于是,`bstring`库所做的事情就是创建一个结构体,它总是包含字符串长度。由于这个长度对于`bstring`来说总是可访问的,它上面的所有操作都会更安全。循环是有限的,内容也是有效的,并且这个主要的缺陷也不存在了。BString库也带有大量所需的字串操作,比如分割、格式化、搜索,并且大多数都会正确并安全地执行。 60 | 61 | `bstring`中也可能有缺陷,但是经过这么长时间,可能性已经很低了。`glibc`中也有缺陷,所以你让程序员怎么做才好呢? 62 | 63 | ## 使用 bstrlib 64 | 65 | 有很多改进后的字符串库,但是我最喜欢`bstrlib`,因为它只有一个程序集,并且具有大多数所需的字符串功能。你已经在使用它了,所以这个练习中你需要从[Better String](http://bstring.sourceforge.net/)获取两个文件,`bstrlib.c`和`bstrlib.h`。 66 | 67 | 下面是我在`liblcthw`项目目录里所做的事情: 68 | 69 | ```sh 70 | $ mkdir bstrlib 71 | $ cd bstrlib/ 72 | $ unzip ~/Downloads/bstrlib-05122010.zip 73 | Archive: /Users/zedshaw/Downloads/bstrlib-05122010.zip 74 | ... 75 | $ ls 76 | bsafe.c bstraux.c bstrlib.h bstrwrap.h license.txt test.cpp 77 | bsafe.h bstraux.h bstrlib.txt cpptest.cpp porting.txt testaux.c 78 | bstest.c bstrlib.c bstrwrap.cpp gpl.txt security.txt 79 | $ mv bstrlib.h bstrlib.c ../src/lcthw/ 80 | $ cd ../ 81 | $ rm -rf bstrlib 82 | # make the edits 83 | $ vim src/lcthw/bstrlib.c 84 | $ make clean all 85 | ... 86 | $ 87 | ``` 88 | 在第14行你可以看到,我编辑了`bstrlib.c`文件,来将它移动到新的位置,并且修复OSX上的bug。下面是差异: 89 | 90 | ```diff 91 | 25c25 92 | < #include "bstrlib.h" 93 | --- 94 | > #include 95 | 2759c2759 96 | < #ifdef __GNUC__ 97 | --- 98 | > #if defined(__GNUC__) && !defined(__APPLE__) 99 | ``` 100 | 101 | 我把包含修改为``,然后修复2759行`ifdef`的问题。 102 | 103 | ## 学习使用该库 104 | 105 | 这个练习很短,只是让你准备好剩余的练习,它们会用到这个库。接下来两个联系中,我会使用`bstrlib.c`来创建Hashmap`数据结构。 106 | 107 | 你现在应该阅读头文件和实现,之后编写`tests/bstr_tests.c`来测试下列函数,来熟悉这个库: 108 | 109 | `bfromcstr` 110 | 111 | 从C风格字符串中创建一个`bstring`。 112 | 113 | `blk2bstr` 114 | 115 | 与上面相同,但是可以提供缓冲区长度。 116 | 117 | `bstrcpy` 118 | 119 | 复制`bstring`。 120 | 121 | `bassign` 122 | 123 | 将一个`bstring`赋值为另一个。 124 | 125 | `bassigncstr` 126 | 127 | 将`bsting`的内容设置为C字符串的内容。 128 | 129 | `bassignblk` 130 | 131 | 将`bsting`的内容设置为C字符串的内容,但是可以提供长度。 132 | 133 | `bdestroy` 134 | 135 | 销毁`bstring`。 136 | 137 | `bconcat` 138 | 139 | 在一个`bstring`末尾连接另一个。 140 | 141 | `bstricmp` 142 | 143 | 比较两个`bstring`,返回值与`strcmp`相同。 144 | 145 | `biseq` 146 | 147 | 检查两个`bstring`是否相等。 148 | 149 | `binstr` 150 | 151 | 判断一个`bstring`是否被包含于另一个。 152 | 153 | `bfindreplace` 154 | 155 | 在一个`bstring`中寻找另一个,并且将其替换为别的。 156 | 157 | `bsplit` 158 | 159 | 将`bstring`分割为`bstrList`。 160 | 161 | `bformat` 162 | 163 | 执行字符串格式化,十分便利。 164 | 165 | `blength` 166 | 167 | 获取`bstring`的长度。 168 | 169 | `bdata` 170 | 171 | 获取`bstring`的数据。 172 | 173 | `bchar` 174 | 175 | 获得`bstring`中的字符。 176 | 177 | 你的测试应该覆盖到所有这些操作,以及你从头文件中发现的更多有趣的东西。在`valgrind`下运行测试,确保内存使用正确。 178 | -------------------------------------------------------------------------------- /ex38.md: -------------------------------------------------------------------------------- 1 | # 练习38:哈希算法 2 | 3 | > 原文:[Exercise 38: Hashmap Algorithms](http://c.learncodethehardway.org/book/ex38.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 你需要在这个练习中实现下面这三个哈希函数: 8 | 9 | FNV-1a 10 | 11 | 以创造者Glenn Fowler、Phong Vo 和 Landon Curt Noll的名字命名。这个算法产生合理的数值并且相当快。 12 | 13 | Adler-32 14 | 15 | 以Mark Adler命名。一个比较糟糕的算法,但是由来已久并且适于学习。 16 | 17 | DJB Hash 18 | 19 | 由Dan J. Bernstein (DJB)发明的哈希算法,但是难以找到这个算法的讨论。它非常快,但是结果不是很好。 20 | 21 | 你应该看到我使用了Jenkins hash作为`Hashmap`数据结构的默认哈希函数,所以这个练习的重点会放在这三个新的函数上。它们的代码通常来说不多,并且没有任何优化。像往常一样我会放慢速度来让你理解。 22 | 23 | 头文件非常简单,所以我以它开始: 24 | 25 | ```c 26 | #ifndef hashmap_algos_h 27 | #define hashmap_algos_h 28 | 29 | #include 30 | 31 | uint32_t Hashmap_fnv1a_hash(void *data); 32 | 33 | uint32_t Hashmap_adler32_hash(void *data); 34 | 35 | uint32_t Hashmap_djb_hash(void *data); 36 | 37 | #endif 38 | ``` 39 | 40 | 我只是声明了三个函数,我会在`hashmap_algos.c`文件中实现它们: 41 | 42 | ```c 43 | #include 44 | #include 45 | 46 | // settings taken from 47 | // http://www.isthe.com/chongo/tech/comp/fnv/index.html#FNV-param 48 | const uint32_t FNV_PRIME = 16777619; 49 | const uint32_t FNV_OFFSET_BASIS = 2166136261; 50 | 51 | uint32_t Hashmap_fnv1a_hash(void *data) 52 | { 53 | bstring s = (bstring)data; 54 | uint32_t hash = FNV_OFFSET_BASIS; 55 | int i = 0; 56 | 57 | for(i = 0; i < blength(s); i++) { 58 | hash ^= bchare(s, i, 0); 59 | hash *= FNV_PRIME; 60 | } 61 | 62 | return hash; 63 | } 64 | 65 | const int MOD_ADLER = 65521; 66 | 67 | uint32_t Hashmap_adler32_hash(void *data) 68 | { 69 | bstring s = (bstring)data; 70 | uint32_t a = 1, b = 0; 71 | int i = 0; 72 | 73 | for (i = 0; i < blength(s); i++) 74 | { 75 | a = (a + bchare(s, i, 0)) % MOD_ADLER; 76 | b = (b + a) % MOD_ADLER; 77 | } 78 | 79 | return (b << 16) | a; 80 | } 81 | 82 | uint32_t Hashmap_djb_hash(void *data) 83 | { 84 | bstring s = (bstring)data; 85 | uint32_t hash = 5381; 86 | int i = 0; 87 | 88 | for(i = 0; i < blength(s); i++) { 89 | hash = ((hash << 5) + hash) + bchare(s, i, 0); /* hash * 33 + c */ 90 | } 91 | 92 | return hash; 93 | } 94 | ``` 95 | 96 | 这个文件中有三个哈希函数。你应该注意到我默认使用`bstring`作为键,并且使用了`bchare`函数从字符串获取字符,然而如果字符超出了字符串的长度会返回0。 97 | 98 | 这些算法中每个都可以在网上搜索到,所以你需要搜索它们并阅读相关内容。同时我主要使用维基百科上的结果,之后参照了其它来源。 99 | 100 | 接着我为每个算法编写了单元测试,同时也测试了它们在多个桶中的分布情况。 101 | 102 | ```c 103 | #include 104 | #include 105 | #include 106 | #include 107 | #include "minunit.h" 108 | 109 | struct tagbstring test1 = bsStatic("test data 1"); 110 | struct tagbstring test2 = bsStatic("test data 2"); 111 | struct tagbstring test3 = bsStatic("xest data 3"); 112 | 113 | char *test_fnv1a() 114 | { 115 | uint32_t hash = Hashmap_fnv1a_hash(&test1); 116 | mu_assert(hash != 0, "Bad hash."); 117 | 118 | hash = Hashmap_fnv1a_hash(&test2); 119 | mu_assert(hash != 0, "Bad hash."); 120 | 121 | hash = Hashmap_fnv1a_hash(&test3); 122 | mu_assert(hash != 0, "Bad hash."); 123 | 124 | return NULL; 125 | } 126 | 127 | char *test_adler32() 128 | { 129 | uint32_t hash = Hashmap_adler32_hash(&test1); 130 | mu_assert(hash != 0, "Bad hash."); 131 | 132 | hash = Hashmap_adler32_hash(&test2); 133 | mu_assert(hash != 0, "Bad hash."); 134 | 135 | hash = Hashmap_adler32_hash(&test3); 136 | mu_assert(hash != 0, "Bad hash."); 137 | 138 | return NULL; 139 | } 140 | 141 | char *test_djb() 142 | { 143 | uint32_t hash = Hashmap_djb_hash(&test1); 144 | mu_assert(hash != 0, "Bad hash."); 145 | 146 | hash = Hashmap_djb_hash(&test2); 147 | mu_assert(hash != 0, "Bad hash."); 148 | 149 | hash = Hashmap_djb_hash(&test3); 150 | mu_assert(hash != 0, "Bad hash."); 151 | 152 | return NULL; 153 | } 154 | 155 | #define BUCKETS 100 156 | #define BUFFER_LEN 20 157 | #define NUM_KEYS BUCKETS * 1000 158 | enum { ALGO_FNV1A, ALGO_ADLER32, ALGO_DJB}; 159 | 160 | int gen_keys(DArray *keys, int num_keys) 161 | { 162 | int i = 0; 163 | FILE *urand = fopen("/dev/urandom", "r"); 164 | check(urand != NULL, "Failed to open /dev/urandom"); 165 | 166 | struct bStream *stream = bsopen((bNread)fread, urand); 167 | check(stream != NULL, "Failed to open /dev/urandom"); 168 | 169 | bstring key = bfromcstr(""); 170 | int rc = 0; 171 | 172 | // FNV1a histogram 173 | for(i = 0; i < num_keys; i++) { 174 | rc = bsread(key, stream, BUFFER_LEN); 175 | check(rc >= 0, "Failed to read from /dev/urandom."); 176 | 177 | DArray_push(keys, bstrcpy(key)); 178 | } 179 | 180 | bsclose(stream); 181 | fclose(urand); 182 | return 0; 183 | 184 | error: 185 | return -1; 186 | } 187 | 188 | void destroy_keys(DArray *keys) 189 | { 190 | int i = 0; 191 | for(i = 0; i < NUM_KEYS; i++) { 192 | bdestroy(DArray_get(keys, i)); 193 | } 194 | 195 | DArray_destroy(keys); 196 | } 197 | 198 | void fill_distribution(int *stats, DArray *keys, Hashmap_hash hash_func) 199 | { 200 | int i = 0; 201 | uint32_t hash = 0; 202 | 203 | for(i = 0; i < DArray_count(keys); i++) { 204 | hash = hash_func(DArray_get(keys, i)); 205 | stats[hash % BUCKETS] += 1; 206 | } 207 | 208 | } 209 | 210 | char *test_distribution() 211 | { 212 | int i = 0; 213 | int stats[3][BUCKETS] = {{0}}; 214 | DArray *keys = DArray_create(0, NUM_KEYS); 215 | 216 | mu_assert(gen_keys(keys, NUM_KEYS) == 0, "Failed to generate random keys."); 217 | 218 | fill_distribution(stats[ALGO_FNV1A], keys, Hashmap_fnv1a_hash); 219 | fill_distribution(stats[ALGO_ADLER32], keys, Hashmap_adler32_hash); 220 | fill_distribution(stats[ALGO_DJB], keys, Hashmap_djb_hash); 221 | 222 | fprintf(stderr, "FNV\tA32\tDJB\n"); 223 | 224 | for(i = 0; i < BUCKETS; i++) { 225 | fprintf(stderr, "%d\t%d\t%d\n", 226 | stats[ALGO_FNV1A][i], 227 | stats[ALGO_ADLER32][i], 228 | stats[ALGO_DJB][i]); 229 | } 230 | 231 | destroy_keys(keys); 232 | 233 | return NULL; 234 | } 235 | 236 | char *all_tests() 237 | { 238 | mu_suite_start(); 239 | 240 | mu_run_test(test_fnv1a); 241 | mu_run_test(test_adler32); 242 | mu_run_test(test_djb); 243 | mu_run_test(test_distribution); 244 | 245 | return NULL; 246 | } 247 | 248 | RUN_TESTS(all_tests); 249 | ``` 250 | 251 | 我在代码中将`BUCKETS`的值设置得非常高,因为我的电脑足够快。如果你将它和`NUM_KEYS`调低,就会比较慢了。这个测试运行之后,对于每个哈希函数,通过使用R语言做统计分析,可以观察键的分布情况。 252 | 253 | 我实现它的方式是使用`gen_keys`函数生成键的大型列表。这些键从`/dev/urandom`设备中获得,它们是一些随机的字节。之后我使用了这些键来调用`fill_distribution`,填充了`stats `数组,这些键计算哈希值后会被放入理论上的一些桶中。所有这类函数会遍历所有键,计算哈希,之后执行类似`Hashmap`所做的事情来寻找正确的桶。 254 | 255 | 最后我只是简单打印出一个三列的表格,包含每个桶的最终数量,展示了每个桶中随机储存了多少个键。之后可以观察这些数值,来判断这些哈希函数是否合理对键进行分配。 256 | 257 | ## 你会看到什么 258 | 259 | 教授R是这本书范围之外的内容,但是如果你想试试它,可以访问[r-project.org](http://www.r-project.org/)。 260 | 261 | 下面是一个简略的shell会话,向你展示了我如何运行`1tests/hashmap_algos_test`来获取`test_distribution`产生的表(这里没有展示),之后使用R来观察统计结果: 262 | 263 | ```sh 264 | $ tests/hashmap_algos_tests 265 | # copy-paste the table it prints out 266 | $ vim hash.txt 267 | $ R 268 | > hash <- read.table("hash.txt", header=T) 269 | > summary(hash) 270 | FNV A32 DJB 271 | Min. : 945 Min. : 908.0 Min. : 927 272 | 1st Qu.: 980 1st Qu.: 980.8 1st Qu.: 979 273 | Median : 998 Median :1000.0 Median : 998 274 | Mean :1000 Mean :1000.0 Mean :1000 275 | 3rd Qu.:1016 3rd Qu.:1019.2 3rd Qu.:1021 276 | Max. :1072 Max. :1075.0 Max. :1082 277 | ``` 278 | 279 | 首先我只是运行测试,它会在屏幕上打印表格。之后我将它复制粘贴到下来并使用`vim hash.txt`来储存数据。如果你观察数据,它会带有显示这三个算法的`FNV A32 DJB`表头。 280 | 281 | 接着,我运行R来使用`read.table`命令加载数据集。它是个非常智能的函数,适用于这种tab分隔的数据,我只要告诉它`header=T`,它就知道数据集中带有表头。 282 | 283 | 最后,我家在了数据并且可以使用`summary`来打印出它每行的统计结果。这里你可以看到每个函数处理随机数据实际上都没有问题。我会解释每个行的意义: 284 | 285 | Min. 286 | 287 | 它是列出数据的最小值。FNV似乎在这方面是最优的,因为它有最大的结果,也就是说它的下界最严格。 288 | 289 | 1st Qu. 290 | 291 | 数据的第一个四分位点。 292 | 293 | Median 294 | 295 | 如果你对它们排序,这个数值就是最重点的那个数。中位数比起均值来讲更有用一些。 296 | 297 | Mean 298 | 299 | 均值对大多数人意味着“平均”,它是数据的总数比数量。如果你观察它们,所有均值都是1000,这非常棒。如果你将它去中位数对比,你会发现,这三个中位数都很接近均值。这就意味着这些数据都没有“偏向”一端,所以均值是可信的。 300 | 301 | 3rd Qu. 302 | 303 | 数据后四分之一的起始点,代表了尾部的数值。 304 | 305 | Max. 306 | 307 | 这是数据中的最大值,代表了它们的上界。 308 | 309 | 观察这些数据,你会发现这些哈希算法似乎都适用于随机的键,并且均值与我设置的`NUM_KEYS`匹配。我所要找的就是如果我为每个桶中生成了1000个键,那么平均每个桶中就应该有100个键。如果哈希函数工作不正常,你会发现统计结果中均值不是1000,并且第一个和第三个四分位点非常高。一个好的哈希算法应该使平均值为1000,并且具有严格的范围。 310 | 311 | 同时,你应该明白即使在这个单元测试的不同运行之间,你的数据的大多数应该和我不同。 312 | 313 | ## 如何使它崩溃 314 | 315 | 这个练习的最后,我打算向你介绍使它崩溃的方法。我需要让你变写你能编写的最烂的哈希函数,并且我会使用数据来证明它确实很烂。你可以使用R来进行统计,就像我上面一样,但也可能你知道其他可以使用的工具来进行相同的统计操作。 316 | 317 | 这里的目标是让一个哈希函数,它表面看起来是正常的,但实际运行就得到一个糟糕的均值,并且分布广泛。这意味着你不能只让你返回1,而是需要返回一些看似正常的数值,但是分布广泛并且都填充到相同的桶中。 318 | 319 | 如果你对这四个函数之一做了一些小修改来完成任务,我会给你额外的分数。 320 | 321 | 这个练习的目的是,想像一下一些“友好”的程序员见到你并且打算改进你的哈希函数,但是实际上只是留了个把你的`Hashmap`搞砸的后门。 322 | 323 | ## 附加题 324 | 325 | + 将`hashmap.c`中的`default_hash`换成`hashmap_algos.c`中的算法之一,并且再次通过所有测试。 326 | + 向`hashmap_algos_tests.c`添加`default_hash`,并将它与其它三个哈希函数比较。 327 | + 寻找一些更多的哈希函数并添加进来,你永远都不可能找到太多的哈希函数! 328 | -------------------------------------------------------------------------------- /ex4.md: -------------------------------------------------------------------------------- 1 | # 练习4:Valgrind 介绍 2 | 3 | > 原文:[Exercise 4: Introducing Valgrind](http://c.learncodethehardway.org/book/ex4.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 现在是介绍另一个工具的时间了,在你学习C的过程中,你会时时刻刻用到它,它就是 `Valgrind`。我现在就向你介绍 `Valgrind`,是因为从现在开始你将会在“如何使它崩溃”一节中用到它。`Valgrind`是一个运行你的程序的程序,并且随后会报告所有你犯下的可怕错误。它是一款相当棒的自由软件,我在编写C代码时一直使用它。 8 | 9 | 回忆一下在上一章中,我让你移除`printf`的一个参数,来使你的代码崩溃。它打印出了一些奇怪的结果,但我并没有告诉你为什么它会这样打印。这个练习中我们要使用`Valgrind`来搞清楚为什么。 10 | 11 | > 注 12 | 13 | > 这本书的前几章讲解了一小段代码,同时掺杂了一些必要的工具,它们在本书的剩余章节会用到。这样做的原因是,阅读这本书的大多数人都不熟悉编译语言,也必然不熟悉自动化的辅助工具。通过先让你懂得如何使用`make`和`Valgrind`,我可以在后面使用它们更快地教你C语言,以及帮助你尽早找出所有的bug。 14 | 15 | > 这一章之后我就不再介绍更多的工具了,每章的内容大部分是代码,以及少量的语法。然而,我也会提及少量工具,我们可以用它来真正了解发生了什么,以及更好地了解常见的错误和问题。 16 | 17 | ## 安装 Valgrind 18 | 19 | 你可以用OS上的包管理器来安装`Valgrind`,但是我想让你学习如何从源码安装程序。这涉及到下面几个步骤: 20 | 21 | + 下载源码的归档文件来获得源码 22 | + 解压归档文件,将文件提取到你的电脑上 23 | + 运行`./configure`来建立构建所需的配置 24 | + 运行`make`来构建源码,就像之前所做的那样 25 | + 运行`sudo make install`来将它安装到你的电脑 26 | 27 | 下面是执行以上步骤的脚本,我想让你复制它: 28 | 29 | ```sh 30 | # 1) Download it (use wget if you don't have curl) 31 | curl -O http://valgrind.org/downloads/valgrind-3.6.1.tar.bz2 32 | 33 | # use md5sum to make sure it matches the one on the site 34 | md5sum valgrind-3.6.1.tar.bz2 35 | 36 | # 2) Unpack it. 37 | tar -xjvf valgrind-3.6.1.tar.bz2 38 | 39 | # cd into the newly created directory 40 | cd valgrind-3.6.1 41 | 42 | # 3) configure it 43 | ./configure 44 | 45 | # 4) make it 46 | make 47 | 48 | # 5) install it (need root) 49 | sudo make install 50 | ``` 51 | 52 | 按照这份脚本,但是如果 `Valgrind` 有新的版本请更新它。如果它不能正常执行,也请试着深入研究原因。 53 | 54 | ## 使用 Valgrind 55 | 56 | 使用 `Valgrind` 十分简单,只要执行`valgrind theprogram`,它就会运行你的程序,随后打印出你的程序运行时出现的所有错误。在这个练习中,我们会崩溃在一个错误输出上,然后会修复它。 57 | 58 | 首先,这里有一个`ex3.c`的故意出错的版本,叫做`ex4.c`。出于练习目的,将它再次输入到文件中: 59 | 60 | ```c 61 | #include 62 | 63 | /* Warning: This program is wrong on purpose. */ 64 | 65 | int main() 66 | { 67 | int age = 10; 68 | int height; 69 | 70 | printf("I am %d years old.\n"); 71 | printf("I am %d inches tall.\n", height); 72 | 73 | return 0; 74 | } 75 | ``` 76 | 77 | 你会发现,除了两个经典的错误外,其余部分都相同: 78 | 79 | + 没有初始化`height`变量 80 | + 没有将`age`变量传入第一个`printf`函数 81 | 82 | ## 你会看到什么 83 | 84 | 现在我们像通常一样构建它,但是不要直接运行,而是使用`Valgrind`来运行它(见源码:"使用Valgrind构建并运行 ex4.c"): 85 | 86 | ```sh 87 | $ make ex4 88 | cc -Wall -g ex4.c -o ex4 89 | ex4.c: In function 'main': 90 | ex4.c:10: warning: too few arguments for format 91 | ex4.c:7: warning: unused variable 'age' 92 | ex4.c:11: warning: 'height' is used uninitialized in this function 93 | $ valgrind ./ex4 94 | ==3082== Memcheck, a memory error detector 95 | ==3082== Copyright (C) 2002-2010, and GNU GPL'd, by Julian Seward et al. 96 | ==3082== Using Valgrind-3.6.0.SVN-Debian and LibVEX; rerun with -h for copyright info 97 | ==3082== Command: ./ex4 98 | ==3082== 99 | I am -16775432 years old. 100 | ==3082== Use of uninitialised value of size 8 101 | ==3082== at 0x4E730EB: _itoa_word (_itoa.c:195) 102 | ==3082== by 0x4E743D8: vfprintf (vfprintf.c:1613) 103 | ==3082== by 0x4E7E6F9: printf (printf.c:35) 104 | ==3082== by 0x40052B: main (ex4.c:11) 105 | ==3082== 106 | ==3082== Conditional jump or move depends on uninitialised value(s) 107 | ==3082== at 0x4E730F5: _itoa_word (_itoa.c:195) 108 | ==3082== by 0x4E743D8: vfprintf (vfprintf.c:1613) 109 | ==3082== by 0x4E7E6F9: printf (printf.c:35) 110 | ==3082== by 0x40052B: main (ex4.c:11) 111 | ==3082== 112 | ==3082== Conditional jump or move depends on uninitialised value(s) 113 | ==3082== at 0x4E7633B: vfprintf (vfprintf.c:1613) 114 | ==3082== by 0x4E7E6F9: printf (printf.c:35) 115 | ==3082== by 0x40052B: main (ex4.c:11) 116 | ==3082== 117 | ==3082== Conditional jump or move depends on uninitialised value(s) 118 | ==3082== at 0x4E744C6: vfprintf (vfprintf.c:1613) 119 | ==3082== by 0x4E7E6F9: printf (printf.c:35) 120 | ==3082== by 0x40052B: main (ex4.c:11) 121 | ==3082== 122 | I am 0 inches tall. 123 | ==3082== 124 | ==3082== HEAP SUMMARY: 125 | ==3082== in use at exit: 0 bytes in 0 blocks 126 | ==3082== total heap usage: 0 allocs, 0 frees, 0 bytes allocated 127 | ==3082== 128 | ==3082== All heap blocks were freed -- no leaks are possible 129 | ==3082== 130 | ==3082== For counts of detected and suppressed errors, rerun with: -v 131 | ==3082== Use --track-origins=yes to see where uninitialised values come from 132 | ==3082== ERROR SUMMARY: 4 errors from 4 contexts (suppressed: 4 from 4) 133 | $ 134 | ``` 135 | 136 | > 注 137 | 138 | > 如果你运行了`Valgrind`,它显示一些类似于`by 0x4052112: (below main) (libc-start.c:226)`的东西,而不是`main.c`中的行号,你需要使用`valgrind --track-origins=yes ./ex4`命令来运行你的`Valgrind`。由于某些原因,`valgrind`的Debian和Ubuntu上的版本会这样,但是其它的不会。 139 | 140 | 上面那段输出非常长,因为`Valgrind`在明确地告诉你程序中的每个错误都在哪儿。让我们从开头逐行分析一下(行号在左边,你可以参照): 141 | 142 | 1 143 | 144 | 你执行了通常的`make ex4`来构建它。确保你看到的`cc`命令和它一样,并且带有`-g`选项,否则`Valgrind`的输出不会带上行号。 145 | 146 | 2~6 147 | 148 | 要注意编译器也会向你报告源码的错误,它警告你“向格式化函数传入了过少的变量”,因为你忘记包含`age`变量。 149 | 150 | 7 151 | 152 | 然后使用`valgrind ./ex4`来运行程序。 153 | 154 | 8 155 | 156 | 之后`Valgrind`变得十分奇怪,并向你报错: 157 | 158 |   14~18 159 | 160 |   在`main (ex4.c:11)`(意思是文件`ex4.c`的`main`函数的第11行)的那行中,有“大小为8的未初始化的值”。你通过查看错误找到了它,并且在它下面看到了“栈踪迹”。最开始看到的那行`(ex4.c:11)`在最下面,如果你不明白哪里出错了,你可以向上看,比如`printf.c:35`。通常最下面的一行最重要(这个例子中是第18行)。 161 | 162 |   20~24 163 | 164 |   下一个错误位于 `main` 函数中的 `ex4.c:11`。`Valgrind`不喜欢这一行,它说的是一些 if 语句或者 while 循环基于一个未初始化的值,在这个例子中是`height`。 165 | 166 |   25~35 167 | 168 |   剩下的错误都大同小异,因为这个值还在继续使用。 169 | 170 | 37~46 171 | 172 | 最后程序退出了,`Valgrind`显示出一份摘要,告诉你程序有多烂。 173 | 174 | 这段信息读起来会相当多,下面是你的处理方法: 175 | 176 | + 无论什么时候你运行C程序并且使它工作,都应该使用`Valgrind`重新运行它来检查。 177 | + 对于得到的每个错误,找到“源码:行数”提示的位置,然后修复它。你可以上网搜索错误信息,来弄清楚它的意思。 178 | + 一旦你的程序在`Valgrind`下不出现任何错误信息,应该就好了。你可能学会了如何编写代码的一些技巧。 179 | 180 | 在这个练习中我并不期待你马上完全掌握`Valgrind`,但是你应该安装并且学会如何快速使用它,以便我们将它用于后面的练习。 181 | 182 | ## 附加题 183 | 184 | + 按照上面的指导,使用`Valgrind`和编译器修复这个程序。 185 | + 在互联网上查询`Valgrind`相关的资料。 186 | + 下载另一个程序并手动构建它。尝试一些你已经使用,但从来没有手动构建的程序。 187 | + 看看`Valgrind`的源码是如何在目录下组织的,并且阅读它的Makefile文件。不要担心,这对我来说没有任何意义。 188 | -------------------------------------------------------------------------------- /ex41.md: -------------------------------------------------------------------------------- 1 | # 练习41:将 Cachegrind 和 Callgrind 用于性能调优 2 | 3 | > 原文:[Exercise 41: Using Cachegrind And Callgrind For Performance Tuning](http://c.learncodethehardway.org/book/ex41.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 这个练习中,我打算上一节速成课,内容是使用`Valgrind`的两个工具`callgrind`和`cachegrind`。这两个工具会分析你程序的执行,并且告诉你哪一部分运行缓慢。这些结果非常精确,因为`Valgrind`的工作方式有助于你解决一些问题,比如执行过多的代码行,热点,内容访问问题,甚至是CPU的缓存未命中。 8 | 9 | 为了做这个练习,我打算使用`bstree_tests`单元测试,你之前用于寻找能提升算法的地方。你需要确保你这些程序的版本没有任何`valgrind`错误,并且和我的代码非常相似,因为我会使用我的代码的转储来谈论`cachegrind`和`callgrind`如何工作。 10 | 11 | ## 运行 Callgrind 12 | 13 | 为了运行Callgrind,你需要向`valgrind`传入`--tool=callgrind`选项,之后它会产生`callgrind.out.PID`文件(其中PID为所运行程序的进程PID)。一旦你这样运行了,你就可以使用一个叫做`callgrind_annotate`的工具分析`callgrind.out`文件,它会告诉你哪个函数运行中使用了最多的指令。下面是个例子,我在`bstree_tests`上运行了`callgrind`,之后得到了这个信息: 14 | 15 | ```sh 16 | $ valgrind --dsymutil=yes --tool=callgrind tests/bstree_tests 17 | ... 18 | $ callgrind_annotate callgrind.out.1232 19 | -------------------------------------------------------------------------------- 20 | Profile data file 'callgrind.out.1232' (creator: callgrind-3.7.0.SVN) 21 | -------------------------------------------------------------------------------- 22 | I1 cache: 23 | D1 cache: 24 | LL cache: 25 | Timerange: Basic block 0 - 1098689 26 | Trigger: Program termination 27 | Profiled target: tests/bstree_tests (PID 1232, part 1) 28 | Events recorded: Ir 29 | Events shown: Ir 30 | Event sort order: Ir 31 | Thresholds: 99 32 | Include dirs: 33 | User annotated: 34 | Auto-annotation: off 35 | 36 | -------------------------------------------------------------------------------- 37 | Ir 38 | -------------------------------------------------------------------------------- 39 | 4,605,808 PROGRAM TOTALS 40 | 41 | -------------------------------------------------------------------------------- 42 | Ir file:function 43 | -------------------------------------------------------------------------------- 44 | 670,486 src/lcthw/bstrlib.c:bstrcmp [tests/bstree_tests] 45 | 194,377 src/lcthw/bstree.c:BSTree_get [tests/bstree_tests] 46 | 65,580 src/lcthw/bstree.c:default_compare [tests/bstree_tests] 47 | 16,338 src/lcthw/bstree.c:BSTree_delete [tests/bstree_tests] 48 | 13,000 src/lcthw/bstrlib.c:bformat [tests/bstree_tests] 49 | 11,000 src/lcthw/bstrlib.c:bfromcstralloc [tests/bstree_tests] 50 | 7,774 src/lcthw/bstree.c:BSTree_set [tests/bstree_tests] 51 | 5,800 src/lcthw/bstrlib.c:bdestroy [tests/bstree_tests] 52 | 2,323 src/lcthw/bstree.c:BSTreeNode_create [tests/bstree_tests] 53 | 1,183 /private/tmp/pkg-build/coregrind//vg_preloaded.c:vg_cleanup_env [/usr/local/lib/valgrind/vgpreload_core-amd64-darwin.so] 54 | 55 | $ 56 | ``` 57 | 58 | 我已经移除了单元测试和`valgrind`输出,因为它们对这个练习没有用。你应该看到了`callgrind_anotate`输出,它向你展示了每个函数所运行的指令数量(`valgrind`中叫做`Ir`),由高到低排序。你通常可以忽略头文件的数据,直接跳到函数列表。 59 | 60 | > 注 61 | 62 | > 如果你获取到一堆“???:Image”的行,并且它们不是你程序中的东西,那么你读到的是OS的垃圾。只需要在末尾添加`| grep -v "???"`来过滤掉它们。 63 | 64 | 我现在可以对这个输出做个简短的分解,来找出下一步观察什么: 65 | 66 | + 每一行都列出了`Ir`序号和执行它们的`file:function `。`Ir`是指令数量,并且如果它越少就越快。这里有些复杂,但是首先要着眼于`Ir`。 67 | + 解决这个程序的方式是观察最上面的函数,之后看看你首先可以改进哪一个。这里,我可以改进`bstrcmp`或者`BStree_get`。可能以`BStree_get`开始更容易些。 68 | + 这些函数的一部分由单元测试调用,所以我可以忽略它们。类似`bformat`,`bfromcstralloc`和 `bdestroy`就是这样的函数。 69 | + 我也可以找到我可以简单地避免调用的函数。例如,或许我可以假设`BSTree`仅仅处理`bstring`键,之后我可以不使用回调系统,并且完全移除`default_compare`。 70 | 71 | 到目前为止,我只知道我打算改进`BSTree_get`,并且不是因为`BSTree_get`执行慢。这是分析的第二阶段。 72 | 73 | ## Callgrind 注解源文件 74 | 75 | 下一步我使用`callgrind_annotate`输出`bstree.c`文件,并且使用所带有的`Ir`对每一行做注解。你可以通过运行下面的命令来得到注解后的源文件: 76 | 77 | ```sh 78 | $ callgrind_annotate callgrind.out.1232 src/lcthw/bstree.c 79 | ... 80 | ``` 81 | 82 | 你的输出会是这个源文件的一个较大的转储,但是我会将它们剪切成包含`BSTree_get`和`BSTree_getnode`的部分: 83 | 84 | ```c 85 | -------------------------------------------------------------------------------- 86 | -- User-annotated source: src/lcthw/bstree.c 87 | -------------------------------------------------------------------------------- 88 | Ir 89 | 90 | 91 | 2,453 static inline BSTreeNode *BSTree_getnode(BSTree *map, BSTreeNode *node, void *key) 92 | . { 93 | 61,853 int cmp = map->compare(node->key, key); 94 | 663,908 => src/lcthw/bstree.c:default_compare (14850x) 95 | . 96 | 14,850 if(cmp == 0) { 97 | . return node; 98 | 24,794 } else if(cmp < 0) { 99 | 30,623 if(node->left) { 100 | . return BSTree_getnode(map, node->left, key); 101 | . } else { 102 | . return NULL; 103 | . } 104 | . } else { 105 | 13,146 if(node->right) { 106 | . return BSTree_getnode(map, node->right, key); 107 | . } else { 108 | . return NULL; 109 | . } 110 | . } 111 | . } 112 | . 113 | . void *BSTree_get(BSTree *map, void *key) 114 | 4,912 { 115 | 24,557 if(map->root == NULL) { 116 | 14,736 return NULL; 117 | . } else { 118 | . BSTreeNode *node = BSTree_getnode(map, map->root, key); 119 | 2,453 return node == NULL ? NULL : node->data; 120 | . } 121 | . } 122 | ``` 123 | 124 | 每一行都显示它的`Ir`(指令)数量,或者一个点(`.`)来表示它并不重要。我所要找的就是一些热点,或者带有巨大数值的`Ir`的行,它能够被优化掉。这里,第十行的输出表明,`BSTree_getnode`开销非常大的原因是它调用了`default_comapre`,它又调用了`bstrcmp`。我已经知道了`bstrcmp`是性能最差的函数,所以如果我想要改进`BSTree_getnode`的速度,我应该首先解决掉它。 125 | 126 | 之后我以相同方式查看`bstrcmp`: 127 | 128 | ```c 129 | 98,370 int bstrcmp (const_bstring b0, const_bstring b1) { 130 | . int i, v, n; 131 | . 132 | 196,740 if (b0 == NULL || b1 == NULL || b0->data == NULL || b1->data == NULL || 133 | 32,790 b0->slen < 0 || b1->slen < 0) return SHRT_MIN; 134 | 65,580 n = b0->slen; if (n > b1->slen) n = b1->slen; 135 | 89,449 if (b0->slen == b1->slen && (b0->data == b1->data || b0->slen == 0)) 136 | . return BSTR_OK; 137 | . 138 | 23,915 for (i = 0; i < n; i ++) { 139 | 163,642 v = ((char) b0->data[i]) - ((char) b1->data[i]); 140 | . if (v != 0) return v; 141 | . if (b0->data[i] == (unsigned char) '\0') return BSTR_OK; 142 | . } 143 | . 144 | . if (b0->slen > n) return 1; 145 | . if (b1->slen > n) return -1; 146 | . return BSTR_OK; 147 | . } 148 | ``` 149 | 150 | 输出中让我预料之外的事情就是`bstrcmp`最糟糕的一行并不是我想象中的字符比较。对于内存访问,顶部的防御性`if`语句将所有可能的无效变量都检查了一遍。与第十七行比较字符的语句相比,这个`if`语句进行了多于两倍的内存访问。如果我要优化`bstcmp`,我会完全把它去掉,或者在其它一些地方来执行它。 151 | 152 | 另一种选择是将这个检查改为`assert`,它只在开发时的运行中存在,之后在发布时把它去掉。我没有足够的证明来表明这行代码不适于这个数据结构,所以我可以证明移除它是可行的。 153 | 154 | 然而,我并不想弱化这个函数的防御性,来得到一些性能。在真实的性能优化环境,我会简单地把它放到列表中,之后挖掘程序中能得到的其它收益。 155 | 156 | ## 调优之道 157 | 158 | > 我们应该忽略微小的效率,对于97%的情况:过早优化是万恶之源。 159 | 160 | > -- 高德纳 161 | 162 | 在我看来,这个引述似乎忽略了一个关于性能调优的重点。在高德纳的话中,当你做性能调优时,如果你过早去做它,可能会导致各种问题。根据他的话,优化应该执行于“稍晚的某个时间”,或者这只是我的猜测。谁知道呢。 163 | 164 | 我打算澄清这个引述并不是完全错误,而是忽略了某些东西,并且我打算给出我的引述。你可以引用我的这段话: 165 | 166 | > 使用证据来寻找最大的优化并花费最少的精力。 167 | 168 | > -- 泽德 A. 肖 169 | 170 | 你什么时候优化并不重要,但是你需要弄清楚你的优化是否真正能改进软件,以及需要投入多少精力来实现它。通过证据你就可以找到代码中的位置,用一点点精力就能取得最大的提升。通常这些地方都是一些愚蠢的决定,就像`bstrcmp`试图检查任何东西不为`NULL`一样。 171 | 172 | 在某个特定时间点上,代码中需要调优的地方只剩下极其微小的优化,比如重新组织`if`语句,或者类似达夫设备这样的特殊循环。这时候,你应该停止优化,因为这是一个好机会,你可以通过重新设计软件并且避免这些事情来获得更多收益。 173 | 174 | 这是一些只想做优化的程序员没有看到的事情。许多时候,把一件事情做快的最好方法就是寻找避免它们的办法。在上面的分析中,我不打算优化`bstrcmp`,我会寻找一个不使用它的方法。也许我可以使用一种哈希算法来执行可排序的哈希计算而不是始终使用`bstrcmp`。也许我可以通过首先尝试第一个字符,如果它们不匹配就没必要调用`bstrcmp`。 175 | 176 | 如果在此之后你根本不能重新设计,那么就开始寻找微小的优化,但是要始终确保它们能够提升速度。要记住目标是使用最少的精力尽可能得到最大的效果。 177 | 178 | ## 使用 KCachegrind 179 | 180 | 这个练习最后一部分就是向你介绍一个叫做[KCachegrind](http://kcachegrind.sourceforge.net/html/Home.html)的神奇的GUI工具,用于分析`callgrind` 和 `cachegrind`的输出。我使用Linux或BSD电脑上工作时几乎都会使用它,并且我实际上为了使用`KCachegrind`而切换到Linux来编写代码。 181 | 182 | 教会你如何使用是这个练习之外的内容,你需要在这个练习之后自己学习如何用它。输出几乎是相同的,除了`KCachegrind`可以让你做这些: 183 | 184 | + 图形化地浏览源码和执行次数,并使用各种排序来搜索可优化的东西。 185 | + 分析不同的图表,来可视化地观察什么占据了大多数时间,以及它调用了什么。 186 | + 查看真实的汇编机器码输出,使你能够看到实际的指令,给你更多的线索。 187 | + 可视化地显示源码中的循环和分支的跳跃方式,便于你更容易地找到优化代码的方法。 188 | 189 | 你应该在获取、安装和玩转`KCachegrind`上花一些时间。 190 | 191 | ## 附加题 192 | 193 | + 阅读[ callgrind 手册页](http://valgrind.org/docs/manual/cl-manual.html)并且尝试一些高级选项。 194 | + 阅读[ cachegrind 手册页](http://valgrind.org/docs/manual/cg-manual.html)并且也尝试一些高级选项。 195 | + 在所有单元测试上使用`callgrind` 和 `cachegrind`,看看你能否找到可优化的地方。你找到一些预料之外的事情了吗?如果没有,你可能观察地不够仔细。 196 | + 使用 KCachegrind 并且观察它和我这里的输出有什么不同。 197 | + 现在使用这些工具来完成练习40的附加题和改进部分。 198 | -------------------------------------------------------------------------------- /ex42.md: -------------------------------------------------------------------------------- 1 | # 练习42:栈和队列 2 | 3 | > 原文:[Exercise 42: Stacks and Queues](http://c.learncodethehardway.org/book/ex42.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 到现在为止,你已经知道了大多数用于构建其它数据结构的数据结构。如果你拥有一些`List`、`DArray`、`Hashmap` 和 `Tree`,你就能用他们构造出大多数其它的任何结构。你碰到的其它任何结构要么可以用它们实现,要么是它们的变体。如果不是的话,它可能是外来的数据结构,你可能不需要它。 8 | 9 | `Stack`和`Queue`是非常简单的数据结构,它们是`List`的变体。它们是`List`的弱化或者转换形式,因为你只需要在`List`的一端放置元素。对于`Stack`,你只能能够在一段压入和弹出元素。而对于`Queue`,你只能够在开头压入元素,并在末尾弹出(或者反过来)。 10 | 11 | 我能够只通过C预处理器和两个头文件来实现这两个数据结构。我的头文件只有21行的长度,并且实现了所有`Stack`和`Queue`的操作,不带有任何神奇的定义。 12 | 13 | 我将会向你展示单元测试,你需要实现头文件来让它们正常工作。你不能创建`stack.c` 或 `queue.c`实现文件来通过测试,只能使用`stack.h` 和 `queue.h`来使测试运行。 14 | 15 | ```c 16 | #include "minunit.h" 17 | #include 18 | #include 19 | 20 | static Stack *stack = NULL; 21 | char *tests[] = {"test1 data", "test2 data", "test3 data"}; 22 | #define NUM_TESTS 3 23 | 24 | 25 | char *test_create() 26 | { 27 | stack = Stack_create(); 28 | mu_assert(stack != NULL, "Failed to create stack."); 29 | 30 | return NULL; 31 | } 32 | 33 | char *test_destroy() 34 | { 35 | mu_assert(stack != NULL, "Failed to make stack #2"); 36 | Stack_destroy(stack); 37 | 38 | return NULL; 39 | } 40 | 41 | char *test_push_pop() 42 | { 43 | int i = 0; 44 | for(i = 0; i < NUM_TESTS; i++) { 45 | Stack_push(stack, tests[i]); 46 | mu_assert(Stack_peek(stack) == tests[i], "Wrong next value."); 47 | } 48 | 49 | mu_assert(Stack_count(stack) == NUM_TESTS, "Wrong count on push."); 50 | 51 | STACK_FOREACH(stack, cur) { 52 | debug("VAL: %s", (char *)cur->value); 53 | } 54 | 55 | for(i = NUM_TESTS - 1; i >= 0; i--) { 56 | char *val = Stack_pop(stack); 57 | mu_assert(val == tests[i], "Wrong value on pop."); 58 | } 59 | 60 | mu_assert(Stack_count(stack) == 0, "Wrong count after pop."); 61 | 62 | return NULL; 63 | } 64 | 65 | char *all_tests() { 66 | mu_suite_start(); 67 | 68 | mu_run_test(test_create); 69 | mu_run_test(test_push_pop); 70 | mu_run_test(test_destroy); 71 | 72 | return NULL; 73 | } 74 | 75 | RUN_TESTS(all_tests); 76 | ``` 77 | 78 | 之后是`queue_tests.c`,几乎以相同的方式来使用`Queue`: 79 | 80 | ```c 81 | #include "minunit.h" 82 | #include 83 | #include 84 | 85 | static Queue *queue = NULL; 86 | char *tests[] = {"test1 data", "test2 data", "test3 data"}; 87 | #define NUM_TESTS 3 88 | 89 | 90 | char *test_create() 91 | { 92 | queue = Queue_create(); 93 | mu_assert(queue != NULL, "Failed to create queue."); 94 | 95 | return NULL; 96 | } 97 | 98 | char *test_destroy() 99 | { 100 | mu_assert(queue != NULL, "Failed to make queue #2"); 101 | Queue_destroy(queue); 102 | 103 | return NULL; 104 | } 105 | 106 | char *test_send_recv() 107 | { 108 | int i = 0; 109 | for(i = 0; i < NUM_TESTS; i++) { 110 | Queue_send(queue, tests[i]); 111 | mu_assert(Queue_peek(queue) == tests[0], "Wrong next value."); 112 | } 113 | 114 | mu_assert(Queue_count(queue) == NUM_TESTS, "Wrong count on send."); 115 | 116 | QUEUE_FOREACH(queue, cur) { 117 | debug("VAL: %s", (char *)cur->value); 118 | } 119 | 120 | for(i = 0; i < NUM_TESTS; i++) { 121 | char *val = Queue_recv(queue); 122 | mu_assert(val == tests[i], "Wrong value on recv."); 123 | } 124 | 125 | mu_assert(Queue_count(queue) == 0, "Wrong count after recv."); 126 | 127 | return NULL; 128 | } 129 | 130 | char *all_tests() { 131 | mu_suite_start(); 132 | 133 | mu_run_test(test_create); 134 | mu_run_test(test_send_recv); 135 | mu_run_test(test_destroy); 136 | 137 | return NULL; 138 | } 139 | 140 | RUN_TESTS(all_tests); 141 | ``` 142 | 143 | 你应该在不修改测试文件的条件下,使单元测试能够运行,并且它应该能够通过`valgrind`而没有任何内存错误。下面是当我直接运行`stack_tests`时它的样子: 144 | 145 | ```sh 146 | $ ./tests/stack_tests 147 | DEBUG tests/stack_tests.c:60: ----- RUNNING: ./tests/stack_tests 148 | ---- 149 | RUNNING: ./tests/stack_tests 150 | DEBUG tests/stack_tests.c:53: 151 | ----- test_create 152 | DEBUG tests/stack_tests.c:54: 153 | ----- test_push_pop 154 | DEBUG tests/stack_tests.c:37: VAL: test3 data 155 | DEBUG tests/stack_tests.c:37: VAL: test2 data 156 | DEBUG tests/stack_tests.c:37: VAL: test1 data 157 | DEBUG tests/stack_tests.c:55: 158 | ----- test_destroy 159 | ALL TESTS PASSED 160 | Tests run: 3 161 | $ 162 | ``` 163 | 164 | `queue_test`的输出基本一样,所以我在这里就不展示了。 165 | 166 | ## 如何改进 167 | 168 | 你可以做到的唯一真正的改进,就是把所用的`List`换成`DArray`。`Queue`数据结构难以用`DArray`实现,因为它要同时处理两端的节点。 169 | 170 | 完全在头文件中来实现它们的缺点,是你并不能够轻易地对它做性能调优。你需要使用这种技巧,建立一种以特定的方式使用`List`的“协议”。做性能调优时,如果你优化了`List`,这两种数据结构都会有所改进。 171 | 172 | ## 附加题 173 | 174 | + 使用`DArray`代替`List`实现`Stack`,并保持单元测试不变。这意味着你需要创建你自己的`STACK_FOREACH`。 175 | -------------------------------------------------------------------------------- /ex43.md: -------------------------------------------------------------------------------- 1 | # 练习43:一个简单的统计引擎 2 | 3 | > 原文:[Exercise 43: A Simple Statistics Engine](http://c.learncodethehardway.org/book/ex43.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 这是一个简单的算法,我将其用于“联机”(不储存任何样本)收集概要统计。我在任何需要执行一些统计,比如均值、标准差和求和中使用它,但是其中我并不会储存所需的全部样本。我只需要储存计算出的结果,它们仅仅含有5个数值。 8 | 9 | ## 计算标准差和均值 10 | 11 | 首先你需要一系列样本。它可以使任何事情,比如完成一个任务所需的时间,某人访问某个东西的次数,或者甚至是网站的评分。是什么并不重要,只要你能得到一些数字,并且你想要知道它们的下列概要统计值: 12 | 13 | `sum` 14 | 15 | 对所有数字求和。 16 | 17 | `sumsq`(平方和) 18 | 19 | 对所有数字求平方和。 20 | 21 | `count(n)` 22 | 23 | 求出样本数量。 24 | 25 | `min` 26 | 27 | 求出样本最小值。 28 | 29 | `max` 30 | 31 | 求出样本最大值。 32 | 33 | `mean` 34 | 35 | 求出样本的均值。它类似于但又不是中位数,但可作为中位数的估计。 36 | 37 | `stddev` 38 | 39 | 使用`$sqrt(sumsq - (sum * mean) / (n - 1) )`来计算标准差,其中`sqrt`为`math.h`头文件中的平方根。 40 | 41 | 我将会使用R来验证这些计算,因为我知道R能够计算正确。 42 | 43 | ```r 44 | > s <- runif(n=10, max=10) 45 | > s 46 | [1] 6.1061334 9.6783204 1.2747090 8.2395131 0.3333483 6.9755066 1.0626275 47 | [8] 7.6587523 4.9382973 9.5788115 48 | > summary(s) 49 | Min. 1st Qu. Median Mean 3rd Qu. Max. 50 | 0.3333 2.1910 6.5410 5.5850 8.0940 9.6780 51 | > sd(s) 52 | [1] 3.547868 53 | > sum(s) 54 | [1] 55.84602 55 | > sum(s * s) 56 | [1] 425.1641 57 | > sum(s) * mean(s) 58 | [1] 311.8778 59 | > sum(s * s) - sum(s) * mean(s) 60 | [1] 113.2863 61 | > (sum(s * s) - sum(s) * mean(s)) / (length(s) - 1) 62 | [1] 12.58737 63 | > sqrt((sum(s * s) - sum(s) * mean(s)) / (length(s) - 1)) 64 | [1] 3.547868 65 | > 66 | ``` 67 | 68 | 你并不需要懂得R,只需要看着我拆分代码来解释如何检查这些运算: 69 | 70 | lines 1-4 71 | 72 | 我使用`runit`函数来获得“随机形式”的数字分布,之后将它们打印出来。我会在接下来的单元测试中用到它。 73 | 74 | lines 5-7 75 | 76 | 这个就是概要,便于你看到R如何计算它们。 77 | 78 | lines 8-9 79 | 80 | 这是使用`sd`函数计算的`stddev`。 81 | 82 | lines 10-11 83 | 84 | 现在我开始手动进行这一计算,首先计算`sum`。 85 | 86 | lines 12-13 87 | 88 | `stddev`公式中的下一部分是`sumsq`,我可以通过`sum(s * s)`来得到,它告诉R将整个`s`列表乘以其自身,之后计算它们的`sum`。R的可以在整个数据结构上做运算,就像这样。 89 | 90 | lines 14-15 91 | 92 | 观察那个公式,我之后需要`sum`乘上`mean`,所以我执行了`sum(s) * mean(s)`。 93 | 94 | lines 16-17 95 | 96 | 我接着将`sumsq`参与运算,得到`sum(s * s) - sum(s) * mean(s)`。 97 | 98 | lines 18-19 99 | 100 | 还需要除以`n - 1`,所以我执行了`(sum(s * s) - sum(s) * mean(s)) / (length(s) - 1)`。 101 | 102 | lines 20-21 103 | 104 | 随后,我使用`sqrt`算出平方根,并得到3.547868,它符合R通过`sd`的运算结果。 105 | 106 | ## 实现 107 | 108 | 这就是计算`stddev`的方法,现在我可以编写一些简单的代码来实现这一计算。 109 | 110 | ```c 111 | #ifndef lcthw_stats_h 112 | #define lctwh_stats_h 113 | 114 | typedef struct Stats { 115 | double sum; 116 | double sumsq; 117 | unsigned long n; 118 | double min; 119 | double max; 120 | } Stats; 121 | 122 | Stats *Stats_recreate(double sum, double sumsq, unsigned long n, double min, double max); 123 | 124 | Stats *Stats_create(); 125 | 126 | double Stats_mean(Stats *st); 127 | 128 | double Stats_stddev(Stats *st); 129 | 130 | void Stats_sample(Stats *st, double s); 131 | 132 | void Stats_dump(Stats *st); 133 | 134 | #endif 135 | ``` 136 | 137 | 这里你可以看到我将所需的统计量放入一个struct,并且创建了用于处理样本和获得数值的函数。实现它只是转换数字的一个练习: 138 | 139 | ```c 140 | #include 141 | #include 142 | #include 143 | #include 144 | 145 | Stats *Stats_recreate(double sum, double sumsq, unsigned long n, double min, double max) 146 | { 147 | Stats *st = malloc(sizeof(Stats)); 148 | check_mem(st); 149 | 150 | st->sum = sum; 151 | st->sumsq = sumsq; 152 | st->n = n; 153 | st->min = min; 154 | st->max = max; 155 | 156 | return st; 157 | 158 | error: 159 | return NULL; 160 | } 161 | 162 | Stats *Stats_create() 163 | { 164 | return Stats_recreate(0.0, 0.0, 0L, 0.0, 0.0); 165 | } 166 | 167 | double Stats_mean(Stats *st) 168 | { 169 | return st->sum / st->n; 170 | } 171 | 172 | double Stats_stddev(Stats *st) 173 | { 174 | return sqrt( (st->sumsq - ( st->sum * st->sum / st->n)) / (st->n - 1) ); 175 | } 176 | 177 | void Stats_sample(Stats *st, double s) 178 | { 179 | st->sum += s; 180 | st->sumsq += s * s; 181 | 182 | if(st->n == 0) { 183 | st->min = s; 184 | st->max = s; 185 | } else { 186 | if(st->min > s) st->min = s; 187 | if(st->max < s) st->max = s; 188 | } 189 | 190 | st->n += 1; 191 | } 192 | 193 | void Stats_dump(Stats *st) 194 | { 195 | fprintf(stderr, "sum: %f, sumsq: %f, n: %ld, min: %f, max: %f, mean: %f, stddev: %f", 196 | st->sum, st->sumsq, st->n, st->min, st->max, 197 | Stats_mean(st), Stats_stddev(st)); 198 | } 199 | ``` 200 | 201 | 下面是` stats.c`中每个函数的作用: 202 | 203 | Stats_recreate 204 | 205 | 我希望从一些数据中加载这些数据,这和函数让我重新创建`Stats`结构体。 206 | 207 | Stats_create 208 | 209 | 只是以全0的值调用`Stats_recreate`。 210 | 211 | Stats_mean 212 | 213 | 使用`sum`和`n`计算均值。 214 | 215 | Stats_stddev 216 | 217 | 实现我之前的公式,唯一的不同就是我使用`t->sum / st->n`来计算均值,而不是调用`Stats_mean`。 218 | 219 | Stats_sample 220 | 221 | 它用于在`Stats`结构体中储存数值。当你向它提供数值时,它看到`n`是0,并且相应地设置`min`和`max`。之后的每次调用都会使`sum`、`sumsq`和`n`增加,并且计算出这一新的样本的`min`和`max`值。 222 | 223 | Stats_dump 224 | 225 | 简单的调试函数,用于转储统计量,便于你看到它们。 226 | 227 | 我需要干的最后一件事,就是确保这些运算正确。我打算使用我的样本,以及来自于R会话中的计算结果创建单元测试,来确保我会得到正确的结果。 228 | 229 | ```c 230 | #include "minunit.h" 231 | #include 232 | #include 233 | 234 | const int NUM_SAMPLES = 10; 235 | double samples[] = { 236 | 6.1061334, 9.6783204, 1.2747090, 8.2395131, 0.3333483, 237 | 6.9755066, 1.0626275, 7.6587523, 4.9382973, 9.5788115 238 | }; 239 | 240 | Stats expect = { 241 | .sumsq = 425.1641, 242 | .sum = 55.84602, 243 | .min = 0.333, 244 | .max = 9.678, 245 | .n = 10, 246 | }; 247 | double expect_mean = 5.584602; 248 | double expect_stddev = 3.547868; 249 | 250 | #define EQ(X,Y,N) (round((X) * pow(10, N)) == round((Y) * pow(10, N))) 251 | 252 | char *test_operations() 253 | { 254 | int i = 0; 255 | Stats *st = Stats_create(); 256 | mu_assert(st != NULL, "Failed to create stats."); 257 | 258 | for(i = 0; i < NUM_SAMPLES; i++) { 259 | Stats_sample(st, samples[i]); 260 | } 261 | 262 | Stats_dump(st); 263 | 264 | mu_assert(EQ(st->sumsq, expect.sumsq, 3), "sumsq not valid"); 265 | mu_assert(EQ(st->sum, expect.sum, 3), "sum not valid"); 266 | mu_assert(EQ(st->min, expect.min, 3), "min not valid"); 267 | mu_assert(EQ(st->max, expect.max, 3), "max not valid"); 268 | mu_assert(EQ(st->n, expect.n, 3), "max not valid"); 269 | mu_assert(EQ(expect_mean, Stats_mean(st), 3), "mean not valid"); 270 | mu_assert(EQ(expect_stddev, Stats_stddev(st), 3), "stddev not valid"); 271 | 272 | return NULL; 273 | } 274 | 275 | char *test_recreate() 276 | { 277 | Stats *st = Stats_recreate(expect.sum, expect.sumsq, expect.n, expect.min, expect.max); 278 | 279 | mu_assert(st->sum == expect.sum, "sum not equal"); 280 | mu_assert(st->sumsq == expect.sumsq, "sumsq not equal"); 281 | mu_assert(st->n == expect.n, "n not equal"); 282 | mu_assert(st->min == expect.min, "min not equal"); 283 | mu_assert(st->max == expect.max, "max not equal"); 284 | mu_assert(EQ(expect_mean, Stats_mean(st), 3), "mean not valid"); 285 | mu_assert(EQ(expect_stddev, Stats_stddev(st), 3), "stddev not valid"); 286 | 287 | return NULL; 288 | } 289 | 290 | char *all_tests() 291 | { 292 | mu_suite_start(); 293 | 294 | mu_run_test(test_operations); 295 | mu_run_test(test_recreate); 296 | 297 | return NULL; 298 | } 299 | 300 | RUN_TESTS(all_tests); 301 | ``` 302 | 303 | 这个单元测试中没什么新东西,除了`EQ`宏。我比较懒,并且不想查询比较两个`double`值的标准方法,所以我使用了这个宏。`double`的问题是等性不是完全相等,因为我使用了两个不同的系统,并带有不同的四舍五入的位数。解决方案就是判断两个数“乘以10的X次方是否相等”。 304 | 305 | 我使用`EQ`来计算数字的10的幂,之后使用`round`函数来获得证书。这是个简单的方法来四舍五入N位小数,并以整数比较结果。我确定有数以亿计的其它方法能做相同的事情,但是现在我就用这种。 306 | 307 | 预期结果储存在`Stats` `struct`中,之后我只是确保我得到的数值接近R给我的数值。 308 | 309 | ## 如何使用 310 | 311 | 你可以使用标准差和均值来决定一个新的样本是否是“有趣”的,或者你可以使用它们计算统计量的统计量。前者对于人们来说更容易理解,所以我用登录的例子来做个简短的解释。 312 | 313 | 假设你在跟踪人们花费多长时间在一台服务器上,并且你打算用统计来分析它。每次有人登录进来,你都对它们在这里的时长保持跟踪,之后调用`Stats_sample`函数。我会寻找停留“过长”时间的人,以及“过短”的人。 314 | 315 | 比起设定特殊的级别,我更倾向于将一个人的停留时间与`mean (plus or minus) 2 * stddev`这个范围进行比较。我计算出`mean`和`2 * stddev`,并且如果它们在这个范围之外,我就认为是“有趣”的。由于我使用了联机算法来维护这些统计量,所以它非常快,并且我可以使软件标记在这个范围外的用户。 316 | 317 | 这不仅仅用于找出行为异常的用户,更有助于标记一些潜在的问题,你可以查看它们来观察发生了什么。它基于所有用户的行为来计算,这也避免了你任意挑出一个数值而并不基于实际情况的问题。 318 | 319 | 你可以从中学到的通用规则是,`mean (plus or minus) 2 * stddev`是90%的值预期所属的范围预测值,任何在它之外的值都是有趣的。 320 | 321 | 第二种利用这些统计量的方式就是继续将其用于其它的`Stats`计算。基本上像通常一样使用`Stats_sample`,但是之后在`min`、`max`、`n`、`mean`和`stddev`上执行`Stats_sample`。这会提供二级的度量,并且让你对比样本的样本。 322 | 323 | 被搞晕了吗?我会以上面的例子基础,并且假设你拥有100台服务器,每台都运行一个应用。你已经在每个应用服务器上跟踪了用户的登录时长,但是你想要比较所有的这100和应用,并且标记它们当中任何登录时间过长的用户。最简单的方式就是每次有人登录进来时,计算新的登录统计量,之后将`Stats structs`的元素添加到第二个`Stats`中。 324 | 325 | 你最后应该会得到一些统计量,它们可以这样命名: 326 | 327 | 均值的均值 328 | 329 | 这是一个`Stats struct`,它向你提供所有服务器的均值的`mean`和`stddev`。你可以用全局视角来观察任何在此之外的用户或服务器。 330 | 331 | 标准差的均值 332 | 333 | 另一个`Stats struct`,计算这些服务器的分布的统计量。你之后可以分析每个服务器并且观察是否它们中的任何服务器具有异常分散的分布,通过将它们的`stddev`和这个`mean of stddevs`统计量进行对比。 334 | 335 | 你可以计算出全部统计量,但是这两个是最有用的。如果你打算监视服务器上的移除登录时间,你可以这样做: 336 | 337 | + 用户John登录并登出服务器A。获取服务器A的统计量,并更新它们。 338 | + 获取`mean of means`统计量,计算出A的均值并且将其加入样本。我叫它`m_of_m`。 339 | + 获取`mean of stddev`统计量,将A的标准差添加到样本中。我叫它` m_of_s`。 340 | + 如果A的`mean`在`m_of_m.mean + 2 * m_of_m.stddev`范围外,标记它可能存在问题。 341 | + 如果A的`stddev`在`m_of_s.mean + 2 * m_of_s.stddev`范围外,标记它可能存在行为异常。 342 | + 最后,如果John的登录时长在A的范围之外,或A的`m_of_m`范围之外,标记为有趣的。 343 | 344 | 通过计算“均值的均值”,或者“标准差的均值”,你可以以最小的执行和储存总量,有效地跟踪许多度量。 345 | 346 | ## 附加题 347 | 348 | + 将`Stats_stddev` 和 `Stats_mean`转换为`static inline`函数,放到`stats.h`文件中,而不是`stats.c`文件。 349 | + 使用这份代码来编写`string_algos_test.c`的性能测试。使它为可选的,并且运行基准测试作为一系列样本,之后报告结果。 350 | + 编写它的另一个语言的版本。确保这个版本基于我的数据正确执行。 351 | + 编写一个小型程序,它能从文件读取所有数字,并执行这些统计。 352 | + 使程序接收一个数据表,其中第一行是表头,剩下的行含有任意数量空格分隔的数值。你的程序应该按照表头中的名称,打印出每一列的统计值。 353 | -------------------------------------------------------------------------------- /ex44.md: -------------------------------------------------------------------------------- 1 | # 练习44:环形缓冲区 2 | 3 | > 原文:[Exercise 44: Ring Buffer](http://c.learncodethehardway.org/book/ex44.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 环形缓冲区在处理异步IO时非常实用。它们可以在一端接收随机长度和区间的数据,在另一端以相同长度和区间提供密致的数据块。它们是`Queue`数据结构的变体,但是它针对于字节块而不是一系列指针。这个练习中我打算向你展示`RingBuffer`的代码,并且之后你需要对它执行完整的单元测试。 8 | 9 | ```c 10 | #ifndef _lcthw_RingBuffer_h 11 | #define _lcthw_RingBuffer_h 12 | 13 | #include 14 | 15 | typedef struct { 16 | char *buffer; 17 | int length; 18 | int start; 19 | int end; 20 | } RingBuffer; 21 | 22 | RingBuffer *RingBuffer_create(int length); 23 | 24 | void RingBuffer_destroy(RingBuffer *buffer); 25 | 26 | int RingBuffer_read(RingBuffer *buffer, char *target, int amount); 27 | 28 | int RingBuffer_write(RingBuffer *buffer, char *data, int length); 29 | 30 | int RingBuffer_empty(RingBuffer *buffer); 31 | 32 | int RingBuffer_full(RingBuffer *buffer); 33 | 34 | int RingBuffer_available_data(RingBuffer *buffer); 35 | 36 | int RingBuffer_available_space(RingBuffer *buffer); 37 | 38 | bstring RingBuffer_gets(RingBuffer *buffer, int amount); 39 | 40 | #define RingBuffer_available_data(B) (((B)->end + 1) % (B)->length - (B)->start - 1) 41 | 42 | #define RingBuffer_available_space(B) ((B)->length - (B)->end - 1) 43 | 44 | #define RingBuffer_full(B) (RingBuffer_available_data((B)) - (B)->length == 0) 45 | 46 | #define RingBuffer_empty(B) (RingBuffer_available_data((B)) == 0) 47 | 48 | #define RingBuffer_puts(B, D) RingBuffer_write((B), bdata((D)), blength((D))) 49 | 50 | #define RingBuffer_get_all(B) RingBuffer_gets((B), RingBuffer_available_data((B))) 51 | 52 | #define RingBuffer_starts_at(B) ((B)->buffer + (B)->start) 53 | 54 | #define RingBuffer_ends_at(B) ((B)->buffer + (B)->end) 55 | 56 | #define RingBuffer_commit_read(B, A) ((B)->start = ((B)->start + (A)) % (B)->length) 57 | 58 | #define RingBuffer_commit_write(B, A) ((B)->end = ((B)->end + (A)) % (B)->length) 59 | 60 | #endif 61 | ``` 62 | 63 | 观察这个数据结构,你会看到它含有`buffer`、`start` 和 `end`。`RingBuffer`的所做的事情只是在`buffer`中移动`start`和`end`,所以当数据到达缓冲区末尾时还可以继续“循环”。这样就会给人一种在固定空间内无限读取的“幻觉”。接下来我创建了一些宏来基于它执行各种计算。 64 | 65 | 下面是它的实现,它是对工作原理更好的解释: 66 | 67 | ```c 68 | #undef NDEBUG 69 | #include 70 | #include 71 | #include 72 | #include 73 | #include 74 | #include 75 | 76 | RingBuffer *RingBuffer_create(int length) 77 | { 78 | RingBuffer *buffer = calloc(1, sizeof(RingBuffer)); 79 | buffer->length = length + 1; 80 | buffer->start = 0; 81 | buffer->end = 0; 82 | buffer->buffer = calloc(buffer->length, 1); 83 | 84 | return buffer; 85 | } 86 | 87 | void RingBuffer_destroy(RingBuffer *buffer) 88 | { 89 | if(buffer) { 90 | free(buffer->buffer); 91 | free(buffer); 92 | } 93 | } 94 | 95 | int RingBuffer_write(RingBuffer *buffer, char *data, int length) 96 | { 97 | if(RingBuffer_available_data(buffer) == 0) { 98 | buffer->start = buffer->end = 0; 99 | } 100 | 101 | check(length <= RingBuffer_available_space(buffer), 102 | "Not enough space: %d request, %d available", 103 | RingBuffer_available_data(buffer), length); 104 | 105 | void *result = memcpy(RingBuffer_ends_at(buffer), data, length); 106 | check(result != NULL, "Failed to write data into buffer."); 107 | 108 | RingBuffer_commit_write(buffer, length); 109 | 110 | return length; 111 | error: 112 | return -1; 113 | } 114 | 115 | int RingBuffer_read(RingBuffer *buffer, char *target, int amount) 116 | { 117 | check_debug(amount <= RingBuffer_available_data(buffer), 118 | "Not enough in the buffer: has %d, needs %d", 119 | RingBuffer_available_data(buffer), amount); 120 | 121 | void *result = memcpy(target, RingBuffer_starts_at(buffer), amount); 122 | check(result != NULL, "Failed to write buffer into data."); 123 | 124 | RingBuffer_commit_read(buffer, amount); 125 | 126 | if(buffer->end == buffer->start) { 127 | buffer->start = buffer->end = 0; 128 | } 129 | 130 | return amount; 131 | error: 132 | return -1; 133 | } 134 | 135 | bstring RingBuffer_gets(RingBuffer *buffer, int amount) 136 | { 137 | check(amount > 0, "Need more than 0 for gets, you gave: %d ", amount); 138 | check_debug(amount <= RingBuffer_available_data(buffer), 139 | "Not enough in the buffer."); 140 | 141 | bstring result = blk2bstr(RingBuffer_starts_at(buffer), amount); 142 | check(result != NULL, "Failed to create gets result."); 143 | check(blength(result) == amount, "Wrong result length."); 144 | 145 | RingBuffer_commit_read(buffer, amount); 146 | assert(RingBuffer_available_data(buffer) >= 0 && "Error in read commit."); 147 | 148 | return result; 149 | error: 150 | return NULL; 151 | } 152 | ``` 153 | 154 | 这些就是一个基本的`RingBuffer`实现的全部了。你可以从中读取和写入数据,获得它的大小和容量。也有一些缓冲区使用OS中的技巧来创建虚拟的无限存储,但它们不可移植。 155 | 156 | 由于我的`RingBuffer`处理读取和写入内存块,我要保证任何`end == start`出现的时候我都要将它们重置为0,使它们从退回缓冲区头部。在维基百科上的版本中,它并不可以写入数据块,所以只能移动`end`和`start`来转圈。为了更好地处理数据块,你需要在数据为空时移动到内部缓冲区的开头。 157 | 158 | ## 单元测试 159 | 160 | 对于你的单元测试,你需要测试尽可能多的情况。最简单的方法就是预构造不同的`RingBuffer`结构,之后手动检查函数和算数是否有效。例如,你可以构造`end`在缓冲区末尾的右边,而`start`在缓冲区范围内的`RingBuffer`,来看看它是否执行成功。 161 | 162 | ## 你会看到什么 163 | 164 | 下面是我的`ringbuffer_tests`运行结果: 165 | 166 | ```sh 167 | $ ./tests/ringbuffer_tests 168 | DEBUG tests/ringbuffer_tests.c:60: ----- RUNNING: ./tests/ringbuffer_tests 169 | ---- 170 | RUNNING: ./tests/ringbuffer_tests 171 | DEBUG tests/ringbuffer_tests.c:53: 172 | ----- test_create 173 | DEBUG tests/ringbuffer_tests.c:54: 174 | ----- test_read_write 175 | DEBUG tests/ringbuffer_tests.c:55: 176 | ----- test_destroy 177 | ALL TESTS PASSED 178 | Tests run: 3 179 | $ 180 | ``` 181 | 182 | 你应该测试至少三次来确保所有基本操作有效,并且看看在我完成之前你能测试到额外的多少东西。 183 | 184 | ## 如何改进 185 | 186 | 像往常一样,你应该为这个练习做防御性编程检查。我希望你这样做,是因为` liblcthw`的代码基本上没有做我教给你的防御型编程检查。我将它们留给你,便于你熟悉使用这些额外的检查来改进代码。 187 | 188 | 例如,这个环形缓冲区并没有过多检查每次访问是否实际上都在缓冲区内。 189 | 190 | 如果你阅读[环形缓冲区的维基百科页面](http://en.wikipedia.org/wiki/Ring_buffer),你会看到“优化的POSIX实现”,它使用POSIX特定的调用来创建一块无限的区域。研究并且在附加题中尝试实现它。 191 | 192 | ## 附加题 193 | 194 | + 创建`RingBuffer`的替代版本,使用POSIX的技巧并为其执行单元测试。 195 | + 为二者添加一个性能对比测试,通过带有随机数据和随机读写操作的模糊测试来比较两个版本。确保你你对每个版本进行了相同的操作,便于你在操作之间比较二者。 196 | + 使用`callgrind` 和 `cachegrind`比较二者的性能。 197 | -------------------------------------------------------------------------------- /ex45.md: -------------------------------------------------------------------------------- 1 | # 练习45:一个简单的TCP/IP客户端 2 | 3 | > 原文:[Exercise 45: A Simple TCP/IP Client](http://c.learncodethehardway.org/book/ex45.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 我打算使用`RingBuffer`来创建一个非常简单的小型网络测试工具,叫做`netclient`。为此我需要向`Makefile`添加一些工具,来处理`bin/`目录下的小程序。 8 | 9 | ## 扩展Makefile 10 | 11 | 首先,为程序添加一些变量,就像单元测试的`TESTS`和`TEST_SRC`变量: 12 | 13 | ```make 14 | PROGRAMS_SRC=$(wildcard bin/*.c) 15 | PROGRAMS=$(patsubst %.c,%,$(PROGRAMS_SRC)) 16 | ``` 17 | 18 | 之后你可能想要添加`PROGRAMS`到所有目标中: 19 | 20 | ```make 21 | all: $(TARGET) $(SO_TARGET) tests $(PROGRAMS) 22 | ``` 23 | 24 | 之后在`clean`目标中向`rm`那一行添加`PROGRAMS`: 25 | 26 | ```make 27 | rm -rf build $(OBJECTS) $(TESTS) $(PROGRAMS) 28 | ``` 29 | 30 | 最后你还需要在最后添加一个目标来构建它们: 31 | 32 | ```make 33 | $(PROGRAMS): CFLAGS += $(TARGET) 34 | ``` 35 | 36 | 做了这些修改你就能够将`.c`文件扔到`bin`中,并且编译它们以及为其链接库文件,就像测试那样。 37 | 38 | ## netclient 代码 39 | 40 | netclient的代码是这样的: 41 | 42 | ```c 43 | #undef NDEBUG 44 | #include 45 | #include 46 | #include 47 | #include 48 | #include 49 | #include 50 | #include 51 | #include 52 | #include 53 | #include 54 | #include 55 | #include 56 | 57 | struct tagbstring NL = bsStatic("\n"); 58 | struct tagbstring CRLF = bsStatic("\r\n"); 59 | 60 | int nonblock(int fd) { 61 | int flags = fcntl(fd, F_GETFL, 0); 62 | check(flags >= 0, "Invalid flags on nonblock."); 63 | 64 | int rc = fcntl(fd, F_SETFL, flags | O_NONBLOCK); 65 | check(rc == 0, "Can't set nonblocking."); 66 | 67 | return 0; 68 | error: 69 | return -1; 70 | } 71 | 72 | int client_connect(char *host, char *port) 73 | { 74 | int rc = 0; 75 | struct addrinfo *addr = NULL; 76 | 77 | rc = getaddrinfo(host, port, NULL, &addr); 78 | check(rc == 0, "Failed to lookup %s:%s", host, port); 79 | 80 | int sock = socket(AF_INET, SOCK_STREAM, 0); 81 | check(sock >= 0, "Cannot create a socket."); 82 | 83 | rc = connect(sock, addr->ai_addr, addr->ai_addrlen); 84 | check(rc == 0, "Connect failed."); 85 | 86 | rc = nonblock(sock); 87 | check(rc == 0, "Can't set nonblocking."); 88 | 89 | freeaddrinfo(addr); 90 | return sock; 91 | 92 | error: 93 | freeaddrinfo(addr); 94 | return -1; 95 | } 96 | 97 | int read_some(RingBuffer *buffer, int fd, int is_socket) 98 | { 99 | int rc = 0; 100 | 101 | if(RingBuffer_available_data(buffer) == 0) { 102 | buffer->start = buffer->end = 0; 103 | } 104 | 105 | if(is_socket) { 106 | rc = recv(fd, RingBuffer_starts_at(buffer), RingBuffer_available_space(buffer), 0); 107 | } else { 108 | rc = read(fd, RingBuffer_starts_at(buffer), RingBuffer_available_space(buffer)); 109 | } 110 | 111 | check(rc >= 0, "Failed to read from fd: %d", fd); 112 | 113 | RingBuffer_commit_write(buffer, rc); 114 | 115 | return rc; 116 | 117 | error: 118 | return -1; 119 | } 120 | 121 | 122 | int write_some(RingBuffer *buffer, int fd, int is_socket) 123 | { 124 | int rc = 0; 125 | bstring data = RingBuffer_get_all(buffer); 126 | 127 | check(data != NULL, "Failed to get from the buffer."); 128 | check(bfindreplace(data, &NL, &CRLF, 0) == BSTR_OK, "Failed to replace NL."); 129 | 130 | if(is_socket) { 131 | rc = send(fd, bdata(data), blength(data), 0); 132 | } else { 133 | rc = write(fd, bdata(data), blength(data)); 134 | } 135 | 136 | check(rc == blength(data), "Failed to write everything to fd: %d.", fd); 137 | bdestroy(data); 138 | 139 | return rc; 140 | 141 | error: 142 | return -1; 143 | } 144 | 145 | 146 | int main(int argc, char *argv[]) 147 | { 148 | fd_set allreads; 149 | fd_set readmask; 150 | 151 | int socket = 0; 152 | int rc = 0; 153 | RingBuffer *in_rb = RingBuffer_create(1024 * 10); 154 | RingBuffer *sock_rb = RingBuffer_create(1024 * 10); 155 | 156 | check(argc == 3, "USAGE: netclient host port"); 157 | 158 | socket = client_connect(argv[1], argv[2]); 159 | check(socket >= 0, "connect to %s:%s failed.", argv[1], argv[2]); 160 | 161 | FD_ZERO(&allreads); 162 | FD_SET(socket, &allreads); 163 | FD_SET(0, &allreads); 164 | 165 | while(1) { 166 | readmask = allreads; 167 | rc = select(socket + 1, &readmask, NULL, NULL, NULL); 168 | check(rc >= 0, "select failed."); 169 | 170 | if(FD_ISSET(0, &readmask)) { 171 | rc = read_some(in_rb, 0, 0); 172 | check_debug(rc != -1, "Failed to read from stdin."); 173 | } 174 | 175 | if(FD_ISSET(socket, &readmask)) { 176 | rc = read_some(sock_rb, socket, 0); 177 | check_debug(rc != -1, "Failed to read from socket."); 178 | } 179 | 180 | while(!RingBuffer_empty(sock_rb)) { 181 | rc = write_some(sock_rb, 1, 0); 182 | check_debug(rc != -1, "Failed to write to stdout."); 183 | } 184 | 185 | while(!RingBuffer_empty(in_rb)) { 186 | rc = write_some(in_rb, socket, 1); 187 | check_debug(rc != -1, "Failed to write to socket."); 188 | } 189 | } 190 | 191 | return 0; 192 | 193 | error: 194 | return -1; 195 | } 196 | ``` 197 | 198 | 代码中使用了`select`来处理`stdin`(文件描述符0)和用于和服务器交互的`socket`中的事件。它使用了`RingBuffer`来储存和复制数据,并且你可以认为`read_some`和`write_some`函数都是`RingBuffer`中相似函数的原型。 199 | 200 | 在这一小段代码中,可能有一些你并不知道的网络函数。当你碰到不知道的函数时,在手册页上查询它来确保你理解了它。这一小段代码可能需要让你研究用于小型服务器编程的所有C语言API。 201 | 202 | ## 你会看到什么 203 | 204 | 如果你完成了所有构建,测试的最快方式就是看看你能否从learncodethehardway.org上得到一个特殊的文件: 205 | 206 | ```sh 207 | $ 208 | $ ./bin/netclient learncodethehardway.org 80 209 | GET /ex45.txt HTTP/1.1 210 | Host: learncodethehardway.org 211 | 212 | HTTP/1.1 200 OK 213 | Date: Fri, 27 Apr 2012 00:41:25 GMT 214 | Content-Type: text/plain 215 | Content-Length: 41 216 | Last-Modified: Fri, 27 Apr 2012 00:42:11 GMT 217 | ETag: 4f99eb63-29 218 | Server: Mongrel2/1.7.5 219 | 220 | Learn C The Hard Way, Exercise 45 works. 221 | ^C 222 | $ 223 | ``` 224 | 225 | 这里我所做的事情是键入创建`/ex45.txt`的HTTP请求所需的语法,在`Host:`请求航之后,按下ENTER键来输入空行。接着我获取相应,包括响应头和内容。最后我按下CTRL-C来退出。 226 | 227 | ## 如何使它崩溃 228 | 229 | 这段代码肯定含有bug,但是当前在本书的草稿中,我会继续完成它。与此同时,尝试分析代码,并且用其它服务器来击溃它。一种叫做`netcat`的工具可以用于建立这种服务器。另一种方法就是使用`Python`或`Ruby`之类的语言创建一个简单的“垃圾服务器”,来产生垃圾数据,随机关闭连接,或者其它异常行为。 230 | 231 | 如果你找到了bug,在评论中报告它们,我会修复它。 232 | 233 | ## 附加题 234 | 235 | + 像我提到的那样,这里面有一些你不知道的函数,去查询他们。实际上,即使你知道它们也要查询。 236 | + 在`valgrind`下运行它来寻找错误。 237 | + 为函数添加各种防御性编程检查,来改进它们。 238 | + 使用`getopt`函数,运行用户提供选项来防止将`\n`转换为`\r\n`。这仅仅用于需要处理行尾的协议例如HTTP。有时你可能不想执行转换,所以要给用户一个选择。 239 | -------------------------------------------------------------------------------- /ex47.md: -------------------------------------------------------------------------------- 1 | # 练习47:一个快速的URL路由 2 | 3 | > 原文:[Exercise 47: A Fast URL Router](http://c.learncodethehardway.org/book/ex47.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 我现在打算向你展示使用`TSTree`来创建服务器中的快速URL路由。它适用于应用中的简单的URL匹配,而不是在许多Web应用框架中的更复杂(一些情况下也不必要)的路由发现功能。 8 | 9 | 我打算编程一个小型命令行工具和路由交互,他叫做`urlor`,读取简单的路由文件,之后提示用户输入要检索的URL。 10 | 11 | ```c 12 | #include 13 | #include 14 | 15 | TSTree *add_route_data(TSTree *routes, bstring line) 16 | { 17 | struct bstrList *data = bsplit(line, ' '); 18 | check(data->qty == 2, "Line '%s' does not have 2 columns", 19 | bdata(line)); 20 | 21 | routes = TSTree_insert(routes, 22 | bdata(data->entry[0]), blength(data->entry[0]), 23 | bstrcpy(data->entry[1])); 24 | 25 | bstrListDestroy(data); 26 | 27 | return routes; 28 | 29 | error: 30 | return NULL; 31 | } 32 | 33 | TSTree *load_routes(const char *file) 34 | { 35 | TSTree *routes = NULL; 36 | bstring line = NULL; 37 | FILE *routes_map = NULL; 38 | 39 | routes_map = fopen(file, "r"); 40 | check(routes_map != NULL, "Failed to open routes: %s", file); 41 | 42 | while((line = bgets((bNgetc)fgetc, routes_map, '\n')) != NULL) { 43 | check(btrimws(line) == BSTR_OK, "Failed to trim line."); 44 | routes = add_route_data(routes, line); 45 | check(routes != NULL, "Failed to add route."); 46 | bdestroy(line); 47 | } 48 | 49 | fclose(routes_map); 50 | return routes; 51 | 52 | error: 53 | if(routes_map) fclose(routes_map); 54 | if(line) bdestroy(line); 55 | 56 | return NULL; 57 | } 58 | 59 | bstring match_url(TSTree *routes, bstring url) 60 | { 61 | bstring route = TSTree_search(routes, bdata(url), blength(url)); 62 | 63 | if(route == NULL) { 64 | printf("No exact match found, trying prefix.\n"); 65 | route = TSTree_search_prefix(routes, bdata(url), blength(url)); 66 | } 67 | 68 | return route; 69 | } 70 | 71 | bstring read_line(const char *prompt) 72 | { 73 | printf("%s", prompt); 74 | 75 | bstring result = bgets((bNgetc)fgetc, stdin, '\n'); 76 | check_debug(result != NULL, "stdin closed."); 77 | 78 | check(btrimws(result) == BSTR_OK, "Failed to trim."); 79 | 80 | return result; 81 | 82 | error: 83 | return NULL; 84 | } 85 | 86 | void bdestroy_cb(void *value, void *ignored) 87 | { 88 | (void)ignored; 89 | bdestroy((bstring)value); 90 | } 91 | 92 | void destroy_routes(TSTree *routes) 93 | { 94 | TSTree_traverse(routes, bdestroy_cb, NULL); 95 | TSTree_destroy(routes); 96 | } 97 | 98 | int main(int argc, char *argv[]) 99 | { 100 | bstring url = NULL; 101 | bstring route = NULL; 102 | check(argc == 2, "USAGE: urlor "); 103 | 104 | TSTree *routes = load_routes(argv[1]); 105 | check(routes != NULL, "Your route file has an error."); 106 | 107 | while(1) { 108 | url = read_line("URL> "); 109 | check_debug(url != NULL, "goodbye."); 110 | 111 | route = match_url(routes, url); 112 | 113 | if(route) { 114 | printf("MATCH: %s == %s\n", bdata(url), bdata(route)); 115 | } else { 116 | printf("FAIL: %s\n", bdata(url)); 117 | } 118 | 119 | bdestroy(url); 120 | } 121 | 122 | destroy_routes(routes); 123 | return 0; 124 | 125 | error: 126 | destroy_routes(routes); 127 | return 1; 128 | } 129 | ``` 130 | 131 | 之后我创建了一个简单的文件,含有一些用于交互的伪造的路由: 132 | 133 | ``` 134 | / MainApp /hello Hello /hello/ Hello /signup Signup /logout Logout /album/ Album 135 | ``` 136 | 137 | ## 你会看到什么 138 | 139 | 一旦你使`urlor`工作,并且创建了路由文件,你可以尝试这样: 140 | 141 | ```sh 142 | $ ./bin/urlor urls.txt 143 | URL> / 144 | MATCH: / == MainApp 145 | URL> /hello 146 | MATCH: /hello == Hello 147 | URL> /hello/zed 148 | No exact match found, trying prefix. 149 | MATCH: /hello/zed == Hello 150 | URL> /album 151 | No exact match found, trying prefix. 152 | MATCH: /album == Album 153 | URL> /album/12345 154 | No exact match found, trying prefix. 155 | MATCH: /album/12345 == Album 156 | URL> asdfasfdasfd 157 | No exact match found, trying prefix. 158 | FAIL: asdfasfdasfd 159 | URL> /asdfasdfasf 160 | No exact match found, trying prefix. 161 | MATCH: /asdfasdfasf == MainApp 162 | URL> 163 | $ 164 | ``` 165 | 166 | 你可以看到路由系统首先尝试精确匹配,之后如果找不到的话则会尝试前缀匹配。这主要是尝试这二者的不同。根据你的URL的语义,你可能想要之中精确匹配,始终前缀匹配,或者执行二者并选出“最好”的那个。 167 | 168 | ## 如何改进 169 | 170 | URL非常古怪。因为人们想让它们神奇地处理它们的web应用所具有的,所有疯狂的事情,即使不是很合逻辑。在这个对如何将`TSTree`用作路由的简单演示中,它具有一些人们不想要的缺陷。比如,它会把`/al`匹配到`Album`,它是人们通常不想要的。它们想要`/album/*`匹配到`Album`以及`/al`匹配到404错误。 171 | 172 | 这并不难以实现,因为你可以修改前缀算法来以你想要的任何方式匹配。如果你修改了匹配算法,来寻找所有匹配的前缀,之后选出“最好”的那个,你就可以轻易做到它。这种情况下,`/al`回匹配`MainApp`或者`Album`。获得这些结果之后,就可以执行一些逻辑来决定哪个“最好”。 173 | 174 | 另一件你能在真正的路由系统里做的事情,就是使用`TSTree`来寻找所有可能的匹配,但是这些匹配是需要检查的一些模式串。在许多web应用中,有一个正则表达式的列表,用于和每个请求的URL进行匹配。匹配所有这些正则表达式非常花时间,所以你可以使用`TSTree`来通过它们的前缀寻找所有可能的结果。于是你就可以缩小模式串的范围,更快速地做尝试。 175 | 176 | 使用这种方式,你的URL会精确匹配,因为你实际上运行了正则表达式,它们匹配起来更快,因为你通过可能的前缀来查找它们。 177 | 178 | 这种算法也可用于所有需要用户可视化的灵活路由机制。域名、IP地址、包注册器和目录,文件或者URL。 179 | 180 | ## 附加题 181 | 182 | + 创建一个实际的引擎,使用`Handler`结构储存应用,而不是仅仅储存应用的字符串。这个结构储存它所绑定的URL,名称和任何需要构建实际路由系统的东西。 183 | + 将URL映射到`.so`文件而不是任意的名字,并且使用`dlopen`系统动态加载处理器,并执行它们所包含的回调。将这些回调放进你的`Handler`结构体中,之后你就用C编写了动态回调处理器系统的全部。 184 | -------------------------------------------------------------------------------- /ex5.md: -------------------------------------------------------------------------------- 1 | # 练习5:一个C程序的结构 2 | 3 | > 原文:[Exercise 5: The Structure Of A C Program](http://c.learncodethehardway.org/book/ex5.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 你已经知道了如何使用`printf`,也有了可以随意使用的一些工具,现在让我们逐行分析一个简单的C程序,以便你了解它是如何组织的。在这个程序里你会编写一些不是很熟悉的东西,我会轻松地把它们拆开。之后在后面的几章我们将会处理这些概念。 8 | 9 | ```c 10 | #include 11 | 12 | /* This is a comment. */ 13 | int main(int argc, char *argv[]) 14 | { 15 | int distance = 100; 16 | 17 | // this is also a comment 18 | printf("You are %d miles away.\n", distance); 19 | 20 | return 0; 21 | } 22 | ``` 23 | 24 | 手动输入这段代码并运行它,之后确保在`Valgrind`下不出现任何错误。你可能不会这样做,但你得习惯它。 25 | 26 | ## 你会看到什么 27 | 28 | 这真是一段无聊的输出,但是这个练习的目的是让你分析代码: 29 | 30 | ```sh 31 | $ make ex5 32 | cc -Wall -g ex5.c -o ex5 33 | $ ./ex5 34 | You are 100 miles away. 35 | $ 36 | ``` 37 | 38 | ## 分解代码 39 | 40 | 当你输出这段代码时,可能你只弄清楚了这段代码中的一小部分C语言特性。让我们快速地逐行分解它,之后我们可以做一些练习来更好地了解每一部分: 41 | 42 |   ex5.c:1 43 | 44 |   这是一个`include`,它是将一个文件的内容导入到这个文件的方式。C具有使用`.h`扩展名作为头文件的惯例。头文件中拥有一些函数的列表,这些都是你想在程序中使用的函数。 45 | 46 |   ex5.c:3 47 | 48 |   这是多行注释,你可以在`/*`和`*/`之间放置任意多行。 49 | 50 |   ex5.c:4 51 | 52 |   这是一个你遇到的更复杂的 `main` 函数。操作系统加载完你的程序,之后会运行叫做`main`的函数,这是C程序的工作方式。这个函数只需要返回`int`,并接受两个参数,一个是`int`作为命令行参数的数量,另一个是`char*`字符串的数组作为命令行参数。这是不是让人难以理解?不用担心,我们稍后会讲解它。 53 | 54 |   ex5.c:5 55 | 56 |   任何函数都以`{`字符开始,它表示“程序块”的开始。在Python中用一个`:`来表示。在其它语言中,可能需要用`begin`或者`do`来表示。 57 | 58 |   ex5.c:6 59 | 60 |   一个变量的声明和同时的赋值。你可以使用语法`type name = value;`来创建变量。在C的语句中,除了逻辑语句,都以一个`;`(分号)来结尾。 61 | 62 |   ex5.c:8 63 | 64 |   注释的另一种形式,它就像Python或Ruby的注释。它以`//`开头,直到行末结束。 65 | 66 |   ex5.c:9 67 | 68 |   调用了我们的老朋友`printf`。就像许多语言中的函数调用,使用语法`name(arg1, arg2);`。函数可以不带任何参数,也可以拥有任何数量的参数。`printf`函数是一类特别的函数,可以带可变数量的参数。我们会在之后说明。 69 | 70 |   ex5.c:11 71 | 72 |   一个`main`函数的返回语句,它会向OS提供退出值。你可能不熟悉Unix软件的返回代码,所以这个也放到后面去讲。 73 | 74 |   ex5.c:12 75 | 76 |   最后,我们以一个闭合的`}`花括号来结束了`main`函数。它就是整个程序的结尾了。 77 | 78 | 在这次分解中有大量的信息,所以你应该逐行来学习,并且确保至少掌握了背后发生了什么。你不一定了解所有东西,但是在我们继续之前,你可以猜猜它们的意思。 79 | 80 | ## 附加题 81 | 82 | + 对于每一行,写出你不理解的符号,并且看看是否能猜出它们的意思。在纸上写下你的猜测,你可以在以后检查它,看看是否正确。 83 | + 回头去看之前几个练习的源代码,并且像这样分解代码,来看看你是否了解它们。写下你不了解和不能自己解释的东西。 84 | -------------------------------------------------------------------------------- /ex6.md: -------------------------------------------------------------------------------- 1 | # 练习6:变量类型 2 | 3 | > 原文:[Exercise 6: Types Of Variables](http://c.learncodethehardway.org/book/ex6.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 你应该掌握了一个简单的C程序的结构,所以让我们执行下一步简单的操作,声明不同类型的变量。 8 | 9 | ```c 10 | include 11 | 12 | int main(int argc, char *argv[]) 13 | { 14 | int distance = 100; 15 | float power = 2.345f; 16 | double super_power = 56789.4532; 17 | char initial = 'A'; 18 | char first_name[] = "Zed"; 19 | char last_name[] = "Shaw"; 20 | 21 | printf("You are %d miles away.\n", distance); 22 | printf("You have %f levels of power.\n", power); 23 | printf("You have %f awesome super powers.\n", super_power); 24 | printf("I have an initial %c.\n", initial); 25 | printf("I have a first name %s.\n", first_name); 26 | printf("I have a last name %s.\n", last_name); 27 | printf("My whole name is %s %c. %s.\n", 28 | first_name, initial, last_name); 29 | 30 | return 0; 31 | } 32 | ``` 33 | 34 | 在这个程序中我们声明了不同类型的变量,并且使用了不同的`printf`格式化字符串来打印它们。 35 | 36 | ## 你会看到什么 37 | 38 | 你的输出应该和我的类似,你可以看到C的格式化字符串相似于Python或其它语言,很长一段时间中都是这样。 39 | 40 | ```sh 41 | $ make ex6 42 | cc -Wall -g ex6.c -o ex6 43 | $ ./ex6 44 | You are 100 miles away. 45 | You have 2.345000 levels of power. 46 | You have 56789.453200 awesome super powers. 47 | I have an initial A. 48 | I have a first name Zed. 49 | I have a last name Shaw. 50 | My whole name is Zed A. Shaw. 51 | $ 52 | ``` 53 | 54 | 你可以看到我们拥有一系列的“类型”,它们告诉编译器变量应该表示成什么,之后格式化字符串会匹配不同的类型。下面解释了它们如何匹配: 55 | 56 | 整数 57 | 58 |   使用`int`声明,使用`%d`来打印。 59 | 60 | 浮点 61 | 62 |   使用`float`或`double`声明,使用`%f`来打印。 63 | 64 | 字符 65 | 66 |   使用`char`来声明,以周围带有`'`(单引号)的单个字符来表示,使用`%c`来打印。 67 | 68 | 字符串(字符数组) 69 | 70 |   使用`char name[]`来声明,以周围带有`"`的一些字符来表示,使用`%s`来打印。 71 | 72 | 你会注意到C语言中区分单引号的`char`和双引号的`char[]`或字符串。 73 | 74 | > 注 75 | 76 | > 当我提及C语言类型时,我通常会使用`char[]`来代替整个的`char SOMENAME[]`。这不是有效的C语言代码,只是一个用于讨论类型的一个简化表达方式。 77 | 78 | ## 如何使它崩溃 79 | 80 | 你可以通过向`printf`传递错误的参数来轻易使这个程序崩溃。例如,如果你找到打印我的名字的那行,把`initial`放到`first_name`前面,你就制造了一个bug。执行上述修改编译器就会向你报错,之后运行的时候你可能会得到一个“段错误”,就像这样: 81 | 82 | ```sh 83 | $ make ex6 84 | cc -Wall -g ex6.c -o ex6 85 | ex6.c: In function 'main': 86 | ex6.c:19: warning: format '%s' expects type 'char *', but argument 2 has type 'int' 87 | ex6.c:19: warning: format '%c' expects type 'int', but argument 3 has type 'char *' 88 | $ ./ex6 89 | You are 100 miles away. 90 | You have 2.345000 levels of power. 91 | You have 56789.453125 awesome super powers. 92 | I have an initial A. 93 | I have a first name Zed. 94 | I have a last name Shaw. 95 | Segmentation fault 96 | $ 97 | ``` 98 | 99 | 在`Valgrind`下运行修改后的程序,来观察它会告诉你什么关于错误“Invalid read of size 1”的事情。 100 | 101 | ## 附加题 102 | 103 | + 寻找其他通过修改`printf`使这段C代码崩溃的方法。 104 | + 搜索“`printf`格式化”,试着使用一些高级的占位符。 105 | + 研究可以用几种方法打印数字。尝试以八进制或十六进制打印,或者其它你找到的方法。 106 | + 试着打印空字符串,即`""`。 107 | -------------------------------------------------------------------------------- /ex7.md: -------------------------------------------------------------------------------- 1 | # 练习7:更多变量和一些算术 2 | 3 | > 原文:[Exercise 7: More Variables, Some Math](http://c.learncodethehardway.org/book/ex7.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 你可以通过声明`int`,`float`,`char`和`double`类型的变量,来对它们做更多的事情,让我们来熟悉它们吧。接下来我们会在各种数学表达式中使用它们,所以我会向你介绍C的基本算术操作。 8 | 9 | ```c 10 | int main(int argc, char *argv[]) 11 | { 12 | int bugs = 100; 13 | double bug_rate = 1.2; 14 | 15 | printf("You have %d bugs at the imaginary rate of %f.\n", 16 | bugs, bug_rate); 17 | 18 | long universe_of_defects = 1L * 1024L * 1024L * 1024L; 19 | printf("The entire universe has %ld bugs.\n", 20 | universe_of_defects); 21 | 22 | double expected_bugs = bugs * bug_rate; 23 | printf("You are expected to have %f bugs.\n", 24 | expected_bugs); 25 | 26 | double part_of_universe = expected_bugs / universe_of_defects; 27 | printf("That is only a %e portion of the universe.\n", 28 | part_of_universe); 29 | 30 | // this makes no sense, just a demo of something weird 31 | char nul_byte = '\0'; 32 | int care_percentage = bugs * nul_byte; 33 | printf("Which means you should care %d%%.\n", 34 | care_percentage); 35 | 36 | return 0; 37 | } 38 | ``` 39 | 40 | 下面是这一小段无意义代码背后发生的事情: 41 | 42 |   ex7.c:1-4 43 | 44 |   C程序的通常开始。 45 | 46 |   ex7.c:5-6 47 | 48 |   为一些伪造的bug数据声明了一个`int`和一个`double`变量。 49 | 50 |   ex7.c:8-9 51 | 52 |   打印这两个变量,没有什么新东西。 53 | 54 |   ex7.c:11 55 | 56 |   使用了一个新的类型`long`来声明一个大的数值,它可以储存比较大的数。 57 | 58 |   ex7.c:12-13 59 | 60 |   使用`%ld`打印出这个变量,我们添加了个修饰符到`%d`上面。添加的"l"表示将它当作长整形打印。 61 | 62 |   ex7.c:15-17 63 | 64 |   只是更多的算术运算和打印。 65 | 66 |   ex7.c:19-21 67 | 68 |   编撰了一段你的bug率的描述,这里的计算非常不精确。结果非常小,所以我们要使用`%e`以科学记数法的形式打印它。 69 | 70 |   ex7.c:24 71 | 72 |   以特殊的语法`'\0'`声明了一个字符。这样创建了一个“空字节”字符,实际上是数字0。 73 | 74 |   ex7.c:25 75 | 76 |   使用这个字符乘上bug的数量,它产生了0,作为“有多少是你需要关心的”的结果。这条语句展示了你有时会碰到的丑陋做法。 77 | 78 |   ex7.c:26-27 79 | 80 |   将它打印出来,注意我使用了`%%`(两个百分号)来打印一个`%`字符。 81 | 82 |   ex7.c:28-30 83 | 84 |   `main`函数的结尾。 85 | 86 | 这一段代码只是个练习,它演示了许多算术运算。在最后,它也展示了许多你能在C中看到,但是其它语言中没有的技巧。对于C来说,一个“字符”同时也是一个整数,虽然它很小,但的确如此。这意味着你可以对它做算术运算,无论是好是坏,许多软件中也是这样做的。 87 | 88 | 在最后一部分中,你第一次见到C语言是如何直接访问机器的。我们会在后面的章节中深入。 89 | 90 | ## 你会看到什么 91 | 92 | 通常,你应该看到如下输出: 93 | 94 | ```sh 95 | $ make ex7 96 | cc -Wall -g ex7.c -o ex7 97 | $ ./ex7 98 | You have 100 bugs at the imaginary rate of 1.200000. 99 | The entire universe has 1073741824 bugs. 100 | You are expected to have 120.000000 bugs. 101 | That is only a 1.117587e-07 portion of the universe. 102 | Which means you should care 0%. 103 | $ 104 | ``` 105 | 106 | ## 如何使它崩溃 107 | 108 | 像之前一样,向`printf`传入错误的参数来使它崩溃。对比`%c`,看看当你使用`%s`来打印`nul_byte`变量时会发生什么。做完这些之后,在`Valgrind`下运行它看看关于你的这次尝试会输出什么。 109 | 110 | ## 附加题 111 | 112 | + 把为`universe_of_defects`赋值的数改为不同的大小,观察编译器的警告。 113 | + 这些巨大的数字实际上打印成了什么? 114 | + 将`long`改为`unsigned long`,并试着找到对它来说太大的数字。 115 | + 上网搜索`unsigned`做了什么。 116 | + 试着自己解释(在下个练习之前)为什么`char`可以和`int`相乘。 117 | -------------------------------------------------------------------------------- /ex8.md: -------------------------------------------------------------------------------- 1 | # 练习8:大小和数组 2 | 3 | > 原文:[Exercise 8: Sizes And Arrays](http://c.learncodethehardway.org/book/ex8.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 在上一个练习中你做了一些算术运算,并且使用了`'\0'`(空)字符。这对于其它语言来说非常奇怪,因为它们把“字符串”和“字节数组”看做不同的东西。但是C中的字符串就是字节数组,并且只有不同的打印函数才知道它们的不同。 8 | 9 | 在我真正解释其重要性之前,我先要介绍一些概念:`sizeof`和数组。下面是我们将要讨论的一段代码: 10 | 11 | ```c 12 | #include 13 | 14 | int main(int argc, char *argv[]) 15 | { 16 | int areas[] = {10, 12, 13, 14, 20}; 17 | char name[] = "Zed"; 18 | char full_name[] = { 19 | 'Z', 'e', 'd', 20 | ' ', 'A', '.', ' ', 21 | 'S', 'h', 'a', 'w', '\0' 22 | }; 23 | 24 | // WARNING: On some systems you may have to change the 25 | // %ld in this code to a %u since it will use unsigned ints 26 | printf("The size of an int: %ld\n", sizeof(int)); 27 | printf("The size of areas (int[]): %ld\n", 28 | sizeof(areas)); 29 | printf("The number of ints in areas: %ld\n", 30 | sizeof(areas) / sizeof(int)); 31 | printf("The first area is %d, the 2nd %d.\n", 32 | areas[0], areas[1]); 33 | 34 | printf("The size of a char: %ld\n", sizeof(char)); 35 | printf("The size of name (char[]): %ld\n", 36 | sizeof(name)); 37 | printf("The number of chars: %ld\n", 38 | sizeof(name) / sizeof(char)); 39 | 40 | printf("The size of full_name (char[]): %ld\n", 41 | sizeof(full_name)); 42 | printf("The number of chars: %ld\n", 43 | sizeof(full_name) / sizeof(char)); 44 | 45 | printf("name=\"%s\" and full_name=\"%s\"\n", 46 | name, full_name); 47 | 48 | return 0; 49 | } 50 | ``` 51 | 52 | 这段代码中我们创建了一些不同数据类型的数组。由于数组是C语言工作机制的核心,有大量的方法可以用来创建数组。我们暂且使用`type name[] = {initializer};`语法,之后我们会深入研究。这个语法的意思是,“我想要那个类型的数组并且初始化为{..}”。C语言看到它时,会做这些事情: 53 | 54 | + 查看它的类型,以第一个数组为例,它是`int`。 55 | + 查看`[]`,看到了没有提供长度。 56 | + 查看初始化表达式`{10, 12, 13, 14, 20}`,并且了解你想在数组中存放这5个整数。 57 | + 在电脑中开辟出一块空间,可以依次存放这5个整数。 58 | + 将数组命名为`areas`,也就是你想要的名字,并且在当前位置给元素赋值。 59 | 60 | 在`areas`的例子中,我们创建了一个含有5个整数的数组来存放那些数字。当它看到`char name[] = "Zed";`时,它会执行相同的步骤。我们先假设它创建了一个含有3个字符的数组,并且把字符赋值给`name`。我们创建的最后一个数组是`full_name`,但是我们用了一个比较麻烦的语法,每次用一个字符将其拼写出来。对C来说,`name`和`full_name`的方法都可以创建字符数组。 61 | 62 | 在文件的剩余部分,我们使用了`sizeof`关键字来问C语言这些东西占多少个字节。C语言无非是内存块的大小和地址以及在上面执行的操作。它向你提供了`sizeof`便于你理解它们,所以你在使用一个东西之前可以先询问它占多少空间。 63 | 64 | 这是比较麻烦的地方,所以我们先运行它,之后再解释。 65 | 66 | ## 你会看到什么 67 | 68 | ```sh 69 | $ make ex8 70 | cc -Wall -g ex8.c -o ex8 71 | $ ./ex8 72 | The size of an int: 4 73 | The size of areas (int[]): 20 74 | The number of ints in areas: 5 75 | The first area is 10, the 2nd 12. 76 | The size of a char: 1 77 | The size of name (char[]): 4 78 | The number of chars: 4 79 | The size of full_name (char[]): 12 80 | The number of chars: 12 81 | name="Zed" and full_name="Zed A. Shaw" 82 | $ 83 | ``` 84 | 85 | 现在你可以看到这些不同`printf`调用的输出,并且瞥见C语言是如何工作的。你的输出实际上可能会跟我的完全不同,因为你电脑上的整数大小可能会不一样。下面我会过一遍我的输出: 86 | 87 | > 译者注:16位机器上的`int`是16位的,不过现在16位机很少见了吧。 88 | 89 |   5 90 | 91 |   我的电脑认为`int`的大小是4个字节。你的电脑上根据位数不同可能会使用不同的大小。 92 | 93 |   6 94 | 95 |   `areas`中含有5个整数,所以我的电脑自然就需要20个字节来储存它。 96 | 97 |   7 98 | 99 |   如果我们把`areas`的大小与`int`的大小相除,我们就会得到元素数量为5。这也符合我们在初始化语句中所写的东西。 100 | 101 |   8 102 | 103 |   接着我们访问了数组,读出`areas[0]`和`areas[1]`,这也意味着C语言的数组下标是0开头的,像Python和Ruby一样。 104 | 105 |   9~11 106 | 107 |   我们对`name`数组执行同样的操作,但是注意到数组的大小有些奇怪,它占4个字节,但是我们用了三个字符来打出"Zed"。那么第四个字符是哪儿来的呢? 108 | 109 |   12~13 110 | 111 |   我们对`full_name`数组执行了相同的操作,但它是正常的。 112 | 113 |   13 114 | 115 |   最后我们打印出`name`和`full_name`,根据`printf`证明它们实际上就是“字符串”。 116 | 117 | 确保你理解了上面这些东西,并且知道这些输出对应哪些创建的变量。后面我们会在它的基础上探索更多关于数组和存储空间的事情。 118 | 119 | ## 如何使它崩溃 120 | 121 | 使这个程序崩溃非常容易,只需要尝试下面这些事情: 122 | 123 | + 将`full_name`最后的`'\0'`去掉,并重新运行它,在`valgrind`下再运行一遍。现在将`full_name`的定义从`main`函数中移到它的上面,尝试在`Valgrind`下运行它来看看是否能得到一些新的错误。有些情况下,你会足够幸运,不会得到任何错误。 124 | + 将`areas[0]`改为`areas[10]`并打印,来看看`Valgrind`会输出什么。 125 | + 尝试上述操作的不同变式,也对`name`和`full_name`执行一遍。 126 | 127 | ## 附加题 128 | 129 | + 尝试使用`areas[0] = 100;`以及相似的操作对`areas`的元素赋值。 130 | + 尝试对`name`和`full_name`的元素赋值。 131 | + 尝试将`areas`的一个元素赋值为`name`中的字符。 132 | + 上网搜索在不同的CPU上整数所占的不同大小。 133 | -------------------------------------------------------------------------------- /ex9.md: -------------------------------------------------------------------------------- 1 | # 练习9:数组和字符串 2 | 3 | > 原文:[Exercise 9: Arrays And Strings](http://c.learncodethehardway.org/book/ex9.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 上一个练习中,我们学习了如何创建基本的数组,以及数组如何映射为字符串。这个练习中我们会更加全面地展示数组和字符串的相似之处,并且深入了解更多内存布局的知识。 8 | 9 | 这个练习向你展示了C只是简单地将字符串储存为字符数组,并且在结尾加上`'\0'`(空字符)。你可能在上个练习中得到了暗示,因为我们手动这样做了。下面我会通过将它与数字数组比较,用另一种方法更清楚地实现它。 10 | 11 | ```c 12 | #include 13 | 14 | int main(int argc, char *argv[]) 15 | { 16 | int numbers[4] = {0}; 17 | char name[4] = {'a'}; 18 | 19 | // first, print them out raw 20 | printf("numbers: %d %d %d %d\n", 21 | numbers[0], numbers[1], 22 | numbers[2], numbers[3]); 23 | 24 | printf("name each: %c %c %c %c\n", 25 | name[0], name[1], 26 | name[2], name[3]); 27 | 28 | printf("name: %s\n", name); 29 | 30 | // setup the numbers 31 | numbers[0] = 1; 32 | numbers[1] = 2; 33 | numbers[2] = 3; 34 | numbers[3] = 4; 35 | 36 | // setup the name 37 | name[0] = 'Z'; 38 | name[1] = 'e'; 39 | name[2] = 'd'; 40 | name[3] = '\0'; 41 | 42 | // then print them out initialized 43 | printf("numbers: %d %d %d %d\n", 44 | numbers[0], numbers[1], 45 | numbers[2], numbers[3]); 46 | 47 | printf("name each: %c %c %c %c\n", 48 | name[0], name[1], 49 | name[2], name[3]); 50 | 51 | // print the name like a string 52 | printf("name: %s\n", name); 53 | 54 | // another way to use name 55 | char *another = "Zed"; 56 | 57 | printf("another: %s\n", another); 58 | 59 | printf("another each: %c %c %c %c\n", 60 | another[0], another[1], 61 | another[2], another[3]); 62 | 63 | return 0; 64 | } 65 | ``` 66 | 67 | 在这段代码中,我们创建了一些数组,并对数组元素赋值。在`numbers`中我们设置了一些数字,然而在`names`中我们实际上手动构造了一个字符串。 68 | 69 | ## 你会看到什么 70 | 71 | 当你运行这段代码的时候,你应该首先看到所打印的数组的内容初始化为0值,之后打印初始化后的内容: 72 | 73 | ```sh 74 | $ make ex9 75 | cc -Wall -g ex9.c -o ex9 76 | $ ./ex9 77 | numbers: 0 0 0 0 78 | name each: a 79 | name: a 80 | numbers: 1 2 3 4 81 | name each: Z e d 82 | name: Zed 83 | another: Zed 84 | another each: Z e d 85 | $ 86 | ``` 87 | 88 | 你会注意到这个程序中有一些很有趣的事情: 89 | 90 | + 我并没有提供全部的4个参数来初始化它。这是C的一个简写,如果你只提供了一个元素,剩下的都会为0。 91 | + `numbers`的每个元素被打印时,它们都输出0。 92 | + `names`的每个元素被打印时,只显示了第一个元素`'a'`,因为`'\0'`是特殊字符而不会显示。 93 | + 然后我们首次打印`names`,打印出了`"a"`,因为在初始化表达式中,`'a'`字符之后的空间都用`'\0'`填充,是以`'\0'`结尾的有效字符串。 94 | + 我们接着通过手动为每个元素赋值来建立数组,并且再次把它打印出来。看看他们发生了什么改变。现在`numbers`已经设置好了,看看`names`字符串如何正确打印出我的名字。 95 | + 创建一个字符串也有两种语法:第六行的`char name[4] = {'a'}`,或者第44行的`char *another = "name"`。前者不怎么常用,你应该将后者用于字符串字面值。 96 | 97 | 注意我使用了相同的语法和代码风格来和整数数组和字符数组交互,但是`printf`认为`name`是个字符串。再次强调,这是因为对C语言来说,字符数组和字符串没有什么不同。 98 | 99 | 最后,当你使用字符串字面值时你应该用`char *another = "Literal"`语法,它会产生相同的东西,但是更加符合语言习惯,也更省事。 100 | 101 | ## 如何使它崩溃 102 | 103 | C中所有bug的大多数来源都是忘了预留出足够的空间,或者忘了在字符串末尾加上一个`'\0'`。事实上,这些bug是非常普遍并且难以改正的,大部分优秀的C代码都不会使用C风格字符串。下一个练习中我们会学到如何彻底避免C风格字符串。 104 | 105 | 使这个程序崩溃的的关键就是拿掉字符串结尾的`'\0'`。下面是实现它的一些途径: 106 | 107 | + 删掉`name`的初始化表达式。 108 | + 无意中设置`name[3] = 'A'`,于是它就没有终止字符了。 109 | + 将初始化表达式设置为`{'a','a','a','a'}`,于是就有过多的`'a'`字符,没有办法给`'\0'`留出位置。 110 | 111 | 试着想出一些其它的办法让它崩溃,并且在`Valgrind`下像往常一样运行这个程序,你可以看到具体发生了什么,以及错误叫什么名字。有时`Valgrind`并不能发现你犯的错误,则需要移动声明这些变量的地方看看是否能找出错误。这是C的黑魔法的一部分,有时变量的位置会改变bug。 112 | 113 | ## 附加题 114 | 115 | + 将一些字符赋给`numbers`的元素,之后用`printf`一次打印一个字符,你会得到什么编译器警告? 116 | + 对`names`执行上述的相反操作,把`names`当成`int`数组,并一次打印一个`int`,`Valgrind`会提示什么? 117 | + 有多少种其它的方式可以用来打印它? 118 | + 如果一个字符数组占四个字节,一个整数也占4个字节,你可以像整数一样使用整个`name`吗?你如何用黑魔法实现它? 119 | + 拿出一张纸,将每个数组画成一排方框,之后在纸上画出代码中的操作,看看是否正确。 120 | + 将`name`转换成`another`的形式,看看代码是否能正常工作。 121 | -------------------------------------------------------------------------------- /img/qr_alipay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apachecn/lcthw-zh/477a8c2af9ccee0781abe5b7295308c0293866ef/img/qr_alipay.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
now loading...
21 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /introduction.md: -------------------------------------------------------------------------------- 1 | # 导言:C的笛卡尔之梦 2 | 3 | > 原文:[Introduction: The Cartesian Dream Of C](http://c.learncodethehardway.org/book/introduction.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | ‍ 8 | 9 | > 直到现在,凡是我当作最真实、最可靠而接受的东西,都是从感官或通过感官得来的。不过,我有时觉得这些感官是骗人的,并且为了小心谨慎起见,对于一经骗过我们的东西就决不完全加以信任。 10 | 11 | > 勒内·笛卡尔,《第一哲学沉思录》 12 | 13 | 如果有一段引述用来描述C语言编程的话,那就是它了。对于大多数程序员,C是极其可怕而且邪恶的。他就像是恶魔、撒旦,或者一个使用指针的花言巧语和对机器的直接访问来破坏你生产力的骗子洛基。于是,一旦这位计算界的路西法将你缠住,他就会使用邪恶的“段错误”来毁掉你的世界,并且揭露出与你交易中的骗局而嘲笑你。 14 | 15 | 然而,C并不应由于这些事实而受到责备。你的电脑和控制它的操作系统才是真正的骗子,而不是朋友。它们通过密谋来向你隐藏它们的真实执行逻辑,使你永远都不真正知道背后发生了什么。C编程语言的失败之处只是向你提供接触背后真正工作原理的途径,并且告诉了你一些难以接受的事实。C会向你展示痛苦的真像(红色药丸),它将幕布拉开来向你展示一些神奇的原理。C即是真理。 16 | 17 | 既然C如此危险,为什么还要使用它?因为C给了你力量来穿越抽象的假象,并且将你从愚昧中解放出来。 18 | 19 | ## 你会学到什么 20 | 21 | 这本书的目的是让你足够熟悉C语言,并能够使用它编写自己的软件,或者修改其他人的代码。这本书的最后,我们会从一本叫做“K&R C”的名著中选取实际的代码,并且用你学过的知识来做代码审查。你需要学习下面这些东西来达到这一阶段: 22 | 23 | + C的基本语法和编写习惯。 24 | + 编译,`make`文件和链接。 25 | + 寻找和预防bug。 26 | + 防御性编程实践。 27 | + 使C的代码崩溃。 28 | + 编写基本的Unix系统软件。 29 | 30 | 截至最后一章,你将会有足够的工具来解决基本的系统软件、库和其它小项目。 31 | 32 | ## 如何阅读本书 33 | 34 | 这本书为那些已经掌握至少一门编程语言的人而设计。如果你还没有接触过编程,我推荐你先学习[笨办法学Python](http://learnpythonthehardway.org/),这本书适用于真正的新手并且适合作为第一本编程书。一旦你学会了Python,你可以返回来开始学习这本书。 35 | 36 | 对于那些已经学会编程的人,这本书的开头可能有些奇怪。它不像其它书一样,那些书中你会阅读一段段的文字然后编写一些代码。相反,这本书中我会让你立即开始编程,之后我会解释你做了什么。这样更有效果,因为你已经经历过的事情解释起来更加容易。 37 | 38 | 由于采用了这样的结构,下面是本书中你必须遵守的规则: 39 | 40 | + 手动输入所有代码。不要复制粘贴! 41 | + 正确地输入所有代码,也包括注释。 42 | + 运行代码并保证产生相同的输出。 43 | + 如果出现了bug则修正它。 44 | + 做附加题时,如果你做不出某道题,马上跳过。 45 | + 在寻求帮助之前首先试着自己弄懂。 46 | 47 | 如果你遵守了这些规则,完成了本书的每一件事,并且还不会编程C代码的话,你至少尝试过了。它并不适用于每个人,但是尝试的过程会让你成为一个更好的程序员。 48 | 49 | ## 核心能力 50 | 51 | 我假设你之前使用为“弱者”设计的语言。这些“易用的”语言之一是Python或者Ruby,它们带给了你草率的思维和半吊子的黑魔法。或者,你可能使用类似Lisp的语言,它假设计算机是纯函数式的奇幻大陆,带有一些为婴儿准备的充气墙。再或者你可能学过Prolog,于是你认为整个世界都是一个数据库,你可以从中寻找线索。甚至更糟糕的是,我假设你一直都在用IDE,所以你的大脑布满了内存漏洞,并且你每打三个字符都要按CTRL+空格来打出函数的整个名字。 52 | 53 | 无论你的背景如何,你都可能不擅长下面四个技能: 54 | 55 | 阅读和编写 56 | 57 | 如果你使用IDE这会尤其正确。但是总体上我发现程序员做了很多“略读”,并且在理解上存在问题。它们会略读需要详细理解的代码,并且觉得他们已经理解了但事实上没有。其它语言提供了可以让他们避免实际编写任何代码的工具,所以面对一种类似C的语言时,他们就玩完了。你需要知道每个人都有这个问题,并且你可以通过强迫自己慢下来并且仔细对待阅读和编写代码来改正它。一开始你可能感到痛苦和无聊,但是这样的次数多了它也就变得容易了。 58 | 59 | 专注细节 60 | 61 | 每个人都不擅长这方面,它也是劣质软件的罪魁祸首。其它语言让你不会集中注意力,但是C要求你集中全部注意力,因为它直接在机器上运行,并且机器比较挑剔。C中没有“相似的类型”或者“足够接近”,所以你需要注意,再三检查你的代码,并假设你写的任何代码都是错的,直到你能证明它是对的。 62 | 63 | 定位差异 64 | 65 | 其它语言程序员的一个关键问题就是他们的大脑被训练来指出那个语言的差异,而不是C。当你对比你的代码和我练习中的代码时,你的眼睛会跳过你认为不重要或者不熟悉的字符。我会给你一些策略来强制你观察你的错误,但是要记住如果你的代码并不完全像书中的代码,它就是错的。 66 | 67 | 规划和调试 68 | 69 | 我喜欢其它较简单的语言,因为我可以想怎么写就怎么写。我将已有的想法输入进解释器,然后可以立即看到结果。你可以把你的想法试验出来,但是要注意,如果你仍然打算“试验代码使其能够工作”,它就行不通了。C对于你来说稍困难,因为你需要规划好首先创建什么。的确,你也可以进行试验,但是比起其他语言,你必须在C中更早地严肃对待代码。我会教给你在编程之前规划程序核心部分的方法,这对于使你成为更好的程序员十分有帮助。即使一个很小的规划,都会使接下来的事情变得顺利。 70 | 71 | 学习C语言会使你变成更好的程序员,因为会强制你更早、更频繁地解决这些问题。你不会再草率地编写半吊子的代码,代码也会能够正常工作。C的优势是,它是一个简单的语言,你可以自己来弄清楚,这使得它成为用于学习机器,以及提升程序员核心技能的最佳语言。 72 | 73 | C比其它语言都要难,而这是由于C并不对你隐藏细节,它们在其它语言中都试图并且未能被掩盖。 74 | 75 | ## 协议 76 | 77 | 原书在完稿之后可以自由分发,并且能在[亚马逊](http://www.amazon.com/Learn-Hard-Way-Practical-Computational/dp/0321884922/)上购买。该中译版本遵循[CC BY-NC-SA 4.0](http://creativecommons.org/licenses/by-nc-sa/4.0/)协议,你可以在保留署名和出处的前提下以非商业目的自由转载。 78 | -------------------------------------------------------------------------------- /postscript.md: -------------------------------------------------------------------------------- 1 | # “解构 K&R C” 已死 2 | 3 | > 原文:[Deconstructing K&RC Is Dead](http://c.learncodethehardway.org/book/krcritique.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 我彻底失败了。我放弃了多年以来尝试理清C语言如何编写的想法,因为它的发明是有缺陷的。起初,我的书中有一章叫做“解构 K&R C”。这一章的目的是告诉人们永远不要假设它们的代码是正确的,或者对于任何人的代码,不管它有多出名,也不能避免缺陷。这看起来似乎并不是革命性的想法,并且对我来说它只是分析代码缺陷和编写更好更可靠代码的一部分。 8 | 9 | 多年以来,我在写这本书的这一块时收到重挫,并且收到了比任何其它事情更多的批评和侮辱。不仅如此,而且书中这部分的批评以这些话而结束,“你是对的,但是你认为他们的代码很烂这件事是错的。”我不能理解,有一群被认为很聪明的人,他们的大脑中充满理性,却坚持“我可以是错的,但是同时也可以是对的”的观点。我不得不与这些学究在C IRC channels、邮件列表、评论上斗争,这包括每一个它们提出一些怪异的、迂腐的刻薄意见的情况,需要我对我的文章进行更多的逻辑性修改来说服他们。 10 | 11 | 有趣的一点是,在我写这部分之前,我收到了本书许多正面的评论。当时本书还在写作中,所以我觉得确实需要改进。我甚至设置了一些奖金让人们帮助改进。但可悲的是,一旦他们被自己的英雄蒙蔽,所崇拜的基调就发生了翻天覆地的变化。我变得十分令人讨厌,只不过是尝试教人们如何安全使用一个极易出错的垃圾语言,比如C语言。这是我很擅长的东西。 12 | 13 | 这些批评者向我承认,他们不写C代码也不教授它,他们只是死记硬背标准库来“帮助”其它人,这对我来说并不重要。我以一个开放的心态试图解决问题,甚至设置奖金给那些有助于修复它的人,这也不重要。这可以使更多的人爱上C语言,并且使其它人入门编程,这更不重要。重要的是我“侮辱”了他们的英雄,这意味着我所说的话永远地完蛋了,没有人会再次相信我。 14 | 15 | 坦率地说,这是编程文化极为的黑暗、丑陋、邪恶的一面。他们整天在说,“我与你们同在”,但是如果你不屈服于大师们海量的学识,以及乞求他们准许你质疑他们所信奉的东西,你突然就会变成敌人。程序员费尽心机地把自己放在权力的宝座上,来要求别人赞许他们高超的记忆能力,或者对一些微不足道的琐事的熟知,并且会尽全力消灭那些胆敢质疑的人。 16 | 17 | 这非常恶心,我对此也没什么能做的。我对老程序员无能为力。但他们注定会失败。它们通过标准化记忆所积累的学识,也会在咸鱼的下一次翻身中蒸发掉。它们对考虑如何事物的运作方式,以及如何改进它们,或者将它们的手艺传授给他人毫无兴趣,除非这里面涉及到大量的阿谀奉承并让他们觉得很爽。老程序员总会完蛋的。 18 | 19 | 他们向现在的年轻程序员施压,我对此并不能做任何事情。我不能阻止无能程序员的诽谤,他们甚至根本不像专业的C程序员那样。然而,我宁愿使本书有助于那些想要学习C语言以及如何编写可靠的软件的人,而不是和那些思维闭锁的保守派做斗争。它们贪图安逸的行为给人一种感觉,就是他们知道更多迂腐的、可怜的小话题,就比如未定义行为。 20 | 21 | 因此,我删除了书中的K&R C部分,并且找到了新的主题。我打算重写这本书,但是并不知道如何去做。我犹如在地狱中,因为我自己非常执着于我觉得很重要的一些事情,但我不知道如何推进。我现在算是明白了这是错的,因为它阻碍我将一些与C不相关的重要技巧教给许多新的程序员,包括编程规范、代码分析、缺陷和安全漏洞的检测,以及学习其它编程语言的方法。 22 | 23 | 现在我明白了,我将为这本书制作一些课程,关于编写最安全的C代码,以及将C语言代码打破为一种学习C和编程规范的方式。我会卑微地说我的书只是一个桥梁,所有人应该去读K&R C来迎合这些学究,并且在这些黄金法则的脚下顶礼膜拜。我要澄清我的C版本限制于一个固定的目的之中,因为这让我的代码更安全。我一定会提到所有迂腐的东西,比如每个书呆子式的,关于20世纪60年代的PDP-11电脑上空指针的要求。 24 | 25 | 之后,我会告诉人们不要再去写别的C程序。这不会很明显,完全不会,但我的目标是将人们从C带到能更好地编程的其它语言中。Go、Rust或者Swift,是我能想到的能处理C语言主要任务新型语言,所以我推荐人们学习它们。我会告诉他们,他们的技能在于发现缺陷,并且对C代码的严格分析将会对所有语言都有巨大的好处,以及使其它语言更易于学习。 26 | 27 | 但是C呢?C已经死了,它是为想要争论A.6.2章第四段的指针未定义行为的老程序员准备的。谢天谢地,我打算去学习Go(或者Rust,或者Swift,或者其它任何东西)了。 28 | -------------------------------------------------------------------------------- /preface.md: -------------------------------------------------------------------------------- 1 | # 前言 2 | 3 | > 原文:[Preface](http://c.learncodethehardway.org/book/preface.html) 4 | 5 | > 译者:[飞龙](https://github.com/wizardforcel) 6 | 7 | 这是本书创作中的转储版本,所用的措辞可能不是很好,也可能缺失了一些章节,但是你可以看到我编写这本书的过程,以及我的做事风格。 8 | 9 | 你也可以随时发送邮件到[help@learncodethehardway.org](mailto:help@learncodethehardway.org)来向我寻求帮助,我通常会在1~2天之内答复。 10 | 11 | 这个列表是一个讨论列表,并不只允许发布公告,它用于讨论本书和询问问题。 12 | 13 | 最后,不要忘了我之前写过[笨办法学Python](http://learnpythonthehardway.org/),如果你还不会编程,你应该先读完它。LCTHW并不面向初学者,而是面向至少读完LPTHW或者已经懂得一门其它编程语言的人。 14 | 15 | ## 常见问题 16 | 17 | 这门课程需要多少时间? 18 | 19 | 你应该花一些时间直到你掌握它,并且每天都要坚持编写代码。一些人花了大约三个月,其它人花了六个月,还有一些人只用了一个星期。 20 | 21 | 我需要准备什么样的电脑? 22 | 23 | 你需要OSX或者Linux来完成这本书。 24 | -------------------------------------------------------------------------------- /styles/ebook.css: -------------------------------------------------------------------------------- 1 | /* GitHub stylesheet for MarkdownPad (http://markdownpad.com) */ 2 | /* Author: Nicolas Hery - http://nicolashery.com */ 3 | /* Version: b13fe65ca28d2e568c6ed5d7f06581183df8f2ff */ 4 | /* Source: https://github.com/nicolahery/markdownpad-github */ 5 | 6 | /* RESET 7 | =============================================================================*/ 8 | 9 | html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { 10 | margin: 0; 11 | padding: 0; 12 | border: 0; 13 | } 14 | 15 | /* BODY 16 | =============================================================================*/ 17 | 18 | body { 19 | font-family: Helvetica, arial, freesans, clean, sans-serif; 20 | font-size: 14px; 21 | line-height: 1.6; 22 | color: #333; 23 | background-color: #fff; 24 | padding: 20px; 25 | max-width: 960px; 26 | margin: 0 auto; 27 | } 28 | 29 | body>*:first-child { 30 | margin-top: 0 !important; 31 | } 32 | 33 | body>*:last-child { 34 | margin-bottom: 0 !important; 35 | } 36 | 37 | /* BLOCKS 38 | =============================================================================*/ 39 | 40 | p, blockquote, ul, ol, dl, table, pre { 41 | margin: 15px 0; 42 | } 43 | 44 | /* HEADERS 45 | =============================================================================*/ 46 | 47 | h1, h2, h3, h4, h5, h6 { 48 | margin: 20px 0 10px; 49 | padding: 0; 50 | font-weight: bold; 51 | -webkit-font-smoothing: antialiased; 52 | } 53 | 54 | h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code, h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code { 55 | font-size: inherit; 56 | } 57 | 58 | h1 { 59 | font-size: 24px; 60 | border-bottom: 1px solid #ccc; 61 | color: #000; 62 | } 63 | 64 | h2 { 65 | font-size: 18px; 66 | color: #000; 67 | } 68 | 69 | h3 { 70 | font-size: 14px; 71 | } 72 | 73 | h4 { 74 | font-size: 14px; 75 | } 76 | 77 | h5 { 78 | font-size: 14px; 79 | } 80 | 81 | h6 { 82 | color: #777; 83 | font-size: 14px; 84 | } 85 | 86 | body>h2:first-child, body>h1:first-child, body>h1:first-child+h2, body>h3:first-child, body>h4:first-child, body>h5:first-child, body>h6:first-child { 87 | margin-top: 0; 88 | padding-top: 0; 89 | } 90 | 91 | a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 { 92 | margin-top: 0; 93 | padding-top: 0; 94 | } 95 | 96 | h1+p, h2+p, h3+p, h4+p, h5+p, h6+p { 97 | margin-top: 10px; 98 | } 99 | 100 | /* LINKS 101 | =============================================================================*/ 102 | 103 | a { 104 | color: #4183C4; 105 | text-decoration: none; 106 | } 107 | 108 | a:hover { 109 | text-decoration: underline; 110 | } 111 | 112 | /* LISTS 113 | =============================================================================*/ 114 | 115 | ul, ol { 116 | padding-left: 30px; 117 | } 118 | 119 | ul li > :first-child, 120 | ol li > :first-child, 121 | ul li ul:first-of-type, 122 | ol li ol:first-of-type, 123 | ul li ol:first-of-type, 124 | ol li ul:first-of-type { 125 | margin-top: 0px; 126 | } 127 | 128 | ul ul, ul ol, ol ol, ol ul { 129 | margin-bottom: 0; 130 | } 131 | 132 | dl { 133 | padding: 0; 134 | } 135 | 136 | dl dt { 137 | font-size: 14px; 138 | font-weight: bold; 139 | font-style: italic; 140 | padding: 0; 141 | margin: 15px 0 5px; 142 | } 143 | 144 | dl dt:first-child { 145 | padding: 0; 146 | } 147 | 148 | dl dt>:first-child { 149 | margin-top: 0px; 150 | } 151 | 152 | dl dt>:last-child { 153 | margin-bottom: 0px; 154 | } 155 | 156 | dl dd { 157 | margin: 0 0 15px; 158 | padding: 0 15px; 159 | } 160 | 161 | dl dd>:first-child { 162 | margin-top: 0px; 163 | } 164 | 165 | dl dd>:last-child { 166 | margin-bottom: 0px; 167 | } 168 | 169 | /* CODE 170 | =============================================================================*/ 171 | 172 | pre, code, tt { 173 | font-size: 12px; 174 | font-family: Consolas, "Liberation Mono", Courier, monospace; 175 | } 176 | 177 | code, tt { 178 | margin: 0 0px; 179 | padding: 0px 0px; 180 | white-space: nowrap; 181 | border: 1px solid #eaeaea; 182 | background-color: #f8f8f8; 183 | border-radius: 3px; 184 | } 185 | 186 | pre>code { 187 | margin: 0; 188 | padding: 0; 189 | white-space: pre; 190 | border: none; 191 | background: transparent; 192 | } 193 | 194 | pre { 195 | background-color: #f8f8f8; 196 | border: 1px solid #ccc; 197 | font-size: 13px; 198 | line-height: 19px; 199 | overflow: auto; 200 | padding: 6px 10px; 201 | border-radius: 3px; 202 | } 203 | 204 | pre code, pre tt { 205 | background-color: transparent; 206 | border: none; 207 | } 208 | 209 | kbd { 210 | -moz-border-bottom-colors: none; 211 | -moz-border-left-colors: none; 212 | -moz-border-right-colors: none; 213 | -moz-border-top-colors: none; 214 | background-color: #DDDDDD; 215 | background-image: linear-gradient(#F1F1F1, #DDDDDD); 216 | background-repeat: repeat-x; 217 | border-color: #DDDDDD #CCCCCC #CCCCCC #DDDDDD; 218 | border-image: none; 219 | border-radius: 2px 2px 2px 2px; 220 | border-style: solid; 221 | border-width: 1px; 222 | font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; 223 | line-height: 10px; 224 | padding: 1px 4px; 225 | } 226 | 227 | /* QUOTES 228 | =============================================================================*/ 229 | 230 | blockquote { 231 | border-left: 4px solid #DDD; 232 | padding: 0 15px; 233 | color: #777; 234 | } 235 | 236 | blockquote>:first-child { 237 | margin-top: 0px; 238 | } 239 | 240 | blockquote>:last-child { 241 | margin-bottom: 0px; 242 | } 243 | 244 | /* HORIZONTAL RULES 245 | =============================================================================*/ 246 | 247 | hr { 248 | clear: both; 249 | margin: 15px 0; 250 | height: 0px; 251 | overflow: hidden; 252 | border: none; 253 | background: transparent; 254 | border-bottom: 4px solid #ddd; 255 | padding: 0; 256 | } 257 | 258 | /* TABLES 259 | =============================================================================*/ 260 | 261 | table th { 262 | font-weight: bold; 263 | } 264 | 265 | table th, table td { 266 | border: 1px solid #ccc; 267 | padding: 6px 13px; 268 | } 269 | 270 | table tr { 271 | border-top: 1px solid #ccc; 272 | background-color: #fff; 273 | } 274 | 275 | table tr:nth-child(2n) { 276 | background-color: #f8f8f8; 277 | } 278 | 279 | /* IMAGES 280 | =============================================================================*/ 281 | 282 | img { 283 | max-width: 100% 284 | } -------------------------------------------------------------------------------- /update.sh: -------------------------------------------------------------------------------- 1 | git add -A 2 | git commit -am "$(date "+%Y-%m-%d %H:%M:%S")" 3 | git push --------------------------------------------------------------------------------