C语言的角落——C之非常用特性(一)

本文搜集整理了一些之前博客中没有提到的,C语言不常用的特性,算是对C系列的最后一次补充。

对C语言有兴趣的朋友可以浏览一下,查漏补缺。


变长参数列表


头文件定义了一些宏,当函数参数未知时去获取函数的参数

变量: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 
#include 

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这样的函数为这些数组分配存储空间,且涉及到多维数组时,不得不显示地编码,用行优先索引将多维数组映射到一维的数组。

ISOC99引入了一种能力,允许数组的维度是表达式,在数组被分配的时候才计算出来

#include 

int 
main(void)
{
    int n, i ;

    scanf("%d", &n) ; 

    int array[n] ; 
    for (; i

注意:

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


 case支持范围取值(gcc扩展特性) MinGW编译通过

#include 

int  main(void)
{
    int i=0; 
    scanf("%d", &i) ;

    switch(i)
    {
     case 1 ... 9: putchar("0123456789"[i]);   
     case 'A' ... 'Z':    //do something
     } 

     return 0;
}


非局部跳转setjmp和longjmp


在C中,goto语句是不能跨越函数的,而执行这类跳转功能的是setjmp和longjmp。这两个对于处理发生在深层嵌套函数调用中的出错情况是非常有用的。

此即为:非局部跳转。非局部指的是,这不是由普通C语言goto语句在一个函数内实施的跳转,而是在栈上跳过若干调用帧,返回到当前函数调用路径的某个函数中。

 

#include

int  setjmp(jmp_buf env) ;  /*设置调转点*/

void longjmp(jmp_bufenv,  int val) ;  /*跳转*/

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

longjmp参数val,它将成为从setjmp处返回的值。(很神奇吧。setjmp根据返回值可知道是哪个longjmp返回来的)


#include 
#include 

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是成对使用的,这样程序流程可以从一个深层嵌套的函数中返回。

 

 

volatile属性


如果你有一个自动变量,而又不想它被编译器优化进寄存器,则可定义其为有volatile属性。这样,就明确地把这个值放在存储器中,而不会被优化进寄存器。

 

setjmp会保存当前栈状态信息,也会保存此时寄存器中的值。(longjmp会回滚寄存器中的值)

 

如果要编写一个使用非局部跳转的可移植程序,则必须使用volatile属性

 

· IO缓冲问题


缓冲输出和内存分配 

    当一个程序产生输出时,能够立即看到它有多重要?这取决于程序。  

    例如,终端上显示输出并要求人们坐在终端前面回答一个问题,人们能够看到输出以知道该输入什么就显得至关重要了。另一方面,如果输出到一个文件中,并最终被发送到一个行式打印机,只有所有的输出最终能够到达那里是重要的。  

    立即安排输出的显示通常比将其暂时保存在一大块一起输出要昂贵得多。因此,C实现通常允许程序员控制产生多少输出后在实际地写出它们。  

    这个控制通常约定为一个称为setbuf()的库函数。如果buf是一个具有适当大小的字符数组,则  

setbuf(stdout, buf);  

将告诉I/O库写入到stdout中的输出要以buf作为一个输出缓冲,并且等到buf满了或程序员直接调用fflush()再实际写出。缓冲区的合适的大小在中定义为BUFSIZ。  

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

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


    不幸的是,这个程序是错误的,因为一个细微的原因。  

    要知道毛病出在哪,我们需要知道缓冲区最后一次刷新是在什么时候。答案:主程序完成之后,库将控制交回到操作系统之前所执行的清理的一部分。在这一时刻,缓冲区已经被释放了!  (即main函数栈清空之后)

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

    首先,使用静态缓冲区,或者将其显式地声明为静态:  

static char buf[BUFSIZ];  

或者将整个声明移到主函数之外。  

    另一种可能的方法是动态地分配缓冲区并且从不释放它:  

char *malloc(); 

setbuf(stdout, malloc(BUFSIZ));  

注意在后一种情况中,不必检查malloc()的返回值,因为如果它失败了,会返回一个空指针。而setbuf()可以接受一个空指针作为其第二个参数,这将使得stdout变成非缓冲的。这会运行得很慢,但它是可以运行的。

 

 

预编译和宏定义


C/C++中几个罕见却有用的预编译和宏定义

 

1:# 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


2:#pragma

其语法格式如下:

# pragma token-sequence

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

 

3:#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

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

 

4:运算符#和##

在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。

 

标准IO的妙用


//指定精确位数
#include 

int main(void)
{
	int m  ; //精确位数
	double input ; //用户输入小数
	
	puts("请输入一个小数:") ;
	scanf("%lf",&input) ; 
	puts("请输入精确到小数点后位数") ;
	scanf("%d" ,&m) ;
	
	puts("结果为");
	printf("%.*lf" ,m,input) ;
	
	return 0 ;
}

 

打印printf:

每一个printf函数的调用都返回一个值——要么是输出字符的个数,要么输出一个负数表示发生输出错误。

 

带域宽和精度的打印

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

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

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

精度与字符串转换说明符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

 

格式化输入scanf

扫描集(实用)

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

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

如果输入流中的第一个字符就不能与扫描集中包含的字符相匹配,那么只有空操作符被存储到字符数组中。

(如果输入的字符属于方括号内字符串中某个字符,那么就提取该字符;如果一经发现不属于就结束提取。该方法会自动加上一个'\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 来解析这一行,就像上面演示的一样。

 

数据类型对应字节数


程序运行平台
      不同的平台上对不同数据类型分配的字节数是不同的。
      个人对平台的理解是CPU+OS+Compiler,是因为: 
      1、64位机器也可以装32位系统(x64装XP); 
      2、32位机器上可以有16/32位的编译器(XP上有tc是16位的,其他常见的是32位的); 
      3、即使是32位的编译器也可以弄出64位的integer来(int64)。 
      以上这些是基于常见的wintel平台,加上我们可能很少机会接触的其它平台(其它的CPU和OS),所以个人认为所谓平台的概念是三者的组合。 
      虽然三者的长度可以不一样,但显然相互配合(即长度相等,32位的CPU+32位的OS+32位的Compiler)发挥的能量最大。 
      理论上来讲 我觉得数据类型的字节数应该是由CPU决定的,但是实际上主要由编译器决定(占多少位由编译器在编译期间说了算)。

常用数据类型对应字节数可用如sizeof(char),sizeof(char*)等得出


 32位编译器:

      char :1个字节
      char*(即指针变量): 4个字节(32位的寻址空间是2^32, 即32个bit,也就是4个字节。同理64位编译器)
      short int : 2个字节
      int:  4个字节
      unsigned int : 4个字节
      float:  4个字节
      double:   8个字节
      long:   4个字节
      long long:  8个字节
      unsigned long:  4个字节

  64位编译器:

      char :1个字节
      char*(即指针变量): 8个字节
      short int : 2个字节
      int:  4个字节
      unsigned int : 4个字节
      float:  4个字节
      double:   8个字节
      long:   8个字节
      long long:  8个字节
      
unsigned long:  8个字节









你可能感兴趣的:(C,系列,C之精华全记录)