作者wtchen (没有存在感的人)
看板C_and_CPP
标题C/C++ 语言新手十三诫 -- Ver. 2016
时间Tue Jun 7 20:58:52 2016
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
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