C语言编译过程
(.c .h源文件)->>预处理器(.i)->>编译器(.a)->>汇编器(.o)->>连接器(.out)
预处理器:(输入.c .h源文件)
- 展开所有#define宏定义
- 处理条件编译指令,例如#if #ifndef等
- 处理#include预编译指令,将包含的文件展开到该指令位置,该过程将递归展开头文件中的头文件
- 删除所有的注释
- 添加行号和文件标识,因为编译器要用到
- 保留所有的#paragma编译器指令,因为编译器要用到
编译器:(输入预处理器处理过的文件)
将预处理器的输出文件编译成汇编代码.a或.asm文件等
汇编器:
将汇编代码编译成二进制的目标文件
连接器:
将汇编器生成的各个目标文件和引用的库代码、启动代码等链接并形成可执行文件
预处理器
预处理器是在编译前对源程序进行初步处理的一个小程序。预处理器只完成简单的文字替换和删减工作。所有的预处理命令都由#开始并直到其后的第一个换行符为止(可以使用连字符"\"扩展到下一行)。所有的预处理命令"#"左侧允许有空格和制表符,但是不能有其他字符,所以{#include}是非法的。
预处理器命令
命令
描述
命令
描述
#define
宏定义
#else
else否则则包含
#include
文件包含
#endif
条件编译结束
#ifdef
如果定义则包含
#line
重设置行号
#ifndef
如果未定义则包含
#error
产生出错信息
#if
如果条件为真则包含
#pragma
编译器命令
#elif
else if否则如果为真则包含
defined
用于#if define()或#if !define()
#define宏
用法: #define [宏名称] [宏定义主体]。程序中应尽量少的使用宏定义,使用const和枚举代替宏常量,使用内联函数代替宏函数。
- 对于#define宏来说,预处理器在程序中将宏替换成#define所定义的字符串,这称为宏展开。所以宏只是简单的字符串替换功能,并不遵从C语言的语法规则。
- 可以使用#undef来取消"#define"的宏定义,之后则可以重新定义一个新值。
- 预处理器将创建一个宏定义的展开替换列表,在源程序中一旦发现宏则将其按替换列表展开;展开后的代码仍包含宏定义,则再次查询替换列表并依次递归展开。但是如果展开代码中包含宏定义自身,则不会将其展开。
- #运算符,可以利用宏参数和#来创建字符串:
#define PRT_INIT(V) printf(#V"is:%d",V)
假设变量var为0则,使用PRT_INIT(var)则输出为var is:0 因为#V将var参数转换为了字符串
- ##运算符,符号链接运算符。#define XNAME(N) X##N 调用时XNAME(4)将被展开为 X4;其将宏参数和其他标识符连接成一个标识符。
- 定义多行宏
当一个宏定义过长或需要定义为多行时,可以使用连字符"\+换行"来连接宏定义。例如:
#define CHECK_VALID(X) ((X)==B || \ (X)==C)/*使用连字符将两行连接为一行 */
其等效为#defineCHECK_VALID(X) ((X)==B||(X)==C)。注意连字符后不能跟任何字符包括注释和空白。
- 宏产生的问题:特别是宏函数,虽然宏函数看起来像函数,但是他们的行为不完全相同。
- 宏定义中每一个函数参数引用都要加括号并且整个宏定义要加外括号。
例如#defineSQU(X) X*X这样的宏定义在展开时将会出现错误,例如SQU(1+2)将会被展开为1+2*1+2,再例如4/SQU(2)将会展开为4/2*2都将会得到错误的结果,应该定义为#defineSQU(X) ((X)*(X))
- 宏函数使用时其参数不得使用包含副作用的语句、函数调用和volatile变量。例如宏定义为:#defineSQU(X) ((X)*(X))如果使用表达式SQU(++i),由于预处理器不进行计算只进行字符串替换,将会被展开为((++i)*(++i)),i被递增了两次,所以宏函数中不得出现有副作用的参数。而使用内联函数不会出现这样的问题。
- 将多行宏函数定义包含在do{}while(0)块中,使其可以出现在需要单条语句或代码块的场合。例如:
#define ECHO(S) \ do{ \ gets(s); \ puts(s); \ } while (0)
当使用if() ECHO(str); else fun();这样的宏调用时将不会出现问题。这样定义宏函数更像是C语言中的一个语句。
预定义宏
C语言标准预定义了以下宏:
示例
说明
__LINE__
当前行号(数字)
__FILE__
当前文件名称(字符串)
__DATA__
当前编译日期,格式为"月日年"(字符串)
__TIME__
当前编译时间,格式为"时:分:秒"(字符串)
__STDC__
如果编译器接受标准C,那么其值为1
__func__(C99)
当前函数名称字符串
__func__在C99中其实并不是定义为宏,是在函数内部定义的一个静态变量:static char __func__[] = "函数的名称";
#include文件包含
预处理器发现#include命令后,就会寻找后跟的文件名并把整个文件的内容替换到#include所在的位置。所以#include完成的也是简单的文字替换工作。
示例
说明
#include <stdio.h>
搜索系统目录,不要对自己编写的头文件使用<>符号
#include "hot.h"
先在当前目录搜索,之后在标准位置搜索
#include "/usr/biff/p.h"
搜索包含目录下的/usr/biff目录
#include可以包含任意的文件,不只是.h头文件。例如可以将字模数据转换为hex字符格式"fontcode.c"文件,可以包含在数组的初始化中。
const unsigned char fontCode[] = { #include "fontcode.c" };
C语言头文件中可以包含宏定义、宏函数、函数声明、外部引用声明、类型定义、常量定义、内联函数。其他数据尽量不要放在头文件中去。
条件编译
可以使用#ifdef、#else和#endir等指令来进行条件编译。预处理器根据设定的条件来决定是否保留相应的代码块。例如:
#ifdef AT24C02 int AT24C02; #endif
则包含在#ifdef和#endif中间的代码只有在AT24C02被宏定义时才会包含到编译器中,否则将会被预处理器删除。
也可以使用#if defined(AT24C02) && !defined(MACRO)来使用更多的条件。
基本数据类型
C语言数据分为基本数据类型(整型数据和浮点数数据)和复合数据类型。
数据关键字
int
long
short
unsigned
char
float
double
signed
void
_Bool
_Complex
_Imaginary
变量声明和初始化
int var, var2; unsigned short adata = 0; float afloat = 3.14f, bfloat = 0.0; double adouble = 3.14; int *ptr1, *ptr2;
声明的方式是[类型名]+[声明表达式]的方式。在声明变量的同时可以使用“=”赋值符号将变量初始化为指定的数值。不建议在一行声明多个变量。
- 一行声明多个变量
- C允许在一行中声明和初始化多个变量,变量之间使用逗号分隔;但这种方式可读性不好,只适用于一些特别显而易见的变量声明。例如i,j等索引变量。
- 对于指针和数组,么个变量都要加*和[]运算符。例如int *ptr1, *ptr2; 将声明两个指向int的指针。int arr1[], arr2[];将声明两个int数组。
- 初始化时应对每一个需要初始化的变量使用赋值语句。int *ptr1, *ptr2 = 0;只会将ptr2初始化为0,ptr1未被初始化。
- 基本数据类型
- C语言中并没有规定整数类型的大小;只保证int不会比short短,long不会比int短。并且保证int和short至少有16位长,long long类型至少64位长。如果对整数的长度要求比较严格,应使用stdint.h中定义的确定长度的类型,例如uint8_t、uint16_t等
- int类型是系统中的基本类型;是编译器处理起来最有效的类型,长度为处理器的字长。所以所有小于int的整数类型都将被默认自动提升为int类型。
- C没有规定浮点数的精度和大小。只规定float至少有6位有效数字,取指范围至少为10^-37~10^37;double至少包含10位有效数字
- 无符号整数溢出后将从0继续计数。但C语言没有定义有符号整数的溢出规则,所以有符号整数溢出后的结果是不可预知的。对于大多数系统有符号数溢出后从最小负值开始计数。
- C语言没有规定char是有符号还是无符号的类型。当用char类型表示数值时,应使用unsigned char 和signed char来明确说明有无符号。如果表示字符和字符串,应单独使用char,因为char本身就代表字符,有无符号无影响。
字面常量
出现在程序源码中的数字常量为字面常量,例如int var = 100;中100就是字面常量。
- 10进制整数
程序中的10进制整数字面常量默认使用int类型存储,如果int类型无法表示,将按long int、unsigned long、long long、unsigned long long依次尝试存储。
- 八进制、十六进制
程序中的八进制和十六进制整数字面常量默认使用int类型存储,如果int类型无法表示,将按unsigned int、long int、unsigned long int、long long int、unsigned long long int依次尝试存储。
- 0+[数字]为八进制表示,例如012为八进制
- 0x+[数字]或0X+[数字]为十六进制表示,例如0x12
- 浮点字面常量
默认情况下,编译器将浮点常量当作double类型。可以使用后缀来强制指定其存储类型。
- 指定字面常量类型
在字面量后面加上修饰符可以使其存储为指定的类型;
符号
类型
示例
U(u)
unsigned int
10U、10u
L(l)
long int
10L、10l
UL(ul)
unsigned long int
10UL、10ul
F(f)
float
10.0F、1E-7f
LL(ll)
long long int(C99)
10LL、10ll
ULL(ull)
unsigned long long int(C99)
10ULL、10ull、0x20ULL
LF(lf)
longdouble(C99)
10.0LF、1lf
由于小写字母l和1比较相似,应优先使用LU、LLU、LF等大写后缀修饰符
- 字符串常量
多个紧靠的字符串常量会自动链接成一个字符串常量
char* simplestr ="hello world " "string " "simple";
中simplestr将变为"hello world string simple",两个字符串自动链接为一个字符串,也可以使用连字符来链接字符串
char* simplestr ="hello world \ simple";
具有同样的效果
定义常量
程序中应避免出现纯数字的常量(称为魔术数字),应使用以下几种方式为常量数字命名一个合适的名称
- 使用#define定义常量;由于宏并不遵从C语言的语法规则,应尽量少使用宏定义。示例:#defineNUM (10)
- 使用const;示例:const int cst = 10;
- 使用枚举;示例:enum {CST = 10,NUM=100};
C99扩展
- 声明位置
C99允许在使用变量前任意地方声明变量,C89只能放在代码块开始处声明;例如:for (int i = 0; i<NUM; ++i){}在C99标准中是合法的。
- _Bool类型
C99引入了_Bool类型来表示布尔类型,在stdbool.h头文件中将其定义未bool类型,并且定义了true和false两个布尔常量。用1表示true用0表示false。
- long double类型
C99增加了long double,保证至少比double范围大。其字面常量需要加后缀表示:3.14LF
- 浮点数十六进制表示法
- C99增加了浮点数的十六进制表示法,例如0xa.12p10这些数字都是十六进制表示法,例如0xa为10,.12表示为1/12^12,后面的10表示指数2^10
- 虚数类型
C99同时增加了虚数类型:_Complex、_Imaginary
可移植类型
C99标准中 stdint.h头文件中包含标准的可移植类型。其类型都为确定长度的类型,例如:uint8_t、int16_t等。对数值大小有要求的场合应优先使用这些类型。
表达式
C语言中使用运算符进行各种各样的运算,由运算符和操作数组成的语句称为表达式。所有的表达式都是一个数值,例如"i++"、"a>b"都是一个数值。
运算符
- 优先级:当共享操作数的两个运算符优先级不同时,操作数优先与高优先级的运算符结合。注意只有在两个运算符共享操作数时,运算符的优先级和结合性才会适用。例如a*b+b/2可以等效为(~a*b)+(b/2),虽然"~"的优先级比"/"的优先级高,但是两个运算符不共享操作数,所以两个运算符的计算顺序不确定。但是~a*b共享操作数a,则将会按照优先级将a与~运算符结合。
- 结合性:当共享操作数的两个运算符优先级相同时,操作数优先按照运算符的结合性,从左到右或从右到左与运算符结合。
- 结合计算不同于计算顺序:C只规定了表达式中按照运算符的优先级和结合性来结合计算,并没有规定一些计算顺序。例如3*4/5+*i;根据优先级和结合性可以转换为表达式:((3*4)/5)+(*i);但是并没有说明加号左面和右面子表达式的计算顺序,C编译器可依据效率来选择哪一个子表达式先计算。运算符优先级和结合方式表:
运算符
结合方式
优先级
++(后缀) --(后缀) ()(调用函数) [](数组下标) {}(代码块) ()(小括号) . ->
一元运算符
左→右
1
++(前缀) --(后缀) +(正号) -(负号) ~ ! sizeof *(取值) &(地)
(类型)(类型转换)
一元运算符
左←右
2
* / %
二元运算符
左→右
3
+(加) -(减)
二元运算符
左→右
4
<<(左移位) >>(右移位)
二元运算符
左→右
5
< > <= >=
二元运算符
左→右
6
== !=(比较运算符)
二元运算符
左→右
7
&(位与)
二元运算符
左→右
8
^(位异或)
二元运算符
左→右
9
|(位或)
二元运算符
左→右
10
&&(逻辑与)
二元运算符
左→右
11
||(逻辑或)
二元运算符
左→右
12
?:(条件表达式)
三元运算符
左←右
13
= *= /= %= += -= <<= >>= &= |= ^=(赋值运算符)
二元运算符
左←右
14
,(逗号运算符)
二元运算符
左→右
15
副作用和顺序点
每个表达式都代表一个数值,在表达式计算中改变某些变量的数值,称为副作用。
- 副作用:在表达式中对变量进行的修改就称为副作用,例如=、+=、-=、*=、/=、++、--等运算符都会产生副作用
- 顺序点:顺序点是表达式副作用的结算点,在顺序点后的所有的副作用将会被计算。所有的变量副作用都将会赋值给变量,例如++、--后缀将会被计算到变量。分号、逗号和完整的一个表达式都是一个顺序点。
子表达式的求值顺序
- C只规定了函数调用、&&、||、?:和逗号运算符的求值顺序,其他子表达式的求值顺序和副作用的发生顺序都是未指定的。例如a=(b+c)*(c++),无法知道哪个括号内的子表达式先求值。
- 在表达式中同时包含引用和副作用时会出现问题,例如a=(++a)*(a+2)结果将是未定义的,有的编译器会先计算++a,有些会先计算a+2,两种方法计算出来的数值完全不同。
- 每个表达式都有一个数值,对于比较表达式,真表达式的值为1,否则为0;所以!0就等于1,而C将所有非0值都认为真。例如(2>1)表达式的值为1,而(4==5)表达式的值为0。
- C程序中条件判断表达式中使用赋值运算符将会产生警告。例如if(x=12)这时可以加入判断语句来防止警告
例如:if(0!=(x=12))
- 对于嵌套函数调用和嵌套括号,C语言规定最内层优先计算(因为外层需要内层的计算值)。例如(a+b*(a*b-b/c))中公式可以改写为(a+(b*(a*b-b/c))),其中"a"和"b*(a*b-b/c)"这两个子表达式的计算顺序是不确定的,但是在计算"b*(a*b-b/c)"这个子表达式时,一定是先计算"(a*b-b/c)"。但是a*b和b/c的计算顺序仍然是不确定。
逻辑运算符
- "||"、"&&"逻辑表达式的子表达式总是从左到右计算;并且&&和||是一个顺序点,这两个运算符后面所有的副作用都将被计算。所以while ((c=getchar)!=' ' && c!='\n')是有效的,因为左面的子表达式将会被先计算。
- "||"、"&&"逻辑表达式在自左向右计算时,一旦发现可以确定整个表达式为假或为真时就停止计算,后面的将不会再计算求值。例如if(a>1||++a<10)中,如果a>1为真则++a表达式将不会被计算,当a>1为假时才会再判断++a的真假。所以逻辑表达式中不要包含副作用运算,因为根据前面的表达式的值,后面语句可能被执行也可能不会被执行。
逗号运算符
- 逗号运算符的子表达式从左向右依次计算,表达式整体的值为最右面的子表达式的值。例如(i=0,i++,i+2)表达式,将会首先计算i=0之后i++,由于逗号运算符是一个顺序点所以在计算i+2之前将i++的副作用(值加1)保存到i中,之后计算i+2。并且整个表达式的值是3(值为最右面子表达式i+2的值)
- 逗号运算符是一个顺序点,逗号运算符右面所有的副作用都将会被计算。
例如:i=0;a=i++,b=i;首先i++将会被计算同时在计算b=i前,将i++的副作用(加1)保存进i,整个表达式为b=i;所以a为1;使用这个特性可以在循环和控制表达式中使用逗号运算符,例如:for(int i=0; ++b,++c,i<8;++i){...}
条件运算符?:
?:选择运算符是C语言中唯一的一个三元运算符,用来表示if else的选择形式。例如:A?B:C在A为真时将计算B表达式,否则计算C表达式,整个表达式的值在A为真是值为B否则为C。同样不得在表达式中使用具有副作用的运算,例如:x=(y>0)?ch=getchar():'b';这样的表达式当y>0为假时,ch=getchar()将不会被计算,会让人产生困惑。
类型转换
两个不同类型操作数一起运算时,C语言将会将其转换为同一个类型再进行运算。
- 自动整型提升
表达式中的有符号和无符号char和short自动转换为int类型(因为int是处理器最有效的处理类型),short和int同样长度的时候,unsigned short将转换为unsigned int。
- 一个运算符的两个操作数类型不同时(int以下类型被提升为int类型),较小类型操作数将会转换为较大的类型。
- 类型级别从低到高的顺序是:int、unsigned int、long、unsigned long、long long、unsigned long long、float、double、long double。在long和int类型长度相同时,将直接转换为unsigned long
- 自动类型转换会产生问题,例如unsigned int a=1;if (-1<a){}中-1被自动转换为unsigned int,数值为0xffffffff则将会比a大。
- 自动类型转换是运算符的两个操作数之间的类型转换,一个表达式的转换则按照运算符的优先级和结合性,依次组合成两个操作数的子表达式,再按上述规则进行类型转换。一定要注意类型转换是按两个操作数进行转换的。例如:
(unsigned long)a+1000000000*100;仍会是错误的计算结果,因为后面两个int计算已经溢出了,之后再加unsigned long类型的变量a,已经是错误的数据了。
- 使用~运算符在自动整型提升时也会出现错误,unsigned char a = 0, b = 0xff;if (a == ~b)将永远不为真,因为b将会被提升为整型0x000000ff,~b为0xffffff00两个值肯定不相等
- 在赋值语句中,计算的结果被转换为被赋值变量的类型。
- 当作为函数参数传递时,如果函数没有参数列表,char和short被转换为int,做整数类型提升,float会被转换为double。有参数列表将按照参数列表转换。
过程控制
关键字
if
else
for
switch
while
case
break
continue
do
goto
return
default
C99中规定,只有返回类型是int类型的main函数时,程序未调用return返回则默认返回值0.
C89只能识别标识符的前31个字符,C99可以识别前63个
条件和循环
示例
说明
入口循环
while(i<num){}
如果条件为真则一直循环
入口循环
for(int i=0;i<num;++i){}
设置初值,判断循环条件是否为真,并修改计数
出口循环
do {}while(i<num)
执行循环,并判断循环条件是否为真
if、else、break、continue的结合性
if、else、break、continue都和前面最靠近的语句结合,例如:
if (X) if (Y) {} else {}
后面的else与最靠近的if(Y)结合
if (X) { if (Y) {} else {} }
函数
函数原型
- 函数原型用于告诉编译器函数类型。函数调用时依据函数原型来检查参数和转换参数类型,如果没有发现函数原型,将隐式创建默认函数原型。函数原型中可以省略参量名称。例如void fun(int , float)也是合格的函数原型。
- 函数原型和声明中可以使用数组的形式来声明参量,例如void fun(int* ptr, int arry[]), 这里int arry[]与int* arry是等效的。其他地方不允许arry[]这样的声明
内联函数
- C99增加了内联函数支持,内联函数是建议编译器对函数的调用进行速度优化,编译器会根据情况判断是否内联,也可能将内联函数转换为正常函数调用。函数内联时,编译器将对函数的调用替换为内联函数代码,减少了入栈、出栈和跳转的时间,提高了运行速度,但是会使代码容量变大,是以空间换取时间的方法。
- 无法获取内联函数的函数地址,如果用代码获取内联函数指针,编译器将产生普通函数
- 应使用static inline来声明内联函数,并且应将函数定义(不是原型)放在首次调用内联函数前
static inline void infun(void){ … } int main(void) { infun(); }
编译器看到内联函数声明后,在函数需要内联时,将函数定义替换到函数调用处。有些类似宏函数展开,但是infun(++i)这样的函数调用对于内联函数会先计算++i表达式的值,之后再使用到内联代码中去。
- 内联函数及其调用必须在同一个文件中,所以对于内部函数,应放在文件最前面。如果多个文件都需要用到该内联函数,则应该将内联函数定义放在头文件中。除了内联函数以外尽量不要在头文件中放可执行代码
参数传递方式
函数调用时C语言支持两种参数传递的方式
示例
参数传递方式
说明
void fun(int a);
按值传递
将参数的值复制一份到堆栈或寄存器中来传递到函数内使用
void fun(int *a);
按地址传递
将参数的地址复制一份到堆栈或寄存器中来传递到函数内使用。参数指针是按值传递的,参数本身是按地址传递的
函数调用过程
- 堆栈和堆
- 堆栈:具有入栈和出栈操作一块后进先出的内存区域,由C编译器自动分配和释放。C编译器通过入栈出栈的操作来分配和释放内存资源。
- 堆栈操作
入栈:程序将CPU寄存器、函数参数、自动变量和中间变量等复制到当前SP栈指针后,并移动SP栈指针。为这些自动变量分配了内存。
出栈:程序直接将SP栈指针恢复为入栈前的数值。栈指针后之前的数据变为了无用值。
- 堆栈按其入栈后栈指针的增加的方向分为两种方式:递增堆栈(入栈后栈指针值变大),递减堆栈(入栈后栈指针变小)
- 堆栈按SP栈指针指向的内存是否为空闲区域分为两种方式:满堆栈(SP指向最后一个入栈对象),空堆栈(SP指向下一个空闲位置)
- 堆:由程序员手动操作分配和释放内存的内存区域。用于动态内存分配,程序中调用分配资源接口时,将从堆的空闲区域划分出一部分内存;调用释放资源时,将对中的这一部分内存更改为空闲区域。
- 函数调用过程
1. 把函数的参数压栈或者储存到寄存器
2. 跳转到函数
3. 把函数使用到的一些寄存器压栈
4. 执行函数
5. 处理函数返回值
6. 对于第3步中压栈的那些寄存器,恢复它们原来的值
7. 根据不同的调用约定,清除第1步中压栈的参数,然后返回,或者先返回然后清除。
高级数据类型
存储类
- 术语
- 存储时期:指示变量什么时候被分配内存,静态:程序开始时被分配并一直存在,自动:程序运行时执行到变量定义处即在栈上分配,退出作用域时释放。使用动态内存分配可以在堆上手动管理存储时期
- 作用域:指示程序中那一部分可以引用访问变量(变量对哪部分代码可见)。文件作用域:在声明后的整个文件范围内都可见;代码块作用域:在声明后的整个代码块范围内都可见。
- 链接:描述了变量可以和那些目标文件链接在一起。空链接:只在声明的代码块中可见,无法和任何代码块链接。内部链接:只能和文件内部的函数链接,可以在文件中任何地方使用。外部链接:可以和其他文件链接,可以在其他文件中使用。
- 存储类
- 自动变量:在函数内部、函数原型中、代码块中声明的变量为自动变量。其只在代码块内可见,并且使用入堆栈自动存储,可以使用auto来显示说明变量为自动变量,也可以忽略。例如代码块中声明:int a,b;
- 寄存器变量:使用register关键字声明的变量,无法取得该类型变量的地址,无法按地址传递。其他同自动变量。例如:register int a,b;
- 空连接静态:代码块内部使用static声明的静态变量,其只在代码块内可见,并且一直存在。
- 内部链接静态:在函数外使用static声明的静态变量,其在整个文件中声明以后都可见,并且一直存在
- 外部链接静态:在函数外声明的全局变量,其对所有引用的文件都可见,并且一直存在
- 动态内存变量:使用内存分配和释放函数在堆中创建的变量。
存储类
持续时间
作用域
链接性
声明方式
自动变量
自动(自动在堆栈中分配和释放)
代码块可见
空
在代码块内、函数原型中声明
寄存器变量
自动(自动在堆栈中分配和释放)
代码块可见
空
在代码块内使用关键字register
具有外部链接的静态
静态(一直存在)
整个文件可见
外部
在所有函数外声明
具有内部链接的静态
静态(一直存在)
整个文件可见
内部
在所有函数外使用关键字static
具有空链接的静态
静态(一直存在)
代码块可见
空
在代码块内使用关键字static
动态内存变量
手动(手动分配和释放)
由指针决定
空
使用动态内存分配
int a; //具有外部链接的静态变量 static int b;//具有内部链接的静态变量 int main(void) { static int c; //空链接的静态变量 int d; //自动变量 register int c;//寄存器变量 }
变量初始值
声明变量时对变量进行初始化操作,则变量将被初始给定值。如果没有显示初始化,不同存储类型变量的默认值是不一样的。
- 自动变量和寄存器变量是前次使用遗留下来的值,其值是不定的。
- 所有的静态存储变量包括函数外声明的、函数外使用static声明的和代码块内使用static声明的变量,在不显示初始化时,在启动代码中被初始化为0。
- 动态内存变量在分配内存后,其值为该内存区域前次使用遗留下来的值,其值是不定的。
复合类型
由int、float等组合在一起构成的数据类型称为复合类型。
数组
- 数组声明和初始化
- 数组只能在声明时进行初始化操作,不能使用port={1,2,3,4,5};这样的赋值操作。
- C99增加了指定元素初始化,例如int smp[5]={[2]=5,1,[4]=2};未被初始化的项目被设置为0。另外[2]=5后面的1将会依次初始化给[3]
- C99允许变长数组,即可以使用已知值的变量来指定数组大小。int n = 5; float array[n];在C99中是合法的
- 如果部分初始化数组(只初始化部分元素),则未被初始化的元素将被自动初始化为0,自动存储类型数组也是如此。如果不初始化数组,则自动存储类型的数组的数据是未知的,静态存储的数据全部初始化为0
- 声明数组时,编译器可以根据初始化列表来确定数组大小。例如int array[]={0,1,2,3,4,5};则array大小是6
- 数组使用
- 定义一个大小为N的数组,其索引范围为0~N-1,超过数组边界的索引的结果是未知的。
- 数组名为数组首元素的地址,所以int arry[8]中arry==&arry[0];
结构体
- struct结构体可以直接进行赋值操作,例如a、b是相同类型的两个结构体变量,则可以a=b这样赋值.
- 结构体初始化。例如struct {int x; int y;} xy = {100, 10};
- C99支持结构指定初始化项目,例如struct {int x;int y;int z;} xy = {.x=100, .y=10};其他未初始化的成员被设置为0
- C99支持伸缩数组成员。struct {int count;int y[];}其中y[]为伸缩数组成员(其实是一个指针),其必须是最后一个成员。例如位图bmp结构体,前面是描述结构体,后面是像素数据,就可以使用该类型结构体。创建伸缩数组结构体指针,并分配相应的存储后,最后一个伸缩数组元素,为位图像素数据数组。
- C语言中未规定位段的分配顺序,例如struct { int bit1:1;int bin2:1;};这个位段C语言没有规定谁在高位谁在低位
- 结构体中的空隙。为了优化效率编译器分配变量和结构体字段时会保证字节对齐。例如short 2字节数据类型起始地址要能被2整除,int 4个字节数据起始地址要能被4整除。有些系统不支持非对齐访问,数据内存对齐是必须的。所以像
struct{char a; short b; int c;};这样的结构声明中,字段a和字段b要空出一个字节内存来保证b是对齐的,就会产生字段空隙。
枚举
- 枚举常量都是int类型,枚举只能在初始化时赋值
- 枚举初始化,enumae{ a, b, c };此时a、b、c的值分别为0、1、2,可以指定其数值enumae{ a=2, b, c };后面没有指定数值的依次排序,b、c的值分别为3、4
- 保证所有枚举常量都映射到唯一的数值,例如enumcolor{red=4, orange, yellow, green=5, violet};这样的初始化会导致orange和green都为5,yellow和violet都为6;而程序员却不容易发现这一问题而出错,要求枚举要么不指定枚举数值,要么就只指定第一个枚举常量数值,要么就指定所有的枚举常量数值。
- 匿名枚举:C语言允许声明一个不带任何名称的枚举,可以代替#define来创建常量。
例如:enum{Hours=24, Minutes=60, Second=60};则Hours、Minutes、Second就被定义为枚举常量。可以用在任何需要常量的场合例如:int data[Hours][Minutes][Second];
const
- const表明声明的对象是一个常量,例如const int x = 10;使用const声明的常量只能在声明时进行初始化,其值以后都将无法修改
- const int* ptr;声明一个指针ptr,其指向一个int类型的常量,意味着*ptr = 0;这样的语句是非法的
- int*constptr;声明了一个常量指针,其指向一个int类型,意味着ptr = 0;这样的语句是非法的
- const int*constptr;声明了一个指向常量的常量指针,意味着不能修改指针的值,也不能修改指针所指向的值。
- 指向常量的指针不能赋值给指向非常量的指针,反之指向非常量的指针可以赋值给指向常量的指针。
const int a = 10; int b = 12; int* ptr1 = &a; //错误,不能赋值 ptr1 = &b; //正确 const int* ptr2 = &a; //正确 ptr2 = &b; //正确
指针
基本概念
程序中所有的变量、函数、数组等都会被放在统一编址的RAM或FLASH中。这些数据在内存中都有一个内存位置称为内存地址。
指针两要素:指针的值:数据对象的内存位置(去哪里找到数据),指针的类型:如何解析数据(按int还是结构体解析数据)。一旦知道指针的值和类型就可以通过指针将数据从内存中读取出来。
指针加1等于指针数值加上其所指向对象的字节数;指针减1等于指针数值减去其所指向对象的字节数。
数组地址
C语言中数组名就是数组第一个元素的地址。
定义一个2维数组:char ary[3][2];所以数组名ary==&ary[0]
在这个二维数组中&ary、ary、&ary[0]、ary[0]、&ary[0][0]的数值是相等的,但是指针类型不同。由于数组名就是数组的首元素地址,所以ary和&ary[0]的指针类型相同,ary[0]和&ary[0][0]的指针类型相同。
数值相等类型不等的地址类型:
表达式
等效值
类型
说明
&ary
(char [3][2])*
指向ary[3][2],类型为一个包含3个元素每个元素为包含2个char元素的数组的数组的指针。
ary
&ary[0]
(char [2])*
数组名==数组首元素地址,类型为指向包含两个char元素的数组的指针
ary[0]
&ary[0][0]
char*
类型为指向char的指针
数组和指针:
表达式
等效值
解释
ary
&day[0]
第1个大小为2个char元素的数组的地址
ary+2
&day[2]
第3个大小为2个char的数组的地址
*(ary+2)
*(&day[2])
day[2]
&day[2][0]
第3个包含2个char的数组中的第1个元素的地址
*(ary+2)+1
&day[2][0]+1
&day[2][1]
第3个包含2个char的数组中的第2个元素的地址
*(*(ary+2)+1)
*(&day[2][0]+1)
第3个包含2个char的数组中的第2个元素
结构体指针
定义一个结构体:
struct { char a; short b; short c; }stt;
stt结构体的地址为&stt,而结构体第一个元素a的地址是&stt.a。由于a是结构体的第一个元素,所以其地址和结构体的地址是重叠的,也就是说&stt和&stt.a在数值上相等的,但是类型不同,&stt.a为指向char的指针。
指向指针的指针
指针可以指向任意类型,同样也可以指向指针。
例如上图int **ptr;中ptr保存在内存0xFFFF0000处,这个值是ptr的地址。
- ptr保存的值是一个地址0xFFFF0014,对ptr解引用*ptr就得到了该地址处保存的数值0xFFFF0008,为一个int变量的地址。
- 再对(*ptr)解引用就得到了0xFFFF0008地址处保存的数值123456,为int变量的数值。
- int **ptr就声明了一个指向指针的指针。可以根据*运算符的结合性将式子改写为int (*(*ptr)),首先(*ptr)表明ptr是一个指针;然后(*(*ptr))表明ptr这个指针指向的类型也是一个指针;而int则表明ptr是一个指针其指向一个指向int类型的指针。
void指针
使用void* ptr;可以声明一个void指针,表明该指针可以指向任意类型。
- 任何指针都可以无需转换而赋值给void指针
type *p;
vp=p;
- void指针赋值给其他类型的指针时都要进行转换
type *p=(type*)vp;
指针声明修饰符
修饰符
含义
*
表示一个指针
()
表示一个函数
[]
表示一个数组
复杂指针识别
指针声明也遵从运算的优先级和结合性,可用于理解一些复杂的指针声明类型
表达式
改写后
分析步骤
int *ptr
int (*ptr)
- (*ptr)说明ptr是一个指针
- int说明ptr指向一个int类型。所以ptr是指向int类型的指针
int (*ptr)[5]
int ((*ptr)[5])
- (*ptr)说明ptr是一个指针,
- ((*ptr)[5])说明ptr是指向的数据类型是含有5个元素的数组,
- int说明该数组每个元素都是int类型你个
- ptr是指向一个包含5个int元素的数组的指针
int *ptr[5]
int (*(ptr[5]))
- (ptr[5])说明ptr是一个数组,数组包含5个元素
- (*(ptr[5]))说明数组中每个元素都是一个指针
- int说明指针是指向int类型的指针
- ptr是一个包含5个元素的数组,每个元素都是指向int的指针
int **ptr;
int (*(*ptr))
- (*ptr)说明ptr是一个指针
- (*(*ptr))说明ptr这个指针指向的数据类型也是一个指针
- int说明ptr是一个指针,其指向的数据为一个int类型的指针,ptr是一个指针的指针
int (*ptr)(float)
int ((*ptr)(float))
- (*ptr)说明ptr是一个指针
- (*ptr)(float)说明ptr指向一个函数,函数包含一个float类型的参数
- int表明ptr指向的这个函数,返回值为int类型
int (*ptr[3])[4]
int ((*(ptr[3]))[4])
- (ptr[3])说明ptr是一个包含3个元素的数组
- (*(ptr[3])说明数组每个元素都是一个指针
- ((*(ptr[3]))[4])说明每个指针都指向一个包含4个元素的数组
- int说明包含的4个元素的数组中每个元素都是int类型
- ptr是一个包含3个元素的数组,每个元素都是一个指向包含4个int元素数组的指针
int *ptr[3][4]
int (*((ptr[3])[4]))
- (ptr[3])说明ptr是一个包含3个元素的数组
- ((ptr[3])[4])这个数组中每个元素都是一个包含4个元素的数组
- (*((ptr[3])[4]))说明4个元素的数组中每个元素都是一个指针
- int说明指针指向int类型
- ptr是一个包含3个元素的数组,每个元素都是包含4个指向int类型指针的数组
函数指针
C语言中函数名就代表函数的地址,函数名和函数地址是同一个概念。定义一个函数与int fun(void);
则fun == &fun。同理函数指针调用也是这样 ptr = fun; ptr()与(*ptr)()是相等的
- fun是一个函数,ptr是一个函数指针。则ptr=&fun;和ptr=fun是等效的。
- ptr是一个函数指针,则ptr();和(*ptr)()是等效的。
- 声明一个函数指针:int (*ptr)(int,float);声明了一个函数指针,所指向的函数是包含int和float两个参数并且返回int类型的函数类型。
- 函数类型转换:(void (*)(void))var;可以将var转换为参数为void返回值为void的函数指针。
C语言编程参考
最新推荐文章于 2022-08-02 17:38:07 发布