C_and_CPP 板


LINE

C/C++ 语言新手十三诫(The Thirteen Commandments for Newbie C/C++ Programmers) by Khoguan Phuann 请注意: (1) 本篇旨在提醒新手,避免初学常犯的错误(其实老手也常犯:-Q)。 但不能取代完整的学习,请自己好好研读一两本 C 语言的好书, 并多多实作练习。 (2) 强烈建议新手先看过此文再发问,你的问题极可能此文已经提出并 解答了。 (3) 以下所举的错误例子如果在你的电脑上印出和正确例子相同的结果, 那只是不足为恃的一时侥幸。 (4) 不守十三诫者,轻则执行结果的输出数据错误,或是程式当掉,重则 引爆核弹、毁灭地球(如果你的 C 程式是用来控制核弹发射器的话)。 ============================================================= 目录: (页码/行号) 2/24 01. 不可以使用尚未给予适当初值的变数 3/46 02. 不能存取超过阵列既定范围的空间 5/90 03. 不可以提取不知指向何方的指标 7/134 04. 不要试图用 char* 去更改一个"字串常数" 12/244 05. 不能在函式中回传一个指向区域性自动变数的指标 16/332 06. 不可以只做 malloc(), 而不做相应的 free() 19/398 07. 在数值运算、赋值或比较中不可以随意混用不同型别的数值 21/442 08. ++i/i++/--i/i--/f(&i)哪个先执行跟顺序有关 24/508 09. 慎用Macro 27/574 10. 不要在 stack 设置过大的变数以避免堆叠溢位(stack overflow) 32/684 11. 使用浮点数精确度造成的误差问题 35/750 12. 不要猜想二维阵列可以用 pointer to pointer 来传递 36/772 13. 函式内 new 出来的空间记得要让主程式的指标接住 40/860 直接输入数字可跳至该页码 或用:指令输入行号 01. 你不可以使用尚未给予适当初值的变数 错误例子: int accumulate(int max) /* 从 1 累加到 max,传回结果 */ { int sum; /* 未给予初值的区域变数,其内容值是垃圾 */ for (int num = 1; num <= max; num++) { sum += num; } return sum; } 正确例子: int accumulate(int max) { int sum = 0; /* 正确的赋予适当的初值 */ for (int num = 1; num <= max; num++) { sum += num; } return sum; } 备注:  根据 C Standard,具有静态储存期(static storage duration)的变数, 例如 全域变数(global variable)或带有 static 修饰符者等,  如果没有显式初始化的话,根据不同的资料型态予以进行以下初始化:  若变数为算术型别 (int , double , ...) 时,初始化为零或正零。  若变数为指标型别 (int*, double*, ...) 时,初始化为 null 指标。  若变数为复合型别 (struct, double _Complex, ...) 时,递回初始化所有成员。  若变数为联合型别 (union) 时,只有其中的第一个成员会被递回初始化。 (以上感谢Hazukashiine板友指正) (但是有些MCU 编译器可能不理会这个规定,所以还是请养成设定初值的好习惯) 补充资料: - z->5->1->1->1 - C11 Standard 5.1.2, 6.2.4, 6.7.9 02. 你不可以存取超过阵列既定范围的空间 错误例子: int str[5]; for (int i = 0 ; i <= 5 ; i++) str[i] = i; 正确例子: int str[5]; for (int i = 0; i < 5; i++) str[i] = i; 说明:宣告阵列时,所给的阵列元素个数值如果是 N, 那麽我们在後面 透过 [索引值] 存取其元素时,所能使用的索引值范围是从 0 到 N-1 C/C++ 为了执行效率,并不会自动检查阵列索引值是否超过阵列边界, 我们要自己来确保不会越界。一旦越界,操作的不再是合法的空间, 将导致无法预期的後果。 备注: C++11之後可以用Range-based for loop提取array、 vector(或是其他有提供正确.begin()和.end()的class)内的元素 可以确保提取的元素一定落在正确范围内。 例: // vector std::vector<int> v = {0, 1, 2, 3, 4, 5}; for(const int &i : v) // access by const reference std::cout << i << ' '; std::cout << '\n'; // array int a[] = {0, 1, 2, 3, 4, 5}; for(int n: a) // the initializer may be an array std::cout << n << ' '; std::cout << '\n'; 补充资料: http://en.cppreference.com/w/cpp/language/range-for 03. 你不可以提取(dereference)不知指向何方的指标(包含 null 指标)。 错误例子: char *pc1; /* 未给予初值,不知指向何方 */ char *pc2 = NULL; /* pc2 起始化为 null pointer */ *pc1 = 'a'; /* 将 'a' 写到不知何方,错误 */ *pc2 = 'b'; /* 将 'b' 写到「位址0」,错误 */ 正确例子: char c; /* c 的内容尚未起始化 */ char *pc1 = &c; /* pc1 指向字元变数 c */ *pc1 = 'a'; /* c 的内容变为 'a' */ /* 动态分配 10 个 char(其值未定),并将第一个char的位址赋值给 pc2 */ char *pc2 = (char *) malloc(10); pc2[0] = 'b'; /* 动态配置来的第 0 个字元,内容变为 'b' free(pc2); 说明:指标变数必需先指向某个可以合法操作的空间,才能进行操作。 ( 使用者记得要检查 malloc 回传是否为 NULL, 碍於篇幅本文假定使用上皆合法,也有正确归还记忆体 ) 错误例子: char *name; /* name 尚未指向有效的空间 */ printf("Your name, please: "); fgets(name,20,stdin); /* 您确定要写入的那块空间合法吗??? */ printf("Hello, %s\n", name); 正确例子: /* 如果编译期就能决定字串的最大空间,那就不要宣告成 char* 改用 char[] */ char name[21]; /* 可读入字串最长 20 个字元,保留一格空间放 '\0' */ printf("Your name, please: "); fgets(name,20,stdin); printf("Hello, %s\n", name); 正确例子(2): 若是在执行时期才能决定字串的最大空间,C提供两种作法: a. 利用 malloc() 函式来动态分配空间,用malloc宣告的阵列会被存在heap 须注意:若是宣告较大阵列,要确认malloc的回传值是否为NULL size_t length; printf("请输入字串的最大长度(含null字元): "); scanf("%u", &length); name = (char *)malloc(length); if (name) { // name != NULL printf("您输入的是 %u\n", length); } else { // name == NULL puts("输入值太大或系统已无足够空间"); } /* 最後记得 free() 掉 malloc() 所分配的空间 */ free(name); name = NULL; //(注1) b. C99开始可使用variable-length array (VLA) 须注意: - 因为VLA是被存放在stack里,使用前要确认array size不能太大 - 不是每个compiler都支援VLA(注2) - C++ Standard不支援(虽然有些compiler支援) float read_and_process(int n) { float vals[n]; for (int i = 0; i < n; i++) vals[i] = read_val(); return process(vals, n); } 正确例子(3): C++的使用者也有两种作法: a. std::vector (不管你的阵列大小会不会变都可用) std::vector<int> v1; v1.resize(10); // 重新设定vector size b. C++11以後,若是确定阵列大小不会变,可以用std::array 须注意:一般使用下(存在stack)一样要确认array size不能太大 std::array<int, 5> a = { 1, 2, 3 }; // a[0]~a[2] = 1,2,3; a[3]之後为0; a[a.size() - 1] = 5; // a[4] = 0; 备注: 注1. C++的使用者,C++03或之前请用0代替NULL,C++11开始请改用nullptr 注2. gcc和clang支援VLA,Visual C++不支援 补充资料: http://www.cplusplus.com/reference/vector/vector/resize/ 04. 你不可以试图用 char* 去更改一个"字串常数" 试图去更改字串常数(string literal)的结果会是undefined behavior。 错误例子: char* pc = "john"; /* pc 现在指着一个字串常数 */ *pc = 'J'; /* undefined behaviour,结果无法预测*/ pc = "jane"; /* 合法,pc指到在别的位址的另一个字串常数*/ /* 但是"john"这个字串还是存在原来的地方不会消失*/ 因为char* pc = "john"这个动作会新增一个内含元素为"john\0"的static char[5], 然後pc会指向这个static char的位址(通常是唯读)。 若是试图存取这个static char[],Standard并没有定义结果为何。 pc = "jane" 这个动作会把 pc 指到另一个没在用的位址然後新增一个 内含元素为"jane\0"的static char[5]。 可是之前那个字串 "john\n" 还是留在原地没有消失。 通常编译器的作法是把字串常数放在一块read only(.rdata)的区域内, 此区域大小是有限的,所以如果你重复把pc指给不同的字串常数, 是有可能会出问题的。 正确例子: char pc[] = "john"; /* pc 现在是个合法的阵列,里面住着字串 john */ /* 也就是 pc[0]='j', pc[1]='o', pc[2]='h', pc[3]='n', pc[4]='\0' */ *pc = 'J'; pc[2] = 'H'; 说明:字串常数的内容应该要是"唯读"的。您有使用权,但是没有更改的权利。 若您希望使用可以更改的字串,那您应该将其放在合法空间 错误例子: char *s1 = "Hello, "; char *s2 = "world!"; /* strcat() 不会另行配置空间,只会将资料附加到 s1 所指唯读字串的後面, 造成写入到程式无权碰触的记忆体空间 */ strcat(s1, s2); 正确例子(2): /* s1 宣告成阵列,并保留足够空间存放後续要附加的内容 */ char s1[20] = "Hello, "; char *s2 = "world!"; /* 因为 strcat() 的返回值等於第一个参数值,所以 s3 就不需要了 */ strcat(s1, s2); C++对於字串常数的严格定义为const char* 或 const char[]。 但是由於要相容C,char* 也是允许的写法(不建议就是)。 不过,在C++试图更改字串常数(要先const_cast)一样是undefined behavior。 const char* pc = "Hello"; char* p = const_cast<char*>(pc); p[0] = 'M'; // undefined behaviour 备注: 由於不加const容易造成混淆, 建议不管是C还是C++一律用 const char* 定义字串常数。 补充资料: http://en.cppreference.com/w/c/language/string_literal http://en.cppreference.com/w/cpp/language/string_literal 字串函数相关:#1IOXeMHX undefined behavior : z -> 3 -> 3 -> 23 05. 你不可以在函式中回传一个指向区域性自动变数的指标。否则,会得到垃圾值 [感谢 gocpp 网友提供程式例子] 错误例子: char *getstr(char *name) { char buf[30] = "hello, "; /*将字串常数"hello, "的内容复制到buf阵列*/ strcat(buf, name); return buf; } 说明:区域性自动变数,将会在离开该区域时(本例中就是从getstr函式返回时) 被消灭,因此呼叫端得到的指标所指的字串内容就失效了。 正确例子: void getstr(char buf[], int buflen, char const *name) { char const s[] = "hello, "; strcpy(buf, s); strcat(buf, name); } 正确例子: int* foo() { int* pInteger = (int*) malloc( 10*sizeof(int) ); return pInteger; } int main() { int* pFromfoo = foo(); } 说明:上例虽然回传了函式中的指标,但由於指标内容所指的位址并非区域变数, 而是用动态的方式抓取而得,换句话说这块空间是长在 heap 而非 stack, 又因 heap 空间并不会自动回收,因此这块空间在离开函式後,依然有效 (但是这个例子可能会因为 programmer 的疏忽,忘记 free 而造成 memory leak) [针对字串操作,C++提供了更方便安全更直观的 string class, 能用就尽量用] 正确例子: #include <string> /* 并非 #include <cstring> */ using std::string; string getstr(string const &name) { return string("hello, ") += name; } 06. [C]你不可以只做 malloc(), 而不做相应的 free(). 否则会造成记忆体漏失 但若不是用 malloc() 所得到的记忆体,则不可以 free()。已经 free()了 所指记忆体的指标,在它指向另一块有效的动态分配得来的空间之前,不可 以再被 free(),也不可以提取(dereference)这个指标。 小技巧: 可在 free 之後将指标指到 NULL,free不会对空指标作用。 例: int *p = malloc(sizeof(int)); free(p); p = NULL; free(p); // free不会对空指标有作用 [C++] 你不可以只做 new, 而不做相应的 delete (除了unique_ptr以外) 注:new 与 delete 对应,new[] 与 delete[] 对应, 不可与malloc/free混用(结果不可预测) 切记,做了几次 new,就必须做几次 delete 小技巧: 可在 delete 之後将指标指到0或nullptr(C++11开始), 由於 delete 本身会先做检查,因此可以避免掉多次 delete 的错误 正确例子: int *ptr = new int(99); delete ptr; ptr = nullptr; delete ptr; /* delete 只会处理指向非 NULL 的指标 */ 备注: C++11後新增智能指标(smart pointer): unique_ptr 当unique_ptr所指物件消失时,会自动释放其记忆体,不需要delete。 例: #include <memory> // 含unique_ptr的标头档 std::unique_ptr<int> p1(new int(5)); 补充资料: http://en.cppreference.com/w/cpp/memory/unique_ptr 07. 你不可以在数值运算、赋值或比较中随意混用不同型别的数值,而不谨慎考 虑数值型别转换可能带来的「意外惊喜」(错愕)。必须随时注意数值运算 的结果,其范围是否会超出变数的型别 错误例子: unsigned int sum = 2000000000 + 2000000000; /* 超出 int 存放范围 */ unsigned int sum = (unsigned int) (2000000000 + 2000000000); double f = 10 / 3; 正确例子: /* 全部都用 unsigned int, 注意数字後面的 u, 大写 U 也成 */ unsigned int sum = 2000000000u + 2000000000u; /* 或是用显式的转型 */ unsigned int sum = (unsigned int) 2000000000 + 2000000000; double f = 10.0 / 3.0; 错误例子: unsigned int a = 0; int b[10]; for(int i = 9 ; i >= a ; i--) { b[i] = 0; } 说明:由於 int 与 unsigned 共同运算的时候,会转换 int 为 unsigned, 因此回圈条件永远满足,与预期行为不符 错误例子: (感谢 sekya 网友提供) unsigned char a = 0x80; /* no problem */ char b = 0x80; /* implementation-defined result */ if( b == 0x80 ) { /* 不一定恒真 */ printf( "b ok\n" ); } 说明:语言并未规定 char 天生为 unsigned 或 signed,因此将 0x80 放入 char 型态的变数,将会视各家编译器不同作法而有不同结果 错误例子(以下假设为在32bit机器上执行): #include <math.h> long a = -2147483648 ; // 2147483648 = 2 的 31 次方 while (labs(a)>0){ // labs(-2147483648)<0 有可能发生 ++a; } 说明:如果你去看C99/C11 Standard,你会发现long 变数的最大/最小值为(被define在limits.h) LONG_MIN -2147483647 // compiler实作时最小值不可大於 -(2147483648-1) LONG_MAX 2147483647 // compiler实作时最小值不可小於 (2147483648-1) 不过由於32bit能显示的范围就是2**32种,所以一般16/32bit作业系统会把 LONG_MIN多减去1,也就是int 的显示范围为(-LONG_MAX - 1) ~ LONG_MAX。 (64bit的作业系统long多为8 bytes,但是依旧符合Standard要求的最小范围) 当程式跑到labs(-2147483648)>0时,由於2147483648大於LONG_MAX, Standard告诉我们,当labs的结果无法被long有限的范围表示, 编译器会怎麽干就看他高兴(undefined behavior)。 (不只long,其他如int、long long等以此类推) 补充资料: - C11 Standard 5.2.4.2.1, 7.22.6.1 - https://www.fefe.de/intof.html 08. ++i/i++/--i/i--/f(&i)哪个先执行跟顺序有关 ++i/i++ 和--i/i-- 的问题几乎每个月都会出现,所以特别强调。 当一段程式码中,某个变数的值用某种方式被改变一次以上, 例如 ++x/--x/x++/x--/function(&x)(能改变x的函式) - 如果Standard没有特别去定义某段叙述中哪个部份必须被先执行, 那结果会是undefined behavior(结果未知)。 - 如果Standard有特别去定义执行顺序,那结果就根据执行顺序决定。 C/C++均正确的例子: if (--a || ++a) {} // ||左边先计算,如果左边为1右边就不会算 if (++i && f(&i)) {} // &&左边先计算,如果左边为0右边就不会算 a = (*p++) ? (*p++) : 0 ; // 问号左边先计算 int j = (++i, i++); // 这里的逗号为运算子,表示依序计算 C/C++均错误的例子: int j = ++i + i++; // undefined behavior,Standard没定义+号哪边先执行 x = x++; // undefined behavior, Standard没定义=号哪边先执行 printf( "%d %d %d", I++, f(&I), I++ ); // undefined behavior, 原因同上 foo(i++, i++); // undefined behavior,这里的逗号是用来分隔引入参数的 // 分隔符(separator)而非运算子,Standard没定义哪边先执行 在C与C++03错误但是在C++11开始(但不包括C)正确的例子: C++11中,++i/--i为左值(lvalue),i++/i--为右值(rvalue)。 左值可以被assign value给它,右值则不行。 而在C中,++i/--i/i++/i--都是右值。 所以以下的code在C++会正确,C则否。 ++++++++++phew ; // C++11会把它解释为++(++(++(++(++phew)))); i = v[++i]; // ++i会先完成 i = ++i + 1; // ++i会先完成 在C++17开始(但不包括C)才正确的例子: cout << i << i++; // 先左後右 a[i] = i++; // i++先做 a[x++] = --x; // 先处理--x,再处理a[x++] (loveflames补充) 补充资料 - Undefined behavior and sequence points http://stackoverflow.com/questions/4176328/undefined-behavior-and- sequence-points) - C11 Standard 6.5.13-17,Annex C - Sequence poit https://en.wikipedia.org/wiki/Sequence_point - Order of evaluation http://en.cppreference.com/w/cpp/language/eval_order 09. 慎用macro(#define) Macro是个像铁鎚一样好用又危险的工具: 用得好可以钉钉子,用不好可以把钉子打弯、敲到你手指或被抓去吃子弹。 因为macro 定义出的「伪函式」有以下缺点: (1) debug会变得复杂。 (2) 无法递回呼叫。 (3) 无法用 & 加在 macro name 之前,取得函式位址。 (4) 没有namespace。 (5) 可能会导致奇怪的side effect或其他无法预测的问题。 所以,使用macro前,请先确认以上的缺点是否会影响你的程式运行。 替代方案:enum(定义整数),const T(定义常数),inline function(定义函式) C++的template(定义可用不同type参数的函式), 或C++11开始的匿名函式(Lambda function)与constexpr T(编译期常数) 以下就针对macro的缺点做说明: (1) debug会变得复杂。 编译器不能对macro本身做语法检查,只能检查预处理(preprocess)後的结果。 (2) 无法递回呼叫。 根据C standard 6.10.3.4, 如果某macro的定义里里面含有跟此macro名称同样的的字串, 该字串将不会被预处理。 所以: #define pr(n) ((n==1)? 1 : pr(n-1)) cout<< pr(5) <<endl; 预处理过後会变成: cout<< ((5==1)? 1 : pr(5 -1)) <<endl; // pr没有定义,编译会出错 (3) 无法用 & 加在 macro name 之前,取得函式位址。 因为他不是函式,所以你也不可以把函式指标套用在macro上。 (4) 没有namespace。 错误例子: #define begin() x = 0 for (std::vector<int>::iterator it = myvector.begin(); it != myvector.end(); ++it) // begin是std的保留字 std::cout << ' ' << *it; 改善方法:macro名称一律用大写,如BEGIN() (5) 可能会导致奇怪的side effect或其他无法预测的问题。 错误例子: #include <stdio.h> #define SQUARE(x) (x * x) int main() { printf("%d\n", SQUARE(10-5)); // 预处理後变成SQUARE(10-5*10-5) return 0; } 正确例子:在 Macro 定义中, 务必为它的参数个别加上括号 #include <stdio.h> #define SQUARE(x) ((x) * (x)) int main() { printf("%d\n", SQUARE(10-5)); return 0; } 不过遇到以下有side effect的例子就算加了括号也没用。 错误例子: (感谢 yaca 网友提供) #define MACRO(x) (((x) * (x)) - ((x) * (x))) int main() { int x = 3; printf("%d\n", MACRO(++x)); // 有side effect return 0; } 补充资料: - http://stackoverflow.com/questions/14041453/why-are-preprocessor- macros-evil-and-what-are-the-alternatives - http://stackoverflow.com/questions/12447557/can-we-have-recursive-macros - C11 Standard 6.10.3.4 - http://en.cppreference.com/w/cpp/language/lambda 10. 不要在 stack 设置过大的变数以避免堆叠溢位(stack overflow) 由於编译器会自行决定 stack 的上限,某些预设是数 KB 或数十KB,当变数所需的空 间过大时,很容易造成 stack overflow,程式亦随之当掉(segmentation fault)。 可能造成堆叠溢位的原因包括递回太多次(多为程式设计缺陷), 或是在 stack 设置过大的变数。 错误例子: int array[10000000]; // 在stack宣告过大阵列 std::array<int, 10000000> myarray; //在stack宣告过大std::array 正确例子: C: int *array = (int*) malloc( 10000000*sizeof(int) ); C++: std::vector<int> v; v.resize(10000000); 说明:建议将使用空间较大的变数用malloc/new配置在 heap 上,由於此时 stack 上只需配置一个 int* 的空间指到在heap的该变数,可避免 stack overflow。 使用 heap 时,虽然整个 process 可用的空间是有限的,但采用动态抓取 的方式,new 无法配置时会丢出 std::bad_alloc 例外,malloc 无法配置 时会回传 null(注2),不会影响到正常使用下的程式功能 备注: 注1. 使用 heap 时,整个 process 可用的空间一样是有限的,若是需要频繁地 malloc / free 或 new / delete 较大的空间,需注意避免造成记忆体破碎 (memory fragmentation)。 注2. 由於Linux使用overcommit机制管理记忆体,malloc即使在记忆体不足时 仍然会回传非NULL的address,同样情形在Windows/Mac OS则会回传NULL (感谢 LiloHuang 补充) 补充资料: - https://zh.wikipedia.org/wiki/%E5%A0%86%E7%96%8A%E6%BA%A2%E4%BD%8D - http://stackoverflow.com/questions/3770457/what-is-memory-fragmentation - http://library.softwareverify.com/memory-fragmentation-your-worst-nightmare/ overcommit跟malloc: - http://goo.gl/V9krbB - http://goo.gl/5tCLQc 11. 使用浮点数千万要注意精确度所造成的误差问题 根据 IEEE 754 的规范,又电脑中是用有限的二进位储存数字,因此常有可 能因为精确度而造成误差,例如加减乘除,等号大小判断,分配律等数学上 常用到的操作,很有可能因此而出错(不成立) 更详细的说明可以参考 z-8-11 或参考冼镜光老师所发表的一文 "使用浮点数最最基本的观念" http://blog.dcview.com/article.php?a=VmhQNVY%2BCzo%3D 12. 不要猜想二维阵列可以用 pointer to pointer 来传递 (感谢 loveme00835 legnaleurc 版友的帮忙) 首先必须有个观念,C 语言中阵列是无法直接拿来传递的! 不过这时候会有人跳出来反驳: void pass1DArray( int array[] ); int a[10]; pass1DArray( a ); /* 可以合法编译,而且执行结果正确!! */ 事实上,编译器会这麽看待 void pass1DArray( int *array ); int a[10]; pass1DArray( &a[0] ); 我们可以顺便看出来,array 变数本身可以 decay 成记忆体起头的位置 因此我们可以 int *p = a; 这种方式,拿指标去接阵列。 也因为上述的例子,许多人以为那二维阵列是不是也可以改成 int ** 错误例子: void pass2DArray( int **array ); int a[5][10]; pass2DArray( a ); /* 这时候编译器就会报错啦 */ /* expected ‘int **’ but argument is of type ‘int (*)[10]’*/ 在一维阵列中,指标的移动操作,会刚好覆盖到阵列的范围 例如,宣告了一个 a[10],那我可以把 a 当成指标来操作 *a 至 *(a+9) 因此我们可以得到一个概念,在操作的时候,可以 decay 成指标来使用 也就是我可以把一个阵列当成一个指标来使用 (again, 阵列!=指标) 但是多维阵列中,无法如此使用,事实上这也很直观,试图拿一个 pointer to pointer to int 来操作一个 int 二维阵列,这是不合理的! 尽管我们无法将二维阵列直接 decay 成两个指标,但是我们可以换个角度想, 二维阵列可以看成 "外层大的一维阵列,每一维内层各又包含着一维阵列" 如果想通了这一点,我们可以仿造之前的规则, 把外层大的一维阵列 decay 成指标,该指标指向内层的一维阵列 void pass2DArray( int (*array) [10] ); // array 是个指标,指向 int [10] int a[5][10]; pass2DArray( a ); 这时候就很好理解了,函数 pass2DArray 内的 array[0] 会代表什麽呢? 答案是它代表着 a[0] 外层的那一维阵列,里面包含着内层 [0]~[9] 也因此 array[0][2] 就会对应到 a[0][2],array[4][9] 对应到 a[4][9] 结论就是,只有最外层的那一维阵列可以 decay 成指标,其他维阵列都要 明确的指出阵列大小,这样多维阵列的传递就不会有问题了 也因为刚刚的例子,我们可以清楚的知道在传递阵列时,实际行为是在传递 指标,也因此如果我们想用 sizeof 来求得阵列元素个数,那是不可行的 错误例子: void print1DArraySize( int* arr ) { printf("%u", sizeof(arr)/sizeof(arr[0])); /* sizeof(arr) 只是 */ } /* 一个指标的大小 */ 受此限制,我们必须手动传入大小 void print1DArraySize( int* arr, size_t arrSize ); C++ 提供 reference 的机制,使得我们不需再这麽麻烦, 可以直接传递阵列的 reference 给函数,大小也可以直接求出 正确例子: void print1DArraySize( int (&array)[10] ) { // 传递 reference cout << sizeof(array) / sizeof(int); // 正确取得阵列元素个数 } 13. 函式内 new 出来的空间记得要让主程式的指标接住 对指标不熟悉的使用者会以为以下的程式码是符合预期的 void newArray(int* local, int size) { local = (int*) malloc( size * sizeof(int) ); } int main() { int* ptr; newArray(ptr, 10); } 接着就会找了很久的 bug,最後仍然搞不懂为什麽 ptr 没有指向刚刚拿到的合法空间 让我们再回顾一次,并且用图表示 (感谢Hazukashiine板友提供图解) ┌────┐ ┌────┐ ┌────┐ ┌────┐ Heap │ │ │ │ │ 新配置 │ │ 已泄漏 │ │ │ │ │ │ 的空间 <─┐ │ 的空间 │ │ │ │ │ │(allocd)│ │ │(leaked)│ │ │ │ │ ├────┤ │ ├────┤ │ │ │ │ │ : │ │ │ │ │ │ │ │ │ : │ │ │ : │ │ │ ├────┤ ├────┤ │ │ : │ │ │ │ local ├─┐ │ local ├─┘ │ │ ├────┤ ├────┤ │ ├────┤ ├────┤ Stack │ ptr ├─┐ │ ptr ├─┤ │ ptr ├─┐ │ ptr ├─┐ └────┘ ╧ └────┘ ╧ └────┘ ╧ └────┘ ╧   未初始化 函式呼叫 配置空间 函式返回 int *ptr; local = ptr; local = malloc(); 用图看应该一切就都明白了,我也不需冗言解释 也许有人会想问,指标不是传址吗? 精确来讲,指标也是传值,只不过该值是一个位址 (ex: 0xfefefefe) local 接到了 ptr 指向的那个位置,接着函式内 local 要到了新的位置 但是 ptr 指向的位置还是没变的,因此离开函式後就好像事什麽都没发生 ( 严格说起来还发生了 memory leak ) 以下是一种解决办法 int* createNewArray(int size) { return (int*) malloc( size * sizeof(int) ); } int main() { int* ptr; ptr = createNewArray(10); } 改成这样亦可 ( 为何用 int** 就可以?想想他会传什麽过去给local ) void createNewArray(int** local, int size) { *local = (int*) malloc( size * sizeof(int) ); } int main() { int *ptr; createNewArray(&ptr, 10); } 如果是 C++,别忘了可以善用 Reference void newArray(int*& local, int size) { local = new int[size]; } 後记:从「古时候」流传下来一篇文章 "The Ten Commandments for C Programmers"(Annotated Edition) by Henry Spencer http://www.lysator.liu.se/c/ten-commandments.html 一方面它不是针对 C 的初学者,一方面它特意模仿中古英文 圣经的用语,写得文诌诌。所以我现在另外写了这篇,希望 能涵盖最重要的观念以及初学甚至老手最易犯的错误。 作者:潘科元(Khoguan Phuann) (c)2005. 感谢 ptt.cc BBS 的 C_and_CPP 看板众多网友提供宝贵意见及程式实例。 nowar100 多次加以修改整理,扩充至 13 项,并且制作成动画版。 wtchen 应板友要求移除动画并根据C/C++标准修改内容(Ver.2016) 如发现 Bug 请推文回报,谢谢您 --



※ 发信站: 批踢踢实业坊(ptt.cc), 来自: 90.41.184.140
※ 文章网址: https://webptt.com/cn.aspx?n=bbs/C_and_CPP/M.1465304337.A.9F2.html ※ 编辑: wtchen (90.41.184.140), 06/07/2016 21:03:40 ※ 编辑: wtchen (90.41.184.140), 06/07/2016 21:04:31
1F:→ wtchen: 有错请指正 06/07 21:05
2F:推 Godkin: 06/08 15:10
3F:推 kikiqqp:   06/08 15:19
4F:推 Hazukashiine:                          06/08 22:20
※ 编辑: wtchen (90.41.66.248), 06/09/2016 14:53:03 ※ 编辑: wtchen (90.41.66.248), 06/10/2016 00:52:56 ※ 编辑: wtchen (90.41.66.248), 06/11/2016 02:55:21
5F:推 Davidhu127: 多谢!长知识了~ (更清楚了malloc,二维阵列ref,fr 08/24 05:56
6F:→ Davidhu127: agmentation) 08/24 05:56
7F:推 zzzz8931: 推 08/30 13:01
8F:推 kobe200525: 推~ 09/30 21:35
※ 编辑: wtchen (90.41.211.206), 11/05/2016 18:37:24 ※ 编辑: wtchen (90.41.211.206), 11/06/2016 01:26:25 ※ 编辑: wtchen (90.41.211.206), 11/07/2016 02:55:06 ※ 编辑: wtchen (90.27.175.198), 11/23/2016 20:08:24 ※ 编辑: wtchen (90.27.175.198), 11/23/2016 20:11:23 ※ 编辑: wtchen (90.41.175.158), 03/07/2017 23:45:11 ※ 编辑: wtchen (90.41.175.158), 03/08/2017 23:42:16 ※ 编辑: wtchen (90.41.175.158), 03/08/2017 23:43:43 ※ 编辑: wtchen (86.209.162.81), 03/09/2017 23:11:25 ※ 编辑: wtchen (86.209.162.81), 03/10/2017 03:46:29
9F:推 a620699999: 推 04/15 13:44
10F:推 hpyhacking: 推 05/19 21:23
11F:推 DemonElf: 感谢分享! 03/23 05:00
12F:推 anarch: 谢谢分享 07/01 20:43
13F:推 leviliang: 推! 07/22 15:06
14F:→ mythnc: 真的有在做事,比前板主好多了 :) 05/12 00:35
15F:推 siuoly: 谢谢 第十二条有帮助到我 程式深入的细节真的很难找资源 05/01 21:53
16F:→ Jeremy174: 已收藏 08/01 11:09
17F:推 imp334: 推 08/21 22:43
18F:推 James7878978: 推 03/27 21:40
19F:推 a58524andy: 关於08.的"x=x++"的UB,这条SO有说明原因 05/20 13:10
20F:→ a58524andy: https://stackoverflow.com/a/47509458/9933842 05/20 13:10
21F:推 filialpiety4: 感谢~对初学者受益良多 06/06 23:18
22F:推 dzwei: 关於12. 如果是dynamic array,就得用pointer to pointer 11/14 13:03
23F:→ dzwei: 传递至func了, 详见 11/14 13:03
24F:→ dzwei: https://tinyurl.com/4bbd75bh 11/14 13:03







like.gif 您可能会有兴趣的文章
icon.png[问题/行为] 猫晚上进房间会不会有憋尿问题
icon.pngRe: [闲聊] 选了错误的女孩成为魔法少女 XDDDDDDDDDD
icon.png[正妹] 瑞典 一张
icon.png[心得] EMS高领长版毛衣.墨小楼MC1002
icon.png[分享] 丹龙隔热纸GE55+33+22
icon.png[问题] 清洗洗衣机
icon.png[寻物] 窗台下的空间
icon.png[闲聊] 双极の女神1 木魔爵
icon.png[售车] 新竹 1997 march 1297cc 白色 四门
icon.png[讨论] 能从照片感受到摄影者心情吗
icon.png[狂贺] 贺贺贺贺 贺!岛村卯月!总选举NO.1
icon.png[难过] 羡慕白皮肤的女生
icon.png阅读文章
icon.png[黑特]
icon.png[问题] SBK S1安装於安全帽位置
icon.png[分享] 旧woo100绝版开箱!!
icon.pngRe: [无言] 关於小包卫生纸
icon.png[开箱] E5-2683V3 RX480Strix 快睿C1 简单测试
icon.png[心得] 苍の海贼龙 地狱 执行者16PT
icon.png[售车] 1999年Virage iO 1.8EXi
icon.png[心得] 挑战33 LV10 狮子座pt solo
icon.png[闲聊] 手把手教你不被桶之新手主购教学
icon.png[分享] Civic Type R 量产版官方照无预警流出
icon.png[售车] Golf 4 2.0 银色 自排
icon.png[出售] Graco提篮汽座(有底座)2000元诚可议
icon.png[问题] 请问补牙材质掉了还能再补吗?(台中半年内
icon.png[问题] 44th 单曲 生写竟然都给重复的啊啊!
icon.png[心得] 华南红卡/icash 核卡
icon.png[问题] 拔牙矫正这样正常吗
icon.png[赠送] 老莫高业 初业 102年版
icon.png[情报] 三大行动支付 本季掀战火
icon.png[宝宝] 博客来Amos水蜡笔5/1特价五折
icon.pngRe: [心得] 新鲜人一些面试分享
icon.png[心得] 苍の海贼龙 地狱 麒麟25PT
icon.pngRe: [闲聊] (君の名は。雷慎入) 君名二创漫画翻译
icon.pngRe: [闲聊] OGN中场影片:失踪人口局 (英文字幕)
icon.png[问题] 台湾大哥大4G讯号差
icon.png[出售] [全国]全新千寻侘草LED灯, 水草

请输入看板名称,例如:Tech_Job站内搜寻

TOP