存储类型
C语言中,每个变量和函数都有两个属性:数据类型和数据的存储类型。
变量的存储类型是指存储变量值的内存类型。变量的存储类型决定变量何时创建、何时销毁以及它的值将保持多久。计算机中有三个地方可以用于存储变量:普通内存,运行时堆和栈,硬件寄存器。变量的存储类型取决于声明变量的位置。
C语言存储类别说明符:
1、静态变量
在代码块之外声明的变量存储于静态内存中,不属于堆和栈的内存,这类变量称为静态(static)变量。静态变量在程序运行之前创建,是在将可执行文件加载到内存的时候创建,其在程序的整个执行期间始终存在。
extern:用于声明全局变量、函数,主要用于在一个文件中定义全局变量、函数,而在另一个文件中引用全局变量、函数。一般来说,函数声明一般放在一个头文件中,供其他文件引用。
static:两种用法含义截然不同
A、修饰局部变量,静态局部变量。静态局部变量和非静态局部变量区别在于存储类不同。非静态局部变量存储在栈上,静态局部变量分配在数据段或bss段静态内存中。静态局部变量的生命周期和全局变量相同,但作用域和链接属性不同,静态局部变量的作用域为代码块作用域,链接属性为无链接;全局变量的作用域为文件作用域,链接属性为外链接。
B、修饰全局变量,静态全局变量。静态全局变量和非静态全局变量的区别在于链接属性不同,静态全局变量为内链接,非静态全局变量为外链接。
2、自动变量
在代码块内部声明的变量的缺省存储类型是自动的 (automatic),存储于栈中,称为自动变量。关键字auto就是用于修饰这种存储类型的,代码块中的变量缺省情况下就是自动变量。在程序执行到声明自动变量的代码块时,自动变量才被创建,当程序的执行流离开代码块时,代码块内创建的自动变量便自行销毁。在代码块内部声明的变量,如果给它加上static,可以使它的存储类型从自动变为静态变量。但是,修改变量的存储类型并不表示修改改变量的作用域,变量的作用域仍然是代码块内部。函数的形式参数不能声明为静态,因为实参总是在堆栈中传递给函数。
函数的形式参数不能声明为静态,因为函数的参数传递是通过堆栈进行的,用于支持递归。
auto:
修饰局部变量表示自动局部变量,自动局部变量分配在栈上,默认定义的普通局部变量。
3、寄存器存储
要使变量存储于寄存器声明自动变量时需要关键字:register。
register:
register修饰的变量编译器会尽量分配在寄存器上,不保证一定会分配在寄存器中,一般的变量分配在内存中。register修饰的变量读写效率较高,用于高频次访问的变量。
C语言程序运行时有一定要求,C语言程序无法直接在内存中运行,需要外部一定的协助,协助的代码叫加载运行代码,主要作用是给全局变量赋值、清除bss段。裸机程序开发中需要。
作用域
作用域是指允许对标识符进行访问的位置范围。
C99规定,C语言的作用域共有 4 种类型:文件作用域、代码块作用域、函数作用域、函数原型作用域。编译器通过变量声明的位置来确定作用域。
1、文件作用域
代码块之外声明的标识符具有文件作用域,文件作用域的范围是从标识符声明处一直到文件的结束。如果声明在头文件中,并且头文件被其他文件用#include 所包含,标识符的作用域也会相应的扩大到包含文件的结束。
2、代码块作用域
一对花括号之间的所有语句称为代码块作用域。在代码块开始位置声明的标识符具有代码块作用域。在一个代码块作用域开始定义的变量可以被该代码块内的所有语句使用。
如果代码块之间有嵌套,那么内层代码块的标识符就会把外层代码块的同名标识符掩藏,对内层代码块标识符的修改不会影响外层代码块的同名标识符;内层代码块可以使用外层代码块的标识符。
对于非嵌套的两个代码块,两个代码块之间没有交集,那么一个代码块内的语句不能使用另一个代码块内的变量。
函数定义中的参数是代码块作用域。
3、函数作用域
只适用于goto语句的语句标签。函数作用域只适用于语句标签,语句标签用于goto语句。一个函数作用域内的语句标签必须唯一
4、函数原型作用域
在函数原型中声明的参数名具有函数原型作用域。
5、标识符的命名空间
命名空间是为了解决在相同作用域内如何区分相同的标识符。
A、只有在相同作用域的情况下才能使用到命名空间去区分标识符,在嵌套的作用域、不同的作用域区分标识符都不会用到命名空间的概念。
B、在相同的作用域内,如果命名空间不同,标识符可以使用相同的名称。否则,即如果命名空间不同,编译器会报错,提示重复定义。
C99规定C语言命名空间可以分为四种:
A、所有的标签(label)都属于同一个命名空间。
在同一个函数内,标签不能相同。
在同一个函数内,标签可以和其他变量名称相同。因为它们所属的命名空间不同。
B、struct、enum和union的名称属于同一个命名空间
C99中将struct、enum和union的名称称之为tag,所有的tag属于同一个命名空间。 也就是说,如果你已经声明struct A { int a }; 就不能在声明 union A{ int a };
C、struct和union的成员各自属于一个命名空间,而且是相互独立的
例如:如果你已经声明struct A { int a }; 其成员的名称为a,你仍然可以声明 struct B{ int a };或者union B{ int a };
struct和union的成员各自成为一个命名空间,是因为它们的成员访问时,需要通过 ".“或”->"运算符,而不会单独使用,所以编译器可以将它们与其他的标识符区分开。由于枚举类型enum的成员可以单独使用,所以枚举类型的成员不在这一名称空间内。
D、其他所有的标识符,属于同一个名称空间。
包括变量名、函数名、函数参数,宏定义、typedef的类型名、enum的成员 等等。
6、重名标识符的处理
如果标识符出现重名的情况,宏定义覆盖所有其它标识符,这是因为它在预处理阶段而不是编译阶段处理。除了宏定义之外其它类别的标识符,处理规则是:内层作用域会隐藏掉外层作用域的标识符。
同名变量的掩蔽规则:
两个同名变量的作用域没有重叠,则两个同名变量互不影响。
两个重名变量的作用域有重叠,则作用域小的变量掩蔽掉作用域大的变量,遵循就近原则。
C89标准的编译器中,所有的局部变量必须先定义在函数的前面,在C99标准的编译器中可以允许在代码块内任意位置定义局部变量,但也必须先定义再使用。
生命周期
变量的生命周期是指程序运行期间,变量从分配到地址到地址被释放的过程。根据变量的存储类型可以将变量的生命周期分为:静态生存期、自动生存期、动态分配生存期。
1、静态生存期
属于文件作用域(即external或internal链接属性)、以及被static修饰的变量,具有static静态生存期。静态生存期的变量存储在静态内存中。静态存储的变量,在程序运行之前就已经创建,在程序整个执行期间一直存在,如果声明时没有被显式的初始化,就会被自动初始化为0。
静态变量当然是属于静态存储方式,但是属于静态存储方式的变量不一定就是静态变量,例如外部变量虽属于静态存储方式,但不一定是静态变量,必须由static加以定义后才能成为静态变量。
const常量、字符串常量存储在代码段或只读数据段,取决于平台。
2、自动生存期
链接属性为none,并且没有static修饰的变量,具有automatic自动生存期。
自动生存期的变量存储于栈或寄存器中。其中在代码块内部声明的变量,在C语言书籍中也被称为“自动变量”,使用auto修饰符,默认可以省略。对于自动存储的变量当程序执行到含有自动变量的代码段时,自动变量才被创建,并且不会被自动初始化,代码段执行结束,自动变量就自动销毁,释放掉内存。如果代码段被反复执行,那么自动变量就会反复被创建和销毁。注意这一点和静态变量不同,静态变量只创建一次,到程序结束才销毁。
3、动态分配生存期
使用malloc函数,在进程的堆空间分配内存的变量。
动态分配生存期的变量存储于堆中,也不会被自动初始化,使用free函数释放内存。
相互关系
标识符的存储类型、作用域、生命周期、链接属性是相互关联的,存储类决定生命周期,链接属性决定作用域。
1、extern关键字
extern用来为一个标识符指定external链接属性。如果使用extern关键字声明某一个变量,说明变量是在别处定义的,可能位于别的文件也可能位于当前文件。
如果一个标识符在声明为extern前已经进行了声明,则链接属性由第一个声明决定(第一个声明的链接属性为external或internal时)。
2、static关键字
对于在代码块内部声明的变量,存储类型为自动变量,作用域为代码块作用域,链接属性为无链接,生命周期为自动生存周期。当static用于代码块内部的变量声明时,会将变量的存储类型从自动变量修改为静态变量,作用域仍然为代码块作用域,生命周期从自动生存期修改为静态生存周期,链接属性仍然为无链接。
对于在代码块外声明的全局变量,存储类型为静态变量,作用域为文件作用域,链接属性为外链接,生命周期为静态生存期。当static用于代码块外部的全局变量声明时,会将变量的链接属性从外链接修改为内链接,存储类型仍然为静态变量,生命周期仍然为静态生存周期,作用域仍然为文件作用域,但作用域将限定为本文件,不能被其它源文件使用。
对于在代码块外的函数定义,作用域为文件作用域,链接属性为外链接。当static用于代码块外的函数声明时,会将函数的链接属性从外链接修改为内链接,作用域仍然为文件作用域,但作用域将限定为本文件,不能被其它源文件使用。
对于在代码块内或代码块外部都有可能出现的情况,例如函数:函数声明的标识符为静态存储的,但是对于其形参是存储于堆栈中的,形参声明的变量作用域为原型作用域。对于存储于堆栈中的变量,即自动变量可以用register关键字使变量存储于机器硬件寄存器中。在代码块内部声明的自动变量可以通过static关键字修改为静态变量。
3、属性修改的一般原则
生命周期、存数类型都是针对变量, 因为在程序运行期间,只有变量才需要分配内存和释放内存,其他的诸如函数等都不需要。
修改变量的存储类型(如用static将自动变量变为静态变量),并不会修改变量的作用域,变量的作用域仍然有其声明的位置决定,
函数的形式参数,如果使用修饰符,只能使用register修饰,表示运行时参数存储在寄存器上。注意:形式参数是不能用auto修饰的。
C语言工程中标识符的使用原则
1、函数的定义与声明
在.h文件中声明的函数,如果在其对应的.c文件中有定义,那么我们在声明这个函数时,不使用extern修饰符, 如果反之,则必须显示使用extern修饰符。显示声明表示是引用外部函数,隐式声明是自己声明并定义的函数。
2、全局变量的定义与声明
全局变量应该定义c文件中并且在h文件中声明,不要定义在头文件中。
C语言的所有文件之中,只能有一个定义声明。顶层声明中,存在初始化语句是表示这个声明是定义声明,其他声明是引用声明。
所有引用声明要显示用存储类型关键字extern声明,而每个外部变量的唯一定义声明中省略存储类说明符。
所有全局变量全部以g_开头,并且尽可能声明成static类型。
尽量杜绝跨文件访问全局变量。如果的确需要在多个文件内访问同一变量,应由该变量定义所在文件内提供GET/PUT函数实现.
全局变量必须要有一个初始值,全局变量尽量放在一个专门的函数内初始化.
如调用的函数少于三个,请考虑改为局部变量实现。
数组的引用声明:extern int G_glob[100];extern int G_glob[];
3、模块化编程
为了实现编程的模块化,一般会按功能模块对函数进行封装,封装成为.h和.c文件。
.h文件是头文件,内含函数声明、宏定义、结构体定义等内容。
.cpp文件是程序文件,内含函数实现,变量定义等内容。
为了防止对头文件的重复包含,一般采用宏定义:
#ifndef XXX
#define XXX
函数声明
#endif
指针的声明和初始化
1、不恰当的指针声明
int* ptr1, ptr2;//声明ptr1为int指针,ptr2为整型
int *ptr1, ptr2;//ptr1,ptr2都声明为指针
#define PINT int *
PINT ptr1, ptr2;//等价于int ptr1, ptr2;
推荐方式:
typedef int * PINT
PINT ptr1, ptr2;//等价于int *ptr1, *ptr2;
2、使用指针前未初始化
初始化指针前使用指针会导致运行时错误,这种指针称为野指针。
int *p;
printf(“%d\n”, p);
指针变量p未被赋值指针(地址),此时p将会导致不可预知的情况。
3、处理未初始化指针
有三种方法可以用来处理未初始化的指针:
A、用NULL初始化指针
int *p = NULL;
if(NULL == p)
{
}
B、用assert函数
assert(NULL != p);
C、用第三方工具
误用指针
很多安全问题聚焦于缓冲区溢出,覆写对象边界以外的内存就会导致缓冲区溢出,这块内存可能是本程序的地址空间,也可能是其他进程的,如果是程序地址空间以外的内存,大部分操作系统会发出一段错误然后中止程序。如果缓冲区溢出发生在应用程序的地址空间内,就会导致对数据的未授权访问和控制转移到其他代码段,导致系统被攻陷。下列情况可能导致缓冲区溢出:
A、访问数组元数时没有检查索引项
B、对数组指针做指针算数运算时不够小心
C、用gets这样的函数从标准输入读取字符串
D、误用strcpy和strcat这样的函数
如果缓冲区溢出发生在栈帧的元数上,就可能把栈帧的返回地址部分覆写为对同一时间创建的恶意代码的调用。
1、测试NULL
对于动态分配内存函数一定要检查返回值,否则如果内存分配失败程序可能会非正常终止。
char *vetcor = (char )malloc(128sizeof(char));
if(NULL == vector)
{
//Malloc failure.
}
2、错误使用解引操作
声明和初始化指针的常用方法如下:
int num;
int *p = #
但是以下是错误的
int num;
int *p ;
*p = #//正确为p = &num
3、迷途指针
释放指针后却仍然在引用原来的内存,就会产生迷途指针。如果在释放指针后仍然在试图操作原来的内存,读操作可能会返回无效数据,写操作可能会破坏这块内存的值,导致其他正在使用这块内存的程序出现异常。
4、数组访问越界
C语言中数组并没有提供防止访问数组越界的机制,因此必须由程序员保证对数组的访问不越界。尤其是用指针方式访问数组元素时一定要保证不能越过数组边界。
5、错误计算数组长度
将数组传递给函数时,一定要同时传递数组长度。数组长度参数可以避免缓冲区溢出。strcpy函数就是一个允许缓冲区溢出的函数,因此尽量避免使用,使用具有缓冲区保护机制的strncpy函数。
释放问题
1、重复释放
重复释放是指将同一块内存释放两次,如:
char *name = (char *)malloc(…);
…
free(name);
…
free(name);
为了避免对同一块内存的重复释放,一般释放指针后将指针置为NULL。
char *name = (char *)malloc(…);
…
free(name);
name = NULL;
2、清除敏感信息
当应用程序终止后,大部分操作系统都不会把用到的内存清零或执行别的操作,系统可能会将之前用过的内存分配给其他程序使用,如果这些内存原来保存的是身份信息、密码信息等敏感数据,这样做就是不安全的,因为其他人可以使用这部分内存,可以通过覆写将内存中敏感数据清空。
指针类型转换
指针类型的转换可以实现一些特殊的功能,如:
访问有特殊目的的地址
判断机器的字节序
1、访问特殊用途的地址
在嵌入式系统开发中,很多特殊功能寄存器是统一编址的,通过访问这些特殊功能寄存器可以控制相应的外设的功能。
#define WTCON ( *((unsigned long *)0xE2700000))
WTCON |= 0X3<<8;
通过指针类型的转换,可以WTCON寄存器的某些位设置为1,进而控制外设。
2、判断机器的字节序
字节序是指数据在内存单元中字节的存储顺序。字节序一般分为小字节序和大字节序,也称小端模式和大端模式。小端模式表示整数的4字节的中的低地址存储整数数据的低位。通过将整数的地址从指针转换为char,打印出每个字节的内存就可以知道机器的字节序。
#include <stdio.h>
int main(int argc, char**argv)
{
int num = 0x12345678;
char *p = (char *)#
int i;
for(i = 0; i < 4; i++)
{
printf("%p:%2x\n", p,(unsigned char)*p++);
}
return 0;
}
运行结果如下:
0x7fffe8f13cb9:78
0x7fffe8f13cba:56
0x7fffe8f13cbb:34
0x7fffe8f13cbc:12
结论:低位存储低字节,小端模式。
静态链接库
静态链接库是obj文件的一个集合,通常静态链接库以".a"为后缀,名字格式一般为libxxx.a,由程序ar生成。静态链接库是在程序编译过程中链接的,已经将调用的相关函数拷贝到程序内部,程序运行时和静态链接库已经没有任何关系。
1、静态链接库的创建
A、编写源码库文件
源码库文件一般包含.c和.h文件,
hello.c文件:
#include <stdio.h>
void display(void)
{
printf(“hello world\n”);
}
hello.h文件:
#ifndef __HELLO_H
#define __HELLO_H
void display(void);
#endif
B、编译源码库文件
gcc -o hello.o -c hello.c
生成hello.o目标文件
C、将目标文件归档生成静态链接库文件
ar -cr libhello.a hello.o
D、发布静态链接库
一般来说,静态链接库需要发布libxxx.a和.h文件,.h文件可以让第三方开发者了解静态链接库中的各函数的功能和函数声明,libxxx.a文件是第三方开发者在调用静态链接库中的函数后在编译链接阶段链接的库。
2、静态链接库的使用
A、查阅静态链接库的.h文件
获取发布的静态链接库后,查看.h文件,看静态链接库的各个函数功能和函数声明。
B、使用静态链接库的某个函数
使用静态链接库时需要声明静态链接库的.h文件
#include “hello.h”
int main(int argc, char**argv)
{
display();
return 0;
}
C、编译工程文件
编译工程文件时,需要在编译链接时添加相关选项:
-Lpath:表示在path目录中搜索库文件,如-L.则表示在当前目录。
-lxxx:表示要链接的静态链接库为libxxx.a
-static:表示将所有链接的库静态加载
gcc -o main main.c -L. -lhello