当前位置: 首页 > news >正文

西宁做网站鹰潭网站商城建设

西宁做网站,鹰潭网站商城建设,wordpress主题付费下载,菏泽做网站建设找哪家好文章目录 第18章 声明18.1 声明的语法18.2 存储类型18.2.1 变量的性质18.2.2 auto存储类型18.2.3 static存储类型18.2.4 extern存储类型18.2.5 register存储类型18.2.6 函数的存储类型18.2.7 小结 18.3 类型限定符18.4 声明符18.4.1 解释复杂声明18.4.2 使用类型定义来简化声明… 文章目录 第18章 声明18.1 声明的语法18.2 存储类型18.2.1 变量的性质18.2.2 auto存储类型18.2.3 static存储类型18.2.4 extern存储类型18.2.5 register存储类型18.2.6 函数的存储类型18.2.7 小结 18.3 类型限定符18.4 声明符18.4.1 解释复杂声明18.4.2 使用类型定义来简化声明 18.5 初始化器18.5.1 未初始化的变量 18.6 内联函数(C99)18.6.1 内联定义18.6.2 对内联函数的限制18.6.3 在GCC中使用内联函数 18.7 函数指定符_Noreturn和头stdnoreturn.h(C1X)18.8 静态断言(C1X)问与答写在最后 第18章 声明 ——定义变量很容易但要时时刻刻控制它很难。 声明在C语言编程中起着核心的作用。通过声明变量和函数可以在两方面为编译器提供至关重要的信息检查程序潜在的错误以及把程序翻译成目标代码。 前面几章已经提供了声明的示例但是没有完整地描述本章将弥补这个缺憾。本章会探讨可以用于声明的复杂选项并且显示变量声明和函数声明之间的几个共同点。此外本章还为存储、作用域以及链接这些重要概念提供了坚实的基础。 18.1节介绍声明的一般语法。接下来的4节将集中讨论声明中出现的数据项存储类型18.2节、类型限定符18.3节、声明符18.4节和初始化器18.5节。18.6节讨论了inline关键字它可以用在C99函数声明中。 18.1 声明的语法 声明为编译器提供有关标识符含义的信息。当编写 int i;时是在告诉编译器名字i表示当前作用域内数据类型为int的变量。声明 float f(float);则是在告诉编译器f是一个返回值为float型的函数并且此函数有一个实际参数此参数类型也为float型。 一般地声明具有下列形式: 声明指定符 声明符;声明指定符declaration specifier描述声明的变量或函数的性质。声明符declarator给出了它们的名字并且可以提供关于其性质的额外信息。 声明指定符分为以下3类: 存储类型。存储类型一共有4种auto、static、extern和register。在声明中最多可以出现一种存储类型。如果存储类型存在则必须把它放置在最前面。类型限定符。C89只有两种类型限定符const和volatile。从C99开始还有一个限定符restrict从C11开始又新增了一个原子类型限定符_Atomic。声明可以包含零个或多个限定符。类型指定符。关键字void、char、short、int、long、float、double、signed和unsigned都是类型指定符。这些单词可以组合使用如第7章所述。这些单词出现顺序并不重要int unsigned long和long unsigned int完全一样。类型指定符也包括结构、联合和枚举的说明例如struct point{int x, y;}、struct {int x, y;}或者struct point。用typedef创建的类型名也是类型指定符。 从C99开始还有第4种声明指定符即函数指定符它只用于函数声明。这一类指定符包括从C99开始引入的关键字inline和从C11开始引入的_Noreturn。类型限定符和类型指定符必须跟随在存储类型的后边但是两者的顺序没有限制。出于书写风格的考虑这里会将类型限定符放置在类型指定符的前面。 声明符可以只是一个标识符简单变量的名字也可能是标识符和[]以及*的各种组合用来表示指针、数组或者函数。声明符之间用逗号分隔。表示变量的声明符后边可以跟随初始化器。 一起看一些说明这些规则的例子。下面是一个带有存储类型和3个声明符的声明 static float x, y, *p;存储类型类型指定符声明符staticfloatx, y, *p 下列声明有类型限定符但是没有存储类型。此外它还有初始化器 const char month[] January;类型限定符类型指定符声明符初始化器constcharmonth[]“January” 下列声明既有存储类型也有类型限定符。此外它还有3个类型指定符当然它们的顺序并不重要 extern const unsigned long int a[10];存储类型类型限定符类型指定符声明符externconstunsigned long inta[10] 和变量声明一样函数声明也有存储类型、类型限定符和类型指定符。下列声明具有存储类型和类型指定符 extern int square(int);存储类型类型指定符声明符externintsquare(int) 下面4节将详细介绍存储类型、类型限定符、声明符和初始化器。 18.2 存储类型 存储类型可以用于变量以及较小范围的函数和形式参数的说明。现在集中讨论变量的存储类型。 回顾一下10.3节的内容术语块block表示函数体或者复合语句可能包含声明。从C99开始 选择语句if和switch、循环语句while、do和for以及它们所控制的“内部”语句也被视为块尽管本质上有一些差别。 18.2.1 变量的性质 C程序中的每个变量都具有以下3个性质: 存储期。变量的存储期决定了为变量预留的内存被释放的时间。具有自动存储期的变量在所属块被执行时获得内存单元并在块终止时释放内存单元从而会导致变量失去值。具有静态存储期的变量在程序运行期间占有同一个存储单元也就允许变量无限期地保留它所占用的空间。作用域。变量的作用域其实是变量名字的作用范围是指可以通过名字引用变量的那部分程序文本。变量可以有块作用域变量的名字从声明的地方一直到所在块的末尾都是可见的或者文件作用域变量的名字从声明的地方一直到所在文件的末尾都是可见的。链接。实际上是指变量名字的链接属性它确定了程序的不同部分可以通过变量名字共享此变量的范围。通过具有外部链接属性的名字变量可以被程序中的几个或许全部文件共享。如果名字具有内部链接属性变量只能属于单独一个文件但是此文件中的函数可以共享这个变量。如果具有相同名字的变量出现在另一个文件中那么系统会把它作为不同的变量来处理。名字属于无链接的变量属于单独一个函数而且根本不能被共享。 默认的存储期、作用域和链接都依赖于变量声明的位置: 在块包括函数体内部声明的变量通常具有自动存储期它的名字具有块作用域并且无链接。在程序的最外层任意块外部声明的变量具有静态存储期它的名字具有文件作用域和外部链接。 下面的例子说明了变量i和变量j的默认性质 int i; //静态存储期、文件作用域、外部链接void f(void){int j; //自动存储期、块作用域、无链接 }对许多变量而言默认的存储期、作用域和链接是符合要求的。当这些性质无法满足要求时可以通过指定明确的存储类型auto、static、extern和register来改变变量的性质。 18.2.2 auto存储类型 auto存储类型只对属于块的变量有效。auto变量具有自动存储期无须惊讶它的名字具有块作用域并且无链接。auto存储类型几乎从来不用显式地指明因为对于在块内部声明的变量它是默认的。 18.2.3 static存储类型 static存储类型可以用于全部变量而无须考虑变量声明的位置。但是作用于块外部声明的变量和块内部声明的变量时会有不同的效果。当用在块外部时单词static说明变量的名字具有内部链接。当用在块内部时static把变量的存储期从自动的变成了静态的。下面的代码说明把变量i和变量j声明为static所产生的效果 static int i; //静态存储期、文件作用域、内部链接void f(void){static int j; //静态存储期、块作用域、无链接 }在用于块外部的声明时static本质上使变量只在声明它的文件内可见。只有出现在同一文件中的函数可以看到此变量。在下面的例子中函数f1和函数f2都可以访问变量i但是其他文件中的函数不可以 static int i; void f1(void) { /* has access to i */ } void f2(void) { /* has access to i */ }static的此种用法可以用来实现一种称为信息隐藏19.2节的技术。 块内声明的static变量在程序执行期间驻留在同一存储单元内。和每次程序离开所在块就会丢失值的自动变量不同static变量会无限期地保留值。static变量具有以下一些有趣的性质: 块内的static变量只在程序执行前进行一次初始化而auto变量则会在每次出现时进行初始化当然需假设它有初始化器。每次函数被递归调用时它都会获得一组新的auto变量。但是如果函数含有static变量那么此函数的全部调用都可以共享这个static变量。虽然函数不应该返回指向auto变量的指针但是函数返回指向static变量的指针是没有错误的。 声明函数中的一个变量为static这样做允许函数在“隐藏”区域内的调用之间保留信息。隐藏区域是程序其他部分无法访问到的地方。然而更通常的做法是用static来使程序更加有效。思考下列函数 char digit_to_hex_char(int digit) { const char hex_chars[16] 0123456789ABCDEF; return hex_chars[digit]; } 每次调用digit_to_hex_char函数时都会把字符0123456789ABCDEF复制给数组hex_chars来对其进行初始化。现在把数组设为static的 char digit_to_hex_char(int digit) { static const char hex_chars[16] 0123456789ABCDEF; return hex_chars[digit]; } //由于static型变量只进行一次初始化这样做就提升了digit_to_hex_char函数的速度。18.2.4 extern存储类型 extern存储类型使几个源文件可以共享同一个变量。15.2节介绍了使用extern的基本概念所以这里的讨论不会太多。回顾讲过的内容可以知道下列声明给编译器提供的信息是i是int型变量 extern int i;但是这样不会导致编译器为变量i分配存储单元。用C语言的术语来说上述声明不是变量i的定义它只是提示编译器需要访问定义在别处的变量可能稍后在同一文件中更常见的是在另一个文件中。变量在程序中可以有多次声明但只能有一次定义。 变量的extern声明不是定义这一规则有一个例外。对变量进行初始化的extern声明是变量的定义。例如声明 extern int i 0;//上述语句等效于 int i 0;这条规则可以防止多个extern声明用不同方法对变量进行初始化。 extern声明中的变量始终具有静态存储期。变量的作用域依赖于声明的位置。如果声明在块内部那么它的名字具有块作用域否则具有文件作用域 extern int i; //静态存储期、文件作用域、什么链接void f(void){extern int j; //静态存储期、块作用域、什么链接 }确定extern型变量的链接有一定难度。如果变量在文件中较早的位置任何函数定义的外部声明为static那么它的名字具有内部链接否则通常情况下具有外部链接。 18.2.5 register存储类型 声明变量具有register存储类型就要求编译器把变量存储在寄存器中而不是像其他变量一样保留在内存中。寄存器是驻留在计算机CPU中的存储单元。存储在寄存器中的数据会比存储在普通内存中的数据访问和更新的速度更快。指明变量的存储类型是register是一种请求而不是命令。编译器可以选择把register型变量存储在内存中。 register存储类型只对声明在块内的变量有效。register变量具有和auto变量一样的存储期、名字的作用域和链接。但是register变量缺乏auto变量所具有的一种性质因为寄存器没有地址所以对register变量使用取地址运算符是非法的。即使编译器选择把变量存储在内存中这一限制仍适用。 register存储类型最好用于需要频繁进行访问或更新的变量。例如在for语句中的循环控制变量就比较适合声明为register int sum_array(int a[], int n) { register int i; int sum 0; for (i 0; i n; i) sum a[i]; return sum; } 现在register存储类型已经不像以前那样在C程序员中流行了。当今的编译器比早期的C语言编译器复杂多了许多编译器可以自动确定哪些变量保留在寄存器中可以获得最大的好处。不过使用register仍然可以为编译器优化程序性能提供有用的信息。特别地编译器知道不能对register变量取地址因而不能用指针对其进行修改。在这一方面register关键字与C99的restrict关键字相关。 18.2.6 函数的存储类型 和变量声明一样函数声明和定义也可以包括存储类型但是选项只有 extern和static。在函数声明开始处的单词extern说明函数的名字具有外部链接也就是允许其他文件调用此函数static说明是内部链接也就是说只能在定义函数的文件内部调用此函数。如果不指明函数的存储类型那么会假设函数具有外部链接。 思考下面的函数声明 extern int f(int i); static int g(int i); int h(int i);函数f具有外部链接函数g具有内部链接而函数h默认情况下具有外部链接。因为g具有内部链接所以在定义它的文件之外不能直接调用它。把g声明为static不能完全阻止在别的文件中对它进行调用通过函数指针进行间接调用仍然是可能的。 声明函数是extern的就如同声明变量是auto的一样两者都没有作用。基于这个原因本书不在函数声明中使用extern。然而你需要知道一些程序员广泛地使用extern也是无害的。 另外声明函数是static的十分有用。事实上当声明不打算被其他文件调用的任意函数时建议使用static存储类型。这样做的好处包括以下2点: 更容易维护。把函数f声明为static存储类型能保证在函数定义出现的文件之外函数f都是不可见的。因此以后修改程序的人可以知道对函数f的修改不会影响其他文件中的函数。一个例外是另一个文件中的函数如果传入了指向函数f的指针它可能会受到函数f变化的影响。幸运的是这种问题很容易通过检查定义函数f的文件来发现因为传递f的函数一定也定义在此文件中。减少了“名字空间污染”。因为声明为static的函数具有内部链接所以可以在其他文件中重新使用这些函数的名字。虽然我们不太可能会为一些其他目的故意重新使用函数名字但是在大规模程序中这种现象是很难避免的。带有外部链接的大量函数名可能导致C程序员所说的“名字空间污染”即不同文件中的名字意外地发生了冲突。使用static存储类型可以有效地预防此类问题。 函数的形式参数具有和auto变量相同的性质自动存储期、块作用域和无链接。唯一能用于形式参数的存储类型是register。 18.2.7 小结 目前已经介绍了各种存储类型现在对已知内容进行一个总结。下面的代码片段说明了变量和形式参数声明中包含或者省略存储类型的所有可能的方法 int a; extern int b; static int c; void f(int d, register int e) { auto int g; int h; static int i; extern int j; register int k; } 表18-1说明了上述例子中每个变量和形式参数的性质 表18-1 变量和形式参数的性质 名字存储期作用域链接a静态文件外部b静态文件无法确定c静态文件内部d自动块无e自动块无g自动块无h自动块无i静态块无j静态块无法确定k自动块无 无法确定: 因为这里没有显示出变量b和j的定义所以无法确定它们的链接。在大多数情况下变量会定义在另一个文件中并且具有外部链接。 在这4种存储类型之中最重要的是extern和static。auto没有任何效果而现代编译器已经使register变得不如以前重要了。从C11开始增加了_Thread_local存储类型和线程存储期第28章中再来详细介绍。 18.3 类型限定符 早先在C语言中一共有两种类型限定符const和volatile。C99引入了第三种类型限定符即restrict它只用于指针受限指针17.8节C11又引入了第4种类型限定符即_Atomic可用于除数组和函数之外的类型将在第28章中介绍。因为volatile只用在底层编程中所以本书将对此限定符的讨论推迟到20.3节。const用来声明一些类似变量的对象但这些变量是“只读”的。程序可以访问const型对象的值但是无法改变它的值。例如下面这个声明创建了名为n的const型对象且此对象的值为10 const int n 10;而下列声明产生了名为tax_brackets的const型数组 const int tax_brackets[] {750, 2250, 3750, 5250, 7000};把对象声明为const有以下几个好处: const是文档格式声明对象是const类型可以提示任何阅读程序的人该对象的值不会改变。编译器可以检查程序没有特意地试图改变该对象的值。当为特定类型的应用特别是嵌入式系统编写程序时编译器可以用单词const来识别需要存储到ROM只读存储器中的数据。 乍一看const好像与前面章节中用于创建常量名的#define指令一样。然而实际上#define和const之间有明显的差异: 可以用#define指令为数值、字符或字符串常量创建名字。const可用于产生任何类型的只读对象包括数组、指针、结构和联合。 const对象遵循与变量相同的作用域规则而用#define创建的常量不受这些规则的限制。特别是不能用#define创建具有块作用域的常量。 和宏的值不同const对象的值可以在调试器中看到。 不同于宏const对象不可以用于常量表达式。例如因为数组边界必须是常量表达式所以不能写成下列形式 const int n 10; int a[n]; /*** WRONG ***/在C99中如果a具有自动存储期那么这个例子是合法的——它会被视为变长数组但是如果a具有静态存储期那么这个例子是不合法的。 对const对象应用取地址运算符是合法的因为它有地址。宏没有地址。 没有绝对的原则说明何时使用#define以及何时使用const。这里建议对表示数或字符的常量使用#define。这样就可以把这些常量作为数组维数并且在switch语句或其他要求常量表达式的地方使用它们。 18.4 声明符 声明符不等于标识符。 声明符包含标识符声明的变量或函数的名字标识符的前边可能有符号*后边可能有[]或()。通过把*、[]和()组合在一起可以创建复杂声明符。 在了解较为复杂的声明符之前先来复习一下前面讲过的声明符的知识。在最简单的情况下声明符就是标识符就如同下面例子中的i int i;声明符还可以包含符号*、[]和()。 用*开头的声明符表示指针 int *p;用[]结尾的声明符表示数组 int a[10];如果数组是形式参数或者数组有初始化器再或者数组的存储类型为extern那么方括号内可以为空 extern int a[];因为a是在程序的别处定义的所以这里编译器不需要知道数组的长度。在多维数组中只有第一维的方括号可以为空。 从C99开始为数组形式参数声明中方括号内的内容提供了两种额外的选项。一个是关键字static后面跟着的表达式指明数组的最小长度另一个是符号*它可以用在函数原型中以指示变长数组参数。9.3节讨论了这两种新特性。 用()结尾的声明符表示函数 int abs(int i); void swap(int *a, int *b); int find_largest(int a[], int n);C语言允许在函数声明中省略形式参数的名字 int abs(int); void swap(int *, int *); int find_largest(int [], int); 甚至圆括号内可以为空 int abs(); void swap(); int find_largest();最后这组声明指明了abs、swap和find_largest的返回类型但是没有提供有关它们的实际参数的信息。圆括号内置为空不等同于把单词void放置在圆括号内后者说明没有实际参数。圆括号内为空的这种函数声明风格正在迅速消失。它比C89的原型形式差因为它不允许编译器检查函数调用是否有正确的实际参数。 如果所有的声明符都这样简单那么C语言的编程将一蹴而就。可惜的是实际程序中的声明符往往组合了符号*、[]和()。我们已经见过这类组合的示例了。我们知道下列语句声明了一个数组此数组的元素是10个指向整数的指针 int *ap[10];我们还知道下列语句声明了一个函数此函数有一个float型的实际参数并且返回指向float型值的指针: float *fp(float);此外我们在17.7节学过下面这条语句它用来声明一个指向函数的指针此函数有int型实际参数和void型返回值 void (*pf)(int);18.4.1 解释复杂声明 到目前为止我们在声明符的理解方面还没有遇到太多的麻烦。但是下面这个声明符是什么意思呢 int *(*x[10])(void);这个声明符组合了*、[]和()所以x是指针、数组还是函数并不明显。 幸运的是无论多么费解都可以根据下面两条简单的规则来理解任何声明: 始终从内往外读声明符。换句话说定位声明的标识符并且从此处开始解释声明。在做选择时始终使[]和()优先于*。如果*在标识符的前面而标识符后边跟着[]那么标识符表示数组而不是指针。同样地如果*在标识符的前面而标识符后边跟着()那么标识符表示函数而不是指针。当然可以使用圆括号来使[]和()相对于*的优先级无效。 首先把这些规则应用于简单的示例。在声明 int *ap[10];中ap是标识符。因为*在ap的前面并且后边跟着[]而[]优先级高所以ap是指针数组。在下列声明中 float *fp(float);fp是标识符。因为*在标识符的前面并且后边跟着()而()优先级高所以fp是返回指针的函数。 下列声明是一个小陷阱 void (*pf)(int);因为*pf包含在圆括号内所以pf一定是指针。但是(*pf)后边跟着(int)所以pf必须指向函数且此函数带有int型的实际参数。单词void表明了此函数的返回类型。 正如最后那个例子所示理解复杂的声明符经常需要从标识符的一边折返到另一边 pf的类型 (*pf)(int)void指针指向具有与int型实际参数的函数返回void型值 下面用这种折返方法来解释先前给出的声明 int *(*x[10])(void);首先定位声明的标识符x。在x前有*而后边又跟着[]。因为[]优先级高于*所以取右侧x是数组。接下来从左侧找到数组中元素的类型指针。再接下来到右侧找到指针所指向的数据类型不带实际参数的函数。最后回到左侧看每个函数返回的内容指向int型的指针。 要想熟练掌握C语言的声明需要花些时间并且要多练习。唯一的好消息是在C语言中有不能声明的特定内容。函数不能返回数组 int f(int)[]; /*** WRONG ***/函数不能返回函数: int g(int)(int); /*** WRONG ***/返回函数型的数组也是不可能的 int a[10](int); /*** WRONG ***/在上述情形中可以用指针来获得所需的效果。函数不能返回数组但可以返回指向数组的指针函数不能返回函数但可以返回指向函数的指针函数型的数组不合法但是数组可以包含指向函数的指针。17.7节有一个这样的数组示例。 18.4.2 使用类型定义来简化声明 一些程序员利用类型定义来简化复杂的声明。考虑一下前面检查过的x的声明 int *(*x[10])(void);为了使x的类型更容易理解可以使用下面一系列的类型定义 typedef int *Fcn(void); typedef Fcn *Fcn_ptr; typedef Fcn_ptr Fcn_ptr_array[10]; Fcn_ptr_array x;反向阅读可以发现x具有Fcn_ptr_array类型Fcn_ptr_array是Fcn_ptr值的数组Fcn_ptr是指向Fcn类型的指针而Fcn是不带实际参数且返回指向int型值的指针的函数。 18.5 初始化器 为了方便C语言允许在声明变量时为它们指定初始值。为了初始化变量可以在声明符的后边书写符号然后在其后加上初始化器。不要把声明中的符号和赋值运算符相混淆初始化和赋值不一样。 在前面章节中已经见过各种各样的初始化器了。简单变量的初始化器就是一个与变量类型一样的表达式 int i 5 / 2 ; /* i is initially 2 */如果类型不匹配C语言会用和赋值运算相同的规则对初始化器进行类型转换 7.4节: int j 5.5; /* converted to 5 */ 指针变量的初始化器必须是具有和变量相同类型或void*类型的指针表达式 int *p i;数组、结构或联合的初始化器通常是带有花括号的一串值: int a[5] {1, 2, 3, 4, 5};从C99开始由于指示器8.1节、16.1节的存在初始化器可以有其他形式。为了全面覆盖声明的范围现在来看看一些控制初始化器的额外规则: 具有静态存储期的变量的初始化器必须是常量 #define FIRST 1 #define LAST 100 static int i LAST – FIRST 1; 因为LAST和FIRST都是宏所以编译器可以计算出i的初始值100-11100。如果LAST和FIRST是变量那么初始化器就是非法的。 如果变量具有自动存储期那么它的初始化器不必是常量 int f(int n) { int last n – 1; ... } 包含在花括号中的数组、结构或联合的初始化器必须只包含常量表达式不允许有变量或函数调用 #define N 2 int powers[5] {1, N, N * N, N * N * N, N * N * N * N}; 因为N是常量所以powers的初始化器是合法的。如果N是变量那么程序将无法通过编译。在C99中仅当变量具有静态存储期时这一限制才生效。 自动类型的结构或联合的初始化器可以是另外一个结构或联合 void g(struct part part1) { struct part part2 part1; ... } 虽然初始化器应该是具有适当类型的表达式但它们不必是变量或形式参数名。例如part2的初始化器可以是*p其中p具有struct part *类型也可以是f(part1)其中f是返回part结构类型的函数。 18.5.1 未初始化的变量 前面的章节中已经暗示未初始化变量有未定义的值但并不总是这样的变量的初始化值依赖于变量的存储期。 具有自动存储期的变量没有默认的初始值。不能预测自动变量的初始值而且每次变量变为有效时值可能不同。具有静态存储期的变量默认情况下的值为0。用calloc分配的内存是简单地给字节的位设为0而静态变量不同于此它是基于类型的正确初始化即整型变量初始化为0浮点变量初始化为0.0指针初始化为空指针。 出于书写风格的考虑最好为静态类型的变量提供初始化器而不是依赖于它们一定为0的事实。如果程序访问了没有明确初始化的变量那么以后阅读程序的人可能不容易确定变量是否为0或者是否在程序中的某处通过赋值初始化。 18.6 内联函数(C99) C99及之后的函数声明中有一个C89中不存在的选项可以包含关键字inline。这个关键字是一个全新的声明指定符不同于存储类型、类型限定符以及类型指定符。为了理解inline的作用需要把C编译器在调用函数和从函数返回过程中产生的机器指令可视化。 在机器层面调用函数之前可能需要预先执行一些指令。调用本身需要跳转到函数的第一条指令函数本身可能也需要执行一些额外的指令来启动执行。如果函数有参数参数需要被复制因为C通过值传递参数。从函数返回也需要被调用的函数和调用函数执行差不多的工作量。调用函数和从函数返回所需的工作量称为“额外开销”因为我们并没有要求函数执行这些工作。尽管函数调用中的额外开销只是使程序稍许变慢但在特定的情况下额外开销会产生累积效应。例如在函数需要调用数百万次或数十亿次使用老式的比较慢的处理器例如在嵌套系统中或者有着非常严格的时限要求例如在实时系统中时。 在C89中避免函数额外开销的唯一方式是使用带参数的宏14.3节。但带参数的宏也有一些缺点。C99提供了一种更好的解决方案创建内联函数inline function。“内联”表明编译器把函数的每一次调用都用函数的机器指令来代替。这种方法虽然会使被编译程序的大小增加一些但可以避免函数调用的常见额外开销。 不过把函数声明为inline并不是强制编译器将代码内联编译而只是建议编译器应该使函数调用尽可能地快也许在函数调用时才执行内联展开。编译器可以忽略这一建议。从这方面来说inline类似于register和restrict关键字后两者也是用于提升程序性能的但可以忽略。 18.6.1 内联定义 内联函数用关键字inline作为一个声明指定符 inline double average(double a, double b) { return (a b) / 2; }下面考虑复杂一点的情形。average有外部链接所以在其他源文件中也可以调用average。但编译器并没有考虑average的定义是外部定义因其是内联定义所以试图在别的文件中调用average将被当作错误。这句话第一时间可能看不懂请仔细阅读下面的解释 有两种方法可以避免这一错误。一种方法是在函数定义中增加单词static static inline double average(double a, double b) { return (a b) / 2; } 现在average具有内部链接了所以其他文件不能调用它。其他文件可以定义自己的average函数可以与这里的定义相同也可以不同。 另一种方法是为average提供外部定义从而可以在其他文件中调用。一种实现方式是将该函数重新写一遍不使用inline并将这一函数定义放在另一个源文件中。这样做是合法的但为同一个函数提供两个版本不太可取因为我们不能保证对程序进行修改时它们仍然一致。 更好的实现方式是首先将average的内联定义放入头文件命名为average.h中 #ifndef AVERAGE_H #define AVERAGE_H inline double average(double a, double b) { return (a b) / 2; } //上面是average函数的内联定义与一般函数不同 //函数的内联定义通常是放在放在头文件中的#endif小插曲假设你只创建了上述average.h仅包含average函数的内联定义并在另一个main.c文件中的main函数里调用了average函数main.c包含了#include average.h头当编译main.c文件时会产生类似“undefined reference to average”的报错信息原因是average没有外部定义 接下来再创建与之匹配的源文件average.c #include average.h extern double average(double a, double b); //使用extern关键字使得average函数的内联定义 //被二次利用有效且一致地实现了average函数的外部定义。现在任何一个需要调用average函数的文件只需要简单地包含average.h就行了该头文件包含了average的内联定义。average.c文件包含了average的原型。由于使用了extern关键字因此average.h中average的定义在average.c中被当作外部定义。 C99中的一般法则是如果特定文件中某个函数的所有顶层声明中都有inline但没有extern则该函数定义在该文件中是内联的。如果在程序的其他地方使用该函数包含其内联定义的文件也算在内则需要在另一个文件中为其提供外部定义。调用函数时编译器可以选择进行正常调用使用函数的外部定义或者执行内联展开使用函数的内联定义。我们没有办法知道编译器会怎样选择所以一定要确保这两处定义一致。刚刚讨论过的方式使用average.h和average.c可以保证定义的一致性。 18.6.2 对内联函数的限制 因为内联函数的实现方式和一般函数大不一样所以需要一些不同的规则和限制。对于具有外部链接的内联函数来说具有静态存储期的变量是一个特别的问题。因此C99对具有外部链接的内联函数未对具有内部链接的内联函数做约束做了如下限制。 函数中不能定义可改变的static变量。函数中不能引用具有内部链接的变量。 这样的函数可以定义同时为static和const的变量但每个内联定义都需要分别创建该变量的副本。 18.6.3 在GCC中使用内联函数 在C99标准之前一些编译器包括GCC已经可以支持内联函数了。因此它们使用内联函数的规则可能与C99标准不一样。特别是前面描述的那种方案使用average.h和average.c文件在这些编译器中可能无效。 不论GCC的版本如何被同时定义为static和inline的函数都可以工作得很好。这样做在C99中也是合法的所以是最安全的。static inline函数可以用于单个文件也可以放在头文件中然后在需要调用的源文件中包含进去。 还有一种方法可以在多个文件中共享内联函数。这种方法适用于旧版本的GCC但与C99相冲突。具体做法是将函数的定义放入头文件中指明其为extern和inline然后在任何包含该函数调用的源文件中包含该头文件并且在其中一个源文件中再次给出该函数的定义不过这次没有extern和inline关键字。这样即便编译器因为某种原因不能对函数进行“内联”函数仍然有定义。 关于GCC最后需要注意的是仅当通过-O命令行选项请求进行优化时才会对函数进行“内联”。 18.7 函数指定符_Noreturn和头stdnoreturn.h(C1X) 在C语言里有些函数是不返回的比如longjmp、exit和abort。从C11开始引入了一个函数指定符也就是关键字_Noreturn意思是“不返回”。如果在一个函数的声明里有这个函数指定符则意味着它不返回到调用者。 C11新增了一个头stdnoreturn.h它很简单只有一个宏noreturn被扩展为_Noreturn。如果在程序中包含了这个头则可以直接使用noreturn来代替_Noreturn。 18.8 静态断言(C1X) 函数assert在程序运行期间做诊断工作从C11开始引入的静态断言_Static_assert可以把检查和诊断工作放在程序编译期间进行。 _Static_assert(常量表达式, 字面串);在这里_Static_assert是C11新增的关键字。“常量表达式”必须是一个整型常量表达式。如果它的值不为0则没有什么效果如果值为0则违反约束条件并且C实现应当产生一条诊断信息在这条信息里应当包含“字面串”的内容除非字面串的内容不是用基本源字符集编码的。 C标准规定unsigned int类型可表示的数值范围至少是-32767~32767当然绝大多数平台支持比这个规定大得多的范围。为了保险起见下面这个小程序要求unsigned int能够表示超出上述范围的数值所以用静态断言来决定是否允许继续编译。 # include limits.h int main(void) { _Static_assert(UINT_MAX 32767, Not support this platform.); // 其他代码 return 0; }基本源字符集: C语言使用的字符集包括基本源字符集和扩展字符集前者包括26个大小写英文字母、数字以及标点符号等后者由你所在地区的文字符号组成。 如果unsigned int的最大值大于32767那么这个常量表达式的值为1这个静态断言什么也不做否则编译不能继续进行并显示第5行出现错误错误的原因是静态断言失败。在C11中静态断言是作为声明出现的。 在引入静态断言之前我们通常是在预处理阶段用#if和#error等预处理指令做一些诊断工作但是预处理器并不认识C的语法元素这就限制了它的功能和应用范围而引入静态断言则可以解决这个问题。 问与答 问1从C99开始为什么把选择语句和重复语句以及它们的“内部”语句视为块 答这条奇怪的规则源于把复合字面量9.3节、16.2节用于选择语句和重复语句时出现的一个问题。该问题与复合字面量的存储期有关所以我们先花点时间讨论一下这个问题。 C99及之后的标准指出如果复合字面量出现在函数体之外那么复合字面量所表示的对象具有静态存储期。否则它具有自动存储期因而对象所占有的内存会在复合字面量所在块的末尾释放。考虑下面的函数该函数返回使用复合字面量创建的point结构 struct point create_point(int x, int y) { return (struct point) {x, y}; }这个函数可以正确地工作因为复合字面量创建的对象会在函数返回时被复制。原始的对象将不复存在但副本会保留。现在假设我们对函数进行微小的改动 struct point *create_point(int x, int y) { return (struct point) {x, y}; }这一版本的create_point函数会导致未定义的行为因为它返回的指针所指向的对象具有自动存储期函数返回后该对象就不复存在。 现在回到开始时提到的问题为什么把选择语句和重复语句视为块考虑下面的示例1 /* Example 1 - if statement without braces */ double *coefficients, value; if (polynomial_selected 1) coefficients (double[3]) {1.5, -3.0, 6.0}; else coefficients (double[3]) {4.5, 1.0, -3.5}; value evaluate_polynomial(coefficients); 这个程序片段显然能按需要的方式工作但是请继续阅读。coefficients将指向由复合字面量创建的两个对象之一并且该对象在调用evaluate_polynomial时仍然存在。现在考虑一下示例2如果在内部语句if语句控制的语句两边加上花括号会有什么不同 /* Example 2 - if statement with braces */ double *coefficients, value; if (polynomial_selected 1) { coefficients (double[3]) {1.5, -3.0, 6.0}; } else { coefficients (double[3]) {4.5, 1.0, -3.5}; } value evaluate_polynomial(coefficients);现在我们遇到问题了。每个复合字面量会创建一个对象但该对象只存在于包含相应语句的花括号所形成的块内。调用evaluate_polynomial时coefficients指向一个不存在的对象从而导致未定义的行为。 C99的创立者对这种现象很不满意因为程序员不可能预料到在if语句中简单地增加花括号就会导致未定义的行为。为了避免这一问题他们决定始终把内部语句视为块。这样一来示例1和示例2就等价了都会导致未定义的行为。 当复合字面量是选择语句或重复语句的控制表达式的一部分时类似的问题也会发生。因此我们把整个选择语句和重复语句也都看作块就好像有一对不可见的花括号包裹在整个语句外面一样。因此带有else子句的if语句包含三个块两个内部语句分别是一个块整个if语句又是一个块。 问2你曾说过具有自动存储期的变量在所在块开始执行时分配内存空间。这对于C99及之后的变长数组是否也成立 答不成立。变长数组的空间不会在所在块开始执行时就分配因为那时候还不知道数组的长度。事实上在块的执行到达变长数组声明时才会为其分配空间。从这一方面说变长数组不同于其他所有的自动变量。 问3“作用域”和“链接”之间的区别到底是什么 答作用域是为编译器服务的链接是为链接器服务的。编译器用标识符的作用域来确定在文件的给定位置访问标识符是否合法。当编译器把源文件翻译成目标代码时它会注意到具有外部链接的名字并最终把这些名字存储到目标文件内的一个表中。因此链接器可以访问到具有外部链接的名字而内部链接的名字或无链接的名字对链接器而言是不可见的。 问4我无法理解一个名字具有块作用域但又有着外部链接。可否详细解释一下 答当然可以。假设某个源文件定义了变量i int i假设变量i的定义放在了任意函数之外所以默认情况下它具有外部链接。在另一个文件中有一个函数f需要访问变量i所以f的函数体把i声明为extern void f(void) { extern int i; ... }在第一个文件中变量i具有文件作用域。但在函数f内i具有块作用域。如果除函数f以外的其他函数需要访问变量i那么它们将需要单独声明i。或者简单地把变量i的声明移到函数f外从而使其具有文件作用域。在整个过程中会混淆的就是每次声明或定义i都会建立不同的作用域有时是文件作用域有时是块作用域。 问5为什么不能把const对象用在常量表达式中呢“constant”不就是常量吗 答在C语言中const表示“只读”而不是“常量”。下面用几个例子说明为什么const对象不能用于常量表达式。 首先const对象只在它的生命期内为常量而不是在程序的整个执行期内。假设在函数体内声明了一个const对象 void f(int n) { const int m n / 2; ... }当调用函数f时m将被初始化为n/2m的值在函数f返回之前都保持不变。当再次调用函数f时m可能会得到不同的值。这就是问题出现的地方。假设m出现在switch语句中 void f(int n) { const int m n / 2; ... switch (...) { ... case m: ... /*** WRONG ***/ ... } ... }那么直到函数f调用之前m的值都是未知的这违反了C语言的规则——分支标号的值必须是常量表达式。 接下来看看声明在块外部的const对象。这些对象具有外部链接并且可以在文件之间共享。如果C语言允许在常量表达式中使用const对象就很容易遇到下列情况 extern const int n; int a[n]; /*** WRONG ***/ n可能在其他文件中定义这使编译器无法确定数组a的长度。假设a是外部变量所以它不可能是变长数组。 如果这样还不能让你信服考虑下面的情况如果一个const对象也用volatile类型限定符20.3节声明它的值可能在程序执行过程中的任何时间发生改变。下面是C标准中的一个例子 extern const volatile int real_time_clock;程序可能不会改变变量real_time_clock的值因为其声明为const但可以通过其他的某种机制修改它的值因其被声明为volatile。 问6为什么声明符的语法如此古怪 答声明试图进行模拟使用。指针声明符的格式为*p这种格式和稍后将用于p的间接寻址运算符方式相匹配。数组声明符的格式为a[...]这种格式和数组稍后的取下标方式相匹配。函数声明符的格式为f(...)这种格式和函数调用的语法相匹配。这种推理甚至可以扩展到最复杂的声明符上。请思考一下17.7节中的数组file_cmd此数组的元素都是指向函数的指针。数组file_cmd的声明符格式为 (*file_cmd[])(void) 而这些函数的调用格式为 (*file_cmd[n])();其中圆括号、方括号和*的位置都一样。 写在最后 本文是博主阅读《C语言程序设计现代方法第2版·修订版》时所作笔记日后会持续更新后续章节笔记。欢迎各位大佬阅读学习如有疑问请及时联系指正希望对各位有所帮助Thank you very much
http://www.w-s-a.com/news/597821/

相关文章:

  • 建网站的公司 快云wordpress的搜索
  • 贷款网站模版东莞网站建设哪家专业
  • 做做网站已更新878网站正在建设中
  • dz旅游网站模板网站上做百度广告赚钱么
  • 青岛外贸假发网站建设seo优化名词解释
  • 四川建设厅网站施工员证查询网站建设行业政策
  • 网站全站出售dw怎么设计网页
  • 合肥网站建设方案服务网站建设推荐郑国华
  • 襄阳网站建设需要多少钱台州网站设计公司网站
  • 东莞专业拍摄做网站照片如何在百度上发布自己的广告
  • 网站建设费 科目做网站建设最好学什么
  • php商城网站建设多少钱深圳市建设
  • 有什么做糕点的视频网站黄岛做网站
  • 做视频课程网站建设一个普通网站需要多少钱
  • 专做化妆品的网站合肥做网站建设公司
  • 唐山企业网站网站建设费计入那个科目
  • 企业网站制作运营彩虹云主机官网
  • 如何建设废品网站如何在阿里云云服务器上搭建网站
  • 如何建立网站后台程序wordpress 后台管理
  • 山东外贸网站建设怎么样wordpress首页左图右文
  • 志丹网站建设wordpress 形式修改
  • 南通seo网站推广费用网站建设就业前景
  • 自适应网站做mip改造浏览器广告投放
  • 网站meta网页描述网站的推广费用
  • 偃师市住房和城乡建设局网站网站个人主页怎么做
  • 做网站要实名认证吗wordpress去掉仪表盘
  • 在哪做网站好Python建网站的步骤
  • 卢松松的网站办公室设计布局
  • 住房城乡建设干部学院网站织梦网站0day漏洞
  • 企业网站seo优帮云手机桌面布局设计软件