C语言笔记

基础知识(较简单)

main基本框架

头文件引用:

  • include<stdio.h> ←包含另一个文件。该行告诉编译器把stdio.h中的所有内容包含在当前程序中。stdio.h是C编译器软件包的标准部分,含义是标准输入/输出头文件
  • #include这行代码是一条C预处理器指令。#include中的#表明C预处理器在编译器接手之前处理这条指令,对源代码做一些准备工作,即预处理。在C程序顶部的信息集合被称为头文件(header)。例如,使用printf()函数,必须包含stdio.h头文件。
  • 通常,EOF(end of file的缩写)定义在stdio.h文件中:#define EOF (-1),用于检测文件结尾
stdio.h头文件(常用输入输出函数)
string.h头文件(字符串函数)

int main(void)

  • 圆括号表明main()是一个函数名。int表明main()函数返回一个整数,(int是main()函数的返回类型)。void表明main()不带任何参数。

return 0

  • return语句。C函数可以给调用方提供(或返回)一个数。目前,可暂时把该行看作是结束main()函数的要求。

函数执行:

  • C程序一定从main()函数开始执行。
  • 可以任意命名其他函数,而且main()函数必须是开始的函数。
  • ()的功能:用于识别是一个函数。通常,函数名后面的()中包含传入函数的信息。若没有传递任何信息,圆括号内是单词void
  • C编译器允许main()没有参数或者有两个参数。
    • main()有两个参数时,第1个参数是命令行中的字符串数量。第2个参数通常是一个指向数组的指针
  • 所有的C函数都使用花括号标记函数体的开始和结束。这是规定,不能省略。作用类似于(begin和end)。还可用于把函数中的多条语句合并。花括号以及被花括号括起来的部分被称为块(block)。
  • 单独一个分号会被视为一条语句。

变量/参数:

  • 变量名称的第1个字符必须是字符或下划线,不能是数字。只能以小写字母、大写字母、数字和下划线(_)来命名。
  • 实际参数(简称实参)是传递给函数的特定值,形式参数(简称形参)是函数中用于储存值的变量

调用函数:只需输入函数名,把所需的参数填入圆括号即可

转义序列:

  • 用于代表难以表示无法输入的字符(如下所示)。每个转义序列都以(\)开始。后面的数字表明该字符在ASCII表中的位置

    \0 空字符 0
    \a 响铃 7
    \b 退格(Backspace) 8
    \t 水平制表符(即横向跳格)(用于对齐文本或分隔数据) 9
    \n 回车换行(Enter) 10
    \v 竖向跳格 11
    \f 换页 12
    \r 回车(把活跃位置移动到当前行的开始处) 13
    " 双引号(") 34
    \’ 单引号(‘’) 39
    ? 问号(?) 63
    \ 反斜线字符(\) 92
    \ddd 1~3位八进制所代表的字符
    \xhh 1~2位十六进制所代表的字符

  • 把转义序列赋给字符变量时,必须用单引号把转义序列括起来。

  • 无论是普通字符还是转义序列,只要是双引号括起来的字符集合,就无需用单引号括起来。

关键字:用于声明变量的类型.基本数据类型由11个关键字组成:int、long、short、unsigned、char、float、double、signed、_Bool、_Complex和_Imaginary。

int(%d):基本的整数类型。long(长整型:占用的存储空间可能比int多)(%ld)、short(短整型:占用的存储空间可能比int类型少)(%hd)、long long(拓展长整型)(%lld)和unsigned(无符号)和signed(有符号)用于提供基本整数类型的变式。
unsigned:只用于非负值的场合。例如,16位unsigned int允许的取值范围是0~65535,而不是-32768~32767。用于表示正负号的位现在用于表示另一个二进制位,所以可以表示更大的数。这种类型的数常用于计数,而且可以表示更大的正数
signed:在任何有符号类型前面添加关键字signed,可强调使用有符号类型的意图。

char(%c[字符]或%s[字符串]):字符类型。用于指定字母和其他字符(如,#、$、%和)。 > 在C语言中,用单引号括起来的单个字符被称为字符常量。

  • 双引号中的字符集合叫作字符串,字符串是以空字符(\0)结尾的char类型数组。
    • 双引号不是字符串的一部分。双引号仅告知编译器它括起来的是字符串,正如单引号用于标识单个字符一样。
    • C语言没有专门用于储存字符串的变量类型,字符串都被储存在char类型的数组中。数组由连续的存储单元组成,字符被储存在相邻的存储单元中,每个单元储存一个字符
    • 根据%s转换说明,scanf()只会读取字符串中的一个单词,而不是一整句。

float(%f)、double(%lf或%f)和long double(%llf):表示浮点数。double表示双精度浮点数。双精度即更高的精度和更宽的数值范围。
%.2f中的.2用于精确控制输出,指定输出的浮点数只显示小数点后面两位。

  • 在浮点数后面加上f或F后缀可覆盖默认设置,编译器会将浮点型常量看作float类型,如2.3f
  • 使用l或L后缀使得数字成为longdouble类型,如54.3l。建议使用L后缀,因为字母l和数字1很容易混淆。
  • 没有后缀的浮点型常量是double类型。

_Bool:表示布尔值,即逻辑值(true或false)。

  • _Bool类型的变量只能储存1(真)或0(假)。如果把其他非零数值赋给_Bool类型的变量,该变量会被设置为1。
  • C99提供了stdbool.h头文件,让bool成为_Bool的别名,还把true和false分别定义为1和0的符号常量。包含该头文件后,写出的代码可以与C++兼容,因为C++把bool、true和false定义为关键字。

_Complex:表示复数.C语言有3种复数类型:float_Complex、double_Complex和long double _Complex。PS.float _Complex类型的变量应包含两个float类型的值,分别表示复数的实部和虚部。
_Imaginary:表示虚数。与复数类似。
* 包含complex.h头文件,便可用complex代替_Complex,用imaginary代替_Imaginary,还可以用I代替-1的平方根。

const:用于限定一个变量为只读。其声明如下:const int MONTHS = 12; 这使得MONTHS成为一个只读值。也就是说,可以在计算中使用MONTHS,但是不能更改MONTHS的值。
* const用起来比#define更灵活。
* const double * pd = rates;
* pd指向数组的首元素,该代码把pd指向的double类型的值声明为const,这表明不能使用pd来更改它所指向的值,但数组rate本身可变
* 声明并初始化一个不能指向别处的指针:
* double * const pc = rates;
* 可以用这种指针修改它所指向的值,但是它只能指向初始化时设置的地址。

进制:

  • 在C语言中,用特定的前缀表示使用哪种进制。
    • 0x或0X前缀表示十六进制,16表示成十六进制是0x10或0X10。以十六进制显示数字,使用%x
      • %lx表示以十六进制格式打印long类型整数
    • 0前缀表示八进制。十进制数16表示成八进制是020。以八进制显示数字,使用%o
      • %lo表示以八进制格式打印long类型整数
    • 以十进制显示数字,使用%d。
    • 如果要显示各进制数的前缀0、0x和0X,必须分别使用%#o、%#x、%#X。
  • 注意,虽然C允许使用大写或小写的常量后缀,但是在转换说明中只能用小写。

转换说明

Define格式:

  • 首先是#define,接着是符号常量名,然后是符号常量的值(注意,其中并没有=符号)。所以,
  • 通用格式如下:#define NAME value
  • 用大写表示符号常量是C语言一贯的传统。这样,在程序中看到全大写的名称就立刻明白这是一个符号常量,而非变量。
  • #define指令还可定义字符和字符串常量。前者使用单引号,后者使用双引号。

字符宽度:

  • 示例:%9d中的数字d代表的就是字符宽度
  • 想把数据打印成列,指定固定字段宽度很有用。因为默认的字段宽度是待打印数字的宽度,如果同一列中打印的数字位数不同,打印出来的数字可能参差不齐。使用足够大的固定字段宽度可以让输出整齐美观。
  • 如果在两个转换说明中间插入一个空白字符,可以确保即使一个数字溢出了自己的字段,下一个数字也不会紧跟该数字一起输出。
    • 这是因为格式字符串中的普通字符(包括空格)会被打印出来。

指针(pointer):

  • 指向变量或其他数据对象位置。例如,在 scanf()中用到的前缀&,便创建了一个指针,告诉scanf()把数据放在何处。

运算:

单目算术运算:取负值等

二目算术运算:+,-,*,/,%

关系运算:<;<=;>;>=;==(判断A是否等于B);!=(不相等)

  • c语言中关系运算结果为1(真)或0(假),且通常0以外的值都被判定为真
  • 如果a,b,c为int型,S为double型,S=(a+b+c)/2不准确,容易出问题,应改为S=(a+b+c)*1.0/2

递增运算符(++)和递减运算符(--):将其运算对象递增1/减1。

  • 前缀模式后缀模式。区别在于递增行为发生的时间不同。
  • ++/--运算符不能作用于解引用的指针
    • 在数组形式中,ar1是地址常量。不能更改ar1,如果改变了ar1,则意味着改变了数组的存储位置(即地址)。
      • 因此可以进行类似ar1+1这样的操作,但是不允许进行++ar1这样的操作。
      • 递增运算符只能用于变量名前(或概括地说,只能用于可修改的左值),不能用于常量。
  • 减少使用问题的出现:如果一个变量出现在一个函数的多个参数中或一个变量多次出现在一个表达式中,不要对该变量使用递增或递减运算符。

强制类型转换运算符

  • 需要进行精确的类型转换,或者在程序中表明类型转换的意图的情况下使用
    • 通用形式是:(type);用实际需要的类型(如,long)替换type即可。
      • 示例:mice = 1.6 + 1.7;mice = (int)1.6 + (int)1.7;

A +=;-=;=;/=;%= B相当于A=A +;-;;/;% B。

与(&&),或(||),非(!):

  • 用if (inword)代替if (inword == true)。用if (!inword)代替if (inword == false)。

条件运算符:?:

  • 条件运算符是C语言中唯一的三元运算符。
    • 示例:x = (y < 0) ? -y : y;
      • 在=和;之间的内容就是条件表达式。
  • 通用形式:expression1 ? expression2 : expression3。如果 expression1 为真(非 0),那么整个条件表达式的值与expression2 的值相同;如果expression1为假(0),那么整个条件表达式的值与expression3的值相同。
  • 通常,条件运算符完成的任务用if else语句也可以完成。但是,使用条件运算符的代码更简洁。
  • 条件运算符的第2个和第3个运算对象可以是字符串。
    • 示例:cans==1?"can":"cans"

地址运算符:&

  • 后跟一个变量名时,&给出该变量的地址。

地址运算符/间接运算符:*

  • 后跟一个指针名或地址时,*给出储存在指针指向地址上的值。
  • 也称为解引用运算符。
  • 不要和二元乘法运算符(*)混淆,虽然它们使用的符号相同,但语法功能不同。
    • 示例:val = *ptr; // 找出ptr指向的值

成员运算符——点(.)

  • 该运算符与结构或联合名一起使用,指定结构或联合的一个成员

间接成员运算符:->

  • 该运算符和指向结构或联合的指针一起使用,标识结构或联合的一个成员。

优先级:

  • 关系运算符的优先级比算术运算符(包括+和-)低
    • 关系运算符之间的优先级:高优先级组: <<= >>=; 低优先级组: == !=
  • .比&的优先级高

数组:按顺序储存的一系列类型相同的值

  • 整个数组有一个数组名,通过整数下标访问数组中单独的项或元素
  • 注意,数组元素的编号从0开始,不是从1开始。
typedef机制:
  • 允许程序员为现有类型创建别名。例如,typedef double real;这样,real就是double的别名。现在,可以声明一个real类型的变量:real deal; 使用typedef编译器查看real时会发现,在typedef声明中real已成为double的名,于是把deal创建为double类型的变量。

类型转换:

  • 类型的级别从高至低依次是long double、double、float、unsigned long long、long long、unsigned long、long、unsigned int、int。
    • 例外:当 long 和 int 的大小相同时,unsigned int比long的级别高。之所以short和char类型没有列出,是因为它们已经被升级到int或unsigned int。
  • 在赋值表达式语句中,计算的最终结果会被转换成被赋值变量的类型。这个过程可能导致类型升级或降级(demotion)。所谓降级,是指把一种类型转换成更低级别的类型。
  • 涉及两种类型的运算时,两个值会被分别转换成两种类型的更高级别。
  • 当作为函数参数传递时,char和short被转换成int,float被转换成double。
  • 当类型转换出现在表达式时,无论是unsigned还是signed的char和short都会被自动转换成int,如有必要会被转换成unsigned int
    • 如果short与int的大小相同,unsigned short就比int大。这种情况下,unsigned short会被转换成unsigned int)。
    • 由于都是从较小类型转换为较大类型,所以这些转换被称为升级(promotion)。

空字符与空指针

  • 空字符('\0'):用于标记C字符串末尾的字符,
    • 其对应字符编码是0。由于其他字符的编码不可能是 0,所以不可能是字符串的一部分。
  • 空指针(NULL):一个值,该值不会与任何数据的有效地址对应。
    • 通常,函数使用它返回一个有效地址表示某些特殊情况发生,例如遇到文件结尾或未能按预期执行。

常用函数:

  • sizeof():C语言规定,sizeof 返回 size_t 类型的值,是无符号整数类型。假如想知道在操作系统中,一个int型变量所占的内存空间为多少字节,则用以下代码:printf(“%zu\n”,sizeof(int));C99新增%zd转换说明,如果编译器不支持%zd,请将其改成%u或%lu。
    • 因为数组本身就算一个地址,因此输入时不需要指针指向,即数组可以不需要()或者&(但要去掉[],如(a+i)),但最好还是加上,标识性高

进阶知识

计算说明:

  • 科学计数法要用浮点数来表示,声明时要声明为浮点类型
    • 代码中3.16e/E7 表示3.16×10的7次方;
    • printf()函数使用%f转换说明打印十进制记数法的floatdouble类型浮点数,用%e打印指数记数法的浮点数。
  • 避免浮点数做相等不相等的运算。
    • 虽然关系运算符也可用来比较浮点数,但是注意:比较浮点数时,尽量只使用<和>。
      • 因为浮点数的舍入误差会导致在逻辑上应该相等的两数却不相等。
    • 使用fabs()函数(声明在math.h头文件中)可以方便地比较浮点数。示例:fabs(response-ANSWER)>1.0E-5
  • C语言把不含小数点和指数的数作为整数。
  • 在C语言中,整数除法结果的小数部分被丢弃,这一过程被称为截断。
  • 标准规定:无论何种情况,只要a和b都是整数值,便可通过a - (a/b)*b来计算a%b
  • C没有指数运算符。不过C的标准数学库提供了一个pow()函数用于指数运算。

if语句:

  • if语句的通用形式如下:
    • if ( expression )
  • 如果满足条件可执行的话,if语句只能测试和执行一次,而while语句可以测试和执行多次

continue语句:

  • 3种循环都可以使用continue语句。执行到该语句时,会跳过本次迭代的剩余部分,并开始下一轮迭代,并且只影响单层循环。
  • 使用continue的好处是减少主语句组中的一级缩进。
  • 对于for循环,执行continue后的下一个行为是对更新表达式求值,然后是对循环测试表达式求值。
  • 所有的循环都可以使用continue语句,但是switch语句不行。
  • 对于do while循环,对出口条件求值后,如有必要会进入下一轮迭代
  • 图解

break语句:

  • 程序执行到循环中的break语句时,会终止包含它的循环,并继续执行下一阶段。
  • 执行完break语句后会直接执行循环后面的第1条语句,连更新部分也跳过
  • 图解

switch语句:

  • C语言的case一般都指定一个值,不能使用一个范围。不能用变量作为case标签。
  • 如果标签没有关联break语句,程序流直接执行下一条语句,即下一个标签。
  • 如果是根据浮点类型的变量或表达式来选择,就无法使用 switch。

goto语句:

  • 分为goto标签名两部分。标签的命名遵循变量命名规则,如下所示:
    • goto part2;要让这条语句正常工作,函数还必须包含另一条标为part2的语句
      • part语句以标签名后紧跟一个冒号开始:
        • part2: printf("Refined analysis:\n");

循环:

  • 不确定循环:指在测试表达式为假之前,预先不知道要执行多少次循环。
  • 计数循环:这类循环在执行循环之前就知道要重复执行多少次。
  • 嵌套循环:在一个循环内包含另一个循环。常用于按行和列显示数据。
  • 一般而言,当循环涉及初始化和更新变量时,用for循环比较合适,而在其他情况下用while循环更好。
  • for/while()中的后缀++/--第一次都不调用,第二次才调用。

while循环:

  • 原理:当程序第1次到达while循环时,会检查圆括号中的条件是否为真。花括号之间的内容就是要被重复执行的内容。

for循环:

  • for循环把初始化、测试和更新这三个行为组合在一起。
    • 关键字for后面的圆括号中有3个表达式,分别用两个分号隔开。
      • 第1个表达式是初始化,只会在for循环开始时执行一次。
      • 第2个表达式是测试条件,在执行循环之前对表达式求值。如果表达式为假,循环结束。
      • 第3个表达式执行更新,在每次循环结束时求值。
        • 顺序不重要,条件可以不要,分号一定要有。
        • 逗号运算符扩展了for循环的灵活性,以便在循环头中包含更多的表达式。它保证了被它分隔的表达式从左往右求值。示例:
          • for (A= 1, B =C; A<= 16;A++,B+= D);

do while循环:

  • 为出口条件循环,即在循环的每次迭代之后检查测试条件,这保证了至少执行循环体中的内容一次。

缓冲:

  • 完全缓冲I/O:指当缓冲区被填满时才刷新缓冲区(内容被发送至目的地),通常出现在文件输入中。
  • 行缓冲I/O:指在出现换行符时刷新缓冲区。
  • conio.h头文件包括用于回显无缓冲输入的getche()函数和用于无回显无缓冲输入的getch()函数
    • 回显输入:用户输入的字符直接显示在屏幕上
    • 无回显输入:击键后对应的字符不显示。

ANSI函数声明:

  • void an(void):前一个void是函数类型,表示该函数没有返回值,后一个表示an不带参数,即不接受任何参数。圆括号表明an是一个函数名
    • 没有返回值,即不给 main()提供(或返回)任何信息。
    • 圆括号中的参数为形式参数:形式参数也是局部变量,属该函数私有。
      • 示例:void show_n_char(char ch, int num)
      • c要求每个形参前都都要声明其类型。
        • 示例:void dibs(int x, y, z) /* 无效的函数头 */
        • void dubs(int x, int y, int z) /* 有效的函数头 */
      • 根据个人喜好,函数声明可以省略变量名,但在函数定义中不可省略
    • 当计算机执行到an语句时,会找到该函数的定义并执行其中的内容。
  • 函数有需要返回时要声明函数的返回类型,在函数名前写出类型即可
    • 函数类型指的是返回值的类型,不是函数参数的类型。
  • 编译器要知道函数返回值的类型,才能知道有多少字节的数据,以及如何解释它们。这就是为什么必须声明函数的原因。
  • 注意,函数定义({})的末尾没有分号,而声明函数原型的末尾有分号。
  • 声明函数、调用函数、定义函数、使用关键字return。都是定义和使用带返回值函数的基本要素。
    • 函数原型指明了函数的返回值类型和函数接受的参数类型。这些信息称为该函数的签名。
    • 编译器在程序中首次遇到函数时,需要知道函数的返回类型。此时,编译器尚未执行到函数的定义,并不知道函数定义中的返回类型是什么。因此,必须通过前置声明预先说明函数的返回类型。
      • 如果把函数定义置于main()的文件顶部,就可以省略前置声明,因为编译器在执行到main()之前已经知道函数的所有信息。
  • 函数方括号中声明的变量是局部变量,该变量只属于该函数。可以在程序中的其他地方(包括main()中)使用同名变量而不会引起名称冲突,是同名的不同变量。
  • 关键字return后面的表达式的值就是函数的返回值。
    • 返回值不一定是变量的值,可以是任意表达式的值。示例:return (n < m) ? n : m;
  • 省略参数名称仅在函数定义中:如果你在函数声明中省略参数名称,确保在函数定义中提供完整的参数列表。

递归:

  • C允许函数调用它自己,这种调用过程称为递归。
    • 结束递归是使用递归的难点,因为如果递归代码中没有终止递归的条件测试部分,一个调用自己的函数会无限递归。
  • 递归方案更简洁,但效率却没有循环高。
  • 每级递归的变量n都属于本级递归私有,这从程序输出的地址值可以看出。
  • 每次函数调用都会返回一次。当函数执行完毕后,控制权将被传回上一级递归。
  • 程序必须按顺序逐级返回递归
  • 递归函数中位于递归调用之前的语句,均按被调函数的顺序执行。
  • 递归函数中位于递归调用之后的语句,均按被调函数相反的顺序执行。
  • 递归函数必须包含能让递归调用停止的语句。
    • 通常,递归函数都使用if或其他等价的测试条件在函数形参等于某特定值时终止递归。
  • 最简单的递归形式是把递归调用置于函数的末尾,即正好在 return语句之前。这种形式的递归被称为尾递归。尾递归是最简单的递归形式,因为它相当于循环。

地址:一元&运算符给出变量的存储地址。

  • 可以把地址看作是变量在内存中的位置。
  • %p是输出地址的转换说明,如果系统不支持这种格式,请使用%u或%lu代替%p
  • 示例:如果pooh是变量名,那么&pooh是变量的地址。

高阶知识(指针与数组的运用)

指针:一个值为内存地址的变量(或数据对象)。

  • 指针变量的值是地址。
    • 把指针作为函数参数使用示例:ptr = &pooh; // 把pooh的地址赋给ptr。
  • 要创建指针变量,先要声明指针变量的类型。
    • 指针指向地址,声明指针变量时必须指定指针所指向变量的类型。
    • 因为不同的变量类型占用不同的存储空间,一些指针操作要求知道操作对象的大小。另外,程序必须知道储存在指定地址上的数据类型。
      • 指针的声明示例:int * pi;// pi是指向int类型变量的指针
        • 类型说明符表明了指针所指向对象的类型
        • 星号(*)表明声明的变量是一个指针。
        • int * pi;声明的意思是pi是一个指针,*pi是int类型。
          • 使用指针:pi=&a(把地址赋给指针)。
          • *pi:获取储存在该地址的值。*和指针名之间的空格可有可无。
  • 普通变量把值作为基本量,把地址作为通过&运算符获得的派生量,
  • 指针变量把地址作为基本量,把值作为通过*运算符获得的派生量。
  • 使用&、*和指针可以操纵地址和地址上的内容。

指针变量的基本操作:

  • 赋值:可以把地址赋给指针
    • 例如,用数组名、带地址运算符(&)的变量名、另一个指针进行赋值。
    • 注意,地址应该和指针类型兼容。
  • 解引用*运算符给出指针指向地址上储存的值。
  • 取址:和所有变量一样,指针变量也有自己的地址和值。对指针而言,&运算符给出指针本身的地址。
  • 指针与整数相加/减:
    • 整数会和指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相加/减。
    • 如果相加/减的结果超出了初始指针指向的数组范围,计算结果则是未定义的。
    • 除非正好超过数组末尾第一个位置,C保证该指针有效。
  • 递增/减指针:
    • 递增指向数组元素的指针可以让该指针移动至数组的下/上一个元素。
      • 变量不会因为值发生变化就移动位置。
  • 指针求差:计算两个指针的差值。
    • 通常求差的两个指针分别指向同一个数组的不同元素,通过计算求出两元素之间的距离。
    • 差值的单位与数组类型的单位相同。
      • 示例:int ptr2 -ptr1得2,意思是这两个指针所指向的两个元素相隔两个int,而不是2字节。
    • 只要两个指针都指向相同的数组(或者其中一个指针指向数组后面的第 1 个地址),C 都能保证相减运算有效。
  • 比较:使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。
  • 注意:一个指针减去另一个指针得到一个整数,一个指针减去一个整数得到另一个指针。

使用指针经常要注意的点:

  • 创建一个指针时,系统只分配了储存指针本身的内存,并未分配储存数据的内存。
    • 因此,在使用指针之前,必须先用已分配的地址初始化它。不要解引用未初始化的指针

数组

数组的初始化:

  • 使用前必须先初始化
  • int类型/float类型初始化列表少于数组容量时剩余的元素都初始化为0/0.0。
    • 如果不初始化数组,数组元素和未初始化的普通变量一样,其中储存的都是垃圾值
  • 如果初始化列表的项数多于数组元素个数,编译器会视为错误。
  • 如果初始化数组时省略方括号中的数字,编译器会根据初始化列表中的项数确定数组的大小。
  • 指定初始化器:利用该特性可以初始化指定的数组元素。
    • 示例:int arr[6] = {[5] = 212}; 特性。
    • 第一,如果指定初始化器后面有更多的值,那么后面这些值将被用于初始化指定元素后面的元素。
    • 第二,如果再次初始化指定的元素,那么最后的初始化将会取代之前的初始化。
  • C不允许把数组作为一个单元赋给另一个数组
  • 除初始化以外不允许使用花括号列表的形式赋值。
  • 多维数组初始化:初始化时也可省略内部的花括号,只保留最外面的一对花括号。
    • 只要保证初始化的数值个数正确,初始化的效果相同。
    • 但是如果初始化的数值不够,则按照先后顺序逐行初始化,直到用完所有的值。后面没有值初始化的元素被统一初始化为0。
  • 让编译器计算数组的大小只能用在初始化数组时。如果创建一个稍后再填充的数组,就必须在声明时指定大小。

数组与指针

  • 数组名是该数组首元素的地址。
  • C保证在给数组分配空间时,指向数组后面第一个位置的指针仍是有效的指针。虽然C保证有效,但是对储存在该位置上的值未作任何保证,所以程序不能访问该位置
  • 初始化数组把静态存储区的字符串拷贝到数组中,而初始化指针只把字符串的地址拷贝给指针。
    • “指向字符串”的意思是指向字符串的首字符。如果不修改字符串,不要用指针指向字符串字面量,否则容易牵一发而动全身

指针与多维数组分析

  • 多维数组示例:int zippo[4][2]; /* 内含int数组的数组 */
    • zippo是数组首元素的地址,所以zippo的值和&zippo[0]的值相同。
      • zippo[0]本身是一个内含两个整数的数组,所以zippo[0]的值和它首元素(一个整数)的地址(即&zippo[0][0]的值)相同。
      • 简而言之,zippo[0]是一个占用一个int大小对象的地址,而zippo是一个占用两个int大小对象的地址。
      • 由于这个整数和内含两个整数的数组都开始于同一个地址,所以zippozippo[0]的值相同
    • 给指针或地址加1,其值会增加对应类型大小的数值。在这方面,zippozippo[0]不同,因为zippo指向的对象占用了两个int大小,而zippo[0]指向的对象只占用一个int大小。
    • zippo[0]是该数组首元素(zippo[0][0])的地址,所以*(zippo[0])表示储存在zippo[0][0]上的值(即一个int类型的值)。
    • *zippo代表该数组首元素(zippo[0])的值,但是zippo[0]本身是一个int类型值的地址。该值的地址是&zippo[0][0],所以*zippo就是&zippo[0][0]
      • 对两个表达式应用解引用运算符表明,**zippo*&zippo[0][0]等价,与·zippo[0][0]等价,即一个int类型的值。
    • 简而言之,zippo地址的地址,必须解引用两次才能获得原始值。
    • zippo[2][1]等价的指针表示法*(*(zippo+2) + 1)
  • 指向数组的指针示例:int (* pz)[2];
    • pz指向一个内含两个int类型值的数组。
    • 把pz声明为指向一个数组的指针,该数组内含两个int类型值。
      • 为什么要在声明中使用圆括号?因为[]的优先级高于*。
    • int / pax[2];``pax是一个内含两个指针元素的数组,每个元素都指向int的指针
    • 当且仅当pz是一个函数的形式参数时,可以这样声明:
      • void somefunction( int pz[][4] );
        • 注意,第1个方括号是空的。空的方括号表明pt是一个指针。
        • 一般而言,声明一个指向N维数组的指针时,只能省略最左边方括号中的值
  • 指向指针的指针示例:int **p2;
    • 一个指向指针的指针,变量p2是指向指针的指针,它指向的指针指向int
  • 变长数组(VLA):允许使用变量表示数组的维度。
    • C99/C11标准规定,可以省略原型中的形参名,但是在这种情况下,必须用星号来代替省略的维度:
      • 示例: int sum2d(int, int, int ar[*][*]);
        • ar是一个变长数组(VLA),省略了维度形参名

字面量:字面量是除符号常量外的常量。

  • 例如,5是int类型字面量, 81.3是double类型的字面量,'Y'是char类型的字面量,"elephant"是字符串字面量。

复合字面量:复合字面量代表数组和结构内容。

  • 示例:int diva[2] = {10, 20};//普通的数组声明.
  •  `(int [2]){10, 20}`// 复合字面量。
    
    • 该复合字面量创建了一个和diva数组相同的匿名数组,也有两个int类型的值,
    • 注意,去掉声明中的数组名,留下的int [2]即是复合字面量的类型名。
    • 因为复合字面量是匿名的,所以不能先创建然后再使用它,必须在创建的同时使用它。
      • 示例:int * pt1; pt1 = (int [2]) {10, 20}
        • 与有数组名的数组类似,复合字面量的类型名也代表首元素的地址,所以本例把它赋给指向int的指针。然后便可使用这个指针。
          • 例如,本例中*pt1是10,pt1[1]是20。

字符串字面量(字符串常量):双引号括起来的内容称为字符串字面量(string literal),也叫作字符串常量。

  • 如果字符串字面量之间没有间隔,或者用空白字符分隔,C会将其视为串联起来的字符串字面量
  • 如果要在字符串内部使用双引号,必须在双引号前面加上一个反斜杠(\):”\”。
  • 用双引号括起来的内容被视为指向该字符串储存位置的指针。类似于把数组名作为指向该数组位置的指针。
    • 如果"are"代表一个地址,使用%p的话printf()将打印该字符串首字符的地址

存储类别:

  • 被储存的每个值都占用一定的物理内存,C语言把这样的一块内存称为对象
  • 指定了特定内存位置的值被称为左值,是一个切实的可操作对象

作用域

  • 用于描述程序中可访问标识符的区域
  • 有块作用域、函数作用域、函数原型作用域或文件作用域
    • 定义在块中的变量具有块作用域(block scope)
      • 可见范围是从定义处到包含该定义的块的末尾。
    • 声明在内层块中的变量,其作用域仅局限于该声明所在的块
    • 函数作用域仅用于goto语句的标签。这意味着即使一个标签首次出现在函数的内层块中,它的作用域也延伸至整个函数。
    • 函数原型作用域用于函数原型中的形参名(变量名)
      • 范围是从形参定义处到原型声明结束
    • 变量的定义在函数的外面,具有文件作用域。
      • 范围从它的定义处到该定义所在文件的末尾均可见。
      • 示例见常见函数
      • 文件作用域变量可用于多个函数,因此也称为全局变量
  • 一旦程序离开作用域,就不能再访问该作用域中的变量
翻译文件
  • 每个翻译单元均对应一个源代码文件(.c扩展名)和它所包含的头文件(.h 扩展名)

链接

  • C 变量有 3 种链接属性:
    • 外部链接、内部链接、无链接。
  • 具有块作用域、函数作用域或函数原型作用域的变量都是无链接变量。
    • 意味着这些变量属于定义它们的块、函数或原型私有。
  • 外部链接变量可以在多文件程序中使用
  • 内部链接变量只能在一个翻译单元中使用
  • 通过查看外部定义中是否使用了存储类别说明符static可以知道文件作用域变量是内部链接还是外部链接

存储期

  • 用于描述通过标识符访问的对象的生存期
  • 分为静态存储期线程存储期自动存储期动态分配存储期
    • 静态存储期:在程序的执行期间一直存在
      • 所有的文件作用域变量都具有静态存储期。
    • 线程存储期:用于并发程序设计,程序执行可被分为多个线程。具有线程存储期的对象,从被声明时到线程结束一直存在。
    • 自动存储期:即动态存储块作用域的变量通常都具有自动存储期。当程序进入定义这些变量的块时,为这些变量分配内存;当退出这个块时,释放刚才为变量分配的内存。
      • 块作用域变量也能具有静态存储期。可以把变量声明在块中,且在声明前面加上static创建

自动变量

  • 属于自动存储类别的变量具有自动存储期、块作用域且无链接。
  • 默认情况下,声明在块或函数头中的任何变量都属于自动存储类别。
  • 如果内层块中声明的变量与外层块中的变量同名,内层块会隐藏外层块的定义,但离开内层块后,外层块变量的作用域又回到了原来的作用域
  • 自动变量不会初始化,除非显式初始化它。
    • 未初始化的变量可能为任何值

寄存器变量

  • 寄存器变量储存在CPU的寄存器中,或者概括地说,储存在最快的可用内存中。
  • 由于寄存器变量储存在寄存器而非内存中,所以无法获取寄存器变量的地址。
  • 但绝大多数方面,寄存器变量和自动变量都一样。
  • 使用存储类别说明符register便可声明寄存器变量

静态变量

  • 即拥有静态存储器的变量
  • 具有文件作用域的变量自动具有(也必须是)静态存储期
  • 静态的意思是该变量在内存中原地不动,并不是说它的值不变。
  • 以存储类别说明符static(提供静态存储期)声明这种变量
  • 如果未显式初始化静态变量,它们会被初始化为0
  • 静态变量只会被初始化一次,更多的初始化会被无效
  • 因为静态变量和外部变量在程序被载入内存时已执行完毕。
  • 把这条声明放在函数中是为了告诉编译器只有该函数才能看到该变量。

外部变量/外部链接的静态变量

  • 具有文件作用域、外部链接和静态存储期
  • 把变量的定义性声明放在在所有函数的外面便创建了外部变量。
  • 为了指出该函数使用了外部变量,可以在函数中用关键字extern再次声明。
  • 如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须用extern在该文件中声明该变量。
  • 外部变量对于文件中的其他函数也是可见的
  • 注意,如果外部变量定义在一个文件中,那么其他文件在使用该变量之前必须先声明它

内部链接的静态变量

  • 具有静态存储期、文件作用域和内部链接
  • 用存储类别说明符static声明
  • 普通的外部变量可用于同一程序中任意文件中的函数,但是内部链接的静态变量只能用于同一个文件中的函数。

存储类别说明符

auto
  • 表明变量是自动存储期
  • 只能用于块作用域的变量声明中
  • 主要是为了明确表达要使用与外部变量同名的局部变量的意图
    • 该关键字在C++中的用法完全不同,如果编写C/C++兼容的程序,最好不要使用其作为存储类别说明符。
register
  • 把变量归为寄存器存储类别,请求最快速度访问该变量。
  • 保护了该变量的地址不被获取。
  • 只能用于块作用域的变量声明中
static
  • 创建的对象具有静态存储期
  • 用于文件作用域声明,作用域受限于该文件。用于块作用域声明,作用域则受限于该块。
extern
  • 表明声明的变量定义在别处
  • 如果包含extern的声明具有文件作用域,则引用的变量必须具有外部链接。如果包含extern的声明具有块作用域,则引用的变量可能具有外部链接或内部链接
_Thread_local
  • 声明一个对象时,每个线程都获得该变量的私有备份
  • 可以和staticextern一起使用

函数的存储类别

  • 外部函数(默认)
    • 外部函数可以被其他文件的函数访问
    • static声明·
  • 静态函数。
    • 静态函数只能用于其定义所在的文件
    • extern声明
  • C99 新增了第 3 种类别——内联函数
  • 通常的做法:
    • 用 extern 关键字声明定义在其他文件中的函数。这样做是为了表明当前文件中使用的函数被定义在别处。
    • 除非使用static关键字,否则一般函数声明都默认为extern

随机数函数与静态变量

  • ANSI C库提供了rand()函数生成随机数
    • 实际上,rand()是“伪随机数生成器”
  • srand()函数重置种子值
  • 它们在stdlib.h头文件

本地查找引用文件

  • 把文件名放在双引号中而不是尖括号中,指示编译器在本地查找文件,而不是到编译器存放标准头文件的位置去查找文件。

动态内存(程序运行时分配更多的内存)

malloc()函数

  • 接受一个参数:所需的内存字节数。malloc()函数会找到合适的空闲内存块,这样的内存是匿名的。
  • malloc()分配内存,但是不会为其赋名,但返回动态分配内存块的首字节地址
    • 可以把该地址赋给一个指针变量,并使用指针访问这块内存
  • 通常该函数的返回值会被强制转换为匹配的类型。
  • 从ANSI C标准开始,C使用一个新的类型:指向void的指针。
    • 把指向void的指针赋给任意类型的指针完全不用考虑类型匹配的问题
  • 如果 malloc()分配内存失败,将返回空指针。
  • 除了用 malloc()在程序运行时请求一块内存,还需要一个指针记录这块内存的位置
    • 示例:
double * ptd;
ptd = (double *) malloc(30 * sizeof(double));
  * 指针ptd被声明为指向一个double类型,而不是指向内含30个double类型值的块。
  * 数组名是该数组首元素的地址。因此,如果让ptd指向这个块的首元素,便可像使用数组名一样使用它。
  • malloc()要与free()配套使用

free()函数

  • 参数是之前malloc()返回的地址
  • 该函数释放之前malloc()分配的内存
  • 动态分配内存的存储期从调用malloc()分配内存到调用free()释放内存为止
  • 可以避免内存泄露
  • 最好每次只释放一个一维数组

exit()函数

  • 如果内存分配失败,可以调用exit()函数结束程序,其原型在stdlib.h中
  • 标准提供了两个返回值以保证在所有操作系统中都能正常工作:
    • EXIT_SUCCESS(或者,相当于0)表示普通的程序结束,
    • EXIT_FAILURE 表示程序异常中止。
    • 示例:
#include <stdio.h>
#include <stdlib.h> /* 为 malloc()、free()提供原型 */
int main(void)
{
double * ptd;
int max;
int number;
int i = 0;
puts("What is the maximum number of type double
entries?");
if (scanf("%d", &max) != 1)
{
puts("Number not correctly entered -- bye.");
exit(EXIT_FAILURE);
}
ptd = (double *) malloc(max * sizeof(double));
if (ptd == NULL)
{
puts("Memory allocation failed. Goodbye.");
exit(EXIT_FAILURE);
}
/* ptd 现在指向有max个元素的数组 */
puts("Enter the values (q to quit):");
while (i < max && scanf("%lf", &ptd[i]) == 1)
++i;
printf("Here are your %d entries:\n", number = i);
for (i = 0; i < number; i++)
{
printf("%7.2f ", ptd[i]);
if (i % 7 == 6)
putchar('\n');
}
if (i % 7 != 0)
putchar('\n');
puts("Done.");
free(ptd);
return 0;
}

calloc()函数

  • 使用方法与malloc()类似
  • 第1个参数是所需的存储单元数量
  • 第2个参数是存储单元的大小(以字节为单位)
  • 特性:它把块中的所有位都设置为0
  • 示例
long * newmem;
newmem = (long *)calloc(100, sizeof (long));
使用动态内存通常比使用栈内存慢
  • 因为这部分的内存用于动态内存分配会支离破碎。

变长数组(VLA)和调用 malloc()在功能上类似

  • 但变长数组是自动存储类型。而用malloc()创建的数组不必局限在一个函数内访问。

恒常性和易变性

  • 分别用关键字constvolatile来声明
  • 类型限定符增加了一个新属性:它们现在是幂等的
    • 即可以在一条声明中多次使用同一个限定符,多余的限定符将被忽略
    • 示例:const const const int n = 6; 与 const int n = 6;相同

malloc()创建二维数组

  • 示例1:
int n = 5;
int m = 6;
int ar2[n][m]; // n×m的变长数组(VLA)
int (* p2)[6]; // C99之前的写法
int (* p3)[m]; // 要求支持变长数组
p2 = (int (*)[6]) malloc(n * 6 * sizeof(int)); // n×6 数组
p3 = (int (*)[m]) malloc(n * m * sizeof(int)); // n×m 数组(要求支持变长数组)
  • 示例2:
int **a,n ,m;
scanf(“%d%d”,&n,&m);
a=(int **)malloc(sizeof(int *)*n);//建立长度为n的动态指针数组
for(i=0;i<n;i++)
a[i]=(int *)malloc(sizeof(int)*m);//建立长度为m的一维整型数组

类型限定符

const限定符

  • 让变量成为只读变量
  • 注意
    • float * const pt; pt 是一个const指针
    • const float * pf; pf 指向一个float类型的const值
    • const float * const ptr;ptr既不能指向别处,所指向的值也不能改变
    • float const * pfc; // 与const float * pfc;相同
  • const放在左侧任意位置,限定了指针指向的数据不能改变;const放在的右侧,限定了指针本身不能改变

volatile限定符

  • 告知计算机,代理(而不是变量所在的程序)可以改变该变量的值。
  • 通常,它被用于硬件地址以及在其他程序或同时运行的线程中共享数据
  • 涉及编译器的优化
  • 可以同时用const和volatile限定一个值。例如,通常用const把硬件时钟设置为程序不能更改的变量,但是可以通过代理改变,这时用volatile。
  • 只能在声明中同时使用这两个限定符,它们的顺序不重要
  • 示例:
volatile int loc1;/* loc1 是一个易变的位置 */
volatile int * ploc; /* ploc 是一个指向易变的位置的指针 */

restrict限定符

  • 只能用于指针,表明该指针是访问数据对象的唯一且初始的方式。
  • 示例:
int ar[10];
int * restrict restar = (int *) malloc(10 * sizeof(int));
int * par = ar;
* 指针restar是访问由malloc()所分配内存的唯一且初始的方式。因此,可以用restrict关键字限定它。
* 而指针par既不是访问ar数组中数据的初始方式,也不是唯一方式。所以不用把它设置为restrict。

_Atomic类型限定符

  • S声明为原子类型
  • 当一个线程对一个原子类型的对象执行原子操作时,其他线程不能访问该对象。
    C11提供一个可选库,由stdatomic.h管理,以支持并发程序设计,而且_Atomic是可选支
    持项
    C99允许把类型限定符和存储类别说明符static放在函数原型和函数头的形式参数的初始方括号中

C的5种存储类别

  • 自动
  • 寄存器
  • 静态、无链接
  • 静态、外部链接
  • 静态、内部链接

进高阶知识(结构)

储存在一个结构中的整套信息被称为记录(record),单独的项被称为字段(field)

结构变量

使用结构必备

  • 为结构建立一个格式或样式
  • 声明一个适合该样式的变量
  • 访问结构变量的各个部分

创建结构声明(结构布局)

  • 示例
struct book {
char title[MAXTITL];
char author[MAXAUTL];
float value;
};
* 该声明描述了一个由两个字符数组和一个float类型变量组成的结构。
* 该声明并未创建实际的数据对象,只描述了该对象由什么组成。
* 有时,我们把结构声明称为模板,因为它勾勒出结构是如何储存数据的。
  * 如果读者知道C++的模板,此模板非彼模板,C++中的模板更为强大。
* 关键字`struct`,它表明跟在其后的是一个结构,后面是一个可选的标记(该例中是`book`),稍后程序中可以使用该标记引用该结构。
  * 所以,我们在后面的程序中可以这样声明:`struct book library;`
    * 这把`library`声明为一个使用`book`结构布局的==结构变量==
  • 在结构声明中,用一对花括号括起来的是结构成员列表。每个成员都用自己的声明来描述。
  • 成员可以是任意一种C的数据类型,甚至可以是其他结构!右花括号后面的分号是声明所必需的,表示结构布局定义结束。
  • 可以把这个声明放在所有函数的外部,也可以放在一个函数定义的内部。
    • 如果把结构声明置于一个函数的内部,它的标记就只限于该函数内部使用。

定义结构变量

  • 结构布局告诉编译器如何表示数据,但是它并未让编译器为数据分配空间。
  • 下一步是创建一个结构变量,即是结构的另一层含义。
    • 示例(结合常用程序获取更详细信息):struct book library;
      • 创建了一个结构变量library。编译器使用book模板为该变量分配空间:一个内含MAXTITL个元素的char数组、一个内含MAXAUTL个元素的char数组和一个float类型的变量。
      • 这些存储空间都与一个名称library结合在一起
      • 甚至可以声明指向struct book类型结构的指针
  • 声明结构的过程和定义结构变量的过程可以组合成一个步骤。
    • 示例:
struct book {
char title[MAXTITL];
char author[AXAUTL];
float value;
} library;
  * 可以把book简化掉
初始化结构
  • 使用在一对花括号中括起来的初始化列表进行初始化,各初始化项用逗号分隔。
  • 示例:见常用程序获取更详细信息),续上一个示例
  • 让每个成员的初始化项独占一行只是为了提高代码的可读性,对编译器而言,只需要用逗号分隔各成员的初始化项即可
访问结构成员
  • 使用结构成员运算符——点(.)访问结构中的成员。
结构的初始化器
  • 注意:对特定成员的最后一次赋值才是它实际获得的值
  • 示例
struct book gift= {.value = 18.90,
.author = "Philionna Pestle",
0.25};
* 赋给value的值是0.25,因为它在结构声明中紧跟在author成员之后。

结构数组

  • 使用结构可能会出现栈溢出的问题
    • 可以使用编译器选项设置栈大小为10000,以容纳这个结构数组;
    • 或者可以创建静态或外部数组(这样,编译器就不会把数组放在栈中);
    • 或者可以减小数组大小
  • 示例
    • struct book library[MAXBKS]; /* book 类型结构的数组 */
    • 注意,数组下标紧跟在library后面,不是成员名后面
  • 使用示例
    • scanf("%f", &library[count++].value);
  • 此外,在结构数组中如果程序不使用浮点数,旧式的Borland C编译器会尝试使用小版本的scanf()来压缩程序。然而,如果在一个结构数组中只有一个浮点值,那么这种编译器就无法发现它存在
    • 一种解决方案是,在程序中添加下面的代码:
#include <math.h>
double dummy = sin(0.0);
/*这段代码强制编译器载入浮点版本的scanf()。*/

嵌套结构

  • 示例
struct names { // 第1个结构
char first[LEN];
char last[LEN];
};
struct guy { // 第2个结构
struct names handle; // 嵌套结构
char favfood[LEN];
char job[LEN];
float income;
};
  • 访问嵌套结构的成员,这需要使用两次点运算符:
    • 示例printf("Hello, %s!\n", fellow.handle.first);

指向结构的指针

  • 声明示例:struct guy * him;
    • 使用示例:him = &barney;和数组不同的是,结构名并不是结构的地址,因此要在结构名前面加上&运算符
    • 如果是结构数组:him = &fellow[0];

用指针访问成员

  • 法一:使用->运算符
    • 示例:如果him == &barney,那么him->incomebarney.income
    • ->运算符后面的结构指针和.运算符后面的结构名工作方式相同(该符号只能指针使用)
      • 不能写成him.incone,因为him不是结构名
  • 法二:使用*解引用运算符
    • 示例:如果him ==&fellow[0],那么*him == fellow[0],因为&和*是一对互逆运算符。
      • fellow[0].income == (*him).income
      • 必须要使用圆括号,因为.运算符比*运算符的优先级高

向函数传递结构的信息

  • 可以选择是传递结构本身
  • 也可以传递指向结构的指针
  • 如果只关心结构中的某一部分,也可以把结构的成员作为参数

传递结构成员

  • 只要结构成员是一个具有单个值的数据类型(即,int及其相关类型、char、float、double或指针),便可把它作为参数传递给接受该特定类型的函数。

传递结构的地址

  • 示例:double sum(const struct funds ); / 参数是一个指针 */

直接传递结构

  • 示例:double sum(struct funds moolah); /* 参数是一个结构 */

结构传递给结构

  • 现在的C允许把一个结构赋值给另一个结构,但是数组不能这样做。
    • 示例:o_data = n_data; // 把一个结构赋值给另一个结构
      • 这条语句把n_data的每个成员的值都赋给o_data的相应成员。即使成员是数组,也能完成赋值。
  • 可以把一个结构初始化为相同类型的另一个结构
    • 示例:
struct names right_field = {"Ruthie", "George"};
struct names captain = right_field; // 把一个结构初始化为另一个结构
  • 函数不仅能把结构本身作为参数传递,还能把结构作为返回值返回。
  • 结构指针也允许这种双向通信
  • 把指针作为参数有两个优点:无论是以前还是现在的C实现都能使用这种方法,而且执行起来很快,只需要传递一个地址。
    • 缺点是无法保护数据。
  • 把结构作为参数传递的优点是,函数处理的是原始数据的副本,这保护了原始数据。代码风格也更清楚。
    • 传递结构的两个缺点是:较老版本的实现可能无法处理这样的代码,而且传递结构浪费时间和存储空间。尤其是把大型结构传递给函数,而它只使用结构中的一两个成员时特别浪费。这种情况下传递指针或只传递函数所需的成员更合理

结构中的字符数组和字符指针

  • 未经初始化的变量,地址可以是任何值
  • 因此,如果要用结构储存字符串,用字符数组作为成员比较简单。用指向 char 的指针也行,但是误用会导致严重的问题。
  • 如果使用malloc()分配内存并使用指针储存该地址,那么在结构中使用指针处理字符串就比较合理。
    • 记得释放内存

复合字面量和结构

  • 可以使用复合字面量创建一个数组作为函数的参数或赋给另一个结构。
  • 示例(是struct book类型的复合字面量):(struct book) {"The Idiot", "Fyodor Dostoyevsky", 6.99}

伸缩型数组成员

  • 其最后一个数组成员具有一些特性。
    • 第1个特性是,该数组不会立即存在。
    • 第2个特性是,使用这个伸缩型数组成员可以编写合适的代码,就好像它确实存在并具有所需数目的元素一样。
  • 声明一个伸缩型数组成员有如下规则
    • 伸缩型数组成员必须是结构的最后一个成员
    • 结构中必须至少有一个成员
    • 伸缩数组的声明类似于普通数组,只是它的方括号中是空的
    • 示例:
struct flex
{
int count;
double average;
double scores[]; // 伸缩型数组成员
};
  * 声明一个struct flex类型的结构变量时,不能用scores做任何事,因为没有给这个数组预留存储空间。
  * 实际上,C99的意图并不是让你声明struct flex类型的变量,而是希望你声明一个指向struct flex类型的指针,然后用malloc()来分配足够的空间,以储存struct flex类型结构的常规内容和伸缩型数组成员所需的额外空间。
  • 使用示例:
struct flex * pf; // 声明一个指针
// 请求为一个结构和一个数组分配存储空间
/*pf = malloc(sizeof(struct flex) + 5 * sizeof(double));
现在有足够的存储空间储存count、average和一个内含5个double类
型值的数组。可以用指针pf访问这些成员:*/
pf->count = 5; // 设置 count 成员
pf->scores[2] = 18.5; // 访问数组成员的一个元素
  • 带伸缩型数组成员的结构有一些特殊的处理要求。
    • 第一,不能用结构进行赋值或拷贝,这样做只能拷贝除伸缩型数组成员以外的其他成员。确实要进行拷贝,应使用memcpy()函数
    • 第二,不要以按值方式把这种结构传递给结构。原因相同,按值传递一个参数与赋值类似。要把结构的地址传递给函数
    • 第三,不要使用带伸缩型数组成员的结构作为数组成员或另一个结构的成员。

把结构内容保存在文件中

  • frintf()函数(效率较低)
    • 示例:如果pbook标识一个文件流
    • fprintf(pbooks, "%s %s %.2f\n", primer.title,primer.author,primer.value);
      • 这条语句可以把信息储存在struct book类型的结构变量primer中
    • 对于成员较多的结构用起来很不方便
  • 使用fread()fwrite()函数读写结构大小的单元

链式结构

  • 这样的结构中通常每个结构都包含一两个数据项和一两个指向其他同类型结构的指针。这些指针把一个结构和另一个结构链接起来,并提供一种路径能遍历整个彼此链接的结构。

联合

  • 联合(union)是一种数据类型,它能在同一个内存空间中储存不同的数据类型(不是同时储存)。
  • 其典型的用法是,设计一种表以储存既无规律、事先也不知道顺序的混合类型。
  • 使用联合类型的数组,其中的联合都大小相等,每个联合可以储存各种数据类型。
  • 结构与联合的区别示例
union hold {
int digit;
double bigfl;
char letter;
};
union hold fit; // hold类型的联合变量
//编译器分配足够的空间以便它能储存联合声明中占用最大字节的类型
union hold save[10]; // 内含10个联合变量的数组
union hold * pu; // 指向hold类型联合变量的指针
* 根据以上形式声明的结构可以储存一个int类型、一个double类型和char类型的值。
* 但声明的联合只能储存一个int类型的值或一个double类型的值或char类型的值
* 可以初始化联合。但联合只能储存一个值,这与结构不同。
* 3 种初始化的方法:
  * 把一个联合初始化为另一个同类型的联合
  * 初始化联合的第1个元素
  * 或者根据C99标准,使用指定初始化器
  • 联合中点运算符表示正在使用哪种数据类型
  • 和用指针访问结构使用->运算符一样,用指针访问联合时也要使用->运算符
  • 联合的另一种用法是,在结构中储存与其成员有从属关系的信息
  • 示例
struct owner {
char socsecurity[12];
...
};
struct leasecompany {
char name[40];
char headquarters[40];
...
};
union data {
struct owner owncar;
struct leasecompany leasecar;
};
struct car_data {
char make[15];
int status; /* 私有为0,租赁为1 */
union data ownerinfo;
...
};

匿名联合

  • 匿名联合和匿名结构的工作原理相同,即匿名联合是一个结构或联合的无名联合成员。

匿名结构

  • 用于嵌套数组中
  • 示例
struct person
{
int id;
struct {char first[20]; char last[20];}; // 匿名结构
};
* 这样在访问时简化了步骤,例如只需把first看作是person的成员那样使用它

枚举

  • 使用enum关键字,用枚举类型声明符号名称来表示整型常量。
  • enum常量是int类型,因此,只要能使用int类型的地方就可以使用枚举类型)。
  • 枚举类型的目的是提高程序的可读性。它的语法与结构的语法相同。
  • 示例
enum spec {red, orange, yellow, green, blue, violet};
//创建了spetrum作为标记名,允许把enum spetrum作为一个类型名使用
//花括号内的标识符枚举了spectrum变量可能有的值,这些符号常量被称为枚举符
enum spec color;
//明使color作为该类型的变量。
int c;
color = blue;
if (color == yellow)
...;
for (color = red; color <= violet; color++)
...;
printf("red = %d, orange = %d\n", red, orange);
//其输出如下:red = 0, orange = 1
//red成为一个有名称的常量,代表整数0。类似地,其他标识符都是有名称的常量
//spec的枚举符范围是0~5,所以编译器可以用unsigned char来表示color变量。
  • 虽然枚举符(如red和blue)是int类型,但是枚举变量可以是任意整数类型,前提是该整数类型可以储存枚举常量
  • 在声明数组时,可以用枚举常量表示数组的大小
  • 在switch语句中,可以把枚举常量作为标签
  • 默认情况下,枚举列表中的常量都被赋予0、1、2等
  • 在枚举声明中,可以为枚举常量指定整数值:
    • 示例:enum levels {low = 100, medium = 500, high = 2000};
    • 如果只给一个枚举常量赋值,没有对后面的枚举常量赋值,那么后面的常量会被赋予后续的值。
  • 注意,枚举类型只能在内部使用。

typedef

  • 为现有类型创建一个名称,看上去多此一举,却可以提高程序的可读性
  • 用typedef来命名一个结构类型时,可以省略该结构的标签
    • 示例
typedef struct {double x; double y;} rect;
rect r1 = {3.0, 6.0};
rect r2;
struct {double x; double y;} r1= {3.0, 6.0};
struct {double x; double y;} r2;
r2 = r1;
/*这两个结构在声明时都没有标记,它们的成员完全相同(成员名及其类型都匹配),C认为这两个结构的类型相同,所以r1和r2间的赋值是有效操作。*/
  • typedef常用于给复杂的类型命名
    • 示例:typedef char (* FRPTC ()) [5];
      • 该语句把FRPTC声明为一个函数类型,该函数返回一个指针,该指针指向内含5个char类型元素的数组

复杂声明

  • 示例
int board[8][8]; // 声明一个内含int数组的数组
int ** ptr; // 声明一个指向指针的指针,被指向的指针指向int
int * risks[10]; // 声明一个内含10个元素的数组,每个元素都是一个指向int的指针
int (* rusks)[10]; // 声明一个指向数组的指针,该数组内含10个int类型的值
int * oof[3][4]; // 声明一个3×4 的二维数组,每个元素都是指向int的指针
int (* uuf)[3][4]; // 声明一个指向3×4二维数组的指针,该数组中内含int类型值
int (* uof[3])[4]; // 声明一个内含3个指针元素的数组,其中每个指针都指向一个内含4个int类型元素的数组
char * fump(int); // 返回字符指针的函数
char (* frump)(int); // 指向函数的指针,该函数的返回类型为char
char (* flump[3])(int); // 内含3个指针的数组,每个指针都指向返回类型为char的函数
  • 数组名后面的[]和函数名后面的()具有相同的优先级。它们比*(解引用运算符)的优先级高
  • []和()都是从左往右结合

函数与指针

  • 函数也有地址,因为函数的机器语言实现由载入内存的代码组成。指向函数的指针中储存着函数代码的起始处的地址
  • 声明一个函数指针时,必须声明指针指向的函数类型。
    • 为了指明函数类型,要指明函数签名,即函数的返回类型和形参类型。
    • 函数名可以用于表示函数的地址
  • 示例
void ToUpper(char *); // 把字符串中的字符转换成大写字符
/*ToUpper()函数的类型是“带char * 类型参数、返回类型是void的函
数”。下面声明了一个指针pf指向该函数类型:*/
void (*pf)(char *); // pf 是一个指向函数的指针
pf = ToUpper; // 有效,ToUpper是该类型函数的地址
pf = ToLower; //有效,ToUpper是该类型函数的地址
pf = round; // 无效,round与指针类型不匹配
pf = ToLower(); // 无效,ToLower()不是地址
pf = ToUpper;
(*pf)(mis); // 把ToUpper 作用于(语法1)和ToUpper(mis)相同
pf = ToLower;
pf(mis); // 把ToLower 作用于(语法2)
/*由于函数名是指针,那么指针和函数名可以互换使用,所以pf(mis)和ToUpper(mis)相同*/
  • 由于运算符优先级的规则,在声明函数指针时必须把*和指针名括起来

位操作

在C语言中,可以单独操控变量中的位。许多压缩和加密操作都是直接处理单独的位。高级语言一般
不会处理这级别的细节,C 在提供高级语言便利的同时,还能在为汇编
语言所保留的级别上工作,这使其成为编写设备驱动程序和嵌入式代码
的首选语言

N进制数

  • 十进制数书写方法基于10的幂,称为以10为基底书写,以此类推,以N为基底表示的数字被称为N进制数
  • 通常,1字节包含8位。C语言用字节(byte)表示储存系统字符集所需的大小。计算机界通常用八位组(octet)这个术语特指8位字节

二进制补码与二进制反码

二进制补码是当今最常用的系统。我们将以1字节为例,讨论这种方法。
二进制补码用1字节中的后7位表示0~127,高阶位设置为0。目前,这种方法和符号量的方法相同。另外,如果高阶位是1,表示的值为负。这两种方法的区别在于如何确定负值。从一个9位组合100000000(256的二进制形式)减去一个负数的位组合,结果是该负值的量。例如,假设一个负值的位组合是 10000000,作为一个无符号字节,该组合为表示 128;作为一个有符号值,该组合表示负值(编码是 7的位为1),而且值为100000000-10000000,即 1000000(128)。因此,该数是-128(在符号量表示法中,该位组合表示−0)。类似地,10000001 是−127,11111111 是−1。
该方法可以表示−128~+127范围内的数
要得到一个二进制补码数的相反数,最简单的方法是反转每一位(即0变为1,1变为0),然后加1。
二进制反码方法通过反转位组合中的每一位形成一个负数。例如,00000001是1,那么11111110是−1。这种方法也有一个−0:11111111。该方法能表示-127~+127之间的数。

二进制浮点数

浮点数分两部分储存:二进制小数和二进制指数。

二进制小数

二进制表示法只能精确地表示多个1/2的幂的和。因此,3/4和7/8可以精确地表示为二进制小数,但是1/3和2/5却不能

浮点数表示法

为了在计算机中表示一个浮点数,要留出若干位(因系统而异)储存二进制分数,其他位储存指数。一般而言,数字的实际值是由二进制小数乘以2的指定次幂组成。例如,一个浮点数乘以4,那么二进制小数不变,其指数乘以2,二进制分数不变。如果一份浮点数乘以一个不是2的幂的数,会改变二进制小数部分,如有必要,也会改变指数部分

八进制转化二进制

八进制位 等价的二进制位

十六进制转化二进制

该系统基于16的幂,用0~15表示数字。但是,由于没有单独的数(digit,即0~9这样单独一位的数)表示10~15,所以用字母A~F来表示。在C语言中,A~F既可用小写也可用大写。

十进制 十六进制 等价二进制 十进制 十六进制 等价二进制
0 0 0000 8 8 1000
1 1 0001 9 9 1001
2 2 0010 10 A 1010
3 3 0011 11 B 1011
4 4 0100 12 C 1100
5 5 0101 13 D 1101
6 6 0110 14 E 1110
7 7 0111 15 F 1111

C按位运算符和移位运算符

按位逻辑运算符

4个按位逻辑运算符都用于整型数据,包括char。之所以叫作按位运算,是因为这些操作都是针对每一个位进行,不影响它左右两边的位。常规的逻辑运算符操作的是整个值

二进制反码或按位取反:~

示例

~(10011010) // 表达式
(01100101) // 结果值

假设val的类型是unsigned char,已被赋值为2。在二进制中,00000010表示2。那么,~val的值是11111101,即253。注意,该运算符不会改变val的值,就像3 * val不会改变val的值一样, val仍然是2。但是,该运算符确实创建了一个可以使用或赋值的新值

按位与:&

二元运算符&通过逐位比较两个运算对象,生成一个新值。对于每个位,只有两个运算对象中相应的位都为1时,结果才为1(从真/假方面看,只有当两个位都为真时,结果才为真)。因此,对下面的表达式求值:(10010011) & (00111101) // 表达式
由于两个运算对象中编号为4和0的位都为1,得:(00010001) // 结果值
C有一个按位与和赋值结合的运算符:&=。下面两条语句产生的最终结果相同:
val &= 0377;
val = val & 0377;

按位或:|

二元运算符|,通过逐位比较两个运算对象,生成一个新值。对于每个位,如果两个运算对象中相应的位为1,结果就为1(从真/假方面看,如果两个运算对象中相应的一个位为真或两个位都为真,那么结果为真)。因此,对下面的表达式求值:(10010011) | (00111101) // 表达式
除了编号为6的位,这两个运算对象的其他位至少有一个位为1,得:(10111111) // 结果值
C有一个按位或和赋值结合的运算符:|=。下面两条语句产生的最
终作用相同:
val |= 0377;
val = val | 0377;

按位异或:^

二元运算符^逐位比较两个运算对象。对于每个位,如果两个运算对象中相应的位一个为1(但不是两个为1),结果为1(从真/假方面看,如果两个运算对象中相应的一个位为真且不是两个为同为1,那么结果为真)。因此,对下面表达式求值:(10010011) ^ (00111101) // 表达式编号为0的位都是1,所以结果为0,得:(10101110) // 结果值
C有一个按位异或和赋值结合的运算符:^=。下面两条语句产生的最终作用相同:
val ^= 0377;
val = val ^ 0377;

用法:掩码

所谓掩码指的是一些设置为开(1)或关(0)的位组合如,假设定义符号常量MASK为2 (即,二进制形式为00000010),只有1号位是1,其他位都是0。语句:flags = flags & MASK;
把flags中除1号位以外的所有位都设置为0,因为使用按位与运算(&)任何位与0组合都得0。1号位的值不变(如果1号位是1,那么1&1得1;如果 1号位是0,那么 0&1也得0)。这个过程叫作“使用掩码”,因为掩码中的0隐藏了flags中相应的位。可以这样类比:把掩码中的0看作不透明,1看作透明。表达式flags& MASK相当于用掩码覆盖在flags的位组合上,只有MASK为1的位才可见

用法:打开位(设置位)

有时,需要打开一个值中的特定位,同时保持其他位不变。这种情况可以使用按位或运算符(|)
因为使用|运算符,任何位与0组合,结果都为本身任何位与1组合,结果都为1

用法:关闭位(清空位)

在不影响其他位的情况下关闭指定的位。
假设要关闭变量flags中的1号位。同样,MASK只有1号位为1(即,打开)。可以这样做:flags = flags & ~MASK;由于MASK除1号位为1以外,其他位全为0,所以~MASK除1号位为0以外,其他位全为1。使用&,任何位与1组合都得本身,所以这条语句保持1号位不变,改变其他各位。另外,使用&,任何位与0组合都是0。所以无论1号位的初始值是什么,都将其设置为0

用法:切换位

切换位指的是打开已关闭的位,或关闭已打开的位。可以使用按位异或运算符()切换位。假设创建了一个掩码,把后n位设置为1,其余位设置为0。然后使用组合掩码和待切换的值便可切换该值的最后n位,而且其他位不变。

用法:检查位的值

以flags和MASK位例:必须覆盖flags中的其他位,在进行比较:if ((flags & MASK) == MASK)

移位运算符

移位运算符向左或向右移动位。

左移运算符:<<
  • 左移运算符(<<)将其左侧运算对象每一位的值向左移动其右侧运算对象指定的位数。左侧运算对象移出左末端位的值丢失,用0填充空出的位置。
  • 可以使用左移赋值运算符(<<=)来更改变量的值。该运算符将变量中的位向左移动其右侧运算对象给定值的位数。
右移运算符

右移运算符(>>)将其左侧运算对象每一位的值向右移动其右侧运算对象指定的位数。左侧运算对象移出右末端位的值丢。对于无符号类型,用 0 填充空出的位置;对于有符号类型,其结果取决于机器。空出的位置可用0填充,或者用符号位(即,最左端的位)的副本填充

位字段

  • 位字段是一个signed intunsigned int类型变量中的一组相邻的位(C99和C11新增了_Bool类型的位字段)。位字段通过一个结构声明来建立,该结构声明为每个字段提供标签,并确定该字段的宽度。示例:
struct {
unsigned int autfd : 1;
unsigned int bldfc : 1;
unsigned int undln : 1;
unsigned int itals : 1;
} prnt;//建立了一个4个1位的字段
  • 带有位字段的结构提供一种记录设置的方便途径。许多设置(如,字体的粗体或斜体)就是简单的二选一。例如,开或关、真或假。如果只需要使用 1 位,就不需要使用整个变量。内含位字段的结构允许在一个存储单元中储存多个设置
  • 有时,某些设置也有多个选择,因此需要多位来表示,示例可以这样
struct {
unsigned int code1 : 2;
unsigned int code2 : 2;
unsigned int code3 : 8;
} prcode;
  • 可以这样赋值:
prcode.code1 = 0;
prcode.code2 = 3;
prcode.code3 = 102;
  • 如果声明的总位数超过了一个unsigned int类型的大小会怎样?会用到下一个unsigned int类型的存储位置。一个字段不允许跨越两个unsigned int之间的边界。编译器会自动移动跨界的字段,保持unsignedint的边界对齐。一旦发生这种情况,第1个unsigned int中会留下一个未命名的“洞”。
位字段示例
  • 通常,把位字段作为一种更紧凑储存数据的方式。
  • 例如,假设要在屏幕上表示一个方框的属性。为简化问题,我们假设方框具有如下属性:
  1. 方框是透明的或不透明的;
  2. 方框的填充色选自以下调色板:黑色、红色、绿色、黄色、蓝色、紫色、青色或白色;
  3. 边框可见或隐藏;
  4. 边框颜色与填充色使用相同的调色板;
  5. 边框可以使用实线、点线或虚线样式。
  • 可以使用单独的变量或全长(full-sized)结构成员来表示每个属性,但是这样做有些浪费位。例如,只需1位即可表示方框是透明还是不透明;只需1位即可表示边框是显示还是隐藏。8种颜色可以用3位单元的8个可能的值来表示,而3种边框样式也只需2位单元即可表示。总共10位就足够表示方框的5个属性设置
  • 方案一:一个字节储存方框内部(透明和填充色)的属性,一个字节储存方框边框的属性,每个字节中的空隙用未命名字段填充。struct box_props声明如下:
struct box_props {
bool opaque : 1 ;
unsigned int fill_color : 3 ;
unsigned int : 4 ;
bool show_border : 1 ;
unsigned int border_color : 3 ;
unsigned int border_style : 2 ;
unsigned int : 2 ;
};
  • 加上未命名的字段,该结构共占用 16 位。如果不使用填充,该结构占用 10 位。但是要记住,C 以unsigned int作为位字段结构的基本布局单元。因此,即使一个结构唯一的成员是1位字段,该结构的大小也是一个unsigned int类型的大小,unsigned int在我们的系统中是32位。
  • 另外,以上代码假设C99新增的_Bool类型可用,在stdbool.h中,bool是_Bool的别名
  • 方案二:
#include <stdio.h>
#include <stdbool.h> // C99定义了bool、true、false
/* 线的样式 */
#define SOLID 0
#define DOTTED 1
#define DASHED 2
/* 三原色 */
#define BLUE 4
#define GREEN 2
#define RED 1
/* 混合色 */
#define BLACK 0
#define YELLOW (RED | GREEN)
#define MAGENTA (RED | BLUE)
#define CYAN (GREEN | BLUE)
#define WHITE (RED | GREEN | BLUE)
const char * colors[8] = { "black", "red", "green", "yellow",
"blue", "magenta", "cyan", "white" };
struct box_props {
bool opaque : 1; // 或者 unsigned int (C99以前)
unsigned int fill_color : 3;
unsigned int : 4;
bool show_border : 1; // 或者 unsigned int (C99以前)
unsigned int border_color : 3;
unsigned int border_style : 2;
unsigned int : 2;
};
void show_settings(const struct box_props * pb);
int main(void)
{
/* 创建并初始化 box_props 结构 */
struct box_props box = { true, YELLOW, true, GREEN, DASHED };
printf("Original box settings:\n");
show_settings(&box);
box.opaque = false;
box.fill_color = WHITE;
box.border_color = MAGENTA;
box.border_style = SOLID;
printf("\nModified box settings:\n");
show_settings(&box);
return 0;
}
void show_settings(const struct box_props * pb)
{
printf("Box is %s.\n",
pb->opaque == true ? "opaque" : "transparent");
printf("The fill color is %s.\n", colors[pb->fill_color]);
printf("Border %s.\n",
pb->show_border == true ? "shown" : "not shown");
printf("The border color is %s.\n", colors[pb->border_color]);
printf("The border style is ");
switch (pb->border_style)
{
case SOLID: printf("solid.\n"); break;
case DOTTED: printf("dotted.\n"); break;
case DASHED: printf("dashed.\n"); break;
default: printf("unknown type.\n");
}
}

对齐特性

  • 对齐指的是如何安排对象在内存中的位置。例如,为了效率最大化,系统可能要把一个 double 类型的值储存在4 字节内存地址上,但却允许把char储存在任意地址。
  • _Alignof运算符给出一个类型的对齐要求,在关键字_Alignof后的圆括号写上类型名即可:
    size_t d_align = _Alignof(float);
    • 假设d_align的值是4,意思是float类型对象的对齐要求是4。也就是说,4是储存该类型值相邻地址的字节数。一般而言,对齐值都应该是2的非负整数次幂。较大的对齐值被称为stricter或stronger,较小的对齐值被称为weaker。
  • 可以使用_Alignas说明符指定一个变量或类型的对齐值。但是,不应该要求该值小于基本对齐值。例如,如果float类型的对齐要求是4,不要请求其对齐值是1或2。该说明符用作声明的一部分,说明符后面的圆括号内包含对齐值或类型:
_Alignas(double) char c1;
_Alignas(8) char c2;
unsigned char _Alignas(long double) c_arr[sizeof(long double)];
  • 在程序中包含 stdalign.h 头文件后,就可以把alignasalignof分别作为_Alignas_Alignof的别名。这样做可以与C++关键字匹配。
  • C11在stdlib.h库还添加了一个新的内存分配函数,用于对齐动态分配的内存。该函数的原型如下:
    void *aligned_alloc(size_t alignment, size_t size);
    • 第1个参数代表指定的对齐
    • 第2个参数是所需的字节数,其值应是第1个参数的倍数。与其他内存分配函数一样,
    • 要使用free()函数释放之前分配的内存。

C预处理器和C库

  • 明示常量(#define)
  • 预处理器指令从#开始运行,到后面的第1个换行符为止。
  • 在类函数宏的替换体中,#号作为一个预处理运算符,可以把记号转换成字符串。这个过程称为字符串化
  • 与#运算符类似,##运算符可用于类函数宏的替换部分。而且,##还可用于对象宏的替换部分。##运算符把两个记号组合成一个记号
#include <stdio.h>
#define XNAME(n) x ## n
#define PRINT_XN(n) printf("x" #n " = %d\n", x ## n);
int main(void)
{
int XNAME(1) = 14; // 变成 int x1 = 14;
int XNAME(2) = 20; // 变成 int x2 = 20;
int x3 = 30;
PRINT_XN(1); // 变成 printf("x1 = %d\n", x1);
PRINT_XN(2); // 变成 printf("x2 = %d\n", x2);
PRINT_XN(3); // 变成 printf("x3 = %d\n", x3);
return 0;
}
  • stdvar.h 头文件提供了工具,让用户自定义带可变参数的函数。C99/C11也对宏提供了这样的工具。
    • 通过把宏参数列表中最后的参数写成省略号(即,3个点...)来实现这一功能。
    • _ VA_ARGS _可用在替换部分中,表明省略号代表什么
    • 例如:#define PR(...) printf(_ _VA_ARGS_ _)
      • PR("Howdy");
      • PR("weight = %d, shipping = $%.2f\n", wt, sp);
      • 对于第2次调用,_ VA_ARGS _展开为3个参数:"weight = %d,shipping = $%.2f\n"、wt、sp。
    • 记住,省略号只能代替最后的宏参数
  • 宏的一个优点是,不用担心变量类型(这是因为宏处理的是字符串,而不是实际的值)

#include

  • 当预处理器发现#include 指令时,会查看后面的文件名并把文件的内容包含到当前文件中
  • 相当于把
    被包含文件的全部内容输入到源文件#include指令所在的位置
    指令形式:
#include <stdio.h> //←文件名在尖括号中
#include "mystuff.h" //←文件名在双引号中
/*在 UNIX 系统中,尖括号告诉预处理器在标准系统目录中查找该文件。双引号告诉预处理器首先在当前目录中(或文件名中指定的其他目录)查找该文件,如果未找到再查找标准系统目录*/
#include <stdio.h> //←查找系统目录
#include "hot.h" //←查找当前工作目录
#include "/usr/biff/p.h" //←查找/usr/biff目录
  • ANSI C不为文件提供统一的目录模型
  • C语言习惯用.h后缀表示头文件,这些文件包含需要放在程序顶部的信息。头文件经常包含一些预处理器指令。有些头文件(如stdio.h)由系统提供,当然你也可以创建自己的头文件。
  • 头文件最常用的形式:
    • 明示常量、宏函数、函数声明、结构模板定义、类型定义、

#undef指令

  • 用于“取消”已定义的#define指令
#define LIMIT 400
#undef LIMIT //移除上面的定义
  • 如果想使用一个名称,又不确定之前是否已经用过,为安全起见,可以用#undef 指令取消该名字的定义

条件编译

  • 使用指令告诉编译器根据编译时的条件执行或忽略信息(或代码)块
#ifdef MAVIS
    #include "horse.h"// 如果已经用#define定义了 MAVIS,则执行下面的指令
    #define STABLES 5
#else
    #include "cow.h" //如果没有用#define定义 MAVIS,则执行下面的指令
    #define STABLES 15
#endif

#ifndef SIZE
#define SIZE 100
#endif

#if SYS == 1
#include "ibm.h"
#endif
//可以按照if else的形式使用#elif(早期的实现不支持#elif)。例如,
可以这样写:
#if SYS == 1
    #include "ibmpc.h"
#elif SYS == 2
    #include "vax.h"
#elif SYS == 3
    #include "mac.h"
#else
    #include "general.h"
#endif
  • 这里使用的较新的编译器和 ANSI 标准支持的缩进格式
  • ifndef指令与#ifdef指令的用法类似,也可以和#else、#endif一起使用,但是它们的逻辑相反。#ifndef指令判断后面的标识符是否是未定义的,常用于定义之前未定义的常量

  • 包含多个头文件时,其中的文件可能包含了相同宏定义。#ifndef指令可以防止相同的宏被重复定义。在首次定义一个宏的头文件中用#ifndef指令激活定义,随后在其他头文件中的定义都被忽略
  • if和#elif指令:#if指令很像C语言中的if。#if后面跟整型常量表达式,如果表达式为非零,则表达式为真

预定义宏

含义
__DATE__ 预处理的日期
__FILE__ 表示当前源代码文件名的字符串字面量
__LINE__ 表示当前源代码文件名中行号的整型常量
__STDC__ 设置为1时,表示实现遵循C标准
__STDC_HOSTER__ 本机环境设置为1;否则设置为0
__STDC_VERSION__ 支持C99标准,设置为199991L;支持C11标准,设置为201112L
__TIME__ 翻译代码的时间,格式:hh:mm:ss
__func__ 展开一个代表函数名的字符串,是预定义标识符而不是预定义宏

#line和#error指令

  • line指令重置_ LINE FILE _宏报告的行号和文件名。

  • error 指令让预处理器发出一条错误消息,该消息包含指令中的文本。如果可能的话,编译过程应该中断

#line 1000 // 把当前行号重置为1000
#line 10 "cool.c" // 把行号重置为10,把文件名重置为cool.c
#if _ _STDC_VERSION_ _ != 201112L
#error Not C11
#endif

函数说明符_Noreturn

  • 表明调用完成后函数不返回主调函数。exit()函数是_Noreturn 函数的一个示例,一旦调用exit(),它不会再返回主调函数。
  • 注意,这与void返回类型不同。void类型的函数在执行完毕后返回主调函数,只是它不提供返回值。
  • _Noreturn的目的是告诉用户和编译器,这个特殊的函数不会把控制返回主调程序。告诉用户以免滥用该函数,通知编译器可优化一些代码

高级数据显示

从数组到链表

  • 例:调用一次malloc()函数创造300单位内存-->>调用300次malloc()函数每次创造一个单位内存
    • 前者分配的是连续的内存块,只需要一个单独的指向struct变量(film)的指针,后者该指针指向已分配块中的第1个结构。
    • 后者无法保证每次调用malloc()都能分配到连续的内存块。这意味着结构不一定被连续储存,解决方法就是链表
      • 需要储存300个指针,每个指针指向一个单独储存的结构。
  • 链表即每次使用 malloc()为新结构分配空间时,也为新指针分配空间。同时还得需要另一个指针来跟踪新分配的指针,用于跟踪新指针的指针本身,也需要一个指针来跟踪,以此类推。
    • 需要定义一个合适的结构来实现,即每个结构中包含指向 next 结构的指针。当创建新结构时,可以把该结构的地址储存在上一个结构中。
  • 结构不能含有与本身类型相同的结构,但是可以含有指向同类型结构的指针。这种定义是定义链表(linked list)的基础
  • 需要一个单独的指针储存第1个结构的地址,该指针被称为头指针。头指针指向链表中的第1项
#include <stdio.h>
#include <stdlib.h> /* 提供malloc()原型 */
#include <string.h> /* 提供strcpy()原型 */
#define TSIZE 45 /* 储存片名的数组大小 */
struct film {
char title[TSIZE];
int rating;
struct film * next; /* 指向链表中的下一个结构 */
};
char * s_gets(char * st, int n);
int main(void)
{
struct film * head = NULL;
struct film * prev, *current;
char input[TSIZE];
/* 收集并储存信息 */
puts("Enter first movie title:");
while (s_gets(input, TSIZE) != NULL && input[0] != '\0')
{
current = (struct film *) malloc(sizeof(struct film));
//如果用户进行输入,程序就分配一个结构的空间,并将其地址赋给指针变量current
if (head == NULL) /* 第1个结构 */
  head = current;
else /* 后续的结构 */
  prev->next = current;//指针prev指向上一次分配的结构
current->next = NULL;//重新设置current指针指向链表中的下一个结构
//随后每个结构的地址应储存在其前一个结构的next成员中
//把next成员设置为NULL,表明当前结构是链表的最后一个结构
strcpy(current->title, input);
puts("Enter your rating <0-10>:");
scanf("%d", &current->rating);
while (getchar() != '\n')
  continue;
puts("Enter next movie title (empty line to stop):");
prev = current;
//要为下一次输入做好准备。尤其是,要设置 prev 指向当前结构。因为在用户下一次输入且程序为新结构分配空间后,当前结构将成为新结构的上一个结构
}
/* 显示电影列表 */
if (head == NULL)
printf("No data entered. ");
else
printf("Here is the movie list:\n");
current = head;
while (current != NULL)
{
printf("Movie: %s Rating: %d\n",current->title, current->rating);
current = current->next;
}
/* 完成任务,释放已分配的内存 */
current = head;
while (current != NULL)
{
current = head;
head = current->next;
free(current);
}
printf("Bye!\n");
return 0;
}
char * s_gets(char * st, int n)
{
char * ret_val;
char * find;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
find = strchr(st, '\n'); // 查找换行符
if (find) // 如果地址不是 NULL,
*find = '\0'; // 在此处放置一个空字符
else
while (getchar() != '\n')
continue; // 处理剩余输入行
}
return ret_val;
}
  • 创建链表涉及下面3步:
    • 使用malloc()为结构分配足够的空间;
    • 储存结构的地址;
    • 把当前信息拷贝到结构中。

程序的栈(Stack)是一种重要的数据结构,它遵循后进先出(Last In First Out,LIFO)的原则。在计算机科学中,栈通常用于存储临时数据,例如函数调用时的局部变量、返回地址等。以下是栈的一些关键特性和用途:

  1. 后进先出(LIFO):栈中最后被添加的元素将是第一个被移除的元素。
  2. 主要操作
    • Push:向栈顶添加一个元素。
    • Pop:从栈顶移除一个元素。
    • Peek/Top:查看栈顶元素,但不移除它。
    • IsEmpty:检查栈是否为空。
  3. 内存分配:在许多编程语言中,栈是自动内存分配的,由编译器管理。
  4. 函数调用:当一个函数被调用时,它的局部变量和返回地址通常被压入栈中。当函数执行完毕后,这些信息被从栈中弹出,控制权返回到调用函数。
  5. 表达式求值:栈常用于中缀表达式(如我们通常写的算术表达式)的求值,通过将操作数压栈,操作符弹出栈来进行计算。
  6. 回溯算法:栈在需要回溯的算法中非常有用,如深度优先搜索(DFS)。
  7. 系统调用:操作系统使用栈来跟踪系统调用和中断。
  8. 异常处理:在异常发生时,栈用于保存状态信息,以便程序可以恢复到异常发生前的状态。
    栈是一种非常基础且重要的数据结构,它在程序设计和操作系统中扮演着关键角色

如何写出可读性高的程序:

  • 多使用空行增加可读性
  • 多使用注释

减少代码行数的小技巧

  • int feet, fathoms = int feet; int fathoms;

编写代码常用:

  • 大写字母的ASCII编码+32=小写字母的ASCII编码
  • 检测空白字符:
    • c == ' ' || c == '\n' || c == '\t';
    • c != ' ' && c != '\n' && c != '\t'
    • 使用ctype.h头文件中的函数isspace()
  • 丢弃后续不需要的字符串示例:
while (getchar() != '\n') // 读取但不储存输入,包括\n
continue;//如果目标数组装不下一整行输入,就丢弃那些多出的字符

编写代码是经常碰见的小错误:

  • 使用scanf忘记标明指针
  • 有返回值的C函数一定要有return语句。
  • 输入变量时,弱同一行中有多个变量,应给每个变量分别指定地址。
  • 不想因为输入多个变量导致提前输出,最好将声明放在一起,中间不要有输入函数分隔
  • 字符串排序的话排序的是指向字符串的指针,而不是字符串本身
  • 使用字符型数组最好把每个位置赋值一次,不然容易出问题。
  • 使用switch记得加方括号。
posted @ 2024-10-05 21:18  micryfotctf  阅读(82)  评论(2)    收藏  举报