C++常见面试题(四)

本博客内容:
一、变量声明与定义的区别?
二、a和&a有什么区别?
三、简述strcpy、sprintf、memcpy的区别
四、对拷贝构造函数和赋值运算符的认识
五、C++设计一个不能被继承的类
六、访问基类的私有虚函数
七、简述多态实现的原理
八、链表和数组有什么区别?
九、简述队列和栈的异同
十、编程规范的理解或认识
十一、如何避免“野指针”

一、变量声明与定义的区别?

为变量分配地址和存储空间称为定义。
不分配地址为声明。
一个变量可以多个地方声明,只能一个地方定义。
加入extern后修饰的是变量的声明。说明此变量将在文件之外定义。
很多时候,只是声明不分配空间,直到具体使用时才初始化,分配内存空间。

二、a和&a有什么区别?

#include<stdio.h>
void main( void )
{
int a[5]={1,2,3,4,5};
int *ptr=(int *)(&a+1);
printf("%d,%d",*(a+1),*(ptr-1));
return;
}

数组名a作为数组的首地址,而&a是数组的指针。结果为2 5

三、简述strcpy、sprintf、memcpy的区别

有以下不同之处:
1.操作对象不同。
strcpy的2个操作对象都是字符串
sprintf的操作源对象可以是多种数据类型,目的操作对象是字符串。
memcpy的2个对象就是2个任意可操作的内存地址,并不限于数据类型。
2.执行效率不同
memcpy最高
strcpy次
sprintf最低
3.实现功能不同
strcpy:实现字符串变量间的拷贝
sprintf:其他数据类型格式到字符串的转换
memcpy:内存块间的拷贝
说明:三者都可以实现拷贝,但是针对的对象不同,根据实际需求,来选择合适的函数实现拷贝函数。

四、对拷贝构造函数和赋值运算符的认识

拷贝构造函数和赋值运算符重载有2个不同之处:
1.拷贝构造函数生成新的类对象,而赋值运算符不能。
2.由于拷贝构造函数是直接构造一个新的类对象,所以在初始化这个对象之前不用检验源对象和新建对象是否相同。而赋值运算符则需要,如果原来的对象中有内存分配要先把内存释放掉。
3.当有类中有指针类型的成员变量时,一定要重写拷贝构造函数和重载运算符,不要使用默认的。

五、C++设计一个不能被继承的类

参考:https://my.oschina.net/cuilili/blog/323696
参考:https://blog.csdn.net/zscfa/article/details/76574359
C++中
要想不被继承,将它的构造函数和析构函数都定义为私有函数。那么当一个类试图从它那继承的时候,必然会由于试图调用构造函数、析构函数而导致编译错误。

class FinalClass1
{
    public:
    static FinalClass1 *GetInstance()
    {
        return new FinalClass1;
    }
    static void DeleteInstance(FinalClass1 * pInstance)
    {
        delete pInstance;
        pInstance=0;
    }
private:
    FinalClass1() {}
    ~FinalClass1() {}
};

该类是不能被继承的,但是它和一般的类有些不同,使用起来不方便,比如,只能得到位于堆上的实例,而不能得到位于栈上的实例。
其他方法

template<typename T>
class Base{
    friend T;
    private:
        Base(){
            cout<<"base"<<endl;
              }
        ~Base() {}
      };
      class B :virtual public Base<B>{  //此处是虚继承(一定注意)
      public:
      B(){
          cout<<"B"<<endl;
          }
};

类Base的构造函数和析构函数是私有的,只有Base类的友元函数可以访问,B类在继承时将模板的参数设置为了B类,所以构造B类对象时可以直接访问父类(Base)的构造函数。
为什么必须是virtual呢?
通常每个类只初始化自己的直接基类,但是在虚继承的时候这个情况发生了变化,可能导致虚基类被多初始化,这显然不是我们想要的。(例如:AA,AB都是类A的派生类,然后类C又继承自AA和AB,如果按之前的方法会导致C里面A被初始化2次,也会存在两份数据)
为了解决重复初始化问题,从具有虚基类的类继承的类在初始化时进行了特殊处理,在虚派生中,由最低层次的派生类的构造函数初始化虚基类。
为什么B类不能被继承?
B是Base的友元,B对象可以正常构建,由于B使用了虚继承,所以如果要创建C对象,那么C类的构造函数就要负责虚基类(base)的构造,但是Base的构造函数是私有的,C没有访问的权限(ps:友元关系是不能被继承的),例1中的C类在编译时会报错。这样B类就不能被继承了。

六、访问基类的私有虚函数

#include <iostream>
using namespace std;
class person
{
public:
    virtual void name()
    {
    cout<<"A::name"<<endl;
    }
private:
    virtual void sex()         //基类的虚函数
    {
    cout<<"A::sex"<<endl;
    }
};
class student:public person
{ 
public:
    virtual void name()
    {
    cout<<"B::name"<<endl;
    }
    virtual void address()
    {
    cout<<"B::address"<<endl;
    }
private:
    virtual void ID()
    {
    cout<<"B::ID"<<endl;
    }
};
typedef void(*Fun)(void);//定义一个函数指针
void main()
{
student stu;
for(int i=0;i<4;i++)
{
    Fun p=(Fun)*((int *)*(int *)(&stu)+i);
    p();
}
return 0;
}

七、简述多态实现的原理

编译器发现一个类中有虚函数,便会立即为此类生成虚函数表,虚函数表的各表项为指向对应虚函数的指针。编译器会会在此类中隐含插入一个指针vptr,指向虚函数表。调用此类的构造函数时,在类的构造函数中,编译器会隐含执行vptr与vtable的关联代码,将其联系到一起,另外在调用此类的构造函数时,指向基础类的指针此时已经变成指向具体的类的this指针,这样依靠此this指针即可得到正确的vtable,这就是动态联编,实现多态的基本原理。

八、链表和数组有什么区别?

有几点不同:
1.存储形式:数组是一块连续的区域,声明时就要确定长度。链表是一块可不连续的动态空间,长度可变,每个结点要保存相邻结点的指针
2.数据查找:数组的线性查找速度快,查找操作直接使用偏移地址。链表需要按顺序检索结点。
3.数据插入或删除:链表可以快速插入和删除节点。数组需要移动大量元素。
4.越界问题:链表不存在的。数组有。
说明:选择2者时,一定要根据实际需要进行选择,数组便于查询,链表便于插入删除。数组节省空间但是长度固定,链表虽然变长但是占了更多的空间。

九、简述队列和栈的异同

都是线性存储结构,但是两者的插入和删除数据的操作不同,队列是“先进先出”,栈是“后进先出”。
注意:区别栈区和堆区。堆区的存取是“顺序随意”,而栈区是“后进先出”。栈由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。堆一般由程序员分配释放,若程序员不释放,程序结束时可能由OS回收分配方式类似于链表。堆栈是一种数据结构,而堆区和栈区是程序的不同内存存储区域。

十、编程规范的理解或认识

可总结为:程序的可行性、可读性、可移植性、以及可测试性。

short i=0;i=i+1L; 有错误吗
1:错   2:正确
在数据安全的情况下大类型的数据向小类型的数据转换一定要显示的强制类型转换

十一、如何避免“野指针”

产生的原因以及解决方法:
1.指针常量声明时没有被初始化。解决:指针声明时初始化,可以是具体的地址值,也可以让它指向NULL
2.指针p被free或者delete之后,没有置为NULL。解决办法:指针指向的内存空间被释放后指针应该指向NULL
3.指针操作超越了变量的作用范围。解决方法:在变量的作用域结束前释放掉变量的地址空间,并且让指针指向NULL
注意:“野指针”的解决方法是编程规范的基本原则。平时使用时一定要避免产生“野指针”,值使用指针前一定要检验指针的合法性。

猜你喜欢

转载自blog.csdn.net/xiongluo0628/article/details/81880047