01
值和地址
程序和数据在计算机中是存储在一个叫存储器的地方. 没有存储器, 计算机就不能计算, 因为没地方计算(这不是废话吗). 像内存称为易失的存储器, 硬盘称为非易失的存储器. 其中SSD又叫闪存.
计算机的内存被编组成地址-值的关系对. 如: 学生的姓名和所在大学的院系之间的关系.
1、小明: 清华大学计算机系
2、小李: 北京大学计算机系
等等.
计算机内存中, 每个位置存储的不是0就是1, 如下面这样:
1、0存储在第1个位置.
2、0存储在第2个位置.
3、1存储在第3个位置.
4、0存储在第4个位置.
5、1存储在第5个位置.
我们暂且把数据大小放一边. 假定每一块数据占据着内存的一个单元. 操作系统保证任何内容都有一个唯一的正数地址. 地址不能为零或负数. NULL被定义为零地址, 表示是一个无效地址. 要记住计算机程序操作的所有内存位数的地址是不可能的. 计算机大牛们找到了一个好的解决办法: 创建标志符, 如counter或者sum来指代内存中的相关位. 若它们在程序运行期间发生变化, 这个标志符就叫作变量. 标志符对编写的计算机程序是有意义的, 编译器(如gcc)会把这些标志转化为地址. 最终的计算机程序会操作这些值, 并不会看到这些标志. 内存中只有地址和值. 请看标志和地址之间的关系:
源代码.c或.h文件
可执行程序
人类可读
通过编译器
计算机可读
标志符
地址
看代码:
int a = 5;
double b = 6.7;
char z = 'c'
标志符、地址和值可能看起来像这样:
标志符
地址
值
a
100
5
b
135
6.7
z
167
‘c’
我们只要下面的规则即可(其他的事情是操作系统和编译器干的事情. 有兴趣可以看看编译原理与linux内核源码, 至少三年起步入门吧.):
1、每个数据片段拥有一个唯一的地址.
2、地址不允许是0(NULL)或是负数.
3、编译器能够把标志符转换为地址.
02
栈
计算机把内存分为三种类型:
1、栈内存.
2、堆内存.
3、程序内存.
1、2用来存储数据, 3、用来存储机器码.
我们先来介绍栈的概念. 英文叫"stack"在"小筑的数据结构与算法模块中"已经介绍过栈了, 这里在简单说下. 栈是一种"后进先出"的结构, 比如: 刷完碗, 把碗摞起来后, 要想使用最底下的碗, 就需要将上面的碗一个一个的拿走, 这个就是栈. "摞碗"称为入栈, "取碗"称为出栈. 是伟大的图灵提出的.栈内存严格地遵守着先入后出的原则. 当然也可以实现双向栈, 这里详说了.
03
调用栈
计算机如何使用栈内存呢? 看代码:
void fi(void)
//f1前面的void表示没有返回值.
//括号中的void表示没有参数.
{
//.....
}
void f2(void)
{
//一堆代码
f1();
//另一堆代码
}
当执行f2()时, 先运行"一堆代码", 然后运行"f1()", 跳入"f1()“中执行完成后, 执行"另一堆代码”. 执行顺序就是机器码被压入栈内存, "后进先出"的方式执行.
为什么栈内存"后进先出"这么重要呢? 栈内存存储着函数调用的倒序. 先调用f2(), 则f2()入栈, 后调用f1(),f1()入栈, 然后f1()出栈执行,记录f1()执行后的位置, f1()执行完后, 从记录的位置, f2()出栈接着执行.
调用栈的规则总结:
1、如果函数有实参, 那么实参是存储在返回位置之上的.
2、实参和返回位置共同构成了调用函数的栈帧.
3、当一个函数被调用时, 这条调用之后的行编号就被压入调用栈. 这个行编号就是"返回位置". 这是在被调用函数结束(即返回)之后程序继续执行的地方.
4、若相同的函数在不同行处被调用, 那么每个调用都有一个相应的返回位置(每个函数调用之后的那行).
5、当一个函数结束之后, 程序将从存储在调用栈顶部的行编号处继续执行. 调用栈顶部的内容就会被弹出.
04
局部变量
若函数有局部变量, 那么局部变量被存储在调用栈中. 看下面的例子:
void f1(int k, int m, int p)
{
int t = k + m; //t, u为f1()中的局部变量, 随着f1()运行结束而销毁.
int u = m * p;
}
void f2(void)
{
f2(5, 11, -8);
}
如果函数有局部变量, 则局部变量被存储在实参之上. 局部变量总是存储在栈上, 函数调用期间一直存放在那里. 与它们的存在形成对比的是, "全局变量"在函数调用之间存在. 全局变量通常是在一个给出的源文件顶部进行指定, 任何函数都可以对它们进行读和写. 虽然C允许使用全局变量, 但我们要避免使用. 主要问题是全局变量可以在程序的任何位置发生改变. 当程序变得越来越大的时候, 要去追踪这些全局变量可能发生变化的位置就会变得越来越困难. 全局变量不建议, 但是全局常量是推荐经常使用的. 因为全局常量是不变的.
05
值地址
C语言中函数是可以返回值的, 看代码:
int f1(int k, int m)
{
return (k + m);
}
void f2(void)
{
int u;
u = f1(7, 2);
}
f2中的局部变量u在f2的栈帧中, 没有被赋值, 因为C不初始化变量, 所以这里u可以存储任何int值. u的地址是在f1被调用之前存储在调用栈的. 这个地址就是值地址, 因为它是函数f1存储返回值的地址. 因此, 当f1的栈帧建立后, 就要为地址再加上一行, 它的值是u的地址.
关键词return可以出于两种不同的目的被使用:
1、如果void位于函数名前面, 此函数不返回任何值. 程序运行到return时函数停止, 程序从调用函数中返回地址继续.
2、如果不是void型, return会为在调用栈中由值地址给出的变量赋值.
只要记住碰到return后程序就终止执行了.
06
数组
看代码:
int arr[5];
表示声明了一个有5个整型元素的数组. 下标从0开始, 到4结束. 数组中的元素地址总是连续的. 如果一个数组的元素没有被初始化, 那么其值就是未占用的.
初始化一个数组:
int arr[5] = {-31, 52, 65, 49, -18};
全部初始化为0:
int arr[5] = {0};
不给出长度的情况下创建一个数组:
int arr[] = {-31, 52, 65, 49, -18}; 表示长度为5的数组.
07
获取地址
可以通过&来获取变量地址. 使用printf("%p", &a);来打印变量的地址. 看代码:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv)
{
int a = 5;
int c = 17;
printf("a's address is %p, c's address is %p\n", &a, &c);
return EXIT_SUCCESS;
}
# 结果为:
a's address is 0xffffcbec, c's address is 0xffffcbe8
08
可见度
当一个函数被调用的时候, 一个新的栈帧就被压入调用栈. 函数只能看到它自己的栈帧. 看代码:
int f1(int k, int m)
{
return (k + m);
}
void f2(void)
{
int a = 5;
int b = 6;
int u;
u = f1(a + 3, b - 4);
}
int f1(int a, int b)
{
return (a + b)
}
代码中两个f1是完全相同的. 只不过将实参k,m换成了a, b, 这里强调了f2中的a,b与f1中的a,b地址是不同的. 计算机不知道标志符, 只知道使用地址和值.
09
总结
咱们介绍了栈内存的概念, 它在函数被调用时会用到. 栈内存为每一个函数存储返回位置, 值地址, 实参和局部变量.