C和指针学习笔记
前置条件
1.1 配置环境
-
下载vscode
-
安装编译器:这里以MinGw-w64为例。
- 下载MinGw-w64的安装包并解压。
- 添加到系统环境
-
编辑tasks.json(该文件负责项目的编译,如果需要同时编译多个文件,需要对该文件进行如下注释内的修改):
-
{ "tasks": [ { "type": "cppbuild", "label": "C/C++: gcc.exe build active file", "command": "D:\\Coding\\Rely\\C\\mingw64_8.1.0\\bin\\gcc.exe", "args": [ "-fdiagnostics-color=always", "-g", "${file}", //"$*.c", // 编译当前目录下所有以.c为结尾的文件 "-o", "${fileDirname}\\${fileBasenameNoExtension}.exe" //"${fileDirname}\\a.exe" // 将单个或多个编译生成的文件命名为a.exe,放在当前目录下 ], "options": { "cwd": "${fileDirname}" }, "problemMatcher": [ "$gcc" ], "group": { "kind": "build", "isDefault": true }, "detail": "Task generated by Debugger." } ], "version": "2.0.0" }
-
-
编辑launch.json(负责项目的调试):
-
生成方式:
Run and Debug-->create a lauch.json-->Add Configuration-->带Run的那一个Configuration
-
{ // Use IntelliSense to learn about possible attributes. // Hover to view descriptions of existing attributes. // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": "(gdb) Launch", "type": "cppdbg", "request": "launch", "program": "${fileDirname}\\${fileBasenameNoExtension}.exe", // 调试的程序名称 "args": [], "stopAtEntry": false, "cwd": "${fileDirname}", "environment": [], "externalConsole": false, "MIMode": "gdb", "miDebuggerPath": "D:\\Coding\\Rely\\C\\mingw64_8.1.0\\bin\\gdb.exe", // 调试器地址 "setupCommands": [ { "description": "Enable pretty-printing for gdb", "text": "-enable-pretty-printing", "ignoreFailures": true }, { "description": "Set Disassembly Flavor to Intel", "text": "-gdb-set disassembly-flavor intel", "ignoreFailures": true } ] } ]
-
一. 快速上手
1.1 预处理指令
是编译C程序的第一步,主要任务包括:
- 删除注释;
- 插入被
include
指令包含的内容; - 定义和替换由
define
定义的符号; - 确定代码的一部分内容是否应该根据一些条件编译指令进行编译。
EG:
#include <stdio.h> // IO库,用于执行输入输出
#include <stdlib.h> // 定义了EXIT_SUCCESS和EXIT_FAILURE符号
#include <string.h> // 定义了字符串相关函数。
#define MAX_COLS 20 /* 能够处理的最大列号 */
#define MAX_INPUT 1000 /* 每个输入行的最大长度 */
Include:引入的源文件,预编译器读入源代码,将对应的源文件代码按照相应的规则替换到对应的位置,效果上类似于将源文件代码直接拷贝过来。然后再将代码交给编译器处理。
define:定义常量值,不可修改。预编译器先将代码中对应的变量替换成常量值,随后将代码交给编译器处理。
1.2 输入输出
常用的输入格式:需要注意的是,scanf是从地址进行读取的,因此传入的变量应当是地址,而不是直接传变量值。
格式 | 含义 | 变量类型 |
---|---|---|
%d | 读取一个整型数 | int |
%ld | 读取一个长整型 | long |
%f | 读取一个实型值 | float |
%lf | 读取一个双精度实型值 | double |
%c | 读取一个字符 | char |
%s | 读取一个字符串 | char型数组 |
常用输出格式:
格式 | 含义 |
---|---|
%d | 十进制打一个整数 |
%o\或%O | 八进制打一个整数 |
%x或%X | 十六进制打印一个整数 |
%g | 打印一个浮点数 |
%c | 打印一个字符 |
%s | 打印一个字符串 |
%n | 换行 |
1.3 字符串处理
字符串本质:
就是一串以'\0'(NUL)结尾的字符的数组。
Eg:“hello world!”: 在计算机中,占据12个字符(根据编码方式,字符与字节的映射关系不同,而一个字节包含8位)
常用函数:
- getchar():
- putchar():
- strcpy():
- strncpy():
- strcat():
- strchr():
- strstr():
1.6 空
C中的空有三种:
- NULL:标准库中的一个宏定义,展开后是一个空指针常量
- NUL:一个字符的名称,在C中属于空字符,也就是'\0'。
- null
1.5 一些规范
大块代码消除:
如果使用注释消除大段代码,容易被中间存在的注释截断,导致代码出错。因此推荐使用if语句。代码逻辑为:
if 0
statements
end if
如此,中间的statements将在逻辑上被消除。
二. 基本概念
2.1 环境
环境分为两部分:
- 翻译环境:将源代码转换成可执行的机器指令。
- 执行环境:运行上一步产生的机器指令。
这两个环境是可以分开的,不必放在同一台机器上。
2.1.1 翻译
大致流程图如下:
步骤解析如下:
-
单个或多个源程序(Source Code)通过编译器(Compiler)编译产生各自对应的目标代码(Object Code)。
- 预编译(preprocess):预编译器在源代码上进行文本操作。如:读入include引入的源文件;替换define定义的常量。
- 解析(parse):对上一步得到的代码进行解析,产生目标代码。这一步是产生绝大多数错误和警告的地方。、
- 这一步还可以在编译程序的命令行中加入要求优化的选项,优化器(optimizer)会对目标代码进行优化,使其效率更高。但这需要消耗额外的时间,因此一般在调试完毕并准备生成正式产品前并不会进行。
注:目标代码包含多个部分(section),如:
- text:存放代码
- data:存放初始化过的数据。
- .bss:存放未初始化的数据。
- 其他一些更特殊的section,例如存放调试信息的section
- 等等
-
单个或多个目标代码经过链接器(Linker)捆绑在一起,产生一个单一可执行的完整程序(Libraries)。
- 在链接过程,会将各个目标文件的.text都拼在一起,.data都拼在一起,.bss都拼在一起… 最终生成一个可执行文件,该可执行文件也包含.text, .data, .bss等section。
文件名规范:
- 源程序后缀名:.c
- 头文件后缀名:.h
- 目标文件名:
- UINX、windows:.o
- MS-DOS:.obj
- 可运行程序:
- windows:.exe
- UINX:.out
编译和链接指令(UINX下):
注意:在windows下,只需将cc转换为gcc,即可实现以下效果。
-
编译并链接一个源文件:
cc hello.c
未指定生成的执行程序名称,因此采用默认的:a.out。
中间会产生一个名为Hello.o的目标文件,但会在链接阶段完成后被删除。
-
编译并链接多个源文件:
cc hello.c sort.c lookup.c
当编译的源文件数量超过一个时,中间的目标文件就不会消失。
最后产生:hello.o、sort.o、lookup.o、a.out
-
编译一个源文件并将其与现存的目标文件链接起来:
cc hello.o main.c
-
编译单个源文件,只产生并保存目标文件
cc -c hello.o
产生hello.o
-
编译多个源程序,产生对应的目标文件:
cc -c hello.c sort.c lookup.c
产生 hello.o、sort.o、lookup.o
-
链接多个目标文件,产生可执行程序:
cc main.o sort.o lookup.o
-
编译并链接文件,生成指定的可执行程序:
cc main.c sort.o lookup.o -o run
将会生成run.exe
2.1.2 执行:
步骤如下:
- 将程序载入内存。
- 执行程序,调用main函数。
- 执行程序代码。
- 程序终止。
2.2 词法规则
2.2.1 字母符
C中并没有对字符集的种类进行限制,但规定字符集必须包含:
-
大小写英语字母
-
0-9
-
! " # % ' ( ) * + , - . / : ; < > = ? [ ] \ ^ _ { } | ~
三字母词:
C标准规定了三个字母词,用三个字符来表示另一个字符,这是因为某些字符集缺少一些特定的字符表示。
??( [ ??< { ??= #
??) ] ??> } ??/ \
??! | ??’ ^ ??- ~
2.2.2 转义字符
转义字符 | 意义 |
---|---|
??) | 用于防止三字母词 |
\' | 表示字符常量' |
\" | 表示一个字符内部的双引号 |
\\ | 表示一个反斜杠 |
\a | 警告字符,蜂鸣 |
\b | 退格键 |
\f | 禁止符 |
\n | 换行 |
\r | 回车 |
\t | 水平制表符 |
\ddd | 表示1-3个八进制的数字。如:\013 |
\xddd | 表示1-3个十六进制的数字。如:\x013 |
三. 数据
数据的三个属性(决定了数据的可视性和生命值):
- 作用域
- 链接属性
- 存储类型
3.1 基本数据类型
C中规定了四大基础类型:
- 整型
- 浮点型
- 指针
- 聚合类型:例如数组、结构等
3.1.1 整数
分类
包括:
- 字符(char)
- 短整型(short int或short)
- 整形(int)
- 长整型(long int或long)
每种类型又可以分为有符号(signed)和无符号(unsigned)
长度比较:
长整型>=整型>=短整型
short、int:至少16位
long:至少32位
存储范围:
注意:
- int、long int的字节数并不固定为2、4,在有些编译器下,int的字节数可能是4。
- C中并不存在long long类型,该类型由C++定义,字节数量为8
- char在不同编译器中默认可能是有符号的、也可能是无符号的,这可能带来移植上的麻烦
- int、long、 long long默认为有符号的。
- 通过查看limits.h头文件,可以找到里面规定的有、无符号时四种整数的范围,当移植问题较重要且难以使用有无符号对char进行明确限定时,最好的方法就是将char型大小限制在有符号和无符号的交集中,并且,只有当参数被显式声明为signed char或unsigned char时,才对其进行运算。
类型 | 数据范围 | 字节数 |
---|---|---|
char | 1 | |
signed char | 1 | |
unsigned char | 1 | |
short int | 2 | |
signed short int | 2 | |
unsigned short int | 2 | |
int | 2 | |
signed int | 2 | |
unsigned int | 2 | |
long int | 4 | |
signed long int | 4 | |
unsigned long int | 4 |
EG:
-
代码:
int a = 10; long b = 20; long long c = 30; printf("int的字节数%d\n",sizeof(a)); printf("long的字节数%d\n",sizeof(b)); printf("long long的字节数%d",sizeof(c));
-
输出:
int的字节数4 long的字节数4 long long的字节数8
可以看出,在本机64位系统下,gcc使用的int、long字节数均为4,而C++中的long long类型字节数
字面量:
关于整数的书写方式:
- 在后面加上l\L:表示为long类型。 12L
- 在后面加上u\U:表示为unsigned类型。 12U
- 在前面以0开头:八进制数字。 012
- 在前面以0x开头,16进制数字。 0x12
枚举类型:
实际上枚举类型是一种整数集合,它的值是符号常量,但实际上是用整数的方式存储的。
-
声明:
-
enum Day{MON, TUE, WED, THU, FRI, SAT, SUN}
默认从0开始,且每一个符号常量代表的值比前一个大1,因此Day
-
enum Day{MON=1, TUE, WED, THU=7, FRI, SAT, SUN}
得到:Day
-
-
使用:
enum Day day; day = TUE; day++;
3.1.2 浮点数
基本类型:
- float
- double
- long double
大小
ANSI规定:long>=double>=long double,同时规定最小范围:所有浮点类型至少容纳\([10^{-37},10^{37}]\)
头文件float.h规定了这三种浮点数的范围,还定义一 些和浮点值的实现有关的某些特性的名字,例如浮点数所使用的基数、不同长度的 浮点数的有效数字的位数等。
表示:
- 默认是double类型
- 后跟L\l,说明是long double
- 后跟F\f,说明是float类型。
3.1.3 指针
用来指向一段内存空间,是C/C++的灵魂所在。
字符串常量:
- 定义:是一串以NUL字节结尾的、用双引号括起来的零个或多个字符。
- 是否可修改:这取决于编译器。但即使可以修改,在实践中也不建议,如果真的需要修改,建议将其存放在数组中。
- 与指针的联系:字符串常量的直接值是一个指向字符的常量指针,因此不能将其赋值给一个字符数组。
3.2 基本类型
3.3 typedef
-
用途:用于为数据类型起别名。
-
EG:
-
代码:
typedef char *ptr_to_char; ptr_to_char a;
-
效果:a是一个指向字符的指针。
-
-
与define的区别:define无法正确的处理指针类型。
-
EG:
#define d_ptr_to_char char * d_ptr_to_char a, b;
-
结果:a被声明为一个指向字符的指针,而b被声明为一个字符。
在定义更为复杂的类型名字时,如函数指针或指向数组的指 针,使用typedef更为合适。
-
3.4 常量
定义后不能修改的量。
3.4.1 const
有两种声明方式:
int const a;
const int a;
3.4.2 define
声明:
#define MAX_LENGTH 10
注意:define是一个预处理指令,由预编译器对其进行替换,和include指令类似,因此后面也不需要加分号。
3.4.3 指针常量、常量指针、指向常量的常指针
指针常量:
int * const p = &a;
指针指向不能变,但指针指向的内存中的值可以变。
常量指针:
const int *p = &a;
指针的只想可以变,但指针指向的值不可以变。
指向常量的常指针
const int * const p = &a;
指针的指向和指向的值都不能改变。
3.4.4 作用域
代码块作用域:
就是用花括号进行分割的代码块。
文件作用域:
任何在代码块之外声明的标识符具有文件作用域。
可用范围:从该标识符被声明的位置直到该文件的结尾。
函数作用域:
只适用于语句标 签,语句标签用于goto语句。基本上,函数作用域可以简化为一条规则——一个函 数中的所有语句标签必须唯一。
原型作用域:
只适用于在函数原形中声明的参数名。
EG:
int func_1(int a, int b);
int func_2(int c){
printf("%d\n", c);
}
其中,a、b是原型作用域,而c是块级作用域。
3.5 链接属性
引入:
加入现在有一个项目包含多个源文件和头文件,现在分别对他们进行编译得到各自对应的目标文件,然后需要将它们链接在一起,问题来了:加入多个源文件中含有同样名字的标识符,链接后的可执行程序应该用哪个?
分类:
- 外部(external):无论生命多少次、位于几个源文件都表示同一个实体。
- 内部(internal):在同一个源文件内的声明指向同一个实体,位于不同源文件的多个声明分属于不同实体。
- 无(none):被当作单独的个体。
3.5.1 external
并非声明与代码块内的变量,缺省状态下 具有external链接属性(需要注意:代码块内声明的函数也具有external的链接属性)
EG:
#include <stdio.h>
typedef char *a;
int b;
int c ( int d ){
int e;
int f ( int g );
external int h; // 注意:这里不能对其进行初始化
}
其中的b、c、f、h具有外部链接属性。
注意:
external只用于源文件中一个标识符的第1次声明时,它指定该标识符具 有external链接属性。但是,如果它用于该标识符的第2次或以后的声明时,它并 不会更改由第1次声明所指定的链接属性。
EG:
#include <stdio.h> static int b; int c ( int d ){ int e; int f ( int g ); external int b; }
此时b依旧是internal属性。也就是说,external不能用来修改连接属性,只能用来初始化连接属性。
3.5.2 internal
通过使用static对缺省状态下为external链接属性的标识符进行限制,使其变成internal链接,其余文件不能访问它。
注意:
不要对链接属性为none的标识使用static,因为这样修改的是存储类型而非链接属性。
EG:
-
代码:
#include <stdio.h> typedef char *a; static int b; static int c ( int d ){ int e; static int f ( int g ); external int h; // 注意:这里不能对其进行初始化 }
-
这样的话,b、c、f为internal链接属性。
3.5.3 none
函数形参和代码块内声明的参数在缺省情况下具有none链接属性。
EG:
-
代码:
#include <stdio.h> typedef char *a; int b; int c ( int d ){ int e; int f ( int g ); external int h; // 注意:这里不能对其进行初始化 }
-
其中的d、e为none。
3.5.4 Tips
-
链接属性不会改变作用域,因此不能跨文件使用。
-
链接不是include,不能直接用"Hello.c"来链接不同源文件,否则会得出不同的结论。链接方式多种多样,可以使用命令:
gcc main.c hello.c
-
如果多个源文件使用同一个external标识符,那么其中只能有一个对其进行了声明以及初始化,其余的只进行声明。如果有复数的文件对其进行初始化,编译器将会报错。原因如下:
- 声明:链接属性不改变作用域,因此如果要使用另一个文件内定义的标识符,必须先声明,否则编译生成目标文件这一步无法通过。
- 单个源文件初始化:多个源文件对同一具有external链接标签进行初始化将会导致编译器在链接阶段无法完成定义,无法生成可执行文件。
-
能够初始化的具有external的标识符,必须是静态变量,且具有文件作用域。
- 静态变量:因为局部变量直到程序运行到该代码块时才会初始化,在堆栈内分配内存,而external链接是在链接阶段发生作用,为了保证链接时变量的一致性,具有external链接的变量必须在链接之前,也就是编译阶段就完成初始化和内存空间的确定。戏外。
- 文件作用域:如果是给代码块内的变量加上static,仅改变存储类型而不改变作用域,该表示符在链接阶段无法访问,因此编译器会在编译阶段报错。
EG:
-
代码:
-
main.c:
#include <stdio.h> #include <stdlib.h> int func_1(); // 这里只声明,不初始化,因为要用到demo1.c里面的函数,因此要避免重复定义,否则编译阶段将会报错。 int main_a; int main(){ extern int main_b; // 不能再这里对其进行初始化 printf("main_a=%d\n", main_a); func_1(); printf("main_a=%d\n", main_a); printf("main_b=%d\n", main_b); system("pause"); return 0; }
-
demo1.c:
#include <stdio.h> #include <stdlib.h> int main_a =10; int main_b = 20; int func_1(){ printf("这里是demo1.c\n"); system("pause"); }
-
输出:
main_a=10 这里是demo1.c 请按任意键继续. . . main_a=10 main_b=20 请按任意键继续. . .
-
3.6 存储类型
3.6.1 概述
存储地点的分类如下:
- 普通内存
- 运行时堆栈
- 硬件寄存器:访问最快,内存最小
作用:
不同的存储地址决定了变量的创建时间、销毁时间、保存总时长。
3.6.2 分类
根据有无关键词,可划分为三种变量:
-
缺省的变量:存储类型取决于其声明的位置
- 位于代码块之外:属于静态变量,存放在静态内存中(不属于堆栈)。
- 在程序运行前被创建了,此时地址已经确定,在程序终止前不会改动。
- 在程序执行期间始终存在。
- 对于静态变量,如不显示的指定其初始值,将被初始化为0。
- 位于代码块之内:自动变量,存储在堆栈中。可以用auto来修饰,但很少使用,因为不修饰效果一致。
- 当程序执行到声明该变量的代码块时,才被创建。
- 当程序离开该代码块时,变量被销毁。
- 代码块多次执行,那么变量将会被多次创建、销毁,且每次占有的地址和上一次可能相同,也可能不同。
- 因为每一次地址都可能不一致,因此自动变量并不具有缺省的初始值。
- 如果被显示的初始化,那么将在代码块的初始处插入一条隐式的赋值语句。
- 位于代码块之外:属于静态变量,存放在静态内存中(不属于堆栈)。
-
static修饰的代码块内的变量:也属于静态变量,存放在静态内存中。需要注意的是:
- static仅仅在修饰代码块内的变量时能改变变量的存储类型,当修饰代码块外的变量时,修改的是链接属性。
- static不能改变作用域。
-
register修饰的自动变量:属于寄存器变量,被存储在机器的硬件寄存器中而不是内存中。
- 访问起来效率更高。
- 如何处理寄存器变量取决于编译器和寄存器的数量。
- 创建与销毁的时间与自动变量相同。
- 保证寄存器使用前后的值不变。当代码块开始执行时,把需要使用的寄存器的内容存放到堆栈中,代码块结束时,再将这些复制会寄存器中。
3.6.3 初始化
静态变量初始化:
- 在程序运行前被创建了,此时地址已经确定,在程序终止前不会改动。
- 对于静态变量,如不显示的指定其初始值,将被初始化为0。
自动变量初始化:
- 需要更多的开销,因为当程序链接时还无法判断自动变量的存储位置。
- 每次占有的地址和上一次可能相同,也可能不同。
- 如果被显示的初始化,那么将在代码块的初始处插入一条隐式的赋值语句。这导致了四个后果:
- 声明变量的同时进行初始化和先声明后赋值只有风 格之差,并无效率之别。
- 这条隐式的赋值语句使自动变量在程序执行到它们 所声明的函数(或代码块)时,每次都将重新初始化。这个行为与静态变量大不相 同,后者只是在程序开始执行前初始化一次。
- 由于初始化在 运行时执行,你可以用任何表达式作为初始化值。
- 除非对自动变量进行显式的初始化,否则当自动变量创建 时,它们的值总是垃圾。
3.7 static
- 修饰代码块外的变量、函数时,修改的是链接属性,将external修改为internal。但不改变存储类型和作用域。
- 修饰代码块内的变量时,修改变量的存储类型,将其从自动变量修改为静态变量。但不改变链接属性和作用域。
3.8 作用域、存储类型、链接属性范例
3.9 总结
变量类型 | 声明的变量 | 是否存于堆栈 | 作用域 | 如果声明为static |
---|---|---|---|---|
全局 | 所有代码块之外 | 否,存在于静态内存 | 从声明处到整个文件尾 | 不允许从其他源文件访问,内部链接 |
局部 | 代码块起始处 | 是 | 整个代码块 | 变量不存在于堆栈,其值在程序的整个执行期一直保持,作用域不变 |
形参 | 函数头部 | 是 | 整个函数 | 不允许添加 |
四. 语句
4.1 空语句
int a = 10;
;
int b = 20;
其中的第二行就是空语句,没有实际效果,主要用途是排版。
4.2 表达式语句
C中不存在专门的赋值语法,C中的赋值就是一种类似于加法和减法的操作,所以赋值是在表达式内进行的。
因此:
int x = 0;
int y = 0;
x = y + 3;
y+3;
最后两条语句都是合法的。
4.3 if语句
语法:
if ( expression ){
statement;
}
else{
statement;
}
EG:
if ( x > 3 )
printf("1\n");
else
printf("1\n");
- 当statement只有一条指令时,花括号可以省略。
- 当没有花括号包裹时,else字句从属于离他最近的不完整的if语句。
if ( x > 3 ){
x++;
printf("1\n");
}
else {
x--;
printf("1\n");
}
布尔
在C中不存在布尔类型,而是使用整数0、1代替,0为假,1为真。
多分支条件语句:
int a = 0;
printf("Enter number a\n");
scanf("%d", &a);
if ( a < 10 )
{
printf("a<10\n");
}
else if ( a > 10 )
{
printf("a>10\n");
}
else{
printf("a==10\n");
}
4.4 while语句
语法:
while ( expression ){
statement;
}
- break:结束当前循环。
- continue:结束这一轮循环
对于空的循环体,可以插入空语句,用于排版:
while ( ( ch == getchat() ) != EOF && ch != '\n' )
; //只有一条语句时花括号可省略
4.5 for语句
语法
for ( expression1; expression2; expression3 ){
statement;
}
其中:三个表达式都是可选的,都可以省 略。如果省略条件部分,表示测试的值始终为真
- expression1为初始化语句,只在循环开始时执行一次。
- expression2为条件部分,每次循环前都要执行一次判断。
- expression3为调整部分,它在循环体每次执 行完毕,在条件部分即将执行之前执行。
EG:
for ( i = 0; i < MAX_SIZE; i += 1 ){
arr[i] = i;
if ( i == 10 ){
break;
}
}
- 在statement中如果只有一条指令,可以略大括号。
break和continue:
for循环中也可以使用这两个关键字,效果和while循环中基本一致。
4.6 do语句
语法:
do{
statement;
} while( expression )
如果statement仅有一条语句,那么可以将大括号忽略。
EG:
int i = 0;
do{
arr[i] = i;
i += 1;
} while( i < MAX_SIZE )
break和continue:
依旧适用。
4.7 switch
语法:
switch ( expression ){
case constant-expression-1:
statement_1;
break;
case constant-expression-2:
statement_2;
break;
....
case constant-expression-n:
statement_n;
break;
default:
statement;
break;
}
其中:
- expression:必须是整型,也就是字符或整数,不能是其他数据类型。
- constant-expression-n:在switch中必须唯一。
- break:执行到break则直接跳到switch的末尾。
- default:如果所有的case都不符合,那么将执行default内的statement。
EG:
switch( command ){
case 'A':
add_entry();
break;
case 'D':
delete_entry();
break;
case 'P':
print_entry();
break;
case 'E':
edit_entry();
break;
default:
printf("没有符合的值\n");
break;
}
多条件语句
-
需求:读取两个整数a、b,若:
- a=10, b = 20,输出1。
- a!=10, b = 20,输出2。
- a=10, b =! 20,输出3。
- a!=10, b != 20,输出4。
-
代码如下:
#include <stdio.h> #include <stdlib.h> void switch_func(); int main(){ switch_func(); return 0; } void switch_func(){ int a = 0; int b = 0; printf("Enter number a and b\n"); scanf("%d", &a); scanf("%d", &b); printf("a=%d, b=%d\n", a, b); // 可以用多条件语句,也可以使用case和if结合的方法实现 switch (a) { case 10: if (b == 20){ printf("1\n"); } else{ printf("3\n"); } break; default: if (b == 20){ printf("2\n"); } else{ printf("4\n"); } break; } system("pause"); }
4.8 goto语句
注意:
该语句往往会导致程序可读性变得极差,不要轻易使用。
语法:
goto 语句标签 ;
语句标签的定义:标识符后面加个冒号。
EG:
goto quit;
quit : ;
使用场景:
大多数场景使用它都是极度不推荐,但有一种情况除外——跳出多层嵌套的循环:因为break仅能跳出使用它的那一层循环,而无法跳出外层循环。
EG:
while ( condition1 ){
while ( condition2 ){
while ( condition3 )
if ( some disater ){
goto quit;
}
}
}
quit : ;
当然,这种情况也可以使用其它方法解决,从而避免goto的使用:
-
设置一个状态值,并在所有的循环内判断该状态值,如果需要跳出嵌套的循环,只需改变状态值即可。
enum Status { EXIT, OK }; Status status = OK; while ( status = OK && condition1 ){ while ( status = OK && condition2 ){ while ( condition3 ) if ( some disater ){ status = EXIT; break; } } }
但这会导致逻辑变得比较麻烦。
-
把所有的循环放在一个单独的函数,这样我们可以直接通过return语句来结束嵌套的循环。
五. 操作符和表达式
5.1 操作符
5.1.1 基本操作符
+、-、*、/、%
5.1.2 移位操作符
只对整数有效。
-
左移:<<
- 作用:将数的二进制形式向左移动相应的位数,右边多出来的用0来补位:
- EG:01101101<<3,得到:011101000
-
右移操作符:>>
- 作用:将数的二进制形式向左移动相应的位数,左边多出来的位置补位分为两种情况:
- 逻辑移位:用0填充
- 算数移位:由原先的符号位决定,符号位为1,则用1填充;为0,则用0填充。
- 作用:将数的二进制形式向左移动相应的位数,左边多出来的位置补位分为两种情况:
-
右移位方式的判定:
- 对于无符号值,无论左移还是右移,均为逻辑移位。
- 对于有符号值,则取决于编译器。
- EG:对10010110进行右移两位:
- 逻辑移位:00100101
- 算数移位:11100101
-
对于负数移位、移位数量超过操作数的个数,如a<<-5、b>>100:
这种类移位是未定义的,由编译器决定,应尽量避免使用,这会导致程序不可移植。
-
EG:计算一个整数的二进制中1的个数(这里使用无符号数,是为了避免有符号数右移带来的歧义):
-
代码:
#include <stdio.h> #include <stdlib.h> // 获取传输整数的位中的1的个数 int get_one_numbers(unsigned char a); int main(){ unsigned char a; printf("请输入数据:\n"); scanf("%c", &a); printf("您输入数据整数值为:%d\n", a); int ones = get_one_numbers(a); printf("%c的二进制状态下,值为1的位有%d个\n", a, ones); return 0; } int get_one_numbers(unsigned char a){ int ones = 0; // 通过移位,每次判断最后一位是不是1,累加。因为如果二进制状态下最后一位是1,那么它一定无法被2整除 for ( ones = 0; a != 0; a = a >> 1){ if ( a%2 != 0 ){ ones++; } } return ones; }
-
输入:a
-
输出:
请输入数据: a 您输入数据整数值为:97 a的二进制状态下,值为1的位有3个
-
5.1.3 按位操作符
只对整数有效,二进制状态下逐位进行操作。
三大位操作符:
- &:与操作符
- |:或操作符
- ^:亦或操作符。
EG:
a = 00101110;
b = 01011011;
- a&b:00001010
- a|b:01111111
- a^b:01110101
5.1.4 复合操作符
+= -= *= /= %=
<<= >>= &= ^= |=
那么,可以对5.1.2中的核心代码进行改写:
int get_one_numbers(unsigned char a){
int ones = 0;
// 通过移位,每次判断最后一位是不是1,累加。因为如果二进制状态下最后一位是1,那么它一定无法被2整除
for ( ones = 0; a != 0; a >>= 1){
if ( ( a & 1 ) != 0 ){ // 只有当a的最后一位为1时,a&1才不为0。
ones++;
}
}
return ones;
}
5.1.5 单目操作符
! ++ - & sizeof
~ -- + * (类型)
-
~:对原数据求补位,即:各位1变0,0变1。
-
&:产生其操作数的地址。
-
*:间接访问符,与指针一起使用,用于访问指针所指向的值。
-
sizeof:获取操作数的类型长度,以字节为单位。返回类型为无符号整型。
-
int a = 10; sizeof(int); sizeof(a); sizeof a;
-
对于数组来说,它返回数组的长度,单位是字节。
-
5.1.6 关系操作符
> >= < <= != ==
5.1.7 逻辑操作符
&& ||
这两个操作符的关键在于其运算方式:短路运算
- &&:对于a&&b的操作,如果已经确认了a是假,那么我们不再计算b,因为表达式整体已经确认为假。
- ||:对于a||b,如果已经确定了a为真,那么我们不再计算b,因为表达式整体已经确认为真。
5.1.8 条件操作符
语法:
expression1 ? expression2 : expression3;
效果:如果expression1为真,那么整个表达式返回expression2,否则返回expression3。
EG:
a = b > 5 ? 10 : 20;
5.1.9 逗号操作符
语法:
expression1, expression2, ...... , expressionN;
效果:逗号操作符,将两个或多个表达式分隔开,并将这些表达式逐个自左向右进行求值,整个表达式的值是最后一个expression的值。
5.1.10 下标引用、函数调用和结构成员
下标引用:
C中的下标引用用于在一段空间中取出其中一段控件承载的值,典型案例便是数组。对于数组来说,其取值可以用两种方式:
arr[2];
*(arr + 2)
函数调用
简单得函数使用,没啥好说的。
结构成员
如果使用结构变量,那么用.
操作符访问结构成员。
如果使用一个指向结构的指针,那么用->
操作符访问结构成员。
5.2 布尔值
在C中使用整数0来表示假,1来表示真。
最佳实践:因此建议在项目开头使用define进行宏定义。
5.3 左值和右值
左值:
左值是被赋值的那一个,因此其关键在于程序运行到此处时,左值的空间位置对计算机来说是固定且已知的。因此字符常量不能作为左值,因为其在静态内存中的具体位置虽然确定,但却未知。
EG:下面的表达式就是错误的,因为程序不知道b+30的具体位置。
b + 30 = a;
5.4 表达式求值
5.4.1 算数转换
隐式类型转换
- 优先级(从高到低):
- long double
- double
- float
- unsigned long int
- long int
- unsigned int
- int
显式类型转换
注意高转低时的数据截取。
5.4.2 操作符
优先级
六. 指针
6.1 内存和指针
记住一点:名字和内存位置之间的关系由编译器提供,而非硬件。
指针必须进行初始化后才能被访问,也就是必须为其分配具体的空间才能被使用,这是因为指针是动态地,编译器无法为其分配初始值,如果未经初始化就使用,将会导致种种难以预测的意外。
6.2 值和类型
对于不同声明类型的变量,虽然占用内存大小可能不同,但它们在内存中的表达形式却无不同,最终的差异取决于编译器对其的翻译方式。
EG:对于下面这个32位的二进制数据,
01100111011011000110111101100010
根据不同的编译器和数据指令,可以有多种解释:
类型 | 值 |
---|---|
1个32位整数 | 1735159650 |
2个16位整数 | 26476和28514 |
4个字符 | glob |
浮点数 | 1.116533×10 24 |
机器指令 | beg .+110和ble .+102 |
因此,声明很关键。
6.3 NULL指针
含义:
表示该指针不指向任何东西,因此也无法解引用。
初始化:
可以使用0/NULL进行初始化,可以使用0是因为这是一种源代码规范,编译器可对其进行翻译,在机器中其存储值未必是0。
EG:
-
代码:
int *p = NULL; if ( p == 0 ) { printf("指针为NULL\n"); }
-
输出:
指针为NULL
6.4 访问指定地址
假设变量a的地址为100,如何通过100这个地址而不是对a进行&操作对其进行赋值:
一种典型的错误:
*100 = 25;
这句话是错误的,因为间接访问操作符只能访问只能作用于指针类型表达式,因此我们需要对100进行类型转换:
*(int *)100 = 25;
通过这个表达式,为[100,104)字节赋值25。
实际上,我们基本很少用到直接用地址进行变量访问,因为变量的地址对我们是未知的。这种操作往往用于硬件的读取。
6.5 多重指针
定义:
多重指针:顾名思义,就是指向指针的指针。二级指针指向一级指针,三级指针指向二级指针……
EG:
void multiple_ptr(){
int a = 10;
int *b = &a;
int **c = &b;
printf("a=%d\n", a);
printf("&a=%d\n", &a);
printf("b=%d\n", b);
printf("&b=%d\n", &b);
printf("*b=%d\n", *b);
printf("c=%d\n", c);
printf("&c=%d\n", &c);
printf("*c=%d\n", *c);
}
输出:
a=10
&a=6421996
b=6421996
&b=6421984
*b=10
c=6421984
&c=6421976
*c=6421996
由此看出:n重指针里面装的是n-1重指针的地址,通过解引用n重指针,可以获取n-1重指针里面装的地址。
6.6 指针间的运算
6.6.1 算术运算
指针+/-整数
指针和整数的加法/减法运算的核心在于类型相关:
- 对于char类型的指针,+1表示向后移动1个字节。
- 对于int类型的指针,+1表示向后移动4个字节。
EG:
-
代码:
void add_ptr(){ int a = 10; int *p = &a; printf("加一前p=%x\n", p); p++; printf("加一后p=%x\n", p); }
-
输出:
加一前p=61fde4 加一后p=61fde8
二者插值是4个字节,由此可见指针的加法与指针的类型自适应变化。
指针-指针
注意:
- 指针之间的减法运算只适用于指向同一数组的指针,否则将产生未知的结果,这与编译器有关,且十分危险。
- 减法产生的结果也与类型相适应,不是以字节为单位,而是以该类型所占字节数为单位,简单来说,就是元素在数组中的坐标之差。
EG:
-
代码:
void subtraction_ptr(){ char *str = "abc123"; char *p1 = &str[0]; char *p2 = &str[4]; printf("指向同一char数组中不同位置的两指针之间的差值为:%d\n", p2 - p1 ); int arr[5] = {10, 20, 30, 40, 50}; int *a1 = &arr[0]; int *a2 = &arr[4]; printf("指向同一int数组中不同位置的两指针之间的差值为:%d\n", a2 - a1 ); }
-
输出:
指向同一char数组中不同位置的两指针之间的差值为:4 指向同一int数组中不同位置的两指针之间的差值为:4
-
结论:由此可见,输出的是二者在数组中的坐标差。
6.6.2 关系运算
>, >=, <, <=
注意:以上四个操作符用于指向同一数组中的元素的指针之间的比较,它们比较的是指针指向的元素在数组中的位置,如果这样的指针进行比较,那么结果未知。
EG:
-
代码:
void logical_ptr(){ int i; int length = 5; int *arr = (int *)malloc( sizeof( int ) * length ); for ( i = 0; i < length; i++ ){ arr[i] = i; } int *p1 = &arr[0]; int *p2 = &arr[3]; if ( p1 <= p2 ) { printf("p1<=p2\n"); } free(arr); }
-
输出:
p1 <= p2
==
注意:该运算是用来比较两个指针是否指向同一方向,因此可以在任意两个指针之间进行比较。
EG:
-
int *p3 = NULL; int a = 10; int *p4 = &a; if ( p3 == p4) { printf("1\n"); }
-
无输出。
6.7 指针常量和常量指针
三大原则:
- 指针和const谁在前面先读谁。
- *意味着地址,而const意味内容。
- 谁在前面谁不能改。
指针常量:
声明:
int * const p = &a;
特征:指针指向的方向不能变,指针指向的值可以变。
常量指针:
声明:
const int * p = &a;
特征:指针的指向可以变,但指针指向的值不可以变。
指向常量的指针:
声明:
const int * const p = &a;
特征:指针的指向和指针指向的值不可以变。
七. 函数
7.1 原型
就是函数的声明,可以在本文件,也可以在其它头文件声明后引入,用来帮助编译器认清函数的名字、返回值以及参数。
如果在函数调用前编译器无法看到函数的原型,那么将会默认该函数返回一个整型值。
7.2 递归
使用的条件:
使用递归要有两大前提:
- 递归存在限制条件,当满足这个条件是递归停止。
- 每次递归都会更加接近该限制条件。
特性:
在递归期间,产生的变量将会被存储在堆栈中,且每一轮递归产生的变量会覆盖上一轮递归产生的变量(但被覆盖的变量依旧存在,而不是消失了),最终形成一个类似于栈的数据结构,当达到递归条件后,开始一层层的弹出之前的变量,遵循先进后出原则。
优缺点:
- 优点: 代码清晰,逻辑简答。
- 缺点:对堆栈的压力较大,内存需求高,运行效率低。
EG
将10进制无符号数转换成二进制输出。
void decimal_to_binary( unsigned int value ){
unsigned int quotient;
quotient = value / 2;
if ( quotient!=1 )
{
decimal_to_binary( quotient );
}
putchar( '0' + value % 2 );
}
7.3 迭代
迭代实际上就是利用循环来完成递归的工作,在一些问题上这会大大提高效率,但同时也会导致代码逻辑复杂程度增大。
7.4 可变参数列表
需求
有时候我们需要的函数的参数数量、类型不固定,也就是可变参数列表,这时候访问它们就需要一些手段。
可以使用<stdarg.h>库,这个库里面定义了一个数据类型和三个宏,内容如下:
- 数据类型
va_list
:用来装未知的参数列表。 - 宏
va_start
:对参数列表进行初始化,需要参数列表变量和列表里面的变量的个数。 va_arg
:逐个访问参数列表中的参数,需要声明该参数的类型。va_end
:当访问完最后一个可变参数时,使用va_end结束参数列表。
EG:
对n个参数求平均数。
// n_values 是后面省略的参数列表的参数个数
float average( int n_values, ... ){
va_list vars; // 声明未知的参数列表,该数据类型由<stdarg.h>定义
int count; // 用于for循环
float sum = 0; // 最终的返回值
va_start( vars, n_values); //对参数列表初始化
// for循环访问参数列表
for ( count = 0; count < n_values; count++ ){
sum += va_arg( vars, int ); // 访问参数列表位置部分,需要va_list变量和参数列表中下一个参数的类型
}
va_end( vars );
return sum / n_values;
}
八. 数组
8.1 一维数组
8.1.1 数组名
数组名在大多数情况下是一个指针常量,指向该数组在内存中的起始地址。其指向的地址不能变,但指向的地址中存储的值可以变。
在以下两种情况下,数组名并不以指针常量的形式出现:
- 使用sizeof求长度时,求得的是整个数组的长度,而不是指针的长度。
- 使用&单目操作符,获取的是数组的起始地址,而不是该数组名变量的存储地址。
EG:
-
代码:
void array_name(){ int a = 10; int arr[10]; // 大多数情况下,数组名被视作指针常量 printf( "&arr[0]=%x\n", &arr[0] ); printf( "arr=%x\n", arr ); // arr = &a; 这是违法的表述,指针常量不能修改指针的指向 // 以下两种情况,不将数组名视作指针常量 printf( "&arr=%x\n", &arr ); // 此时获取的是数组的起始地址 printf( "sizeof(arr)=%d\n", sizeof(arr) ); // 获得的是数组的长度而不是arr的长度 return; }
-
输出:
&arr[0]=61fdc0 arr=61fdc0 &arr=61fdc0 sizeof(arr)=40
8.1.2 下标引用与间接访问
注意:C中的下标引用与间接访问表达式是一样的,二者的区别体现在效率上。
EG:
-
代码:
void subscript_and_indirect(){ int a = 10; int *p1 = &a; printf( "p1[0] = %d\n", p1[0] ); printf( "p1 = %x\n", p1); printf( "p1 + 1 = %x\n", p1 + 1 ); int arr[5] = { 0, 1, 2, 3, 4}; int *p2 = &arr[2]; printf( "*p2 = %d\n", *p2 ); printf( "*( p2 + 7 ) = %d\n ", *( p2 + 7) ); // 此时数组越界,但不报错,因为C的编译器往往不会对数组的下标有效性进行检查。 printf( "p2[0] = %d\n", p2[0]); printf( "p2[7] = %d\n", p2[7] ); }
-
输出:
p1[0] = 10 p1 = 61fddc p1 + 1 = 61fde0 *p2 = 2 *( p2 + 7 ) = 0 p2[0] = 2 p2[7] = 0
-
结论:
- 对于指针而言,也可以使用下标来进行访问,对于数组而言,也可以使用间接访问表达式来访问数组。
- 指针与数组间的灵活关系导致大多数编译器并不会检查数组下标的合法性,运行时往往也不会报错,因此需要格外注意。
8.1.3 指针与下标的效率
当两者使用都正确时,下标绝不会比指针更有效率,而指针有时比下标更有效率。
EG:遍历数组,将其归0。
-
使用下标:
int i; int array[10]; // 下标版本 for ( i = 0; i < 10; i++ ){ array[i] = 0; }
在这份代码中,每一轮循环都要进行一次额外乘法运算:*(array + i * 4) = 0;这要花费一定时间和空间。
-
使用指针:
int *p; int array[10]; //指针版本 for ( p = array; p < array + 10; p++ ) { *p = 0; }
在本代码块中,也存在乘法运算,但这仅在for的括号中,且每次都是1*4=4,也就是固定的;因此,该乘法仅仅在编译时运行一次,而在运行时无需运行,这就得以提高了效率。
8.1.4 指针的效率
结论如下:
- 当按照固定步长在数组中移动时,指针比下标更有效率。移动次数越多,效率提升越明显。(案例可见8.1.3代码片段)。
- 寄存器变量的指针往往比位于静态内存和堆栈中的指针效率更高。
- 一些只有在运行时才能求值的表达式,入&array[SIZE]或array+SIZEZ这样的表达式往往代价更高。
- 判断循环是否该终止时,如果可以不使用计数器(for循环中的i),那就不要用。
8.1.5 指针与数组
指针和数组虽然有很多相似之处,但他们是不同的:
- 数组在声明时会由编译器分配整个数组需要的内存,数组名指向数组的首位。
- 指针在声明时,编译器只为指针本身分配空间。
EG:
-
代码
int a[5]; int *b;
-
分配的空间:
8.1.6 函数的数组参数
两种形式:
-
int strlen ( char *string );
-
int strlrn (char string[] );
需要注意的是,以上两种形式只是写法不同,本质上都是将数组的首位地址当作参数传递给函数,这也导致了数组长度信息的丢失,想要获取数组长度,只有以参数的形式显式传递。
8.1.7 数组的初始化:
-
完整初始化:
int arr[] = { 0, 1, 2, 3 }; int array[5] = { 0, 1, 2, 3, 4 };
-
不完整初始化:
int array[5] = { 0, 1, 2, 3 };
第五个数字默认初始化为0。
-
静态和自动初始化:
-
静态初始化:静态数组的初始化在链接阶段完成,若没有进行初始化,则默认为0。
-
自动初始化:如果是自动变量的数组,由于程序在执行到该代码块前,自动变量的内存位置不确定,因此无法为其进行默认的初始化。
注意:数组的每一次初始化都需要编译器产生多条隐式的赋值语句,这回占据一定的时间和空间。而自动变量每次进入该代码段都需要初始化,静态变量则只需要初始化一次即可,因此要权衡利弊,想好使用哪一种变量。
-
-
字符串变量初始化:
-
int str[] = "Hello!";
-
int *str = "Hello!";
这一种声明方式得到的是一个常量值指针,指向的是一个字符串常量,也就是"Hello!"里面的值不能变。
-
8.2 多维数组
8.2.1 概述
n维数组的指针含义(以整形为例):
- 一维数组:指向整型变量的指针常量。
- 二维数组:指向一维整形数组的指针常量。
- ……
- N维数组:指向N-1维数组的指针常量。
EG:二维数组的排序和访问:
-
代码:
/* 二维数组 */ void two_dimension(){ int arr[3][4] = { { 0, 1, 2, 3 }, { 4, 5, 6, 7 }, { 8, 9, 10, 11 } }; printf( "arr = %x\n", arr ); // 指向整型数组的指针常量,单位为4个整型,也就是16个字节 printf( "arr[0] = %x\n", arr ); // 指向整型变量的指针常量,虽然值与arr相同,但单位不同,为1个整型 printf( "arr + 1 = %x\n", arr + 1 ); printf( "arr[0] + 1 = %x\n", arr[0] + 1 ); printf( "arr[0][0] = %x\n", arr[0][0] ); printf( "sizeof( arr ) = %d\n", sizeof( arr ) ); printf( "sizeof( arr[0] ) = %d\n", sizeof( arr[0] ) ); return ; }
-
输出:
arr = 61fdc0 arr[0] = 61fdc0 arr + 1 = 61fdd0 arr[0] + 1 = 61fdc4 arr[0][0] = 0 sizeof( arr ) = 48 sizeof( arr[0] ) = 16
行与列
在一些编译器中,前一个和后一个谁是行、谁是列是一样的,因为它们在内存中实际上是一串连续的数据,并不是分成一行一列,只是C为了让我们使用更加方便,才有了行、列之分。
这同时引出了另一个问题:在声明多维数组时,是否可以忽略掉所有的维度数量,也就是:
int arr[][][];
答案是不可以全部省略,只能省略第一个。这里以上面的arr为例说明:arr是一个三维数组,同时也是一个指向二维整形数组的指针,如果我们要对arr进行运算,如arr+1,那么我们必须知道arr的单位长度,也就是二维整型数组的长度,因此后两个必须确认;N维数组同理。
8.2.2 指针与数组
指向一维数组的指针:
int arr[10];
int *p = arr;
指向多维指针的数组
-
正确的方式:
int matrix[3][10]; int (*p)[10] = matrix; // 下标访问的优先级高于间接访问,因此要加上括号
说明p是一个指向长度为10的一维整型数组的指针。
-
一个典型的错误案例:
int matrix[3][10]; int *p = matrix;
其错误在于类型不同,p是一个指向整型变量的指针,而matrix是一个指向整型数组的指针常量,二者类型不一致。
-
另一个错误案例:
int matrix[3][10]; int (*p)[] = matrix;
必须要指明指针指向的数组的长度,否则内存无法分配。
8.2.3 作为函数参数的多维数组
两种形式:
-
void func ( int (*mat)[10] );
-
void func ( int mat[][10] );
编译器必须知道第2个及以后各维的长度才能对各下标进行求值,因此在原型中必须声明这些维的长度.
8.3 指针数组
作用:用来装指针的数组。
声明:
-
int *api[5];
因为下标的优先级比间接访问符更高,所以先确认api是一个数组,其数据类型为int *,也就是整数指针。
EG:
// 指针数组:用来装指针的数组
char const *keyword[] = {
"do",
"for",
"if",
"register",
"return",
"switch",
"while"
};
九. 字符串、字符和字节
注意:以下使用的字符串相关函数均来自string.h头文件。
9.1 字符串
定义:所谓字符串,就是一串零个或多个字符,并且以一个位模式全为0的NUL
字节结尾。
NUL:在二进制状态下所有位均为0,占据一个字节。
-
代码:
int main(){ char str[] = "Hello"; printf( "str[5] = %d\n", str[5] ); printf( "sizeof ( str[5] ) = %d\n", sizeof ( str[5] ) ); return 0; }
-
输出:
str[5] = 0 sizeof ( str[5] ) = 1
9.1.1 字符串长度
原型:
size_t strlen( char const *string );
注意:
-
返回的
size_t
类型在stddef.h
中被定义,是一个无符号整数。无符号数和有符号数同时参与运算,将会产生意外的结果。因此二者混合运算时要显式的将无符号数转化为有符号数。void unsigned_and_signed(){ unsigned int a = 5; int b = 10; printf( "a - b = %d\n", a - b ); printf( " a - b >= 0 为 %d\n", a - b >= 0 ); }
输出:
a - b = -5 a - b >= 0 为 1
分析:a为无符号数,用其减有符号数,大于等于0恒成立。
-
最后求得的字符串长度不包含结尾的NUL。
9.1.2 不受长度限制的字符串函数
所谓不受长度限制,即这些字符串函数不显式接收字符串数组长度,而是自己通过NUL
来判断字符串是否结束。
字符串复制
函数原型:
char *strcpy( char *dst, char const *src );
作用:将src内的字符串复制到dst中。
返回值:返回dst的首位地址。
关键:
- dst和src的空间如果重合,将产生难以预知的情况。
- dst的长度必须>=src的长度,否则src将会覆盖dst后面内存的位数。
字符串连接
函数原型:
char *strcat( char *dst, char const *src );
作用:将src的字符串复制到dst的末尾。
返回值:返回dst的首位地址。
关键:
- dst和src的空间如果重合,将产生难以预知的情况。
- dst的长度必须>=src的长度,否则src将会覆盖dst后面内存的位数。
字符串比较
函数原型:
int strcmp( char const *s1, char const *s2 );
比较逻辑:
- 对两个字符串逐个比较,直到发现不匹配为止,其中字符小的那个字符串小。
- 如果一个字符串是另一个字符串的前缀,那么该字符串<=另一个字符串。
返回值:
- s1<s2:返回一个小于0的数。注意:是一个小于零的数,但值不固定。
- s1==s2:返回0。
- s1>s2:返回一个大于零的数。
9.1.3 长度受限的字符串函数
拷贝固定长度的字符串
函数原型:
char *strnpy( char *dst, char const *src, size_t len );
关键:
- 如果src长度小于len,那么额外部分用NUL填充。
- 如果strlen(drc)>=len,那么将只有len个字符被复制到dst。注意:它的结果不会以NUL字节结尾。
将固定长度的字符串连接到另一个字符串上
函数原型:
char *strcat( char *dst, char const *src, size_t len );
关键:
- 如果src长度小于len,也不会使用
NUL
对额外部分进行填充。 - strcat在连接后,固定在字符串结尾加上
NUL
。
9.1.4 字符串查找
查找一个字符串
函数原型:
char *strchr( char const *str, int ch);
char *strchrr( char const *str, int ch);
作用:
- strchr:ch是一个整数,也是需要查找的字符。查找字符ch第一次出现的位置。如果没有,则返回NULL。
- strchr:查找字符ch最后一次出现的位置。如果没有,则返回NULL。
查找字符串group中任意一个字符在str中文第一次出现的位置:
函数原型:
char *strpbrk( char const *str, char const *group);
作用:
- 查找group中任意一个字符在str中第一次出现的位置,并用指针返回其在str中的位置。如果没有,返回NULL。
EG:
-
代码:
char *p; p = strpbrk( "Hello, world!", "le" ); if ( *p != '\0' ) { printf( "%c\n", *p ); }else{ printf( "空\n" ); }
-
输出:
e
-
分析:输出e是因为l在str中下标为2,而e的下标为1,e在前面,因此返回e。
查找子串
函数原型:
char *strstr( char const *s1, char const *s2 );
作用:查找s2在s1中首次出现的位置,如果没有,则返回NULL。
9.1.5 高级字符串查找
查找一个字符串前缀
原型:
size_t strspn( char const *str, char const *group );
size_t strcspn( char cosnt *str, char const *group );
功能
strspn
:遍历str中的字符,令其依次与group中的任意字符比较,如果相等,那么跳到下一个,直到不相等或str遍历完毕结束,最终返回相等的字符数(从开头连续的)。strcspn
:与strspn
相反,返回的是不相等的字符数,逻辑与strspn一致。
EG:
-
strspn
:-
void prefix(){ int len1, len2; char buffer[] = "25,142,330,Beasts777"; len1 = strspn( buffer, "0123456789" ); len2 = strspn( buffer, ",0123456789" ); printf( "len1 = %d\n",len1 ); printf( "len2 = %d\n", len2 ); return ; }
-
输出:
len1 = 0 len2 = 11
-
分析:
- 对于len1,2、5都能在"0123456789"中找到匹配,但','不行,因此len1为2。
- len2中,",0123456789"能匹配buffer中前11个字符,直到'B'字符匹配停止,因此len2为11。
-
-
strcspn
:-
void prefix(){ int len1, len2; char buffer[] = "25,142,330,Beasts777"; len1 = strspn( buffer, "01" ); printf( "len1 = %d\n",len1 ); return ; }
-
输出:
len1 = 3
-
分析:
buffer前3位的字符均未在"01"中出现,因此累加,第四位为1,在"01"中,因此终止,得到3。
分割字符串
原型:
char *strtok( char *str, char const *sep );
作用:
- 将传入的字符或字符串中的每一个字符作为分隔符,对原有字符串中的分隔符使用
NUL
进行替换,返回第一段。
关键:
- 如果想要获取第二段、第三段等,就使用NULLstr,上一段的指针作为sep,返回结果即为指向下一段的指针。
EG:
-
void find_mark(){ char *p; char str[] = "12,abc,efg"; p = strtok( str, "," ); if ( p != NULL ){ printf( "%s\n", p ); } p = strtok ( NULL, "," ); if( p != NULL ){ printf( "%s\n", p ); } p = strtok ( NULL, "," ); if( p != NULL ){ printf( "%s\n", p ); } return ; }
-
输出:
12 abc efg
-
9.2 字符
库文件:ctype.h
两组函数:
- 字符分类
- 字符转换
9.2.1 字符分类
作用:接收一个包含字符值的整型参数,测试该字符的类型,返回真假。
函数 | 返回真的情况 |
---|---|
iscntrl | 任何控制字符 |
isspace | 空格' '、换行'\n'、换页'\f'、回车'\r'、制表符'\t'、垂直制表符'\v' |
isdigit | 十进制数字0-9 |
isxdigit | 十六进制数组 |
islower | 小写字母a-z |
isupper | 大写字母A-Z |
isalpha | 字母 |
isalnum | 字母或数字 |
ispunct | 标点符号,任何部署数字或字母的圆形字符(可打印符号) |
isgraph | 任何图形字符 |
isprint | 任何可打印字符,包括图形字符和空白字符 |
9.2.2 字符转换
函数原型:
int tolower( int ch );
int toupper( int ch );
作用:
- tolower:大写转小写。
- toupper:小写转大写。
- 如果参数不相适应,会返回原本的值。
注意:直接操纵字符将会降低程序的可移植性,因此建议使用字符函数进行操作。
9.3 内存操纵
字符串以NUL('\0',也就是位上全为0)结尾,这是因为字符中不存在该值,因此不会因为输入失误等原因导致字符串意外结束,但非字符串内部数据出现0值并不奇怪,因此无法用字符串数据操作它们,但我们可以以字节为单位对其进行操作。
十. 结构体
10.1 结构基础知识
聚合数据类型
是指能够存储超过一个的单独数据的数据类型。包括:
- 数组:
- 是同类型元素的集合。
- 因为类型固定,所以长度固定,可以通过下标访问。
- 可以在表达式中用指针替换。
- 结构体:
- 可以是多种类型元素的集合。
10.1.1 结构声明
原型:
struct tag { member-list } vatiable-list
EG:
-
没有名字的定义:
struct { int a; char b; float c; } x; struct { int a; char b; float c; } y[20], *z ;
创建了变量c、y[20]、*z。需要注意的是,在没有为结构体命名时,两个结构体即使内部一致,但编译器仍将其视作两个不同的数据类型。
-
有名字的结构体的定义:
struct Data{ int a; char b; float c; } data, y[20], *z; struct Data x;
结构体结合typedef(推荐):
typedef struct
{
int a;
char b;
float c;
} Simple;
Simple x;
Simple y[20], *z;
提示:如果想在多个源文件中使用同一类型的结构,使用typedef在一个头文件中声明结构,使用时引入即可。
10.1.2 结构成员访问
EG:
struct SIMPLE{
int a;
char b;
float c;
} data, b[20], *c;
struct COMPLEX
{
float f;
int a[20];
long *lp;
struct SIMPLE s;
struct SIMPLE sa[10];
struct SIMPLE *sp;
};
访问COPLEX内部成员:
/* 使用变量访问成员 */
struct COMPLEX comp;
comp.a[0] = 0;
comp.a + 1 = 1;
comp.s.a = 0;
comp.sa[0].a = 0;
/* 使用指针访问变量 */
struct COMPLEX *p;
(*p).a[0] = 0; // 注意:点操作符的优先级高于间接访问操作符
p->f = 1.0;
10.1.3 结构体自引用
结构体不能将自己作为成员变量,这是因为在对其分配内存时,会出现死循环的现象。因此下面这种写法是错误的
struct Data{
int a;
struct Data b;
int c;
};
但结构体可以用指向自己的指针作为自己的成员变量,这是因为指针本身的大小是固定的,因此在分配内存时不会出现无限死循环的现象。
struct Data{
int a;
struct Data *b;
int c;
};
当使用typedef时,必须定义一个结构标签才能进行自引用,否则自引用创建失败:
-
错误的:
typedef struct{ int a; struct Data *b; // 此处报错,因为Data在尾部定义,此时还无法识别 int c; }Data;
-
正确的写法:声明一个结构标签
typedef struct DATA_TAG{ int a; struct Data_TAG *b; // 此处报错,因为Data在尾部定义,此时还无法识别 int c; }DATA;
10.1.4 结构体间的相互引用
当多个结构体相互引用时。以A、B为例,A中引用了B,B中引用了A,那么如果先定义A,此时引用B失败,因为B还没有声明;先定义B同样的道理。为了解决这一问题,我们需要对结构体进行不完整声明:
struct B; // 先声明B,此时B的结构还未知
struct A{
struct B *p; // 在A中创建指向B的指针。
};
struct B{ // 再详细定义B
struct A *p;
};
10.1.5 结构体初始化
EG:
typedef struct{
int a;
char b;
float c;
} Simple;
struct INIT_EX{
int a;
short b[10];
Simple c;
};
struct INIT_EX x;
x = {
10,
{ 1, 2, 3 , 4, 5 },
{ 25, 'x', 1.9 }
};
如果初始化时的值不够,那么将对剩下的值按缺省值进行初始化处理。
10.2 结构的存储分配
结构体内部成员内存分配原则如下:
- 结构体变量的首地址是其最长基本类型的整数倍。
- 结构体每个成员相对于结构体首地址的偏移量(offset)都是该成员变量大小的整数倍,如果不是,编译器将在成员之间填充字节。
- 结构体总大小为结构体最宽基本成员大小的整数倍,如有需要,编译器将在最后一个成员上加上首字节。
- 结构内部类型相同的连续元素将在连续的空间内,和数组一样。
- 如果结构体内部存在长度大于处理器位数(本电脑为64位,8个字节)的基本类型,那么就以处理器的倍数为处理单位;否则,如果结构体内的基本类型长度都小于处理器的倍数时,便以结构体里面最长的数据元素类型长度为对齐单位。就是二者谁小谁当对齐单位。
EG:
-
代码:
typedef struct DATA { char a[3]; char b[20]; char c; double d; char e; } Data; Data data = { 'a', 'a', 3, 4, 'a' }; printf( "结构体的占用的字节数为:%d\n", sizeof(data) ); printf( "%d, %d, %d, %d, %d\n", &data.a, &data.b, &data.c, &data.d, &data.e );
-
输出:
结构体的占用的字节数为:40 6421760, 6421763, 6421783, 6421784, 6421792
-
步骤:
- 本电脑位数为64位,结构体中最大的基本单位为double,长度为8,接下来以8为单位进行计算。
- a的地址为6421760,为8的整数倍。此时长度为3,不是8的整数倍,暂时在尾部填充5个字节,得到8字节的长度。
- b和a的类型一致,二者之间无需填充字节,因此忽略之前填充的字节,直接连接。但二者相加为23个字节,不是8的位数,因此暂时在末尾填充一个字节。
- c和b的类型一致,和b直接连接,此时a+b+c=24,是8的倍数,尾部无需填充。
- d长度为8,和c的类型不一致,但其长度为8,8+24依旧为24的倍数,因此直接连接。
- e和d的类型不一致,偏移量是1的倍数,直接连接,此时长度为33,不是8的倍数,因此在尾部补上7个字节,最终长度为40个字节。
相对偏移量:
函数原型:宏的原型定义于stddef.h
头文件中
size_t offsetof( typedef, member )
- typedef:结构体变量类型
- member:成员变量的名字
返回值:返回该成员变量相对于结构体首地址的相对偏移量。
EG:
int a = offsetof( struct DATA, b) // 得到 a= 3
优化:
结构体变量所占内存与变量的排列顺序有关。通过将对倍数限制最严格的变量移动到最前面,可以减少冗余的字节。
EG:
-
之前:
struct ALIGN{ char a; int b; char c; } ;
字节数量:
- 最长基础数据类型为int,以4为单位。
- a长度为1,不是4的单位,尾部补3。
- b长度为4,与a类型不一致。前面补的3保留,随后连接b。此时长度为8,是4的倍数,尾部无需补字节。
- c长度为1,与b类型不一致,前面没有补的字节,连接上。此时长度为9,补成4的倍数,为12。
-
优化后:
struct ALIGN{ int b; char a; char c; } ;
字节数量:
-
b长为4,不用补字节。
-
a长为1,与a不一致且前无补的字节,连上,此时为5。5暂时补为8。
-
c长为1,与a类型一致,消去a后面补的字节后连接,长度为6。补为8。
-
最终得到8。
-
指针:
因为存在补字节的情况,因此使用指针访问结构体数组往往并不理想。
10.3 作为函数参数
变量传递结构体:
只有在结构相当小时(长度和指针相同或更小),结构传递的效率才不输给指针传递方案。
指针传递结构体
如果使用一般的变量名来进行参数传递,最后返回参数名,这会导致结构体变量被复制了两次,这对于较为复杂的结构体来说无疑会消耗相当多的资源,因此建议使用指针。
EG:
-
一般的传参:传入时复制了一遍Data,返回时又复制了一遍。
Data test( Data data ){ data.c = 'a'; return data; }
-
使用指针进行传参:
void test( register Data * const data ){ data.c = 'a'; return data; }
10.4 位段
与结构体的不同:
- 位段的成员类型只能是
int
、unsigned int
、signed int
三种类型,且标明有无符号。 - 成员后跟一个冒号和整数,整数表示该位段所占位的数量。
EG:
-
代码:
struct CHAR{ unsigned a : 7; unsigned b : 6; unsigned c : 19; }; struct CHAR ch;
-
分析:
该位段各成员变量表示:
- a:可以处理128个不同的字符值。
- b:64种不同的字体。
- c: 0--\(2^{19}\)个单位的长度。
特点:如果比较注重程序的可移植性,则应当尽可能避免使用位段,这是因为位段在不同系统中可能有不同的结果:
- int是否有符号。
- 许多编译器将位成员的长度限制在一个整形值的长度之中,因此如果位段中位的最大数目为31位,那么它可以在一个32位的机器上运行,但不能在16位的机器上运行。
- 位段中的成员内存是从左向右还是从右向左进行分配的。
- 当一个声明指定了两个位段,第2个位段比较大,无法容纳于第一个位段剩余的位时,编译器是将第2个位段放在内存的下一个字还是直接放在第1个位段后面。
使用的时候:
- 如果比较注重程序的可移植性,则应当尽可能避免使用位段,这是因为位段在不同系统中可能有不同的结果。
- 位段能够将长度为奇数的数据包装在一起,节省存储空间。当这类结构使用次数极多时,位段的使用就是有必要的。
- 当需要访问一个整型值的部分为内容时,建议使用位段。
位段与位操作:
需要注意的是:任何使用位段的操作都可以使用移位和屏蔽来实现。
10.5 联合
10.5.1 概述
声明:
声明形式与结果体基本一致:
union {
float f;
int i;
} fi;
特点:
联合中所有成员变量引用的都是内存中的相同位置。
一种应用:
联合的用途主要在于节省空间,这里以BASIC解释器为例。BASIC解释器需要记录变量的类型和值,一种可用的结构如下:
struct VARIABLE
{
enum { INT, FLOAT, STRING} type;
int int_value;
float float_value;
char *string_value;
};
其中type记录变量类型,下面的三个变量用来存储不同类型的值,显然,该解释器在运行时,每创建一个该结构,该结构的成员变量就有两个被浪费了。
使用联合进行优化:
struct VARIABLE_UNION
{
enum { INT, FLOAT, STRING} type;
union
{
int int_value;
float float_value;
char *string_value;
} value;
};
使用联合,避免了结构体中两个成员变量的浪费。
长度
如果联合的不同成员有不同的长度,取最长的那个作为联合的长度。
EG:
-
代码:
union FI { char c; float f; int i; } fi; printf( "大小为: %d\n", sizeof( fi ) );
-
输出:
大小为: 4
因为里面最大的成员长度为4。
10.5.2 变体记录
概念:
将结构体作为联合的成员变量,此时联合的大小取决于变量中最大的结构体的大小。
优化:
需要注意的是:有时候联合内的结构体之间的大小区别差异过于悬殊,这时如果选取小的结构体,将会产生相当程度的空间浪费,因此可以在联合中使用指向结构体的指针来代替原有的结构体。当决定使用哪一个结构体作为成员时,再对其进行动态内存分配
。
10.5.3 初始化
两种方式:
-
使用
.
操作符来初始化:union { int a; char c; } x; x.a = 10; x.c = '0';
-
直接初始化:初始值必须是联合第一个成员的类型,且必须位于花括号内:
union { int a; char c; } x; x = { 10 };
10.6 总结
- 向函数传递结构参数是低效的。
- 把结构标签声明和结构的typedef声明放在头文件中,当源文件需要这些声明时,可以通过
#include
指令将它们包含进来。 - 结构成员的最佳排列形式并不一定就是考虑边界对齐而浪费内存空间最少的 那种排列形式。
- 将位段成员为显式的声明是否有符号。
- 位段不可移植,因为位段的处理与环境高度相关。
- 位段使源代码中的位的操作更加清晰。
十一. 动态内存分配
11.1 概述
为什么进行动态分配:
C程序在变量声明时分配内存,这是在运行前决定的,但许多时候变量的内存大小只有在运行时才能够确认,此时就需要进行动态内存分配。
相关函数
以下函数均在stdlib.h
头文件中定义。
-
malloc:
-
原型:
void *malloc( size_t size )
-
效果:返回一块连续的地址,参数size的以字节为单位。如果剩余的内存不够,则返回NULL。
-
特点:有时,malloc返回的内存可能比我们请求的内存要多一点。
-
-
free:
-
原型:
void free( void *pointer );
-
效果:释放之前申请的内存。
-
特点:参数要么为NULL,要么是一个
malloc
、calloc
、realloc
返回的地址。且不能释放一部分,要释放就全释放。
-
-
calloc:
-
原型:
void *calloc( size_t num_elements, size_t element_size );
-
效果:num_elements表示元素的个数,element_size表示每个元素的字节数,并对该段内存初始化为0.
-
-
realloc:
-
原型:
void *realloc( void *ptr, size_t new_size );
-
效果:对之前动态申请的空间进行扩大或缩小。
-
特点:
- 扩大内存时,原先的值保留,在尾部开辟一块新的内存(未经初始化)。如果原先的内存块无法改变大小,则令外分配一块大小正确的内存,并将原有的内容复制到新的块上。
- 缩小内存时,砍掉尾部,剩余的内容不变。
-
11.2 常见错误
- 忘记检查内存分配是否成功。
- 操作内存时超出了分配内存的边界。
- 内存泄露。分配内存但在使用完毕后不释放将引起内存泄漏(memory leak),这可能榨干机器的内存。
11.3 内存操作实例
十二. 使用结构和指针
12.1 单链表
12.2 双链表
十三. 高级指针话题
13.1 高级声明
-
int *f();
函数调用操作符
()
的优先级高于间接访问操作符*
,因此,f首先是一个函数,其次返回值为一个指向整型的指针。 -
函数指针:
int (*f)();
首先执行第一个括号内的,
*f
使得f成为一个指针,后面的()
说明f是一个函数指针,返回一个整型值。程序中每个函数都位于内存中的某个位置,所以当然存在指向函数的指针。
-
int *f[];
由于下标
[]
的优先级更高,所以f是一个数组,其元素类型是指向整形的指针。 -
两种错误案例:
-
int f[]();
顺序执行
[]
,这表明f是一个数组,其成员类型是返回值为整数的函数。但这是非法的,因为数组成员的长度必须固定,而不同函数可能具有不同的长度。 -
int f()[];
顺序执行,先执行
f()
,说明f是一个函数,然后执[]
和int
,说明返回值是一个整型数组。但这是非法的,因为函数的返回值必须是标量,不能返回数组。
-
-
int *(*f[])()
根据优先级,首先访问
f[]
,说明f是一个数组;然后执行前面的*
,说明f是一个指针数组,内部的元素是指针;随后执行后面的()
,说明数组元素是函数;最后执行最前面的int *
,说明数组元素(函数)的返回值为整型指针。 -
完整的函数原型案例:
-
int ( *f )( int, float );
f是一个函数指针,其指向的函数接收int、float类型的两个参数,返回值类型为int。
-
int *( *g[] )( int, float);
g是一个数组,数组内的元素类型是一个函数指针,指向的函数接收int、float类型的两个参数,返回值类型为int。
-
13.2 函数指针
13.2.1 简介
声明与初始化:
函数指针也是指针,在使用前必须进行初始化。
int f( int );
int (*pf)( int ) = &f;
需要注意的是:函数名在使用时,总是由编译器将其转换为函数指针,因此&
操作符在这里可以忽略,因为它只是显式的说明了编译器将隐式执行的任务。
执行
int ans;
ans = f( 25 );
ans = *(pf)( 25 );
ans = pf( 25 );
上述三条函数调用语句本质上都是一致的,因为最终都是由编译器转化为函数指针进行调用。
作用:
- 函数重载。
- 回调函数。
- 转移表
13.2.2 回调函数
回调函数:即在目标函数的参数列表中使用函数指针,然后将工具函数传入到目标函数,目标函数可通过函数指针来调用目标函数。
EG:现在需要在单链表中查找一个值。其参数是传向第一个节点的指针以及那个需要查找到值。
-
优化前:
Node *search_list ( Node *node, int const value ){ while( node != NULL ){ if( node->value == value ){ break; } node = node->link; } return node; }
-
优化思路:
上述代码仅能用于查找数据为int类型的链表内的值,一旦链表内的数据类型不是int类型,比较将失效。因此优化思路如下:
- 将比较部分抽离出来变成一个函数,在查找的参数列表添加函数指针,用于客户调用不同的比较函数。
- 编写比较函数时,参数列表内的参数类型应为
void *
,这是为了与函数指针相匹配,函数具体实现是再对类型进行强转。 - 函数传递值时,使用指针而不是值本身,这是因为数组和字符串无法作为参数在函数间传递,自然也无法调用函数比较,所以用指针。
-
优化后的代码(包括int类型和float类型的值得比较):
/* 定义一个链表的数据类型 */ typedef struct DATA{ char a; int b; float c; }Data; /* 不同的比较函数 */ int compare_ints( void const *a, void const *b ){ /* 使用void const *是为了与函数指针类型相匹配,*是因为字符串和数组无法通过值传递传给数组*/ if( *(int *)a == *(int *)b ){ return 0; /* 比较函数用0表示相等,1表示不等,这是为了与一些标准库的比较函数相匹配。如:strcmp*/ }else{ return 1; } } int compare_floats( void const *a, void const *b ){ /* 使用void const *是为了与函数指针类型相匹配,*是因为字符串和数组无法通过值传递传给数组*/ if( *(float *)a == *(float *)b ){ return 0; /* 比较函数用0表示相等,1表示不等,这是为了与一些标准库的比较函数相匹配。如:strcmp*/ }else{ return 1; } } int compare_datas( void const *a, void const *b ){ /* 使用void const *是为了与函数指针类型相匹配,*是因为字符串和数组无法通过值传递传给数组*/ Data *p1, *p2; p1 = (Data *)a; p2 = (Data *)b; if( p1->a == p2->a && p1->b == p2->b && p1->c == p2->c ){ return 0; /* 比较函数用0表示相等,1表示不等,这是为了与一些标准库的比较函数相匹配。如:strcmp*/ }else{ return 1; } } /* 查找函数 */ Node *search_list( Node *node, void *const value, int (*compare)( void const *, void const * ) ){ /* 函数指针,void类型是为了适应多种参数的比较函数;const是为了防止比较时修改参数,*是因为字符串和数组*/ while( node != NULL ){ if( compare( &node->value, value ) == 0 ){ /* 比较函数用0表示相等,1表示不等,这是为了与一些标准库的比较 函数相匹配。如:strcmp*/ break; } node = node->link; } return node; } // 调用函数 int main(){ // 声明并初始化函数指针 int (*pf)( void const *, void const *); pf = compare_ints( void const *a, void const *b ); // 在这里可以根据需要选择比较的函数 Node *pn; int a = 10; // 假设首节点已经存在,为root pn = search_list( root, &a, pf ); return 0; }
13.2.3 转移表
概述:
转移表就是一个函数指针数组,数组的下标代表不同的应用场景,该下标处的函数指针指向的函数代表该应用场景下的具体逻辑。
EG:现在有一个计算机,它读入两个数字(op1, op2)和一个操作符 (oper),返回运算结果(这里仅计算加减乘除):
-
优化前:
#define ADD '+' #define SUB '-' #define MUL '*' #define DIV '/' double operation( double const op1, double const op2, char const oper ){ int result; switch( oper ){ case ADD: result = add( op1, op2 ); break; case SUB: result = sub( op1, op2 ); break; case MUL: result = mul( op1, op2 ); break; case DIV: result = div( op1, op2 ); break; default: result = -1; } return result; }
-
优化分析:
对于一个大型计算器,oper可能有上百个操作符,switch语句将会极长,显然需要优化。
如果我们为每个运算符都进行编号,将其变为0,1,2……等一串从0开始的连续整数,就可以用数组的下标来对应不同运算场景;然后在该位置存储函数指针,指向该运算场景对应的函数。这样就可以通过数组来快速调用对应函数,并进行运算。
-
优化后:
// 函数原型声明 double add( double, double ); double sub( double, double ); double mul( double, double ); double div( double, double ); // 声明并初始化函数指针数组,元素指向参数为两个double,返回值类型为double的函数 double (*oper_func[])( double, double ) = { add, sub, mul, div }; /* 这里的&可以去掉,因为编译器在编译时 会将函数名替换为函数指针。*/ // 执行函数 result = oper_func[ oper ]( op1, op2 );
转移表下标越界:程序可能在三个地方终止:
- 如果下标值远远越过了数 组的边界,它所标识的位置可能在分配给该程序的内存之外。有些操作系统能检测到这个错误并终止程序,但 有些操作系统并不这样做。如果程序被终止,这个错误将在靠近转换表语句的地方被报告,问题相对而言较易诊断。
- 如果程序并未终止,非法下标所标识的值被提取,处理器跳到该位置。这个不可预测的值可能代表程序中 一个有效的地址,但也可能不是这样。如果它不代表一个有效地址,程序此时也会终止,但错误所报告的地址 从本质上说是一个随机数。此时,问题的调试就极为困难。
- 如果程序此时还未失败,机器将开始执行根据非法下标所获得的虚假地址的指令,此时要调试出问题根源就更为困难了。
13.3 命令行参数
引入:
某些程序的运行是通过命令行进行的,例如:
cc -c -o main.c insert.c -o test
那么程序该如何识别命令行传递过来的具体指令?在哪里接收?
13.3.1 接收命令行参数
main函数:
C语言程序均存在且唯一存在一个main函数入口,该函数的具体形式如下:
int main( int argc, char ** argv ){
return 0;
}
其中:
- argc代表传递参数的个数;
- argv是一个二维指针,是指向“指向传递的具体参数的一维指针”的指针。使用二维指针的原理是:
- 如果使用一维指针,我们只能指向一个变量的地址,而由于参数以字符串形式传递,导致每一个参数的长度无法直接确定,而且参数的地址不一定连续(这取决于系统,因此最好不要运用这一点),因此不能采用。
- 使用二维指针,因为指针的长度是确定的,那么就可以直接通过加法来调整二维指针的指向,找到一维指针,从而找到对应的参数。
- 需要注意的是:命令行参数中,默认第一个参数是程序的名称,因此*argv指向的是程序的名称的首地址,如果不添加其他参数,argv默认为1。
指令分析:
prog -c -o main.c insert.c -o test
-
传递过来了7个参数,argc = 7。
-
argv指向的第一个指针指向“cc”字符串的首地址,即:
printf( "%s\n", *argv ); printf( "%c\n", **argv );
输出:
prog p
-
通过对argv指针进行++运算,遍历所有参数,直到argv==NULL。
效果图:
EG:一个用于打印命令行参数的程序:
-
代码:
#include <stdio.h> #include <stdlib.h> #include <windows.h> int main( int argc, char **argv ){ SetConsoleOutputCP(65001); // 将程序在Windows控制台的输出编码由默认的utf-8修改为gbk printf( "main函数运行\n" ); printf( "传入的argc = %d\n", argc); printf( "遍历传入的参数如下:\n"); /* 遍历传入的参数 */ do { printf( "%s\n", *argv ); } while ( *++argv != NULL); // 先++运算,再*运算,最后!=运算 system( "pause" ); return 0; }
-
命令行编译并运行:
gcc -c main.c // 编译文件 gcc main.o -o main // 链接main.o文件,产生main.exe可执行程序 main.exe -a -b name1 name2
-
输出:
main函数运行 传入的argc = 5 遍历传入的参数如下: main.exe -a -b name1 name2
13.3.2 处理命令行参数
常见的命令行参数形式如下:
-
程序名+零个或多个选项+零个或多个文件名。
-
EG:
prog -a -b -c name1 name2 name3
其中对于文件名,以某种需要的方式处理,如果后面没有文件名,则以标准输入流进行处理。
-
处理代码:
#include <stdio.h> #define TRUE 1 /* ** 处理文件名的函数的原型(其具体实现这里忽略) */ void process_stdard_input( void ); void process_file( char *file_name ); /* ** 定义全局变量,用于记录不同的选项是否被调用,缺省默认为FALSE(0) */ int option_a, option_b; int main( int argc, char **argv ){ /* ** 检查横杠后的字母 */ while( *++argv != NULL && **argv =='-' ){ // 跳过了第一个参数,也就是程序自身的名字 /* 检查横线后面的字母,就是`-a`中的a */ switch( *++*argv ){ case 'a': option_a = TRUE; break; case 'b': option_b = TRUE; break; default: break; } } /* ** 处理文件名参数 */ if ( *argv == NULL){ process_standard_input(); }else{ do{ process_file( *argv ); }while( *++argv !=NULL ) } return 0; }
13.4 字符串常量
首先回顾一下字符串:
字符串本身是一个常量,并不允许修改。但是当一个字符串常量出现在某一个表达式中时,他的值就是一个指针常量,编译器将字符串常量复制到一段内存中,并使用该指针常量指向这段内存的首地址。
此外,当数组出现在表达式中时,也是一个指针常量。
EG:
-
“xyz” + 1;
字符串
xyz
出现在表达式中,是一个指向xyz的指针常量,+1后得到一个指向y的指针,相当于:char * const p = "xyz"; char * p2; p2 = p + 1;
-
*"xyz"
相当于间接操作符访问指针,得到字母
x
。 -
“xyz”[2]
得到字母
z
十四. 预处理器
是编译C程序的第一步,主要任务包括:
- 删除注释;
- 插入被
include
指令包含的内容; - 定义和替换由
define
定义的符号; - 确定代码的一部分内容是否应该根据一些条件编译指令进行编译。
14.1 预定义符号
C中定义的关于程序编译时的一些属性符号,包括:
__FILE__
&&__LINE__
:用于确认调试输出的来源。__DATA__
&&__TIME__
:在编译的环境中加入版本信息。__STDC__
:用于那些在ANSI环境和非ANSI环境中都必须进行编译的程序中结合条件编译。
下表表示详细信息:
符号 | 例子 | 含义 |
---|---|---|
FILE | 'name.c' | 进行编译的源文件名 |
LINE | 15 | 文件当前的行号 |
DATA | "Jan 31 1997" | 文件被编译的日期 |
TIME | "18:04:30" | 文件被编译的具体时间 |
STDC | 1 | 如果编译器遵循ANSI,其值为1,否则未定义 |
14.2 #define
语法:
#define name stuff
需要注意的是:define不仅可以替换数字,还可以替换任何文本:
#define A 10
#define REG register
#define DO_FOREVER for(;;)
#define DEBUG_PRINT printf( "FILE %s line %d: " \
"A = %d", \
__FILE__, __LINE__, \
A )
DEBUG_PRINT;
如果stuff有多行,使用\
来进行连接
14.2.1 宏
在使用define进行定义时,允许将参数替换到文本中。
EG:
#define SQUARE( x ) x + x // 注意:SQUARE后面的第一个括号不能与SQUARE有空格,否则参数将会被解释为stuff的一部分
printf( "运算的到的值为:%d\n", SQUARE( 5 ) );
注意:使用宏定义的文本会在编译的第一步,由预处理器进行文本替换,这个过程不涉及运算,只是原文替换,因此在定义运算的宏时需要加括号。即:
#define SQUARE( x ) ( ( x ) + ( x ) )
14.2.2 define替换
替换步骤如下:
- 首先检查代码,如果代码中存在define定义的文本,进行替换。
- 替换文本随后被插入到程序中原来的文本的位置。对于宏,参数名会被其值所替换。
- 最后,对第二步得到的结果进行扫描,查看是否还包含由define定义的符号,如果有,重复上述过程。
如何将宏参数插入到字符串常量中?
特性:
- define定义的常量中,字符串常量的内容并不进行检查。
- 临近字符串自动连接。
- 预处理器可以将
#argument
这种结构翻译为"argument"
,从而将宏参数转换为字符串常量 ##
符号将宏参数贴合在一起
EG1:
-
代码:
#define PTINTF( FORMAT, VALUE ) \ printf( "The value of " #VALUE " is " FORMAT "\n", VALUE ) PTINTF( "%d", 3 + 5 );
-
输出:
The value of 3 + 5 is 8
EG2:
-
代码:
#define ADD_TO_SUM( sum_number, value ) \ sum ## sum_number +=value #define CONS( a, b ) int( a##e##b ) int main( int argc, char **argv ){ int sum1 = 10; ADD_TO_SUM( 1, 2 ); //此处逻辑:sum##sum_number = sum##1 = sum1; sum1 += 2; sum1 = 12; printf( "sum1 = %d\n", sum1 ); printf( "a##e##b = %d\n", CONS( 2, 3 ) ); // a##e##b = 2e3 = 2000 return 0; }
-
输出:
sum1 = 12 a##e##b = 2000
-
注意:当使用参数与字符相连时,如代码中的sum##sum_number,得到的结果一定要是合法的标识符,否则结果将是未定义的。
14.2.3 宏与函数
宏的优点:
-
在执行小型任务时,用于调用和从函数返回的代码可能比实际执行这个小型计算工作的代码更大,所以使用宏在程序规模和速度上更胜一筹。
#define MAX( a, b ) ( ( a ) > ( b ) ? ( a ) : ( b ) )
-
函数的参数必须被声明为一种特定的类型,而宏却类型无关,可以应用于多种类型。
-
有一些任务根本无法使用函数来实现。例如函数无法将类型作为参数进行传递,而宏却可以。
#define MALLOC( n, type )\ ( ( type * ) malloc( ( n ) * sizeof( type ) ) ) int *p = MALLOC( 10, int );
宏的缺点:
每次使用宏,一份宏定义的代码代码的拷贝都会被插入到程序中,除非宏的长度极短,否则这将会大大增加程序的长度。
14.2.4 带副作用的宏参数
使用宏函数有时会带来一些负作用,情况如下:
参数多次使用:
当宏参数在宏定义中出现的次数超过一次时,如果这个参数具有副作用,那么 当你使用这个宏时就可能出现危险,导致不可预料的结果。
EG:
-
#define MAX( a, b ) ( (a) > (b) ? (a) : (b) ) int x, y, z; x = 5; y = 10; z = MAX( x++, y++ ); printf( "x = %d, y = %d, z = %d\n", x, y, z);
-
输出:
x = 5, y = 11, z = 10
-
分析:实际的操作是:
z = ( ( x++ ) > ( y++ ) ? ( x++ ) : ( y++ ) );
需要注意的是:
- 宏参数替换是文本替换,替换时直接拷贝,不涉及运算。
z = ( y++ )
:虽然用()
括住了,但依然先z = y
,然后再y++
。
其他情况:
如使用getchar()
,其每运行一次就会得到一的字符,抛弃前一位字符,如果将它拷贝到宏参数中,将会导致多次输入。
EG:
-
代码:
#define COM( ch ) ( ( ch ) & 1 ? ( ch ) | 1 : ( ch ) ); COM( getchar() );
-
分析:
此时的实际效果为:
( ( getchar() ) & 1 ? ( getchar() ) | 1 : ( getchar() ) );
getchar()
被执行了两次,因此实际上?
前的字符和?
后的字符不是同一个。
14.2.5 命名规范
原因
宏和函数在功能上有所区别,但在C语言中,其使用形式十分相似,尤其是带有参数的宏。因此命名规范是必要的,便于我们分辨调用的究竟是一个函数还是一个宏。
规范:
-
宏全部大小,字母间下划线连接。
-
函数全部小写,字母间下划线连接。
宏和函数的具体区别:
属性 | #difine宏 | 函数 |
---|---|---|
代码长度 | 每次使用时,宏代码都被拷贝到程序中,除了非常小的宏之外,程序的长度将会大幅增加 | 函数代码只出现于一个地方,使用该函数时,都调用哪一个地方的代码。 |
执行速度 | 更快。 | 存在函数调用、返回的额外开销。 |
操作符优先级 | 宏参数的求值是在所有周围表达式的 上下文环境里,除非它们加上括号,否则 邻近操作符的优先级可能会产生不可预料 的结果。 | 函数参数只在函数调用时求值一次, 它的结果值传递给函数。表达式的求值结 果更容易预测。 |
参数求值 | 参数每次用于宏定义时,它们都将重 新求值。由于多次求值,具有副作用的参 数可能会产生不可预料的结果。 | 参数在函数被调用前只求值一次。在 函数中多次使用参数并不会导致多种求值 过程。参数的副作用并不会造成任何特殊 的问题。 |
参数类型 | 宏与类型无关。只要对参数的操作是 合法的,它可以使用于任何参数类型。 | 函数的参数是与类型有关的。如果参数的类型不同,就需要使用不同的函数, 即使它们执行的任务是相同的。 |
14.2.6 undef
作用:用于移除一个宏定义。
EG:
#define MAX_LENGTH 100
#undef MAX_LENGTH
14.2.7 命令行定义
大多数C语言编译器提供在命令行中定义符号的能力,用于启动编译过程。
在Uinx中,语法如下:
-Dname
-Dname=stuff
第一行定义了符号name
,值为1;
第二行定义了name
,值为stuff
。
Uinx实例:
cc -DMAX_LENGTH=100 main.c
效果:在编译main.c时,传递了一个MAX_LENGTH
的量,大小为100。
14.3 条件编译
14.3.1 概述
有时我们希望在编译源文件时,可以根据需求,选择哪些语句被编译,哪些语句被忽略,这当然是有需求的:
-
只用于调试程序的语句。我们不希望物理删除它,因为后面修改时可能还要用到。
#define MESSAGE 1 #define WARNING 1 #define ERROR 1 #if MESSAGE printf( "这是一条一般信息" ); #elif WARNING printf( "这是一条警告信息" ); #elif ERROR printf( "这是一条错误信息" ); #else printf( "前面的信息均错误" ); #endif return ;
-
对于同一个程序,我们希望它依照需求可以被编译出不同的版本。
14.3.2 是否被定义
有时我们需要在源代码中确定有些标识符是否已被定义。
语法:
/* 被定义 */
#if defined(symbol)
#ifdef symbol
/* 未被定义 */
#if !defined(symbol)
#ifndef symbol
上述中的每一块的两句效果等加。但是#if
形式功能更强,因为它可以包含额外的条件:
#if x > 0 || defined( ABC ) && defined( BCD )
14.4 文件包含
在编译文件时,会对该文件使用#include
指令引入的文件进行替换。因此,如果一个头文件被包含到10个源文件中,它实际上被编译了10次。
14.4.1 函数库文件包含
语法:
#include <filename>
对于函数库文件,编译器定义了一系列的标准来查找函数文件。如典型情况下,Uinx系统上的C编译器在/user/include
目录查找函数头文件。
14.4.2 本地文件包含
语法:
#include "filename"
查找:
一种常见的策略是先在源文件当前目录下进行查找,如果未找到,则像寻找函数库头文件一样在标准位查找本地头文件。
UNIX系统和Borland C编译器支持使用绝对路径名称来指定引入的头文件。
14.4.3 嵌套文件包含
多重嵌套场景:
main.c
包含a.h
和b.h
,而a.h
和b.h
又都包含了x.h
,那么对main.c
进行编译时会导致x.h
在main.c
内出现两次。这显然是不好的。
较好的原则:
-
头文件尽量不要引入其余的头文件,除非必要。
-
在源文件内引入需要的头文件。
-
使用条件编译处理头文件,以
header.h
为例:#ifdef _HEADER_H #define _HEADER_H 1 /* 具体逻辑 */ #enfidif
使用这种方法,可以有效避免头文件被多次编译。但是,这样仅能避免多次编译,仍旧无法解决在一个源文件中多次包含某一头文件导致其代码被多次读入,这将会增大代码量,拖慢编译速度。因此,如果有可能,要尽量避免多重包含。
14.5 其他指令
14.5.1 #error
作用:该指令允许你生成错误信息。当预处理器执行该语句时,将会停止编译,并抛出错误信息。
语法:
#error error_message
需要注意:
- 后面的信息不需要加上双引号,也不要带
;
。
EG:
#define CONST_NAME "CONST_NAME1"
printf("%s\n",CONST_NAME);
#undef CONST_NAME
#ifndef CONST_NAME
#error No defined Constant Symbol CONST_NAME1
#endif
如此,程序将会自运行到
14.5.2 #line
语法:
#line number "string"
注意:
- 后面的
"string"
是可选的,如果给了,预处理器就将其作为当前文件的名字。 number
是告知预处理器下一行输入的行号。
作用:
这条指令最常用于把其他语言的代码转换为C代码的程序。
14.5.3 #program
用于支持因编译器而异的特性,其语法也因编译器而异。因此其具有不可移植性。
不同的效果:
- 有些编译器使用#progma指令在编译过程中打开或关闭清单显示;
- 有些编译器则使用
#program
把汇编代码插入到C程序中。
14.5.4
作用:
是无效指令,预处理器遇到该符号时会将其删除,因此可以用来凸显一些代码。
可以通过插入空行取得同样效果。