笨办法学C 练习22:栈、作用域和全局
练习22:栈、作用域和全局
原文:Exercise 22: The Stack, Scope, And Globals
译者:飞龙
许多人在开始编程时,对“作用域”这个概念都不是很清楚。起初它来源于系统栈的使用方式(在之前提到过一些),以及它用于临时变量储存的方式。这个练习中,我们会通过学习站数据结构如何工作来了解作用域,然后再来看看现代C语言处理作用域的方式。
这个练习的真正目的是了解一些比较麻烦的东西在C中如何存储。当一个人没有掌握作用域的概念时,它几乎也不能理解变量在哪里被创建,存在以及销毁。一旦你知道了这些,作用域的概念会变得易于理解。
这个练习需要如下三个文件:
ex22.h
用于创建一些外部变量和一些函数的头文件。
ex22.c
它并不像通常一样,是包含main
的源文件,而是含有一些ex22.h
中声明的函数和变量,并且会变成ex22.o
。
ex22_main.c
main
函数实际所在的文件,它会包含另外两个文件,并演示了它们包含的东西以及其它作用域概念。
ex22.h 和 ex22.c
你的第一步是创建你自己的ex22.h
头文件,其中定义了所需的函数和“导出”变量。
#ifndef _ex22_h
#define _ex22_h// makes THE_SIZE in ex22.c available to other .c files
extern int THE_SIZE;// gets and sets an internal static variable in ex22.c
int get_age();
void set_age(int age);// updates a static variable that's inside update_ratio
double update_ratio(double ratio);void print_size();#endif
最重要的事情是extern int THE_SIZE
的用法,我将会在你创建完ex22.c
之后解释它:
#include <stdio.h>
#include "ex22.h"
#include "dbg.h"int THE_SIZE = 1000;static int THE_AGE = 37;int get_age()
{return THE_AGE;
}void set_age(int age)
{THE_AGE = age;
}double update_ratio(double new_ratio)
{static double ratio = 1.0;double old_ratio = ratio;ratio = new_ratio;return old_ratio;
}void print_size()
{log_info("I think size is: %d", THE_SIZE);
}
这两个文件引入了一些新的变量储存方式:
extern
这个关键词告诉编译器“这个变量已存在,但是他在别的‘外部区域’里”。通常它的意思是一个.c
文件要用到另一个.c
文件中定义的变量。这种情况下,我们可以说ex2.c
中的THE_SIZE
变量能变为ex22_main.c
访问到。
static
(文件)
这个关键词某种意义上是extern
的反义词,意思是这个变量只能在当前的.c
文件中使用,程序的其它部分不可访问。要记住文件级别的static
(比如这里的THE_AGE
)和其它位置不同。
static
(函数)
如果你使用static
在函数中声明变量,它和文件中的static
定义类似,但是只能够在该函数中访问。它是一种创建某个函数的持续状态的方法,但事实上它跟梢用于现代的C语言,因为它们很难和线程一起使用。
在上面的两个文件中,你需要理解如下几个变量和函数:
THE_SIZE
这个你使用extern
声明的变量将会在ex22_main.c
中用到。
get_age
和set_age
它们用于操作静态变量THE_AGE
,并通过函数将其暴露给程序的其它部分。你不能够直接访问到THE_AGE
,但是这些函数可以。
update_ratio
它生成新的ratio
值并返回旧的值。它使用了函数级的静态变量ratio
来跟踪ratio
当前的值。
print_size
打印出ex22.c
所认为的THE_SIZE
的当前值。
ex22_main.c
一旦你写完了上面那些文件,你可以接着编程main
函数,它会使用所有上面的文件并且演示了一些更多的作用域转换:
#include "ex22.h"
#include "dbg.h"const char *MY_NAME = "Zed A. Shaw";void scope_demo(int count)
{log_info("count is: %d", count);if(count > 10) {int count = 100; // BAD! BUGS!log_info("count in this scope is %d", count);}log_info("count is at exit: %d", count);count = 3000;log_info("count after assign: %d", count);
}int main(int argc, char *argv[])
{// test out THE_AGE accessorslog_info("My name: %s, age: %d", MY_NAME, get_age());set_age(100);log_info("My age is now: %d", get_age());// test out THE_SIZE externlog_info("THE_SIZE is: %d", THE_SIZE);print_size();THE_SIZE = 9;log_info("THE SIZE is now: %d", THE_SIZE);print_size();// test the ratio function staticlog_info("Ratio at first: %f", update_ratio(2.0));log_info("Ratio again: %f", update_ratio(10.0));log_info("Ratio once more: %f", update_ratio(300.0));// test the scope demoint count = 4;scope_demo(count);scope_demo(count * 20);log_info("count after calling scope_demo: %d", count);return 0;
}
我会把这个文件逐行拆分,你应该能够找到我提到的每个变量在哪里定义。
ex22_main.c:4
使用了const
来创建常量,它可用于替代define
来创建常量。
ex22_main.c:6
一个简单的函数,演示了函数中更多的作用域问题。
ex22_main.c:8
在函数顶端打印出count
的值。
ex22_main.c:10
if
语句会开启一个新的作用域区块,并且在其中创建了另一个count
变量。这个版本的count
变量是一个全新的变量。if
语句就好像开启了一个新的“迷你函数”。
ex22_main.c:11
count
对于当前区块是局部变量,实际上不同于函数参数列表中的参数。
ex22_main.c:13
将它打印出来,所以你可以在这里看到100,并不是传给scope_demo
的参数。
ex22_main.c:16
这里是最难懂得部分。你在两部分都有count
变量,一个数函数参数,另一个是if
语句中。if
语句创建了新的代码块,所以11行的count
并不影响同名的参数。这一行将其打印出来,你会看到它打印了参数的值而不是100。
ex22_main.c:18-20
之后我将count
参数设为3000并且打印出来,这里演示了你也可以修改函数参数的值,但并不会影响变量的调用者版本。
确保你浏览了整个函数,但是不要认为你已经十分了解作用娱乐。如果你在一个代码块中(比如if
或while
语句)创建了一些变量,这些变量是全新的变量,并且只在这个代码块中存在。这是至关重要的东西,也是许多bug的来源。我要强调你应该在这里花一些时间。
ex22_main.c
的剩余部分通过操作和打印变量演示了它们的全部。
ex22_main.c:26
打印出MY_NAME
的当前值,并且使用get_age
读写器从ex22.c
获取THE_AGE
。
ex22_main.c:27-30
使用了ex22.c
中的set_age
来修改并打印THE_AGE
。
ex22_main.c:33-39
接下来我对ex22.c
中的THE_SIZE
做了相同的事情,但这一次我直接访问了它,并且同时演示了它实际上在那个文件中已经修改了,还使用print_size
打印了它。
ex22_main.c:42-44
展示了update_ratio
中的ratio
在两次函数调用中如何保持了它的值。
ex22_main.c:46-51
最后运行scope_demo
,你可以在实例中观察到作用域。要注意到的关键点是,count
局部变量在调用后保持不变。你将它像一个变量一样传入函数,它一定不会发生改变。要想达到目的你需要我们的老朋友指针。如果你将指向count
的指针传入函数,那么函数就会持有它的地址并且能够改变它。
上面解释了这些文件中所发生的事情,但是你应该跟踪它们,并且确保在你学习的过程中明白了每个变量都在什么位置。
你会看到什么
这次我想让你手动构建这两个文件,而不是使用你的Makefile
。于是你可以看到它们实际上如何被编译器放到一起。这是你应该做的事情,并且你应该看到如下输出:
$ cc -Wall -g -DNDEBUG -c -o ex22.o ex22.c
$ cc -Wall -g -DNDEBUG ex22_main.c ex22.o -o ex22_main
$ ./ex22_main
[INFO] (ex22_main.c:26) My name: Zed A. Shaw, age: 37
[INFO] (ex22_main.c:30) My age is now: 100
[INFO] (ex22_main.c:33) THE_SIZE is: 1000
[INFO] (ex22.c:32) I think size is: 1000
[INFO] (ex22_main.c:38) THE SIZE is now: 9
[INFO] (ex22.c:32) I think size is: 9
[INFO] (ex22_main.c:42) Ratio at first: 1.000000
[INFO] (ex22_main.c:43) Ratio again: 2.000000
[INFO] (ex22_main.c:44) Ratio once more: 10.000000
[INFO] (ex22_main.c:8) count is: 4
[INFO] (ex22_main.c:16) count is at exit: 4
[INFO] (ex22_main.c:20) count after assign: 3000
[INFO] (ex22_main.c:8) count is: 80
[INFO] (ex22_main.c:13) count in this scope is 100
[INFO] (ex22_main.c:16) count is at exit: 80
[INFO] (ex22_main.c:20) count after assign: 3000
[INFO] (ex22_main.c:51) count after calling scope_demo: 4
确保你跟踪了每个变量是如何改变的,并且将其匹配到所输出的那一行。我使用了dbg.h
的log_info
来让你获得每个变量打印的具体行号,并且在文件中找到它用于跟踪。
作用域、栈和Bug
如果你正确完成了这个练习,你会看到有很多不同方式在C代码中放置变量。你可以使用extern
或者访问类似get_age
的函数来创建全局。你也可以在任何代码块中创建新的变量,它们在退出代码块之前会拥有自己的值,并且屏蔽掉外部的变量。你也可以响函数传递一个值并且修改它,但是调用者的变量版本不会发生改变。
需要理解的最重要的事情是,这些都可以造成bug。C中在你机器中许多位置放置和访问变量的能力会让你对它们所在的位置感到困扰。如果你不知道它们的位置,你就可能不能适当地管理它们。
下面是一些编程C代码时需要遵循的规则,可以让你避免与栈相关的bug:
不要隐藏某个变量,就像上面
scope_demo
中对count
所做的一样。这可能会产生一些隐蔽的bug,你认为你改变了某个变量但实际上没有。避免过多的全局变量,尤其是跨越多个文件。如果必须的话,要使用读写器函数,就像
get_age
。这并不适用于常量,因为它们是只读的。我是说对于THE_SIZE
这种变量,如果你希望别人能够修改它,就应该使用读写器函数。在你不清楚的情况下,应该把它放在堆上。不要依赖于栈的语义,或者指定区域,而是要直接使用
malloc
创建它。不要使用函数级的静态变量,就像
update_ratio
。它们并不有用,而且当你想要使你的代码运行在多线程环境时,会有很大的隐患。对于良好的全局变量,它们也非常难于寻找。避免复用函数参数,因为你搞不清楚仅仅想要复用它还是希望修改它的调用者版本。
如何使它崩溃
对于这个练习,崩溃这个程序涉及到尝试访问或修改你不能访问的东西。
试着从
ex22_main.c
直接访问ex22.c
中的你不能访问变量。例如,你能不能获取update_ratio
中的ratio
?如果你用一个指针指向它会发生什么?移除
ex22.h
的extern
声明,来观察会得到什么错误或警告。对不同变量添加
static
或者const
限定符,之后尝试修改它们。
附加题
研究“值传递”和“引用传递”的差异,并且为二者编写示例。(译者注:C中没有引用传递,你可以搜索“指针传递”。)
使用指针来访问原本不能访问的变量。
使用
Valgrind
来观察错误的访问是什么样子。编写一个递归调用并导致栈溢出的函数。如果不知道递归函数是什么的话,试着在
scope_demo
底部调用scope_demo
本身,会形成一种循环。重新编写
Makefile
使之能够构建这些文件。
笨办法学C 练习22:栈、作用域和全局相关推荐
- 计算机编程书籍-笨办法学Python 3:基础篇+进阶篇
编辑推荐: 适读人群 :本书适合所有已经开始使用Python的技术人员,包括初级开发人员和已经升级到Python 3.6版本以上的经验丰富的Python程序员. "笨办法学"系列, ...
- python教程第四版pdf下载-笨办法学python第四版
笨办法学python第四版是由Zed Shaw所编写的一本书.如果你还是Python新手,那么这是一本非常不错的入门书籍.书本里以习题方式,引导读者慢慢学会了编程. 目录: 习题 0: 准备工作 习题 ...
- python教程第四版pdf下载-笨办法学python第四版 电子书(pdf格式)
笨办法学python第四版是由Zed Shaw所编写的一本书.如果你还是Python新手,那么这是一本非常不错的入门书籍.书本里以习题方式,引导读者慢慢学会了编程. 目录: 习题 0: 准备工作 习题 ...
- python教程第四版pdf下载-笨办法学python 第四版 中文pdf高清版
笨办法学 Python是Zed Shaw 编写的一本Python入门书籍.适合对计算机了解不多,没有学过编程,但对编程感兴趣的朋友学习使用.这本书以习题的方式引导读者一步一步学习编 程,从简单的打印一 ...
- 笨办法学python pdf 第三版_笨办法学python第三版
笨办法学python第三版pdf电子书是一本Python学习参考书,是美国程序员Zed A.Shaw编著,通过简单通俗的方法,结合内部的集体,让程序员学懂python,适用于初级学习python编程的 ...
- 笔记37 笨办法学python练习43面向对象OOP的游戏代码(二)代码的反复理解
笔记37 笨办法学python练习43面向对象OOP的游戏代码(二)代码的反复理解 连续贴着这个练习43的代码折腾了整整两天,把那些英文文本翻译为中文文本,重新装进这个代码之中.本想一段一段的运行,发 ...
- [IT学习]Learn Python the Hard Way (Using Python 3)笨办法学Python3版本
黑客余弦先生在知道创宇的知道创宇研发技能表v3.1中提到了入门Python的一本好书<Learn Python the Hard Way(英文版链接)>.其中的代码全部是2.7版本. 如果 ...
- 笨办法学python在线阅读_笨办法学python全集.pdf
TableofContents 笨办法学Python 1.1 序言 1.2 前言 1.3 简介 1.4 练习0.安装和准备 1.5 练习1.第一个程序 1.6 练习2.注释和井号"#&quo ...
- 物联网课程学习目标_【笨办法学物联网】之一: 物联网工程专业四年里应该学习哪些实用技能傍身?...
我列举一些我认为物联网工程专业毕业生应该具备的基本功. 你只需要花费5000元,同时投入一定的时间,就可以相比你的同学,大大提高理论水平和动手能力. 这些内容大四出去实习前最好能都入门,同时相对比较深 ...
最新文章
- “Python之父”从Dropbox退休
- 模型评估方法和性能指标
- “中国式招标”的一些趣闻
- node版本管理工具n包使用教程
- 去哪儿-03-index-swiper
- UOJ#218. 【UNR #1】火车管理 线段树 主席树
- hiho_100_八数码
- 040_Auto.js
- cxf超时设置不起效_晚上不限时,白天1小时!上海限时长停车场来了
- 构建ai数据库_为使用AI的所有人构建更安全的互联网
- 74系列相关芯片说明
- SMOTE算法(处理非平衡数据)
- lte网络测试用什么软件,LTE_测试软件使用教程.doc
- 准备了个freyja实例项目(单数据源版)
- The server encountered an internal error () that prevented it from fulfilling this request
- nodejs stream 经典解析
- win10下shell入门学习(一)
- node php v2ex,仿V2EX开源二次元论坛程序+安装教程
- html页面,文字的自动换行
- 新绝代双骄三常见问题解答(不同于网上那片)