首页 > 代码库 > C语言的角落——C之很常使用特性(一)

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

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

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


变长參数列表


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

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

#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字节。考虑到目标平台的栈大小,我们必须更加慎重小心,以保证程序不会面临栈溢出、下个内存段的数据损坏的尴尬局面。


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

#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;
}


非局部跳转setjmp和longjmp


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

此即为:非局部跳转。

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

 

#include <setjmp.h>

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

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

setjmp參数env的类型是一个特殊类型jmp_buf。

这一数据类型是某种形式的数组,当中存放 在调用longjmp时能用来恢复栈状态的全部信息。

由于需在还有一个函数中引用env变量。所以应该将env变量定义为全局变量。

longjmp參数val,它将成为从setjmp处返回的值。(非常奇妙吧。setjmp依据返回值可知道是哪个longjmp返回来的)


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

 

 

volatile属性


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

 

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

(longjmp会回滚寄存器中的值)

 

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

 

· IO缓冲问题


缓冲输出和内存分配 

    当一个程序产生输出时,可以马上看到它有多重要?这取决于程序。  

    比如,终端上显示输出并要求人们坐在终端前面回答一个问题,人们可以看到输出以知道该输入什么就显得至关重要了。还有一方面,假设输出到一个文件里。并终于被发送到一个行式打印机,仅仅有全部的输出终于可以到达那里是重要的。

  

    马上安排输出的显示通常比将其临时保存在一大块一起输出要昂贵得多。

因此,C实现通常同意程序猿控制产生多少输出后在实际地写出它们。  

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

setbuf(stdout, buf);  

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

缓冲区的合适的大小在中定义为BUFSIZ。

  

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

#include <stdio.h>
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 <stdio.h>

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之很常使用特性(一)