C/C++笔试题集(一)

来源:互联网 发布:js中怎么验证邮箱 编辑:程序博客网 时间:2024/05/16 16:18

1、链表反转

    单向链表的反转是一个经常被问到的一个面试题,也是一个非常基础的问题。比如一个链表是这样的: 1->2->3->4->5 通过反转后成为5->4->3->2->1。

    最容易想到的方法遍历一遍链表,利用一个辅助指针,存储遍历过程中当前指针指向的下一个元素,然后将当前节点元素的指针反转后,利用已经存储的指针往后面继续遍历。源代码如下:

       1. struct linka {
       2. int data;
       3. linka* next;
       4. };
       5. void reverse(linka*& head) {
       6. if(head ==NULL)
       7.                   return;
       8. linka *pre, *cur, *ne;
       9. pre=head;
      10. cur=head->next;
      11. while(cur)
      12. {
      13.    ne = cur->next;
      14.    cur->next = pre;
      15.    pre = cur;
      16.    cur = ne;
      17. }
      18. head->next = NULL;
      19. head = pre;
      20. }

    还有一种利用递归的方法。这种方法的基本思想是在反转当前节点之前先调用递归函数反转后续节点。源代码如下。不过这个方法有一个缺点,就是在反转后的最后一个结点会形成一个环,所以必须将函数的返回的节点的next域置为NULL。因为要改变head指针,所以我用了引用。算法的源代码如下:

       1. linka* reverse(linka* p,linka*& head)
       2. {
       3. if(p == NULL || p->next == NULL)
       4. {
       5.    head=p;
       6.    return p;
       7. }
       8. else
       9. {
      10.    linka* tmp = reverse(p->next,head);
      11.    tmp->next = p;
      12.    return p;[Page]
      13. }
      14. }

    ②已知String类定义如下:

    class String
    {
    public:
    String(const char *str = NULL); // 通用构造函数
    String(const String &another); // 拷贝构造函数
    ~ String(); // 析构函数
    String & operater =(const String &rhs); // 赋值函数
    private:
    char *m_data; // 用于保存字符串
    };

    尝试写出类的成员函数实现。

    答案:

    String::String(const char *str)
    {
    if ( str == NULL ) //strlen在参数为NULL时会抛异常才会有这步判断
    {
    m_data = new char[1] ;
    m_data[0] = /'{content}/' ;
    }
    else
    {
    m_data = new char[strlen(str) + 1];
    strcpy(m_data,str);
    }

    }

    String::String(const String &another)
    {
    m_data = new char[strlen(another.m_data) + 1];
    strcpy(m_data,other.m_data);
    }


    String& String::operator =(const String &rhs)
    {
    if ( this == &rhs)
    return *this ;
    delete []m_data; //删除原来的数据,新开一块内存
    m_data = new char[strlen(rhs.m_data) + 1];
    strcpy(m_data,rhs.m_data);
    return *this ;
    }


    String::~String()
    {
    delete []m_data ;
    }

    ③网上流传的c++笔试题汇总
    1.求下面函数的返回值(微软)

    int func(x)
    {
    int countx = 0;
    while(x)
    {
    countx ++;
    x = x&(x-1);
    }
    return countx;

}

 

    假定x = 9999。 答案:8

    思路:将x转化为2进制,看含有的1的个数。

 

2. 什么是“引用”?申明和使用“引用”要注意哪些问题?

    答:引用就是某个目标变量的“别名”(alias),对应用的操作与对变量直接操作效果完全相同。申明一个引用的时候,切记要对其进行初始化。引用声明完毕后,相当于目标变量名有两个名称,即该目标原名称和引用名,不能再把该引用名作为其他变量名的别名。声明一个引用,不是新定义了一个变量,它只表示该引用名是目标变量名的一个别名,它本身不是一种数据类型,因此引用本身不占存储单元,系统也不给引用分配存储单元。不能建立数组的引用。[Page]

 

3. 将“引用”作为函数参数有哪些特点?

    (1)传递引用给函数与传递指针的效果是一样的。这时,被调函数的形参就成为原来主调函数中的实参变量或对象的一个别名来使用,所以在被调函数中对形参变量的操作就是对其相应的目标对象(在主调函数中)的操作。

    (2)使用引用传递函数的参数,在内存中并没有产生实参的副本,它是直接对实参操作;而使用一般变量传递函数的参数,当发生函数调用时,需要给形参分配存储单元,形参变量是实参变量的副本;如果传递的是对象,还将调用拷贝构造函数。因此,当参数传递的数据较大时,用引用比用一般变量传递参数的效率和所占空间都好。

    (3)使用指针作为函数的参数虽然也能达到与使用引用的效果,但是,在被调函数中同样要给形参分配存储单元,且需要重复使用/"*指针变量名/"的形式进行运算,这很容易产生错误且程序的阅读性较差;另一方面,在主调函数的调用点处,必须用变量的地址作为实参。而引用更容易使用,更清晰。

 

4. 在什么时候需要使用“常引用”? 

    如果既要利用引用提高程序的效率,又要保护传递给函数的数据不在函数中被改变,就应使用常引用。常引用声明方式:const 类型标识符 &引用名=目标变量名;

    例1

    int a ;
    const int &ra=a;
    ra=1; //错误
    a=1; //正确

    例2

    string foo( );
    void bar(string & s);

    那么下面的表达式将是非法的:

    bar(foo( ));
    bar(/"hello world/");

    原因在于foo( )和/"hello world/"串都会产生一个临时对象,而在C++中,这些临时对象都是const类型的。因此上面的表达式就是试图将一个const类型的对象转换为非const类型,这是非法的。

    引用型参数应该在能被定义为const的情况下,尽量定义为const 。

 

5. 将“引用”作为函数返回值类型的格式、好处和需要遵守的规则?

    格式:类型标识符 &函数名(形参列表及类型说明){ //函数体 }

    好处:在内存中不产生被返回值的副本;(注意:正是因为这点原因,所以返回一个局部变量的引用是不可取的。因为随着该局部变量生存期的结束,相应的引用也会失效,产生runtime error!

    注意事项:

    (1)不能返回局部变量的引用。这条可以参照Effective C++[1]的Item 31。主要原因是局部变量会在函数返回后被销毁,因此被返回的引用就成为了/"无所指/"的引用,程序会进入未知状态。

    (2)不能返回函数内部new分配的内存的引用。这条可以参照Effective C++[1]的Item 31。虽然不存在局部变量的被动销毁问题,可对于这种情况(返回函数内部new分配内存的引用),又面临其它尴尬局面。例如,被函数返回的引用只是作为一个临时变量出现,而没有被赋予一个实际的变量,那么这个引用所指向的空间(由new分配)就无法释放,造成memory leak。[Page]

    (3)可以返回类成员的引用,但最好是const。这条原则可以参照Effective C++[1]的Item 30。主要原因是当对象的属性是与某种业务规则(business rule)相关联的时候,其赋值常常与某些其它属性或者对象的状态有关,因此有必要将赋值操作封装在一个业务规则当中。如果其它对象可以获得该属性的非常量引用(或指针),那么对该属性的单纯赋值就会破坏业务规则的完整性。

    (4)流操作符重载返回值申明为“引用”的作用:

    流操作符<<和>>,这两个操作符常常希望被连续使用,例如:cout << /"hello/" << endl; 因此这两个操作符的返回值应该是一个仍然支持这两个操作符的流引用。可选的其它方案包括:返回一个流对象和返回一个流对象指针。但是对于返回一个流对象,程序必须重新(拷贝)构造一个新的流对象,也就是说,连续的两个<<操作符实际上是针对不同对象的!这无法让人接受。对于返回一个流指针则不能连续使用<<操作符。因此,返回一个流对象引用是惟一选择。这个唯一选择很关键,它说明了引用的重要性以及无可替代性,也许这就是C++语言中引入引用这个概念的原因吧。赋值操作符=。这个操作符象流操作符一样,是可以连续使用的,例如:x = j = 10;或者(x=10)=100;赋值操作符的返回值必须是一个左值,以便可以被继续赋值。因此引用成了这个操作符的惟一返回值选择。

    例3

    #i nclude <iostream.h>
    int &put(int n);
    int vals[10];
    int error=-1;
    void main()
    {
    put(0)=10; //以put(0)函数值作为左值,等价于vals[0]=10;
    put(9)=20; //以put(9)函数值作为左值,等价于vals[9]=20;

cout<<vals[0];
    cout<<vals[9];
    }
    int &put(int n)
    {
    if (n>=0 && n<=9 ) return vals[n];
    else { cout<</"subscript error/"; return error; }
    }

 

    (5)在另外的一些操作符中,却千万不能返回引用:+-*/ 四则运算符。它们不能返回引用,Effective C++[1]的Item23详细的讨论了这个问题。主要原因是这四个操作符没有side effect,因此,它们必须构造一个对象作为返回值,可选的方案包括:返回一个对象、返回一个局部变量的引用,返回一个new分配的对象的引用、返回一个静态对象引用。根据前面提到的引用作为返回值的三个规则,第2、3两个方案都被否决了。静态对象的引用又因为((a+b) == (c+d))会永远为true而导致错误。所以可选的只剩下返回一个对象了。

 

6. “引用”与多态的关系?

    引用是除指针外另一个可以产生多态效果的手段。这意味着,一个基类的引用可以指向它的派生类实例。[Page]

    例4

    Class A; Class B : Class A{...}; B b; A& ref = b;

 

7. “引用”与指针的区别是什么?

    指针通过某个指针变量指向一个对象后,对它所指向的变量间接操作。程序中使用指针,程序的可读性差;而引用本身就是目标变量的别名,对引用的操作就是对目标变量的操作。此外,就是上面提到的对函数传ref和pointer的区别。

1)引用在创建的同时必须初始化,即引用到一个有效的对象;而指针在定义的时候不必初始化,可以在定义后面的任何地方重新赋值.

(2)不存在NULL引用,引用必须与合法的存储单元关联;而指针则可以是NULL.

(3)引用一旦被初始化为指向一个对象,它就不能被改变不另一个对象的引用;而指针在任何时候都可以改变为指向另一个对象.给引用赋值并不是改变它和原始对象的绑定关系.

(4)引用的创建和销毁并不会调用类的拷贝构造函数

(5)在语言层面,引用的用法和对象一样;在二进制层面,引用一般都是通过指针来实现的,只不过编译器帮我们完成了转换.

总的来说:引用既具有指针的效率,又具有变量使用的方便性和直观性.

1值传递

void Func(int x)

{

x=x+10;//修改的是n在堆栈中的拷贝x

}

int n=0;

Func(n);

cout<<"n="<<n<<endl;//n=0

2指针传递

void Func2(int *x)

{

(*x)=(*x)+10;//修改指针x指向的内存单元的值

}

int n=0;

Func(&n);

cout<<"n="<<n<<endl;//n=10

3.引用传递

void Func3(int &x)

{

x=x+10;//修改的是x引用到的对象n

}

int n=0;

Func3(n);

cout<<"n="<<n<<endl;//n=10

指针与引用看上去完全不同(指针用操作符’*’和’->’,引用使用操作符’.’),但是它们似乎有相同的功能。指针与引用都是让你间接引用其他对象。你如何决定在什么时候使用指针,在什么时候使用引用呢?

  首先,要认识到在任何情况下都不能用指向空值的引用。一个引用必须总是指向某些对象。因此如果你使用一个变量并让它指向一个对象,但是该变量在某些时候也可能不指向任何对象,这时你应该把变量声明为指针,因为这样你可以赋空值给该变量。相反,如果变量肯定指向一个对象,例如你的设计不允许变量为空,这时你就可以把变量声明为引用。

  “但是,请等一下”,你怀疑地问,“这样的代码会产生什么样的后果?”

 

char *pc = 0; // 设置指针为空值
char& rc = *pc; // 让引用指向空值

 

  这是非常有害的,毫无疑问。结果将是不确定的(编译器能产生一些输出,导致任何事情都有可能发生),应该躲开写出这样代码的人除非他们同意改正错误。如果你担心这样的代码会出现在你的软件里,那么你最好完全避免使用引用,要不然就去让更优秀的程序员去做。我们以后将忽略一个引用指向空值的可能性。

  因为引用肯定会指向一个对象,在C里,引用应被初始化。

 

string& rs; // 错误,引用必须被初始化
string s("xyzzy");
string& rs = s; // 正确,rs指向s

 

  指针没有这样的限制。

 

string *ps; // 未初始化的指针
// 合法但危险

 

  不存在指向空值的引用这个事实意味着使用引用的代码效率比使用指针的要高。因为在使用引用之前不需要测试它的合法性。

 

void printDouble(const double& rd)
{
cout << rd; // 不需要测试rd,它
} // 肯定指向一个double值

 

  相反,指针则应该总是被测试,防止其为空:

 

void printDouble(const double *pd)
{
if (pd) { // 检查是否为NULL
cout << *pd;
}
}

 

  指针与引用的另一个重要的不同是指针可以被重新赋值以指向另一个不同的对象。但是引用则总是指向在初始化时被指定的对象,以后不能改变。

 

string s1("Nancy");
string s2("Clancy");
string& rs = s1; // rs 引用 s1
string *ps = &s1; // ps 指向 s1
rs = s2; // rs 仍旧引用s1,
// 但是 s1的值现在是
// "Clancy"
ps = &s2; // ps 现在指向 s2;
// s1 没有改变

 

  总的来说,在以下情况下你应该使用指针,一是你考虑到存在不指向任何对象的可能(在这种情况下,你能够设置指针为空),二是你需要能够在不同的时刻指向不同的对象(在这种情况下,你能改变指针的指向)。如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么你应该使用引用。

  还有一种情况,就是当你重载某个操作符时,你应该使用引用。最普通的例子是操作符[]。这个操作符典型的用法是返回一个目标对象,其能被赋值。

 

vector v(10); // 建立整形向量(vector),大小为10;
// 向量是一个在标准C库中的一个模板
v[5] = 10; // 这个被赋值的目标对象就是操作符[]返回的值

 

  如果操作符[]返回一个指针,那么后一个语句就得这样写:

 

*v[5] = 10;

 

  但是这样会使得v看上去象是一个向量指针。因此你会选择让操作符返回一个引用。

  当你知道你必须指向一个对象并且不想改变其指向时,或者在重载操作符并为防止不必要的语义误解时,你不应该使用指针。而在除此之外的其他情况下,则应使用指针。

 

8. 什么时候需要“引用”?

    流操作符<<和>>、赋值操作符=的返回值、拷贝构造函数的参数、赋值操作符=的参数、其它情况都推荐使用引用。

    以上 2-8 参考:http://blog.csdn.net/wfwd/archive/2006/05/30/763551.aspx

 

 9. 结构与联合有和区别?
    1. 结构和联合都是由多个不同的数据类型成员组成, 但在任何同一时刻, 联合中只存放了一个被选中的成员(所有成员共用一块地址空间), 而结构的所有成员都存在(不同成员的存放地址不同)。
    2. 对于联合的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了, 而对于结构的不同成员赋值是互不影响的。

 

10. 下面关于“联合”的题目的输出?

    a)

    #i nclude <stdio.h>
    union
    {
    int i;
    char x[2];
    }a;


    void main()
    {
    a.x[0] = 10;
    a.x[1] = 1;
    printf(/"%d/",a.i);
    }
    答案:266 (低位低地址,高位高地址,内存占用情况是Ox010A)

    b)

    main()
    {
    union{ /*定义一个联合*/
    int i;
    struct{ /*在联合中定义一个结构*/
    char first;
    char second;
    }half;
    }number;
    number.i=0x4241; /*联合成员赋值*/
    printf(/"%c%c//n/", number.half.first, mumber.half.second);
    number.half.first=/'a/'; /*联合中结构成员赋值*/
    number.half.second=/'b/';
    printf(/"%x//n/", number.i);
    getch();
    }
    答案: AB (0x41对应/'A/',是低位;Ox42对应/'B/',是高位)

    6261 (number.i和number.half共用一块地址空间)

   

11. 已知strcpy的函数原型:char *strcpy(char *strDest, const char *strSrc)其中strDest 是目的字符串,strSrc 是源字符串。不调用C++/C 的字符串库函数,请编写函数 strcpy。


    答案:
    char *strcpy(char *strDest, const char *strSrc)[Page]
    {
    if ( strDest == NULL || strSrc == NULL)
    return NULL ;
    if ( strDest == strSrc)
    return strDest ;
    char *tempptr = strDest ;
    while( (*strDest++ = *strSrc++) != ‘{content}’)
    return tempptr ;
    }

   

12. 已知String类定义如下:

    class String
    {
    public:
    String(const char *str = NULL); // 通用构造函数
    String(const String &another); // 拷贝构造函数
    ~ String(); // 析构函数
    String & operater =(const String &rhs); // 赋值函数
    private:
    char *m_data; // 用于保存字符串
    };

    尝试写出类的成员函数实现。

    答案:

    String::String(const char *str)
    {
    if ( str == NULL ) //strlen在参数为NULL时会抛异常才会有这步判断
    {
    m_data = new char[1] ;

m_data[0] = /'{content}/' ;
    }
    else
    {
    m_data = new char[strlen(str) + 1];
    strcpy(m_data,str);
    }

 

    }

    String::String(const String &another)
    {
    m_data = new char[strlen(another.m_data) + 1];
    strcpy(m_data,other.m_data);
    }


    String& String::operator =(const String &rhs)
    {
    if ( this == &rhs)
    return *this ;
    delete []m_data; //删除原来的数据,新开一块内存
    m_data = new char[strlen(rhs.m_data) + 1];
    strcpy(m_data,rhs.m_data);
    return *this ;
    }


    String::~String()
    {
    delete []m_data ;
    }

   

13. .h头文件中的ifndef/define/endif 的作用?

    答:防止该头文件被重复引用。

   

14. #i nclude<file.h> 与 #i nclude /"file.h/"的区别?

    答:前者是从Standard Library的路径寻找和引用file.h,而后者是从当前工作路径搜寻并引用file.h。

   

15.在C++ 程序中调用被C 编译器编译后的函数,为什么要加extern “C”?

    首先,作为extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。[Page]

    通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数

    extern /"C/"是连接申明(linkage declaration),被extern /"C/"修饰的变量和函数是按照C语言方式编译和连接的,来看看C++中对类似C的函数是怎样编译的:

    作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:

    void foo( int x, int y );
      

    该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。

    _foo_int_int 这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。例如,在C++中,函数void foo( int x, int y )与void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。

    同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以/"./"来区分。而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。

    未加extern /"C/"声明时的连接方式

    假设在C++中,模块A的头文件如下:

    // 模块A头文件 moduleA.h
    #ifndef MODULE_A_H
    #define MODULE_A_H
    int foo( int x, int y );
    #endif  

    在模块B中引用该函数:

    // 模块B实现文件 moduleB.cpp
    #i nclude /"moduleA.h/"
    foo(2,3);
      

    实际上,在连接阶段,连接器会从模块A生成的目标文件moduleA.obj中寻找_foo_int_int这样的符号!

    加extern /"C/"声明后的编译和连接方式

    加extern /"C/"声明后,模块A的头文件变为:

    // 模块A头文件 moduleA.h
    #ifndef MODULE_A_H
    #define MODULE_A_H
    extern /"C/" int foo( int x, int y );
    #endif  

    在模块B的实现文件中仍然调用foo( 2,3 ),其结果是:
    (1)模块A编译生成foo的目标代码时,没有对其名字进行特殊处理,采用了C语言的方式;[Page]

    (2)连接器在为模块B的目标代码寻找foo(2,3)调用时,寻找的是未经修改的符号名_foo。

    如果在模块A中函数声明了foo为extern /"C/"类型,而模块B中包含的是extern int foo( int x, int y ) ,则模块B找不到模块A中的函数;反之亦然。

    所以,可以用一句话概括extern “C”这个声明的真实目的(任何语言中的任何语法特性的诞生都不是随意而为的,来源于真实世界的需求驱动。我们在思考问题时,不能只停留在这个语言是怎么做的,还要问一问它为什么要这么做,动机是什么,这样我们可以更深入地理解许多问题):实现C++与C及其它语言的混合编程。  

    明白了C++中extern /"C/"的设立动机,我们下面来具体分析extern /"C/"通常的使用技巧:

    extern /"C/"的惯用法

(1)在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:

    extern /"C/"
    {
    #i nclude /"cExample.h/"
    }

    而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern /"C/"声明,在.c文件中包含了extern /"C/"时会出现编译语法错误。

    C++引用C函数例子工程中包含的三个文件的源代码如下:

    /* c语言头文件:cExample.h */
    #ifndef C_EXAMPLE_H
    #define C_EXAMPLE_H
    extern int add(int x,int y);
    #endif


    /* c语言实现文件:cExample.c */
    #i nclude /"cExample.h/"
    int add( int x, int y )
    {
    return x + y;
    }


    // c++实现文件,调用add:cppFile.cpp
    extern /"C/"
    {
    #i nclude /"cExample.h/"
    }
    int main(int argc, char* argv[])
    {
    add(2,3);
    return 0;
    }

    如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern /"C/" { }。

    (2)在C中引用C++语言中的函数和变量时,C++的头文件需添加extern /"C/",但是在C语言中不能直接引用声明了extern /"C/"的该头文件,应该仅将C文件中将C++中定义的extern /"C/"函数声明为extern类型。

    C引用C++函数例子工程中包含的三个文件的源代码如下:

    //C++头文件 cppExample.h
    #ifndef CPP_EXAMPLE_H
    #define CPP_EXAMPLE_H
    extern /"C/" int add( int x, int y );
    #endif


    //C++实现文件 cppExample.cpp[Page]
    #i nclude /"cppExample.h/"
    int add( int x, int y )
    {
    return x + y;
    }


    /* C实现文件 cFile.c
    /* 这样会编译出错:#i nclude /"cExample.h/" */
    extern int add( int x, int y );
    int main( int argc, char* argv[] )
    {
    add( 2, 3 );
    return 0;
    }

    15题目的解答请参考《C++中extern “C”含义深层探索》注解:

   

16. 关联、聚合(Aggregation)以及组合(Composition)的区别?

    涉及到UML中的一些概念:关联是表示两个类的一般性联系,比如“学生”和“老师”就是一种关联关系;聚合表示has-a的关系,是一种相对松散的关系,聚合类不需要对被聚合类负责,如下图所示,用空的菱形表示聚合关系:

 

    从实现的角度讲,聚合可以表示为:

    class A {...} class B { A* a; .....}

    而组合表示contains-a的关系,关联性强于聚合:组合类与被组合类有相同的生命周期,组合类要对被组合类负责,采用实心的菱形表示组合关系:

 

    实现的形式是:

    class A{...} class B{ A a; ...}

    参考文章:http://blog.csdn.net/wfwd/archive/2006/05/30/763753.aspx

    http://blog.csdn.net/wfwd/archive/2006/05/30/763760.aspx

   

17.面向对象的三个基本特征,并简单叙述之?

    1. 封装:将客观事物抽象成类,每个类对自身的数据和方法实行protection(private, protected,public)

    2. 继承:广义的继承有三种实现形式:实现继承(指使用基类的属性和方法而无需额外编码的能力)、可视继承(子窗体使用父窗体的外观和实现代码)、接口继承(仅使用属性和方法,实现滞后到子类实现)。前两种(类继承)和后一种(对象组合=>接口继承以及纯虚函数)构成了功能复用的两种方式。

    3. 多态:是将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单的说,就是一句话:允许将子类类型的指针赋值给父类类型的指针。

   

18. 重载(overload)和重写(overried,有的书也叫做“覆盖”)的区别?

    常考的题目。从定义上来说:

    重载:是指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者都不同)。

    重写:是指子类重新定义复类虚函数的方法。

    从实现原理上来说:

    重载:编译器根据函数不同的参数表,对同名函数的名称做修饰,然后这些同名函数就成了不同的函数(至少对于编译器来说是这样的)。如,有两个同名函数: function func(p:integer):integer;和function func(p:string):integer;。那么编译器做过修饰后的函数名称可能是这样的:int_func、str_func。对于这两个函数的调用,在编译器间就已经确定了,是静态的。也就是说,它们的地址在编译期就绑定了(早绑定),因此,重载和多态无关![Page]

重写:和多态真正相关。当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态的调用属于子类的该函数,这样的函数调用在编译期间是无法确定的(调用的子类的虚函数的地址无法给出)。因此,这样的函数地址是在运行期绑定的(晚绑定)。

   

19. 多态的作用?

    主要是两个:1. 隐藏实现细节,使得代码能够模块化;扩展代码模块,实现代码重用;2. 接口重用:为了类在继承和派生的时候,保证使用家族中任一类的实例的某一属性时的正确调用。

   

20. Ado与Ado.net的相同与不同?

    除了“能够让应用程序处理存储于DBMS 中的数据“这一基本相似点外,两者没有太多共同之处。但是Ado使用OLE DB 接口并基于微软的COM 技术,而ADO.NET 拥有自己的ADO.NET 接口并且基于微软的.NET 体系架构。众所周知.NET 体系不同于COM 体系,ADO.NET 接口也就完全不同于ADO和OLE DB 接口,这也就是说ADO.NET 和ADO是两种数据访问方式。ADO.net 提供对XML 的支持。

   

21. New delete 与malloc free 的联系与区别?
    答案:都是在堆(heap)上进行动态的内存操作。用malloc函数需要指定内存分配的字节数并且不能初始化对象,new 会自动调用对象的构造函数。delete 会调用对象的destructor,而free 不会调用对象的destructor.

   

22. #define DOUBLE(x) x+x ,i = 5*DOUBLE(5); i 是多少?
    答案:i 为30。

   

23. 有哪几种情况只能用intialization list 而不能用assignment?

    答案:当类中含有const、reference 成员变量;基类的构造函数都需要初始化表。

   

24. C++是不是类型安全的?
    答案:不是。两个不同类型的指针之间可以强制转换(用reinterpret cast)。C#是类型安全的。

   

25. main 函数执行以前,还会执行什么代码?
    答案:全局对象的构造函数会在main 函数之前执行。

   

26. 描述内存分配方式以及它们的区别?
    1) 从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。
    2) 在栈上创建。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。
    3) 从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活,但问题也最多。

   

27.struct 和 class 的区别

    答案:struct 的成员默认是公有的,而类的成员默认是私有的。struct 和 class 在其他方面是功能相当的。

    从感情上讲,大多数的开发者感到类和结构有很大的差别。感觉上结构仅仅象一堆缺乏封装和功能的开放的内存位,而类就象活的并且可靠的社会成员,它有智能服务,有牢固的封装屏障和一个良好定义的接口。既然大多数人都这么认为,那么只有在你的类有很少的方法并且有公有数据(这种事情在良好设计的系统中是存在的!)时,你也许应该使用 struct 关键字,否则,你应该使用 class 关键字。[Page]

   

28.当一个类A 中没有生命任何成员变量与成员函数,这时sizeof(A)的值是多少,如果不是零,请解释一下编译器为什么没有让它为零。(Autodesk)
    答案:肯定不是零。举个反例,如果是零的话,声明一个class A[10]对象数组,而每一个对象占用的空间是零,这时就没办法区分A[0],A[1]…了。

   

29. 在8086 汇编下,逻辑地址和物理地址是怎样转换的?(Intel)
    答案:通用寄存器给出的地址,是段内偏移地址,相应段寄存器地址*10H+通用寄存器内地址,就得到了真正要访问的地址。

   

30. 比较C++中的4种类型转换方式?

    请参考:http://blog.csdn.net/wfwd/archive/2006/05/30/763785.aspx,重点是static_cast, dynamic_cast和reinterpret_cast的区别和应用。

   

31.分别写出BOOL,int,float,指针类型的变量a 与“零”的比较语句。
    答案:
    BOOL : if ( !a ) or if(a)
    int : if ( a == 0)
    float : const EXPRESSION EXP = 0.000001
    if ( a < EXP && a >-EXP)
    pointer : if ( a != NULL) or if(a == NULL)

 

32.请说出const与#define 相比,有何优点?
    答案:1) const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。
    2) 有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调试。

   

33.简述数组与指针的区别?
    数组要么在静态存储区被创建(如全局数组),要么在栈上被创建。指针可以随时指向任意类型的内存块。
    (1)修改内容上的差别
    char a[] = “hello”;
    a[0] = ‘X’;
    char *p = “world”; // 注意p 指向常量字符串
    p[0] = ‘X’; // 编译器不能发现该错误,运行时错误
    (2) 用运算符sizeof 可以计算出数组的容量(字节数)。sizeof(p),p 为指针得到的是一个指针变量的字节数,而不是p 所指的内存容量。C++/C 语言没有办法知道指针所指的内存容量,除非在申请内存时记住它。注意当数组作为函数的参数进行传递时,该数组自动退化为同类型的指针。

char a[] = /"hello world/";
    char *p = a;
    cout<< sizeof(a) << endl; // 12 字节
    cout<< sizeof(p) << endl; // 4 字节
    计算数组和指针的内存容量
    void Func(char a[100])
    {
    cout<< sizeof(a) << endl; // 4 字节而不是100 字节
    }[Page]

 

34.类成员函数的重载、覆盖和隐藏区别?
    答案:
    a.成员函数被重载的特征:
    (1)相同的范围(在同一个类中);
    (2)函数名字相同;
    (3)参数不同;
    (4)virtual 关键字可有可无。
    b.覆盖是指派生类函数覆盖基类函数,特征是:
    (1)不同的范围(分别位于派生类与基类);
    (2)函数名字相同;
    (3)参数相同;
    (4)基类函数必须有virtual 关键字。
    c.“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
    (1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
    (2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)

   

35. There are two int variables: a and b, don’t use “if”, “? :”, “switch”or other judgement statements, find out the biggest one of the two numbers.
    答案:( ( a + b ) + abs( a - b ) ) / 2

   

36. 如何打印出当前源文件的文件名以及源文件的当前行号?
    答案:
    cout << __FILE__ ;
    cout<<__LINE__ ;
    __FILE__和__LINE__是系统预定义宏,这种宏并不是在某个文件中定义的,而是由编译器定义的。

   

37. main 主函数执行完毕后,是否可能会再执行一段代码,给出说明?
    答案:可以,可以用_onexit 注册一个函数,它会在main 之后执行int fn1(void), fn2(void), fn3(void), fn4 (void);
    void main( void )
    {
    String str(/"zhanglin/");
    _onexit( fn1 );
    _onexit( fn2 );
    _onexit( fn3 );
    _onexit( fn4 );
    printf( /"This is executed first.//n/" );
    }
    int fn1()
    {
    printf( /"next.//n/" );
    return 0;
    }
    int fn2()
    {
    printf( /"executed /" );
    return 0;
    }
    int fn3()
    {
    printf( /"is /" );
    return 0;
    }
    int fn4()
    {
    printf( /"This /" );
    return 0;
    }
    The _onexit function is passed the address of a function (func) to be called when the program terminates normally. Successive calls to _onexit create a register of functions that are executed in LIFO (last-in-first-out) order. The functions passed to _onexit cannot take parameters.[Page]

   

38. 如何判断一段程序是由C 编译程序还是由C++编译程序编译的?
    答案:
    #ifdef __cplusplus
    cout<</"c++/";
    #else
    cout<</"c/";
    #endif

   

39.文件中有一组整数,要求排序后输出到另一个文件中
    答案:

    #i nclude<iostream>

    #i nclude<fstream>

    using namespace std;


    void Order(vector<int>& data) //bubble sort
    {
    int count = data.size() ;
    int tag = false ; // 设置是否需要继续冒泡的标志位
    for ( int i = 0 ; i < count ; i++)
    {
    for ( int j = 0 ; j < count - i - 1 ; j++)
    {
    if ( data[j] > data[j+1])
    {
    tag = true ;
    int temp = data[j] ;
    data[j] = data[j+1] ;
    data[j+1] = temp ;
    }
    }
    if ( !tag )
    break ;
    }
    }


    void main( void )
    {
    vector<int>data;
    ifstream in(/"c:////data.txt/");
    if ( !in)
    {
    cout<</"file error!/";

exit(1);
    }
    int temp;
    while (!in.eof())
    {
    in>>temp;
    data.push_back(temp);
    }
    in.close(); //关闭输入文件流
    Order(data);
    ofstream out(/"c:////result.txt/");
    if ( !out)
    {
    cout<</"file error!/";
    exit(1);
    }
    for ( i = 0 ; i < data.size() ; i++)
    out<<data<</" /";
    out.close(); //关闭输出文件流
    }

 

40. 链表题:一个链表的结点结构
    struct Node
    {
    int data ;
    Node *next ;
    };
    typedef struct Node Node ;


    (1)已知链表的头结点head,写一个函数把这个链表逆序 ( Intel)

    Node * ReverseList(Node *head) //链表逆序
    {[Page]
    if ( head == NULL || head->next == NULL )
    return head;
    Node *p1 = head ;
    Node *p2 = p1->next ;
    Node *p3 = p2->next ;
    p1->next = NULL ;
    while ( p3 != NULL )
    {
    p2->next = p1 ;
    p1 = p2 ;
    p2 = p3 ;
    p3 = p3->next ;
    }
    p2->next = p1 ;
    head = p2 ;
    return head ;
    }
    (2)已知两个链表head1 和head2 各自有序,请把它们合并成一个链表依然有序。(保留所有结点,即便大小相同)
    Node * Merge(Node *head1 , Node *head2)
    {
    if ( head1 == NULL)
    return head2 ;
    if ( head2 == NULL)
    return head1 ;
    Node *head = NULL ;
    Node *p1 = NULL;
    Node *p2 = NULL;
    if ( head1->data < head2->data )
    {
    head = head1 ;
    p1 = head1->next;
    p2 = head2 ;
    }
    else
    {
    head = head2 ;
    p2 = head2->next ;
    p1 = head1 ;
    }
    Node *pcurrent = head ;
    while ( p1 != NULL && p2 != NULL)
    {
    if ( p1->data <= p2->data )
    {
    pcurrent->next = p1 ;
    pcurrent = p1 ;
    p1 = p1->next ;
    }
    else
    {
    pcurrent->next = p2 ;
    pcurrent = p2 ;
    p2 = p2->next ;
    }
    }
    if ( p1 != NULL )
    pcurrent->next = p1 ;
    if ( p2 != NULL )
    pcurrent->next = p2 ;
    return head ;
    }
    (3)已知两个链表head1 和head2 各自有序,请把它们合并成一个链表依然有序,这次要求用递归方法进行。 (Autodesk)
    答案:
    Node * MergeRecursive(Node *head1 , Node *head2)
    {
    if ( head1 == NULL )
    return head2 ;
    if ( head2 == NULL)
    return head1 ;[Page]
    Node *head = NULL ;
    if ( head1->data < head2->data )
    {
    head = head1 ;
    head->next = MergeRecursive(head1->next,head2);
    }
    else
    {
    head = head2 ;
    head->next = MergeRecursive(head1,head2->next);
    }
    return head ;
    }

   

41. 分析一下这段程序的输出 (Autodesk)
    class B
    {
    public:
    B()
    {
    cout<</"default constructor/"<<endl;
    }
    ~B()
    {
    cout<</"destructed/"<<endl;
    }
    B(int i):data(i) //B(int) works as a converter ( int -> instance of B)
    {
    cout<</"constructed by parameter /" << data <<endl;

}
    private:
    int data;
    };

 


    B Play( B b)
    {
    return b ;
    }

    (1) results:
    int main(int argc, char* argv[]) constructed by parameter 5
    { destructed B(5)形参析构
    B t1 = Play(5); B t2 = Play(t1);   destructed t1形参析构
    return 0;               destructed t2 注意顺序!
    } destructed t1

    (2) results:
    int main(int argc, char* argv[]) constructed by parameter 5
    { destructed B(5)形参析构
    B t1 = Play(5); B t2 = Play(10);   constructed by parameter 10
    return 0;               destructed B(10)形参析构
    } destructed t2 注意顺序!

    destructed t1

   

42. 写一个函数找出一个整数数组中,第二大的数 (microsoft)
    答案:
    const int MINNUMBER = -32767 ;
    int find_sec_max( int data[] , int count)
    {
    int maxnumber = data[0] ;
    int sec_max = MINNUMBER ;
    for ( int i = 1 ; i < count ; i++)
    {
    if ( data > maxnumber )
    {
    sec_max = maxnumber ;
    maxnumber = data ;[Page]
    }
    else
    {
    if ( data > sec_max )
    sec_max = data ;
    }
    }
    return sec_max ;
    }

   

43. 写一个在一个字符串(n)中寻找一个子串(m)第一个位置的函数。

    KMP算法效率最好,时间复杂度是O(n+m)。

   

44. 多重继承的内存分配问题:
    比如有class A : public class B, public class C {}
    那么A的内存结构大致是怎么样的?

    这个是compiler-dependent的, 不同的实现其细节可能不同。
    如果不考虑有虚函数、虚继承的话就相当简单;否则的话,相当复杂。
    可以参考《深入探索C++对象模型》,或者:
    http://blog.csdn.net/wfwd/archive/2006/05/30/763797.aspx

   

45. 如何判断一个单链表是有环的?(注意不能用标志位,最多只能用两个额外指针)

    struct node { char val; node* next;}

    bool check(const node* head) {} //return false : 无环;true: 有环

    一种O(n)的办法就是(搞两个指针,一个每次递增一步,一个每次递增两步,如果有环的话两者必然重合,反之亦然):
    bool check(const node* head)
    {
    if(head==NULL) return false;
    node *low=head, *fast=head->next;
    while(fast!=NULL && fast->next!=NULL)
    {
    low=low->next;
    fast=fast->next->next;
    if(low==fast) return true;
    }
    return false;
    }

          

    

C++笔试题及答案下载

 

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

#define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL
我在这想看到几件事情:
1). #define 语法的基本知识(例如:不能以分号结束,括号的使用,等等)
2). 懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的。
3). 意识到这个表达式将使一个16位机的整型数溢出-因此要用到长整型符号L,告诉编译器这个常数是的长整型数。
4). 如果你在你的表达式中用到UL(表示无符号长整型),那么你有了一个好的起点。记住,第一印象很重要。

2. 写一个“标准”宏MIN,这个宏输入两个参数并返回较小的一个。

#define MIN(A,B) ((A) <= (B) (A) : (B))
这个测试是为下面的目的而设的:
1). 标识#define在宏中应用的基本知识。这是很重要的,因为直到嵌入(inline)操作符变为标准C的一部分,宏是方便产生嵌入代码的唯一方法,
对于嵌入式系统来说,为了能达到要求的性能,嵌入代码经常是必须的方法。
2). 三重条件操作符的知识。这个操作符存在C语言中的原因是它使得编译器能产生比if-then-else更优化的代码,了解这个用法是很重要的。
3). 懂得在宏中小心地把参数用括号括起来
4). 我也用这个问题开始讨论宏的副作用,例如:当你写下面的代码时会发生什么事?
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 integer)
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有三个明显的作用:
1). 在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
2). 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。
3). 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。
大多数应试者能正确回答第一部分,一部分能正确回答第二部分,同是很少的人能懂得第三部分。这是一个应试者的严重的缺点,因为他显然不懂得本地化数
据和代码范围的好处和重要性。

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呢?我也如下的几下理由:
1). 关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。)
2). 通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。
3). 合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。

Volatile

8. 关键字volatile有什么含意 并给出三个不同的例子

 

 

 

 

 

 

原创粉丝点击