目录

1. 声明的语法

2. 变量的性质

2.1 变量性质的构成

2.2 变量默认性质

2.3 修改变量默认性质

2.3.1 修改局部变量默认性质

2.3.2 修改全局变量默认性质

2.4 extern声明变量的性质

2.5 auto存储类型

2.6 register存储类型

3. 函数的存储类型

4. 类型限定符(const)

4.1 声明只读变量

4.2 const变量相较于宏定义的优点

4.3 const变量的限制

4.4 const和宏定义使用建议

5. 声明符

5.1 简单声明符

5.2 复杂声明符

6. 初始化式

6.1 初始化式含义

6.2 初始化式规则

6.2.1 基础规则

6.2.2 额外规则

7. 内联函数(C99)

7.1 提出背景

7.2 内联定义

7.3 内联函数的共享方式

7.3.1 无法实现内联的共享

7.3.2 可在一个文件实现内联的共享

7.3.3 可在多个文件实现内联的共享

7.4 对内联的限制


1. 声明的语法

说明1:存储类型的限制

① 最多出现一个

② 如果有,必须放置在最前面

说明2:声明中可以包含零个或多个类型限定符

说明3:用typedef创建的类型别名也是类型说明符

说明4:类型限定符和类型说明符必须跟在存储类型后面,但对二者的顺序没有限制。

extern const int a; // 个人倾向于前这种
extern int const a;

2. 变量的性质

2.1 变量性质的构成

① 存储期限:为变量预留和内存被释放的时间

② 作用域:可以引用变量的那部分程序文本(以文件为单位)

③ 链接:程序的不同部分可以共享此变量的范围(以工程为单位)

说明:作用域和链接

作用域:为编译器服务。编译器用标识符的作用域来确定在文件的给定位置访问标识符的合法性

链接:为链接器服务。编译器在编译源文件时,将具有外部链接的名字存储到目标文件的一个表中,因此链接器可以访问到具有外部链接的名字

2.2 变量默认性质

变量的默认性质取决于变量声明的位置

① 在块内部声明

自动存储期限 / 块作用域 / 无链接

② 在程序最外层(任意块外部)声明

静态存储期限 / 文件作用域 / 外部链接

说明1:准确说本节讨论的不仅限于变量的性质,而是标识符(identifier)的性质,即除了变量名还包括数组名、指针名和函数名

说明2:一个名字具有块作用域却有外部链接的场景

在文件1中定义全局变量i,因此在文件1中,变量i 是文件作用域

// file1.c
int i;

在文件2的函数f 内声明变量i,此时在函数f 内,变量i 是块作用域

// file2.c
void f(void)
{extern int i;...
}

2.3 修改变量默认性质

2.3.1 修改局部变量默认性质

自动存储期限  +  static  ---> 静态存储期限

块作用域

无链接

说明1:局部static变量仅在程序执行前初始化一次

怎么证明在程序执行前就初始化了呢? 可以查看反汇编代码哦~~

结论:局部static变量存储在.data 段,其值在编译阶段就已经确定

说明2:修改带来的结果

① 对递归调用的影响

② 函数可以返回指向局部static变量的指针

说明3:修改的应用

① 允许函数在隐藏区域内的调用之间保留信息

e.g. 在多次调用间传递信息

② 使程序更加高效

char digit_to_hex_char(int digit)
{const char hex_chars[16] = "01234567879ABCDEF";return hex_chars[digit];
}
char digit_to_hex_char(int digit)
{static const char hex_chars[16] = "01234567879ABCDEF";return hex_chars[digit];
}

这样hex_chars数组仅初始化一次,可提升函数digit_to_hex_char 的速度

2.3.2 修改全局变量默认性质

静态存储期限

文件作用域

外部链接  +  static  --->  内部链接

目的:

① 实现信息隐藏技术

② 避免名字空间污染

2.4 extern声明变量的性质

extern声明的变量都具有静态存储期限

作用域依赖变量声明的位置

链接依赖变量定义的位置

补充:正常使用情况都是在一个文件中定义,并在头文件中extern声明,然后在需要使用的地方引入该头文件

2.5 auto存储类型

① 只对块内变量有效

② 几乎从不明确指出

2.6 register存储类型

① 作用:请求编译器将变量存储在寄存器中

② 只对块内变量有效

③ 由于寄存器没有地址,对register变量取地址(&)是非法的,即使编译器选择把变量存储在内存中(经过验证,编译阶段就会报错)

④ register存储类型最好用于频繁访问或更新的变量(e.g. 循环控制变量)

⑤ 目前已不常用

⑥ register是唯一能用于函数形式参数的存储类型

3. 函数的存储类型

① 只能使用extern和static

函数默认具有extern存储类型,即具有外部链接(所以一般不显式用extern修饰函数)

② 用static 修饰函数声明 --->  该函数具有内部链接

注意:此时只能避免其他文件对该函数的直接调用。通过获取指向该函数的指针,其他文件中的函数仍可以间接调用。

好处:

a. 易于维护

b. 减少名字空间污染

4. 类型限定符(const)

说明:类型限定符有3个,const / volatile / restrict,其中,

volatile 只用于底层编程,第20章说明

restrict(C99)只用于指针,第17章已有说明

4.1 声明只读变量

const用于声明"只读"变量,程序可以访问const 变量的值,但不能直接修改其值

那么间接修改呢~~我们来验证一下

说明:虽然编译时有警告,但依然可以运行。在C++中,类型要求更严格,会给出错误而非警告

4.2 const变量相较于宏定义的优点

① const可以创建任何类型的只读对象,而宏定义只是实现字符串替换

② const对象遵循作用域规则,而宏定义由预处理器处理,不受作用域规则限制

经典示例:不能用#define创建具有块作用域的常量

③ const对象可以取地址(&),宏定义不可以

④ const对象可以在调试器中看到,而宏定义在预处理后会被替换

4.3 const变量的限制

const对象不能用于常量表达式,而#define可以

示例1:const对象不能用于数组定义

const int n = 10;
int a[n]; // 编译报错

C99 补充:如果a 具有自动存储期限,C99 将其解释为变长数组,是合法的。如果a 具有静态存储期限,那么这个例子非法

示例2:const 对象不能用于case 分支

const int n = 1;
switch(...)
{case  n: // 编译报错...
}

根源:本质上const变量是一个只读变量,并不是常量

4.4 const和宏定义使用建议

建议对表示数或字符的常量使用#define ,这样可以把这些常量用于要求常量表达式的地方(e.g. 数组维数,case标号)

5. 声明符

5.1 简单声明符

声明符由标识符以及可能的* /  [ ] / ( ) 组合而成

① 声明符 = 标识符,声明变量

int i;

② 声明符 = 标识符 + *,声明指针变量

int *p;

③ 声明符 = 标识符 + [ ],声明数组

int a[10];// 如果数组是形式参数,或者数组有初始化式,或者数组的存储类型为extern的声明
// 方括号内可以为空
extern int a[];

④ 声明符 = 标识符 + ( ),声明函数

int abs(int);

说明:声明形式为了和使用方式匹配

int i;        // 表达式i 就是int型值
int *p;       // 表达式*p 就是int型值
int a[10];    // 表达式a[0] 就是int型值
int abs(int); // 表达式abs(0) 就是int型值

5.2 复杂声明符

解释复杂声明符的规则,

① 始终从内往外解释声明符

定位声明符中的标识符,并且从此处开始解释声明

② 在作选择时,始终使[]和()优于*

当然可以使用圆括号来使[]和()相对于*的优先级无效

示例,

float *fp(float);/*
1. fp先与()结合,所以fp是一个函数
2. fp左侧为*,所以函数返回值为指针
3. 函数返回的是指向float类型的指针
*/
float (*fp)(float);/*
1. 使用()使得fp优先与*结合,所以fp是一个指针
2. fp右侧为(),所以fp是一个指向函数的指针
3. 该函数参数类型为float,返回值类型为float
*/

说明1:可以使用typedef类型定义简化声明

假设有声明如下,

int *(*x[10])(void);/*
1. x先与[]结合,所以x是一个数组
2. x左边是*,所以x是一个指针数组
3. 出了x外侧的小括号,还是优先与()结合,所以指针指向函数
4. 函数的参数为void类型,返回值为int *类型
*/

对于上面比较复杂的类型声明,可以使用typedef简化如下,

// Fcn为一个函数类型,函数参数为void类型,返回值为int *类型
typedef int *Fcn(void);// Fcn_ptr为指向Fcn函数类型的指针类型
typedef Fcn *Fcn_ptr;// 该声明的效果与int *(*x[10])(void)相同,都是一个函数类型指针数组
Fcn_ptr[10];

说明2:声明的限制

① 函数不能返回数组

int f(int)[]; // wrong

但是函数可以返回指向数组的指针

② 函数不能返回函数

int g(int)(int); // wrong

但是函数可以返回指向函数的指针(典型示例:signal函数)

③ 没有函数类型的数组

int a[10](int); // wrong

但是可以有函数指针类型的数组

也就是说,C语言提供了这3种情况下的替代方式

6. 初始化式

6.1 初始化式含义

① C语言允许在声明变量时为他们指定初始值,e.g.

int a[10](int); // wrong

② 不要把声明中的符号=和赋值运算符相混淆,初始化和赋值不一样

6.2 初始化式规则

6.2.1 基础规则

① 简单变量的初始化式就是一个与变量类型相同的表达式

int i = 1;

② 如果类型不匹配,C语言会用和赋值运算符相同的规则对初始化式进行类型转换

int j = 5.5; // j 被初始化为5

③ 指针变量的初始化式必须是具有和变量相同类型或void *类型的指针表达式

int i;
int *p = &i;

④ 数组、结构或联合的初始化式通常是一串括在花括号内的值

int a[5] = {1, 2, 3, 4, 5};

说明:从初始化式的规则可见C语言是一种强类型语言,变量和表达式均有类型

6.2.2 额外规则

① 具有静态存储期限的变量的初始化式必须是常量

静态存储期限变量 = 全局变量 + static修饰的局部变量

假设以如下方式定义全局变量,

int i;
int *p = &i; // 变量的地址是一个常量,此处初始化式合法
int j = i;

编译时第3行会报如下错误,

② 如果变量具有自动存储期限,那么他的初始化式不需要是常量

③ 包含在花括号中的数组、结构或联合的初始化式必须只包含常量表达式,不允许有变量或函数调用

补充:C99中仅当变量具有静态存储期限时,这一限制才生效

④ 自动类型的结构或联合的初始化式可以是另外一个结构或联合

void g(struct part part1)
{struct part part2 = part1;
}

核心:从本质上说,初始化式是一个具有适当类型的表达式

说明:未初始化的变量

① 具有自动存储期限的变量没有默认的初始值

② 具有静态存储期限的变量默认情况下的值为零(因为在bss段),此处的值为零,是基于类型的正确初始化,即整数类型变量初始化值为0,浮点变量初始化为0.0,指针则初始化为空指针

7. 内联函数(C99)

7.1 提出背景

① 函数的调用与返回是有开销的

e.g. 返回地址入栈,拷贝函数参数,函数栈的恢复等

② C89中避免函数开销的唯一方式就是使用带参数的宏,但该方法有诸多缺点

e.g. 没有参数类型检查机制等

③ C99中提供了内联函数(inline function)解决方案,内联表明编译器把函数的每一次调用都用函数的机器指令来代替

④ 内联后的特性

a. 会被编译的程序大小增加

b. 可以避免函数调用的额外开销

c. 拥有函数的类型检查机制

7.2 内联定义

内联函数使用inline关键字修饰,示例如下,

inline double average(double a, double b)
{return (a + b) / 2;
}

说明:inline关键字只是建议编译器将函数内联,并非强制(类似register和restrict关键字)

7.3 内联函数的共享方式

7.3.1 无法实现内联的共享

假设在file1.c中定义内联函数,然后在file2.c中调用

// file1.c
inline double average(double a, double b)
{return (a + b) / 2;
}
// file2.c
extern double average(double a, double b);int main(void)
{double average(1.1, 2.2);return 0;
}

首先,这种方式是可以实现对average函数调用的,但是file2.c在编译时无法进行内联,因为内联是用函数的机器指令来代替函数调用,所以编译器需要在此时知道内联函数的实现方式

而上述实现中,在编译file2.c时,编译器只是得到average函数的声明,并不知道函数实现,所以无法内联,最终编译后,仍然是普通的函数调用方式

7.3.2 可在一个文件实现内联的共享

改进:将内联函数定义在头文件中,在需要使用该函数的文件中包含头文件

// inline.h
#ifndef INLINE_H
#define INLINE_H
inline double average(double a, double b)
{return (a + b) / 2;
}
#endif
// file2.c
#include "inline.h"int main(void)
{double ret = 0.0;ret = average(1.1, 2.2);printf("ret = %lf\n", ret);return 0;
}

说明1:此处增加对average返回值的使用,是为了避免编译优化直接取消对average函数的调用

说明2:需要使用至少-O1编译,这是GCC编译器的特性,只有通过-O命令行选项请求进行优化时,才会对函数进行内联

此时编译出的代码就使用内联方式处理average函数,反汇编内容如下,

7.3.3 可在多个文件实现内联的共享

假设在工程中的多个源文件中均要使用average函数,则需要分别包含inline.h,假设测试场景如下,

// inline.h
#ifndef INLINE_H
#define INLINE_H
inline double average(double a, double b)
{return (a + b) / 2;
}
#endif
// file3.c
#include "inline.h"
double func(double a, double b)
{double ret = 0.0;ret = average(a, b);return ret;
}
// file2.c
#include "inline.h"
extern double func(double a, double b);int main(void)
{double ret = 0.0;ret = average(1.1, 2.2);printf("ret = %lf\n", ret);ret = func(1.1, 2.2);printf("ret = %lf\n", ret);return 0;

使用如下方式编译会发生函数重定义错误,

分别查看file2.o和file3.o,可见这2个文件中对average均进行了内联处理,之所以最终链接阶段仍然报错,是因为这2个目标文件的符号表中均包含average函数

更正:上面的理解是错误的,即使使用内联,average函数也在这2个目标文件中,后续增加static修饰只是修改了average符号的链接属性为内部链接

改进:在inline.h头文件中增加static修饰

// inline.h
#ifndef INLINE_H
#define INLINE_H
static inline double average(double a, double b)
{return (a + b) / 2;
}
#endif

此时再使用-O1选项编译,可以编译成功,且main函数与func函数中对average函数均进行了内联处理

7.4 对内联的限制

C99对具有外部链接的内联函数有如下限制,

① 函数中不能定义可改变的static变量

② 函数中不能引用具有内部链接的变量

C程序设计语言现代方法18:声明相关推荐

  1. C程序设计语言现代方法17:指针的高级应用

    目录 1. 动态存储分配 1.1 malloc函数 1.2 calloc函数 1.3 realloc函数 1.4 free函数 2. 空指针NULL解析 2.1 NULL的定义形式 2.2 程序如何知 ...

  2. C程序设计语言现代方法15:编写大型程序

    目录 1. C语言程序一般构成 2. 源文件 2.1 源文件内容 2.2 将文件划分成多个源文件的优点 3. 头文件 3.1 包含头文件的3种方式 3.2 头文件共享内容 3.2.1 宏定义和类型定义 ...

  3. C程序设计语言现代方法13:字符串

    目录 1. 字符串字面量 1.1 定义 1.2 字符串字面量包含转义序列 1.3 延续字符串字面量 1.3.1 使用续行符 1.3.2 仅用空白字符分割字符串字面量 1.4 存储字符串字面量 1.5 ...

  4. C程序设计语言现代方法12:指针和数组

    目录 1. 指针的算术运算 1.1 概述 1.2 C语言支持的算术运算类型 2. 指针的比较 3. 指针用于数组处理 4. 数组名与指针 4.1 用数组名作指针 4.2 数组取下标操作 4.3 惯用法 ...

  5. C程序设计语言现代方法08:数组

    目录 1. C语言中的变量 2. 一维数组 2.1 数组的声明 2.2 数组初始化 2.3 对数组使用sizeof运算符 3. 多维数组 4. 常量数组 4. C语言数组类型 4.1 数组类型 4.2 ...

  6. C程序设计语言现代方法07:基本类型

    目录 1. 基本类型和构造类型 1.1 基本类型 1.2 构造类型 2. C语言两大类型(存储格式根本上不同) 3. 整数类型 3.1 6种有效组合 3.2 整数常量 3.3 整数溢出 3.4 读写整 ...

  7. C程序设计语言现代方法14:预处理器

    目录 1. 预处理器工作原理 1.1 预处理器性质 1.2 预处理器主要功能 1.3 GCC编译过程及常用选项 1.3.1 GCC编译过程 1.3.2 编译选项实例 1.4 注意事项 2. 预处理指令 ...

  8. C程序设计语言现代方法11:指针

    目录 1. 指针变量 1.1 指针变量含义 1.2 int *p含义 1.3 指针变量4句圣经 2. 指针与函数 2.1 指针作为函数参数 2.2 指针作为函数返回值 3. 指针的四大用途 4. 什么 ...

  9. C程序设计语言现代方法10:程序结构

    目录 1. 局部变量 2. 外部变量(全局变量) 2.1 全局变量属性 2.2 函数间通信方式 2.3 全局变量初始化 3. 程序块 4. 作用域 5. 单文件程序布局 1. 局部变量 局部变量的默认 ...

最新文章

  1. docker run后台启动命令_Docker命令详解之run
  2. Zookeeper高级
  3. python介绍和用途-Python --- Python的简介
  4. 如何使用PHP自动备份数据库
  5. 面试官问我:什么是JavaScript闭包,我该如何回答
  6. 读者诉苦:Redis 宕机,数据丢了,老板要辞退我
  7. 视达配色教程2 好的配色的第一条件是什么
  8. C#课外实践——校园二手平台(心得篇)
  9. 大疆DJI Thermal SDK Linux libdirp.so: cannot open shared object file: No such file or directory
  10. memcache的安装,配置和使用
  11. Python基本数据类型之字典
  12. 开启TOGAF架构之路
  13. SQL基础系列(八)——排序、分组排序(RANK)
  14. 小程序审核规则大致内容
  15. 医学统计python之ROC比较:Delong test
  16. 解决 chrome 访问 https 网站出现“您的连接不是私密的问题”
  17. 机器学习:浅谈先验概率,后验概率
  18. 美国旅游签证归来(上海领馆)
  19. 迭代求解线性方程组的解
  20. android ble mvp,Android mvparms 踩坑

热门文章

  1. java怎么实现日程提醒_如何用java和xml实现日程提醒
  2. python 速成学堂_Python 与数据科学入门
  3. php 魔术函数,PHP魔术函数、魔术常量、预定义常量
  4. MongoDB在本地安装与启动
  5. php 的cookie设置时间,php cookie时间设置的方法
  6. python 读取csv带表头_python读csv文件时指定行为表头或无表头的方法
  7. qq浏览器网页翻译_如何通过Edge浏览器调用“谷歌翻译”,将整个网页翻译为中文...
  8. linux 终端最大化命令,11个让你吃惊的Linux终端命令
  9. 揭秘网络:互联网调查入门 出版发行时间_cqy、cdx、zqsg……啥意思?揭秘QQ上的“00后黑话”...
  10. Mysql把查询的列作为判断条件(case函数)