C语言的技术特点探究

举报
Jet Ding 发表于 2020/09/29 17:47:55 2020/09/29
【摘要】 C语言许多年来一直是编程语言中排名前二的语言,学好C语言无疑可以大大的扩展大家的就业空间。这一章节我们就来学习和研究一下C编程语言。

【引言】

前段时间软件学院的刘杰珍(340654)给软件教练们作软件可信认证的相关介绍时,提到了目前的可信认证包括如下的内容:

设计模式,重构,面向对象设计,编程语言高阶特性,其中编程语言包括C, C++, Java, Go, Python, Javascript

C语言许多年来一直是编程语言中排名前二的语言,学好C语言无疑可以大大的扩展大家的就业空间。这一章节我们就来学习和研究一下C编程语言。

C语言概况】

【通用化】

C是一种通用的、程序化的计算机编程语言,支持结构化编程、变量范围限定和递归操作,而它的静态类型系统可以防止很多意外的错误操作。

【支持机器指令】

C语言提供了能有效地使用机器指令的机制,并在以前用汇编语言编码的应用中得到了持久的推广。这些应用包括操作系统和各种计算机的应用软件,从超级计算机到PLC和嵌入式系统。

【起源】

C语言最初是由Dennis Ritchie1972年至1973年在贝尔实验室开发的,用于开发运行在Unix上的工具程序。

【重写UNIX内核】

后来,它被用于Unix操作系统的内核的重写。

【最广泛使用的编程语言之一】

20世纪80年代,C语言逐渐普及。目前已经成为应用最广泛的编程语言之一。

不同厂商的C语言编译器已经可以用于大多数现有的计算机体系结构和操作系统。

C语言从1989年起就被美国国家标准学会(ANSI)和国际标准化组织(InternationalOrganization for Standardization)标准化。

【命令式过程】

 

C语言是一种命令式过程语言。

C语言的编译器相对简单,在最小化的运行时环境的支持下,提供对内存和语言结构的低级访问,这些低级访问可以有效地调用机器指令。

【跨平台编程】

尽管它具有低级的功能,但C语言的设计是为了跨平台编程。

一个符合标准的C语言程序应该在编写时考虑到了可移植性,只需对其源代码进行少量的修改,就可以在各种计算机平台和操作系统上进行编译。

从嵌入式微控制器到超级计算机, C语言可用于各种平台,。

【函数】

C语言中,所有可执行的代码都包含在子程序中,也被称为"函数",虽然严格意义上来说并不是功能编程。

函数的参数总是通过值传递。

C语言中,通过显式传递指针值来模拟引用传递。

【自由格式】

C语言的程序源代码是自由格式的,用分号作为语句的终结符,用大括号将语句块分组。

【其他特点】

C语言还有以下特点:

·        该语言有少量固定的关键字,包括一整套控制流单元:if/elsefordo/whilewhileswitch。用户定义的名称与关键字没有任何形式的符号区分。

·        它有大量的算术、位运算和逻辑运算符,如+, +=, ++, ++, &, ||等。

·        在一条语句中可以执行多个赋值。

·        函数:

o  函数的返回值可以被忽略。

o  函数和数据指针允许临时运行时的多态性。

o  不能在其他函数的词法范围内定义函数。

·        数据类型是静态的;所有数据都有一个类型,可以进行隐式转换。

·        声明语法: C语言没有 "定义"关键字,而是以类型名称开头进行声明。没有 "function"关键字,一个函数定义是通过括号中的参数列表来表示的。

·        可以使用用户定义的类型(typedef)和复合类型。

o  结构数据类型(struct)允许将相关的数据元素作为一个单元进行访问和分配。

o  联合是一个具有重叠成员的结构,只有最后一个成员的存储才是有效的。

o  数组索引是指定元素的顺序编号,用指针运算来定义。与结构不同,数组不是一级对象:不能使用单一的内置运算符进行赋值或比较。在使用或定义中没有 "数组"关键字,而是用方括号在语法上表示数组,例如month[11]

o  枚举类型可以用enum关键字来表示。它们可以与整数自由互换。

o  字符串不是一个独立的数据类型,通常是作为字符数组来实现的,有个空元素结尾。

·        通过将机器地址转换为类型化指针,可以实现对计算机内存的低级访问。

·        过程(不返回值的子程序)是函数的一种特殊情况,其返回类型为void

·        预处理程序执行宏定义、源码文件包含和条件编译。

·        有一种基本的模块化形式:文件可以单独编译,也可以链接在一起,通过静态(static)和外部(extern)属性控制哪些函数和数据对象对其他文件可见。

·        复杂的功能,如I/O、字符串操作和数学函数等都通过标准程序库来完成。

虽然C语言不包括其他语言中的某些功能,如面向对象和垃圾回收。这些功能可以通过外部库(如GLib对象系统或Boehm垃圾回收器)来实现或模拟。

 

【与其他语言的关系】

许多后来的语言都直接或间接地借鉴了C语言,包括C++C#UnixC shellDGoJavaJavaScript(包括转换器)、LimboLPCObjective-CPerlPHPPythonRustSwiftVerilogSystemVerilog(硬件描述语言)等。

这些语言从C语言中汲取了许多控制结构和其他基本特征,其中大多数语言(Python是一个例外)也采用了与C语言高度相似的语法。

它们往往将C语言中可识别的表达式和语句语法与底层的类型系统、数据模型和语义结合在一起,而这些语言的语义可能会有本质上的不同。这些不同也是此语言区别于彼语言的原因。

 

【历史发展】

【早期】

C的起源与Unix操作系统的开发密切相关,最初是由Dennis RitchieKenThompson用汇编语言在PDP-7上实现的,他们吸收了同事们的一些想法,用汇编语言实现。最终,他们决定将操作系统移植到PDP-11上。最初的PDP-11版本的Unix也是用汇编语言开发的。

Thompson希望有一种编程语言来开发新平台的应用程序。起初,他试图制作一个Fortran编译器,但很快就放弃了这个想法。相反,他创造了一个最近开发的BCPL系统编程语言的删减版。BCPL的官方描述当时还没有,Thompson对语法进行了修改,使其更加简洁,产生了类似但略显简单的B

1972年,Ritchie开始改进B,从而创建了一种新的语言CC编译器和用它制作的一些实用程序被包含在第二版Unix中。

197311月发布的第四版Unix中,Unix内核被广泛地用C语言重新实现。

Unix是最早用汇编以外的语言实现的操作系统内核之一。早期的实例包括1961年的Multics系统(PL/I编写的)Burroughs B5000的主控程序(MCP)(ALGOL编写的)

1977年左右,RitchieStephen C. Johnson对该语言作了进一步的修改,以方便Unix操作系统的可移植性。Johnson的便携式C语言编译器成为了C语言在新平台上的几个实现的基础。

K&R C

1978年,Brian KernighanDennis Ritchie出版了《C编程语言》的第一版,这本书被C程序员们称为K&R,多年来一直是C语言的非正式规范。它所描述的C语言版本通常被称为 "K&R C"。本书的第二版涵盖了后来的ANSIC标准。

 

K&R引入了几个功能:

·        标准的I/O

·        长整型的数据类型

·        无符号整型数据类型

·        =op格式的复合赋值运算符(=-)改为op=格式(-=),用以消除i=-10等构造所产生的语义歧义,因为i=-10被解释为i=-10(i减去10),而不是可能的i=-10(设置i-10)

 

即使在1989ANSI标准发布后的许多年里,当C语言程序员们希望拥有最大限度的可移植性时, K&R C仍然被认为是"最低限度的共同点"

很长时间仍然被限定在这个范围内,因为许多旧的编译器仍然在使用,而且因为编写的K&R C代码也可以成为合法的标准C语言。

C语言的早期版本中,函数只定义返回int以外的类型,只有在函数定义之前使用时,才必须声明,没有事先声明的函数被推定为返回int类型。

比如说:

 

long some_function();

/* int */ other_function();

 

/* int */ calling_function()

{

    long test1;

    register /* int */ test2;

 

    test1 = some_function();

    if (test1 > 0)

          test2 = 0;

    else

          test2 = other_function();

    return test2;

}

 

K&R C中,被注释出来的int类型指定可以省略,但在以后的标准中改为是必须的。

由于K&R函数声明中没有包含任何关于函数参数的信息,所以函数参数类型检查并没有被执行。如果本地函数的参数数量不对,或者多次调用外部函数时使用了不同的参数数量或类型,那么有些编译器会在调用本地函数时发出警告信息。

独立的工具,如Unixlint工具,可以检查多个源文件中的函数使用是否一致。

K&R C发布后的几年里,在AT&T(特别是PCC)和其他一些厂商的编译器的支持下,该语言增加了一些功能。这些功能包括:

·        void函数(即无返回值的函数)

·        返回结构类型或联合类型的函数(而不是指针)

·        结构数据的分配

·        枚举类型

由于大量的扩展和标准库缺乏共识,再加上语言的普及,甚至连Unix编译器都没有精确地实现K&R规范,这才导致了后来的标准化。

ANSI C and ISO C

20世纪70年代末和80年代,随着C语言的普及,各种大型机、微型计算机和微型计算机(包括IBMPC)都实现了C语言的版本。

1983年,美国国家标准协会(ANSI)成立了一个委员会,X3J11,建立了C的标准规范。1989年,C语言标准被批准为ANSI X3.159-1989《程序设计语言C》。这个版本的语言通常被称为ANSI C、标准C,有时也被称为C89

1990年,国际标准化组织(ISO)ANSI C标准(在格式上作了修改)作为ISO/IEC 9899:1990,有时也被称为C90。因此,"C89" "C90"指的是同一种编程语言规范。

ANSI和其他国家标准机构一样,不再独立制定C标准,而是服从国际C标准,由ISO/IEC JTC1/SC22/WG14工作组维护。对国际标准的更新通常在ISO发布后一年内通过。

标准化进程的目标之一是编制一个K&RC的超集,纳入了许多后来引入的非官方特征。标准委员会还加入了一些额外的特性,如函数原型(借用C++)、void指针、对国际字符集和地域的支持,以及预处理程序的增强。虽然参数声明的语法被增强到包括C++中使用的风格,但为了与现有的源代码兼容,允许继续使用K&R接口。

 

目前的C语言编译器都支持C89,大多数现代的C语言代码都是基于它的。任何只用标准C语言编写的程序,在没有任何硬件依赖性假设的情况下,都可以在任何具有符合C语言实现的平台上正常运行,并在其资源限制范围内。

如果没有这些预防措施,程序可能只在特定的平台或特定的编译器上编译,例如,由于使用了非标准的库,如GUI库,或依赖于编译器或平台特定的属性,如数据类型的确切大小和字节数的大小等等情况。

在代码可能由标准或基于K&R C的编译器编译的情况下,可以使用__STDC__宏将代码分成标准和K&R两部分,以防止在基于K&R C的编译器上使用仅在标准C中可用的特性。

ANSI/ISO标准化过程之后,C语言规范在几年内相对静止。1995年,1990C语言标准的规范性修正案1ISO/IEC9899/AMD1:1995,非正式地称为C95)发布,修正了一些细节,并增加了对国际字符集的更广泛的支持。

 

C99

 

C标准在20世纪90年代末经过进一步修订,于1999年发布了ISO/IEC 9899:1999,也就是通常所说的 "C99"。此后,该标准通过技术更正进行了三次修订。

C99引入了几个新的特性,包括内联函数、几个新的数据类型(包括longlong int和一个表示复数的复杂类型)、可变长度的数组和灵活的数组成员、改进了对IEEE 754浮点的支持、支持变量宏(可变算术量的宏)、支持以//开头的单行注释,就像BCPLC++中的那样。其中许多已经在一些C语言编译器中作为扩展实现了。

C99基本上与C90向后兼容,但在某些方面更加严格;特别是缺乏类型指定的声明时不再隐含int。定义了一个标准宏 __STDC_VERSION__,其值为199901L,以表示C99支持。

GCCSolaris Studio 和其他 C 编译器现在都支持 C99 的许多或全部的新特性。

MicrosoftVisual C++中的C编译器实现了C89标准和C99中与C++11兼容所需的部分。

 

C11

2007年,C标准的另一次修订工作开始进行,非正式地称为 "C1X",直到2011-12-08正式发布。C标准委员会通过了一些准则,限制采用没有经过测试的新特性。

C11标准为C语言和库增加了许多新的特性,包括类型通用宏、匿名结构、改进的Unicode支持、原子操作、多线程和边界检查函数。它还使现有的C99库中的某些部分成为可选的,并提高了与C++的兼容性。标准宏__STDC_VERSION__被定义为201112L,表示对C11的支持。

 

C18

C18发布于20186月,是C编程语言的现行标准。它没有引入新的语言特性,只是对C11中的技术修正和缺陷进行了澄清。标准宏__STDC_VERSION__定义为201710L

【嵌入式C语言】

 

一直以来,嵌入式C语言编程需要对C语言进行非标准的扩展,以支持诸如定点算术、多个独立的内存库和基本的I/O操作等特别功能。

2008年,C语言标准委员会发布了一份技术报告,对C语言进行了扩展,通过了一个通用的标准。要求所有的实现都遵守这个标准以解决这些问题。

它包括了一些普通C语言中不具备的功能,如定点算术、命名地址空间和基本的I/O硬件寻址等。

 

C语言语法】

语言有一个由 C 标准指定的正式语法。

 C 语言中,行的结尾一般不重要但是,在预处理阶段,行的边界是有意义的。

注释可以出现在分隔符/**/之间,或者(从C99年开始)出现在//之后,直到行尾。

 /*  */ 划分的注释不嵌套,如果这些字符序列出现在字符串或字符字元中,则不被解释为注释分隔符。

 

C源文件包含声明和函数定义。函数定义反过来也包含了声明和语句。声明可以使用structunionenum等关键字定义新的类型,也可以为新的变量分配类型,或者为新的变量预留存储空间,通常是在变量名后面写上类型。像charint这样的关键字指定了内置类型。

代码的部分用大括号来限制声明的范围,并作为控制结构的单一语句。

作为一种命令式语言,C语言使用语句来指定动作。

最常见的语句是表达式语句,由一个要执行的表达式组成,后面是分号。

表达式可以调用函数,也可以赋予变量新的值。

为了修改语句的正常顺序执行,C语言提供了几个由保留关键字标识的控制流语句。

结构化编程支持if(-else)条件执行,支持do-whilewhile和迭代执行(循环)。

for 语句有独立的初始化、判断和再初始化的表达式,可以省略其中的任何一个或全部。

还有一个非结构化的goto语句,直接跳转到函数内指定的标签。

switch根据整数表达式的值选择要执行的分支。

表达式可以使用各种内置的运算符,它可能包含函数调用。

对函数的参数和大多数操作符的参数和运算符的运算顺序是没有规定的。

表达式评估可以交错进行。

但是,所有的附带结果(包括存储到变量)都会在下一个 "序列点"之前得出.

序列点包括每个表达式语句的结束,以及每个函数调用的进入和返回。

序列点也发生在包含某些运算符(&&, |||, ?: 和逗号运算符)的表达式评估过程中。

这允许编译器对目标代码进行高度的优化,但同时也要求C语言的程序员比其他编程语言的程序员要更加谨慎,以获得可靠的结果。

 

KernighanRitchie在《C语言程序设计语言导论》中说。"C语言和其他语言一样,也有它的缺陷。有些运算符的优先级不对;语法的某些部分可以做得更好。"

C语言标准并没有试图纠正其中的许多缺陷,因为那样的改变对已经存在的软件代码会有影响。

 

【字符集】

基本的C源字符集包括以下字符:

·        ISO拉丁文字母表的小写和大写字母:a-z A-Z

·        数字:09

·        图形字符: ! # % & ' ( ) * + , - . / : ; < = > ? [ \ ] ^_ { | } ~

·        空格字符:空格、水平制表符、垂直制表符、换行

 

Newline表示文本行的结尾,尽管为了方便起见,C将其视为一个字符, 它不需要对应于实际的单个字符。

额外的多字节编码字符可以在字符串字段中使用,但它们并不完全可移植。最新的C标准(C11)允许多国Unicode字符可通过使用\uXXXXXX\UXXXXXX编码(其中X表示十六进制字符)可移植地嵌入到C源代码中,目前这个功能还没有被广泛地推广。

基本的C语言执行字符集包含了相同的字符,以及警报、退格和回车的表示。随着C标准的每次修订,对扩展字符集的运行时支持也在增加。

【保留词】

C8932个保留词,又称关键词,是指除了预设的目的之外,不能用于其他目的的词。

 

auto

break

case

char

const

continue

default

do

double

else

enum

extern

float

for

goto

if

int

long

register

return

short

signed

sizeof

static

struct

switch

typedef

union

unsigned

void

volatile

while

 

 

C99再增加5个:

 

_Bool

_Complex

_Imaginary

inline

restrict

 

 

C11再增加7个:

 

_Alignas

_Alignof

_Atomic

_Generic

_Noreturn

_Static_assert

_Thread_local

 

最近保留的大多数保留词都是以下划线开头,后面是大写字母。

 

【操作符】

 

C语言支持丰富的操作符集,这些操作符是在表达式中使用的符号,用于指定在评估该表达式时要执行的操作。C语言中的运算符有:

·        算术运算符。+, -, *, /,%

·        赋值: =

·        增强的赋值:+=, -=, *=,/=,  %=, &=, |=, ^=, <<=, >>=

·        位逻辑:~, &, |, ^

·        移位:<<,>>

·        布尔逻辑: !, &&,||

·        条件评价: ?

·        相同测试: ==, !=

·        调用函数: ( )

·        增量和减量: ++, --

·        成员选择: ., ->

·        对象大小:sizeof

·        顺序关系:<, <=,>, >=

·        引用和取值: &, *, []

·        顺序:,

·        子表达式分组:( )

·        类型转换:(类型名)

 

C语言使用运算符=(在数学中用于表示等值)来表示赋值,沿用了FortranPL/I的先例,但与ALGOL及其继承者不同。

C使用运算符==来检验相等。

这两个运算符(赋值和相等)之间的相似性可能会导致意外地使用其中一个来代替另一个,而且在很多情况下,这种错误不会产生错误信息(有些编译器会产生警告)。

例如,条件表达式if(a == b + 1)可能会被错误地写成if(a =b + 1),如果赋值后a不为零,则会被评价为真。

 

C语言的运算符优先级并不总是直观的。

例如,在表达式中,运算符==比运算符 & (bitwise AND) | (bitwise OR)有更高优先级,比如 x & 1 == 0,就必须写成 (x & 1) == 0, 除非编码者就想要前者的结果。

 

Hello World例子】

K&R第一版中出现的"helloWorld "的例子,已经成为大多数编程教科书中的入门程序的范本。该程序将"helloWorld"打印到标准输出。

 

最初的版本是:

main()

{

    printf("hello, world\n");

}

 

一个符合标准的Hello World程序:

#include <stdio.h>

 

int main(void)

{

    printf("hello, world\n");

}

 

程序的第一行包含一个预处理指令,用#include表示。编译器会用 stdio.h 标准头的整个文本取代这一行,这个文件包含标准输入和输出函数(如 printf scanf)的声明。

stdio.h周围的角括号表示stdio.h 是使用编译器系统提供的头文件。如果是双引号的话,这个头文件会在本地或项目特定的头文件中查找。

下一行表示定义一个名为main的函数。

C语言程序中,main函数有一个特殊的作用。

运行时环境会调用main函数开始执行程序。

类型int表示main函数的结果类型。作为参数列表的关键字void 表示这个函数不接受任何参数。。

 

开头的大括号表示主函数定义的开始。

 

下一行调用一个名为 printf 的函数,该函数由系统库提供。

在这个调用中,printf函数传入一个参数,即字符串"hello,world"中的第一个字符的地址。

这个字符串是一个未命名的数组,其元素为 char 类型的元素,由编译器自动设置,最后一个 0 值的字符作为数组的结尾。

\n是一个转义序列,C翻译成换行字符,在输出时表示当前行的结束。

printf函数的返回值是int类型的,由于没有使用,所以会被忽略。

分号;结束语句。

结尾的大括号表示主函数的代码结束。根据C99规范和更新的规范,主函数与其他函数不同,当结束函数的即到达}时,会隐式返回一个值为0值。

 

【数据类型】

C语言的类型系统是静态的、弱类型化的,这点上与ALGOL后裔如Pascal等的类型系统类似。

对于整数类型:

·        有符号和无符号的整数。

·        浮点数。

·        枚举类型(enum)类型。

·        整数类型char常用于单字节字符。

·        C99增加了一个布尔数据类型。

还有派生类型包括数组、指针、记录(struct)和联合(union)。

在低级系统编程中,C语言经常被用于可能需要转义类型系统的低级系统编程。编译器试图确保大多数表达式的类型正确性,但程序员可以通过各种方式来覆盖检查,或者通过使用类型转换来将值从一种类型转换为另一种类型,或者通过使用指针或联合来重新解释数据对象的底层位。

有些人觉得C语言的声明语法很不直观,尤其是对于函数指针来说。

C语言中常用的算术转换可以产生高效的代码,但有时会产生不可预知的结果。

例如,比较宽度相等的有符号和无符号整数,需要将有符号值转换为无符号值。如果有符号的值是负数,这可能会产生不可预知的结果。

 

【指针】

C语言支持使用指针,指针是一种记录对象或函数在内存中的地址或位置的引用类型。

1.  指针可以用来访问存储在所指向的地址上的数据,或者调用指向的函数。

2.  指针可以使用赋值或指针运算来操作。

3.  指针通常是一个原始的内存地址,但是由于指针的类型包括了被指向的事物的类型,所以在编译时可以对使用指针在的表达式进行类型检查。

4.  指针算术会根据指向的数据类型的大小自动调整。

5.  通常使用指针将字符串当成字符数组操作。

【动态内存分配】

动态内存分配是使用指针来进行的。

许多数据类型,如树结构,通常是以动态分配的结构对象的形式实现的,这些结构对象使用指针连接在一起。

函数指针作为参数传递给高阶函数(如qsortbsearch),或者作为事件处理程序调用的回调,都是非常有用的。

【空指针】

空指针没有有效的位置。

空指针值是没有定义的,通常会导致分段错误。

空指针值用于表示特殊情况,例如在链表的最后节点中没有 "下一个"指针,或者作为返回指针的函数的错误返回。

在源代码中的适当上下文中,如用于分配给指针变量,空指针常数可以写成0,也可以写成指针类型,也可以写成标准头文件定义的NULL宏。

在有条件判断的上下文中,null指针值会评估为false,而其他所有的指针值都会评估为true

【泛化指针】

泛化指针(void *)指向未指定类型的对象,因此可以作为 "通用"数据指针使用。

由于不知道指向对象的大小和类型,所以void指针不能用来调用对应的值或函数,也不允许对其进行指针运算。

当然它们可以很容易地(在很多情况下是隐含地)转换为任何其他对象的指针类型。

【小心的使用指针】

使用指针不当是有潜在危险的。

1.  由于可以让指针变量指向任意位置,这可能会造成不良的影响。

2.  虽然正确使用的指针指向安全的地方,但也可以通过使用无效的指针运算使它们指向不安全的地方。

3.  指针所指向的对象在释放之后可能会继续使用(dangling pointers)。

4.  指针可能在没有被初始化的情况下被使用(wild pointers)。

5.  指针可能被通过castunion或另一个损坏的指针分配一个不安全的值。

 

一般来说,尽管编译器通常提供了不同级别的检查,C语言是允许在指针类型之间进行操作和转换的。

其他一些编程语言通过使用更多的限制性引用类型来解决这些问题。

 

 

【数组】

 

C语言中的数组类型传统上是在编译时指定一个固定的静态空间。但是,也可以在运行时使用标准库的malloc函数在运行时分配一个内存块(任意大小),并将其作为数组处理。C语言对数组和指针的统一意味着声明的数组和这些动态分配的模拟数组可以互相转换。

由于数组总是通过指针来访问,所以尽管有些编译器可能会提供边界检查的选项,数组访问通常不会根据底层数组的大小进行检查。因此,在马虎编写的代码中,违反数组边界的情况是可能发生的,而且相当普遍,可能会导致各种后果,包括非法内存访问、数据损坏、缓冲区超限和运行时异常。

如果需要进行边界检查,必须手动进行。

【多维数组】

C语言没有专门声明多维数组的规范,而是依靠类型系统中的递归来声明数组的数组。由此产生的"多维数组"的索引值可理解为是按行的顺序递增。

多维数组常用于数值算法中(主要来自于应用线性代数),用于存储矩阵。

C数组的结构很适合这个特殊的任务。

但是,由于数组仅仅作为指针传递,因此数组的边界必须是已知的固定值,否则就必须显式传递给任何调用它们的函数,而且动态大小的数组的数组不能使用双索引来访问。

一个解决这个问题的方法是在数组中分配一个额外的 "行向量"指向列的指针。

C99引入了 "可变长度数组",解决了普通C语言数组的一些问题。

 

【数组指针的可互换性】

【元素读取】

下标符号x[i](其中x表示一个指针)实际上是*(x+i)

利用编译器对指针类型的了解,x+i指向的地址不是从基本地址xi字节递增的地址,而是基本地址加上i乘以x指向元素单元的大小。因此,x[i]表示数组的第i+1个元素。

【数组参数】

此外,在大多数表达式的上下文中(一个值得注意的例外是作为sizeof的操作符),数组的名称会自动转换为指向数组的第一个元素的指针。这意味着当一个数组作为参数被命名为函数的参数时,数组永远不会被作为一个整体复制,而是只传递其第一个元素的地址。因此,虽然C语言中的函数调用使用了逐值传递的语义,但实际上数组是通过引用传递的。

【元素大小和个数】

元素的大小可以通过对x的任何一个被引申的元素使用运算符sizeof来确定,如n = sizeof *xn = sizeof x[0],而数组A中的元素个数可以通过sizeof A / sizeof A[0]来计算。

 

由于C语言的语义,无法通过指向数组或动态分配(malloc)创建的数组的指针来确定整个数组的大小。

sizeof arr/sizeof arr[0]这样的代码(其中arr指定了一个指针)将无法工作,因为编译器假定请求的是指针本身的大小。

然而,动态分配创建的数组是通过指针访问的,而不是真正的数组变量,所以它们和数组指针一样存在sizeof问题。

因此,尽管数组和指针变量之间存在着这种表面上的等同性,但它们之间还是有区别的。

 

【数组名称,指向和内容】

 

尽管在大多数表达式上下文中,数组的名称被转换成一个指针(指向它的第一个元素),但这个指针本身并不占用任何存储空间。

数组的名称不是一般意义上的值,它的地址是一个常数,这与指针变量不同。

因此,数组的 "指向"是不能改变的,不可能给数组名分配一个新的地址。

但是,数组内容可以通过使用memcpy函数或访问单个元素来修改。

 

【内存管理】

编程语言最重要的功能之一就是提供管理内存和存储对象的机制。C语言提供了三种不同的方法来为对象分配内存:

·        静态内存分配:在编译时,在二进制文件中提供了对象的空间;只要包含这些对象的二进制被加载到内存中,这些对象就有一个存储范围(或者说生命周期)。

·        自动内存分配:临时对象可以存储在栈中,在声明对象所在的块退出后,这些空间会自动释放并可重复使用。

·        动态内存分配:可以在运行时使用库函数(如malloc)从一个叫做堆的内存区域请求任意大小的内存块,这些内存块会持续存在,直到通过调用库函数reallocfree来重新分配或者释放为止。

 

这三种方法适用于不同的情况,并有不同的权衡策略。

静态的内存分配几乎没有分配开销,

自动分配可能会涉及到稍多的开销,

而动态的内存分配可能会有大量的分配和释放的开销。

 

静态对象的持久性对于维护跨函数调用的状态信息非常有用,

自动分配很容易使用,

但栈空间通常比静态内存或堆空间在时间上和空间存在更多的限制。

动态内存分配允许分配在运行时才知道大小的对象。

大多数C语言程序都广泛地使用了这三种方式来分配内存。

 

在可能的情况下,自动或静态分配通常是最简单的,因为存储空间是由编译器管理的,这样程序员就可以省去了手动分配和释放存储空间的麻烦。

然而,许多数据结构在运行时的大小会发生变化,而且由于静态分配(以及C99之前的自动分配)在编译时必须有一个固定的大小,因此在许多情况下,动态分配是必要的。

C99标准之前,可变大小的数组是一个常见的例子。

与自动分配不同的是,自动分配在运行时可能会失败,造成不可控的后果,而动态分配函数在所需的存储无法分配时,会以空指针值的形式返回一个指示。

静态分配通常会在程序开始执行之前就被链接器或加载器检测到。

 

除非另有规定,在程序启动时,静态对象包含零或空指针值。

自动和动态分配的对象只有在明确指定了一个初始值的情况下才会被初始化.

否则,它们的初始值是不确定的。

如果程序试图访问一个未初始化的值,结果是不可预知的。许多现代编译器试图检测并警告这个问题,但报告的准确性有待提高。

另一个问题是堆内存分配必须与程序中的实际使用情况同步,这样才能尽可能多地重复使用。

例如,如果在调用free()之前,堆内存分配的指针超出了作用域或其值被覆盖,那么该内存就无法正确释放,这种现象称为内存泄漏。

 

反之,也有可能是内存被释放,但仍然被继续使用,从而导致无法预测的结果。

 

通常情况下,这些问题难以追踪。

在具有自动垃圾回收功能的语言中这类问题得到了改善。

 

 

【程序库】

C编程语言使用程序库作为其主要的扩展方法。在C语言中,程序库是包含在一个单一的 "归档"文件中的一组函数。

每个库通常都有一个头文件,其中包含了程序可能使用的库中包含的函数的原型,以及与这些函数一起使用的特殊数据类型和宏符号的声明。

为了让程序很好的使用一个程序库,它须包含该库的头文件,而且该库必须与调用程序进行链接,这在很多情况下需要使用编译器的标志选项(例如,-lm"链接数学库"等等)。

最常见的C语言程序库是C标准库,它是由ISOANSI C标准指定的C标准库,每一个C语言实现都会附带一个标准库(针对有限环境的实现,如嵌入式系统,可能只提供标准库的一个子集)。

这个库支持流输入和输出、内存分配、数学、字符串和时间等。

几个独立的标准库头(例如,stdio.h)指定了这些标准库的接口。

另一组常见的C语言库函数是专门针对Unix和类似Unix系统的应用程序使用的函数,特别是提供内核接口的函数。这些函数在各种标准中都有详细的介绍,如POSIX和单UNIX规范。

库通常是用C语言编写的,因为C语言的编译器会生成高效的目标代码。

 

【文件处理和数据流】

文件输入和输出(I/O)不是C语言本身的一部分,而是由程序库(如C标准库)及其相关的头文件(如stdio.h)来处理的。

文件处理一般是通过高级I/O来实现的,它是通过流来工作的。

从这个角度来说,流是一个独立于设备的数据流,而文件是一个具体的设备。

高层I/O是通过流与文件的关联来实现的。

C语言标准库中,缓冲区(一个内存区域或队列)在数据被发送到最终目的地之前,暂时用来存储数据。

这样可以减少等待速度较慢的设备(例如硬盘或固态硬盘)的时间。

 

低级I/O函数不是标准C语言库的一部分,这些通常是 "裸机"编程(独立于任何操作系统的编程,如大多数的嵌入式编程)的一部分。除少数例外,"裸机"编程包含了低级I/O功能。

 

【语言工具】

许多工具帮助C语言程序员找到并修复未定义或其他可能错误的表达式语句,并且这类工具通常比编译器提供的更严格。

lint工具是这样的一个工具,许多其他工具的出现也是受这个工具的启发。

自动化的源代码检查和审计在任何语言中都是有帮助的,对于C语言来说,有很多这样的工具,比如Lint

一个常见的做法是在程序第一次编写时使用Lint来检测有问题的代码。

一旦程序通过了Lint,然后使用C语言编译器进行编译。同时,许多编译器可以选择性地警告一些语法上的一些问题,而这些警告可能实际上是错误的。

MISRA C是一套专门针对嵌入式系统开发的、避免此类问题代码的针对性准则。

 

还有一些编译器、程序库和操作系统级的机制来判断不是C语言标准的部分,比如数组的边界检查、缓冲区溢出检测、序列化、动态内存跟踪和自动化垃圾回收。

诸如PurifyValgrind等工具以及与包含特殊版本的内存分配函数的库链接,可以帮助发现运行时内存使用中的错误。

 

【使用场景】

 

在操作系统和嵌入式系统开发中,C语言被广泛的使用。

这是因为C语言的代码在编写的时候,可以用来访问特定的硬件地址,并执行类型检查,以及匹配外部的接口要求,对系统资源的使用开销要求很低。

C语言也可以用于网站编程,使用CGI作为Web应用程序、服务器和浏览器之间的信息 "网关",由于C语言的速度、稳定性和近乎于通用的可用性,C语言经常被选中。

C语言的广泛可用性和高效率的一个结果是,其他编程语言的编译器、程序库和解释器通常都是用C语言实现的。

由于C语言的抽象层很薄,开销也很低,所以C语言使程序员能够创建高效的算法和数据结构的实现,这对于计算量大的程序来说非常有用。

例如,GNU 多精度算术库、GNU 科学库、Mathematica MATLAB 都是完全或部分用 C 语言编写的。

语言有时被其他语言的实现者用作中间语言。这种方法可能是为了移植性或方便性。

通过使用C语言作为中间语言,不需要额外的机器专用代码生成器。C语言有一些特性,例如行数预处理器指令和初始化器列表末尾的多余逗号,这些特性支持编译生成的代码。

值得一提的是,由于C语言存在的一些缺点,促使人们开发出了其他基于C语言的专门用于中间语言的语言,如C--

 

C语言也被广泛用于实现终端用户应用程序。当然,这些应用程序也可以用较新的、更高级别的语言来编写。

 

【小结】

本文对C语言的发展概况,语法,数据类型,内存管理,程序库,文件处理,工具和使用场景等方面进行了学习和探讨,希望对大家有所裨益。

 

欢迎讨论。


【更多文章】

https://en.wikipedia.org/wiki/C_(programming_language)

C&C++代码编译和分析工具探究

Jet Ding文章归类索引表



【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。