C语言存储类别和链接
最近详细的复习C语言,看到存储类别的时候总感觉一些概念模糊不清,现在认真的梳理一下。C语言的优势之一能够让程序员恰到好处的控制程序,可以通过C语言的内存管理系统指定变量的作用域和生存周期,实现对程序的控制。
存储类别
- 基本概念
对象:在C语言中所有的数据都会被存储到内存中,被存储的值会占用一定的物理内存,这样的一块内存被称为对象,它可以储存一个或者多个值,在储存适当的值时一定具有相应的大小。(C语言对象不同于面向对象语言的对象)
标识符:程序需要一种方法来访问对象,这就需要声明变量来实现,例如: int identifier = 1
,在这里identifier
就是一个标识符,标识符是一个名称并遵循变量的命名规则。所以在本例中identifier
即是C程序指定硬件内存中的对象的方式并提供了存储的值的大小“1”。在其它的情况中 int * pt
、int arr[10]
,pt就是一个标志符,它指定了储存地址的变量,但是表达式*p不是一个标志符,因为它不是一个名称。arr
的声明创建了一个可容纳10个int
类型元素的对象,该数组的每一个元素也是一个对象。
作用域:描述程序中可访问标识符的区域。因为一个C变量的作用域可以是块作用域、函数作用域、文件作用域和函数原型作用域。
块作用域:简单来说块作用域就是一对花括号括起来的代码区域。定义在块中的变量具有块作用域,范围是定义处到包含该定义块的末尾。
函数原型作用域:范围是从形参定义处到函数原型声明的结束。我们知道编译器在处理函数形参时只关心它的类型,而形参的名字通常无关紧要。例如:
void fun(int n,double m); 同样可以声明为
void fun(int ,double );
还有一点要注意的是函数体的形参虽然声明在函数的左花括号之前但是它具有的是块作用域属于函数体这个块。
文件作用域:变量的定义在所有函数的外面,从它的定义处到该文件的末尾处均可见称这个变量拥有文件作用域。所以文件作用域变量也被称为全局变量
链接:C变量有三种链接属性:内部链接、外部链接和无链接。具有块作用域、函数原型作用域的变量都是无链接变量,这就意味这他们属于定义他们的块或者函数原型私有。文件作用域变量可以是外部链接或是内部链接,外部链接可以在多个文件中使用,内部链接只能定义它的文件单元中使用。
存储期
指对象在内存中保留了多长时间,作用域和链接描述了对象的可见性。存储期则描述了标识符访问对象的生存期。
C对象有4种存储期:
- 静态存储期:如果一个对象具有静态存储期,那么它在程序的执行期间一直存在。文件作用域变量具有静态存储期,注意关键字
static
表明的是链接属性而不是存储期。以static
声明的文件作用域变量具有内部链接,无论具有内部链接还是外部链接,所有的文件作用域变量都具有静态存储期。
还有一种情况块作用域变量也可以拥有静态存储期,把变量声明在块中并在变量名前加static
关键字,例:
int fun(int num)
{
static int Index;
...
}
在这里变量Index
就被存储在静态内存中,从程序被载入到程序结束都会存在,但是只有程序进入这个块中才会访问它指定的对象。
-
线程存储期:用于并发程序设计,一个程序的执行可以分为多个线程,具有线程存储期的变量从被声明时到线程结束一直存在。以关键字
_Thread_local
声明一个对象时,每个线程都获得该变量的私有备份。 -
自动存储期:块作用域变量通常具有自动存储期,当程序进入定义这些变量的块时,会为这些变量分配内存,当程序离开这个块时会自动释放变量占用的内存,这种做法相当于把自动变量占用的内存视为可重复利用的工作区或暂存区。
-
动态分配存储期
五种存储类别
- 五种存储类别
存储类别 | 存储期 | 作用域 | 链接 | 声明方式 |
---|---|---|---|---|
自动 | 自动 | 块 | 无链接 | 块内 |
寄存器 | 自动 | 块 | 无链接 | 块内 关键字regsiter |
静态外部链接 | 静态 | 文件 | 外部 | 所有函数外 |
静态内部链接 | 静态 | 文件 | 外部 | 所有函数外 关键字static |
静态无链接 | 静态 | 块 | 无 | 块内 关键字static |
- 自动变量
自动变量属于自动识别的变量具有自动存储期,块作用域且无链接。可以显示的使用auto
关键字进行声明。
注意: auto是存储类别说明符和C++中的auto用法完全不同
一个变量具有自动存储期就意味着当程序进入这个块时变量存在,退出块时变量消失,原来变量占用的内存另作他用。
void hiding()
{
int x = 30;
printf("x in outer block: %d at %p
", x, &x);
{
x = 77;
printf("x in inner block: %d at %p
", x, &x);
}
// 块中内存被释放隐藏的x恢复 x = 30
printf("x in outer block: %d at %p
", x, &x);
while (x++ < 33)
{
int x = 100;
x++;
printf("x in while loop: %d at %p
", x, &x);
}
printf("x in outer block: %d at %p
", x, &x);
}
没有花括号时
void forc()
{
int n = 8;
printf(" Initially, n = %d at %p
", n, &n);
for (int n = 1; n < 3; ++n)
printf(" loop 1:n = %d at %p
", n &n);
// 离开循环后原始的你又起作用了
printf("After loop 1:n = %d at %p
", n &n);
for (int n = 1; n < 3; ++n)
{
printf("loop 2 index n = %d at %p
", n, &n);
// 重新初始化的自动变量,作用域没有到循环里的n
int n = 6;
printf(" loop 2:n = %d at %p
", n, &n);
// 起作用的仍然是循环中的n
n++;
}
// 离开循环后原始的n又起作用了
printf(" loop 2:n = %d at %p
", n, &n);
}
输出为
- 寄存器变量
使用关键字register,储存在CPU的寄存器中,存储在最快的可用内存中。
- 块作用域的静态变量
首先要明确概念静态变量并不是指值不改变的变量,而是指它在内存中的位置不变。具有文件作用域的静态变量自动具有静态存储期。
前面提到我们可以创建一个静态存储期,块作用域的局部变量,这种变量和自动变量一样具有相同的作用域,但是在程序离开块时并不会消失,
void trystat();
int main()
{
int count = 1;
for (count = 1; count <= 3; count++)
{
printf("Here comes iteration %d:
", count);
trystat();
}
trystat();
return 0;
}
void trystat()
{
int fade = 1;
static int stay = 1;
printf(" fade = %d and stay = %d
", fade++, stay++);
}
输出:
可以看出每次离开块fade变量的值都会被重新的初始化,而stay只是在编译函数void trystat()
的时候被初始化了一次,在离开自己函数体的块和for循环块之后都会递增,说明stay访问的对象一直存在并没有像自动变量一样被释放掉。
- 外部链接的静态变量
具有外部链接、静态存储期和文件作用域,属于该类别的变量属于外部变量。只需要把变量的声明放在所有函数的外面就创建了一个外部变量。为了表明该函数使用了外部变量,需要使用关键字extern
来再次申明。如果在一个源文件中使用的外部变量声明在了另一个源文件中,则必须要使用extern来申明。
外部变量可以显示初始化,如果没有则会被默认初始化为0。
- 内部链接的静态变量
具有文件作用域、静态存储期和内部链接,在所有函数外用static
来声明一个变量就是内部链接的静态变量。
例:
static int val = 1;
int main()
{
...
}
普通的外部变量可以用于程序中的任意一个函数处,但是内部链接的静态变量只能用于同一个文件中的函数。都可以使用extern
说明符,在函数中重复任何声明文件作用域变量并不会改变他们的链接属性。
例:
int global = 1;
static int local_global = 2;
int main
{
extern int global = 1;
extern int local_global = 2;
...
}
只有在多文件中才能区别内部链接和外部链接的重要性。
总结一下存储类别的说明符中关键字共有六个auto
、register
、_Thread_local
、static
、extern
和typedef
,其中static
和extern
的含义取决于上下文。
存储类别和函数
函数也有存储类别,分为外部函数(默认)和静态函数。外部函数可以被其它的文件访问,而静态函数只能被定义所在的文件访问。
例:
double gamma(double);
static double beta(int,int);
extern double(double,int);
通常使用extern
关键字声明定义在其他文件中的函数,这样做是为了表明当前文件中使用的函数定义在别处。除非使用static
关键字,否则一般函数声明都默认为extern
。
- 随机函数和静态变量
使用内部链接的静态变量的函数,随机函数(伪随机数)
// 程序 12.7
static unsigned long int next = 1;
unsigned int rand0();
int main()
{
int count = 1;
for (count = 0; count < 5; count++)
{
printf("%d
", rand0());
}
return 0;
}
unsigned int rand0()
{
next = next * 1103515245 + 12345;
return (unsigned int) (next/65536) % 32768;
}
分配内存malloc()和free()
存储类别有一个共同之处,在确定使用哪一种类型之后,会根据已经规定好的内存管理规则自动选择其作用域和储存储期。现在我们使用更加灵活的方式库函数分配和管理内存。
静态数据在程序载入内存时分配,而自动数据在程序执行时自动分配,在程序离开时销毁。现在我们可以在使用malloc()
函数在程序中动态的分配内存,malloc()
接收一个参数,所需要要内存的字节数,在内存中自动寻找一个空闲的内存块使用,malloc()
分配内存是匿名的并不会为分配的内存块赋名称,但是动态内存会返回这个内存的首地址,所以使用一个指针类型的变量来接收它,而malloc()
函数可用于返回指向数组的指针、指向结构的指针等,通常使用强制类型转换将返回的地址转为匹配的类型。
例如申请一个可容纳30个double类型值的数组
double* pt;
pt = (double*)malloc(30*sizeof(double))
注意:pt是数组的首元素地址,按照C语言的用法数组名就是首元素的地址,所以访问这个数组中元素的方法就可以这样表示pt[0]、pt[1]. . .
因此也就有了三种来表示数组的方法:
1)使用常量表达式来表示数组的维度,用数组名来访问数组的元素 。可以使用静态内存和自动内存
2)声明变长数组,用变量表达式来表示数组的维度,用数组名访问数组的元素,具有这种特性的数组只能在自动内存中创建
3)使用malloc()
动态内存来创建一个数组,先声明一个指针,接收函数返回的地址。可以使用指针访问数组的元素,指针的类型可以是静态的或者自动的。
malloc()
函数要和free()配套使用,申请的内存从malloc()
开始到free()
结束。
void dyn_arr()
{
double* ptd;
int max;
int number;
int i = 0;
puts("what is the maxnum number of type double entries!");
if (scanf("%d", &max) != 1)
{
puts("Number not correctly entered --bye");
exit(EXIT_FAILURE);
}
ptd = (double*)malloc(max * sizeof(double));
if (ptd == NULL)
{
puts("Memory allocation failed. GoodBye.");
exit(EXIT_FAILURE);
}
puts("Enter ther values (q to quit)");
while (i < max && scanf("%lf", &ptd[i]) == 1)
++i;
printf("Here are your %d enteries:
", number = i);
for (i = 0; i < number; i++)
{
printf("%7.2f", ptd[i]);
if (i % 7 == 6)
putchar('
');
}
if (i % 7 != 0)
putchar('
');
puts("Done.");
free(ptd);
return 0;
}