C/C++ 知识点

//高质量C/C++编程指南

1>:匿名枚举

enum{value1 = 0,value2 = 1};

enum{value3 = 2};

-------他的功能等价于静态常成员变量

匿名枚举类型的一个常见应用是作为定义整数常量的另一种方式,rg:

enum{ value1 = 3,value2 = 12,value3 = 1780};

这个枚举包含了3个已显示赋值的枚举成员,尽管没有声明这种枚举的数据类型变量,但可以在算术表达式中使用其中的枚举成员,rg:cout<<value1*value2*value3;

 

 

2>:c语言中的char* 和char []的区别:

在实习过程中发现了一个以前一直默认的错误,同样char *c = "abc"char c[]="abc",前者改变其内容程序是会崩溃的,而后者完全正确。

 

程序演示:

 

view plaincopy to clipboardprint?

#include    

using namespace std;   

main()   

{   

    char *c1 = "abc";   

    char c2[] = "abc";   

    char *c3 = ( char* )malloc(3);   

    c3 = "abc";   

    printf("%d %d %s\n",&c1,c1,c1);   

    printf("%d %d %s\n",&c2,c2,c2);   

    printf("%d %d %s\n",&c3,c3,c3);   

    getchar();   

}     

#include

using namespace std;

main()

{

    char *c1 = "abc";

    char c2[] = "abc";

    char *c3 = ( char* )malloc(3);

    c3 = "abc";

    printf("%d %d %s\n",&c1,c1,c1);

    printf("%d %d %s\n",&c2,c2,c2);

    printf("%d %d %s\n",&c3,c3,c3);

    getchar();

}   

 

参考资料:

 

首先要搞清楚编译程序占用的内存的分区形式:

 

一、预备知识—程序的内存分配

 

一个由c/C++编译的程序占用的内存分为以下几个部分

 

 

1、栈区(stack)—由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于

 

数据结构中的栈。

2、堆区(heap)—一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收。注意它与数据

 

结构中的堆是两回事,分配方式倒是类似于链表,呵呵。

3、全局区(静态区)(static)—全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态

 

变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统

 

释放。

4、文字常量区—常量字符串就是放在这里的。程序结束后由系统释放。

5、程序代码区

这是一个前辈写的,非常详细

 

view plaincopy to clipboardprint?

//main.cpp   

   int a=0;     //全局初始化区   

   char *p1;    //全局未初始化区   

   main()   

   {   

    int b;栈   

    char s[]="abc";    //栈   

    char *p2;          //栈   

    char *p3="123456";    //123456\0在常量区,p3在栈上。   

    static int c=0;    //全局(静态)初始化区   

    p1 = (char*)malloc(10);   

    p2 = (char*)malloc(20);    //分配得来得1020字节的区域就在堆区。   

    strcpy(p1,"123456");    //123456\0放在常量区,编译器可能会将它与p3所向"123456"优化成一个地方。   

}  

//main.cpp

   int a=0;     //全局初始化区

   char *p1;    //全局未初始化区

   main()

   {

    int b;

    char s[]="abc";    //

    char *p2;          //

    char *p3="123456";    //123456\0在常量区,p3在栈上。

    static int c=0;    //全局(静态)初始化区

    p1 = (char*)malloc(10);

    p2 = (char*)malloc(20);    //分配得来得1020字节的区域就在堆区。

    strcpy(p1,"123456");    //123456\0放在常量区,编译器可能会将它与p3所向"123456"优化成一个地方。

}

 

二、堆和栈的理论知识

 

2.1申请方式

stack:

由系统自动分配。例如,声明在函数中一个局部变量int b;系统自动在栈中为b开辟空间

heap:

需要程序员自己申请,并指明大小,在cmalloc函数

p1=(char*)malloc(10);

C++中用new运算符

p2=(char*)malloc(10);

但是注意p1p2本身是在栈中的。

 

 

2.2

申请后系统的响应

栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,

会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将

 

该结点的空间分配给程序,另外,对于大多数系统,会在这块内存空间中的首地址处记录本次分配的大

 

小,这样,代码中的delete语句才能正确的释放本内存空间。另外,由于找到的堆结点的大小不一定正

 

好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

 

 

2.3申请大小的限制

栈:在Windows,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地

 

址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译

 

时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间

 

较小。

堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地

 

址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的

 

虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

 

 

2.4申请效率的比较:

:由系统自动分配,速度较快。但程序员是无法控制的。

:是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.

另外,在WINDOWS下,最好的方式是用Virtual Alloc分配内存,他不是在堆,也不是在栈,而是直接在进

 

程的地址空间中保留一块内存,虽然用起来最不方便。但是速度快,也最灵活。

 

 

2.5堆和栈中的存储内容

栈:在函数调用时,第一个进栈的是主函数中后的下一条指令(函数调用语句的下一条可执行语句)的

 

地址,然后是函数的各个参数,在大多数的C编译器中,参数是由右往左入栈的,然后是函数中的局部变

 

量。注意静态变量是不入栈的。

当本次函数调用结束后,局部变量先出栈,然后是参数,最后栈顶指针指向最开始存的地址,也就是主

 

函数中的下一条指令,程序由该点继续运行。

堆:一般是在堆的头部用一个字节存放堆的大小。堆中的具体内容由程序员安排。

 

 

2.6存取效率的比较

 

view plaincopy to clipboardprint?

char s1[]="aaaaaaaaaaaaaaa";   

char *s2="bbbbbbbbbbbbbbbbb";  

char s1[]="aaaaaaaaaaaaaaa";

char *s2="bbbbbbbbbbbbbbbbb";

 

aaaaaaaaaaa是在运行时刻赋值的;

bbbbbbbbbbb是在编译时就确定的;

但是,在以后的存取中,在栈上的数组比指针所指向的字符串(例如堆)快。

比如:

 

view plaincopy to clipboardprint?

#include   

voidmain()   

{   

char a=1;   

char c[]="1234567890";   

char *p="1234567890";   

a = c[1];   

a = p[1];   

return;   

}  

#include

voidmain()

{

char a=1;

char c[]="1234567890";

char *p="1234567890";

a = c[1];

a = p[1];

return;

}

 

对应的汇编代码

10:a=c[1];

004010678A4DF1movcl,byteptr[ebp-0Fh]

0040106A884DFCmovbyteptr[ebp-4],cl

11:a=p[1];

0040106D8B55ECmovedx,dwordptr[ebp-14h]

004010708A4201moval,byteptr[edx+1]

004010738845FCmovbyteptr[ebp-4],al

第一种在读取时直接就把字符串中的元素读到寄存器cl中,而第二种则要先把指针值读到edx中,在根据

 

edx读取字符,显然慢了。

 

 

2.7小结:

堆和栈的区别可以用如下的比喻来看出:

使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会

 

切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。

使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

 

自我总结:

char *c1 = "abc";实际上先是在文字常量区分配了一块内存放"abc",然后在栈上分配一地址给c1并指向

 

这块地址,然后改变常量"abc"自然会崩溃

 

然而char c2[] = "abc",实际上abc分配内存的地方和上者并不一样,可以从

4199056

2293624 看出,完全是两块地方,推断4199056处于常量区,而2293624处于栈区

 

2293628

2293624

2293620 这段输出看出三个指针分配的区域为栈区,而且是从高地址到低地址

 

2293620 4199056 abc 看出编译器将c3优化指向常量区的"abc"

 

 

继续思考:

 

view plaincopy to clipboardprint?

#include    

using namespace std;   

main()   

{   

    char *c1 = "abc";   

    char c2[] = "abc";   

    char *c3 = ( char* )malloc(3);   

    //   *c3 = "abc" //error   

    strcpy(c3,"abc");   

    c3[0] = 'g';   

    printf("%d %d %s\n",&c1,c1,c1);   

    printf("%d %d %s\n",&c2,c2,c2);   

    printf("%d %d %s\n",&c3,c3,c3);   

    getchar();   

}      

//输出:   

//2293628 4199056 abc   

//2293624 2293624 abc   

//2293620 4012976 gbc  

#include

using namespace std;

main()

{

    char *c1 = "abc";

    char c2[] = "abc";

    char *c3 = ( char* )malloc(3);

    //   *c3 = "abc" //error

    strcpy(c3,"abc");

    c3[0] = 'g';

    printf("%d %d %s\n",&c1,c1,c1);

    printf("%d %d %s\n",&c2,c2,c2);

    printf("%d %d %s\n",&c3,c3,c3);

    getchar();

}   

//输出:

//2293628 4199056 abc

//2293624 2293624 abc

//2293620 4012976 gbc

 

写成注释那样,后面改动就会崩溃

可见strcpy(c3,"abc");abc是另一块地方分配的,而且可以改变,和上面的参考文档说法有些不一定。

 

http://tech.e800.com.cn/articles/2009/1117/1258438057809_1.html

 

 

--------------------------------------------------------

 

C语言中char *s char s[]之间的区别

很多人觉得这两个定义效果一样,其实差别很大。以下是个人的一些看法,有不正确的地方望指正。

 

本质上来说,char *s定义了一个char型的指针,它只知道所指向的内存单元,并不知道这个内存单元有多大,所以:

char *s = "hello";,不能使用s[0]='a';语句进行赋值。这是将提示内存不能为"written"

当用char s[]="hello";后,完全可以使用s[0]='a';进行赋值,这是常规的数组操作。

char s[] = "hello";

    char *p = s;

    也可以使用p[0] = 'a';因为这是p ==s,都是指向数组的指针。

下面看另外一种定义:

char *s = (char *)malloc(n);//其中n为要开辟空间的大小

这句话其实相当于:

char s[n];定义的也是一个指向数组的指针,便可进行数组的下标操作。

 

 

3>:函数指针:

函数指针是指向函数的指针变量 因而“函数指针”本身首先应是指针变量,只不过该指针变量指向函数。这正如用指针变量可指向整型变量、字符型、数组一样,这里是指向函数。如前所述,C在编译时,每一个函数都有一个入口地址,该入口地址就是函数指针所指向的地址。有了指向函数的指针变量后,可用该指针变量调用函数,就如同用指针变量可引用其他类型变量一样,在这些概念上是一致的。

函数指针有两个用途:调用函数和做函数的参数

函数指针的声明方法为:

数据类型标志符 (指针变量) (形参列表);

1函数类型说明函数的返回类型,由于“()”的优先级高于“*”,所以指针变量名外的括号必不可少,后面的形参列表表示指针变量指向的函数所带的参数列表。例如:

int func(int x); /* 声明一个函数 */

int (*f) (int x); /* 声明一个函数指针 */

f=func; /* func函数的首地址赋给指针f */

  赋值时函数func不带括号,也不带参数,由于func代表函数的首地址,因此经过赋值以后,指针f就指向函数func(x)的代码的首地址。

2:函数括号中的形参可有可无,视情况而定。

  回调函数是一个程序员不能显式调用的函数;通过将回调函数的地址传给调用者从而实现调用。要实现回调,必须首先定义函数指针。尽管定义的语法有点不可思议,但如果你熟悉函数声明的一般方法,便会发现函数指针的声明与函数声明非常类似

pfv是一个函数指针,它指向的函数没有输入参数,返回类行为void。使用这个类型定义名可以隐藏复杂的函数指针语法。

指针变量应该有一个变量名:

void (*p) (); //p是指向某函数的指针

    p是指向某函数的指针,该函数无输入参数,返回值的类型为void。左边圆括弧里星号后的就是指针变量名。有了指针变量便可以赋值,值的内容是署名匹配的函数名和返回类型。例如:

void func() 
{
/* do something */

p = func; 

p的赋值可以不同,但一定要是函数的地址,并且署名和返回类型相同。

传递回调函数的地址给调用者

    现在可以将p传递给另一个函数(调用者)- caller(),它将调用p指向的函数,而此函数名是未知的:

void caller(void(*ptr)())
{
ptr(); /* 调用ptr指向的函数 */ 
}
void func();
int main()
{
p = func; 
caller(p); /* 传递函数地址到调用者 */
}

    如果赋了不同的值给p(不同函数地址),那么调用者将调用不同地址的函数。赋值可以发生在运行时,这样使你能实现动态绑定。

 

 

4>:函数指针数组的妙用

 

         笔者在开发某软件过程中遇到这样一个问题,前级模块传给我二进制数据,输入参数为 char* buffer和 int length,buffer是数据的首地址,length表示这批数据的长度。数据的特点是:长度不定,类型不定,由第一个字节(buffer[0])标识该数据的类型,共有256(28 )种可能性。我的任务是必须对每一种可能出现的数据类型都要作处理,并且我的模块包含若干个函数,在每个函数里面都要作类似的处理。若按通常做法,会写出如下代码:

void MyFuntion( char* buffer, int length )
{
__int8 nStreamType = buffer[0];

switch( nStreamType )
{
case 0:
function1();
break;
case 1:
......
case 255:
function255();
break;
}
}

        如果按照这种方法写下去,那么在我的每一个函数里面,都必须作如此多的判断,写出的代码肯定很长,并且每一次处理,都要作许多次判断之后才找到正确的处理函数,代码的执行效率也不高。针对上述问题,我想到了用函数指针数组的方法解决这个问题。

  函数指针的概念,在潭浩强先生的C语言程序设计这本经典的教程中提及过,在大多数情况下我们使用不到,也忽略了它的存在。函数名实际上也是一种指针,指向函数的入口地址,但它又不同于普通的如int*、double*指针,看下面的例子来理解函数指针的概念:
int funtion( int x, int y );
void main ( void )
{
    int (*fun) ( int x, int y );
    int a = 10, b = 20;
    function( a, b );
    fun = function;
    (*fun)( a, b );
     ……
}
  语句1定义了一个函数function,其输入为两个整型数,返回也为一个整型数(输入参数和返回值可为其它任何数据类型);语句3定义了一个函数指针,int*或double*定义指针不同的是,函数指针的定义必须同时指出输入参数,表明这是一个函数指针,并且*fun也必须用一对括号括起来;语句6将函数指针赋值为funtion,前提条件是*fun和function的输入参数和返回值必须保持一致。语句5直接调用函数function(),语句7是调用函数指针,二者等效。

  当然从上述例子看不出函数指针的优点,目的主要是想引出函数指针数组的概念。我们从上面例子可以得知,既然函数名可以通过函数指针加以保存,那们也一定能定义一个数组保存若干个函数名,这就是函数指针数组。正确使用函数指针数组的前提条件是,这若干个需要通过函数指针数组保存的函数必须有相同的输入、输出值。

这样,我工作中所面临的问题可以解决如下:

首先定义256个处理函数(及其实现)。

void funtion0( void );
……
void funtion255(void );

其次定义函数指针数组,并给数组赋值。
void (*fun[256])(void);

fun[0] = function0;
……
fun[255] = function();
最后,MyFunction()函数可以修改如下:

void MyFuntion( char* buffer, int length )
{
__int8 nStreamType = buffer[0];
    (*fun[nStreamType])();
}

  只要2行代码,就完成了256条case语句要做的事,减少了编写代码时工作量,将nStreamType作为数组下标,直接调用函数指针,从代码执行效率上来说,也比case语句高。假如多个函数中均要作如此处理,函数指针数组更能体现出它的优势。

函数指针与typedef

关于C++中函数指针的使用(包含对typedef用法的讨论)
(一)简单的函数指针的应用。
//形式1:返回类型(*函数名)(参数表)
char (*pFun)(int);
char glFun(int a){ return;}
void main()
{
    pFun = glFun;
    (*pFun)(2);
}

        第一行定义了一个指针变量pFun。首先我们根据前面提到的“形式1”认识到它是一个指向某种函数的指针,这种函数参数是一个int型,返回值是char类型。只有第一句我们还无法使用这个指针,因为我们还未对它进行赋值。
        第二行定义了一个函数glFun()。该函数正好是一个以int为参数返回char的函数。我们要从指针的层次上理解函数——函数的函数名实际上就是一个指针,函数名指向该函数的代码在内存中的首地址。
        然后就是可爱的main()函数了,它的第一句您应该看得懂了——它将函数glFun的地址赋值给变量pFun。main()函数的第二句中“*pFun”显然是取pFun所指向地址的内容,当然也就是取出了函数glFun()的内容,然后给定参数为2。
(二)使用typedef更直观更方便。
//形式2:typedef 返回类型(*新类型)(参数表)
typedef char (*PTRFUN)(int);
PTRFUN pFun;
char glFun(int a){ return;}
void main()
{
    pFun = glFun;
    (*pFun)(2);
}

        typedef的功能是定义新的类型。第一句就是定义了一种PTRFUN的类型,并定义这种类型为指向某种函数的指针,这种函数以一个int为参数并返回char类型。后面就可以像使用int,char一样使用PTRFUN了。
        第二行的代码便使用这个新类型定义了变量pFun,此时就可以像使用形式1一样使用这个变量了。
(三)在C++类中使用函数指针。
//形式3:typedef 返回类型(类名::*新类型)(参数表) 
class CA
{
 public:
    char lcFun(int a){ return; }
};
CA ca;
typedef char (CA::*PTRFUN)(int);
PTRFUN pFun;
void main()
{
    pFun = CA::lcFun;
    ca.(*pFun)(2);
}

        在这里,指针的定义与使用都加上了“类限制”或“对象”,用来指明指针指向的函数是哪个类的,这里的类对象也可以是使用new得到的。比如:
CA *pca = new CA;
pca->(*pFun)(2);
delete pca;

        而且这个类对象指针可以是类内部成员变量,你甚至可以使用this指针。比如:
        类CA有成员变量PTRFUN m_pfun;
void CA::lcFun2()

   (this->*m_pFun)(2);
}

        一句话,使用类成员函数指针必须有“->*”或“.*”的调用。

 

在调用动态库时,习惯用typedef重新定义动态库函数中的函数地址(函数指针),如在动态库(test.dll)中有如下函数:

      int   DoCase(int, long);

则,在调用动态库是有两种方法:

  1.  先声明一个与动态库中类型一致的指针函数变量:

        int (*DOCASE)(int ,long);//用于指向动态库中的DoCase函数地址

        HINSTANCE gLibMyDLL = NULL;

       gLibMyDLL = LoadLibrary("test.dll");

       if(gLibMyDLL != NULL)

         {

                   //得到函数地址

                     DOCASE = (int(*)(int,long))GetProcAddress(gLibMyDLL, "DoCase");

          }  

         //调用函数

         int s = DOCASE(1,1000);

   2.用typedef定义一个指针函数:typedef (*DOCASE)(int ,long);

         HINSTANCE gLibMyDLL = NULL;

        DOCASE _docase;

       gLibMyDLL = LoadLibrary("test.dll");

      if(gLibMyDLL != NULL)

          {

                _docase = (DOCASE)GetProcAddress(gLibMyDll, "DoCase");

         }

      //调用函数

      int s=_docase(1,1000);

 

 

5>使用 typedef 抑制劣质代码

 

摘要:Typedef 声明有助于创建平台无关类型,甚至能隐藏复杂和难以理解的语法。不管怎样,使用 typedef 能为代码带来意想不到的好处,通过本文你可以学习用 typedef 避免缺欠,从而使代码更健壮。

 

 

typedef 声明,简称 typedef,为现有类型创建一个新的名字。比如人们常常使用 typedef 来编写更美观和可读的代码。所谓美观,意指 typedef 能隐藏笨拙的语法构造以及平台相关的数据类型,从而增强可移植性和以及未来的可维护性。本文下面将竭尽全力来揭示 typedef 强大功能以及如何避免一些常见的陷阱。

如何创建平台无关的数据类型,隐藏笨拙且难以理解的语法?

使用 typedefs 为现有类型创建同义字。

定义易于记忆的类型名
typedef 使用最多的地方是创建易于记忆的类型名,用它来归档程序员的意图。类型出现在所声明的变量名字中,位于 ''typedef'' 关键字右边。例如:

typedef int size;

  此声明定义了一个 int 的同义字,名字为 size。注意 typedef 并不创建新的类型。它仅仅为现有类型添加一个同义字。你可以在任何需要 int 的上下文中使用 size:

void measure(size * psz);

size array[4];

size len = file.getlength();

std::vector <size> vs;

typedef 还可以掩饰符合类型,如指针和数组。例如,你不用象下面这样重复定义有 81 个字符元素的数组:

char line[81];

char text[81];

定义一个 typedef,每当要用到相同类型和大小的数组时,可以这样:

typedef char Line[81];

Line text, secondline;

getline(text);

同样,可以象下面这样隐藏指针语法:

typedef char * pstr;

int mystrcmp(pstr, pstr);

  这里将带我们到达第一个 typedef 陷阱。标准函数 strcmp()有两个‘const char *’类型的参数。因此,它可能会误导人们象下面这样声明 mystrcmp():

int mystrcmp(const pstr, const pstr);

  这是错误的,按照顺序,‘const pstr’被解释为‘char * const’(一个指向 char 的常量指针),而不是‘const char *’(指向常量 char 的指针)。这个问题很容易解决:

typedef const char * cpstr;

int mystrcmp(cpstr, cpstr); // 现在是正确的

记住:不管什么时候,只要为指针声明 typedef,那么都要在最终的 typedef 名称中加一个 const,以使得该指针本身是常量,而不是对象。

代码简化
  上面讨论的 typedef 行为有点像 #define 宏,用其实际类型替代同义字。不同点是 typedef 在编译时被解释,因此让编译器来应付超越预处理器能力的文本替换。例如:

typedef int (*PF) (const char *, const char *);

  这个声明引入了 PF 类型作为函数指针的同义字,该函数有两个 const char * 类型的参数以及一个 int 类型的返回值。如果要使用下列形式的函数声明,那么上述这个 typedef 是不可或缺的:

PF Register(PF pf);

Register() 的参数是一个 PF 类型的回调函数,返回某个函数的地址,其署名与先前注册的名字相同。做一次深呼吸。下面我展示一下如果不用 typedef,我们是如何实现这个声明的:

int (*Register (int (*pf)(const char *, const char *)))

(const char *, const char *);

  很少有程序员理解它是什么意思,更不用说这种费解的代码所带来的出错风险了。显然,这里使用 typedef 不是一种特权,而是一种必需。持怀疑态度的人可能会问:“OK,有人还会写这样的代码吗?”,快速浏览一下揭示 signal()函数的头文件 <csinal>,一个有同样接口的函数。

typedef 和存储类关键字(storage class specifier
  这种说法是不是有点令人惊讶,typedef 就像 auto,extern,mutable,static,和 register 一样,是一个存储类关键字。这并是说 typedef 会真正影响对象的存储特性;它只是说在语句构成上,typedef 声明看起来象 static,extern 等类型的变量声明。下面将带到第二个陷阱:

typedef register int FAST_COUNTER; // 错误

  编译通不过。问题出在你不能在声明中有多个存储类关键字。因为符号 typedef 已经占据了存储类关键字的位置,在 typedef 声明中不能用 register(或任何其它存储类关键字)。

促进跨平台开发
typedef 有另外一个重要的用途,那就是定义机器无关的类型,例如,你可以定义一个叫 REAL 的浮点类型,在目标机器上它可以i获得最高的精度:

typedef long double REAL;

在不支持 long double 的机器上,该 typedef 看起来会是下面这样:

typedef double REAL;

并且,在连 double 都不支持的机器上,该 typedef 看起来会是这样: 、

typedef float REAL;

  你不用对源代码做任何修改,便可以在每一种平台上编译这个使用 REAL 类型的应用程序。唯一要改的是 typedef 本身。在大多数情况下,甚至这个微小的变动完全都可以通过奇妙的条件编译来自动实现。不是吗? 标准库广泛地使用 typedef 来创建这样的平台无关类型:size_t,ptrdiff 和 fpos_t 就是其中的例子。此外,象 std::string 和 std::ofstream 这样的 typedef 还隐藏了长长的,难以理解的模板特化语法,例如:basic_string<char, char_traits<char>,allocator<char>> 和 basic_ofstream<char, char_traits<char>>。

 

 

 

 

 

 

6>:回调函数

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用为调用它所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。

回调函数实现的机制是

  (1)定义一个回调函数;

  (2)提供函数实现的一方在初始化的时候,将回调函数的函数指针注册给调用者;

3)当特定的事件或条件发生的时候,调用者使用函数指针调用回调函数对事件进行处理。

为什么要使用回调函数

  因为可以把调用者与被调用者分开。调用者不关心谁是被调用者,所有它需知道的,只是存在一个具有某种特定原型、某些限制条件(如返回值为int)的被调用函数。

  如果想知道回调函数在实际中有什么作用,先假设有这样一种情况,我们要编写一个库,它提供了某些排序算法的实现,如冒泡排序快速排序shell排序shake排序等等,但为使库更加通用,不想在函数中嵌入排序逻辑,而让使用者来实现相应的逻辑;或者,想让库可用于多种数据类型(intfloatstring),此时,该怎么办呢?可以使用函数指针,并进行回调。

  回调可用于通知机制,例如,有时要在程序中设置一个计时器,每到一定时间,程序会得到相应的通知,但通知机制的实现者对我们的程序一无所知。而此时,就需有一个特定原型的函数指针,用这个指针来进行回调,来通知我们的程序事件已经发生。实际上,SetTimer() API使用了一个回调函数来通知计时器,而且,万一没有提供回调函数,它还会把一个消息发往程序的消息队列。

  另一个使用回调机制的API函数是EnumWindow(),它枚举屏幕上所有的顶层窗口,为每个窗口调用一个程序提供的函数,并传递窗口的处理程序。如果被调用者返回一个值,就继续进行迭代,否则,退出。EnumWindow()并不关心被调用者在何处,也不关心被调用者用它传递的处理程序做了什么,它只关心返回值,因为基于返回值,它将继续执行或退出。

  不管怎么说,回调函数是继续自C语言的,因而,在C++中,应只在与C代码建立接口,或与已有的回调接口打交道时,才使用回调函数。除了上述情况,在C++中应使用虚拟方法或函数符(functor),而不是回调函数。

简单的回调函数实现

代码实现

  下面创建了一个sort.dll的动态链接库,它导出了一个名为CompareFunction的类型--typedef int (__stdcall *CompareFunction)(const byte*, const byte*),它就是回调函数的类型。另外,它也导出了两个方法:Bubblesort()Quicksort(),这两个方法原型相同,但实现了不同的排序算法

void DLLDIR __stdcall Bubblesort(byte* array,int size,int elem_size,CompareFunction cmpFunc);

void DLLDIR __stdcall Quicksort(byte* array,int size,int elem_size,CompareFunction cmpFunc);

  这两个函数接受以下参数:

·byte * array:指向元素数组的指针(任意类型)

·int size:数组中元素的个数。

·int elem_size:数组中一个元素的大小,以字节为单位。

·CompareFunction cmpFunc:带有上述原型的指向回调函数的指针。

  这两个函数的会对数组进行某种排序,但每次都需决定两个元素哪个排在前面,而函数中有一个回调函数,其地址是作为一个参数传递进来的。对编写者来说,不必介意函数在何处实现,或它怎样被实现的,所需在意的只是两个用于比较的元素的地址,并返回以下的某个值(库的编写者和使用者都必须遵守这个约定)

·-1:如果第一个元素较小,那它在已排序好的数组中,应该排在第二个元素前面。

·0:如果两个元素相等,那么它们的相对位置并不重要,在已排序好的数组中,谁在前面都无所谓。

·1:如果第一个元素较大,那在已排序好的数组中,它应该排第二个元素后面。

  基于以上约定,函数Bubblesort()的实现如下,Quicksort()就稍微复杂一点:

void DLLDIR __stdcall Bubblesort(byte* array,int size,int elem_size,CompareFunction cmpFunc)

{

for(int i=0; i < size; i++)

{

for(int j=0; j < size-1; j++)

{

//回调比较函数

if(1 == (*cmpFunc)(array+j*elem_size,array+(j+1)*elem_size))

{

//两个相比较的元素相交换

byte* temp = new byte[elem_size];

memcpy(temp, array+j*elem_size, elem_size);

memcpy(array+j*elem_size,array+(j+1)*elem_size,elem_size);

memcpy(array+(j+1)*elem_size, temp, elem_size);

delete [] temp;

}

}

}

}

  注意:因为实现中使用了memcpy(),所以函数在使用的数据类型方面,会有所局限。

  对使用者来说,必须有一个回调函数,其地址要传递给Bubblesort()函数。下面有二个简单的示例,一个比较两个整数,而另一个比较两个字符串:

int __stdcall CompareInts(const byte* velem1, const byte* velem2)

{

int elem1 = *(int*)velem1;

int elem2 = *(int*)velem2;

if(elem1 < elem2)

return -1;

if(elem1 > elem2)

return 1;

return 0;

}

int __stdcall CompareStrings(const byte* velem1, const byte* velem2)

{

const char* elem1 = (char*)velem1;

const char* elem2 = (char*)velem2;

return strcmp(elem1, elem2);

}

  下面另有一个程序,用于测试以上所有的代码,它传递了一个有5个元素的数组给Bubblesort()Quicksort(),同时还传递了一个指向回调函数的指针。

int main(int argc, char* argv[])

{

int i;

int array[] = {5432, 4321, 3210, 2109, 1098};

cout << "Before sorting ints with Bubblesort\n";

for(i=0; i < 5; i++)

cout << array<< '\n’;

Bubblesort((byte*)array, 5, sizeof(array[0]), &CompareInts);

cout << "After the sorting\n";

for(i=0; i < 5; i++)

cout << array<< ’\n’;

const char str[5][10] = {"estella","danielle","crissy","bo","angie"};

cout << "Before sorting strings with Quicksort\n";

for(i=0; i < 5; i++)

cout << str<< ’\n’;

Quicksort((byte*)str, 5, 10, &CompareStrings);

cout << "After the sorting\n";

for(i=0; i < 5; i++)

cout << str<< ’\n’;

return 0;

}

  如果想进行降序排序(大元素在先),就只需修改回调函数的代码,或使用另一个回调函数,这样编程起来灵活性就比较大了。

调用约定

  上面的代码中,可在函数原型中找到__stdcall,因为它以双下划线打头,所以它是一个特定于编译器的扩展,说到底也就是微软的实现。任何支持开发基于Win32的程序都必须支持这个扩展或其等价物。以__stdcall标识的函数使用了标准调用约定,为什么叫标准约定呢,因为所有的Win32 API(除了个别接受可变参数的除外)都使用它。标准调用约定的函数在它们返回到调用者之前,都会从堆栈中移除掉参数,这也是Pascal的标准约定。但在C/C++中,调用约定是调用者负责清理堆栈,而不是被调用函数;为强制函数使用C/C++调用约定,可使用__cdecl。另外,可变参数函数也使用C/C++调用约定。

Windows操作系统采用了标准调用约定(Pascal约定),因为其可减小代码的体积。这点对早期的Windows来说非常重要,因为那时它运行在只有640KB内存的电脑上。

  如果你不喜欢__stdcall,还可以使用CALLBACK宏,它定义在windef.h中:

#define CALLBACK __stdcallor

#define CALLBACK PASCAL //PASCAL在此被#defined__stdcall

  作为回调函数的C++方法

  因为平时很可能会使用到C++编写代码,也许会想到把回调函数写成类中的一个方法,但先来看看以下的代码:

class CCallbackTester

{

public:

int CALLBACK CompareInts(const byte* velem1, const byte* velem2);

};

Bubblesort((byte*)array, 5, sizeof(array[0]),

&CCallbackTester::CompareInts);

  如果使用微软的编译器,将会得到下面这个编译错误:

error C2664: ’Bubblesort’ : cannot convert parameter 4 from ’int (__stdcall CCallbackTester::*)(const unsigned char *,const unsigned char *)’ to ’int (__stdcall *)(const unsigned char *,const unsigned char *)’ There is no context in which this conversion is possible

  这是因为非静态成员函数有一个额外的参数:this指针,这将迫使你在成员函数前面加上static

 

7> C语言中的位域

位域又叫位段,用于指定该成员在内存存储时所用到的位数,从而更紧凑的表示数据。

有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态,用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”,是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。

一、位域的定义和位域变量的说明位域定义与结构定义相仿,其形式为:
    struct 位域结构名
    { 位域列表 };
    其中位域列表的形式为: 类型说明符 位域名:位域长度
    例如:
    struct bs
    {
        int a:8;
        int b:2;
        int c:6;
    };

   位域变量的说明与结构变量说明的方式相同。 可采用先定义后说明,同时定义说明或者
   直接说明这三种方式。例如:
   struct bs
   {
        int a:8;
        int b:2;
        int c:6;
   }data;
   说明data为bs变量,共占两个字节。其中位域a占8位,位域b占2位,位域c占6位。

   对于位域的定义尚有以下几点说明:
   1. 一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从
   下一单元起存放该位域。也可以有意使某位域从下一单元开始。
   例如:
   struct bs
   {
        unsigned a:4
        unsigned :0 /*空域*/
        unsigned b:4 /*从下一单元开始存放*/
        unsigned c:4
   }
   在这个位域定义中,a占第一字节的4位,后4位填0表示不使用,b从第二字节开始,占用4位,c占用4位。
   2. 由于位域不允许跨两个字节,因此位域的长度不能大于一个字节的长度,就是说不能超过8位二进位。
   3. 位域可以无位域名,这时它只用来作填充或调整位置。无名的位域是不能使用的。
   例如:
   struct k
   {
        int a:1
        int :2 /*该2位不能使用*/
        int b:3
        int c:2
   };
   从以上分析可以看出,位域在本质上就是一种结构类型, 不过其成员是按二进位分配的。

二、位域的使用位域的使用和结构成员的使用相同,其一般形式为:
   位域变量名·位域名
   位域允许用各种格式输出。
   main(){
        struct bs
        {
                unsigned a:1;
                unsigned b:3;
                unsigned c:4;
        } bit,*pbit;
        bit.a=1;
        bit.b=7;
        bit.c=15;
        printf("%d,%d,%d\n",bit.a,bit.b,bit.c);
        pbit=&bit;
        pbit->a=0;
        pbit->b&=3;
        pbit->c|=1;
        printf("%d,%d,%d\n",pbit->a,pbit->b,pbit->c);
   } 


上例程序中定义了位域结构bs,三个位域为a,b,c。说明了bs类型的变量bit和指向bs类型的指针变量 pbit。这表示位域也是可以使用指针的。程序的9、10、11三行分别给三个位域赋值。( 应注意赋值不能超过该位域的允许范围)程序第12行以整型量格式输出三个域的内容。第13行把位域变量bit的地址送给指针变量pbit。第14行用指针方式给位域a重新赋值,赋为0。第15行使用了复合的位运算符"&=",该行相当于:pbit->b=pbit->b&3位域b中原有值为7,与3作按位与运算的结果为3(111&011=011,十进制值为3)。同样,程序第16行中使用了复合位运算"|=" 相当于:pbit->c=pbit->c|1其结果为15。程序第17行用指针方式输出了这三个域的值。

注意:
1.位域的长度不能大于int对象所占用的位数。
2.由于位域的实现会因编译程序的不同而不同,因此使用位域会影响程序的可移植性,在不是非要使用时最好
  不要使用.
3.尽管位域可以节省空间,却增加了处理时间。
4.位域的位置不能访问,因此不能对位域使用地址运算符&,也不能使用位域的数组。
5.位域只能用在结构体或共同体中。
6.带位域的结构内存中各个位域的存储方式取决于具体的编译程序:可以从左向右,也可一从右向左存储。

 

 

8>:转义字符的完整诠释

转义字符是C语言中表示字符的一种特殊形式。通常使用转义字符表示ASCII码字符集中不可打印的控制字符和特定功能的字符,如用于表示字符常量的单撇号 ' ),用于表示字符串常量的双撇号 " )和反斜杠 \ )等。转义字符用反斜杠\后面跟一个字符或一个八进制或十六进制数表示

转义字符 意义 ASCII码值(十进制)
\a 响铃(BEL) 007
\b 退格(BS) 008
\f 换页(FF) 012
\n 换行(LF) 010
\r 回车(CR) 013
\t 水平制表(HT) 009
\v 垂直制表(VT) 011
\\ 反斜杠 092
\? 问号字符 063
\' 单引号字符 039
\" 双引号字符 034
\0 空字符(NULL) 000
\ddd 任意字符 三位八进制
\xhh 任意字符 二位十六进制


字符常量中使用单引号和反斜杠以及字符常量中使用双引号和反斜杠时,都必须使用转义字符表示,即在这些字符前加上反斜杠。 
C程序中使用转义字符\ d d d或者\ x h h可以方便灵活地表示任意字符。\ d d d为斜杠后面跟三位八进制数,该三位八进制数的值即为对应的八进制A S C I I码值。\ x后面跟两位十六进制数,该两位十六进制数为对应字符的十六进制A S C I I码值。

使用转义字符时需要注意以下问题: 
1) 转义字符中只能使用小写字母,每个转义字符只能看作一个字符。
2) \v 垂直制表和\f 换页符对屏幕没有任何影响,但会影响打印机执行响应操作。
3) 在C程序中,使用不可打印字符时,通常用转义字符表示


注: 
1,\v垂直制表和\f换页符对屏幕没有任何影响,但会影响打印机执行响应操作。
2,\n其实应该叫回车换行。换行只是换一行,不改变光标的横坐标;回车只是回到行首,不改变光标的纵坐标。
3,\t 光标向前移动四格或八格,可以在编译器里设置
4,\' 在字符里(即单引号里)使用。在字符串里(即双引号里)不需要,只要用 ' 即可。
5,\? 其实不必要。只要用 ? 就可以了(在windows VC6 和tc2 中验证)
转义字符是一种特殊的字符常量。转义字符以反斜线"\"开头,后跟一个或几个字符。转义字符具有特定的含义,不同于字符原有的意义,故称“转义”字符。例如,在前面各例题printf函数的格式串中用到的“\n”就是一个转义字符,其意义是“回车换行”。转义字符主要用来表示那些用一般字符不便于表示的控制代码。
常用的转义字符及其含义
转义字符  转义字符的意义

广义地讲,C语言字符集中的任何一个字符均可用转义字符来表示。表2.2中的\ddd和\xhh正是为此而提出的。ddd和hh分别为八进制和十六进制的ASCII代码。如\101表示字?quot;A" \102表示字母"B"\134表示反斜线,\XOA表示换行等。转义字符的使用
void main()
{
int a,b,c;
a=5; b=6; c=7;
printf("%d\n\t%d %d\n %d %d\t\b%d\n",a,b,c,a,b,c);
}
此程序练习转义字符的使用
a、b、c为整数 5->a,6->b,7->c
调用printf显示程序运行结果
printf("%d\n\t%d %d\n %d %d\t\b%d\n",a,b,c,a,b,c);
程序在第一列输出a值5之后就是“\n”,故回车换行;接着又是“\t”,于是跳到下一制表位置(设制表位置间隔为8),再输出b值6;空二格再输出c 值7后又是"\n",因此再回车换行;再空二格之后又输出a值5;再空三格又输出b的值6;再次后"\t"跳到下一制表位置(与上一行的6 对齐),但下一转义字符“\b”又使退回一格,故紧挨着6再输出c值7。

 

 

 

9>:C语言测试:想成为嵌入式程序员应知道的0x10个基本问题

C语言测试:想成为嵌入式程序员应知道的0x10个基本问题

                        ----------这个是我转载的文章,感觉很好。

C语言测试是招聘嵌入式系统程序员过程中必须而且有效的方法。这些年,我既参加也组织了许多这种测试,在这过程中我意识到这些测试能为带面试者和被面试者提供许多有用信息,此外,撇开面试的压力不谈,这种测试也是相当有趣的。
从被面试者的角度来讲,你能了解许多关于出题者或监考者的情况。这个测试只是出题者为显示其对ANSI标准细节的知识而不是技术技巧而设计吗?这个愚蠢的问题吗?如要你答出某个字符的ASCII值。这些问题着重考察你的系统调用和内存分配策略方面的能力吗?这标志着出题者也许花时间在微机上而不上在嵌入式系统上。如果上述任何问题的答案是“是”的话,那么我知道我得认真考虑我是否应该去做这份工作。
从面试者的角度来讲,一个测试也许能从多方面揭示应试者的素质:最基本的,你能了解应试者C语言的水平。不管怎么样,看一下这人如何回答他不会的问题也是满有趣。应试者是以好的直觉做出明智的选择,还是只是瞎蒙呢?当应试者在某个问题上卡住时是找借口呢,还是表现出对问题的真正的好奇心,把这看成学习的机会呢?我发现这些信息与他们的测试成绩一样有用。
有了这些想法,我决定出一些真正针对嵌入式系统的考题,希望这些令人头痛的考题能给正在找工作的人一点帮住。这些问题都是我这些年实际碰到的。其中有些题很难,但它们应该都能给你一点启迪。
这个测试适于不同水平的应试者,大多数初级水平的应试者的成绩会很差,经验丰富的程序员应该有很好的成绩。为了让你能自己决定某些问题的偏好,每个问题没有分配分数,如果选择这些考题为你所用,请自行按你的意思分配分数。
预处理器(Preprocessor)

1 . 用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)

    #define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL
我在这想看到几件事情:
    #define 语法的基本知识(例如:不能以分号结束,括号的使用,等等)

懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的。
意识到这个表达式将使一个16位机的整型数溢出-因此要用到长整型符号L,告诉编译器这个常数是的长整型数。
如果你在你的表达式中用到UL(表示无符号长整型),那么你有了一个好的起点。记住,第一印象很重要。
2 . 写一个“标准”宏MIN ,这个宏输入两个参数并返回较小的一个。

#define MIN(A,B) ((A) <= (B) ? (A) : (B))

这个测试是为下面的目的而设的:
标识#define在宏中应用的基本知识。这是很重要的,因为直到嵌入(inline)操作符变为标准C的一部分,宏是方便产生嵌入代码的唯一方法,对于嵌入式系统来说,为了能达到要求的性能,嵌入代码经常是必须的方法。
三重条件操作符的知识。这个操作符存在C语言中的原因是它使得编译器能产生比if-then-else更优化的代码,了解这个用法是很重要的。
 懂得在宏中小心地把参数用括号括起来
我也用这个问题开始讨论宏的副作用,例如:当你写下面的代码时会发生什么事?

least = MIN(*p++, b);

3. 预处理器标识#error的目的是什么?
如果你不知道答案,请看参考文献1。这问题对区分一个正常的伙计和一个书呆子是很有用的。只有书呆子才会读C语言课本的附录去找出象这种问题的答案。当然如果你不是在找一个书呆子,那么应试者最好希望自己不要知道答案。
死循环(Infinite loops)

4. 嵌入式系统中经常要用到无限循环,你怎么样用C编写死循环呢?
这个问题用几个解决方案。我首选的方案是:

while(1)
{
?}

 一些程序员更喜欢如下方案:

for(;
{
?}
 
这个实现方式让我为难,因为这个语法没有确切表达到底怎么回事。如果一个应试者给出这个作为方案,我将用这个作为一个机会去探究他们这样做的基本原理。如果他们的基本答案是:“我被教着这样做,但从没有想到过为什么。”这会给我留下一个坏印象。
第三个方案是用 goto

Loop:
...
goto Loop;

应试者如给出上面的方案,这说明或者他是一个汇编语言程序员(这也许是好事)或者他是一个想进入新领域的BASIC/FORTRAN程序员。

数据声明(Data declarations)

5. 用变量a给出下面的定义
a) 一个整型数(An integer)
b)一个指向整型数的指针( A pointer to an integer)
c)一个指向指针的的指针,它指向的指针是指向一个整型数( A pointer to a pointer to an intege)r
d)一个有10个整型数的数组( An array of 10 integers)
e) 一个有10个指针的数组,该指针是指向一个整型数的。(An array of 10 pointers to integers)
f) 一个指向有10个整型数数组的指针( A pointer to an array of 10 integers)
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument and returns an integer)
h)一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数( An array of ten pointers to functions that take an integer argument and return an integer )

答案是:
a) int a; // An integer

b) int *a; // A pointer to an integer

c) int **a; // A pointer to a pointer to an integer

d) int a[10]; // An array of 10 integers

e) int *a[10]; // An array of 10 pointers to integers

f) int (*a)[10]; // A pointer to an array of 10 integers

g) int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer

h) int (*a[10])(int); // An array of 10 pointers to functions that take an integer argument and return an integer

人们经常声称这里有几个问题是那种要翻一下书才能回答的问题,我同意这种说法。当我写这篇文章时,为了确定语法的正确性,我的确查了一下书。但是当我被面试的时候,我期望被问到这个问题(或者相近的问题)。因为在被面试的这段时间里,我确定我知道这个问题的答案。应试者如果不知道所有的答案(或至少大部分答案),那么也就没有为这次面试做准备,如果该面试者没有为这次面试做准备,那么他又能为什么出准备呢?


Static
6. 关键字static的作用是什么?
这个简单的问题很少有人能回答完全。在C语言中,关键字static有三个明显的作用:
   在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
   在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。
   在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。
大多数应试者能正确回答第一部分,一部分能正确回答第二部分,同是很少的人能懂得第三部分。这是一个应试者的严重的缺点,因为他显然不懂得本地化数据和代码范围的好处和重要性。

Const
7.关键字const有什么含意?
我只要一听到被面试者说:“const意味着常数,我就知道我正在和一个业余者打交道。去年Dan Saks已经在他的文章里完全概括了const的所有用法,因此ESP(译者:Embedded Systems Programming)的每一位读者应该非常熟悉const能做什么和不能做什么.如果你从没有读到那篇文章,只要能说出const意味着只读就可以了。尽管这个答案不是完全的答案,但我接受它作为一个正确的答案。(如果你想知道更详细的答案,仔细读一下Saks的文章吧。)
如果应试者能正确回答这个问题,我将问他一个附加的问题:
下面的声明都是什么意思?

const int a;
int const a;
const int *a;
int * const a;
int const * a const;

/******/
前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。顺带提一句,也许你可能会问,即使不用关键字 const,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const呢?我也如下的几下理由:
   关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。)
    通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。
    合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。


Volatile
8. 关键字volatile有什么含意?并给出三个不同的例子。
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:
    并行设备的硬件寄存器(如:状态寄存器)
    一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
    多线程应用中被几个任务共享的变量

回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,所有这些都要求用到volatile变量。不懂得volatile的内容将会带来灾难。
假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。
    一个参数既可以是const还可以是volatile吗?解释为什么。
    一个指针可以是volatile 吗?解释为什么。
    下面的函数有什么错误:
int square(volatile int *ptr)
{
return *ptr * *ptr;
}

下面是答案:
    是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
    是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。
    这段代码有点变态。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:


int square(volatile int *ptr)
{
int a,b;
a = *ptr;
b = *ptr;
return a * b;
}

由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:

long square(volatile int *ptr)
{
int a;
a = *ptr;
return a * a;
}

位操作(Bit manipulation)

9. 嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在以上两个操作中,要保持其它位不变。
对这个问题有三种基本的反应
    不知道如何下手。该被面者从没做过任何嵌入式系统的工作。
    bit fields。Bit fields是被扔到C语言死角的东西,它保证你的代码在不同编译器之间是不可移植的,同时也保证了的你的代码是不可重用的。我最近不幸看到 Infineon为其较复杂的通信芯片写的驱动程序,它用到了bit fields因此完全对我无用,因为我的编译器用其它的方式来实现bit fields的。从道德讲:永远不要让一个非嵌入式的家伙粘实际硬件的边。
     #defines 和 bit masks 操作。这是一个有极高可移植性的方法,是应该被用到的方法。最佳的解决方案如下:


#define BIT3 (0x1 << 3)
static int a;

void set_bit3(void) {
a |= BIT3;
}
void clear_bit3(void) {
a &= ~BIT3;
}

一些人喜欢为设置和清除值而定义一个掩码同时定义一些说明常数,这也是可以接受的。我希望看到几个要点:说明常数、|=&=~操作。
访问固定的内存位置(Accessing fixed memory locations)

10. 嵌入式系统经常具有要求程序员去访问某特定的内存位置的特点。在某工程中,要求设置一绝对地址为0x67a9的整型变量的值为0xaa66。编译器是一个纯粹的ANSI编译器。写代码去完成这一任务。
这一问题测试你是否知道为了访问一绝对地址把一个整型数强制转换(typecast)为一指针是合法的。这一问题的实现方式随着个人风格不同而不同。典型的类似代码如下:

 int *ptr;
 ptr = (int *)0x67a9;
 *ptr = 0xaa55;

 A more obscure approach is:
一个较晦涩的方法是:

*(int * const)(0x67a9) = 0xaa55;

即使你的品味更接近第二种方案,但我建议你在面试时使用第一种方案。

中断(Interrupts)

11. 中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标准C支持中断。具代表事实是,产生了一个新的关键字 __interrupt。下面的代码就使用了__interrupt关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。

__interrupt double compute_area (double radius)
{
double area = PI * radius * radius;
printf("\nArea = %f", area);
return area;
}

这个函数有太多的错误了,以至让人不知从何说起了:
    ISR 不能返回一个值。如果你不懂这个,那么你不会被雇用的。
    ISR 不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项。
    在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。
    与第三点一脉相承,printf()经常有重入和性能上的问题。如果你丢掉了第三和第四点,我不会太为难你的。不用说,如果你能得到后两点,那么你的被雇用前景越来越光明了。

*****
代码例子(Code examples)

12 . 下面的代码输出是什么,为什么?

void foo(void)
{
   unsigned int a = 6;
   int b = -20;
   (a+b > 6) ? puts("> 6") : puts("<= 6");
}
这个问题测试你是否懂得C语言中的整数自动转换原则,我发现有些开发者懂得极少这些东西。不管如何,这无符号整型问题的答案是输出是 ”>6”。原因是当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。因此-20变成了一个非常大的正整数,所以该表达式计算出的结果大于6。这一点对于应当频繁用到无符号数据类型的嵌入式系统来说是丰常重要的。如果你答错了这个问题,你也就到了得不到这份工作的边缘。
13. 评价下面的代码片断:

unsigned int zero = 0;
unsigned int compzero = 0xFFFF;
/*1's complement of zero */

对于一个int型不是16位的处理器为说,上面的代码是不正确的。应编写如下:

unsigned int compzero = ~0;

这一问题真正能揭露出应试者是否懂得处理器字长的重要性。在我的经验里,好的嵌入式程序员非常准确地明白硬件的细节和它的局限,然而PC机程序往往把硬件作为一个无法避免的烦恼。
到了这个阶段,应试者或者完全垂头丧气了或者信心满满志在必得。如果显然应试者不是很好,那么这个测试就在这里结束了。但如果显然应试者做得不错,那么我就扔出下面的追加问题,这些问题是比较难的,我想仅仅非常优秀的应试者能做得不错。提出这些问题,我希望更多看到应试者应付问题的方法,而不是答案。不管如何,你就当是这个娱乐吧

动态内存分配(Dynamic memory allocation)
14. 尽管不像非嵌入式计算机那么常见,嵌入式系统还是有从堆(heap)中动态分配内存的过程的。那么嵌入式系统中,动态分配内存可能发生的问题是什么?
这里,我期望应试者能提到内存碎片,碎片收集的问题,变量的持行时间等等。这个主题已经在ESP杂志中被广泛地讨论过了(主要是 P.J. Plauger, 他的解释远远超过我这里能提到的任何解释),所有回过头看一下这些杂志吧!让应试者进入一种虚假的安全感觉后,我拿出这么一个小节目:
下面的代码片段的输出是什么,为什么?

char *ptr;
   if ((ptr = (char *)malloc(0)) == NULL)

        puts("Got a null pointer");
   else
       puts("Got a valid pointer");

这是一个有趣的问题。最近在我的一个同事不经意把0值传给了函数malloc,得到了一个合法的指针之后,我才想到这个问题。这就是上面的代码,该代码的输出是“Got a valid pointer”。我用这个来开始讨论这样的一问题,看看被面试者是否想到库例程这样做是正确。得到正确的答案固然重要,但解决问题的方法和你做决定的基本原理更重要些。
Typedef
:
15 Typedef C语言中频繁用以声明一个已经存在的数据类型的同义字。也可以用预处理器做类似的事。例如,思考一下下面的例子:

#define dPS struct s *
typedef struct s * tPS;

以上两种情况的意图都是要定义dPS 和 tPS 作为一个指向结构s指针。哪种方法更好呢?(如果有的话)为什么?
这是一个非常微妙的问题,任何人答对这个问题(正当的原因)是应当被恭喜的。答案是:typedef更好。思考下面的例子:

dPS p1,p2;
tPS p3,p4;

第一个扩展为

struct s * p1, p2;
.
上面的代码定义p1为一个指向结构的指,p2为一个实际的结构,这也许不是你想要的。第二个例子正确地定义了p3 和p4 两个指针。

晦涩的语法

16 . C语言同意一些令人震惊的结构,下面的结构是合法的吗,如果是它做些什么?

int a = 5, b = 7, c;
c = a+++b;

这个问题将做为这个测验的一个愉快的结尾。不管你相不相信,上面的例子是完全合乎语法的。问题是编译器如何处理它?水平不高的编译作者实际上会争论这个问题,根据最处理原则,编译器应当能处理尽可能所有合法的用法。因此,上面的代码被处理成:

c = a++ + b;

因此, 这段代码持行后a = 6, b = 7, c = 12。
如果你知道答案,或猜出正确答案,做得好。如果你不知道答案,我也不把这个当作问题。我发现这个问题的最大好处是这是一个关于代码编写风格,代码的可读性,代码的可修改性的好的话题。
好了,伙计们,你现在已经做完所有的测试了。这就是我出的C语言测试题,我怀着愉快的心情写完它,希望你以同样的心情读完它。如果是认为这是一个好的测试,那么尽量都用到你的找工作的过程中去吧。天知道也许过个一两年,我就不做现在的工作,也需要找一个。
Nigel Jones 是一个顾问,现在住在Maryland,当他不在水下时,你能在多个范围的嵌入项目中找到他。 他很高兴能收到读者的来信,他的email地址是:     NAJones@compuserve.com 。
References
    Jones, Nigel, "In Praise of the #error directive," Embedded Systems Programming, September 1999, p. 114.
    Jones, Nigel, " Efficient C Code for Eight-bit MCUs ," Embedded Systems Programming, November 1998, p. 66.

10>:认识(大端--小端)端模式

端模式(Endian)的这个词出自Jonathan Swift书写的《格列佛游记》。这本书根据将鸡蛋敲开的方法不同将所有的人分为两类,从圆头开始将鸡蛋敲开的人被归为Big Endian,从尖头开始将鸡蛋敲开的人被归为Littile Endian。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开。在计算机业Big Endian和Little Endian也几乎引起一场战争。在计算机业界,Endian表示数据在存储器中的存放顺序。下文举例说明在计算机中大小端模式的区别。

    如果将一个32位的整数0x12345678存放到一个整型变量(int)中,这个整型变量采用大端或者小端模式在内存中的存储由下表所示。为简单起见,这里使用OP0表示一个32位数据的最高字节MSB(Most Significant Byte),使用OP3表示一个32位数据最低字节LSB(Least Significant Byte)。
 
地址偏移   大端模式    小端模式 
0x00      12(OP0)   78(OP3) 
0x01      34(OP1)   56(OP2) 
0x02      56(OP2)   34(OP1) 
0x03      78(OP3)   12(OP0) 

    如果将一个16位的整数0x1234存放到一个短整型变量(short)中。这个短整型变量在内存中的存储在大小端模式由下表所示。 

地址偏移    大端模式    小端模式 
0x00       12(OP0)   34(OP1) 
0x01       34(OP1)   12(OP0) 

    由上表所知,采用大小模式对数据进行存放的主要区别在于在存放的字节顺序,大端方式将高位存放在低地址,小端方式将低位存放在低地址。采用大端方式进行数据存放符合人类的正常思维,而采用小端方式进行数据存放利于计算机处理。到目前为止,采用大端或者小端进行数据存放,其孰优孰劣也没有定论。
    有的处理器系统采用了小端方式进行数据存放,如Intel的奔腾。有的处理器系统采用了大端方式进行数据存放,如IBM半导体和Freescale的PowerPC处理器。不仅对于处理器,一些外设的设计中也存在着使用大端或者小端进行数据存放的选择。
    因此在一个处理器系统中,有可能存在大端和小端模式同时存在的现象。这一现象为系统的软硬件设计带来了不小的麻烦,这要求系统设计工程师,必须深入理解大端和小端模式的差别。大端与小端模式的差别体现在一个处理器的寄存器,指令集,系统总线等各个层次中。

判断大端小端

    int i=1;  
    char *p=(char *)&i;  
    if(*p==1)    
           printf("1"); 
    else
           printf("2");

      大小端存储问题,如果小端方式中(i占至少两个字节的长度)则i所分配的内存最小地址那个字节中就存着1,其他字节是0.大端的话则1i的最高地址字节处存放,char是一个字节,所以强制将char型量p指向ip指向的一定是i的最低地址,那么就可以判断p中的值是不是1来确定是不是小端。

请写一个C函数,若处理器是Big_endian的,则返回0;若是Little_endian的,则返回1

解答:

int checkCPU( )

{

    {

     union w

      {  

         int a;

         char b;

      } c;

      c.a = 1;

      return(c.b ==1);

    }

}

剖析:

嵌入式系统开发者应该对Little-endian和Big-endian模式非常了解。采用Little-endian模式的CPU对操作数的存放方式是从低字节到高字节,而Big-endian模式对操作数的存放方式是从高字节到低字节。例如,16bit宽的数0x1234在Little-endian模式CPU内存中的存放方式(假设从地址0x4000开始存放)为:

 

内存地址

0x4000

0x4001

存放内容

0x34

0x12

而在Big-endian模式CPU内存中的存放方式则为

内存地址

0x4000

0x4001

存放内容

0x12

0x34

32bit宽的数0x12345678在Little-endian模式CPU内存中的存放方式(假设从地址0x4000开始存放)为:

内存地址

0x4000

0x4001

0x4002

0x4003

存放内容

0x78

0x56

0x34

0x12

而在Big-endian模式CPU内存中的存放方式则为:

内存地址

0x4000

0x4001

0x4002

0x4003

存放内容

0x12

0x34

0x56

0x78

联合体union的存放顺序是所有成员都从低地址开始存放,面试者的解答利用该特性,轻松地获得了CPU对内存采用Little-endian还是Big-endian模式读写。

 

11>: C语言程序编译的内存分配,堆与栈的区别

C语言程序编译的内存分配:
1.栈区(stack) --编译器自动分配释放,主要存放函数的参数值,局部变量值等;
2.堆区(heap) --由程序员分配释放;
3.全局区或静态区 --存放全局变量和静态变量;程序结束时由系统释放,分为全局初始化区和全局未初始化区;
4.字符常量区 --常量字符串放与此,程序结束时由系统释放;
5.程序代码区

例:  //main.c

int a=0;        //全局初始化区

char *p1;       //全局未初始化区

void main()

{

   int b;          //栈

   char s[]="bb";  //栈

   char *p2;       //栈

   char *p3="123"; //其中,“123\0”常量区,p3在栈区

   static int c=0; //全局区

   p1=(char*)malloc(10);   //10个字节区域在堆区

   strcpy(p1,"123");    //"123\0"在常量区,编译器 可能 会优化为和p3的指向同一块区域

}

 

 

12>:浮点数在内存中的存储格式

浮点数:
    浮点型变量在计算机内存中占用4字节(Byte),32-bit。遵循IEEE-754格式标准。一个浮点数由2部分组成:底数m 和指数e。 ±mantissa × 2exponent
(注意,公式中的mantissa 和 exponent使用二进制表示)
   底数部分 使用2进制数来表示此浮点数的实际值。
   指数部分 占用8-bit的二进制数,可表示数值范围为0-255。
   指数应可正可负,所以IEEE规定,此处算出的次方须减去127才是真正的指数。所以float的指数可从 -126到128
   底数部分实际是占用24-bit的一个值,由于其最高位始终为 1 ,所以最高位省去不存储,在存储中只有23-bit。
   到目前为止, 底数部分 23位加上指数部分 8位使用了31位。那么前面说过,float是占用4个字节即32-bit,那么还有一位是干嘛用的呢? 还有一位,其实就是4字节中的最高位,用来指示浮点数的正负,当最高位是1时,为负数,最高位是0时,为正数。

   浮点数据就是按下表的格式存储在4个字节中:

   Address+0 Address+1 Address+2 Address+3

   Contents SEEE EEEE EMMM MMMM MMMM MMMM MMMM MMMM
 S: 表示浮点数正负,1为负数,0为正数
 E: 指数加上127后的值的二进制数
 M: 24-bit的底数(只存储23-bit)

   注意:这里有个特例,浮点数为0时,指数和底数都为0,但此前的公式不成立。因为2的0次方为1,所以,0是个特例。当然,这个特例也不用认为去干扰,编译器会自动去识别。
   举例1:计算机存储中的二进制数如何转换成实际浮点数
   通过上面的格式,我们下面举例看下-12.5在计算机中存储的具体数据:
Address+0 Address+1 Address+2 Address+3 Contents 0xC1 0x48 0x00 0x00
   接下来我们验证下上面的数据表示的到底是不是-12.5,从而也看下它的转换过程。 

   由于浮点数不是以直接格式存储,他有几部分组成,所以要转换浮点数,首先要把各部分的值分离出来。

   Address+0 Address+1 Address+2 Address+3

   格式 SEEEEEEE EMMMMMMM MMMMMMMM MMMMMMMM

   二进制 11000001 01001000 00000000 00000000

   16进制 C1 48 00 00

 可见: 

S: 1,是个负数。

E: 10000010 转为10进制为130,130-127=3,即实际指数部分为3. 

M: 10010000000000000000000。这里,在底数左边省略存储了一个1,使用实际底数表示为 1.10010000000000000000000

   到此,我们吧三个部分的值都拎出来了,现在,我们通过指数部分E的值来调整底数部分M的值。调整方法为:如果指数E为负数,底数的小数点向左移,如果指数E为正数,
   底数的小数点向右移。小数点移动的位数由指数E的绝对值决定。

   这里,E为正3,使用向右移3为即得:

   1100.10000000000000000000

   至次,这个结果就是12.5的二进制浮点数,将他换算成10进制数就看到12.5了,如何转换,看下面:

   小数点左边的1100 表示为 (1 × 23) + (1 × 22) + (0 × 21) + (0 × 20), 其结果为 12 。

   小数点右边的 .100… 表示为 (1 × 2-1) + (0 × 2-2) + (0 × 2-3) + ... ,其结果为.5 。

   以上二值的和为12.5, 由于S 为1,使用为负数,即-12.5 。所以,16进制 0XC1480000 是浮点数 -12.5 。
举例2:浮点数装换成计算机存储格式中的二进制数。
   举例将 17.625换算成 float型。

   首 先,将17.625换算成二进制位:10001.101 ( 0.625 = 0.5+0.125, 0.5即 1/2, 0.125即1/8 如果不会将小数部分转换成二进制,请参考其他书籍)
   再将 10001.101 向左移,直到小数点前只剩一位成了 1.0001101 x 2的4次方(因为左移了4位)。此时我们的底数M和指数E就出来了:
   底数部分M,因为小数点前必为1,所以IEEE规定只记录小数点后的就好,所以此处底数为 0001101 。

   指数部分E,实际为4,但须加上127,固为131,即二进制数 10000011 符号部分S,由于是正数,所以S为0.
综上所述,17.625的 float 存储格式就是:

 0 10000011 00011010000000000000000

 转换成16进制:0x41 8D 00 00

 所以,一看,还是占用了4个字节。

 

13>: C/C++中修饰符constexternstaticvolatile的用法

 

C/C++中修饰符const、extern、static、volatile的用法

1.const的用法:

为什么使用const

采用符号常量写出的代码更容易维护;

指针常常是边读边移动,而不是边写边移动;

许多函数参数是只读不写的。

const最常见用途是作为数组的界和switch分情况标号(也可以用枚举符代替)

用法1:常量

取代了C中的宏定义,声明时必须进行初始化。const限制了常量的使用方式,并没有描述常量应该如何分配。如果编译器知道了某const的所有使用,它甚至可以不为该const分配空间。最简单的常见情况就是常量的值在编译时已知,而且不需要分配存储。C++ Program Language

const声明的变量虽然增加了分配空间,但是可以保证类型安全。

C标准中,const定义的常量是全局的,C++中视声明位置而定。

用法2:指针和常量

使用指针时涉及到两个对象:该指针本身和被它所指的对象。将一个指针的声明用const预先固定将使那个对象而不是使这个指针成为常量。要将指针本身而不是被指对象声明为常量,必须使用声明运算符*const

所以出现在 * 之前的const是作为基础类型的一部分:

char *const cp;   // charconst指针
char const *pc1;   // const char的指针
const char *pc2; // const char的指针(后两个声明是等同的)

从右向左读的记忆方式:

cp is a const pointer to char.
pc2 is a pointer to const char.

用法3const修饰函数传入参数

将函数传入参数声明为const,以指明使用这种参数仅仅是为了效率的原因,而不是想让调用函数能够修改对象的值。同理,将指针参数声明为const,函数将不修改由这个参数所指的对象。

通常修饰指针参数和引用参数:

void Fun( const A *in); // 修饰指针型传入参数
void Fun(const A &in);   // 修饰引用型传入参数

用法4:修饰函数返回值

可以阻止用户修改返回值。返回值也要相应的付给一个常量或常指针。

用法5const修饰成员函数

const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数;

const对象的成员是不能修改的,而通过指针维护的对象却是可以修改的;

const成员函数不可以修改对象的数据,不管对象是否具有const性质。编译时以是否修改成员数据为依据进行检查。


2.static的用法:

静态变量作用范围在一个文件内,程序开始时分配空间,结束时释放空间,默认初始化为0,使用时可以改变其值。

静态变量或静态函数只有本文件内的代码才能访问它,它的名字在其它文件中不可见。

用法1:函数内部声明的static变量,可作为对象间的一种通信机制

如果一局部变量被声明为static,那么将只有唯一的一个静态分配的对象,它被用于在该函数的所有调用中表示这个变量。这个对象将只在执行线程第一次到达它的定义时初始化。

用法2:局部静态对象

对于局部静态对象,构造函数是在控制线程第一次通过该对象的定义时调用。在程序结束时,局部静态对象的析构函数将按照他们被构造的相反顺序逐一调用,没有规定确切时间。

用法3:静态成员和静态成员函数

如果一个变量是类的一部分,但却不是该类的各个对象的一部分,它就被成为是一个static静态成员。一个static成员只有唯一的一份副本,而不像常规的非static成员那样在每个对象里各有一份副本。同理,一个需要访问类成员,而不需要针对特定对象去调用的函数,也被称为一个static成员函数。

类的静态成员函数只能访问类的静态成员(变量或函数)。

3.extern的用法

extern可以声明其他文件内定义的变量。在一个程序里,一个对象只能定义一次,它可以有多个声明,但类型必须完全一样。如果定义在全局作用域或者名字空间作用域里某一个变量没有初始化,它会被按照默认方式初始化。

将变量或函数声明成外部链接,即该变量或函数名在其它函数中可见。被其修饰的变量(外部变量)是静态分配空间的,即程序开始时分配,结束时释放。

C++中,还可以指定使用另一语言链接,需要与特定的转换符一起使用。

extern “C” 声明语句
extern “C” { 声明语句块 }

extern 表明该变量在别的地方已经定义过了,在这里要使用那个变量。

static 表示静态的变量,分配内存的时候,存储在静态区,不存储在栈上面。static 作用范围是内部连接的关系,和extern有点相反,它和对象本身是分开存储的,extern也是分开存储的,但是extern可以被其他的对象用extern 引用,而static 不可以,只允许对象本身用它。

4.volatile的用法

类型修正符(type-modifier),限定一个对象可被外部进程(操作系统、硬件或并发进程等)改变。volatile与变量连用,可以让变量被不同的线程访问和修改。

声明时语法:

int volatile vInt;

常用于像中断处理程序之类的异步进程进行内存单元访问。

除了基本类型外,对用户定义类型也可以用volatile类型进行修饰。

注意:可以把一个非volatile int赋给volatile int,但是不能把非volatile对象赋给一个volatile对象。

一个有volatile标识符的类只能访问它接口的子集,一个由类的实现者控制的子集。用户只能用const_cast来获得对类型接口的完全访问。此外,volatileconst一样会从类传递到它的成员。

14>:全局变量、静态全局变量、静态局部变量和局部变量

 

术语static有着不寻常的历史.起初,在C中引入关键字static是为了表示退出一个块后仍然存在的局部变量。随后,static在C中有了第二种含义:用来表示不能被其它文件访问的全局变量和函数。为了避免引入新的关键字,所以仍使用static关键字来表示这第二种含义。最后,C++重用了这个关键字,并赋予它与前面不同的第三种含义:表示属于一个类而不是属于此类的任何特定对象的变量和函数(Java中此关键字的含义相同)
   变量可以分为:全局变量、静态全局变量、静态局部变量和局部变量。
   按存储区域分:全局变量、静态全局变量和静态局部变量都存放在内存的静态存储区域,局部变量存放在内存的栈区。
   按作用域分:全局变量在整个工程文件内都有效;静态全局变量只在定义它的文件内有效;静态局部变量只在定义它的函数内有效,只是程序仅分配一次内存,函数返回后,该变量不会消失;局部变量在定义它的函数内有效,但是函数返回后失效。
   全局变量(外部变量)的说明之前再冠以static 就构成了静态的全局变量。全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式。 这两者在存储方式上并无不同。这两者的区别虽在于非静态全局变量的作用域是整个源程序, 当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的。 而静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它。由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用, 因此可以避免在其它源文件中引起错误。 
        
    从以上分析可以看出, 把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域, 限制了它的使用范围。
     
    static函数与普通函数作用域不同。仅在本文件。只在当前源文件中使用的函数应该说明为内部函数(static),内部函数应该在当前源文件中说明和定义。对于可在当前源文件以外使用的函数,应该在一个头文件中说明,要使用这些函数的源文件要包含这个头文件.
static全局变量与普通的全局变量有什么区别?static全局变量只初使化一次,防止在其他文件单元中被引用;
static局部变量和普通局部变量有什么区别?static局部变量只被初始化一次,下一次依据上一次结果值;
   static函数与普通函数有什么区别?static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝
   全局变量和静态变量如果没有手工初始化,则由编译器初始化为0。

(a)若程序由一个源文件构成时,全局变量与全局静态变量没有区别。
(b)若程序由多个源文件构成时,全局变量与全局静态变量不同:全局静态变量使得该变量成为定义该变量的源文件所独享,即:全局静态变量对组成该程序的其它源文件是无效的。
静态全局变量的作用:
a)不必担心其它源文件使用相同变量名,彼此相互独立。
b)在某源文件中定义的静态全局变量不能被其他源文件使用或修改。
例如:一个程序由两个源文件组成,其中在一个源文件中定义了“int n;”,在另一个源文件中定义了“static int n;”则程序给它们分别分配了不同的空间,两个值互不干扰。
例如:下面在file1.cpp中声明全局变量n,在file2.cpp中定义全局静态变量n。文件file1.cpp和file2.cpp单独编译都能通过,但连接时,file1.cpp中的变量n找不到定义,产生连接错误。
// file1.cpp

# include <iostream.h> 
void fn() 
extern int n; 
void main() 
{ 
n=20; 
cout<<n<<endl; 
fn(); 
} 
// file2.cpp

# include <iostream.h> 
static int n; // 默认初始化为0,注意此处定义的n 只能在file2.cpp中使用。

void fn() 
{ 
n++; 
cout<<n<<endl; 
} 
静态函数:使某个函数只在一个源文件中有效,不能被其他源文件所用。
定义:在函数前面加上static。
说明:函数的声明和定义默认情况下在整个程序中是extern的。
静态函数的效果:
1)它允其他源文件建立并使用同名的函数,而不相互冲突。
2) 声明为静态的函数不能被其他源文件所调用,因为它的名字不能得到。
    
拙见:
静态变量和函数一般都局限于一个编译单元也就是.cpp文件中。
我想这是最主要的区别。

 

15>: C语言出错信息速查

Ambiguous operators need parentheses
不明确的运算需要用括号括起
Ambiguous symbol ``xxx``
不明确的符号
Argument list syntax error
参数表法
Array bounds missing
丢失数界限符
Array size toolarge
数尺寸太大
Bad character in paramenters
参数中有不适当的字符
Bad file name format in include directive
包含命令中文件名格式不正确
Bad ifdef directive synatax
编译预处理ifdef有法
Bad undef directive syntax
编译预处理undef有法
Bit field too large
位字段太
Call of non-function
调用未定的函数
Call to function with no prototype
调用函数没有函数的明
Cannot modify a const object
不允修改常量象
Case outside of switch
漏掉了case 语句
Case syntax error
Case 语法
Code has no effect
代不可述不可能行到
Compound statement missing{
分程序漏掉"{"
Conflicting type modifiers
不明确的型明符
Constant expression required
要求常量表达式
Constant out of range in comparison
在比中常量超出范
Conversion may lose significant digits
转换时会失意的数字
Conversion of near pointer not allowed
不允近指
Could not find file ``xxx``
找不到XXX文件
Declaration missing ;
说明缺少";"
Declaration syntax error
说明中出法
Default outside of switch
Default 出在switch 语句之外
Define directive needs an identifier
定理需要符
Division by zero
用零作除数
Do statement must have while
Do-while 语句中缺少while部分
Enum syntax error
枚型法
Enumeration constant syntax error
枚常数法
Error directive :xxx
错误的理命令
Error writing output file
写出文件
Expression syntax error
表达式法
Extra parameter in call
调用出多余
File name too long
文件名太
Function call missing )
函数用缺少右括号
Fuction definition out of place
函数定位置
Fuction should return a value
函数必需返回一个
Goto statement missing label
Goto 语句没有号
Hexadecimal or octal constant too large
16 进制或8 进制常数太大
Illegal character ``x``
非法字符x
Illegal initialization
非法的初始化
Illegal octal digit
非法的8 进制数字
Illegal pointer subtraction
非法的指相减
Illegal structure operation
非法的构体操作
Illegal use of floating point
非法的浮点运算
Illegal use of pointer
指使用非法
Improper use of a typedefsymbol
类型定符号使用不恰当
In-line assembly not allowed
不允使用行
Incompatible storage class
存不相容
Incompatible type conversion
不相容的型
Incorrect number format
错误的数据格式
Incorrect use of default
Default使用不当
Invalid indirection
无效的接运算
Invalid pointer addition
指相加无效
Irreducible expression tree
无法行的表达式运算
Lvalue required
需要0或非0 值
Macro argument syntax error
宏参数法
Macro expansion too long
宏的展以后太
Mismatched number of parameters in definition
定中参数个数不匹配
Misplaced break
此不出break 语句
Misplaced continue
此不出continue 语句
Misplaced decimal point
此不出小数点
Misplaced elif directive
不理elif
Misplaced else
此不出else
Misplaced else directive
此不出理else
Misplaced endif directive
此不出理endif
Must be addressable
必是可以址的
Must take address of memory location
必存定位的地址
No declaration for function ``xxx``
没有函数xxx的明
No stack
缺少堆
No type information
没有型信息
Non-portable pointer assignment
不可移的指(地址常数)
Non-portable pointer comparison
不可移的指(地址常数)比
Non-portable pointer conversion
不可移的指(地址常数)
Not a valid expression format type
不合法的表达式格式
Not an allowed type
不允使用的型
Numeric constant too large
数常太大
Out of memory
内存不用
Parameter ``xxx`` is never used
能数xxx没有用到
Pointer required on left side of ->
符号->的左必是指
Possible use of ``xxx`` before definition
在定之前就使用了xxx(警告)
Possibly incorrect assignment
赋值可能不正确
Redeclaration of ``xxx``
重定了xxx
Redefinition of ``xxx`` is not identical
xxx的两次定不一致
Register allocation failure
寄存器定址失
Repeat count needs an lvalue
重数需要
Size of structure or array not known
结构体或数大小不确定
Statement missing ;
语句后缺少";"
Structure or union syntax error
结构体或合体法
Structure size too large
结构体尺寸太大
Sub scripting missing ]
下缺少右方括号
Superfluous & with function or array
函数或数中有多余的"&"
Suspicious pointer conversion
可疑的指
Symbol limit exceeded
符号超限
Too few parameters in call
函数用的参少于函数的参数不
Too many default cases
Default太多(switch 语句中一个)
Too many error or warning messages
错误或警告信息太多
Too many type in declaration
说明中型太多
Too much auto memory in function
函数用到的局部存太多
Too much global data defined in file
文件中全局数据太多
Two consecutive dots
两个的句点
Type mismatch in parameter xxx
参数xxx 类型不匹配
Type mismatch in redeclaration of ``xxx``
xxx重定的型不匹配
Unable to create output file ``xxx``
无法建立出文件xxx
Unable to open include file ``xxx``
无法打被包含的文件xxx
Unable to open input file ``xxx``
无法打入文件xxx
Undefined label ``xxx``
没有定的号xxx
Undefined structure ``xxx``
没有定的构xxx
Undefined symbol ``xxx``
没有定的符号xxx
Unexpected end of file in comment started on line xxx
xxx行始的注解尚未束文件不能束
Unexpected end of file in conditional started on line xxx
xxx 开始的条件句尚未束文件不能束
Unknown assemble instruction
未知的构
Unknown option
未知的操作
Unknown preprocessor directive: ``xxx``
不的理命令xxx
Unreachable code
无路可达的代
Unterminated string or character constant
字符串缺少引号
User break
强用行中断了程序
Void functions may not return a value
Void 类型的函数不有返回
Wrong number of arguments
调用函数的参数数目
``xxx`` not an argument
xxx不是参数
``xxx`` not part of structure
xxx不是构体的一部分
xxx statement missing (
xxx 语句缺少左括号
xxx statement missing )
xxx 语句缺少右括号
xxx statement missing ;
xxx缺少分号
xxx`` declared but never used
说明了xxx但没有使用
xxx`` is assigned a value which is never used
xxx 赋了但未用
Zero length structure
结构体的度零

 

16>: C/C+语言struct 深层探索

1. struct 的巨大作用


面对一个人的大型 C/C++程序时,只看其对struct 的使用情况我们就可以对其编写者的编程经
验进行评估。因为一个大型的C/C++程序,势必要涉及一些(甚至大量)进行数据组合的结构体,这些结
构体可以将原本意义属于一个整体的数据组合在一起。从某种程度上来说,会不会用struct,怎样用
struct 是区别一个开发人员是否具备丰富开发经历的标志。
在网络协议、通信控制、嵌入式系统的C/C++编程中,我们经常要传送的不是简单的字节流(char
型数组),而是多种数据组合起来的一个整体,其表现形式是一个结构体。
经验不足的开发人员往往将所有需要传送的内容依顺序保存在char 型数组中,通过指针偏移的
方法传送网络报文等信息。这样做编程复杂,易出错,而且一旦控制方式及通信协议有所变化,程序
就要进行非常细致的修改。
一个有经验的开发者则灵活运用结构体,举一个例子,假设网络或控制协议中需要传送三种报
文,其格式分别为packetA、packetB、packetC:
struct structA
{
int a;
char b;
};
struct structB
{
char a;
short b;
};
struct structC
{
int a;
char b;
float c;
}
优秀的程序设计者这样设计传送的报文:
struct CommuPacket
{
3
int iPacketType; //报文类型标志

union //每次传送的是三种报文中的一种,使用union

{
struct structA packetA; struct structB packetB;
struct structC packetC;
}
};
在进行报文传送时,直接传送struct CommuPacket 一个整体。
假设发送函数的原形如下:
// pSendData:发送字节流的首地址,iLen:要发送的长度

Send(char * pSendData, unsigned int iLen);
发送方可以直接进行如下调用发送struct CommuPacket 的一个实例sendCommuPacket:
Send( (char *)&sendCommuPacket , sizeof(CommuPacket) );
假设接收函数的原形如下:
// pRecvData:发送字节流的首地址,iLen:要接收的长度

//返回值:实际接收到的字节数

unsigned int Recv(char * pRecvData, unsigned int iLen)
接收方可以直接进行如下调用将接收到的数据保存在struct CommuPacket 的一个实例recvCommuPacket 中:
Recv( (char *)&recvCommuPacket , sizeof(CommuPacket) );
接着判断报文类型进行相应处理:
switch(recvCommuPacket. iPacketType)
{
case PACKET_A:
//A 类报文处理

break;
case PACKET_B:
//B 类报文处理

break;
case PACKET_C:
//C 类报文处理

break;
}
以上程序中最值得注意的是
Send( (char *)&sendCommuPacket , sizeof(CommuPacket) );
Recv( (char *)&recvCommuPacket , sizeof(CommuPacket) );
中的强制类型转换:(char *)&sendCommuPacket、(char *)&recvCommuPacket,先取地址,再转化为char 型指针,
这样就可以直接利用处理字节流的函数。
利用这种强制类型转化,我们还可以方便程序的编写,例如要对sendCommuPacket 所处内存初始化为0,可以这
样调用标准库函数memset()
memset((char *)&sendCommuPacket,0, sizeof(CommuPacket));


2. struct的成员对齐


Intel、微软等公司曾经出过一道类似的面试题:
#include <iostream.h>
4
#pragma pack(8)
struct example1
{
short a;
long b;
};
struct example2
{
char c;
example1 struct1;
short e;
};
#pragma pack()
int main(int argc, char* argv[])
{
example2 struct2;
cout << sizeof(example1) << endl;
cout << sizeof(example2) << endl;
cout << (unsigned int)(&struct2.struct1) - (unsigned int)(&struct2) << endl;
return 0;
}
问程序的输入结果是什么?
答案是:
8
16
4
不明白?还是不明白?下面一一道来:
2.1 自然对界
struct 是一种复合数据类型,其构成元素既可以是基本数据类型(如intlongfloat 等)的变量,也可以是
一些复合数据类型(如arraystructunion 等)的数据单元。对于结构体,编译器会自动进行成员变量的对齐,
以提高运算效率。缺省情况下,编译器为结构体的每个成员按其自然对界(natural alignment)条件分配空间。各
个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。
自然对界(natural alignment)即默认对齐方式,是指按结构体的成员中size 最大的成员对齐。
例如:
struct naturalalign
{
char a;
short b;
char c;
};
在上述结构体中,size 最大的是short,其长度为2 字节,因而结构体中的char 成员ac 都以2 为单位对齐,
sizeof(naturalalign)的结果等于6
如果改为:
struct naturalalign
5
{
char a;
int b;
char c;
};
其结果显然为12
2.2 指定对界
一般地,可以通过下面的方法来改变缺省的对界条件:
· 使用伪指令#pragma pack (n),编译器将按照n 个字节对齐;
· 使用伪指令#pragma pack (),取消自定义字节对齐方式。
注意:如果#pragma pack (n)中指定的n 大于结构体中最大成员的size,则其不起作用,结构体
仍然按照size 最大的成员进行对界。
例如:
#pragma pack (n)
struct naturalalign
{
char a;
int b;
char c;
};
#pragma pack ()
n 4816 时,其对齐方式均一样,sizeof(naturalalign)的结果都等于12。而当n 2
时,其发挥了作用,使得sizeof(naturalalign)的结果为6
VC++ 6.0 编译器中,我们可以指定其对界方式(见图1),其操作方式为依次选择projetct >
setting > C/C++菜单,在struct member alignment 中指定你要的对界方式。
1 VC++ 6.0 中指定对界方式

这个地方有个图,没弄好

 

另外,通过__attribute((aligned (n)))也可以让所作用的结构体成员对齐在n 字节边界上,但是它较少被使用,因而不作详细讲解。
2.3 面试题的解答
至此,我们可以对Intel、微软的面试题进行全面的解答。
   程序中第2 行#pragma pack (8)虽然指定了对界为8,但是由于struct example1 中的成员最大size 为4(long 变量size 为4),故struct example1 仍然按4 字节对界,struct example1 的size为8,即第18 行的输出结果;
   struct example2 中包含了struct example1,其本身包含的简单数据成员的最大size 为2(short 变量e),但是因为其包含了struct example1,而struct example1 中的最大成员size 为4,struct example2 也应以4 对界,#pragma pack (8)中指定的对界对struct example2 也不起作用,故19 行的输出结果为16;
由于struct example2 中的成员以4 为单位对界,故其char 变量c 后应补充3 个空,其后才是成员struct1 的内存空间,20 行的输出结果为4。


3. C C++struct 的深层区别

    C++语言中struct 具有了的功能,其与关键字class 的区别在于struct 中成员变量和函数的默认访问权限为public,而class 的为private
例如,定义struct 类和class 类:
struct structA
{
char a;

}
class classB
{
char a;

}
则:
structA a;
a.a = 'a'; //访问public 成员,合法


classB b;
b.a = 'a'; //访问private 成员,不合法


   许多文献写到这里就认为已经给出了C++struct class 的全部区别,实则不然,另外一点
需要注意的是:
    C++中的struct 保持了对C struct 的全面兼容(这符合C++的初衷——“a better c”),
因而,下面的操作是合法的:
//定义struct


struct structA
{
char a;
char b;
int c;
};
7
structA a = {'a' , 'a' ,1}; // 定义时直接赋初值


   struct 可以在定义的时候直接以{ }对其成员变量赋初值,而class 则不能,在经典书目《thinking C++ 2nd edition》中作者对此点进行了强调。
4. struct 编程注意事项
看看下面的程序:
1. #include <iostream.h>
2. struct structA
3. {
4. int iMember;
5. char *cMember;
6. };
7. int main(int argc, char* argv[])
8.{
9. structA instant1,instant2;
10. char c = 'a';
11. instant1.iMember = 1;
12. instant1.cMember = &c;
13. instant2 = instant1;
14. cout << *(instant1.cMember) << endl;
15. *(instant2.cMember) = 'b';
16. cout << *(instant1.cMember) << endl;
17. return 0;
}
14 行的输出结果是:a
16 行的输出结果是:b
   Why?我们在15 行对instant2 的修改改变了instant1 中成员的值!
   原因在于13 行的instant2 = instant1 赋值语句采用的是变量逐个拷贝,这使得instant1 instant2 中的cMember 指向了同一片内存,因而对instant2 的修改也是对instant1 的修改。
   C 语言中,当结构体中存在指针型成员时,一定要注意在采用赋值语句时是否将2 个实例中的指针型成员指向了同一片内存。

   C++语言中,当结构体中存在指针型成员时,我们需要重写struct 的拷贝构造函数并进行“=”操作符重载。

 

17>:考查嵌入式C开发人员的最好的0x10道题

非常基本关于C语言的问题,一个信息类(计算机,资讯工程,电子工程, 通信工程)专业的本科毕业生应该达到的水平。题目不难,全部都能快速地答完,当然也需要一定的知识储备。
       对于大多数人,我们预期你可能答错 3) 4) 15)题,所以答错3道以内的,我们认为你很棒
       答错5道题以内,我们认为你还不错(你还可能答错第9题)
       如果你有6道以上的题目不能答对,基本上我们都不好说什么了....

约定:
1) 下面的测试题中,认为所有必须的头文件都已经正确的包含了
2) 数据类型
      char 一个字节 1 byte
      int 两个字节 2 byte (16位系统,认为整型是2个字节)
      long int 四个字节 4 byte
      float 四个字节4 byet
      double 八个字节 8 byte
      long double 十个字节 10 byte
      pointer 两个字节 2 byte(注意,16位系统,地址总线只有16位)


1题: 考查对volatile关键字的认识

#include<setjmp.h>
static jmp_buf buf;

main() 
{
volatile int b;
b =3;

if(setjmp(buf)!=0) 
{
    printf("%d ", b); 
    exit(0);
}
b=5;
longjmp(buf , 1);
} 

请问,这段程序的输出是
(a) 3
(b) 5
(c) 0
(d) 以上均不是

2题:考查类型转换

main()
{
   struct node
   {
     int a;
     int b;
     int c; 
   };
   struct node s= { 3, 5,6 };
   struct node *pt = &s;
   printf("%d" , *(int*)pt);

}

这段程序的输出是:
(a) 3
(b) 5
(c) 6
(d) 7

3题:考查递归调用

int foo ( int x , int n)
{
int val;
val =1;

if (n>0) 
{
    if (n%2 == 1) val = val *x;
    
    val = val * foo(x*x , n/2);
}
return val;
}

这段代码对x和n完成什么样的功能(操作)?
(a) x^n (x的n次幂)
(b) x*n(x与n的乘积)
(c) n^x(n的x次幂)
(d) 以上均不是

4题:考查指针,这道题只适合于那些特别细心且对指针和数组有深入理解的人

main() 
{
int a[5] = {1,2,3,4,5};
int *ptr = (int*)(&a+1);

printf("%d %d" , *(a+1), *(ptr-1) );

}

这段程序的输出是:

(a) 2 2
(b) 2 1
(c) 2 5
(d) 以上均不是

5题:考查多维数组与指针

void foo(int [][3] ); 

main()
{
int a [3][3]= { { 1,2,3} , { 4,5,6},{7, ,9}};
foo(a);
printf("%d" , a[2][1]);
}

void foo( int b[][3]) 
{
++ b;
b[1][1] =9;
}

这段程序的输出是:

(a) 8
(b) 9
(c) 7
(d)以上均不对


6题目:考查逗号表达式

main()
{
int a, b,c, d;
a=3;
b=5;
c=a,b;
d=(a,b);

printf("c=%d" ,c);
printf("d=%d" ,d);

}

这段程序的输出是:

(a) c=3 d=3
(b) c=5 d=3
(c) c=3 d=5
(d) c=5 d=5

7题:考查指针数组

main()
{
int a[][3] = { 1,2,3 ,4,5,6};
int (*ptr)[3] =a;

printf("%d %d " ,(*ptr)[1], (*ptr)[2] );

++ptr;
printf("%d %d" ,(*ptr)[1], (*ptr)[2] );
}

这段程序的输出是:

(a) 2 3 5 6
(b) 2 3 4 5
(c) 4 5 0 0
(d) 以上均不对

8题:考查函数指针

int *f1(void)
{
int x =10;
return(&x);
}

int *f2(void)
{
int*ptr;
*ptr =10;
return ptr;
}

int *f3(void)
{
int *ptr;
ptr=(int*) malloc(sizeof(int));
return ptr;
}

上面这3个函数哪一个最可能引起指针方面的问题

(a) 只有 f3
(b) 只有f1 and f3
(c) 只有f1 and f2
(d) f1 , f2 ,f3

9题:考查自加操作(++)

main()
{
int i=3;
int j;

j = sizeof(++i+ ++i);

printf("i=%d j=%d", i ,j);
}

这段程序的输出是:

(a) i=4 j=2
(b) i=3 j=2
(c) i=3 j=4
(d) i=3 j=6

10题:考查形式参数,实际参数,指针和数组

void f1(int *, int); 
void f2(int *, int); 
void(*p[2]) ( int *, int);

main()
{
int a;
int b;

p[0] = f1;
p[1] = f2;
a=3;
b=5;

p[0](&a , b);
printf("%d\t %d\t" , a ,b);

p[1](&a , b);
printf("%d\t %d\t" , a ,b);
}

void f1( int* p , int q)
{
int tmp;
tmp =*p;
*p = q;
q= tmp;
}

void f2( int* p , int q)
{
int tmp;
tmp =*p;
*p = q;
q= tmp;
}

这段程序的输出是:

(a) 5 5 5 5
(b) 3 5 3 5
(c) 5 3 5 3
(d) 3 3 3 3

11题:考查自减操作(--)

void e(int ); 

main()
{
int a;
a=3;
e(a);
}

void e(int n)
{
if(n>0)
{
    e(--n);
    printf("%d" , n);
    e(--n);
}
}

这段程序的输出是:

(a) 0 1 2 0
(b) 0 1 2 1
(c) 1 2 0 1
(d) 0 2 1 1

12题:考查typedef类型定义,函数指针

typedef int (*test) ( float * , float*)
test tmp;

tmp 的类型是

(a) 函数的指针,该函数以 两个指向浮点数(float)的指针(pointer)作为参数(arguments)
      Pointer to function of having two arguments that is pointer to float
(b) 整型
(c) 函数的指针,该函数以 两个指向浮点数(float)的指针(pointer)作为参数(arguments),并且函数的返回值类型是整


      Pointer to function having two argument that is pointer to float and return int
(d) 以上都不是


13题:数组与指针的区别与联系

main()
{
char p;
char buf[10] ={ 1,2,3,4,5,6,9,8};
p = (buf+1)[5];
printf("%d" , p);
}

这段程序的输出是:

(a) 5
(b) 6
(c) 9
(d) 以上都不对

14题: 考查指针数组的指针

void f(char**);

main()
{
char * argv[] = { "ab" ,"cd" , "ef" ,"gh", "ij" ,"kl" };
f( argv );
}

void f( char **p )
{
char* t;

t= (p+= sizeof(int))[-1];

printf( "%s" , t);
}

这段程序的输出是:

(a) ab
(b) cd
(c) ef
(d) gh

15题:此题考查的是C的变长参数,就像标准函数库里printf()那样,这个话题一般国内大学课堂是不会讲到的,不会也情

有可原呵呵,

#include<stdarg.h>
int ripple ( int , ...);

main()
{
int num;
num = ripple ( 3, 5,7);
printf( " %d" , num);
}

int ripple (int n, ...)
{
int i , j;
int k; 
va_list p;

k= 0;
j = 1;
va_start( p , n); 

for (; j<n; ++j) 
{
    i = va_arg( p , int);
    for (; i; i &=i-1 )
      ++k;
}
return k;
}

这段程序的输出是:

(a) 7
(b) 6
(c) 5
(d) 3

16题:考查静态变量的知识

int counter (int i)
{
static int count =0;
count = count +i;
return (count );
}
main()
{
int i , j;

for (i=0; i <=5; i++)
    j = counter(i);
}

本程序执行到最后,j的值是:

(a) 10
(b) 15
(c) 6
(d) 7

 

18>:细说 #pragma pack(n)

 

C语言中,结构是一种复合数据类型,其构成元素既可以是基本数据类型(如int、long、float等)的变量,也可以是一些复合数据类型(如数组、结构、联合等)的数据单元。在结构中,编译器为结构的每个成员按其自然对界(alignment)条件分配空间。各个成员按照它们被声明的顺序在内存中顺序存储,第一个成员的地址和整个结构的地址相同。

例如,下面的结构各成员空间分配情况:
struct test 
{
     char x1;
     short x2;
     float x3;
     char x4;
};

结构的第一个成员x1,其偏移地址为0,占据了第1个字节。第二个成员x2为short类型,其起始地址必须2字节对界,因此,编译器在x2和x1之间填充了一个空字节。结构的第三个成员x3和第四个成员x4恰好落在其自然对界地址上,在它们前面不需要额外的填充字节。在test结构中,成员x3要求4字节对界,是该结构所有成员中要求的最大对界单元,因而test结构的自然对界条件为4字节,编译器在成员x4后面填充了3个空字节。整个结构所占据空间为12字节。

更改C编译器的缺省字节对齐方式
在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。一般地,可以通过下面的方法来改变缺省的对界条件:
     · 使用伪指令#pragma pack (n)C编译器将按照n个字节对齐。
     · 使用伪指令#pragma pack (),取消自定义字节对齐方式。

另外,还有如下的一种方式:
     · __attribute((aligned (n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。
     · __attribute__ ((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

以上的n = 1, 2, 4, 8, 16... 第一种方式较为常见。

( via http://blog.csdn.net/wenddy112/articles/300583.aspx )

下面有一道在 CSDN论坛 上讨论火热的题:

Intel和微软和本公司同时出现的面试题

#pragma pack(8)

struct s1{
short a;
long b;
};

struct s2{
char c;
s1 d;
long long e;
};

#pragma pack()


1.sizeof(s2) = ?
2.s2的c后面空了几个字节接着是d?

感谢 redleaves(ID最吊的网友) 的解答,结果如下:

sizeof(S2)结果为24.
成员对齐有一个重要的条件,即每个成员分别对齐.即每个成员按自己的方式对齐.
也就是说上面虽然指定了按8字节对齐,但并不是所有的成员都是以8字节对齐.其对齐的规则是,每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是8字节)中较小的一个对齐.并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节.
S1中,成员a是1字节默认按1字节对齐,指定对齐参数为8,这两个值中取1,a按1字节对齐;成员b是4个字节,默认是按4字节对齐,这时就按4字节对齐,所以sizeof(S1)应该为8;
S2中,c和S1中的a一样,1字节对齐,d 是个结构,它是8个字节,它按什么对齐呢?对于结构来说,它的默认对齐方式就是它的所有成员使用的对齐参数中最大的一个,S1的就是4.所以,成员d就是按4字节对齐.成员e是8个字节,它是默认按8字节对齐,和指定的一样,所以它对到8字节的边界上,这时,已经使用了12个字节了,所以又添加了4个字节的空,从第16个字节开始放置成员e.这时,长度为24,已经可以被8(成员e按8字节对齐)整除.这样,一共使用了24个字节.
                          a b
S1的内存布局:11**,1111,
                          c S1.a S1.b d
S2的内存布局:1***,11**,1111,****11111111

这里有三点很重要:
1.每个成员分别按自己的方式对齐,并能最小化长度
2.复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度
3.对齐后的长度必须是成员中最大的对齐参数的整数倍,这样在处理数组时可以保证每一项都边界对齐

补充一下,对于数组,比如:
char a[3];这种,它的对齐方式和分别写3个char是一样的.也就是说它还是按1个字节对齐.
如果写: typedef char Array3[3];
Array3这种类型的对齐方式还是按1个字节对齐,而不是按它的长度.
不论类型是什么,对齐的边界一定是1,2,4,8,16,32,64....中的一个.

测试的编译器:

GCC 2.95 3.1 3.3 3.4 4.0
MS C/C++ 7.0 7.1 8.0 beta
Borland C/C++ 5.6 6.0
Intel C/C++ 7.0 8.0 8.1
DigitalMars C/C++ 8.4
OpenWatcom 1.3
Codeplay C/C++ 2.1.7


http://blog.iyi.cn/billy/2006/12/sizeof_pragma_packn.html

http://blog.csdn.net/wenddy112/articles/300583.aspx

 

 

 

 

19>: sizeof #pragma pack(n)

sizeof  #pragma pack(n) 的认识

今天在rukia的空间看到关于 sizeof  #pragma pack(n) 的文章,
发现自己在这一方面上没有弄懂。有这个必要把它完全搞明白。所以就到网上看了一些文章,自己也做了一些测试。以下是我的心得。以下
都是在32位的PC上 VC6.0 上得到。

sizeof 是用来求一个变量或一种类型占用多少个Byte. (32位PC)
sizeof(bool) = 1 sizeof(char) = 1
sizeof(short) = 2 sizeof(int) = 4;
sizeof(float) = 4 sizeof(double) = 8;
sizeof(指针) = 4

#pragma pack(n) 是用来确定编译时的字节对齐方式, 默认为
#pragma pack(sizeof(int))。当n大于sizeof(int)的效果跟 n=sizeof(int)是一样的。

 

接下来对结构体进行分析。
* 当结构体中最长宽度的数据成员的宽度小于 n 时,按该成员宽度对齐;
* 当最长宽度的数据成员的宽度大于或等于 n 时,按 n 对齐。 

 


struct TEST
{
char a;
}
#pragram pack(1) 对齐长度 1 内存布局 '1' sizeof(TEST) = 1
#pragram pack(2) 对齐长度 1 内存布局 '1' sizeof(TEST) = 1
#pragram pack(4) 对齐长度 1 内存布局 '1' sizeof(TEST) = 1

struct TEST
{
char a;
short b;
}
#pragram pack(1) 对齐长度 1 内存布局 1,11 sizeof(TEST) = 3
#pragram pack(2) 对齐长度 2 内存布局 1*,11 sizeof(TEST) = 4
#pragram pack(4) 对齐长度 2 内存布局 1*,11 sizeof(TEST) = 4

struct TEST
{
char a;
short b;
char c;
}
#pragram pack(1) 对齐长度 1 内存布局 1,11,1 sizeof(TEST) = 4
#pragram pack(2) 对齐长度 2 内存布局 1*,11,1* sizeof(TEST) = 6
#pragram pack(4) 对齐长度 2 内存布局 1*,11,1* sizeof(TEST) = 6

struct TEST
{
char a;
int b;
short c;
}
#pragram pack(1) 对齐长度 1 内存布局 1,1111,11 sizeof(TEST) = 7
#pragram pack(2) 对齐长度 2 内存布局 1*,1111,11 sizeof(TEST) = 8
#pragram pack(4) 对齐长度 4 内存布局 1***,1111,11** sizeof(TEST) = 12


从这些例子可以看出,当n取值越小,结构体所占的内存空间就越小。
不过这也会带来一个问题,访问这些数据所花的时间也变长了。^_^
这样的问题在实际编程中一般不会遇到。
一般在考试中或某些公司的面试笔试中才会出现。

 

 

20>: enum的用法

enum的用法如果一个变量你需要几种可能存在的值,那么就可以被定义成为枚举类型。之所以叫枚举就是说将变量或者叫对象可能存在的情况也可以说是可能的值一一例举出来。
(1)枚举的声明:枚举声明用于声明新的枚举类型。

  访问修辞符 enum 枚举名:基础类型

{

  枚举成员

} ;

  基础类型必须能够表示该枚举中定义的所有枚举数值。枚举声明可以显式地声明 bytesbyteshortushortintuintlong ulong 类型作为对应的基础类型。没有显式地声明基础类型的枚举声明意味着所对应的基础类型是 int

  举个例子来说明一吧,为了让大家更明白一点,比如一个铅笔盒中有一支笔,但在没有打开之前你并不知道它是什么笔,可能是铅笔也可能是钢笔,这里有两种可能,那么你就可以定义一个枚举类型来表示它!


enum box{pencil,pen};//这里你就定义了一个枚举类型的变量叫box,这个枚举变量内含有两个元素也称枚举元素在这里是pencilpen,分别表示铅笔和钢笔。


  这里要说一下,如果你想定义两个具有同样特性枚举类型的变量那么你可以用如下的两种方式进行定义!

enum box{pencil,pen}; 

enum box box2;//或者简写成box box2;


  再有一种就是在声明的时候同时定义。

enum {pencil,pen}box,box2; //在声明的同时进行定义!


  枚举变量中的枚举元素系统是按照常量来处理的,故叫枚举常量,他们是不能进行普通的算术赋值的,(pencil=1;)这样的写发是错误的,但是你可以在声明的时候进行赋值操作!

enum box{pencil=1,pen=2};

但是这里要特别注意的一点是,如果你不进行元素赋值操作那么元素将会被系统自动从0开始自动递增的进行赋值操作,说到自动赋值,如果你只定义了第一个那么系统将对下一个元素进行前一个元素的值加1操作,例如

enum box{pencil=3,pen};//这里pen就是4系统将自动进行pen=4的定义赋值操作!


  前面说了那么多,下面给出一个完整的例子大家可以通过以下的代码的学习进行更完整的学习!

#include <iostream> 
using namespace std; 

void main(void) 
{ 
    enum egg {a,b,c}; 
    enum egg test; //在这里你可以简写成egg test;


     test = c; //对枚举变量test进行赋予元素操作,这里之所以叫赋元素操作不叫赋值操作就是为了让大家明白枚举变量是不能直接赋予算数值的,例如(test=1;)这样的操作都是不被编译器所接受的,正确的方式是先进行强制类型转换例如(test = (enum egg) 0;)


    if (test==c) 
     { 
        cout <<"枚举变量判断:test枚举对应的枚举元素是c" << endl; 
     } 

    if (test==2) 
     { 
        cout <<"枚举变量判断:test枚举元素的值是2" << endl; 
     } 

    cout << a << "" << b << "" << test <<endl; 

     test = (enum egg) 0; //强制类型转换

    cout << "枚举变量test值改变为:" << test <<endl; 
    cin.get(); 
}

  看到这里要最后说一个问题,就是枚举变量中的枚举元素(或者叫枚举常量)在特殊情况下是会被自动提升为算术类型的!

#include <iostream> 
using namespace std; 

void main(void) 
{ 
    enum test {a,b}; 
    int c=1+b; //自动提升为算术类型

    cout << c <<endl; 
    cin.get(); 
}

输出: 2

 

21>:  c语言中可变参数的原理---printf()函数

函数原型: int printf(const char *format[,argument]...)
       返 回 值: 成功则返回实际输出的字符数,失败返回-1.
 函数说明:
       printf()函数中,format后面的参数个数不确定,且类型也不确定,这些参数都存放在栈内.调用printf()函数时,根据format里的格式("%d %f...")依次将栈里参数取出.而取出动作要用到va_argva_endva_start这三个宏定义,再加上va_list.
     (1)va_list事实上是一char *类型,即:
            typedef char* va_list;
     (2)三个宏定义:
            #define _INTSIZEOF(n)    ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ) 
            #define va_start(ap,v)     ( ap = (va_list)&v + _INTSIZEOF(v) ) 
            #define va_arg(ap,type)  ( *(type *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) ) 
            #define va_end(ap)          ( ap = (va_list)0 ) 
   attentionc语言中可变参数的原理---printf()函数 
            int printf(const char* format,...);   
      使用过C语言的人所再熟悉不过的printf函数原型,它的参数中就有固定参数format和可变参数(”…”表示).而程序员又可以用各种方式来调用printf,: 
            printf("%d ",value);   
            printf("%s ",str);   
            printf("the number is %d,string is:%s ",value,str); 
       可以看出,该函数的参数格式不固定,参数类型不固定.C语言中使用宏来处理这些可变参数.这些宏看起来很复杂,其实原理挺简单,即根据参数入栈的特点从最靠近第一个可变参数的固定参数开始,依次获取每个可变参数的地址.
 (1)va_start
      通过该宏定义可以获取到可变参数表的首地址,并将该地址赋给指针ap.
 (2)va_arg
      通过该宏定义可以获取当前ap所指向的可变参数,并将指针ap指向下一个可变参数.注意,该宏的第二个参数为类型.
 (3)va_end
      通过该宏定义可以结束可变参数的获取.


      程序员通过这三个宏定义就可以实现对可变参数的处理.例如:

view plaincopy to clipboardprint?

1.    #include <stdio.h>  

2.   

3. typedef char* va_list;   

4.    #define _INTSIZEOF(n)    ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )   

5.    #define va_start(ap,v)   ( ap = (va_list)&v + _INTSIZEOF(v) )   

6.    #define va_arg(ap,type)  ( *(type *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )   

7.    #define va_end(ap)       ( ap = (va_list)0 )   

8.   

9. int cal_val(int c, ...)   

10. {   

11.     int sum = c;   

12.     va_list ap;              //声明指向char型的指针  

13.     va_start(ap,c);          //获取可变参数列表的首地址,并赋给指针ap  

14.   

15.     c = va_arg(ap,int);      //从可变参数列表中获取到第一个参数(返回值即为参数)  

16.     while(0 != c)   

17.     {   

18.         sum += c;   

19.         c = va_arg(ap,int);  //循环的从可变参数列表中获取到参数(返回值即为参数)  

20.     }  

21.     va_end(ap);              //结束从可变参数列表中获取参数  

22.     return sum;   

23. }    

24.    

25. int main(int argc, char* argv[])   

26. {   

27.     int value1  

28.       

29.     value1 = cal_val(1,2,3,4,5,6,7,8,9,0);   

30.     printf("value1=%d\n",value1);  

31.     value2 = cal_val(6,7,8,9,0);   

32.     printf("value2=%d\n",value2);  

33.       

34.     return 0;   

35. }   

 

 

 

 

22>: C标准库和glibc

C 标准主要由两部分组成,一部分描述C的语法,另一部分描述C标准库。C标准库定义了一组标准头文件,每个头文件中包含一些相关的函数、变量、类型声明和宏定义。要在一个平台上支持C语言,不仅要实现C编译器,还要实现C标准库,这样的实现才算符合C标准。不符合C标准的实现也是存在的,例如很多单片机的C 语言开发工具中只有C编译器而没有完整的C标准库.

      Linux平台上最广泛使用的C函数库是glibc,其中包括C标准库的实现,也包括本书第三部分介绍的所有系统函数。几乎所有C程序都要调用glibc的库函数,所以glibcLinux平台C程序运行的基础。glibc提供一组头文件和一组库文件,最基本、最常用的C标准库函数和系统函数在libc.so库文件中,几乎所有C程序的运行都依赖于libc.so,有些做数学计算的C程序依赖于libm.so,以后我们还会看到多线程的C程序依赖于libpthread.so。以后我说libc时专指libc.so这个库文件,而说glibc时指的是glibc提供的所有库文件.

      glibc并不是Linux平台唯一的基础C函数库,也有人在开发别的C函数库,比如适用于嵌入式系统的uClibc.

 

23>:二级指针与指针数组的关系

view plaincopy to clipboardprint?

1. #include <stdio.h>  

2.   

3. void test(char *argv[]);  

4.   

5. int main(void)  

6. {  

7.  char *argv[3]={{"abcdefg"},{"1234567"},{"q1w2e3r"}};  

8.   

9.  test(argv);   /*调用指针数组时,可直接使用指针数组名*/  

10.  return 0;  

11. }  

12.   

13. void test(char *argv[])  

14. {  

15.  char **p=argv;  

16.    

17.  /***测试1***/  

18.  printf("argv[0]=0x%x,argv[0]=%s\n",argv[0],argv[0]);   

19.  printf("argv[1]=0x%x,argv[1]=%s\n",argv[1],argv[1]);  

20.  printf("argv[2]=0x%x,argv[2]=%s\n",argv[2],argv[2]);  

21.   

22.  /***测试2***/  

23.  printf("p=0x%x\n",p);  

24.  printf("argv[0]=0x%x\n",argv[0]);  

25.  printf("&argv[0]=0x%x\n",&argv[0]);  

26.    

27.  /***测试3***/  

28.  printf("*p=%s\n",*p);  

29.  printf("*(p+1)=%s\n",*(p+1));  

30.  printf("*(p+2)=%s\n",*(p+2));  

31.    

32.  /***测试4***/  

33.  printf("*p=%s\n",*p);  

34.  printf("(*p+1)=%s\n",(*p+1));  

35.  printf("(*p+2)=%s\n",(*p+2));  

36.    

37.  /***测试5***/  

38.  printf("p=0x%x\n",p);  

39.  printf("argv=0x%x\n",argv);  

40.  printf("&argv[0]=0x%x\n",&argv[0]);   

41.  printf("&argv[1]=0x%x\n",&argv[1]);  

42.  printf("&argv[2]=0x%x\n",&argv[2]);  

43.    

44.  /***测试6***/  

45.  printf("sizeof(argv)=%d\n",sizeof(argv));  

46. }  

(1)指针数组argv中每个元素都是指针,即每个元素都是字符串的首地址.因此测试1组输出结果为:
       argv[0]=0x4270ac,argv[0]=abcdefg
       argv[0]=0x426034,argv[0]=1234567
       argv[0]=0x42601c,argv[0]=q1w2e3r
    
(2)二级指针p指向指针数组argv的首地址处.因此测试1组输出结果为:
       p=0x13ff74                /*二级指针p中存放了指针数组argv所在(连续)地址空间的首地址*/
       argv[0]=0x4270ac     /*字符串0首地址*/
       &argv[0]=0x13ff74    /*字符串0首地址所在存储空间的地址*/
   可以看出,二级指针p所指向的地址与指针数组中首元素所在存储空间地址相同,均为0x13ff74.

(3)从测试2组可知,二级指针p中存放了指针数组argv所在(连续)地址空间的首地址,换句话说,二级指针p中存放的是地址,所以,*p是该地址中的内容,即字符串0的首地址.因此测试3组输出结果为:
       *p=abcdefg
       *(p+1)=1234567
       *(p+2)=q1w2e3r

(4)从测试2组可知,二级指针p中存放了指针数组argv所在(连续)地址空间的首地址,因此测试4组输出结果为:
       (*p)=abcdefg
       (*p+1)=bcdefg
       (*p+2)=cdefg

(5)从测试2组可知,二级指针p中存放了指针数组argv所在(连续)地址空间的首地址,因此测试5组输出结果为:
       p=0x13ff74
       argv=0x13ff74
       &argv[0]=0x13ff74
       &argv[1]=0x13ff78
       &argv[2]=0x13ff7c
   再次验证了二级指针p与指针数组中各成员之间的关系.

(6)由于是指针型数据,因此测试6组输出结果为:  sizeof(argv)=4

24>:函数参数传递的原理

数据复制一份传递给函数临时创建的变量中 注意传递指针是也是复制一份地址


25>:malloc和free    new和delete 区别


malloc 和free是C++/C语言的标准库函数,new/delete是C++的运算符,他们都可以用于申请动态内存和释放内存。
但是:
malloc和free 主要用于动态内存的分配和释放内存。


new 能完成动态内存分配和初始化工作
delete 能完成清理与释放内存工作

26>:函数的输入参数和输出参数?


void StringCopy(char *strDestination, const char *strSource);
如果参数作输出用,不论它是什么数据类型,也不论它采用“指针传递”还是“引
用传递”,都不能加const 修饰,否则该参数将失去输出功能。
其中strSource 是输入参数,strDestination 是输出参数。给strSource 加上const
修饰后,如果函数体内的语句试图改动strSource 的内容,编译器将指出错误。


27>:用const 修饰函数的返回值


 如果给以“指针传递”方式的函数返回值加const 修饰,那么函数返回值(即指针)
的内容不能被修改,该返回值只能被赋给加const 修饰的同类型指针。
例如函数
const char * GetString(void);
如下语句将出现编译错误:
char *str = GetString();
正确的用法是
const char *str = GetString();

28>:PC_Lint LogiScope等工具进行代码审查




  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值