C++面经(3)(牛客)

1. 简述一下什么是面向对象?
面向对象是一种编程思想,面向对象程序人为现实中一切东西都可以看做是一个个对象,它们各自都有属性,把这些对象共有的拥有的属性变量和操作抽象成一个类来表示。

面向过程和面向对象的区别:
面向过程:根据业务逻辑从上到下写代码
面向对象:将数据与函数绑定到一起进行封装,能够更快速的开发,减少了重复代码的重写过程,同时程序也更好理解

2. 简述一下面向对象的三大特征?
面向对象的三大特征是封装、继承、多态
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。通过protected(本类函数、子类函数和友元函数访问)/private(本类函数和友元函数访问)可以把成员封装起来。
继承:可以使用现有类的所有功能,并在无需重新编写原来类的情况下对这些功能进行扩展。
在这里插入图片描述
多态:通过父类型的指针指向其子类的实例对象,然后通过父类型的指针调用实际的成员函数。实现多态,有两种方式,重写和重载。

3. 简述一下C++的重载和重写,以及它们的区别?

  • 重写
    重写是指派生类中存在重新定义的函数。其函数名,参数列表,返回值类型,所有都必须同基类中被重写的函数一致。只有函数体不同(花括号内),派生类对象调用时会调用派生类的重写函数,不会调用被重写函数。重写的基类中被重写的函数必须有virtual修饰。
#include <iostream>
using namespace std;
class A
{
public:
    virtual void fun()
    {
        cout << "A";
    }
};
class B : public A
{
public:
    virtual void fun()
    {
        cout << "B";
    }
};
int main(void)
{
    A *a = new B();
    a->fun(); //输出B,A类中的fun在B类中重写
}
  • 重载
    我们在平时写代码中会用到几个函数他们的实现功能相同,但是有些细节不同。例如:交换两个数的值其中包括(int,float,char,double)类型。如果用不同的函数名来加以区分,这样的代码不美观且不方便。于是C++中提出了函数重载。函数重载是指同一可访问区内被声明的几个具有不同参数列(参数的类型,个数,顺序不同)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
#include <iostream>
using namespace std;
class A
{
    void fun(){};
    void fun(int i){};
    void fun(int i, int j){};
};

4. 说说构造函数有几种,分别是什么作用?
C++的构造函数可以分为4类:默认构造函数、初始化构造函数、拷贝构造函数、移动构造函数

  • 默认构造函数与初始化构造函数:在定义类的对象时,完成对象的初始化操作。
class Student
{
public:
    Student() //默认构造函数
    {
        num = 1001;
        age = 18;
    }
    Student(int n, int a) : num(n), age(a) {} //(参数初始化列表方式初始化)初始化构造函数

private:
    int num;
    int age;
};
int main()
{
    S1 Student s1;           //用默认构造函数初始化对象
    S2 Student s2(1002, 18); //用初始化构造函数初始化对象
    return 0;
}

假如没有定义构造函数,编译器就会自动提供默认的构造函数。

  • 拷贝(复制)构造函数
#include <iostream>
class Test
{
    int i;
    int *p;

public:
    Test(int ai, int value)
    {
        i = ai;
        p = new int(value);
    }
    ~Test()
    {
        delete p;
    }
    Test(const Test &t)//复制构造函数用于复制本类的对象(深拷贝)
    {
        this->i = t.i;
        this->p = new int(*t.p);
    }
};

int main(int argc, char *argv[])
{
    Test t1(1, 2);
    Test t2(t1); //将对象t1复制给t2
    return 0;
}

编译器默认实现的拷贝构造函数是值拷贝(浅拷贝)
  • 移动构造函数
    移动构造函数是拷贝构造函数的优化,首先将传递参数的内存地址空间接管,然后将内部所有指针设置为空指针,并且在原地址上进行新对象的构造,最后调用原对象的的析构函数,这样做既不会产生额外的拷贝开销,也不会给新对象分配内存空间。
    但是需要注意的是,移动构造函数是对传递参数进行的一次浅拷贝,也就是说如果参数为指针变量,进行拷贝之后将会有两个指针指向同一地址空间,这个时候如果前一指针对象进行了析构,则后一个指针将会变成野指针,从而引发错误。所以当变量为指针时,要将临时对象(源对象)的指针置为空,这样在调用析构函数的时候判断临时对象指针是否为空,如果为空则不回收指针的地址空间,这样就不会释放掉前一个指针。

5. 简述一下深拷贝和浅拷贝,如何实现深拷贝?

  • 浅拷贝:浅拷贝又称为值拷贝,将源对象的值拷贝到目标对象中去,本质上来说源对象和目标对象共用一份实体,知识所引用的变量名不同,地址其实还是相同的。
  • 深拷贝:拷贝时首先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去,这样两个指针就指向了不同的内存位置。并且里面的内容是一样的,这样不但达到了我们想要的目的,还不会出现问题,两个指针先后去调用析构函数,分别释放自己所指向的位置。即为每次增加一个指针,便申请一块新的内存,并让这个指针指向新的内存,深拷贝情况下,不会出现重复释放同一块内存的错误。

深拷贝的实现:深拷贝的拷贝构造函数和赋值运算符的重载

//深拷贝的拷贝构造函数
STRING(const STRING &s)
{
    //_str = s._str;
    _str = new char[strlen(s._str) + 1];
    strcpy_s(_str, strlen(s._str) + 1, s._str);
}

//赋值运算符的重载
STRING &operator=(const STRING &s)
{
    if (this != &s)
    {
        // 目的:this->_str = s._str;
        delete[] _str;//释放旧空间
        this->_str = new char[strlen(s._str) + 1];//开辟出一段能够容纳内容的空间
        strcpy_s(this->_str, strlen(s._str) + 1, s._str);//赋值
    }
    return *this;
}

这里的拷贝构造函数我们很容易理解,先开辟出和源对象一样大的内存区域,然后将需要拷贝的数据复制到目标拷贝对象。
赋值运算符的做法:
起初s2被构造出来以后,想把s1的内存内容放到s2里,这里存在一个问题,s2的空间比s1大就可以直接将内容拷贝给s2,那如果s2的空间比s1还小的话是不是还得分情况讨论,所以赋值运算符的做法是,首先先释放旧空间,然后开辟出一段和s1一样大的空间来,将s1的值赋给s2。

深拷贝解决了指针悬挂的问题,通过不断地开空间让不同的指针指向不同的内存空间,以防止同一块内存被释放两次的问题

6. 简述一下C++中的多态?
由于派生类重写基类方法,然后用基类引用指向派生类对象,调用方法时候会进行动态绑定,这就是多态。多态分类静态多态和动态多态:

  • 静态多态:编译器在编译期间完成的,编译器会根据实参类型来推断该调用哪个函数,如果有对应的函数,就调用,没有则在编译时报错。
#include <iostream>
using namespace std;

int Add(int a, int b) // 1
{
    return a + b;
}

char Add(char a, char b) // 2
{
    return a + b;
}

int main()
{
    cout << Add(666, 888) << endl; // 调用函数1
    cout << Add('1', '2');         // 调用函数2
    return 0;
}
  • 动态多态:动态多态是用虚函数机制实现的,在运行期间动态绑定。
#include <iostream>
using namespace std;

class A
{
public:
    virtual void function()
    {
        cout << "base" << endl;
    }
};

class B : public A
{
public:
    void function()
    {
        cout << "child" << endl;
    }
};
int main()
{
    A *a = new B;
    a->function();
    return 0;
}
//打印child

7. 简述一下虚函数和纯虚函数,以及实现原理?
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有”多种形态“,这是一种泛型技术。如果调用非虚函数,则无论实际对象是什么类型,都执行基类类型所定义的函数。非虚函数总是在编译时根据调用该函数的对象引用或指针类型而确定。如果调用虚函数,则直到运行时才能确定调用哪个函数,运行的虚函数是引用所绑定或指针所指向的对象所属类型定义的版本。虚函数必须是基类的非静态成员函数。虚函数的作用是实现动态联编,也就是在程序的运行阶段动态地选择合适的成员函数,在定义了虚函数后,可以在基类的派生类中对虚函数重新定义,在派生类中重新定义的函数应与虚函数具有相同的函数名、形参和返回值,以实现同一的接口,不同定义过程。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。

#include <iostream>
using namespace std;
class Person
{
public:
    virtual void GetName()//虚函数
    {
        cout << "PersonName:xiaosi" << endl;
    };
};
class Student : public Person
{
public:
    void GetName()
    {
        cout << "StudentName:xiaosi" << endl;
    };
};
int main()
{
    Person *person = new Student();
    person->GetName(); //基类调用子类的函数,打印结果:StudentName:xiaosi
}

虚函数是通过一张虚函数表来实现的。简称为V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其中真实反映的函数。这样,在有虚函数类的实例中这个表被分配在这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得尤为重要,它像一个地图一样,指明了实际所调用的函数。

纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加"=0",即 virtual void GetName() = 0。在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象是不合常理。为了解决上述问题,将函数定义为纯虚函数,则编译器要求在派生类中必须予以重写以实现多态性。 同时含有纯虚函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述问题。所以纯虚函数地声明就是在告诉子类的设计者,你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它。

#include <iostream>
using namespace std;
class Person
{
public:
    virtual void GetName() = 0; //纯虚函数
};
class Student : public Person
{
public:
    Student(){};
    void GetName()
    {
        cout << "StudentName:xiaosi" << endl;
    };
};
int main()
{
    Student student;
    student.GetName();
}

8. 说说C++中什么是菱形继承问题,如何解决?
在这里插入图片描述
假设我们有类B和类C,它们都继承了相同的类A。另外我们还有类D,类D通过多重继承机制继承了类B和类C。因为上述图表的形状类似于菱形,因此这个问题被形象地称为菱形继承问题。

#include <iostream>
using namespace std;
/*  Animal类对应于图表的类A */
class Animal // 基类
{
private:
    int weight;

public:
    int getWeight()
    {
        return weight;
    }
};
class Tiger : public Animal
{
    /* ... */
};
class Lion : public Animal
{
    /* ... */
};
class Liger : public Tiger, public Lion
{
    /* ... */
};

在上述的代码中,我们给出了一个具体的菱形继承问题例子。Animal类对应于最顶层类(A),Tiger和Lion分别对应于图表的B和C,Liger对应于D。

int main()
{
    Liger lg; /*编译错误,下面的代码不会被任何C++编译器通过 */
    int weight = lg.getWeight();
}

在我们的继承结构中,我们可以看出Tiger和Lion类都继承于Animal基类。所以问题是:因为Liger多重继承了Tiger和Lion类,因此Liger类会有两份Animal类的成员(数据和方法),Liger对象lg会包含Animal基类的两个子对象。
所以,lg.getWeight()将会导致一个编译错误。因为编译器不知道调用Tiger类的getWeight()还是调用Lion类的getWeight()。所以,调用getWeight方法时有歧义的,不能通过编译。

解决方案:虚继承
虚继承可以解决内存重复的问题,同时避免访问冲突。

#include <iostream>
using namespace std;
/*  Animal类对应于图表的类A */
class Animal // 基类
{
private:
    int weight;

public:
    int getWeight()
    {
        return weight;
    }
};
class Tiger : virtual public Animal
{
    /* ... */
};
class Lion : virtual public Animal
{
    /* ... */
};
class Liger : public Tiger, public Lion
{
    /* ... */
};
int main()
{
    Liger lg;
    int weight = lg.getWeight();
}
//顺利编译通过
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值