您的位置:首页 > 编程语言 > C语言/C++

理解C语言——从小菜到大神的晋级之路(14)——C编程常见错误

2016-03-20 16:27 531 查看
本期视频:点击这里

1、混淆变量的作用域和生存期

变量的作用域和生存期实际上是两个完全不同的概念。

变量的作用域:可以应用这个变量的范围,强调变量使用的空间范围;
变量的生存期:变量的生命周期,强调变量有效的时间;

这两个概念中,作用域更强调变量可以被引用,而生存期更强调其本身是否存在,这二者实际上并没有必然联系。通常情况下,如果处于某个变量的作用域内,那么这个变量一定还在生存期;但是相反,某个变量已经不在其作用域,并不意味着其生存期已结束。变量的生存期常常远大于其作用域。

简单起见,我们常常根据变量的作用域将变量分为两种类型:局部变量和全局变量。所谓局部变量,通常指在函数或代码块内部定义的变量,通常具有块作用域;全局变量指在函数外部定义的变量,通常具有文件作用域。无论是局部变量还是全局变量,都有不同的声明方式,其含义也不尽相同:

(1)局部变量:

局部变量默认是auto,即自动类型,这一类型的局部变量属于真正的“局部变量”,即其生存期从进入变量所在的函数体开始,到函数结束为止。每次进入该函数,这个变量又会被分配新的内存和地址,因此在同一个函数的先后两次调用期间,自动类型局部变量不存在任何关联。

局部变量可以使用static关键字定义为静态类型,此类型的局部变量属于“静态局部变量”,其作用域与默认类型一致,但是其生存期大大延长。虽然静态局部变量依然只能在本函数内部访问,但是在程序的整个运行期间内都将保持有效,并不会被释放。此类型的变量在编译期初始化,在每次执行该函数期间,静态局部变量将会记忆上一次执行时的数据结果。

除了auto和static类型外,还有两种常用的关键字用于修饰局部变量:其一是register类型,表示寄存器变量,常用在频繁访问的少数变量用于提高运行速度。寄存器类型变量同普通变量区别不大,只是需注意此类型变量是保存在寄存器中的,没有内存地址,因此不能进行使用取地址操作。其二是volatile类型,表示变量可能会随时变化,因此指示编译器禁止对相关代码进行编译优化防止出现未知错误,常用于多线程程序中。

(2)全局变量:

所有全局变量都是静态类型的,其生存期从程序启动开始,直到程序结束退出。根据对全局变量声明的不同,全局变量的作用域会有所差别。我们在函数外部定义的全局变量,其作用域为定义变量开始一直到该文件末尾。通常,不带任何修饰的全局变量存在两个问题:其一是只能在当前文件内部使用,因为其他源文件不属于它的作用域;其二是不同源文件不能带有重名的全局变量,因为每一个源文件都将编译为一个单独的目标文件(.obj),如果源文件中的全局变量重名,那么在将目标文件链接成可执行文件(.exe)时,重复定义的全局变量将会造成链接错误。针对这两个问题,C语言分别提供了处理方法:

extern关键字:声明外部全局变量,将在本文件之外定义的全局变量的作用域扩展到这里。extern关键字会通知编译器:“当前正在声明一个外部的局部变量,这个变量已经定义过,不要在为其分配内存”。需注意全局变量在定义时不能带extern,而在外部文件声明该变量时必须使用extern声明。
static关键字:声明“静态”全局变量。所谓“静态”,并非表示其存储类型,而是限定其作用范围。静态全局变量只能在当前文件中使用,且无法通过extern关键字进行作用域扩展。使用了static关键字的全局变量,在其他文件中可以定义与其重名的全局变量而不会引发错误或数据干扰。

事实上,除了变量意外,函数也可以定义为extern和static类型。其中extern类型是函数的默认类型,因为所有的函数都可认为是外部函数,C语言不允许在函数内部定义函数。而静态函数同静态全局变量一样,只允许在当前源文件内部使用。

2、函数返回局部变量的地址

程序员自定义函数通常会按照定义的返回值类型将一个变量、表达式的值或另一函数的返回值返回给调用者。但需要注意的是,无论任何时候,绝不应将当前函数内部定义的动态局部变量的地址返回给上级。例如下面的程序是完全错误的,运行时将不能得到正确结果:
int * getArray()
{
int arr[3] = {1,2,3};

return arr;
}


这是因为数组arr作为局部变量只会保存在栈空间,只在当前函数内部有效。一旦函数结束,栈空间的数据都将被清空,返回的局部变量的地址指向的是一个不存在和无意义的对象。如果一定希望在函数内部返回一个地址,那么必须确保这个对象不会再函数结束时被释放,比如使用全局/静态变量或字符串常量等。

3、头文件缺少保护导致代码重复包含

我们知道,程序源代码在预处理阶段,使用预处理指令引入的头文件的内容会插入到源文件中进行编译。头文件中通常用于声明一些公有的API、定义一些结构体等。如果对头文件中的代码没有任何保护措施,那么某些头文件在被多个源文件引用时,同样的代码会被编译到不同的程序模块中。如果此时这个头文件中存在对符号的定义,那么符号定义就会存在于多个模块中,在链接为可执行文件时,多个符号定义就会造成冲突,导致出现“符号重定义”的链接错误。如下面的程序在Build时是一定会出错的:
//header.h
typedef struct
{
int num1;
int num2;
} TwinInt;
void PrintTwinInt(TwinInt ti);
int g_val = 10;
//header .c
#include <stdio.h>
#include "header.h"
void PrintTwinInt(TwinInt ti)
{
printf("Twin Int Numbers: %d and %d.\n", ti.num1, ti.num2);
}

//main.c
#include "header.h"
int main()
{
TwinInt ti = {1, 2};
PrintTwinInt(ti);
return 0;
}


为了避免出现此类情况,通常需要对头文件中的代码进行保护,避免重复包含。通常在头文件中保护代码有两种方法:

(1)宏定义

宏定义是早期常用的方法。将上述代码用宏定义保护可以用如下的方式实现:
//header.h
#ifndef _HEADER_H_
#define _HEADER_H_
typedef struct
{
int num1;
int num2;
} TwinInt;
void PrintTwinInt(TwinInt ti);
#endif


这种宏定义的方式首先会检查某个用作标记的宏是否有定义,如果有那么头文件中所有的内容就都不会生效,头文件实际是空的;如果没有定义,那么头文件包含我们在其中实现的有效内容。使用这种方式,不但可以避免同一个文件的重复包含,还可以保证完全相同的两个文件也可以只包含一次。

使用这种方法需要注意,标识头文件的宏必须在整个代码中唯一。如果跟其他地方定义的宏出现了“撞车”,那么后面的代码就会被认为是重复包含而被抛弃,造成代码不完整。

(2)#pragma once

除了宏定义,另一种方式是在头文件开始加入一行#pragma once。这条预编译指令为微软编译器所独有,因此通常只适用于VS等开发环境。加入这条指令后,针对这一个头文件,即使是在多个模块中重复包含,那也只会打开一次。这条指令针对的是某一个文件,而不是文件内容,因此处理速度相对于宏定义要稍快一些。但是使用这个指令有一个不足,就是如果同样的内容的头文件存在多个,那么它无法识别相同的头文件内容,依然有可能造成重复包含。
//header.h
#pragma once
typedef struct
{
int num1;
int num2;
} TwinInt;
void PrintTwinInt(TwinInt ti);
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: