《C和指针》 旧笔记迁移

目录

No.01 快速上手

No.02 基本概念

No.03 数据

No.04 语句

No.05 操作符和表达式

No.06 指针

No.07 函数

No.08 数组

No.09 字符串、字符和字节

No.10 结构和联合

No.11 动态内存分配

No.14 预处理器


No.01 快速上手

  • #if 0
    statements
    #endif
    这段语句有删除中间一段C代码的功能
  • 预处理:如#include<stdio.h>
    预处理器读入源代码,根据预处理指令对其修改,再将修改过的源代码交给编译器
    #include<stdio.h>中,预处理器用名为stdio.h的库函数头文件内容替换该语句,其结果是stdio.h的内容被逐字写到源文件的那个位置。
  • char const *input
    const表示函数将不会修改函数调用者所传递的这个参数
  • 软件开销最大之处不在于编写,而在于维护。修改代码时的首要问题就是看懂代码,所以注释是很必要的。
  • gets函数,从标准输入读取一行文本并把它储存于作为参数传递给它的数组中。
    一行输入由一串字符组成,以一个换行符结尾。gets会丢弃换行符,并在改行末尾存储一个NUL字节('\0')。然后gets函数返回一个非NULL值。
    如果gets函数被调用但实际上不存在输入行的时候,它会返回NULL值。
  • NUL是ASCⅡ中‘\0’的名字。
    NULL是一个值为0的指针。
    它们都是整型值,其值也相同,可以互换使用。不过,还是应该使用适当的常量。告诉阅读程序的人使用这个值的目的。
  • 函数声明中的数组参数未指定长度,这种格式是正确的。但如果需要数组长度时,值应该作为一个单独的参数传递给函数。
  • 用scanf函数时,标量参数前要加&,数组参数前不需要&,但如果数组参数出现下标引用也必须加&。
  • 标准C并没有硬性规定C编译器对数组下标有效性进行检查。所以如果超过数组长度,多出来的数值会存储在紧随数组后的内存位置,破坏原先存储在该位置的数据。
  • scanf函数在每次调用时都从标准输入读取。如果转换失败,不管是文件已经读完还是因为下一次输入的字符无法转换为整数,函数都会返回0。如果输入字符可以成功转换,这个值会存储到目标中,并且scanf函数返回1。
  • while( (ch=getchar())!=EOF && ch !='\n') ;
    上面语句可以剔除当前输入行最后的剩余字符,并且把赋值操作蕴含于while语句内部,消除冗余语句。如果不像上面一样写,应该是这样的:
    ch=getchar();
    while(ch!=EOF && ch!='\n')
    ch=getchar();
  • EOF:即End Of File,EOF在stdio.h中定义,它是一个整型值,通常为-1。
    C语言中,EOF常被作为文件结束的标志。还有很多文件处理函数处错误后的返回值也是EOF,因此常被用来判断调用一个函数是否成功。
    例如上述getchar()函数,如果输入中不在存在任何字符,函数就会返回常量EOF。
  • 当数组名作为实参时,传递给函数实际上是一个指向数组起始位置的指针。
    函数可以按照操纵指针的方式来操纵实参,也可以像使用数组名一样用下标来引用数组的元素。
    当然,由于传递的是地址,如果函数修改了数组元素,它实际上将修改实参数组的对应元素。
    所以函数声明中:
    void rearrange(char *output,char const *input,int n_columns,int const columns[]);
    前两个是指针,可以修改实参数组元素。而最后一个columns声明为const,表示使用者希望这个参数不能被修改,编译器会去验证是否违背该意图,如果函数中修改了columns,编译器就会报错。
  • 在使用str系列函数时,确保目标字符串有足够的空间是程序员的责任,函数并不对其进行检查。

No.02 基本概念

  • C中的两种环境:
    翻译环境(translation environment):在该环境中,源代码被转换为可执行的机器指令。
    执行环境(execution environment):用于实际执行代码。
    这两种环境不必位于同一台机器上。例如,交叉编译器(cross compiler)就是在一台机器上运行,但它产生的可执行代码运行于不同类型的机器上。
  • 翻译过程:

    程序源文件(可能有多个)通过编译过程(compiler)分别转换为目标代码(object code),各文件由链接器(linker)捆绑在一起,形成一个单一完整的可执行程序。
    链接器同时会引入标准C函数库中任何被该程序所用到的函数,也可以搜索个人程序库,将需要的函数链接到程序中。
  • 编译过程:
    ①先是预处理器处理。在该阶段预处理器在源代码执行一些文本操作。例如,用实际值代替由#define 定义的数值,读入由#include 指令包含的文件内容。
    ②第二阶段是解析(parse),判读语句意思,这是产生绝大多数错误和警告信息的地方。随后产生目标代码。如果在编译程序的命令行中加入要求进行优化选项,优化器(optimizer)会对目标代码进一步处理,提高效率。
  • UNIX系统中的编译和链接:

    ①编译并链接一个完全包含于一个源文件的C程序:
    cc program.c
    该命令将产生一个 a.out的可执行程序。中间产生program.o的目标文件,连接完成后删除。

    ②编译并连接几个C源文件:
    cc main.c sort.c lookup.c
    有几个源文件时,目标文件不会被删除

    ③编译一个C源文件并把它和现有目标文件链接一起:
    cc main.o lookup.o sort.c

    ④编译单个C源文件,产生目标文件,然后不链接:
    cc -c program.c

    ⑤编译几个C源文件,并产生目标文件:
    cc -c main.c sort.c lookup.c

    ⑥链接几个目标文件:
    cc main.o sort.o lookup.o

    命令行最后加上“-o name”最终可执行程序保存在name文件中,而不是a.out
  • MS-DOS命令行界面与UNIX的差别:
    编译是bcc
    目标文件名字是file.obj
    单个源文件编译并链接时,编译器不删除目标文件
    使用“-ename”把可执行程序命名为“name.exe”
  • C中的三字母词(即三个字符合起来会变成另外一个符号):
    ??( [     ??< {     ??= #
    ??) ]     ??> {     ??/ \
    ??! |     ??' ^      ??- ~
    虽然我们一般不会用到,但要防止出现我们不想要的输出,例如输出??!时产生 | 
  • /* */ 注释不允许嵌套,注释在预处理的时候会被删掉
  • 当我们在写C代码时,想用某个特定字符,但却无法如愿。例如,上面的三字母词;例如,"一般用来定界字符串常量,但如果我们要在字符串里包含一个双引号呢?

    这时要用到反斜杠\:
    \? \" \' \\ 它们分别可以代表? " ' \
    \在这里称为“转义符”,转义符可以代表它后面出现的符号,但又没有给这个符号增加特别的意义
    \a 表示警告,蜂鸣或闪动 \b 退格键 \f 进纸字符
    \n 换行符 \r 回车符 \t 水平制表符 \垂直制表符
    \40 表示八进制的40 \x40 表示十六进制的40
    (注意: \n:回车+换行,就是到下一行首列 \r:回车,就是回到本行首列)

No.03 数据

  • C仅有4种基本数据类型:整形、浮点型、指针、聚合类型(数组和结构等)
  • 整型:
    包括字符、短整型、整型、长整型(每一种又分有符号【singed】和无符号【unsigned】版本)
    (标准C规定:长整型至少应该和整型一样长,而整型至少应该和短整型一样长【也就都是大于等于关系】)

  • short int至少 2bytes,long int至少 4bytes。缺省的int是2bytes 是 4 bytes,由编译器设计者决定。
    这三个值也可以一样,例如把这三个整型值都设为4 bytes。
  • 头文件limits.h有说明各种不同的整数类型的特点。详情可以查看头文件limits.h和下表:

  • 常量【也叫字面值】(literal)
    十进制、八进制、十六进制整型字面值可能是int、long或unsigned long。缺省情况下,它是最短类型但能完整容纳这个值。
    在这些字面值后面加L或者l(小写字母),可以使这个整数被解释为long整型值
    加U或u,把数值解释为unsigned整型值。两个都加上就会被解释为unsigned long
    如果一个多字节字符常量前面有L,它是宽字符常量。
  • 枚举类型
    格式: enum typeName{ valueName1, valueName2, valueName3, ...... };
    运用:例如当我们要定义一周7天,可以用“#define”。但如果用枚举会更方便
    例如:enum week{ Mon, Tues, Wed, Thurs, Fri, Sat, Sun }; 其中Mon到Sun的值分别是从0到6。
    enum week{ Mon = 1, Tues = 2, Wed = 3, Thurs = 4, Fri = 5, Sat = 6, Sun = 7 }; 我们也可以每一个名字都定义一个值。
    enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun }; 或者只定义第一个的值,后面会依次递增,和上一语句等效。

    枚举是一种类型,我们可以用它定义枚举变量:
    enum week a, b, c;

    以下语句都是合法的:
    enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a, b, c;
    enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };
    enum week a = Mon, b = Wed, c = Sat;
    enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun } a = Mon, b = Wed, c = Sat;

    注意:Mon、Tues、Wed 等都是常量,不能对它们赋值,只能将它们的值赋给其他的变量。
    声明为枚举类型的变量实际上是整数类型。
    虽然一些编译器中,枚举变量赋一个超过定义范围的值时不会报错,但我们要避免这么做。例如:enum week a; a=10; 在C-Free中不会报错。
  • 浮点类型:浮点数包括float、double、long double。标准规定long double至少和double一样长,double至少和float一样长。同时规定一个最小范围:所有浮点数至少能够容纳从10^-37到10^37之间的任何值。

    头文件float.h中,定义了FLT_MAX、DBL_MAX和LDBL_MAX,分别表示float、double、long double所能存储的最大值。FLT_MIN、DBL_MIN和LDBL_MIN表示所能存储的最小值。

    浮点数字常量总写成十进制形式,它必须有一个小数点或者指数。(25. 是合法的)

    浮点数字常量缺省情况下都是double类型的,除非后面跟L或l表示它是long double,跟F或f表示它是一个float。
  • 当一个函数每次被调用时,它的自动变量(局部变量)可能每次分配的内存位置都不同。
  • signed关键字一般只用于char类型,因为其他整型在缺省情况下都是有符号数。至于char是否是signed,因编译器而异。
  • int* a; 和int *a; 都是合法的。但int* a,b,c; 是不合法的指针声明,应该是:int *a, *b, *c;
  • char *str="abcd"; str是指向字符串首字符的指针,即:*str 为 a
  • typedef:
    typedef char *ptr_to_char;
    ptr_to_char a;
    该语句声明a是一个指向字符的指针。

    使用typedef能有效处理一些复杂的声明
  • 另外,我们应该使用typedef而不是#define:

    因为#define无法正确地处理指针类型。当:

    #define ptr_to_char char*
    ptr_to_char a,b;
    上述语句只能正确地把a声明为指向字符的指针,但b却被声明为一个字符。
  • 常量:用关键字const来声明,它们的值不能修改
    int const a; 和const int a; 均合法
    可以在初始化的时候给a赋值,之后不能修改:int const a=5;
    当然函数形参声明为const时,在函数被调用时会得到实参的值
  • int const *pci; 指向整型指针常量的指针,可以修改指针的值,但不能修改它所指向的值。
    int *const cpi; 指向整型的常量指针,指针为常量,无法修改,但它的指向的值可以修改。
    int const * const cpci; 指针和它所指向的值都为常量,不允许修改。
  • #define指令是另一种创建名字常量的机制。有时使用#define比使用const好,因为只要允许使用常量的地方都可以使用前者,比如声明数组长度。但const变量只能用于允许使用变量的地方。
  • 在编程语言中,标识符是用户编程时使用的名字,对于变量、常量、函数、语句块也有名字;我们统统称之为标识符。
  • 作用域:当变量在程序某部分被声明时,它只有在程序的一定区域才能被访问。只要分属不同的作用域,你可以给不同变量起同一名字。

  • 代码块作用域:一对花括号之间所有语句称为一个代码块。图中6、7、9、10都具有代码块作用域。
    当代码块嵌套时,位于内层代码块的标识符的作用域到该代码块的尾部便终止。如果内层代码块有一个和外层代码块标识符同名。内层标识符将隐藏外层标识符。即9的f和6的f是不同的变量,6的f无法在内层代码块中通过名字来访问。(我们应该避免在嵌套的代码块中出现相同的变量名)
    图中9的i 和 10的i 可以共享同一个内存地址,因为在任何时刻,两个非嵌套的代码块最多只有一个处于活动状态。
  • 如果在函数体内部声明了名字和形参相同的局部变量,形参无法被函数体调用,相当于没有意义。所以这时会报错
  • 文件作用域:任何在所有代码块之外声明的标识符都具有文件作用域。例如图中的1和2,它们从声明之处到源文件结尾处都是可以访问的。
  • 原型作用域:只适用于函数原型中声明的参数名。如图中3和8。参数名字并非必须,出现参数名的时候,可以随意取名。它们不必和函数定义中的形参名匹配,更不用和函数实际使用时实参匹配。所以下面代码是可以编译通过的:

  • 链接属性:相同的标识符出现在几个不同的源文件中时,标识符的链接属性决定如何处理不同文件中出现的标识符。

    一共有三种:external(外部)、internal(内部)和none(无)。
    属于none属性的,总是被当做单独个体,多个声明被当作独立不同的实体
    属于internal属性的,在同一个源文件内所有声明指向一个实体,但位于不同源文件的多个声明则分属不同实体
    属于external属性的,无论声明多少次、位于几个源文件都表示同一实体


    如图,在缺省情况下。b , c, f 的链接属性为external,其余标识符的链接属性为none。所以,如果另一个源文件也定义了b和c,它们实际上访问的是这个源文件所定义的实体。f的链接属性之所以是external,是因为它是个函数名,在此源文件中调用f,可能链接到其他文件所定义的函数。
  • 在缺省链接属性为external的标识符加上static,可以将其链接属性改为internal。【只有在缺省且为external时加static才是这个作用】如:
    上图中的b改为 static int b; b将为此源文件私有,如果其他源文件有b,那么它所引用的是另一个不同的变量
  • extern可以使标识符具有external链接属性。但是第二次或以后的声明不会改变第一次声明所指定的链接属性。如下图所示,声明4并不会改变声明1:

  • 存储类型是指存储变量的内存类型,决定变量何时创建、何时销毁、它的值保存多久。有三个地方存储变量:普通内存、运行时堆栈、硬件寄存器。

    存储类型取决于声明位置。任何代码块外声明的内存存储于静态内存中,这类变量称为静态(static)变量。这类变量在程序运行前创建,整个执行过程都存在,直到程序结束。(静态变量不显式初始化,它将初始化为0)

    代码块内部声明变量的缺省存储类型是自动的,存储与堆栈中,成为自动(auto)变量。关键字auto就是用于修饰这种类型的。程序执行到自动变量的代码块时,自动变量才被创建,当程序执行离开代码块时,自动变量会被销毁。(自动变量创建时要初始化,否则它们的值总是垃圾)

    在代码块内部的变量,加上关键字static时,它会变为静态变量,这个操作不改变其作用域。(函数形参不能声明为静态)

    register可以用于自动变量声明,提醒它们存储于硬件寄存器而不是内存。通常寄存器变量访问效率较高
  • 对于函数而言,存储类型并不是问题,因为代码总是存储于静态内存中
  • 作用域、链接属性和存储类型总结:

No.04 语句

  • y+3; 或getchar(); 是合法的。第一条语句并不具备任何效果;第二条语句读取输入中的下一个字符,接着便将其丢弃。
  • C中不存在布尔类型,而是用整型代替。所以判断时的表达式是任何能产生整型结果的表达式子:零表示“假”,非零值表示“真”。
  • else字句(if中)从属于最靠近它的不完整if语句
  • while语句,在循环的测试在循环体开始之前执行,如果测试结果一开始就是假,循环体根本不会运行
  • break:用于永远终止循环; continue:用于终止当前那次循环,然后重新测试表达式的值
  • 如果我们需要处理一列以以负数最为结束标志的值,可以这么做:
while( scanf("%f",&value)==1 ){
    if(value<0)
        break;
}

//或者:

while( scanf("%f",&value)==1 && value>=0 ){

}
  • while语句的执行过程:

  • C的for语句是while循环的一种极为常用的语句组合形式的简写法:
    for( expression1 ; expression2 ; expression3 )
        statement

    expression1是初始化,只在循环开始时执行一次
    expression2是条件,每次循环体执行前都要执行一次
    expression3是调整,在循环体每次执行完毕,在条件部分即将执行之前执行
    所有这三个表达式都是可选的,都可以省略
    for语句中,break跳出循环,continue把控制流直接转移到调整部分
  • for语句的执行过程:

  • for循环相比while循环在风格上有一优势,它把所有用于操纵循环的表达式收集在一起,放在同一地点方便寻找。当循环体比较庞大时,这个优点更为突出。
  • switch语句中的判断结果必须是整型值
  • 运用switch语句时,要特别注意break语句。break语句实际效果是把语句列表划分为不同部分
  • 当没有default语句,并且表达式的值与所有case标签都不匹配的话,所有语句都会被跳过;如果有default语句,会执行default子句后面的语句。(虽然不写default语句没错误,但在每个switch中放上一条default子句是个好习惯)
  • 之所以不推荐使用goto语句,是因为在学习C的过程中,很容易形成对其依赖,使用goto语句来避免考虑程序设计。也增加维护的难度。
  • 当然,在一些情况下,goto语句可能也非常合适。例如跳出多重嵌套的循环。break语句只能跳出包围它的最内层循环,要快速从深层嵌套的循环中跳出只能使用goto语句。

No.05 操作符和表达式

  • 算术操作符: + - * / %
    除了%,其他几个操作符都是既适用于整型也适用于浮点型。当/两个操作数都是整数时,它执行整除运算。
    %为取模操作符,它接受两个整型操作数,把左操作数除以右操作数,但返回的是余数而不是商。
  • 移位操作符:左移位操作符为<<,右移位操作符为>>。
    • 左移位:左移位中,被移出左边边界的几位被丢弃,右边多出来的几个空位由0补齐。(左移位的逻辑移位和算术移位结果一样)
    • 右移位:右移位中,逻辑移位时,左边移入的位用0填充。算术移位时,左边的空位由原先该值的符号位决定,符号位为1则移入的位均为1,符号位为0则移入的位均为0。(计算机中的符号位,就是在处理二进制数据时,专门规定有一位,是用来确定数据的正负,符号位是1表示负数,是0表示正数,当然这里说的是有符号数。这个符号位通常是数据的最高位,如8位数据,左边第一位是符号位,后边七位用来表示数据大小。)
    • 无符号值执行所有移位操作都是逻辑移位。对于有符号值,采用的是逻辑移位还是算术移位取决于编译器。
    • a<<-5 这种移位结果是由编译器决定的,我们要避免这种用法。
  • 位操作符:位操作符有& | ^。分别表示AND、OR和XOR(异或:两个位不同结果为1,两个位相同结果为0)
  • 赋值表达式:r=s+(t=u-v)/3; 是合法的,等同于t=u-v; r=s+t/3; (实际上还是后面的写法好一点)
  • 复合赋值符: +=     -=     *=     /=     %=
    <<=     >>=     &=     ^=     |=
    (a+=expression; 等同于a=a+(expression); 注意括号,在执行加法运算前,括号内的表达式已经执行)
  • 单目操作符:
    • !:对操作数执行逻辑反操作,如果操作数为真,结果为假;如果操作数为假,结果为真。
    • ~ :按位取反运算符,操作数中所有原先为1的位变为0,所有原先为0的位变为1。
    • - :产生操作数的负值。
    • & :产生它操作数的地址
    • * :间接访问操作符
    • sizeof :判断它操作数的类型长度,以字节为单位表示。 sizeof x; 和sizeof (x); 均为合法的。 当sizeof操作数是数组名时,返回的是数组的长度,以字节为单位。
    • (类型) :强制类型转换。例如(float)a,可以将整型值a成为对应的浮点数值。它具有很高的优先级,所以直接把强制类型转换放在一个表达式前,只会改变表达式第一个项目的类型。如果要对整个表达式结果进行转换,必须用括号把整个表达式括起来。
    • ++和--操作符:该操作符可以前缀也可以后缀。以++操作符为例,不管是前缀还是后缀,它们都会复制一份变量值的拷贝,用于周围表达式(例如赋值操作)。不同的是,前缀操作符在进行复制之前增加变量的值,后缀操作符在进行复制之后才增加变量的值。所以,这些操作符的结果,不是被它们所修改的值(而是这份拷贝),所以++a=10; 是不合法的。
  • 关系操作符:> >= < <= != ==。 这些关系操作符产生的结果是整型(1或0),而不是布尔类型,所以可以用于赋值。
  • 逻辑操作符(||和&&)
  • 条件操作符(expression1 ? expression2 : expression3)
  • 下标引用其实也是操作符,例如array[下标],实际上是以*(array+(下标) )的形式实现的
  • 左值和右值:左值就是那些能出现在赋值符号左边的东西,右值就是那些能出现在赋值符号右边的东西
  • 隐式类型转换:表达式中的字符型和短整型操作数在使用之前被转换为普通型,再执行运算。
  • 操作符属性:复杂表达式的求值顺序由3个因素决定:优先级、结合性以及操作符是否控制执行顺序。
    • 优先级:相邻操作符哪个先执行取决于优先级,如果优先级相同取决它们的结合性。
    • 结合性:一串操作符是从左向右依次执行还是从右向左逐个执行
    • 操作符是否控制执行顺序:一个有4个操作符可以控制求值顺序
  • 操作符优先级表格:(lexp代表左值表达式,rexp表示右值表达式)

No.06 指针

  • 错误的初始化例子:
    int *a;
    *a=12;

    我们声明了指针a,但未对其进行初始化,所以我们没办法预测12这个值将存储于什么地方。声明一个指向整型的指针并不会“创建”用于存储整型值的内存空间。
  • NULL指针:空指针,表示指针未指向任何东西。对一个NULL指针进行解引用操作是非法的。
  • int a; int *d=&a;
    *d是一个左值,所以*d=10 - *d; 是合法的
  • 如果很想把25存储于位置100(实际上,这种做法并没有什么卵用):

    *100=25; 这样是错的,因为100的类型是整型,而间接访问操作只能用于指针类型表达式
    *(int *)100=25; 这样才是正确的。用强制转换把“整型”转换为“指向整型的指针”
    (这个技巧唯一用处是:偶尔你需要通过地址访问内存中某个特定位置,并不是访问某个变量而是访问硬件本身)
  • 指向指针的指针:
    int a=12;
    int *b=&a;
    int **c=&b;
  • char ch='a';
    char *cp=&ch;

    ①*cp+1:*的优先级高于+,所以先执行简介访问、再取得这个值的拷贝并与1相加,整个式子的结果是'b'
    ②*(cp+1):可作为左值使用,访问ch后面那个内存位置,但我们不知道原先存储于那个地方的是什么,所以这样的表达式可能是非法的
    ③*++cp和*cp++:前缀++最终访问的是ch后面的那个内存位置,而后缀++访问的是ch
    ④++*cp:两个操作符的结合性都是从右向左,所以先执行间接访问操作,然后cp所指向的值加1。式子的最终结果是'b'
    ⑤(*cp)++:式子结果是'a'
    ⑥++*++cp:结合性从右至左,首先执行++cp,接着对其间接访问(也就是访问ch后面那个内存位置),最后在该位置执行++操作
    ⑦++*cp++:原理和上面相似。后缀++操作符优先级较高,所以先执行它,但间接访问的是ch
  • strlen函数:
int strlen(char *string){
    int length=0;
    while(*string++!='\0')
        length++;
    return length;
}
  • 当两个指针都指向同一个数组中的元素时,允许一个指针减去另一个指针。运算结果是两个内存中的距离(以数组元素长度为单位而不是字节为单位)。
    (也就是,如果p1指向array[i],p2指向array[j]。p2-p1的值是j-i的值)
  • 可以用下列关系操作符对两个指向同一数组中元素的指针进行比较:
    <、<=、>、>=

No.07 函数

  • C函数的所有参数均以“传值调用”方式进行传递,这意味着函数将获得参数值的一份拷贝。这样,函数修改这个拷贝值不会修改调用程序实际传递给它的参数。但如果被传递的参数是一个数组名,并且在函数中使用下标引用该数组的参数,那么函数中实际修改的是调用程序中的数组元素。
    数组名实际上是一个指针,传递给函数的就是这个指针的一份拷贝。下标引用实际上是间接访问的另一种形式。
  • 使用字符常量而不是整形常量可以提高程序的可移植性:
    '0'+0='0'
    '0'+1='1'
    '0'+2='2'
    所以可以考虑用该关系,把0~9的整型常量转换成字符常量

No.08 数组

  • 数组具有确定数量的元素,而指针只是一个标量值。编译器用数组名来记住这些属性。只有当数组名在表达式中使用时,编译器才会为它产生一个指针常量。
  • sizeof返回整个数组的长度,而不是指向数组的指针的长度
  • 如果有:
    int array[10];
    int *ap=array+2;

    则:
    ap[0]等同于array[2](C的下标引用和间接访问表达式是一样的)
    *ap+6:先进行间接访问,结果再与6相加,即等同于array[2]+6
    ap[-1]等同于array[1]
  • 一个有趣的例子(然而并没什么卵用):
    2[array] 等同于 *(2+(array)) 等同于 *(array+2)
  • int a[5]; int *b;
    表达式*a是合法的,*b是非法的,*b将访问内存中某个不确定的位置,程序或将终止。b++可以通过编译,但a++不行,因为a的值是个常量。
  • int matrix[3][5];
    则*(*(matrix+1)+5)等同于matrix[1][5]
  • 指向数组的指针:
    int vector[10],*vp=vector;
    int matrix[3][10];
    int (*p)[10]=matrix;

No.09 字符串、字符和字节

  • 查找一个字符:strchr和strrchr原型:
    char *strchr(char const *str,int ch);
    char *strrchr(char const *str,int ch);
    strchr在字符串str中查找字符ch(注意是整型值)第一次出现的位置,找到后函数返回一个指向该位置的指针。如果不存在,则返回一个NULL指针
    strrchr功能和strchr基本一致,返回的是一个指向字符串中该字符最后一次出现的位置。
  • 查找任何几个字符:strpbrk原型:
    char *strpbrk(char const *str ,char const *group)
    该函数返回一个指向str中第一个匹配group中任何一个字符的字符位置。如果找不到匹配,则返回一个NULL指针。
  • 查找一个子串:strstr函数:
    char *strstr(char const *s1,char const *s2);
    在s1中查找整个s2第一次出现的起始位置,并返回一个指向该位置的指针。如果s2没有完整出现在s1中,则返回一个NULL指针。

No.10 结构和联合

  • 声明结构时的另一种良好的技巧是用typedef创建一种新的类型:
     
    typedef struct {
        int a;
        char b;
        float c;
    }Simple;

    这个技巧和声明一个结构标签的效果几乎相同,区别在于Simple现在是个类型名而不是个结构标签,所以后续声明可能像下面这个样子:
    Simple x;
    
    Simple y[20],*z;
    (如果你想在多个源文件中使用同一种类型的结构,你应该把标签声明或typedef形式的声明放在一个头文件中,当源文件需要这个声明时可使用#include指令把那个头文件包含进来)
  • 结构成员的间接访问:
    struct COMPLEX *cp;
    (*cp).f;
  • C中提供了一个更为方便的操作符来完成这项工作:->(箭头操作符)
    箭头操作符接受两个操作数,但左操作数必须是一个指向结构的指针
    则有:
    cp->f cp->a cp->s

No.11 动态内存分配

  • 声明数组时候,必须用一个编译时常量指定数组的长度。但是,数组长度常常在运行时才知道。如果声明一个比较大的数组,当实际运用时元素数量比较少时,巨型数组的绝大部分内存空间都被浪费了。所以动态内存分配是必要的。
  • malloc:malloc从内存池中提取一块合适的内存,并向该程序返回一个指向这块内存的指针。这块内存并没有以任何方式进行初始化。它和free都在头文件stdlib.h中声明。
    原型:void *malloc(size_t size);
    size就是需要分配的内存字节(字符)数,如果内存池中的可用内存可以满足这个需求,malloc就返回一个指向被分配的内存块起始位置的指针。
  • malloc所分配的是一块连续的内存。
    如果内存池是空的,或者它的可用内存无法满足你的请求,malloc函数向操作系统请求,要求得到更多的内存。如果操作系统无法向malloc提供更多的内存,malloc就返回一个NULL指针。
  • free:当一块以前分配的内存不再使用的时候,程序调用free函数把它归还给内存池供以后之需。
    void free(void *pointer);
  • free的参数要么是NULL,要么是一个先前从malloc、calloc或realloc返回的指针。
  • calloc:void *calloc(size_t num_elements,size_t element_size);
    calloc也用于分配内存,和malloc的区别是,calloc在返回指向内存的指针之前把它初始化为0。calloc 的参数包括所需元素的数量和每个元素的字节数。
  • realloc:void realloc(void *ptr,size_t new_size);

    realloc函数用于修改一个原先已经分配的内存块的大小。如果扩大一个内存块,那么这块内存原先的内容原先的内容依然保留, 新增加的内存添加到原先内存块的后面,新内存并未以任何方法进行初始化。如果用它缩小一个内存块,该内存块尾部的部分内存被拿掉,剩余部分内存的原先内容依然保留
  • 如果原先的内存块无法改变大小,realloc将分配另一块正确大小的内存,并把原先那块内存的内容复制到新的块上。因此,在使用realloc之后,你就不能再使用指向旧内存的指针,而是改用realloc所返回的新指针。

No.14 预处理器

  • 预处理器定义了以下符号,它们的值是字符串常量或十进制数字常量:
    _FILE_:进行编译的源文件名,例如:name.c
    _LINE_:文件当前行的行号,例如:25
    _DATE_:文件被编译的日期,例如:“Jan 31 1997”
    _TIME_:文件被编译的时间,例如:“18:04:30”
    _STDC_:如果编译器遵循ANSI C,其值为1,否则未定义
  • 宏:#define name(parameter-list) stuff
    其中parameter-list是一个由符号分隔的符号列表,它们可能出现在stuff中。当参数出现在程序中时,与每个参数对应的实际值都将被替换到stuff中。
  • 例如:
    #define SQUARE(x) x*x
    SQUARE(5)
    替换之后变成5*5
    但是,如果:
    a=5; printf("%d\n",SQUARE(a+1));
    这时候结果并不是36,而是11,因为语句被替换成:
    printf("%d\n",a+1*a+1);
  • 条件编译:
    用于程序调试的语句不应该出现在程序的产品版本中,但是你可能并不想把他们从源代码中物理删除,因为如果需要一些维护性修改时,你可能需要重新调试这个程序,还需要这些语句。

    #if constant-expression
    statements
    #endif

    constant-expression由预处理器求值,如果非零那么statements正常编译,否则将被删除。
    这样,我们可以将所有调试代码以一下形式实现:

    #if DEBUG
    printf("x=%d,y=%d\n",x,y);
    #endif

    如果想编译它,则#define DEBUG 1,如果想忽略定义为0即可
  • 条件编译另一个用途是在编译时选择不同的代码部分,这时用到#elif 和 #else子句:

    #if constant-expression

    statements

    #elif constant-expression

    other statements

    #else

    other statements

    #endif

猜你喜欢

转载自blog.csdn.net/weixin_39731083/article/details/82463649
今日推荐