当前位置: 首页 > 科技观察

全栈必备:C语言基础

时间:2023-03-15 20:16:36 科技观察

【简介】温故知新,“三日不玩,手上长刺”,代码也是如此。另一方面,你必须填补你自己挖的洞。在埋下了4种编程语言的伏笔,实现了Javacript、Python和Java。本来想把C/C++一起整理一下,但是涉及到面向对象的设计技术,最后整理一下从0到1吧。C语言简洁易用灵活,可以直接访问物理地址并执行高效的位操作。生成的目标文件质量高,执行效率高,但这是相对而言,效率还是比汇编语言低15%左右。数据处理特别是图像处理能力强,便携性也好。关键字ANSIC共有32个关键字和9种控制语句,按照约定编成顺口溜。签名返回时,无符号大小写继续default.register转到自动联合,做shortlongstruct.voidtypedefswitchextern,volatilechardoubleconst.ifbreakstaticint,enumsizeofelsefloat。在C99中又增加了5个关键字inlinerestrict_Bool_Complex_Imaginary,后来在C11中又增加了7个关键字_Alignas_Alignof_Atomic_Static_assert_Noreturn_Thread_local_Generic,所有这些关键字,不仅要了解,还要知道它们的典型应用场景。数据结构C语言为用户提供了丰富的数据结构,也允许用户定义复杂的数据结构。C语言提供的数据结构以数据类型的形式给出,C的数据类型分为:基本类型数值类型字符类型枚举类型构造类型数组类型结构类型联合类型指针类型数据可分为常量和变量,习惯上常量用大写字母,变量用小写字母。数值类型要注意数值的范围不同。字符常量是用单引号括起来的单个字符,也允许以“\”开头的特殊字符常量。枚举类型是原始数据类型,而不是构造类型,因为它不能分解为任何原始类型。在编译中,枚举元素被视为常量,因此称为枚举常量。它们不是变量,不能给它们赋值。数组是有序数据的集合,数组中的每个元素都属于同一种数据类型,用统一的数组名和下标来唯一确定数组中的元素。结构体是C语言提供的一种数据结构。一般形式如下:struct结构名{成员列表}变量名列表;通常,可以使用宏来获取结构内的偏移量:#undefoffsetofstruct#defineoffsetofstruct(TYPE,ELEMENT)((size_t)&((TYPE*)0)->ELEMENT)#endifunion也是派生类型,语法与结构体相同,区别在于其成员共享存储空间。联合定义了一组共享内存块的替代值。变量在内存中的地址称为变量的指针,这是C语言的精髓,下面将单独介绍。C语言还提供了非常丰富的运算符集,主要包括以下34种运算符:算术运算符:+、-、*、/、++等关系运算符:>、<、==、!=等逻辑运算符:&&,||,!等价:>>、<<、~等赋值:等号(=)及其扩展赋值运算符(+=、-=、*=、/=等)指针:*、&使用各种运算符进行运算对象被连接起来形成表达式。指针C语言的核心是指针,它的灵活性和超长都来自于指针。指针提供了动态操作内存的机制,加强了对数据结构的支持,实现了访问硬件的功能。指针是保存内存地址的变量。定义指针时,必须指定它所指向的变量的类型。任何指针都是某种类型的变量。当通过指针访问指针指向的内存区域时,指针指向的类型决定了编译器将把该内存区域的内容当作什么。需要注意的是,指针的类型(即指针本身的类型)和指针指向的类型是两个概念。void指针类型,即没有指定指向哪种数据类型的指针变量。void指针可以指向任何类型的数据,任何类型的指针都可以直接给void指针赋值。但是,如果需要将指针的值赋值给另一种类型的指针,则需要进行强制类型转换。在指针定义语句的类型前加上const,表示指向的对象是常量。一个指针变量可以指向另一个指针,一个指针指向一个指针。程序中的函数代码也占用内存空间,而每个函数都有一个地址,所以指针也可以指向函数,指向函数地址的指针称为函数指针。总之,指针可以指向的对象是没有限制的,它可以是变量、数组元素、动态分配的内存块和函数。正确理解指针变量和函数指针的声明,例如:(*(void(*)())0)();注意*p()和(*p)()的区别,前者表示函数的返回值是一个Pointer类型,后者表示p是指向函数的指针。指针的典型用法:直接访问系统内存引用函数构造链式数据结构引用动态分配的数据结构实现引用调用传递数组参数访问和迭代数据元素表示字符串作为其他值函数的别名一个大程序可以分成几个Applet模块,每个模块用于实现特定的功能,这个模块称为功能。一个C程序可以由一个主函数和几个子函数组成。main函数调用其他函数,其他函数之间也可以相互调用。同一个函数可以被一个或多个函数调用任意次数。从用户的角度来看,函数可以分为库函数和自定义函数。从函数本身来看,可以分为带参数和不带参数两种。在传递参数的过程中,要根据需要进行值传递和地址传递,即形参和实参。只有当函数调用发生时,函数中的形参才会被分配内存单元。调用结束后,形参占用的内存单元也被释放。该函数应该在同一个文件中调用它的地方之前定义,否则它会默认返回一个整数。如果调用函数和被调用函数不在同一个文件中,返回值类型不同,连接时会报错。如果被调用函数的参数包括char、short、float等,则必须在调用该函数的文件中声明该函数,参数类型必须包含在括号中。本质上,函数表示法就是指针表示法,函数名被求值成为函数的地址,然后将函数参数传递给函数。程序栈是支持函数执行的内存区域,通常与堆共享,包括返回地址、本地数据存储、参数存储、栈指针和基指针(运行时管理栈的指针)。当系统创建一个堆栈帧时,它按照声明的相反顺序将参数压入帧,最后压入局部变量。从函数返回指针时可能存在的潜在问题:返回未初始化的指针返回指向无效地址的指针返回指向局部变量的指针返回指针但不释放内存函数指针可以按不确定的顺序执行函数在编译时。void(*foo)()使用函数指针时一定要小心,因为c不会检查参数传递是否正确,建议使用fptr作为前缀。函数指针数组允许根据特定条件选择要执行的函数。将指针传递给指针允许参数指针指向不同的内存地址。内存存储C中主要有4种存储类型:auto只能用来标识局部变量的存储类型。对于局部变量,auto是默认的存储类型,不需要显式声明。所以auto标识的变量存放在栈区。extern用于声明全局变量。如果全局变量没有被初始化,它会被存放在BBS区,编译时它的值会被自动赋值为0。如果已经初始化,则存放在数据区。全局变量,不管是否初始化,都有整个程序运行的生命周期。为了节省内存空间,在当前文件中使用extern声明其他文件中定义的全局变量时,不会为其分配内存空间。.寄存器变量从内存传送到CPU寄存器后常驻CPU寄存器,所以寄存器会大大提高效率,因为变量从内存传送到寄存器的过程中多条指令省略了循环。static不管是全局的还是局部的,都是存放在数据区的,它的生命周期就是整个程序。如果是静态局部变量,则其作用域在一对{}内;如果它是一个静态全局变量,它的作用域就是当前文档。静态变量如果没有被初始化,会自动初始化为0。静态变量只能初始化一次。使用内存时,应用和释放应该配对。本着谁申请谁释放的原则,释放后指针应该置为NULL。常见的内存使用问题有以下三种:野指针:Free后指针没有清空,继续使用指针;内存泄漏:申请越界后内存不释放:数组索引和内存访问溢出避免内存越界,数组的索引必须要检查有效值,最好使用字符串操作API如strncpy、strncat等。需要检查内存拷贝的大小,避免出现野指针。如果条件允许,可以自己实现内存池管理,按字节划分内存池(比如8字节的整数倍)。每次分配的内存地址空间在开始和结束位置初始化特殊值,然后使用单独的线程每隔很短的时间扫描内存池中的每个有效块,进行内存整理。在动态分配存储字符串的空间时(malloc方法),注意不要忘记字符串需要多分配一个字节来保存以'\0'结尾的字符串。编译C语言的编译过程包括预编译->语法分析->代码生成->优化->汇编->连接。预编译器进行宏替换、词法分析并创建符号表。语法分析包括语义分析以创建语法树。代码生成器生成中间代码,优化器负责指令优化,汇编器生成汇编代码,最后链接器生成目标文件和可执行文件。链接器在目标模块中检查是否与外部对象同名,如果没有命名冲突,则将其添加到加载模块中。函数和已初始化的全局变量(包括初始化为0的)是强符号,未初始化的全局变量是弱符号。该符号的含义是指向同一个内存块进行同名的读写操作,即使这些操作分散在不同的.o中。对它们来说,以下三个规则适用:同名的强符号只能有一个,否则编译器会报“重复定义”错误。允许一个强符号和多个弱符号,但定义时会选择强符号。当多个弱符号相同时,链接器选择占用内存空间最大的一个。记住比较运算符==不要误写成赋值符号=,反之亦然,两者有很大区别。词法分析采用从左到右的贪心法,例如a---b等价于a---b,不等价于a---b;预编译通常是在C编译系统编译程序之前,对程序中的一些特殊命令进行“预处理”,然后将预处理后的结果与源程序进行编译处理,得到目标代码,以“#”开头的行成为预处理指令.带参数的宏与函数非常相似。引用函数时,实参也写在函数名后的括号内,要求实参个数与形参个数相等。但是,它们之间还是有区别的:参数的使用不同。.调用函数时,先计算实参表达式的值,再引入形参;该宏仅执行简单的字符替换。处理机制不同。函数调用是在程序运行时处理的,必须分配内存;编译时进行宏展开,不分配内存单元,不发生值传递处理,对返回值定义也没有不同的要求。定义函数时,实参和形参都必须定义类型;定义宏时,没有预处理器提供条件编译的功能。可以根据不同的条件编译不同的程序部分,从而生成不同的目标代码文件,这对于程序的移植和调试非常有用。条件编译有三种形式:#ifdefidentifiercodes1#elsecodes2#endif#ifdefidentifiercodes3#endif#ifndefidentifiercodes4#elsecodes5#endif头文件一般通过头文件调用库函数。很多时候,源代码是不方便(或不允许)发布给用户的,只要把头文件和二进制库提供给用户即可。用户只需要根据头文件中的接口声明调用库函数即可,无需关心接口是如何实现的。编译器从库中提取相应的代码。头文件还强制执行类型安全检查。如果接口的实现或使用方式与头文件中的声明不一致,编译器会指出错误。这个简单的规则可以大大减轻程序员调试和改错的负担。尖括号引入的头文件是在include文件目录中查找(include目录是用户在设置环境时设置的),而不是在源文件目录中查找。使用双引号表示首先查找当前源文件目录,如果找不到则在包含目录中查找。用户在编程时,可以根据自己的文件所在目录选择一定的命令形式。程序框架和库C语言的程序框架由头文件、变量声明、主函数和子函数组成。在C中无处不在的helloword看起来像这样:#includeintmain(){printf("Hello,World!\n");return0;}里面没有变量声明和子函数。没有主要功能可以吗?或者,不写main函数,是否可以将其更改为其他名称?这里涉及到指定编译,main是c中默认的调用入口。C中的大多数库都没有main函数。C语言中的库分为静态库(.a)和动态库(.so)。静态库实际上是链接器用来生成可执行文件的目标文件的集合。链接器会将程序中使用的函数的代码从库文件中复制到应用程序中。一旦连接完成并生成可执行文件,程序执行时就不需要静态库了。动态库也称为共享库。它只是在程序链接时被标记,然后在程序开始运行时动态加载所需的库(模块)。C标准库有多种实现,比如最著名的glibc,嵌入式Linux的uClibc,还有ARM自己的C语言标准库。不同标准库的实现不尽相同,提供的功能也不完全相同,但都有一个它们都支持的最小子集,这就是最典型的C语言标准库。C标准库由在15个头文件中声明的函数、类型定义和宏组成,每个头文件代表一系列编程功能。有人说C标准库可以分为3组。如何正确熟练地使用它们可以区分出3个级别的程序员:合格的程序员:,,,熟练的程序员:,,,优秀的程序员:,,,,,,运行时C语言运行时的数据结构中,栈提供了存储空间对于局部变量,为函数调用提供恢复信息,其暂存区用于计算复杂的算术表达式;调用记录支持过程调用,并记录调用结束后返回调用点所需的所有信息;全局变量的数据包括静态变量、常量等。BSS段(bsssegment)通常是指程序中用来存放未初始化的全局变量的一块内存区域。BSS段是静态内存分配。数据段通常是指程序中用来存放初始化的全局变量的一块内存区域。数据段是静态内存分配。代码段(codesegment/textsegment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行之前就已经确定了,内存区域通常是只读的,有些架构还允许代码段是可写的,这样就可以修改程序。在代码段中,还可以包含一些只读的常量变量,如字符串常量等。程序段是程序代码在内存中的映射。堆(heap)堆用于存放进程运行过程中动态分配的内存段。它的大小不固定,可以动态扩展或缩小。当进程调用malloc/free等函数分配内存时,将新分配的内存动态添加到堆中(堆扩大)/释放的内存从堆中移除(堆缩小)。栈(stack)栈,又称堆栈,存放程序的局部变量(但不包括静态声明的变量,static是指在数据段存放变量)。另外,调用函数时,栈是用来传递参数和返回值的。由于栈的先进先出特性,栈对于调用场景的保存/恢复特别方便。在程序进入main函数之前,已经完成了内存中数据的分配和初始化,包括数据区、栈区等。关于这部分代码对开发者是不可见的,是C标准运行时的一部分。在调用和被调用的过程中,函数伴随着出栈和出栈,所以栈起到了重要的作用。函数的局部变量、参数、返回值都存放在栈区。函数结束后,栈区自动释放。栈区作为一个暂存区,是计算机使用内存空间的一种机制。仅仅了解C运行时的空间分布是不够的。最好了解一段编译后的代码是如何运行的,以及库中的函数是如何链接到目标代码的,尤其是函数指针链表的维护。之后会有一种完全掌控代码的感觉。不是总结的总结C语言不仅可以让我们了解编程的相关概念,还可以让我们了解程序的运行原理,例如计算机子系统如何交互,内存中有什么样的程序、操作系统和程序之间的“爱恨情仇”,这些底层知识对程序员的职业生涯大有裨益。C语言,被一些人誉为“上帝的语言”,几乎奠定了软件工业的基础,创造了许多其他语言。不过鉴于水平有限,举重若轻很难,本文的基本描述也只是老码友的想法。