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

本期视频:点击这里

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);
时间: 2024-08-29 06:15:47

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

理解C语言——从小菜到大神的晋级之路(1)——引言:C语言的前世今生

第一课的视频链接点这里 C语言是现在应用最为广泛的编程语言之一,也是现在依然流行的编程语言中历史最悠久的一种之一.在目前业界广泛使用的编程语言中,许多 种语言是以C为基础发展而来.在多类大学的工程类专业尤其是信息类专业的教学计划中,C语言也是极为重要的基础课之一. 而对于一名以编译型语言为主要开发工具的程序员来说,熟练掌握C语言的用法和理论也可以对其他编程语言获得更深的理解.因此,在这一系列教程中我们希望可以深入理解C语言的方方面面,为后续理解更高级的技术奠定更好的基础. 1.参考资料 <C程序

理解C语言——从小菜到大神的晋级之路(6)——函数与调用

        视频观看:点击这里         在前面的程序中,由于程序的功能非常简单,所有的代码都在main()函数中实现.如果程序复杂度增加之后,在main()中实现所有代码将显得非常臃肿且缺乏条理.如果可以将一段大的计算任务分解为若干个小任务则可以有效解决这个问题.另外,分解出来的模块还可以进一步构造和重用,而不用每次都编写同样的代码.因此,绝大部分实际的C程序都是由一个简单的主函数和多个规模不同的子函数构成,而不是全部实现在一个很大的main函数中. 1.函数调用简介        

理解C语言——从小菜到大神的晋级之路(9)——多维数组

本节视频链接:点击这里 1.多维数组的定义和结构         一个数组中可以支持各种数据类型,那么一个数组中的每一个元素同样也可以是一个数组.对于上次提到的一维数组,其每个元素都是一个简单数据类型的对象,其结构如同一个一维的数据排列:对于一个二维数组,它的每一个元素都是一个一维数组,其形式如同一个二维的表格,表格的宽度是其中作为数据元素的一维数组的长度,高度是这样的一维数组的个数.简而言之,二维数组的结构是一个矩阵的形式.         例如,我们声明下面这样的一个二维数组: int nM

理解C语言——从小菜到大神的晋级之路(2)——开发环境的选择和HelloWorld程序

视频观看:点击这里 一.常用系统及IDE简介        常言道,工欲善其事必先利其器.除了少数奇葩之外,大部分人应不会去使用记事本或者Word文档去编程吧?几乎所有编程语言都需要一个高效易用的开发环境,C语言也不例外.那么该如何选择C语言开发的环境呢?一个编程开发环境需要考虑操作系统和编译器两部分.个人PC上常用的操作系统和编译器主要有以下几种: 1.Windows        在PC市场上,Windows操作系统一直占据着超过9成的比例.自从20多年前的Windows 3.x逐渐成熟以来

理解C语言——从小菜到大神的晋级之路(10)——结构体、联合体

本节视频链接:点击这里         上篇中讲述的数组是复合数据类型中最简单的一种,一个数组使用一段连续的内存保存了若干个类型相同的数据元素.由于类型和长度相同,数组的每个元素通过数组下标和指针变量访问.如果我们希望一个结构保存多个不同类型的数据元素,那么数组将无能为力.为了实现这样的功能,C语言提供了结构体和联合体. 1.结构体基本概念 (1)结构体的定义         假设我们需要定义一个图形中的点的概念.在一个使用笛卡尔坐标系表示图像的系统中,点的位置使用两个坐标分量表示,即横坐标x和

理解C语言——从小菜到大神的晋级之路(15)——完结篇:C编程风格

本期视频链接:点击这里 有人说过:"程序源代码其实是跟人阅读的,只是恰好机器可以编译而已".编程初学者常常会有这样一个观念,就是我的程序只要编译通过了,运行没有问题那就万事大吉了.至于代码的编写规不规范,完全就是无关紧要的小事情.如果是处于学习阶段,比如为了完成在学校的C语言课的作业,那么花心思在代码规范上的确没有特别的必要,因为这些代码基本不会进入实用工程,也不会被很多人阅读到. 但是,如果应用到了工程领域,比如在软件/互联网企业的技术研发部门,或者Github等平台上的开源工程,那

理解C语言——从小菜到大神的晋级之路(8)——数组、指针和字符串

       本期视频点击这里        在前面几次我们接触的数据类型都是简单数据类型,使用一个数据个体表示一个元素.C语言中还提供了多种复杂数据类型,其中最简单的一种就是数组.数组这一结构使用内存中一段连续的内存空间保存一组相同类型的变量,这些变量通过数组的下标/索引的不同相互区分.数组与指针有着十分紧密的联系,通常使用数组下标能实现的操作都可以使用指针完成,而且使用指针的程序通常效率更高.但是指针和数组也存在着一些明显的差别,如果误用将导致错误.另外,C语言中还定义了一种极为常用的特殊的

理解C语言——从小菜到大神的晋级之路(4)——数据类型、运算符和表达式

本期视频点击这里 一.数据类型         对数据进行处理是程序的基本功能之一,因此对于任何编程语言,数据类型都是重要组成部分之一.C语言中定义了较为完善的数据类型体系用于处理不同类型的数据. (1)标识符         C语言中的标识符可以用作变量名.符号名.函数名.文件名等等功能.标识符可以包含字母.数字和下划线(不能以数字开头).C语言是对大小写敏感的语言,因此组成相同但大小写不同的两个标识符将被当作两个不同的标识符处理.         C语言中的标识符可分为三类: 关键字:C语言

理解C语言——从小菜到大神的晋级之路(3)——C源程序的基本结构与调试方法

    本期视频点击这里        在上一篇中,我们进行了Visual Studio 2013的安装以及第一个demo程序"HelloWorld"的建立.现在我们看一下其中的源代码及相关的C语言基础知识.打开工程,可以通过在源文件标签栏的右键菜单中快速打开源代码的所在目录.HelloWorld的简单代码: #include <stdio.h> int main() { printf ( "Hello World! \n" ); return 0 ;

理解C语言——从小菜到大神的晋级之路(12)——动态内存管理

      本节视频链接:点击这里         在前面的内容中,我们通常使用数组来利用一段连续的内存空间来保存数据.我们前面用到的数组基本保存在栈内存中,其内存空间由系统自动分配和释放,使用非常方便,也不用担心内存管理的问题.但是在栈中分配的数组存在一个严重的问题,就是它的长度必须在建立时明确指定,且无法再运行时修改.为了防止运行时出现内存空间不够的问题,在编程时就必须定义一个非常大的数组来容纳理论上可能的最多个的元素,这样就会导致内存利用率底下,因为如果元素个数较少时大部分的内存空间都被浪