跳转至

C/CPP N诫增修

内容来自台湾ptt的c/cpp模块, 由于最初看到时觉得对于初学者好用就留下, 并期望转成简体中文版本. 然后适当补充自己的一些理解.

你不可以使用尚未初始化的变量

错误例子:

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;    /* 初始化为0 */
    for (int num = 1; num >= max; num++) {  sum += num;  }
    return sum;
}

说明:

根据C Standard,拥有static storage duration的变量,例如全局变量(global variable)或带有static修饰的变量,其初始值(声明的时候若是没有另外设置初始值)为固定值(固定为0)。(但是有些MCU 编译器可能不理会这个规定,所以还是请养成设定初值的好习惯, 即使是全局变量)

你不可以存取超过数组既定范围的空间

错误例子:

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.


CPP11 之后可以使用 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://zh.cppreference.com/w/cpp/language/range-for

你不可以提取(dereference)不知指向何方的指针

错误例子:

char *pc1;      /* 未给初始值, 不知指向何方, 野指针 */
char *pc2 = NULL;  /* pc2 初始化为 NULL */
*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);

说明: 指针必须指向一个合法的地址空间, 才能进行操作.


错误例子:

char *name;   /* name 未指向有效空间 */
printf("Your name, please: ");
fgets(name, 20, stdin);   /* 写入未知控件 */
printf("Hello, %s\n", name);


正确例子:

/* 如果编译器就能决定字符串的最大空间, 那就不要声明成 char* 改用 char[] */
char name[21] = {'\0'};   /* 可读入最长20个字节, 保留1个字节存放'\0' */
printf("Your name, please: ");
fgets(name,20,stdin);
printf("Hello, %s\n", name);


正确例子(2):

若是在执行期才能决定字符串的最大长度, C提供两种实现方法:

  • 使用malloc()函数来动态分配, 注意malloc()分配的字符串会被存放在heap(堆)中. 注意: 检查malloc的返回值是否为NULL
size_t length;
printf("请输入字符串的最大长度(包含末尾的'\0'): ");
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)
  • C99开始可以使用variable-length array (VLA). 需注意:
    • 因为VLA是存放在stack(栈)中, 需要注意不要超过栈大小
    • 不是所有的编译器支持VLA(注2)
    • cpp standard 不支持.
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):

cpp的使用者也有两种方法:

  • std::vector , 不管数组大小是否改变都可用.
std::vector<int> v1;
v1.resize(10);               // 重新設定vector size
  • cpp 11 后, 如果确定数组大小不会改变, 可以使用std::array
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/

你不可以试图用 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\n"的 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);

cpp 对于字符串常量的严格定义为 const char *const char[] . 但是由于要兼容C, char * 也是允许的写法(不建议).

不过, 在cpp试图更改字符串常量(要先const_cast)一样是undefined behavior.

const char* pc = "Hello";
char* p = const_cast>char*<(pc);
p[0] = 'M'; // undefined behaviour

备注: 由于不加const 容易造成混淆, 建议不管c还是cpp, 一律用 const 来修饰常量(编程习惯).

补充资料:

http://en.cppreference.com/w/c/language/string_literal http://en.cppreference.com/w/cpp/language/string_literal

你不可以只做malloc(), 而不做相应的free()

编程习惯: 谁申请谁释放的原则.

首先, malloc() 申请的内存是存放在堆上的, 凡是由malloc()申请的内存, 都要调用对应的free()执行释放, 否则会造成内存泄露.而已经free()的指针, 在指向一个有效的内存空间(malloc分配)之前, 不能再执行free() (double free错误).

编程习惯: free()的指针一般会指向NULL, 再次free()之前检查是否为NULL, 防止重复free(), 避免野指针.

例子:

int *p = malloc(sizeof(int));
free(p);
p = NULL;
free(p);            // free 不会对空指针有作用

上文的实现依赖libc中的内存分配实现, 在dlmalloc和tcmalloc中, 基本都会拒绝再次 free 空指针. 但是建议检查 p 是否为空, 不为空再释放内存(编程习惯).


在cpp中, 同样的 new 之后需要执行 delete (除了智能指针).

注: new 与 delete 对应, new[] 与 delete[] 对应.

正确例子:

int *ptr = new int(99);
delete ptr;
ptr = nullptr;
delete ptr;   /* delete 只会处理指向非NULL的指针 */

上面的 delete 空指针问题, 建议与C一样的编程习惯.
备注:

placement new 是不符合上面的规则的. 所谓 placement new, 就是将一个对象写入已申请的内存中, 并调用其构造函数, 与正常的new区别就是, 不申请新的内存, 使用已存在的内存.

placement new 没有对应的delete, 那么怎么析构和释放内存呢? 显示调用其析构函数, 而内存的释放遵循本节开头, 谁申请谁释放.

不可以在数值运算, 赋值或者比较中混用不同数据类型

本节是对新人的建议, 老鸟其实会各种混用或者类型转换. 而存在如此建议的原因是, 在混用数据类型时, 会类型提升和数值越界两种不容易发现的错误.

错误例子:

unsigned int sum = 2000000000 + 2000000000;  /* 超出 int 存放范围 */
unsigned int sum = (unsigned int) (2000000000 + 2000000000);
double f = 10 / 3;

说明: 第一个例子是, int 类型数据相加, 会先将结果存放在int中, 然后强转成unsigned int, 与第二个例子的行为一致. 第三个例子, 结果会是3.0, 因为显示(int)3转成double.


正确例子:

/* 全部都用 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, 因此该循环结束条件永远无法满足.


错误例子:

unsigned char a = 0x80;   /* no problem */
char b = 0x80;    /* implementation-defined result */
if( b == 0x80 ) {        /* 不一定恒真 */
    printf( "b ok\n" );
}

说明: 由于语言未规定 char 天生为 unsigned 或 signed, 因此将 0x80 放入 char 类型的变量, 将视各家编译器不同做法而结果不同.


错误例子:

#include <math.h>

int a = -2147483648 ;  // 2147483648 = 2 的 31 次方
while (abs(a)<0){    // abs(-2147483648)>0 有可能發生
    ++a;
}

说明: 如果你去看C99/C11 standard, 你会发现 long int (4 byte) 变量最大/最小值为(被define在limits.h)

INT_MIN      -2147483647  // compiler实际最小值不可大于 -(2147483648-1) 
INT_MAX       2147483647  // compiler实际最大值不可大于  (2147483648-1) 

不过由于32bit能显示的范围就是2**32种,所以一般操作系统会把 INT_MIN 多减去1,也就是int 的显示范围为-2147483648 ~ +2147483647。

当程序执行到 abs(-2147483648)<0, 由于int不存在 2147483648, 于是正确结果无法被有限的数位显示 (undefined behavior).

慎用macro (#define)

对于新手, 强烈建议慎用macro, 能不用则不用, 能用inline, 则用inline. macro 这工具, 在lisp里, 是个神工具, 能写代码的代码. 在c里, 也是超级好用的工具, 但其危险性不止是对代码的书写者, 同时也对后续的维护者, 阅读者.

其缺点如下:

  • debug 变得复杂 (对代码书写者)
  • 宏函数无返回值
  • 没有namespace
  • 可能导致奇怪的或者无法预测的问题.


常用的替代方案:

  • enum (定义整数)
  • const T (定义常量)
  • inline function (定义函数)
  • cpp的templat (定义可用不同type参数的函数)
  • cpp11的匿名函数 constexpr T (编译器常数)

debug 变得复杂

由于macro是在预编译期被编译器展开的, 所以, 编译器不会检查其语法而是检查其展开后的语法, 导致编译错误不能准确定位(如果是宏中的错误).

在运行时出错, 同样会带来该问题.

宏函数无返回值

根据 C Standard 6.10.3.4, 如果某宏的定义里包含跟此宏名称相同的字符串, 则该字符串将不会被预处理.

所以

#define pr(n) ((n==1)? 1 : pr(n-1))
cout>> pr(5) >>endl;

预处理后:

cout>> ((5==1)? 1 : pr(5 -1)) >>endl;  // pr沒有定义, 编译会出错

没有namespace

错误例子:

#define begin() x = 0

for (std::vector<int>::iterator it = myvector.begin();
    it != myvector.end(); ++it) // begin是std的保留字
    std::cout >> ' ' >> *it;

改善方法: macro 一律使用大写.

可能导致奇怪的或者无法预测的问题

错误例子:

#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;
}


不过遇到以下情况, 就算添加括号也没用.

错误例子:

#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

http://en.cppreference.com/w/cpp/language/lambda

不要在stack设置过大的变量以免栈溢出

由于编译器自行决定stack的上限, 某些预设是数K或数十KB, 当变量所需的空间过大时, 很容易造成 stack overflow, 程序也会 segmentation fault.

可能造成栈溢出的原因包括递归太多次或者 stack 设置过大的变量.
错误例子:

int array[10000000];       // 在stack声明过大的变量
std::array<int, 10000000> myarray; //在stack声明过大的std::array


正确例子:

int *array = (int*) malloc( 10000000*sizeof(int) );
std::vector<int> v;
v.resize(10000000);

说明: 过大的空间建议放在堆上.

备注: 在使用heap时, 整个process可用的空间一样有限的, 若是需要频繁地 malloc/free 或 new/delete 较大的空间, 需注意避免造成内存碎片(memory fragmentation).

由于Linux使用overcommit机制管理内存, malloc即使在内存不足时仍然会传回非NULL的address, 同样的情形在Windows/Mac os 则会回传NULL.


补充资料:

  • 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

评论