天天看点

C和指针读书笔记——部分简介C和指针读书笔记——部分简介

C和指针读书笔记——部分简介

标签(空格分隔): 读书笔记

1 C语言的重要性

1.C语言效率高。

2.C语言具有可移植性。

1.1 重排字符

1.1.1 空白和注释

注释以符号/开始,以符号**/结束。注释不能被嵌套。但是可以用:

#if 0
    statements
#endif
           

在#if和#endif之间的程序就可以有效地从程序中去除,不论之间有没有其他注释。

1.1.2 预处理指令

就是.h文件,在main主函数中声明

1.1.3 main函数

int main(void)
{
}
           

主要讲了printf的用法。

1.1.4 read_column_number 函数

主要讲scanf用法。

1.2 补充

1.3 编译

1.4 总结

1.5 警告的总结

  • 在scanf函数的标量参数前未添加&字符。
  • 机械地把printf函数的格式代码照搬于scanf函数。
  • 在应该使用&&操作符的地方误使用了&操作符。
  • 误用=操作符而不是==操作符来测试相等性。

1.6 编程提示的总结

  • 使用#inculde指令避免重复声明。
  • 使用#define指令给常量值取名。
  • 在#include文件中放置函数原型。
  • 在while或if表达式中蕴含复制操作。
  • 如何编写一个空循环体。
  • 始终要进行坚持,确保数组不越界。

2 基本概念

2.1 环境

2.1.1 翻译

2.1.2 执行

  • 程序必须载入到内存中。
  • 程序开始执行。
  • 开始执行程序代码。
  • [x] 在绝大数机器里,程序将使用一个运行时堆栈(stack),它用来存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程中将一直保留他们的值。
  • 程序的终止。

2.2 词法规则

2.2.1 字符

  • [x] 注意使用转义序列
  • \? 在书写连续多个问号时使用,防止他们被解释为三个字母词。
  • \” 用于表示一个字符串常量内部的双引号。
  • \’ 用于表示字符常量’。
  • \\ 用于表示一个反斜杠,防止它被解释为一个转义序列符。
  • \a 警告字符。
  • \b 退格键。
  • \f 进纸字符。
  • \n 换行符。
  • \r 回车符。
  • \t 水平制表符。
  • \v 垂直制表符。

2.2.2 注释

2.2.3 自由形式的源代码

  • 相邻的标记之间必须出现一个至多个空白字符(或注释),不然他们可能被解释为单个标记。

3.3.4 标识符

  • 标识符就是变量、函数、类型等的名字,他们由大小写字母、数字和下划线组成,不能以数字开头。
  • 标识符不能使用关键字。

2.2.5 程序的形式

2.3 程序风格

让人更加方便的读写代码

3 数据

3.1 基本数据类型

  • 在C语言中,仅有4种基本数据类型——整型、浮点型、指针和聚合类型(如数组和结构等)。

3.1.1 整型家族

  • 包括字符、短整型、整型和长整型,都分为有符号(singed)和无符号(unsigned)两种版本。
  • 长整型至少应该和整型一样长,而整型至少应该和短整型一样长。

一、整型字面值

二、枚举类型

3.1.2 浮点类型

  • 浮点数家族包括float、double和long double类型。
  • 规定:long double至少和double一样长,而double至少和float一样长。
  • 所有浮点类型至少能够容纳从 10−37 到 1037 。

3.1.3 指针

一、指针常量

二、字符串常量

3.2 基本声明

形式:

说明符(一个或多个) 声明表达式列表

例如:

int a;
cha b;
           

3.2.1 初始化

例如:

3.2.2 声明简单数组

例如:

int values[];
           

3.2.3 声明指针

例如:

int *a;
           

3.2.4 隐式声明

3.3 typedef

  • 使用typedef比#define来创建新的类型名要好,因为后者无法处理指针类型。

3.4 常量

3.5 作用域

  • 编译器可以确认4中不同类型的作用域——文本作用域、代码作用域、函数作用域和原型作用域。
  • 标识符声明的位置决定它的作用域。

3.5.1 代码作用域

  • 位于一对花括号之间的所有语句称为一个代码块。任何在代码块的开始位置声明的标识符都具有代码块作用域。
  • 函数形参的作用域开始于形参的声明处,位于函数体之外。

3.5.2 文件作用域

  • 任何在所有代码块之外声明的标识符都具有文件作用域,它表示这些标识符从他们声明之处直到它所在的源文件结尾处都是可以访问的。

3.5.3 原型作用域

  • 原型作用域只适用于在函数原型中声明的参数名。

3.5.4 函数作用域

  • 一个函数中所有语句标签必须唯一。

3.6 链接属性

  • 链接属性一共有三中——external(外部)、internal(内部)和none(无)。

3.7 存储类型

  • 凡是在任何代码块之外声明的变量总是存储于静态内存中,也就是不属于堆栈的内存,这类变量称为静态(static)内存。
  • 在代码块内部声明的变量的缺省存储类型是自动的(automatic),也就是说它存储于堆栈中,称为自动(auto)变量。
  • 函数的形式参数不能声明为静态,因为实参总是在堆栈中传递给函数,用于支持递归。

3.8 static关键字

3.9 作用域、存储类型示例

3.12 编程提示的总结

  • 为了保持最佳的可移植性,把字符的值限制在有符号字符和无符号字符范围的交集之内,或者不要在字符上致谢算数运算。
  • 用他们在使用时最自然的形式来表示字面值。
  • 不要把整型值和枚举值混在一起使用。
  • 不要依赖隐式声明。
  • 在定义类型的新名字时,使用typedef而不是#define。
  • 用const声明其值不会被修改的变量。
  • 使用名字常量而不是字面值常量。
  • 不要在嵌套的代码块之间使用相同的变量名。

4 语句

4.1 空语句

4.2 表达式语句

4.3 代码块

4.4 if语句

语法:

if(expression)
    statement
else
    statement
           

4.5 while语句

语法:

while(expression)
    statement
           

4.5.1 break和continue语句

  • break用于永久终止循环。
  • continue用于永久种植当前的那次循环。

4.5.3 while语句的执行过程

4.6 for语句

语法:

for(expression1;expression2;expression3)
    statement
           

4.7 do语句

语法:

do
    statement
while(expression);
           
  • 当你需要循环体至少执行一次时,选择do。

4.8 switch语句

语法:

switch(expression)
    statement
           

4.8.1 switch中的break语句

  • 如果在switch语句执行中遇到了break语句,执行流就会立即调到语句列表的末尾。

4.8.2 default子句

  • 当switch表达式的值并不匹配所有case标签的值时,这个default子句后面的语句就会执行。所以,每个switch语句中只能出现一条default子句。

4.8.3 switch语句的执行过程

4.9 goto语句

语法:

goto 语句标签;
           
  • 一般使用goto来结束深层循环。

    如:

while(condition1){
    while(condition2){
        while(condition3){
            if(some disaster){
                goto quit;
            }
        }
    }
}
quit:;
           

5 操作符和表达式

5.1 操作符

5.1.1 算数操作符

5.1.2 移位操作符

5.1.3 位操作符

5.1.4 赋值

赋值操作符:=
复合赋值符:+= -= *= /= %= <<= >>= &= ^= |=
           

5.1.5 单目操作符

5.1.6 关系操作符

5.1.7 逻辑运算符

5.1.8 条件操作符

5.1.9 逗号操作符

expression1,expression2,...,expressionN
           
  • 逗号操作符将两个或多个表达式分割开来。这些表达式自左向右逐个进行求值,整个逗号表达式的值及时最后那个表达式的值。

5.1.10下标引用、函数调用和结构成员

5.2 布尔值

规则:零是假,任何非零值皆为真。

5.3 左值和右值

5.4 表达式求值

5.4.1 隐式类型转换

5.4.2 算数转换

  • 如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数转换为另一个操作数的类型,否则操作就无法进行。

5.4.3 操作符的属性

5.4.4 优先级和求值的顺序

6 指针

6.1 内存和地址

  • 内存中的每个位置由一个独一无二的地址标识。
  • 内存中的每个位置都包含一个值。

6.2 值和类型

  • 不能简单地通过检查一个值得位来判断它的类型。为了判断值的类型(以及它的值),你必须观察程序中这个值的使用方式。

6.3 指针变量的内容

6.4 间接访问操作符

通过一个指针访问它所指向的地址的过程称为间接访问(indirection)或解引用指针(dereferencing the pointer)。

6.5 末初始化和非法的指针

6.6 NULL指针

6.7 指针、间接访问和左值

  • 指针变量可以作为左值,并不是因为他们是指针,而是因为他们是变量。

6.8 指针、间接访问和变量

6.9 指针常量

6.10 指针的指针

6.11 指针表达式

6.13 指针的运算

  • 一个指针加1,它会根据类型进行调整,指向下一个类型。

6.13.1 算术运算

第一种形式

指针 ± 整数

  • 标准定义这种形式只能用于指向数组中某个元素的指针。

第二种形式

指针 - 指针

  • 只有当两个指针都指向同一个数组中的元素时,才允许从一个指针减去另一个指针。

6.13.2 关系运算

  • 前提是他们都指向同一个数组中的元素。

7 函数

7.1 函数定义

函数的定义就死函数体的实现

语法:

返回值类型 函数名(形式参数){
    代码块
}
           

7.2 函数声明

7.2.1 原型

返回值类型 函数名(形式参数);
           
  • 现在函数原型具有文件作用域,所以原型的一份拷贝可以作用于整个源文件,较之在该函数每次调用前单独书写一份函数原型药容易得多。
  • 现在函数原型只书写一次,这样就不会出现多份原型的拷贝之间的不匹配现象。
  • 如果函数的定义进行了修改,我们只需要修改原型,并重新编译所有包含了该原型的源文件即可。
  • 如果函数的原型同时也被#include指令包含到定义函数的文件中,编译器就可以确认函数原型与函数定义的匹配。

7.2.2 函数的缺省认定

  • 所有的函数都应该具有原型,尤其是那些返回值不是整型的函数,记住,值的类型并不是值的内在本质,而是取决于它被使用的方式。

7.3 函数的参数

  • 传递给函数的标量参数是传值调用的。
  • 传递给函数的数组参数在行为上就像是通过传址调用的那样。

7.4 ADT和黑盒

7.5 递归

递归函数就是直接或者间接调用自身的函数。

7.5.1 追踪递归函数

7.5.2 递归与迭代

阶乘

递归举例:

long factorial(int n ){
    if(n <= ){
        return ;
    }else{
        return n * factorial(n-);
    }
}
           

迭代举例:

long factorial(int n ){
    int result = ;
    while(n > ){
        result *= n;
        n -= ;
    }
    return result;
}
           

7.6 可变参数列表

7.6.1 stdarg宏

可变参数列表是通过宏来实现的,这些宏定义于stdarg.h头文件,它是标准库的一部分。

7.6.2 可变参数的限制

  • 注意,可变参数必须从头到尾按照顺序逐个访问,如果你在访问了几个可变参数后想半途终止,这是可以的。但是,如果你想一开始就访问参数列表中间的参数,那是不行的。

8 数组

8.1 一维数组

8.1.1 数组名

8.1.2 下标引用

8.1.3 指针与下标

8.1.4 指针的效率

指针有时比下标更有效率,前提是他们被正确的使用

  • 当你根据某个固定数目的增量在一个数组中移动时,使用指针变量将比使用下标产生更高的代码。当这个增量是1并且机器具有地址自动增量模型时,这点表现得更为突出。
  • 声明为寄存器变量的指针通常比位于静态内存和堆栈中的指针效率更高(具体提高的幅度取决于你所使用的机器)。
  • 如果你可以通过测试一些已经初始化并经过调整的内容来判断循环是否应该终止,那么你就不需要使用一个单独的计数器。
  • 那些必须在运行时求值的表达式较之诸如&array[SIZE]或array+SIZE这样的常量表达式往往代价更高。

8.1.5 数组和指针

8.1.6 作为函数参数的数组名

8.1.7 声明数组参数

int strlen(char *string);
int strlen(char string[]);
           

这两个函数原型是相等的。

8.1.8 初始化

9.1.9 不完整的初始化

编译器只知道初始值不够,但它无法知道缺少的是哪些值,所以,只允许省略最后几个初始值。

8.1.10 自动计算数组长度

上面自动计算出数组长度是3。

8.1.11 字符数组的初始化

char a[] = "Hello";
           

8.2 多维数组

int a[5][3];
           

8.2.1 存储顺序

8.2.2 数组名

8.2.3 下标

8.2.4 指向数组的指针

8.2.5 作为函数参数的多维数组

8.2.6 初始化

int a[2][3] = {{1,2,3},{4,5,6}};
           

8.3 指针数组

9 字符串、字符和字节

9.1 字符串基础

9.2 字符串长度

  • 注意strlen返回一个类型为size_t的值,这个类型是在头文件stddef.h中定义的,它是一个无符号整型类型。

9.3 不受限制的字符串函数

9.3.1 复制字符串

用于复制字符串的函数是strcpy,它的原型如下所示:

char *strcpy(char *dst, char const *src);
           

这个函数把参数src字符串复制到dst参数。

  • 注意:程序员必须保证目标字符数组的空间足以容纳需要复制的字符串。

9.3.2 链接字符串

要想把一个字符串添加(链接)到另一个字符串的后面,你就可以使用strcat函数,它的原型如下:

char *strcat(char *dst, char const *src);
           

9.3.3 函数的返回值

9.3.4 字符串比较

库函数strcmp用于比较两个字符串,它的原型如下:

int strcmp(char const *s1, char const *a2);
           

9.4 长度受限的字符串函数

9.5 字符串查找基础

9.5.1 查找一个字符

使用strchr和strrchr函数,他们的原型如下:

char *strchr(char const *str, int ch);
char *strrchr(char const *str, int ch);
           

9.5.2 查找任何几个字符

用strpbrk函数,原型如下:

char *strpbrk(char const *str, char const *group);
           

9.5.3 查找一个子串

用函数strstr函数,原型如下:

char *strstr(char const *s1, char const *s2);
           

9.6 高级字符串查找

9.6.1 查找一个字符串前缀

用函数strspn和strcspn,原型如下:

size_t strspn(char const *str, char const *group);
size_t strcspn(char const *str, char const *group);
           

9.6.2 查找标记

使用函数strtok,原型如下:

char *strtok(char *str, char const *sep);
           

9.7 错误信息

9.8 字符操作

9.8.1 字符分类

9.8.2 字符转换

toupper函数返回其参数的对应大写形式,tolower函数返回其参数的对应小写形式,原型如下:

int tolower(int ch);
int toupper(int ch);
           

9.9 内存操作

10 结构和联合

10.1 结构基础知识

10.1.1 结构声明

在声明结构时,必须列出它包含的所有成员。

struct tag {member-list}variable-list;
           

声明结构时可以使用一种良好技巧,用typedef创建一种新的类型,例如:

typedef struct{
    int a;
    char b;
    float c;
}Simple;
           

之后就直接可以使用Simple进行定义变量。

10.1.2 结构成员

10.1.3 结构成员的直接访问

10.1.4 结构成员的间接访问

10.1.5 结构的自引用

10.1.6 不完整的声明

10.1.7 结构的初始化

举例:

struct INIT_EX{
    int a;
    short b[];
    simple c;
    }x = {
        ,
        {,,,,},
        {,'x',}
    };
           

10.2 结构、指针和成员

10.2.1 访问指针

10.2.2 访问结构

10.2.3 访问结构成员

10.3 结构的存储分配

  • 只有当存储成员需要满足正确的边界对齐要求时,成员之间才可能出现用于填充的额外内存空间。

10.4 作为函数参数的结构

10.5 位段

10.6 联合

联合的所有成员引用的是内存中的相同位置。

10.6.1 变体记录

10.6.2 联合的初始化

如:

union{
    int a;
    float b;
    char c[];
} x = {};
           

把x.a初始化为5。

10.9 编程提示的总结

  • 把结构标签声明和结构的typedef声明放在头文件中,当源文件需要这些声明时可以通过#include指令把他们包含进来。
  • 结构成员的最佳排列形式并不一定就是考虑边界对齐而浪费内存空间最少的那种排列形式。
  • 把位段成员显式地声明为signed或unsigned int类型。
  • 位段是不可移植的。
  • 位段使源代码中位的操作表达得更为清楚。

11 动态内存分配

11.1 为什么使用动态内存分配

11.2 malloc和free

原型如下:

void *malloc(size_t size);
void free(void *pointer);
           

11.3 calloc和realloc

原型如下:

void *calloc(size_t num_elements, size_t element_size);
void realloc(void *ptr, size_t new_size);
           
  • calloc也用于分配内存。malloc和calloc之间的主要区别是后者在返回指向内存的指针之前把它初始化为0。
  • realloc函数用于修改一个原先已经分配的内存块的大小。

11.4 使用动态分配的内存

一般使用:

int *pi;
pi = malloc( * sizeof(int));
           

11.5 常见的动态内存错误

11.9 编程提示总结

  • 动态内存分配有助于消除程序内部存在的限制。
  • 使用sizeof计算数据类型的长度,提高程序的可移植性。

12 使用结构和指针

12.1 链表

  • 链表(linked list)就一些包含数据的独立数据结构(通常称为节点)的集合。

12.2 单链表

12.2.1 在单链表中插入

12.2.2 其他链表操作

12.3 双链表

13 高级指针话题

13.1 进一步探讨指向指针的指针

13.2 高级声明

13.3 函数指针

13.3.1 回调函数

13.3.2 转移表

13.4 命令行参数

13.4.1 传递命令行参数

13.4.2 处理命令行参数

13.5 字符串常量

14 预处理器

14.1 预定义符号

14.2 #define

define name stuff
           

有了这条命令以后,每当有符号name出现在这条指令后面时,预处理器就会把它替换成stuff。

14.2.1 宏

14.2.2 #define替换

14.2.3 宏与函数

例如在两个表达式中寻找其中较大(或较小)的一个:

14.2.4 带副作用的宏参数

14.2.5 命名约定

14.2.6 #undef

这条预处理指令用于移除一个宏定义。

14.2.7 命名行定义

14.3 条件编译

14.3.1 是否被定义

14.3.2 嵌套指令

14.4 文件包含

14.4.1 函数库文件包含

14.4.2 本地文件包含

14.4.3 嵌套文件包含

14.7 警告的总结

  • 不要在一个红定义的末尾加上分号,使其成为一条完整的语句。
  • 在宏定义中使用参数,但忘记在他们周围加上括号。
  • 忘记在整个宏定义的两倍加上括号。

15 输入/输出函数

16 标准库函数

17 经典抽象数据类型

18 运行时环境

继续阅读