Jade Dungeon

C语言基础

变量和赋值

声明变量

在使用变量前必须对其进行声明(为编译器做描述):

int height, length, width, volume;
float profit, loss;

在老版本C规范中,声明变量的代码段必须在语句代码段之前,在 C99 中声明变量的代 码段可以和语句代码段混在一起。

赋值变量

在给float变量赋值时,最好加上f,防止编译器警告:

float profit = 2150.48f;

初使化变量

没有经过初始化的变量其值很有可能是不可预知的。解决的方法有二个:

  • 一个是在声明以后对变量进行赋值。
  • 另一个是在声明时就时行初始化。
14int height = 8;
int width = 12, length =10;

局部变量

静态局部变量

关键字static修饰修饰局部变量为静态存储期限。静态局部变量只在当前作用域可见,在 整个程序执行其间变量的内存位置不会改变,所以其值也在程序执行期间一直保留。

外部变量

外部变量如同静态变量一样其值会在程序执行期间一直保留。作用域为文件作用域:从声 明的地方开始到文件结束。

局部变量

关键字static修饰修饰局部变量为静态存储期限。静态局部变量只在当前作用域可见,在 整个程序执行其间变量的内存位置不会改变,所以其值也在程序执行期间一直保留。

标识符

C语言标识符的第一个字符只能是「下划线」或「字母」,其后的字符可以是「下划 线」、「字母」或「数字」。

关键字不能用作标识符,下表列出了关键字,星号为C99新关键字:

auto enum restrict * unsigned
break extern return void
case float short volatile
char for signed while
continue goto sizeof _bool *
const if static _complex *
default inline struct _imaginary *
do int switch  
double long typedef  
else register union  

表达式

算术运算符

/%用于负数时,结果难以确定。C89 中除法可能是向上取整也有可能是向下取整, C99 中除法总是向0取整。C99中i % j的符号与i相同。

赋值运算符

简单赋值运算

对于整形变量i的简单赋值运算:i = 7.235也会有运算结果(运算结果为10)和副作用 (i 的值变为 7,因为类型转换为整形)。

复合赋值

i += 2错写成i =+ 2不会产生编译错误。相当于i = (+2)加号作为一元正数操作。

由于是右结合性,所以i += j += k;相当于i += ( j += k) ;

+=运算并不等同于「先加再赋值」:

  • 因为+=运算只作为一次运算,副作为只发生一次。
  • 而「先加再赋值」是两个运算,副作为会发生两次。

比如:

  • a[i++] += 2;i只增加一次;
  • a[i++] = a[i++] + 2i会自增加两次。

对于表达式k = ++i+j++相当于i = i+1; k = i + j; j = j + 1;

表达式求值

对于子表达式的执行顺序会有二义性,如:c = (b=a+2) − (a=1)。先执行哪个子表达式 会有不同的结果,所以有的编译器会产生警告。

选择语句

逻辑表达式

在同一表达式内的语句也会有强制转型,如:

i > 0 ? i : f

i是整形,f为浮点。那如果条件为真会把i的值转为float。

关系运算符

比较运算的结果可能是0或1。表达式i < j < k是合法的,含意为(i < j) < k

布尔类型

C89 中的布尔值

C89 中没有布尔类型,可以用int来模拟。用int型的零或非零代替,为了增加可读性会 使用宏定义:

#define BOOL int
#define TRUE 1
#define FALSE 0

// 声明与赋值
BOOL flag = FALSE;
flag = TRUE;

// 判断真假

C99 中的布尔值

C99中提供了_Bool类型,它是一个无符号整形。只能赋值为0或1,任何向其中赋值 非零值的操作都会让其值变为1。可以对_Bool类型进行输出与算术运算。C99提供的 新头文件<stdbool.h>中提供了bool宏来代表_Bool类型。

switch 语句

switch语句的条件分支后可以写多条语句而不用花括号包裹起来。

循环

for 语句

C99 中的 for 语句

C99 中允许for语句的第一个表达式中使用声明,并且可以声明类型相同的多个变量。如:

for (int i=0, j=0; i<5; i++) {
	...
}

逗号运算符

逗号运算符分别计算两部分,并只返回第二部分的值。

退出循环

goto 语句

标号只是放置在语句开始外的标识符:

标识符 : 语句

goto 语句的格式:

goto 标识符

标号所在的位置必须要和goto语句是在同一个函数中。

for (d=2; d<n; d++) {
	if (n%d == 0) {
		goto done;
	}
}

done:
	printf("hello\n");

基本类型

整数类型

整形可以指定是否有符号以及长度:

  • sort int
  • unsigned sort int
  • int
  • unsigned int
  • long int
  • unsigned long int

这些声明中的int是可以省略的。

C99 中的整数类型

增加范围更大的:

  • long long int
  • unsigned long long int

整数常量

表示不同的进制
  • 十进制不能用0开头
  • 八进制一定要以0开头
  • 十六进制以0x开头

其中字母大小写不敏感。

长整数
  • 强作作为长整数处理,需要要后面加上字母L
  • 指明为无符号常量在后面加上字母Uu

二者可以共同使用,如表示0xfffffffffUL

C99 中的整数常量

C99中的:

  • long long int要用大小写一致的LLll表示。
  • unsigned long long int用后加Uu表示。

整数溢出

  • 有符号整数发生溢出结果是不可知的。
  • 无符号整数溢出后的结果是对\(2^n\)取模。如:无符16位65,5351结果是为0

浮点类型

三种类型:

  • float
  • double
  • long double

C99 中新增加了复数类型:

  • float _Complex
  • double _Complex
  • long double _Complex

C 标准没有说明精度应该是多少,所以各种实现可能有不同。定义浮点相关的宏可以在 头文件<float.h>中找到。

浮点常量

浮点数可用多种方式表示,如57.0可表示为:

57.0       57.      // 0 可省略,小数点不可省略
57.0e0     57E0     // 指数 e 大小写无所谓
 5.7e1      5.7e+1  // 缩放 10 的幂次
  .57e2   570.e-1   // 缩放 10 的幂次

字符类型

字符类型char类型。其性质相当于整形,操作也与整数操作类似。

字符的符号

C语言标准不说明char是否有符号。如果要把char用来存储整数,考虑到可移植性还是 用修饰符指定是signed char还是unsigned char

类型操作

类型转换

常用算术转换

整数转换时会把有符号转为无符,这时候如果有负值会发生把符号位作为最高位的错误。 所以能不用无符就不要用。

强制类型转换

强制转换可以用来防止溢出。

long i;
int j = 1000;

i =(long) j * j;       // 强制转换可以用来防止溢出
i =(long) (j * j);     // 这样不行,因为溢出在转型前已经发生了

类型定义

通过typedef定义新的类型。

sizeof 运算符

计算指定类型数据占用的空间大小。编译器自身就已经可以确定表达式的大小了,但是在 C99 中的「可变长数组」是没有办法在编译时就确定大小。

因为是运算符,所以后面的括号可以省略。但是因为 sizeof 优先级比较高,加上括号安全 一些,不然

sizeof i+j

是先算的是i的大小再加上j

sizeof返回的类型是size_t,C89中最好转换成一个已知的类型(一般是强转成 un-signed long)。C99 中可以直接处理size_t,printf输出时用"%zu"

数组

一维数组

一维数组初始化

初始化的内容或比数组短,剩余元素赋值为 0。

int a[10] = { 0 };        // 简单地全置为 0

C99 中指定元素初始化

C99 中可以指定元素初始化,而且与顺序无关:

int a[10] = {[2] = 30, [9] = 7, [1] = 8};

还可以把顺序与指定下标混合使用:

int a[10] = {4, 9, 1, 8, [0] = 8};

在指定的标号初始化后,后一个没有标号的值会赋给标号的下一个标号(相当于从 0 开 始,遇到标号重新定位,再顺序开始)。

多维数组

访问i行j列的元素正确格式是arr[i][j],不要错写成了arr[i,j]。不然逗号运算返 回的是 j 的值,相当于arr[j]

多维数组初始化

C99 中可以指定元素初始化在多维数组中也可以使用:

double arr[2][2] = {[0][0] = 1.0, [1][1] = 1.0};

常量数组

通过 const 声明数组为常量,数组的内容不可修改:

const char hex_chars [] = {
	'0', '1', '2', '3', '4',
	'5', '6', '7', '8', '9',
	'A', 'B', 'C', 'D', 'E',
	'F' };

C99 中可变长数组

可变长数组是数组的长度是程序运行时确定的,而不是在编译时指定的。通过表达式定义 数据的长度。

C99 不允许 goto 语句绕过可变长声明,因为可变长数组的长度是在程序运行时才确定的, 如果绕过了会造成数组没有初始化的错误。

例:

/*********************************************************
 * From C PROGRAMMING: A MODERN APPROACH, Second Edition *
 * By K. N. King                                         *
 * Copyright (c) 2008, 1996 W. W. Norton & Company, Inc. *
 * All rights reserved.                                  *
 * This program may be freely distributed for class use, *
 * provided that this copyright notice is retained.      *
 *********************************************************/

/* reverse2.c (Chapter 8, page 174) */
/* Reverses a series of numbers using a variable-length
   array - C99 only */

#include <stdio.h>

int main(void)
{
  int i, n;

  printf("How many numbers do you want to reverse? ");
  scanf("%d", &n);

  int a[n];   /* C99 only - length of array depends on n */

  printf("Enter %d numbers: ", n);
  for (i = 0; i < n; i++)
    scanf("%d", &a[i]);

  printf("In reverse order:");
  for (i = n - 1; i >= 0; i--)
    printf(" %d", a[i]);
  printf("\n");

  return 0;
}

函数

函数的声明与定义

函数定义

没有返回类型的函数声明在C89中默认为是int,但是在 C99 中是不合法的,一定要声明 为void。

C89 中变量的声明必须要在语句之前,C99 中可以混写在一起。

函数调用

如果对所调用函数的返回值不感兴趣,可以直接丢弃。另一种更加清楚明白表明丢掉返回 值的方式是地函数调用前加上(void),这样效果和直接丢弃没有区别,就是明确告诉 其他阅读源代码的人,函数的返回值是不要的,如:

(void) printf("Hi, Mcm!\n"); // 表示不需要函数的返回值

函数声明

声明函数的函数原型不需要说明参数的名字,只要说明类型就可以了(这种情况非常常 见)。省略原型中的参数名字通常是出于防御目的:如果有一个宏和名字和参数一样, 预处理时参数的名字就会被替换。

如果函数调用一个没有声明过也没有定义过的函数,编译器会为该函数建立一个隐匿声明 (implicit declaration)假设函数类型为int;但编译器无法检查实参个数与类型, 只能进行默认的实际函数提升并期待最好的情况发生。等遇到了真正的函数定义时发现 原来的假设错误就会报错。

提升的规则为:

  • 把 float 实参转为 double
  • C99 中实现了把 char 和 short 转换为 int

C99 规定在调用函数前必须先进行声明或定义。

函数声明可以放在另一个函数的函数体内:

int main (void) {
	int sumSub( int, int );
	...
}

这表示 sumSub 函数只能在 main 函数中被调用,其他函数要调用就要再声明一次。

和声明变量一样,如果两个函数类型相同,可以声明在一起:

int sum(int, int), count(int, int);

// 甚至可以和类型相同的变量声明在一起
int a, b, sum(int, int), count(int, int);

函数的参数

数组形式参数

无法通过数组参数取得数组的长度,必须用另一个参数把数组长度传递进来。虽然可以通 过 sizeof 计算出数组变量的长度,但是没有办法取得数组数组参数的长度。

int func(int a []) {
	// 无法取得数组参数的长度,以下这句错误
	int len = sizeof(a) / sizeof(a[0]);
}

如果形参数是多维数组,那么除了第一维,其他的维度都要声明长度,不然无法确定指针 移动的距离:

int func(int a[][10], n) { ... }

C99 可变长数组型形式参数

在 C99 的可变长数组参数使用中,可以声明整数参数与数据长度的关系。下例中明确说 明数组a的长度就是n,但是要注意,n一定要在a之前,不然n处于还没有 声明过的状态:

int sumArray( int n, int a[n] ) { ... }

int sumArray( int m, int n, int a[m], int b[n], int c[m+n] ) { ... }

C99 声明数组长度为 static

在 C99 中给数组长度加 static 声明保证长度至少为声明的长度。

数组 a 长度至少为 3:

int sumArray(int n, int a[static 3]) { ... }

这种方式不会对程序产生任何影响,仅仅通知编译器可能对程序进行优化。还有一点 static 只能用于多维数组的第一维。

C99 复合字面量

在 C99 中对于临时使用的数组可以不起变量名,相当于一个临时变量。

数组 b 只用一次,别的地方用不到:

int b [] = {1, 2, 3, 4, 5};
total = sum( b, 5 );

在 C99 中可以使用复合字面量,不用定义一个数组变量 b:

total = sum( (int []){1, 2, 3, 4, 5}, t);

一般不用指定长度,当然要指定也可以:

total = sum( (int [5]){1, 2, 3, 4, 5}, t);

不仅是常量,表达式也可以作为元素:

total = sum( (int [5]){2*i, m+n, 3, 4, 5}, t);

复合字面量是一个左值,其值是可以改变的,如果要不可变的可以加个 const,如:

(const int []){5,4}。

程序结束

C89 规定 main 不指定类型默认类型为 int,C99 中必须声明 main 函数类型。

main 函数返回 0 给操作系统表示程序正常结束,非 0 值表示各种异常终止状态。

返回的方法两种:

  • return 语句
  • exit 函数(定义在<stdlib.h>中)

使用 return与 exit 的区别是:

  • return 只表示退出当前函数,退出 main 表示程序结束
  • 而 exit 在任何地方被调用都会让程序结束。

<stdlib.h>中还定义了宏EXIT_SUCCESSEXIT_FAILURE表示程序退出的正常与异 常状态,通常分别代表 0 和 1。

在 main 函数为 int 型情况下,没有 return 语句的话:

  • 在 C89 下会有「control reachs end of non-void function」警告
  • 在 C99 下会返回默认值 0;

指针

指针作为参数

关键字 const 保护参数

保护传递过来的指针参数指向的内容不会被函数改变,但指针还是可以指向别的变量:

void fun(const int * p) {
	int j;
	*p = 0;    /* wrong */
	*p = &j;   /* legal */
}

指针的指向不可改变:

void fun(int * const p) { ... }

指向与指向的值都不可改变:

void fun(const int * const p) { ... }

指针作为返回值

/* function type is pointer */
int * max(int * a, int * b) {
	if(*a > *b) {
		return a;
	} else {
		return b;
	}
}

/* call the function */
int main(void) {
	int * p, i, j;
	p = max(&i, &j);
	return 0;
}

永远不要返回局部变量的指针,因为函数一结束局部变量就不存在了。

指针与数组

C语言中数组和指针的紧密的联系,但二者不能完全互换。在形式参数中*aa[] 都说明期望的参数是指针,也二者可以在函数内给 a 赋予新的值,可以互换。

但数组变量不等于指针变量。数组的名字不是指针,是由编译器在需要时把数组的名字 转换为指针。

例如:

  • sizeof(a)的结果是数组中字切的总数(每个元素的大小乘发数量);
  • sizeof(p)的结果是存储指针所需要的字节数量。

对于编译器来说,i[a]a[i]是一样的。因为一个是*(i+a)另一个是*(a+i),加法 可以交换,所以二者相等。

指针的算术运算

指针操作只有:「指针加减整数」和「两个指针相减」两种。对指针变量进行加减 n 的操 作并不是移动 n 个地址,而是移动(n * 类型长度)个地址,两样两指针相减的结果 也是地址相离除以类型长度。

指针的比较运算

指针比较操作有:<<===!=>>=

C99 指向复合常量的指针

原来的语句:

int a[] = {1, 2, 3};
int * p = &a[0];

可以通过 C99 的复合常量简化为:

int * p = (int []){1, 2, 3};

指针用于数组处理

常用的*++操作的结合:

  • 移动指针
    • *p++等于*(p++)
    • *++p等于*(++p)
  • 递增指向的值
    • (*p)++
    • ++*p等于++(*p)

用数组名作为指针

数组名可作为指向第一个元素的指针,但指向的目标是不可变的。

通常情况下a+i等同于&a[i];并且*(a+i)等同于a[i]

int a[10];
*a = 0;
*(a+1) = 1;

同样地也可以把指针作为数组名。

数组名作为实参

数组名在传递给函数时,总是视为指针,所以不会复制整个数组的内容,只复制数组的起 始地址给函数。为了声明数组的内容不可改变,声明形参为 const:

int findLargest( const int a[], int n ) { ... }

如果需要,完全可以声明形参为指针,对于编译器来说,二者是相同的:

int findLargest( int * a, int n ) { ... }

但是对于变量,指针和数组是不同中的。因为编译器要根据数组的长度先分配好空间。

指针与多维数组

处理多维数组的元素

把多维数组看作一维数组处理:

int *p; // 用来指向元素的指针
for(p = &a[0][0]; p<&a[ROW_NUM-1][COL_NUM-1]; p++)
{ *p=0; /* 初始化为 0 */ }

把二维数组视为一维数组处理对大多数编译器是合法的,但有些编译会检查数组长度。

如果指针p指向a[0][0],从技术上说p指向的是a[0]的第一个元素,在p 移动的过程中p会超过a[0]的最后一个元素。有些编译器会在这种情况下报错。

处理多维数组的行

取第i行是的下标n的元素的方法为p = &a[i][n]。因为a[i]等价于*(a+i), 所以&a[i][n]等于&(*(a[i]+n))。由于一次取值操作(*)与取地址操作(&) 可以相抵消,最后相简化为p=a[i]+n

int a[ROW_NUM][COL_NUM], *p, i;
for(p=a[i]; p<a[i]+COL_NUM; p++)
{ *p=0; /* 初始化为 0 */ }

处理多维数组的列

主要思想是定义长度为数组列数的数组指针,这样每次移动距离正好是数组的列数:

int a[ROW_NUM][COL_NUM], (*p)[COL_NUM], i;
for(p=&a[0]; p<&a[ROW_NUM]; p++)
{ (*p)[i]=0; /* 初始化为 0 */ }

要注意(*p),一定要加括号,不然意思是p为指针的数组。

用多维数组名作为指针

对于a[ROW_NUM][COL_NUM]这样的多维数组,aa[0]虽然都是指向元素a[0][0]。 但二者的数据类型是不一样的:

编译器把a转换为指向a[0]的指针。因为Ca看作一个每个元素都一维数组的 一维数组,所以用作指针时a的类型是int (*)[COL_NUM]

所以对于接收一维数组的函数:

int findMax( int * a, int size );

在调用时要明白a的类型是int (*)[COL_NUM],不能作为参数传入;而a[0]的类型才 是int * a,可以传入:

maxValue = findMax( a, ROW_NUM * COL_NUM );         /* 错误 */
maxValue = findMax( a[0], ROW_NUM * COL_NUM );      /* 正确 */

a[0]是指向a[0][0]的指针。

了解a指向的是a[0]有助于简化处理,如要把a的第i列清零,可以用:

for(p=&a[0]; p<&a[0]+ROW_NUM; p++) { (*p)[i]=0; }

取代:

for(p=a; p<a+ROW_NUM; p++) { (*p)[i]=0; }

C99 中的指针和可变长数组

C99 中指针指向可变长数组:

void f(int n) {
	int a[n], *p;
	p = a;
}

如果可变长数组是多维的,那指针的类型就取决于除第一维外的其他所有维度的长度:

void f(int m, int n) {
	int a[m][n], (*p)[n];
	p = a;
}

需要注意编译器无法检查可变数组长度,所以下面的代码虽然可以通过编译,但只有在 m=n时才是正确的:

void f(int m, int n) {
	int a[m][n], (*p)[m];
	p = a;
}

指向其中一行的指针可以声明为:

int (*p)[n];

循环清零第i列代码:

for(p=a; p<a+m; p++) { 
	(*p)[i]=0; 
}

字符串

字符串字面量

拼接字符串字面量

用空白字符分隔的字符串字面量会自动拼接成一个:

printf("hello " "world" "!");

存储字符串字面量

'\0'结尾的连续字符,可用char *类型指针指向第一个字符。

C 语言允许对字符串取下标操作:

char ch;
ch = "abc"[1];

通过这个特性,实现一个函数把 0 到 15 数字转为十六进制字符:

char numToHex(int d) { return "0123456789ABCDEF"[d]; }

警告:不能试图改变字符串字面量,不然不什么会发生什么情况。因为有的编译器会给 相同的字符串字面量只有一个副本来节约内存(就像 Java 一样),或是放在内存块中的 只读区域。

char * p = "abc";
*p = 'd'; /* WRONG */

字符串字变量

初始化字符串变量

在声明时初始化:

char data[8] = "abc";

虽然看起来很像是字面量,但实际上不是。编译器完成转换:

char data[8] = {'a', 'b', 'c', '\0'};

数组只能声明时初始化,不能赋值操作:

char str1[10], str2[10];
str1 = 'abc'; /* WRONG */
str2 = str1; /* WRONG */

字符数组与字符指针

char data = "abc";
char *data = "abc";
  • 虽然看起来一样但是不相等,数组中的内容可以修改但不能指向其他的字符串
  • 而指针的内容不可以修改,但可以指向其他的内容
char data[8] = {'a', 'b', 'c', '\0'};

字符串数组

如果用二维数组的话,指定每行共同的长度很浪费空间:

char plants[][8] = {
	"Mercury", "Venus", "Earth",
	"Mars", "Jupiter", "Saturn",
	"Uranus", "Neptune", "Pluto" };

使用字符指针的数组不会有空间浪费:

char * plants[] = {
	"Mercury", "Venus", "Earth",
	"Mars", "Jupiter", "Saturn",
	"Uranus", "Neptune", "Pluto" };

预处理器

预处理指令

  • 预处理指定以#开头,之前可以是空白字符
  • 指令的符号之间可以有任意的空格或制表符
  • 换行表示一条指令结束,除非以\\声明。
  • 注释可以和指令放在同一行。
  • #号可以单占一行,作为一个空指令。

宏定义

简单宏定义

定义一个为空的宏是合法的,如:

#define DEBUG

宏定义中常见的错误:

#define N = 100 // wrong
int a[N]; // becomes a[= 100]

#define N 100; // wrong
int a[N]; // becomes a[100;]

如果宏被交叉定义,早期的预处理器会陷入无限循环替换。当今的规范规定如果宏名重复 出现不会再次被替换:

#define N (2*M)
#define M (N+1)

i=N;
// 应该转换为
i = (2*(N+1));

带参数的宏

在定义带参数的宏时,宏名与左括号之间不能有空格。否则会作为简单宏处理。

#define MAX(x,y) ( (x)>(y) ? (x) : (y) )
#define IS_EVEN(n) ( (n)%2 == 0 )

使用宏的优点有:没有函数调用的开销程序速度更快一点(C99 提供的内联函数也可以避 免相同的开销)、没有参数类型限制。

但作为一种文本替换的实现方式,缺点也有如:处理后文件变大,没有类型安全检查、会 多次计算参数表达式的值(在有++这类有副作用的操作时尤其要注意)等。

#运算符

运算符#把宏转换为字符串字面量。

##运算符

运算符##可以把两个标记(如标识符)合并成为一个记号。如果合并的内容包含宏参数, 合并动作会在参数替换之后进行:

#define MK_ID(n) i##n
int MK_ID(1), MK_ID(2), MK_ID(3);

在经过预处理后会被替换为:

int i1, i2, i3;

使用##的宏不能嵌套调用。如:

MK_ID(MK_ID(1));

实用例子,定义一个取最大值的函数,但不想为 int 和 float 等类型各写一遍实现:

#define GENERIC_MAX(type) //
type type##_max(type x, type y) //
{ return (x>y ? x : y); }

要让预处理器自动建立一个 float 版的,只要:

GENERIC_MAX( float )

宏的通用性质

一个宏的定义中可以调用其他的宏:

#define PI 3.1415926
#define TWO_PI ( 2 * PI )

宏定义的作用范围是整个文件。

不可以重复定义宏,除非两次定义的值一样,如果有参数的话,参数也要一样。

可以用#undef取消一个宏的定义。而#undef最常用的地方就是取消一个宏的定义, 再赋予一个新的定义。

调用多个语句的宏

宏替换后的结果常常会引起意料之外的结果:

#define ECHO(s) { gets(s); puts(s); }

if( flag )
ECHO(str) ;             // 注意到结尾的根号了没有?
else
gets(str)

会被替换为:

#define ECHO(s)

if( flag )
{ gets(s); puts(s); }; // 上面的分号结束了 if 语句, 后面的 else 没有了归属
else
gets(str)

一种解决办法是调用时判断一下是不是要加分号;

另一种方法是运用逗号表达式,可以让宏执行多个操作:

#define ECHO(s) (gets(s), puts(s))

一个宏的定义中可以调用其他的宏:

#define PI 3.1415926
#define TWO_PI ( 2 * PI )

C89 中预定义的宏

有一些字符串或是整数常量被作为 C 语言预定义的宏:

  • __LINE__源文件中的行号;
  • __FILE__源文件名;
  • __DATE__编译的时间;
  • __STDC__编译器符合 C89 或 C99 标准时为 1;

C99 中新增预定义的宏

C99 标准中新增加了以下宏:

  • __STD_HOST__
    • 托管实现时为 1,能接受任何符合 C99 标准的程序;
    • 独立实现为 0,不一定实现复数形式,不用实现<stdio.h>
  • __STD_VERSION__支持 C 语言的版本:
    • C89 为199409L;
    • C99 为199901L
  • __STD_IEC_559__支持 IEC60599 浮点算术运算则为 1;
  • __STD_IEC_559_COMPLEX__支持 IEC60599 复术算术运算则为 1;
  • __STD_10646__如果wchar_t值与 ISO 10646 标准相匹配,则值为yyyymmL;

C99 中空的宏参数

C99 标准中可以任意或所有参数为空:

#define ADD(x,y) (x + y)
i=ADD(j,k);

预处理后成为:

i=(j + k);

对于:

i=ADD(,k);

预处理后成为:

i=(+k);

#运算会把空参数字符串化,空相当于""

#define MK_STR(x) #x

char emptyString [] = MK_STR();

预处理后成为:

char emptyString [] = "";

##运算会把空参数字跳过:

#define JOIN(x,y,z) x##y##z

int JOIN(a,b,c), JOIN(a,b,), JOIN(a,,c), JOIN(,,c);

预处理后成为:

int abc, ab, ac, c;

C99 中参数个数可变的宏

可变参数用参数列表最后加上...。个省略号至少要对应一个参数。 有一个专门的标识符__VA_ARGS__代表与省略号对应的参数。

#define TEST( condition, ... ) (    \
(condition) ?                       \
printf("test: %s\n", #condition) :  \
printf(__VA__ARGS__)                \
)

TEST( a <= b, " %d <==> %d", c, d);

会被预处理为:

(
( a <= b )
printf("test: %s\n", "a <=b ") :
printf(" %d <==> %d", c, d)
);

C99 中 func 标识符

__func__标识符代表当前函数的名称。

#define FUNC_START() printf("%s start...", __func__);
#define FUNC_END() printf("%s end...", __func__);

void f (void) {
	FUNC_START();
	FUNC_END();
}

另一个常见的用处是把它用为一个参数,让被调用的函数知道是哪个函数在调用它。

条件编译

#if#endif指令

#if会把没有定义过的宏标识符当作0处理。

#if DEBUG
#if !DEBUG

defined 运算符

defined判断标识符是否被定义过,定义过返回1,没有定义过返回0。

#if defined(DEBUG)
...
#endif

可以同时测试多个宏是否定义:

#if defined(DEBUG) && defined(FOO) && !defined(WIN32)
...
#endif

#ifdef#ifndef指令

#ifdef#ifndef都以#endif结束:

#ifdef DEBUG

相当于:

#if defined(DEBUG)

而:

#ifndef DEBUG

相当于:

#if !defined(DEBUG)

#elif#else指令

条件指令可以嵌套,合理利用缩进与注释让条理更加清晰:

#ifdef DEBUG
#endif /* DEBUG */

条件编译的使用场合

不同操作系统中移植:

#ifdef WIN32
...
#elif MAC_OS
...
#elif LINUX
...
#endif

不同编译器中移植:

#if __STDC__
//函数原型
#else
//老式函数原型
#endif

检查宏是否已经被定义:

#ifndef BUFF_SIZE
#define BUFF_SIZE 256
#endif

因为/* */不能嵌套,不如用宏来屏蔽代码:

#if 0
...
#endif

#error指令

表示程序遇到不应该发生的严重错误,并输出指定消息:

#ifdef WIN32
...
#elif MAC_OS
...
#elif LINUX
...
#else
#error Unsupport OS
#endif

#line指令

在开发过程中有时会用到合并多个代码文件为一个代码文件的工具:UNIX 下常用 yacc, 对应在 linux 下 bison。

这时需要改变程序行号的编号方式,可以也行号加上一个值 (C89 中小于 32,767,C99中小于2,147,483,647)。

#line n

也还可以指定一个文件名:

#line 10 "bar.c"

如果在foo.c的第 5 行出错了,那显示出错误在bar.c的第 13 行: 因为指令点了foo.c的一行,所以从第二行开始作为bar.c的第 10 行。

#pragma指令

定义与编译器相关的特殊处理:

#pragma data(heap_size => 1000, stack_size => 2000)

C99 中的 Pragma 运算符

#pragma相同是定义与编译器相关的特殊处理。它可以实现字符串的字面意义:

#pragma data(heap_size => 1000, stack_size => 2000)

等同于:

#_Pragma ("data(heap_size => 1000, stack_size => 2000)")

由于_Pragma是运算符,所以不受预处理器限制。如下面这个 GCC 手册中的例子:

#define DO_PRAGMA(x) _Pragma(#x)

// 宏调用如下
DO_PRAGMA( GCC dependency "parse.y" )

// 扩展后的结果为
#pragma GCC dependency "parse.y"

构造大型程序

头文件

#include指令

#include <文件名>用来引入系统文件目录中 C 语言自身的函数库。 #include "文件名"优先在当前目录(可以通过 GCC 参数-I修改)再到系统目录查找。

#include "C:\aa\bb\cc.h" /* 绰号和斜杠会自动转义 */

还可以用宏标记:

#if defined(IA32)
#define CPU_FILE "ia32.h"
#elif defined(IA64)
#define CPU_FILE "ia64.h"
#endif
#include CPU_FILE

声明变量

extern关键字表示这个变量是一个外部变量。在这里只作声明而不定义,因为实际变量 的定义在其他的源文件里。

一般把声明放在头文件中,在使用的地方声明变量的代码如下:

extern int x;      // 只声明,不分配内存
extern int arr []; // 只声明,不分配内存

原始文件中定义变量的代码如下:

int x;

如果对于已经声明了外部变量x,再有如下的同名的定义会引起错误:

int long x;

复合类型:结构、联合和枚举

结构体变量

结构变量声明

struct {
	char name [20];
	int number;
} a, b;

结构变量初始化

初始化必须按照声明顺序给成员赋值:

struct {
	char name [20];
	int number;
}
a={"aa", 100},
b={"bb", 200};

C99 结构变量指定成员初始化

初始化必须按照声明顺序给成员赋值:

a={ .name="aa", .number=100 },

没有指定的默认作为前一个指定之后的成员:

b={ .name="bb", 200};

结构变量的操作

结构操作支持赋值复制,直接复制每个成员的值到目标变量(包括成员中的数组)。因为 数组不支持赋值的复制操作,可以利用这一特点来复制数组:

a = b;

不能用来比较两个结构变量:

if(a == b) { ... } /* error */

取成员操作的点号优先级与后自增同级,如下代码是对number属性的增加:

a.number++

优先级高于取地址,所以以下代码得到的是成员的地址:

scanf("%d", &a.number);

结构体类型

结构标记

声明结构标记:

struct part{
	char name [20];
	int number;
};

定义好了结构标记以后,就可以用来声明变量。声明变量时不能漏掉struct关键字 声明这个一个结构标记:

struct part a, b;

还有定义结构标记时注意不能少掉分号,不然会的二义性。如:

struct part{
char name [20];
int number;
}
f ( void ) { ... };

上面代码中定义的结构被解释为函数的返回类型。

结构标记声明和变量的声明可以放在一起,甚至可以二者可以用相同的名称:

typedef struct part {
	char name [20];
	int number;
} part;

结构类型

typedef定义真实的类型名,这名字必须出现在类型的结尾:

typedef struct {
	char name[20];
	int number;
} Part;

用类型来声明变量不用加struct关键字:

Part a, b;

可以同时指定结构标记与结构的类型,如:

typedef struct part {
char name[20];
int number;
} Part;

一般情况下可以结构标记也可以用类型,但在结构用于链表时,强制使用结构标记。

结构作为参数或返回值

struct part checkPart(struct part p)	{
	return p;
};

结构类型作为参数与返回值所传递的都是是副本,所以复制副本会有开销。 一般用指针来传递会比较合适。

C99 结构复合字面量

临时建立一个无名类型变量传递给函数:

checkPart( (struct part) {"aaa", 100} );
checkPart( (struct part) {.name="aaa", .number=100} );

临时建立一个无名变量传递给变量:

a = ( (struct part) {"aaa", 100} );
b = ( (struct part) {.name="aaa", .number=100} );

即使是复合字面量也可以用指针来表示:

checkPart( &(struct part){"aaa", 100} );

嵌套结构与数组

结构的成员是结构

数组的成员是结构

struct part inventory[100];
checkPart(inventory[i]);
inventory[i].number = 200;

结构数组的初始化:

struct part {
	char * name;
	int number;
};
struct part inventory[] = {
	{"aaa", 100}, {"bbb", 200},
	{"ccc", 300}, {"ddd", 400}
};

联合

初始化联合必须要用花括号包围,联合初始化时只有第一个属性能被初始化,而且 C89 中只能用常量来初始化。

union part {
	int i;
	double d;
} u = {0};

C99 中可以指定初始化:

union part {
	int i;
	double d;
} .d = {1.0};

利用联合节约空间

在下面的例子中为了节约空间,我们使用联合针对不同类型的记录会存放不同的字段:

struct item {
	int item_type;
	int stock_numer;
	float price;
	union {
		struct { 
			char title[100];
			char author[100];
			int page_number;
		} book;
		struct {
			char desine[100];
		} cup;
		struct {
			char desine[100];
			int color;
			int size;
		} shirt;
	} item;
};

struct item c;

printf("%s", c.item.book.title);
strcpy(c.item.mug.design, "Cats");

一般情况下联合型的一个成员赋值以后,其他的类型就不可用了。在这一点上 C 语言给 结构联合的同类型同顺序同名成员可以通用,如下:

strcpy(c.item.mug.design, "Cats");
printf("%s", c.item.shirt.title);

利用联合创建混合类型

建立即能存储整数也能存储浮点的Number类型:

typedef union { int i; doubled; } Number;
Number numArr[100];

这样数组中能存放整数也能存放小数:

numArr[0].i = 100;
numArr[1].d = 0.5;

标记联合状态

建立即能存储整数也能存储浮点的Number类型:

#define INT_KIND 0
#define DOUBLE_KIND 1

typedef struct {
	int kind; /* tag kind */
	union {int i; double d;} u;
} Number;

枚举

与枚举相比,虽然宏也可以增加可读性,但预处理后宏名就被替换掉了,在调试期间没有 办法再使用定义的宏名。

定义与使用枚举

建立标记:

enum suit { CLUBS, DIAMOUDS, HEARTS, SPADES };
enum suit s1, s2;

定义类型:

typedef enum { CLUBS, DIAMOUDS, HEARTS, SPADES } Suit;
Suit s1, s2;

枚举非常适合用来作前面联合类型结构的标志位。

枚举作为整数

枚举类型与整数相通。编译器会把枚举中成员从 0 开始赋值,当然也可以指定赋值:

enum suit { CLUBS, DIAMOUDS, HEARTS, SPADES };
enum suit { CLUBS=21, DIAMOUDS=2, HEARTS=78, SPADES=8 };

枚举允许在最后一个项目后面加逗号,这样以后修改代码更加容易了:

enum suit { CLUBS=21, DIAMOUDS=2, HEARTS=78, SPADES=8, };

枚举可以作为数组的下标,C99 中还可以用来指定初始化表达式的下标:

63enum week { SUN, MON, TUE, WEN, THU, FIR, SAT};
const char * daliy = {
	[MON] = "Beef ravioli",
	[TUE] = "Pizza"
};

重名的结构、联合、枚举定义

如果在不同的文件中定义了重名的结构、联合、枚举类型不是同一个类型,但可以相互兼 容。在用法上可以当作同一类型使用。在 C89 中,不同文件中定义的类型如果相同的 成员名字和顺序则兼容。

在 C99 中,更进一步要求两个类型要么有相同的标记名,要么都没有标记名。

指针的高级应用

动态内存分配

在头文件<stdlib.h>中声明内存分配相关的函数:

  • malloc函数只分配内存,不负责清空分配的内存
  • calloc函数会分配被清0的内存空间,所以要两个参数,一个指定元素的长度,一个是元素的个数
  • realloc函数调整先前分配内存的大小。

返回的类型都是通用指针(void *)。在内存分配失败的时候会返回空指针NULL, 程序员有责任添加检查返回的指针是否等于NULL的判断。

free函数负责动态分配的内存回收,以指向要回收内存指针作为参数。

内存管理错误:

  • 如果有一块内存没有指针指向就没有办法访问这块内存,造成内在漏泄;
  • 如果有指针指向了已经被回收的内存,那就造成了悬空指针。

动态分配数组

先动态分配数组的空间,然后用指针指向第一个元素的位置就可以访问了:

int * a;
a = malloc( num * sizeof(int) );

通过以上代码,可以通过指针的算术运算来访问数组元素。 也可以忽略 a 是一个指针,把它当作数组的名字来用。

通过指针访问结构成员

C 语言提供了操作符->对指向结构的指针访问结构成员:

node->value = 0;

相当于:

(*node).value = 0;

指向指针的指针

void addToList(struct node ** list, int n) {
	struct node * newNode = malloc(sizeof(struct node));
	*list = newNode;
}

struct node * first;
addToList(&first, 10);

指向函数的指针

把指向函数的指针作为参数

把指向函数的指针作为形参时可以省略睡号,如下面这个计算积分的函数:

double intergrate(double (*func)(double), double a, double b) { 
	return (*func)(a, b); 
}

可以简写为:

double intergrate(double func(double), double a, double b) { 
	return (*func)(a, b); 
}

在调用时,不写函数后面的括号表示不是要调用这个函数,而是把指向这个函数的指针传 递给函数,如下面代码中我们调用sin函数从 0 到 PI/2 的积分:

result = intergrate(sin, 0.0, PI/2);

用变量存放指向函数的指针

例如下面的变量:

void (*pf)(int);

可以指向任何返回类型为void参数列表只有一个int的函数,注意函数不用加 取址操作符&

void count(int n);
fp = count;

通过指针调用函数的方法如下:

(*pf)(i);   /* 方法 1 */
pf(i);      /* 方法 2 */

用数组存放指向函数的指针

定义一个数组来存放多个函数指针:

void (*file_cmd[])(void) = { new_cmd, open_cmd, close_cmd,
close_all_cmd, save_cmd };

用户可以通过下标调用对应的函数:

(*file_cmd[n])(); /* 方法 1 */
file_cmd[n]();    /* 方法 2 */

C99 中的受限指针

C99 中通过关键字restrict声明指针为受限指针,受限指针指向了一个目标以后, 这个目标就不能被除这个受限指针以外的方式修改,不然结果是末定义的:

int * restrict p;
int * restrict q;

p = malloc(sizeof(int));
q = p;
*q = 0;         /* cause undefined behavior */
  • 如果这里的受限指针p声明为局部变量而且没有用extern存储类型,那个受限作用 只限于当前的程序块
  • 反之如果restrict作用于文件作用域的变量,则会在整个程序中起作用。

如果受限指针p是一个函数的局部变量,而另外一个受限指针q定义嵌套于 这个函数体的程序块内,那p可以复制到q中。

一个应用受限指针的例子是在头文件<string.h>中定义的两个复制字节的函数:

void * memmove( void * s1, const void * s2, size_t n );
void * memcpy( void * restict s1, const void * restict s2, size_t n );

不同之处是,memmove可以在源与目的相同的情况下执行复制,如把元素偏移一个位置:

int a[100];
memmove(a[0], a[1], 99 * sizeof(int) );

而在 C99 中,memcpy的参数中增加了restict,这样就表明两个参数不能指向同一 目标。

C99 中的灵活数组成员

例如要用一个结构来保存字符串,包含长度:

struct vstring {int len; char[N]};

但是用宏限定长度会浪费空间。C89 环境下的传统方法只声明长度为 1,调用时用动态分 配空间。这种方法欺骗 malloc 分配了更多的内存用来存放数组的元素,在过去曾经非常 流行,被称为「struct hack」。如:

struct vstring {int len; char[1]};

...

struct vstring * str = malloc( sizeof(struct vstring) + n -1);
str->len = n;

C99 中提供了灵活数组成员:当结构成员的最后一个是数组时,数组的长度可以省略。如:

struct vstring {int len; char[];};

...

struct vstring * str = malloc( sizeof(struct vstring) + n );
str->len = n;

使用灵活数组成员有三个限制:

  1. 灵活数组一定要是结构的最后一个成员
  2. 结构中至少还有另外一个成员
  3. 复制包含灵活数组的成员时,其他的成员会复制,但不会复制灵活数组。

有灵活数组的结构是不完全的类型,因为缺少决定占用内存大小的信息。

声明

对变量的声明

变量的性质

存储期限:

  • 「自动存储期限」变量会在进入到所在代码块时被分配内存空间,离开代码块后回收空间
  • 「静态存储期限」变量在程序运行期间一直占用同一个内存空间,所以值会一直保留下来。

作用域:可以引用到该变量的代码:

  • 「块作用域」的变量可以被声明的地方开始到其所在代码块中的代码调用
  • 「文件作用域」的变量从声明的地方开始到整个文件结束都可以被访问。

链接:变量的链接影响程序中不同部分可以共享该变量的范围:

  • 「外部连接」变量可以被多个文件或是全部文件共享
  • 「内部链接」变量可以被单一文件中的函数共享
  • 「无链接」变量只能被一个函数访问,不能共享。

C 语言中默认的变量属性如下:

int i; /* 静态存储期限、文本作用域、外部链接 */

void func(void) { 
	int j; /* 自动存储期限、块作为域、无链接 */
}

存储类型

修饰存储类型的关键字有四个,使用时只能使用一个,而且必须放在最前面:

  • auto
  • static
  • extern
  • register
auto

auto声明变量为「自动存储期限」与「块作用域」只能用来修饰代码块中的变量,所以几乎 用不着声明,代码块中的变量默认就是了。

static

static用于修饰块代码外的变量时声明变量从「外部链接」变为「内部链接」; 修饰代码块内变量时说明变量由「自动存储期限」变为「静态存储期限」。

int i;               /* 内部链接 */
void func(void) { 
	int j;             /* 静态存储期限 */
}

局部 static 变量的特点:

  • 的 static 变量只初始化一次
  • 函数内的 static 变量在递归调用中是共享的
  • 函数不应该返回 auto 局部变量的指针,但可以返回 static 局部变量的指针。

使用static局部变量的指针作为返回值:

char digit_to_hex_char(int n) { 
	static const arr[16] = "0123456789ABCDEF";  // 声明为 static 只要初始化一次
	arr[n];
}
extern

extern 修饰符让多个源文件可以共享一个变量。

在一般情况下 extern 表明这不是对变量的定义,只是声明在其他的部分或是别的文件里 有定义这个变量,应该在本文件的其他行或是别的源文件里去找这个变量。

变量在程序中可以有多次声明,但只能有一次定义。

有一个特殊情况:对变量时行初始化的 extern 声明是定义,这可以用来防止多个 extern 声明用不同的方法用不同方法对变量进行初始化:

extern int i = 0;

等于:

int i = 0;

extern 变量始终具有「静态存储期限」。变量的作用域供事于声明的位置:

  • 如果在代码块内部就是「块作用域」
  • 不然就是文件作用域。
extern int i;          /* 内部链接 */

void func(void) { 
	extern int j;        /* 静态存储期限 */
}
register

register 会申请把变量存入在 CPU 的寄存器里以取得更已然的访问速度。

由于是在寄存器里没有内在地址,所以不能用操作符&进行取址操作。

函数也用存储类型,可以在函数的返回类型前声明前添加关键字 static 或 extern:

  • static表明函数只能被定义函数的源文件访问(其实还是可以通过指向函数的指针访问)
  • extern说明允许其他文件调用该函数(函数默认情况下可以被外部文件访问)。

限制类型

const

const限制变量初始化以后就不能更改。在某些特定环境中(如嵌入式环境)变优化 const 到 ROM 中。

const 的意思是变量是「只读的」,不表示它是常量,所以不能用于常量表达式。如: 声明数组的长度(C99 中自动存储期限的数组可以用 const 变量作可变长数组)。

const 只在它的生命周期内只读,而不是整个程序的执行时间内都是它的生命周期。

volatile

volatile关键字通知编译器这个变量的值可能随时被其他行为改变,不要认为自己的 代码没有改变它而以为它不会变,默认进行优化。

restrict

C99 中通过关键字restrict声明指针为受限指针,受限指针指向了一个目标以后, 这个目标就不能被除这个受限指针以外的方式修改,不然结果是末定义的。

变量类型

int,float,double 等 C语言支持类型。

声明标识

解释复杂的声明

在解读复杂的声明时,()[]*更代表变量类型。如:

int * arr[];				//是数组
int * fun(); 				//是函数

以下的括号表明fp是指针,后面的括号表示指向的是函数参数列表是(int)。返回类型 是void

void (* fp)(int);

再看以下的表达式:

int * (*x[10])(void)

数组优先于指针,所以x是指针数组。再向外面看到指向的是函数,参数为空, 返回类型为int指针。

全类型定义增加可读性

复杂的声明验证理解难度:

int * (*x[10])(void)

可以一步一步地通过定义:

  • 先定义指向函数的参数与返回类型
  • 定义指向这类函数的指针
  • 定义指针数组
  • 声明变量
typedef int * Fcn(void);				  /* 1 */
typedef Fcn * Fcn_ptr             /* 2 */
typedef Fcn_ptr Fcn_ptr_arr[10]   /* 3 */
Fcn_ptr_arr x                     /* 4 */

变量初始化

静态变量只能用常量或常量表达式来初始化。

#define FIRST 1
#define LAST 100

static int i = LAST - FIRST + 1;

包含在花括号中的数组、结构、联合的初始化只能用常量表达式(C99 中自动存储变量可 以用变量)。

自动类型的结构或联合初始化式可以是另外一个结构或联合:

void fun( struct part a ) {
	struct part b = a;
}

内联函数

C99 增加inline关键字表明函数为内联函数,建议编译器把调用函数替代为对机器代码 的调用以避免函数调用以及函数退出的开销。在没有内联函数以前,为了避免这些开销 只能用带参数的宏。

定义函数

因为函数默认有外部链接,所以从语法上来说可以被其他源文件调用,但编译器没有 考虑到内联定义,所以其他文件调用时会出错。为了增加语法检查,一种方法是增加 static声明:

static inline double average(double a, double b) { 
	return (a+b)/2;
}

更好一点的办法是,先在头文件average.h中定义:

#ifndef AVERAGE_H
#define AVERAGE_H
inline double average(double a, double b) { return (a+b)/2; }
#endif

这样以后要调用的源代码只要包含头文件就可以了:

extern double average(double a, double b);

因为无法确定编译器是通过外部定义函数调用还是通过内联展开。所以这个方法的优点是 统一所有的函数都是外部定义。

内联函数不能定义可改变的 static 变量,而且不能引用有内部链接的变量。函数中可以 定义同时为 static 和 const 的变量,但每个内联函数都需要分别创建该变量的副本。

在 GCC 中,老版本实现和 C99 有冲突:前面使用的定义头文件的方法可能无效,但还是 可以用 static inline 的方法。

程序设计

隐藏信息

C 语言中主要通过 static 声明文件作用域的变量为内部链接,防止被其他的源文件 访问或声明函数只能被同一文件的其他函数调用。

出于风格考虑会用宏定义私有与公有的标识符。

由于 static 在 C 语言中很多种用法,定义了宏可以很明确地表示,在这里 使用 static 仅仅是为了隐藏信息:

#defind PUBLIC          /* empty */
#defind PRIVATE static

抽象数据类型

一般把新类型的定义与相关的开放函数声明放在头文件中:

#define STACK_SIZE 100

typedef struct {
	int contents[STACK_SIZE];
	int top;
} Stack;

void emptyStack(Stack * s);
bool isEMpty(const Stack * s);

以上的声明这样会把数据结构展现暴露给了用户,用户可能会修改 Stack 的结构。 代替的办法是:不在头文件中声明结构,而是只声明指向这个结构的指针,对应的操作函数 也以指针为参数:

typedef struct stack_type * Stack;

void emptyStack(Stack s);
bool isEmpty(const Stack s);

但这样会引起一个新的问题:用户得到了指针,但不能对指针用->取结构成员操作。 因为没有结构的定义,编译器根本不知道有哪些成员。

便于修改数据类型的代码

通过类型定义让源代码中的数据类型更加容易修改:

typedef int Item;
typedef struct stack_type * Stack;

void emptyStack(Stack s);
bool isEmpty(const Stack s);
void add(Stack s, Item i);
void remove(Stack s, Item i);

栈类的实现:

struct static_type {
	Item contents[static_size];
	int top;
};

可以把数组成员改变为指向数组的指针来方便临时分配指定大小的数组:

struct static_type {
	Item * contents;
	int top;
	int size;
};

底层程序设计

位运算符

  • 位移运算符<<左边溢出右边补 0
  • >>非负数时左边补 0,如果是负值时有的情况下有的编译器会补 0 有的补 1;

其他的几个位操作符的优先级为:

  • 位取反~
  • 位与操作&
  • 异或^
  • 位或操作|

设置指第 n 位通常用位或操作:

i |= 1 << n;

清零第 n 位通常用反操作结合位与操作:

i &= ~(1 << n);

测试第 n 位是否有设置:

if( i & (i << j) ) { ... }

修改连续的位,如把 101 存入第 4 到 6 位:

i = (i & ~0x0070) | (j<<4);

取得连续的位,如取得 0 到 2 位:

j = i & 0x0007;

如果要取的不是最右端的数据,要先移到最右端,如取得 4 到 6 位:

j = (i>>4) & 0x0007;

结构中的位域

比如保存年月日就不用很大的数字,只要几个位数就够了:

struct birth_date {
	unsigned int day: 5;
	unsigned int month: 4;
	unsigned int year: 7;
};

相同类型的可以简化声明:

struct birth_date {
	unsigned int day: 5, month: 4, year: 7;
};

类型上的限制是必须为 int、unsigned int 或 signed int。使用 int 会引起二义性, 因为不是所有的编译器都把最高位作为符号位的。为了增加可移植性,推荐声明到底是 用 unsigned int 还是 signed int。还有操作的时候不能用取址操作。

编译器会按机器的字长来把位域一位位地存入,如果剩下的空间不够存放下一个位域 的话编译器会跳到下一个单元里继续开始。可以给一个长度为 0 的域通知编译器在 下一个内存单元存放,这样起始位置对齐:

struct birth_date {
	unsigned int a: 4;
	unsigned int : 0;
	unsigned int b: 8;
};

如果不准备使用某个成员,就像上面的可以让成员名为空。

使用枚举实现位域与字长的转换

例一:日期结构的长度正好和一个短整形相同,这样同样的值用 union 就可以把它一会 当数值用一会当结构都,不用转换的操作:

union int_date {
	unsigned short i;
	struct file_date fd;
};

例二:x86 处理器有四个 16 位寄存器:AX、BX、CX、DX;每个个寄存器又能当作两个 8 位寄存器来用:如把 AX 当作 AH/AL 两个寄存器来用,为了方便地读取目前寄存器中的值 在作为 16 位和 8 位处理时,用 union 来代表所有的寄存器:

typedef unsigned char BYTE;   // 定义一个字节
typedef unsigned short WORD;  // 定义一个字长度

union rege_set {
	struct {
		WORD ax, bx, cx, dx;
	} word;
	struct {
		BYTE al, ah, bl, bh, cl, ch dl, dh;
	} byte;
} regs;

在这种方式时工注意当数据多于一个字节时,有的从左边开始存(big-endian);有的从 右边开始存(little-endian)。x86 使用的是小端,所以regs.word.ax 的第一个字节是低 位。

将指针作为地址处理

以下的程序根据用户输入的 16 进制地址和长度,分别用二进制与字符显示内存块的 内容:

/*********************************************************
 * From C PROGRAMMING: A MODERN APPROACH, Second Edition *
 * By K. N. King                                         *
 * Copyright (c) 2008, 1996 W. W. Norton & Company, Inc. *
 * All rights reserved.                                  *
 * This program may be freely distributed for class use, *
 * provided that this copyright notice is retained.      *
 *********************************************************/

/* viewmemory.c (Chapter 20, page 521) */
/* Allows the user to view regions of computer memory */

#include <ctype.h>
#include <stdio.h>

typedef unsigned char BYTE;

int main(void)
{
  unsigned int addr;
  int i, n;
  BYTE *ptr;

  printf("Address of main function: %x\n", (unsigned int) main);
  printf("Address of addr variable: %x\n", (unsigned int) &addr);

  printf("\nEnter a (hex) address: ");
  scanf("%x", &addr);
  printf("Enter number of bytes to view: ");
  scanf("%d", &n);

  printf("\n");
  printf(" Address              Bytes              Characters\n");
  printf(" -------  -----------------------------  ----------\n");

  ptr = (BYTE *) addr;
  for (; n > 0; n -= 10) {
    printf("%8X  ", (unsigned int) ptr);
    for (i = 0; i < 10 && i < n; i++)
      printf("%.2X ", *(ptr + i));
    for (; i < 10; i++)
      printf("   ");
    printf(" ");
    for (i = 0;  i < 10 && i < n; i++) {
      BYTE ch = *(ptr + i);
      if (!isprint(ch))
        ch = '.';
      printf("%c", ch);
    }
    printf("\n");
    ptr += 10;
  }

  return 0;
}

程序的结果可能如下:

Address of main function: 400694
Address of addr variable: a3eac228

Enter a (hex) address: 400600
Enter number of bytes to view: 40

Address              Bytes              Characters
-------- -----------------------------  ----------
  400600 55 48 89 E5 53 48 83 EC 08 80  UH..SH....
  40060A 3D 30 0A 20 00 00 75 4B BB 30  =0. ..uK.0
  400614 0E 60 00 48 8B 05 2A 0A 20 00  .`.H..*. .
  40061# 48 81 EB 28 0E 60 00 48 C1 FB  H..(.`.H..

标准库

标准库的使用

标准库中全称规范

  • 由一个下划线和一个大写字母开头和由两个下划线开头的都不要用
  • 由一个下划线和一个小写字母开头的被保留作为文本作用域的标记,自定义的 就在作函数内部变量时用吧。

用宏替换函数

标准头了为了提高程序效率把不少函数的定义又用宏也定义了一次。所以可能会有宏替换 带来的参数反复求值等问题。

如在<stdio.h>中把getchar定义了两次,宏和函数各都有:

int getchar(void);
#define getchar() getc(stdin)

一般情况下问题不大,当然如果考虑到宏的副作用只想调用函数,而不想用宏替换,给 函数加上括号会让预处理器无法识别宏(宏后面要紧跟左括号),而编译器会把它识别为 函数:

ch = (getchar) ();

另一种方法是直接删除宏定义

#undef getchar

常用的定义stddef.h

头文件<stddef.h>提供了常用的类型和宏定义。

主要类型有:

  • ptrdiff_t是当两个指针相减时结果的类型,是有符号的整数。
  • size_tsizeof运算符的返回类型,是无符号的整数。
  • wchar_t是表示多国语言用的宽字符类型。

主要宏有:

  • NULL,用来表示空指针。
  • offsetof(结构名, 成员名)计算出成员在结构中的偏移量。因为字对齐可能在成员与 成员之间有空洞。offsetof出来的结果准确,增加程序可移植性。

C99 定义 Bool 头文件

头文件<stdbool.h>定义了四个宏:

  • bool定义为_Bool
  • true定义为 1。
  • false定义为 0。
  • __bool_true_false_are_defined定义为 1。

错误处理

assert.h 诊断

assert虽然是一个宏,但即是按照函数的使用方式来设计的。当检查的表达式不为真时, 它会身 stderr 输出一条信息,并调用 abort 函数终止程序。

  • C89 中要求参数一定要是 int 型
  • C99 中允许参数为任何标量类型。
assert(0<i && i<10);

使用 assert 会带来性能的消耗,可以在包含 assert 头文件前定义宏NDEBUG即可( 值是什么不要紧,只要名字叫 NDEBUG 就行):

#define NDEBUG
#include <assert.h>

errno.h 头文件

errno.h头文件中有一个errno变量(有可能是宏,但这个宏可以用来作为 左值赋值)。

很多情况下程序发生错误会把错误代码(整数)保存在这个变量中,我们可以在函数调用 后检查它是否为 0 来判断函数的调用是否出错。

要注意的是,因为很多函数出错的都会把错误代码存放在这个 errno 中,所以在 函数调用前先清 0,函数调用后立刻检查,不然是谁设置的都不知道。

errno.h 头中通常定义了一些错误代码的宏,常见的如下:

  • 84EDOM:定义域错误,传递给函数的参数超出了函数的定义域,比如把负数 作为sqrt()的参数。
  • ERANGE:返回值太大。
  • EILSEQ:编码错误。

还有很多这样的宏用不着刻意去记,因为在 stdio.h 中提供了perror()函数会输出信息 并提供对错误代码的解释:

errno = 0;
y = sqrt(x);
if(0 != errno) {
	perror("Ooooooops!!!")
	exit( EXIT_FALIURE);
}

输出的错误信息为:

Ooooooops!!!: Numberical argument out of domain

还有 string.h 头中提供了 strerror 函数以错误代码为参数,返回一个指针指向解释 错误代码的含意:

puts(strerror(EDOM));

输出的错误信息为:

Numberical argument out of domain

在有些情况下,在调用可能出错的函数前没有把 errno 设为 0。这是因为这些函数 本来就会的错误的时候返回 -1,这样肯定可以知道函数已经出错了。errno 只是判断 到底出了什么错。但是由于很多调用出错时都会设置 errno。所以一定要一出错就 检查 errno,不然 errno 可能被其他调用修改。

signal.h 头文件信号处理

信号有可能是为了控制程序运行而产生的,也有可能是发生了错误,可能会在任何时候 异步产生。

信号宏

signal.h头中定义了一系列的宏表示不同的信号,一般都在SIG和一个大写字母开头。 常用的有:

  • SIGABRT:异常终止(可能是因为abort函数导致)。
  • SIGFPE:算术运算中发生错误(如除以 0 或溢出)。
  • SIGILL:无效的指令。
  • SIGINT:中断。
  • SIGSEGV:无效存储访问。
  • SIGTERM:终止请求。

预定义的处理函数

signal.h头中已经定义了些默认的处理函数,代表了一般的处理方法,函数名都以 SIG_开头。如以下两个用宏表示的:

  • SIG_DFL:按「默认」方式处理信号,各编译器自己实现,大多数情况下是会导致终止程序
  • SIG_IGN:忽略这个信号。

signal 函数

signal设定对于指定信号会调用什么函数对它进行处理:

  • 第一个参数是特定的信号编码
  • 第二个参数是要调用的函数的指针。

被调用的函数必须有一个 int 参数而且返回类型为 void,信号编码会作为参数被传递给 处理函数。

一般情况下除非信号是由abort函数或是raise函数引发的,否则信号处理函数 不应该调用库函数或是试图使用静态存储期限的变量。例外的情况是:信号处理函数 可以调用singal函数,只要第一个参数是当前正在处理的信号,因为它允许信号处理函数 进行自身重新安装到信号上。

在 C99 中,信号处理函数还可以调用abort函数或_Exit函数。

还有上一段中提到的信号处理函数通常不应该访问具有静态存储期限的变量,它的 例外情况是:

  1. 这个静态变量的类型一定要是在 signal.h 头是声明的一个名为sig_atomic_t的 类型,它可以被 CPU 只用一条指令从内存中读写。
  2. 这个静态变量还要用volatile警告编译器这个值随时可能被改变。

信号处理函数返回之后:

  • 如果信号是SIGABRT,程序会终止
  • 如果信号是SIGFPE,处理函数的返回结果是末定义的(也就是说不要处理它)
  • 其他的信号会回到信号发生点恢复并继续执行。

signal函数的返回值是指向指定信号的前一个处理函数的指针,一般情况下不会理会。 如果需要,可以通过它来恢复原来的处理函数:

void (*orig_handler)(int);
orig_handler = signal(SIGINT, handler);

如果要恢复原来的处理函数:

signal(SIGINT, orig_handler);

要注意的是 UNIX 实现通常会保留对信号处理函数的设置,但有的系统下处理完后 会把信号的处理函数又重设为SIG_DEF(系统默认的处理函数),这样要在处理函数 返回前调用signal再次设置信号的处理函数。

如果signal调用失败,不能成功设置对信号的处理函数,它会返回SIG_ERR并设置 错误代码到errno。如下面的代码查检signal调用是否成功:

if( SIG_ERR == signal(SIGINT, handler) ) {
	perror("Ooops!!");
}

为了避免像是「信号是由处理这个信号的函数引发的」这种无限递归的情况。C89 的 解决办法是要分两步设置信号:

  • 第一步:要么把该信号的处理函数设置为SIG_DFL即默认的信号处理函数; 要么在处理函数执行时阻塞该信号(SIGILL是一个特殊情况,这两种行为都不用做)。
  • 第二步:再调用程序员提供的处理函数。

在 C99 中,当信号发生时,不仅可以禁用该信号,还可以禁用别的信号。对于处理 SIGILLSIGFPESIGSEGV信号的处理函数,其返回结果是末定义的。

而且 C99 中对于abort函数或是raise函数产生的信号,信号处理函数本身一定 不能调用raise函数。

raise函数

raise 函数会产生一个参数指定的信号,返回 0 表示成功, 非 0 表示失败。格式:

int raise(int sig);

下面的程序说明如何使用信号:

  1. 先定义一个处理函数(并保存了原先的处理函数)
  2. 然后调用raise_sig产生信号
  3. 接下来设置用SIG_IGN处理SIGINT信号并再次raise_sig产生信号
  4. 最后重新设置用原来的处理函数,并最后一次调用raise_sig

例:

/*********************************************************
 * From C PROGRAMMING: A MODERN APPROACH, Second Edition *
 * By K. N. King                                         *
 * Copyright (c) 2008, 1996 W. W. Norton & Company, Inc. *
 * All rights reserved.                                  *
 * This program may be freely distributed for class use, *
 * provided that this copyright notice is retained.      *
 *********************************************************/

/* tsignal.c (Chapter 24, page 634) */
/* Tests signals */

#include <signal.h>
#include <stdio.h>

void handler(int sig);
void raise_sig(void);

int main(void)
{
  void (*orig_handler)(int);

  printf("Installing handler for signal %d\n", SIGINT);
  orig_handler = signal(SIGINT, handler);
  raise_sig();

  printf("Changing handler to SIG_IGN\n");
  signal(SIGINT, SIG_IGN);
  raise_sig();

  printf("Restoring original handler\n");
  signal(SIGINT, orig_handler);
  raise_sig();

  printf("Program terminates normally\n");
  return 0;
}

void handler(int sig)
{
  printf("Handler called for signal %d\n", sig);
}

void raise_sig(void)
{
  raise(SIGINT);
}

一开始默认的处理应该是SIG_DFL,它一般会导致程序结束。所以生为设置为原来的 处理函数以后,它会终止程序。在这种情况下,应该会看到: 「Program terminates normally」。

setjmp.h 非局部跳转

通常函数会返回到它被调用的位置。goto语句只能转到同一函数内的标记处。但是 setjmp.h可以使一个函数直接跳转到另一个函数而不需要返回。其中最重要的是以下 两个宏:

int setjmp(jmp_buf env);
void logjmp(jmp_buf env, int val);
  1. setjmp宏能标记程序中的一个位置,把相关信息保存在jmp_bef类型变量env中, 返回值为 0。
  2. 在需要跳转时调用longjmp函数:
    • 第一个参数是前面setjmp函数保存了位置信息的变量env
    • 另一个参数val可以设定一个非 0 的整数,表示跳转过来的位置。
  3. 因为回到了之前的位置,所以setjmp函数又会执行一次。但这次它的返回值不是0了, 而是longjmp函数的参数val,可以根据这个返回值来判断从哪个地方跳转过来的。

一定要保证参数env已经被setjmp函数初始化过了。还有要保证setjmp最初调用的 函数一定不能在调用longjmp之前返回。不然不知道会发生什么。例子:

/*********************************************************
 * From C PROGRAMMING: A MODERN APPROACH, Second Edition *
 * By K. N. King                                         *
 * Copyright (c) 2008, 1996 W. W. Norton & Company, Inc. *
 * All rights reserved.                                  *
 * This program may be freely distributed for class use, *
 * provided that this copyright notice is retained.      *
 *********************************************************/

/* tsetjmp.c (Chapter 24, page 636) */
/* Tests setjmp/longjmp */

#include <setjmp.h>
#include <stdio.h>

jmp_buf env;

void f1(void);
void f2(void);

int main(void)
{
  if (setjmp(env) == 0)
    printf("setjmp returned 0\n");
  else {
    printf("Program terminates: longjmp called\n");
    return 0;
  }

  f1();
  printf("Program terminates normally\n");
  return 0;
}

void f1(void)
{
  printf("f1 begins\n");
  f2();
  printf("f1 returns\n");
}

void f2(void)
{
  printf("f2 begins\n");
  longjmp(env, 1);
  printf("f2 returns\n");
}
  • 第一次在 23 行调用 setjmp 返回 0
  • 接下来调用f1f1再调用f2
  • f2中调用longjmp又回到 23 行。
  • 这次再调用setjmp的返回值等于longjmp时的参数 1

所以也可以根据这一点来判断是从哪里跳过来的。

只能在两种情况下使用setjmp

  • 作为表达式语句中的表达式(可能会强转为 void)。
  • 作为 if、switch、while、do 或 for 语句中控制表达式的一部分。

模式为():

  • setjmp(...)
  • !setjmp(...)
  • constExp
  • setjmp(...)
  • setjmp(...)
  • op constExp

constExp是一个整数常量表达式,op是关系或判断运算符;

在 C89 中信号处理函数中可以调用longjmp函数,只要这个处理函数不是由另一个 信号处理函数发出的信号引发的。C99 中没有这一限制。