0%

C language

常量命名

有一个不常用的命名约定,即在名称前带c或k前缀来表示常量(如,c_level或k_line)。

显示八进制和十六进制

在C程序中,既可以使用和显示不同进制的数。不同的进制要使用不同的转换说明。以十进制显示数字,使用%d;以八进制显示数字,使用%o;以十六进制显示数字,使用%x。另外,要显示各进制数的前缀00x0X必须分别使用%#o%#x%#X。,

打印short、long、long long和unsigned类型

打印unsigned int类型的值,使用%u转换说明;打印long类型的值,使用%ld转换说明。如果系统中int和long的大小相同,使用%d就行。但是,这样的程序被移植到其他系统(int和long类型的大小不同)中会无法正常工作。在x和o前面可以使用l前缀,%lx表示以十六进制格式打印long类型整数%lo表示以八进制格式打印long类型整数

  • 注意,虽然C允许使用大写或小写的常量后缀,但是在转换说明中只能用小写。

C语言有多种printf()格式。对于short类型,可以使用h前缀。%hd表示以十进制显示short类型的整数%ho表示以八进制显示short类型的整数。h和l前缀都可以和u一起使用,用于表示无符号类型。例如,%lu表示打印unsigned long类型的值。

scanf()

scanf()的输入形式,scanf("%d",&Alphabet) 要先将%d包含以后才能使用&进行传参。

打印浮点数

十六进制浮点数
printf("And it's %a in hexadecimal, powers of 2 notation\n",a boat);

printf()

ANSI C标准 printf() 转换说明转换标准.png)

printf()的修饰符的修饰符.png)
注意类型可移植性

printf()中的标记中的标记.png)

scanf()

ANSI C中scanf()的转换说明转换说明.png)

scanf()转换说明中的修饰符转换说明中的修饰符.png)
scanf()转换说明中的修饰符续转换说明中的修饰符续.png)

  • scanf() 更像是获取单词的函数,而不是获取整个字符串的函数。
  • 使用 %s 转换说明时,scanf() 会从第一个非空白字符开始,读取直到遇到下一个空白字符(空格、制表符、换行符等)为止。
  • 如果指定了字段宽度,如 %10sscanf() 将读取指定数量的字符或者在遇到第一个空白字符时停止(以先满足条件为准)。
    字段宽度和scanf().png)

scanf()的典型用法是读取并转换混合数据类型为某种标准形式。
例如,如果输入行包含一种工具名、库存量和单价,就可以使用scanf()

真值

从数值方面而不是从真/假方面来看测试条件。要牢记:关系表达式为真,求值得1;关系表达式为假,求值得0。因此,这些表达式实际上相当于数值。
例如,用while(goats)替换while (goats !=0),因为表达式goats != 0goats都只有在goats的值为0时才为0或假。
第1种形式(while (goats != 0))对初学者而言可能比较清楚,但是第2种形式(while (goats))才是C程序员最常用的。

比较常量

如果待比较的一个值是常量,可以把该常量放在左侧有助于编译器捕获错误:
5 = canoes <—— 语法错误
5 == canoes <—— 检查canoes的值是否为5
可以这样做是因为C语言不允许给常量赋值,编译器会把赋值运算符的这种用法作为语法错误标记出来。许多经验丰富的程序员在构建比较是否相等的表达式时,都习惯把常量放在左侧。

比较算数运算符优先级

比较算数运算符优先级

ctype.h 头文件中的字符测试函数

 ctype.h头文件中的字符测试函数
ctype.h头文件中的字符映射函数

ctype.h

ctype.h系列的字符函数(如,issapce()isalpha())为创建以分类字符为基础的测试表达式提供了便捷的工具。

条件运算符: ?:

expression1 ? expression2 : expression3

如果 expression1 为真(非 0),那么整个条件表达式的值与 expression2的值相同;如果expression1为假(0),那么整个条件表达式的值与expression3的值相同。

1
2
3
(5 > 3) ? 1 : 2 值为1
(3 > 5) ? 1 : 2 值为2
(a > b) ? a : b 如果a >b,则取较大的值

switch 语句

switch在圆括号中的测试表达式的值应该是一个整数值(包括char类型)。case标签必须是整数类型(包括char类型)的常量或整型常量表达式(即,表达式中只包含整型常量)。不能用变量作为case标签。
switch的构造如下:

1
2
3
4
5
6
7
8
9
switch ( 整型表达式)
{
case 常量1:
语句 <--可选
case 常量2:
语句 <--可选
default : <--可选
语句 <--可选
}

输入流

考虑下面的输入:
is 28 12.4
在我们眼中,这就像是一个由字符、整数和浮点数组成的字符串。但是对 C程序而言,这是一个字节流。第1个字节是字母i的字符编码,第2个字节是字母s的字符编码,第3个字节是空格字符的字符编码,第4个字节是数字2的字符编码,等等。所以,如果get_long()函数处理这一行输入,第1个字符是非数字,那么整行输入都会被丢弃,包括其中的数字,因为这些数字只是该输入行中的其他字符:

1
2
while ((ch = getchar()) != '\n')
putchar(ch); // 处理错误的输入

虽然输入流由字符组成,但是也可以设置scanf()函数把它们转换成数值。例如,考虑下面的输入:
42
如果在scanf()函数中使用%c转换说明,它只会读取字符4并将其储存在char类型的变量中。如果使用%s转换说明,它会读取字符4和字符2这两个字符,并将其储存在字符数组中。如果使用%d转换说明,scanf()同样会读取两个字符,但是随后会计算出它们对应的整数值:4×10+2,即42,然后将表示该整数的二进制数储存在 int 类型的变量中。如果使用%f 转换说明,scanf()也会读取两个字符,计算出它们对应的数值42.0,用内部的浮点表示法表示该值,并将结果储存在float类型的变量中。

函数

1.1 ANSI C要求在每个变量前都声明其类型

不能像普通变量声明那样使用同一类型的变量列表:

1
2
void dibs(int x, y, z) /* 无效的函数头 */
void dubs(int x, int y, int z) /* 有效的函数头 */

如果变量是同一类型,这种形式可以用逗号分隔变量名列表,如下所示:
1
2
void dibs(x, y, z)
int x, y, z; /* 有效 */

1.2 使用函数前先声明

在使用函数之前,要用ANSI C形式声明函数原型:
void show_n_char(char ch, int num);
当函数接受参数时,函数原型用逗号分隔的列表指明参数的数量和类型。根据个人喜好,你也可以省略变量名:
void show_n_char(char, int);
在原型中使用变量名并没有实际创建变量,char仅代表了一个char类型的变量,以此类推。

1.3 省略函数原型却保留函数原型的优点

1
2
3
4
5
6
7
8
9
// 下面这行代码既是函数定义,也是函数原型
int imax(int a, int b) { return a > b ? a : b; }
int main()
{
int x, z;
...
z = imax(x, 50);
...
}

递归

递归的关键在于每个递归调用都会等待它的下一级递归完成,然后再继续执行后面的代码,最终实现整个递归的效果。

可以假设有一条函数调用链——fun1()调用fun2()、fun2()调用 fun3()、fun3()调用fun4()。当 fun4()结束时,控制传回
fun3();当fun3()结束时,控制传回 fun2();当fun2()结束时,控制传回fun1()。递归的情况与此类似,只不过fun1()、fun2()、fun3()和fun4()都是相同的函数。

递归的基本原理

  1. 每级函数调用都有自己的变量。也就是说,第1级的n和第2级的n不同,所以程序创建了4个单独的变量,每个变量名都是n,但是它们的值各
    不相同。当程序最终返回 up_and_down()的第1 级调用时,最初的n仍然是它的初值1。
    递归中的变量
  2. 每次函数调用都会返回一次。当函数执行完毕后,控制权将被传回上一级递归。程序必须按顺序逐级返回递归,从某级up_and_down()返回
    上一级的up_and_down(),不能跳级回到main()中的第1级调用。
  3. 递归函数中位于递归调用之前的语句,均按被调函数的顺序执行。例如,程序清单9.6中的打印语句#1位于递归调用之前,它按照递归的
    顺序:第1级、第2级、第3级和第4级,被执行了4次。
  4. 递归函数中位于递归调用之后的语句,均按被调函数相反的顺序执行。例如,打印语句#2位于递归调用之后,其执行的顺序是第4级、第3
    级、第2级、第1级。递归调用的这种特性在解决涉及相反顺序的编程问题时很有用。稍后将介绍一个这样的例子。
  5. 虽然每级递归都有自己的变量,但是并没有拷贝函数的代码。程序按顺序执行函数中的代码,而递归调用就相当于又从头开始执行函数的代
    码。除了为每次递归调用创建变量外,递归调用非常类似于一个循环语句。实际上,递归有时可用循环来代替,循环有时也能用递归来代替。
  6. 递归函数必须包含能让递归调用停止的语句。通常,递归函数都使用if或其他等价的测试条件在函数形参等于某特定值时终止递归。为此,
    每次递归调用的形参都要使用不同的值。例如,程序中的up_and_down(n)调用up_and_down(n+1)。最终,实际参数等于4时,if的测试
    条件(n < 4)为假。

return

返回值不仅可以赋给变量,也可以被用作表达式的一部分。
返回值不一定是变量的值,也可以是任意表达式的值。
return (n < m) ? n : m;

void variables(double *, double *, double *);

数组

如果初始化数组时省略方括号中的数字,编译器会根据初始化列表中的项数来确定数组的大小。

整个数组的大小除以单个元素的大小就是数组元素的个数。

for (index = 0; index < sizeof days / sizeof days[0]; index++)
sizeof days是整个数组的大小(以字节为单位),sizeof day[0]是数组中一个元素的大小(以字节为单位)。

只有在函数原型或函数定义头中,才可以用int ar[]代替int * ar

由于函数原型可以省略参数名,所以下面4种原型都是等价的:

1
2
3
4
int sum(int *ar, int n);
int sum(int *, int);
int sum(int ar[], int n);
int sum(int [], int);

但是,在函数定义中不能省略参数名。下面两种形式的函数定义等价:
1
2
3
4
5
6
7
8
9
int sum(int *ar, int n)
{

}

int sum(int ar[], int n)
{

}

如果函数的意图不是修改数组中的数据内容,那么在函数原型和函数定义中声明形式参数时应使用关键字const

1
2
3
4
5
6
7
8
9
int sum(const int ar[], int n); /* 函数原型 */
int sum(const int ar[], int n) /* 函数定义 */
{
int i;
int total = 0;
for( i = 0; i < n; i++)
total += ar[i];
return total;
}

一般而言,如果编写的函数需要修改数组,在声明数组形参时则不使用const;如果编写的函数不用修改数组,那么在声明数组形参时最好使用const。

复合字面量

普通的数组声明:int diva[2] = {10, 20};
下面的复合字面量创建了一个和diva数组相同的匿名数组,也有两个int类型的值:
(int [2]){10, 20} // 复合字面量

  • 去掉声明中的数组名,留下的int [2]即是复合字面量的类型名
    (int []){50, 20, 90} // 内含3个元素的复合字面量
    因为复合字面量是匿名的,所以不能先创建然后再使用它,必须在创建的同时使用它。
    还可以把复合字面量作为实际参数传递给带有匹配形式参数的函数:
    1
    2
    3
    4
    int sum(const int ar[], int n);
    ...
    int total3;
    total3 = sum((int []){4,4,4,5,5,5}, 6);
    第1个实参是内含6个int类型值的数组,和数组名类似,这同时也是该数组首元素的地址。这种用法的好处是,把信息传入函数前不必先创建
    数组,这是复合字面量的典型用法。

可以把这种用法应用于二维数组或多维数组。例如,下面的代码演示了如何创建二维int数组并储存其地址:

1
2
3
int (*pt2)[4]; // 声明一个指向二维数组的指针,该数组内含2个数组元素,
// 每个元素是内含4个int类型值的数组
pt2 = (int [2][4]) { {1,2,3,-9}, {4,5,6,-8} };

如上所示,该复合字面量的类型是int [2][4],即一个2×4的int数组。

变长数组

C99引入了变长数组(Variable-Length Arrays,VLA),允许使用变量表示数组的维度。以下是一个使用变长数组的例子:

1
2
3
int quarters = 4;
int regions = 5;
double sales[regions][quarters]; // 变长数组(VLA)

变长数组的限制和特点:

  1. 存储类别限制: 变长数组必须是自动存储类别,不能使用static或extern存储类别说明符。

  2. 初始化限制: 不能在声明中初始化变长数组。

  3. 可选特性: C11标准将变长数组作为可选特性,而不是必须强制实现的特性。

  4. 不能改变大小: 变长数组的”变”指的是在创建数组时可以使用变量指定数组的维度,而不是可以修改已创建数组的大小。一旦创建,数组的大小保持不变。

计算二维数组元素之和的示例:
考虑一个函数 sum2d,计算int类型的二维数组所有元素之和。以下是该函数的声明和定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int sum2d(int rows, int cols, int ar[rows][cols]); // ar是一个变长数组(VLA)

int sum2d(int rows, int cols, int ar[rows][cols])
{
int r;
int c;
int tot = 0;

for (r = 0; r < rows; r++)
for (c = 0; c < cols; c++)
tot += ar[r][c];

return tot;
}

  • 函数原型中,ar 是一个二维变长数组,使用了 rowscols 作为两个维度。
  • 函数定义中,使用了 rowscols 来表示二维数组的大小,可以处理任意大小的二维int数组。
    注意: 形参列表中必须在声明 ar 之前先声明 rowscols

此外,函数的定义也可以使用省略形参名的方式:
int sum2d(int, int, int ar[*][*]); // ar是一个变长数组(VLA),省略了维度形参名

指针

声明指针

声明指针时需要需要带有变量,如果不想加变量则需要有*

不要混淆 *(dates+2)*dates+2 。间接运算符()的优先级高于+,所以 `dates+2` 相当于(*dates)+2:

1
2
3
*(dates + 2) // dates第3个元素的值

*dates + 2 // dates第1个元素的值加2

一元运算符*++的优先级相同,但结合律是从右往左,所以start++先求值,然后才是*start。也就是说,指针start先递增后指向。使用后缀形式(即start++而不是++start)如果使用*++start,顺序则反过来,先递增指针,再使用指针指向位置上的值。如果使(*start)++,则先使用start指向的值,再递增该值,而不是递增指针。这样,指针将一直指向同一个位置,但是该位置上的值发生了变化。虽然*start++的写法比较常用,但是*(start++)这样写更清楚。

当涉及到 const 修饰符和指针时,有几个要点需要注意:

  • const修饰指针所指向的数据,表示指针所指向的数据不能通过这个指针进行修改。例如:
    1
    2
    3
    4
    const double *pd = rates; // pd指向数组的首元素
    *pd = 29.89; // 不允许修改
    pd[2] = 222.22; // 不允许修改
    rates[0] = 99.99; // 允许修改,因为rates未被const限定
  • 指向 const 的指针 (const double *pc) 可以指向非 const 的数据,但不能通过这个指针修改所指向的数据:
    1
    2
    3
    4
    double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
    const double *pc = rates; // 有效
    pc = &rates[3]; // 有效
    *pc = 92.99; // 不允许修改
  • 指向非 const 的指针(double *pnc)可以指向 const 数据,但也不能通过这个指针修改所指向的 const 数据:
    1
    2
    3
    4
    5
    double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
    const double locked[4] = {0.08, 0.075, 0.0725, 0.07};
    double *pnc = rates; // 有效
    pnc = &rates[3]; // 有效
    pnc = locked; // 不允许
  • const 可以用于指向数组的指针,保护数组数据不被修改:
    1
    2
    3
    4
    double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
    const double *pc = rates; // 指向数组的首元素
    show_array(rates, 5); // 有效,数组名转换成指向 const 的指针
    show_array(locked, 4); // 有效,数组名转换成指向 const 的指针
  • const 还可以用于创建常量指针,该指针一旦指向一个地址,就不能再指向别处:
    1
    2
    3
    4
    double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
    double * const pc = rates; // pc指向数组的开始
    pc = &rates[2]; // 不允许,因为该指针不能指向别处
    *pc = 92.99; // 允许修改 rates[0] 的值
  • const 也可以用于创建既不能更改所指向地址,也不能修改指向地址上的值的指针:
    1
    2
    3
    4
    double rates[5] = {88.99, 100.12, 59.45, 183.11, 340.5};
    const double * const pc = rates; // 不能修改指向的地址,也不能修改地址上的值
    pc = &rates[2]; // 不允许
    *pc = 92.99; // 不允许
  • 演示程序
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /* order.c -- 指针运算中的优先级 */
    #include <stdio.h>
    int data[2] = { 100, 200 };
    int moredata[2] = { 300, 400 };
    int main() {
    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", *p1, *p2, *p3);
    return 0;
    }
  • 该程序输出
    1
    2
    3
    *p1 = 100, *p2 = 100, *p3 = 300
    *p1++=100, *++p2=200, (*p3)++=300
    *p1 = 200, *p2 = 200, *p3 = 301
    只有(*p3)++改变了数组元素的值,其他两个操作分别把p1和p2指向数组的下一个元素。

字符串声明比较

两种声明几乎相同:

1
2
const char *pt1 = "Something is pointing at me;";
const char ar1[] = "Something is pointing at me;";

数组形式(ar1[])

  • 在内存中分配一个内含29个元素的数组,每个元素对应一个字符,还有一个末尾的空字符’\0’。
  • 字符串作为可执行文件的一部分储存在数据段中,静态存储区。
  • 程序开始运行时为数组分配内存,将字符串拷贝到数组中,此时有两个副本:一个在静态内存中,一个在数组中。
  • 数组名ar1是该数组首元素地址的别名,是地址常量,不能更改。可以进行ar1+1等操作,但不允许进行++ar1这样的操作。

指针形式(*pt1)

  • 编译器为字符串在静态存储区预留29个元素的空间,并为指针变量pt1留出一个储存位置,将字符串的地址储存在指针变量中。
  • 指针形式只拷贝字符串的地址给指针,不会拷贝字符串本身。指针可以改变指向的位置,可以使用递增运算符。
  • 字符串字面量被视为const数据,因此指针pt1需要声明为指向const数据的指针,不能通过pt1改变所指向的数据。

总结:初始化数组把静态存储区的字符串拷贝到数组中,而初始化指针只把字符串的地址拷贝给指针。

空字符与空指针的区别

  • 空字符 (‘\0’):

    • 用于标记C字符串的末尾。
    • 对应字符编码是0。
    • 在字符串中不可能是其他字符的一部分。
    • 是整数类型,占1字节。
  • 空指针 (NULL):

    • 有一个值,该值不会与任何数据的有效地址对应。
    • 通常用于表示特殊情况,例如遇到文件结尾或未能按预期执行。
    • 是指针类型,占4字节(通常)。

注意:
虽然它们可以用数值0来表示,但从概念上看,空字符和空指针是不同类型的0。

字符串函数

  • 请注意,那些使用const关键字的函数原型表明,函数不会更改字符串。

char *strcpy(char * restrict s1, const char * restrict s2);
该函数把s2指向的字符串(包括空字符)拷贝至s1指向的位置,返回值是s1。

char *strncpy(char * restrict s1, const char * restrict s2, size_t n);
该函数把s2指向的字符串拷贝至s1指向的位置,拷贝的字符数不超过n,其返回值是s1。该函数不会拷贝空字符后面的字符,如果源字符串的字符少于n个,目标字符串就以拷贝的空字符结尾;如果源字符串有n个或超过n个字符,就不拷贝空字符。

char *strcat(char * restrict s1, const char * restrict s2);
该函数把s2指向的字符串拷贝至s1指向的字符串末尾。s2字符串的第1个字符将覆盖s1字符串末尾的空字符。该函数返回s1。

char *strncat(char * restrict s1, const char * restrict s2, size_t n);
该函数把s2字符串中的n个字符拷贝至s1字符串末尾。s2字符串的第1个字符将覆盖s1字符串末尾的空字符。不会拷贝s2字符串中空字符和其后的字符,并在拷贝字符的末尾添加一个空字符。该函数返回s1。

int strcmp(const char * s1, const char * s2);
如果s1字符串在机器排序序列中位于s2字符串的后面,该函数返回一个正数;如果两个字符串相等,则返回0;如果s1字符串在机器排序序列中位于s2字符串的前面,则返回一个负数。

int strncmp(const char * s1, const char * s2, size_t n);
该函数的作用和strcmp()类似,不同的是,该函数在比较n个字符后或遇到第1个空字符时停止比较。

char *strchr(const char * s, int c);
如果s字符串中包含c字符,该函数返回指向s字符串首位置的指针(末尾的空字符也是字符串的一部分,所以在查找范围内);如果在字符串s中未找到c字符,该函数则返回空指针。

char *strpbrk(const char * s1, const char * s2);
如果 s1 字符中包含 s2 字符串中的任意字符,该函数返回指向 s1 字符串首位置的指针;如果在s1字符串中未找到任何s2字符串中的字符,则返回空字符。

char *strrchr(const char * s, int c);
该函数返回s字符串中c字符的最后一次出现的位置(末尾的空字符也是字符串的一部分,所以在查找范围内)。如果未找到c字符,则返回空指针。

char *strstr(const char * s1, const char * s2);
该函数返回指向s1字符串中s2字符串出现的首位置。如果在s1中没有找到s2,则返回空指针。

size_t strlen(const char * s);
该函数返回s字符串中的字符数,不包括末尾的空字符。

s_gets()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
char * s_get(char * st, int n)
{
char * ret_val;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
while (*st != '\n' && *st != '\0')
st++;
if (*st == '\n')
*st = '\0';
else
while (getchar() != '\n')
continue;
}
return ret_val;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>        // 提供 fgets()和getchar()的原型
#include <string.h> // 提供 strchr()的原型
char * s_gets(char * st, int n)
{
char * ret_val;
char * find;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
find = strchr(st, '\n');
if (find) // 如果地址不是 NULL,
*find = '\0'; // 在此处放置一个空字符
else
while (getchar() != '\0')
continue;
}
return ret_val;
}

内存分配:malloc() 和 free()

malloc()

malloc()函数会找到合适的空闲内存块,这样的内存是匿名的。也就是说, malloc()分配内存,但是不会为其赋名。然而,它确实返回动态分配内存块的首字节地址。因此,可以把该地址赋给一个指针变量,并使用指针访问这块内存。malloc()函数可用于返回指向数组的指针、指向结构的指针等,所以通常该函数的返回值会被强制转换为匹配的类型。如果 malloc()分配内存失败,将返回空指针。

1
2
3
double * ptd;

ptd = (double *) malloc(30 * sizeof(double));

以上代码为30个double类型的值请求内存空间,并设置ptd指向该位置。
注意,指针ptd被声明为指向一个double类型,而不是指向内含30个double类型值的块。

1
2
3
double item[n];

ptd = (double *) malloc(n * sizeof(double));

free()

静态内存的数量在编译时是固定的,在程序运行期间也不会改变。自动变量使用的内存数量在程序执行期间自动增加或减少。但是动态分配的内存
数量只会增加,除非用 free()进行释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
int main()
{
double glad[2000];
int i;
...
for (i = 0; i < 1000; i++)
gobble(glad, 2000);
...
}
void gobble(double ar[], int n)
{
double * temp = (double *) malloc( n * sizeof(double));
.../* free(temp); // 假设忘记使用free() */
}

calloc()

1
2
3
long * newmem;

newmem = (long *)calloc(100, sizeof (long));

和malloc()类似,返回指向void的指针。如果要储存不同的类型,应使用强制类型转换运算符。

calloc()函数还有一个特性:它把块中的所有位都设置为0(注意,在某些硬件系统中,不是把所有位都设置为0来表示浮点值0)。

free()函数也可用于释放calloc()分配的内存。

动态内存分配和变长数组

对多维数组而言,使用变长数组更方便。当然,也可以用 malloc() 创建二维数组,但是语法比较繁琐。如果编译器不支持变长数组特性,就只能固
定二维数组的维度,如下所示:

1
2
3
4
5
6
7
8
int n = 5;
int m = 6;
int ar2[n][m]; // n×m的变长数组(VLA)
int (* p2)[6]; // C99之前的写法
int (* p3)[m]; // 要求支持变长数组
p2 = (int (*)[6]) malloc(n * 6 * sizeof(int)); // n×6 数组
p3 = (int (*)[m]) malloc(n * m * sizeof(int)); // n×m 数组(要求支持变长数组)
ar2[1][2] = p2[1][2] = 12;

先复习一下指针声明。由于malloc()函数返回一个指针,所以p2必须是一个指向合适类型的指针。第1个指针声明:
int (* p2)[6]; // C99之前的写法

表明p2指向一个内含6个int类型值的数组。因此,p2[i]代表一个由6个整数构成的元素,p2[i][j]代表一个整数。
第2个指针声明用一个变量指定p3所指向数组的大小。因此,p3代表一个指向变长数组的指针,这行代码不能在C90标准中运行。

fopen()

fopen()的模式字符串.png)

文件结尾

为了避免读到空文件,应该使用入口条件循环(不是do while循环)。鉴于getc() (和其他C输入函数)的设计,程序应该在进入循环体之前先尝试读取。

1
2
3
4
5
6
7
8
9
10
// 设计范例 #1
int ch; // 用int类型的变量储存EOF
FILE * fp;
fp = fopen("wacky.txt", "r");
ch = getc(fp); // 获取初始输入
while (ch != EOF)
{
putchar(ch); // 处理输入
ch = getc(fp); // 获取下一个输入
}

以上代码可简化为:

1
2
3
4
5
6
7
8
// 设计范例 #2
int ch;
FILE * fp;
fp = fopen("wacky.txt", "r");
while (( ch = getc(fp)) != EOF)
{
putchar(ch); //处理输入
}

fseek()

文件的起始点模式

fseek()的第1个参数是FILE指针,指向待查找的文件,fopen()应该已打开该文件。
fseek()的第2个参数是偏移量(offset)。该参数表示从起始点开始要移动的距离(参见表列出的起始点模式)。该参数必须是一个long类型的值,可以为正(前移)、负(后移)或0(保持不动)。
fseek()的第3个参数是模式,该参数确定起始点。

下面是调用fseek()函数的一些示例,fp是一个文件指针:
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个字节

文本模式调用.png

指针访问结构成员

如果him == &fellow[0],那么*him == fellow[0],因为&*是一对互逆运算符。
因此,可以做以下替代:
fellow[0].income == (*him).income
必须要使用圆括号,因为.运算符比*运算符的优先级高。
总之,如果him是指向guy类型结构barney的指针,下面的关系恒成立:
barney.income == (*him).income == him->income // 假设 him == &barney

位移运算

移位运算符针对2的幂提供快速有效的乘法和除法:

number << n number 乘以2的n次幂
number >> n 如果number为非负,则用number除以2的n次幂

这些移位运算符类似于在十进制中移动小数点来乘以或除以10。

掩码

因为ASCII码只使用最后7位,所以有时需要用掩码关闭其他位,其相应的二进制掩码是什么?分别用十进制、八进制和十六进制来表示这个掩码。

掩码的二进制是1111111;十进制是127;八进制是0177;十六进制是0x7F

条件编译

#ifdef 判断是否定义了标识符。如果定义了则执行#else#endif指令之前的所有指令并编译所有C代码,如果未定义则执行#else#endif指令之间的所有代码。

#ifndef 判断后面的标识符是否是未定义的。通常用于防止多次包含一个文件。

#if指令很像C语言中的if#if后面跟整型常量表达式,如果表达式为非零,则表达式为真。
可以按照if else的形式使用#elif
较新的编译器提供另一种方法测试名称是否已定义,即用#if defined(VAX)代替#ifdef VAX
这里,defined是一个预处理运算符,如果它的参数是用#defined定义过,则返回1;否则返回0。这种新方法的优点是,它可以和#elif一起使用。

1
2
3
4
5
6
7
8
9
#if defined (IBMPC)
#include "ibmpc.h"
#elif defined (VAX)
#include "vax.h"
#elif defined (MAC)
#include "mac.h"
#else
#include "general.h"
#endif

如果在VAX机上运行这几行代码,那么应该在文件前面用下面的代码定义VAX:
#define VAX

#define 中使用参数

在#define中使用参数可以创建外形和作用与函数类似的类函数宏。带有参数的宏看上去很像函数,因为这样的宏也使用圆括号。类函数宏定义的圆括号中可以有一个或多个参数,随后这些参数出现在替换体中。

函数宏定义的组成

  • #define SQUARE(X) X*X

#运算符

下面是一个类函数宏:
#define PSQR(X) printf("The square of X is %d.\n", ((X)*(X)));
假设这样使用宏:
PSQR(8);
输出为:
The square of X is 64.
C允许在字符串中包含宏参数。在类函数宏的替换体中,#号作为一个预处理运算符,可以把记号转换成字符串。例如,如果x是一个宏形参,那么#x就是转换为字符串”x”的形参名。这个过程称为字符串化(stringizing)。

1
2
3
4
5
6
7
8
9
10
11
12
13
/* subst.c -- 在字符串中替换 */
#include <stdio.h>
#define PSQR(x) printf("The square of " #x " is %d.\n",((x)*(x)))
int main(void)
{
int y = 5;
PSQR(y);
PSQR(2 + 4);
return 0;
}
该程序的输出如下:
The square of y is 25.
The square of 2 + 4 is 36.

调用第1个宏时,用”y“替换#x。调用第2个宏时,用”2 + 4“替换#x。ANSI C字符串的串联特性将这些字符串与printf()语句的其他字符串组合,生成最终的字符串。

预处理器粘合剂:##运算符

#运算符类似,##运算符可用于类函数宏的替换部分。而且,##还可用于对象宏的替换部分。##运算符把两个记号组合成一个记号。例如,可以这样做:
#define XNAME(n) x ## n
然后,宏XNAME(4)将展开为x4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// glue.c -- 使用##运算符
#include <stdio.h>
#define XNAME(n) x ## n
#define PRINT_XN(n) printf("x" #n " = %d\n", x ## n);
int main(void)
{
int XNAME(1) = 14; // 变成 int x1 = 14;
int XNAME(2) = 20; // 变成 int x2 = 20;
int x3 = 30;
1212
PRINT_XN(1); // 变成 printf("x1 = %d\n", x1);
PRINT_XN(2); // 变成 printf("x2 = %d\n", x2);
PRINT_XN(3); // 变成 printf("x3 = %d\n", x3);
return 0;
}
该程序的输出如下:
x1 = 14
x2 = 20
x3 = 30

  • 注意,PRINT_XN()宏用#运算符组合字符串,##运算符把记号组合为一个新的标识符。

变参宏: ...__VA_ARGS__

stdvar.h 头文件提供了工具,让用户自定义带可变参数的函数。
通过把宏参数列表中最后的参数写成省略号(即,3个点…)来实现这一功能。这样,预定义宏_ _VA_ARGS_可用在替换部分中,表明省略号代表什么。例如,下面的定义:
`#define PR(…) printf(
VA_ARGS _)`
假设稍后调用该宏:

1
2
3
4
5
PR("Howdy");
PR("weight = %d, shipping = $%.2f\n", wt, sp);
对于第1次调用,_ _VA_ARGS_ _展开为1个参数:"Howdy"。
对于第2次调用,_ _VA_ARGS_ _展开为3个参数:"weight = %d,
shipping = $%.2f\n"、wt、sp。

因此,展开后的代码是:
1
2
printf("Howdy");
printf("weight = %d, shipping = $%.2f\n", wt, sp);

宏和函数的选择

宏的一个优点是,不用担心变量类型(这是因为宏处理的是字符串,而不是实际的值)。
对于简单的函数,程序员通常使用宏,如下所示:

1
2
3
#define MAX(X,Y) ((X) > (Y) ? (X) : (Y))
#define ABS(X) ((X) < 0 ? -(X) : (X))
#define ISSIGN(X) ((X) == '+' || (X) == '-' ? 1 : 0)

(如果x是一个代数符号字符,最后一个宏的值为1,即为真。)
用圆括号把宏的参数和整个替换体括起来。这样能确保被括起来的部分在下面这样的表达式中正确地展开:
forks = 2 * MAX(guests + 3, last);
用大写字母表示宏函数的名称。

预定义宏

预定义宏

内联函数

最简单的方法是使用函数说明符 inline 和存储类别说明符static。通常,内联函数应定义在首次使用它的文件中,所以内联函数也相当于函数原型。
编译器优化内联函数必须知道该函数定义的内容。这意味着内联函数定义与函数调用必须在同一个文件中。最简单的做法是,把内联函数定义放入头文件,并在使用该内联函数的文件中包含该头文件即可。

1
2
3
4
5
6
7
8
9
// eatline.h
#ifndef EATLINE_H_
#define EATLINE_H_
inline static void eatline()
{
while (getchar() != '\n')
continue;
}
#endif

一般都不在头文件中放置可执行代码,内联函数是个特例。因为内联函数具有内部链接,所以在多个文件中定义同一个内联函数不会产生什么问题。

数学库

数学函数

可变参数:stdarg.h

必须按如下步骤进行:

  1. 提供一个使用省略号的函数原型;
  2. 在函数定义中创建一个va_list类型的变量;
  3. 用宏把该变量初始化为一个参数列表;
  4. 用宏访问参数列表;
  5. 用宏完成清理工作。

因为va_arg()不提供退回之前参数的方法,所以有必要保存va_list类型变量的副本。

1
2
3
4
5
6
7
8
9
10
11
va_list ap;                 // 声明一个对象储存参数
va_list apcopy; // 声明一个复制对象储存参数
double
double tic;
int toc;
...
va_start(ap, lim); // 把ap初始化为一个参数列表
va_copy(apcopy, ap); // 把apcopy作为ap的副本
tic = va_arg(ap, double); // 检索第1个参数
toc = va_arg(ap, int); // 检索第2个参数
va_end(ap); // 清理工作