《C Primer Plus (第6版) 中文版》学习笔记,引用原书部分内容仅供学习,版权归原作者、出版社所有。
C语言常见英文缩写释义及术语中英对照表
常见英文缩写释义
//头文件
stdio.h - standard input/output.header
tgmath.h - type-generic macros
//关键字或函数名
printf - print formatted
int - integer
char - character
const - constant
sqrt - square root
cbrt - cube root
fabs - float absolute value
EOF - End Of File
strlen - string length
strcat - string concatenate
strcmp - string compare
strpbrk - string pointer break
atoi - ASCII to Integer
malloc - memory allocation
calloc - contiguous allocation
fflush - Flush File Buffer
//转换说明符
%c - character
%i - integer
%d - decimal
%o - octal
%x - hexadecimal
%u - unsigned int
%ld - long int (decimal)
%lo - long int (octal)
//常量后缀
LLU/ull - unsigned long long
//转义序列
\a - Bell (alert)
\b - Backspace
\f - Form feed
\n - New line
\r - Carriage return
\t - Horizontal tab
\v - Vertical tab
常用词及常见术语中英对照表
Integrated Development Environment - 集成开发环境
basename - 基本文件名
extension - 文件扩展名
preprocessor directive - 预处理器指令
preprocessing - 预处理
declaration - 声明
constant - 常量
variable - 变量
identifier - 标识符
formal argument - 形式参数
formal parameter - 形式参数
actual argument - 实际参数
calling function - 主调函数
function call - 函数调用
newline character - 换行符
escape sequence - 转义序列
prototype - (函数)原型
function definition - 函数定义
function declaration - 函数声明
state - 状态
debugger - 调试器
reserved identifier - 保留标识符
complex - 复数
imaginary - 虚数
bit - 比特(以0或1来表示)
byte - 字节(通常为8bit)
initialize - 初始化
bit pattern - 位组合(由二进制数0和1组成的一个序列)
integer - 整数
decimal - 十进制数
octal - 八进制数
hexadecimal - 十六进制数
pragma - 编译指示(C或C++的预处理命令)
American Standard Code for Information Interchange (ASCII) - 美国信息交换标准码
Unicode - 统一的字符编码标准
Universal Character Set (UCS) - 通用字符集
Unicode Transformation Format (UTF) - 统一码转换格式
character constant - 字符常量
active position - 活跃位置
exact-width integer type - 精确宽度整数类型
minimum width type - 最小宽度类型
fastest minimum width type - 最快最小宽度类型
overflow - 上溢
underflow - 下溢
subnormal - 低于正常的(浮点值)
pointer - 指针
character string - 字符串
buffer - 缓冲区
null character - 空字符
symbolic constant - 符号常量
manifest constant - 明示常量
compile-time substitution - 编译时替换
conversion specification - 转换说明
underlying type - 底层类型
stack - 栈
block - 块
binary - 二进制的
modifiable lvalue - 可变左值
object locator value - 对象定位值
rvalue - 右值
value of an expression - 表达式的值
operand - 运算对象
operator - 运算符
unary operator - 一元运算符
binary operator - 二元运算符
truncation - 截断
expression tree - 表达式树
modulus operator - 求模运算符
remainder - 余数
increment operator - 递增运算符
decrement operator - 递减运算符
infinite loop - 无限循环
relational operator - 关系运算符
relational expression - 关系表达式
subexpression - 子表达式
statement - 语句
side effect - 副作用
sequence point - 序列点
full expression - 完整表达式
compound statement - 复合语句
promotion - 升级
demotion - 降级
cast operator - 强制类型转换运算符
argument - 实参
parameter - 形参
pseudocode - 伪代码
iteration - 迭代
entry condition - 入口条件
null statement - 空语句
Boolean variable - 布尔变量
indefinite loop - 不确定循环
counting loop - 计数循环
exit-condition loop - 出口条件循环
nested loop - 嵌套循环
outer loop - 外层循环
inner loop - 内层循环
array - 数组
element - 元素
subscript - 下标
indice - 索引
offset - 偏移量
modularity - 模块化
forward declaration - 前置声明
branching statement - 分支语句
selection statement - 选择语句
conditional expression - 条件表达式
low-level I/O - 底层I/O
standard I/O package - 标准I/O包
stream - 流
binary search - 二分查找
recursion - 递归
tail recursion - 尾递归
double recursion - 双递归
algorithm - 算法
indirection operator - 间接运算符
dereferencing operator - 解引用运算符
scalar variable - 标量变量
variable-length array - 变长数组
double indirection - 双重间接
compound literal - 复合字面量
string literal - 字符串字面量
string constant - 字符串常量
static memory - 静态存储区
buffer overflow - 缓冲区溢出
null pointer - 空指针
macro - 宏
selection sort algorithm - 选择排序算法
command-line argument - 命令行参数
argument count - 参数计数
argument value - 参数值
storage class - 存储类别
designate - 指定
storage duration - 存储期
scope - 域
linkage - 链接
block scope - 块作用域
global variable - 全局变量
translation unit - 翻译单元
register - 寄存器
extern - 外部
external variable - 外部变量
defining declaration - 定义式声明
referencing declaration - 引用式声明
memory allocation - 内存分配
dynamic array - 动态数组
memory leak - 内存泄露
contiguous allocation - 连续分配
constancy - 恒常性
volatility - 易变性
qualified type - 限定类型
idempotent - 幂等的
dummy value - 哑值
structure variable - 结构变量
designated initializer - 指定初始化器
flexible array member - 伸缩型数组成员
linked structure - 链式结构
enumerated type - 枚举类型
enumerator - 枚举符
namespace - 名称空间
octet - 八位字节
high-order bit - 高阶位
low-order bit - 低阶位
sign-magnitude - 符号量(原码)
two's-complement - 二进制补码
one's-complement - 二进制反码
Align - 对齐
object-like macro - 类对象宏
function-like macro - 类函数宏
macro expansion - 宏展开
stringizing - 字符串化
generic programming - 泛型编程
generic selection expression - 泛型选择表达式
ceil() - 向正无穷取整
floor() - 向负无穷取整
printf / scanf 函数转换说明、修饰符与标记
printf 函数
转换说明
%c | 单个字符 |
%s | 字符串 |
%d/%i | 有符号十进制整数 |
%u | 无符号十进制整数 |
%o | 无符号八进制整数 |
%x/%X | 无符号十六进制整数,使用十六进制数 0f/0F |
%f | 浮点数,十进制记数法 |
%e/%E | 浮点数,e记数法 |
%a/%A | 浮点数、十六进制数和p记数法 |
%g/%G | 根据值的不同,自动选择 %f 或 %e/%E。后者用于指数小于 -4 或者大于或等于精度时 |
%p | 指针 |
%% | 一个百分号 |
转换说明修饰符
数字 | 最小字段宽度 (如果不能容纳待打印内容,系统会使用更宽的字段) |
.数字 | 精度 对于 %f 和 %e/%E 转换,表示小数点右边数字的位数 对于 %g/%G 转换,表示有效数字最大位数 对于 %s 转换,表示待打印字符的最大数量 对于整型转换,表示待打印数字的最小位数 如有必要,使用前导 0 来达到这个位数 只使用 . 表示其后跟随一个 0,所以 %.f 和 %.0f 相同 |
h | 和整型转换说明一起使用,表示 short int 或 unsigned short int 类型的值 (例: %hu %hx %hd) |
hh | 和整型转换说明一起使用,表示 signed char 或 unsigned char 类型的值 (例: %hhu %hhx %hhd) |
j | 和整型转换说明一起使用,表示 intmax_t 或 uintmax_t 类型的值 (定义在 stdint.h 中,例: %jd %jx) |
l | 和整型转换说明一起使用,表示 long int 或 unsigned long int 类型的值 (例: %ld %lu) |
ll | 和整型转换说明一起使用,表示 long long int 或 unsigned long long int 类型的值 (例: %lld %llu) (C99) |
L | 和浮点转换说明一起使用,表示 long double 类型的值 (例: %Lf %Le) |
t | 和整型转换说明一起使用,表示 ptrdiff_t 类型的值,ptrdiff_t 是两个指针差值的类型 (例: %td %ti) (C99) |
z | 和整型转换说明一起使用,表示 size_t 类型的值,size_t 是 sizeof 返回的类型 (例: %zd) (C99) |
标记
– | 待打印项左对齐 (例: %-20s) |
+ | 有符号值若为正,则在值前面显示加号;若为负,则在值前面显示减号 (例: %+6.2f) |
空格 | 有符号值若为正,则在值前面显示前导空格 (不显示任何符号);若为负,则在值前面显示减号标记并覆盖空格 (例: % 6.2f) |
# | 把结果转换为另一种形式。如果是 %o,则以 0 开始;如果是 %x/%X,则以 0x 或 0X 开始。 对于所有的浮点格式,# 保证了即使后面没有任何数字,也打印一个小数点字符。 对于 %g/%G 格式,# 防止结果后面的 0 被删除。 |
0 | 对于数值格式,用前导 0 代替空格填充字符宽度 对于整数格式,如果出现 – 标记或指定精度,则忽略该标记 |
scanf 函数
转换说明
%c | 把输入解释成字符 |
%s | 把输入解释成字符串。从第一个非空白字符开始,到下一个空白字符之前的所有字符都是输入。 |
%d、%i | 把输入解释成有符号十进制整数 |
%u | 把输入解释成无符号十进制整数 |
%o | 把输入解释成有符号八进制整数 |
%x、%X | 把输入解释成有符号十六进制整数 |
%e、%f、%g、%a | 把输入解释成浮点数 |
%E、%F、%G、%A | 把输入解释成浮点数 |
%p | 把输入解释成指针 (地址) |
转换说明修饰符
* | 抑制赋值 (例: %*d) |
数字 | 最大字段宽度 (例: %10s) 输入达到最大字段宽度处,或第一次遇到空白字符时停止 |
hh | 把整数作为 signed char 或 unsigned char 类型读取 (例: %hhd %hhu) |
ll | 把整数作为 long long 或 unsigned long long 类型读取 (例: %lld %llu) (C99) |
h、l 或 L | %hd 和 %hi 表明把对应的值存储为 short int 类型 %hu 和 %ho %hx 表明把对应的值存储为 unsigned short int 类型 %ld 和 %li 表明把对应的值存储为 long 类型 %lu 和 %lo %lx 表明把对应的值存储为 unsigned long 类型 %lf 和 %le %lg 表明把对应的值存储为 double 类型 在 f、e、g 前面使用 L 而不是 l 时,表明把对应的值存储为 long double 类型 (printf 函数中所有的 float 类型的参数自动转换成 double 类型) 如果没有修饰符,d、i、o、x 表明对应的值被存储为 int 类型,f 和 g 表明把对应的值存储为 float 类型 |
j | 在整型转换说明后面时,表明使用 intmax_t 或 uintmax_t 类型 (例: %jd %ju) (C99) |
z | 在整型转换说明后面时,表明使用 sizeof 的返回类型 (例: %zd %zo) (C99) |
t | 在整型转换说明后面时,表明使用表示两个指针差值的类型 (例: %td %tx) (C99) |
C运算符
运算符(优先级从高至低) | 结合律 |
++(后缀) –(后缀) ()(函数调用) [] {} (复合字面量) . -> | 从左往右 |
++(前缀) –(前缀) – + ~ ! *(解引用) &(取址) sizeof _Alignof(类型名)(本栏都是一元运算符) | 从右往左 |
(类型名) | 从右往左 |
* / % | 从左往右 |
+ -(都是二元运算符) | 从左往右 |
<<>> | 从左往右 |
< > <= >= | 从左往右 |
== != | 从左往右 |
& | 从左往右 |
^ | 从左往右 |
| | 从左往右 |
&& | 从左往右 |
|| | 从左往右 |
?:(条件表达式) | 从右往左 |
= *= /= += -= <<= >>= &= |= ^= | 从右往左 |
,(逗号运算符) | 从左往右 |
fopen()的模式字符串
模式字符串 | 含义 |
“r” | 读模式打开文件 |
“w” | 写模式打开文件,把现有文件的长度截为0,如果文件不存在,则创建一个新文件 |
“a” | 写模式打开文件,在现有文件末尾添加内容,如果文件不存在,则创建一个新文件 |
“r+” | 更新模式打开文件(即可以读写文件) |
“w+” | 更新模式打开文件(即,读和写),如果文件存在,则将其长度截为0;如果文件不存在,则创建一个新文件 |
“a+” | 更新模式打开文件(即,读和写),在现有文件的末尾添加内容,如果文件不存在则创建一个新文件;可以读整个文件,但只能从末尾添加内容 |
“rb” “wb” “ab” “rb+” “r+b” “wb+” “w+b” “ab+” “a+b” | 与上一个模式类似,但是以二进制模式而不是文本模式打开文件 |
“wx” “wbx” “w+x” “wb+x”或”w+bx” | (C11)类似非x模式,但是如果文件已存在或以独占模式打开文件,则打开文件失败 |
各种整数类型
笔记
C 语言中,除存储整数用的各种取值范围不同的整数类型外,实际上 char 与 _Bool 类型本质上也是一种整数型。
char 用于存储字符,但其实际上存储的是字符编码对应的整数 (例如 ASCII 码中,整数 65 代表大写字母 A),你可以用单引号括起的单个字符 (字符常量) 为其赋值,也可以直接使用编码值为其赋值 (但不建议,这样会降低可移植性)。标准 ASCII 码的范围是 0~127,只需 7 位二进制数即可表示,char 类型通常被定义为 8 位的存储单元 (1 字节),但现代许多字符集都已远超 ASCII 码的范围。printf 函数中使用 %d 或 %c 可以改变数据的显示方式,但不改变数据的存储方式。
_Bool 类型表示布尔值,即逻辑值 true 和 false。C 语言用值 1 表示 true,值 0 表示 false,_Bool 是无符号 int 类型,原则上它仅占用 1 bit 存储空间。
示例代码
#include <stdio.h>
#include <limits.h>
#include <stdbool.h>
#include <stdint.h>//C99
#include <inttypes.h>//字符串宏 输出输入说明符
int main(void)
{
int one = 1;
printf("wrong: %d, %d, %d\n", one);//转换说明符缺少参数、参数过多、参数类型不匹配,C标准对这些情况下的行为未作要求
int decimal = 16;//十进制
int octal = 020;//八进制
int hexadecimal = 0x10;//十六进制
printf("dec = %d; octal = %o; hex = %x\n", octal, hexadecimal, decimal);
printf("dec = %d; octal = %#o; hex = %#x or %#X\n\n", decimal, decimal, decimal, decimal);
//在%和转换字符之间插入修饰符,来对转换说明加以修饰
//转换修饰标志#: 使用转换说明符的可选形式;%o格式则以0开始;%x或%X格式则以0x或0X开始;对于所有的浮点形式,#保证了即使不跟任何数字也打印一个小数点字符
//short占用的存储空间不能多于int,long占用的存储空间不能小于int
//通常来说,short占16位,int占16或32位,long占32位,long long占64位
short int test1 = SHRT_MAX;
short test2 = SHRT_MIN;
printf("max short = %hd\nmin short = %hd\n", test1, test2);
int max = INT_MAX;
int min = INT_MIN;
printf("max int = %d\nmin int = %d\n", max, min);
long int test3 = LONG_MAX;
long test4 = LONG_MIN;
printf("max long = %ld\nmin long = %ld\n", test3, test4);
long long int test5 = LLONG_MAX;
long long test6 = LLONG_MIN;//C99
printf("max long long = %lld\nmin long long = %lld\n\n", test5, test6);
unsigned int test7 = UINT_MAX;
unsigned test8 = 0;//无符号整型,只用于非负值
printf("max unsigned int = %u\nmin unsigned int = %u\n\n", test7, test8);
//16位的取值范围由-32768~32767变为0~65535
unsigned short test9 = USHRT_MAX;
unsigned long test10 = ULONG_MAX;
unsigned long long test11 = ULLONG_MAX;//C99
printf("max unsigned short = %hu\nmax unsigned long = %lu\nmax unsigned long long = %llu\n\n", test9, test10, test11);
//d(decimal),i(integer):带符号十进制整数; u(unsigned):无符号十进制整数; o(octal):无符号八进制整数; x,X(hex):无符号十六进制整数
short same1;
short int same2;
signed short same3;
signed short int same4;
//以上四条都表示同一种类型
printf("Some long number: %ld, %llu\n", 123L, 456ull);//常量后缀不分大小写,L:作为long对待,LLU/ull:作为unsigned long long对待
char char_bit = CHAR_BIT;//C语言把1字节定义为char类型占用的位(bit)数
signed char char1 = CHAR_MIN;//C90
unsigned char char2 = CHAR_MAX;
unsigned char char3 = UCHAR_MAX;
printf("\nchar bit = %i\n", char_bit);
printf("min char = %i\nmax char = %d\n", char1, char2);
printf("unsigned char max = %u\n\n", char3);
_Bool bt = 1;//C99
bool bf = false;//stdbool.h
printf("true = %i\nfalse = %i\n\n", bt, bf);
//C99、C11
int32_t aint32 = INT32_MAX;//stdint.h
//32位有符号整数型(精确宽度整数类型),可能是int或long的别名
int_least8_t ain8 = INT_LEAST8_MAX;
//最小容纳8位有符号整数(最小宽度类型)
int_fast8_t fin8 = INT_FAST8_MAX;
//系统中8位有符号值运算最快的整数类型(最快最小宽度类型)
intmax_t maxint = INTMAX_MAX;
uintmax_t maxuint = UINTMAX_MAX;
//C99定义,最大的有符号和无符号整数类型
printf("int32_t max = %" PRId32 "\nint_least8_t max = %" PRIiLEAST8 "\nint_fast8_t max = %" PRIiFAST8 "\n\n", aint32, ain8, fin8);//inttypes.h
//PRId32字符串宏(print 输出说明符),代表打印32位有符号值的合适转换说明
//SCNd32(scan 输入说明符)
printf("intmax_t max = %" PRIdMAX "\nuintmax_t max = %" PRIuMAX "\n", maxint, maxuint);
intptr_t ptra;
uintptr_t uptra;
//可精确存储指针值
int32_t changeTOint32_t = INT32_C(1234567890);
//扩展的整型常量
return 0;
}
各种浮点数类型
示例代码
#include <stdio.h>
int main(void)
{
//科学记数法:1.0×10^9 = e记数法:1.0e9
//C99→p记数法:十六进制数,p代替e,2的幂代替10的幂
//通常系统存储一个浮点数占用32位,8位表示指数的值和符号,24位表示非指数部分
//标准:float至少能表示6位有效数字,取值范围至少是1e-37~1e37
//标准:double至少能表示10位有效数字,与float的最小取值范围相同;C只保证long double至少与double精度相同
//(一般而言,double占用64位,至少有13位有效数字)
printf("float is: %zi bits\n", sizeof(float) * 8);//C99
printf("double is: %zd bits\n", sizeof(double) * 8);//转换修饰符z,与整数转换说明符一起使用,表示一个size_t值(sizeof返回的类型)
//sizeof以字节为单位给出指定类型的大小,C语言定义了char类型是1字节,所以char一定是1字节,但char不一定为8位,在有的系统中,char为16位。
float f0 = 2e2;
float f1 = .8E-1;//从“double”到“float”截断
printf("some float number: %f, %f\n\n", f0, f1);
float f2, f3;
long double ld0;
//编译器默认浮点型常量是double类型
f2 = 4.0 * 2.0;//双精度乘法运算后截断乘积为float的宽度(减慢运行速度)
f3 = 1.0f * 2.0e1F;//F后缀将常量看作float
ld0 = 1.0l * 1.0L;//L后缀看作long double
printf("4.0 * 2.0 = %f\n1.0f * 2.0e1F = %e\n1.0l * 1.0L = %Lf\n\n", f2, f3, ld0);//%f:double十进制;%e:浮点数e记数法;%a:浮点数p记数法
//转换修饰符L,与浮点数转换说明符一起使用,表示一个long double值
float overflow = 3.4E38 * 100.0f;//上溢,赋无穷大
printf("float overflow: %e\n", overflow);//输出inf(infinity)
float underflow = 1E-50f / 2.0f;
printf("float underflow: %e\n", underflow);
//下溢会损失原末尾有效位上的数字,以十进制为例:0.1234E-10除以10得到0.0123E-10
//如果下溢过多,会导致所有的位都为0
//特殊浮点值:NaN(not a number)
float fa, fb;
fb = 2.0e20 + 1.0f;//浮点数舍入错误,缺乏足够精度完成运算
fa = fb - 2.0e20;//float通常只能存储按指数比例缩小或放大的6或7位有效数字
printf("wrong answer: %f\n\n", fa);
/*
float _Complex fc;
double _Complex dc;
long double _Complex ldc;
float _Imaginary fi;
double _Imaginary di;
long double _Imaginary ldi;
*/
//可选实现复数和虚数类型,float _Complex类型应包含两个float类型的值,分别表示复数的实部和虚部
return 0;
}
字符串和格式化输入输出
示例代码
#include <stdio.h>
#include <string.h>//strlen函数原型
#define HELLO "Hello"//C预处理器指令,定义明示常量(符号常量)
int main()
{
char year[5];//数组末尾需要存储空字符(\0)标记字符串的结束,数组容量必须至少比待存储字符串的字符数多1
//最大容纳4字节
printf("Year: ");
scanf_s("%s", year, 5);//scanf_s函数读字符串时需要提供一个参数表示接收缓冲区的大小,避免读入的内容溢出导致内存泄露
//最大读入4字节
//当scanf把字符串放进指定数组时,它会在字符序列的末尾加上'\0',让数组中的内容成为一个C字符串
//根据%s转换说明,scanf在遇到第一个空白(空格、制表符、换行符)时就不再继续读取。如果使用字段宽度,则在字段末尾或第一个空白字符处停止读取。
printf("%s, %s.\n\n", HELLO, year);
const int MONTHS = 12;//const声明一个变量为只读(而不是声明常量)
float n1 = 3.0;
long n2 = 500000;
printf("%ld, \
%ld\n", //使用反斜杠断行不能缩进
n1, n2);
//如果printf有其他不匹配的地方,即使用对了转换说明也会生成虚假的结果。
//参数传递(P77):程序先根据变量类型将传入的值放入内存栈中,然后函数根据转换说明从栈中读取值。若有不匹配可能读错字节。
printf("LONG "
"TEXT.\n");
//scanf如何读取输入(P81):
//若根据%d读取一个整数,scanf每次读取一个字符,从第一个非空白字符开始读取(希望发现一个数字或符号+、-),直至遇到一个非数字字符,便认为读到了整数的末尾
//如果使用字段宽度,scanf会在字段结尾或第一个空白字符处停止读取
//若第一个非空白字符不合法,scanf将停在那里,并把该字符放回输入中,不会赋值,程序在下一次读取输入时会首先读到这个字符,如果只使用%d转换说明,scanf就一直无法越过它读下一个字符
//如果使用带多个转换说明的scanf,C规定在第一个出错处停止读取输入
unsigned width, precision;
int number = 256;
double weight = 242.5;
printf("Enter a field width: ");
scanf_s("%d", &width);
printf("Number = '%*d'\n", width, number);//*修饰符代替字段宽度,需要一个参数表示它对应的值
printf("Enter a width and a precision: ");
scanf_s("%d %d", &width, &precision);
printf("Weight = '%*.*f'\n\n", width, precision, weight);
int n;
printf("Please enter 3 integers: ");
scanf_s("%*d %*d %d", &n);//*会使scanf跳过相应的输入项
printf("The last integer: %d\n\n", n);
}
运算符、表达式和语句
示例代码
#include <stdio.h>
#include <math.h>//标准数学库
#define S_PER_M 60
int main(void)
{
printf("4^2 = %.2lf\n", pow(4, 2));//4的2次幂
int one, two, three;
one = two = three = 0;//赋值的顺序是从右往左
printf("5 / 3 = %d\n5.0 / 3.0 = %.2f\n\n", 5 / 3, 5.0 / 3.0);//整数除法的结果是整数,小数部分被丢弃,称为截断(truncation)
//C99规定使用趋零截断
int y = 6 * 12 + 5 * 20;//先做乘法再做加法,但先进行哪一个乘法由具体实现决定。
//只有当运算符共享同一个运算对象时,求值顺序才由优先级决定。否则由具体实现决定,不影响最终的结果。
int n = 0;
size_t intsize = sizeof(int);//size_t类型是一个无符号整数类型
printf("n = %d, n has %zd bytes; all ints have %zd bytes.\n\n",
n, sizeof n, intsize);
//sizeof运算符以字节为单位返回运算对象的大小(C语言将1字节定义为char类型占用的大小)
int sec, min, leftsec;
printf("Enter the number of seconds (<=0 to quit): ");
scanf_s("%d", &sec);//地址运算符(&)
while (sec > 0)
{
min = sec / S_PER_M;
leftsec = sec % S_PER_M;//求模运算符:左侧整数除以右侧整数的余数(只能用于整数,不能用于浮点数)
//若第一个运算对象为负数,求模的结果为负数;第一个运算对象为正数,求模的结果也是正数。(C99标准)
printf("%d minutes, %d seconds.\n", min, leftsec);
printf("Enter the number of seconds (<=0 to quit): ");
scanf_s("%d", &sec);
}
printf("\n");
int loop = 0;
while (++loop < 6)//运算符出现在操作对象前面:前缀模式
{
//执行顺序:先递增loop,再判断条件,为真则执行语句
printf("%d ", loop);//loop第一次进入循环时就已经+1了
}
printf("\n\n");
int a = 1, b = 1;
int a_post, pre_b;
a_post = a++;//后缀递增:先使用值,再递增
pre_b = ++b;//前缀递增:先递增,后使用值
printf("a a_post b pre_b\n");
printf("%d %-6d %d %-5d\n\n", a, a_post, b, pre_b);
//C语言中,编译器可以自行选择先对函数中的哪个参数求值,这样做提高了编译器的效率,但如果在函数的参数中使用了递增运算符,就会有一些问题。
//多项式中编译器也可能不会按你预想的顺序来执行,无法保证编译器到底先计算哪一项。
//遵循以下规则避免问题:
//如果一个变量出现在一个函数的多个参数中,不要对它使用递增递减运算符
//如果一个变量多次出现在一个表达式中,不要对它使用递增递减运算符
//表达式(expression):由运算符和运算对象组成,最简单的表达式是一个单独的运算对象,一些表达式由子表达式组成
//每个表达式都有一个值,有赋值运算符的表达式的值与赋值运算符左侧变量的值相同,关系表达式的值为0或1
//语句(statement):一条语句相当于一条完整的计算机指令,大部分语句都以分号结尾,最简单的语句是空语句
;//空语句
//C把末尾加上一个分号的表达式都看作是一条语句(表达式语句),这些语句在程序中可能什么也不做
//语句可以改变值或调用函数
int x1, y1;
x1 = 6 + (y1 = 5);
//并不是所有的指令都是语句,上述语句中的子表达式y1=5是一条完整的指令,但它只是语句的一部分
//根据C标准,声明不是语句,这与C++有所不同
//在C语言中,赋值和函数调用都是表达式,没有所谓的「赋值语句」「函数调用语句」,这些语句实际上都是表达式语句
//副作用(side effect):是对数据对象或文件的修改
//赋值表达式中的副作用就是变量值改变,从C语言的角度看,主要目的是对表达式求值;相同地,递增递减运算符也有副作用
//类似地,调用printf函数(返回值是待显示字符的个数)时,它显示的信息其实是副作用
//序列点(sequence point):是程序执行的点,在该点上,所有副作用都在进入下一步之前发生
//在C语言中,语句中的分号标记了一个序列点。意思是,在一个语句中,运算符对运算对象做的改变必须在程序执行下一条语句之前完成
//一些运算符也有序列点,任何一个完整表达式(full expression)的结束也是一个序列点
//完整表达式(full expression):指这个表达式不是另一个更大表达式的子表达式
//例如,表达式语句中的表达式和while循环中作为测试条件的表达式,都是完整表达式
//复合语句(compound statement):是用花括号括起来的一条或多条语句,也被称为块(block)
//类型转换
//升级(promotion):从较小类型转换为较大类型
//降级(demotion)
//当类型转换出现在表达式时,无论是unsigned还是signed的char和short都会被自动转换成int,如有必要会被转换成unsigned int,称为升级
//类型的级别从高到低依次是:long double, double, float, unsigned long long, long long, unsigned long, long, unsigned int, int
//例外情况:long和int大小相同时,unsigned int比long级别高
//赋值表达式语句中,计算的最终结果会被转换成被赋值变量的类型,这个过程可能导致类型升级或降级
//当char和short类型出现在表达式里或作为函数参数传递时,会被自动升级为int;float在函数参数中时,会被升级为double;函数原型会覆盖自动升级
//待赋值的值与目标类型不匹配时,规则如下:
//目标类型是无符号整型,且待赋的值是整数时,额外的位将被忽略
//目标类型是有符号整型,且待赋的值是整数时,结果因实现而异
//目标类型是一个整型,且待赋的值是浮点数时,该行为是未定义的
//当浮点类型被降级为整数类型时,原来的浮点值会被截断
char ch = 1107;//无符号数溢出后发生回绕:以2^(8*sizeof(type))作模运算 //有符号数溢出将导致无法确定的行为
char ch2 = 1107 % 256;
char ch3 = 83;
printf("%c = %c = %c\n\n", ch, ch2, ch3);
//强制类型转换运算符(cast operator):(type)
int intt;
intt = 1.6 + 1.7;//运算后再截断
printf("%i, ", intt);
intt = (int)1.6 + (int)1.7;//转换后再运算
printf("%d\n\n", intt);
//原则上不应该混合使用类型,不要养成依赖自动类型转换的习惯
//C99规定,实参使用术语argument,形参使用术语parameter
//只需要一个运算对象的运算符称为一元运算符,需要两个运算对象的运算符称为二元运算符
}
C控制语句:循环
示例代码
#include <stdio.h>
#include <math.h>
#include <stdbool.h>//C99
int main(void)
{
int num;
printf("Enter a non-numeric character: ");
//while循环是使用入口条件(entry condition)的有条件循环,在循环的每次迭代之前检查测试条件
//若没有花括号,while语句从while这行运行至下一个分号
//花括号括起的多条语句为复合语句(块),根据while语句的结构,整个复合语句被视为一条语句
while (scanf_s("%d", &num) == 1)
;//跳过整数输入
//应该让空语句独占一行,提醒这是有意为之(更好是使用continue语句)
//不能用关系运算符(relational operator)比较字符串
//比较浮点数时,尽量只用<和>,因为浮点数的舍入错误会导致逻辑上应该相等的两数却不相等
while (getchar() != '\n')
;//循环取出缓冲区直到换行符,清空键盘缓冲区
const double ANSWER = 3.14159;
double response;
printf("What is the value of pi?\n");
scanf_s("%lf", &response);
while (fabs(response - ANSWER) > 0.0001)//fabs函数返回一个浮点值的绝对值
{
printf("Try again.\n");
scanf_s("%lf", &response);
}
printf("Close enough.\n\n");
//对C而言,表达式为真的值是1,表达式为假的值是0,故while(1)循环会一直进行
//一般而言,所有的非零值都视为真,只有0被视为假,只要测试条件的值为非零,就会执行while循环
//(C99新增)_Bool是C语言中布尔变量的类型名,_Bool类型的变量只能存储1或0,如果把其他非零数值赋给_Bool类型的变量,该变量会被设置为1
//stdbool.h头文件让bool成为_Bool的别名,还把true和false定义为1和0的符号常量;包含该头文件后,写出的代码可以与C++兼容,因为C++把bool、true、false定义为关键字
//一些while循环是不确定循环(indefinite loop),在测试表达式为假之前,预先不知道要执行多少次循环
//而计数循环(counting loop)在执行循环之前就知道要重复执行多少次
//初始化(initialize表达式)(循环开始时执行一次)、测试条件(test表达式)(执行循环前)、执行更新(update表达式)(循环结束时)
for (int i = 1; i <= 10; i++)//可以省略一个或多个控制表达式,但不能省略分号,省略第2个表达式视为真;3个表达式可以是不同的变量,第3个表达式可以使用任意合法的表达式
{
printf("%d ", i);
}
printf("\n");
//for (; test; ) 与 while (test) 效果相同
int a = 0, b = 0, c = 0;
//其他赋值运算符:+=、-=、*=、/=、%=(优先级均与=相同)
a += 20;//a = a + 20;
a *= 3 * b + 12;//a = a * (3 * b + 12);
for (a = 0, b = 0; c++, a < 0; a++, b++)//使用逗号运算符以便在循环头中包含更多的表达式
;
//逗号运算符保证了被它分隔的表达式从左往右求值(即,逗号是一个序列点,左侧项的所有副作用都在执行右侧项之前发生)
//整个逗号表达式的值是右侧项的值
//do while循环为出口条件循环(exit-condition loop),在循环的每次迭代之后检查测试条件,保证至少执行循环体中的内容一次
const int key = 0;
int key_userentered;
do
{
printf("Enter the secret key: ");
while (getchar() != '\n');
scanf_s("%d", &key_userentered);
} while (key_userentered != key);
printf("Correct.\n");
//数组简介P140
//数组由相邻的内存位置组成,通过整数下标(subscript)访问数组(array)中单独的项或元素(element),数组元素的编号从0开始
//注意:考虑到执行的速度,C编译器不会检查数组的下标是否正确
//如果字符数组的末尾包含一个表示字符串末尾的空字符\0,则该数组中的内容就构成了一个字符串
//用于识别数组元素的数字被称为下标(subscript)、索引(indice)或偏移量(offset);下标必须是整数且要从0开始计数
}
C控制语句:分支和跳转
ctype.h 头文件中的字符测试函数
函数名 | 如果是下列参数时,返回值为真 |
isalnum() | 字母数字(字母或数字) |
isalpha() | 字母 |
isblank() | 标准的空白字符(空格、水平制表符或换行符)或任何其他本地化指定为空白的字符 |
iscntrl() | 控制字符,如 Ctrl + B |
isdigit() | 数字 |
isgraph() | 除空格以外的任意可打印字符 |
islower() | 小写字母 |
isprint() | 可打印字符 |
ispunct() | 标点符号(除空格或字母数字字符以外的任何可打印字符) |
isspace() | 空白字符(空格、换行符、换页符、回车符、垂直制表符、水平制表符或其他本地化定义的字符) |
isupper() | 大写字母 |
isxdigit() | 十六进制数字符 |
ctype.h 头文件中的字符映射函数
tolower() | 如果参数是大写字符,该函数返回小写字符;否则,返回原始参数 |
toupper() | 如果参数是小写字符,该函数返回大写字符;否则,返回原始参数 |
示例代码
#include <stdio.h>
#include <ctype.h> //字符分析函数系列
#include <stdbool.h>
#define PI 3.14
#define PI2 PI / 2 //预处理器是不进行计算的
int main(void)
{
//if语句被称为分支语句(branching statement)或选择语句(selection statement)
//expression可以使用任意表达式,表达式的值为0则为假
//即使if语句由复合语句构成,整个if语句仍被视为一条语句
//如果没有花括号,else与离它最近的if匹配,除非最近的if被花括号括起来
//实际上,else if是if else语句的变式(嵌套),记住,编译器是忽略缩进的
//C99标准要求编译器最少支持127层嵌套
//用_Bool类型作为标记更合适
//字符输入输出函数:getchar()和putchar()
//getchar不带任何参数,它从输入队列中返回下一个字符
//putchar打印它的参数
//由于这些函数只处理字符,所以它们比通用的函数更快、更简洁,而且他们通常是预处理宏,而不是真正的函数
char ch;
while ((ch = getchar()) != '\n') //赋值表达式的值是赋值运算符左侧运算对象的值
{
if (isalpha(ch))
putchar(ch + 1);
else
putchar(ch);
}
putchar(ch);
//逻辑运算符:
//&&与、||或、!非
//逻辑运算符的优先级比关系运算符低
//&&运算符的优先级比||高,但两者的优先级都比关系运算符低,比赋值运算符高
//C通常不保证先对复杂表达式中哪部分求值,但C保证逻辑运算符的求值顺序是从左往右
//&&和||都是序列点,而且,C保证一旦发现某个元素让整个表达式无效,便立即停止求值
//if (number != 0 && 12/number == 2) 第一个子表达式为假后,不再对关系表达式求值,避免了把0作为除数
//C99标准新增了可代替逻辑运算符的拼写,如果包含iso646.h头文件,便可用and代替&&、or代替||、not代替!
printf("\nwordcnt.c\n=========================\n");
char c, prev;
long n_chars = 0L;
int n_lines = 0;
int n_words = 0;
int p_lines = 0;
bool inword = false;
printf("Enter text to be analyzed (| to terminate):\n");
prev = '\n';
while ((c = getchar()) != '|')
{
n_chars++;
if (c == '\n')
{
n_lines++;
}
if (!isspace(c) && !inword)
{
inword = true;
n_words++;
}
if (isspace(c) && inword)
{
inword = false;
}
prev = c;
}
if (prev != '\n')
{
p_lines = 1;
}
printf("characters = %ld, words = %d, lines = %d, ", n_chars, n_words, n_lines);
printf("partial lines = %d\n", p_lines);
//C提供条件表达式(conditional expression)作为表达if else语句的一种便捷方式,该表达式使用?:条件运算符
//条件运算符分为两部分,需要3个运算对象,是C语言中唯一的三元运算符
int max, a = 0, b = 1;
max = (a > b) ? a : b;
//表达相同含义
if (a > b)
max = a;
else
max = b;
//expression1 ? expression2 : expression3
//如果表达式1为真(非0),那么整个条件表达式的值与表达式2的值相同,否则与表达式3的值相同
//3种循环都可以用continue、break语句;执行到continue语句时,会跳过本次迭代的剩余部分,并开始下一轮迭代;执行到break语句时,会终止包含它的循环
//对于while和do while循环,执行continue语句后的下一个行为是对循环的测试表达式求值;对于for循环,下一个行为是对更新表达式求值,然后对循环测试表达式求值
//多重选择:switch和break
switch (a)
{
case 0:
max = b;
break;
case 1:
max = a;
break;
default:
break;
}
//先对圆括号中的表达式求值,然后扫描标签列表,若发现匹配的值则跳转至那一行,若无匹配,跳转到default标签行,无default行则继续执行switch后面的语句
//若没有break语句,就会从匹配标签开始执行至switch末尾
//C语言的case一般都指定一个值,不能使用一个范围
//switch在圆括号中的测试表达式的值应该是一个整数值(包括char类型);case标签必须是整数类型(包括char)的常量或整型常量表达式(表达式中只包含整型常量)
//不能用变量作为case标签
/*
switch (整型表达式)
{
case 常量1:
语句
default:
语句
}
*/
//多重标签:
/*
switch (ch)
{
case 'a':
case 'A':
a++;
break;
default:
break;
}
*/
//上例可以使用ctype.h系列的toupper()函数避免使用多重标签
//ch = toupper(ch); 或 switch(toupper(ch))
//使用switch程序通常运行快一些,生成的代码少一些
//谨慎使用或完全不用goto语句
goto label;
label: printf("\n");
//实际上,break和continue是goto的特殊形式,但它们不使用标签,不用担心把标签放错位置导致的危险
//但C程序员可以接受一种goto的用法——出现问题时从一组嵌套循环中跳出
}
字符输入输出和输入验证
示例代码
#include <stdio.h>
int main(void)
{
//char ch;
//while ((ch = getchar()) != '#')
// putchar(ch);
//【缓冲区】P187
//如果用户输入字符后立刻重复打印属于:无缓冲输入(直接输入)
//按下Enter键之前不会重复打印属于:缓冲输入,用户输入的字符被收集并存储在缓冲区(buffer)
//缓冲分为两类:完全缓冲I/O 和 行缓冲I/O
//完全缓冲:指当缓冲区被填满时才刷新缓冲区,通常出现在文件输入中;缓冲区的大小取决于系统,常见的大小是512字节和4096字节
//行缓冲:指出现换行符时刷新缓冲区,键盘输入通常是行缓冲输入
//C标准规定输入是缓冲的,因为一些计算机不允许无缓冲输入,标准没有提供调用无缓冲输入的标准方式
//【文件、流和键盘输入】P188
//直接调用操作系统的函数被称为底层I/O(low-level I/O),由于系统各不相同,不可能为普通的底层I/O函数创建标准库
//C还可以通过标准I/O包(standard I/O package)来处理文件,具体的C实现负责处理不同系统的差异
//例如:
//存储文件方式不同、换行符标记不同、衡量文件大小方式不同
//使用标准I/O包就不用考虑这些差异,因此可以用 if (ch == '\n') 检查换行符,即使系统实际用的是回车符和换行符的组合来标记行末尾,I/O函数会在两种表示法之间转换
//从概念上看,C程序处理的是流(stream)而不是直接处理文件
//流(stream)是一个实际输入或输出映射的理想化数据流。这意味着不同属性和不同种类的输入,由属性更统一的流来表示。
//于是,打开文件的过程就是把流与文件相关联,而且读写都通过流来完成。
//C把输入和输出设备视为存储设备上的普通文件,尤其是把键盘和显示设备视为每个C程序自动打开的文件。
//stdin流表示键盘输入,stdout流表示屏幕输出。getchar()、putchar()、printf()、scanf()函数都是标准I/O包的成员,处理这两个流。
//上述内容说明,可以用处理文件的方式来处理键盘输入,而C的输入函数内置了文件结尾检测器
//【文件结尾】P189
//操作系统可以使用内嵌的字符标记文件结尾,也可以通过存储文件大小信息来判断文件末尾
//无论操作系统实际使用何种方法检测文件结尾,在C语言中,用getchar()读取文件检测到文件结尾时将返回一个特殊的值,即EOF(End Of File)
//scanf()函数检测到文件结尾时也返回EOF,通常EOF定义在stdio.h文件中( #define EOF (-1) ),因为字符集的-1通常不对应任何字符,所以可用于标记文件结尾
//某些系统也许把EOF定义为-1以外的值,但定义的值一定与输入字符所产生的返回值不同
//关键是要理解EOF是一个值,标志着检测到文件结尾,并不是在文件中找得到的符号
int ch2; //char类型只能表示0~255的无符号整数
while ((ch2 = getchar()) != EOF) //getchar()的实际返回值类型是int
putchar(ch2);
//使用上面的程序需要找到系统输入EOF字符的方法,大多数UNIX和Linux系统中,在一行开始处按下Ctrl+D会传输文件结尾信号,
//一些系统把一行开始处的、或任意位置的Ctrl+Z解释为文件结尾信号
//【重定向和文件】P191
//C程序默认使用标准I/O包查找标准输入作为输入源(stdin)
//可以显式使用特定函数操作文件,也可以通过重定向输入至文件和从文件输出,也就是把stdin流重新赋给文件
//重定向的一个主要问题是它与操作系统有关,许多C环境都有重定向的特性,且一些C实现还可以在某些缺乏重定向特性的系统中模拟它
//重定向输入让程序使用文件而不是键盘来输入,重定向输出让程序输出至文件而不是屏幕
//假设可执行文件名为echo_eof,文本文件名为words
//重定向输入:./echo_eof < words
//<是UNIX和DOS/Windows的重定向运算符,它使words文件与stdin流相关联
//因为C把文件和I/O设备放在一个层面,所以文件就是现在的I/O设备
//重定向输出:./echo_eof > mywords
//>是第2个重定向运算符,它创建了一个名为mywords的新文件,然后把echo_eof的输出重定向至该文件中
//重定向把stdout从显示设备赋给mywords文件
//组合重定向
//假设你希望制作一份mywords的副本:./echo_eof < mywords > savewords
//这样也可以,因为命令与重定向运算符的顺序无关:./echo_eof > savewords < mywords
//注意:在一条命令中,输入文件名和输出文件名不能相同,原因是在输入之前就已导致原文件长度被截断为0
//在UNIX、Linux或Windows/DOS系统中使用重定向运算符时,要遵循以下原则:
//1. 重定向运算符连接一个可执行程序(包括标准操作系统命令)和一个数据文件,不能用于连接一个数据文件和另一个数据文件,也不能用于连接一个程序和另一个程序。
//2. 使用重定向运算符不能读取多个文件的输入,也不能把输出定向至多个文件。
//3. 通常,文件名和运算符之间的空格不是必须的。
//UNIX、Linux或Windows/DOS还有>>运算符,它可以把数据添加到现有文件的末尾,而|运算符能把一个文件的输出连接到另一个文件的输入。
//验证输入:检验scanf的返回值(成功读取项的个数),添加char变量存储响应并用if语句筛选
//注意在处理复杂任务时,让函数把任务委派给另一个函数,这样可以让程序更模块化。
//混合使用getchar和scanf时,如果在调用getchar之前,scanf在输入行留下一个换行符,会导致一些问题。
}
函数与递归
示例代码
#include <stdio.h>
//#include "Chapter9.h"
long r_factorial(int);//尾递归
void print_binary(unsigned long);
unsigned long Fibonacci(unsigned);//双递归!
int main(void)
{
//函数原型(function prototype)告诉编译器函数的类型(和参数);可以省略参数变量名;函数的前置声明可以在主调函数外面或里面
//函数调用(function call)表明在此处执行函数
//函数定义(function definition)指出函数要做什么
//函数的返回值类型和函数接受的参数类型(形参列表),这些信息称为该函数的签名(signature)
//定义在函数中的变量是局部变量(local variable),该变量只属于该函数,在程序其他地方使用不会引起名称冲突。
//形式参数(formal parameter)也是局部变量。
//形参是被调函数中的变量,实参是主调函数赋给被调函数的具体值
//函数返回值不仅可以赋给变量,也可以被用作表达式的一部分;返回值可以是任意表达式的值
//返回值类型与声明的类型不匹配时,实际得到的返回值相当于把指定的返回值赋给与函数类型相同的变量所得到的值
//主调函数把它的参数存储在被称为栈(stack)的临时存储区,被调函数从栈中读取这些参数
//主调函数根据函数调用中的实参决定传递的类型,被调函数根据它的形参读取值
//对于较小的函数,可以把定义放在第一次调用前,省略原型
//【递归】P220
//C允许函数调用它自己,这种调用过程称为递归(recursion)
//结束递归是使用递归的难点,递归函数必须包含能让递归调用停止的语句
//可以使用循环的地方通常都可以使用递归
//每级递归的变量都属于本级递归私有
//每次函数调用都会返回一次,函数执行完毕后,控制权将被传回上一级递归,程序必须按顺序逐级返回递归
//递归函数中位于递归调用之后的语句,均按被调函数相反的顺序执行。这种特性在解决涉及相反顺序的编程问题时很有用。
//最简单的递归形式是把递归调用置于函数的末尾,即return语句之前,这种形式的递归被称为尾递归(tail recursion),它相当于循环
//一般而言选择循环比较好,每次递归都会创建一组变量,所以递归使用的内存更多,且每次递归调用都会把创建的一组新变量放在栈中,递归调用的数量受限于内存空间。
//每次函数调用要花费一定的时间,递归的执行速度也较慢。
printf("12 factorial = %ld\n", r_factorial(12));
printf("255 binary equivalent: ");
print_binary(255);
printf("\n1024 binary equivalent: ");
print_binary(1024);
putchar('\n');
//递归优点是为某些问题提供了最简单的解决方案,缺点是一些递归算法会快速消耗计算机内存资源,且递归不方便阅读和维护
//双递归(double recursion)变量的数量呈指数增长,会消耗大量内存
printf("Fibonacci 10:\n%lu\n", Fibonacci(10));
putchar('\n');
//C语言中每个函数都可以调用其他函数,或被其他函数调用
//把函数原型和已定义的符号常量放在头文件中是一个良好的编程习惯
}
long r_factorial(int n)
{
long answer;
if (n > 0)
answer = n * r_factorial(n - 1);
else
answer = 1;
return answer;
}
void print_binary(unsigned long n)
{
int r;
r = n % 2;
if (n >= 2)
{
print_binary(n / 2);
}
putchar(r == 0 ? '0' : '1');
return;
}
unsigned long Fibonacci(unsigned n)
{
if (n > 2)
return Fibonacci(n - 1) + Fibonacci(n - 2);
else
return 1;
}
数组和指针
示例代码
#include <stdio.h>
#define MONTHS 12
void call(int one) { printf("In call(), one = %d, &one = %p\n", one, &one); }
void interchange(int[], int*);
void arrsize(int arr[]) { printf("In arrsize(), the size of arr is %zd\n", sizeof(arr)); }//array是一个数组,arr是一个指向array数组首元素的指针,指针变量的大小是8字节
int sump(int*, int*);
int main(void)
{
//【查找地址:&运算符】P229
//指针(pointer)用于存储变量的地址,scanf函数就有使用地址作为参数(读取一个值,然后把该值存储到指定的地址上)
//概括地说,如果主调函数不使用return返回的值,则必须通过地址才能修改主调函数中的值
//一元运算符&给出变量的存储地址,可以把地址看作是变量在内存中的位置
int one = 1;
printf("In main(), one = %d, &one = %p\n", one, &one);
call(one);
//函数调用把实参的值传递给形参,这种传递只传递了值
//这样可以防止原始变量被被调函数中的副作用意外修改
//在一些语言中,子例程会影响主调例程的原始变量,它们的地址相同
//用return语句只能把被调函数中的一个值传回主调函数,要传回多个值,则要使用指针
//【指针简介】P231
//从根本上看,指针是一个值为内存地址的变量(或数据对象),指针变量的值是地址。
int* ptr;
ptr = &one;//ptr“指向”one,ptr是变量(可修改的左值),而&one是常量(右值)
//要创建指针变量,先要声明指针变量的类型
//声明指针变量时必须指定指针所指向变量的类型,因为不同的变量类型占用不同的存储空间,一些指针操作要求知道操作对象的大小,另外,程序必须知道存储在指定地址上的数据类型
//把ptr声明为存储int类型变量地址的指针,要使用间接运算符*
//【间接运算符*】P232
//间接运算符(indirection operator)有时也称为解引用运算符(dereferencing operator)
//后跟一个指针名或地址时,*给出存储在指针指向地址上的值
// ptr = &one; 和 val = *ptr; 放在一起相当于 val = one;
int val = *ptr;
printf("In main(), val = %d, &val = %p\n\n", val, &val);
//ptr指向的值(*ptr)是int类型,那么ptr本身是什么类型?
//我们描述它的类型是“指向int类型的指针”,ptr的值是一个地址,在大部分系统中,该地址由一个无符号整数表示
//但不要把指针认为是整数类型,一些处理整数的操作不能用来处理指针,所以指针实际上是一个新类型,不是整数类型
//【使用指针在函数间通信】P233
int x = 5, y = 10;
printf("In main(), Originally x = %d, y = %d\n", x, y);
interchange(&x, &y);//传递的是x,y的地址,形式参数必须是一个指向正确类型的指针
printf("In main(), Now x = %d, y = %d\n", x, y);
//简而言之,普通变量把值作为基本量,把地址作为通过&运算符获得的派生量
//而指针变量把地址作为基本量,把值作为通过*运算符获得的派生量
//【初始化数组】P238
//只存储单个值的变量有时也称为标量变量(scalar variable)
const int power[8] = { 1,2,4,6,8,16,32,64 };//初始化数组
//使用const声明只读数组
//推荐使用符号常量表示数组大小
//如果不初始化数组,数组元素和未初始化的普通变量一样,存储的都是垃圾值
//如果部分初始化数组,剩余的元素就会被初始化为0
//数组和其他变量类似,可以把数组创建成不同的存储类别(storage class)
//本章描述的数组属于自动存储类型,意思是这些数组在函数内部声明,且声明时未使用关键字static,对于一些其他存储类别的变量和数组,如果在声明时未初始化,编译器会自动把它们的值设置为零
//可以省略方括号中的数字,让编译器自动匹配数组大小和初始化列表中的项数
const int days[] = { 31,30 };
for (int i = 0; i < sizeof(days) / sizeof(days[0]); i++)
{
//循环测试条件:整个数组的大小 除以 数组中一个元素的大小(单位均为字节),得到数组元素的个数
}
//【指定初始化器(C99)】P241
//C99新特性:指定初始化器(designated initializer)
//初始化指定的数组元素
int arr[6] = { [5] = 123 };
//对于一般的初始化,在初始化一个元素后,未初始化的元素都会被设置为0
int arr2[6] = { [3] = 1,2,3,[4] = 1 };
//如果指定初始化器后面有更多的值,将被用于初始化指定元素后面的元素
//如果再次初始化指定的元素,那么最后的初始化将会取代之前的初始化
//如果未指定元素大小,编译器会把数组的大小设置为足够装得下初始化的值
for (int i = 0; i < 6; i++)
printf("%d%6d\n", i, arr2[i]);
//声明数组后可以借助下标(或索引)给数组元素赋值,C不允许把数组作为一个单元赋给另一个数组,除初始化外也不允许使用花括号列表赋值
//使用越界下标的结果是未定义的(不检查边界,C可以运行的更快)
//数组元素的编号从0开始,最好在声明数组时使用符号常量(而不是字面常量)表示数组大小
//C99引入了变长数组(variable-length array),C11将VLA改为可选特性
//【多维数组】P244
//在计算机内部,多维数组是按顺序存储的
const int array2[2][MONTHS] =
{
{1,2,3,4,5,6,7,8,9,10,11,12},
{1,2,3,4,5,6,7,8,9,10,11,12}
};//初始化二维数组
//初始化时也可以省略内部的花括号,但要保证初始化的数值个数正确,若数值不够,则按照顺序初始化,没有值初始化的元素统一初始化为0
//嵌套循环结构常用于处理多维数组,分别处理数组的不同下标
//可以把一维数组想象成一行数据,二维数组想象成数据表,三维数组想象成一叠数据表
int array[MONTHS] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
//【指针和数组】P248
//数组表示法其实是在变相地使用指针
//数组名是数组首元素的地址
if (array == &array[0])//两者都是常量
printf("\narray == &array[0]\n\n");
int* array_ptr = array;
printf("%-20s%-20s%-20s\n", "array_ptr:", "&array[0]:", "array");
printf("%-20p%-20p%-20p\n", array_ptr, &array[0], array);
printf("%-20s%-20s%-20s\n", "array_ptr + 1:", "&array[1]:", "array + 1:");
printf("%-20p%-20p%-20p\n\n", array_ptr + 1, &array[1], array + 1);
//在C中,指针加1指的是增加一个存储单元。对数组而言,这意味着加1后的地址是下一个元素的地址,而不是下一个字节的地址。这是为什么必须声明指针所指向对象类型的原因之一。
//指针的值是它所指向对象的地址。许多计算机都是按字节编址,即内存中的每个字节都按顺序编号。
//在指针前面使用*运算符可以得到该指针所指向对象的值
//指针加1,指针的值递增它所指向类型的大小(以字节为单位)
if (array + 2 == &array[2] && *(array + 2) == array[2])//相同的地址 && 相同的值
{
printf("array + 2 == &array[2]\n*(array + 2) == array[2]\n");
}
//可以使用指针标识数组的元素和获得元素的值
//既然能使用指针表示数组名,也可以用数组名表示指针
//因为数组名是该数组首元素的地址,作为实参的数组名要求形参是一个与之匹配的指针
printf("In main(), the size of array is %zd\n", sizeof(array));
arrsize(array);
//array是一个数组,arr是一个指向array数组首元素的指针,指针变量的大小是8字节
//【使用指针形参】P251
//可以用一个指针形参标识数组的开始,用一个整数形参表明数组元素个数
//也可以直接传递两个指针,分别指明数组的开始和结束处
int answer = sump(array, array + MONTHS);//因为下标从0开始,所以参数2指向数组末尾的下一个位置
printf("\nThe total number of array is %d\n\n", answer);
//使用这种“越界”指针的函数调用更为简洁
int data[2] = { 100,200 };
int moredata[2] = { 300,400 };
int* p1, * p2, * p3;
p1 = p2 = data;
p3 = moredata;
printf("*p1 = %d , *p2 = %d , *p3 = %d\n", *p1, *p2, *p3);
printf("*p1++ = %d , *++p2 = %d , (*p3)++ = %d\n", *p1++, *++p2, (*p3)++);
printf("*p1 = %d , *p2 = %d , *p3 = %d\n\n", *p1, *p2, *p3);
//只有(*p3)++改变了数组元素的值
//【指针表示法和数组表示法】
//C语言中,ar[i]和*(ar+i)这两个表达式是等价的(无论ar是数组名还是指针变量),但只有当ar是指针变量时,才能使用ar++这样的表达式
//【指针操作】P253
//赋值:把地址赋给指针。使用数组名、带地址运算符&的变量名、另一个指针进行赋值。
//解引用:*运算符给出指针指向地址上存储的值。
//取值:指针变量也有自己的地址和值。&运算符给出指针本身的地址。指针变量的值是它指向的地址。(ptr = array,&ptr是指向ptr的指针,ptr是指向array[0]的指针)
//指针与整数相加:整数与指针所指向类型的大小(以字节为单位)相乘,然后把结果与初始地址相加。(ptr + 4 == &array[4])
//递增指针:递增指向数组元素的指针可以让该指针移动至数组的下一个元素。(指针本身的地址不会变化)
//指针减去一个整数:从一个指针中减去一个整数(指针必须是第1个运算对象,整数是第2个运算对象),该整数乘以指针指向类型的大小(字节),然后用初始地址减去乘积。
//递减指针:前缀或后缀的递增和递减运算符都可以使用。
//指针求差:通常,求差的两个指针分别指向同一个数组的不同元素,计算求出两元素之间的距离。(差值的单位与数组类型的单位相同)
// 只要两个指针都指向相同的数组(或其中一个指针指向数组后面的第1个地址),C都能保证相减运算有效。
//比较:使用关系运算符可以比较两个指针的值,前提是两个指针都指向相同类型的对象。
//(指针加减:如果的结果超出初始指针指向的数组范围,计算结果是未定义的。除非正好超过数组末尾第一个位置,C保证该指针有效。)
//(减法有两种:指针减指针得到一个整数,指针减整数得到指针)
//千万不要解引用未初始化的指针!
//创建一个指针时,系统只分配了存储指针本身的内存,并未分配存储数据的内存,因此使用指针前必须先用已分配的地址初始化它
//【对形参使用const】P257
//函数传参时通常都是直接传递数值,只有程序需要在函数中改变该数值时才会传递指针。
//传递数组则必须传递指针,因为这样效率高,但传递地址可能会导致函数意外修改原始数据
//如果函数的意图不是修改数组中的数据内容,那么在函数原型和函数定义中声明形参时应使用关键字const
//使用const并不是要求原数组是常量,而是该函数在处理数组时将其视为常量,不可更改
//【指向const的指针】
//指向const的指针通常用于函数形参中,表明该函数不会使用指针改变数据
const int* const_ptr = array;//指向const的指针
const_ptr++;//可以让它指向别处
//const_ptr[0] = 1;
//*const_ptr = 1;//但不允许修改它所指向数据的值
//可以把const数据或非const数据的地址初始化为指向const的指针或为其赋值,但只能把非const数据的地址赋给普通指针
//否则,通过指针就能改变const数组中的数据
//因此,【对函数的形参使用const不仅能保护数据,还能让函数处理const数组】
//C标准规定,使用非const标识符修改const数据导致的结果是未定义的
//【const指针】
int* const con_pt = array;//不能指向别处的指针
//con_pt++;//不能指向别处
con_pt[0] = 1;
*con_pt = 1;//可以修改它所指向数据的值
//既不能更改指向的地址,也不能修改指向地址上的值
const int* const dcon = array;
//【指针和多维数组】P259
int zippo[4][2] = { {2,4},{6,8},{1,3},{5,7} };
//zippo是数组首元素的地址,zippo == &zippo[0]
//zippo[0]是一个内含2个整数的数组,所以zippo[0]的值和它首元素(&zippo[0][0])的地址相同
//zippo[0]是一个占用一个int大小对象的地址,zippo是一个占用两个int大小对象的地址
//由于这个整数和内含两个整数的数组都开始于同一个地址,所以zippo和zippo[0]的值相同
//给指针或地址加1,其值会增加对应类型大小的数值。
//zippo和zippo[0]不同,因为zippo指向的对象占用了两个int大小,而zippo[0]指向的对象只占用一个int大小
//因此,zippo + 1 和 zippo[0] + 1 的值不同
//解引用一个指针(*运算符)或在数组名后使用带下标的[]运算符,得到引用对象代表的值。
//因为zippo[0]是该数组首元素(zippo[0][0])的地址,所以*(zippo[0])表示存储在zippo[0][0]上的值
//*zippo代表该数组首元素(zippo[0])的值,但zippo[0]本身是一个int类型值的指针,该值的地址是&zippo[0][0],所以*zippo就是&zippo[0][0]
//**zippo与*&zippo[0][0]等价,这相当于zippo[0][0],即一个int类型的值
//简而言之,zippo是地址的地址,必须解引用两次才能获得原始值
//地址的地址或指针的指针是双重间接(double indirection)的例子
//增加数组维数会增加指针的复杂度
printf("zippo = %p , zippo + 1 = %p\n", zippo, zippo + 1);
printf("zippo[0] = %p , zippo[0] + 1 = %p\n", zippo[0], zippo[0] + 1);
printf("*zippo = %p , *zippo + 1 = %p\n", *zippo, *zippo + 1);
printf("zippo[0][0] = %d *zippo[0] = %d **zippo = %d\n", zippo[0][0], *zippo[0], **zippo);
printf("zippo[2][1] = %d *(*(zippo + 2) + 1) = %d\n\n", zippo[2][1], *(*(zippo + 2) + 1));//数组表示法和等价的指针表示法
//二维数组zippo的地址和一维数组zippo[0]的地址相同,都是各自数组首元素的地址
//int为4字节时,zippo[0]加1,其值加4。zippo加1,其值加8。
//【指向多维数组的指针】P261
//如何声明一个指针变量pz指向二维数组zippo
//把指针声明为指向int的类型还不够,因为指向int只能与zippo[0]的类型匹配
//zippo是它首元素的地址,该元素是一个内含两个int类型值的一维数组,因此pz必须指向一个内含两个int类型值的数组,而不是指向一个int类型值
int(*pz)[2];//把pz声明为指向一个数组的指针,该数组内含两个int类型值 //这行声明了1个指向数组的指针
int* pax[2];//pax是一个内含两个指针元素的数组,每个元素都指向int的指针 //这行声明了2个指向int的指针
pz = zippo;
printf("pz = %p , pz + 1 = %p\n", pz, pz + 1);
printf("pz[0] = %p , pz[0] + 1 = %p\n", pz[0], pz[0] + 1);
printf("*pz = %p , *pz + 1 = %p\n", *pz, *pz + 1);
printf("pz[0][0] = %d , *pz[0] = %d , **pz = %d\n", pz[0][0], *pz[0], **pz);
printf("pz[2][1] = %d , *(*(pz + 2) + 1) = %d\n", pz[2][1], *(*(pz + 2) + 1));
//虽然pz是一个指针,不是数组名,但也可以使用pz[2][1]这样的写法。
//可以用数组表示法或指针表示法来表示一个数组元素,既可以使用数组名,也可以使用指针名。
//zippo[m][n] == *(*(zippo + m) + n)
//pz[m][n] == *(*(pz + m) + n)
//指针间赋值比数值类型之间的赋值要严格。
//int** pp2;
//pp2 = zippo;
//pp2是指向指针的指针,它指向的指针指向int;而zippo是指向数组的指针,该数组内含2个int类型的元素。所以两者类型不同,不能赋值。
//const指针赋给非const指针不安全,非const指针赋给const指针没问题(前提是只进行一级解引用)
//多重解引用不安全:
const int** pp2;//指向const的指针
int* pp1;
const int n = 13;
printf("\nconst int n = 13\n");
pp2 = &pp1;//导致const限定符失效
*pp2 = &n;//导致pp1指向n
*pp1 = 10;//导致n的值改变
printf("n == %d\n", n);//结果未定义
//【函数和多维数组】P263
//一种处理二维数组的方法是,利用for循环把处理一维数组的函数应用到二维数组的每一行,但这种方法无法记录行和列的信息
//void somefunction(int(*pt)[4]);//方括号不能留空,否则编译器就不知道所指向的对象大小
//void somefunction(int pt[][4]);//声明形参的另一种语法,也可以在第一对方括号中写上大小,但编译器会忽略该值
//一般而言,声明一个指向N维数组的指针时,只能省略最左边方括号中的值(因为第一对方括号只用于表明这是一个指针,而其他方括号则用于描述指针所指向数据对象的类型)
//【变长数组】P266
//C11可选特性
//void sum2d(int rows, int cols, int ar[rows][cols]);
//void sum2d(int, int, int ar[*][*]);
//变长数组必须是自动存储类别,无论在函数中声明还是作为函数形参声明,都不能用static或extern存储类别说明符,而且不能在声明中初始化它们
//变长数组还允许动态内存分配
//【复合字面量】P269
//字面量是除符号常量外的常量。例如5是int类型字面量。
//复合字面量代表数组和结构内容。可以作为实参传递给带数组形参的函数。
// int diva[2] = { 10,20 }; //普通的数组声明
//(int[2]) { 10, 20 } //复合字面量,一个匿名数组(括号括起来类型名)
//复合字面量也可以省略大小
//(int[]) { 10, 20 }
//因为复合字面量是匿名的,所以必须在创建的同时使用它
//使用指针记录地址就是一种用法
int* clpt1;
clpt1 = (int[2]){ 10,20 };
int(*clpt2)[2];
clpt2 = (int[2][2]){ {1,2},{3,4} };
//复合字面量是提供只临时需要的值的一种手段
//复合字面量具有块作用域,一旦离开定义复合字面量的块,程序无法保证该字面量是否存在
}
int sump(int* start, int* end)//end指向的位置实际上在数组最后一个元素的后面。C保证在给数组分配空间时,指向数组后面第一个位置的指针仍是有效的指针。(但并未保证存储在该位置上的值,故不能访问这个位置)
{
int total = 0;
//指针形参是变量,可以用索引表明访问数组中的哪一个元素
while (start < end)//循环最后处理的元素是end所指向位置的前一个元素
{
total += *start;
start++;//递增指针变量,让指针指向下一个元素
//start是指向int的指针,递增1相当于其值递增int类型的大小
}
return total;
}
//只有在函数原型或函数定义头中才可以用int u[](只能用于声明形式参数)代替int* u
void interchange(int u[], int* v)//u指向x
{
int temp;
temp = *u;
*u = *v;//存储到u指向的位置
*v = temp;
}
字符串和字符串函数
示例代码
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <stdlib.h>
#define MSG "Symbolic string constant."
char* s_gets(char*, int);
char* s_gets_2(char*, int);
int put1(const char*);
void fit(char*, unsigned int);
int main(int argc, char* argv[])
{
//字符串是以空字符\0结尾的char类型数组
char words[] = "String in array.";
const char* pt1 = "Pointing.";
puts("Hi.");
puts(MSG);
puts(words);
puts(pt1);
//puts()只显示字符串,且自动在显示的字符串末尾加上换行符
//双引号括起来的内容称为字符串字面量(字符串常量)
//双引号中的字符和编译器自动加入末尾的\0字符,都作为字符串存储在内存中
puts("\"Test,""Test\"");//字符串内使用双引号需用反斜杠转义
//如果字符串字面量之间没有间隔或用空白字符分隔,C会将其视为串联起来的字符串字面量
//字符串常量属于静态存储类别(static storage class)
//如果在函数中使用字符串常量,该字符串只会被存储一次,在整个程序的生命期内存在,即使函数被调用多次
//用双引号括起来的内容被视为指向该字符串存储位置的指针
printf("%s, %p, %c\n", "We", "are", *"here");
//%p打印该字符串首字符的地址
//在指定数组大小时,要确保数组的元素个数至少比字符串长度多1(容纳空字符),所有未被使用的元素都被自动初始化为\0空字符
char car[10] = "Tata";
if (car == &car[0] && *car == 'T' && *(car + 1) == car[1] && car[1] == 'a')
{
puts("char car[10] = \"Tata\";");
puts("car == &car[0] *car == 'T' *(car + 1) == car[1] car[1] == 'a'");
}
//【数组和指针】P277
const char ar0[] = "Something is pointing at me.";
const char* pt0 = "Something is pointing at me.";
//数组形式(ar1[])在内存中分配为一个内含29个元素的数组(加上末尾的空字符),每个元素被初始化为字符串字面量对应的字符
//通常字符串作为可执行文件的一部分存储在数据段中,当把程序载入内存时,也载入了程序中的字符串,字符串存储在静态存储区(static memory)中
//但是,程序在开始运行时才会为该数组分配内存,此时才将字符串拷贝到数组中
//【注意】,此时字符串有两个副本。一个是在静态内存中的字符串字面量,另一个是存储在ar1数组中的字符串
//
//此后,编译器便把数组名ar1识别为该数组首元素地址(&ar1[0])的别名。
//「在数组形式中,ar1是地址常量,不能更改ar1」
//指针形式(*pt1)也使得编译器为字符串在静态存储区预留29个元素的空间
//一旦开始执行程序,它会为指针变量pt1留出一个存储位置,并把字符串的地址存储在指针变量中
//「该变量最初指向该字符串的首字符,但它的值可以改变」
//【字符串字面量被视为const数据】,所以应该把pt1声明为指向const数据的指针(不能用pt1改变它所指向的数据,但可以改变pt1的值即它指向的位置)
//如果把一个字符串字面量拷贝给一个数组,就可以随意改变数据,除非把数组声明为const
//总结:初始化数组把静态存储区的字符串拷贝到数组中,初始化指针只把字符串的地址拷贝给指针
char ar[] = MSG;
const char* pt = MSG;
printf("\naddress of \"Symbolic string constant.\": %p\n", "Symbolic string constant.");
printf(" address MSG: %p\n", MSG);
printf(" address pt: %p\n", pt);
printf("address of \"Symbolic string constant.\": %p\n", "Symbolic string constant.");//编译器可以把多次使用的相同字面量存储在一处或多处
printf(" address ar: %p\n", ar);
//静态数据使用的内存与ar使用的动态内存不同。不仅值不同,特定编译器甚至使用不同的位数表示两种内存。
//数组名ar是常量,而指针名pt是变量
//只有指针表示法可以进行递增操作
//可以让指针指向数组 pt = ar; 但不能反过来
//pt = ar; 不会导致pt指向的字符串消失,只是改变了存储在pt中的地址。除非已经保存了字符串的地址,否则当pt指向别处时,就无法再访问该字符串。
//可以改变数组中元素的信息,数组的元素是变量(除非数组被声明为const),但是数组名不是变量
//通过未使用const限定符的指针pt修改字符串的行为是未定义的(可能影响所有使用该字符串的代码),因此建议把指针初始化为字符串字面量时使用const限定符
//【字符串数组】P280
const char* strarpt[2] = { //指向字符串的指针数组
"strarpt1",
"strarpt2"
};//strarpt数组是一个内含2个指针的数组,占用16字节
char strar[2][10] = { //char类型数组的数组
"strarpt1",
"strarpt2"
};//strar是一个内含2个数组的数组,每个数组内含10个char类型的值,占用20字节
//strarpt中的指针指向初始化时所用的字符串字面量的位置,这些字符串字面量被存储在静态内存中
//strar中的数组则存储着字符串字面量的副本,所以每个字符串都被存储了两次
//此外,为字符串数组分配内存的使用率极低,strar中的每个元素的大小必须相同,而且必须是能存储最长字符串的大小
//如果要用数组表示一系列待显示的字符串,请使用指针数组,因为它比二维字符数组的效率高
//但指针指向的字符串字面量不能更改,如果要改变字符串或为字符串输入预留空间,不要使用指向字符串字面量的指针
//【指针和字符串】P281
const char* mesg = "TEST";
const char* copy;
copy = mesg; //未拷贝字符串,只是让copy也指向mesg指向的字符串
printf("\n&mesg = %p; value = %p\n", &mesg, mesg);
printf("© = %p; value = %p\n\n", ©, copy);
//【字符串输入】P282
//第一步分配空间,最简单的方法是声明时显式指明数组大小
char ws[128];
printf("ws[128], sizeof(ws):%llu\n", sizeof(ws));
//读入字符串函数:scanf、gets、fgets
//scanf和转换说明%s只能读取一个单词
//gets读取整行输入,直至遇到换行符,然后丢弃换行符,存储其余字符,并在末尾添加一个空字符
//数组名会被转换成该数组首元素的地址,因此gets只知道数组的开始处,如果输入过长,会导致缓冲区溢出(buffer overflow),C11标准废除了gets函数
//fgets和fputs专门设计用于处理文件输入输出
//fgets函数的第2个参数指明了读入字符的最大数量,如果该参数的值是n,则fgets将读入n-1个字符,或者读到遇到的第一个换行符为止
//如果fgets读到一个换行符,会把它存储在字符串中(gets会丢弃换行符)
//第3个参数指明读入的文件
fgets(ws, 128, stdin);
fputs(ws, stdout); //不会在字符串末尾添加换行符
//fgets函数返回指向char的指针,如果一切顺利,返回的地址与传入的第1个参数相同
//如果读到文件结尾,它返回空指针(null pointer),该指针保证不会指向有效的数据,在代码中可以用数字0代替,但在C语言中用宏NULL代替更常见(如果读入数据时出现某些错误,该函数也返回NULL)
while (fgets(ws, 10, stdin) != NULL && ws[0] != '\n')
{
fputs(ws, stdout);
}
//以上循环如果输入过长字符串,fgets先读取一部分,存储,fputs打印(未换行),fgets再继续从剩余输入中读入数据
//系统使用缓冲的I/O,意味着用户在按下Return之前输入都被存储在缓冲区,按下Return后在输入中增加一个换行符,并把整行输入发送给fgets
//对于输出,fputs把字符发送给另一个缓冲区,当发送换行符时,缓冲区的内容被发送至屏幕上
//存储换行符的好处是检查字符串末尾是否有换行符就可以判断是否读取了一整行
int i = 0;
while (ws[i] != '\n') //可以这样处理掉换行符
i++;
ws[i] = '\0';
//空字符是整数类型,空指针是指针类型;对空指针求值为0(即为假)
gets_s(ws, sizeof(ws)); //stdio.h的可选扩展函数(C11)
puts(ws); //在字符串末尾加上换行符
//gets_s只从标准输入中读取数据,读到换行符会丢弃它
//gets_s读到最大字符数都没有读到换行符,会执行以下几步:
//首先把目标数组中的首字符设置为空字符,读取并丢弃随后的输入直至读到换行符或文件结尾,然后返回空指针。接着调用依赖实现的“处理函数”(或你选择的其他函数)
//fgets通常是最佳选择
//scanf和(f)gets的区别在于如何确定字符串的末尾
//如果预留的存储区装得下输入行,(f)gets会读取第1个换行符之前所有的字符
//scanf从第1个非空白字符作为字符串的开始,如果使用%s转换说明,以下一个空白字符作为字符串的结束(字符串不包括空白字符)
//scanf也可能导致数据溢出,但在转换说明中使用字段宽度可以防止溢出
//【字符串输出】P289
//3个标准库函数用于打印字符串:puts、fputs、printf
//puts函数只需把字符串的地址作为参数传递给它即可,该函数遇到空字符时就停止输出
//用双引号括起来的内容是字符串常量,且被视为该字符串的地址
puts(&words[3]);
puts(pt1 + 2);
//char dontdothis[] = { 'H','e','y' ,'!' };
//puts(dontdothis);
//fputs的第2个参数指明要写入数据的文件,fputs不会在输出的末尾添加换行符
//注意:gets丢弃输入中的换行符,puts在输出中添加换行符;fgets保留输入中的换行符,fputs不在输出中添加换行符
//【自定义输入输出函数】P291
printf("%d\n", put1("Test\n"));
//【字符串函数】P293
//string.h头文件中
//常用的有strlen、strcat、strcmp、strncmp、strcpy、strncpy
char mesg2[] = "TEST Message";
fit(mesg2, 4); //strlen函数统计字符串的长度,不包括末尾的空字符
puts(mesg2);
puts(mesg2 + 5);
//strcat(concatenate)用于拼接字符串,该函数接受两个字符串作为参数,把第2个字符串的备份附加在第1个字符串末尾,返回第1个字符串的地址
//第2个字符串的第1个字符将覆盖第1个字符串末尾的空字符
char addon[] = "addon";
strcat_s(mesg2, sizeof(mesg2), addon);
puts(mesg2);
puts(addon);
//strcat函数无法检查第1个数组能否容纳第2个字符串,strncat函数的第3个参数指定了最大添加字符数
//不会拷贝第2个字符串中空字符和其后的字符,并在拷贝字符的末尾添加一个空字符
fit(mesg2, 4);
strncat_s(mesg2, sizeof(mesg2), addon, 3);
puts(mesg2);
puts(addon);
//strcmp函数用于字符串比较,如果两个字符串参数相同,该函数返回0,否则返回非零值(即为真)
char pwd[64];
char pass[] = "admin";
puts("PassWord: ");
gets_s(pwd, 64);
while (strcmp(pwd, pass)) //不相同为真
{
puts("Wrong.");
gets_s(pwd, sizeof(pwd));
}
//strcmp比较的是字符串,不是整个数组,所以可以用它比较存储在不同大小数组中的字符串
//strcmp函数比较字符串中的字符,直到发现不同的字符为止
//strncmp函数比较两个字符串时,可以比较到字符不同的地方,也可以只比较第3个参数指定的字符数
const char* list[6] =
{
"astronomy", "astounding",
"astrophysics", "ostracize",
"asterism", "astrophobia"
};
int countn = 0;
for (int i = 0; i < 6; i++)
{
if (strncmp(list[i], "astro", 5) == 0)
{
printf("Found: %s\n", list[i]);
countn++;
}
}
printf("%d words beginning with astro.\n", countn);
//strcpy用于拷贝字符串
char temp[5]; //声明数组将分配存储数据的空间,而声明指针只分配存储一个地址的空间
s_gets(temp, 5);
strcpy_s(pass, sizeof(pass), temp); //目标字符串、源字符串
puts(pass);
//strcpy返回第1个参数的值,即一个字符的地址
//第1个参数不必指向数组的开始,可以拷贝数组的一部分;strcpy把源字符串中的空字符也拷贝在内
//strcpy和strcat有相同的问题,它们都不能检查目标空间能否容纳源字符串的副本
//strncpy函数的第3个参数指明可拷贝的最大字符数
strncpy_s(pass, sizeof(pass), "password", 6 - 1);
pass[5] = '\0'; //这样做确保存储的是一个字符串
puts(pass);
//如果拷贝到第n个字符时还未拷贝完整个源字符串,就不会拷贝空字符,所以拷贝的副本中不一定有空字符
//sprintf函数声明在stdio.h中
//该函数和printf类似,但它是把数据写入字符串,该函数可以把多个元素组合成一个字符串
//sprintf的第1个参数是目标字符串的地址,其余参数和printf相同,即格式字符串和待写入项的列表
sprintf_s(pass, sizeof(pass), "%d%.2f", 5, 3.152);
puts(pass);
//strchr函数查找字符串中是否包含某字符,返回指向字符串首次出现该字符的指针(空字符也在查找范围),如果字符串中无该字符,返回空指针
char* cache;
cache = strchr(pass, '1');
printf("%p: %c\n", cache, *cache);
//strrchr函数返回字符串中某字符最后一次出现的位置,未找到返回空指针
cache = strrchr(pass, '5');
printf("%p: %c\n", cache, *cache);
//strpbrk查找字符串中是否包含另一字符串中的任意字符,如果有返回指向被查找字符串中第一个匹配位置的指针,否则返回空指针
cache = strpbrk(pass, "1.1");
printf("%p: %c\n", cache, *cache);
//strstr函数返回指向第1个字符串中第2个字符串出现的首位置,未找到返回空指针
cache = strstr(pass, "3.1");
if (cache)
{
printf("%p: %c\n", cache, *cache);
}
//【ctype.h字符函数和字符串】P310
//可以使用字符相关函数处理字符串中的字符
//ctype.h中的函数通常作为宏(macro)来实现
char uptest[6] = "admin";
uptest[0] = toupper(uptest[0]);
puts(uptest);
//【命令行参数】P311
//C编译器允许main没有参数或者有两个参数 main(int argc, char* argv[])
//main有两个参数时,第1个参数是命令行中的字符串数量(argument count 参数计数)
//程序把命令行字符串存储在内存中,并把每个字符串的地址存储在指针数组中,该数组的地址则被存储在main的第2个参数中(argument value 参数值)
//程序本身的名称赋给argv[0],然后把随后的第1个字符串赋给argv[1],以此类推
printf("The command line has %d arguments\n", argc - 1);
for (int i = 1; i < argc; i++)
{
printf("%d: %s\n", i, argv[i]);
}
//argv也可以声明为char** argv,这与char* argv[]等价,argv是一个指向指针的指针
//许多环境都允许用双引号把多个单词括起来形成一个参数
//【把字符串转换为数字】P313
//C有一些函数专门用于把字符串形式转换成数值形式
//atoi函数(ASCII to Integer)用于把字母数字转换成整数,该函数接受一个字符串作为参数,返回相应的整数值
printf("%d\n", atoi("11test") + atoi("11number")); //stdlib.h
//如果参数不是数字,函数行为是未定义的,使用有错误检测的strtol函数会更安全
//stdlib.h中还包含了atof和atol函数的原型,atof把字符串转换成double类型,atol转换成long类型
//strtol函数把字符串转换成long,strtoul转换成unsigned long,srttod转换成double
//这套函数可以识别和报告字符串中的首字符是否是数值,且strtol和strtoul还可以指定数字的进制
//参数:指向待转换字符串的指针、被设置为标识输入数字结束字符的地址、以什么进制输入数字
char* end;
long longv = strtol("10", &end, 16); //base 16 input, base 10 output
//此时end指向空字符
printf("%ld\n", longv);
longv = strtol("10atom", &end, 10); //base 10 input, base 10 output
//此时end的值是'a'字符的地址
printf("%ld\n", longv);
longv = strtol("10atom", &end, 16);
//此时10a被看作是16进制数转换为266,end指向't'
printf("%ld\n", longv);
//strtol最多可以转换三十六进制,'a'~'z'字符都可用作数字
//strtod函数只以十进制转换,所以只需要两个参数
//许多实现使用itoa和ftoa函数把整数和浮点数转换成字符串,但它们不是C标准库的成员,可以用sprintf函数代替它们,sprintf兼容性更好
}
char* strtest(const char* str)
{
return (char*)str; //通过强制类型转换取消const
}
//【字符串排序】P307
void string_handler(char** string, int str_num) //冒泡
{
char* temp;
for (int i = 0; i < str_num - 1; i++)
{
for (int j = 0; j < str_num - 1 - i; j++)
{
if (*string[j] > *string[j + 1])
{
temp = string[j];
string[j] = string[j + 1];
string[j + 1] = temp;
}
}
}
for (int i = 0; i < str_num; i++)
{
puts(string[i]);
}
}
void string_handler_2(char** string, int str_num) //选择
{
char* temp;
for (int i = 0; i < str_num - 1; i++)
{
int min = i;
for (int j = i + 1; j < str_num; j++)
{
if (*string[j] < *string[min])
{
min = j;
}
}
if (min != i)
{
temp = string[i];
string[i] = string[min];
string[min] = temp;
}
}
for (int i = 0; i < str_num; i++)
{
puts(string[i]);
}
}
char* s_gets(char* st, int n)
{
char* ret_val;
int i = 0;
ret_val = fgets(st, n, stdin);
if (ret_val) // ret_val != NULL 读到文件结尾或出错
{
while (st[i] != '\n' && st[i] != '\0')
{
i++;
}
if (st[i] == '\n')
{
st[i] = '\0';
}
else
{
while (getchar() != '\n')
continue;
}
}
return ret_val;
}
char* s_gets_2(char* st, int n)
{
char* ret_val, * find;
int i = 0;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
find = strchr(ret_val, '\n');
if (find)
{
*find = '\0';
}
else
{
while (getchar() != '\n')
continue;
}
}
return ret_val;
}
int put1(const char* string) //形参用*可以提醒用户实参不一定是数组
{
int count = 0;
while (*string) //*string != '\0' 指向空字符时*string的值是0,即为假
{
putchar(*string++); //因为要从右向左运算,因此这条语句打印string指向的值,递增string本身而不是递增它指向的字符
count++;
}
return(count);
}
void fit(char* string, unsigned int size)
{
if (strlen(string) > size)
{
string[size] = '\0';
}
}
存储类别、链接和内存管理
示例代码
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
int test; //外部链接文件作用域;外部定义的变量;未初始化会被自动初始化为0
int iarr[128];
extern char tc; //如果tc被定义在另一个文件,则必须这样声明
static int get = 0; //内部链接文件作用域
static unsigned long int next = 1;//随机数种子,具有内部链接的静态变量
void trystat(void);
unsigned int rand0(void);
int main(int argc, char* argv[])
{
//【存储类别】P320
//C提供多种不同的模型或存储类别(storage class)在内存中存储数据
//从硬件上看,被存储的每个值都占用一定的物理内存,C语言把这样的一块内存称为对象(object)
//一个对象可能并未存储实际的值,但它在存储适当的值时一定具有相应的大小
//从软件上看,程序需要一种方法访问对象,这可以通过声明变量来完成
//声明创建了一个标识符(identifier),标识符是一个名称,可以用来指定(designate)特定对象的内容,标识符遵循变量的命名规则
int int1 = 1;
//标识符int1即是软件指定硬件内存中对象的方式,该声明还提供了存储在对象中的值
//变量名不是指定对象的唯一途径
int* pt1 = &int1;
//pt1是一个标识符,它指定了一个存储地址的对象
//但表达式*pt1不是标识符,因为它不是一个名称,但它确实指定了一个对象,它与int1指定的对象相同
int arr1[10];
//arr1的声明创建了一个可容纳10个int类型元素的对象,该数组的每个元素也是一个对象
//一般而言,指定对象的表达式被称为左值
//如果可以使用左值改变对象中的值,该左值就是一个可修改的左值(modifiable lvalue)
const char* pc = "Behold a string literal!";
//字符串字面量就是一个对象,由于字符串字面量中的每个字符都能被单独访问,所以每个字符也是一个对象
//该声明还创建了一个标识符为pc的对象,存储着字符串的地址,标识符pc是一个可修改的左值
//*pc指定了存储'B'字符的数据对象,所以*pc是一个左值,但不可修改
//字符串字面量本身指定了存储字符串的对象,所以它也是一个左值,但不可修改
//==================================================
//可以用存储期(storage duration)描述对象,存储期指对象在内存中保留了多长时间
//标识符用于访问对象,可以用作用域(scope)和链接(linkage)描述标识符,作用域和链接表明了程序的哪些部分可以使用它
//不同存储类别具有不同存储期、作用域和链接
//标识符可以在源代码的多文件中共享、可用于特定文件的任意函数中、可仅限于特定函数中使用,甚至只在函数中的某部分使用
//对象可存在于程序的执行期,也可以仅存在于它所在函数的执行期
//对于并发编程,对象可以在特定线程的执行期存在
//可以通过函数调用的方式显式分配和释放内存
//==================================================
//【作用域】P321
//作用域描述程序中可访问标识符的区域
//一个C变量的作用域可以是 块作用域、函数作用域、函数原型作用域、文件作用域
//块是用一对花括号括起来的代码区域。(例如,整个函数体是一个块,函数中的任意复合语句也是一个块)
//定义在块中的变量具有块作用域(block scope)
//块作用域变量的可见范围是从定义处到包含该定义的块的末尾
//函数的形式参数声明虽然在函数的左花括号之前,但它们也具有块作用域,属于函数体这个块
//声明在内层块中的变量,其作用域仅局限于该声明所在的块
for (int i = 0; i < 2; i++)
printf("A C99 feature: i = %d\n", i);
//C99允许在块中的任意位置声明变量,C99把块的概念扩展到包括循环和if语句所控制的代码,即使没有用花括号括起来也算是块的一部分
//函数作用域(function scope)仅用于goto语句的标签
//即使一个标签首次出现在函数的内层块中,它的作用域也延伸至整个函数
//函数原型作用域(function prototype scope)用于函数原型中的形参名
//函数原型作用域的范围是从形参定义处到原型声明结束,这意味着形参名通常无关紧要,即使有形参名也不必与函数定义中的形参名相匹配;只有在变长数组中形参名才有用
//定义在函数外面的变量具有文件作用域(file scope)
//从它的“定义处”到该定义所在文件的末尾均可见
//由于这样的变量可用于多个函数,所以文件作用域变量也称为全局变量(global variable)
//C预处理实际上是用包含的头文件内容替换#include指令,所以编译器把源代码文件和所有的头文件都看成是一个包含信息的单独文件
//这个文件被称为翻译单元(translation unit)
//描述一个具有文件作用域的变量时,它的实际可见范围是整个翻译单元
//如果程序由多个源代码文件组成,那么该程序也将由多个翻译单元组成,每个翻译单元均对应一个源代码文件和它所包含的文件
//【链接】P322
//C变量有3种链接属性:外部链接、内部链接、无链接(链接描述定义在程序某翻译单元中的变量可被链接的程度)
//具有块作用域、函数作用域、函数原型作用域的变量都是“无链接变量”,这些变量属于定义它们的块、函数、原型私有
//具有文件作用域的变量可以是“外部链接”或“内部链接”,外部链接变量可以在多文件程序中使用,内部链接变量只能在一个翻译单元中使用
//一些程序员把“内部链接的文件作用域”简称为“文件作用域”
//把“外部链接的文件作用域”简称为“全局作用域”或“程序作用域”
//外部定义中使用了存储类型说明符static的是内部链接文件作用域,属文件私有
//【存储期】P323
//作用域和链接描述了标识符的可见性,存储期描述了通过这些标识符访问的对象的生存期
//C对象有4种存储期:静态存储期、线程存储期、自动存储期、动态分配存储期
//如果对象具有“静态存储期”,那么它在程序的执行期间一直存在
//所有的文件作用域变量都具有静态存储期(static表明了链接属性而非存储期)
//“线程存储期”用于并发程序设计,从被声明时到线程结束一直存在
//以关键字 _Thread_local 声明一个对象时,每个线程都获得该变量的私有备份
//块作用域的变量通常都具有“自动存储期”
//程序进入定义这些变量的块时,为这些变量分配内存;退出块时,释放刚刚分配的内存
//变长数组的存储期从声明处到块末尾,而不是块的开始处到块末尾
//块作用域变量也能具有静态存储期
//在声明在块中的变量前加上关键字static
static int ct = 0;
//变量ct存储在静态内存中,它从程序被载入到程序结束期间都存在
//但它的作用域定义在函数块中,只有在执行该函数时,程序才能使用ct访问它所指定的对象
//(但是,该函数可以给其他函数提供该存储区的地址以便间接访问该对象,例如通过指针形参或返回值)
//“动态分配存储期”在后面介绍
//【存储类别】
//5种存储类别:自动、寄存器、静态块作用域、静态外部链接、静态内部链接
//[自动变量]:自动存储期、块作用域、无链接
//默认情况下,声明在块或函数头中的任何变量都属于自动存储类别
//也可以显式使用关键字auto
//auto int autoint;
//auto是存储类别说明符(storage-class specifier),auto的用法在C++中完全不同,如果编写C/C++兼容程序,最好不要用auto作为存储类别说明符
//自动变量不会初始化,除非显式初始化它
//如果内层块中声明的变量与外层块中的变量同名,内层块会隐藏外层块的定义,离开内层块后恢复
//在while循环中定义变量,每轮迭代结束后,变量就消失
while (ct++ < 2)
{
int ct = 0;
ct++;
printf("%d\n", ct);
}
//整个循环是它所在块的子块(sub-block),循环体是整个循环块的子块
//[寄存器变量]:和自动变量一样
//变量通常存储在计算机内存中,如果幸运的话,寄存器变量存储在CPU的寄存器中,或者概括的说,存储在最快的可用内存中
//与普通变量相比,访问和处理这些变量的速度更快;由于寄存器变量存储在寄存器而非内存中,所以无法获取寄存器变量的地址
//使用存储类别说明符register可用声明寄存器变量
register int quick;
//声明变量为register类别与直接命令相比更像是一种请求
//编译器必须根据寄存器或最快可用内存的数量衡量你的请求,或者直接忽略你的请求,所以可能不会如你所愿
//在这种情况下,寄存器变量就变成普通的自动变量,但即使是这样,仍然不能对该变量使用地址运算符
//在函数头中使用关键字register,便可请求形参是寄存器变量
//可声明为register的数据类型有限,例如处理器中的寄存器可能没有足够大的空间来存储double类型的值
//[块作用域的静态变量]:静态存储期、块作用域、无链接
//静态的意思是该变量在内存中原地不动
//这种变量和自动变量一样,具有相同的作用域,但程序离开它们所在的函数后,这些变量不会消失,计算机在多次函数调用之间会记录它们的值
//在块中以存储类别说明符static(提供静态存储期)声明这种变量
trystat();//stay只在编译trystat()时被初始化一次
trystat();
//如果未显式初始化静态变量,它们会被初始化为0
//静态变量和外部变量在程序被载入内存时已执行完毕,把声明放在trystat()函数中是为了告诉编译器只有trystat()函数才能看到该变量,这条声明并未在运行时执行
//不能在函数的形参中使用static
//“局部静态变量”是描述具有块作用域的静态变量的另一个术语
//[外部链接的静态变量]:静态存储期、文件作用域、外部链接
//该类别有时称为外部存储类别(external storage class),属于该类别的变量称为外部变量(external variable)
//把变量的定义性声明(defining declaration)放在所有函数的外面便创建了外部变量
//为了指出该函数使用了外部变量,可用在函数中用关键字extern再次声明
extern int iarr[]; //可选的声明,不用指明数组大小
//如果省略掉上面的extern,则创建一个独立的局部变量;块作用域中的变量将“隐藏”文件作用域中的同名变量,如果不得已要声明同名变量,可在局部变量中用auto存储类别说明符明确表达这种意图
//如果一个源代码文件使用的外部变量定义在另一个源代码文件中,则必须用extern在该文件中声明该变量
//与自动变量不同,如果未初始化外部变量,它们会被自动初始化为0,这也适用于外部定义的数组元素
//与自动变量不同,只能使用常量表达式初始化文件作用域变量
//int tern = 1; //定义式声明(defining declaration)
//main() { extern int tern; //引用式声明(referencing declaration)
//第1次声明为变量预留了存储空间,该声明构成了变量的定义
//第2次声明不是定义,它指示编译器去别处查询其定义
//extern int tern;
//int main(void) {
//编译器会假设tern实际的定义在该程序的别处,也许在别的文件中,该声明不会引起分配存储空间(不要用extern创建外部定义)
//外部变量只能初始化一次,且必须在定义该变量时进行
//[内部链接的静态变量]:静态存储期、文件作用域、内部链接
//在所有函数外部用存储类别说明符static定义的变量具有这种存储类别
//普通的外部变量可用于同一程序中任意文件中的函数,但内部链接的静态变量只能用于同一个文件中的函数
//可用使用extern在函数中重复声明任何具有文件作用域的变量,这不会改变它们的链接属性
//【多文件】P331
//C通过在一个文件中进行定义式声明,在其他文件中进行引用式声明来实现共享一个外部变量
//除了一个定义式声明外,其他声明都要使用extern关键字,且只有定义式声明才能初始化变量
//如果外部变量定义在一个文件中,其他文件在使用该变量之前必须先声明它
//【存储类别说明符】P332
//C语言有6个关键字作为存储类别说明符:auto、register、static、extern、_Thread_local、typedef
//typedef关键字与任何内存存储无关,把它归于此类有一些语法上的原因
//不能在声明中使用多个存储类别说明符,不能使用多个存储类别说明符作为typedef的一部分
//唯一例外是_Thread_local,它可用和static或extern一起使用
//auto说明符表面变量是自动存储期,只能用于块作用域的变量声明中,由于在块中声明的变量本身就具有自动存储期
//所以使用auto主要是为了明确表达要使用与外部变量同名的局部变量的意图
//register说明符也只用于块作用域的变量,它把变量归为寄存器存储类别,请求最快速度访问该变量
//同时,还保护了该变量的地址不被获取
//static说明符创建的对象具有静态存储期,载入程序时创建对象,程序结束时对象消失
//如果static用于文件作用域声明,作用域受限于该文件;如果用于块作用域声明,作用域则受限于该块
//只要程序在运行对象就存在并保留其值!但只有在执行块内代码时才能通过标识符访问
//块作用域的静态变量无链接,文件作用域的静态变量具有内部链接
//extern说明符表明声明的变量定义在别处,如果包含extern的声明具有文件作用域,则引用的变量必须具有外部链接
//【存储类别和函数】P334
//函数也有存储类别,可以是外部函数(默认)、静态函数,C99新增了第3种类别——内联函数
//外部函数可以被其他文件的函数访问,静态函数只能用于其定义所在的文件
//以static存储类别说明符创建的函数属于特定模块私有,这样做可以避免名称冲突
//通常的做法是:用extern关键字声明定义在其他文件中的函数,这样是为了表明当前文件种使用的函数被定义在别处
//除非使用static关键字,否则一般函数声明都默认为extern
//【存储类别的选择】P334
//随意使用外部存储类别的变量导致的后果远远超过它所带来的便利
//唯一例外的是const数据,不用担心它们被意外篡改
//“按需知道”原则:尽量在函数内部解决该函数的任务,只共享那些需要共享的变量
//【随机数函数和静态变量】P334
//库提供了rand()函数生成随机数,rand()是“伪随机数生成器”,意思是可预测生成数字的实际序列,但数字在其取值范围内均匀分布
next = (unsigned long)time(0);//空指针0
for (int i = 0; i < 5; i++)
{
printf("%d ", rand0());
}
int roll;
srand((unsigned)time(0));//初始化随机数发生器
roll = (rand() % 6) + 1;
printf("\n%d\n", roll);
//随机数求模6,获得的整数在0~5之间,结果+1,新值在1~6之间
//【分配内存:malloc()和free()】P340
//前面讨论的存储类别都是在确定用哪种存储类别后,根据已制定好的内存管理规则,自动选择其作用域和存储期
//还有更灵活的选择,即用库函数分配和管理内存
//所有程序都必须预留足够的内存来存储程序使用的数据,这些内存中有些是自动分配的
//静态数据在程序载入内存时分配,自动数据在程序执行块时分配,并在程序离开该块时销毁
//C可以在程序运行时分配更多的内存,主要的工具是malloc(memory allocation)函数
//该函数接受一个参数——所需的内存字节数,malloc函数会找到合适的空闲内存块,这样的内存是匿名的
//malloc分配内存,但是不会为其赋名,但它返回动态分配内存块的首字节地址,可以把该地址赋给一个指针变量
//因为char表示1字节,malloc的返回类型通常被定义为指向char的指针,但从ANSIC标准开始,C使用一个新的类型——指向void的指针,该类型相当于一个“通用指针”
//malloc函数可用于返回指向数组的指针、指向结构的指针等,所以通常该函数的返回值会被强制转换为匹配的类型(在ANSIC中,应该坚持使用强制类型转换,提高代码的可读性)
//但把指向void的指针赋给任意类型的指针完全不用考虑类型匹配的问题
//如果malloc分配内存失败,返回空指针
double* ptd, * ptd2;
ptd = (double*)malloc(30 * sizeof(double));
//以上代码为30个double类型的值请求内存空间,并设置ptd指向该位置
//让ptd指向这个块的首元素,便可像使用数组名一样使用它
//现在我们有3种创建数组的办法:
//·声明数组时,用常量表达式表示数组的维度,用数组名访问数组的元素;可以用静态内存或自动内存创建这种数组
//·声明变长数组(C99)时,用变量表达式表示数组的维度,用数组名访问数组的元素;具有这种特性的数组只能在自动内存中创建
//·声明一个指针,调用malloc(),将其返回值赋给指针,使用指针访问数组的元素;该指针可以是静态的或自动的
//使用第2种和第3种方法可以创建动态数组(dynamic array),这种数组可以在程序运行时选择数组的大小和分配内存
int n = 5;
ptd2 = (double*)malloc(n * sizeof(double));
//这比变长数组更灵活
//通常malloc()要与free()配套使用,free()的参数是之前malloc返回的地址,该函数释放之前malloc分配的内存
//因此,动态分配内存的存储期从调用malloc分配内存到调用free释放内存为止
free(ptd);
free(ptd2);
//free()的参数应该是一个指针,指向由malloc分配的一块内存,不能用free释放通过其他方式分配的内存
//malloc和free的原型都在stdlib.h头文件中
ptd = (double*)malloc(n * sizeof(double)); //在C中不一定要使用强制类型转换,但C++中必须要用
if (ptd == NULL)
{
puts("Memory allocation failed.");
exit(EXIT_FAILURE); //标准提供了两个返回值以保证在所有操作系统中都能正常工作:EXIT_SUCCESS 表示普通的程序结束,EXIT_FAILURE 表示程序异常中止,一些操作系统还接受一些表示其他运行错误的整数值
}
free(ptd);
//一些操作系统在程序结束时会自动释放动态分配的内存,但是有些系统不会
//保险起见,请使用free(),不要依赖操作系统来清理
//【free()的重要性】
//静态内存的数量在编译时是固定的,在程序运行期间也不会改变
//自动变量使用的内存数量在程序执行期间自动增加或减少
//但动态分配的内存数量只会增加,除非用free()进行释放
//遗漏free()的这类问题被称为内存泄漏(memory leak)
//【calloc()函数】
//分配内存还可以使用calloc(contiguous memory allocation)函数
//和malloc()类似,ANSI之前calloc()返回指向char的指针,ANSI之后返回指向void的指针
long* newmem;
newmem = (long*)calloc(100, sizeof(long)); //calloc()接受两个无符号整数作为参数(ANSI规定是size_t类型),第1个参数是所需的存储单元数量,第2个参数是存储单元的大小(以字节为单位)
//上面的代码创建了100个sizeof(long)字节的存储单元
//calloc()函数还有一个特性——它把块中所有位都设置为0
free(newmem); //free()函数也可用于释放calloc()分配的内存
//动态内存分配是许多高级程序设计技巧的关键,有些编译器可能还提供其他内存管理函数
//【动态内存分配和变长数组】P344
//变长数组是自动存储类型,程序在离开变长数组定义所在的块时,变长数组占用的内存空间会被自动释放
//另一方面,malloc()创建的数组不必局限在一个函数内访问,可以这样做:被调函数创建一个数组并返回指针供主调函数访问,主调函数在末尾free掉之前被调函数分配的内存
//但不能释放同一块内存两次
//静态存储类别所用的内存数量在编译时确定,该类别的变量在程序开始执行时被创建,在程序结束时被销毁
//自动存储类别随着程序调用函数和函数结束,自动变量所用的内存数量也相应地增加和减少,这部分内存通常作为栈来处理,这意味着新创建的变量按顺序加入内存,然后以相反的顺序销毁
//动态分配的内存由程序员管理,可以在一个函数中创建,在另一个函数中销毁;正因为此,这部分的内存用于动态内存分配会支离破碎,也就是说未使用的内存块分散在已使用的内存块之间
//「另外,使用动态内存通常比使用栈内存慢」
//总而言之,程序把静态对象、自动对象、动态分配的对象存储在不同的区域(动态分配的数据占用的区域通常被称为内存堆或自由内存)
//【ANSI C 类型限定符】P346
//我们通常用类型和存储类别来描述一个变量
//C90还新增了两个属性:恒常性(constancy)、易变性(volatility)
//可以分别用关键字const和volatile来声明这两个属性,以这两个关键字创建的类型是限定类型(qualified type)
//C99增加了第3个限定符:restrict,用于提高编译器优化
//C11增加了第4个限定符:_Atomic,C11提供一个可选库,由stdatomic.h管理,以支持并发程序设计,而且_Atomic是可选支持项
//C99为类型限定符增加了一个新属性:它们现在是幂等的(idempotent)
//意思是可以在一条声明中多次使用同一个限定符,多余的限定符会被忽略
//const const const int j = 6;
//typedef const int zip;
//const zip q = 8;
//【const类型限定符】
//以const关键字声明的对象成为只读,初始化后,就不能再修改
float const* ptc; //与 const float * ptc; 相同
//const放在*左侧任意位置,限定了指针指向的数据不能改变;const放在*的右侧,限定了指针本身不能改变
//const关键字的常见用法是声明为函数形参的指针
//如果一个指针仅用于给函数访问值,应将其声明为一个指向const限定类型的指针
//对全局数据使用const可以避免意外更改的危险,可以创建const变量、const数组和const结构
//在文件间共享const数据要小心,可以采用两个策略:
//遵循外部变量的常用规则,即在一个文件中使用定义式声明,在其他文件中使用引用式声明(用extern关键字)
//另一种方案是把const变量放在一个头文件中,然后在其他文件中包含该头文件(这种方案必须在头文件中用关键字static声明全局const变量,否则将导致每个文件中都有一个相同标识符的定义式声明,C标准不允许这样)(相当于给每个文件提供了一个单独的数据副本,缺点是数据是重复的)
//【volatile类型限定符】
//volatile限定符告知计算机,代理(而不是变量所在的程序)可以改变该变量的值;语法和const一样
volatile int* ploc;
//通常,它被用于硬件地址以及在其他程序或同时运行的线程中共享数据
//例如,一个地址上可能存储着当前的时钟信息;或者一个地址用于接受另一台计算机传入的信息
//volatile涉及编译器的优化
//如果声明中没有volatile关键字,编译器会假定变量的值在使用过程中不变,然后再尝试优化代码
//可以同时用const和volatile限定一个值,顺序不重要
//例如,通常用const把硬件时钟设置为程序不能更改的变量,但是可以通过代理改变
const volatile int loc;
//【restrict类型限定符】
//restrict关键字允许编译器优化某部分代码以更好地支持计算
//它只能用于指针,表明该指针是访问数据对象的唯一且初始的方式
//如果未使用restrict关键字,编译器就必须假设最坏的情况(即,在两次使用指针之间,其他标识符可能已经改变了数据);如果用了restrict关键字,编译器就可以选择捷径优化计算
int* restrict restar = (int*)malloc(10 * sizeof(int));
free(restar);
//restrict还可用于函数形参中的指针,这意味着编译器可以假定在函数体内其他标识符不会修改该指针指向的数据,而且编译器可以尝试对其优化,使其不做别的用途
//restrict关键字有两个读者。一个是编译器,它告知编译器可以自由假定一些优化方案;一个是用户,它告知用户要使用满足restrict要求的参数
//【_Atomic类型限定符(C11)】P349
//并发程序设计需要考虑如何管理访问相同数据的不同线程,C11通过包含可选的头文件 stdatomic.h 和 threads.h 提供了一些可选的管理方法
//值得注意的是,要通过各种宏函数来访问原子类型;当一个线程对一个原子类型的对象执行原子操作时,其他线程不能访问该对象
int hogs;
hogs = 12;
//上面两行可以替换为:
//_Atomic int hogs;
//atomic_store(&hogs, 12); //在hogs中存储12是一个原子过程,其他线程不能访问hogs
//【旧关键字的新位置】
//C99允许把类型限定符和存储类别说明符static放在函数原型和函数头的形式参数的初始方括号中
//void ofmouth(int* const a1, int* restrict a2, int n);
//void ofmouth(int a1[const], int a2[restrict], int n);
//根据新标准,声明函数形参时,指针表示法和数组表示法都可以使用这两个限定符
//新标准为static引入了一种新用法,现在static除了表明静态存储类别变量的作用域或链接外,新的用法告知编译器如何使用形参
//double stick(double ar[static 20]);
//这表明函数调用中的实际参数应该是一个指向数组首元素的指针,且该数组至少有20个元素
//目的是让编译器使用这些信息优化函数的编码
//如果未初始化自动变量、寄存器变量,它们的值是未定义的
//如果未显式初始化静态变量,它们的字节都被设置为0
}
void trystat(void)
{
int fade = 0;
static int stay; //这条声明实际上并不是trystat()函数的一部分
printf("fade = %d and star = %d\n", fade++, stay++);
}
/// <summary>
/// 返回一个 0~32767 之间的值
/// </summary>
/// <param name=""></param>
/// <returns></returns>
unsigned int rand0(void)
{
next = next * 1103515245 + 12345;
return(unsigned int)(next / 65536) % 32768;
}
文件输入输出
示例代码
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[])
{
//C把文件看作是一系列连续的字节,每个字节都能被单独读取,这与UNIX环境中的文件结构相对应
//由于其他环境中可能无法完全对应这个模型,C提供两种文件模式:文本模式、二进制模式
//所有文件的内容都以二进制形式存储,但如果文件最初使用二进制编码的字符(例如ASCII或Unicode)表示文本,该文件就是文本文件,其中包含文本内容
//如果文件中的二进制值代表 机器语言代码 或数值数据 或图片 或音乐编码,该文件就是二进制文件,其中包含二进制内容
//为了规范文本文件的处理,C提供两种访问文件的途径:二进制模式和文本模式
//在二进制模式中,程序可以访问文件的每个字节
//在文本模式中,程序所见的内容和文件的实际内容不同
//程序以文本模式读取文件时,把本地环境表示的行末尾或文件结尾映射为C模式
//例如:C程序在旧式Macintosh中以文本模式读取文件时,把文件中的\r转换成\n;以文本模式写入文件时,把\n转换成\r;在MS-DOS平台读取文件时,把\r\n转换成\n;在其他环境中编写的文本模式程序也会做类似的转换
//除了以文本模式读写文本文件,还能以二进制模式读写文本文件
//如果读写一个旧式MS-DOS文本文件,程序会看到文件中的\r和\n字符,不会发生映射
//虽然C提供了二进制模式和文本模式,但这两种模式的实现可以相同
//因为UNIX使用一种文件格式,这两种模式对于UNIX实现而言完全相同,Linux也是如此
//【I/O的级别】P355
//除了选择文件的模式,大多数情况下,还可以选择I/O的两个级别(即处理文件访问的两个级别)
//底层I/O(low-level I/O)使用操作系统提供的基本I/O服务
//标准高级I/O(standard high-level I/O)使用C库的标准包和stdio.h头文件定义
//因无法保证所有操作系统都使用相同的底层I/O模型,C标准只支持标准I/O包
//有些实现会提供底层库,但C标准建立了可移植的I/O模型
//【标准文件】P356
//C程序会自动打开3个文件,它们被称为标准输入(standard input)、标准输出(standard output)、标准错误输出(standard error output)
//默认情况下,标准输入是系统的普通输入设备,通常为键盘;标准输出和标准错误输出是系统的普通输出设备,通常为显示屏
//通常,标准输入为程序提供输入,它是getchar()和scanf()使用的文件
//程序通常输出到标准输出,它是putchar()、puts()、printf()使用的文件
//之前提到的重定向把其他文件视为标准输入或标准输出
//标准错误输出提供了一个逻辑上不同的地方来发送错误信息
//【标准I/O】P356
//标准I/O包除了可移植外还有两个好处:标准I/O有许多专门的函数简化了处理不同I/O的问题,输入和输出都是缓冲的
//缓冲极大地提高了数据传输速率。程序可以检查缓冲区中的字节,缓冲在后台处理,所以让人有逐字符访问的错觉 (如果使用底层I/O,要自己完成大部分工作)
int ch;
FILE* fp;
unsigned long count = 0;
char filename[128];
puts("Enter Filename:");
gets_s(filename, 128);
if (fopen_s(&fp, filename, "r")) //if ((fp = fopen(filename, "r")) == NULL)
{
puts("Can't open file.");
exit(EXIT_FAILURE);
}
while ((ch = getc(fp)) != EOF)
{
putc(ch, stdout); //与putchar(ch)相同
count++;
}
//if (fclose(fp) != 0)
//{
// puts("\nError in closing file.");
//}
printf("\nFile has %lu characters.\n", count);
//exit()函数关闭所有打开的文件并结束程序,它的参数被传递给一些操作系统,供其他程序使用
//通常的惯例是:正常结束的程序传递0,异常结束的程序传递非零值,不同的退出值可用于区分程序失败的不同原因,这也是UNIX和DOS编程的通常做法
//但不是所有操作系统都能识别相同范围内的返回值,因此C标准规定了一个最小的限制范围
//标准要求0或宏EXIT_SUCCESS用于表示成功结束程序,宏EXIT_FAILURE用于表明结束程序失败(stdlib.h)
//根据ANSI C的规定,在最初调用的main()中使用return与调用exit()的效果相同,因此在main()中 return 0; 与 exit(0); 作用相同
//如果main()在一个递归程序中,exit()仍会终止程序,但return只会把控制权交给上一级递归;即使在其他函数中,exit()也能结束整个程序
//fopen()函数的第1个参数是待打开文件的名称,更确切地说是一个包含该文件名的字符串地址,第2个参数是一个字符串,指定待打开文件的模式:r读模式、w写模式(覆盖)、a写模式(添加)
//像UNIX和Linux这样只有一种文件类型的系统,带b字母的模式和不带b字母的模式相同
//C11新增了带x字母的写模式,如果以传统的一种写模式打开一个现有文件,fopen会把该文件的长度截为0,这样就丢失了该文件的内容
//但使用带x字母的写模式,即使fopen操作失败,原文件的内容也不会被删除;如果环境允许,x模式的独占特性使得其他程序或线程无法访问正在被打开的文件
//如果使用任何一种"w"模式打开一个现有文件,该文件的内容会被删除,以便程序在一个空白文件中开始操作,若使用带x字母的任何一种模式,将无法打开一个现有文件
//程序成功打开文件后,fopen返回文件指针(file pointer),其他I/O函数可以使用这个指针指定该文件
//文件指针的类型是指向FILE的指针,FILE是一个定义在stdio.h的派生类型
//文件指针并不指向实际的文件,它指向一个包含文件信息的数据对象,其中包含操作文件的I/O函数所用的缓冲区信息;因为标准库中的I/O函数使用缓冲区,所以它们不仅要知道缓冲区的位置,还要知道缓冲区被填充的程度以及操作哪一个文件
//fp指向的数据对象包含了这些信息(该数据对象是一个C结构)
//getc()和putc()函数与getchar()和putchar()函数类似,不同的是要告诉getc()和putc()函数使用哪一个文件
//ch = getchar(); //从标准输入中获取一个字符
//ch = getc(fp); //从fp指定的文件中获取一个字符
//putc(ch, fpout); //把字符ch放入FILE指针fpout指定的文件中 //putc()函数的参数列表里,第1个参数是待写入的字符,第2个参数是文件指针
//如果getc()函数在读取一个字符时发现是文件结尾,它将返回一个特殊值EOF,所以C程序只有在读到超过文件末尾时才会发现文件的结尾
//为了避免读到空文件,应该使用入口条件循环进行文件输入,程序应该在进入循环体之前先尝试读取
//fclose(fp)函数关闭fp指定的文件,必要时刷新缓冲区
//较正式的程序应该检查是否成功关闭文件,成功关闭fclose()返回0,否则返回EOF
//如果磁盘已满、移动硬盘被移除或出现I/O错误,都会导致调用fclose函数失败
//stdio.h头文件把3个文件指针与3个标准文件相关联,C程序会自动打开这3个标准文件
//标准文件:标准输入、标准输出、标准错误
//文件指针:stdin、stdout、stderr
//这些文件指针都是指向FILE的指针,所以它们可用作标准I/O函数的参数
//注意程序同时打开的文件数量是有限的,取决于系统和实现
//【文件I/O:fprintf()、fscanf()、fgets()、fputs()】P361
fprintf(stderr, "Error %d\n", 0);
//fprintf和printf类似,但它的第1个参数必须是一个文件指针
//使用stderr指针把错误信息发送至标准错误,C标准通常都这样做
//fprintf()和fscanf()的工作方式与printf()和scanf()类似,但与putc()不同的是,这两个函数都把FILE指针作为第1个参数,而不是最后一个参数
//虽然a+模式只允许在文件末尾添加内容,但是该模式下可以读整个文件
//rewind()函数让程序回到文件开始处,方便while循环打印整个文件的内容,rewind()接受一个文件指针作为参数
char words[128];
rewind(fp);
fscanf_s(fp, "%s", words, 128);
fprintf(stdout, "%s\n", words);
fscanf_s(fp, "%s", words, 128);
fprintf(stdout, "%s\n", words);
rewind(fp);
fscanf_s(fp, "%s", words, 128);
fprintf(stdout, "%s\n\n", words);
//fgets(buf, STLEN, fp); 第1个参数表示存储输入位置的指针,第2个参数是一个整数表示待输入字符串的大小(指该字符串占用多少空间),最后一个参数是文件指针,指定待读取的文件
//fgets()读取输入直到第1个换行符的后面,或读到文件结尾,或者读取STLEN-1个字符,然后fgets在末尾添加一个空字符使之成为一个字符串;字符串的大小是其字符数加上一个空字符
//如果fgets()在读到字符上限之前已读完一整行,它会把表示行结尾的换行符放在空字符前面
//fgets()在遇到EOF时将返回NULL值,可以利用这一机制检查是否到达文件结尾,未遇到EOF则返回之前传给它的第一个参数地址
//fputs(buf, fp); 第1个参数是字符串的地址,第2个是文件指针;该函数把根据传入地址找到的字符串写入指定的文件中
//fputs在打印字符串时不会在其末尾添加换行符
//【随机访问fseek()和ftell()】P363
//有了fseek函数便可把文件看作是数组,在fopen打开的文件中直接移动到任意字节处
//fseek有3个参数,返回int类型的值;ftell函数返回一个long类型值,表示文件中的当前位置
//fseek()的第1个参数是FILE指针,指向待查找的文件,fopen应该已打开该文件
//fseek()的第2个参数是偏移量(offset),该参数表示从起始点开始要移动的距离,该参数必须是long类型,可以为正(前移)负(后移)或0(保持不动)
//fseek()的第3个参数是模式,该参数确定起始点,标准定义了几个表示模式的明示常量
//文件的起始点模式:可以用0L、1L、2L分别表示这3种模式
//SEEK_SET 文件开始处
//SEEK_CUR 当前位置
//SEEK_END 文件末尾
fseek(fp, 0L, SEEK_SET);//定位至文件开始处
fseek(fp, 10L, SEEK_SET);//定位至文件中的第10个字节
fseek(fp, 2L, SEEK_CUR);//从文件当前位置前移2个字节
fseek(fp, 0L, SEEK_END);//定位至文件结尾
fseek(fp, -10L, SEEK_END);//从文件结尾处回退10个字节
//如果一切正常,fseek()的返回值为0;如果出现错误(如试图移动的距离超出文件的范围),其返回值为-1
//ftell()函数的返回类型是long,它返回的是参数指向文件的当前位置距文件开始处的字节数
//文件的第1个字节到文件开始处的距离是0,以此类推
//该定义适用于以二进制模式打开的文件,以文本模式打开文件的情况不同
//if (ch != '\032' && ch != '\r')
//许多MS-DOS编辑器都用Ctrl+Z标记文本文件的结尾,以二进制模式打开相同的文件时,Ctrl+Z字符被看作是文件中的一个字符,而实际的文件结尾符在该字符的后面
//MS-DOS用\r\n组合表示文本文件换行,通常UNIX文本文件中没有Ctrl+Z和\r,所以不打印这两个字符不会影响大部分UNIX文本文件
//ANSIC规定,对于文本模式,ftell()返回的值可以作为fseek()的第2个参数;对于MS-DOS,ftell()返回的值把\r\n当作一个字节计数
//【可移植性】
//在二进制模式中,实现不必支持SEEK_END模式;移植性更高的方法是逐字节读取整个文件直到文件末尾,C预处理器的条件编译指令提供了一种系统方法来处理这种情况
//在文本模式中,只有以下调用能保证其相应的行为:
//fseek(fp, 0L, SEEK_SET);//定位至文件开始处
//fseek(fp, 0L, SEEK_CUR);//保持当前位置不动
//fseek(fp, 0L, SEEK_END);//定位至文件结尾
//fseek(fp, ftell-pos, SEEK_SET);//到距文件开始处ftell-pos的位置,ftell-pos是ftell的返回值
//fseek和ftell都把文件大小限制在long类型能表示的范围内
//ANSIC新增了两个处理较大文件的新定位函数:fgetpos()和fsetpos()
//fgetpos()和fsetpos()使用一种新类型:fpos_t(file position type),它根据其他类型来定义 不是基本类型
//fpos_t类型的变量或数据对象可以在文件中指定一个位置,它不能是数组类型,除此之外没有其他限制
//fgetpos()的函数原型如下
//int fgetpos(FILE * restrict stream, fpos_t * restrict pos);
//调用该函数时,它把fpos_t类型的值放在pos指向的位置上,该值描述了文件中的当前位置距文件开头的字节数
//如果成功,fgetpos返回0;如果失败,返回非0
//int fsetpos(FILE *stream, const fpos_t *pos);
//调用该函数时,使用pos指向位置上的fpos_t类型值来设置文件指针指向偏移该值后指定的位置
//如果成功,返回0,否则返回非0
//fpos_t类型的值应通过之前调用fgetpos()获得
//【标准I/O的机理】P366
//通常,使用标准I/O的第1步是调用fopen()打开文件,fopen不仅打开一个文件,还创建了一个缓冲区(在读写模式下会创建两个缓冲区)、以及一个包含文件和缓冲区数据的结构
//另外,fopen还返回一个指向该结构的指针;我们说fopen函数“打开一个流”;若以文本模式打开就获得一个文本流,以二进制模式打开就获得一个二进制流
//这个结构通常包含一个指定流中当前位置的文件位置指示器,还包含错误和文件结尾的指示器、一个指向缓冲区开始处的指针、一个文件标识符和一个计数(统计实际拷贝进缓冲区的字节数)
//通常,使用标准I/O的第2步是调用一个输入函数,如fscanf、getc、fgets
//考虑文件输入,一调用这些函数,文件中的缓冲大小数据块就被拷贝到缓冲区中,缓冲区的大小因实现而异,一般是512字节或是它的倍数,如4096、16384
//最初调用函数,除了填充缓冲区外,还要设置fp所指向的结构中的值,尤其要设置流中的当前位置和拷贝进缓冲区的字节数,通常当前位置从字节0开始。
//在初始化结构和缓冲区后,输入函数按要求从缓冲区中读取数据
//在它读取数据时,文件位置指示器被设置为指向刚读取字符的下一个字符
//由于stdio.h系列的所有输入函数都使用相同的缓冲区,所以调用任何一个函数都将从上一次函数停止调用的位置开始
//输入函数发现已读完缓冲区中所有字符时,会请求把下一个缓冲大小的数据块从文件拷贝到该缓冲区中
//函数在读取缓冲区的最后一个字符后,把结尾指示器设置为真,于是,下一次被调用的输入函数将返回EOF
//输出函数以类似的方式把数据写入缓冲区,当缓冲区被填满时,数据将被拷贝至文件
//【其他标准I/O函数】P366
//int ungetc(int c, FILE *fp);
//ungetc函数把c指定的字符放回输入流中。如果把一个字符放回输入流,下次调用标准输入函数时将读取该字符。
//ungetc(ch, stdin);
//int fflush(FILE *fp);
//调用fflush函数引起输出缓冲区中所有的未写入数据被发送到fp指定的输出文件,这被称为刷新缓冲区
//如果fp是空指针,所有输出缓冲区都被刷新
//在输入流中使用fflush函数的效果是未定义的
//只要最近一次操作不是输入操作,都可以用该函数来更新流(任何读写模式)
//int setvbuf(FILE * restrict fp, char * restrict buf, int mode, size_t size);
//setvbuf函数创建了一个供标准I/O函数替换使用的缓冲区;在打开文件后且未对流进行其他操作之前,调用该函数
//指针fp识别待处理的流,buf指向待使用的存储区;如果buf的值不是NULL,则必须创建一个缓冲区(例如,声明一个内含1024个字符的数组,并传递该数组的地址)
//如果把NULL作为buf的值,该函数会为自己分配一个缓冲区
//变量size告诉setvbuf数组的大小
//mode的选择如下:_IOFBF表示完全缓冲(在缓冲区满时刷新)、_IOLBF表示行缓冲(缓冲区满或写入一个换行符时)、_IONBF无缓冲
//如果操作成功,函数返回0,否则返回一个非零值
//二进制I/O:fread()和fwrite()
//为保证数值在存储前后一致,最精确的做法是使用与计算机相同的位组合来存储,double类型的值应该存储在一个double大小的单元中
//如果以程序所用的表示法把数据存储在文件中,称为以二进制形式存储数据
//如果文件中的所有数据都被解释成字符码,则称该文件包含文本数据;如果部分或所有的数据都被解释成二进制形式的数值数据,则称该文件包含二进制数据(用数据表示机器语言指令的文件都是二进制文件)
//通常文字处理器生成的文件都是二进制文件,因为这些文件中包含了大量非文本信息,如字体和格式等
//size_t fwrite(const void * restrict ptr, size_t size, size_t nmemb, FILE * restrict fp);
//指针ptr是待写入数据块的地址,size表示待写入数据块的大小(以字节为单位),nmemb表示待写入数据块的数量,fp指定待写入的文件
//char buffer[256];
//fwrite(buffer, 256, 1, fp);
//以上调用保存一个大小为256字节的数据对象(如数组)(把一块256字节的数据从buffer写入文件)
//double earnings[10];
//fwrite(earnings, sizeof(double), 10, fp);
//以上调用保存一个内含10个double类型值的数组(把earnings数组中的数据写入文件,数据被分为10块,每块都是double的大小)
//fwrite()返回成功写入项的数量,正常情况下返回值就是nmemb,如果出现写入错误,返回值会比nmemb小
//size_t fread(void * restrict ptr, size_t size, size_t nmemb, FILE * restrict fp);
//fread()接受的参数和fwrite()相同,在fread中ptr是待读取文件数据在内存中的地址,fp指定待读取的文件;该函数用于读取被fwrite写入文件的数据
//double earnings[10];
//fread(earnings, sizeof(double), 10, fp);
//以上调用把10个double大小的值拷贝进earnings数组中
//fread()返回成功读取项的数量,正常情况下返回值就是nmemb,如果出现读取错误或读到文件结尾,返回值就会比nmemb小
//int feof(FILE *fp);
//int ferror(FILE *fp);
//如果标准输入函数返回EOF,则通常表明函数已到达文件结尾,然而,出现读取错误时,函数也会返回EOF
//feof()和ferror()用于区分这两种情况
//当上一次输入调用检测到文件结尾时,feof()返回一个非零值,否则返回0
//当读或写出现错误,ferror()返回一个非零值,否则返回0
if (ferror(fp) != 0)
fprintf(stderr,"Error.\n");
else
fputs("No error.\n", stdout);
//标准错误不受标准输出重定向的影响
fclose(fp);
}
结构和其他数据形式
示例代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>
#define MAXTITLE 81
#define MAXAUTHOR 61
#define MAXBOOKS 100
#define LEN 21
void onebook(const struct Book*);
struct Book onebook_cpy(struct Book);
struct Book //结构声明(structure declaration)描述了一个结构的组织布局
{
//每个部分都称为成员(member)或字段(field)
char title[MAXTITLE];
char author[MAXAUTHOR];
float value;
};
int main(void)
{
//设计程序时,最重要的步骤之一是选择表示数据的方法
//C提供了结构变量(structure variable)提高你表示数据的能力
//结构声明并未创建实际的数据对象,只描述了该对象由什么组成(有时,我们把结构声明称为模板)
//关键字struct表明跟在其后的是一个结构,后面是一个可选的标记,稍后程序中可以使用该标记引用该结构
struct Book alibrary; //把alibrary声明为一个使用Book结构布局的结构变量
//结构声明中,花括号括起来的是结构成员列表,每个成员都用自己的声明来描述
//成员可以是任意一种C的数据类型,甚至可以是其他结构
//右花括号后面的分号是声明所必需的,表示结构布局定义结束
//可以把结构声明置于一个函数内部,它的标记就只限于该函数内部使用;如果置于函数外部,那么该声明之后的所有函数都能使用它的标记
//【定义结构变量】P379
//结构布局告诉编译器如何表示数据,但它并未让编译器为数据分配空间
//创建结构变量时,编译器才使用模板为变量分配空间
//在结构变量的声明中,struct Book所起的作用相当于一般声明中的int或float
//例如,可以定义两个struct Book类型的变量,或者甚至是指向struct Book类型结构的指针
struct Book doyle, panshin, * ptbook;
//指针可以指向任何Book类型的结构变量
//就计算机而言,struct aBook blibrary; 是以下声明的简化:
struct aBook {
char title[MAXTITLE];
char author[MAXAUTHOR];
float value;
} blibrary;
//换言之,声明结构的过程和定义结构变量的过程可以组合成一个步骤
//组合后的结构声明和结构变量定义不需要使用结构标记,但如果打算多次使用结构模板,就要使用带标记的形式,或者使用typedef
struct
{
char title[MAXTITLE];
char author[MAXAUTHOR];
float value;
} clibrary;
//【初始化结构】
struct Book library = {
"TEST TITLE",
"THE AUTHOR",
58.8
};
//使用花括号括起来的初始化列表进行初始化,各初始化项用逗号分隔
//初始化静态存储期的变量必须使用常量值,这同样适用于结构
//如果是自动存储期,初始化列表中的值可以不是常量
//【访问结构成员】
//使用结构成员运算符——点(.)访问结构中的成员
//.比&优先级高,&library.value 等于 &(library.value)
puts(library.title);
//【结构的初始化器】
//C99和C11为结构提供了指定初始化器(designated initializer),也被称为标记化结构初始化语法,其语法与数组的指定初始化器类似
//但结构的指定初始化器使用点运算符和成员名(而不是方括号和下标)标识特定的元素
//与数组类似,在指定初始化器后面的普通初始化器,为指定成员后面的成员提供初始值;对特定成员的最后一次赋值才是它实际获得的值
struct Book gift = {
.value = 18.9,
.author = "GIFT AUTHOR",
8.8
};
fprintf(stdout, "%f\n", gift.value);
//【结构数组】P381
//结构数组过大可能会导致一些问题,若数组是自动存储类别的对象,其中的信息被存储在栈(stack)中,如果出现栈大小或栈溢出的问题,可以通过编译器选项设置较大的栈大小,或者创建静态或外部数组
struct Book morelibrary[MAXBOOKS];
morelibrary[0].value = 9.9;
strcpy_s(morelibrary[1].title, MAXTITLE, "TEST");
printf("%f,%c\n", morelibrary[0].value, morelibrary[1].title[2]);
//【嵌套结构】P384
struct names
{
char first[LEN];
char last[LEN];
};
struct man
{
struct names handle; //嵌套结构成员
char job[LEN];
};
struct man oneman[2] = {
{
{ "FIRST", "LAST" },
"HR"
},
{
{ "FIRST2", "LAST2" },
"HR2"
}
};
puts(oneman[0].handle.first);
//【指向结构的指针】P386
struct man* him;
him = &oneman[0]; //和数组不同的是,结构变量名不是结构变量的地址,因此要加上&运算符
printf("address #1: %p #2: %p\n", &oneman[0], &oneman[1]);
printf("pointer #1: %p #2: %p\n", him, him + 1);
printf("him->job = %s (*him).job = %s\n", him->job, (*him).job);
him++; //指向下一个结构
printf("him->handle.last = %s\n", him->handle.last);
//在有些系统中,一个结构的大小可能大于它各成员大小之和,这是因为系统对数据进行校准的过程中产生了一些“缝隙”
//例如有些系统必须把每个成员都放在偶数地址上,或4的倍数的地址上,在这种系统中结构的内部就存在未使用的“缝隙”
//使用->运算符访问结构成员
//如果 him == &oneman[0],him->job 即是 oneman[0].job
//指向结构的指针后面的->运算符和结构变量名后面的.运算符工作方式相同
//him是一个指针,him->job 是该指针所指向结构的一个成员
//如果 him == &oneman[0],那么 *him == oneman[0]
//所以 oneman[0].job == (*him).job
//必须要使用圆括号,因为.运算符比*运算符优先级高
//【向函数传递结构的信息】P387
//ANSI C允许把结构作为参数使用,所以程序员可以选择是传递结构本身,还是传递指向结构的指针,也可以把结构的成员作为参数
//只要结构成员是一个具有单个值的数据类型,就可以把它作为参数传递给接受该特定类型的函数
onebook(&library);
//传递结构的地址;必须使用&运算符,结构变量名不是其地址的别名
onebook_cpy(library);
//传递结构;调用时,编译器根据Book的模板创建了一个名为book的自动结构变量,然后该结构的各成员被初始化为library结构变量相应成员的值的副本
//【其他结构特性】P390
//现在的C允许把一个结构赋值给另一个结构,但是数组不能这样做
alibrary = library;
//这条语句把library的每个成员的值都赋给alibrary的相应成员,即使成员是数组,也能完成赋值
//还可以把一个结构初始化为相同类型的另一个结构
struct Book cpylib = library;
//现在的C函数不仅能把结构本身作为参数传递,还能把结构作为返回值返回;结构指针也允许这种双向通信,因此可以任选一种方法来解决编程问题
cpylib = onebook_cpy(library);
//把指针作为参数的优点是快,缺点是无法保护数据(除非使用const限定符)
//把结构作为参数的优点是保护了原始数据、代码风格更清楚,缺点是浪费时间和存储空间、较老的实现可能无法使用
//结构中使用字符数组:字符串存储在结构内部,结构要分配空间存储
//结构中使用字符指针:字符串存储在编译器存储常量的地方,结构本身只存储了两个地址,结构不会为字符串分配空间
//结构变量中的指针应该只用来在程序中管理那些已分配和在别处分配的字符串
//如果使用malloc()分配内存并使用指针存储该地址,那么在结构中使用指针处理字符串就比较合理
//这种方法的优点是可以请求malloc()为字符串分配合适的存储空间
struct namect
{
char* fname;
char* lname;
} namect_t;
char temp[81];
strcpy_s(temp, 81, "MNAME");
if (namect_t.fname = (char*)malloc(strlen(temp) + 1)) //分配内存存储字符串
{
strcpy_s(namect_t.fname, strlen(temp) + 1, temp); //把字符串拷贝到已分配的内存
puts(namect_t.fname);
}
free(namect_t.fname); //别忘了释放内存
//【复合字面量和结构】
//C99的复合字面量特性可用于结构和数组,如果只需要一个临时结构值,复合字面量很好用
alibrary = (struct Book){ "compound","literal",1.1 };
//还可以把复合字面量作为函数的参数;如果函数接受一个地址,可以传递复合字面量的地址
onebook(&(struct Book) { "compound", "literal", 1.1 });
//复合字面量在所有函数的外部,具有静态存储期;在块中则具有自动存储期
//可以在复合字面量中使用指定初始化器
//【伸缩型数组成员 flexible array member】P398
//C99特性,利用这项特性声明的结构,其最后一个数组成员不会立即存在
//声明一个伸缩型数组成员有以下规则:
//伸缩型数组成员必须是结构的最后一个成员。结构中必须至少有一个成员。伸缩数组的声明类似于普通数组,只是它的方括号中是空的。
struct flex
{
size_t 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));
//现在有足够的空间存储一个内含5个double类型值的数组和其他成员,可以用指针pf访问这些成员
if (pf)
{
pf->count = 5;
pf->scores[2] = 18.5;
}
free(pf);
//带伸缩型数组成员的结构有一些特殊的处理要求:
//第一,不能用结构进行赋值或拷贝。
//struct flex* pf1, pf2;
//*pf2 = *pf1
//这样做只能拷贝除伸缩型数组成员以外的其他成员;确实要进行拷贝,应使用memcpy()函数
//第二,不要以按值方式把这种结构传递给函数,原因相同,按值传递一个参数与赋值类似,要传递地址
//第三,不要使用带伸缩型数组成员的结构作为数组成员或另一个结构的成员
//【匿名结构 C11】P400
//匿名结构是一个没有名称的结构成员
//可以用类似oneman[0].handle.first的表达式访问嵌套结构成员
struct annperson
{
int id;
struct { char first[20]; char last[20]; }; //匿名结构
};
struct annperson ted = { 8483,{"A","B"} };
//初始化上面结构的方式与初始化嵌套结构相同,但访问时简化了步骤,只需把first看作是annperson的成员那样使用它
puts(ted.first);
//【使用结构数组的函数】
//可以把数组名作为数组中第一个结构的地址传递给函数(该函数还需访问结构模板)
//然后可以用数组表示法访问数组中的其他结构
//【把结构内容保存到文件中】P401
//存储在一个结构中的整套信息被称为记录(record),单独的项被称为字段(field)
//或许存储记录最没效率的方法是用fprintf()
//更好的方案是使用fread()和fwrite()函数读写结构大小的单元(这两个函数使用与程序相同的二进制表示法)
FILE* database;
if (fopen_s(&database, "D:\\TEST.dat", "w+b") == 0)
{
fwrite(&library, sizeof(struct Book), 1, database); //定位到library结构变量开始的位置,并把结构中所有的字节都拷贝到与database相关的文件中
fclose(database);
}
//带相同参数的fread()函数从文件中拷贝一块结构大小的数据到&library指向的位置
//简而言之,这两个函数一次读写整个记录,而不是一个字段
//注意:以二进制表示法存储数据的缺点是,不同系统可能使用不同的二进制表示法,数据文件可能不具可移植性,同一个系统的不同编译器设置也可能导致不同的二进制布局
//这种方法浪费存储空间,因为这还保存了结构中未使用的部分,但让每个输入块的大小相同在检索数据时很方便
//另一个方法是使用可变大小的记录,为了方便读取文件中的这种记录,每个记录以数值字段规定记录的大小,这比较复杂,通常涉及链式结构和动态内存分配
//【链式结构】P405
//结构的多种用途之一:创建新的数据形式
//计算机用户已经开发出的一些数据形式比我们提到过的数组和简单结构更有效地解决特定的问题
//这些形式包括队列、二叉树、堆、哈希表、图表;许多这样的形式都由链式结构(linked structure)组成
//通常,每个结构都包含一两个数据项和一两个指向其他同类型结构的指针,这些指针把一个结构和另一个结构链接起来,并提供一种路径能遍历整个彼此链接的结构
//例如二叉树结构,每个单独的结构(或节点)都和它下面的两个结构(或节点)相连
//考虑一个有10级节点的树的情况,它有2^10-1个节点,可以存储1023个单词,如果这些单词以某种规律排列,那么可以从最顶层开始,逐级向下移动查找,最多只需移动9次便可找到任意单词;如果放在一个数组里,最多要查找1023个元素
//【联合简介】P405
//联合(union)是一种数据类型,它能在同一个内存空间中存储不同的数据类型(不是同时存储)
//其典型用法是,设计一种表以存储既无规律、事先也不知道顺序的混合类型
//使用联合类型的数组,其中的联合都大小相等,每个联合可以存储各种数据类型
//创建联合和创建结构的方式相同,需要一个联合模板和联合变量;可以用一个步骤定义联合,也可以用联合标记分两步定义
union hold
{
int digit;
double bigfl;
char letter;
};
//以上声明的联合只能存储一个int类型的值或一个double类型的值或char类型的值
//下面定义了3个与hold类型相关的变量
union hold fit; //hold类型的联合变量,编译器分配足够的空间以便它能存储联合声明中占用最大字节的类型
union hold save[10]; //内含10个联合变量的数组,每个元素都是联合声明中占用最大字节的类型的大小
union hold* pu; //指向hold类型联合变量的指针
//可以初始化联合,需要注意的是联合只能存储一个值
//有3种初始化的方法:把一个联合初始化为另一个同类型的联合;初始化联合的第1个元素;根据C99使用指定初始化器
union hold valA;
valA.letter = 'R';
union hold valB = valA;
union hold valC = { 88 };
union hold valD = { .bigfl = 118.2 };
//下面是联合的一些用法
fit.digit = 23; //把23存储在fit,占4字节
fit.bigfl = 2.0; //清除23,存储2.0,占8字节
fit.letter = 'h'; //清除2.0,存储h,占1字节
//点运算符标识正在使用哪种数据类型
//在联合中,一次只能存储一个值,即使有足够的空间也不能同时存储
//和用指针访问结构使用->运算符一样,用指针访问联合时也要使用->运算符
pu = &fit;
int x = pu->digit;
//联合的一种用法是,在结构中存储与其成员有从属关系的信息
struct owner { char socsecurity[12]; };
struct leasecompany { char name[40]; };
union data
{
struct owner owncar;
struct leasecompany leasecar;
};
struct car_data
{
char make[15];
int status; //私有为0,租赁为1
union data ownerinfo;
};
//匿名联合(C11)
//匿名联合和匿名结构的工作原理相同,即匿名联合是一个结构或联合的无名联合成员
//【总结】P407
//成员运算符:.
// 该运算符与结构变量或联合变量名一起使用,指定结构变量或联合变量的一个成员
//间接成员运算符:->
// 该运算符和指向结构或联合的指针一起使用,标识结构变量或联合变量的一个成员
//【枚举类型】P408
//可以用枚举类型(enumerated type)声明符号名称来表示整型常量
//使用enum关键字,可以创建一个新“类型”并指定它可具有的值(实际上,enum常量是int类型,因此只要能使用int类型的地方就可以使用枚举类型)
//枚举类型的目的是提高程序的可读性,它的语法与结构的语法相同
enum spectrum
{
red, orange, yellow, green, blue, violet
};
enum spectrum color;
//第一个声明创建了spectrum作为标记名,允许把enum spectrum作为一个类型名使用
//第二个声明使color作为该类型的变量
//第一个声明中花括号内的标识符枚举了spectrum变量可能有的值
//这些符号常量被称为枚举符(enumerator)
color = blue;
if (color == yellow)
{
for (color = red; color <= violet; color++)
{ }
}
//虽然枚举符是int类型,但枚举变量可以是任意整数类型,前提是该整数类型可以存储枚举常量;例如编译器可以用unsigned char来表示color变量
//C枚举的一些特性并不适用于C++,例如C++标准不允许枚举变量使用++运算符
//enum常量
//通过枚举可以创建一系列代表整型常量(枚举常量)的符号和定义相关联的枚举类型
//从技术层面看,blue和red它们是int类型的常量
printf("red = %d, orange = %d\n", red, orange);
//能用整型常量的地方就可以用枚举常量,例如声明数组可以用枚举常量表示数组的大小,switch语句中可以把枚举常量作为标签
//默认值
//默认情况下,枚举列表中的常量都被赋予0、1、2等
//赋值
//在枚举声明中,可以为枚举常量指定整数值
enum levels
{
low = 100,
medium = 500,
high = 2000
};
//如果只给一个枚举常量赋值,没有对后面的枚举常量赋值,那么后面的常量会被赋予后续的值
//enum的用法
//枚举类型的目的是为了提高程序的可读性和可维护性
//注意,枚举类型只能在内部使用;输入中只能用数字,或者让程序读入字符串再转换
const char* colors[] = { "red","orange","yello","green","blue","violet" };
bool color_is_found = false;
for (color = red; color <= violet; color++)
{
if (strcmp("yello", colors[color]) == 0)
{
color_is_found = true;
break;
}
}
if (color_is_found)
{
switch (color)
{
case red:
break;
case orange:
break;
case yellow:
puts("Found yello.");
break;
case green:
break;
case blue:
break;
case violet:
break;
}
}
//【共享名称空间】P410
//C语言使用名称空间(namespace)标识程序中的各部分,即通过名称来识别
//作用域是名称空间概念的一部分
//名称空间是分类别的,在特定作用域中的结构标记、联合标记和枚举标记都共享相同的名称空间,该名称空间与普通变量使用的空间不同
//这意味着在相同作用域中变量和标记的名称可以相同,不会引起冲突;但是不能在相同作用域中声明两个同名标签或同名变量
struct rect { double x; double y; };
int rect;
//尽管这样,以两种不同的方式使用相同的标识符会造成混乱
//C++不允许这样做,因为它把标记名和变量名放在相同的名称空间中
//【typedef简介】P411
//typedef工具是一个高级数据特性,利用typedef可以为某一类型自定义名称,这方面与#define类似,但两者有3处不同:
//与#define不同,typedef创建的符号名只受限于类型,不能用于值
//typedef由编译器解释,不是预处理器
//在其受限范围内,typedef比#define更灵活
typedef unsigned char BYTE;
BYTE bx;
//通常,typedef定义中用大写字母表示被定义的名称,以提醒用户这个类型名实际上是一个符号缩写,当然也可以用小写
//typedef中使用的名称遵循变量的命名规则
//使用tpyedef可以提高程序的可移植性
typedef char* STRING;
STRING name, sign; //如果用#define则只有name才是指针
//还可以把typedef用于结构
typedef struct //用typedef来命名一个结构类型时,可以省略该结构的标签
{
float real;
float imag;
} COMPLEX;
COMPLEX comnum = { 3.5,6.0 };
COMPLEX c2;
c2 = comnum;
//这两个结构在声明时都没有标记,它们的成员完全相同,C认为这两个结构的类型相同,所以赋值是有效操作
//typedef常用于给复杂的类型命名
//typedef没有创建任何新类型,它只是为某个已存在的类型增加了一个方便使用的标签
//【其他复杂的声明】P412
//声明时可使用的符号:* () [] 表示指针、函数、数组
//下面是一些较复杂的声明示例:
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的函数
//这3个函数都接受int类型的参数
typedef int arr5[5];
typedef arr5* p_arr5;
typedef p_arr5 arrp10[10];
arr5 togs; //togs是一个内含5个int类型值的数组
p_arr5 p2; //p2是一个指向数组的指针,该数组内含5个int类型的值
arrp10 ap; //ap是一个内含10个指针的数组,每个指针都指向一个内含5个int类型值的数组
//如果把这些放入结构中,声明会更复杂
//【函数和指针】P413
//通常,函数指针常用作另一个函数的参数,告诉该函数要使用哪一个函数
//函数也有地址,因为函数的机器语言实现由载入内存的代码组成,指向函数的指针中存储着函数代码的起始处的地址
//声明一个函数指针时,必须声明指针指向的函数类型;为了指明函数类型,要指明函数签名,即函数的返回类型和形参类型
void ToUpper(char*);
void (*fpf)(char*);
//第一对圆括号表明pf是一个指向函数的指针,(*fpf)是一个参数列表为(char*)、返回类型为void的函数
//要声明一个指向特定类型函数的指针,可以先声明一个该类型的函数,然后把函数名替换成(*fpf)形式的表达式
//声明了函数指针后,可以把类型匹配的函数地址赋给它
//在这种上下文中,函数名可以用于表示函数的地址:
void ToLower(char*);
fpf = ToUpper;
fpf = ToLower;
//ToUpper、ToLower都是该类型函数的地址,赋值均有效
//既然可以用数据指针访问数据,也可以用函数指针访问函数
char mis[] = "Nina";
fpf = ToUpper;
(*fpf)(mis); //把 ToUpper 作用于 mis (语法1)
fpf = ToLower;
fpf(mis); //把 ToLower 作用于 mis (语法2)
//语法1:由于fpf指向ToUpper函数,那么*fpf就相当于ToUpper函数,所以表达式(*fpf)(mis)和ToUpper(mis)相同,ToUpper和(*fpf)是等价的
//语法2:由于函数名是指针,那么指针和函数名可以互换使用,从fpf的赋值表达式语句就能看出ToUpper和fpf是等价的
//ANSI C认为这两种形式等价
//作为函数的参数是数据指针最常见的用法之一,函数指针亦如此
void show(void (*fp)(char*), char* str);
//上面声明了两个形参,fp和str;fp是一个函数指针,str是一个数据指针
//fp指向的函数接受char*类型的参数,其返回类型为void
show(ToLower, mis); //show()使用ToLower()函数:fp = ToLower
//function1(sqrt); //传递sqrt函数的地址
//function2(sqrt(4.0)); //传递sqrt函数的返回值
}
void onebook(const struct Book* book)
{
char fulln[MAXTITLE + MAXAUTHOR + 1];
strcpy_s(fulln, MAXTITLE + MAXAUTHOR, book->author);
strcat_s(fulln, MAXTITLE + MAXAUTHOR, ": ");
strcat_s(fulln, MAXTITLE + MAXAUTHOR, book->title);
puts(fulln);
}
struct Book onebook_cpy(struct Book book)
{
char fulln[MAXTITLE + MAXAUTHOR + 1];
strcpy_s(fulln, MAXTITLE + MAXAUTHOR, book.author);
strcat_s(fulln, MAXTITLE + MAXAUTHOR, ": ");
strcat_s(fulln, MAXTITLE + MAXAUTHOR, book.title);
puts(fulln);
return book;
}
void ToUpper(char* a) {}
void ToLower(char* a) {}
void show(void (*fp)(char*), char* str)
{
(*fp)(str); //把所选函数作用于str
puts(str); //显示结果
}
位操作
示例代码
#include <stdio.h>
#include <stdbool.h>
int main(int argc, char* argv[])
{
//以2为基底表示的数字被称为二进制数(binary number)
//例如,二进制数 1101 可表示为:1×2^3 + 1×2^2 + 0×2^1 + 1×2^0
//以十进制数表示为:1×8 + 1×4 + 0×2 + 1×1 = 13
//通常,1字节包含8位。C语言用字节(byte)表示存储系统字符集所需的大小,所以C字节可能是8位、9位、16位或其他值
//不过,描述存储器芯片和数据传输率中所用的字节指的是8位字节
//计算机界常用八位组(octet)这个术语特指8位字节
//可以从左往右给这8位分别编号为7~0,在1字节中,编号是7的位被称为高阶位(high-order bit),编号是0的位被称为低阶位(low-order bit),每1位的编号对应2的相应指数
//位编号:7 6 5 4 3 2 1 0
//位值: 128 64 32 16 8 4 2 1
//该字节能表示的最大数字是把所有位都设为1,即255
//因此,1字节可存储0~255范围内的数字,共256个值
//通过不同的方式解释位组合(bit pattern),程序可以用1字节存储-128~+127范围内的整数,总共还是256个值
//例如unsigned char和signed char
//如何表示有符号整数取决于硬件,最简单的方式是用1位存储符号,剩下7位表示数字本身
//用这种符号量(sign-magnitude)表示法10000001表示-1,00000001表示1,因此其表示范围是-127~+127
//这种方法的缺点是有两个0
//【二进制补码】(two's-complement)方法避免了这个问题,是当今最常用的系统
//二进制补码用1字节中的后7位表示0~127,高阶位设置为0,如果高阶位是1,表示的值为负
//确定负值的方法:从一个9位组合100000000(256的二进制形式)减去一个负数的位组合,结果是该负值的量
//假设一个负值的位组合是10000000,作为一个无符号字节,该组合表示128;作为一个有符号值,该组合表示负值,值为100000000(256)-10000000(128),即10000000(128)
//因此该数是-128(在符号量表示法中,该位组合表示-0)
//类似地,10000001是-127,11111111是-1,该方法可以表示-128~+127范围内的数
//要得到一个二进制补码数的相反数,最简单的方法是反转每一位,然后加1
//因为1是00000001,那么-1则是11111110+1,即11111111【正数和0的补码就是该数字本身,负数的补码则是将其对应正数按位取反再加1】
//深入理解:
//https://www.zhihu.com/question/352057791/answer/876413629
//https://www.zhihu.com/question/352057791/answer/1154685577
//【二进制反码】(one's-complement)方法通过反转位组合中的每一位形成一个负数
//例如,00000001是1,那么11111110是-1,这种方法也有两个0,能表示-127~+127之间的数
//【二进制浮点数】P424
//浮点数分两部分存储:二进制小数和二进制指数
//二进制小数:
//一个普通的浮点数0.527,表示如下 5/10 + 2/100 + 7/1000,从左往右各分母都是10的递增次幂
//在二进制小数中,使用2的幂作为分母,所以二进制小数.101表示为 1/2 + 0/4 + 1/8,十进制即为0.625
//许多分数(如1/3)不能用十进制表示法精确地表示,与此类似,许多分数也不能用二进制表示法精确表示
//实际上,二进制表示法只能精确地表示多个1/2的幂的和
//浮点数表示法:
//在计算机中表示一个浮点数,要留出若干位(因系统而异)存储二进制分数,其他位存储指数
//一般而言,数字的实际值是由二进制小数乘以2的指定次幂组成
//【其他进制数】P425
//计算机界通常使用八进制计数系统和十六进制计数系统,因为8和16都是2的幂,比十进制系统更接近计算机的二进制系统
//八进制(octal)
//该系统基于8的幂,用0~7表示数字,例如八进制数0451表示为 4×8^2 + 5×8^1 + 1×8^0
//了解八进制的一个简单方法是,每个八进制对应3个二进制位;例如八进制数0377的二进制形式是11111111
//这表明比0377大的八进制要用多个字节表示,这是八进制唯一不方便的地方:一个3位的八进制数可能要用9位二进制数来表示
//十六进制(hexadecimal)
//基于16的幂,用0~15表示数字,10~15用字母A~F表示
//例如十六进制数0×A3F表示为 10×16^2 + 3×16^1 + 15×16^0
//在C语言中,A~F既可用小写也可用大写
//每个十六进制为都对应一个4位的二进制数,两个十六进制位恰好对应一个8位字节,因此十六进制很适合表示字节值
//C有两个操控位的工具,第1个工具是一套(6个)作用于位的按位运算符,第2个工具是字段(field)数据形式,用于访问int中的位
//【C按位运算符】P426
//C提供 按位逻辑运算符 和 移位运算符
//【按位逻辑运算符】
//4个按位逻辑运算符都用于整型数据,包括char
//之所以叫做按位(bitwise)运算,是因为这些操作都是针对每一个位进行,不影响它左右两边的位
//按位取反:~
//一元运算符~把1变为0,把0变为1
unsigned char t1 = 0377;
printf("~(0377) = %hhu\n", ~t1);
//按位与:&
//二元运算符&逐位比较两个运算对象,只有相应位都为1时结果才为1
unsigned char t2 = 1;
printf("(0377) & (1) = %hhu\n", t1 & t2);
t1 &= t2; //t1 = t1 & t2;
printf("t1 &= 1; t1 = %hhu\n", t1);
//按位或:|
//二元运算符|逐位比较两个运算对象,相应的位至少有一个为1结果就为1
t1 = 0377;
t2 = 0;
t1 |= t2;
printf("(0377) | (0) = %hhu\n", t1);
//按位异或:^
//二元运算符^逐位比较两个运算对象,相应的位只有一个为1结果才为1
t1 ^= t2;
printf("(0377) ^ (0) = %hhu\n", t1);
t2 = 1;
t1 ^= t2;
printf("(0377) | (1) = %hhu\n", t1);
//用法:掩码
//按位与&运算符常用于掩码(mask)
//掩码是指一些设置为开(1)或关(0)的位组合
unsigned char mask = 2;
t1 = 0377;
t1 &= mask;
puts("\nt1 = 0377, mask = 2");
printf("t1 &= mask; t1 = %hhu\n", t1);
//t1中除1号位以外的所有位都设置为了0,因为掩码中的0隐藏了t1中相应的位
//可以这样类比:把掩码中的0看作不透明,1看作透明
int ch = 4095;
//下面这条语句是按位与的一种常见用法:
ch &= 0xff;
//这个掩码保持ch中的最后8位不变,其他位都设置为0,在该例中,掩码的宽度为8位
printf("ch = 4095; ch &= 0xff; ch = %d\n", ch);
//用法:打开位(设置位)
//有时,需要打开一个值中的特定位,同时保持其他位不变
//例如,一台IBM PC通过向端口发送值来控制硬件,这种情况可以使用按位或运算符|
ch = 253;
ch |= mask; //把ch的1号位设置为1,且其他位不变
printf("ch = 253; ch |= mask; ch = %d\n", ch);
//用法:关闭位(清空位)
//有时也需要在不影响其他位的情况下关闭指定的位
ch = ch & ~mask; //~mask只有1号位为0
printf("ch = ch & ~mask; ch = %d\n", ch);
//用法:切换位
//使用按位异或运算符切换位(打开已关闭的位,关闭已打开的位)
//如果b是一个位,若b为1,1^b=0,b为0,1^b=1,无论b是多少,0^b均为b
ch ^= mask;
printf("ch ^= mask; ch = %d\n", ch);
ch ^= mask;
printf("ch ^= mask; ch = %d\n", ch);
//用法:检查位的值
if ((ch & mask) == mask) //检查1号位是否为1,需要先覆盖其他位再比较
puts("is 1\n");
else
puts("not 1\n");
//为了避免信息漏过边界,掩码至少要与其覆盖的值宽度相同
//【移位运算符】P429
//移位运算符向左或向右移动位
//左移:<<
//左移运算符将其左侧运算对象每一位的值向左移动其右侧运算对象指定的位数,移出左末端位的值丢失,用0填充空出的位置
//可以使用左移赋值运算符(<<=)来更改变量的值
ch = 1;
ch <<= 2;
printf("ch = 1; ch <<= 2; ch = %d\n", ch);
//右移:>>
//移出右末端位的值丢失,对于无符号类型,用0填充空出的位置
//对于有符号类型,结果取决于机器,空出的位置可用0填充,或者用符号位(最左端的值)的副本填充
ch >>= 2;
printf("ch >>= 2; ch = %d\n", ch);
//用法:移位运算符针对2的幂提供快速有效的乘法和除法,类似于在十进制中移动小数点
//number << n number乘以2的n次幂
//number >> n 如果number为非负,则用number除以2的n次幂
//移位运算符还可用于从较大单元中提取一些位
//例如,假设用一个unsigned long类型的值表示颜色值,低阶位字节存储红色的强度,下一个字节存储绿色的强度,第3个字节存储蓝色的强度
//随后你希望把每种颜色的强度分别存储在3个不同的unsigned char类型的变量中
unsigned long color = 0x002a162f;
unsigned char blue, green, red;
red = color & 0xff;
green = (color >> 8) & 0xff;
blue = (color >> 16) & 0xff;
//以上代码使用右移运算符将8位颜色值移动到低阶字节,然后用掩码把低阶字节赋给指定变量
//【位字段】P433
//操控位的第2种方法是位字段(bit field)
//位字段是一个signed int或unsigned int类型变量中的一组相邻的位(C99和C11新增了_Bool类型的位字段)
//位字段通过一个结构声明来建立,该结构声明为每个字段提供标签,并确定该字段的宽度
//例如,下面的声明建立了一个4个1位的字段
struct
{
unsigned int autfd : 1;
unsigned int bldfc : 1;
unsigned int undln : 1;
unsigned int itals : 1;
} prnt;
//prnt包含4个1位的字段,可以通过普通的结构成员运算符单独给这些字段赋值
prnt.itals = 0;
prnt.undln = 1;
//由于每个字段恰好为1位,所以只能为其赋值1或0
//变量prnt被存储在int大小的内存单元中,但在本例中只使用了其中的4位
//许多设置就是简单的二选一,如果只需要使用1位,就不需要使用整个变量
//内含位字段的结构允许在一个存储单元中存储多个这样的设置
//字段不限制1位大小,可以使用如下代码
struct
{
unsigned int code1 : 2;
unsigned int code2 : 2;
unsigned int code3 : 8;
} prcode;
//以上代码创建了两个2位的字段和一个8位的字段
prcode.code1 = 0;
prcode.code2 = 3;
prcode.code3 = 102;
//要确保所赋的值不超出字段可容纳的范围
//如果声明的总位数超过了一个unsigned int类型的大小,会用到下一个unsigned int类型的存储位置
//一个字段不允许跨越两个unsigned int之间的边界,编译器会自动移动跨界的字段,保持unsigned int的边界对齐
//一旦发生这种情况,第1个unsigned int中会留下一个未命名的“洞”
//可以用未命名的字段宽度“填充”未命名的“洞”,使用一个宽度为0的未命名字段迫使下一个字段与下一个整数对齐
struct
{
unsigned int field1 : 1;
unsigned int : 2;
unsigned int field2 : 1;
unsigned int : 0;
unsigned int field3 : 1;
} stuff;
//这里,在stuff.field1和stuff.field2之间,有一个2位的空隙
//stuff.field3将存储在下一个unsigned int中
//字段存储在一个int中的顺序取决于机器,可能是从左往右或从右往左,不同的机器中两个字段边界的位置也有区别
//由于这些原因,位字段通常都不易移植,尽管如此,有些情况却要用到这种不可移植的特性,例如以特定硬件设备所用的形式存储数据
//通常,把位字段作为一种更紧凑存储数据的方式
//C以unsigned int作为位字段结构的基本布局单元,即使一个结构唯一的成员是1位字段,该结构的大小也是一个unsigned int类型的大小
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;
};
//每个字节中的空隙用未命名字段填充
//switch语句中也可以使用位字段成员,还可以把位字段成员用作数组下标
//在同类型的编程问题中,位字段和按位运算符(一般较麻烦)是两种可替换的方法
union Views
{
struct box_props st_view;
unsigned short us_view;
};
//结构的哪一个位字段与unsigned short中的哪一位对应取决于实现和硬件
//位字段视图和按位视图的区别是,按位视图需要位置信息
//用十六进制更容易看出要设置二进制的哪一位,十六进制的每一位代表二进制的4位,0×8是1000(8),0×800是100000000000(2048)
//【对齐特性(C11)】P442
// https://docs.microsoft.com/zh-cn/cpp/c-language/alignment-c?view=msvc-170
//C11的对齐特性比用位填充字节更自然
//在这种上下文中,对齐指的是如何安排对象在内存中的位置
//_Alignof运算符给出一个类型的对齐要求
size_t d_align = _Alignof(float);
printf("\n_Alignof(float) = %zd\n", d_align);
//若d_align的值是4,意思是float类型对象的对齐要求是4,也就是说,4是存储该类型值相邻地址的字节数
//可以用_Alignas说明符指定一个变量或类型的对齐值,但不应该要求该值小于基本对齐值
_Alignas(double) char c1;
_Alignas(8) char c2;
unsigned char _Alignas(long double) c_arr[sizeof(long double)];
double dx;
char ca;
char cx;
double dz;
char cb;
char _Alignas(double) cz;
printf("char alignment: %zd\n", _Alignof(char));
printf("double alignment: %zd\n", _Alignof(double));
printf("&dx: %p\n", &dx); //double的对齐值是8,这意味着地址的类型对齐可以被8整除(以0或8结尾的十六进制地址可被8整除)
printf("&ca: %p\n", &ca); //char的对齐值是1,对于普通的char类型变量,编译器可以使用任何地址
printf("&cx: %p\n", &cx);
printf("&dz: %p\n", &dz);
printf("&cb: %p\n", &cb);
printf("&cz: %p\n", &cz);
//包含stdalign.h头文件后,就可以把alignas和alignof分别作为_Alignas和_Alignof的别名,这样做可以与C++关键字匹配
//C11在stdlib.h库还添加了一个新的内存分配函数,用于对齐动态分配的内存,函数原型如下
//void* aligned_alloc(size_t alignment, size_t size);
//第1个参数代表指定的对齐,第2个参数是所需的字节数,其值应是第1个参数的倍数
//与其他内存分配函数一样,要使用free()释放之前分配的内存
typedef struct
{
char a;
short b;
short c;
char d;
} test_8;
typedef struct
{
char a;
char b;
short c;
short d;
} test_6;
printf("\n\nstruct test_8 size: %zd\n", sizeof(test_8));
printf("struct test_6 size: %zd\n", sizeof(test_6));
//参考资料:https://blog.csdn.net/G_METHOD/article/details/79535178
}
//通常,使用这些特性的程序仅限于特定的硬件平台或操作系统,而且设计为不可移植的
//这些位工具帮助C程序处理硬件问题,因此它们通常用于依赖实现的场合中
C预处理器和C库
示例代码
#include <stdio.h>
#include <tgmath.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include <stdarg.h>
//_Static_assert(CHAR_BIT == 16, "16-bit char falsely assumed");
inline static void inlinecode() { }
void sign_off(void) { puts("Exit!"); }
void bye(void) { puts("\nbye!"); }
void fillarray(double[], int);
void showarray(const double[], int);
int comp(const void*, const void*);
void f1(int n, ...); //至少有一个形参和一个省略号,省略号在最后
int main(int argc, char* argv[])
{
//【翻译程序的第一步】P447
//在预处理之前,编译器必须对程序进行一些翻译处理
//首先,把源代码中出现的字符映射到源字符集(该过程处理多字节字符和三字符序列——字符扩展让C更加国际化)
//第二,定位每个反斜杠后面跟着换行符的实例,并删除它们(把物理行physical line转换为逻辑行logical line)
//第三,把文本划分为预处理记号序列、空白序列、注释序列(记号是由空格、制表符或换行符分隔的项)
//注意:编译器用一个空格字符替换每一条注释
int/*一个空格*/x = 1;
//实现可以用一个空格替换所有的空白字符序列(不包括换行符)
//最后,程序准备好进入预处理阶段,预处理器查找一行中以#号开始的预处理指令
//【明示常量:#define】P448
//指令可以出现在源文件的任何地方,其定义从指令出现的地方到该文件末尾有效
//我们大量使用#define指令来定义明示常量(manifest constant)(也叫符号常量),但该指令还有许多其他用途
//预处理指令从#开始,到后面的第1个换行符为止,也就是说,指令的长度仅限于一个逻辑行
//每行#define都由3部分组成
//第一部分是#define指令本身
//第二部分是宏(选定的缩写),有些宏代表值,这些宏被称为类对象宏(object-like macro);C语言还有类函数宏(function-like macro);宏的名称中不允许有空格,遵循C变量的命名规则
//第三部分(指令行的其余部分)称为替换列表或替换体,预处理器会用替换体代替该宏,从宏变成最终替换文本的过程称为宏展开(macro expansion)
//注意,可以在#define行使用注释,如前述,每条注释都会被一个空格代替
#define PX printf("x is %d.\n", x)
PX;
//宏可以表示任何字符串,甚至可以表示整个C表达式
#define TWO 2
#define FOUR TWO*TWO //宏定义还可以包含其他宏
x = FOUR; //宏展开为x = 2*2;
//编译器在编译期对所有的常量表达式求值,所以预处理器不会进行实际的运算,这一过程在编译时进行
//预处理器不做计算,不对表达式求值,它只进行替换
#define FMT "X is %d.\n"
const char* fmt = "X is %d.\n";
printf(FMT, x);
printf(fmt, x);
#define HAL 'Z' //定义了一个字符常量
#define HAP "Z" //定义了一个字符串
printf("TWO.\n"); //双引号中的宏不会被替换
//在算式中用字符常量代替数字,常量名能更清楚地表达该数字的含义
//表示数组大小的数字用符号常量更容易改变数组的大小和循环次数
//系统代码用符号常量表示更容易移植
//【记号】P450
//从技术角度来看,可以把宏的替换体看作是记号(token)型字符串,而不是字符型字符串
//C预处理器记号是宏定义的替换体中单独的“词”,用空白把这些词分开
#define SIX1 3+3 //该宏定义有一个记号:3+3序列
#define SIX2 2 * 3 //该宏定义有3个记号:2、*、3
//替换体中有多个空格时,字符型字符串和记号型字符串的处理方式不同
#define EIGHT 4 * 8
//如果预处理器把上面的替换体解释为字符型字符串,将用4 * 8替换EIGHT,即额外的空格是替换体的一部分
//如果预处理器把上面的替换体解释为记号型字符串,则用3个记号4 * 8(分别由单个空格分隔)来替换EIGHT
//换言之,解释为字符型字符串,把空格视为替换体的一部分;解释为记号型字符串,把空格视为替换体中各记号的分隔符
//在实际应用中,一些C编译器把宏替换体视为字符串而不是记号,在比这个例子更复杂的情况下,两者的区别才有实际意义
//另外,C编译器处理记号的方式比预处理器复杂,由于编译器理解C语言的规则,所以不要求代码中用空格来分隔记号
//例如,C编译器可以把2*2直接视为3个记号,因为它可以识别2是常量,*是运算符
//【重定义常量】
//ANSI标准只有新定义和旧定义完全相同才允许重定义
//具有相同的定义意味着替换体中的记号必须相同,且顺序也相同
#define EIGHT 4 * 8 //这条定义与上面一条都有3个相同的记号,额外的空格不算替换体的一部分
#define EIGHT 4*8 //这条定义与上面的不同,它只有一个记号,有些实现会将其视为错误,如果需要重定义宏,使用#undef指令
//不同的实现采用不同的重定义方案
//如果确实需要重定义常量,使用const关键字和作用域规则更容易些
//【在#define中使用参数】P451
//在#define中使用参数可以创建外形和作用与函数类似的类函数宏
//类函数宏定义的圆括号中可以有一个或多个参数,随后这些参数出现在替换体中
#define MEAN(X,Y) (((X)+(Y))/2)
#define SQUARE(X) X*X //SQUARE是宏标识符,X是宏参数,X*X是替换列表
int z = SQUARE(2); //可以这样调用
//这看上去像函数调用,但它的行为和函数调用完全不同
PX;
printf("SQUARE(x + 2) = %d\n", SQUARE(x + 2));
//宏参数与函数参数不完全相同,因为预处理器不做计算、不求值,只替换字符序列
//因此,SQUARE(x + 2) 变成了 x+2*x+2 ,x=4 时该表达式值为 4+2*4+2 = 14,而不是我们期望的6*6=36
//要修复这个问题需要多加几个圆括号
#undef SQUARE
#define SQUARE(X) (X)*(X)
printf("SQUARE(x + 2) = %d\n", SQUARE(x + 2));
//但这并未解决所有问题
printf("100/SQUARE(2) = %d\n", 100 / SQUARE(2));
//100 / SQUARE(2) 展开为 100/2*2,根据优先级规则,从左往右求值得到100,而不是期望的100/4=25
//要处理前面两种情况,要这样定义
#undef SQUARE
#define SQUARE(X) ((X)*(X))
printf("100/SQUARE(2) = %d\n", 100 / SQUARE(2));
//上面的例子演示了函数调用和宏调用的重要区别,函数调用在程序运行时把参数的值传递给函数,宏调用在编译之前把参数记号传递给程序
//必要时要使用足够多的圆括号来确保运算和结合的正确顺序
//尽管如此,还是无法避免最后一种情况的问题:
SQUARE(++x); //递增了2次x,在C标准中,对该表达式求值是未定义行为
//一般而言,不要在宏中使用递增或递减运算符
//【用宏参数创建字符串:#运算符】
#define PSQR(X) printf("The square of " #X " is %d.\n", ((X)*(X))); //ANSIC字符串的串联特性将字符串组合在一起
PSQR(2 + 4);
PSQR(x);
//C允许在字符串中包含宏参数,在类函数宏的替换体中,#号作为一个预处理运算符,可以把记号转换成字符串
//例如x是一个宏形参,那么#x就是转换为字符串"x"的形参名,这个过程称为字符串化(stringizing)
//参考资料(预处理器运算符):https://docs.microsoft.com/zh-cn/cpp/preprocessor/preprocessor-operators?view=msvc-170
//【预处理器黏合剂:##运算符】
//##运算符把两个记号组合成一个记号
#define XNAME(n) x ## n
int XNAME(4) = 14; //XNAME(4)展开为x4,变成int x4 = 14;
#define PRINT_XN(n) printf("x" #n " = %d\n", x ## n); //##运算符把记号组合为一个新的标识符
PRINT_XN(4);
//【变参宏:...和__VA_ARGS__】
//一些函数(如printf)接受数量可变的参数,stdvar.h头文件提供了工具,让用户自定义带可变参数的函数
//C99/C11也对宏提供了这样的工具,虽然标准中未使用“可变”(variadic)这个词,但它已成为描述这种工具的通用词
//通过把宏参数列表中最后的参数写成省略号来实现这一功能,这样,预定义宏__VA_ARGS__可用在替换部分中,表明省略号代表什么
#define PR(X, ...) printf("Message " #X ":" __VA_ARGS__)
PR(1, "x = %d\n", x);
//省略号只能代替最后的宏参数,#define WRONG(X, ..., Y) //不能这样做
//【宏和函数的选择】P454
//使用宏比使用普通函数复杂一些,稍有不慎会产生奇怪的副作用
//一些编译器规定宏只能定义成一行,不过,即使编译器没有这个限制,也应该这样做
//宏和函数的选择实际上是时间和空间的权衡
//宏生成内联代码,即在程序中生成语句。如果调用20次宏,即在程序中插入20行代码。
//如果调用函数20次,程序中只有一份函数语句的副本,所以节省了空间
//然而另一方面,程序的控制必须跳转至函数内,随后再返回主调程序,这显然比内联代码花费更多的时间
//宏的一个优点是,不用担心变量类型(因为宏处理的是字符串,而不是实际的值)
//C99提供了第3种可替换的方法——内联函数
//对于简单的函数,通常使用宏
#define MAX(X,Y) ((X) > (Y) ? (X) : (Y))
#define ABS(X) ((X) < 0 ? -(X) : (X))
#define ISSIGN(X) ((X) == '+' || (X) == '-' ? 1 : 0)
//注意:
//宏名中不允许有空格,但是在替换字符串中可以有空格,ANSI C允许在参数列表中使用空格
//用圆括号把宏的参数和整个替换体括起来,这样能确保其正确展开
//用大写字母表示宏函数的名称。该惯例不如用大写字母表示宏常量应用广泛,但大写字母可以提醒程序员注意宏可能产生的副作用
//如果打算使用宏来加快程序的运行速度,首先要确定使用宏和使用函数是否会导致较大差异。在嵌套循环中使用宏更有助于提高效率。许多系统提供程序分析器帮助程序员压缩程序中最耗时的部分。
//【文件包含:#include】P455
//预处理器发现#include指令时,会查看后面的文件名并把文件的内容包含到当前文件中,即替换源文件中的#include指令,相当于把被包含文件的全部内容输入到源文件#include指令所在的位置
//在UNIX系统中,尖括号告诉预处理器在标准系统目录中查找该文件,双引号告诉预处理器首先在当前目录中(或文件名中指定的其他目录)查找该文件,如果未找到再查找标准系统目录
//IDE中也有标准路径或系统头文件的路径,许多IDE提供菜单选项,指定用尖括号时的查找路径,双引号具体先查找哪个目录取决于编译器的设定
//C语言习惯用.h后缀表示头文件,这些文件包含需要放在程序顶部的信息
//头文件经常包含一些预处理器指令,你可以创建自己的头文件
//包含一个大型头文件不一定显著增加程序的大小,大部分情况下,头文件的内容是编译器生成最终代码时所需的信息,而不是添加到最终代码中的材料
//可执行代码通常在源代码文件中,而不是在头文件中
//通常,应该用#ifndef和#define防止多重包含头文件
//头文件中最常用的形式如下
//明示常量:例如stdio.h中定义的EOF、NULL、BUFSIZE(标准I/O缓冲区大小)
//宏函数:例如getchar()通常使用getc(stdin)定义,而getc()经常用于定义较复杂的宏,ctype.h通常包含ctype系列函数的宏定义
//函数声明:例如string.h包含字符串函数系列的函数声明,函数声明都是函数原型形式
//结构模板定义:标准I/O函数使用FILE结构,该结构中包含了文件和与文件缓冲区相关的信息,FILE结构在头文件stdio.h中
//类型定义:标准I/O函数使用指向FILE的指针作为参数,通常,stdio.h用#define或typedef把FILE定义为指向结构的指针,类似地,size_t和time_t类型也定义在头文件中
//还可以用头文件声明外部变量供其他文件共享
//例如,已经开发了共享某个变量的一系列函数,该变量报告某种状况,这种方法就很有效
//可以在包含这些函数声明的源代码文件定义一个文件作用域的外部链接变量:int status = 0;
//然后,可以在与源代码文件相关联的头文件中进行引用式声明:extern int status;
//这行代码会出现在包含了该头文件的文件中,这样使用该系列函数的文件都能使用这个变量
//虽然源代码文件中包含该头文件后也包含了该声明,但只要声明的类型一致,在一个文件中同时使用定义式声明和引用式声明没问题
//需要包含头文件的另一种情况是,使用具有文件作用域、内部链接和const限定符的变量或数组
//static意味着每个包含该头文件的文件都获得一份副本,因此不需要在一个文件中进行定义式声明,在其他文件中进行引用式声明
//【其他指令】P458
//【#undef指令】
//#undef用于取消已定义的#define指令
#define LIMIT 400
#undef LIMIT
//即使原来没有定义LIMIT,取消LIMIT的定义仍然有效,如果不确定一个名称之前是否用过,为安全起见,可以用#undef指令取消该名字的定义
//【从C预处理器角度看已定义】
//预处理器在预处理器指令中发现一个标识符时,会把该标识符当作已定义的或未定义的
//这里的已定义表示由预处理器定义,如果标识符是同一个文件中由前面的#define指令创建的宏名且没有用#undef关闭,那么该标识符是已定义的
//如果标识符不是宏,假设是一个C变量,那么该标识符对预处理器而言就是未定义的
//已定义宏可以是对象宏,包括空宏或类函数宏
//#define宏的作用域从它在文件中的声明处开始,直到用#undef取消宏为止,或延伸至文件尾
//如果宏通过头文件引入,那么#define在文件中的位置取决于#include指令的位置
//预定义宏不能取消定义
//【条件编译(conditional compilation)】
//#ifdef、#else和#endif指令
#ifdef MAVIS
#define STABLES 5 //若MAVIS已定义,执行下面的指令
puts("TEST1");
#else
#define STABLES 15 //若MAVIS未定义,执行下面的指令
puts("TEST2");
#endif // MAVIS
//endif必须存在,指令结构可以嵌套,可以用这种方法来调试程序
//#ifndef指令
//逻辑与ifdef相反,判断是否未定义
#ifndef SIZE
#define SIZE 100
#endif // !SIZE
//ifndef指令可以防止相同的宏被重复定义
//可以在include包含以上宏定义文件之前定义SIZE,用于测试时使用,测试完毕后移除定义重新编译即可避免修改头文件本身
//ifndef指令通常用于防止多次包含一个文件,也就是说,应该像下面这样设置头文件
#ifndef FILENAME_H__
#define FILENAME_H__
;//头文件中的其他内容
#endif // !FILENAME_H_
//许多被包含的文件中都包含着其他文件,所以显式包含的文件中可能包含着已经包含的其他文件
//标准保留了使用下划线作为前缀,所以建议在自己的代码中不要这样写:__FILENAME_H__
//#if和#elif指令
//#if后面跟整型常量表达式,表达式非0则表达式为真,可以在指令中使用C的关系运算符和逻辑运算符
#if SIZE == 10
//代码
#elif SIZE == 20
//代码
#elif SIZE == 30
//代码
#else
//代码
#endif // SIZE==10
#if defined (VAX) //可以代替#ifdef VAX,它可以和#elif一起使用
//#include "vax.h"
#elif defined (IBMPC)
#elif defined (MAC)
#endif
//条件编译让程序更容易移植,改变文件开头部分的几个关键定义,即可根据不同的系统设置不同的值和包含不同的文件
puts("");
//【预定义宏】P462
puts(__DATE__);//预处理的日期(字符串字面量)
puts(__TIME__);//翻译代码的时间
puts(__FILE__);//当前源代码文件名(字符串字面量)
printf("__LINE__ %d\n", __LINE__);//当前源代码文件行号(整型常量)
//puts(__STDC__);//设置为1,表明实现遵循C标准
printf("__STDC_HOSTED__ %d\n", __STDC_HOSTED__);//本机环境设置为1,否则设置为0
printf("__STDC_VERSION__ %ld\n", __STDC_VERSION__);//支持C99标准,设置为199901L;C11设置为201112L;C17设置为201710L
printf("This function is %s\n", __func__);
//C99提供一个名为__func__的预定义标识符,展开为一个代表包含该标识符的函数名的字符串
//__func__具有函数作用域,而宏具有文件作用域,故__func__是C语言的预定义标识符,而不是预定义宏
//【#line和#error】
//#line指令重置__LINE__和__FILE__宏报告的行号和文件名。
#line 1000 //重置当前行号为1000
#line 10 "TEST.c" //重置行号和文件名
puts(__FILE__);
//#error指令让预处理器发出一条错误消息,该消息包含指令中的文本
//如果可能,编译过程应中断
#if __STDC_VERSION__ != 201710L
#error Not C17
#endif // __STDC_VERSION__ != 201112L
//#pragma
//现代编译器可以通过命令行参数或IDE菜单修改编译器的一些设置,#pragma把编译器指令放入源代码中
//编译器都有自己的编译指示集,例如编译指示可能用于分配给自动变量的内存量、设置错误检查的严格程度、启用非标准语言特性等
//C99还提供了_Pragma预处理器运算符,该运算符把字符串转换成普通的编译指示
_Pragma("TEST"); //等价于#pragma TEST
//由于该运算符不使用#符号,所以可以把它作为宏展开的一部分
#define PRAGMA(X) _Pragma(#X)
//【泛型选择(C11)】
//在程序设计中,泛型编程(generic programming)指那些没有特定类型,但是一旦指定一种类型就能转换成指定类型的代码
//C11新增了一种表达式,泛型选择表达式(generic selection expression)
//可根据表达式的类型选择一个值。泛型选择表达式不是预处理器指令,但在一些泛型编程中它常用作#define宏定义的一部分
_Generic(x, int: 0, float: 1, double: 2, default: 3);
//_Generic是C11的关键字,括号中第1个项是一个表达式,后面的每项都由一个类型、一个冒号、一个值组成
//第1个项的类型匹配哪个标签,整个表达式的值就是该标签后面的值
//泛型选择语句与switch语句类似,前者用表达式的类型匹配标签,后者用表达式的值匹配标签
#define MYTYPE(X) _Generic((X),\
int: "int",\
float: "float",\
double: "double",\
default: "other"\
)
int d = 5;
puts(MYTYPE(d));
puts(MYTYPE(2.0 * d));
puts(MYTYPE(3L));
puts(MYTYPE(&d));
//对一个泛型表达式求值时,程序不会先对第一个项求值,它只确定类型,只有匹配标签的类型后才会对表达式求值
//【内联函数(C99)】P465
//使用宏使代码内联,可以避免函数调用开销;C99还提供另一种方法:内联函数(inline function)
//标准描述是「把函数变成内联函数意味着尽可能快地调用该函数,其具体效果由实现定义」
//因此,把函数变成内联函数,编译器可能会用内联代码替换函数调用,并(或)执行一些其他的优化,但也可能不起作用
//创建内联函数的定义由多种方法,标准规定具有内部链接的函数可以成为内联函数,还规定了内联函数的定义与调用该函数的代码必须在同一个文件中
//因此,最简单的方法是使用函数说明符inline和存储类别说明符static
//通常,内联函数应定义在首次使用它的文件中,所以内联函数也相当于函数原型
inlinecode();
//编译器查看内联函数的定义(也是原型),可能会用函数体中的代码替换函数调用
//由于并未给内联函数预留单独的代码块,所以无法获得内联函数的地址(实际上可以获得地址,不过这样做之后,编译器会生成一个非内联函数),另外,内联函数无法在调试器中显示
//内联函数应该比较短小,较长的函数变成内联并未节约多少时间
//一般情况下内联函数都具有内部链接,因此如果多个文件都要用某个内联函数,最简单的做法是把内联函数定义放入头文件
//一般都不在头文件中放置可执行代码,内联函数是个特例,因为内联函数具有内部链接,所以在多个文件中定义同一个内联函数不会产生什么问题
//与C++不同的是,C还允许混合使用内联函数定义和外部函数定义
//【_Noreturn函数(C11)】P467
//C99新增inline关键字时,它是唯一的函数说明符(关键字extern和static是存储类别说明符,可应用于数据对象和函数)
//C11新增了第2个函数说明符_Noreturn,表明调用完成后函数不返回主调函数
//exit()函数是_Noreturn函数的一个示例
//_Noreturn的目的是告诉用户和编译器,这个特殊的函数不会把控制返回主调函数,告诉用户以免滥用该函数,通知编译器可优化一些代码
//【C库】
//库包含:在编译或链接程序的某些阶段,可能需要指定库选项。即使在自动检查标准库的系统中,也会有不常用的函数库,必须通过编译时选项显式指定这些库
//头文件提供函数声明或原型,而库选项告诉系统到哪里查找函数代码
//【数学库】
//数学库中包括许多有用的数学函数,math.h头文件提供这些函数的原型。
//注意:函数中涉及的角度都以弧度为单位(一周的弧度数为2πr/r=2π 即360°角=2π弧度,1弧度=180/π=57.296°)
//
//double acos(double x) 返回余弦值为x的角度(0~π弧度)
//asin 返回正弦值为x的角度(-π/2~π/2弧度)
//atan 返回正切值为x的角度(-π/2~π/2弧度)
//atan2(double y, double x) 返回正切值为y/x的角度(-π~π弧度)
//cos、sin、tan 返回x的余弦、正弦、正切值,x的单位为弧度
//exp 返回x的指数函数的值(e^x)
//log 返回x的自然对数值 log_e x(ln x)
//log10 返回x的以10为底的对数值
//double pow(double x, double y) 返回x的y次幂
//sqrt、cbrt、fabs 返回x的平方值、立方值、绝对值
//ceil (向正无穷取整) 返回不小于x的最小整数值
//floor(向负无穷取整) 返回不大于x的最大整数值
//
//把弧度转换为度,只需将弧度值乘以180,再除以pi即可
//pi的值通过计算表达式 4*atan(1) 得到
//【类型变体】
//基本的浮点型数学函数接受double参数并返回double值,如果传递float、long double类型参数给它们,可能会降低计算速度或损失精度
//C标准专门为float、long double类型提供了标准函数,即在原函数名后加上f或l后缀
//sqrtf()是sqrt的float版本,sqrtl()是long double版本
#define RAD_TO_DEG (180/(4 * atanl(1)))
//利用C11新增的泛型选择表达式定义一个泛型宏
//泛型平方根函数,_Generic表达式的值是一个指向函数的指针
#define SQRT(X) _Generic((X), \
long double: sqrtl, \
default: sqrt, \
float: sqrtf)(X)
//泛型正弦函数,角度的单位为度
#define SIN(X) _Generic((X), long double: sinl((X) / RAD_TO_DEG), default: sin((X) / RAD_TO_DEG), float: sinf((X) / RAD_TO_DEG))
//对于SIN(),函数调用在泛型选择表达式内部;对于SQRT(),先对泛型选择表达式求值得一个指针,然后通过该指针调用它所指向的函数
//【tgmath.h库(C99)】
//C99标准提供的tgmath.h头文件(包含math.h complex.h)定义了泛型类型宏
//如果math.h中为一个函数定义了3种类型,tgmath.h就创建一个泛型类型宏,与原来double版本的函数名同名
//如果编译器支持复数运算,就会支持complex.h,例如:sqrt()宏可以展开为sqrtf、sqrt、sqrtl以及csqrtf、csqrt(返回double complex类型的复数平方根)、csqrtl
//如果包含了tgmath.h,要调用sqrt()函数而不是sqrt()宏,可以用圆括号把被调用的函数名括起来,还可以使用另一种形式
(sqrt)(2);
(*sqrt)(2);
//不借助C标准以外的机制,C11新增的_Generic表达式是实现tgmath.h最简单的方式
//【通用工具库】P472
//stdlib.h
//【exit()和atexit()函数】
//在main()返回系统时将自动调用exit()函数
//ANSI标准还可以指定在执行exit()时调用的特定函数,atexit()通过注册要在退出时调用的函数来提供这一特性,atexit()函数接受一个函数指针作为参数
atexit(sign_off);
atexit(bye); //先执行
//atexit()注册函数列表中的函数,当调用exit()时就会执行这些函数(执行顺序与列表中的函数顺序相反:最后添加的函数最先执行)。ANSI保证在这个列表中至少可以放32个函数
//atexit()注册的函数应该不带任何参数且返回类型为void,通常这些函数会执行一些清理任务
//exit()执行完atexit指定的函数后,会完成一些清理工作:刷新所有输出流、关闭所有打开的流和关闭由标准I/O函数tmpfile()创建的临时文件
//然后exit()把控制权返回主机环境,如果可能则向主机环境报告终止状态
//通常UNIX程序用0表示成功终止,用非零值表示终止失败;ANSI为了可移植性要求,定义EXIT_FAILURE宏表示终止失败,EXIT_SUCCESS表示成功终止
//在ANSIC中,在非递归的main中使用exit()函数等价于使用关键字return,尽管如此,在main()以外的函数中使用exit()也会终止整个程序
//【qsort()函数】
//快速排序算法在C实现中的名称是qsort(),它排序数组的数据对象,原型如下
//void qsort(void* base, size_t nmemb, size_t size, int (*compar)(const void*, const void*));
//参数:一是待排序数组指针,二是待排序项数,三是待排序数组每个元素的大小,四是指向比较函数的指针
//最后一个指针指向的比较函数用于确定排序的顺序,该函数接受两个参数:分别指向待比较两项的指针。
//如果第1项的值大于第2项,比较函数返回正数,两项相同返回0,第1项小于第2项返回负数
//(qsort()根据给定的其他信息计算两个指针的值,然后把它们传递给比较函数)
#define NUM 36
double vals[NUM];
fillarray(vals, NUM);
puts("List:");
showarray(vals, NUM);
qsort(vals, NUM, sizeof(double), comp);
puts("\nSorted List:");
showarray(vals, NUM);
putchar('\n');
//C和C++都可以把任何类型的指针赋给void类型的指针,但C++要求把void*指针赋给任何类型指针时必须进行强制类型转换,而C没有这样的要求
//【断言库】P477
//assert.h头文件支持的断言库是一个用于辅助调试程序的小型库,它由assert()宏组成,接受一个整型表达式作为参数
//如果表达式求值为假(0),assert宏就在标准错误流stderr中写入一条错误信息,并调用abort()函数终止程序(abort()的原型在stdlib.h中)
//assert()宏是为了标识出程序中某些条件为真的关键位置,如果其中的一个条件为假,就终止程序
//如果assert()中止了程序,它首先会显示失败的测试、包含测试的文件名和行号
assert(1);
//用if语句+abort()也能完成类似的任务,但使用assert()有几个好处:它不仅能自动标识文件和出问题的行号,还有一种无需更改代码快速开关的机制
#define NDEBUG
//把宏定义NDEBUG写在包含assert.h的位置前面,编译器就会禁用文件中所有assert()语句
//_Static_assert(C11)
//assert()在运行时检查,_Static_assert可以在编译时检查
//assert()导致正在运行的程序中止,_Static_assert()导致程序无法通过编译
//_Static_assert()接受2个参数,一是整型常量表达式,二是一个字符串;如果参数1求值为0(或_False)编译器会显示字符串,而且不编译该程序
_Static_assert(CHAR_BIT == 8, "Message");
//根据语法,_Static_assert()被视为声明,因此它可以出现在函数中或函数的外部
//_Static_assert要求它的第1个参数是整型常量表达式,这保证了能在编译期求值
//【string.h库中的memcpy()和memmove()】P479
//不能把一个数组赋给另一个数组,有一个例外是:使用strcpy()和strncpy()函数来处理字符数组
//memcpy()和memmove()函数提供类似的方法处理任意类型的数组
//void* memcpy(void* restrict s1, const void* restrict s2, size_t n);
//void* memmove(void* s1, const void* s2, size_t n);
//这两个函数都从s2指向的位置拷贝n字节到s1指向的位置,且都返回s1的值
//不同的是,memcpy()的参数带关键字restrict,即memcpy()假设两个内存区域之间没有重叠
//memmove()不做这样的假设,所以拷贝过程类似于先把所有字节拷贝到一个临时缓冲区,再拷贝到最终目的地
//如果使用memcpy()时出现区域重叠,其行为是未定义的
double memcpytest[5] = { 0.1,0.2,0.3,0.4,0.5 };
double memcpynew[5];
memcpy(memcpynew, memcpytest, 5 * sizeof(double));
showarray(memcpytest, 5);
showarray(memcpynew, 5);
//memcpy()函数不知道也不关心数据的类型,它只负责从一个位置把一些字节拷贝到另一个位置,拷贝过程中也不会进行数据转换
//【可变参数:stdarg.h】P481
//stdarg.h为函数提供了一个类似变参宏的功能,但用法比较复杂,必须按如下步骤进行
//1.提供一个使用省略号的函数原型
//2.在函数定义中创建一个 va_list 类型的变量
//3.用宏把该变量初始化为一个参数列表
//4.用宏访问参数列表
//5.用宏完成清理工作
//省略号的前一个形参起着特殊的作用,标准中用 parmN 这个术语来描述该形参;【传递给该形参的实际参数是省略号部分代表的参数数量】
f1(4, 10, 50, 100, 500); //4个额外参数
//定义在stdarg.h中的 va_list 类型代表一种用于存储形参对应的形参列表中省略号部分的数据对象
//然后该函数使用定义在stdarg.h中的va_start()宏,把参数列表拷贝到 va_list 类型的变量中
//访问参数列表的内容使用 va_arg() 宏,第一次调用返回参数列表第一项,第二次返回第二项
//最后,要使用 va_end() 宏完成清理工作,例如释放动态分配用于存储参数的内存
//因为 va_arg()不提供退回之前参数的方法,所以有必要保存 va_list 类型变量的副本
//C99新增了 va_copy() 宏用于处理这种情况
}
void f1(int n, ...)
{
va_list ap; //声明一个存储参数的对象
//n 是 parmN 形参,它表明变参列表中参数的数量
va_start(ap, n); //把ap初始化为参数列表;两个参数:va_list 类型的变量、parmN 形参
va_list apcopy;
va_copy(apcopy, ap);//把第2个参数拷贝给第1个参数
//此时,即使删除了ap,也可以从apcopy中检索参数
int a, b, c, d;
a = va_arg(ap, int); //检索第一个参数。接受两个参数:va_list 类型的变量、类型名
b = va_arg(ap, int); //检索第二个参数。传入的参数类型必须与宏参数的类型匹配
c = va_arg(ap, int);
d = va_arg(ap, int);
printf("%d %d %d %d\n", a, b, c, d);
va_end(ap); //清理工作
//调用va_end()后,只有用va_start()重新初始化ap后才能使用变量ap
}
void fillarray(double ar[], int n)
{
for (int i = 0; i < n; i++)
{
ar[i] = (double)rand() / ((double)rand() + 0.1);
}
}
void showarray(const double ar[], int n)
{
int i;
for (i = 0; i < n; i++)
{
printf("%9.4f ", ar[i]);
if (i % 6 == 5)
{
putchar('\n');
}
}
if (i % 6 != 0)
{
putchar('\n');
}
}
int comp(const void* p1, const void* p2)
{
const double* a1 = (const double*)p1;
const double* a2 = (const double*)p2; //C++要求void指针赋给其他类型指针时必须进行强制类型转换
if (*a1 < *a2)
{
return -1;
}
else if (*a1 == *a2)
{
return 0;
}
else
{
return 1;
}
}
高级数据表示
示例代码: 链表
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//通常,程序开发最重要的部分是找到程序中表示数据的好方法
//找出正确的数据表示不仅仅是选择一种数据类型,还要考虑必须进行哪些操作
//也就是说,必须确定如何存储数据,并且为数据类型定义有效的操作
//C语言为它的基本类型都定义了有效的操作,但你可能需要自己定义有效操作,可以把所需的操作设计成C函数来表示
//设计一种数据类型:设计如何存储该数据类型、设计一系列管理该数据的函数
//【从数组到链表】P488
//理想情况是用户可以不确定地添加数据(直到用完内存),而不是先指定要输入多少项(数组大小),也不用让程序分配多余的空间
//调用malloc()一次为300个结构分配空间获得的是连续的内存块,只需存储指向已分配块中第一个结构的指针,而调用300次malloc()结构不一定被连续存储
//可以在每个结构中包含指向next结构的指针,然后当创建新结构时,把该结构的地址存储在上一个结构中
#define TSIZE 45
struct film
{
char title[TSIZE];
int rating;
struct film* next;
};
//虽然结构不能含有与本身类型相同的结构,但是可以含有指向同类型结构的指针
//这种定义是定义链表(linked list)的基础,链表中的每一项都包含着在何处能找到下一项的信息
//为了表明结构后面没有其他结构,要把next成员指针设置为空指针NULL
//还需要一个单独的指针存储第1个结构的地址,该指针被称为头指针(head pointer),指向链表中的第1项
char* s_gets(char*, int);
int main(void)
{
struct film* head = NULL;
struct film* prev = NULL, * 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));
if (!current) //检查是否成功请求到内存
{
puts("Memory Error.");
break;
}
if (head == NULL) //第1个结构
head = current;
else
prev->next = current;
current->next = NULL;
strcpy_s(current->title, TSIZE, input);
puts("Enter your rating <0-10>:");
scanf_s("%d", ¤t->rating);
while (getchar() != '\n')
continue;
prev = current;
puts("Enter next movie title (empty line to stop):");
}
//显示电影列表
if (head == NULL)
puts("No data entered.");
else
puts("Here is the movie list:");
current = head;
while (current != NULL)
{
printf("Movie: %s Rating: %d\n", current->title, current->rating);
current = current->next;
}
//完成任务,释放内存
current = head;
while (current != NULL)
{
head = current->next;
free(current); //在许多环境中,程序结束时都会自动释放malloc()分配的内存,但最好还是成对调用malloc()和free()
current = head;
}
puts("Bye!");
//这种用特定方法解决特定问题,并且在需要时才添加相关功能的编程方式不是最好的解决方案,也难以预料程序要完成的所有任务
//首先应该强调最初的设计,并简化其他细节
//该程序的概念模型是在一个链表中添加项,但程序却把一些细节放在了最明显的位置,没有突出接口
//如果程序能以某种方式强调给链表添加项,并隐藏具体的处理细节会更好
//把用户接口和代码细节分开的程序,更容易理解和更新
}
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)
{
*find = '\0';
}
else
{
while (getchar() != '\n')
continue;
}
}
return ret_val;
}
示例代码: 抽象数据类型(ADT)
//【抽象数据类型(ADT)】P494
//编程时应根据编程问题匹配合适的数据类型
//前例中,我们定义了一个结构代表单独的项,然后设计了一些方法把一系列结构构成一个链表
//本质上,我们使用C语言的功能设计了一种符合程序要求的新数据类型
//但我们的做法并不系统
//现在,我们用更系统的方法来定义数据类型
//什么是类型?类型特指两类信息:属性和操作
//例如,int类型的属性是它代表一个整数值,因此它共享整数的属性。允许对int进行的算数操作有改变符号、相加减乘除求模。声明一个int变量时就表明了只能对该变量进行这些操作。
//(C并未很好地实现整数。例如整数是无穷大的数,但2字节的int只能表示65536个整数。不要混淆抽象概念和具体的实现)
//要定义一个新的数据类型,首先必须提供存储数据的方法(例如设计一个结构),其次必须提供操控数据的方法
//计算机科学领域已开发了一种定义新类型的好方法,用3个步骤完成从抽象到具体的过程:
//1.提供类型属性、和相关操作的抽象描述。这种正式的抽象描述被称为抽象数据类型(ADT)。(这些描述既不能依赖特定的实现,也不能依赖特定的编程语言)
//2.开发一个实现ADT的编程接口。也就是指明如何存储数据和执行所需操作的函数。(例如在C中可以提供结构定义和操控该结构的函数原型)
// 这些作用于用户定义类型的函数相当于作用于C基本类型的内置运算符,需要使用该新类型的程序员可以使用这个接口进行编程。
//3.编写代码实现接口。这一步至关重要,但使用该类型的程序员无需了解具体的实现细节。
//【建立抽象】P494
//链表具有哪些属性?链表应该能存储一系列的项,且这些项以某种方式排列,其次链表类型应该提供一些操作,下面是链表的一些有用的操作:
//初始化一个空链表、在链表末尾添加一个新项、确定链表是否为空、确定链表是否已满、确定链表中的项数、访问链表中的每一项执行某些操作 如显示该项
//对电影项目而言暂时不需要其他操作,但一般的链表还应包括以下操作:
//在链表的任意位置插入一个项、移除链表中的一个项、在链表中检索一个项(不改变)、用另一个项替换链表中的一个项、在链表中搜索一个项
//非正式但抽象的链表定义是:链表是一个能存储一系列项且可以对其进行所需操作的数据对象
//该定义既未说明链表中可以存储什么项,也未指定是用数组、结构还是其他数据形式来存储项,且并未规定用什么方法来实现操作,这些细节都留给实现完成
//为让示例尽量简单,采用一种简化的链表作为抽象数据类型,该类型总结如下:
//类型名: 简单链表
//类型属性:可以存储一系列项
//类型操作:初始化链表为空、确定链表为空、确定链表已满、确定链表中的项数、在链表末尾添加项、遍历链表 处理链表中的项、清空链表
//下一步是为简单链表ADT开发一个C接口
//【建立接口】P495
//这个简单链表的接口有两个部分:描述如何表示数据、描述实现ADT操作的函数
//接口设计应尽量与ADT的描述保持一致,因此应用某种通用的Item类型而不是一些特殊类型,如int或struct film
//可以用typedef来定义所需的Item类型
/*
#define TSIZE 45
struct film
{
char title[TSIZE];
int rating;
};
typedef struct film Item;
//然后,就可以在定义的其余部分使用Item类型。
//如果以后需要其他数据形式的链表,可以重新定义Item类型,不必更改其余的接口定义
//定义了Item后,现在必须确定如何存储这种类型的项,实际上这一步属于实现步骤,但现在决定好可以让示例更简单些
typedef struct node
{
Item item;
struct node* next;
} Node;
typedef Node* List;
//在链表的实现中,每一个链节叫作节点(node)。每个节点包含形成链表内容的信息和指向下一个节点的指针。
//最后,为了管理链表,还需要一个指向链表开始处的指针,我们使用typedef把List作为该类型的指针名
//这不是定义List类型的唯一方法,还可以添加一个变量记录项数
typedef struct list
{
Node* head;
int size;
} List;
//这里要着重理解下面的声明创建了一个链表,而不是一个指向节点的指针或一个结构
List movies;
//movies代表的确切数据应该是接口层次不可见的实现细节
//例如,程序启动后应把头指针初始化为NULL,但不要使用下面的代码
//movies = NULL;
//因为稍后你可能会发现List类型的结构实现更好,它这样初始化
//movies.head = NULL;
//movies.size = 0;
//使用List的人都不用担心这些细节,只要能使用下面的代码就行:
//InitializeList(movies);
//使用该类型的程序员只需知道用InitializeList()函数来初始化链表,不必了解List类型变量的实现细节
//这是「数据隐藏」的一个示例,数据隐藏是一种从编程的更高层次隐藏数据表示细节的艺术
//为了指导用户使用,可以在函数原型前面提供注释
//操作:初始化一个链表
//前提条件(precondition):plist指向一个链表
//后置条件(postcondition):该链表初始化为空
void InitializeList(List* plist);
//注释中的前提条件是调用该函数前应具备的条件,后置条件是执行完该函数后的情况
//该函数的参数是一个指向链表的指针,而不是一个链表,所以应该这样调用函数:
//InitializeList(&movies);
*/
//C语言把所有类型和函数的信息集合成一个软件包的方法是:
//把类型定义和函数原型(包括注释)放在一个头文件中,该文件应该提供程序员使用该类型所需的所有信息
//在头文件中,把组成函数名的每个单词首字母大写,表明这些函数是接口包的一部分
//【使用接口】P498
//我们的目标是,使用这个接口编写程序,但不必知道具体的实现细节
//【实现接口】P500
//我们还必须实现List接口。C方法是把函数定义统一放在list.c文件中
//然后,整个程序由list.h(定义数据结构和提供用户接口的原型)、list.c(提供函数代码实现接口)、film.c(把链表接口应用于特定编程问题的源代码文件)组成
//ADT方法是根据待解决的问题来表达程序,而不是解决问题所需的具体工具来表达程序,可读性更高,而且针对的是最终用户所关心的问题
//其次,list.h和list.c文件一起组成了可复用的资源,如果需要另一个简单的链表,只需重新定义Item类型就可以了
//另外,用户接口是根据抽象链表操作定义的,不是根据某些特定的数据表示和算法来定义;不用重写最后的程序就能随意修改实现
//例如,当前使用AddItem()函数效率不高,因为它总是从链表第一个项开始,然后搜索至链表末尾,可以通过保存链表结尾处的地址来解决这个问题
//typedef struct list {
// Node* head;
// Node* end;
//} List;
//当然,还要根据新的定义重写处理链表的函数,但不用修改film.c的内容
//对大型编程项目而言,这种把实现和最终接口隔离的做法相当有用;这称为数据隐藏,因为对终端用户隐藏了数据表示的细节
//如果程序运行出现问题,可以把问题定位到具体的函数上;如果想用更好的方法来完成某个任务,只需重写相应的函数即可
//如果需要新功能,可以添加一个新的函数;重构实现的代码不用修改使用实现的程序
list.h
#pragma once
//https://zh.wikipedia.org/wiki/Pragma_once
//在C和C++中,#pragma once是一个非标准但是被广泛支持的前置处理符号,会让所在的文件在一个单独的编译中只被包含一次。
//它提供类似include防范的目的,但是拥有较少的代码且能避免名称的碰撞。
//由于编译器每次都需要打开头文件才能判定是否有重复定义,因此在编译大型项目时,ifndef会使得编译时间相对较长
//使用#pragma once代替include防范可能加快编译速度,因为这是一种高端的机制;编译器会自动比对文件名称或inode而不需要在头文件去判断#ifndef和#endif。
#include <stdbool.h>
/* 特定程序的声明 */
#define TSIZE 45 //存储电影名的数组大小
struct film
{
char title[TSIZE];
int rating;
};
/* 一般类型定义 */
typedef struct film Item;
typedef struct node
{
Item item;
struct node* next;
} Node;
typedef Node* List;
/* 函数原型 */
//操作:初始化一个链表
//前提条件(precondition):plist指向一个链表
//后置条件(postcondition):该链表初始化为空
void InitializeList(List* plist);
//操作:确定链表是否为空,plist指向一个已初始化的链表
//后置条件:链表为空返回true,否则返回false
bool ListIsEmpty(const List* plist);
//操作:确定链表是否已满,plist指向一个已初始化的链表
//后置条件:如果链表已满,返回true,否则返回false
bool ListIsFull(const List* plist);
//返回链表中的项数,plist指向一个已初始化的链表
unsigned int ListItemCount(const List* plist);
//在链表末尾添加项,成功返回ture,失败返回false
//item是一个待添加至链表的项目,plist指向一个已初始化的链表
bool AddItem(Item item, List* plist);
//plist指向一个已初始化的链表
//pfun指向一个函数,该函数接受一个Item类型的参数,且无返回值
//pfun作用于链表中的每一项一次
void Traverse(const List* plist, void (*pfun)(Item item));
//释放已分配的内存(如果有的话)
//释放为链表分配的所有内存,链表设置为空
void EmptyTheList(List* plist);
//从技术上看,只有要修改链表的函数需要指针参数,但如果某些函数接受List变量,其他函数却接受List地址作为参数,用户会很困惑
//为了减轻用户的负担,所有的函数均使用指针参数
list.c
#include <stdio.h>
#include <stdlib.h>
#include "list.h"
/* 局部函数原型 */
static void CopyToNode(Item item, Node* pnode);
/* 接口函数 */
//把链表设置为空
void InitializeList(List* plist)
{
*plist = NULL;
}
//链表为空返回true
bool ListIsEmpty(const List* plist)
{
if (*plist == NULL)
{
return true;
}
else
{
return false;
}
}
//链表已满返回true
bool ListIsFull(const List* plist)
{
Node* pt;
bool full;
pt = (Node*)malloc(sizeof(Node));
if (pt == NULL)
{
full = true;
}
else
{
full = false;
}
free(pt);
return full;
}
//返回节点的数量
unsigned int ListItemCount(const List* plist)
{
unsigned int count = 0;
Node* pnode = *plist;
while (pnode != NULL)
{
++count;
pnode = pnode->next;
}
return count;
}
//创建存储项的节点,并将其添加至链表末尾(较慢的实现)
bool AddItem(Item item, List* plist)
{
Node* pnew;
Node* scan = *plist;
pnew = (Node*)malloc(sizeof(Node));
if (pnew == NULL)
{
return false; //给链表添加项之前应调用ListIsFull()函数,但用户可能并未这样做
}
CopyToNode(item, pnew);
pnew->next = NULL;
if (scan == NULL) //空链表
{
*plist = pnew;
}
else
{
while (scan->next != NULL)
{
scan = scan->next;
}
scan->next = pnew;
}
return true;
}
//访问每个节点并执行pfun
void Traverse(const List* plist, void (*pfun)(Item item))
{
Node* pnode = *plist;
while (pnode != NULL)
{
(*pfun)(pnode->item);
pnode = pnode->next;
}
}
//释放内存,设置链表指针为NULL
void EmptyTheList(List* plist)
{
Node* psave;
while (*plist != NULL)
{
psave = (*plist)->next;
free(*plist);
*plist = psave;
}
}
/* 局部函数定义 */
//把每一个项拷贝到节点中
static void CopyToNode(Item item, Node* pnode)
{
pnode->item = item;
}
//const List* plist 声明可以提供一些保护,它防止*plist被修改
//但*plist被看作是const并不意味着*plist指向的数据是const,因此可以编写下面的代码
//(*plist)->item.rating = 3;
//即使*plist是const也可以这样做,因为上面的代码并未改变*plist,它改变的是*plist指向的数据
film.c
/* 使用抽象数据类型(ADT)风格的链表 */
/* 与list.c一起编译 */
#include <stdio.h>
#include <stdlib.h>
#include "list.h"
void showmovies(Item item);
char* s_gets(char* st, int n);
int main(void)
{
List movies;
Item temp;
/* 初始化 */
InitializeList(&movies);
if (ListIsFull(&movies))
{
fprintf(stderr, "No memory available! Bye!\n");
exit(1);
}
/* 获取用户输入并存储 */
puts("Enter first movie title:");
while (s_gets(temp.title, TSIZE) != NULL && temp.title[0] != '/0')
{
puts("Enter your rating <0-10>:");
scanf_s("%d", &temp.rating);
while (getchar() != '\n')
continue;
if (AddItem(temp, &movies) == false)
{
fprintf(stderr, "Problem allocating memory\n");
break;
}
if (ListIsFull(&movies))
{
puts("The list is now full.");
break;
}
puts("Enter next movie title (empty line to stop):");
}
/* 显示 */
if (ListIsEmpty(&movies))
{
puts("No data entered.");
}
else
{
puts("Here is the movie list:");
Traverse(&movies, showmovies);
}
printf("You entered %d movies.\n", ListItemCount(&movies));
/* 清理 */
EmptyTheList(&movies);
puts("Bye!");
return 0;
}
void showmovies(Item item)
{
printf("Movie: %s Rating: %d\n", item.title, item.rating);
}
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)
{
*find = '\0';
}
else
{
while (getchar() != '\n')
continue;
}
}
return ret_val;
}
示例代码: 队列ADT
//【队列ADT】P505
//【定义队列抽象数据类型】
//队列(queue)是具有两个特殊属性的链表:新项只能添加到链表的末尾、只能从列表的开头移除项
//队列是一种「先进先出」(first in, first out; FIFO)的数据形式
//下面是一个非正式的抽象定义:
//类型名: 队列
//类型属性:可以存储一系列项
//类型操作:初始化队列为空、确定队列为空、确定队列已满、确定队列中的项数、在队列末尾添加项、在队列开头删除或恢复项、清空队列
//【定义一个接口】
//接口定义放在 queue.h 文件中;使用typedef创建两个类型名:Item和Queue
//相应结构的具体实现应该是queue.h的一部分,但从概念上看,应该在实现阶段才设计结构
//我们假定已经定义了这些类型,着重考虑函数的原型
//涉及改变Queue类型的函数应该以Queue的地址作为参数
//不更改队列的函数可以接受Queue类型的参数,但传递Queue的地址更快更节省内存,这样做的好处是所有的函数都以地址作为参数
//为了表明函数不更改队列,应该使用const限定符
//添加项函数可以通过返回值来表示是否成功完成操作
//bool EnQueue(Item item, Queue* pq);
//删除项函数可以使用如下原型
//bool DeQueue(Item* pitem, Queue* pq);
//从队列中待删除的项存储在pitem指向的位置,返回值表明是否删除成功
//【实现接口数据表示】
//第一步是确定在队列中使用何种C数据形式。有可能是数组,数组的优点是方便使用,且向末尾添加项很简单
//问题是如何从队列的开头删除项,可以把各项依次向前移动一个位置,但这会浪费大量的计算机时间
//第二种方法是改变队列首端的位置,其余元素不动
//解决这种问题的一个好方法是,使队列成为环形,这意味着把数组的首尾相连
//另一种方法是使用链表,好处是删除首项不必移动其余元素,只需重置头指针即可
//我们用一个整数队列开始测试
//链表由节点组成,所以下一步是定义节点
//对队列而言,要保存首尾项,另外可以用一个计数器来记录队列中的项数
//接下来考虑队列的大小,对链表而言,其大小受限于可用的内存量,我们把队列的最大长度设置为10
//Item类型留给用户定义。使用该接口时,可根据特定的程序插入合适的定义
//把项添加到队列中,包括以下几个步骤:
//创建一个新节点、把项拷贝到节点中、设置节点的next指针为NULL、设置当前尾节点的next指针指向新节点、把rear指针指向新节点、项数+1
//函数还要处理两种特殊情况:如果队列为空,应该把front指针设置为指向新节点;如果函数不能为节点分配所需内存,则必须执行一些动作
//从队列的首端删除项,涉及以下几个步骤:
//把项拷贝到给定的变量中、释放空出的节点使用的内存空间、重置首指针指向队列中的下一个项、如果删除最后一项 把首尾指针都重置为NULL、项数-1
//【注意 保持纯正的ADT】
//定义ADT接口后,应该只使用接口函数处理数据类型
//例如DeQueue()依赖EnQueue()函数来正确设置指针和把rear节点的next指针设置为NULL
//如果在一个使用ADT的程序中,决定直接操控队列的某些部分,有可能破坏接口包中函数之间的协作关系
//【测试队列】P512
//在重要程序中使用一个新的设计之前,应该先测试该设计
//测试的一种方法是编写一个小程序,称为驱动程序(driver),其唯一的用途是进行测试
//【用队列进行模拟】P514
//现实中许多情形涉及队列,我们可以用队列包来模拟这些情形
//假设某店铺在商业街设置了一个提供建议的摊位,客户可以购买1分钟、2分钟、3分钟的建议,商业街规定排队的客户最多为10人(最大队列长度),客户随机出现,咨询时间也随机选择
//该店铺平均每小时要接待多少客户?每位客户平均要花多少时间?排队等待的客户平均有多少人?
//队列模拟能回答类似的问题
//必须用新定义的Item替换上一个示例中的int类型
//这里有一种方法,让时间以1分钟为单位递增,每递增1分钟就检查是否有新顾客到来,如果有一位顾客且队列未满则添加到队列中,否则让顾客离开
//为了统计,要记录顾客总数和被拒顾客总数
//如果队列不为空且前面的顾客没有在咨询,则删除队列首端的项,如果店铺正忙则不用让任何人离开队列,尽管如此,记录等待时间的变量应该递减1
//队列包让你把注意力集中在模拟问题上,而不是编程细节上
queue.h
#pragma once
#include <stdbool.h>
// 在这里插入 Item 类型的定义,例如
//typedef int Item; //用于 use_q.c
typedef struct item //用于 mall.c
{
long arrive; //一位顾客加入队列的时间
int processtime; //顾客咨询花费的时间
} Item;
#define MAXQUEUE 10
typedef struct node
{
Item item;
struct node* next;
} Node;
typedef struct queue
{
Node* front; //指向队列首项的指针
Node* rear; //指向队列尾项的指针
int items; //队列中的项数
} Queue;
//初始化队列;pq指向一个队列;队列将被初始化为空
void InitializeQueue(Queue* pq);
//检查队列是否已满;pq指向已初始化的队列;已满返回true,否则返回false
bool QueueIsFull(const Queue* pq);
//检查队列是否为空;pq指向已初始化的队列;为空返回true,否则返回false
bool QueueIsEmpty(const Queue* pq);
//确定队列中的项数;pq指向已初始化的队列;返回队列中的项数
int QueueItemCount(const Queue* pq);
//在队列末尾添加项;pq指向已初始化的队列,item是要被添加在队列末尾的项;若队列未满item将被添加在队列末尾,返回true,否则队列不改变,返回false
bool EnQueue(Item item, Queue* pq);
//从队列开头删除项;pq指向已初始化的队列,若队列不为空队列首项的item将被拷贝到*pitem中并被删除,返回true,若该操作使队列为空,则重置队列为空,若队列在操作前为空,返回false
bool DeQueue(Item* pitem, Queue* pq);
//清空队列;pq指向已初始化的队列;队列将被清空
void EmptyTheQueue(Queue* pq);
queue.c
#include <stdio.h>
#include <stdlib.h>
#include "queue.h"
/* 局部函数 */
static void CopyToNode(Item item, Node* pn)
{
pn->item = item;
}
static void CopyToItem(Node* pn, Item* pi)
{
*pi = pn->item;
}
void InitializeQueue(Queue* pq)
{
pq->front = pq->rear = NULL;
pq->items = 0;
}
bool QueueIsFull(const Queue* pq)
{
return pq->items == MAXQUEUE;
}
bool QueueIsEmpty(const Queue* pq)
{
return pq->items == 0;
}
int QueueItemCount(const Queue* pq)
{
return pq->items;
}
bool EnQueue(Item item, Queue* pq)
{
Node* pnew;
if (QueueIsFull(pq))
{
return false;
}
pnew = (Node*)malloc(sizeof(Node));
if (pnew == NULL)
{
fprintf(stderr, "Unable to allocate memory!\n");
exit(1);
}
CopyToNode(item, pnew);
pnew->next = NULL;
if (QueueIsEmpty(pq))
{
pq->front = pnew;
}
else
{
pq->rear->next = pnew;
}
pq->rear = pnew;
pq->items++;
return true;
}
bool DeQueue(Item* pitem, Queue* pq)
{
Node* pt;
if (QueueIsEmpty(pq))
{
return false;
}
CopyToItem(pq->front, pitem);
pt = pq->front;
pq->front = pq->front->next;
free(pt);
pq->items--;
if (pq->items == 0)
{
pq->rear = NULL;
}
return true;
}
void EmptyTheQueue(Queue* pq)
{
Item dummy;
while (!QueueIsEmpty(pq))
{
DeQueue(&dummy, pq);
}
}
use_q.c
#include <stdio.h>
#include "queue.h"
int main(void)
{
Queue line;
Item temp;
char ch;
InitializeQueue(&line);
puts("Testing the Queue interface. Type a to add a value, ");
puts("type d to delete a value, and type q to quit.");
while ((ch = getchar()) != 'q')
{
if (ch != 'a' && ch != 'd')
{
continue;
}
if (ch == 'a')
{
puts("Interger to add:");
scanf_s("%d", &temp);
if (!QueueIsFull(&line))
{
printf("Putting %d into queue\n", temp);
EnQueue(temp, &line);
}
else
{
puts("Queue is full!");
}
}
else //ch == 'd'
{
if (QueueIsEmpty(&line))
{
puts("Nothing to delete!");
}
else
{
DeQueue(&temp, &line);
printf("Removing %d from queue\n", temp);
}
}
printf("%d items in queue\n", QueueItemCount(&line));
puts("Type a to add, d to delete, q to quit:");
}
EmptyTheQueue(&line);
puts("Bye!");
return 0;
}
mall.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include "queue.h"
#define MIN_PER_HR 60.0
bool newcustomer(double x); //是否有新顾客到来?
Item customertime(long when); //设置顾客参数
int main(void)
{
Queue line;
Item temp; //新的顾客数据
int hours; //模拟的小时数
int perhour; //每小时平均多少位顾客
long cycle, cyclelimit; //循环计数器、计数器的上限
long turnaways = 0; //因队列已满被拒的顾客数量
long customers = 0; //加入队列的顾客数量
long served = 0; //在模拟期间咨询过的顾客数量
long sum_line = 0; //累计的队列总长
int wait_time = 0; //从当前到店铺空闲所需的时间
double min_per_cust; //顾客到来的平均时间
long line_wait = 0; //队列累计的等待时间
InitializeQueue(&line);
srand((unsigned int)time(0)); //rand()随机初始化
puts("Case Study: Advice Booth");
puts("Enter the number of simulation hours:");
scanf_s("%d", &hours);
cyclelimit = hours * MIN_PER_HR;
puts("Enter the average number of customers per hour:");
scanf_s("%d", &perhour);
min_per_cust = MIN_PER_HR / perhour;
for (cycle = 0; cycle < cyclelimit; cycle++)
{
if (newcustomer(min_per_cust))
{
if (QueueIsFull(&line))
{
turnaways++;
}
else
{
customers++;
temp = customertime(cycle);
EnQueue(temp, &line);
}
}
if (wait_time <= 0 && !QueueIsEmpty(&line))
{
DeQueue(&temp, &line);
wait_time = temp.processtime;
line_wait += cycle - temp.arrive;
served++;
}
if (wait_time > 0)
{
wait_time--;
}
sum_line += QueueItemCount(&line);
}
if (customers > 0)
{
printf("Customers accepted: %ld\n", customers);
printf("Customers served: %ld\n", served);
printf("Turnaways: %ld\n", turnaways);
printf("Average queue size: %.2f\n", (double)sum_line / cyclelimit);
printf("Average wait time: %.2f minutes\n", (double)line_wait / served);
}
else
{
puts("No customers!");
}
EmptyTheQueue(&line);
puts("Bye!");
return 0;
}
bool newcustomer(double x) //x是顾客到来的平均时间(分钟) 若1分钟内有顾客到来,则返回true
{
if (rand() * x / RAND_MAX < 1)
{
return true;
}
else
{
return false;
}
}
Item customertime(long when) //when是顾客到来的时间 该函数返回一个Item结构,顾客到达的时间设置为when,咨询时间设置为1-3的随机值
{
Item cust;
cust.processtime = rand() % 3 + 1;
cust.arrive = when;
return cust;
}
示例代码: 二叉查找树
//【链表和数组】P518
//对数组而言,可以直接访问任意元素,这叫随机访问(random access);对链表而言,必须从链表首节点开始逐个移动到要访问的节点,这叫顺序访问(sequential access)
//假设要查找链表中的特定项,一种算法是从列表的开头开始按顺序查找,这叫作顺序查找(sequential search)
//如果项并未按某种顺序排列,则只能顺序查找;如果待查找的项不在链表里,必须查找完所有的项才知道(这种情况下可以使用并发编程,同时查找列表中的不同部分)
//我们可以先排序列表,以改进顺序查找;这样就不必查找排在待查找项后面的项
//例如,假设在一个按字母排序的列表中查找Susan,从开头查找每一项直到Sylvia都没有找到,这时就可以退出查找,因为如果Susan在列表中,应该排在Sylvia前面
//平均下来,这种方法查找不在列表中的项的时间减半
//对于一个排序的列表,用二分查找(binary search)比顺序查找好得多
//假设列表中各项按字母排序,先比较列表的中间项和目标项,如果相等查找结束;假设目标项在列表中,如果中间项排在目标项前面,则目标项一定在后半部分项中,反之同理
//无论哪种情况,两项比较的结果都确定了下次查找的范围只有列表的一半
//接着,继续使用这种方法,把需要查找的剩下一半的中间项与目标项比较,以此类推,直到找到目标项或最终发现列表中没有目标项
//这种方法非常有效率,假设有127个项,顺序查找平均要64次比较才能找到目标或发现不在其中,二分查找最多只用进行7次比较
//一般而言,n次比较能处理有(2^n)-1个元素的数组,所以项数越多,越能体现二分查找的优势
//用数组实现二分查找很简单,因为可用数组下标确定数组中任意部分的中点,这体现了随机访问的特性
//但是,链表只支持顺序访问,不提供跳至中间节点的方法,所以在链表中不能使用二分查找
//如果因频繁地插入和删除项导致经常调整大小,而且不需要经常查找,选择链表更好
//如果只是偶尔插入或删除项,但是经常进行查找,使用数组会更好
//如果需要一种既支持频繁插入和删除项,又支持频繁查找的数据形式,这种情况下应该选择二叉查找树
//【二叉查找树】P521
//二叉查找树是一种结合了二分查找策略的链表结构
//二叉树的每个节点都包含一个项和两个指向其他节点(子节点)的指针
//二叉树中每个节点都包含两个子节点——左节点、右节点
//顺序按照如下规定确定:左节点的项在父节点的项前面,右节点的项在父节点的项后面;这种关系存在于每个有子节点的节点中(所有可追溯其祖先回到一个父节点的左节点的项,都在该父节点的前面;反之同理)
//二叉树的顶部被称为根(root),如果二叉树是满的,那么每一级的节点数都是上一级节点数的两倍
//二叉查找树中的每个节点是其后代节点的根,该节点与其后代节点构成一个子树(subtree)
//假设要在二叉树中查找一个项,如果目标项在根节点项的前面,则只需查找左子树;反之则只需查找右子树
//因此,每次比较就排除半个树,与二分查找类似,每次比较都能排除一半的可能匹配项
//二叉查找树在链式结构中结合了二分查找的效率,但这样编程的代价是构建一个二叉树比创建一个链表更复杂
//【二叉树ADT】P522
//和前面一样,先从概括定义二叉树开始。该定义假设树不包含相同的项,区别在于数据层次的安排。下面建立一个非正式的树定义:
//类型名: 二叉查找树
//类型属性:二叉树要么是空节点的集合(空树),要么是有一个根节点的节点集合
// 每个节点都有两个子树 左子树、右子树
// 每个子树本身也是一个二叉树,也有可能是空树
// 二叉查找树是一个有序的二叉树,每个节点包含一个项
// 左子树的所有项都在根节点项的前面,右子树的所有项都在根节点项的后面
//类型操作:初始化树为空、确定树是否为空 是否已满 项数、在树中添加 删除 查找 访问一个项、清空树
//【二叉查找树接口】
//只需要知道根节点的位置就可以访问整个树,但使用有成员大小的结构能很方便地记录树的大小
//我们把树的大小限制为10,较小的树便于在树已满时测试程序的行为是否正确
//【二叉树的实现】P524
//在树中添加一个项,首先要检查该树是否有空间放得下一个项,由于我们定义二叉树时规定项不能重复,所以接下来要检查树中是否有该项,通过这两步检查后,便可创建一个新节点
//把待添加项拷贝到该节点,并设置节点的左右指针为NULL,更新Tree结构的size成员,统计新增了一项,接下来找出应该把这个新节点放在树中哪个位置
//如果树为空,应设置根节点指针指向该节点,否则遍历树找到合适的位置放置该节点;根据这个思路实现AddItem(),并把一些工作交给几个尚未定义的函数
//AddNode()是二叉查找树包中第二麻烦的函数,该函数要比较新项和根项,确定应该把新项放在左子树还是右子树中,因为项是内含两个字符串的结构,所以必须自定义用于比较的函数
//假设应把新项放在左子树,如果左子树为空,AddNode()只需让左子节点指针指向新项即可,如果不为空,则应把新项和左子节点中的项做比较,这个过程一直持续到函数发现一个空子树为止
//递归是一种实现这种查找过程的方法,root是指向当前子树顶部的指针,每次递归调用它都指向一个新的下一级子树
//几个接口函数都要在树中查找特定项,这些函数的实现中使用SeekItem()函数进行查找,DeleteItem()函数有一个额外的要求,该函数要知道待删除项的父节点以便更新父节点指向子节点的指针
//因此我们设计SeekItem()返回的结构包含两个指针,一个指针指向包含当前项的节点(未找到则为NULL),一个指针指向父节点(如果该指针为根节点没有父节点则为NULL)
//SeekItem()可用递归的方式实现,但为了介绍更多编程技巧,这次使用while循环处理树中从上到下的寻找
//删除项是最复杂的任务,因为必须重新连接剩余的子树形成有效的树
//最简单的情况是:待删除节点没有子节点,这样的节点被称为叶节点(leaf),这种情况只需把父节点中的指针重置为NULL,free掉已删除节点占用的内存
//删除带有一个子节点的情况:要把被删除节点父节点中存储该节点的地址更新为该节点子树的地址
//删除有两个子树的节点:牢记树的基本设计,左子树的所有项都在父节点的前面,右子树所有项都在父节点后面,也就是右子树的所有项都在左子树所有项的后面
//故直接连接左子树与被删除项的父节点
//然后要查找左子树的右支是否有新节点的空位,如果没有就要沿着左子树的右支向下找,一直找到一个空位为止,把右子树与该空位连接
//遍历树比遍历链表更复杂,因为每个节点都有两个分支,这种分支特性很适合使用分而治之的递归来处理,对于每一个节点,执行遍历任务的函数都要做如下的工作:
//处理左子树(递归调用)、处理节点中的项、处理右子树(递归调用)
//【使用二叉树】P535
//该程序把所有字母都转换为大写字母
//【树的思想】P538
//二叉查找树也有一些缺陷。例如,二叉查找树只有在满员(或平衡)时效率最高,假设存储用户随即输入的单词,树应该是平衡的
//假设用户按字母顺序输入数据,那么每个新节点都被添加到右边,该树是不平衡的树,查找这种树并不比查找链表要快
//避免串状树的方法之一是在创建树时多加注意,如果树或子树的一边或另一边太不平衡,就需要重新排列节点使之恢复平衡
//与此类似,可能在进行删除操作后要重新排列树
//俄国数学家发明了一种算法来解决这个问题,根据他们的算法创建的树称为AVL树
//因为要重构,所以创建一个平衡的树所花费的时间更多,但这样的树可以确保最大化搜索效率
//你可能需要一个能存储相同项的二叉查找树,例如在分析文本时统计某个单词在文本中出现的次数,一种方法是把Item定义成包含一个单词和一个数字的结构
//第二次遇到同样的单词时,程序找到包含该单词的节点,并递增表示该单词数量的值
//【其他】
//栈(stack)是链表系列的另一种数据形式。在栈中,只能在链表的一端添加和删除项,项被「压入」栈和「弹出」栈
//因此,栈是一种LIFO(后进先出,last in first out)结构
tree.h
#pragma once
#include <stdbool.h>
/* 根据具体情况重新定义 Item */
#define SLEN 20
typedef struct item
{
char petname[SLEN];
char petkind[SLEN];
} Item;
#define MAXITEMS 10
typedef struct trnode
{
Item item;
struct trnode* left; //指向左分支的指针
struct trnode* right; //指向右分支的指针
} Trnode;
typedef struct tree
{
Trnode* root; //指向根节点的指针
int size; //树的项数
} Tree;
/* 函数原型 */
//把树初始化为空,ptree指向一个树
void InitializeTree(Tree* ptree);
//确定树是否为空,为空返回true,否则返回false
bool TreeIsEmpty(const Tree* ptree);
//确定树是否已满,已满返回true,否则返回false
bool TreeIsFull(const Tree* ptree);
//返回树的项数
int TreeItemCount(const Tree* ptree);
//在树中添加一个项;pi是待添加项的地址,ptree指向一个已初始化的树;添加成功返回true,否则返回false
bool AddItem(const Item* pi, Tree* ptree);
//在树中查找一个项;pt指向一个项;找到返回true,否则返回false
bool InTree(const Item* pi, const Tree* ptree);
//从树中删除一个项;pi是删除项的地址;成功删除返回true,否则返回false
bool DeleteItem(const Item* pi, Tree* ptree);
//把函数应用于树中每一项;pfun指向一个函数,该函数接受一个Item类型的参数,无返回值;pfun指向的函数为树中每一项执行一次
void Traverse(const Tree* ptree, void(*pfun)(Item item));
//清空树
void DeleteAll(Tree* ptree);
tree.c
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "tree.h"
/* 局部数据类型 */
typedef struct pair
{
Trnode* parent;
Trnode* child;
} Pair;
/* 局部函数的原型 */
static Trnode* MakeNode(const Item* pi);
static bool ToLeft(const Item* i1, const Item* i2);
static bool ToRight(const Item* i1, const Item* i2);
static void AddNode(Trnode* new_node, Trnode* root);
static void InOrder(const Trnode* root, void (*pfun)(Item item));
static Pair SeekItem(const Item* pi, const Tree* ptree);
static void DeleteNode(Trnode** ptr);
static void DeleteAllNodes(Trnode* ptr);
/* 函数定义 */
void InitializeTree(Tree* ptree)
{
ptree->root = NULL;
ptree->size = 0;
}
bool TreeIsEmpty(const Tree* ptree)
{
if (ptree->root == NULL)
return true;
else
return false;
}
bool TreeIsFull(const Tree* ptree)
{
if (ptree->size == MAXITEMS)
return true;
else
return false;
}
int TreeItemCount(const Tree* ptree)
{
return ptree->size;
}
bool AddItem(const Item* pi, Tree* ptree)
{
Trnode* new_node;
if (TreeIsFull(ptree))
{
fprintf(stderr, "Tree is full\n");
return false; //提前返回
}
if (SeekItem(pi, ptree).child != NULL)
{
fprintf(stderr, "Attempted to add duplicate item\n");
return false; //提前返回
}
new_node = MakeNode(pi); //指向新节点
if (new_node == NULL)
{
fprintf(stderr, "Couldn't create node\n");
return false; //提前返回
}
/* 成功创建了一个新节点 */
ptree->size++;
if (ptree->root == NULL) /* 情况1: 空树 */
ptree->root = new_node; //新节点为树的根节点
else /* 情况2: 树不为空 */
AddNode(new_node, ptree->root); //在树中添加新节点
return true;
}
bool InTree(const Item* pi, const Tree* ptree)
{
return (SeekItem(pi, ptree).child == NULL) ? false : true;
}
bool DeleteItem(const Item* pi, Tree* ptree)
{
Pair look;
look = SeekItem(pi, ptree);
if (look.child == NULL)
return false;
if (look.parent == NULL) //删除根节点项
DeleteNode(&ptree->root);
else if (look.parent->left == look.child)
DeleteNode(&look.parent->left);
else
DeleteNode(&look.parent->right);
ptree->size--;
return true;
}
void Traverse(const Tree* ptree, void (*pfun)(Item item))
{
if (ptree != NULL)
InOrder(ptree->root, pfun);
}
void DeleteAll(Tree* ptree)
{
if (ptree != NULL)
DeleteAllNodes(ptree->root);
ptree->root = NULL;
ptree->size = 0;
}
/* 局部函数 */
static void InOrder(const Trnode* root, void (*pfun)(Item item))
{
if (root != NULL)
{
InOrder(root->left, pfun);
(*pfun)(root->item);
InOrder(root->right, pfun);
}
}
static void DeleteAllNodes(Trnode* root)
{
Trnode* pright;
if (root != NULL)
{
pright = root->right;
DeleteAllNodes(root->left);
free(root);
DeleteAllNodes(pright);
}
}
static void AddNode(Trnode* new_node, Trnode* root)
{
if (ToLeft(&new_node->item, &root->item))
{
if (root->left == NULL) //空子树
root->left = new_node;
else
AddNode(new_node, root->left); //否则继续处理该子树
}
else if (ToRight(&new_node->item, &root->item))
{
if (root->right == NULL)
root->right = new_node;
else
AddNode(new_node, root->right);
}
else //不允许有重复项
{
fprintf(stderr, "location error in AddNode()\n");
exit(1);
}
}
static bool ToLeft(const Item* i1, const Item* i2)
{
int comp1;
if ((comp1 = strcmp(i1->petname, i2->petname)) < 0)
return true;
else if (comp1 == 0 &&
strcmp(i1->petkind, i2->petkind) < 0)
return true;
else
return false;
}
static bool ToRight(const Item* i1, const Item* i2)
{
int comp1;
if ((comp1 = strcmp(i1->petname, i2->petname)) > 0)
return true;
else if (comp1 == 0 &&
strcmp(i1->petkind, i2->petkind) > 0)
return true;
else
return false;
}
static Trnode* MakeNode(const Item* pi)
{
Trnode* new_node;
new_node = (Trnode*)malloc(sizeof(Trnode));
if (new_node != NULL)
{
new_node->item = *pi;
new_node->left = NULL;
new_node->right = NULL;
}
return new_node;
}
static Pair SeekItem(const Item* pi, const Tree* ptree)
{
Pair look;
look.parent = NULL;
look.child = ptree->root;
if (look.child == NULL)
return look; //提前返回
while (look.child != NULL)
{
if (ToLeft(pi, &(look.child->item)))
{
look.parent = look.child;
look.child = look.child->left;
}
else if (ToRight(pi, &(look.child->item)))
{
look.parent = look.child;
look.child = look.child->right;
}
else //前两种情况都不满足,则必定是相等的情况
break;
}
return look;
}
static void DeleteNode(Trnode** ptr)
/* ptr 是指向目标节点的父节点指针成员的指针 */
{
Trnode* temp;
if ((*ptr)->left == NULL)
{
temp = *ptr;
*ptr = (*ptr)->right;
free(temp);
}
else if ((*ptr)->right == NULL)
{
temp = *ptr;
*ptr = (*ptr)->left;
free(temp);
}
else //被删除的节点有两个子节点
{
/* 找到重新连接右子树的位置 */
for (temp = (*ptr)->left; temp->right != NULL; temp = temp->right)
continue;
temp->right = (*ptr)->right;
temp = *ptr;
*ptr = (*ptr)->left;
free(temp);
}
}
petclub.c
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include "tree.h"
char menu(void);
void addpet(Tree* pt);
void droppet(Tree* pt);
void showpets(const Tree* pt);
void findpet(const Tree* pt);
void printitem(Item item);
void uppercase(char* str);
char* s_gets(char* st, int n);
int main(void)
{
Tree pets;
char choice;
InitializeTree(&pets);
while ((choice = menu()) != 'q')
{
switch (choice)
{
case 'a': addpet(&pets);
break;
case 'l': showpets(&pets);
break;
case 'f': findpet(&pets);
break;
case 'n': printf("%d pets in club\n", TreeItemCount(&pets));
break;
case 'd': droppet(&pets);
break;
default: puts("Switching error");
}
putchar('\n');
}
DeleteAll(&pets);
puts("Bye.");
return 0;
}
char menu(void)
{
int ch;
puts("Pet Club Membership Program");
puts("Enter the letter corresponding to your choice:");
puts("a) add a pet l) show list of pets");
puts("n) number of pets f) find pets");
puts("d) delete a pet q) quit");
while ((ch = getchar()) != EOF)
{
while (getchar() != '\n')
continue;
ch = tolower(ch);
if (strchr("alfndq", ch) == NULL)
puts("Please enter an a, l, f, n, d, or q:");
else
break;
}
if (ch == EOF)
ch = 'q';
return ch;
}
void addpet(Tree* pt)
{
Item temp;
if (TreeIsFull(pt))
puts("No room in the club!");
else
{
puts("Please enter name of pet:");
s_gets(temp.petname, SLEN);
puts("Please enter pet kind:");
s_gets(temp.petkind, SLEN);
uppercase(temp.petname);
uppercase(temp.petkind);
AddItem(&temp, pt);
}
}
void showpets(const Tree* pt)
{
if (TreeIsEmpty(pt))
puts("No entries!");
else
Traverse(pt, printitem);
}
void printitem(Item item)
{
printf("Pet: %-19s Kind: %-19s\n", item.petname, item.petkind);
}
void findpet(const Tree* pt)
{
Item temp;
if (TreeIsEmpty(pt))
{
puts("No entries!");
return;
}
puts("Please enter name of pet you wish to find:");
s_gets(temp.petname, SLEN);
puts("Please enter pet kind:");
s_gets(temp.petkind, SLEN);
uppercase(temp.petname);
uppercase(temp.petkind);
printf("%s the %s ", temp.petname, temp.petkind);
if (InTree(&temp, pt))
printf("is a member.\n");
else
printf("is not a member.\n");
}
void droppet(Tree* pt)
{
Item temp;
if (TreeIsEmpty(pt))
{
puts("No entries!");
return;
}
puts("Please enter name of pet you wish to delete:");
s_gets(temp.petname, SLEN);
puts("Please enter pet kind:");
s_gets(temp.petkind, SLEN);
uppercase(temp.petname);
uppercase(temp.petkind);
printf("%s the %s ", temp.petname, temp.petkind);
if (DeleteItem(&temp, pt))
printf("is dropped from the club.\n");
else
printf("is not a member.\n");
}
void uppercase(char* str)
{
while (*str)
{
*str = toupper(*str);
str++;
}
}
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)
*find = '\0';
else
while (getchar() != '\n')
continue;
}
return ret_val;
}
参考资料
[1] 普拉达, 姜佑. C Primer Plus (第6版) 中文版[M]. 北京:人民邮电出版社, 2019.
[2] Microsoft. C 文档 - 入门、教程、参考。 | Microsoft Docs [OL]. [2021-10-06]. https://docs.microsoft.com/zh-cn/cpp/c-language/
*引用原书部分内容,仅供学习,版权归原作者、出版社所有。