iLike职场大学生就业指导:C和C++方向
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

第2章 C/C++程序设计基础

如今,各种编程语言迅速发展,C语言以其简单、快捷、高效的特点,得到了众多开发者的青睐,成为计算机专业的必修课程。各大企业一般通过面试考量求职者的编程能力,如编程风格、语言运用熟练程度等。在面试过程中,无法让求职者做项目考查其能力,但是可以通过对程序设计基本概念的理解,尤其对细节的考查,来了解求职者的编程风格和语言的熟练运用程度。因此,求职者要对基础概念的细节加以注意。

2.1 数据类型

在C语言程序中,类型转换遇到的比较多,主要为整数、浮点数与字符串的互相转换,有时也会出现二进制、十六进制的数值与浮点数的转换,这时实现起来就比较复杂一点。面试时,C语言的字符串问题对于一般的求职者来说没有什么难度。但是一些细节问题,往往会被忽略,而这些细节问题大多是面试中涉及到的知识点。

【试题1】

在C++中,变量的声明和定义有什么区别?

【答案】

在C++语言中,使用变量前必须定义或声明。在同一作用域内,变量必须有且只能有一次定义,但是可以声明多次。现在一般这样描述变量的声明和定义:把不需要分配存储空间的称之为声明,而把分配存储空间的称之为定义。其实,定义也是声明。变量的声明可以分为以下两种情况。

(1)需要建立存储空间的声明。

下面的代码就在声明变量时,创建了该变量的存储空间。

            int a=0;

(2)不需要建立存储空间的声明。

【解析】

这个试题考查了最基础的概念,但是这个问题会被忽略。在编写代码时,初学者很少遇到变量的声明和定义方面的错误,这是因为大家都知道在C/C++中,变量声明和定义后才能使用。即使遇到这方面错误,也就是未赋值而使用的警告。因为其功能相似,差别较小,它们的区别一般都会被忽略,但是它们还是有些区别的。

现在常常把不需要分配存储空间的称之为声明,而把分配存储空间的称之为定义。其实,定义也是声明。下面的代码只是声明变量,并没有创建该变量的存储空间。这只是引用性声明,告诉编译系统这是引用其他地方定义的变量,不需要为其分配一个存储空间。这种声明可以有多次,只是表明变量的类型和名字。从这里可以看出,并非所有的变量声明都是定义。

            extern int a;

具有初始化的变量声明时,声明必须有定义才可以,因为变量初始化需要存储空间来存储变量值,只有定义才能分配存储空间。如果声明变量的同时进行了变量初始化,就是使用了关键字extern进行声明,那么它也可以视为进行了定义。

下面的代码在Visual C++ 6.0系统中编译通过。

            extern int a;
            extern int a=0;

全局变量的声明可以有多次,即可以在函数之内,也可以在函数之外进行。

另外,使用关键字static声明一个变量时,其作用有两点。

(1)static声明局部变量时,在整个程序的执行期内,该变量分配的空间始终都存在,其值在该作用域内一直可以用。

(2)static声明外部变量时,该变量只限于本文件模块内使用。

【试题2】

以下三条输出语句分别输出什么?

            char str1[]      = "abc";
            char str2[]      = "abc";
            const char str3[]  = "abc";
            const char str4[]  = "abc";
            const char* str5  = "abc";
            const char* str6  = "abc";
            cout << ( str1==str2 ) << endl;
            cout << ( str3==str4 ) << endl;
            cout << ( str5==str6 ) << endl;

【答案】

            0
            0
            1

【解析】

该题输出的是比较的结果。

由C/C++编译的程序占用内存可以分为以下几部分。

(1)栈区(stack):编译器自动分配和释放,用于存放函数参数值、局部变量值等。

(2)堆区(heap):一般由程序分配和释放。若程序在结束前没有释放,程序结束时可能由系统释放回收。

(3)全局区(static):用于存放全局变量和静态变量。程序结束后由系统释放。

(4)常量区:常量存放于该区。字符串常量就是放在这里的。程序结束后由系统释放。

(5)程序代码区:用于存放可执行代码。

str1为字符数组,其长度由系统依据其初始值设置。编译系统在栈区为“abc”分配了一块内存,这块内存存储的内容是可以被修改的,在栈上分配一个地址给str1并指向为“abc”分配的地址。同理,系统也为str2分配了内存空间。

str1和str2为字符数组,它们的值分别为各自存储区首地址。因为str1和str2为不同数组,各有自己的存储空间,因此,str1和str2的值不等。

str3和str4与str1、str2相同,都是字符数组,它们的值也分别为各自存储区首地址。str3和str4由关键字const修饰,它们所指向的存储空间的内容不能修改。str3和str4各有自己的存储空间,因此,str3和str4的值不等。

编译系统在遇到str5时,实际上先在常量区为“abc”分配了一块内存,然后在栈上分配一个地址给str5并指向为“abc”分配的地址。str5实际上是常量区的地址,这个区的内容是不容被修改的。str6和str5都是字符指针,并不分配存储区,系统对其优化后,会为它们分配指向常量区的同一常量“abc”地址的首地址,因此相等。

读者可以使用下面的代码输出这些数组的地址值,查看一下结果。笔者在Visual C++ 6.0系统中测试,str5的值与str6的值相同。

            char str1[]      = "abc";
            char str2[]      = "abc";
            const char str3[]  = "abc";
            const char str4[]  = "abc";
            const char* str5  = "abc";
            const char* str6  = "abc";
            cout <<  ( str1==str2 ) << endl;
            cout <<  ( str3==str4 ) << endl;
            cout <<  ( str5==str6 ) << endl;
            cout << "str1:"  <<  (int)str1  << endl;
            cout << "str2:"  <<  (int)str2  << endl;
            cout << "str3:"  <<  (int)str3  << endl;
            cout << "str4:"  <<  (int)str4  << endl;
            cout << "str5:"  <<  (int)str5  << endl;
            cout << "str6:"  <<  (int)str6 << endl;

【试题3】

以下代码能够编译通过吗,为什么?

            unsigned int const n1 = 2;
            char sz1[n1];
            unsigned int i = 0;
            unsigned int const n2 = i;
            char sz2[n2];

【答案】

不能。数组的长度需要由整型常量或整型常量表达式确定,而n2的值在编译期间不能确定其值,无法指定数组的长度。

【解析】

这道题考查了数组的声明问题。在不支持可变长数组的编译系统中,数组的长度需要在编译期间确定其值,也就是编译期常量,如长度为一个整数或整型的常量表达式,也可以使用define来定义。如果编译系统在编译期间不能确定数组的长度,就无法创建该数组。

使用define定义常量时,编译系统在编译的时候将常量标识符放入符号表中,这就是编译期常量。这时,系统并没有为其分配内存,分配内存是运行时做的事情。下面的代码就是定义一个常量。

            #define   MAX   128

在编译时,所有的MAX都会使用128进行替换。

使用const变量也可以指定数组的长度。考题代码第一条语句就声明了一个常量n1。在C++中,编译器尽量不为n1分配内存,而是在进行类型检查之后将其值折叠到代码里,也就是进行值替代,然后存放到符号表中。这与define定义常量的过程类似。编译系统在编译期间就可以确定数组的长度,也就可以通过编译。

如果const定义的常量值太复杂,需要在运行期间确定其值,就不能使用该值指定数组的长度。n2也是const修饰的变量,但是其值为变量i的值,n2的值需要运行期间确定,编译期间也就无法确定数组的长度。

【试题4】

下列哪两个是等同的?

            A) const int* a = &b;
            B) int * const a = &b;
            C) const int* const a = &b;
            D) int const* const a = &b;

【答案】

            C和D。

【解析】

const与指针结合使用,有两种情况。

(1)使指针所指为const,定义时将const写在*的左边。这种定义方式,使用const修饰的是*,也就是const修饰的指针所指向的变量,指针指向的变量为常量,其值不能修改,指针的地址可以改变。形式如下所示。

            int a=2;
            int const* b= &a;
            const int * d=&a;

不能通过这类指针修改其内容。如不能通过b或者d修改a的内容,但是可以通过变量a修改a的内容,也就是说这类指针所指地址不必是const。由于指针本身并不是const,所以定义指针时不必初始化。下面两种定义格式相同。

            int const* c;
            int const* b= &a;

(2)指针为const,定义时将const写在*的右边。这种定义方式,使用const修饰的是指针,指针本身是常量,指针的地址是固定的,不能再指向其他地址,但是指针地址所指内容可以被更改。由于指针是const的,所以定义时必须初始化,如下所示。

            int d = 1;
            int* const pi = &d;

从以上分析可以看出,A项表示*a是const,但指针a可变,但是*a不能修改。B项a是const,但是*a可以变化,也就是a所指地址的内容可以修改。C项和D项的a和*a都是const,常量和指针的值都不能改变。因此C和D两者是相同的。

【试题5】

写出下面定义所描述的意思。

            a) int a;
            b) int *a;
            c) int **a;
            d) int a[10];
            e) int *a[10];
            f) int (*a)[10];
            g) int (*a)(int);
            h) int (*a[10])(int);
            i) int *a(int);

【答案】

a):一个整型数变量。

b):一个指向整型数的指针变量。

c):一个指向整型数指针的指针变量。

d):一个有10个整型数元素的数组。

e):一个有10个元素的数组,每个元素是指向一个整型数的指针。

f):一个指向有10个整型数数组的指针。

g):一个指向函数的指针,该函数有一个整型参数并返回一个整型数。

h):这是一个函数指针数组。这个数组有10元素,每个元素指向有一个整型参数并返回一个整型数的函数。

i):这是一个指针函数,这个函数有一个整型数值参数。

【解析】

这道试题概括了大部分指针定义,可以考查求职者的基本功。如果用语言描述出其定义,让求职者使用变量写出其定义,更能考查求职者的基本功。

【试题6】

在C语言中,int、char和short类型数据在内存中所占用的字节数( )。

            A)由用户自己定义                     B)均为2字节
            C)是任意的                           D)由所用机器的机器字长决定

【答案】

            D

【解析】

在C语言中,int、char和short类型数据在内存中所占用的字节数由所用机器的机器字长决定。

【试题7】

以下代码能够编译通过吗?如果能,输出结果是什么?

            int/*This is a test code.*/i=10;
            int/*This is a test
              code.*/j=\
            5;
            int//This is a test \
              code.
            k=6;
            /*This is */#define aaaa/* a//##// */13/* code/\/&%*/
            cout<<"i="<<i<<endl;
            cout<<"j="<<j<<endl;
            cout<<"k="<<k<<endl;
            cout<<"aaaa="<<aaaa<<endl;

【答案】

            i=10
            j=5
            k=6
            aaaa=13

【解析】

C/C++语言里可以有两种注释方式:/* */和//。“/*”和“*/”可以对多行进行注释,“/*”和“*/”必须对应,“/*”总是与离它最近的“*/”匹配,所以“/*”和“*/”内不能有嵌套。“//”可以对一行进行注释,多行注释可以使用接续符“\”进行连接。注释用于解释、标识代码,编译器在编译代码时,会使用空格代替注释。

接续符“\”表示其后行接续到该行后。在编译时,编译器会将反斜杠剔除掉,将其后行的字符接续到前一行。接续符“\”之后不能有空格。如果有空格,编译器会报错。

注释是为了保证代码的可读性,让别人看懂代码。注释时,有些规则需要注意。下面列举了一些注释规则。

(1)注释风格要统一,“/* */”和“//”都可以,但是要保证统一。

(2)注释要简单易读,准确,不要有冗余。对简单的代码不要添加注释。

(3)注释有适当缩进,易于阅读才好。

(4)文件开始要添加注释。每个文件开始的注释要包含版权、作者、功能等。

(5)类、函数前都要加注释,要包含版权、作者、功能等。函数前的注释还要包含函数参数、返回值等说明。

(6)对于全局数据(常量)、类数据成员要加注释。变量名称虽然可以说明变量的用途,但是难懂的变量还是需要添加注释。

(7)代码前可以添加注释,比较难懂的行后可以添加注释,但是不能在代码的行下面添加注释。

(8)当代码有多重嵌套时,应当在每个嵌套段落的结束处加注释,便于阅读。

(9)尽量使用英文进行注释。注释语句也要注意标点、拼写和语法,即使使用中文也不要出现错别字。

【试题8】

写出下面代码的输出结果。

            int t1=5;
            float t2=12;
            int *p=&t1;
            t1=t2/*p;
            cout<<t1<<endl;

【答案】

编译不通过。代码“t1=t2/*p;”应该修改为下面的形式才能编译通过。

            t1=t2/(*p);

【解析】

C/C++语言里可以有两种注释方式:/* */和//。编译器将代码“t1=t2/*p;”中“/*”后代码看作是注释,而不是进行除法运算。“/*”后为除法,这段代码肯定存在编译不过的问题。这主要考查求职者对C/C++语言关键字的敏感程度。

【试题9】

指出下面代码的错误。

            typedef enum
            {
              table,
              chair,
              stool
            } Furniture_1
            typedef enum Furniture
            {
              table,
              sofa = 0,
              bed,
            } Furniture_2;
            int main(void)
            {
                Furniture_2 a,b;
                a=table;
                b=-1;
                a=desk;
            printf("desk:%d\t%d\n",a,b);
            return 0;
            }

【答案】

            typedef enum
            {
              table,
              chair,
              stool
            } Furniture_1;
            typedef enum Furniture
            {
              desk,
              sofa = 0,
              bed,
            } Furniture_2;
            int main(void)
            {
                Furniture_2 a,b;
                a=(enum Furniture) table;
                b=(enum Furniture) -1;
                a=desk;
                printf("desk:%d\t%d\n",a,b);
                return 0;
            }

【解析】

这道题考查了枚举的声明及枚举类型变量的赋值。枚举也是一种数据类型,可以像基本数据类型一样对变量进行声明和定义。枚举类型的定义和变量的声明可以分开进行,也可以同时进行。下面的代码是分开定义枚举类型和声明变量。

            enum Furniture_1
            {
              table,
              chair,
              stool
            };
            enum Furniture_1 a;
            enum                                    //省略enum类型名称
            {
              table,
              chair,
              stool
            }a;                                     //变量名称

也可以使用typedef将枚举类型定义别名,并利用该别名进行变量声明。下面的代码是正确的。

            //Furniture_1是Furniture的别名。Furniture_1不是变量名称
            typedef enum Furniture
            {
              table,
              chair,
              stool
            }Furniture_1;
            Furniture_1 a;
            Furniture b;

但是同一程序内声明和定义枚举类型时,需要注意下面两点。

(1)存在同名的枚举类型。

(2)存在同名的枚举成员。在这道试题中,Furniture_1与Furniture_2就存在同名的枚举成员,这是不允许的。

对枚举型的变量赋整数值时,需要进行类型转换。

【试题10】

写出下面代码的输出结果。

            void main()
            {
                char *p;
                *p=-130;
                printf("%d",*p);
            }

【答案】

            126

【解析】

这道题考查了两方面的内容:

(1)负数在计算机内部的存储形式。

(2)char类型。

下面是获取-130在计算机内部的存储形式。在计算机内,有符号数有3种表示法:原码、反码和补码。原码的最高位为符号位,“0”表示正,“1”表示负,其余位表示数值的大小;正数的反码与其原码相同,负数的反码是对其原码逐位取反,但符号位除外;正数的补码与其原码相同,负数的补码是在其反码的末位加1。

负数在计算机内以补码形式进行存储。130的二进制是10000010,其补码是1111111101111110。因为C语言的char类型占用8位,其余位丢掉,所以*p的内容为01111110,其十进制为126。

2.2 类型转换

在C/C++语言中,不同类型的数据运算时,需要进行类型转换。类型转换有两种形式:自动转换和强制转换。自动转换是系统根据不同类型的转换规则,自动将两个不同数据类型的运算对象转换成同一种数据类型的过程;强制转换则是允许代码编写者依据自己的意愿将一种数据类型强制转换成另一种数据类型的过程。

【试题11】

写出下面代码的输出结果。

            char ch='\377';
            int i;
            i=ch;
            printf("%d",i);

【答案】

            -1

【解析】

这道题考查了char和int类型转换的知识。char类型为unsigned char还是signed char,不同的编译系统会有不同的结果。char类型变量通常为长度为1字节的存储空间。字符型数据赋给整型变量时,分两种情况,如下。

(1)对于无符号字符类型的整型变量,字符类型变量低八位不变,高位补零后赋值给整型变量。

(2)对于有符号字符类型的整型变量,若字符类型变量的符号位为零时,与无符号整型变量的转换规则相同;若字节的符号位为1时,将高位全部置1后,低位数值不变赋值。

这样的转换规则,当其数值为正的字符变量转换成整型变量时,其值不变,仍然为正数;当字符变量的值大于127,也就是符号位为1时,都将其看做为一个负数,并将其赋值给整型变量,这样就保证了数值的相同。

'\377'为八进制形式,其十进制的值为255,255的二进制表示为11111111。当变量ch的数值为255时,其符号位为1,将其赋值给i时,高位补1,也就是变成了1111111111111111 (以16位演示,int的具体位数与系统有关),这是-1的补码,转换成有符号数值为-1。

【试题12】

写出下面代码的输出结果。

            unsigned int i1;
            unsigned short index1=0;
            long ncount=0;
            for(i1 = 0; i1 <index1-1; i1++)
            {
                ncount++;
                if(ncount>1000)break;
            }
            cout<<"first:"<<ncount<<endl;
            unsigned int i2=6;
            int ii ;
            int index2=-10;
            ncount=0;
            for(ii= 0; ii<index2+i2; ii++)
            {
                ncount++;
                if(ncount>1000)break;
            }
            cout<<"second:"<<ncount<<endl;
            unsigned int i3;
            unsigned int index3=0;
            ncount=0;
            for(i3 = 0; i3<index3-1; i3++)
            {
                ncount++;
                if(ncount>1000)break;
            }
            cout<<"third:"<<ncount<<endl;

【答案】

            first:0
            second:1001
            third:1001

【解析】

这道题考查了类型隐式自动转换。自动转换需要根据不同类型的转换规则进行转换。转换的基本原则是低精度类型向高精度类型转换,如图2-1所示。

在第一个循环中,index1是无符号短整型unsigned short。在进行index1-1运算时,由于类型不匹配,发生隐式类型转换,index1将被转换成有符号整型,转换之后的index1还是0,因此index-1的结果就是-1。而i1的初始值为0,表达式0<-1不成立,立即退出循环。ncount的值仍然为初始值0,输出的结果仍然是0。

在第二个循环中,index2是无符号长整型unsigned int,而i2为整型int。因此,当执行到语句index2+i2时,由于类型不匹配,i2自动转换为unsigned int。i2转换前的值为-10,也就是0xfffffff6,转换后变成一个无符号整数,即4294967286。这是一个非常大的数值。因此,循环可以进行,ncount的值会大于1000,输出1001。

图2-1 隐式自动转换规则

index3为无符号长整型unsigned int,在进行减1时,1将被转换成无符号长整型。因此,index3-1的结果就是0xffffffff,即4294967295。因此,循环可以进行,ncount的值会大于1000,输出1001。

在编写代码时,语句和表达式通常只使用一种类型的变量和常量。如果使用了混合类型,最好强制进行类型转换,否则系统会自动进行类型转换,带来潜在的危险。

下面是比较容易出错的几种运算符。

● →、[]、()的优先级高于*。

*p.next的实际运行结果是*(p.next),而不是(*p).next;同样,*p[10]的运行结果是*(p[10]),*p(10)的实际结果是*(p(10)),fp是个函数,返回值为指针类型。

● 算术运算符与移位、比较、逻辑运算符。

a<< 4 + b的实际运行结果是a<<(4 + b)。

● == 和!=高于位运算符和赋值运算符,但是低于其他比较运算符。

● 逗号运算符最低。

【试题13】

写出下面代码的输出结果。

            short aa;
            unsigned long bb;
            bb=32768;
            aa=bb;
            cout<<"aa="<<(int)aa<<endl;
            char a=512;
            unsigned char b=-128;
            cout<<"a="<<(int)a<<endl;
            cout<<"b="<<(int)b<<endl;

【答案】

            aa=-32768
            a=0
            b=128

【解析】

这道题考查了高精度类型向低精度类型转换的知识。a为短整型,一般为16位;bb为无符号长整型,一般为32位。bb的值为32768,使用二进制可以表示为1000000000000000。无符号长整型数据赋值给短整型变量时,系统会自动截取低16位传给短整型变量,短整型变量的第一位当做符号位。因此,aa的值为1000000000000000,即a为-0。在计算机内部存储时,使用补码形式存储,-0也就是-32768。无符号和有符号整型互相赋值时,二进制数各位值不变,有符号整型数把首位当做符号。

512的二进制为1000000000。char类型变量长度一般为8位,a的二进制形式为00000000,其十进制为0。

-128的二进制形式为111110000000。unsigned char类型变量的长度仍然为8位,b的二进制为10000000,也就是二进制的128。

2.3 结构体、联合体和sizeof

结构体可以看做由基本数据类型构成的并用一个标识符来命名的各种变量的组合。当然结构体可以含有不同的基本数据类型,也可以含有结构体和联合体的复合数据类型,结构体是其中所有变量的集合。联合体与结构体类似,但是其中的成员共用一个内存地址,只保存一个数据,但是可以按照不同类型来读取。

【试题14】

写出下面代码的输出结果。

            Struct
            {
                short   t1;
                short   t2;
            }TestA;
            Struct
            {
                long   t1;
                short   t2;
            }TestB;
            int main(int argc, char* argv[])
            {
                cout<<"TestA:"<<sizeof(TestA)<<endl;
                cout<<"TestB:"<<sizeof(TestB)<<endl;
            }

【答案】

下面是在32位机Visual C++ 6.0下运行的结果。

            TestA:4
            TestB:8

【解析】

这道题考查了内存字节对齐的知识点。结构体是其中所有变量的集合,其变量存放为依次存放。不同的编译系统会采用不同的字节对齐方式,这里以Windows系统下的Visual C++字节对齐方式介绍。

结构体TestA中有2个short类型变量,长度都是2字节。该结构所有成员中最大对齐单元就是2,这两个变量可以以2字节对齐,则sizeof(TestA)为4,这也是2的整数倍。

TestB中t1的长度为4字节,t2长度为2字节。为了保证读取和传送效率,编译系统需要对字节进行对齐,该结构所有成员中最大对齐单元就是4,则t1取4字节对齐,t2需要补空2字节,保证结构体大小是4的整数倍,则sizeof(B)为8。

C++编译器为了使CPU的性能达到最佳,会对struct的内存结构进行优化。默认的情况下,编译器会对struct的结构进行数据对齐,以提高读取效率。现代计算机中内存空间都是按照字节划分的,从理论上讲可以访问内存的任何地址。编译系统在内存空间上存放数据,不一定是按照其占有字节数顺序的、一个接一个存放,而是将数据按照一定的规则在空间上存放,这就是对齐。

对齐有自然对齐和指定对齐。

(1)自然对齐。

结构体是一种复合数据类型,其构成元素既可以是基本数据类型的变量,如int、long、float等,也可以是一些复合数据类型的变量,如数组、结构体等。在编译时,编译器会自动对成员变量进行对齐,默认对齐方式就是自然对齐(natural alignment)。自然对齐时,编译器按照结构体成员中长度最大的成员进行对齐,为结构体的每个成员分配空间。各个成员在内存中存放的顺序就是它们被声明的顺序,第一个成员的地址和整个结构的地址也就相同。例如,32位的计算机的数据传输值是4字节,它的默认对齐字节就是4字节,这样也可以保证传输数据的完整性。

在对齐时,变量存放的起始地址相对于结构的起始地址的偏移量与变量类型相关。下面是常见数据类型的偏移量(32位机器)。

● char:偏移量必须为sizeof(char)即1的倍数。

● int:偏移量必须为sizeof(int)即4的倍数。

● float:偏移量必须为sizeof(float)即4的倍数。

● double:偏移量必须为sizeof(double)即8的倍数。

● short:偏移量必须为sizeof(short)即2的倍数。

结构体的成员变量依据在结构中声明的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节由编译器自动填充。在Visual C++ 6.0中,整个结构体大小需要为结构字节边界数的倍数,也就是该结构中占用最大空间的类型所占用的字节数的倍数。这表明,最后一个成员申请空间后,如果不够上述要求,编译系统就会自动补缺字节。

(2)指定对齐。

指定对齐就是改变默认对齐条件,按照指定的对齐条件对结构体成员进行对齐。可以通过下面的方法来改变默认的对齐条件。

● 伪指令#pragma pack (n):指示编译器按照n字节对齐。如果n大于结构体中最大成员的长度,则编译器仍按照结构体中最大成员的长度进行对齐。

● 伪指令#pragma pack ():指示编译器取消自定义字节对齐方式。

【试题15】

写出下面代码的输出结果。

            Struct
            {
                short   t1;
                short   t2;
            }TestA;
            Struct
            {
                long   t1;
                short  t2;
                short  t3;
            }TestB;
            Struct
            {
                short  t2;
                long   t1;
                short  t3;
            }TestC;
            int main(int argc, char* argv[])
            {
                cout<<"TestA:"<<sizeof(TestA)<<endl;
                cout<<"TestB:"<<sizeof(TestB)<<endl;
                cout<<"TestC:"<<sizeof(TestC)<<endl;
            }

【答案】

下面是在32位机Visual C++ 6.0下运行的结果。

            TestA:4
            TestB:8
            TestC:12

【解析】

这道题和上道题差不多,TestB增加了一个成员变量t3,还增加了一个结构体TestC。TestB中t1的长度为4字节,t2长度为2字节,t3长度为2字节,t1、t2和t3依次顺序存放。为了保证读取和传送效率,编译系统需要对字节进行对齐,该结构所有成员中最大对齐单元就是4,则取4字节对齐,t2和t3共有4字节,3个成员共有8字节保证结构体大小是4的整数倍,则sizeof(B)为8。

而TestC结构体成员的存放顺序是t2、t1和t3。该结构所有成员中最大对齐单元是4,则取4字节对齐,而t2只有2字节,其后为占有4字节的t1,它需要补空2字节;同样,t3也需要补空2字节。这就是TestC需要占用12字节。

【试题16】

指出下面代码的错误之处。

            union un
            {
                int n;
                float a;
            }unt;
            struct
            {
                char c[6];
            }aa;
            struct
            {
                int c[6];
            }bb;
            struct
            {
                un c;
            }cc;
            struct st
            {
                char c[6];
                int i[4];
                un b;
            }dd;
            struct
            {
                char c[4];
                int i[4];
                un b;
            }ee;
            int main(int argc, char* argv[])
            {
                struct st * sp=&ee;
                cout<<sizeof(unt)<<endl;
                cout<<sizeof(aa)<<endl;
                cout<<sizeof(bb)<<endl;
                cout<<sizeof(cc)<<endl;
                cout<<sizeof(dd)<<endl;
                cout<<sizeof(ee)<<endl;
                cout<<sizeof(sp)<<endl;
                cout<<sizeof(*sp)<<endl;
            }

【答案】

            4
            6
            24
            4
            28
            24
            4
            24

【解析】

这道题还是考查结构体和联合体结构大小的知识。联合体的大小就是其成员长度最大所占的空间大小。所以sizeof(unt)大小就是float型变量的长度4。

变量dd对齐字节应为4字节,其第一个成员c占用6字节,i占用16字节,c需要补充2字节。这样,dd的长度为28字节。

sizeof(sp)是对指针sp求值,sp指针占用4字节,所以其值为4。而sizeof(*sp)是对结构体ee变量求值,结果应为24字节。在面试时,一定注意sizeof()是对指针求值还是对其指向的内容求值。这些细节需要注意。

【试题17】

写出下面代码的输出结果。

            struct  s_BTestB
            {
                char t ;
                char k;
            };
            int main(int argc, char* argv[])
            {
                //0x1234二进制形式为1 0010 0011 0100
                int   mm =0x1234;
                //将mm的值赋给结构体BTestB
                s_BTestB BTestB=*((s_BTestB *)& mm);
                cout<<(unsigned char)(BTestB.t)<<";"<<(int)(BTestB.k) <<endl;
                //将结构体BTestB的值赋给mm
                mm=*((unsigned short *)&BTestB);
                cout<<mm<<endl;
                return 0;
            }

【答案】

            52;18
            4660

【解析】

这道题考查了结构体成员变量顺序存储的知识。因为结构体成员变量顺序存储,所以可使用其内存空间存放数值。0x1234的二进制形式为1001000110100,而结构体BTestB的空间为2字节,可以存放0x1234的值,其结构如下。

            BTestB   15 14 13 12 |11 10 9 8 | 7 6 5 4 |3 2 1 0
            BTestB.t                    | 0 0 1 1 |0 1 0 0
            BTestB.k   0 0  0  1 |0  0 1 0 |

所以BTestB.t的值为52,BTestB.k的值为18。

【试题18】

写出下面代码的输出结果。

            struct  s_BTestA
            {
                char t:4;
                char k:6;
                unsigned short i:7;
            };
            int main(int argc, char* argv[])
            {
                //0x1234二进制形式为1 0010 0011 0100
                int   mm =0x1234;
                //将mm的值赋给结构体BTestA
                s_BTestA BTestA=*((s_BTestB *)& mm);
                cout<<(unsigned char)( BTestA.t)<<";"<<(int)( BTestA.k) <<";"<<(BTestA.i)
                                                                          <<endl;
                //将结构体BTestA的值赋给mm
                mm=*((unsigned short *)&BTestA);
                cout<<mm<<endl;
                cout<<sizeof(BTestA)<<endl;
                return 0;
            }

【答案】

            4;18;0
            4660
            4

【解析】

这道题主要考查结构体中位域的使用。有时结构体中的成员元素存储信息时,并不需要占用一个完整的字节,而只需一个或者几个二进制位。例如存放一个BOOL值时,用一个二进制位就可以存放TRUE和FALSE两种状态。为了节省存储空间和方便使用,C/C++语言支持位域。位域就是把一个字节中的二进位分为几个不同的区域,并为每个区域指定域名(也可以无域名)和每个区域的位数。程序可以对域名进行操作。

(1)位域的定义和位域变量。

位域定义与结构体定义相仿,其形式如下。

            struct 位域结构名
            {
                类型说明符 位域名:位域长度;
                类型说明符 位域名:位域长度;
                ……
            };

(2)位域的说明。

一个位域不能跨2字节,只能存储在同一个字节中。如果需要多个字节,就不需要使用位域来声明变量,可以直接使用基础类型变量。不同位域的存放顺序和其声明顺序一致,也是一个接一个顺序存放。如果一个字节所剩位不够存放另一位域时,应从下一字节起存放该位域。下面的代码就占用了2字节。

            struct  BTestA
            {
                char t:4;
                char k:6;
            };

也可以指定某位域从下一个单元开始存放,下面的例子就指定k从下一个字节存放。在这里,t占第一字节的4位,后4位填0表示不使用,k从第二字节开始,占用4位。

            struct  BTestA
            {
                char t:4;
                char  :0;/*空域*/
                char k:4; /*从下一字节开始存放*/
            };

由于位域不允许跨2字节,也就是说位域的长度不能大于一字节的长度。

这道题中,t和k的位数和超过了一个字节,它们分别占用一字节空间。同样的,k和i的位数和也超过了一个字节,i也重新分配内存空间。i是unsigned short类型,虽然占用7位,但是系统也会为它分配2字节空间(作者使用的是32位Visual C++ 6.0,以下同)。所以sizeof(BTestA)的值为4,而不是3。

因为结构体成员变量顺序存储,所以可使用其内存空间存放数值。0x1234的二进制形式为1001000110100,而结构体BTestA的空间为4字节,可以存放0x1234的值,其前两个字节的结构如下。由于mm为int类型,占用4字节,后两个字节内容均是0,也全部填充到BTestA的后两个字节中,这里不再绘出。

            BTestB   15 14 13 12 |11 10 9 8 | 7 6 5 4 |3 2 1 0
            BTestB.t                    | 0 0 1 1 |0 1 0 0
            BTestB.k   0 0  0  1 |0  0 1 0 |

所以BTestB.t的位数是4,只能计算其4位的值,所以其值为4;同样的,BTestB.k的值为18,BTestB.i的值为0。

【试题19】

写出下面程序的输出结果。

            typedef struct
            {
              int a:2;
              int b:2;
              int c:1;
            }TestC;
            int main(int argc, char* argv[])
            {
                int kk=0;
                TestC t =*((test *)&kk);
                t.a = 1;
                t.b = 3;
                t.c = 1;
                kk=*(( short *)&t);
                cout<< t.a <<endl;
                cout<< t.b<<endl;
                cout<< t.c<<endl;
                cout<< kk<<endl;
                return 0;
            }

【答案】

            1
            -1
            -1
            29

【解析】

这道题主要考查结构体中位域的使用。t.a占用2位,其二进位为01,又是有符号数值,所以其值为1。t.b占用2位,其二进位为11,又是有符号数值,所以其值为-1。t.c占用2位,其二进位为1,又是有符号数值,所以其值为-1。

在定义t时,使用kk对其初始化,也就是说t的内存空间存放的是0。最后把t的内存空间数据赋值给kk。kk的二进位为011101,也就是29。

【试题20】

写出下面程序的输出结果。

            #define  OFFSET(s,e) (int)&(((struct s*)0)->e)
            struct Test_addr
            {
                  int  a;
                  char b;
                  double c;
                  char d;
            };
            int main(int argc, char* argv[])
            {
                struct Test_addr s;
                int offset = OFFSET(Test_addr,a);
                cout<<offset<<endl;
                offset = OFFSET(Test_addr,b);
                cout<<offset<<endl;
                offset = OFFSET(Test_addr,c);
                cout<<offset<<endl;
                offset = OFFSET(Test_addr,d);
                cout<<offset<<endl;
            cout<<sizeof(Test_addr)<<endl;
                return 0;
            }

【答案】

            0
            4
            8
            16
            24

【解析】

这道题输出了结构体成员变量的偏移量和结构体大小。这是考查求职者对sizeof()和结构体大小理解的另外一种方法。对结构体成员变量的偏移量的获取方法,这段代码使用了一个小技巧。这个小技巧在宏OFFSET里体现。宏将地址0强制转换成指定结构体类型,该结构体第一个成员变量的地址就是0,该成员相对于结构体地址的偏移量也是0。同理,第二个成员地址也是其相对于结构体地址的偏移量。这里,没有对地址0进行操作,还是安全的。

其实,读者可以将强制转换地址设置成其他数值。但是求偏移量时,需要减去这个值。上面的代码可以修改成以下形式,仍然可以获得正确的结果。

            #define  OFFSET(l,s,e) (int)&(((struct s*)l)->e)- l
            struct Test_addr
            {
                  int  a;
                  char b;
                  double c;
                  char d;
            };
            int main(int argc, char* argv[])
            {
                struct Test_addr s;
                int offset = OFFSET(0x2000,Test_addr,a);
                cout<<offset<<endl;
                offset = OFFSET(0x2000,Test_addr,b);
                cout<<offset<<endl;
                offset = OFFSET(0x2000,Test_addr,c);
                cout<<offset<<endl;
                offset = OFFSET(0x2000,Test_addr,d);
                cout<<offset<<endl;
                cout<<sizeof(Test_addr)<<endl;
                return 0;
            }

【试题21】

下面代码的输出结果是什么?

            int aa = 0;
            cout<<sizeof(aa=12)<<endl;
            cout<<aa<<endl;

【答案】

            4
            0

【解析】

上面的答案在Visual C++ 6.0环境下运行获得。最新的C++标准支持sizeof()在运行时计算,但大多数没有完全实现C++标准的编译器,仍然在编译阶段处理sizeof()。由于sizeof()不能被编译成机器码,其参数也不能被编译,所以直接被替换成类型。在Visual C++ 6.0环境下,sizeof()为编译阶段处理,语句“aa=12”也就是被替换成aa的类型,输出4。对aa的赋值并没有执行,所以aa的值仍然为0。

【试题22】

下面代码的输出结果是什么?

            class CTest{};
            class CTestA
            {
                int s;
                double d;
                char c;
            };
            class CTestB
            {
                int s;
                double d;
                static int e;
                char c;
            };
            class CTestC
            {
                int s;
            char c;
                double d;
            };
            int main(int argc, char* argv[])
            {
                cout<<"sizeof(CTest):"<<sizeof(CTest)<<endl;
                cout<<"sizeof(CTestA):"<<sizeof(CTestA)<<endl;
                cout<<"sizeof(CTestB):"<<sizeof(CTestB)<<endl;
                c cout<<"sizeof(CTestC):"<<sizeof(CTestC)<<endl;
                CTest  a;
                CTestA a1;
                CTestB b1;
                c CTestC c1;
                cout<<"sizeof(a) :"<<sizeof(a)<<endl;
                cout<<"sizeof(a1) :"<<sizeof(a1)<<endl;
                cout<<"sizeof(b1) :"<<sizeof(b1)<<endl;
                c cout<<"sizeof(c1) :"<<sizeof(c1)<<endl;
                return 0;
            }

【答案】

在32位Visual C++ 6.0中测试结果如下。

            sizeof(CTest):1
            sizeof(CTestA):24
            sizeof(CTestB):24
            sizeof(CTestC):16
            sizeof(a) :1
            sizeof(a1) :24
            sizeof(b1) :24
            sizeof(c1) :16

【解析】

类CTest是空类,但是编译器输出的结果为1。类声明后,可以实例化。编译系统为实例化的类,也就是对象,在内存中都会分配一个地址,用以标识该对象。为此,编译器往往会给空类一个字节,所以sizeof(CTest)为1。

类一般由非静态成员变量、static成员变量、函数、虚函数等构成。static成员变量独立于该类的任意对象,它是只与类关联的,由该类的所有对象共享访问,并不与该类的对象关联。对于非静态成员变量,每个类对象都有其复制,只有在对象创建时才存在。static成员变量类似于全局变量,分配在全局数据区,并不在类中占用内存空间,无论类是否被实例化,它都已存在。使用sizeof()对类计算,是获取类所占用的内存空间,static成员变量并不占用类内存空间,只是在类中声明,所以sizeof()所求值不包含static成员变量所占用的内存空间。

这就是CTestA和CTestB的sizeof()结果是一样的原因。

在C++中,非虚函数的声明是在编译/连接时使用,运行期使用重定位地址跳转调用非虚函数,不占用存储空间。函数代码在代码区存放,也没有存放在类的内存空间中。所以,对类进行sizeof()运算,也不包含非虚函数所占的内存空间。

sizeof(CTestB)和sizeof(CTestC)的结果不同,是因为内存字节对齐造成的。这在讲解struct时已经介绍过,不再重复。该题不同类所占内存大小不同,从中看出类中成员变量的定义顺序会影响到内存的利用率,这也是跟编译器的对齐方式有关。

【试题23】

下面代码的输出结果是什么?

            class CTest{};
            class CTestA{int s;};
            class CTestB{};
            class CTestC:public CTestA{
                virtual void fun()=0;
            };
            class CTestD:public CTest{
                virtual void fun()=0;
            };
            class CTestE:public CTestB,public CTestC{};
            int main(int argc, char* argv[])
            {
                cout<<"sizeof(CTest):"<<sizeof(CTest)<<endl;
                cout<<"sizeof(CTestA):"<<sizeof(CTestA)<<endl;
                cout<<"sizeof(CTestB):"<<sizeof(CTestB)<<endl;
                cout<<"sizeof(CTestC):"<<sizeof(CTestC)<<endl;
                cout<<"sizeof(CTestD):"<<sizeof(CTestD)<<endl;
                cout<<"sizeof(CTestE):"<<sizeof(CTestE)<<endl;
                CTestA a1;
                CTestB b1;
                cout<<"sizeof(a1) :"<<sizeof(a1)<<endl;
                cout<<"sizeof(b1) :"<<sizeof(b1)<<endl;
                return 0;
            }

【答案】

在32位Visual C++ 6.0中测试结果如下。

            sizeof(CTest):1
            sizeof(CTestA):4
            sizeof(CTestB):1
            sizeof(CTestC):8
            sizeof(CTestD):4
            sizeof(CTestE):8
            sizeof(a1) :4
            sizeof(b1) :1

【解析】

虚函数可以重载,以改写虚拟函数,实现一些特定需求或改变系统的默认处理。C++支持多态性。为了实现多态性,C++编译器也提供动态联编。动态联编,也称之为晚捆绑,就是在运行阶段,才将函数的调用与对应的函数体连接的一种方式。

编译器编译时,发现一个虚函数,会为该类创建一个虚函数表vtable。该表通常包含该类和其父类的所有虚函数的地址。如果该类重载了其父类的虚函数,表vtable中指向其父类虚函数的对应表项则指向重载后的此函数;如果没有重载,该函数仍然为虚函数,其指针并未改变。

编译器并非将该虚函数表直接放入该类的内存空间中,而是在该类中隐含插入一个指针指向虚函数表的指针vptr。在32位系统下,指针为4字节。

调用该类的函数时,编译器会将vptr指向对应的vtable。调用基类的构造函数时,指向基类的指针此时已经变成指向具体类的this指针,借此this指针即可得到正确的vtable,也就实现了多态性。这才能真正与函数体进行连接,实现动态联编。

所以,在对含有虚函数的类进行sizeof()运算时,会多出一个vptr指针大小的值。在32位系统下,也就是多出4字节。

这就是CTestC和CTestB的sizeof()结果是一样的原因。

对子类进行sizeof()运算结果为父类和子类所占内存空间。sizeof(CTestC)的值为CTestA所占内存空间加上虚函数表指针的和。CTestA所占内存空间为4,虚函数表的指针为4,因此sizeof(CTestC)的值为8。如果CTestA的成员变量s的类型为char类型,则sizeof(CTestA)为1。但是sizeof(CTestC)的值仍然为8。这是因为CTestC的内存空间需要字节对齐,虚函数表指针为4字节,则CTestA所占空间也需要补齐4字节。

CTestD继承自CTest类。CTestD需要复制CTest到其空间内,CTest为空类,因此也不需要占用内存空间。虽然sizeof(CTest)的结果为1,但是在CTestD内,需要为其分配一个字节,用以标识该类的对象。

下面是sizeof()的总结。

(1)基本类型。

基本类型的长度由系统决定,不同硬件系统、操作系统或者编译器得到的结果可能是不同的。下面是获取基本数据类型长度的代码。

            cout<<"sizeof(CTestA )"<<sizeof(a1)<<endl;
            cout<<"sizeof(CTestB )"<<sizeof(b1)<<endl;
            cout<<"sizeof(bool)="<<sizeof(bool)<<endl;
            cout<<"sizeof(char)="<<sizeof(char)<<endl;
            cout<<"sizeof(short)="<<sizeof(short)<<endl;
            cout<<"sizeof(long)="<<sizeof(long)<<endl;
            cout<<"sizeof(int)="<<sizeof(int)<<endl;
            cout<<"sizeof(float)="<<sizeof(float)<<endl;
            cout<<"sizeof(double)="<<sizeof(double)<<endl;

下面是在32位机器、Visual C++ 6.0环境下得到对基本类型进行sizeof()运算的结果。

            sizeof(bool)=1;
            sizeof(char)=1;
            sizeof(short)=2;
            sizeof(long)=4;
            sizeof(int)=4;
            sizeof(float)=4;
            sizeof(double)=8;

(2)数组、指针和引用。

对数组进行sizeof()运算,需要注意有时数组退化成指针,例如数组作为参数以指针形式传给函数,函数内对其参数进行sizeof()运算,就是对指针进行运算。下面的代码就是将数组处理为一个指针,对s进行sizeof()运算,不是对数组运算,而是对指针运算,结果为4。

            int func(char s [15])
            {
                cout<<"fun:sizeof(s):"<<sizeof(s)<<endl;
                return 1;
            }

如果对func()进行运算,如sizeof(func(s)),则结果将依据func()的返回值决定。如上面的func()函数返回int类型,sizeof(func(s))=4。

下面代码为对不同数组变量进行sizeof()运算。

            char* s = "abcd";
            //对指针进行运算,指针长度为4。
            cout<<"sizeof(s)="<<sizeof(s)<<endl;
            //对字符*s进行运算,*s为s所指的第一个字符。
            cout<<"sizeof(*s)="<<sizeof(*s)<<endl;
            //s1为数组,其长度由编译器依据其初始值字符串长度决定,即初始值字符串长度加1。
            char s1[] = "abcd";
            cout<<"sizeof(s1)="<<sizeof(s1)<<endl;
            //对字符*s1进行运算,*s1为s1所指的第一个字符。
            cout<<"sizeof(*s1)="<<sizeof(*s1)<<endl;
            //s2为含有100个元素的字符数组。
            char s2[100] = "abcd";
            cout<<"sizeof(s2)="<<sizeof(s2)<<endl;
            //s3为含有100个元素的整型数组,每个元素占用4字节。
            int   s3[100] ;
            cout<<"sizeof(s3)="<<sizeof(s3)<<endl;
            double s4;
            double* s5=&s4;
            //s6为引用变量,对引用变量进行sizeof()运算时,结果等于所引用的变量的sizeof()结果。
            double& s6=s4;
            cout<<"sizeof(s4)="<<sizeof(s4)<<endl;
            //s5为指针。
            cout<<"sizeof(s5)="<<sizeof(s5)<<endl;
            cout<<"sizeof(s6)="<<sizeof(s6)<<endl;

下面是上面的代码在32位机器、Visual C++ 6.0环境下得到对基本类型进行sizeof()运算的结果。

            sizeof(s)=4
            sizeof(*s)=1
            sizeof(s1)=5
            sizeof(*s1)=1
            sizeof(s2)=100
            sizeof(s3)=400
            sizeof(s4)=8
            sizeof(s5)=4
            sizeof(s6)=8

(3)结构。

Sizeof()对类和结构运算时,其处理情况相同。对结构运算时,其值就是非静态数据成员所占内存和,还需要加上字节对齐所占用的内存空间。空的结构sizeof()值为1。这里不再举例。

(4)无父类的类。

对无父类的类进行sizeof()运算,其值原则上等于非静态成员变量的size之和。如果有虚函数,则需要加上虚函数表指针所占用的字节。在32位系统中,虚函数表指针占用4字节。通过上面的例子,可以得到这样一个共识,定义成员变量的顺序可能会影响到类的sizeof()运算结果。这是由字节对齐造成的。

对空类进行sizeof运算,结果为1。

(5)派生类。

对派生类进行sizeof()运算,需要加上其基类的size。基类所占空间还要与派生类的成员变量字节对齐。

这里顺便介绍strlen函数和sizeof的区别,总结如下。

(1)sizeof是运算符,strlen是函数。

(2)sizeof获取变量所占用的空间,可以对不同数据类型进行运算;strlen获取字符串或字符串指针所指字符串的长度,其参数为char型,且字符串必须是以 '\0 '结尾的。

(3)sizeof运算符的结果类型是size_t,也就是unsigned int类型。

(4)sizeof是运算符,在编译时就可进行运算;strlen是函数,在运行时才能计算。

(5)因为sizeof是运算符,对类型进行sizeof运算,sizeof后必须加括弧;如果是变量名可以不加括弧。

(6)数组作为strlen的参数不会退化成指针,但是传递给sizeof就有可能退化为指针。

2.4 运算符

C/C++提供了非常灵活的语法,但是也会给用户带来不便。运算符是编程中最常用的,但是一些细节往往会被忽视,出现一些预料不到的结果。这也成了面试中经常出现的问题。

【试题24】

下面代码的输出结果是什么?

            int m=1,x=2,y,z;
            x==(m<x)?m:x;
            y=(y=z=4);
            z=x&&y;
            x=y&z;
            printf("x=%d\n",x);
            printf("y=%d\n",y);
            printf("z=%d\n",z);

【答案】

            x=0
            y=4
            z=1

【解析】

代码“x==(m<x)?m:x;”的意思是左侧变量和右侧表达式的值比较,左侧x的值并未变化。右侧表达式“(m<x)?m:x”意思是如果m小于x,则返回m值;否则返回x值。这行代码并未改变任何变量的值。冒号两侧变量类型要一致,否则会出错。下面的代码在Visual C++ 6.0中会编译不过。

            cout<<( m<x?1:’2’) << endl;

代码“y=(y=z=4);”意思是把4赋值给z,然后再赋值给y,最后再赋值给左侧的变量y,因此y的变量就是4。

代码“z=x&&y;”是将x和z与运算的结果赋值给z。与运算就是如果x和y为真,则结果为真,返回1;否则,为假,返回0。因为x为2,z为4,因此返回1,z的值最后为1。

代码“x=y&z;”意思是将y和z的按位与结果赋值给x。按位与就是将y和z的二进制形式值的每一位进行与运算,然后将按位与后的数值返回给x。按位与的结果如表2-1所示。

表2-1 按位与结果表

【试题25】

使用位运算符实现一个十六进制表示的正整数值的高低位交换。例如,一个正整数值为0xABCD,高低位交换后的数值是0xDCBA。

【答案】

            int HLT( int n)
            {
                //获取n的字节数。
                int k=sizeof(n);
                //保存交换后的数值。
                int r=0;
                //保存移位后的临时数值。
                int t=0;
                //保存第一位非0数的位置。
                int f=0;
                /*
                    一个十六进制数值,每个十六进制位对应4位二进制位。
                    每个字节8位,因此每个字节可以保存2位十六进制数值。
                    要处理的十六进制位数为字节数的2倍。
                    从数值的高位进行处理。
            */
            for(int i=2*k-1;i>=0;i--)
            {
                    //获取待处理的4位二进制,并将其左移到低4位。
                    t=n>>i*4;
                    //与0x000f进行按位与,获取低4位的数值。
                    t&=0x000f;
                    //寻找高位中第一个非0数值的位置。
                    if(t==0 && f==0)continue;
                    //当t不为0时,f记录i的值。
                    if(f==0)f=i;
                    //t右移相应位置。
                    t<<=(f-i)*4;
                    //将t与r相加。
                    r |= t;
                }
                return r;
            }
            int  main()
            {
                int  a  = 0xABCD;
                a=HLT(a);
                printf("%x\n",a);
                return  0;
            }

【解析】

这道题考查了移位操作的知识点。1位十六进制数可以使用4位二进制数表示。在这道题中,需要一次移位4位二进制数,也就是1位十六进制数。这道题实现流程如下。

(1)获取十六进制数的数的长度。

(2)从高位开始处理十六进制数。

(3)将待处理的高位数(共4位二进制位)右移至低4位。

(4)判断该数是否为0,且未碰到非0位,返回(2)继续执行(查找第一位非0数,因为只对十六进制的有效数进行交换)。

(5)如果数值非0,设置第一位非0数值位置。

(6)将该数左移相应位置,并与r相加。

(7)重复步骤(2)~(6),直至处理完所有位。

(8)返回r。

【试题26】

写出下面代码的输出结果。

            int  main()
            {
                int x=0,y=0;
                for( ;y<=1 && !x++;y++)
                {}
                printf("%d,%d\n",x,y);
                return 0;
            }

【答案】

            2,1

【解析】

这道题主要考查运算符!和++的优先级。因为!和++运算符具有相同的优先级,结合性是从右向左运算,故++运算先于!运算。请读者注意,运算符~的级别比较高,高于++等运算符。

【试题27】

写出下面代码的输出结果。

            int  main()
            {
                int b = 3;
                int arr[] = {6,7,8,9,10};
                int *ptr = arr;
                *ptr=*ptr+++b;
                printf("%d,%d",*ptr,*ptr++);
                return 0;
            }

【答案】

在Visual C++ 6.0环境中运行结果如下。

            7,7

【解析】

该段程序考查++、+、*运算符的优先级,以及printf的计算顺序。所有的优先级中,只有三个优先级是从右至左结合的,它们是单目运算符(!、~、++、--、+、-、*、(type))、条件运算符(三目运算符?:)、赋值运算符(+=、-=、*=、/=、%=、&=、^=、|=、<<=、>>=)。其他都是从左至右结合。

++运算符和*运算符具有相同优先级,但结合性是自右向左,先计算++然后才能运算*。但是代码“*ptr=*ptr+++b;”的执行结果可能与编译器相关。这是因为ptr在执行后加1,有些编译器是在整行代码运算结束后加1,有些是在加b结束后加1。这两种不同的处理方式导致的结果会不同。在Visual C++ 6.0环境中运行时,是整行代码执行后加1。

printf()语句的计算顺序是自右向左,也就是先进行计算*ptr++,再计算*ptr。在计算*ptr++时,是获取*ptr值后ptr加1,还是输出后加1,这也要看编译器的处理方式。在Visual C++ 6.0环境中运行时,是整行代码执行后加1,所以才有上面的运行结果。

所以在写代码时,不要写这样的代码,以防出现不可预料的结果。

【试题28】

写出下面代码的输出结果。

            int  main()
            {
                int  a,b,d=241;
                a=d/10%9;
                b= (-2)&&3;
                d=(-2)&3;
                printf("%d,%d,%d",a,b,d);
                return 0;
            }

【答案】

在Visual C++ 6.0环境中运行结果如下。

            6,1,2

【解析】

该段程序考查/和%优先级。/和%属于同一级优先级,左结合顺序。在代码“a=d/10%9;”中,先计算/,才能计算%。

&&运算符是进行与运算,这是一个两目运算符,只要两侧数值为true,结果就为true。因为非0数值都会被认为是true,因此结果为true,输出格式为整数,显示为1。读者可以测试一下对小数进行与运算的结果。

【试题29】

new和malloc的区别是什么?在C++中,new是否可以不分配内存,而创建指定的对象呢?

【答案】

malloc是一个库函数,是已经编译好的代码,编译器无法改变它。调用malloc时,编译器从堆中为其分配内存,需要调用free函数释放为其分配的内存。malloc函数不能初始化对象。

而new是运算符,在编译器控制范围之内。调用new时,通常需要以下3个步骤。

(1)调用operator new分配内存。

(2)调用构造函数生成类对象,初始化对象。

(3)返回相应指针。

使用new时,需要调用delete释放申请的内存,并调用对象的析构函数释放内存,而free不会调用对象的析构函数。

可以使用new在已经申请的内存空间上建立对象,就是placement new。

【解析】

在C++中,有关new的形式可以有3种:new、operator new、placement new。下面简要介绍一下这3种形式。

new操作符和delete操作符对应,就是对内存进行申请和释放,不能被重载。调用new时,通常需要以下3个步骤实现内存分配。

(1)调用operator new分配内存。

(2)调用构造函数生成类对象,初始化对象。

(3)返回相应指针。

如果想实现与new不同的内存分配方法,可以通过重载的operator new实现。operator new就像operator+一样,是可以重载的。同样的,operator delete也是可以重载的。如果类中没有重载operator new,那么系统就会调用全局的::operator new实现内存的分配。

placement new也是operator new的一个重载版本。如果需要在已经分配的内存中创建一个对象,就可以通过placement new实现。它允许在一个已经分配好的内存中构造一个新的对象,原型如下:

            void *operator new( size_t, void *p ) throw()  { return p; }

其中,void *p指向一个已经分配的内存缓冲区的首地址。执行placement new时,参数size_t常被忽略,由系统指定。下面的代码在申请的内存空间buf上,创建两个task对象。

            class task ;
            char * buf = new [10*sizeof(task )];
            task  *pt= new (buf) task[2];

placement new的使用方法和其他普通的new有所不同。通常的使用步骤如下。

(1)提前分配缓存。

下面为类task的对象申请内存空间。

            class task ;
            char * buf = new [10*sizeof(task )];

(2)分配对象。

在已分配的缓存区调用placement new来构造一个对象。

            //task  *pt= new (buf) task[2];
            task  *pt= new (buf) task;

(3)使用对象。

对对象的使用,可以按照普通方式使用。

            // pt[0]->fun();//fun()为类task的方法。
            pt->fun();//fun()为类task的方法。

(4)对象析构。

在使用完对象后,和普通对象的使用方法一样,必须调用析构函数。下面就是调用的析构函数。

            pt->~task();

(5)释放空间。

在使用完对象后,如果申请的空间不再使用,可以把空间释放掉。释放后的空间不能再被使用。如果还要将该空间分配给其他新对象,就不能释放空间。

            delete [] buf;

可见,使用placement new就可以不重新创建内存空间,而创建指定的对象。

2.5 预处理

预处理是C/C++语言的一种特殊而又重要的功能,它由预处理程序负责完成。当对一个源文件进行编译时,在第一遍扫描(也就是词法扫描和语法分析)前,系统将自动调用预处理程序对源程序中的预处理部分进行处理,处理完毕后才对源程序进行编译。预处理包括3种形式:宏(#include)、文件包含(#define)和条件编译(# ifdef、# ifndef等)。

【试题30】

下面代码的输出结果是什么?

            #define XX(x)  x+x*x
            int main(int argc, char* argv[])
            {
                int x=2;
                x=3*XX(x);
                printf("%d",x);
                return 0;
            }

【答案】

            10

【解析】

上面例子定义了一个宏。把代码“x=3*XX(x);”的宏展开,代码就变成了下面的形式。

            x=3*x+x*x;

变量x的值是3,最后x的值变为10。这道题很简单,但是有些考生会做错。原因不外乎两种:马虎和对宏代换理解不够。马虎就是忘记了细节,忽略了宏代换之后的表达式的形式;对宏代换理解不够,就是对宏概念理解不正确导致的结果。其实这两种原因都是对宏代换理解的问题。宏定义就是用宏名表示一个字符串,宏展开时以该字符串代换宏名。宏代换就是简单的代换,因此,宏定义时必须十分注意,保证在宏代换之后不发生错误。两边的括号最好不要少。

定义宏时,要注意以下几点。

(1)宏中可以包含常数、字符,也可以是表达式。但是宏定义不要添加不必要的符号,如分号。预处理程序也会把分号作为宏的一部分进行替换。

(2)宏定义允许嵌套其他宏,也就是定义宏时,可以使用已经定义的宏名。在展开宏时,预处理程序进行代换。

(3)宏名习惯上用大写字母表示。

(4)宏定义可以表示数据类型,也可以指定数值的类型。常用的宏定义就是使用宏定义一个常数,表示1年中有多少秒。UL就是告诉预处理程序,这个常数是UL类型。这样写出的表达式比较直观,可使维护者一看就知道这个宏的意义。预处理程序计算该常数的值,没有降低程序的效率。

            #define SEC_PERYEAR  (60 * 60 * 24 * 365)UL

(5)编译时,预处理程序对定义的宏不做语法检查。只有展开宏之后,编译系统对展开宏的代码编译时,才能发现宏定义的错误。

(6)宏定义中,可以带有参数,宏名和形参表之间不能有空格。如果出现空格,系统将会认为是无参数宏定义。

(7)调用带有参数的宏时,宏才展开,参数也是符号代换,不存在传值。宏定义时的形式参数(宏定义中的参数)是不分配内存的,也不必在定义宏时指定参数类型。

宏定义中的形参只是标识符,对应的实参(宏调用中的参数)可以为变量,也可以为表达式。因此,定义宏时,最好也为宏参数加上括号。

【试题31】

在C/C++语言中,文件包含的命令行一般有以下两种形式:

            #include "文件名"
            #include <文件名>

简要说明一下这两种形式的区别。

【答案】

使用尖括号的文件包含表示在包含目录中去查找包含的文件,不是在源文件目录中去查找。包含目录是由用户在环境中设置的文件目录。使用双引号的文件包含则表示首先在当前源文件目录中查找包含的文件,如果没有找到则到包含目录中去查找。

【解析】

C/C++提供了很多头文件,这些头文件包含了各个标准库函数的函数原型。程序调用库函数时,需要包含该函数原型所在的头文件。文件包含也是C/C++预处理程序的一个重要功能。文件包含的功能是把指定的文件插入该代码行位置,并取代该代码行,使指定的文件和当前源程序文件组成一个源文件。在对其编译时,编译系统将为其生成一个目标文件。文件包含允许嵌套。

【试题32】

写出下面代码的输出结果。

            #define PI 3.14
            int main(int argc, char* argv[])
            {
                int r=10;
                #ifdef PI
                    printf("s=%d",(int)(PI*r*r));
                #else
                    printf("s=%d",int)(3*r*r));
                #endif
            }

【答案】

            s=314

【解析】

预处理程序提供了条件编译功能,使编译系统按照不同的条件编译不同的程序部分,产生不同的目标代码文件。条件编译一般的语法格式如下:

            #ifdef 标识符
                程序段1
            #else
                程序段2
            #endif

如果标识符已使用#define命令定义,则对程序段1编译;否则,对程序段2进行编译。该格式中的#else也可以没有,格式如下:

            #ifdef 标识符
                程序段
            #endif

另外,也可以使用#ifndef形式指定编译条件,格式如下:

            #ifndef 标识符
                程序段1
            #else
                程序段2
            #endif

如果标识符未被#define命令定义,则对程序段1编译;否则,对程序段2进行编译。这正好与#ifdef预编译命令的功能相反。

【试题33】

如何定义当前源文件的文件名及源文件的当前行号。

【答案】

            cout << __FILE__ << endl;
            cout<<__LINE__ << endl ;

【解析】

__FILE__和__LINE__是编译系统预定义的宏,分别输出当前源文件的文件名和当前所在的行号。标准的C/C++除了支持这两个宏外,还支持如下两个宏。

__DATE__:表示编译时的日期,字符串格式。

__TIME__:表示编译时的时间,字符串格式。

【试题34】

写出下面代码的输出结果。

            #include "stdafx.h"
            #include <iostream>
            #include "stdio.h"
            using namespace std;
            int main(int argc, char* argv[])
            {
            cout<<__LINE__ << endl ;
                #line 100
            cout<<__LINE__ << endl ;
                return 0;
            }

【答案】

在Visual C++ 6.0下的运行结果如下。

            7
            100

【解析】

#line是预处理程序提供的预处理指令,用于改变当前所在的行号和文件名称。#line命令的基本格式如下:

            #line num["filename"]

其中,num为所设置的行号,[]内的文件名可以省略。

#line 100就是省略了文件名,改变当前行号为100。编译系统在编译源代码时,会产生一些中间文件。使用该指令,可以使中间文件名固定,有利于进行调试。

另外,预处理程序还提供有#pragma预处理指令。该指令是设定编译器的状态,或设置编译器执行特定的动作。常用的#pragma指令如下。

(1)#pragma message。

该指令在编译时,在输出窗口输出指定的信息,其格式如下:

            #pragma message(msg)

其中,msg为待输出的文本信息。

下面的代码在定义了宏PI时,输出提示信息。

            #ifdef PI
            #Pragma message(“macro PI defined!”)
            #endif

(2)#pragma once。

该指令一般加在头文件的开始处,保证头文件被编译一次。

(3)#pragma warning。

#pragma warning的形式比较多,主要有如下几种。

● #pragma warning(disable: n)

该指令将某个警报设置为失效,n为警报的序号。

● #pragma warning(default: n)

该指令将某个警报设置为默认,n为警报的序号。

● #pragma warning(error:n)

该指令将某个警报信息设置为错误,n为警报的序号。

● #pragma warning( push)

存储当前警报信息设置。

● #pragma warning(push, n)

存储当前警报信息设置,并设置报警级别为n。n为1~4的自然数。

● #pragma warning( pop )

恢复之前压入堆栈的警报信息设置。pop和push是对应的,该指令使push和pop之间的任何警报信息设置都失效。

例如,下面代码将不显示4507警告信息。

            #pragma warning(disable:4507)

(4)#pragma comment。

            #pragma comment(comment-type [,"commentstring"])

其中,comment-type是一个预定义的标识符,指定注释的类型,应该是compiler、exestr、lib、linke之一;commentstring是一个为comment-type提供附加信息的字符串。

comment-type的类型简单解释如下。

● compiler:将编译器的版本或名字放入一个对象文件。

● lib:可将库文件加入到工程中。

            #pragma comment(lib, "user32.lib")

该指令用来将user32.lib库文件加入到本工程中。

● linker:指定一个连接选项。

(5)#pragma pack。

该指令的格式如下。

● #pragma pack (n):该指令指示编译器将按照n个字节对齐。

● #pragma pack ():该指令指示编译器将取消自定义字节对齐方式。#pragma pack (n)和#pragma pack ()之间的代码按n个字节对齐。

如果指定的对齐字节数n超过了系统默认的字节对齐字节数,系统将按照默认的字节数进行对齐。只有n小于系统默认的字节数,指定的对齐方式才有效。

2.6 其他

本小节的试题以编程题为主。这些试题考查了面试者的综合能力。

【试题35】

编写一个程序,以小数形式输出一个分数。用户输入分母和分子,表示一个分数形式的数值,用户输入小数位数后,程序以小数形式输出分子的计算结果。程序需要满足以下条件:

(1)用户输入分母、分子和输出位数;

(2)保证输入的数值为大于0的整数。

【答案】

            #include <stdio.h>
            #include <string.h>
            #include <stdlib.h>
            void GetFloat(int x,int y,char * str,int nnum)
            {
                //保存转换后的数值。
                char tmp[30];
                //获取x除以y的整数部分,并将其转换成字符串保存在str中。
                strcat(str,itoa(x/y,tmp,10));
                //加入小数点。
                strcat(str,".");
                //计算小数部分的长度。字符串结束标志“\0”占用一个数组元素。
                int num=nnum-strlen(str)-1;
                //获取x除以y的余数。
                x%=y;
                //i表示已经获取的小数数目。
                int i=0;
                //获取小数,循环条件是获取的小数数目不够num,并且余数不为0。
                //对余数扩大10倍,求其对y的商,就是当前小数的数值部分。
                while(i<num && (x!=0))
                {
                    x*=10;
                    //获取x除以y的整数部分,并将其转换成字符串保存在str中。
                    strcat(str,itoa(x/y,tmp,10));
                    //获取x除以y的余数。
                    x%=y;
                    //小数数目增1。
                    i++;
                }
                return;
            }
            int main(int argc, char* argv[])
            {
              int x;
              int y;
                int nnum;
                //获取用户输入的分子。如果小于0则重复等待用户输入正确的值。
                do
                {
                    printf("input integer x(>=0): ");
                    scanf("%d", &x);
                }while(!(x>=0));
                //获取用户输入的分母。如果小于0则重复等待用户输入正确的值。
                do
                {
                    printf("input integer y(>0): ");
                    scanf("%d", &y);
                }while(!(y>0));
                do
                {
                    printf("input integer nnum(>0): ");
                    scanf("%d", &nnum);
                }while(!(nnum>0));
                //申请保存结果的内存空间。
              char* str=new char[nnum];
                //初始化内存空间。
                memset(str,0,nnum);
              GetFloat(x,y,str,nnum);
                printf("%s\n",str);
                delete[] str;
              return 0;
            }

【解析】

这道题实现起来比较简单,但也会给一些求职者带来麻烦。造成麻烦的原因是获取求分数的办法。笔者给出的答案是将余数扩大10倍除以分母,并将商作为小数一位的方法,实现求小数。如果余数为0,则终止求小数。

这道题还是有很多细节问题需要注意,如果程序要实现的完美,还是要下点功夫。

【试题36】

编写一个程序,输出由字母组成的“字母塔”。 例如:输入C,则输出:

            A
            ABA
            ABCBA

【答案】

            int main(int argc, char* argv[])
            {
                char c,*d;
                //获取用户输入的字母。字母需要在字母“A”和“Z”(或者字母“a”和“z”)之间;
                //否则,程序将强行要求用户再次输入,直至输入正确为止。
                do
                {
                    printf("input char(<='z' and >='a'): ");
                    scanf("%c", &c);
                    d=strupr(&c);
                    c=*d;
                }while(! (c>='A' && c<='Z'));
                //计算输出的行数。行数依据用户输入的字母确定。
                int l=c-'A'+1;
                for(int i=0;i<l;i++)
                {
                    //输出每行开始字符左侧的空格。
                    for(int j=0;j<l-i-1;j++)
                        printf("%c",' ');
                    //输出每行字符。
                    for(int k=0;k<=i;k++)
                        printf("%c",'A'+k);
                    //输出每行最后一个字符右侧的空格。
                    for(int m=i-1;m>=0;m--)
                        printf("%c",'A'+m);
                    printf("\n");
                }
              return 0;
            }

【解析】

这道题的设计难点主要有以下3个:

(1)输出的行数;

(2)每行字符串左侧和右侧的空格数;

(3)每行输出的字符及数目。

解决了这3个问题,该程序实现起来还是比较简单的。

【试题37】

编写一个程序:当用户输入小数时,程序输出该小数对应的分数。例如,输入0.125,则程序输出1/8;输入1.375,则程序输出1+3/8。

【答案】

            /*
                辗转相除法求x与y最大公约数,并将x和y转换成分数形式。
                x:转换后的分子。
                y:转换后的分母。
                ch:保存转换后的分数形式。
            */
            bool getcd(long int  x,long int  y,char  ch[])
            {
                int m,n;
                m=x;
                n=y;
                long int r,t;
                //如果m小于n,则m和n交换,保证m大于n。
                if(m<n)
                {
                    t=m;
                    m=n;
                    n=t;
                }
                //获取m对n的余数。
                r=m%n;
                //求公约数,直至r为0。
                while(r!=0)
                {
                    //此时n大于m,将n赋值给m,保证m大于n。
                    m=n;
                    n=r;
                    //获取m除以n的余数。
                    r=m%n;
                }
                //将x和y分别除以n,获取分子和分母。
                x=(long)x/n;
                y=(long)y/n;
                int i=0;
                int j=0;
                m=x;
                n=y;
                //获取x和y的位数,以便申请足够内存空间存放其字符串形式。
                //将数值除以10,直至为0,则获取该数值的位数。
                while(m!=0)
                {
                    m/=10;
                    //计位数数目。
                    i++;
                }
                while(n!=0)
                {
                    n/=10;
                    j++;
                }
                //比较两个数的位数,并存入i中。
                if(j>i)i=j;
                //使用m和n的最大位数申请内存空间,以便能存放分子和分母。
                //多申请一个空间用于保存字符串结束标志“\0”。
      char* p=new char[i+1];
      //如果x大于y,则获取该分数的整数部分,并存入ch。
      if(x>y)
      {
          m=x/y;
          x=x%y;
          itoa(m,p,10);
          strcpy(ch,p);
          strcat(ch," ");
      }
      itoa(x,p,10);
      strcat(ch,p);
      itoa(y,p,10);
      strcat(ch,"/");
      strcat(ch,p);
      delete [] p;
      return true;
  }
  int main(int argc, char* argv[])
  {
      float x,m;
      long int g,n;
      int i=0;
      //接受用户的输入,要求x大于0。
      do
      {
          cout<<"input a decimal fraction(x>0):";
          cin>>x;
      }while(x<=0);
      m=x;
      n=1;
      /*
      将x(m)扩大成整数m作为分子,将扩大倍数(n)作为分母。
      求m和n的最大公约数,并将m和n除以最大公约数,就得到分数形式。
      */
      while(m-long(m)!=0)
      {
          m=m*10.0;
          n=n*10;
          i++;
      }
      char*p=new char[(i+2)*2];
      getcd(long(m),n,p);
      cout<<"Result fraction: "<<p<<endl;
      delete [] p;
      return 0;
  }

【解析】

这道题的设计难点主要是小数转换分数的方法。读者如果没有碰到过小数转换成分数的例子,会感觉到无从下手。本题通过将小数扩大成整数作为分子,将扩大倍数作为分母,求两者的最大公约数,两者除以最大公约数就是分子和分母。

这道题也是从一个侧面考察了求最大公约数的方法。该例使用辗转相除法获取最大公约数。

【试题38】

编写一个程序,求1……n的和,要求不能使用乘除法、循环语句(如for、while等)、条件判断语句(如if、else、switch、case、A?B:C等)。

【答案】

以下程序在Visual C++6.0上运行通过。

  #include <stdio.h>
  int GetSum1(int n)
  {
      int l;
      //n为0的时候,执行下面的语句,返回0。
      !n && (l=n);
      //n不为0时,执行下面的语句,递归调用。
      n && (l=n+GetSum1(n-1));
      return l;
  }
  int main(int argc, char* argv[])
  {
      printf("%d\n",GetSum1(100));
      return 0;
  }

【解析】

这道题的设计难点主要是不能使用乘除法、循环语句、条件判断语句等关键字。这就使得不能使用通常的办法来实现。实现这道题的办法很多,所给答案使用的是递归实现循环。

递归可以实现循环的功能,但是递归需要终止递归的判定条件。因为这道题不能使用条件判断语句,设置递归终止条件就需要费一番周折。所给答案利用逻辑与运算实现了判断的功能。在逻辑与运算中,如果前一个条件为假,系统将不判断后一个条件的真假,直接返回假;如果前一个条件为真,才会判断后一个条件是否为真。利用这个特点,通过下面的语句实现依据n值不同执行不同的语句的功能。

n=0:!n为真,系统判断执行(l=n)语句,l的值也就是为0;n不等于0的时候,不执行(l=n)语句。

  !n && (l=n);

n!=0:n为真,执行判断后一个条件,进入递归调用;n为0时,不执行后一个条件。

  n && (l=n+GetSum1(n-1));

这只是一种实现办法。使用类也可以实现这道题的功能。类具有静态成员变量。类的静态成员变量与类的对象无关,可以作为类创建对象的计数器使用。利用这个特点,可以使用类的静态成员变量记录累加和。

下面是使用类静态成员变量实现累加功能。

  #include <stdio.h>
  class SUM
  {
  public:
      SUM()
      {
          //使N增1,并求和。
          //每创建一个SUM对象,就执行该语句一次:N的值增1,Sum值增加。
          //N的值也可作为类对象的计数器使用。
          Sum += ++N;
      }
      static int GetSum()
      {
          //获取累加和。
          return Sum;
      }
  private:
      //N为类静态成员变量,用以保存当前累加的数。
      static int N;
      // Sum为类静态成员变量,用以保存当前累加的和。
      static int Sum;
  };
  //初始化类SUM的静态成员变量。
  int SUM::N = 0;
  int SUM::Sum = 0;
  //获取和。
  int GetSum(int n)
  {
      //创建100个对象,就对1…N求和。
      SUM *a = new SUM[n];
      delete []a;
      a = 0;
      return SUM::GetSum();
  }
  int main(int argc, char* argv[])
  {
      printf("%d\n",GetSum(100));
      return 0;
  }

通过类的虚函数也可以实现递归。继承类对象调用虚函数时,会依据对象调用不同类的虚函数。依据这个特点,可以实现判断功能:当n为0时,调用基类的虚函数;当n不为0时,调用派生类的虚函数。

下面是使用虚函数实现累加功能。

  /*

声明两个类:基类CSUMA和派生类CSUMB。

两个类都有虚函数Sum():基类的Sum()返回0;派生类的Sum()递归调用本身,实现累加。

  */
  class CSUMA
  {
  public:
      //定义虚函数。
      virtual int Sum (int n)
      {
          return 0;
      }
  };
  //声明一个具有两个元素的全局数组,保存CSUMA对象的指针。
  CSUMA*  Arr[2];
  //声明一个派生类。
  class CSUMB: public CSUMA
  {
  public:
      //定义虚函数,实现递归调用。
      virtual int Sum (int n)
      {
          /*
          当n为0时,!!n的值为0,调用数组Arr[0]所保存对象的虚函数。因为Arr[0]所
          保存对象为CSUMA,调用CSUMA的虚函数;
          当n不为0时,!!n的值为1,调用数组Arr[1]所保存对象的虚函数。因为Arr[0]
          所保存对象为CSUMB,调用CSUMB的虚函数,递归调用本身,实现累加。
          */
          return Arr[!!n]->Sum(n-1)+n;
      }
  };
  int GetSum2(int n)
  {
      //声明两个对象。
      CSUMA a;
      CSUMB b;
      //将两个对象保存到数组Arr中。
      Arr[0] = &a;
      Arr[1] = &b;
      int value = Arr[1]->Sum(n);
      return value;
  }
  int main(int argc, char* argv[])
  {
      printf("%d\n",GetSum2(100));
      return 0;
  }

另外,也可以通过模板实现累加的功能。

【试题39】

编写一个C语言程序,输入一个表示数的字符串,把该字符串转换成数字并输出,并将数字转换成大写金额的字符串输出。如输入“123”,输出123和“壹佰贰拾叁”。

程序中出现的字符串转换成数字,不能使用atoi()、strtol()等函数。

【答案】

  #include <iostream>
  using namespace std;
  #define MAX 0xffffff
  /*
  将字符串转换为数字。
  str为待转换字符串;
  nJ为字符串所表示数字的进制。
  */
  float StrToInt(const char* str,int nJ=10)
  {
      //限制进制范围。
      if(nJ>16)
          return 0;
      if(nJ<2)
          return 0;
      long int num = 0;
      double  fnj=nJ;
      double  fnum=0;
      if(str != NULL)
      {
          const char* digit = str;
          //标识当前处理的是小数部分还是整数部分:false标识整数,true标识小数。
          bool bfloat=false;
          //下面判断字符串所表示数字是正数还是负数:minus为1表示正数;minus为-1表示负数。
          int minus = 1;
          if(*digit == '+')
                digit++;
          else if(*digit == '-')
          {
                digit++;
                minus = -1;
          }
          //处理剩余字符。
          while(*digit != '\0')
          {
                //处理字符。
                if((*digit >= '0' && *digit <= '9') || (*digit >= 'A' && *digit
                    <= 'F') || (*digit >= 'a' && *digit <= 'f'))
                {
                    int m;
                    //将字符转换成十进制数值。
                    if( *digit >= 'A' && *digit <= 'F')
                        m=(*digit - 'A'+10);
                    else if(*digit >= 'a' && *digit <= 'f')
                        m=(*digit - 'a'+10);
                    else
                        m=*digit - '0';
                    //如果数值超出进制表示范围,则返回。
                    if(m>=nJ)
                        return 0;
                    //处理整数部分。
                    if(!bfloat)
                    {
                        num = num * nJ +m;
                        //超出最大值,则返回。
                        if(num > MAX)
                        {
                        num = 0;
                        break;
                        }
                    }
                    else
                    {
                        //将小数部分转换成十进制数。
                        fnum+= (*digit - '0')*fnj;
                        //fnj要适当变小。
                        fnj*=1/((float)nJ);
                    }
                    digit ++;
                }
                //处理小数点。
                else if(*digit == '.')
                {
                    if(fnj>=nJ)
                    {
                        fnj=1/((float)nJ);
                        bfloat=true;
                        digit++;
                    }
                    else
                    {
                        fnum=0;
                        break;
                    }
                }
                //处理非法字符。
                else
                {
                    num = 0;
                    break;
                }
          }
          //如果处理结束,则将数值乘以符号值。
          if(*digit == '\0')
          {
                if(bfloat)
                    fnum=num+fnum;
                else
                    fnum=num;
                fnum = fnum*minus;
          }
      }
      if(fnum==((int)fnum))
          return  (int)fnum;
      else
          return fnum;
  }
  /*
  将指定数转换成汉字大写格式。
  num为待转换的数,这里只处理整数;
  nL为num的位数;
  pdes保存转换后的汉字。
  */
  void GetChinese(__int64 num,int nL,char* pdes)
  {
      char *pNum="零壹贰叁肆伍陆柒捌玖";
      char pUnit[][3]={"","拾","佰","仟","万","亿"};
      //用于保存临时的转换结果。
      char p[3];
      //k用于保存每位对应的权值,如第2位的权值是10,第3位的权值为100。
      __int64 k=1;
      for(int i=0;i<nL;i++)
      {
          //获取当前处理位上的数值。
          int l=(num%(10*k))/k;
          /*
          如“23”,读作“贰拾叁”。2后需要加“权”,而个位则不需要。因此,i为0时,也
          就是处理个位时,直接将数保存在pdes。
          */
          if(i>=1)
          {
                //处理亿和万以上数值时,需要单独处理。i为8时,处理亿以上的数。
                if(i>=8)
                {
                    //i为8时,获取“亿”。
                    if(i==8)
                        strcpy(p,pUnit[5]);
                    else
                        //如果是十亿、百亿以上的数,只需要获取除8的余数,
                        //就可以获取其对应的位。
                        strcpy(p,pUnit[i%8]);
                }
                else if(i>=4)
                {
                    //i为4时,获取“万”。
                    if(i==4)
                        strcpy(p,pUnit[4]);
                    else
                    //如果是十万、百万、千万以上,小于亿的数,只需要获取除4的余数,
                    //就可以获取其对应的位。
                        strcpy(p,pUnit[i%4]);
                }
                else
                    strcpy(p,pUnit[i]);
                //将获取的位值复制到pdes中。pdes保存的是正常读顺序的逆序。
                strcat(pdes,p);
          }
          //将数值保存到pdes。
          strncat(pdes,pNum+l*2,2);
          k*=10;
      }
      //将pdes保存的字符顺序逆转,就是正常读的顺序。
      int m=strlen(pdes)-2;
      pdes[strlen(pdes)]='\0';
      for( i=0;i<m/4;i++)
      {
          //相应字符交换,汉字占用两个字符,一次交换两个字符。
          strncpy(p,pdes+i*2,2);
          strncpy(pdes+i*2,pdes+m-i*2,2);
          strncpy(pdes+m-i*2,p,2);
      }
      p[0]='\0';
      /*
      下面将字符串中的“零”去掉。如“壹拾零万零仟叁佰零拾零”,就需要转换成“壹拾万叁佰”。
      如果“零”字符后是“万”或者是“亿”,使用空格代替“零”;
      如果“零”字符后是“仟”、“佰”、“拾”等,则使用空格代替。
      然后将空格去掉,实现转换完毕。
      */
      //每个汉字占用两个字符位置,这里针对汉字操作,m/2就是汉字字符数。
      for( i=0;i<m/2;i++)
      {
        //获取当前汉字字符。
        strncpy(p,pdes+i*2,2);
        //如果是“零”,则进行处理。
        if(strcmp(p,"零")==0)
        {
            //设置下一个汉字字符的位置。
            int n=i*2+2;
            char p1[3]="";
            //如果没有到达字符串尾端,则继续处理。
            if(n<m)
            {
                //获取下一个汉字字符。
                strncpy(p1,pdes+n,2);
                //如果不是“万”和“亿”,则全部使用空格替换。
                if(!((strcmp(p1,"万")==0) || (strcmp(p1,"亿")==0)))
                {
                    p1[0]=' ';
                    p1[1]=' ';
                    strncpy(pdes+i*2,p1,2);
                    strncpy(pdes+n,p1,2);
                }
                //如果是“万”和“亿”,则使用空格替换“零”。
                else
                {
                    p1[0]=' ';
                    p1[1]=' ';
                    strncpy(pdes+i*2,p1,2);
                }
            }
            else
            {
                //处理“个位数”。
                p1[0]=' ';
                p1[1]=' ';
                strncpy(pdes+i*2,p1,2);
            }
        }
    }
    //下面的代码将空格清除掉。
    //i保存当前处理字符位置。
    i=0;
    //n为字符前移个数。
    int n=0;
    while(i<strlen(pdes))
    {
        if(pdes[i]==' ')
        {
            //记录i当前位置。
            int mn=i;
            //查找到下一个非空字符。
            while(pdes[++i]==' ');
            //计算前移个数。
            n=i-mn+n;
            //将下一个空格前的字符前移。
            while(i<strlen(pdes) && pdes[i]!=' ')
            {
                pdes[i-n]=pdes[i];
                i++;
            }
        }
        else
            i++;
    }
    pdes[i-n]='\0';
}
int main(int argc, char* argv[])
{
    float fnum=StrToInt("123.123",8);
    cout<<fnum<<endl;
    char p[107]="";
    GetChinese(123456700089,12,p);
    cout<<"123456700089:";
    cout<<p<<endl;
    return 0;
}

【解析】

这道题考查面试者的基本功。该题实现起来并不难,但是要考虑周全,却也不简单。在实现字符转换数字时,需要注意以下几点。

(1)正负符号。

如果第一个字符是“+”号,则不需要做任何操作;如果第一个字符是“-”号,则说明这个数是负数,需要将得到的数值变成负数。

(2)非法字符。

非法字符主要为进制外的字符,不同的进制有不同的字符序列,需要针对不同进制进行判断。进制的范围也需要限制。

(3)字符转换成数值的实现方法。

在字符转换成数值时,每扫描到一个字符,将该字符转换成数值,并将之前得到的数字乘以10再加上当前数值。

(4)数值的溢出。

在转换成汉字大写字符时,需要注意以下几点。

(1)数字与汉字大写字符的对应。

这种对应比较容易实现。所给答案就是将汉字大写字符保存为数组,大写字符与下标对应。知道数字,就可以获取对应的汉字大写字符。

(2)对应“权”。

除了个位不需要加“权”外,其他位都需要加“权”。如123,1后要加“佰”,2后要加“拾”。如果这个数字很大,如123456,1后要加“拾”,而不是“拾万”。

本题所给答案的实现办法是将对应的位使用数字标识,如个位使用0标识,十位使用1标识,十万位就用5标识。5除以4(4是万的位标识)的余数所对应的“权”就是十万位的权。对于亿以上的数,也采取类似操作。

(3)清除“零”。

这样处理后的字符串与人们正常读的顺序还是有差别的。如果数值里有很多零,转换后的字符就会出现很多“零”,如“零仟零佰”之类的。这需要清除这些“零”,并且清除相应的“权”。

(4)“零万”、“零亿”中清除“零”。

如果“零万”、“零亿”出现时,就要谨慎一些。这是因为“万”和“亿”不能清除,只能清除“零”,否则,转换的结果会不正确。

【试题40】

编写一个程序,不使用第三方参数交换两个参数的值。

【答案】

            #include <stdio.h>
            void TranXY( int &x, int &y)
            {
                x=x+y;
                y=x-y;
                x=x-y;
            }
            int main(int argc, char* argv[])
            {
    int x=60;
                int y=50;
    printf("x=%d\n",x);
                printf("y=%d\n",y);
    TranXY(x,y);
    printf("TranXY:x=%d\n",x);
    printf("TranXY:y=%d\n",y);
    return 0;
            }

【解析】

一般的数据交换办法是借用一个临时变量实现,这道题要求不借助临时变量,可能出乎一些面试者的意料。这道题考查面试者对基本编程能力的运用。如果面试者只记住一些常用的方法而不能灵活运用,在工作中也很难开拓局面。

这道题实现起来并不难,还可以有以下两种办法实现。

            /*
            使用异或运算实现交换。
            异或运算符合交换律。一个数与其本身进行异或后,结果为0。利用这个特点,可以实现数据的交换。
            */
            void TranXY1( int &x, int &y)
            {
    //x保存x和y的异或结果。
    x^=y;
    /*
    y与x进行异或,实际是x与y异或后再与y进行异或。
    异或运算符合交换律、结合律。y与x进行异或可以转换成x与y和y的异或结果进行异或。
    因为一个数与其本身进行异或后,结果为0。任何数与0进行异或,结果还是本身。
    所以y与x的异或结果为x。
    */
    y^=x;
    //同样的,x与y(此时的y的内容是原来x没有进行运算前的内容)的结果为y。
    x^=y;
            }
            /*
            通过x和y的加减运算进行交换。
            这个方法是在运算时同时赋值,而没有使用一个临时变量。
            */
            void TranXY2( int &x, int &y)
            {
    x = x+y-(y=x) ;
            }

2.7 总结

有关C/C++程序设计基础知识的试题都是对多个知识点的综合考查,很少有对某个知识点进行考查。这也就说明招聘单位已经默认应聘者对知识点都已经掌握,只是通过考查了解应聘者灵活运用能力和对知识点掌握的程度。这就要求应聘者不但要掌握这些知识点,而且要深入、灵活地掌握这些知识点。应聘者可以多读一些程序代码,特别是一些“大牛”写的经典程序代码。通过阅读大量代码,应聘者的经验和能力会有很大的提高。但是通过阅读大量代码的方法提高能力,需要时间;如果应聘者时间有限,可以通过做有关的面试题来增加经验,增加熟练程度,这可以在较短时间内提高应聘者的能力和经验。只要应聘者注意细节,考虑周全,灵活应用,肯定能顺利过关。