暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

C高阶教程

原创 yBmZlQzJ 2023-09-16
291

Cover

Table of Contents

前言

第 1 章 写在前面的小测试

第 2 章 数组与指针(一)

数组:

指针

多维数组

指针数组与数组指针

数组参数与指针参数

函数指针

第 5 章 类型转换

第 6 章 流

第 6 章 输入

fgets

scanf

第 7 章 输出

fputs

printf

第 7 章 IO缓冲问题

第 8 章 那些不安全的库函数

第 9 章 预处理

第 9 章 预定义的宏

非局部跳转

变长参数列表

可变长数组

switch语句中的case

指定初始化(C99)

受限指针(C99)

静态数组索引(C99)

多字符常量

前言

C 是一门简洁且功能强大的编程语言,几乎每一个理工科的学生在大学都接触过C 语言。

本文不会尝试涵盖 C 的全部知识和每个特性,只会介绍一些那些不常见的边角知识和一些高阶特性。

希望认真读过本系列的朋友能够有所收获,期待您的反馈!

适用人群

学过 C 语言的、想继续深入的朋友。

学习前提

在学习本教程之前,你需要对 c语言基础 知识有一定了解。

作者简介

杨玉磊,西安电子科技大学软件工程硕士。宅男一枚,航天二院混迹过,创新工场做过手游,一直未有成绩。读读书,敲敲代码,写写博客,思考思考人生。
(google/baidu: 菜鸟的自留地

更新日期

更新内容

2015-07-27

C 语言高阶教程

1

写在前面的小测试

本系列主要讲的是 C 语言的一些深入的知识,故在开始之前大家可以先完成这个小测试,如果你对下面的题目感觉毫无压力,那么你对于 C 语言基础的掌握已经比较牢固了。

    1. 下面代码的输出是什么?

#include <stdio.h>
 
int main(void)
{
int a[3][2] = { (0,1), (2,3), (4,5) } ;
int *p ;
p = a[0] ;
printf(“%d”, p[0]) ;
}

    1. int a[10]; 问下面哪些不可以表示 a[1] 的地址?
      A. a+sizeof(int)
      B. &a[0]+1
      C. (int*)&a+1
      D. (int*)((char*)&a+sizeof(int))
    2. 下面的C程序是合法的吗?如果是,那么输出是什么?

#include <stdio.h>
 
int main()
{
int a=3, b = 5;
 
printf(&a["Ya!Hello! %s\n"], &b["junk/super"]);
 
printf(&a["WHAT%c%c%c %c%c %c !\n"], 1["this"],
2["beauty"],0["tool"],0["is"],3["sensitive"],
4["CCCCCC"]);
 
return 0;
}

    1. 32 位机上根据下面的代码,问哪些说法是正确的?(多选题类型)
      signed char a = 0xe0;
      unsigned int b = a;
      unsigned char c = a;
      A. a>0 && c>0 为真
      B. a == c 为真
      C. b 的十六进制表示是:0xffffffe0
      D. 上面都不对
    2. 下面程序的输出结果(32位小端机)

#include <stdio.h>
 
int main()
{
long long a = 1, b = 2, c = 3;
printf("%d %d %d\n", a, b, c);
return 0;
}

    1. 请问下面的程序的输出值是什么?

#include <stdio.h>
 
#include <stdlib.h>
 
 
#define SIZEOF(arr) (sizeof(arr)/sizeof(arr[0]))
 
#define PrintInt(expr) printf("%s:%d\n",#expr,(expr))
 
 
int main()
{
int pot[] = {
0001,
0010,
0100,
1000
};
int i;
for(i=0;i<SIZEOF(pot);i++)
PrintInt(pot[i]);
 
return 0;
}

    1. 请问下面的程序的输出值是什么?

#include <stdio.h>
 
int main(void)
{
char a[1000] ;
int i ;
for(i=0; i<1000; i++)
{
a[i]= -1-i ;
}
printf(“%d”,strlen(a)) ;
 
return 0 ;
}

    1. 下面这段代码会挂么?会挂在哪一行?

#include <stdio.h>
 
struct str{
int len;
char s[0];
};
 
struct foo {
struct str *a;
};
 
int main(int argc, char** argv) {
struct foo f = {0};
if (f.a->s) {
printf( f.a->s);
}
return 0;
}

    1. 在X86系统下,输出的值为多少?

#include <stdio.h>
 
int main(void)
{
int a[5] = {1,2,3,4,5} ;
int *ptr1 = (int *)(&a+1) ;
int *ptr2 = (int *)((int)a+1) ;
 
printf(“%x,%x”,ptr1[-1], *ptr2) ;
return 0 ;
}

    1. 请问下面的程序的输出值是什么?

#include <stdio.h>
 
 
int main()
{
int a[5][5] ;
int (*p)[4] ;
p = a ;
printf("%d", &p[4][2]-&a[4][2]) ;
 
return 0 ;
}

    1. 以下代码在 windows 系统下成员 i 的偏移量是多少?在 Linux 系统下 i 的偏移量是多少?

struct S
{
char c ;
int i[2];
double v ;
} ;

    1. 下面代码中有BUG,请找出

int tadd_ok(int x, int y) //判断加法溢出
{
int sum = x+y ;
return (sum-x == y) && (sum-y == x) ;
}
int tsub_ok(int x, int y) //判断减法溢出
{
return tadd_ok(x, -y) ;
}

##

    1. 下面变量的核心是什么,并用多个typedef改写下面的声明式
      int *(*(*(*abc) ( ) ) [6]) ( ) ;
    2. (*(void(*)( ))0 )( ) 这是什么?
    3. 编写一些代码,确定一个变量是有符号数还是无符号数

以上问题的参考答案点击

2

数组与指针(一)

指针是 C 的精华
,如果未能很好地掌握指针,那 C 也基本等于没学。

先附上两句话:
第一句话:指针就是存放地址的变量。(就是这么简单。)
第二句话:指针是指针,数组是数组。(只是它们经常穿着相似的衣服来逗你玩罢了。)

轻松一下:(见识一下数组和指针的把戏)
1、引用一维数组
某个值的方式:(先定义指针p=a)

  • a[2]
  • *(a+2)
  • (&a[1])[1]
  • *(p+2)
  • p[2]

2、引用二维数组
某个值的方式:

例:int a[4][5];

  • ⑴ a[i][j]
  • ⑵ *(a[i]+j)
  • ⑶ *(*(a+i)+j)
  • ⑷ (*(a+i))[j]
  • ⑸ *(&a[0][0]+i*5+j)

若定义:int * p[4], m ;
for(m=0; m<4;m++) p[m] = a[m] ;

  • ⑹ p[i][j]
  • ⑺ *(p[i]+j)
  • ⑻ *(*(p+i)+j)
  • ⑼ (*(p+i))[j] //请与⑴-⑷对比

若定义 int (*q)[5]; q=a ;

  • ⑽ q[i][j]
  • ⑾ *(q[i]+j)
  • ⑿ *(*(q+i)+j)
  • ⒀ (*(q+i))[j] //请与⑴-⑷ ⑹-⑼对比

进入正题:

数组:

数组是指具有相同类型的数据组成的序列,是有序集合。(教科书上的定义)
(即:数组就是内存中一段连续的存储空间。那么我们怎么使用它呢?用数组名。也就是我们用数组名可以在内存中找到对应的数组空间,即数组名对应着地址。
那么数组中有这么多元素,对应的是哪个元素的地址呢?对应着首元素的地址。 所以,我们可以通过数组的首元素地址来找到数组)

故:数组名是一个地址(首元素地址),即是一个指针常量。(不是指针变量)
只有在两种场合下,数组名并不用指针常量来表示:

  1. sizeof(数组名) ; sizeof返回整个数组的长度,而不是指向数组的指针长度。
  2. &数组名 ; 产生的是一个指向整个数组的指针,而不是一个指向某个指针常量的指针。

&a[0] 与 &a 的区别

两者的值相同,但意义不同。
&a[0]是指数组首元素的地址。&a是整个数组的地址。
(问题来了,整个数组跨越几个存储单位,怎么表示这几个存储单位组成的整体呢?如果你是编译器,你会怎么做?呃,取其第一个存储单位的值来代表会比较好点。没错,编译器是这么做的。 所以两者的值相同)

a+1 与 &a+1 的区别

数组名a除了在上述两种情况下,均用&a[0]来代替。(实际上编译器也是这么做的)
a+1即等同于&a[0]+1。

注意:指针(地址)与常数相加减,不是简单地算术运算,而是以当前指针指向的对象的存储长度为单位来计算的。
即:指向的地址+常数*(指向的对象的存储长度)

&a[0]为数组首元素的地址,故&a[0]+1 越过一个数组元素长度的位置。即:&a[0]+1*sizeof(a[0])
&a为整个数组的地址,(只是用首元素地址来表示,其实际代表的意义是整个数组)
故&a+1 越过整个数组长度的位置,到达数组a后面第一个位置。 即:&a+1*sizeof(a)

指针

定义与解引用

int *p = NULL; 与 *p = NULL ;
指针的定义与解引用都用到 * ,这是让人晕的一个地方。
(不妨这样理解:在定义时,星号只是表示这是一个指针,int * 表示这是一个int型的指针,把int * 放在一起看,表示这是一个整型指针类型。如果我是 C 的设计者,那么用$符号来定义指针类型 会不会让大家少些迷惑)

向指针变量赋值,右值必须是一个地址。例:int * p = &i ;
这样,编译器在变量表里查询变量 i 对应的地址,然后用地址值把 &i 替换掉。 那么我们能不能直接把地址值写出来作为右值呢?当然。指针不就是存储地址的变量嘛,直接把数字型的地址值赋给它有什么问题。(前提是这个地址值必须是程序可访问的)
例:
int * p = (int *)0x12ff7c ;
*p = 0x100 ;
这里的 0x12ff7c 可看做某个变量的地址。 需要注意的是:将地址 0x12ff7c 赋值给指针变量 p 的时候必须强制转换。(我们要保证赋值号两边的数据类型一致)

地址的强制转换

例:double * p ;假设p的值为 0x100000
求下列表达式的值:
p + 0x1 = ___
(unsigned long)p + 0x1 = ___
(unsigned int *)p + 0x1 = ___

注意:
一个指针与一个整数相加减。这个整数的单位不是字节,而是指针所指向的元素的实际存储大小

所以 p + 0x1,p 指向的是一个 double 型变量,故值应为:0x100000+0x1*8=0x100008

(unsigned long)p则意为:将表示地址值的 p 强制转换成无符号的长整型。(即:告诉编译器,以前变量p里存储的是内存中的某个地址,现在变量p里存储的是一个长整型。即让编译器看待变量 p 的眼光改变一下
,以后p是一个整型变量了,不是指针了,不要把它里面的值当做某个变量的地址了,不能根据这个地址去找某变量了。)
任何数值一旦被强制转换,其类型就变了。即编译器解释其值代表的含义就变了。
故:(unsignedlong)p + 0x1 是一个长整型值加一个整型值,结果为:0x100001

(unsigned int *)p则意为:将一个表示double型变量的地址值的指针,转换成一个表示unsigned int型变量地址的指针。
故(unsigned int*)p + 0x1 值为:0x100000+sizeof(unsignedint)*0x1 等于 0x100004
【强制转换指针类型的目的是为了:改变指针的步长(偏移的单位长度)】

注意:
两个指针直接相加是不允许的。(你要真想把两个地址值相加,把它们先都强制转换为int型即可)
两个指针直接相减在语法上是允许的。(但必须相对于同一个数组,结果是两指针指向位置相隔的元素个数)

指针表达式

注意:*与++优先级相同,且它们的结合性都是从右向左的。
例:char ch ;char *cp=&ch ;
指针表达式:
*++cp
先运算++cp,再解引用*。
当其为右值时,是ch下一个存储单元的值(是一个垃圾值)
当其为左值时,是ch的下一个存储单元

(*cp)++
当其为右值时,表达式的值等于ch的值,(但它使ch值自增1)
当其为左值时,非法。
【注意:++,--的表达式(及大部分的表达式,数组的后缀表达式除外)的值都只是一种映像(暂存于寄存器),不在内存区中,故无法得到它们的地址,它们也无法做左值】 ★故:(*cp)++表达式的值虽与ch相同,但它只是ch值的一份拷贝,不是真正的ch

++*cp++

当其为右值时:表达式的值等于ch+1,这个值只是一个映像(寄存器中)。(但这个表达式实际做了一些工作:使cp指向ch的下一个单元,使ch中的值增1)
当其为左值时,非法。

【++,--,与 * 组合的指针表达式只是把几个工作融合在一个表达式中完成,使代码简洁,但可读性差】
例:对于 *cp++ ; 我们可以把它分解为: *cp 之后再 cp++
对于 *++cp ; 我们可以把它分解为:++cp 之后再*cp

数组与指针的纠葛

在C语言中,根据定义,表达式 e1[e2] 准确地对应于表达式 *((e1)+(e2))。因此,要求表达式 e1[e2] 的其中一个操作数是指针,另一个操作数是整数。且这两个操作数的顺序可以颠倒。

故: a[4] 等同于 4[a] 等同于 *(a+4)

编译器把所有的e1[e2]表达式转换成 *((e1)+(e2))。 所以,以下标的形式访问在本质上与以指针的形式访问没有区别,只是写法上不同罢了!

多维数组

二维数组a[i][j]
编译器总是将二维数组看成是一个一维数组,而一维数组的每个元素又都是一个数组。

多维数组定义的下标从前到后可以看做是最宏观的维到最微观的维

例:三维数组 a[i][j][k] 可理解为 共有 i 个大组,每个大组里有 j 个小组,每个小组里有k个元素。
所以:
a 表示为整个三维数组,其值为 &a[0][0][0]
&a+1 为整个三维数组后面的第一个位置。(偏移整个三维数组的长度)
a+1 为第二个大组的首位置处(偏移一个大组的长度)
【数组名a代表的是数组首元素的首地址,即:第一个大组的首地址】

a[0] 表示为三维数组的 i 个大组中的第一个大组【可看做一个二维数组】,其值与 &a[0][0][0] 的值相同。
&a[0]+1 为第二个大组的首位置处(偏移一个大组的长度)
a[0]+1 为第一个大组中第二个小组的首位置处(a[0]可看做是一个二维数组名,故其代表的是第一个小组的首地址)(偏移一个小组的长度)

a[0][0] 表示为第一个大组中的第一个小组【可看做一个一维数组】,其值与 &a[0][0][0] 的值相同。
&a[0][0]+1 为第一个大组中第二个小组的首位置处(偏移一个小组的长度)
a[0][0]+1 为第一个大组中第一个小组的第二个元素位置处(偏移一个元素的长度)
a[0][0][0] 表示为第一个大组中的第一个小组中的第一个元素。其值为&a[0][0][0],a[0][0][0]+1为首元素值加1。(因为a[0][0][0]为元素值而不是地址)

数组的数组名(即:二维数组名)退化为数组的(常量)指针,而不是指针的指针。 同理, n 维数组名退化为 n-1 维数组的(常量)指针。
【总结:指针代表的是谁的首地址 就以谁的长度为偏移单位
。】
【规律:与定义比较,缺少几对方括号,就是几维数组的数组名,如上例:a缺少3对方括号,即为3维数组的数组名(代表的是2维数组的地址);a[0]缺少2对方括号,即为2维数组的数组名(代表的是1维数组的地址);a[0][0]缺少1对方括号,即为1维数组的数组名(代表的是数组元素的地址)】

【数组名与整数相加,首先要转换成数组的首元素地址与整数相加,而首元素的存储大小就是相加的单位】

对多维数组的解析

我们可以用上面那种从前到后的解析方式来思考,
a:就表示整个多维数组。
a[m]:就表示第m+1大组(大组即数组最大的维),
a[m][n]:就表示第m+1大组中的第n+1小组。(小组即次大的维),
以此类推,即多维数组的解析是层层细化的。

指针数组与数组指针

指针数组:首先它是一个数组。数组的元素都是指针。它是“存储指针的数组”的简称。
数组指针:首先它是一个指针。它指向一个数组。它是“指向数组的指针”的简称。

例:
int * p1[10]; //它是指针数组。(因为[]的优先级比*高,p1先与[]结合,构成一个数组的定义)
int (*p2)[10] ; //它是数组指针。(括号的优先级较高,*与p2构成一个指针的定义) 它指向一个包含10个int型数据的数组。
int (*p)[10][5] ; //则p指向一个int型的二维数组a[10][5]。
【规律:数组指针,把定义中括号内的指针看成是一个普通的字母,则其表示的就是 数组指针所指的对象类型】

int a[5][5] ;
int (*p)[4] ;
p=a ;
问:&p[4][2]-&a[4][2]的值为多少?

设二维数组的首地址为0,则a[4][2]为第5组的第3个位置,因为int a[5][5];即有5组,每组有5个元素。故:&a[4][2]是(4*5+2)*sizeof(int)。
int (*p)[4] ; 指针指向一个含4个int型的元素的数组,故p[4]相对于p[0]向后移动了“4个int型数组”的长度,然后在此基础上再向后移动2个int型的长度(即,其步长按维度逐步递减,多维数组也可按此方式理解)。最后其值为(4*4+2)* sizeof(int)
最后切记:地址值参与的加减运算(地址不能被乘),整数的单位是地址值代表的元素的存储大小!
&p[4][2]-&a[4][2]结果为-4。若分开比较&p[4][2]和&a[4][2]则相差4* sizeof(int)个字节。

数组参数与指针参数

1、二维数组名做实参

int main(void)
{
int a[4][5] ;
………
………
fun(a);
………
}
被调函数:
①fun( inta[4][5] )
②fun( inta[ ][5] )
③fun( int(*a)[5] )
{ ………
a[i][j]=………
………
}

以上三种方式皆可。无论是那种方式,它们只是写法不同,但编译器的处理方式相同,都把它们看做是一维数组指针。
因为二维数组名退化为一个一维数组指针,故是以一维数组指针的形式来传递二维数组的。

2、指针数组做实参

int main(void)
{
int a[4][5] , i, *p[4] ;
for(i=0;i<4; i++)
p[i]= a[i] ;
………
fun(p);
………
}
被调函数:
①fun(int*q[4])
②fun(int *q[])
③fun(int **q)
{ ………
q[i][j]=……… //取出指针数组中的第i个元素(为指针),再偏移j个单位
………
}

以上三种方式皆可。无论是那种方式,写法不同,但编译器的处理方式相同,都把它们看做是二级指针。
因为指针数组名退化为数组首元素的地址,即二级指针,故是以二级指针的形式来传递指针数组的。
而多维数组名退化为次维数组的指针,即数组指针,故是以数组指针的形式来传递多维数组的。
【C中函数实参与形参之间是传值引用的,所以你要改变实参的值,就传递它的地址】

函数指针

函数指针就是函数的指针。它是一个指针,指向一个函数。(即函数在内存中的起始位置地址)
实际上,所有的函数名在表达式和初始化中,总是隐式地退化为指针。

例:
int r , (*fp)( ) , func( ) ;
fp= func ; //函数名退化为指针
r= (*fp)( ) ; //等价于r=fp( ) ;
无论fp是函数名还是函数指针,都能正确工作。因为函数总是通过指针进行调用的!

--

例:
int f(int) ; //函数声明
int (*fp)(int) = &f ;//此取地址符是可选的。编译器就把函数名当做函数的入口地址。
int ans ;
//以下三种方式可调用函数
ans= f(25) ;
ans= (*fp)(25) ;
ans= fp(25) ;
函数名就是一个函数指针常量,函数调用操作符(即一对括号)相当于解引用。
函数的执行过程:
函数名首先被转换为一个函数指针常量,该指针指定函数在内存中的位置。然后函数调用操作符调用该函数,执行开始于这个地址的代码。

强制类型转换

void fun() { printf("Call fun "); }
int main(void)
{
void(*p)( ) ;
*(int*)&p = (int)fun ;
(*p)() ;
return 0 ;
}

指针的强制类型转换只不过是改变了编译器对二进制位的解释方法罢了。
*(int *)&p = (int)fun ;中的fun是一个函数地址,被强制转换为int数字。 左边的(int *)&p是把函数指针p转换为int型指针。
*(int *)&p = (int)fun ;表示将函数的入口地址赋值给指针变量p。
(*p)( ) ;表示对函数的调用。

函数指针的用途

1,转移表(转移表就是一个函数指针数组)
即可用来实现“菜单驱动系统”。系统提示用户从菜单中选择一个选项,每个选项由不同的函数提供服务。
【若每个选项包含许多操作,用switch操作,会使程序变得很长,可读性差。这时可用转移表的方式】

例:
void(*f[3])(int) = {function1, function2, function3} ; //定义一个转移表
(*f[choice])( ) ; //根据用户的选择来调用相应的函数

2,回调函数
(用函数指针做形参,用户根据自己的环境写个简单的函数模块,传给回调函数,这样回调函数就能在不同的环境下运行了,提高了模块的复用性)
【回调函数实现与环境无关的核心操作,而把与环境有关的简单操作留给用户完成,在实际运行时回调函数通过函数指针调用用户的函数,这样其就能适应多种用户需求】

例:C库函数中的快速排序函数
void qsort(void *base, int nelem, size_t width, int (*fcmp)(void*, void*) );//fcmp为函数指针。
这样,由用户实现fcmp的比较功能(用户可根据需要,写整型值的比较、浮点值的比较,字符串的比较 等)这样qsort函数就能适应各种不同的类型值的排序。

使用函数指针的好处在于:
可以将实现同一功能的多个模块统一起来标识,这样一来更容易后期维护,系统结构更加清晰。或者归纳为:便于分层设计、利于系统抽象、降低耦合度以及使接口与实现分开。

指针的复杂声明

函数指针数组的指针

例:char *(*(*pf)[3])(char *)

这个指针指向一个数组,这个数组里存储的都是指向函数的指针。它们指向的是一种返回值为字符指针,参数为字符指针的函数。

解读复杂声明的方式

从外到内,层层剥开,先找核心,再向右看

找到核心变量后,从右向左读。
* 读作”指向…的指针”
[] 读作”…的数组”
() 读作”返回…的函数”

简单的例子:
int *f() ; // f: 返回指向int型的指针
步骤:
1)找标识符f:读作”f是…”
2)向右看,发现”()”读作”f是返回…的函数”
3)向右看没有什么,向左看,发现*,读作”f是返回指向…的指针的函数”
4)继续向左看,发现int,读作”f是返回指向int型的指针的函数”

int (*pf)() ; // pf是一个指针——指向返回值为int型的函数
1)标识符pf,读作“pf是…”
2)向右看,发现),向左看,发现*,读作 “pf是指向…的指针” 3)向右看,发现”()”,读作“pf是指向返回…的函数的指针” 4)向右看,没有,向左看发现int,读作”pf是指向返回int型的函数的指针

复杂指针的举例: void (*b[10]) (void (*)());

首先找到核心:b是一个数组,这个数组有10个元素,每一个元素都是一个指针,指针指向一个函数,函数参数是“void(*)()”【 这个参数又是一个指针,指向一个函数,函数参数为空,返回值是“void”】 返回值是“void”。完毕!

使用typedef简化声明

“建立一个类型别名的方法很简单,在传统的变量声明表达式里用类型名替代变量名,然后把关键字typedef加在该语句的开头”。

举例:

1、
void (*b[10]) (void (*)());
typedef void (*pfv)(); //先把上式的后半部分用typedef换掉
typedef void (*pf_taking_pfv)(pfv); //再把前半部分用typedef换掉
pf_taking_pfv b[10]; //整个用typedef换掉

跟void (*b[10]) (void (*)());的效果一样!

2、
doube(*)() (*pa)[9];
typedef double(*PF)(); //先替换前半部分
typedef PF (*PA)[9]; //再替换后半部分
PA pa;

跟doube(*)() (*pa)[9];的效果一样!

指针的反思

1、我们为什么需要指针?
因为我们要访问一个对象,我们要改变一个对象。要访问一个对象,必须先知道它在哪,也就是它在内存中的地址。地址就是指针值。
所以我们有
函数指针:某块函数代码的起始位置(地址)
指针的指针:因为我要访问(或改变)某个变量,只是这个变量是指针罢了

2、为什么要有指针类型?
因为我们访问的对象一般占据多个字节,而代表它们的地址值只是其中最低字节的地址,我们要完整的访问对象,必须知道它们总共占据了多少字节。而指针类型即向我们提供这样的信息。
注意:一个指针变量向我们提供了三种信息:
①一个首字节的地址值(起始位置)
②这个指针的作用范围(步长)
③对这个范围中的数位的解释规则(解码规则)
【编译器就像一个以步数测量距离的盲人。故你要告诉它从哪开始走,走多少步,并且告诉他如何理解这里面的信息】

3、强制类型转换的真相?
学过汇编的人都知道,什么指针,什么char,int,double,什么数组指针,函数指针,指针的指针,在内存中都是一串二进制数罢了。只是我们赋予了这些二进制数不同的含义,给它们设定一些不同的解释规则,让它们代表不同的事物。
(比如1000 0000 0000 0001 是内存中某4个字节中的内容,如果我们认为它是int型,则按int型的规则解释它为-2^31+ 1;如果我们认为它是unsigned int ,则被解释为2^31+ 1;当然我们也可把它解释为一个地址值,数组的地址,函数的地址,指针的地址等)

如果我们使用汇编编程,我们必须根据上下文需要,用大脑记住这个值当前的代表含义,当程序中有很多这样的值时,我们必须分别记清它们当前代表的含义。这样极易导致误用,所以编译器出现了,让它来帮我们记住这些值当前表示的含义。
当我们想让某个值换一种解释的方案时,就用强制类型转换的方式来告诉编译器,编译器则修改解释它的规则,而内存中的二进制数位是不变的(涉及浮点型的强制转换除外,它们是舍掉一些位,保留一些位)。

5

类型转换

在 C 语言的表达式赋值时,要小心背后默默的隐式类型转换,它们会导致隐藏的 Bug。

C语言的整型提升规则

C的整型算数运算总是至少以缺省整型类型的精度来进行的。为了获得这个精度,表达式中的字符型和短整型操作数在使用之前被转换为普通整型。

例:
char a,b,c ;
a = b+c ;

注意,根据整型提升规则,上面b和c的值被提升为普通整型,然后再执行加法运算。加法运算的结果被截断再存储于a中。

在整数运算时,操作数是放到两个寄存器中进行的(32位计算机寄存器是32位 故字符型变量被提升为整型,计算的结果又被传回到内存中的字符型存储位置中故被截断)

【所以,最好把字符型定义为int型,尤其是涉及运算时】

例:
signed char a = 0xe0;
unsigned int b = a;
unsigned char c = a;

b的十六进制表示:
a 是一个 signed char,赋值给 unsigned int 的 b,并不是直接在前面若干个字节补 0。
而是,首先 signed char a 转换为 int(0xffffffe0),然后 int 转换成 unsigned int(0xffffffe0),所以最初是符号扩展,然后一个 int 赋值给了 unsigned int。

c的十六进制表示为0xe0,但 a == c 为假。
虽然 a 和 c 的二进制表示一模一样,都是 0xe0,但a == c中,a和c都要先转换成int型,再比较。a转int型为负数,b转int型为正数,故它俩不等。

对无符号类型的建议:

  • 尽量不要在你的代码中使用无符号类型,以免增加不必要的复杂性。
  • 尽量使用像int那样的有符号类型,这样在涉及升级混合类型的复杂细节时,不必担心边界情况。
  • 只有在使用位段和二进制掩码时,才使用无符号数。

在涉及数值运算时,还要注意溢出的情况。

例:
int a=5000 ;
int b=25 ;
long c=a*b ;

表达式a*b以整型进行计算,若在16位的计算机上,这个乘法可能会产生溢出!
故应该显式转换: long c =(long)a*b ;
【在进行算术运算时一定要警惕乘法加法的可能溢出,尤其注意赋值号两边的数据类型保持一致】

6

流是什么?形象的比喻——水流,文件
和程序
之间连接一个管道,水流就在之间形成了,自然也就出现了方向:可以流进,也可以流出。
便于理解,这么定义流: 流就是一个管道里面有流水,这个管道连接了文件和程序。

UNIX系统认为一切皆文件,所有的外部设备都被看做文件。

C语言系统定义了三个默认的文件指针:
1、stdin 即标准输入文件,与键盘连接。(即把键盘当做文件)
2、stdout 即标准输出文件,与屏幕连接。(即把屏幕当做文件)
3、stderr 即标准出错文件,与屏幕连接。

注意:
stdout 和 stderr 是不同的设备描述符。
stdout是块设备,stderr则不是。
对于块设备,只有当下面几种情况下才会被输入:1)遇到回车,2)缓冲区满,3)flush被调用。而stderr则不会。

例:
fprintf(stdout,"hello-std-out");//不一定会输出
fprintf(stderr,"hello-std-err");//一定会输出

流的性质

你只能顺序地访问并提取流中的数据,未提取的数据只能阻塞在流中,等待下次被访问或提取。
访问文件时,流指针会自动的改变当前访问内容的位置。即文件指针会随着读写的流向而移动,它不是一直指向文件头部的。

由于UNIX一切皆文件的思想,所以,所有操作文件的函数也都可以用来操作输入/输出设备。

第 6 章 输入

从输入流中读取数据时,停留在流中的字符可能会影响到下次的正确读入。(这往往会造成隐藏的Bug)

丢弃输入流中字符的方法:

  • 输入结束后,把流中剩余的垃圾字符手动读掉

while( (ch = getchar())!=EOF && ch!= ‘\n’) NULL ;

  • 用函数fflush清除一个流

例:fflush(stdin);

fgets

从文件中读取字符串
char * fgets (char *string, int n, FILE *fp) ;
功能:
从文件 fp 中读取 n-1 个字符放入以 string 为首地址的空间里。读入结束后,系统将自动在最后加 ’\0’,并以 string 作为函数值返回。

gets( )和fgets( )不同,gets( )会丢弃换行符,并不把它存储在缓冲数组中。
但gets( )对于输入长度没有限制,很可能导致输入长度超过缓冲数组的长度,导致缓冲区溢出

【我们一般用fgets( )函数来接收用户输入。(这样可允许用户输入任意字符)再在程序中分析用户输入,提取数据】

fgetc

fgetc( ) 接受一个输入流作为参数,它从这个流中读取一个字符(可读入回车等空白符),如果发生错误或流已到结尾,则返回 EOF。
注意:其返回值是读入的字符的 ASCII 码值,是 int 型。

小心下面代码中的隐式转换的Bug

char ch ;
While( (ch=fgetc(stdin)) != EOF ) …..//错误!

fgetc() 返回一个整型值而不是字符值,若把 fgetc 返回值存储于 ch 中,将导致它被截断!然后这个被截断的值被提升为整形并与 EOF 比较,循环会出错。
【用整形来定义一个字符变量更好!字符就是一个小整数】

scanf

scanf系列函数
int scanf( char const *format, …… ) ;
int fscanf( FILE *fp, char const *format, …… ) ;
int sscanf( char const *string, char const *format, ……) ;
//以上函数的读入处理规则都相同,不同的是它们读取的源不同,一个是从键盘读取、一个从文件流读取、一个从字符串读取。(注意:字符串不是流,其没有流指针保存读取位置)

扫描集

一个字符序列可以用一个扫描集(scan set)来输入。
扫描集是位于格式控制字符串中,以百分号开头、用方括号[]括起来的一组字符.

检查与扫描集中的字符相匹配的字符。一旦找到匹配的字符,那么这个字符将被存储到扫描集对应的实参(即指向一个字符数组的指针)中。只有遇到扫描集中没有包含的字符时,扫描集才会停止输入字符。

如果输入流中的第一个字符就不能与扫描集中包含的字符相匹配,那么只有空操作符被存储到字符数组中。
(如果输入的字符属于方括号内字符串中某个字符,那么就提取该字符;如果一经发现不属于就结束提取。该方法会自动加上一个'\0'到已经提取的字符后面。)

例:
char str[512] ;
printf(“Enter string:\n”) ;
scanf(“%[aeiou]”, str) ;
//程序使用扫描集[aeiou]在输入流中寻找元音字符,直到遇到非元音字符。

我们还可以用缩写a-z表示abcd….xyz字母集。
scanf(“%[a-z]”, str) ;
同理,也可以用缩写0-9 缩写A-Z。
想只取字母,那就可以写成 %[A-Za-z]

对于字符串"abDEc123"如果想按照字母和数字读到两个字符串中就应该是 "%[a-zA-Z]%[0-9]",buf1,buf2 ;

逆向扫描集

逆向扫描集还可以用来扫描那些没有出现在扫描集中的字符

创建一个逆向扫描集的方法是,在方括号内扫描字符前面加一个“脱字符号”(^)。这个符号将使得那些没有出现在扫描集中的字符被保存起来。只有遇到了逆向扫描集中包含的字符时,输入才会停止。(即取其后字符们的补集作为扫描集)

scanf(“%[^aeiou]”, str) ;
//即接受输入流中的非元音字符。

用这种方法还可以解决scanf的输入中不能有空格的问题。只要用 scanf("%[^\n]",str); 就可以了。很神奇吧。

【注意】
[]内的字符串可以是1或更多字符组成。空字符集(%[])是违反规定的,可导致不可预知的结果。%[^]也是违反规定的。

指定域宽

我们可以在scanf函数的转换说明符中指定域宽来从输入流中读取特定数目的字符。

例:
scanf(“%2d%d”, &x, &y) ;
程序从输入流中读取一系列连续的数字,然后,将其前两位数字处理为一个两位的整数,将剩余的数字处理成另外一个整数。

赋值抑制字符

即星号 * 。
赋值抑制字符使得 scanf 函数从输入流中读取任意类型的数据,并将其丢弃,而不是将其赋值给一个变量。如果你想忽略掉某个输入
,使用在% 后使用 * 。

%* [^=] 前面带 * 号表示不保存变量。跳过符合条件的字符串。

char s[]="notepad=1.0.0.1001";
char szfilename [32] = "" ;
int i = sscanf( s, "%* [^=]", szfilename ) ;
// szfilename=NULL,因为没保存
int i =sscanf( s, "%* [^=]=%s", szfilename ) ;
// szfilename=1.0.0.1001

所有对%s起作用的控制,都可以用于%[]
比如"%* [^\n]%*c"就表示跳过一行,"%-20[^\n]"就表示读取\n前20个字符。

把扫描集、赋值抑制符和域宽等综合使用,可实现简单的正则表达式那样的分析字符串的功能。

返回值

scanf的返回值是读入数据的个数;
比如scanf("%d%d",&a,&b);读入一个返回1,读入2个返回2,读入0个返回0;读入错误返回EOF即-1

安全性

你应该非常小心的使用 scanf 因为它可能会是你的输入缓冲溢出!
通常你应该使用 fgets 和 sscanf 而不是仅仅使用 scanf,使用fgets 来读取一行,然后用 sscanf 来解析这一行,就像上面演示的一样。

7

输出

fputs

用fputs()把一个字符串写入到文件中
int fputs (char *string, FILE *fp) ;
功能:把字符串string写入到文件fp中
若fp为stdout,则为向屏幕输出。

printf

int printf ( char const *format, …… ) ;
int fprintf ( FILE *fp, char const *format, …… ) ;
int sprintf ( char const *string, char const *format, ……) ;
//以上函数的输出字符规则都相同,不同的是它们写入的流不同,一个是向屏幕写入、一个向文件流写入、一个向字符串写入。

带域宽和精度的打印

printf函数允许你为欲打印的数据指定精度
。对于不同类型的数据而言,精度的含义是不一样的。

精度与整型转换说明符%d
一起使用时,表示要打印的数据的最少数字位数。如果将要打印的数据所包含的数字的位数小于指定的精度,同时精度值前面带有一个0或者一个小数点,则加填充0。

精度与浮点型转换说明符%f
一起使用时,表示将要打印的最大有效数字位数。

精度与字符串转换说明符%s
一起使用时,表示将要从一个字符串中打印出来的最大字符个数。(可用于控制打出的字符的个数) 表示精度的方法是:在百分号和转换说明符之间,插入一个表示精度的整数,并在整数的前面加上一个小数点。

域宽和精度可以放在一起使用,方法是:在百分号和转换说明符之间,先写上域宽,然后加上一个小数点,后面再写上精度。

例:
printf(“%9.3f”, 123.456789) ;
的输出结果是123.456

还可以用变量来控制域宽和精度(可用于关于精度的舍入)
在格式控制字符串中表示域宽或精度的位置上写上一个星号 * ,然后程序将会计算实参列表中相对应的整型实参值,并用其替换星号。

例:
printf(“%* . *f”, 7, 2, 98.736) ; 将以7为域宽,2为精度,输出右对齐的98.74
表示域宽的值可以是正数,也可以是负数(将导致输出结果在域宽内左对齐)

使用标记

printf函数还提供了一些标记来增加它的输出格式控制功能,在格式控制字符串中可以使用的标记有:

  • -(减号)
    在域宽内左对齐显示输出结果
  • +(加号)
    在正数前面显示一个加号,在负数前面显示一个减号
  • 空格
    在不带加号标记的正数前面打印一个空格
  • #(井号)
    当使用的是八进制转换说明符o时,在输出数据前面加上前缀0 ; 当使用的是十六进制转换说明符x或X时,在输出数据前面加上前缀0x或0X
  • 0(零)
    在打印的数据前面加上前导0

逆向打印参数(POSIX扩展语法)

printf("%4$d %3$d %2$d %1$d", 1, 2, 3, 9); //将会打印9 3 2 1

返回值

printf 返回值是输出的字符个数。

#include <stdio.h>
 
int main()
{
int i=43;
printf("%d\n",printf("%d",printf("%d",i)));
return 0;
}//程序会输出4321

第 7 章 IO缓冲问题

在进行输入/输出时,程序并不是马上从输入/输出设备处理数据,而是先把数据放到缓存中,当缓存满时才进行输入/输出操作(或是遇到刷新操作,比如遇到换行或 fflush )。

C 实现通常允许程序员设置流的缓存大小。

void setbuf (FILE * steam, char * buf) ;
参数 buf 必须指向一个长度为 BUFSIZ (定义在 stdio.h 头文件中)的缓冲区。

例:
setbuf (stdout, buf) ;
将告诉 I/O 库写入到 stdout 中的输出要以 buf 作为一个输出缓冲,并且等到 buf 满了或程序员直接调用 fflush() 再实际写出。

将buf参数设置为NULL,可关闭缓冲。

注意缓存的生命期问题

例:
下面的程序解释了通过使用 setbuf() 来把标准输入复制到标准输出:

#include <stdio.h>
 
int main()
{
int c;
char buf[BUFSIZ];
setbuf(stdout, buf);
while((c = getchar()) != EOF)
putchar(c);
return 0 ;
}

这段程序隐藏着一个细微的Bug:
缓冲区最后一次刷新是在主程序完成之后,库将控制交回到操作系统之前所执行的清理的一部分。在这一时刻,缓冲区已经被释放了!(即main函数栈清空之后)

有两种方法可以避免这一问题:

  1. 使用静态缓冲区,或者将其显式地声明为静态
    static char buf[BUFSIZ];
    或者将整个声明移到主函数之外。
  2. 动态地分配缓冲区并且从不释放它
    setbuf (stdout, malloc(BUFSIZ));
    注意在后一种情况中,不必检查malloc()的返回值,因为如果它失败了,会返回一个空指针。而setbuf()可以接受一个空指针作为其第二个参数,这将使得stdout变成非缓冲的。这会运行得很慢,但它是可以运行的。

8

那些不安全的库函数

C 和 C++ 不能够自动地做边界检查,边界检查的代价是效率。一般来讲,C 在大多数情况下注重效率。然而,获得效率的代价是,C 程序员必须十分警觉以避免缓冲区溢出问题。

C语言标准库中的许多字符串处理和IO流读取函数是导致缓冲区溢出的罪魁祸首。我们有必要了解这些函数,在编程中多加小心。

一、字符串处理函数

strcpy()

strcpy() 函数将源字符串复制到缓冲区。没有指定要复制字符的具体数目!如果源字符串碰巧来自用户输入,且没有专门限制其大小,则有可能会造成缓冲区溢出!

我们也可以使用strncpy来完成同样的目的:
strncpy (dst, src, dst_size-1);
如果 src 比 dst 大,则该函数不会抛出一个错误;当达到最大尺寸时,它只是停止复制字符。注意上面调用 strncpy() 中的 -1。如果 src 比 dst 长,则那给我们留有空间,将一个空字符放在 dst 数组的末尾。
但是! strncpy()也不完全安全,也有可能把事情搞糟。即使“安全”的调用有时会留下未终止的字符串,或者会发生微妙的相差一位错误。

确保 strcpy() 不会溢出的另一种方式是,在需要它时就分配空间,确保通过在源字符串上调用 strlen() 来分配足够的空间

dst = (char *)malloc(strlen(src));
strcpy(dst, src);

strcat()

strcat() 函数非常类似于 strcpy(),除了它可以将一个字符串合并到缓冲区末尾。它也有一个类似的、更安全的替代方法 strncat()。如果可能,使用 strncat() 而不要使用 strcat()。

sprintf()、vsprintf

函数 sprintf() 和 vsprintf() 是用来格式化文本和将其存入缓冲区的通用函数。它们可以用直接的方式模仿 strcpy() 行为。换句话说,使用 sprintf() 和 vsprintf() 与使用 strcpy() 一样,都很容易对程序造成缓冲区溢出。

sprintf() 的许多版本带有使用这种函数的更安全的方法。可以指定格式字符串本身每个自变量的精度。sprintf 采用” * ”来占用一个本来需要一个指定宽度或精度的常数数字的位置,而实际的宽度或精度就可以和其它被打印的变量一样被提供出来。

例如:
sprintf (usage, "USAGE: %*s\n", BUF_SIZE, argv[0]);

二、字符读取函数

gets()

永远不要使用 gets()。
该函数从标准输入读入用户输入的一行文本,它在遇到 EOF 字符或换行字符之前,不会停止读入文本。也就是:gets() 根本不执行边界检查。因此,使用 gets() 总是有可能使任何缓冲区溢出。

作为一个替代方法,可以使用方法 fgets()。它可以做与 gets() 所做的同样的事情,但它接受用来限制读入字符数目的大小参数,因此,提供了一种防止缓冲区溢出的方法。

getchar()、fgetc()、getc()、read()

如果在循环中使用这些函数,确保检查缓冲区边界

scanf()系列 : sscanf()、fscanf()、vfscanf()、vscanf()、vsscanf()

scanf系列的函数也设计得很差。目的地缓冲区也可能会发生溢出。
同样地,我们用设置宽度也可以解决这个问题。

getenv()

使用系统调用 getenv() 的最大问题是您从来不能假定特殊环境变量是任何特定长度的。

三、使用安全版本的代码库

微软对于有缓冲溢出危险的API使用其开发的安全版本的库来替代。 SafeCRT自Visual Studio 2005起开始支持。当代码中使用了禁用的危险的CRT函数,Visual Studio 2005编译时会报告相应警告信息,以提醒开发人员考虑将其替代为Safe CRT中更为安全。

  1. 有关字符串拷贝的API

例如:strcpy, wcscpy等

替代的Safe CRT函数:strcpy_s

  1. 有关字符串合并的API

例如:strcat, wcscat等

替代的Safe CRT函数:strcat_s

  1. 有关sprintf的API

例如:sprintf, swprintf等

替代的Safe CRT函数:

_snprintf_s

_snwprintf_s

其它被禁用的API还有scanf, strtok, gets, itoa等等。 ”n”系列的字符串处理函数,例如strncpy等,也在被禁用之列。

9

预处理

下面介绍一些 C/C++ 中几个不常见却有用的预编译和宏定义。

# error

语法格式如下:
#error token-sequence 其主要的作用是在编译的时候输出编译错误信息token-sequence,从方便程序员检查程序中出现的错误。

例:
#include "stdio.h"
 
int main(int argc, char* argv[])
{
#define CONST_NAME1 "CONST_NAME1"
 
printf("%s\n",CONST_NAME1);
#undef CONST_NAME1
 
#ifndef CONST_NAME1
 
#error No defined Constant Symbol CONST_NAME1
#endif
 
{
#define CONST_NAME2 "CONST_NAME2"
printf("%s\n",CONST_NAME2);
}
printf("%s\n",CONST_NAME2);
return 0;
}

在编译的时候输出如编译信息
fatal error C1189: #error : No definedConstant Symbol CONST_NAME1

#pragma

其语法格式如下:
# pragma token-sequence
此指令的作用是触发所定义的动作。如果token-sequence存在,则触发相应的动作,否则忽略。此指令一般为编译系统所使用。例如在Visual C++.Net 中利用# pragma once 防止同一代码被包含多次。

#line

此命令主要是为强制编译器按指定的行号,开始对源程序的代码重新编号,在调试的时候,可以按此规定输出错误代码的准确位置。

  • 形式1 # line constant “filename”
    其作用是使得其后的源代码从指定的行号constant重新开始编号,并将当前文件的名命名为filename。

例:
#include "stdio.h"
 
void Test();
#line 10 "Hello.c"
 
int main(int argc, char* argv[])
{
#define CONST_NAME1 "CONST_NAME1"
printf("%s\n",CONST_NAME1);
#undef CONST_NAME1
printf("%s\n",CONST_NAME1);
{
#define CONST_NAME2 "CONST_NAME2"
printf("%s\n",CONST_NAME2);
}
printf("%s\n",CONST_NAME2);
return 0;
}
 
void Test()
{
printf("%s\n",CONST_NAME2);
}

提示如下的编译信息:
Hello.c(15) : error C2065: 'CONST_NAME1' :undeclared identifier
表示当前文件的名称被认为是Hello.c, #line 10 "Hello.c"所在的行被认为是第10行,因此提示第15行出错。

  • 形式2 # line constant
    其作用在于编译的时候,准确输出出错代码所在的位置(行号),而在源程序中并不出现行号,从而方便程序员准确定位。

运算符#和##

在ANSI C中为预编译指令定义了两个运算符——#和##。

# 的作用是实现文本替换(字符串化)

例:
#define HI(x) printf("Hi,"#x"\n");
 
void main()
{
HI(John);
}

程序的运行结果:
Hi,John
在预编译处理的时候, #x的作用是将x替换为所代表的字符序列。(即把x宏变量字符串化)在本程序中x为John,所以构建新串“Hi,John”。

##的作用是串连接

例:
#define CONNECT(x,y) x##y
 
void main()
{
int a1,a2,a3;
CONNECT(a,1)=0;
CONNECT(a,2)=12;
a3=4;
printf("a1=%d\ta2=%d\ta3=%d",a1,a2,a3);
}

程序的运行结果为:
a1=0 a2=12 a3=4
在编译之前, CONNECT(a,1)被翻译为a1, CONNECT(a,2)被翻译为a2。

第 9 章 预定义的宏

标准C的预处理器定义了一些宏,这些宏的名称都是以两个下划线字符开始
和结束的。程序员不能取消这些预定义宏的定义或对它们进行重新定义。
几个常用的预定义宏:

__LINE__ 当前源程序行的行号,用十进制整数常量表示
__FILE__ 当前源文件的名称,用字符串常量表示
__DATA__ 编译时的日期,用“Mmm dd yyyy”形式的字符串常量表示
__TIME__ 编译时的时间,用“hh:mm:ss”形式的字符串常量表示。

(注意,前后是各两个下划线)

非局部跳转

在 C 中,goto 语句是不能跨越函数的,而执行这类跳转功能的是 setjmp 和 longjmp 宏
。这两个宏对于处理发生在深层嵌套函数调用中的出错情况是非常有用的。
此即为:非局部跳转。非局部指的是,这不是由普通 C 语言 goto 语句在一个函数内实施的跳转,而是在栈上跳过若干调用帧,返回到当前函数调用路径的某个函数中。

#include <setjmp.h>
 
int setjmp (jmp_buf env) ; /*设置调转点*/
void longjmp (jmp_buf env, int val) ; /*跳转*/

setjmp 参数 env 的类型是一个特殊类型 jmp_buf。这一数据类型是某种形式的数组,其中存放 在调用 longjmp 时能用来恢复栈状态的所有信息。因为需在另一个函数中引用 env 变量,所以应该将 env 变量定义为全局变量。
longjmp 参数 val,它将成为从 setjmp 处返回的值。

#include <stdio.h>
 
#include <setjmp.h>
 
 
static jmp_buf buf;
 
void second(void)
{
printf("second\n");
longjmp(buf,1);
// 跳回setjmp的调用处使得setjmp返回值为1
}
 
void first(void)
{
second();
printf("first\n");
// 不可能执行到此行
}
 
int main()
{
if (!setjmp(buf))
{
// 进入此行前,setjmp返回0
first();
}
else
{
// 当longjmp跳转回,setjmp返回1,因此进入此行
printf("main\n");
}
 
return 0;
}

直接调用 setjmp 时,返回值为 0,这一般用于初始化(设置跳转点时)。以后再调用 longjmp 宏时用 env 变量进行跳转。程序会自动跳转到 setjmp 宏的返回语句处,此时 setjmp 的返回值为非 0,由 longjmp 的第二个参数指定。
一般地,宏 setjmp 和 longjmp 是成对使用的,这样程序流程可以从一个深层嵌套的函数中返回。

变长参数列表

\<stdarg.h> 头文件定义了一些宏,当函数参数未知时去获取函数的参数 变量:typedef va_list

宏:
va_start()
va_arg()
va_end()

va_list 类型通过 stdarg 宏定义来访问一个函数的参数表,参数列表的末尾会用省略号省略
( va_list 用来保存 va_start , va_end 所需信息的一种类型。为了访问变长参数列表中的参数,必须声明 va_list 类型的一个对象 )

我们通过初始化( va_start )类型为 va_list 的参数表指针,并通过 va_arg 来获取下一个参数

//求任意个整数的最大值
#include <stdio.h>
 
#include <stdarg.h>
 
 
int maxint(int n, ...) /* 参数数量由非变长参数n直接指定 */
{
va_list ap;
int i, arg, max;
 
va_start(ap, n); /* ap为参数指针,首先将其初始化为最后一个具名参数, 以便va_arg获取下一个省略号内参数 */
for (i = 0; i < n; i++) {
arg = va_arg(ap, int); /* 类型固定为int, 按照给定类型返回下一个参数 */
if (i == 0)
max = arg;
else {
if (arg > max)
max = arg;
}
}
va_end(ap);
return max;
}
 
void main()
{
printf("max = %d\n", maxint(5, 2, 6, 8, 11, 7));
}

可变长数组

历史上,C语言只支持在编译时就能确定大小的数组。程序员需要变长数组时,不得不用malloc或calloc这样的函数为这些数组分配存储空间,且涉及到多维数组时,不得不显示地编码,用行优先索引将多维数组映射到一维的数组。
ISO C99引入了一种能力,允许数组的维度是表达式,在数组被分配的时候才计算出来。

#include <stdio.h>
 
 
int main(void)
{
int n, i ;
 
scanf("%d", &n) ;
 
int array[n] ;
for (; i<n; i++)
{
array[i] = i ;
}
 
for (i=0; i<n; i++)
{
printf("%d,", array[i]) ;
}
 
return 0;
}

注意:
如果你需要有着变长大小的临时存储,并且其生命周期在变量内部时,可考虑VLA(Variable Length Array,变长数组)。但这有个限制:每个函数的空间不能超过数百字节。因为 C99 指出边长数组能自动存储,它们像其他自动变量一样受限于同一作用域。即便标准未明确规定,VLA 的实现都是把内存数据放到栈中。VLA 的最大长度为 SIZE_MAX 字节。考虑到目标平台的栈大小,我们必须更加谨慎小心,以保证程序不会面临栈溢出、下个内存段的数据损坏的尴尬局面。

switch语句中的case

case支持范围取值(gcc扩展特性)

#include <stdio.h>
 
 
int main(void)
{
int i=0;
scanf("%d", &i) ;
 
switch(i)
{
case 1 ... 9: putchar("0123456789"[i]);
case 'A' ... 'Z': //do something
}
 
return 0;
}

case 关键词可以放在if-else或者是循环当中

switch (a)
{
case 1: ;
// ...
if (b==2)
{
case 2:;
// ...
}
else case 3:
{
// ...
for (b=0;b<10;b++)
{
case 5: ;
// ...
}
}
break;
 
case 4:
}

指定初始化(C99)

在C99之前,你只能按顺序初始化一个结构体。在C99中你可以这样做:

struct Foo {
int x;
int y;
int z;
};
Foo foo = {.z = 3, .x = 5};

这段代码首先初始化了foo.z,然后初始化了foo.x. foo.y 没有被初始化,所以被置为0。
这一语法同样可以被用在数组中。以下三行代码是等价的:

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

受限指针(C99)

关键字 restrict 仅对指针有用,修饰指针,表明要修改这个指针所指向的数据区的内容,仅能通过该指针来实现,此关键字的作用是使编译器优化代码,生成更高效的汇编代码。

int foo (int* x, int* y)
{
*x = 0;
*y = 1;
return *x;
}

很显然函数foo()的返回值是0,除非参数x和y的值相同。可以想象,99%的情况下该函数都会返回0而不是1。然而编译起必须保证生成100%正确的代码,因此,编译器不能将原有代码替换成下面的更优版本:

int f (int* x, int* y)
{
*x = 0;
*y = 1;
return 0;
}

现在我们有了 restrict 这个关键字,就可以利用它来帮助编译器安全的进行代码优化了,由于指针 x 是修改 * x的唯一途径,编译起可以确认 “* y=1; ”这行代码不会修改 * x的内容,因此可以安全的优化。

int f (int *restrict x, int *restrict y)
{
*x = 0;
*y = 1;
return 0;
}

很多C的库函数中用restrict关键字:
void * memcpy( void * restrict dest ,const void * restrict src,sizi_t n)
这是一个很有用的内存复制函数,由于两个参数都加了 restrict 限定,所以两块区域不能重叠,即 dest 指针所指的区域,不能让别的指针来修改,即 src 的指针不能修改. 相对应的别一个函数 memmove(void *dest,const void * src,size_t)则可以重叠。

静态数组索引(C99)

void f(int a[static 10]) {
/* ... */
}

你向编译器保证,你传递给 f 的指针指向一个具有至少10个 int 类型元素的数组的首个元素。我猜这也是为了优化;例如,编译器将会假定 a 非空。编译器还会在你尝试要将一个可以被静态确定为 null 的指针传入或是一个数组太小的时候发出警告。

void f(int a[const]) {
/* ... */
}

你不能修改指针 a.,这和说明符 int * const a.作用是一样的。然而,当你结合上一段中提到的 static 使用,比如在int a[static const 10] 中,你可以获得一些使用指针风格无法得到的东西。

多字符常量

int x = 'ABCD' ;

这会把 x 的值设置为 0×41424344(或者0×44434241,取决于大小端)我们一般的小端机上,低位存在低字节处,DCBA 依次从低字节到高字节排列。
这只是一种看起来比较炫酷的写法,一般没什么用。

jk_book.png

jk_weixin.png

更多信息请访问 book_view.png

http://wiki.jikexueyuan.com/project/c-advance/

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论